Skip to content

[Bugfix][Core] Fail requests queued during level-2 sleep when a wake resumes before the weights are restored - #65326

Open
DuFanYin wants to merge 1 commit into
vllm-project:mainfrom
DuFanYin:fix/l2-wake-weights
Open

DuFanYin wants to merge 1 commit into
vllm-project:mainfrom
DuFanYin:fix/l2-wake-weights

Conversation

@DuFanYin

@DuFanYin DuFanYin commented Nov 3, 2026 •

Copy link
Copy Markdown

Overview

After level-2 sleep, a wake_up() that remaps the discarded weights and resumes scheduling in the same call (e.g. a tagless wake) ran every request queued during sleep on the discarded weights and returned it with HTTP 200. Such a wake now fails those requests with a retryable error (HTTP 503) instead. Fixes #65325 (duplicate: #65324).

Claims

  • A wake that both remaps weights discarded by level-2 sleep and resumes scheduling finishes the requests queued at that moment with RequestError(code="weights_not_restored", retryable=True) before resuming, so none of them reaches the model. The server returns 503, as it does for engine_paused.
  • Every other path is unchanged: the staged wake (weights, restore, kv_cache), with reload_weights, a weight update or a custom loader; a tagless wake with nothing queued, then reload_weights (the documented order, test_cumem sleep-2); level-1 sleep; checkpoint restore. wake_up's return value and is_sleeping are unchanged, nothing waits on a weight-restore signal, and no API is added.
  • Not covered, by design: requests that arrive after such a wake but before the weights are restored still run on them; the engine cannot tell that window from a custom loader having finished. docs/features/sleep_mode.md and the warning point deployments that receive requests while asleep to the staged wake.

Validation

Reproduction from #65325 against vllm serve Qwen/Qwen3-0.6B --enable-sleep-mode (greedy, 32 tokens):

Request queued during level-2 sleep main this PR
tagless wake_up, then reload_weights 200, !!!!!!!!… 503 weights_not_restored, warning logged
request after reload_weights 200, matches baseline 200, matches baseline
staged wake: weights, reload_weights, kv_cache 200, matches baseline 200, matches baseline

Tests (VLLM_USE_FLASHINFER_SAMPLER=0 only because the test host has no CUDA toolkit for the FlashInfer sampler JIT), on this branch rebased onto main 30cff7b:

pytest tests/v1/engine/test_async_llm.py::test_level2_wake_fails_requests_queued_before_reload  # 1 passed
pytest tests/v1/engine/test_engine_core.py -k "not test_engine_core_tp"                         # 48 passed
pytest tests/basic_correctness/memory/cumem/test_cumem.py                                       # 22 passed
pytest tests/basic_correctness/memory/sleep_mode/test_sleep_mode.py                             # 15 passed, 1 skipped (ROCm-only)
  • test_level2_wake_fails_requests_queued_before_reload (real AsyncLLM, Qwen/Qwen3-0.6B): a request queued during level-2 sleep fails with RetryableRequestError on a tagless wake and the next request after reload_weights matches the pre-sleep output; under a staged wake and under level-1 sleep the queued request matches it.
  • test_wake_up_fails_requests_on_discarded_weights (CPU, real Executor sleep/wake bookkeeping): tagless, ["weights", "kv_cache"] and kv-first-then-weights wakes fail the queued requests before resuming; staged, level-1, level-2-on-level-1 and level-1-after-a-staged-level-2 wakes do not.
  • On the base before rebasing, tests/v1/worker/test_sleep_mode_backend.py passed (9), and tests/model_executor/test_sleep_mode_tensor_ownership.py and tests/entrypoints/rl/state_transitions/test_sleep.py passed except cases that need gated or audio-only models on the host. test_engine_core_tp fails identically on main on this host.
  • pre-commit run on the changed files passes, including mypy.

Details

Root cause. EngineCore.wake_up resumes scheduling once all executor memory is resident. Level-2 sleep discards the weights (SleepModeBackend.suspend), so memory being resident does not mean the weights are valid. When one call remaps the weights and resumes, the caller cannot restore them in between: the next engine step runs before any reload_weights RPC arrives, which is why #65325 reproduced 10 of 10 times.

Fix. EngineCore records that level-2 sleep discarded the weights (only when the executor actually slept; a level-2 sleep on top of level 1 is a no-op there) and clears it on the first wake that remaps them. If that wake also resumes scheduling, the queued requests would deterministically run on discarded weights, so they are finished first, with the same retryable error path pause_generation uses for engine_paused. The check only reads executor state after a level-2 discard.

Why fail instead of holding scheduling until the reload. The previous revision of this PR held a tagless wake until another wake_up after the reload. As pointed out in review, that broke the documented wake_up() then reload_weights order (test_cumem sleep-2) and turned the wrong output into a hang. Holding scheduling and resuming it automatically once the weights are restored (the approach in #65668) keeps that order working, but needs the engine to know when the weights are valid: a weight update through finish_weight_update or a custom loader passed to collective_rpc restores them without a signal the engine can rely on, so such engines would stay paused, including under the staged wake. This revision only acts on requests whose output was certain to be wrong, so no previously correct sequence changes behavior.

Related: #48310 (RL sleep/wake correctness RFC; its canonical transition is the staged wake), #65668, #64184, #29341.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (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.

🤖 Generated with Claude Code

@DuFanYin
DuFanYin requested a review from njhill as a code owner November 3, 2026 07:16

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify

mergify Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--65326.org.readthedocs.build/en/65326/

@mergify mergify Bot added documentation Improvements or additions to documentation bug Something isn't working labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@DuFanYin
DuFanYin force-pushed the fix/l2-wake-weights branch from 0d4f12e to c94e9ae Compare November 3, 2026 07:49
@DuFanYin DuFanYin changed the title [Bugfix][Core] Hold scheduling after a tagless wake from level-2 sleep until the weights are reloaded [Bugfix][Core] Keep scheduling paused on a tagless wake from level-2 sleep while the weights are incomplete Nov 3, 2026
@seansie0830

Copy link
Copy Markdown

Tested end-to-end on WSL2 with an NVIDIA GeForce RTX 5060 Laptop GPU.

Environment Details

==============================
        System Info
==============================
OS                           : Ubuntu 26.04.1 LTS (x86_64)
GCC version                  : (Ubuntu 15.2.0-16ubuntu1) 15.2.0
Clang version                : 21.1.8 (6ubuntu1)
CMake version                : version 4.2.3
Libc version                 : glibc-2.43

==============================
       PyTorch Info
==============================
PyTorch version              : 2.14.0+cu130
Is debug build               : False
CUDA used to build PyTorch   : 13.0
ROCM used to build PyTorch   : N/A
XPU used to build PyTorch    : N/A

==============================
      Python Environment
==============================
Python version               : 3.12.14 (main, Sep 24 2026, 17:57:43) [Clang 22.1.3 ] (64-bit runtime)
Python platform              : Linux-6.18.33.2-microsoft-standard-WSL2-x86_64-with-glibc2.43

==============================
       CUDA / GPU Info
==============================
Is CUDA available            : True
CUDA runtime version         : 12.8.93
GPU models and configuration : GPU 0: NVIDIA GeForce RTX 5060 Laptop GPU
Nvidia driver version        : 591.44
cuDNN version                : Could not collect

==============================
          CPU Info
==============================
Model name                   : Intel(R) Core(TM) i5-14450HX
Architecture                 : x86_64
CPU(s)                       : 16
NUMA node(s)                 : 1
NUMA node0 CPU(s)            : 0-15

==============================
Versions of relevant libraries
==============================
[pip3] flashinfer-python==0.7.2.post1
[pip3] torch==2.14.0
[pip3] transformers==5.22.1
[pip3] triton==3.8.0
[pip3] pytest==9.2.0

==============================
         vLLM Info
==============================
vLLM Version                 : 0.33.0rc1.dev582+g77d2790e7.precompiled
VLLM_WORKER_MULTIPROC_METHOD : spawn

Verification Details

Test Target: tests/v1/engine/test_async_llm.py::test_queued_request_waits_for_reload_after_level2_sleep

Model: Qwen/Qwen3-0.6B

Execution Command:

VLLM_USE_FLASHINFER_SAMPLER=0 pytest -v -s tests/v1/engine/test_async_llm.py::test_queued_request_waits_for_reload_after_level2_sleep

  1. Before (97c198c)

Tested against the base commit. The queued request was scheduled immediately after memory allocation and evaluated against uninitialized/discarded memory (zeroed out weights), leading to repeated ! tokens under greedy decoding:

FAILED

======================================================= FAILURES =======================================================
_______________________________ test_queued_request_waits_for_reload_after_level2_sleep ________________________________

    @pytest.mark.asyncio
    async def test_queued_request_waits_for_reload_after_level2_sleep():
        ...
        expected = await final_text(
            await engine.add_request("before-sleep", TEXT_PROMPT, params)
        )
        await engine.sleep(level=2)
        # The engine receives this request before the wake_up below.
        queued = await engine.add_request("queued", TEXT_PROMPT, params)
        held = await engine.wake_up()
        await engine.collective_rpc("reload_weights")
        resumed = await engine.wake_up()

>       assert await final_text(queued) == expected
E       assert '!!!!!!!!!!!!...!!!!!!!!!!!!!' == " I'm a 25-ye...f the city of"
E
E         -  I'm a 25-year-old student. I'm studying in the university of the city of the city of the city of the city of the city of
E         + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

tests/v1/engine/test_async_llm.py:1108: AssertionError

=============================================== short test summary info ================================================
FAILED tests/v1/engine/test_async_llm.py::test_queued_request_waits_for_reload_after_level2_sleep - assert '!!!!!!!!!!!!...!!!!!!!!!!!!!' == " I'm a 25-ye...f the city of"
============================================ 1 failed, 2 warnings in 24.21s ============================================
  1. After (PR branch fix/l2-wake-weights, commit c94e9ae)

Ran the test on the PR HEAD. Scheduling was held after the tagless wake, and unpaused only after reload_weights() finished:

(EngineCore pid=3853) INFO 11-03 18:25:42 [abstract.py:434] It took 0.043327 seconds to fall asleep.
(EngineCore pid=3853) INFO 11-03 18:25:42 [abstract.py:452] It took 0.068457 seconds to wake up tags {'kv_cache', 'weights'}.
(EngineCore pid=3853) WARNING 11-03 18:25:42 [core.py:1144] The weights are incomplete, e.g. discarded by level-2 sleep; scheduling stays paused. Reload them, then call wake_up again, or wake_up(tags=['scheduling']) if they were restored otherwise.
(EngineCore pid=3853) INFO 11-03 18:25:42 [inplace.py:91] Reloading weights inplace...
Loading safetensors checkpoint shards: 100% Completed | 1/1 [00:00<00:00,  2.03it/s]
(EngineCore pid=3853) INFO 11-03 18:25:43 [inplace.py:114] Reloading and processing weights took 0.50 seconds
(EngineCore pid=3853) WARNING 11-03 18:25:43 [abstract.py:440] Executor is not sleeping.
(EngineCore pid=3853) INFO 11-03 18:25:43 [scheduler.py:3075] setting pause state to UNPAUSED
...
PASSED
====================================== 1 passed, 2 warnings in 205.87s (0:03:25) =======================================

Confirmed that:

The first tagless wake_up() detected incomplete weights, kept the scheduler paused (PAUSED_NEW), and logged the warning as designed.

The scheduler transitioned to UNPAUSED only after weight reload.

The output of the queued request matched the expected inference result before sleep without generating gibberish.

LGTM!

Comment thread vllm/v1/engine/core.py Outdated
"scheduling stays paused. Reload them, then call wake_up again, "
"or wake_up(tags=['scheduling']) if they were restored otherwise."
)
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it breaks test_release_kv_cache_memory_preserves_generation[sleep-2] in test_cumem.py: it does wake_up(), then reload_weights, then asserts not is_sleeping(), and with this return the scheduler's still PAUSED_NEW at that point. I ran that sequence on CPU against the real EngineCore, Scheduler and Worker methods with the memory backend stubbed, and a queued request never got scheduled after the reload, even over 1000 schedule() calls. The steps in #65325 hit the same thing, so that queued curl now hangs instead of returning garbage, and the only sign is a warning in the server log. Could the engine resume once reload_weights clears the flag when a tagless wake is being held, so the old order keeps working?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Paolo, thanks, you’re right: holding the wake broke the documented wake_up() → reload_weights order and turned wrong output into a hang. I compared a few approaches and decided to only act on requests that are certain to run on discarded weights, rather than have the engine guess when the weights are valid.

the New approach: a wake that remaps the discarded weights and resumes scheduling in one call fails the queued requests with a retryable 503; every other order, including test_cumem, is unchanged.

…resumes before the weights are restored

A wake_up that remaps the weights discarded by level-2 sleep and resumes
scheduling in the same call, such as a tagless wake, leaves no point to
restore them first, so the next step ran every queued request on the
discarded weights and returned it with HTTP 200. Such a wake now finishes
those requests with a retryable error (weights_not_restored, HTTP 503)
before resuming. Every other wake order, level-1 sleep and checkpoint
restore are unchanged, and nothing waits on a weight-restore signal.

Signed-off-by: Hang Zhengyang <hang.zhengyang1010@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DuFanYin
DuFanYin force-pushed the fix/l2-wake-weights branch from c94e9ae to d171179 Compare November 5, 2026 17:14
@DuFanYin DuFanYin changed the title [Bugfix][Core] Keep scheduling paused on a tagless wake from level-2 sleep while the weights are incomplete [Bugfix][Core] Fail requests queued during level-2 sleep when a wake resumes before the weights are restored Nov 5, 2026
Comment thread vllm/v1/engine/core.py
return fully_awake

def _fail_requests_on_discarded_weights(self) -> list[Request]:
failed = self.scheduler.finish_requests(None, RequestStatus.FINISHED_ERROR)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With multiprocessing disabled, a request queued during level-2 sleep is removed here without sending a final result back to the caller. It still looks unfinished, so the caller can keep looping on empty results. Please send the error back on that path too and add a regression test.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Requests queued during level-2 sleep run before reload_weights and return wrong output with HTTP 200

4 participants