[Bugfix][V1] SimpleCPUOffload: defer sliding-window mid-flight block free (load-WAR alternative to #47291) - #47653
Closed
Saddss wants to merge 1 commit into
Closed
Conversation
…free (load-WAR) Under async scheduling a running sliding-window request (chunked-local / R-SWA behave the same) frees KV blocks mid-flight when its attention window advances (remove_skipped_blocks -> _remove_blocks_in_range -> free_blocks). The scheduler treats the block as free, but the in-flight forward dispatched in the previous step is still reading it on the compute stream. With SimpleCPUOffload the freed block is immediately reused as a load destination and the CPU->GPU copy overwrites it mid-read -> garbled output. vllm-project#45357 only defers request-completion frees, so it does not cover this mid-flight path. Extend its fence machinery: hold skipped-block frees under the current schedule-step fence and return them to the pool only after processed_step_seq reaches that fence (the in-flight reader step has completed). Gated on defer_block_free (async + kv consumer); identical to the old immediate free when disabled. Mamba / linear-attention layers are unaffected: a freed state snapshot has no in-flight reader since the recurrent forward reads only the current state. Add tests/v1/core/test_block_pool_defer.py covering the fence/drain semantics. Signed-off-by: Saddss <28726669061@qq.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Alternative, host-side fix for the SimpleCPUOffload load-path write-after-read
(WAR) race in #47282 — the residual gap left after the store-path fix #46278
(#45704). The worker-side CUDA-event fix is already up in #47291; this PR
implements the block-lifetime deferral approach @hclsys suggested, so the two
can be compared.
Root cause
Under async scheduling the scheduler runs ahead of the device. A running
sliding-window request (chunked-local / R-SWA behave the same) frees KV blocks
mid-flight when its attention window advances:
The scheduler treats the block as free, but the in-flight forward dispatched in
the previous step (processed+1) is still reading it on the compute stream. With
SimpleCPUOffload the freed block is immediately reused as a load destination and
the CPU->GPU copy (separate stream) overwrites it mid-read -> garbled output.
This is the mid-flight free path. #45357's deferral only covers
request-completion frees, so it does not cover this case.
Fix
Extend #45357's fence machinery to the mid-flight skipped-block free:
block_pool.free_blocks_maybe_deferred()holds skipped blocks under thecurrent schedule-step fence instead of returning them to the pool;
drain_skipped_frees()releases them onceprocessed_step_seqreaches thatfence (the in-flight reader step has completed).
single_type_kv_cache_manager._remove_blocks_in_range()frees throughfree_blocks_maybe_deferred().schedulerenables it only whendefer_block_freeis active (async + kvconsumer), sets the fence at the top of
schedule(), drains afterprocessed_step_seqadvances.Disabled -> byte-for-byte the old immediate free.
Scope
Only mid-flight-freeing attention types are affected: sliding-window,
chunked-local, R-SWA. Full / sink / cross attention free only at request
completion (already covered by #45357).
Mamba / linear-attention layers are not affected — confirmed empirically
(Falcon-H1-0.5B, both
alignandallmamba cache modes, same repro; 0 garbleover 1128 valid completions while mid-flight frees fired 1.5k-2.5k times and
external-hit loads ran ~25%). A freed Mamba state snapshot has no in-flight
reader: the recurrent forward reads only the current state block.
Correctness + performance (measured)
Gemma-4-31B-NVFP4 + fp8 KV,
VLLM_USE_SIMPLE_KV_OFFLOAD=1,--kv-offloading-size 100 --max-num-seqs 64 --gpu-memory-utilization 0.95(GPU KV ~30.8k tokens = 1.88x concurrency, i.e. heavy offload). 300 prompts x
3 rounds x concurrency 64 = 900 completions/variant,
max_tokens 256,temperature 0.7. #45357 active in all variants. All three variants overlaid on
the same
vllm/vllm-openai:nightly(files verified byte-identical to the forkbaseline), so only the changed files differ.
Steady-state engine metrics (mean over 72x 10s samples):
Throughput / KV util / queue depth / external-hit / preemptions all line up;
wall clock within 3% noise (defer marginally fastest). DEFER instrumentation:
863,525 blocks deferred over the run, but each is held ~1 step only (max
deferred-queue depth 30, drained every processed step), so the in-flight held
set is tiny and swamped by offload cost itself. Free-pool idle blocks dipped to
106 at the tightest point without triggering preemption — a tighter
memory / higher-concurrency setup could make defer bite, but this run did not
measure it.
Relationship to #47291 (event fix)
Same race, two places to cut it:
worker — one spot, covers both load-WAR and store-RAW.
done — covers load-WAR; store-RAW still relies on the existing store
event (the DEFER variant above keeps it; dropping it would re-break the
store path, which is a pure cross-stream RAW that host-side lifetime
management cannot reach).
Defer is a correct, equally-fast load-side alternative, but it is a partial
fix (still needs the event for store) and more intrusive (per-manager, SWA-only
today; chunked-local / R-SWA / any future mid-flight-freeing manager must be
wired individually), whereas the event is one path-agnostic spot in
worker.get_finished.Test Plan
tests/v1/core/test_block_pool_defer.py(fence/drain semantics, 3 passed).Draft — opening to compare defer vs the event fix (#47291) before either lands.