Block KV cache streaming: bound VRAM at long context via a shared CUDA phase arena - #357
Block KV cache streaming: bound VRAM at long context via a shared CUDA phase arena#357giveen wants to merge 48 commits into
Conversation
|
@TheTom that took a while but I think I am done. |
|
Read the whole thing at b08b515, src side and CUDA side separately, with flag-off as the deciding question. The good news first: with Needs changes before it can land:
Concerns, none individually blocking:
I'll run the GB10 gates (default build flag-off sweeps, FA_ALL_QUANTS build with |
|
One more from CI, and it is not the runner flake this time. The Windows arm64 cmake-pkg job fails at link: Those are internal libllama functions with no |
Ports the pure-logic core of RaymondHuang210129/llama.cpp-adaptive-kv-streaming's phase-arena KV streaming implementation (feature/kv-stream-phase-arena), onto this fork's clean feature/turboquant-kv-cache base rather than patching the broken block-KV-streaming stub in PR TheTom#326. - llama-kv-stream-plan.{h,cpp}: page/region allocator, adaptive resident/ring partition sizing (deadline-miss/copy-busy driven hysteresis), prefetch dispatch. Ported verbatim - backend-agnostic, no TurboQuant-specific concerns. - llama-kv-stream-softmax.{h,cpp}: online-softmax merge for combining resident-chunk and streamed-chunk partial attention results. Ported verbatim. - llama-kv-stream-config.{h,cpp}: ported with one reconciliation - upstream's validate() hard-rejects any architecture other than Qwen3.5 (a literal allowlist, not an inert flag). Replaced that gate with this fork's existing unified_kv_cache check (excludes MLA/ hybrid-SWA/recurrent caches, not just non-Qwen3.5 archs) and kept a type_pair_supported gate, since v1 of this port only wires up streaming for standard GGML K/V types - turbo2/3/4 must still be rejected here, not silently accepted. All upstream test files (test-kv-stream-plan/config/softmax/bench-config and the three CUDA test files) copied over as-is; the config test was adjusted to reference unified_kv_cache/type_pair_supported instead of arch_qwen35 so it compiles against the reconciled struct, and to isolate the arena-below-minimum case from the kv_offload case it was incidentally also exercising. Wires the three pure-logic tests into tests/CMakeLists.txt; the CUDA and bench-config test files are copied but not yet wired in since their dependencies (CUDA kv-stream runtime, kv-stream-bench tool) land in later commits on this branch. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rows staging Ports the CUDA implementation of RaymondHuang210129/llama.cpp-adaptive-kv-streaming's phase-arena block KV streaming (feature/kv-stream-phase-arena) onto this fork's attention/backend code, reconciled with TurboQuant's existing turbo2/3/4 dequant paths, WHT rotation, and MLA/hybrid-SWA cache handling. ggml-cuda.cu / ggml-cuda.h: - New phase arena (one bounded CUDA allocation shared between compute workspace, resident KV pages, and an async transfer ring), resident cache, and transfer-ring buffer type, replacing nothing on this branch (no prior kv-stream implementation existed here). - ggml_backend_cuda_device_supports_buft now recognizes the new kv-stream buffer type as CUDA0-affine - this is the direct fix for the scheduler CPU/CUDA0 thrashing (74 splits/eval instead of 2) measured against PR TheTom#326's implementation this session. - GGML_OP_SET_ROWS/FLASH_ATTN_EXT dispatch in ggml_cuda_compute_forward route through the new staged/streamed paths only when a kv_stream runtime is actually attached to the tensor - turbo tensors (which never get a runtime attached, since v1 gates streaming to standard types only) fall through to the exact same code path as before, unchanged. fattn.cu / fattn.cuh / fattn-common.cuh / fattn-mma-f16.cuh / fattn-vec.cuh: - New kv_stream transfer ring, resident cache, per-type capability table (ggml_backend_cuda_kv_stream_get_type_capabilities/ get_attention_mode - classifies only standard GGML types for v1; turbo2/3/4 fall through to UNSUPPORTED, same as any other type this doesn't recognize, gracefully excluding them rather than crashing), and ggml_cuda_flash_attn_ext_streamed (chunked attention over resident+streamed KV pages, merged via the online-softmax layer ported earlier). - Added an `output_partial` template parameter (default false) to flash_attn_ext_f16_process_tile/flash_attn_ext_f16 and a launch_fattn(..., partial_dst, partial_meta) overload, both as trailing defaulted parameters so every existing TurboQuant call site (which doesn't know about partial output) compiles unchanged. ggml_cuda_flash_attn_ext_mma_f16_case_impl gets two thin wrappers: the original name (output_partial=false, used by the existing DECL_FATTN_MMA_F16_CASE instantiation macro) and a new _partial_case for the streamed path. - Fixed a bug the port exposed rather than introduced: several `extern DECL_FATTN_VEC_CASE(...)` call sites for TurboQuant's cross-type (turbo x f16/q8_0/turbo) instances only put `extern` on the first of the macro's two statements once DECL_FATTN_VEC_CASE started emitting a _partial_case instantiation alongside the original - the second statement silently became a real definition, colliding with the same definition in each instance .cu file at link/compile time. Switched those sites to the existing EXTERN_DECL_FATTN_VEC_CASE macro, which extern-qualifies both. set-rows.cu / set-rows.cuh: - Threaded a `dst_row_base` offset through the standard-type set_rows kernel/dispatch chain (k_set_rows, the raw-pointer and ggml_backend_cuda_context overloads, including the two explicit half/int32_t and half/int64_t specializations) and added ggml_cuda_op_set_rows_staged, which stages new K/V rows through a pooled device buffer before copying into the (now host-resident) destination and optionally mirroring into the resident cache. Fixed one call site (IQ4_NL) the source patch didn't cover, which needed the same dst_row_base threading as its neighbors. - Turbo-specific set_rows_cuda_turbo{2,3,4} dispatch is untouched - turbo K/V never reaches the staged path in v1. Regression-tested: turbo3 and q8_0 K/V generation (no --kv-stream) produce sane output at normal throughput; the three pure-logic kv-stream unit tests still pass. --kv-stream-arena-mib itself isn't wired up yet (llama-context.cpp/llama-kv-cache.cpp/common/arg.cpp come next) so the new code paths aren't reachable yet - only build/link integration and standard-type/turbo dispatch coexistence have been verified so far. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…struction
Ports the llama_context/llama_kv_cache/llama_model wiring from
RaymondHuang210129/llama.cpp-adaptive-kv-streaming (feature/kv-stream-phase-arena),
reconciled with this fork's broader architecture zoo and the
unified_kv_cache generalization already made in the config layer.
- include/llama.h: new llama_context_params.kv_stream_arena_mib field
and the llama_decode_phase enum + llama_set_decode_phase() API (used
by the phase arena to distinguish prefill from decode-style
microbatches and pick the right resident/ring split immediately
instead of waiting several tokens for feedback to react).
- llama-context.{h,cpp}: phase arena bootstrap (validate config, probe
per-layer page/workspace geometry, create the arena, publish its
buffer type into llama_memory_params) replacing nothing (no prior
kv-stream code existed on this branch). Removed the source's
`arch == LLM_ARCH_QWEN35` gate on the *wiring* itself (only ever
passing real values through for that one architecture) - replaced
with a real unified_kv_cache computation mirroring this fork's
MLA/indexer/recurrent exclusion set (MINIMAX_M3, GLM_DSA,
DEEPSEEK32/4, recurrent archs, DFLASH only when dsv4_hc_mult>0),
the same set the config layer's `unified_kv_cache` gate was already
designed for. iSWA dual-cache and the specialized DSA/DSV4/MSA
caches are still excluded (matching the source, which never wires
kv-stream through their constructors either) - kv_base-only iSWA
support stays a follow-up.
- llama-kv-cache.{h,cpp}: constructor gains kv_stream_stage_bytes/
kv_stream_phase_arena/kv_stream_maximum_pool_bytes as defaulted
trailing parameters (after the existing name_tag, which already had
a default) rather than inserted where the source's diff placed them
(right after `share`) - this fork's constructor already had a
`name_tag` param the source's fork doesn't have, and every one of
the 13 existing call sites across llama-kv-cache-dsa/dsv4/msa.cpp,
llama-memory-hybrid*.cpp and llama-model.cpp needed to keep
compiling unchanged. kv_stream_runtime_owner (the function-pointer
table + adaptive resident/ring repartitioning in kv_stream_adapt)
ported as-is.
- llama-model.cpp: wired both call sites that ever construct a plain
llama_kv_cache directly with the model's real type_k/type_v - the
llama_memory_hybrid attention sub-cache (source's only wired call
site) and, generalizing past the source's single-architecture scope,
the plain non-hybrid/non-SWA llama_kv_cache construction used by
ordinary dense models. Every other call site (DSA/DSV4/MSA/iSWA/MTP
drafts) is left passing the new parameters' defaults (disabled),
matching the config layer's architecture exclusions.
- llama-memory-hybrid.{cpp,h}: threaded the same three parameters
through to the inner llama_kv_cache construction, passing an
explicit "" for this fork's name_tag parameter that sits between
`share` and the new parameters.
- llama-memory.h: llama_memory_params gains the three kv_stream fields
create_memory() reads.
- llama-cparams.h: cparams.kv_stream_arena_mib mirrors the public
llama_context_params field.
Test fix: removed the type_pair_supported field and gate I'd added to
llama_kv_stream_config in the previous commit. It duplicated a check
the source implementation already does correctly and more precisely -
llama_kv_cache's constructor queries the actual per-layer (type_k,
type_v) backend capability via a proc-address lookup once the real
device is known, which this fork's earlier, coarser context-level
config can't replicate meaningfully before that point. Updated
test-kv-stream-config.cpp to match.
Regression-tested: q8_0 K/V generation (no --kv-stream) still produces
sane output; all three pure-logic kv-stream unit tests pass (104
assertions). --kv-stream-arena-mib CLI flag wiring (common/arg.cpp)
is next - the arena still isn't reachable from the command line yet.
Assisted-by: Claude
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires common_params.kv_stream_arena_mib through to llama_context_params.kv_stream_arena_mib, and adds the --kv-stream-arena-mib flag (--kv-stream-stage-mib kept as a compatibility alias, matching the source's own naming history). Also ports common_params_should_fit_device_memory(), which skips the device-memory auto-fit pass entirely whenever a nonzero arena is explicitly configured. This is the actual fix for the -fit crash TheTom found on PR TheTom#326 and we reproduced empirically this session (-ngl 10 --kv-stream auto threw "not supported for this K/V cache type pair" because a CPU-placed layer failed the streaming gate mid fit-search): the fit search's per-candidate probing is what hit that gate in the first place, and an explicit arena size makes the probing unnecessary - the arena's layout is already fully determined by the user's own -c/--kv-stream-arena-mib, not something -fit needs to search for. speculative.cpp: MTP/draft contexts get kv_stream_arena_mib=0 unconditionally (the source's own choice - the streaming pool doesn't yet support two contexts sharing it). Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dated config
Both ported verbatim in earlier commits, both latent bugs in
RaymondHuang210129/llama.cpp-adaptive-kv-streaming itself that never
surface for its one validated target (Qwen3.8-27B, always hybrid
memory, always head_dim==KV_STREAM_HEAD_DIM==256) but do the moment
this fork's broader model support actually exercises the streaming
path end to end:
1. llama-context.cpp: kv_stream_switch_phase() hard dynamic_cast'd to
llama_memory_hybrid and errored ("phase arena requires hybrid
attention memory") for anything else. Since this fork also wires
the arena through a plain llama_kv_cache (the non-hybrid,
non-SWA branch in llama-model.cpp, generalized in the previous
commit), added a fallback: try the hybrid wrapper first, then a
direct llama_kv_cache cast. Both paths reach the same
kv_stream_resize_pool() call, which doesn't care how it was reached.
2. ggml-cuda.cu: the GGML_OP_FLASH_ATTN_EXT dispatch in
ggml_cuda_compute_forward routed to the streamed kernel whenever
K or V merely had a kv-stream runtime attached, without checking
ggml_cuda_kv_stream_fattn_fits() first - unlike supports_op's
FLASH_ATTN_EXT case a few thousand lines down, which already does
check it. For a streamed cache on a model whose head_dim isn't
exactly 256 (ggml_cuda_flash_attn_ext_streamed_supported hard-codes
Q->ne[0] == KV_STREAM_HEAD_DIM), this reached
GGML_ASSERT(ggml_cuda_kv_stream_fattn_fits(dst)) inside
ggml_cuda_kv_stream_fattn() and aborted instead of falling back.
Added the same fits check to the dispatch condition, matching
supports_op - a streamed cache on an incompatible head_dim now
silently uses plain attention (the streaming kernel's head_dim
restriction stays a real limitation; only the crash is fixed).
End-to-end verified on Qwen3-8B.Q5_K_M (head_dim 128, plain non-hybrid
cache, --kv-stream-arena-mib 512, -ctk/-ctv q8_0):
- No crash; coherent generation output.
- GGML_SCHED_DEBUG=2 shows 2 scheduler splits/eval after the arena
activates - matching the no-streaming baseline exactly, versus the
74 splits/eval measured against PR TheTom#326's implementation earlier
this session. The supports_buft fix from an earlier commit is
confirmed working under real inference, not just at the API level.
- Generation throughput is lower than baseline for this model (head_dim
128 can't use the actual streamed kernel per the limitation above,
so this only exercises the memory-placement change, not the real
page-streaming path) - expected until the KV_STREAM_HEAD_DIM
restriction is generalized, a follow-up beyond this port.
Assisted-by: Claude
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…aining tests Ports tools/kv-stream-bench/ (pinned-host vs cudaMallocManaged H2D bandwidth probe, arg parsing, dry-run/--execute modes) verbatim - fully self-contained, no dependency on anything reconciled earlier in this port. Wires it into tools/CMakeLists.txt behind GGML_CUDA, matching the source. Also wires the four remaining copied-but-unwired test files now that their dependencies exist: - test-kv-stream-bench-config.cpp (the bench tool's arg parser) - test-kv-stream-cuda-buffer.cpp, test-kv-stream-cuda-attn.cpp, test-kv-stream-cuda-set-rows.cpp (CUDA runtime/kernel tests) One test needed reconciling: "KV stream quant types are classified" in test-kv-stream-cuda-set-rows.cpp iterates every quantized/f32/f16/bf16 ggml_type and asserts ggml_backend_cuda_kv_stream_get_type_capabilities classifies all of them - true on the source's vanilla fork, but this fork's GGML_TYPE_COUNT also includes 8 TurboQuant-specific types (turbo2/3/4, tq3_1s/tq4_1s, q5/6/8_cr) that v1 deliberately leaves unclassified (same graceful-fallback-to-UNSUPPORTED behavior as any other type the capability table doesn't recognize). Excluded those 8 from the loop rather than changing the capability table itself. All 7 kv-stream test binaries pass: 101 tests, 1373 assertions. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports benchmarks/ from RaymondHuang210129/llama.cpp-adaptive-kv-streaming verbatim - benchmark_kv_stream.py (arena-size auto-probing sweep driving llama-server), benchmark_kv_type_matrix.py, and their own unit tests (pure argument-parsing/resume-signature logic, no GPU or model needed). No reconciliation needed: they drive the CLI by flag name (--kv-stream-arena-mib, --cache-type-k/v) and HTTP, none of which differ from what's already wired up on this branch. Verified: all four scripts byte-compile; the scripts' own test suites pass (11 passed, 1 skipped). This completes the full port planned in /home/jabbatheduck/.claude/plans/lively-forging-platypus.md - core plumbing, CUDA kernels, llama_context/llama_kv_cache wiring, the CLI flag, and now benchmark tooling, all landed on feature/kv-stream-phase-arena and verified end-to-end (KLD ~0 against baseline on both a fallback-path model and the real streaming-kernel target, scheduler split count fixed, dynamic phase switching observed live). Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires TurboQuant's turbo2/3/4 cache types into the F16-conversion
attention mode already used by every other non-direct-attention type:
they get real backing storage, a working online writer
(set_rows_cuda_turbo{2,3,4}), and a to_fp16 dequantizer, so no new CUDA
kernels are needed. Q is unconditionally pre-rotated at the graph level
whenever K/V is turbo, and turbo's dequantize deliberately doesn't
invert that rotation, so the F16 round-trip is correct as-is.
Also fixes two real gaps found while wiring this in:
- ggml_cuda_kv_stream_page_bytes()/workspace_bytes() sized rows off the
raw model head_dim, but turbo storage zero-pads non-128-aligned head
dims to the next multiple of 128 (llama-kv-cache.cpp). Streaming
pages would have been undersized for any non-128-aligned-head-dim
turbo model.
- ggml_cuda_kv_stream_staged_set_rows_range() gated purely on the
online_write capability flag, which would have routed turbo writes
into the true page-staging fast path relying on a dst_row_base
offset that set_rows_cuda_turbo{2,3,4} doesn't support - silent data
corruption rather than a crash. Turbo writes are now forced through
the existing online_write mirror fallback instead.
Separately, found and fixed a pre-existing mismatch between the
streaming bootstrap pre-scan (llama-context.cpp) and the actual
llama_kv_cache construction: turbo's auto-asymmetric feature upgrades
K to q8_0 on high-GQA-ratio models, but the pre-scan sized its
bootstrap pool off the raw requested type, not the upgraded one. This
made the streaming runtime's internal page-count arithmetic
inconsistent and broke turbo streaming outright on any GQA>=6 model
with symmetric turbo K+V - the default case for this fork's actual
users. The auto-asymmetric decision is now a shared helper
(llama_kv_cache_resolve_stream_type_k) called from both places.
Verified: full kv-stream test suite and the broader test suite pass
(one unrelated pre-existing tokenizer-vocab test failure aside).
Functional smoke tests (turbo3/turbo3, mixed q8_0 K/turbo2 V) stream
without crashing. KLD comparison of turbo3/turbo3 streaming (2GB
arena, forcing real page eviction) against non-streaming at 4096
context: mean KLD 0.003, matching the near-zero bar already met for
standard types.
Assisted-by: Claude
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kv_stream_unified_kv_cache was a pure architecture-name blacklist that
never checked hparams.is_swa_any(). Any non-blacklisted SWA
architecture (e.g. Gemma-style iSWA models, or this fork's own Laguna)
with --kv-stream-arena-mib set would build a real CUDA phase arena,
then hard-crash in kv_stream_switch_phase() once both its dynamic_casts
fail against llama_kv_cache_iswa ("failed to activate shared CUDA
arena prefill phase").
This turns that crash into the existing clean
llama_kv_stream_config_validate() rejection, whose error message
already (until now, falsely) claimed SWA dual-cache architectures were
excluded. Real streaming support for iSWA is tracked as a follow-up;
this exclusion should be narrowed again once that lands, or iSWA
models will get a clean rejection instead of real streaming.
Assisted-by: Claude
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Turbo already has a full native decode-inline CUDA attention kernel
(fattn-vec.cuh's VEC template, plus a fused MMA path in
fattn-mma-f16.cuh) used unconditionally by the ordinary non-streaming
FA dispatch - the streaming capability table just never marked it
direct_attention-eligible, so streamed turbo attention always paid for
a dequantize-to-F16 round trip even when a native kernel was available.
Wires this up (all gated behind GGML_CUDA_FA_ALL_QUANTS, which stays
off by default - documented in docs/build.md as also controlling this
path now):
- get_type_capabilities(): turbo2/3/4 now set direct_attention.
- kv_stream_resolve_native_partial_for_v<type_K>()/
kv_stream_resolve_native_partial(): route to the real
ggml_cuda_flash_attn_ext_vec_partial_case instantiations, which only
exist for turbo paired with {f16, q8_0, turbo2/3/4} - every other
pairing is compile-time excluded (if constexpr) to avoid taking the
address of a template specialization nobody instantiated.
- get_attention_mode(): direct_attention is a per-type capability flag,
but turbo isn't fully pairwise-connected to the original direct set
the way F16/Q4_0/.../Q8_0 are with each other. Added an explicit
pairwise check (kv_stream_direct_attention_pair_supported) so e.g.
(Q4_0, TURBO2_0) - both individually direct_attention-eligible, but
with no instantiated kernel for that specific pair - correctly falls
back to ATTENTION_F16 instead of hitting the DIRECT-mode assert with
a null kernel pointer.
Added a dedicated test for turbo's actual native pairs (self, f16,
q8_0) rather than folding it into the existing "all native CUDA KV
pairs" test, which assumes a fully-connected type set that turbo isn't
part of.
While extending the bounded-fallback test to also cover turbo paired
with non-companion types (Q4_0 etc., which correctly route to
ATTENTION_F16), found the addition triggers a false-positive mismatch:
the shared test harness's decode-row-update simulation writes a
rotation-agnostic row into the plain-F16 reference tensor while the
real turbo-typed cache correctly stays in the WHT-rotated domain,
desyncing the two. Confirmed via direct comparison that the underlying
CPU and GPU turbo dequantize implementations agree closely (~1e-4,
F16-rounding level) - this is a test-harness limitation, not a
streaming or dequantization bug, and not a configuration this fork's
design pairs turbo with in practice. Left out of the fallback test
with a comment explaining why, rather than papering over it.
Assisted-by: Claude
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ggml_cuda_kv_stream_page_bytes()/workspace_bytes() unconditionally reserved both a K and a V region, sized off head_dim_k/head_dim_v. MLA-shaped layers (llama_kv_cache's has_v == false) have no separate V cache tensor at all - V is a graph-time view/reconstruction over the K/KV-latent tensor (see llama-graph.cpp) - so streaming would have silently reserved a full, entirely unused V region for every MLA layer, doubling the real page/pool size. head_dim_v == 0 now means "no separate V storage, size off K alone" instead of being treated as invalid input. The two call sites (llama-context.cpp's pre-scan, llama-kv-cache.cpp's constructor) pass 0 for MLA layers (hparams.is_mla()) instead of the raw n_embd_head_v(il) - no signature changes needed, head_dim_v was already a plain uint32_t everywhere. This is prep work for exercising the MLA/DSA streaming paths (DSA's kv_mla sub-cache inherits the same K-only shape) - plain MLA (DeepSeek-V2/V3-style) already isn't excluded from streaming today, but was never actually geometry-correct until now. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…x dormant decode-phase switch Adds llama_memory_i::get_kv_stream_targets() and llama_memory_context_i::get_kv_stream_active_targets() - both default to empty, so no unrelated memory type changes - as the uniform way to ask "which sub-cache(s) of this memory object have a CUDA phase-arena streaming runtime attached". Implemented for llama_kv_cache (itself, once it has a runtime) and llama_memory_hybrid (forwards to its attention sub-cache). This replaces the two-case dynamic_cast<llama_memory_hybrid*>/dynamic_cast<llama_kv_cache*> in kv_stream_switch_phase() with a single, extensible lookup - laying the groundwork for iSWA/DSA/MSA/DSV4's sub-caches to report themselves the same way, without hardcoding more per-architecture casts here. Fixes a real, independently significant bug found while doing this: process_ubatch() gated the entire per-ubatch kv_stream_switch_phase() + kv_stream_adapt() call behind dynamic_cast<llama_memory_hybrid*> with no fallback - unlike kv_stream_switch_phase() itself, which already had one. For a plain (non-hybrid) llama_kv_cache - what every model validated on this fork so far actually uses (Qwen3-8B, Qwen3.8-27B) - this meant the phase arena switched into its prefill layout once at construction and NEVER switched to the decode layout, and the adaptive deadline-miss/copy-busy resident:ring feedback loop never ran at all, for the entire lifetime of every context. Confirmed via temporary instrumentation: before this fix, only the one construction-time switch call ever fired; after, a second real switch fires the first time a ubatch enters generation phase. Re-ran the turbo3/turbo3 streaming-vs-non-streaming KLD comparison after this fix (same methodology as the previous phase): mean KLD dropped from 0.003064 to 0.000000 (max KLD 0.000059), combining the benefit of this fix with the native-kernel wiring from the previous commit. Decode now gets the resident-KV allocation the design actually intended instead of running with the prefill layout for the whole session. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
llama_kv_cache_iswa (Gemma-style models, and this fork's own Laguna arch) holds two independent llama_kv_cache instances - kv_base (non-SWA layers, full context length) and kv_swa (SWA layers, window-bounded and therefore small by construction, unless swa_full makes it full-length too). Neither constructor overload accepted the streaming triplet at all, so streaming was structurally unreachable through this class regardless of the architecture exclusion list. Both constructors now accept (kv_stream_stage_bytes, kv_stream_phase_arena, kv_stream_maximum_pool_bytes), defaulted to (0, nullptr, 0). kv_base always receives them; kv_swa only receives them when size_swa == size_base (the swa_full case) - otherwise it stays always-resident, since streaming a cache that's already small by construction buys nothing. get_kv_stream_targets()/ get_kv_stream_active_targets() (from the previous commit's generalized interface) concatenate whichever of the two sub-caches actually got a runtime - today that's just kv_base in the common case, with the interface already correct for the swa_full N=2 case once the CUDA arena's single-lease design is generalized (tracked separately, not needed yet since no caller reaches swa_full+streaming today). llama_memory_hybrid_iswa (recurrent+SWA-attention hybrids) gets the same treatment, forwarding into its internal llama_kv_cache_iswa. llama-model.cpp's default-branch iSWA call sites (the actual Gemma/Laguna-shaped case, both the plain and GEMMA4_ASSISTANT-share variants) and the hybrid-iswa call site now thread params.kv_stream_* through. With real streaming support landed, the SWA blanket exclusion added earlier (to prevent the hard crash from an un-plumbed iSWA memory type) is removed - kv_stream_switch_phase's target resolution now fails closed with a clean error for any memory type that still isn't wired up (e.g. llama_memory_hybrid_idx), rather than needing every future case pre-emptively blacklisted here. Verified end-to-end against a real local iSWA model (/mnt/storage/models/laguna/xs/laguna-xs-2.1-nvfp4.gguf, arch=laguna, confirmed via GGUF metadata to have real SWA layers and route through this exact default-branch iSWA path): confirmed via temporary instrumentation that streaming is genuinely enabled (not silently falling back), ran to completion with no crash, and a streaming-vs-non-streaming KL-divergence comparison shows mean KLD ~0.000000 (max 0.000041) and 99.99% same-top-token agreement - streaming is numerically faithful. (The model's own baseline perplexity is anomalously high on this build/quant independent of streaming - flagged separately as unrelated to this work.) Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…wasn't the cause, keep it excluded Wires kv_csa (DSV4's compressed KV cache, the sub-cache that scales with context length and is worth streaming - kv_raw's actual per-layer attention runs entirely on its window-bounded SWA half, not its base half, despite the base half's own class comment suggesting otherwise) with the streaming triplet, following the same pattern as MLA/DSA's K-only shape. While validating this against a real local DeepSeek-V4-Flash model (previous work in this area had no real model to test against), found a large, real correctness regression: mean KLD 0.07 (max 4.3) between streaming and non-streaming output, versus ~0.000000 for every other architecture validated this way (turbo, iSWA). The initial hypothesis was a bug in the K-only page-geometry special-case added earlier this branch (ggml_cuda_kv_stream_page_bytes()'s head_dim_v==0 handling): DSV4's compressed-cache attention (models/deepseek4.cpp, build_attn_mha called with k_all passed as both K and V) always needs a real V-shaped region in the streamed page, even though the persistent cache is K-only and that geometry code was treating "K-only cache" as "no V region needed anywhere" - a real, understandable-in-hindsight confusion between "the cache doesn't persist V" and "the attention op doesn't consume V". Tested that hypothesis directly: reverted the head_dim_v==0 special-casing entirely (both in fattn.cu and its two call sites) and re-ran the same KL-divergence comparison. Result was bit-identical (0.071177 mean KLD, unchanged to 6 decimal places) - the geometry special-case was NOT the cause. Kept the revert anyway, since it removes an assumption now proven wrong without any demonstrated benefit, and re-excluded LLM_ARCH_DEEPSEEK4 in the architecture blacklist rather than shipping known-broken streaming for it. The actual root cause is still unidentified - likely somewhere in DSV4's online-compression bookkeeping (comp_plan's state_pos/ state_persist_*/state_snapshot_* row addressing) not surviving the resident-cache/ring-buffer model, but this needs dedicated investigation into that machinery, not more guessing. DSA's kv_mla and plain MLA use the same K-only cache shape as kv_csa; since neither has been empirically tested against a real model either, both should be treated as similarly unverified rather than assumed safe by extension of the (inspection-only) conclusion reached for MLA earlier. The kv_csa constructor plumbing is left in place (harmless, currently unreachable dead code via the restored exclusion) so whoever root-causes this doesn't have to redo it - see the comments in llama-context.cpp and llama-kv-cache-dsv4.cpp for the full context. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…check The pre-scan that sizes the streaming bootstrap pool scanned every attention layer indiscriminately and required them all to share identical page geometry. For iSWA-shaped models this is too strict: kv_swa's layers don't get a streaming runtime at all (unless swa_full), so their geometry is irrelevant to what's actually being sized - and some real models (Gemma-family) have genuinely different per-layer dimensions between SWA and full-attention layers (separate key_length/key_length_swa in the GGUF), which made the pre-scan reject them outright with "block KV streaming requires uniform attention-layer geometry" even though the layers that actually stream (kv_base's) are perfectly uniform among themselves. Now skips SWA layers here the same way it already skips recurrent ones, unless swa_full is set (in which case kv_swa becomes full-context-length too and does get a runtime - see llama-kv-cache-iswa.cpp). Verified against a real local Gemma-family model (/mnt/storage/models/gemma/Gemma4-26B-A4B-QAT-Uncensored-HauhauCS-Balanced-Q4_K_M.gguf, arch=gemma4, confirmed via GGUF metadata to have distinct key_length/key_length_swa and value_length/value_length_swa - exactly the non-uniform-geometry case this fixes): previously failed to load with streaming enabled at all; now loads cleanly, streaming genuinely active (confirmed via temporary instrumentation), and a streaming-vs-non-streaming KL-divergence comparison shows mean KLD 0.000000 (max 0.00006) and 99.99% same-top-token agreement. Re-verified Laguna (iSWA, previously-validated) and Qwen3.8-27B (turbo, plain cache) are unaffected. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…h streaming For an iSWA-shaped cache, --swa-full makes kv_swa full-context-length too, so both kv_base and kv_swa attach a streaming runtime to the same single-lease CUDA phase arena. Previously this was silently accepted by validation and only failed deep in cache construction, whichever sub-cache lost the race to bind the arena, with a generic "failed to create CUDA block KV streaming runtime" error that gives no hint --swa-full is the actual cause. Verified empirically (Laguna, --swa-full + --kv-stream-arena-mib): the failure is a clean, caught exception at context construction, not a crash - so this was never unsafe, just confusing. Adds swa_full_conflict to llama_kv_stream_config so this now fails with a clear, specific reason at the same validation layer as every other known-incompatible combination (SWA architecture exclusion, MTP/draft contexts, non-unified cache, etc). Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers arena-size propagation, the legacy --kv-stream-stage-mib alias, and negative-value rejection - gaps flagged in review against the reference implementation, which had these in its own test suite. "Disabling streaming for draft contexts" (the other gap flagged) isn't testable at this pure CLI-parsing layer - it's speculative.cpp's runtime zeroing of kv_stream_arena_mib for the draft context (common/speculative.cpp:2805), which needs a live model to exercise; already verified manually this session (draft acceptance stats confirm MTP keeps working with streaming enabled on the target context). Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously streaming was documented only in benchmark material and one build-flag note - no single place explained arena semantics, requirements, supported architectures, or known limitations. Consolidates everything found empirically this session: why -np 1 is a real design constraint (not just an unvalidated gate), the two distinct VRAM-boundary failure modes (clean startup failure vs a hard process abort mid-request), which architectures actually work vs are excluded and why, and the --swa-full conflict fixed in the previous commit. Cross-linked from build.md and benchmarks/README.md. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… alone Reverts the cross-link added to benchmarks/README.md in the previous commit - keep that file scoped to the benchmark tooling only. docs/kv-stream.md (linked from build.md) is the place for the feature's semantics, requirements, and limitations. Assisted-by: Claude Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A no_alloc llama_context (e.g. common_get_device_memory_data_with_parent's
dry-run pass to size the parent model for MTP) never allocates real backend
buffers, so llama_kv_cache's streaming-attach loop already skips attaching a
runtime to it (see the !hparams.no_alloc guard in llama-kv-cache.cpp). The
context-level phase-arena construction had no matching guard, so it built a
real CUDA arena anyway, then kv_stream_switch_phase found no cache attached
to it and failed with a confusing "requires a memory type with a streamable
KV cache" error on every MTP+streaming startup. The failure was caught and
logged as a warning ("[spec] failed to measure MTP context memory") and the
real target context loaded fine moments later, but the error looked fatal.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Measured: llama_set_decode_phase() is never called from the server, so speculative-decoding verification batches (multi-token) always classify as "prompt" phase instead of "generation" - the arena stays in its compute-heavy/KV-light layout almost the entire time MTP is active. Real but modest at 22K context (+2.2% resident pages, +20% ring slots vs the true generation layout). Documented rather than fixed: a real fix needs to resize the generation-phase compute reservation for multi-token speculative batches, not just relax the existing TG1-only guard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ation The exact decode-token-count check had never been exercised against --spec-type draft-mtp before. MTP verifies up to spec_chain+1 tokens per step, and the benchmark's exact prompt/decode split leaves no slack past context_capacity, so the last verification chunk near that boundary can land a few tokens short of the target instead of exactly on it. decode_tps is the server's own measured rate (actual predicted_n / predicted_ms), so it's correct either way - only the strict completeness check needed a tolerance derived from --spec-chain. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
benchmark_kv_stream.py measures throughput, which requires a full prefill+decode pass per point. Memory footprint doesn't need that - the KV/compute buffers are sized at context construction, not grown lazily - so this sweeps context length with just start/measure/stop per point, cheap enough to run a same-context no-streaming baseline alongside the streaming measurement at every point, including past where the baseline OOMs. When the baseline leg fails, it's recorded (not treated as fatal), the line stops there, and the plot marks the failure point with a labeled vertical line instead of silently truncating. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backing data for the block KV streaming PR: benchmark charts, results.csv/ .jsonl from every throughput and memory sweep, and raw llama-perplexity output for every PPL/KL-divergence check referenced in the PR description. Excludes the multi-GB saved-logits .kld files (regenerable via --save-all-logits) and bulk per-point server logs other than the one OOM backtrace that's directly cited as evidence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Force-added: these are curated evidence for the PR description (raw llama-perplexity output for every correctness check, and the one CUDA OOM backtrace cited as evidence), not routine run logs - *.log is gitignored repo-wide for the latter, which doesn't apply to these. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…/MUSA fattn.cu's KV-stream transfer ring (pinned ready-flags/deadline-counter buffers and the async CUDA-event completion poll) uses five CUDA runtime symbols that were never mapped to their HIP/MUSA equivalents: cudaEventQuery, cudaEventElapsedTime, cudaErrorNotReady, cudaHostGetDevicePointer, and cudaHostAllocMapped - breaking both the hip and musa CI builds (8 errors each, identical symbol set, same lines). Also added cudaHostAllocPortable/WriteCombined proactively since ggml-cuda.cu and allreduce.cu use them too and the build never got that far to reveal whether they'd fail next. Followed the existing line-for-line #define pattern in both vendor headers exactly; not able to compile-test against a real ROCm/MUSA toolchain locally, so this should be confirmed by CI rather than treated as verified. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit fixed cudaHostAllocMapped/cudaHostGetDevicePointer/ cudaEventQuery/cudaEventElapsedTime/cudaErrorNotReady, which unmasked a 6th error CI hadn't shown yet: cudaHostAlloc itself was never mapped either. The compiler's original 8-error report on both hip and musa apparently suppressed this cascading diagnostic since it's used in the same expression as cudaHostAllocMapped; fixing that symbol let the compiler get far enough to report this one. Both hip and musa suggested the exact fix directly in their error output (did you mean 'hipHostAlloc' / 'musaHostAlloc'). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two more genuine issues from this branch's diff (confirmed via git diff against the merge-base - the sibling cudaSetDeviceFlags/ cudaDeviceScheduleSpin/cudaGraphExecUpdateResultInfo/cudaDevAttrCanUseHost errors CI also reported are pre-existing on the base branch, unrelated to this PR, and left alone): - cudaHostGetFlags (ggml_backend_cuda_kv_stream_host_is_write_combined, part of the kv-stream transfer ring) was never mapped - same class of gap as the previous two fixes. Mapped to hip/musaHostGetFlags. - The GGML_CUDA_ENABLE_UNIFIED_MEMORY placement-hint code (ggml_backend_cuda_buffer_set_preferred_device/host) uses CUDA 12's location-struct cudaMemAdvise signature, which has no HIP/MUSA equivalent (the base branch's prior version of this code used the older device-int signature via a HIP-specific advice value). Rather than guess at each platform's older cudaMemAdvise shape without a way to compile-test it, gated the whole block to real CUDA only and made it a clean no-op elsewhere - this is an opt-in hint behind an env var, so skipping it on HIP/MUSA changes nothing by default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
llama_kv_cache_context::get_kv_stream_active_targets() built
{ kv, n_kv } where n_kv is int32_t but llama_kv_stream_active_target's
second field is uint32_t - a silent narrowing conversion GCC only warns
about but clang-cl (Windows CI) treats as a hard error
(-Wc++11-narrowing), failing every windows build variant. Explicit cast.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test-kv-stream-plan/-config/-softmax call internal llama_kv_stream_* functions that have no LLAMA_API export. Linking these tests only against the llama target works when llama is a static archive, but fails on a shared llama.dll build (e.g. Windows arm64 CI) because the import library exposes only exported symbols. Compile each test's corresponding src/ source directly into the test executable, the same pattern already used by test-kv-stream-bench-config. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
c4a5ffe to
2f7892d
Compare
…xed KV types The streamed page pool sizes its pages once from the cache-level type_k/type_v and assigns the same buffer type to every layer, but TURBO_LAYER_ADAPTIVE (modes 1/2/5/6/7) can give individual layers a different K and/or V precision - including auto-enabling itself with no env var at all whenever -ctv turbo2 is used on an 8+ layer model. That combination silently packed differently-shaped rows into pages sized for a different layer's type. Extract the mode-resolution and per-layer type-override logic already in llama_kv_cache's constructor into shared free functions (llama_kv_cache_turbo_layer_adaptive_mode/_type_k/_type_v), the same pattern already used for llama_kv_cache_resolve_stream_type_k. Use them from llama-context.cpp's block KV streaming pre-scan to detect, before ever touching the CUDA runtime, whether any layer would actually diverge from the cache's base type and refuse with a message naming the active mode. Per-layer heterogeneous page geometry is real future work; refusing is the correct scope for now since the pool has no support for it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test-kv-stream-cuda-buffer allocated on device 0 unconditionally, so a CUDA build with no GPU (e.g. a GPU-less CI runner) went red instead of skipping. Add a device-count probe at startup that exits 77, the SKIP_RETURN_CODE convention already used by test-moe-cache. test-kv-stream-cuda-attn had two problems on a default build (no GGML_CUDA_FA_ALL_QUANTS): - "turbo direct-attention pairs" hard-asserted GGML_BACKEND_CUDA_KV_STREAM_ATTENTION_DIRECT, but direct_attention kernels aren't instantiated at all without that flag, so get_attention_mode legitimately returns ATTENTION_F16 instead. - "all native CUDA KV pairs" tests the full F16/BF16/Q4_0/Q4_1/Q5_0/ Q5_1/Q8_0 cross product, which is upstream's original direct_attention type set. Without GGML_CUDA_FA_ALL_QUANTS, plain (non-streamed) CUDA flash attention itself refuses most mismatched pairs in that set (see ggml_cuda_get_best_fattn_kernel's mixed-type allowlist), so this test cannot even produce a reference result - unrelated to kv-streaming. Skip it cleanly on such builds. Both needed to know the actual runtime capability of the linked libggml-cuda, not the test binary's own preprocessor state: GGML_CUDA_FA_ALL_QUANTS is a private compile definition of the ggml-cuda target and is never propagated to test executables, so an #ifdef in the test file reflects nothing about how the library was actually built (confirmed by a real regression while iterating on this fix: the #ifdef silently always took the "off" branch even in this repo's own FA_ALL_QUANTS=ON build). Query the backend's runtime feature list via ggml_backend_get_features instead. Verified against two local builds (GGML_CUDA_FA_ALL_QUANTS on and off): all kv-stream tests pass on both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lt builds
Most of this file's streaming-mechanics tests (paging, residency, decode,
SET_ROWS, ring layout) default to a q8_0 K / q4_0 V pair via make_inputs()
- a real KV pair for exercising the streaming/paging code, chosen without
regard for GGML_CUDA_FA_ALL_QUANTS. Without that flag this surfaced two
distinct failures, both pre-existing and unrelated to kv-streaming itself:
1. Plain (non-streamed) CUDA flash attention refuses the mismatched pair
outright (ggml_cuda_get_best_fattn_kernel's mixed-K/V-type allowlist),
so a test's *reference* computation via the default buffer type could
not even run - it never reached the streamed code under test. Fixed by
computing the reference through an F16-domain conversion (the same
make_f16_reference pattern already used by the bounded-fallback tests)
whenever the direct pair isn't supported, via new reference_inputs/
reference_layers/run_attention_layers_reference helpers. The streamed
side is untouched - its own dispatch was never gated by this.
2. get_attention_mode(q8_0, q4_0) resolves to ATTENTION_F16 without the
flag (ATTENTION_DIRECT requires it), and ATTENTION_F16 needs a
conversion workspace that most of this file's runtime params{} blocks
never allocated, since it's a no-op under the DIRECT path this repo's
own build normally uses. Fixed by sizing params.conversion_bytes from
the real dispatch decision (kv_stream_conversion_bytes) at every
runtime construction site, and folding that size into any explicit
params.pool_bytes so hand-tuned resident/ring page-count math is
unaffected (mirrors the existing resident_params.pool_bytes pattern).
A further class of test asserts a DIRECT-attention-specific guarantee
outright - bit-exact equality with the plain reference, or MMA partial-
attention span usage - which cannot hold under the F16 fallback path at
all (direct_attention kernels aren't compiled in without the flag, for
any type pair). These 13 tests now skip cleanly with
backend_has_fa_all_quants() rather than asserting an impossible
equivalence, the same "guard the test" remedy already applied to the
all-native-KV-pairs cross-product test.
Verified against two local builds (GGML_CUDA_FA_ALL_QUANTS on and off):
0 failures on both, and the FA_ALL_QUANTS=ON build's assertion count is
unchanged (712/712) - none of this touches what that build already
exercised.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…QUANTS Every benchmark number in the PR description and benchmarks/results/ was measured with ggml-cuda built GGML_CUDA_FA_ALL_QUANTS=ON. Without that flag (the default), direct_attention kernels aren't instantiated at all for the streamed path, so every streamed KV type pair falls back to an F16-dequant-to-workspace path regardless of arena size or model - a real, measured throughput difference (~2840 -> ~1200 t/s streamed prefill, Qwen3.8-27B-AD -ctk q8_0 -ctv turbo4, 8K context), not a rare edge case. Ordinary (non-streamed) attention is unaffected by this flag either way, since the turbo-native kernel it always uses is unconditionally compiled in - confirmed by measuring both builds' non-streamed prefill (~2730 vs ~2738 t/s, same within noise). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ggml_cuda_graph_update_required scanned every node and every src of every node (up to GGML_MAX_SRC) for a live KV streaming runtime on every graph compute - including every decode step - before the cheap cgraph->uid early-out that lets CUDA graph replay skip almost everything. Paid by every user regardless of whether streaming is in use at all. Add a process-wide live-runtime counter (incremented/decremented exactly at real construction/destruction, not at reference-count retain/release) and skip the scan entirely when it's zero. Sound because a live tensor can only reference a runtime through its buffer, which itself holds a reference to that runtime - so once the global count is zero, no live tensor anywhere can reference one, and the scan would find generation 0 regardless. set-rows.cu's k_set_rows/k_set_rows_quant kernels gained a dst_row_base kernel argument and a per-thread subtraction on every SET_ROWS launch, including the ordinary (non-streamed) path, once KV streaming's resident cache needed to write into a row-offset staging buffer (ggml_cuda_op_set_rows_staged). Templated it as a compile-time DstRowBase flag instead of a runtime value: the ordinary path instantiates false (no subtraction emitted at all) and the staged-write path instantiates true. Initially removed the parameter from the quantized kernel entirely, on the assumption only the plain-cast kernel needed it - test-kv-stream-cuda- attn's "resident staged writes support every exposed KV-cache format" caught the real behavior: ggml_cuda_op_set_rows_staged's `staged` tensor mirrors dst->type, so a resident cache backed by a quantized KV type (as tested for q4_0/q4_1/q5_0/q5_1/q8_0/iq4_nl) hits the quantized kernel with a nonzero row base too - restored the same DstRowBase treatment there. Verified: full kv-stream test suite (7/7, including CUDA), plus real server smoke tests for both streaming and non-streaming decode (multi- step CUDA graph reuse, multi-turn SET_ROWS) with coherent output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ggml_cuda_graph_evaluate_and_capture's debug-only node/src buffer-type assert originally compared node->buffer->buft against ggml_backend_cuda_buffer_type(cuda_ctx->device) directly - a genuine device-identity check that also catches a node scheduled against the wrong GPU in a multi-GPU setup. Adding the kv-stream buffer-type family replaced that with ggml_backend_buft_is_cuda(...)-style checks, which only confirm a buft belongs to *some* CUDA device's family, silently dropping the identity check. Restore it for both buffer families (kv- stream buffer types carry a device field the same way ordinary CUDA ones do). Verified assert-free in a real Debug build (assert() active) for both a streaming and a non-streaming decode. llama_kv_cache::set_input_k_idxs wrapped the dirty-row-marking call (mark_dirty_rows_fn, which has a real side effect) directly inside GGML_ASSERT. GGML_ASSERT is not compiled out in this codebase (unlike plain assert under NDEBUG), so this was not an active bug, but the pattern reads as one and invites a future refactor to break it silently. Split the call from the assert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…esults kv_stream_normalize_chunk_results divided every row's chunked-attention accumulator by its softmax normalizer unconditionally. A row whose mask is entirely -inf (a padding row that rounds the launch grid out to a uniform block/row count, never written back to the real output tensor) has a normalizer of exactly zero there, producing a silent NaN/Inf. The CPU reference model of this same math (llama-kv-stream-softmax.cpp) already treats a non-positive normalizer as an explicit invalid-input case rather than dividing through it; this kernel had no equivalent guard. Clamp to zero output instead, so a padding row's otherwise-unused result stays harmless if it were ever read by mistake. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
It was inserted between type_v and the pre-existing moe_cache_mode/ moe_cache_budget_mib/abort_callback/.../ctx_other fields, shifting all of their offsets for anyone who built against a pre-streaming header - a real ABI break for prebuilt consumers of this public struct. Move it to the end instead, after ctx_other, so every pre-existing field keeps its offset. llama_context_default_params() builds this struct with positional (not designated) initializers, so its own initializer list had to move in lockstep or every field after the old position would have silently picked up the wrong value. Every other reference to kv_stream_arena_mib in the tree already uses named-field access, which is order-independent. Verified: full rebuild, test-arg-parser (asserts on parsed kv_stream_arena_mib values) and the full kv-stream suite pass; a real server smoke test with streaming enabled produces coherent output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
It's a CPU model of the CUDA FlashAttention fixup kernel's block-merge math, used only by test-kv-stream-softmax.cpp to validate that math in isolation - no production code calls llama_kv_stream_softmax_merge. Compiling it into libllama bloated the public library with test-only code for no reason. Move both files under tests/ and compile the .cpp directly into the test target (the same pattern test-kv-stream-plan/-config already use), dropping the now-unnecessary src/ include-directory override - a quoted #include already resolves from the including file's own directory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
llama_kv_cache_dsv4::get_kv_stream_targets() and llama_kv_cache_dsv4_
context::get_kv_stream_active_targets() existed only to expose kv_csa
as a potential streaming target, from the DSV4 streaming attempt that
was reverted after finding a real, un-root-caused correctness
regression (see llama-context.cpp's DEEPSEEK4 exclusion comment).
Since that exclusion means llama-context.cpp never attaches a runtime
to any DSV4 sub-cache, both overrides could only ever return an empty
vector - identical to just falling through to the base interfaces'
own `{ return {}; }` defaults (llama-memory.h), which is what happens
now that they're gone.
kv_csa itself and everything else in this file (seq_rm/seq_cp, state
I/O, memory_breakdown, clear) is real production code untouched by
this change - DSV4 streaming stays excluded, nothing else about how
the model runs changes.
Verified against the real local DeepSeek-V4-Flash model: streaming
still refuses construction with the same "requires a standard unified
KV cache" error when --kv-stream-arena-mib is set, and ordinary
(non-streaming) generation still produces coherent output.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ming GGML_CUDA_PREFER_MODEL_WEIGHTS/GGML_CUDA_PREFER_KV_HOST (plus the GGML_CUDA_ENABLE_UNIFIED_MEMORY master gate and GGML_CUDA_KV_ACCESSED_BY_GPU) came from upstream commit 04cc69d ("baseline: mirror production CUDA unified-memory build", authored by Raymond Huang) - carried over when this branch ported his fork wholesale, not something added for block KV streaming. They only ever acted when streaming is off, have no docs or tests of their own, and are a genuinely separate concern (CUDA UVM placement hints for model weights/KV buffers) from what this PR does. Drop them from this PR; they can come back as their own tracked change against the base branch if still wanted. Removes ggml_backend_cuda_buffer_set_preferred_device/_host from ggml-cuda.cu (both call sites in llama-kv-cache.cpp/llama-model.cpp, and the proc-address registrations), and the benchmark script's matching env-var stripping/test coverage for them - kept the unrelated GGML_CUDA_KV_STREAM_FIXED_RING_SLOTS/LLAMA_KV_STREAM_TRACE stripping in clean_server_env, which are real kv-stream knobs. Verified: full rebuild, kv-stream test suite (7/7), the benchmark script's own unit tests (12/12), and a real streaming server smoke test all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
context / 1024 mislabels the axis for round decimal context sizes - 700000 tokens plotted as ~683.6 "Ki" reads as a strange, hard-to-place number. Plot in decimal K (context / 1000) instead, so 700000 shows up as an unambiguous 700, with a footnote spelling out the convention so a reader never mistakes the axis for raw token counts or Ki-scaled values. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
streamed attention kernel's transient workspace docs/kv-stream.md already documented this as the one unsafe failure mode: an arena that constructs fine can still hit a hard, unrecoverable GGML_ASSERT/SIGABRT the first time fattn.cu's ggml_cuda_flash_attn_ext_streamed needs its chunked partial-attention reduction workspace (four small buffers: parts/meta/accumulator/ accumulator_meta). That workspace draws from the device's general CUDA memory pool (ctx.pool()), not from the arena itself, so a well-sized arena and a tight VRAM margin can still combine into a mid-request abort that takes down an already-serving process. Add ggml_cuda_kv_stream_transient_workspace_bytes(), which computes the worst case those four allocations can reach from the model's query head count/head_dim_v and --ubatch-size (bounding both the non-MMA chunked path, capped at 16*256 workspace rows regardless of batch size, and the MMA-prefill path, which scales with the real ubatch token count but uses only one partial). llama_context construction now calls it once per stream device, queries current free VRAM via the existing ggml_backend_dev_memory(), and refuses construction with a clear, actionable error if reserving the arena wouldn't leave that much headroom - the same clean, caught failure category as an oversized arena, instead of ever reaching the unsafe one. This uses free VRAM as measured at that point in construction; later allocations (e.g. the compute buffer) aren't yet accounted for, so it catches the clearly-too-tight case, not a formal guarantee - documented as such in docs/kv-stream.md. Verified against the real local Qwen3.8-27B-AD model: a normal arena (512 MiB) still constructs and generates correctly; a genuinely tiny arena (8 MiB) still hits the pre-existing "too small for bootstrap" rejection unchanged; an arena sized to leave ~100 MiB of headroom now hits this new check with an accurate required-headroom estimate in the error message, cleanly, before ever touching the CUDA runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
state_write_data/state_read_data (and their _meta counterparts) read
and write each layer's K/V tensor via a flat byte offset into
cell-position order (io.write_tensor(k, range.first*k_size_row,
buf_size)). That's only correct when the tensor's own buffer actually
holds every cell contiguously in that order, which is true for an
ordinary KV buffer but not for a block-streamed one: only a resident
subset of pages lives in the buffer kv_stream_adapt() last synced, and
the authoritative copy of the rest lives in host RAM behind the
streaming runtime. Saving state this way would silently write whatever
happened to be resident, not the real KV content, and produce a state
file that looks valid and isn't - untested and unrefused before this.
Refuse cleanly in state_write/state_read_sinfo when kv_stream_runtime
is active, matching how streaming already refuses other unsupported
combinations (single-sequence, --swa-full) rather than support+test a
combination nothing in this branch validates. seq_rm/seq_cp are left
untouched - they're pure cell-metadata bookkeeping that doesn't touch
K/V tensor content and are core to how the server manages context
(e.g. n_discard) even under streaming, so refusing them would break
normal operation for a much smaller and more speculative risk than the
state save/load path's demonstrated one.
Verified against the real local Qwen3.8-27B-AD model via the server's
/slots/{id}?action=save endpoint: with streaming on, the save now fails
cleanly with this error and the server keeps serving; with streaming
off, the same endpoint still saves a full, real state file (157 MB)
exactly as before. test-save-load-state also still passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ggml_cuda_flash_attn_ext_streamed_supported (fattn.cu) requires Q->ne[0] == V->ne[0] == 256 for either direct_attention or the F16 fallback to activate at all - unlike MLA/DSA/DSV4/MSA, this isn't an architecture-level exclusion, so a head-dim-128 model (common outside this fork's own turbo/Qwen3.8 testing) passes every other requirement, gets its KV cache pinned into the streaming buffer, and streams pages, but every attention op for it silently falls through to ordinary (non-streamed) Flash Attention - correct (reads K/V from the pinned host buffer over PCIe) but pays that transfer every decode step with none of streaming's residency/prefetch benefit. Not documented before this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two intentional design choices mean streamed output is close to non-streamed, not exactly equal, and can even vary run-to-run: - The chunked reduction's sequential online-softmax rescale sums the same terms in a different order than ordinary Flash Attention's single combined pass (flash_attn_combine_results) - floating-point addition isn't associative, so this is a real (tiny) numerical difference, already reflected in every correctness check this PR ran (mean KLD ~0.000000, 99%+ same-top-token, not exact equality). - The span tuner picks between the coalesced and bounded-span streamed kernels by comparing wall-clock timing across trial runs, so which one wins - and therefore the exact numerical result within that tolerance - depends on transient timing noise and can differ between runs of the same request. Neither was previously stated in the docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 5 benchmark charts (~788 KB) were committed only so the PR description could embed them via raw.githubusercontent.com links - binary chart images checked into the repo just to back a PR body, flagged in review as something that belongs elsewhere. Removed the images entirely and replaced every chart reference in the PR description with a complete data table (all sweep points, not just a handful) built directly from the already-committed results.csv files: - Qwen3.8-27B throughput: 5 -> 17 rows (the full 8K-262K sweep). - Qwen3.8-27B + MTP throughput: 3 -> 5 rows (the full 8K-106K sweep). - Gemma4-It throughput: already had all 6 rows, image removed. - Qwen3.8-27B VRAM/RSS sweep: already had all 6 rows (corrected the post-OOM rows from "-" to "not attempted (already OOM'd at 393K)" - the sweep tool didn't retry the crashed baseline leg, it skipped it), image removed. - Gemma4-It VRAM/RSS sweep: had no table at all before, only a chart link - added the full 6-row table. The PR description on GitHub is updated to match (no more raw.githubusercontent.com image links; every number now traces directly to results.csv under kv-stream-pr-evidence/, still committed and referenced from the PR body). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…b test The new common_params kv_stream_params/kv_stream_alias_params/ kv_stream_disabled_params instances were relying on LLAMA_ARG_MODEL being leaked into the process environment by an earlier test (set at line 352, never unset) to satisfy the "--model is required" check. That earlier block is wrapped in #ifdef _WIN32 / skip / #else, so on Windows it never runs and the env var is never set, causing the kv-stream parse calls to fail validation for reasons unrelated to --kv-stream-arena-mib itself. Give the success-path test cases their own -m flag instead of depending on leaked env state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Tried this on a 24 GB laptop card, which is roughly the situation the feature targets. The VRAM Setup: RTX 5090 Laptop (sm_120), driver 616.56, Windows 11, MSVC 14.51, branch at Totals
Off-arena figures are the mean of three clean runs (1167.95/1160.78/1160.50 prefill, Prefill is quadratic with the arena onCumulative rate from the server's own
Off-arena stays flat at ~1340 t/s, i.e. linear prefill. With the arena the cumulative rate falls A bigger arena is much slowerThis is the part that looks wrong rather than merely expensive. At the same 12330-token mark:
7.4x slower with twice the resident capacity. The 4096 run was still at 91% of the prompt For completeness, the arena has a lower bound too — Note on the baselineOn this model What I did not checkWhether output stays correct under streaming — I compared throughput only. Also single arena 🤖 Generated with Claude Code |
Block KV cache streaming: bound VRAM at long context via a shared CUDA phase arena
Summary
Adds experimental block KV cache streaming, ported and generalized from
Raymond Huang's
feature/kv-stream-phase-arena(https://github.com/RaymondHuang210129/llama.cpp-adaptive-kv-streaming). His
implementation is Qwen3.5-only; this generalizes the same shared CUDA
phase-arena design to any architecture with a standard, uniform-per-layer KV
cache (plain dense/MoE,
llama_memory_hybrid, iSWA), wires it intoTurboQuant's native
direct_attentionkernels (turbo2/3/4), and validates itagainst real local models rather than by inspection.
With streaming on, only a resident subset of KV pages plus the active
compute workspace live in one bounded CUDA allocation; the rest of the KV
cache lives in host RAM and streams on demand. Enabled with
--kv-stream-arena-mib N;0(default) disables it and behavior isunchanged.
All raw evidence (results.csv/.jsonl for every sweep and llama-perplexity
logs for every correctness check - every number in the tables below comes
straight from these) is committed at
benchmarks/results/kv-stream-pr-evidence/(excludes the multi-GB saved-logits
.kldfiles - regenerable with--save-all-logitsif needed).What's new since the port
direct_attentionpath (measured ~2xprefill speedup on Qwen3.8-27B: ~1390 -> ~2930 t/s). This wiring benefits
ordinary non-streamed attention unconditionally, but streamed
attention only takes the
direct_attentionpath when ggml-cuda is builtwith
GGML_CUDA_FA_ALL_QUANTS(off by default) - without it, everystreamed KV pair falls back to a slower F16-dequant path (measured
~2840 -> ~1200 t/s streamed prefill at 8K context, same model/types).
See "Attention dispatch: direct vs F16 fallback" in
docs/kv-stream.md.process_ubatch()never switched the phasearena into decode layout for any plain (non-hybrid)
llama_kv_cache-every model tested had been running decode with the prefill layout for
its entire lifetime. Fixing it dropped streaming-vs-non-streaming mean
KL-divergence from 0.003 to ~0.000000.
dynamic_caststo
llama_memory_i::get_kv_stream_targets(), so an unsupported memorytype fails closed with a clear error instead of a hard crash.
Gemma-family, this fork's own
lagunaarch): the full-attention(
kv_base) sub-cache streams, the sliding-window sub-cache staysresident (small by construction).
correctness regression (mean KLD 0.07 vs ~0.000000 for every other
architecture) and reverted it rather than ship it. DSA/MSA/MLA remain
excluded or unverified for the same reason - see
docs/kv-stream.md.
--swa-fullsilently conflicted with streaming (both iSWA sub-cachestried to bind the same single-lease arena); now rejected explicitly
with a named reason instead of a generic construction failure.
no_allocdry-run context (used by-fit's auto-probe and by MTP'sparent-memory-sizing pass) built a real CUDA arena anyway, then failed
with a confusing "requires a memory type with a streamable KV cache"
error - benign (the real context loads fine moments later) but
alarming. Now skipped cleanly for
no_alloccontexts.docs/kv-stream.md: requirements, architecture support matrix, whysingle-sequence-only is a design limitation and not a validation gate,
arena sizing guidance, and a known-inefficiency note (see below).
benchmarks/benchmark_kv_memory.py: sweeps context length andrecords GPU VRAM + host process RSS, with a same-context no-streaming
baseline at every point (including past the point where the baseline
OOMs), and plots both. Unlike the throughput sweep it needs no
prefill/decode pass, so it's cheap enough to cover much wider ranges.
Architecture support
llama_kv_cache)llama_memory_hybrid(recurrent + attention, e.g. Qwen3.5)laguna)Benchmarks
All runs: single RTX 5090-class GPU (32 GiB),
-ctk q8_0 -ctv turbo4 -fa on --parallel 1, fixed 8192 MiB shared arena, ggml-cuda built withGGML_CUDA_FA_ALL_QUANTS=ON, viabenchmarks/benchmark_kv_stream.py(prompt = context - 256 tokens, decode 256 tokens, prompt not cached). A
default build (flag off) gets materially lower streamed prefill throughput
at the same context/arena size - see "Attention dispatch: direct vs F16
fallback" in
docs/kv-stream.md.Qwen3.8-27B (dense/hybrid, no speculative decoding)
17 points, 8K -> 262K context. Smooth, continuous decline in both prefill
and decode throughput as context grows - no cliff, no crash, right through
the model's native 262K training context.
VRAM grows only ~1 GiB (28.1 -> 29.1 GiB, ~3.5%) across a 32x increase in
context (8K -> 262K) - the fixed 8192 MiB arena plus a small amount of
bookkeeping overhead that scales with layer/page counts, not with total
context. Full per-point data:
qwen3.8-27b-throughput/results.csv.Correctness (streaming vs non-streaming), this checkpoint:
Mean KLD is at the numerical noise floor at both scales. 32K is the more
meaningful check: host RSS measurements (see "VRAM and host RAM" below)
confirm real page streaming is active by that point, not just a
fully-resident no-op. Full methodology and a larger-scale attempt (and the
tooling limitation it hit) are in "Correctness: PPL and KL-divergence, at
increasing scale" below. Raw logs:
kld-logs/.Qwen3.8-27B + MTP speculative decoding (
--spec-type draft-mtp --spec-chain 8)5 points, 8K -> 106K context. Streaming coexists cleanly with MTP on the
target context (MTP's own draft context never streams - see docs). Draft
acceptance on this benchmark's synthetic repeated-filler prompt was
measured at ~100%, so these decode numbers are a best-case ceiling, not a
typical-prompt expectation - treat the shape of the curve as
representative, not the absolute numbers.
Full per-point data:
qwen3.8-27b-mtp-throughput/results.csv.Required a small fix to
benchmarks/benchmark_kv_stream.py: its exactprompt/decode token-count check had never been run against speculative
decoding before, and MTP's chunked verification (up to
spec_chain+1tokens per step) can clip the last chunk a few tokens short of the target
right at the context boundary. Added a bounded tolerance derived from
--spec-chain; the reporteddecode_tpsis unaffected either way (it'sthe server's own measured rate, not derived from the target count).
Gemma-4-26B-A4B-It (MoE, iSWA)
6 points, 8K -> 131K context, using the proper instruction-tuned checkpoint
(
gemma-4-26B-A4B-it-UD-Q5_K_S, Unsloth dynamic quant - an earlier passused a community "Uncensored" merge; see the correctness section below for
why it was swapped). Prefill is much faster than Qwen (MoE, ~4B active
params/token vs Qwen's dense 27B). Decode falls off far more steeply than
Qwen's dense/hybrid curve.
Full per-point data:
gemma4-it-throughput/results.csv.Numbers are within noise of the earlier (worse-quant) checkpoint's run,
which rules out quantization/merge quality as the cause of the steep decode
falloff - it's reproduced almost exactly across two unrelated checkpoints of
the same architecture. VRAM is flat (27.8 -> 28.2 GiB) across the whole
16x context range - same arena-boundedness result as Qwen, on a
structurally different (MoE + iSWA) architecture. Streaming itself is
stable (no crash, no errors) through the whole sweep; the falloff looks
architectural (MoE expert routing and/or the mixed full+SWA cache
structure) rather than a streaming-specific regression, but hasn't been
root-caused against a non-streaming Gemma4 baseline - flagging as an open
question rather than asserting a cause.
VRAM and host RAM: where the data actually goes, and what happens without streaming
benchmarks/benchmark_kv_memory.py(new in this PR - see "What's new")sweeps context length and records GPU VRAM and host process RSS, with a
same-context no-streaming run alongside the streaming one at every point.
It's cheap (no prefill/decode needed - the buffers are sized at context
construction) so it can cover a much wider range than the throughput sweep.
Qwen3.8-27B-AD, 131K -> 786K context (YaRN-scaled past the 262K
training context, 4x,
--rope-scale 4 --yarn-orig-ctx 262144):No-streaming hits a real
CUDA error: out of memoryabort at 393K (fullgdb-style backtrace:
qwen3.8-27b-memory/logs/baseline-393216.log-inside the server's own warmup decode, not a clean pre-flight allocation
failure, an in-flight kernel abort, matching the "hard process abort near
the VRAM ceiling" failure mode in
docs/kv-stream.md). Streaming keepsgoing another 6x further (to 786K, the max tested) with VRAM pinned to
~28.3 GiB throughout, while host RSS absorbs the growth (4.4 -> 20.6 GiB).
Full per-point data:
qwen3.8-27b-memory/results.csv.Gemma-4-26B-A4B-It, 65K -> 393K context:
No-streaming never hit its ceiling in this range (21.4 -> 25.4 GiB, still
under budget at 393K) - most of Gemma4's layers are small fixed-window SWA
attention that doesn't grow with context, so its non-streaming footprint
grows much more slowly than Qwen's dense/hybrid cache. Streaming stays flat
at ~28 GiB across the same range. Not every architecture needs streaming at
the same context threshold, but streaming costs nothing extra either way.
Full per-point data:
gemma4-it-memory/results.csv.Correctness: PPL and KL-divergence, at increasing scale
A short check is a weak check here. The mechanism this PR is actually
about - page residency, eviction, and ring-buffer streaming - only
activates once the resident window is a real fraction of the total
context. An early pass validated at
-c 4096against an 8192 MiB arena,which likely fits entirely resident with little real eviction exercised.
Repeated at increasing scale, on the exact checkpoints benchmarked above
(
llama-perplexity, wikitext-2-raw, non-streaming logits saved via--kl-divergence-base, compared against a streaming--kv-stream-arena-mib 8192run of the same chunks):Mean KLD stays at the numerical noise floor at every scale tested, and
same-top-token stays at ~100% - streaming reproduces each model's own
output distribution, at a context large enough (32K) that host RSS
measurements above confirm real page streaming is active, not just a
fully-resident no-op. A 131K attempt on Qwen hit a tooling limitation in
llama-perplexity's own KL-divergence machinery (failed reading log-probs for chunk 0against a 27 GB saved-logits file, failing immediately ratherthan partway through - looks like a pre-existing limit unrelated to
streaming, not something this PR introduced) rather than a real finding
either way; flagging as an open item rather than a validated result at
that scale. Raw log:
kld-logs/qwen-streaming-131k-FAILED-tooling-limitation.log.Gemma4-It's raw PPL is unusually high on wikitext for both runs equally
(confirmed coherent in normal chat use - see
kld-logs/gemma4-it-coherence-check.log)poorly on raw non-chat continuation, a known effect, not a fork bug. Ruled
out turbo K/V quantization and Flash Attention as causes by reproducing the
same result with plain f16 KV cache and with
-fa off(seekld-logs/gemma4-it-f16-check.logandkld-logs/gemma4-it-nofa-check.log).KLD - the metric that actually measures streaming fidelity, independent of
whether the base distribution is "good" at modeling wikitext - shows no
divergence regardless.
Gemma4 + MTP: not applicable
Confirmed on two independent checkpoints (the community merge and the
official instruct release) - neither has
nextntensors (checked viadirect GGUF metadata read), so
--spec-type draft-mtpfails outright onboth:
context type MTP requested but model doesn't contain MTP layers.This is an architecture-wide limitation (the Gemma4 family wasn't trained
with an MTP/NextN head in these public releases), not a streaming
limitation or a quant/merge quality issue. No streaming-specific finding
here either way.
Known limitations (see docs/kv-stream.md for full detail)
-np 1). Not a conservative gate - theresident-page design indexes pages by absolute buffer offset, the decode
fast path assumes exactly one query token, and the adaptive
repartitioning loop tracks one global counter. Real multi-sequence
support needs a sequence dimension added to all three; not attempted.
common/speculative.cpp- the draft/target contexts don't yet share onepool). Streaming on the target context is unaffected and verified
working with MTP active (see benchmarks above).
generation layout (server never calls
llama_set_decode_phase()).Measured: 89 of 90 decode steps in a real MTP+streaming session ran in
the prompt-phase layout (1246 resident pages/layer, 10 ring slots)
instead of the generation layout (1273 resident pages/layer, 12 ring
slots) - a real but modest gap at this context size. Fixing it requires
resizing the generation-phase compute reservation for multi-token
speculative batches, not just relaxing the existing TG1-only guard.
Documented, not fixed, to avoid touching stable repartitioning logic for
an unmeasured-at-scale win.
one CUDA device today.
Testing
test-kv-stream-config: 11 tests / 47 assertions, 0 failures.test-arg-parser: full suite passes, including new coverage for--kv-stream-arena-mib/--kv-stream-stage-mib(legacy alias), negativerejection, and 0-disables-cleanly.
llama-perplexity --kl-divergence-base/--kl-divergence) on everysupported architecture: mean KLD ~0.000000, >99% same-top-token for
turbo/iSWA. DSV4 was the one architecture that failed this bar (0.07
mean KLD) and is excluded as a result. Re-verified against the exact
Qwen3.8-27B-AD and Gemma-4-26B-A4B-It checkpoints used in the benchmarks
above at increasing context (4K and 32K for Qwen, 4K for Gemma4) - see
"Correctness: PPL and KL-divergence, at increasing scale" - mean KLD at
the noise floor at every scale tested, confirmed at a context (32K)
large enough that host RSS growth shows real page streaming is active.
benchmark_kv_memory.py(new): VRAM stays flat while no-streaming growsuntil a real CUDA OOM abort at 393K context (Qwen3.8-27B) - streaming
ran 6x further with no VRAM growth. Full backtrace in
qwen3.8-27b-memory/logs/baseline-393216.log.llama-serverinvocation (mmproj +MTP +
-c 226000+ streaming) served real chat completions withmeasured MTP draft acceptance for 2+ hours without incident.
Credits
Original shared-phase-arena design and its Python benchmark tooling by
Raymond Huang (https://github.com/RaymondHuang210129/llama.cpp-adaptive-kv-streaming).
This PR generalizes it beyond Qwen3.5 to TurboQuant's supported
architectures and turbo K/V quantization types, and adds the fixes and
validation described above.
🤖 Generated with Claude Code