Skip to content

[Bugfix] Size the extensible KV cache from UMA-corrected free memory - #65413

Merged
njhill merged 6 commits into
vllm-project:mainfrom
aijanai:amassidda/extensible-kv-uma-free-memory
Nov 3, 2026
Merged

njhill merged 6 commits into
vllm-project:mainfrom
aijanai:amassidda/extensible-kv-uma-free-memory

Conversation

@aijanai

@aijanai aijanai commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Fixes #65378. On integrated (UMA) GPUs the extensible KV cache (#56492) sized itself from raw cudaMemGetInfo, which does not count reclaimable page cache as free, so it undersized the KV cache by up to 84% on a DGX Spark (GB10) and to zero blocks in disaggregated serving. This reads free memory through one helper with the same psutil correction the classic sizing path already applies (#35920).

Claims

  • On GB10 the extensible KV cache now gets the KV cache the classic path finds: 1302 blocks / 67.6 GiB instead of 216 blocks / 11.2 GiB for Qwen3.8-27B NVFP4 at gpu_memory_utilization=0.8, and no longer fails with "No available memory for the cache blocks" in P/D.
  • Discrete GPUs are unchanged: the helper returns torch.accelerator.get_memory_info as is when is_integrated_gpu() is false.
  • No new configuration; --no-enable-extensible-kv-cache is no longer needed as a workaround on UMA parts.

Validation

