Enforces "one fact, one owner": technique pages keep mechanism, laws, traps and verdicts; per-endpoint shapes, digests and era measurements are cited to the endpoint page that owns them. Also repairs the four pointers the pass left empty (each verified against the cited owner before landing): - EMULATION dequant doubles host RAM (118 GiB -> ~238 GiB) -> [[frank-archon]] - 12.11 tok/s flat 304 -> 16,095 tokens -> [[Tcclaviger Fork Ops]] - bundled-head acceptance ladder 0.739/0.554/0.427/0.340 -> [[Qwen3.8 PP Deployment]] - llama.cpp PR #23897 dedup provenance -> [[qwen3.8-spec-matrix]] Replaces the retired-shape illustration in Llama Cpp Serving Sizing with the owned MiMo Q5_K_M row (6.40 GiB payload -> 20,530 MiB used), and unwraps a hard-wrapped [[Dankserver System Specs]] link that failed link integrity. Orphan census over the diff: 0 numbers lost (comma-insensitive match against the whole vault plus AGENTS.md).
177 lines
10 KiB
Markdown
177 lines
10 KiB
Markdown
---
|
||
type: reference
|
||
title: Llama Cpp Serving Sizing
|
||
description: Slot-pool sizing against a headroom band, per-device VRAM accounting, prompt-cache/state sizing and the QSA flash-attn fix — the measurement-first procedures for llama.cpp serving shapes.
|
||
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.
|
||
- **Context lengths: derive, never remember.** `llama-bench` computes
|
||
`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
|
||
[[frank-medium-llamacpp-dflash2]]; 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
|
||
- [[frank-medium-llamacpp-dflash2]] — the state-law worked example behind section 3
|
||
- [[DFlash DSpark Speculative Decoding]] — the spec A/B protocol on llama.cpp
|
||
- [[Dual-R9700-Cooperation]] / [[x4-pair-experiment]] — the multi-device side
|