Skip to content

[Perf][Core] Skip the null head of sliding-window block tables - #61724

Merged
njhill merged 6 commits into
vllm-project:mainfrom
Juntian777:perf/swa-null-head-skip
Oct 15, 2026
Merged

njhill merged 6 commits into
vllm-project:mainfrom
Juntian777:perf/swa-null-head-skip

Conversation

@Juntian777

@Juntian777 Juntian777 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

A sliding-window KV group keeps one null block per out-of-window block, so a long request's block table is almost all null (8,183 of 8,192 entries for a 128K-token prompt with a 128-token window and 16-token blocks). KVCacheBlocks.get_unhashed_block_ids_all_groups and SingleTypeKVCacheManager.free walked every entry in Python. This PR skips that null head.

  • null_head_len() finds the leading null run by binary search and checks it with one list.count of the shared null block. If the null blocks are not one contiguous head, it falls back to a scan.
  • get_unhashed_block_ids_all_groups and pop_blocks_for_free (so both free and the deferred-free path) start after the head. free_blocks skips any other null blocks; the null block's ref_cnt is not maintained (see BlockPool.__init__).
  • No behavior change: the same block ids are collected, and the same blocks reach the free queue in the same order.

Not a duplicate: I searched open PRs for null block sliding window free, null_block free_blocks, sliding window null blocks and get_unhashed_block_ids. #42276 and #55818 change SWA eviction and allocation, not these walks.

Where it matters

The cost is paid per sliding-window group, on the scheduler's critical path, each time a request is admitted with async KV loads or freed. It shows up when GPU steps are short and requests turn over quickly, as in DeepSeek V4.1 Flash, WideEP DEP16 decode (GB300, 64 requests/rank, 169.5K-token prompts):

  • A decode step takes ~22 ms on the GPU, and all 16 DP ranks wait for the slowest rank's scheduler.
  • Each rank admits and frees ~9 long requests per second.
  • Each request has 8 sliding-window groups x 5,298 blocks, 5,292 of them null.
Scheduler CPU per request main PR
free 2.0 ms 0.5 ms
async-KV admission (135K prompt) 0.73 ms 0.47 ms

End to end, output throughput went from 8,892 to 9,025 tok/s/GPU (+1.5%).

Microbenchmark

One request on one sliding-window group (window 128, block size 16): KVCacheBlocks.get_unhashed_block_ids_all_groups (collect) and SlidingWindowManager.free (free). Median of 300 calls on a Grace CPU, main (8530aed2b7) vs this PR, consistent across two runs.

Prompt tokens Blocks (null) Collect, main / PR (us) Free, main / PR (us)
32K 2,048 (2,039) 54.1 / 9.7 92.9 / 10.8
128K 8,192 (8,183) 214.9 / 34.8 370.6 / 35.8
512K 32,768 (32,759) 867.5 / 135.9 1471.7 / 137.5

Test Plan

pytest tests/v1/core/test_kv_cache_utils.py tests/v1/core/test_single_type_kv_cache_manager.py tests/v1/core/test_prefix_caching.py -q
pre-commit run --files <changed files>

New test test_null_head_len: empty, no-null, all-null, contiguous-head and non-contiguous tables, plus get_unhashed_block_ids_all_groups on two groups.

Test Result

312 passed. Pre-commit (ruff, format, typos, mypy) passes.


AI assistance (Claude Code) was used to write and benchmark this change.

🤖 Generated with Claude Code

A sliding-window block table keeps one null block per out-of-window block,
so for long requests it is almost all null. Find that head by binary search
and skip it when collecting unhashed block ids and when freeing a request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Juntian Liu <Juntianl777@gmail.com>

@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.

@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 @Juntian777!

Do you know whether this change by itself made noticeable difference e2e?

Comment thread vllm/v1/core/block_pool.py Outdated
Comment thread vllm/v1/core/block_pool.py Outdated
Comment thread vllm/v1/core/single_type_kv_cache_manager.py Outdated
Address review: move the null-head trim from free() into
pop_blocks_for_free() (new trim_null_head(), no copy when there is no
null head) so the deferred-free path benefits too, and trim the residency
row HiSparseResidentManager returns. touch() skips the null block like
free_blocks() does, which drops its now-redundant is_null check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Juntian Liu <Juntianl777@gmail.com>
@Juntian777

Copy link
Copy Markdown
Contributor Author

@njhill Thanks for the review comments! I've addressed them and added the E2E improvement numbers to the summary.

This happens with DSV41F wideEP because it's GPU computation too fast, and larger DEP is more likely to have CPU stragglers at high concurrency.

Comment thread vllm/v1/core/kv_cache_manager.py Outdated
@njhill

njhill commented Oct 15, 2026

Copy link
Copy Markdown
Member

/ci run

@njhill njhill added the ready ONLY add when PR is ready to merge/full CI is needed label Oct 15, 2026
@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #95212 for commit 4bcbd20c5111.

@github-actions

Copy link
Copy Markdown

✅ @Juntian777, 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.

@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 @Juntian777

@mergify

mergify Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Hi @Juntian777, the pre-commit checks have failed. Please run:

uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

@njhill

njhill commented Oct 15, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #95240 for commit e076e3f15992.

@njhill
njhill merged commit 19b0efc into vllm-project:main Oct 15, 2026
148 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kv-cache-manager ready ONLY add when PR is ready to merge/full CI is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants