Repository navigation
feat(kvcache): host-RAM KV tier for sparse-attention models (--kv-host-pages) - #499
alvarorsouza-arch wants to merge 3 commits into
Conversation
- mirror_store_quant (new Triton kernel, graph-safe UVA): the write-through mirror now copies e4m3 codes + fp32 scales read from the pool AFTER quantization (the pool is the source of truth). The bf16-era mirror corrupted fp8 pages silently on rehydrate. - mirror_scales host bank + fused-copy descriptor extended with the scale banks (per-bank row sizes). - set_select_width takes max_bs: the whole batch selection must fit the physical pool by construction (fixes assert-crash on multi-row decode with small pools). - measured: 299,648 logical tokens with kv-cache-dtype fp8 + kv-host-pages, needle at 253k tokens answered correctly (~1026 t/s), quality suite identical to baseline.
|
Update pushed (20f3b23): fp8-aware write-through mirror + batch-safe selection cap. What changed and why:
Measured on RTX 4070 Ti SUPER 16 GB, composing this PR with #354: 299,648 logical tokens, needle at 253k answered correctly (246 s end to end, ~1026 t/s), quality suite identical to baseline (39/40 QA, 30/30 cloze). Decode costs about 23% against pure-VRAM fp8 (29 vs 37.7 t/s) due to the mirror write-through; noted in the docs. Validation of the mirror fix: full GPU pool flush, then a needle read of the rehydrated pages: bit-correct. |
…ite-pages From the Sep 15-16 debugging sessions (boolean-indexing anti-pattern family): - the indexer buffers must use the pool COMPUTE dtype (bf16), never the e4m3 store dtype of an fp8 KV pool, or CUDA-graph capture fails on tl.dot - ensure_write_pages takes token_to_req for the fixed-shape kernel path
…ention models (--kv-host-pages) # Conflicts: # python/freetoken/attention/qsa_sparse.py # python/freetoken/kvcache/__init__.py # python/freetoken/kvcache/qsa_pool.py
…cales they cannot mirror Upstream FlashML-org#499/FlashML-org#525 mirror KV codes plus the fp8 pool's per-token _scale_buffer; the nvfp4 pool (FlashML-org#408) keeps per-block scales in _block_scale_buffer, which neither the offloader nor the prefix tier copies, so --kv-host-pages or FT_PREFIX_HOST would rehydrate codes without scales. Both constructors now raise a clear error naming the knob; the GDN-snapshot-only tier (FT_GDN_HOST_TIER) never reads the KV pool and stays usable. Assisted-by: Claude Fable 5.1
…ention models (--kv-host-pages) # Conflicts: # python/freetoken/attention/qsa_sparse.py # python/freetoken/kvcache/__init__.py # python/freetoken/kvcache/qsa_pool.py # Conflicts: # python/freetoken/attention/qsa_sparse.py # python/freetoken/kvcache/__init__.py # python/freetoken/kvcache/qsa_pool.py
…cales they cannot mirror Upstream FlashML-org#499/FlashML-org#525 mirror KV codes plus the fp8 pool's per-token _scale_buffer; the nvfp4 pool (FlashML-org#408) keeps per-block scales in _block_scale_buffer, which neither the offloader nor the prefix tier copies, so --kv-host-pages or FT_PREFIX_HOST would rehydrate codes without scales. Both constructors now raise a clear error naming the knob; the GDN-snapshot-only tier (FT_GDN_HOST_TIER) never reads the KV pool and stays usable. Assisted-by: Claude Fable 5.1 (cherry picked from commit 4bb35fc)
|
I needed the same capacity for a 1M-token window on a 24 GB card and took a different route. I'm posting it so the two designs can be compared, not as a competing PR. Where the K/V lives. With Measured on an RTX 3090 Ti 24 GB, 256 GB DDR5, PCIe 4.0 x16,
The trade-off against this PR: reading in place has no hit rate to warm and nothing to evict, but every decode pays PCIe for its whole selection, where your LRU keeps hot pages on the device. At 1M on 24 GB the K/V alone is 24 GiB, so there was no device pool to cache into, and reading in place was what fit. The branch: https://github.com/YevheniiKotyrlo/FreeToken/tree/feat-kv-host-placement (tests in |
|
Thanks for posting the branch and the measurements. Reading the two designs side by side is useful. The trade-off is clear once the hardware is held constant, and ours isn't: our #499 bench ran on a 4070 Ti S, yours on a 3090 Ti, so the absolute t/s should not be read as a head-to-head. The real difference is where the cost lands. #499 keeps hot pages on the device through an LRU write-through mirror, so decode stays fast when there is VRAM to cache into (we saw about 23% over pure-VRAM fp8 on the 4070 Ti S). Your So the two are different points in the same trade-off rather than one superseding the other. I won't push #499 as a competing path; the in-place design is what fits the no-VRAM-to-spare window, and I'm happy for the prefill staging and fold to be carried into main if the maintainers want it. I'll close #499 and leave the design comparison here. Good measurements on the fold accumulator too. |
|
Thanks for the comparison. The in-place pool is now its own PR, #672. |
What
Adds an opt-in second KV tier in pinned host RAM for models whose attention only re-reads a small selected subset of pages per step (Qwen3.8-Flash-Next QSA:
indexer_budget=2048). The pinned mirror is the backing store; the GPU pool becomes an LRU cache of physical slots over the logical page space.New flag:
--kv-host-pages N(default 0 = off; zero behavior change when off).Why this works for sparse attention
Every attend step first runs the indexer (top-k block selection over a compressed slab, fully GPU-resident) and only then reads the selected pages. Pages are fetched host→device after selection, bounded at ~34-520 pages/request/layer (fixed shape, CUDA-graph safe). Cold pages never cross PCIe; selected pages do, once.
Design
store_kvis mirrored to a pinnedHostBankover UVA, so a GPU slot is always drop-clean; eviction is a pure rebind (no write-back, no dirty tracking)lru_ensure(device-side LRU, fixed shapes, graph-safe) +fast_index_copy_multi_jit(one launch copies K+V of a page across all 12 QSA layers)qsa_sparse_paged_attentionruns unmodifiedensure_write_pagesuses a single fixed-shape Triton launch (stride samples + one boundary sample per request run). The first version used boolean-mask indexing andtorch.unique, both data-dependent shapes that hide device-to-host syncs per layer and serialize the prefill-overlap pipeline (the same pattern later found inmoe/offload_cache.py)compact_selected_pagesgained a drop counter;max_sel_pagessized from2*block_topk + 8instead of the contiguous-selection estimateMeasured (RTX 4070 Ti SUPER 16 GB, EPYC 7K62, 251 GB RAM, PCIe gen4)
Config A (fp8 KV via #354 composed):
--memory-ratio 0.92 --max-prefill-length 4096 --moe-cache-size 1024 --ple-backend pinned --max-running-requests 1 --enable-special-token-ckpt --kv-cache-dtype fp8 --kv-host-pages 5000 --num-pages 1635Composes with #354 (fp8 KV): fp8 pages halve fetch bytes and double the resident pool.
Known limitations
dsv4_sparseFiles
kvcache/kv_host_offload.py,kernel/triton/qsa/offload.pyattention/qsa_sparse.py,kvcache/qsa_pool.py,kvcache/__init__.py,core.py,engine/config.py,engine/engine.py,server/args.pyTest protocol
All numbers measured end-to-end over the OpenAI API. The quality suite (40 known-answer QA + 30 cloze at temperature 0) scored identical to the non-offload baseline on the same server. Happy to add unit tests for the offloader where maintainers prefer.