Repository navigation
[Bugfix][KV Offload] Count each key once per request toward store_threshold - #64558
Conversation
…hreshold When prepare_store() can't allocate, it returns None and the offloading scheduler offers the same keys again on a later step. Each retry counted toward store_threshold, so a request's own retries could admit chunks that no other request had offered. Remember the keys of a request's failed offers and don't count them again; count their threshold skips once too. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Sohom Chakraborty <16609933+sohom-cs@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
0c0b9f7 belongs to vllm-project#64558. This restores the tree of d73a68a. Signed-off-by: Sohom Chakraborty <16609933+sohom-cs@users.noreply.github.com>
|
Thanks @sohom-cs ! |
A preempted request restarts its stores from chunk 0 and offers its keys again through the same request context. Each re-offer counted again, so the request could admit chunks that no other request had offered. Keep the keys a request has counted (counted_keys) instead of the keys of its failed offers. A key counts toward store_threshold once per request, whether it comes back through a retry of a failed offer or after preemption, and its threshold skip is counted once. The set lives on the request's _RequestCacheAccess: it is kept across preemption, goes away with the request, and starts over on a cache reset. Three tests offered twice through one context to stand for two requests; they now use two contexts. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Sohom Chakraborty <16609933+sohom-cs@users.noreply.github.com>
|
@orozery Done in 6304ad3. |
|
/ci run |
|
✅ @sohom-cs, CI is now available for this PR.
|
|
✅ Triggered Buildkite CI #99139 for commit |
|
/ci run |
|
✅ Triggered Buildkite CI #99303 for commit |
|
/ci run |
|
✅ Triggered Buildkite CI #99476 for commit ℹ️ This PR is 33 commits behind upstream |
Overview
With
store_threshold >= 2,CPUOffloadingManageradmits a chunk to the CPU tier only after it has been offered that many times. Whenprepare_store()can't allocate, it returnsNone, and the offloading scheduler offers the same keys again on a later step. Each retry counted as another offer, so a chunk that only one request ever had could be admitted by that request's own retry. A preempted request's re-offers counted the same way: the scheduler restarts its stores from chunk 0 through the same request context. With this PR, each key counts once per request.Claims
store_thresholdonce. When it offers a key again, as a retry of a failedprepare_store()call or after preemption, the key doesn't count again; it still moves to the most recent end of the tracker, as before. A cache reset starts the request's counting over.vllm:kv_offload_stores_skippedcounts a key's skip once per request too.store_threshold < 2.Validation
test_store_threshold_does_not_count_retries_of_a_failed_offerruns withstore_threshold2 and 3:None.STORES_SKIPPEDmust be 1.test_store_threshold_counts_a_key_once_per_request(also 2 and 3) is the preemption case: the request stores the eligible key, then offers both keys again through the same context, as a resumed request does. The cold key must not be stored, its count must stay 1,STORES_SKIPPEDmust be 1, and another request's offer must still count it.With main's
manager.pyall four fail, at the store; with this PR's first commit (bb7b8b6) the two preemption cases fail the same way:b'2...'is the cold key: main stores it, although only this request ever offered it.Through the scheduler. I ran the offloading unit tests'
RequestRunner, which drives the realSchedulerandOffloadingConnectorScheduler, with a realCPUOffloadingManager(num_chunks=2, store_threshold=2)behind it:4440f0e263The same runner with one request preempted once (free GPU blocks set to 0, as
test_request_preemptiondoes) and resumed, with sync and async scheduling: the request offers its two blocks, and after the resume offers them again through the sameReqContext. On maindc45a5f552and on bb7b8b6 that re-offer stores both of its own blocks; with this PR it stores neither.The script (copy it into tests/v1/kv_connector/unit/offloading_connector/ and run it with pytest -s)
On this branch (Linux CPU):
test_manager.py: 72 passed.tests/v1/kv_offload: 893 passed, 34 failed. Main fails the same 34: 32 incpu/test_gpu_worker.py, which needs a GPU, and 2 intiering/p2p/test_manager.py::TestIdleWhileServingPeers.CI: both tests are in
tests/v1/kv_offload, which the "V1 KV Offload" step runs. I have no GPU, so I haven't run vLLM end to end. This changes which chunks are admitted, not how they are written or read, and the scheduler run above covers the path from the scheduler's retry to the manager. If you'd like an end-to-end run before merging, I'll do it when I have GPU time. Model outputs are unaffected, so no evals.Details
Root cause:
prepare_store()counts every offered key towardstore_threshold(_record_accesses) before it allocates.None._build_store_jobscountsvllm:kv_offload_allocation_failureand moves on without advancing the request's store position. So the same keys are offered again on a later step, and again when the request finishes (test_scheduler_reports_allocation_failureasserts both attempts).store_threshold=2, one failed allocation and its retry admit every cold chunk in the offer. That happens under CPU-tier pressure, where the filter matters most.Why a retry isn't another offer: #52227 moved the counter from lookups to store offers. It relied on
prepare_store()receiving "the request's whole offerable key set", so that "a block crosses store_threshold after that many repetitions of the content". A retry resends that same offer, and so does a preempted request when it resumes: no new request, no new repetition. The metric is likewise documented as skips under the "reuse threshold". The same function already counts a request's keys once across calls in two places: the policy'son_store_miss(throughstore_miss_keys) and the request's access record (throughseen_keys). Before this PR, the threshold counter and its skip metric were the per-key counts that a retry or a resumed request incremented again.Scope: the keys a request has counted are kept in
counted_keyson its_RequestCacheAccess, next tostore_miss_keysandseen_keys, only whenstore_threshold >= 2. The scheduler keeps a request'sReqContextacross preemption and drops it once the request is finished, so the set lasts as long as the request; a cache reset starts it over, as it does the rest of that state.The first commit skipped only retries of failed offers. After review, every key counts once per request, as
store_miss_keysdoes, so a request's re-offers after preemption don't count either. Three existing tests offered twice through one context to stand for two requests:test_filter_reused_manager,test_filter_reused_manager_oversized_offer_makes_progressandtest_store_threshold_does_not_hide_ready_reuse. They now use one context per request.Not a duplicate: no open PR changes
_record_accessesor the threshold filter. I searched open PRs forstore_threshold,_record_accessesandprepare_store. #63686 rewords thestore_thresholddocs. #57813, #63900 and #64216 change other parts ofmanager.py.AI assistance
AI-assisted; reviewed and tested by me.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.