Conversation
njhill
force-pushed
the
njhill/extensible-kv-cache
branch
from
August 2, 2026 23:53
bbd90e8 to
34382e3
Compare
njhill
force-pushed
the
njhill/extensible-kv-cache
branch
from
August 4, 2026 19:05
ab371fd to
aaee56d
Compare
njhill
force-pushed
the
njhill/extensible-kv-cache
branch
2 times, most recently
from
August 4, 2026 19:41
fab567e to
a2bfba8
Compare
Member
Author
|
/ci run |
|
✅ Triggered Buildkite CI #82367 for commit |
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
njhill
force-pushed
the
njhill/extensible-kv-cache
branch
from
August 6, 2026 01:31
a2bfba8 to
d7f10f2
Compare
Member
Author
|
/ci run |
|
✅ Triggered Buildkite CI #82597 for commit |
LucasWilkinson
force-pushed
the
lwilkinson/kv-layout/core-standardize
branch
from
August 6, 2026 02:06
d458dc6 to
040cb42
Compare
njhill
force-pushed
the
njhill/extensible-kv-cache
branch
from
August 6, 2026 17:58
d7f10f2 to
4961f67
Compare
njhill
force-pushed
the
njhill/extensible-kv-cache
branch
from
August 6, 2026 20:16
4961f67 to
5516aaf
Compare
Member
Author
|
/ci run |
LucasWilkinson
force-pushed
the
lwilkinson/kv-layout/core-standardize
branch
from
August 6, 2026 20:49
b6fcd8e to
296e5f6
Compare
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
Assisted-by: OpenAI Codex Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Add a block-size-aware state-count helper and use it when constructing kernel block views. Keep num_states as the manager-block convenience property and preserve one recurrent state for Mamba. Assisted-by: OpenAI Codex Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Member
Author
|
/ci run |
|
✅ Triggered Buildkite CI #84556 for commit |
Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
The V2 runner's decode-shaped autotune dummy run reaches trtllm_batch_decode_mla, which MRV1's warmup never exercised; its 21-tactic sweep faults on the dummy decode batch (zeroed block tables). Keep the op on its heuristic fallback -- exactly main's behavior -- until the sweep is made safe. Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThBqMtrBGwXZnMYHky55on
…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 #51718 single-allocation model, substantially simplified by it: - The shared allocate_and_reshape_kv_cache gains a reserve(size, num_segments) hook that stands in for the plain torch.zeros backing allocation; one VMM buffer backs the whole KV cache. - Buffer segmentation falls out of the KVCacheTensor stride model: num_segments = size / (num_blocks * block_stride), i.e. one segment per contiguous run of blocks (1 for block-outermost layouts, one per layer region under layer-compact layouts), 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. 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>
- 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. - Mooncake likewise registers the byte span the layer views cover, and requires the DMA-BUF registration path (WITH_NVIDIA_PEERMEM=0): nvidia-peermem cannot pin VMM-backed memory. - Extensible KV cache unit tests against the single-allocation surface (kv_cache_num_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>
Three ROCm-driven fixes; no behaviour change on CUDA. Granule floor. ROCm reports a 4 KiB minimum allocation granularity where CUDA reports 2 MiB (measured 4 KiB on ROCm 7.2.2, 7.13 and 7.14 alike), so the per-granule bookkeeping in _VirtualBuffer would hold 26.2M entries for a 100 GiB cache -- ~1.9 GB of Python objects and ~2.8 s just to build the set. Commit in units of at least 2 MiB instead, always a multiple of what the driver requires. On CUDA this is a no-op; a 1 TiB cache stays at 524k granules / 33 MB. Disable the feature on HIP runtimes before 7.12. hipMemSetAccess there rejects a validly mapped range once a reservation holds several differently-sized allocations, which is exactly how a growable KV cache is committed. Without this, an AMD user passing --enable-extensible-kv-cache got `GPU driver error 1: invalid argument` at engine init; now the engine warns and falls back to the standard allocator. This is ROCm's rocm-systems#2516, fixed by rocm-systems#2451 -- merged to develop on 2026-01-26 and present in release/therock-7.12 and later, but absent from every release/rocm-rel-7.x branch, so no shipped ROCm below the TheRock line has it. Gating on version rather than probing is deliberate: the defect is order-dependent (measured 19/20 failures at 8 mapped segments, but 0/20 at two), so a probe cheap enough to run at startup passes on affected runtimes and would give false assurance. Return early from commit() before releasing. With defragment=True and a request at or below the committed size, the old ordering released the mapping and then re-committed the smaller size, discarding live KV data. Not reachable today, but elastic EP re-runs warmup -- and hence ensure_blocks -- while the cache holds live data, so the failure mode would be silent corruption rather than a crash. Pinned by a test. Verified: 25 units on CUDA and ROCm. CUDA e2e unchanged across default, extensible, sleep/wake and deferred-connector paths, with byte-identical output and the gate inert. On ROCm 7.2.2 the engine now falls back gracefully instead of failing to start. On ROCm 7.13 the gate opens (runtime_version (7, 13), no reason reported) and the mapping sequence that fails 0/20 on 7.2.2 passes 20/20. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
Builds on the extensible KV cache: since it commits physical pages only after warmup, it can size the cache from memory measured at that point rather than from a profiling estimate plus a hand-tuned safety fraction. That is more accurate, so make it the default where the driver supports it and stop reserving an unused fraction of the device for it. Utilization ----------- `gpu_memory_utilization` gains a default of 1.0 when the extensible KV cache is active: the unused fraction exists to cover what profiling mis-estimates, and measured sizing does not need it. Its margin is the explicit post-warmup buffer instead. It remains a fraction of the device's *total* memory in all cases, never rescaled against free memory. Rescaling would quietly break the documented case of two instances at 0.5 each: the second would see half the memory free and take a quarter of the device. Two consequences had to be handled: - The budget can now exceed memory that was free when the initial snapshot was taken, since this worker's own context and communication buffers are already resident. `post_warmup_available` therefore clamps to what is measured free, and `request_memory` tolerates the shortfall only for the extensible path, where exceeding free memory over-reserves address space rather than over-committing pages. - Falling back to profiling-based sizing has to undo the raised default, so `CacheConfig.disable_extensible_kv_cache` restores it in one place, used by both the config-level and driver-level fallbacks. Sharing a device ---------------- Refusing to start whenever another process holds GPU memory is too strict; a process fitting inside this instance's headroom is not necessarily a problem. But the default assumes exclusive use, so sharing should be deliberate: warn when utilization was set explicitly, raise when it was left to default, and keep raising when the requested fraction is not free at all. Detecting a resident process needs a process query, not a memory reading: by the time a worker reads free memory it has created its own context and comm buffers, so used memory is not attributable. Added `Platform.get_foreign_device_processes`, implemented for NVML, returning None where a platform cannot enumerate processes -- treated as unknown, so missing telemetry never refuses startup. ROCm inherits the None default; `amdsmi_get_gpu_process_list` is the equivalent. Processes sharing this process's session are excluded, since the engine process may itself hold a context on device 0 from capability queries. Post-warmup buffer ------------------ The buffer absorbs what the measurement cannot see: fragmentation, activation peaks at shapes warmup did not exercise, workspaces that grow at runtime. Those scale with the workload rather than the device, so it is a fraction of the profiled transient activation peak with an absolute floor, rather than the previous fixed 150 MiB -- which was ~0.08% of a 185 GiB device and 2.5% of a 6 GiB one. The fraction is not yet fitted; it wants a sweep against a long-running workload, which is the only thing that exercises what it covers. Explicit requests are mandatory ------------------------------- `enable_extensible_kv_cache` becomes tri-state so "requested" is distinguishable from "defaulted". An explicit request fails startup where it cannot be honored, rather than silently sizing the cache the less accurate way; left unset it falls back. Since the default is now on, the incompatible cases are detected at config time -- KV connectors on the V1 runner, and sleep mode alongside a KV connector -- so they disable the feature before workers spawn instead of failing during startup or at first sleep. The gate also excludes non-CUDA/ROCm platforms locally, without a collective RPC. Both tri-state fields follow the sentinel-default pattern CacheConfig already uses for `block_size`, and their derived `user_specified_*` flags are excluded from compute_hash so the compilation cache stays independent of how the values were supplied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Gate ordering (CI build 82729) ------------------------------ The driver gate originally resolved in the engine after worker memory profiling, and `disable_extensible_kv_cache` only reset the engine's config copy. Workers therefore profiled against the extensible defaults (util=1.0) even on platforms taking the fallback path, and the estimate-based sizing consumed the whole device: every AMD CI job (ROCm 7.2 fails the VMM version gate) OOMed with ~100% of VRAM allocated to KV cache. Each worker now resolves the gate against its own driver before taking the memory snapshot, and non-CUDA/ROCm platforms are excluded at config time (before workers spawn) since CPU sizing reads the same utilization value. The post-warmup buffer also gains a proportional component (2% of the KV budget, still capped at 10%): some allocations happen only after warmup (first spec-decode rejection batch, multimodal frame staging), and the 150 MiB floor was not covering them on devices where the old default left ~8% idle. Signed-off-by: Nick Hill <nickhill123@gmail.com>
Sizing the KV cache from measured post-warmup memory removes the reason
most of these caps existed. They were set to leave slack for memory the
profiling estimate could not see -- NCCL buffers, CUDA graphs, quantized
scratch space -- which measurement now accounts for directly. Worse, the
caps have become actively harmful: measured sizing subtracts an explicit
buffer where the estimate relied on the unused fraction as an implicit
margin, and it counts CUDA graph memory that the estimate omits unless
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS is set, so a low cap can leave no
room for a cache at all. Three CI jobs failed exactly that way.
Removed from 84 test files (116 call sites plus constants, helper
parameters and CLI arguments that existed only to carry the value).
Retained where the value is load-bearing:
- The argument itself is under test: test_gpu_utilization,
test_kv_cache_utils, test_utils, test_config_utils, test_mp_reducer,
test_nixl_rocm_gpu_mem_diag, test_lmcache_integration, and the torchrun
examples, which deliberately vary it per rank.
- Deliberate device sharing, where the default cannot apply because
another engine is alive: test_logprobs (module-scope fixtures),
test_custom_offline, test_spec_decode, test_batch_invariance,
test_speculators_correctness, test_mtp_parallel_load, test_shutdown,
test_elastic_ep, test_transcription_api_correctness, and
test_rocm_compressed_tensors_w4a16 ("shared nodes").
- Synthetic configs in unit tests that never allocate, left untouched.
Adjusted rather than removed:
- test_full_cudagraph llm_pair: the fixture holds two engines (full and
piecewise cudagraph modes) alive concurrently at 0.43 each; the sweep
misread them as sequential. Reinstated (CI build 82729).
- test_logprobs_mode / test_prompt_logprobs_mode: 0.05 of a 16 GiB device
is 0.8 GiB, all of which goes to weights (0.52) and CUDA graphs (0.23).
0.1 keeps the sharing intact while leaving the cache room to exist. The
other shared-device values are an order of magnitude above the level at
which this bites, so they stand.
- test_encoder_instance_zero_kv_cache: 0.7 left the consumer 2.12 GiB
against the 2.0 GiB minimum for max_model_len. ec_producer's 0.01 stays,
being what forces the zero-block assertion.
- test_compile_correctness[test_setting3]: 0.98 left 0.52 GiB against a
0.66 GiB minimum; removing the cap raises the budget, the default now
being 1.0.
conftest's ROCm memory-settle helper read CacheConfig.gpu_memory_utilization
as a class attribute, which is now the unresolved sentinel, so
`1.0 - None` raised on every ROCm vllm_runner test that did not pass the
argument. It now takes the concrete default.
Verified: 1540 tests collect across the changed files (the six that do not
are missing optional deps, datasets and lm_eval, unrelated to this change).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThBqMtrBGwXZnMYHky55on
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Measured post-warmup KV sizing only accounts for memory that warmup actually allocates: torch's allocator retains warmup's high-water segments, and the measurement reads free memory after warmup. The multimodal encoder and the encoder cache budget were exercised only inside memory_profiling, which drops those allocations with empty_cache() on exit; warmup itself was decoder-only, so the post-warmup measurement credited that memory to the KV cache and the first runtime encoder batch OOMed inside the vision tower. Re-run the worst-case encoder pass in warmup_kernels with the other scheduler-realistic steps. It runs unconditionally since it also JIT-warms the vision tower's kernels (removing the first-vision-request spike on any sizing path). On small GPUs the encoder worst case may not fit alongside everything else warmup keeps resident (it fit during profiling, which ran it nearly alone). Rather than leaving a latent runtime OOM, halve the per-step encoder token budget until the batch fits (floored at one worst-case item) and hand the reduced budget to the scheduler via CompilationTimes, so it never batches more encoder tokens than warmup proved to fit. The extensible KV cache is V2-only, so the V1 runner needs no equivalent. Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThBqMtrBGwXZnMYHky55on
Member
Author
|
/ci run |
|
✅ Triggered Buildkite CI #84683 for commit |
Contributor
|
Documentation preview: https://vllm--50779.org.readthedocs.build/en/50779/ |
Member
Author
|
Replaced by #56492. |
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 #51718.This is the productization of the demonstration #47363 from @zhuohan123.
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 #51718 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.