ADR 0049 — llama.cpp GPU offload through upstream's Vulkan wheels
- Status: Amended
- Date: 2026-10-24
- Amends:
- Amended by:
- Related: ADR 0032, ADR 0038
- Restructured: 2026-10-25
Context
Section titled “Context”On a Windows install with an RTX 4080, a GTX 1060 and an i9-13900HX, imprint learn stored 73 of 140 conversations in about six hours, with tagger.llm on. Measured per exchange (about 1,500 prompt tokens), the local Qwen3.5-4B tagger took 38 s on CPU, against about 1.5 s for the embedding. So LLM tagging was about 95% of ingest time.
The tagger ran on CPU because llama-cpp-python had no GPU offload. On the gpu and auto profiles, setup rebuilt it from PyPI source with CMAKE_ARGS=-DGGML_CUDA=on, which needs the CUDA toolkit’s nvcc. That machine had no toolkit, so the rebuild failed. The failure was recorded in data/gpu_state.json and never retried. That path was also the one unpinned, unhashed install left (ADR 0035 kept it “unchanged”). It only ever covered NVIDIA.
The maintainer publishes prebuilt GPU wheels of the pinned version as release assets with sha256 digests: v0.3.36-vulkan (Windows x64 40 MB, manylinux x86_64 121 MB) and v0.3.36-cu130/-cu132 (Windows x64 217 MB, manylinux_2_35 x86_64 746 MB), among others.
Measured on that machine with Imprint’s own tagging call, a different exchange each time, no grammar change:
| Build | Seconds per exchange |
|---|---|
| CPU wheel, 4 threads | 38.4 |
| Vulkan, model split over both NVIDIA GPUs (llama.cpp default) | 8.0 |
| Vulkan, whole model on the RTX 4080 | about 8 (no faster than the split) |
| CUDA 13, RTX 4080 only | 4.7 |
| CUDA 13, all GPUs visible | aborts: ggml-cuda.cu: CUDA error (CUDA 13 has no code for the 1060) |
Two facts about the wheels shape the design:
- The Vulkan build links the Vulkan loader directly. On Windows,
ggml.dllimportsggml-vulkan.dll, which importsvulkan-1.dll. GPU drivers install the loader on Windows and desktop Linux, but servers and containers often lack it, and the binding then fails to import. llama_supports_gpu_offload()asks llama.cpp’s device registry. It is true only when a GPU device was found.
Decision
Section titled “Decision”requirements/llm-gpu.txt is the GPU build of the LLM overlay, with its own hash lock, llm-gpu.lock.txt:
- Windows x64 and Linux x64 use the
v0.3.36-vulkanwheels. - Every other platform matches
llm.txt: macOS builds from source with Metal, and Linux arm64 uses the CPU wheel.
The overlay has the same version and dependency pins as llm.lock.txt, so uv swaps the two builds by wheel URL.
installLLMOverlay installs the GPU build on the gpu and auto profiles, then runs llama_supports_gpu_offload() in the venv:
- When it prints
True, the GPU build stays. - When it prints
Falseor the import fails, setup installsllm.lock.txt.
The cpu profile installs llm.lock.txt directly. Setup checks this on every run, and nothing is recorded: swapping is a cached wheel install, and a machine that gains a GPU or driver picks it up on the next setup.
The nvcc rebuild is removed, along with its toolkit discovery, compute-capability parsing, the llama_cuda_failed record and the CUDA toolkit install hints. data/gpu_state.json keeps only the ONNX Runtime verdict. --retry-gpu clears that verdict as before.
n_gpu_layers=-1 offloads every layer, and llama.cpp picks the devices: all discrete GPUs, or the integrated GPU when there is no discrete one.
Local classification no longer constrains its reply with a JSON-schema grammar, which amends ADR 0010 for tagging. llama.cpp checks the grammar on the CPU for every generated token, and on a GPU that cost more than the inference itself. The reply goes through _parse_llm_classification and _validate_classification, the same path remote providers’ replies take, including its fallback when parsing fails. Summaries keep their grammar: their output is longer and nested, and was not measured without it. local_llm.complete therefore takes an optional schema.
Rejected alternatives
Section titled “Rejected alternatives”- The CUDA 13 wheel. It is about 1.7× faster than Vulkan on the RTX 4080. But it covers only NVIDIA Turing and newer, and it needs
cublas64_13.dllfrom the GPU embedding runtime (loaded first withonnxruntime.preload_dlls()). It also aborts when an unsupported GPU is visible, which would needCUDA_VISIBLE_DEVICESfiltering. It could later become a second GPU build for NVIDIA machines on the GPU embedding runtime. - Keep the nvcc rebuild. It needs a multi-gigabyte toolkit and a matching compiler, it is unhashed, and it covers NVIDIA only.
- Run
llama-serverbehind the OpenAI-compatible provider. That adds a managed service, which ADR 0035 already rejected. - Pin the model to the fastest GPU (
split_mode=NONE). On the measured machine Vulkan enumerated the GTX 1060 first, so a fixedmain_gpupicked the slow card (17 s). Picking a card needs a speed heuristic and was no faster than llama.cpp’s default split.
Consequences
Section titled “Consequences”- On the measured machine, LLM tagging is about 4.8× faster with no toolkit or compiler. AMD and Intel GPUs get offload for the first time.
- An integrated-only machine offloads to its iGPU. Setting
tagger.local.n_gpu_layersandchat.n_gpu_layersto0restores CPU inference. - Without the grammar, a tagging call takes about 4.1 s on Vulkan instead of 8.0 s (CUDA: 1.7 s instead of 4.7 s). All 13 unconstrained replies in those runs were valid JSON, but nothing now guarantees it. A malformed reply falls back to keyword domains, as a malformed remote reply already did.
- Upgrading the binding means updating the version in every URL of both overlays and recompiling both locks from scratch.
- macOS and platforms without a prebuilt wheel still build from source, as before.
Invalidation conditions
Section titled “Invalidation conditions”- If NVIDIA machines on the GPU embedding runtime would be better served by the CUDA wheel, about 1.7 times faster than Vulkan and deferred here, add it as a second GPU build.
- If upstream stops publishing hashed Vulkan wheels for the pinned version, the GPU overlay cannot move without compiling; revisit.
- If unconstrained local classification replies fail to parse often enough that the keyword fallback becomes common, restore a constraint or change the prompt.
Validation
Section titled “Validation”- Wheels: the published sha256 digests matched the downloads, both archives passed
zipfile.testzip, and their DLL/ELF imports were inspected. - Lock:
llm-gpu.lock.txtwas compiled with the pinned uv 0.5.11. Its non-llama pins and hashes are identical tollm.lock.txt. - Switching: in a fresh Linux venv, uv switched CPU → Vulkan → CPU by URL with plain
uv pip install --require-hashes, asdirect_url.jsonand the installedlibggml-vulkan.soshowed. - Windows: the Windows uv installed
llm-gpu.lock.txtwith--require-hashesinto a fresh venv. Setup’s offload check printed the three Vulkan devices andTrue. - Go:
TestInstallLLMOverlayKeepsTheGPUBuildOnlyWhenItOffloadscovers four cases with a fake pip and python:- the cpu profile
- offload found
- no GPU
- failed import
- Not yet verified: a full
imprint setupon Windows from a release build; Linux desktops with AMD or Intel GPUs; machines without a Vulkan loader, where the fallback is covered only by the unit test.