Feature/adaptive kv stream integration - #326
Conversation
Adds the feature gate for experimental block-granular KV cache streaming: --kv-stream (MiB budget, 0 = disabled) plumbed from CLI through common_params/llama_context_params/llama_cparams into llama_context, which validates the requested configuration (unified KV cache, single sequence, Flash Attention, GPU KV offload, cache-type-pair support) before allowing construction to proceed. No execution backend yet, so the feature stays inert. Assisted-by: Claude Sonnet
Adds two optional ggml_backend proc-address symbols (ggml_backend_kv_stream_supported_t / ggml_backend_kv_stream_type_pair_supported_t), resolved the same way other optional backend extensions already are. llama_context now queries the KV cache's actual device instead of a hardcoded Q8_0/Q4_0-only stub; every backend still reports unsupported until an execution backend implements the symbols, so behavior is unchanged for now, on the real mechanism instead of a placeholder. Assisted-by: Claude Sonnet
New, self-contained file: a ggml_backend_buffer_type for pinned host KV storage, following the same pattern as the reference adaptive- KV-streaming fork's buffer shim. Deliberately scoped to just the allocator integration - no FlashAttention dispatch yet (that needs its own pass against this fork's fattn.cu, which is structured differently from the reference). Not registered or reachable from anywhere yet, so behavior is unchanged. The pinned host buffer is the authoritative KV storage; FlashAttention dereferences it directly via CUDA's unified virtual addressing once a later commit wires it in, so no separate device-side resident cache or transfer ring is needed for correctness. Assisted-by: Claude Sonnet
Pulls the mixed-KV-type compatibility check that ggml_cuda_get_best_fattn_kernel already used inline into a standalone ggml_cuda_fattn_kv_type_pair_supported(type_k, type_v), exposed via fattn.cuh. Pure extraction - the existing dispatch function now calls it instead of duplicating the logic; verified the check order change doesn't affect the returned boolean for any input, under either GGML_CUDA_FA_ALL_QUANTS setting. Meant as the type-only pre-check for KV cache streaming's config validation (no tensors exist yet at that point), generalizing it off a single hardcoded pair. Not yet wired to anything - see the branch notes for why the remaining integration (llama-kv-cache.cpp requesting the streaming buffer type, and driving the transfer ring from the decode loop) is left as follow-up work that needs real hardware, not another blind pass. Assisted-by: Claude Sonnet
Adds ggml_backend_cuda_kv_stream_supported and _type_pair_supported (delegating to the fattn.cu type-pair check from the previous commit) to the CUDA backend's proc-address table, so llama_kv_stream_backend_caps_query gets real answers instead of always reporting unsupported. Unblocks a real build/GPU session (see docs/adaptive-kv-stream-session-summary.md) that hit this as a missing-symbol dead end while smoke-testing --kv-stream. --kv-stream should now validate and start the server normally for a supported device/type pair instead of throwing, but nothing streams yet: llama-kv-cache.cpp doesn't request the pinned buffer type for KV tensors yet, so the cache still allocates ordinary VRAM storage. That remaining wiring needs a llama_kv_cache constructor change across its several call sites in llama-model.cpp - left as follow-up now that real hardware is available to verify it against. Assisted-by: Claude Sonnet
llama_kv_cache's constructor gains a trailing kv_stream_stage_bytes parameter (defaulted, so the one arch-excluded call site needs no change). Before the layer loop it now resolves the CUDA backend's runtime-lifecycle symbols via the same reg_get_proc_address plugin pattern used elsewhere, creates one streaming runtime shared by the eligible layers on its device, and requests its pinned buffer type for their K/V tensors instead of the ordinary device buffer type. Any failure at any step falls back to today's behavior unchanged. Threaded llama_memory_params.kv_stream_stage_bytes through llama_memory_hybrid down to its internal llama_kv_cache, and through the two real call sites in llama-model.cpp's arch switch that can reach unified_kv_cache == true (the plain default case and the non-SWA hybrid case). Added a generic runtime_new_for_device wrapper (plain scalar args) in kv-stream.cu/.cuh since the existing constructor took a CUDA-internal struct generic code can't reference, and registered it plus _free/_buffer_type in ggml-cuda.cu's proc-address table. Conservatively excludes turbo K or V types on >= 8 layers: the already-shipping TURBO_LAYER_ADAPTIVE override (auto-enabled by default for turbo2 V-cache models) can give layers different K/V types there, which the streaming pool's single page-byte-size assumption can't represent. Provably safe (that's the only condition under which the override changes anything) rather than duplicating its ~50-line dispatch table to predict it exactly. Assisted-by: Claude Sonnet
llama_kv_cache_iswa (used standalone, or inside llama_memory_hybrid_iswa for hybrid+SWA models like Qwen3.8-27B) is internally just two plain llama_kv_cache instances - kv_base (non-SWA layers, already constructed with swa_type = LLAMA_SWA_TYPE_NONE) and kv_swa (sliding-window layers). Threads kv_stream_stage_bytes through both wrapper constructors into kv_base only: it's the component whose size scales with total context length (the memory pressure this feature targets), while kv_swa is already bounded by the sliding window and doesn't need it. Updated the 3 of 5 llama_kv_cache_iswa/llama_memory_hybrid_iswa call sites in llama-model.cpp that aren't already excluded by the DSA/DSV4/MSA arch denylist (the hybrid+SWA case, and the two standalone-SWA cases). Left DeepSeek4's two call sites untouched. Dropped the swa_type == LLAMA_SWA_TYPE_NONE requirement from llama_kv_stream_arch_uses_unified_cache(): kv_base is always constructed with swa_type == NONE regardless of the model's own setting, so the model-level check was never load-bearing once kv_base itself can stream. Added LLM_ARCH_DFLASH to the exclusion list while auditing the call sites - its DSpark-stage path stores "a single MLA-style K per position" per its own code comment, the same class of shape as the already-excluded DSA/DSV4 caches. Verified: llama library rebuilds clean, all four test-kv-stream-* tests pass, and test-llama-archs (synthetic models + real forward passes across essentially every registered architecture, SWA and hybrid-SWA included) passes in 114s - the change doesn't disturb construction anywhere in the switch. Still unverified: whether streaming actually engages and produces correct output on real SWA hardware (e.g. Qwen3.8-27B). Assisted-by: Claude Sonnet
The variable-head-dim safety guard used hparams.is_n_embd_k_gqa_variable()/_v_gqa_variable(), which check uniformity across every layer in the whole model - including recurrent layers, and n_embd_head_k(il) itself returning is_swa(il) ? n_embd_head_k_swa : n_embd_head_k_full (two separate config fields). For a hybrid+SWA model this almost certainly returns true even when the attention layers this specific llama_kv_cache instance (e.g. kv_base) actually manages are uniform among themselves - rejecting based on layers it doesn't own. Replaces the global check with one scoped to the layers this instance actually manages, folded into the existing pre-scan loop: compares each eligible layer's actual K/V byte size against the first one found, using the same v_trans logic already used elsewhere in this file. This is the check that was actually needed - "uniform among the layers this cache streams" - which the global check never verified. Also adds LLAMA_LOG_INFO breadcrumbs at every decision point in the pre-scan (request received with guard values, why skipped if skipped, layer-count/device found, symbol-resolution results, page_bytes/runtime/buft results, and a positive "pool created" confirmation that didn't exist before on the success path), so log output shows exactly where it accepts or rejects instead of producing silence either way. Assisted-by: Claude Sonnet
Adds one INFO line, printed whenever --kv-stream sets a non-zero MiB budget, before calling llama_kv_stream_config_validate: shows stage_bytes, minimum_stage_bytes, and all seven individual gates (unified_kv_cache, context_default, single_sequence, flash_attention, kv_offload, type_pair_supported) plus the resolved device name. A failing gate makes validate() throw (a visible startup error, not silence), so this line appearing right before that error shows exactly which field was false. Assisted-by: Claude Sonnet
Adds full sweep-parameter support for the experimental KV cache streaming staging pool, mirroring the existing n_batch/fit_min_ctx pattern: CLI parsing (comma-range values), cmd_params/instance/test struct fields, cartesian-product combination, and a results-table column that only appears when swept away from the default. llama-perplexity and llama-cli already support --kv-stream with no code changes needed: the flag is registered under LLAMA_EXAMPLE_COMMON in common/arg.cpp, which every example inherits, and both tools thread common_params through common_init_from_params the same way llama-server does. Assisted-by: Claude
Replace the blanket "any turbo K/V + >= 8 layers is unsafe to stream" exclusion with a precise check that mirrors the actual TURBO_LAYER_ADAPTIVE dispatch conditions, so a plain turbo3/turbo4 K/V config (no per-layer type variation) streams normally instead of always falling back; turbo2-V still correctly falls back since its auto-enabled mode does vary per-layer types. As part of this, hoisted the adaptive-mode resolution out of a function-local static (which pinned the mode from the first cache ever constructed in the process, regardless of later type_v - relevant now that llama-bench can sweep --cache-type-v) into a per-construction local shared by both the pre-scan and the per-layer loop. Add --kv-stream auto: derives the staging pool size from -c instead of a hand-picked MiB value, targeting a fixed percentage of the context resident per layer (not the full context size, which would defeat the point of streaming), floored and capped to sane bounds. Threaded as a new kv_stream_auto flag through the same path as kv_stream_stage_mib end to end, resolved once in llama_context's constructor where model/hparams/n_ctx are all available. Assisted-by: Claude
tools/perplexity/perplexity.cpp's plain-PPL/KLD path computed n_parallel = max(1, n_batch / n_ctx) unconditionally, discarding whatever -np/--parallel was passed - including an explicit -np 1 needed to satisfy --kv-stream's single-sequence gate. Added n_parallel_explicit tracking (same pattern as the existing moe_cache.mode_explicit field) so an explicit -np now sticks; the auto-batching heuristic still applies unchanged when -np isn't passed. Assisted-by: Claude
llama-bench's --kv-stream sweep only accepted numeric MiB values and rejected "auto". Introduced a kv_stream_spec value type (MiB + auto flag) threaded through cmd_params/cmd_params_instance/test so "auto" and mixed sweeps like "0,2304,auto" parse correctly; the results column reclassifies as a string since it can now print either a number or "auto". Assisted-by: Claude
Multi-GPU tensor-split: the streaming pre-scan previously only ever built one runtime, on whichever device hosted the first eligible layer - every other device's layers silently never streamed. Group eligible layers by device instead and create an independent runtime per device that has any (kv_stream_stage_bytes is a per-device MiB budget, not split across devices). A device whose runtime fails to create falls back locally without affecting the others. llama_kv_cache::kv_stream_runtime/kv_stream_runtime_free_fn (single handles) became kv_stream_entries (one per device). DFLASH architecture extension: it was blanket-excluded via the same denylist as genuinely special MLA/indexer caches, but it only uses a non-standard cache when hparams.dsv4_hc_mult > 0 (the DSV4 DSpark case) - otherwise it builds the same standard cache as any other architecture. llama_kv_stream_arch_uses_unified_cache now checks dsv4_hc_mult instead of blanket-excluding the whole architecture. common_fit_params hard-error fix: -fit is on by default and probes candidate configurations with mparams.no_alloc=true (a dry run that measures sizes without real allocation). The streaming validation gate threw a hard exception on any failing gate unconditionally, no_alloc or not, and the probe treated any failure to construct its initial trial as fatal - producing "failed to fit parameters to device memory (hard error)" even though no real device memory was ever at stake. Skip the entire streaming gate (llama-context.cpp) and runtime-creation loop (llama-kv-cache.cpp) during a no_alloc probe, matching the existing convention the K/V tensor buffers and turbo rotation matrices already follow. Assisted-by: Claude
|
@TheTom what i was working on remotely. The idea is to take KV cache OFF the VRAM, and place it on system ram, allowing you to squeeze a bit more space out of the GPU for a better model, but it does take a small 3% hit to tgs. Also, because of this you can scale up context to fit your RAM size. Think of it as a "RAM Disk" of a sort. |
|
This is promising, but I do not think it is ready to merge yet. The current branch still needs three concrete checks:
Please also rerun the 512/2K/4K/8K PPL parity sweep after syncing onto the current base, since several memory and model changes landed today. The 262K MTP completion is good evidence for the single-GPU path. These remaining numbers will tell us whether the wider integration is safe to land. |
I absolutely agree, thats why I marked this as experimental. Its a promising direction but needs more work. |
|
Status from the maintainer side: this branch now conflicts with Moving this to draft until those land and the branch is rebased. Ping me when it's ready and I'll re-review. Two pieces look ready to go on their own if you want them in sooner: the ~2,250-line dead-code removal plus the phantom device allocation fix, and the |
…egration # Conflicts: # src/llama-kv-cache.cpp # src/llama-kv-cache.h # src/llama-model.cpp
PR #326 Validation — Adaptive Block KV StreamingPR: TheTom/llama-cpp-turboquant#326 — 0. Branch status going into this round
1. PPL parity sweep (re-run on current merged base)Reruns the exact sweep from the PR description, now on top of the 73 commits pulled in from Model: Exact commands run (8 total, one process per row of the table below): ./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 512 -b 512 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream 0 -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.raw
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 512 -b 512 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream auto -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.raw
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 2048 -b 2048 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream 0 -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.raw
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 2048 -b 2048 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream auto -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.raw
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 4096 -b 4096 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream 0 -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.raw
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 4096 -b 4096 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream auto -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.raw
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 8192 -b 8192 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream 0 -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.raw
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 8192 -b 8192 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream auto -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.rawRun from
Result: exact match to the PR's original table. All four context sizes reproduce identically (to the last reported digit), confirming the sync onto the current base introduced no PPL regression. 2. KL-divergence with streaming active (previously unmeasured)The PR explicitly flagged this as not yet run: "KLD with streaming active: not yet measured... no KLD-with-streaming number has actually been computed." Run here for the first time. Model: Baseline logits generated fresh on the current head ( # baseline: generate logits with streaming off
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 4096 -b 4096 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream 0 --kl-divergence-base /mnt/storage/Projects/llama-cpp-turboquant/results/pr326_validation/kld_base_8b_c4096.bin -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.raw
# compare: streaming auto against that baseline
./build/bin/llama-perplexity -m /mnt/storage/models/qwen3/Qwen3-8B-Q8_0.gguf -c 4096 -b 4096 --chunks 1 --no-warmup -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -t 8 -np 1 --kv-stream auto --kl-divergence-base /mnt/storage/Projects/llama-cpp-turboquant/results/pr326_validation/kld_base_8b_c4096.bin --kl-divergence -f /mnt/storage/blackbeard/wikitext-2-raw/wiki.test.rawPerplexity statistics
KL-divergence statistics
Token probability statistics
Result: negligible divergence. A mean KLD of 0.0008 and 98.8% top-token agreement is consistent with ordinary floating-point reordering noise (different memory layout / transfer path, same math), not a quality regression from streaming. 3. VRAM / RAM re-measurement on current headThe PR's own VRAM/RAM table was explicitly marked stale: "this measurement predates the fix that removed an extra full-size Model: # baseline server
./build/bin/llama-server -m /mnt/storage/models/qwen3.8/Qwen3.8-27B-Q6_CR.gguf -c 65536 -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -np 1 --kv-stream 0 --port 8091
# streaming server (run after stopping the baseline one)
./build/bin/llama-server -m /mnt/storage/models/qwen3.8/Qwen3.8-27B-Q6_CR.gguf -c 65536 -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -np 1 --kv-stream auto --port 8091
# same completion request issued against each
curl -s http://127.0.0.1:8091/completion -d '{"prompt":"The quick brown fox jumps over","n_predict":16}'
# memory sampled after load and again after the completion, for each condition
nvidia-smi --query-gpu=memory.total,memory.used,memory.free --format=csv,noheader
free -m
# separately, to capture the exact auto-derived stage size and gate values (llama-server's
# default log level doesn't print them; -v does), a one-shot llama-cli run with full verbosity:
./build/bin/llama-cli -m /mnt/storage/models/qwen3.8/Qwen3.8-27B-Q6_CR.gguf -c 65536 -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -np 1 --kv-stream auto -no-cnv --simple-io -n 1 -p "hi" -v
Both conditions produced correct, coherent completions ("...the lazy dog."). Diagnostic log (
|
| Value | |
|---|---|
| GPU used (steady state, post-completion) | 28,214 MiB / 32,607 MiB (86.5%) |
| Host used | ~23,067 MiB |
The value proposition, directly: baseline cannot even load
Same config, same forced 66/66 GPU layers, --kv-stream 0:
./build/bin/llama-server -m /mnt/storage/models/qwen3.8/Qwen3.8-27B-Q6_CR.gguf \
-md /mnt/storage/models/qwen3.8/MTP/mtp-Qwen3.8-27B-Q4_0.gguf \
-c 262144 -ctk q8_0 -ctv q8_0 -fa on -ngl -1 -np 1 --kv-stream 0 -fit off \
--spec-type draft-mtp --spec-chain 8 --port 8091ggml_backend_cuda_buffer_type_alloc_buffer: allocating 1346.62 MiB on device 0: cudaMalloc failed: out of memory
failed to initialize the context: failed to allocate buffer for rs cache
Baseline OOMs immediately — this exact model/context/MTP combination is not loadable on a 32 GB GPU without streaming. With streaming, it runs comfortably at 86.5% peak VRAM with headroom to spare. This is the clearest direct evidence of the feature's value: not a percentage optimization, but the difference between "runs" and "doesn't."
Result: reproduces the PR's headline claim on the current merged base, and surfaces one real regression to flag — the -fit/--kv-stream interaction needs either -fit to account for streaming's savings, or a clearer error when it doesn't, before this can be a smooth default-on experience at the sizes where streaming matters most.
5. Multi-GPU tensor-split — NOT RUN
This environment has a single GPU (RTX 5090). TheTom's requested real multi-GPU tensor-split validation could not be performed here and remains outstanding. The multi-GPU code path (independent per-device streaming runtime, added in kv-cache: multi-GPU streaming, DFLASH extension, fit-params fix) is otherwise unexercised beyond compilation and the existing unit tests.
Summary
| Check requested by TheTom | Status |
|---|---|
Rebase/resync onto current feature/turboquant-kv-cache |
✅ Done — merged, conflicts resolved, builds clean, PR now MERGEABLE |
| Rerun 512/2K/4K/8K PPL parity sweep on current base | ✅ Done — exact match to original numbers |
| KLD with streaming active vs baseline | ✅ Done — mean KLD 0.000775, 98.8% top-token agreement |
| Re-measure device VRAM / host RAM on current head | ✅ Done — −1,990 MiB device / +1,808 MiB host at 65K ctx, 27B model |
| Real multi-GPU tensor-split run | ❌ Not possible on this single-GPU machine |
Additionally, reproduced the PR's 262,144-context / MTP headline scenario on the current base: confirmed baseline cannot load at all at this scale (CUDA OOM) while streaming runs a real completion at 86.5% peak VRAM with working speculative decoding — but also surfaced a new regression from the feature/turboquant-kv-cache sync: default-on -fit doesn't account for streaming's VRAM savings and will misconfigure (or outright fail) this exact scenario unless run with -fit off.
Three of TheTom's four requested checks are complete and clean; multi-GPU tensor-split remains the one item this machine can't cover. The -fit/--kv-stream interaction is a new, real issue worth fixing (or at minimum documenting) before this leaves draft.
CI's stricter warning flags (-Werror -Wmissing-declarations, not enabled in this repo's default local build) flag every function defined in kv-stream.cu's anonymous namespace as missing a prior declaration, even though their linkage is already internal. Add forward declarations for each; no behavior change. Verified: the exact 12 warnings from the CI log (job 100723464145, PR TheTom#326) are gone from a local rebuild with -Wmissing-declarations enabled, llama-cli/llama-bench/llama-perplexity/llama-server still build and link cleanly.
kv-stream.cu's cudaHostAlloc() call itself already resolves under HIP and MUSA, but the CUDA-only flag constants cudaHostAllocMapped / cudaHostAllocWriteCombined have no equivalent macro in vendors/hip.h or vendors/musa.h: HIP names them hipHostMallocMapped/WriteCombined (not "Alloc"), and MUSA mirrors CUDA's naming under a musa* prefix, matching the existing cudaFreeHost -> musaFreeHost / cudaMallocHost -> musaMallocHost mappings already in vendors/musa.h. Same file-local per-backend branching pattern already used in allreduce.cu. Fixes the hip / musa / ubuntu-22-hip-quality-check CI failures on PR TheTom#326 (job 100723464556 / 100723464078 / 100723464913): "use of undeclared identifier 'cudaHostAllocMapped'"/"'cudaHostAllocWriteCombined'". HIP flag names verified against a real hip_runtime_api.h available locally (hipHostAllocMapped/WriteCombined also exist there as equivalent aliases). MUSA flag names follow this repo's own established 1:1 naming convention but weren't verified by compilation - no MUSA toolchain available locally; CI will confirm.
The previous fix only mapped the flag constants and assumed cudaHostAlloc() itself already resolved on HIP/MUSA - it doesn't. The compiler just hadn't gotten far enough to check the callee name while its arguments were also unresolved, so the first CI run only surfaced the flag errors. Once those were fixed, both hip and musa failed on the function name itself: hip: "use of undeclared identifier 'cudaHostAlloc'; did you mean 'hipHostAlloc'?" musa: "use of undeclared identifier 'cudaHostAlloc'; did you mean 'musaHostAlloc'?" Both suggestions confirm the exact names guessed in the previous commit's macro block, so this just extends it to also cover the function name, matching the flags' verified spelling. Fixes the hip / musa / ubuntu-22-hip-quality-check CI failures on PR TheTom#326 (run 33795505410, jobs 100782100161 / 100782100291).
|
Looks like someone had a similar idea but did it a bit better and fixed the regression of tg for low to mid context size. I'll wait to adapt his ideas if this is merged. |
|
Thanks for the validation round, the KLD (0.0008 mean, 98.8% top-1) and the VRAM re-measure answer two of my three asks properly. I went through the diff at 84b00af hunk by hunk with the flag off as the deciding question, and then looked at what the flag actually does. Holding this one, with reasons: What the flag does today is not what it says. Nothing tells the CUDA backend it may consume that buffer type. Default Two flag-off behavior changes are buried in here and both are good. Please send them as their own small PRs today and I will merge them: (1) Smaller: Given your note about RaymondHuang's implementation handling the low-context tg regression better, that may be the better base to build the real pool on. Back to draft for now; the two split-out fixes are welcome any time. |
|
Confirmed the CPU/CUDA0 thrashing empirically before touching anything — ran
Once the streaming pool is constructed, every decode graph fragments into ~74 scheduler splits alternating I couldn't confirm the "reserve pass sizes the compute buffer for full n_kv" part at Also split the two flag-off fixes out as you suggested: #353 ( Given the split-thrashing is now measured and not just inferred, I'll hold off on any further changes to this PR until there's a decision on whether to build the real staging pool here or rebase onto RaymondHuang's implementation as you suggested. |
|
@TheTom I have decided to close this out and rebuild off of RaymondHuang work since I made mistakes along the way. Once I am confident in its success, I'll do a PR |
PR: Adaptive block KV streaming (experimental)
Adds an experimental
--kv-stream <MiB|auto>flag that pins the K/V cache for eligible layers in host memory and streams only a small resident "staging pool" onto the GPU, instead of keeping the full KV cache resident in VRAM. Goal: trade a small throughput hit for a large VRAM reduction, especially at long context.Commits in this PR:
kv-cache: backend-agnostic streaming planning layer + per-backend capability querycuda: pinned KV streaming buffer type, capability symbolskv-cache: wirellama_kv_cacheto request the streaming buffer type; extend to SWA/hybrid-SWAkv_basellama-bench/llama-perplexity/llama-cli: expose--kv-stream--kv-stream auto: derive the staging pool size from-c(10% of context resident per layer, 2048-token floor, 8192 MiB ceiling)perplexity: fix-npbeing silently overridden on the plain-PPL path (was blocking--kv-stream's single-sequence gate)fit-paramsdry-run crash fixTest environment
RTX 5090 (32 GB VRAM, sm_120), Qwen3-8B Q8_0 and Qwen3.8-27B Q6_CR (ConvRot), wikitext-2-raw.
1. Correctness — PPL parity, streaming on vs off
Fresh sweep, Qwen3-8B Q8_0, single-chunk PPL at each context size,
-fa on -ctk q8_0 -ctv q8_0:--kv-stream 0)--kv-stream autoAll deltas are within run-to-run noise — no measurable quality regression from streaming at these sizes. (Sweep also targeted 16K–262K; those runs were killed by unrelated host/GPU resource limits before producing a result — see Limitations.)
Separately, on real hardware with the
-npfix applied (512 ctx, Qwen3-8B, no workaround needed): baselinePPL = 6.9576 ± 0.045vs--kv-stream autoPPL = 6.9568 ± 0.045— ~0.0% difference.KLD with streaming active: not yet measured. The
.kld/logits-dump files in this branch are just saved baseline logits for a future comparison; no KLD-with-streaming number has actually been computed. Worth running before merge if KLD parity is a hard requirement.2. Real-world validation — 262K context, 27B model, MTP speculative decode
llama-serverwith Qwen3.8-27B Q6_CR,-c 262144 --kv-stream auto, MTP draft chain enabled:Both the main context and the MTP draft context independently sized and engaged their own streaming pool — this is the thing the last commit added. A real chat completion ran end-to-end: draft acceptance 50% (66/132 tokens), ~107 tok/s generation, clean completion, no errors.
3. VRAM / RAM behavior
262K ctx, 27B model, streaming + MTP (from
nvidia-smipolled through the whole run):This config is right at the card's limit even with streaming — 262K context on a 27B model on a single 32GB consumer GPU, which is the point of the feature, but it leaves very little headroom.
65,536 ctx, q8_0/q8_0, Qwen3.8-27B, server startup + 1 completion (from the earlier session, before the dead-code/VRAM-pool fix):
Note: this measurement predates the fix that removed an extra full-size
cudaMallocthe old (now-deleted) resident-cache code was silently reserving on top of the pinned host buffer. Actual device VRAM savings with the current code should be better than shown here — not yet re-measured on hardware.4. Throughput
llama-bench, Qwen3.8-27B, 512 ctx / 128 gen / 3 reps:Consistent with expected PCIe transfer overhead for the resident-page/transfer-ring mechanism.
5. Known limitations / not yet verified
exit -9(a ~40 GiB host logits buffer whennum_batches > 1for large-vocab models), not a streaming-specific bug, but not cleanly reproduced/fixed here either.--kv-stream auto's constants (10% resident, 2048 floor, 8192 MiB ceiling) are defensible defaults, not tuned against A/B throughput data.Test commands
PPL parity sweep (produces the table in section 1):
262K / MTP validation:
VRAM sampled throughout with
nvidia-smi --query-gpu=power.draw,memory.used,memory.total --format=csv,noheader,nounits -l 1.Throughput comparison:
AI Usage: yes