[Core] Extensible (growable) KV cache - #50779
Draft
njhill wants to merge 5 commits into
Draft
Conversation
…umbing Ported from the extensible-kv-cache branch (originally #47363): VmmDriver (CUDA cuMem* / ROCm hipMem* bindings with probe), ExtensibleTensor / ExtensibleKVCacheBuffers (grow-only per-segment prefix commits over a stable VA reservation), enable_extensible_kv_cache config/CLI plumbing, and the register_kv_caches views-are-authoritative contract note. Co-authored-by: Zhuohan Li <zhuohan123@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho2gVXA5r7PhrM2sk8oJUn Signed-off-by: Nick Hill <nickhill123@gmail.com>
Rework of the extensible-kv-cache V2 integration on the #44458 layout model, substantially simplified by it: - Buffer segmentation is derived directly from KVCacheLayout via a new num_outer_segments() helper (product of the physical dims outer to the block dim), replacing per-backend shape/stride/block-dim probing. - Connector registration views are a plain narrow(0) of the logical [B, H, N, C] per-layer views. - The packed-layout special cases disappear with block_stride. Ported unchanged: measured post-warmup sizing, deferred KV-transfer init, sleep release/recommit, VMM probe/fallback, elastic-EP skip, and encoder-cache profiling reservation. Co-authored-by: Zhuohan Li <zhuohan123@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho2gVXA5r7PhrM2sk8oJUn Signed-off-by: Nick Hill <nickhill123@gmail.com>
…eometry - V1 GPUModelRunner: extensible allocation through the standardized _allocate_and_reshape_kv_cache (VMM buffers segmented per layout), extend_kv_cache, profiling-time encoder cache reservation. - NIXL register_kv_caches and get_kv_cache_block_regions derive all geometry from the registration views instead of untyped storage nbytes: with the extensible KV cache the storage spans the reserved virtual-address capacity, of which only each view's per-segment block prefix is physically backed. Fully-backed storages keep the single per-allocation registration. - Rewrite extensible KV cache unit tests against the layout-based surface (num_outer_segments, narrow, shared allocation path). Co-authored-by: Zhuohan Li <zhuohan123@gmail.com> Co-authored-by: zjy0516 <riverclouds.zhu@qq.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho2gVXA5r7PhrM2sk8oJUn Signed-off-by: Nick Hill <nickhill123@gmail.com>
The V2 encoder profiling path held only one batch of dummy encoder outputs, while at runtime the encoder cache grows to encoder_cache_size tokens of embeddings. KV sizing therefore over-committed for multimodal models. Mirrors the reservation the V1 runner already makes in profile_run(). Co-authored-by: Zhuohan Li <zhuohan123@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
Keep engine/core.py and the two model runners thin by relocating the extensible KV cache machinery to the utility modules that own each concern: - kv_cache_utils: configure_kv_cache (KV sizing + applying the result to the config, previously inline in EngineCore and now needed twice), use_extensible_kv_cache (validation + worker VMM probe), and post_warmup_available_memory. - kv_cache_interface: tensor_num_outer_segments, the per-tensor wrapper that was duplicated in both runners. - extensible_tensor: ExtensibleKVCacheBuilder, replacing the duplicated per-tensor VMM setup, plus ensure_blocks()/extend() carrying the mechanism docs. - gpu/kv_connector: get_deferred_kv_connector, the narrow-then-register step the V2 runner performs after extend_kv_cache. No functional change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
njhill
force-pushed
the
njhill/extensible-kv-cache
branch
from
August 2, 2026 23:53
bbd90e8 to
34382e3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Opt-in growable KV cache (
--enable-extensible-kv-cache) built on the standardized KV cache layout from #44458.vLLM sizes the KV cache from a profiling run, then allocates it up front. Any memory the engine consumes after profiling — CUDA graph pools, the spec-decode logits all-gather, workspace growth — has to be guessed at via
headroom margins, and a wrong guess is either an OOM or wasted HBM.
This change reserves virtual address space for the KV cache capacity but commits physical pages incrementally (CUDA
cuMem*/ ROCmhipMem*):extend_kv_cache()maps the remaining pages under the same base pointer.Because each block keeps a fixed offset within its layout segment and the base pointer never moves, captured graphs and layer views stay valid across the growth — no re-view, no re-capture.
What #44458 buys this
The layout standardization removes most of what made the earlier version of this work invasive. Buffer segmentation is now a pure function of
KVCacheLayout.stride_order(newnum_outer_segments()helper) instead of per-backend shape/stride/block-dim probing; connector registration views are a plainnarrow(0)of the logical[B, H, N, C]view; and the packed /block_stridespecial cases disappear entirely. The integration is ~30%smaller than the equivalent against
main.Views are authoritative, not
storage.nbytes()The one contract change worth reviewer attention. Under an extensible cache the untyped storage spans the reserved capacity, of which only each view's per-segment block prefix is physically backed. Any code deriving block geometry or registration extents from
untyped_storage().nbytes()will compute a size that includes unbacked pages.Updated accordingly, with the contract documented on
KVConnectorBase_V1.register_kv_caches:NixlBaseConnectorWorker.register_kv_caches— collects per-region committed spans and registers those; falls back to the single per-allocation registration when the storage is fully backed. Block-major packing isdetected from view strides rather than
nbytes().get_kv_cache_block_regions(v1/worker/utils.py) — same, which also covers the CPU-offload connector.KV-transfer init is deferred until after
extend_kv_cache, so connectors only ever register committed memory. On the NIXL path, extend also defragments (release + single-chunk recommit per segment): UCX cannot transfer a VMM region spanning multiplecuMemCreatehandles.Relationship to existing work
Contributors: @zhuohan123 (original demo), @ZJY0516 (packed-storage bounding, sleep gate).
Testing
Unless noted, run against this branch at its current head.
Unit —
pytest tests/v1/worker/test_extensible_kv_cache.py tests/utils_/test_extensible_tensor.py tests/v1/engine/test_engine_args.py -v→ 29/29 pass (GB200, aarch64).e2e, 1×GB200, CUDA graphs on — outputs byte-identical to a non-extensible baseline for: standard generation, sleep(level=1)/wake, and
--kv-cache-memory-bytes.e2e, sizing under pressure — Qwen3.5-4B TP4, MTP
num_speculative_tokens=100, chunk 256,gpu-memory-utilization=0.92(a config that OOMs without this change): passes in both eager and cudagraph modes. Sizing 35,277→34,721 tokens (eager), 35,243→34,592 (cudagraph). Functional check 64/43 answers correct.e2e, NIXL P/D disaggregation — Qwen3-0.6B, HND, block 128, gsm8k via P/D proxy:
Model evaluation — the feature is opt-in and does not change model outputs; the byte-identical-output comparison above and the gsm8k table are the evidence for that.
Known gaps
main(Qwen2-VL-2B: 16,384-token budget reserved, 238,148 blocks committed) but has not been re-run on this branch — the cluster battery above has no multimodal configuration. Will re-run before marking ready.commit(defragment=True)or the connector sleep gate; both are exercised only by the e2e NIXL runs. Adding these before ready.hipMem*backend is written but untested on AMD hardware.AI assistance
AI assistance (Claude Code) was used for this change: for the port onto the #44458 layout model, the connector geometry audit, and drafting. I have reviewed every changed line, and ran the tests and cluster validation above
myself.