Rolling-KV β overflow throughput evaluations (consolidated)
What this is. The measured performance record for rolling-KV / POSITION_WINDOW (streaming the host KV tail over PCIe when the cache overflows VRAM). The numbers were historically scattered across three feature docs and one study; this file is the single evals-side index. Design + code live in the feature docs; this file is results only, with back-links.
Sources consolidated here:
docs/features/rolling_kv.mdβ M7 design + M7-G gate + the bug-1843 fix + the #588 3090 cliff curve.docs/features/rolling_kv_step_prefetch.mdβ the 3090 overflow-decode hyperbola (both model classes) + the step-prefetch design.docs/features/rolling_kv_compute_reserve.mdβ the bs2 boot-OOM fix (bug-1342) that unblocked the RTX 6000 curve, + the prefill cliff.docs/evaluations/hal_kv_compression.mdΒ§3a β the bs2 host-stream-exact spill cliff (route-(a), the reason compression wins over spill).
0. The one-line verdict
Rolling-KV's re-stream design is bandwidth-bound by physics: every decoded
token restreams the spilled tail over PCIe, so decode tps follows
t(token) β resident_compute_ms + spilled_bytes / PCIe_BW. After the bug-1843
compute-side fix, the curve sits within ~5 % of that model. No copy-plumbing
lever can beat the link itself β the rolling-KV perf line is CLOSED (NO-GO)
for deep overflow; window mode ships as the graceful-overflow fallback (linear,
correctness-clean, no cliff pathology). The real capacity lever is residency-side
compression, not spill β see hal_kv_compression.md Β§4
and context.md Β§5b.
1. The mechanism (why it is bandwidth-bound)
POSITION_WINDOW keeps a resident window of recent cells and streams the older tail
from host RAM every decode step. At n_q=1 an attention op's compute is a sub-ms
VRAM read, but its tail-tile DMA is ~30Γ that, so the op-level 1-tile-ahead
ping-pong is DMA-bound from MiB 1. Full-attention models stream a tail on every
layer; iSWA models stream only their few global layers β an order-of-magnitude gap.
Mechanism + the cs_sync cross-layer barrier: rolling_kv_step_prefetch.md
Β§"Root cause".
2. 3090 overflow-decode cliff β the original hyperbola (2026-07-01)
Op-level streaming, pre-bug-1843. PCIe ~6.4 GB/s, +0.156 ms/MiB.
| model | attn | streaming layers | overflow decode | shape |
|---|---|---|---|---|
| Gemma-4-A4B-128e | iSWA | ~5 global | 71 β 19.5 tps @236 MiB tail | graceful hyperbola |
| Qwen2.5-14B-1M | full | 48 | 26.9 β 0.86 tps @~1.2 GB tail | ~10Γ worse (48 streaming layers) |
Qwen engages POSITION_WINDOW (GPU 60β84 % busy) β it is slow purely because
full-attention restreams a tail on every layer. Source:
rolling_kv_step_prefetch.md Β§"Problem (measured, 3090)". (#583/#584)
3. The compute-side fix β bug-1843 / patch 0098 (2026-07-05)
The S0-cliff redirects (#586 staging ring / #587 barrier-drop) were perf-inert
on the 3090 (bug-1838: the PCIe link is already saturated β no copy-side slack).
The real waste was a per-tile streaming_lse_kernel recompute that every
head_dim β€ 256 decode tile paid, because the Stage-3a LSE channel was gated on
head_dim > 256. Patch 0098-rolling-kv-lse-decode arms the LSE channel for
D β€ 256 at all three streaming-FA sites.
Gate (3090, DSO df46dc36, Qwen3-8B-Q8_0 ctx 40960 / vt 14000):
- spill decode 1.14 β 6.04 tps (5.3Γ, == NOOP ceiling), needle PRESENT
- resident 42.5 tps unchanged;
OPENCOTI_LSE_NOOPnow speed-inert (recompute structurally off the path) - correctness: teacher-forced logit-equiv (ours-resident REF vs POSITION_WINDOW at 256/16384 cells resident) real_frac 0.0, frac_full_agree 1.0, mean_tv 0.0068 β PASS
Source: rolling_kv.md Β§"2026-07-05 β bug-1843 shipped". (#623)
4. #588 perf gate β post-fix 3090 cliff curve β CLOSED NO-GO (2026-07-06)
bug-1843-fixed DSO (df46dc36), 3090, Qwen3-8B-Q8_0, ctx 16384 (~13.1k-token
prompt = 2304 MiB f16 KV), needle PRESENT in every cell.
| vt (MiB) | window split | occupied tail | decode tps | ms/token |
|---|---|---|---|---|
| 22000/16000/14000/13000 | FULLY RESIDENT | 0 | 42.3β42.5 | 23.6 |
| 12000 | 11008 / 16384 cells | ~2.1k cells β 295 MiB | 15.5 | 64.6 |
| 8000/4000/2000 | 256 / 16384 (floor) | ~12.9k cells β 1815 MiB | 3.33 | 300 |
The PCIe model fits within ~5 %: t β 23.6 ms + occupied_tail_bytes / 6.5 GB/s
(vt12000 predicted 67 ms vs 64.6 measured; floor predicted 303 vs 300). Spill
decode is now purely bandwidth-bound β the structural floor for a re-stream design.
Verdict: NO-GO for further re-stream perf work. The "within 10β15 % of resident"
bar is only reachable for tails β€ 25 MiB (170 cells) on a 6.5 GB/s 3090. No
#586-class copy-plumbing beats the link. Source: rolling_kv.md Β§"2026-07-06 β
#588 perf gate".
5. bs2 RTX 6000 β the serving-relevant host
opencoti serves on the RTX 6000 (PCIe ~50 GB/s, ~8Γ the 3090's link β see memory
reference_pcie_bandwidth_hosts). Two bs2 measurements exist plus one open cell.
5a. Host-stream-exact spill cliff (route-(a), simulated 24 GB card, 2026-07-01)
From hal_kv_compression.md Β§3a β pre-bug-1843, f16 KV, 8 GiB budget:
| cell | resident / host tail | decode tps | needle |
|---|---|---|---|
| f16 @32k | all / 0 | 67.3 | β |
| f16 @49k | 43k cells / 1152 MiB | 57.6 | β |
| f16 @64k | 43k / 4224 MiB | 0.99 | β |
| f16 @98k | 43k / 10368 MiB | 0.71 | β |
| q4_0 @64k (resident) | all / 0 | 27.1 | β |
| q4_0 @128k (resident) | all / 0 | 16.2 | β |
The cliff is catastrophic past ~2 GiB tail (0.99 tps @64k). Same-context resident compression is 27Γ faster β this is why route-(a) spill is demoted to beyond-max-compression fallback and compression is the primary mechanism.
5b. Prefill cliff after the boot-OOM fix (bug-1342, 2026-07-01)
rolling_kv_compute_reserve.md Β§"Gate result": with the two-pass compute-reserve
fix, deep-overflow configs boot on the RTX 6000; prefill falls 4000 β ~450 tok/s
as the host tail grows (expected cliff). This fix is what makes the Β§5c curve
runnable on bs2 at all.
5c. bs2 real-spill characterization β qw35a3, 256k fine tail ladder, DCA-on vs DCA-off (2026-07-10, #631/#633)
This closes the Β§5c gap with the first fine bs2 spill curve, and it OVERTURNS the
initial coarse read. Vehicle: the serving-size Qwen3.6-35B-A3B (qw35a3, hybrid
DeltaNet+attention MoE, D256 attn, fast A3B-active decode), 256k ctx, f16 KV, fixed binary
llamafile.dualctx633. A fine --vram-target ladder (200 MiB tail steps) run both
DCA-on and DCA-off shows: there IS a graceful, bandwidth-bound overflow band (the
earlier "hard cliff / no graceful band" claim is RETRACTED); the catastrophic collapse
seen in the first coarse sweep was a DCA-specific bug (bug-2144), not a hardware limit
and not a hybrid-recurrent property β the coarse sweep's smallest tail (1710 MiB) simply
landed past the DCA-on knee, so it only ever sampled the buggy regime.
Two gate fixes landed first. (1) bug-2142 / #633 β --vram-target /
--kv-residency-mode window were silently ignored for every hybrid-recurrent model
(LLM_ARCH_QWEN35/QWEN35MOE, Nemotron-H, Falcon-H1): the llama_memory_hybrid /
llama_memory_hybrid_iswa wrappers (llama-model.cpp create_memory ~L2041/L2022) built
their internal attention llama_kv_cache with hardcoded residency defaults, so the sizer
always logged FULLY RESIDENT regardless of the flag. Fix (opencoti-hook: rolling-KV hybrid residency (bug-2142), host-only, 6 edits/5 files): thread vram_target_mib / pcie_bw_gbps / kv_residency_mode / kv_compute_reserve_mib / kv_window_measure_pass through
both hybrid ctors into the attention cache, mirroring the iSWA (L2100) / plain-dense (L2140)
paths that already forwarded them. Verified-engaged + regression-clean (shield below). (2)
Note bug-1839: --vram-target is a TOTAL device cap (weights+KV+compute), so a spill
target must be < weights + resident-KV + compute; the old "24 GB card = --vram-target 9600" recipe (14B) now floors resident-KV to 0. Empirical tailβtarget map for qw35a3 @256k:
tail_MiB β 36708 β vram_target_MiB (fits every cell to Β±5 MiB).
qw35a3 KV is tiny (hybrid β only the few full-attention layers grow): f16 KV =
5120 MiB @256k, 10240 MiB @512k (~20 KiB/tok), an order of magnitude under the 14B-1M
full-attention class (Β§5a). On the 96 GB RTX 6000 it never needs to spill; the ladder
forces spill via --vram-target to characterize a smaller card / multi-session KV pressure.
DCA-OFF β the correct rolling-KV behavior (bandwidth-bound, graceful)
The non-DCA build_attn overload routes spilling layers through the bug-1840 streaming
fast-path (build_attn_mha_position_window), which FAs the resident window on-device and
streams only the tail once. Result: a smooth, bandwidth-bound decline β the right shape.
| host tail | resident cells | prefill tps | decode tps | achieved GiB/sΒΉ | needle |
|---|---|---|---|---|---|
| 0 (resident) | 262144 / 262144 | 4799 | 123.5 | β | β |
| ~0 (VT 36308) | resident | 4797 | 123.0 | β | β |
| 80 MiB | 258048 | 326 | 105.4 | ~15 (fixed engage cost) | β |
| 280 MiB | 247808 | 326 | 103.6 | ~29 | β |
| 680 MiB | 227328 | 326 | 104.4 | ~71 | β |
| 1080 MiB | 206848 | 325 | 57.7 | ~62 | β |
| 1880 MiB | 165888 | 324 | 31.7 | ~60 | β |
| 2680 MiB | 124928 | 315 | 21.4 | ~57 | β |
ΒΉ tail_MiB / (1000/decode_tps) ms, tail-streamed-once framing. Knee at ~680β1080 MiB:
decode holds within ~15 % of resident to a 680 MiB tail (104 vs 123.5), then declines
cleanly, saturating at ~57β62 GiB/s β i.e. PCIe-bandwidth-bound near the RTX 6000
link, exactly the correct rolling-KV shape (Β§4's t β compute + tail_bytes/BW, now
confirmed on the 50 GB/s bus). Needle PRESENT at every depth. The β15 % step at first spill
(80 MiB β 105 tps) is the fixed streaming-path engagement cost.
β DCA-off PREFILL is broken though (bug-2145) β a second, mirror-image gap. The prefill column above cliffs from 4797 β ~325 tok/s the instant any tail forms (80 MiB) and then stays FLAT ~325 all the way to a 2.7 GB tail (325/325/325/324/315 β tail-INDEPENDENT). Tail-independence is the smoking gun: a bandwidth-bound prefill would slow with the tail; a flat 325 means the fast prefill path is simply switched off whenever the window is active (a routing gap, not a copy cost). So DCA-off is graceful for decode but not prefill.
DCA-ON β perfect to ~800 MiB, then a sharp cliff (bug-2144 β NOW FIXED, see Β§5d)
β HISTORICAL / pre-fix. The cliff below is the diagnosis that motivated the fix. bug-2144 is fixed by the C2 streaming
dca_fusedkernel (Β§5d): DCA-on spill now tracks/beats the DCA-off reference at matched tails, no cliff. The pre-fix ladder is retained because it is the evidence base for the mechanism; read it as "what was broken," not current behaviour.
| host tail | resident cells | prefill tps | decode tps | needle | |---|---|---|---|---|---| | 0 (resident) | 262144 | 4392 | 112.5 | β | | 205 MiB | 251648 | 4077 | 111.7 | β | | 405 MiB | 241408 | 4080 | 112.5 | β | | 605 MiB | 231168 | 4077 | 113.2 | β | | 805 MiB | 220928 | 4073 | 112.8 | β | | 1205 MiB | 200448 | 3668 | 2.93 β | β | | 1605 MiB | 179968 | 3011 | 3.09 β | β | | 2405 MiB | 139008 | 2330 | 3.19 β | β | | 3205 MiB | 98048 | 2010 | 3.33 β | β |
DCA-on is penalty-free (full 112β113 tps) out to an ~805 MiB tail β actually better
than DCA-off in that band (DCA-off already pays β15 % at 80 MiB) β then cliffs 38Γ in a
single 400 MiB step (805 β 1205 MiB: 112.8 β 2.93 tps) and stays dead-flat 3 tps past it.
Past-cliff decode is 341 ms/token: only 3.5 GiB/s on the tail-once framing (βͺ link β
not bandwidth-bound), but **26 GiB/s** on the ~9 GiB full-KV-round-trip framing
(window-D2H 3920 MiB + full-concat-H2D 5120 MiB) β the fingerprint of bug-2144.
Matched-tail contrast (the empirical lock on the code diagnosis): at ~1.1 GiB tail, DCA-off decodes 57.7 tps vs DCA-on 2.93 tps β ~20Γ faster. Same model, same binary, same tail; the only difference is DCA on/off. This confirms the JOB-1 code diagnosis below.
Mechanism β bug-2144: DCA bypasses the streaming window path (confirmed)
The bug-1840 perf fast-path build_attn_mha_position_window (llama-graph.cpp:2503) is
reached only from the non-DCA build_attn overload (llama-graph.cpp:2999-3007).
DCA's build_attn_dca_core (dca.cpp:229) fetches K/V with the raw whole-cache accessors
get_k/get_v (dca.cpp:251-252), which for a spilling layer return the cross-backend
correctness fallback ggml_concat(win_v /*device*/, tail_v /*pinned host*/, 2)
(llama-kv-cache.cpp:2585-2611; its own comment names build_attn_mha_position_window as
the perf path it is not taking). The scheduler materialises that concat on the compute
backend β a synchronous ~2Γ full-KV hostβdevice round-trip every decode step (bug-1840's
documented "12.6 GiB/step both ways β 0.99 tps at 4.2 GB tail", llama-graph.cpp:2989-2990);
the async DMA tile overlap lives in ggml_cuda_streaming_flash_attn (fattn.cu:1170) which
DCA never enters. This predicts the ~26 GiB/s effective full-KV rate, the flat-past-cliff
plateau (round-trip volume β window-D2H + full-KV-H2D, ~constant), and the slight rise
2.93 β 3.33 as the tail grows (smaller window β less window-D2H).
The sharp ~1 GiB step (805 β 1205 MiB) β CONFIRMED as the ~1-chunk knee. The DCA chunk is 32768 cells β 640 MiB; the step sits between a 1.26-chunk tail (805 MiB, fast) and a 1.88-chunk tail (1205 MiB, slow) β the tail crossing into a second DCA chunk is the trigger, exactly as hypothesised. The C2 fix (Β§5d) reproduces this knee on the fixed binary as a graceful step down (37 β 15 tps at the same crossing) instead of a cliff, since the streaming kernel reads only the chunks it needs rather than round-tripping the full KV. Below ~1 chunk of tail DCA-on stays entirely device-resident and pays nothing (unchanged pre/post fix).
Resident-compression anchors (no spill; DCA-on, everything on-device)
| ctx | KV type | KV MiB | prefill tps | decode tps | needle |
|---|---|---|---|---|---|
| 256k | f16/f16 | 5120 | 4358 | 116.3 | β |
| 256k | q8_0K/q4_0V | ~2080 | 4241 | 75.2 | β |
| 256k | turbo2/turbo2 | 680 | 3664 | 46.4 | β |
| 512k | f16/f16 | 10240 | 2856 | 87.65 | β |
| 512k | q8_0K/q4_0V | ~4110 | 2741 | 48.8 | β |
| 512k | turbo2/turbo2 | 1360 | 2384 | 27.3 | β |
Regression shield. Non-hybrid 14B-1M-Q8_0 @65536, --vram-target 9600, fixed binary β
256 / 65536 cells resident, host tail = 12240 MiB β byte-identical to the pre-fix
binary, so the bug-2142 fix is isolated to the hybrid path.
Dense control (Qwen-27B-omni, full attention) β DEFERRED. Only the resident calibration
landed: KV 16384 MiB @256k (full-attention β ~10Γ the hybrid KV, as expected), DCA-off
resident decode 36.9 tps. The dense spill ladder did not run (GPU time). It is not
blocking β the qw35a3 DCA-on-vs-off contrast already isolates bug-2144 as DCA-specific
(not hybrid-specific) β but a clean single-GPU dense ladder is worth running later (after
#632's tps work) to confirm the DCA-off graceful band and the DCA-on cliff reproduce on a
dense arch.
The full 4-quadrant picture β phase Γ DCA (two halves of one routing gap)
Both anomalies are the same defect seen from two sides: the streaming-window fast-path is wired for only two of the four (phase Γ DCA) quadrants. Where it is wired, behaviour is graceful; where it is not, decode/prefill falls to a slow fallback.
| DCA-OFF | DCA-ON | |
|---|---|---|
| DECODE | β graceful β bandwidth-bound, 123β104@680MiBβ21@2.7GB, ~57β62 GiB/s (streaming window path) | β
graceful (bug-2144 FIXED, Β§5d) β was 112β2.9 cliff; C2 streaming dca_fused now tracks/beats DCA-off at matched tails |
| PREFILL | β cliff β 4797 β flat ~325, tail-INDEPENDENT from 80 MiB to 2.7 GB (bug-2145: fast prefill path disabled under window mode) | β graceful β 4076@605MiB β 3668@1.2GB β 2010@2.7GB (declines with tail) |
The diagonal was the tell: non-DCA-decode β and DCA-prefill β always took the fast path;
non-DCA-prefill (bug-2145) and DCA-decode (bug-2144) were the two uncovered quadrants.
DCA-decode is now covered (bug-2144 FIXED, Β§5d) β the C2 streaming-tail kernel routes DCA
spill through the same async tile machinery. bug-2145 remains open (non-DCA prefill): its
tail-independence (80 MiB costs the same 325 tok/s as 2.7 GB) proves it is a path switch
β the fast prefill kernel is turned off the moment window_cells < kv_size, regardless of how
little actually spilled β not a bandwidth cost. It is an additive routing/plumbing gap around
the same build_attn_mha_position_window / ggml_streaming_flash_attn machinery, not a model
or hardware limit. Neither quadrant ever lost the needle β correctness is clean throughout;
these are purely throughput-path gaps.
Verdict (revised)
- A graceful overflow band exists on bs2 β for DECODE. With DCA off, qw35a3 spill decode is bandwidth-bound (~57β62 GiB/s, needle clean) and holds within ~15 % of resident to a ~680 MiB tail β the correct rolling-KV behavior on the 50 GB/s link. The Β§0/Β§4 "deep overflow is bandwidth-bound" verdict stands (for decode); the "no graceful band for hybrid" over-claim is withdrawn. DCA-on decode is now graceful too (bug-2144 FIXED, Β§5d). The one remaining non-graceful quadrant is DCA-off prefill (bug-2145) β a DCA-off spill config still cliffs on prefill; closing it needs the same streaming-window routing applied to the non-DCA prefill path.
- DCA-on had a ~1 GiB decode cliff (bug-2144) β FIXED (Β§5d, C2 streaming
dca_fused). Was perfect below800 MiB tail then a full-KV concat round-trip (20β55Γ slower). The C2 streaming-tail kernel now makes DCA-on spill track/beat the DCA-off reference at matched tails (18Γ recovery at a 6.4 GiB tail). GatedWS2_DCA_STREAM=1pending ship green-light. - Compression remains the primary capacity lever (Β§0,
hal_kv_compression.mdΒ§3a/Β§4): turbo2 keeps qw35a3 resident at 0.68/1.36 GiB KV (46/27 tps), and qw35a3's KV is small enough to stay resident on a 96 GB card regardless. Spill (DCA-off) is a valid graceful-overflow fallback for smaller cards / multi-session pressure; the bug-2144 fix (Β§5d) now extends that graceful fallback to DCA-on configs too.
Contention caveat. These ran on GPU1 with #632's tps work on GPU0 concurrently. GPU1 is a
separate device (isolated VRAM/compute), and the load-bearing conclusions β the ~20Γ DCA-off
vs DCA-on gap, the DCA-off bandwidth saturation, the DCA-on cliff position β are far outside
any cross-GPU noise and internally consistent (monotone ladders). Fine-grained tps (Β±1β2 tps
between adjacent cells) may carry minor host/PCIe contention; correctness (needle) and the
resident/tail splits are contention-immune. Method: fixed binary, DSO 2377635512 (bug-2141
guard, inert for these non-degenerate rows), no rebuild during the run.
5d. bug-2144 C2 fix β DCA-on spill made graceful (streaming dca_fused), Qwen2.5-14B-1M @128k (2026-07-10)
The fix. bug-2144's DCA-on decode cliff (Β§5c: full-KV concat round-trip, ~55Γ collapse past
~1 chunk of tail) is eliminated by giving the fused DCA kernel a two-region window+tail streaming
path β the same async tile ping-pong the non-DCA build_attn_mha_position_window uses. Host
plumbing threads k_tail/v_tail/n_win into ggml_flash_attn_ext_dca_fused (tail on
src[7]/src[8], op_params[5]=n_win); gated behind WS2_DCA_STREAM=1, decode-only via
WS2_NQ_MAX (n_q β€ 16). Legacy concat path is byte-identical when off. Marker
opencoti-hook: DCA streaming-tail (bug-2144-C / Stage C) (dca.cpp).
Decisive ladder β DCA-on (C2 stream) vs DCA-off (rolling-KV reference). Both modes re-measured
on the reserve-fixed C2r binary (bug-2146 below), so these are the clean, accurate numbers β no
reserve artifact, no run contamination. Qwen2.5-14B-1M Q8_0, f16 KV, -c 131072, needle
QUASAR-4417, bs2 RTX 6000 single-GPU, ~51 GB/s eff link. Both prefill / decode tps recorded;
needle PRESENT in every cell of both ladders:
| vram-target | DCA-on tail | DCA-on prefill / decode | DCA-off tail | DCA-off prefill / decode |
|---|---|---|---|---|
| 41888 | 0 (resident) | 2094 / 38.50 | 0 (resident) | 1547 / 40.13 |
| 41000 | ~864 MiB | 2016 / 37.27 | ~864 MiB | 1342 / 35.82 |
| 40200 | ~1680 | 2015 / 37.35 | ~1680 | 1335 / 35.92 |
| 39400 | ~2496 | 2016 / 37.06 | ~2496 | 1338 / 35.91 |
| 38600 | ~3264 | 2016 / 37.23 | ~3264 | 1340 / 35.92 |
| 37800 | ~4080 | 2015 / 37.23 | ~4080 | 1344 / 35.56 |
| 37000 | ~4896 | 2017 / 37.19 | ~4896 | 1335 / 34.75 |
| 36200 | ~5808 | 1598 / 14.45 (knee) | ~5664 | 1172 / 10.78 (knee) |
| 35400 | ~6720 | 1361 / 11.69 | ~6480 | 1028 / 9.29 |
| 34600 | ~7680 | 1164 / 9.72 | ~7296 | 933 / 8.17 |
| 33000 | ~9600 | 925 / 7.29 | ~8880 | 782 / 6.55 |
| 31000 | ~12000 | 765 / 5.56 | ~10896 | 670 / 5.22 |
Verdict: the C2 streaming path makes DCA-on spill track and beat the DCA-off "normal" rolling-KV
reference at every point β both hold their full-speed plateau out to a ~4.9 GiB tail (DCA-on
37.2, DCA-off ~35.8), knee together at **5.6 GiB** (tail crossing 1 DCA chunk = 32768 cells),
then the same graceful bandwidth-bound rolloff, no cliff. DCA-on decode is faster than DCA-off
at every cell (plateau and deep tail) because the fused kernel reads only the chunks it needs,
whereas DCA-off window mode streams the whole tail; DCA-on prefill is also higher on the plateau
(2015 vs 1340). Pre-fix DCA-on cliffed to ~0.68 tps past ~1 GiB of tail β now 11.69 at a 6.7 GiB
tail = **17Γ recovery.** bug-2144 CLOSED on perf. (vt=41888 DCA-on decode 38.50 confirms full
residency under the bug-2146 fix; the harness's tail-grep prints a stale iter-0 value there because
the final "FULLY RESIDENT" boot line carries no "host tail =" token β decode is ground truth.)
Caveat for the pending bug-2145 fix (non-DCA prefill cliff, #636): that fix, if it lands, can only move the DCA-off prefill column (when the window engages) β decode is untouched in both modes and the DCA-on ladder is entirely unaffected. Worst case is a cheap one-column prefill re-run of the dcaoff mode, never the whole matrix.
Reserve artifact β root cause of the pre-fix forced β₯3 GiB tail (now fixed; the ladder above is
already the fixed run). The bug-1342 two-pass
compute-reserve sizer measures pass 1 with a degenerate wc=256 window (llama-kv-cache.cpp
measure_pass), which for the streaming graph means a maximal ~130816-cell host tail. Instrumented
boot (-lv 5, vt=41888):
Pass 1 (measure, wc=256, tail=130816): CUDA0 compute buffer = 4088.81 MiB β reserve 4344 MiB
Pass 2 (real, wc=114176, tail=16896): CUDA0 compute buffer = 926.01 MiB
The streaming decode graph's real device compute buffer is only 926 MiB; the reserve holds
back 4344 MiB β a 3.16 GiB over-reservation, a pure measurement artifact of the wc=256 pass
(confirmed vs a WS2_NQ_MAX=0 control: 18216 vs 15054 MiB budget at identical free VRAM). This is
why vt=41888 was forced to a 3168 MiB tail instead of staying fully resident.
FIX β bug-2146, IMPLEMENTED + validated (host-only, no DSO rebuild). llama_context ctor
(llama-context.cpp) now iterates the two-pass reserve to a fixpoint instead of trusting the
single degenerate wc=256 measurement: each re-size grows the window β shrinks the tail β shrinks
the measured buffer, so the reserve decreases monotonically and converges (to the plain resident
buffer, tail 0, when the KV fits). The resident graph's buffer is a subset of the streaming
graph's, so a re-size only ever reveals a smaller-or-equal buffer β it can never under-reserve.
Capped at 3 iterations; stops as soon as the reserve stops shrinking (128 MiB margin). For the
non-streaming case pass 2 measures what pass 1 did β converges in ONE step, byte-identical to
prior behaviour. Validated on the same C2r binary (Qwen2.5-14B-1M Q8_0 @128k, bs2):
| config | pre-fix | post-fix (bug-2146) |
|---|---|---|
| vt=41888 (KV fits) | 3168 MiB tail, 37.10 tps | FULLY RESIDENT, tail 0, 38.40 tps (converged iter 0β1β2) |
| vt=35400 (genuine spill) | 9648 MiB tail, 7.24 tps | 6720 MiB tail, 11.72 tps (+62%) (reserve 14966β17858 budget) |
Needle PRESENT both cells. This recovers the ~3.16 GiB and, because the corrected (smaller) reserve
grows the resident window at every vram-target, every Β§5d/Β§5e streaming ladder cell above was
measured with the over-large reserve and improves under the fix (the tables are the conservative
lower bound). Still-open cleanup: strip leftover get_k_tail: ENTRY / ACTUAL tail
LLAMA_LOG_WARNs (llama-kv-cache.cpp:3252/3389/3408) before ship.
5e. Rolling-KV spill vs upstream CPU-layer offload β bytes vs compute (Qwen2.5-14B-1M @128k, 2026-07-10)
The honest baseline for "what does everyone else do when VRAM runs out" is not rolling-KV β it
is upstream llama.cpp's -ngl < n_layer, which pushes whole transformer layers (weights +
their KV + their compute) to CPU. This section pits that against rolling-KV spill on the same
model/host, to show the two overflow mechanisms are categorically different.
Two ways to move data off a too-small card:
- Rolling-KV spill β all compute stays on GPU; only KV bytes stream over PCIe (window on device, tail on host). Cost is PCIe-bandwidth-bound.
-ngllayer offload β whole layers' compute moves to CPU. Cost is CPU-matmul-bound (and the CPU also owns those layers' KV). This is what stock llama.cpp does when the model+KV won't fit.
-ngl offload ladder (DCA off, full-KV-in-VRAM for GPU-resident layers, -c 131072, needle
QUASAR-4417, bs2 RTX 6000; RAM column = weights 279 MiB + KV 512 MiB per CPU layer, + ~790 MiB
output tensor which drops to CPU at any -ngl β€ 48, + ~790 MiB token-embedding always on CPU):
-ngl |
layers on CPU | β RAM offloaded | prefill tps | decode tps | needle |
|---|---|---|---|---|---|
| 99 | 0 (output on GPU) | ~0.8 GiB (embd) | β | 39.81 (all-VRAM baseline) | β |
| 48 | 0 (outputβCPU) | ~1.6 GiB | 1439 | 13.67 | β |
| 46 | 2 | ~3.1 GiB | 1213 | 7.02 | β |
| 44 | 4 | ~4.6 GiB | 1097 | 4.51 | β |
| 42 | 6 | ~6.2 GiB | 1008 | 3.41 | β |
| 40 | 8 | ~7.7 GiB | 917 | 2.67 | β |
| 38 | 10 | ~9.3 GiB | 847 | 2.19 | β |
| 36 | 12 | ~10.8 GiB | 795 | 1.86 | β |
| 34 | 14 | ~12.4 GiB | 745 | 1.63 | β |
| 32 | 16 | ~13.9 GiB | 721 | 1.44 | β |
| 31 | 17 | ~14.7 GiB | 678 | 1.38 | β |
Gotcha (load-bearing for reading this table): -ngl 48 is not all-in-VRAM. llamafile keeps
the output/LM-head projection (152k-vocab Γ 5120) on CPU until -ngl > n_layer (=48), so at
-ngl 48 all 48 transformer layers are on GPU but the final logit projection runs on CPU every
decode step β that alone is the 39.81 β 13.67 drop (3Γ). The true all-VRAM number is -ngl 99
(39.81 tps, bounded by reading model 14 GiB + KV 24.5 GiB per token on the RTX 6000).
The three-way verdict β decode tps at matched GiB moved off the GPU:
| off-GPU tail | rolling-KV C2 (DCA-on, Β§5d) | DCA-off window (Β§5d) | -ngl CPU offload |
|---|---|---|---|
| ~3.3 GiB | 37.2 | 35.9 | 7.0 (β3.1 GiB) |
| ~4.9 GiB | 37.2 | 34.8 | 4.5 (β4.6 GiB) |
| ~5.7 GiB | 14.5 | 10.8 | 3.4 (β6.2 GiB) |
| ~6.6 GiB | 11.7 | 9.3 | 2.7 (β7.7 GiB) |
| ~9.5 GiB | 7.3 | 6.6 | 2.2 (β9.3 GiB) |
| ~11 GiB | 5.6 | 5.2 | 1.9 (β11 GiB) |
Rolling-KV spill beats upstream CPU-layer offload by ~3β8Γ at every matched off-GPU volume. Of
the two rolling-KV variants, DCA-on C2 is fastest at every point (it reads only the DCA chunks it
needs; DCA-off window streams the whole tail) β the streaming dca_fused path is not merely
on-par with the non-DCA reference, it edges past it. Even at the shallowest possible offload (just
the LM head, 1.6 GiB), CPU offload (13.67) is already below rolling-KV at a 3 GiB tail (37). The
reason is structural, not tuning: rolling-KV relocates bytes
(streamed over the ~50 GB/s link while the GPU keeps computing), whereas -ngl relocates compute
(CPU matmul at a fraction of GPU throughput, and the CPU-owned KV must be read on the CPU too). This
is the quantitative case for rolling-KV as the overflow fallback: it degrades gracefully on the PCIe
axis instead of falling off the CPU-compute cliff. (Compression remains the primary capacity lever
above both β Β§0; spill of either kind is the fallback when compression is exhausted.) Needle PRESENT
in every cell of all three ladders.
Full merged three-way ladder (decode / prefill tps). The three modes don't share an identical
x-grid β the two rolling-KV modes are keyed by KV host-tail, -ngl by whole layers
(weights+KV+compute) pushed to CPU β so rows are aligned on approximate GiB moved off the GPU and
each cell shows the mode's own nearest measured point (β = no cell near that bin). Format is
decode (prefill):
| β GiB off GPU | DCA-on streaming (C2r) | DCA-off rolling-KV window (C2r) | CPU layer offload, resident KV (-ngl) |
|---|---|---|---|
| 0 (all resident) | 38.5 (2094) | 40.1 (1547) | 39.8 (β) -ngl 99 |
| ~0.9 | 37.3 (2016) | 35.8 (1342) | β |
| ~1.6 | β | β | 13.67 (1439) -ngl 48ΒΉ |
| ~1.7 | 37.4 (2015) | 35.9 (1335) | β |
| ~2.5 | 37.1 (2016) | 35.9 (1338) | β |
| ~3.1β3.3 | 37.2 (2016) | 35.9 (1340) | 7.02 (1213) -ngl 46 |
| ~4.1 | 37.2 (2015) | 35.6 (1344) | β |
| ~4.6β4.9 | 37.2 (2017) | 34.8 (1335) | 4.51 (1097) -ngl 44 |
| ~5.7 | 14.5 (1598) β knee | 10.8 (1172) β knee | β |
| ~6.2 | β | β | 3.41 (1008) -ngl 42 |
| ~6.5β6.7 | 11.7 (1361) | 9.3 (1028) | β |
| ~7.3β7.7 | 9.7 (1164) | 8.2 (933) | 2.67 (917) -ngl 40 |
| ~8.9 | β | 6.6 (782) | β |
| ~9.3β9.6 | 7.3 (925) | β | 2.19 (847) -ngl 38 |
| ~10.8β10.9 | β | 5.2 (670) | 1.86 (795) -ngl 36 |
| ~12β12.4 | 5.6 (765) | β | 1.63 (745) -ngl 34 |
| ~14.7 | β | β | 1.38 (678) -ngl 31 |
ΒΉ -ngl 48 is not all-VRAM β llamafile keeps the LM-head on CPU until -ngl > n_layer, so the
152k-vocab logit projection runs on CPU and alone costs 39.8β13.7 (3Γ). True all-VRAM is -ngl 99.
Full-residency ceiling β 38β40 tps for all three; in the spill regime the ordering is unambiguous:
DCA-on β₯ DCA-off β« CPU-offload.
6. What ships
- Window mode = graceful-overflow fallback (shipping default): linear degradation β spilled bytes, correctness clean (needle + logit-equiv real_frac 0), no cliff pathology. Not a compression substitute.
- Rolling-KV perf line: CLOSED. No further re-stream/copy-plumbing investment β the link is the wall.
- Capacity levers are residency-side: quant-KV decode (#582, made
bandwidth-bound), DCA, PolyKV, low-bit-resident. See
hal_kv_compression.mdΒ§4 +context.mdΒ§5b.
7. Provenance
Consolidated 2026-07-10 from rolling_kv.md, rolling_kv_step_prefetch.md,
rolling_kv_compute_reserve.md, hal_kv_compression.md Β§3a. Tasks: #583/#584
(3090 cliffs), #585β#589 (S0βS3 + boot fix), #623 (bug-1843), #588 (perf gate),
#626 (eval refresh in progress). Memory: reference_pcie_bandwidth_hosts,
project_qwen_retrieval_distributed_hal.