Skip to content

[Core] Preempt hybrid requests whose sync KV load failed - #56733

Draft
ivanium wants to merge 1 commit into
mainfrom
fix/hybrid-kv-load-failure-preempt
Draft

ivanium wants to merge 1 commit into
mainfrom
fix/hybrid-kv-load-failure-preempt

Conversation

@ivanium

@ivanium ivanium commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Follow-up to #50388, which stopped EngineCore from crashing on a KV load failure with a hybrid KV cache by recomputing the affected request in full. For a request in the WAITING_FOR_REMOTE_KVS state that works: its blocks are freed once the transfer finishes and it is re-admitted. For a running request (a synchronous load under kv_load_failure_policy="recompute") the recompute is done in place, rewinding num_computed_tokens to 0 while the request keeps its blocks and stays in running. That is not a valid restart:

  • With overlapping batches (async scheduling, pipeline parallelism) the next step was scheduled before the failure was reported, so its allocate_slots already ran remove_skipped_blocks on the loaded prefix: the sliding-window and Mamba groups now point at the shared null block for the positions they no longer needed. The recompute from 0 writes those positions' KV into the null block and the attention right after them reads it back.
  • The in-flight step's output for that request was computed on the failed load, but nothing marks it stale, so it is delivered as a real token.
  • Any per-request worker state derived at admission stays as it was. The DeepSeek-V4.1 SWA bounded replay in [Feat][Model] Support encoder-side SWA-bounded replay for DeepSeek-V4.1-Flash #56227 has such state (the replay boundary), which is what surfaced this.

This PR preempts such requests instead of rewinding them in place. _preempt_request already does everything a from-scratch restart needs: it frees the blocks, moves the request to the head of the waiting queue, reports it in preempted_req_ids so the worker drops its state, and with drop_stale_output=True drops the in-flight output. Before preempting, the request's blocks are evicted from the prefix cache; a sync load is cached at allocation, so the retry would otherwise hit the garbage. Single-group models keep the block-precise in-place rewind, and the async path is unchanged.

Retry semantics: the re-admitted request goes through normal admission, so the connector is asked for a match again (as it is today for the request-level _handle_failed_recving path). A connector that keeps offering a load it cannot deliver would retry each admission.

Not a duplicate: #56135, #50742, #45497, #48216 and #54733 address the crash that #50388 fixed or work out a precise per-group truncation point; #53298 keeps the in-place rewind and repairs Model Runner V2 state around it for single-group models. None of them changes how a running hybrid request is restarted after #50388.

Test Plan

pytest tests/v1/kv_connector/unit/test_kv_load_failure_recovery.py tests/v1/core/test_scheduler.py tests/v1/core/test_async_scheduler.py

The new test_hybrid_sync_load_failure_preempts_request covers a running hybrid request whose sync load fails: it is preempted with no output appended, its blocks are released and no longer cached, and the next schedule() re-admits it from scratch with preempted_req_ids set.

Test Result

207 passed. Pre-commit (ruff, mypy) clean. Not run end to end against a KV connector that injects load failures; the unit test exercises the scheduler path only.

🤖 Developed with Claude Code; all changes reviewed and tested by the author.

#50388 recovers a KV load failure on a hybrid model by recomputing the
request in full: it rewinds num_computed_tokens to 0 in place and keeps the
request's blocks. A running request cannot restart that way. With
overlapping batches (async scheduling, pipeline parallelism) the next step
is already scheduled, so its sliding-window and Mamba groups have swapped
blocks they no longer need for the shared null block, which the recompute
then writes and reads back, and the in-flight step's output, computed on
the failed load, is delivered as if valid.

Preempt such requests instead. Re-admission allocates everything afresh and
the stale in-flight output is dropped. The request's blocks leave the
prefix cache first: a sync load is cached at allocation, so the retry would
otherwise hit the garbage. Single-group models keep the in-place rewind and
the async path already frees the blocks before retrying.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant