Skip to content

[Bugfix] Skip non-prefix-cacheable KV groups in the offloading divisibility assert - #55037

Closed
ntheanh201 wants to merge 1 commit into
vllm-project:mainfrom
ntheanh201:clean-pr1-assert
Closed

ntheanh201 wants to merge 1 commit into
vllm-project:mainfrom
ntheanh201:clean-pr1-assert

Conversation

@ntheanh201

@ntheanh201 ntheanh201 commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Native KV offloading (OffloadingConnector) cannot boot on a hybrid model that carries a KV cache group opting out of prefix caching — e.g. a CircularBufferSpec scratch group, a small per-request ring buffer whose block size (4 tokens) is unrelated to the hash granularity. build_offloading_config() asserts divisibility over every group:

AssertionError: tokens_per_block=4 not divisible by tokens_per_hash=1152.
Hybrid models (e.g. Mamba+Attention) need --enable-prefix-caching to align block sizes.

Such a group is exempt by construction: CircularBufferSpec.prefix_cacheable is False, so its blocks are never addressed by block hashes and constraining them by tokens_per_hash is meaningless. The assert fires anyway and the engine dies at init.

resolve_kv_cache_block_sizes() in vllm/v1/core/kv_cache_utils.py — the very function that produces tokens_per_hash — already filters on exactly this attribute, with an or-fallback for when no group participates:

hashing_sizes = [
    block_size
    for group, block_size in zip(groups, group_block_sizes)
    if group.kv_cache_spec.prefix_cacheable
] or group_block_sizes

This patch applies the same idiom in the consumer of that value, so the assert is checked against the same set of groups the hash unit was derived from.

Who this affects upstream. The only producer of CircularBufferSpec in the tree is Qwen4-exp's QSA cache (vllm/models/qwen4_exp/common/qsa_cache.py), which pairs the ring with an MLAAttentionSpec group — exactly the hybrid shape the tests below model. QSA deliberately sizes the ring so it divides the attention block size:

assert self.cache_config.block_size % capacity == 0, (
    f"QSA ring capacity {capacity} must divide the attention block "
    f"size {self.cache_config.block_size}"
)

That keeps the scheduler's LCM well-formed, but it says nothing about the hash unit: tokens_per_hash is the GCD over prefix-cacheable groups, i.e. the attention block size, which the strictly smaller ring never divides. So the assert fires and native offloading cannot start. Any future model carrying a compression or scratch ring lands in the same place.

Changes

  • build_offloading_config(): restrict the divisibility assert to groups whose spec reports prefix_cacheable, falling back to all groups when none do (mirroring the upstream idiom above).
  • New tests, tests/v1/kv_connector/unit/offloading_connector/test_build_offloading_config.py:
    • a FullAttentionSpec (block 1152) + CircularBufferSpec (block 4) group set now builds an offloading config, where stock code raises the assert above;
    • divisibility is still enforced for prefix-cacheable groups — a mamba group outside align mode pins the hash unit to the LCM (48) of the group block sizes, which the attention group's own block size (16) does not divide, and the assert still fires;
    • the all-non-cacheable fallback, where the hash unit falls back to the GCD of every group.

The tests reuse _make_vllm_config() and _full_attention_spec() from the neighbouring test_config.py rather than hand-rolling fixtures.

Test plan

  • New unit tests under tests/v1/kv_connector/unit/offloading_connector/, alongside the existing test_config.py
  • Verified on a live hybrid deployment (GLM-5.3-Flash, KDA+MLA, single node 8xH200) whose scratch group has the same shape as CircularBufferSpec — 4-token blocks against a 1152-token hash unit: the engine boots with KV offloading enabled where stock code crashes at init (3/3 attempts).

Follow-up: #55038 fixes three later crashes in the offloading scheduler that only surface once this lands and such a group is actually scheduled. That branch is this one plus exactly one commit.

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

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@mergify mergify Bot added bug Something isn't working kv-connector labels Sep 2, 2026
ntheanh201 added a commit to ntheanh201/vllm that referenced this pull request Sep 3, 2026
Follow-up to the offloading divisibility-assert fix (vllm-project#55037): with a
group whose tokens_per_chunk < tokens_per_hash (e.g. a
CircularBufferSpec scratch ring: 4-token blocks vs a 1152-token hash)
actually scheduled, two later points crash, all rooted in
hashes_per_chunk == 0 for that group:

1. update_offload_keys(): islice(step=0) raises ValueError on the first
   scheduled request.
2. _build_store_jobs(): offload_keys for the zero-hash group are never
   appended while offload_block_ids positions advance via the shared
   hybrid allocator -> assert len(offload_keys) ==
   len(offload_block_ids).

Zero-hash groups carry no hash-addressable offload blocks by
construction; skip them rather than crash. The
len(offload_keys)/len(offload_block_ids) assert for participating
groups is kept — zero-hash groups never reach it.

Regression tests included: hashes_per_chunk==0 derivation for a
CircularBuffer group, update_offload_keys skip behavior, and
idempotency across repeated scheduler steps.

Signed-off-by: The Anh Nguyen <ntheanh201@gmail.com>
@ntheanh201 ntheanh201 changed the title [Bugfix] Skip non-participating KV groups in offloading divisibility assert [Bugfix] Skip non-prefix-cacheable KV groups in the offloading divisibility assert Sep 3, 2026
…ty assert

build_offloading_config() asserts that every KV cache group block size is
divisible by tokens_per_hash. Groups that opt out of prefix caching (e.g.
CircularBufferSpec, a one-block-per-request ring whose block size is
unrelated to the hash unit) are never addressed by block hashes, so the
constraint is meaningless for them and makes native KV offloading
unbootable on any hybrid model that carries one:

  AssertionError: tokens_per_block=4 not divisible by tokens_per_hash=1152.
  Hybrid models (e.g. Mamba+Attention) need --enable-prefix-caching to
  align block sizes.

Restrict the assert to prefix-cacheable groups, falling back to every
group when none participate. This mirrors the filter, fallback included,
that resolve_kv_cache_block_sizes() already applies when deriving
tokens_per_hash.

Signed-off-by: The Anh Nguyen <ntheanh201@gmail.com>
@ntheanh201

Copy link
Copy Markdown
Author

Closing as a duplicate of #54743, which predates this PR (opened 2026-09-01) and addresses the same bug more thoroughly.

Same root cause, same failure: build_offloading_config asserts divisibility over every KV cache group while tokens_per_hash is derived from prefix-cacheable groups only, so a CircularBufferSpec scratch group makes native offloading unbootable. #54743 additionally reports the same repro on GLM-5.3-Flash with a kpool-tail group at block size 4.

The approaches differ in the right direction for #54743: this PR exempts non-prefix-cacheable groups from the assert, whereas #54743 scopes the offload group list itself to prefix-cacheable groups at construction, so those groups are never keyed, stored, or looked up anywhere downstream. Removing the cause is better than widening the assert, and it also obsoletes the follow-up scheduler guards I had proposed in #55038 (now closed).

My mistake was not running the AGENTS.md duplicate-work check before opening this. Sorry for the extra review load.

AI assistance was used in preparing this PR.

@ntheanh201 ntheanh201 closed this Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working kv-connector

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant