Repository navigation
[KV Connector] Fix PD async scheduling race condition for hybrid attn models - #48481
Conversation
|
LGTM, thanks a lot for taking this over and doing the extra accuracy validation in PD mode — really appreciate it! Let's proceed with #48481 |
Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com> Co-authored-by: llx-08 <2596671364@qq.com>
Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com> Co-authored-by: llx-08 <2596671364@qq.com>
6f23f3b to
bb62bf0
Compare
Sure, added you as a co-author. |
|
Thanks @llx-08 @arpera. I also encountered this bug and it's a critical one to fix. I am a bit uneasy with the approach in this PR though for the following reasons:
So it's overall quite fragile and we'd want to make sure we cover all connectors in addition to the NIXL one, including multiconnector with appropriate aggregation logic. I have been experimenting with an alternative approach here njhill@dc75fd8, which has some of its own downsides, but does not put any burden on connectors apart from that they should perform their async loading logic (and nothing else) within This runs the zeroing on a separate stream and defers the connector's async loading until the zeroing is complete. I'm actually still not sure what's best. We could repair / fill in the deficiencies in this PR for now, but it's making an interface change that I'm not sure we'll necessarily want to keep/require. |
|
As for me, I need any kind of fix for async scheduling because otherwise I cannot publish reproducible results on PD Qwen3.5 - this is currently my priority. Async scheduling significantly improves the performance in my case, so I would prefer to have any kind of solution to this problem for now even if it does not support all the connectors. |
Replace the KVConnectorMetadata.get_blocks_to_skip_kv_cache_zeroing hook (and its NIXL implementation) with a generic derivation: the external-token grant from get_num_new_matched_tokens already defines exactly which blocks an async load will overwrite, so the scheduler computes the skip set itself from the request's blocks covering [num_local_tokens, num_local_tokens + num_external_tokens). This works for every consumer connector with no connector involvement, and can't drift from the allocation arithmetic. Failure cases are handled without per-request state: everything still allocated beyond the valid prefix of an async load is by construction exactly the skipped-but-unwritten set, so when a load fails and the request will recompute locally, those blocks are re-recorded into the managers' normal new-block flow and the next step's new_block_ids_to_zero zeroes them before local recompute consumes them. Fully-failed or aborted loads release their blocks, which are re-recorded for zeroing on reallocation as usual. The only remaining state is the transient per-step skip set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
Makes the OffloadingConnector path functional for hybrid
(attention + GDN) models on TPU, with every transfer carried by raiden
direct DMA between the per-layer KV arrays and raiden's DmaMapped host
pool.
Canonicalization and routing: each per-layer array (attention KV and
every mamba/GDN state) becomes an int8 (num_blocks, row_bytes)
canonical tensor; block ids arrive ordered by KV-cache group
(GPULoadStoreSpec.group_sizes) and route to only that group's arrays.
The scheduler-side mamba external-KV patch extends to
OffloadingConnector for hybrid models, and the CPU pool is sized from
the bytes the TPU arrays physically hold per block row rather than
vLLM's shared-page accounting.
Raiden managers are created per (KV-cache group, row class): a manager
sizes and addresses every registered layer from layer 0's slice, so
tensors sharing one must share slice size and kernel-row factor.
Attention arrays register as source-major views at kernel-block
granularity with per-block copy_sizes runs; a registration-time probe
routes any tensor raiden cannot row-address to a host-staged torch
sidecar (empty for Qwen3.5 with conv-state padding enabled).
Transfer dispatch is gated on per-step device completion: raiden DMA
has no ordering against TensorCore programs, and any fence program
referencing a KV array races the per-step buffer donation swap
('Buffer has been deleted or donated' whenever its Execute enqueue is
delayed past the next step's donation). Jobs queue stamped with an
engine step and dispatch - always on the runner main thread - once
that step's sampled-token copy has resolved (AsyncTPUCopyState),
which under serial TensorCore execution means every device program
through that step has finished. The same gating supports async
scheduling, but async additionally requires vLLM fixes past 0.22.1
(vllm-project/vllm#45357, vllm-project/vllm#48481 - scheduler races
that corrupt KV consumers); run with --no-async-scheduling until then.
KV_OFFLOAD_HANG_DUMP_S arms a watchdog that dumps all thread stacks
when the engine stops stepping.
Validated on Qwen3.5-35B-A3B-FP8 TP4 (tpu7x, forced-overflow 512-block
HBM pool): 2-pass gsm8k holds accuracy across restore-heavy reuse runs
(baseline 0.77; reuse 0.80/0.76/0.757; 13.9M external hits), and a
prefill-heavy benchmark (150 x ~4.8K-token prompts, 94.5% external
hits) reaches 14,318 total tok/s vs 8,900 without offload at identical
accuracy.
Makes the OffloadingConnector path functional for hybrid
(attention + GDN) models on TPU, with every transfer carried by raiden
direct DMA between the per-layer KV arrays and raiden's DmaMapped host
pool.
Canonicalization and routing: each per-layer array (attention KV and
every mamba/GDN state) becomes an int8 (num_blocks, row_bytes)
canonical tensor; block ids arrive ordered by KV-cache group
(GPULoadStoreSpec.group_sizes) and route to only that group's arrays.
The scheduler-side mamba external-KV patch extends to
OffloadingConnector for hybrid models, and the CPU pool is sized from
the bytes the TPU arrays physically hold per block row rather than
vLLM's shared-page accounting.
Raiden managers are created per (KV-cache group, row class): a manager
sizes and addresses every registered layer from layer 0's slice, so
tensors sharing one must share slice size and kernel-row factor.
Attention arrays register as source-major views at kernel-block
granularity with per-block copy_sizes runs; a registration-time probe
routes any tensor raiden cannot row-address to a host-staged torch
sidecar (empty for Qwen3.5 with conv-state padding enabled).
Transfer dispatch is gated on per-step device completion: raiden DMA
has no ordering against TensorCore programs, and any fence program
referencing a KV array races the per-step buffer donation swap
('Buffer has been deleted or donated' whenever its Execute enqueue is
delayed past the next step's donation). Jobs queue stamped with an
engine step and dispatch - always on the runner main thread - once
that step's sampled-token copy has resolved (AsyncTPUCopyState),
which under serial TensorCore execution means every device program
through that step has finished. The same gating supports async
scheduling, but async additionally requires vLLM fixes past 0.22.1
(vllm-project/vllm#45357, vllm-project/vllm#48481 - scheduler races
that corrupt KV consumers); run with --no-async-scheduling until then.
KV_OFFLOAD_HANG_DUMP_S arms a watchdog that dumps all thread stacks
when the engine stops stepping.
Validated on Qwen3.5-35B-A3B-FP8 TP4 (tpu7x, forced-overflow 512-block
HBM pool): 2-pass gsm8k holds accuracy across restore-heavy reuse runs
(baseline 0.77; reuse 0.80/0.76/0.757; 13.9M external hits), and a
prefill-heavy benchmark (150 x ~4.8K-token prompts, 94.5% external
hits) reaches 14,318 total tok/s vs 8,900 without offload at identical
accuracy.
Reverts the scheduler half of vllm-project#48481, which skipped zeroing the blocks an async KV load would overwrite in order to avoid the zeroing racing the out-of-band RDMA write. The preceding commit's KVConnectorLoadGate makes that race impossible by construction - a connector's loads never start until the zeroing event for the step's blocks has fired - so the exclusion is no longer load-bearing, and full zeroing of every freshly allocated block is restored. Removed: - Scheduler._skip_zero_block_ids and the filtering in _get_new_block_ids_to_zero (the helper itself is kept). - KVCacheManager.get_zeroing_block_ids_in_range, which only fed the skip set, and record_blocks_for_zeroing plus its call on the failed-load path, which existed solely to re-zero blocks the skip had left unwritten. With no skip, those blocks were zeroed at allocation. - SingleTypeKVCacheManager.records_new_block_ids, used only by the two removed manager methods. - The three unit tests covering the skip behavior. The NixlConnector's own receive-side zeroing of untransferred regions (post_process_device_kv_on_receive) is deliberately left in place; it is now partly redundant but is a separate revert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
… models (vllm-project#48481) Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com> Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: llx-08 <2596671364@qq.com> Co-authored-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… models (vllm-project#48481) Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com> Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: llx-08 <2596671364@qq.com> Co-authored-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Purpose
IMPORTANT: PR was fully copied from #47373This PR fixes an accuracy regression in PD disaggregation with async scheduling for hybrid attention + SSM/GDN models.
When a decode-side request receives KV blocks from a remote prefill worker, the newly allocated attention blocks are overwritten by the incoming NIXL transfer. For hybrid models, vLLM also schedules KV cache zeroing for newly allocated attention blocks. Under async scheduling, that zeroing kernel can be queued behind a previous forward pass while the NIXL RDMA transfer writes directly into GPU memory. If the RDMA transfer completes first, the delayed zeroing kernel can erase the received attention KV, causing severe accuracy degradation.
The fix makes NIXL metadata report the local attention blocks that will be overwritten by remote KV loads. The scheduler then excludes those blocks from , while preserving zeroing for other newly allocated attention blocks.
This keeps the change scoped to connector-loaded attention blocks and avoids adding a CUDA synchronization point to the async pipeline.
Details about this fix
Initially the issue with async scheduling on PD Qwen3.5 was reported in these two bug reports:
--no-async-schedulingwhen TP>1 #37285 (TP>1)Then these issues were addressed by this PR #45096.
But this PR #45096 introduces two different fixes (see this comment):
During discussion in PR #45096 maintainers decided firstly to fix (2) race condition (comment) using another approach from #45357, so PR #45096 was closed because of this reason.
Recently fix for (1) race condition was published #47373, so this current PR #48481 finishes it's integration.
Test Result
Qwen3.5 PD with TP==1
Fixes this bug #42182
Hardware: 4xGB200
Model: Qwen3.5-35B-A3B
Details
Results:
Qwen3.5 PD with TP>1
Fixes this bug #37285 (ANOTHER BUG!!!)
Hardware: 4xGB200
Model: Qwen3.5-397B-A17B-NVFP4
Details
Results:
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.