--- type: reference title: Llama Cpp Serving Sizing description: "Measurement-first sizing for llama.cpp shapes: slot pools against a headroom band, per-device accounting, prompt cache." tags: [llama.cpp, vram, kv-cache, slots, flash-attn, sizing] timestamp: 2026-10-05T00:00:00-07:00 --- # Llama Cpp Serving Sizing ## 1. Slot-pool sizing against a headroom band Goal: choose `CTX_SIZE`/`PARALLEL`/KV types so N conversations fit a GPU while keeping a requested amount of VRAM free — from measurement, never arithmetic guesswork. 1. **Fix the invariants.** Read the payload's KV geometry (attention layers only, KV heads, head_dim, KV dtype bytes) and the per-slot rule `n_ctx_slot = CTX_SIZE / PARALLEL`. A hybrid model with `kv_unified = 'false'` gives each slot its own cache AND a fixed per-slot state buffer — slot count is a memory term, not free. 2. **Two boots on the real card** (production stopped, per the bench law) each near the target; record `nvidia-smi` used/total plus the boot log's `n_slots`/`n_ctx_slot`: shape A = current/cheap, shape B = new demand. `non-KV = used − (tokens × KiB/token)`; the two rows separate weights/buffers/graphs from the new demand; a projector is billed after the fit at roughly 1.5x its file size ([[frank-zealot]] Contract has that measurement). 3. **Solve the band:** `KV_tokens ≤ (card − non-KV − headroom_low) / KiB/token`, round the pool DOWN to a multiple of `PARALLEL × 4096` so every slot gets an equal integer window. Boot and measure; if outside the band, walk one step — never re-derive from a formula. 4. **Propagate the per-slot number — it is the client contract:** `models.yml` `contextWindow` = `n_ctx_slot` (NOT the model's native window) or long conversations die with `400 exceed_context_size_error` instead of compacting; compaction/summary budgets sized to the old window get re-checked; endpoint docs and the rollback table move too. 5. **Prove behaviour:** boot-log slot count, `nvidia-smi` free in band, N concurrent requests answered in parallel with N+1 queued, and — if a projector ships — a solid-color image probe through an **`image_url` content part** expecting the exact color. The legacy top-level `images` field is silently ignored, so a probe that uses it measures a hallucination, not vision. Gotchas: shorter slots can make decode FASTER (~5 % measured); the KV pool is allocated whole at boot so post-boot `memory.used` IS the ceiling; record both measured rows in the service `.env` comment block and mirror the non-KV price in the bats budget gate so a shape that busts the band fails the suite instead of the boot. ## 2. Per-device VRAM accounting ("where did the memory go?") First decide which kind of memory is in question: | device | kind | read it with | |---|---|---| | RTX 3090 (Dankclaw) | discrete GDDR6X | `nvidia-smi --query-gpu=memory.total,memory.used,memory.free` | | gfx1151 iGPU (Dankclaw) | **UMA** — "VRAM" is host RAM | `/sys/class/drm/card1/device/mem_info_gtt_{total,used}` vs `mem_info_vram_*`; the aperture and carve-out values are [[Dankclaw System Specs]]'s | | R9700s (Dankserver) | discrete | `rocm-smi --showmeminfo vram`; the per-card byte/MiB value (and the stale "64 GiB SVM-backed" figure it replaced) is [[Dankserver System Specs]]'s | Per-process truth on NVIDIA: `nvidia-smi --query-compute-apps=pid,process_name,used_memory`. Never add the iGPU's GTT to host RAM as a separate pool; `free -g` is the arbiter for co-tenants. - **Engine ledger first.** Halogen prints weights pinned (+ remaining 2 MiB contiguous blocks — the honest fragmentation signal: a block census that drops across a restart means the model did not come back into the same hole; the before/after census this was learned on is that era's, on [[frank-dragoon]]) device budget, KV pool/working split, "left for everything else". llama.cpp prints them at `llama-model.cpp:1827` (per-backend model buffer) and `llama-context.cpp:692` (compute buffer) on the era's build; on today's `master` the same `LLAMA_LOG_INFO` lines sit at `llama-model.cpp:2009` ("model buffer size =") and `llama-context.cpp:842` ("compute buffer size =") ([source](https://raw.githubusercontent.com/ggml-org/llama.cpp/master/src/llama-context.cpp) re-read 2026-10-18) — but **`llama-bench` suppresses loader INFO and a systemd unit did not put them in the journal** (verified `grep -c` = 0). Recipe: one launch through `llama-cli`/`llama-server` keeping the first ~200 lines. - **Otherwise compose from tensor sizes.** Group the GGUF tensor table by role (Flash-Next example: 75.0 GiB experts at 1.5625 GiB/layer, 32.8 GiB PLE table, 3.2 GiB everything else), KV ≈ 2 KiB/position per f16 attention layer, × derived context. Vendor delta is real: same layers+KV cost 12.80 GiB under Vulkan vs 14.99 GiB under CUDA (~3.3 vs ~1.1 GiB of graph/compute pool). - **Traps:** mmap'd PLE is reclaimable page cache; `-ot` makes it pinned — "move layers to the card" converts reclaimable cache into device memory and frees nothing for co-tenants. A served model's footprint is not its weight size: the MiMo Q5_K_M era booted a 6.40 GiB payload to **20,530 MiB used** of the 3090's addressable 24,576 — the Eras row and its addressable ≠ advertised law are on [[frank-zealot]]. What the card reports used is weights + KV + compute buffers + projector, never weights alone. Do the capacity subtraction before designing, on the card number the owner page quotes, not the advertised one. `n_ctx = n_prompt + n_gen + n_depth` (defaults 512/128/0, type_k/v F16, flash_attn AUTO, split_mode LAYER). `-p 512,16384 -n 64` = contexts 576/16,448; a plain `tg` row has ~zero depth — `-d` is the only thing that puts KV in the picture. - **Label every number** measured / derived / inference, and state the context each figure was taken at — 12.8 GiB at 65,568 tokens is not the same claim as at 200k. ## 3. Prompt cache and state sizing `--cache-ram` (alias `-cram`, `common/arg.cpp:1719`) is HOST RAM for saved prompt states — unpinned, on demand, moves no VRAM. `--ctx-checkpoints` (alias `-ctxcp`/`--swa-checkpoints`, `common/arg.cpp:1701`) is a different mechanism: per-slot KV snapshots; `0` disables them. Both flags are current upstream; the two defaults quoted here (8192 MiB and 32) are the era's values and were not re-readable in the upstream sources checked *(unverified 2026-10-18)* — print them from the binary (`llama-server --help`) before sizing on them. **State law** (compute before trusting any cap): a state is the serialized KV of the whole sequence + constant recurrent state + the draft context's KV: `per_token = 2 × n_full_attn_layers × n_kv_heads × head_dim × bytes (K and V) + draft_per_token`. The worked qwen35-hybrid instance of that formula (full layer count, f16 KV, MTP draft) and the slot-size table it prices are on ; cross-check any such arithmetic against the journal's `prompt state size` lines — the two must agree. **The journal is the instrument:** `exceeds cache size limit … skipping` = cap binding in production; `making room … removing oldest` / `cache size limit reached` = eviction, and the printed size proves the active cap. Do NOT claim a raise is proven by `prompt_tokens_cached_total` rising — on exact repeats that counter is 0 by construction. **Hybrid + zero-checkpoint trap:** for a hybrid (DeltaNet/SSM + attention) model, `seq_pos_min` sits at the END of the slot's own prompt (a recurrent cell stores only the last position), so with `--ctx-checkpoints 0` there is no checkpoint to fall back on and the server logs `forcing full prompt re-processing … (likely due to SWA or hybrid/recurrent memory)`. Consequences: intra-slot reuse only survives when the request echoes the assistant's previous reply verbatim (a pure user resend is always a full re-prefill even at f_sim 1.000); the RAM prompt cache becomes the ONLY carrier of a conversation state ACROSS slots, so a cap that refuses deep states costs minutes of re-prefill whenever slots are reassigned; state restoration only runs via the LRU path (or LCP with f_keep < 0.5) — a plain prefix hit never calls `prompt_load`. **Verify a change:** live argv carries the flag (not the deploy log); `/health` 200 + real completion; VRAM unchanged and host RAM mostly free; no new `exceeds cache size limit` lines, with a `making room` entry larger than the old cap as direct proof. If the ceiling itself was not exercised live, say so — crossing it needs a >120k-token conversation and must not run beside a live service (bench law). ## 4. QSA flash-attn assert fix (qwen4exp) `--flash-attn on` aborts graph build for qwen4exp models: `GGML_ASSERT(a->type == GGML_TYPE_F32)` at `ggml.c:5371` through `build_attn_qsa`. Root cause: `build_attn_qsa` calls `ggml_fill` on the F16-cast kq mask returned by `get_kq_mask()` when flash attn is on, but `ggml_fill` requires F32. Fix (mirrors the MLA sparse path in `llm_graph_context::build_attn`): run the top-k mask surgery on the F32 base mask (`inp->self_kq_mask`), cast to F16 only at the attention call when `flash_attn` is enabled (`kq_mask_attn = cparams.flash_attn ? ggml_cast(ctx0, kq_mask_top_k, GGML_TYPE_F16) : kq_mask_top_k`). The patch lives at `scripts/patches/qwen4exp-qsa-f32-mask.patch`. Only the charlie12345/ROCmFPX build-qwen38 (PR #27742 qwen4exp merged) takes it; CIRU, upstream and stock builds do not. Validate live: load with `--flash-attn on`, health 200, a real completion finishing `"stop"`, journal clean of `GGML_ASSERT`/`cudaMalloc`/`missing tensor`, and flash-attn engaged in the timings. The context cap that build reached for full-model residency is part of that era's record on [[frank-dragoon]]. ## Related - [[GGUF Compatibility]] — what a payload is before it is priced - [[frank-zealot]] — the endpoint whose Eras rows are the two-boot priced shapes this method produced - the state-law worked example behind section 3 - [[DFlash DSpark Speculative Decoding]] — the spec A/B protocol on llama.cpp - / — the multi-device side