Conversation
…e compute stream The GPU->CPU copy was issued on its own transfer stream from a background thread, without ever ordering that stream behind the compute stream, and build_params requested CU_MEMCPY_SRC_ACCESS_ORDER_ANY for both directions. The store therefore reads the live KV cache while the compute stream is still writing it, and the driver is free to pull those reads ahead of anything else. Blocks can be captured half-written, get their hash stamped anyway, and enter the CPU prefix cache as valid data. Nothing fails; a later request whose prefix matches restores the block and answers from a poisoned context. The sibling path in vllm/v1/kv_offload/cpu/gpu_worker.py already waits on the compute stream and keeps STREAM source ordering for GPU->CPU, with a comment stating why. Do the same here. ANY stays for CPU->GPU, whose source is host pinned memory that no GPU stream writes. Measured on DeepSeek-V4-Flash (PP5, 2x RTX 8000 + 3x V100, FP8 KV cache) by re-reading each completed store and comparing it against its GPU source: 29 of ~21500 block/tensor pairs differed per run, in partial runs inside the block; end to end a 62k needle whose first blocks came back from the CPU pool returned 0 of 4 facts and degenerated into token salad. With this change the comparison reports zero differing pairs on all five ranks and the needle is 4 of 4, repeated twice. Step time is unchanged (82/84 ms vs 81/83 ms). Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Peuqui <peuqui@github.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
SimpleCPUOffloadWorker.get_finished()issues the GPU→CPU copy on its own transfer stream, from a background thread, without ever ordering that stream behind the compute stream. On top of that,build_params()requestsCU_MEMCPY_SRC_ACCESS_ORDER_ANYfor both directions.The store therefore reads the live KV cache while the compute stream is still writing into it, and the driver is explicitly permitted to pull those source reads ahead of anything else on the stream. A block can be captured half-written. Its hash is stamped regardless, so the damaged copy enters the CPU prefix cache as valid data. Nothing fails and nothing is logged; a later request whose prefix matches restores the block and answers from a poisoned context.
The sibling path in this repository already handles this correctly.
vllm/v1/kv_offload/cpu/gpu_worker.pydoesstream.wait_stream(torch.cuda.current_stream())before offloading and deliberately keeps STREAM source ordering for GPU→CPU, with a comment stating why: the compute stream keeps writing the source.SimpleCPUOffloadConnectordoes neither.Both halves are needed. The barrier alone is not sufficient, because
ANYlets the driver read past it.ANYstays for CPU→GPU, whose source is host pinned memory that no GPU stream writes, so it can keep pipelining those reads.How it was found
Measured on DeepSeek-V4-Flash, 5-stage pipeline parallel (2× RTX 8000 + 3× V100), FP8 KV cache,
cpu_bytes_to_use5 GiB. Request sequence: two 18k requests, a 30k needle-in-a-haystack, then a 62k needle that shares a ~2k-token prefix with the 30k one, so its opening blocks are restored from the CPU pool.Each completed store was re-read and compared against its GPU source, right after the transfer event completed. The scheduler still holds a reference on those GPU blocks until every worker reports the event, so the source cannot have been reused in the meantime — any difference means the copy captured data the compute stream had not finished writing.
Without this change:
Partial differences in the middle of a block — a mix of old and new content, which is what a copy looks like when it races the writer. End to end, the 62k needle returned 0 of 4 facts and degenerated into token salad.
With this change, same sequence, same comparison: zero differing pairs on all five ranks, needle 4 of 4, repeated twice. Step time is unchanged, 82/84 ms against 81/83 ms before, so the barrier costs nothing measurable on this setup.
Please note the fault is timing-dependent: the identical sequence produced both a clean 4-of-4 run and a corrupted one under otherwise identical conditions. Judging this by output quality alone is unreliable, which is why the block comparison was used. A regression test that reliably fails without the fix therefore cannot exist; the added tests pin the structure (barrier present, source ordering correct per direction), not the timing.
Test Plan
New
tests/v1/simple_kv_offload/test_store_stream_ordering.pycovers: the store queues the barrier before the copy is handed to the backend, a load does not take one, andbuild_paramsselects STREAM ordering for stores and ANY for loads.End-to-end on the hardware above: the sequence described in Purpose, run twice, plus a restore probe (30k needle, 62k evictor with a different seed, identical 30k needle again) verifying the answer is unchanged.
Test Result
Against the unpatched sources,
test_store_is_ordered_behind_the_compute_streamfails withassert 'store_stream.wait_stream' in ['backend.launch_copy'], i.e. the copy is queued with no ordering at all.End-to-end: zero differing block/tensor pairs, needle 4 of 4 in both runs, restore probe returns a character-identical answer, step time unchanged.
Duplicate check
gh pr list --repo 1CatAI/1Cat-vLLM --state open --search "offload"— [KV Offload] Grouped hybrid RAM caching, filesystem restoration and retention-sized Mamba pools (stacked on #617) #598 and [Bugfix][Core] Backport upstream Mamba prefix-cache retention #617 touch the native/QSA offload path, [Bugfix] PLE offload: send registration inputs by file descriptor #654 and [Feature][Qwen4Exp] PLE overflow cascade: device, pinned host, spare GPU, disk #646 the PLE offload, none of themSimpleCPUOffloadConnector.gh pr list --state all --search "SimpleCPUOffloadConnector in:body"— only [Bugfix][KV Offloading] Allow native CPU offload with expandable_segments #248 (merged,expandable_segments), a different fix.gh pr list --state all --search "wait_stream srcAccessOrder"— no results.AI assistance
This change was developed with AI assistance (Claude). The diagnosis, the instrumentation used to produce the numbers above, and the measurements were carried out on the hardware described. Every changed line has been reviewed by me and I can defend the change end to end.