Rolling-KV: two-pass compute-buffer reserve (fix the resident-allocation OOM)
Status: SHIPPED 2026-07-01 as 0089-rolling-kv-compute-reserve.patch (9 files,
byte-identical-verified). Host-only change (llama-kv-cache*, llama-context.cpp,
llama-model.cpp, llama-cparams.h, llama-memory-hybrid{,-iswa}.cpp) β
build:llamafile:make rebuild ONLY (no CUDA DSO, no restamp). Vendor backup:
.opencoti/vendor-backups/20260701-123044-two-pass-compute-reserve.
Gate result (bs2, RTX PRO 6000 96 GB, 2026-07-01): the exact repro config below
(-c 524288 --kv-residency-mode window --vram-target 88000 -ctk f16 -ctv f16, KV
96 GB) that OOM'd the pre-0089 binary at 450 tok/s as the host tail
grows (the expected cliff, per graph_reserve now BOOTS and prefills
with the streaming window engaged β prefill tps falls 4000βdocs/features/rolling_kv.md). Two-pass gate conditions
(offload && flash_attn && !devices.empty() && kv_residency_mode != head) are all met on
this path, so boot-success β the measured reserve replaced the too-small 1536 MiB fixed
default. Streaming-window FA math is unchanged (size-only patch), correctness carried by
the 0070 M7 logit-equiv/RULER gates.
The bug (bug-1342)
The POSITION_WINDOW resident-window sizer
(opencoti_compute_resident_window_cells, llama-kv-cache.cpp:227) reserves a
fixed OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES = 1536 MiB (line 120) for the
compute scratch that graph_reserve allocates AFTER the KV cache. But the
compute buffer scales with context β measured 3.63 GiB (3 800 045 568 B)
at 262 144β524 288 ctx on the RTX PRO 6000. So the sizer keeps ~2.1 GiB too
much KV resident and graph_reserve OOMs:
ggml_gallocr_reserve_n_impl: failed to allocate CUDA0 buffer of size 3800045568
graph_reserve: failed to allocate compute buffers
llama_init_from_model: failed to initialize the context: failed to allocate compute pp buffers
Repro: RTX6000, Qwen2.5-14B-1M Q8_0, -c 524288 --kv-residency-mode window --vram-target 88000 -ctk f16 -ctv f16. 128k boots (KV ~25.7 GiB fits); 512k
(KV ~96 GiB) does not.
The author KNEW (comment at llama-kv-cache.cpp:114-119): "this reserve only covers the not-yet-allocated compute buffers. --vram-target gives the operator a tighter knob when this default is wrong." The fix removes the manual-guess dependency.
Fix: two-pass pre-measure (user-chosen 2026-07-01)
Model-agnostic: MEASURE the real compute buffer, don't estimate it.
- Pass 1 (measure):
create_memorywith a measurement flag β the sizer returns a MINIMAL window (waligncells resident, rest host) β tiny device KV β boots for ANY compute size. Runsched_reserve()(creates the sched at llama-context.cpp:525, runs the pp worst-case reserve at :697). Then read the actual GPU compute buffer: sumggml_backend_sched_get_buffer_size(sched, b)over backends whoseggml_backend_dev_type(...)==GPU. - Pass 2 (real): re-
create_memorywith `kv_compute_reserve_mib = measured- safety(256 MiB)
. Sizer now sizes the window againstbudget β model β measured_compute. Setsched_need_reserve=true;sched_reserve()` again.
- safety(256 MiB)
- Gate the two-pass so the common resident case stays SINGLE-pass /
byte-identical: only engage when
offload && kv_residency_mode != head(1) && full-KV-demand + a generous compute margin > free_vram(overflow possible). Reuse thekv_demandvsfree_vramcompare already in the sizer β expose a cheapbool opencoti_kv_may_overflow(...)helper. If it can't overflow β today's single pass, no behavior change.
Compute buffer size is INDEPENDENT of the window/tail split (it depends on n_ubatch Γ n_kv Γ n_embd, and n_kv = full ctx regardless of resident split), so the Pass-1 minimal-window measurement equals the Pass-2 compute β one measurement suffices.
Exact edit surface (threading map, verified 2026-07-01)
- llama-cparams.h (struct at :9): add
uint32_t kv_compute_reserve_mib;(0 = use 1536 default) andbool kv_window_measure_pass;(default false). - llama-context.cpp:
- Init defaults near :104-108 (beside
cparams.vram_target_mib/cparams.kv_residency_mode):kv_compute_reserve_mib=0,kv_window_measure_pass=false. - Two-pass orchestration wrapping the
memory.reset(model.create_memory(...))at :385 + thesched_reserve()at :451. Backend-init block (:388-449) is memory-independent β keep it before the firstsched_reserve. After the Pass-1 reserve, sum GPU compute buffer, setcparams.kv_compute_reserve_mib, clearkv_window_measure_pass,memory.reset(create_memory(...))again,sched_need_reserve=true,sched_reserve(). Guard whole block behindopencoti_kv_may_overflow.
- Init defaults near :104-108 (beside
- llama-kv-cache.h (ctor decl :96-145): add
uint32_t compute_reserve_mibandbool window_measure_passargs (besidevram_target_mib:123 /kv_residency_mode:133). - llama-kv-cache.cpp:
opencoti_compute_resident_window_cells(:227): takecompute_reserve_mibwindow_measure_pass. Replace the fixedOPENCOTI_HEADINFER_AUTO_RESERVE_BYTESat :271-273 withcompute_reserve_mib ? (size_t)compute_reserve_mib<<20 : OPENCOTI_HEADINFER_AUTO_RESERVE_BYTES. Ifwindow_measure_passβ early-return the minimal window (walign, using the same align math at :296-298) so Pass-1 device KV is ~0.5 MiB.
- ctor (:316): accept + forward the 2 args to the sizer call at :396.
- (optional) expose
opencoti_kv_may_overflow(reuse the auto-frac budget math) for the context-level gate.
- llama-kv-cache-iswa.{h,cpp}: thread the same 2 args through the wrapper
to both inner
llama_kv_cachectors. - llama-model.cpp create_memory (:2082 iswa, :2113 plain): pass
cparams.kv_compute_reserve_mib,cparams.kv_window_measure_passalongsidecparams.vram_target_mib(:2098/:2128) andcparams.kv_residency_mode(:2102/:2132).
Gates
- Boot repro (the fix): the exact 512k config above now BOOTS with a host
tail (was OOM). Read the logged
position window = C / kv_size cells resident- host tail; decode tps > 0, needle held.
- Resident unchanged (no regression): a config that fits resident (e.g. 128k
or
--vram-targethigh) still logs FULLY RESIDENT, single-pass, and decodes at the same ~38 tps β byte-identical path (measure gate not tripped). - Curve enabled: with boot fixed, the tps-vs-host-tail curve (#583/#584 on
- is runnable on RTX6000: fix a ctx, sweep
--vram-targetdown to grow the tail, read (host-tail MiB, decode tps). NOTE the binary must LOG the device/host split β theposition window = ...INFO line at llama-kv-cache.cpp:310 is the measurement source (the bench's grep must match THAT line, not a non-existentCUDA_Host KV bufferline).
- is runnable on RTX6000: fix a ctx, sweep
Constraints (standing)
Host rebuild only (bun run build:llamafile:make from repo, or the make path) β
NOT a CUDA DSO rebuild, NO restamp (llama-kv-cache/llama-context are host TUs).
Additive soft-fork + opencoti-hook: markers (two-pass reserve) + UPSTREAM_SYNC
registry. Correctness via boot + niah, never greedy byte-equality. Commit only
when asked (dev; trailers). Vendor-backup already taken (above).