All runs on one GB10 (DGX Spark, 119.7 GiB unified memory, CC 12.1) or one RTX PRO 6000 (discrete, 96 GB), image vllm/vllm-openai:nightly at 40941f2 (the extensible code it ships is identical to this branch's base), with this PR's mem_utils.py and extensible_kv_cache.py copied over the installed package for the "with fix" rows.

End to end, vllm serve nvidia/Qwen3.8-27B-NVFP4 --quantization modelopt_fp4 --kv-cache-dtype fp8 --max-model-len 4096 --gpu-memory-utilization 0.8 --max-num-seqs 32 --speculative-config '{"method":"mtp","num_speculative_tokens":3}', V2 model runner, extensible KV cache at its default:

Device Load strategy Fix Extensible KV cache after warmup Classic path (Available KV cache memory)
RTX PRO 6000 (discrete) prefetch no 560 blocks, 29.1 GiB, starts 52.95 GiB
GB10 prefetch no 216 blocks, 11.2 GiB (42,130 tokens) 71.04 GiB
GB10 default no 302 blocks, 15.7 GiB 71.55 GiB
GB10 prefetch yes 1302 blocks, 67.6 GiB (253,952 tokens) 70.80 GiB

Before the server started on GB10, cudaMemGetInfo reported 59-81 GiB free while psutil.virtual_memory().available reported 114-116 GiB: the difference is page cache, from this load and earlier runs on the node. In disaggregated P/D (NIXL, same model, MTP k=3) the larger transient peak (7.28 GiB vs 1.71 GiB) took the same deficit below zero: both workers failed with 0 KV cache blocks (0.0 GiB) fit within the requested 95.71 GiB / ValueError: No available memory for the cache blocks.

Device allocations reclaim the page cache. After filling the page cache by reading the checkpoint, committing through the extensible cache's own _VirtualBuffer (cuMemCreate + cuMemMap) in 1 GiB steps reached 106 GiB, 1.8x the 59.2 GiB cudaMemGetInfo reported free, with no error; psutil "cached" fell from 56.4 to 4.1 GiB in step with the commits. So the psutil figure is the one an allocation can actually obtain.

New test fails before the fix, passes after (GB10):

$ pytest tests/v1/worker/test_extensible_kv_cache.py::test_measured_sizing_counts_reclaimable_memory_on_integrated_gpu
# nightly 40941f2c4b as shipped
>       assert num_blocks == 80
E       assert 0 == 80
tests/v1/worker/test_extensible_kv_cache.py:285: AssertionError
1 failed in 3.87s
# with this PR
1 passed in 3.89s

tests/utils_/test_mem_utils.py and tests/v1/worker/test_extensible_kv_cache.py with the fix on GB10: 26 passed, 1 failed. The failure, test_memory_profiling (asserts 256 MiB of non-torch memory, measures ~269-273 MB), also fails on the unmodified nightly with the upstream test file, 2 out of 2 runs, so it is a pre-existing UMA flake unrelated to this change. Both files are already run by CI (pytest -v -s v1/worker, pytest -v -s utils_ in .buildkite/test_areas/misc.yaml).

Details

Root cause. MemorySnapshot.measure() (vllm/utils/mem_utils.py) replaces cudaMemGetInfo's free memory with psutil.virtual_memory().available on integrated GPUs, and the extensible cache receives that as init_free_memory. But committable_blocks() and measure_kv_cache_blocks() in vllm/v1/worker/extensible_kv_cache.py re-read raw torch.accelerator.get_memory_info. The sizing in measure_kv_cache_bytes() is min(requested - non_kv_used, free + committed - margin) - transient_peak with non_kv_used = init_free - free - committed: with the raw figure, page cache counts as engine memory in the first term and is missing from the headroom in the second. In the P/D failure the headroom term alone was already negative: 10.46 + 0.73 - 5.46 - 7.28 = -1.55 GiB. Warmup was capped by the same raw read (only 0.73 GiB committed).

Fix. Move the existing correction into device_memory_info(device) in mem_utils and use it from MemorySnapshot.measure() and the three extensible call sites (committable_blocks, measure_kv_cache_blocks, and the free-memory read when releasing for a weight update). The one remaining get_memory_info call in the extensible cache reads only the total. Main-code change is ~30 lines; the rest is tests.

Alternative considered. Turning the extensible cache off on integrated GPUs in _resolve_extensible_kv_cache would also avoid the failure, but would hide the inconsistency and drop the feature on UMA parts. The reclaim measurement above shows the corrected figure is safe to size from, so this PR fixes the measurement instead.

Tests. The new tests patch current_platform.is_integrated_gpu, psutil and torch.accelerator.get_memory_info: CI has no integrated GPU, and these stand in for the hardware/OS readings rather than for vLLM objects. One covers the helper (integrated vs discrete), one covers the extensible sizing end to end through measure_kv_cache_blocks with the readings from this report.

Related. #55828 (open) sizes the KV cache from the process's own memory via NVML with a fallback to the device-wide delta; on GB10 that fallback would hit the same raw reading, so the two changes should agree on device_memory_info. #65204 / #65225 (granule rounding with offload connectors) is a different cause with the same error message.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

🤖 Generated with Claude Code

On integrated GPUs (DGX Spark, GH200, Jetson) cudaMemGetInfo does not count reclaimable OS memory (page cache, buffers) as free, although a device allocation reclaims it. MemorySnapshot already corrects for this with psutil, but the extensible KV cache read torch.accelerator.get_memory_info directly in committable_blocks, measure_kv_cache_blocks and the weight-update release check, so after the weights had been read through the page cache (e.g. safetensors_load_strategy=prefetch) the measured headroom was a fraction of the real one. On a 119.7 GiB DGX Spark with a 27B NVFP4 model this sized the cache to 216 blocks instead of the 71 GiB the classic path finds, and to zero blocks in disaggregated serving, where the larger transient peak exhausted the headroom term. Commit via cuMemCreate/cuMemMap on the same part reclaimed 106 GiB with 59 GiB reported free, so the psutil figure is the right one there too.

Move the correction into a shared device_memory_info() helper in mem_utils and use it from MemorySnapshot.measure and the three extensible KV cache call sites.

Fixes vllm-project#65378

Signed-off-by: Alberto Massidda <amassidda@nvidia.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mergify mergify Bot added the bug Something isn't working label Nov 3, 2026
@aijanai
aijanai marked this pull request as ready for review November 3, 2026 15:35
@aijanai
aijanai requested a review from njhill as a code owner November 3, 2026 15:35

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Signed-off-by: Nick Hill <nickhill123@gmail.com>

@njhill njhill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aijanai! fyi I added a small code simplification.

@github-project-automation github-project-automation Bot moved this to Ready in NVIDIA Nov 3, 2026
@njhill njhill added the ready ONLY add when PR is ready to merge/full CI is needed label Nov 3, 2026
@njhill

njhill commented Nov 3, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

✅ @aijanai, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • Run commands test your branch with every commit currently on its upstream target branch. If it is behind, the bot merges the target branch into it first (pull before pushing again). Append --allow-stale to a run command to test an outdated branch as is, at your own risk.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #99614 for commit 7bce154a3825.

ℹ️ This PR is 13 commits behind upstream main. Merged upstream main into this branch as 7bce154a3825 before starting CI. Pull the branch before pushing more changes.

@mergify

mergify Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @aijanai.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Nov 3, 2026
…kv-uma-free-memory

# Conflicts:
#	vllm/v1/worker/extensible_kv_cache.py

Signed-off-by: Nick Hill <nickhill123@gmail.com>
@njhill

njhill commented Nov 3, 2026

Copy link
Copy Markdown
Member

/ci retry

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

✅ The previous CI build is still running: https://buildkite.com/vllm/ci/builds/99614

@mergify mergify Bot removed the needs-rebase label Nov 3, 2026
@njhill

njhill commented Nov 3, 2026

Copy link
Copy Markdown
Member

/ci retry

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

✅ No failed, timed-out, or expired jobs need retrying in Buildkite CI #99614.

@njhill

njhill commented Nov 3, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #99713 for commit 9e768ea65cd3.

ℹ️ This PR is 5 commits behind upstream main. Merged upstream main into this branch as 9e768ea65cd3 before starting CI. Pull the branch before pushing more changes.

@njhill
njhill merged commit 260b0c6 into vllm-project:main Nov 3, 2026
281 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in NVIDIA Nov 3, 2026
khluu added a commit to atalman/vllm that referenced this pull request Nov 4, 2026
Picks up the main fixes for the fusion E2E mixed-precision test (vllm-project#65495),
the DeepSeek-V4.1 _Mxfp8Linear test (vllm-project#63921) and UMA KV cache sizing on
DGX Spark (vllm-project#65413).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: khluu <khluu000@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working nvidia ready ONLY add when PR is ready to merge/full CI is needed

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: Extensible KV cache undersizes the KV cache on integrated (UMA) GPUs, down to 0 blocks: free memory read with raw cudaMemGetInfo

3 participants