{{ message }}
Repository navigation
Conversation
…hanging
<!-- markdownlint-disable -->
## Overview
`max_num_scheduled_tokens=0` passed config validation. `LLM.generate()` then hung forever with no error. This PR changes the field's bound from `ge=0` to `ge=1`, so `0` is now rejected when the config is built.
Fixes vllm-project/vllm issue 60349. The bug was introduced by vllm-project/vllm pull request 44207.
## Claims
- `max_num_scheduled_tokens=0` now raises a pydantic `ValidationError` (a subclass of `ValueError`) at config time, instead of a silent hang. The error names the field and the allowed range.
- `0` is rejected on every user route: `SchedulerConfig`, `EngineArgs`, `EngineArgs` with speculative decoding, and `--max-num-scheduled-tokens` on the CLI.
- `None` (the default), `1` and larger values work as before.
- A new unit test fails on the old bound and passes with the fix.
## Validation
Hardware and software: 1 × NVIDIA B200. vLLM at `43b4aaea3e40e20ef53ce2bfecfa1b72060bf2b5` (before), and the same commit plus this patch (after). Python 3.12, pydantic 2.13. The bug was also reproduced in the `vllm/vllm-openai:nightly` image at the same commit.
Reproducer (`repro.py`):
```python
from vllm import LLM, SamplingParams
llm = LLM(
model="Qwen/Qwen3-0.6B",
max_num_scheduled_tokens=0,
)
print(llm.generate(
["Hello, my name is"],
SamplingParams(max_tokens=8),
))
```
```bash
VLLM_ENGINE_READY_TIMEOUT_S=3600 timeout 600 python3 repro.py; echo "EXIT_CODE=$?"
```
| Tree | Runs | Result |
| --- | --- | --- |
| Before (venv) | 1 | Engine ready after 158 s. Then `generate()` made no progress for ~362 s until `timeout` killed it. No exception, no error log. `EXIT_CODE=124` |
| Before (nightly container, `timeout 180`) | 1 | Same hang. `EXIT_CODE=124` |
| After | 2 (one by me, one by a separate review check) | `ValidationError` about 1 s after start. `EXIT_CODE=1` |
| After, with `max_num_scheduled_tokens=1` | 1 | Generated 8 tokens (`finish_reason=length`). `EXIT_CODE=0` |
Before:
```
Processed prompts: 0%| | 0/1 [00:00<?, ?it/s, est. speed input: 0.00 toks/s, output: 0.00 toks/s]
(EngineCore pid=457933) INFO 10-08 07:22:45 [core.py:1423] [shutdown] EngineCore: trigger received signal=SIGTERM
(EngineCore pid=457933) INFO 10-08 07:22:45 [core.py:1581] [shutdown] EngineCore: aborting in-flight requests count=1
EXIT_CODE=124
```
After:
```
pydantic_core._pydantic_core.ValidationError: 1 validation error for SchedulerConfig
max_num_scheduled_tokens
Input should be greater than or equal to 1 [type=greater_than_equal, input_value=0, input_type=int]
For further information visit https://errors.pydantic.dev/2.13/v/greater_than_equal
EXIT_CODE=1
```
Other checks:
- `pytest tests/test_config.py -k scheduler_config`: `2 passed, 439 deselected`. With `ge=0` put back, the new test fails with `Failed: DID NOT RAISE ValidationError`. I ran it with `--noconftest` because my local venv lacks the test dependencies for `tests/conftest.py`. The test is in `tests/test_config.py`, which Buildkite already runs (`.buildkite/test_areas/engine.yaml`).
- `pre-commit run --files vllm/config/scheduler.py tests/test_config.py`: every hook passed or was skipped, including mypy.
<details>
<summary>Config probe on the patched tree (no GPU work)</summary>
```
SchedulerConfig(max_num_scheduled_tokens=0): ValidationError ValueError=True: ... Input should be greater than or equal to 1 [type=greater_than_equal, input_value=0, input_type=int]
SchedulerConfig(max_num_scheduled_tokens=-1): ValidationError ValueError=True: ... Input should be greater than or equal to 1 [type=greater_than_equal, input_value=-1, input_type=int]
SchedulerConfig(max_num_scheduled_tokens=1): OK -> 1
SchedulerConfig(max_num_scheduled_tokens=2048): OK -> 2048
SchedulerConfig(max_num_scheduled_tokens=None): OK -> None
EngineArgs(max_num_scheduled_tokens=0).create_engine_config(): ValidationError ValueError=True: ...
EngineArgs(max_num_scheduled_tokens=1).create_engine_config(): OK -> 1
EngineArgs(max_num_scheduled_tokens=None).create_engine_config(): OK -> None
ngram + max_num_scheduled_tokens=0: ValidationError ValueError=True: ...
ngram + max_num_scheduled_tokens=1: OK -> 1
ngram + max_num_scheduled_tokens=None: OK -> 2048
CLI --max-num-scheduled-tokens 0: ValidationError ValueError=True: ...
CLI --max-num-scheduled-tokens 1: OK -> 1
```
With ngram speculative decoding and `None`, the value still resolves to `max_num_batched_tokens` (2048), as before.
</details>
## Details
**Root cause.** `SchedulerConfig.max_num_scheduled_tokens` is `Field(default=None, ge=0)`, so `0` passes. The only other check, `<= 0` in `VllmConfig._set_max_num_scheduled_tokens` (`vllm/config/vllm.py`), runs only when speculative decoding is on. `Scheduler.__init__` keeps the value because it `is not None`. So every `schedule()` call starts with `token_budget = 0`. Both scheduling loops (running and waiting) run only `while token_budget > 0`. No token is ever scheduled, the request stays in `waiting`, and nothing raises. A debug run on the old code, with prints added to the scheduler, showed this on every step:
```
LYUTIY-DEBUG Scheduler.__init__ config max_num_scheduled_tokens=0 max_num_batched_tokens=16384 -> self.max_num_scheduled_tokens=0 speculative_config=None
LYUTIY-DEBUG schedule step=1 start_budget=0 end_budget=0 waiting=1 running=0 total_num_scheduled_tokens=0
```
**Where it came from.** Before vllm-project#44207 (commit `51e0c579b`), the field had no bound and the scheduler used a truthiness test, so `0` quietly fell back to `max_num_batched_tokens`. vllm-project#44207 added `ge=0` and changed the test to `is not None`. Its goal was to make the non-speculative path reject bad values the same way the speculative path does. But the speculative path rejects `<= 0` ("does not allow any tokens to be scheduled"), while the new bound rejects only `< 0`. This off-by-one turned `0` into a real zero budget.
**Why a bound and not a scheduler change.** The scheduler treats a zero budget as "paused" on purpose, so its behavior is correct. A budget of zero tokens per step can never make progress, on any model or hardware. The sibling fields `max_num_batched_tokens` and `max_num_seqs` already use `ge=1`. The issue asks for a config-time `ValueError`.
**Not done.**
- I did not map `0` back to the default. That would silently replace a value the user set. The documented default is `None`, and the speculative path rejects `0`.
- I kept the speculative-path `<= 0` check. A user value can no longer reach it, but removing it is cleanup outside this fix.
**Limitation.** Setting the field after construction is not validated, because the `@config` dataclass does not validate on assignment. Today only internal code does this (`vllm/config/vllm.py` sets it to `max_num_batched_tokens`, which is `ge=1`), so no user route can reach the scheduler with `0`.
---
<details>
<summary> Pull Request Checklist </summary>
- [x] I used vLLM's `/pr-checklist` skill. (Mandatory for agents, optional for humans).
- [x] 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.
</details>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Vadim Gimpelson <vadim.gimpelson@gmail.com>
vadiklyutiy
requested review from
ProExpertProg,
WoosukKwon,
houseroad,
mgoin,
robertgshaw2-redhat,
tlrmchlsmth,
yewentao256 and
youkaichao
as code owners
October 8, 2026 20:47
njhill
approved these changes
Oct 8, 2026
Member
|
/ci run |
njhill
enabled auto-merge (squash)
October 8, 2026 23:48
|
✅ Triggered Buildkite CI #93730 for commit |
Contributor
|
Thanks for the nudge — regression test now added to #60399 (opened first, same fix). Suggest closing this as duplicate to keep review in one place. |
yewentao256
approved these changes
Oct 9, 2026
yewentao256
left a comment
Member
There was a problem hiding this comment.
LGTM, thanks for the work!
njhill
disabled auto-merge
October 9, 2026 18:13
Member
|
/ci retry |
|
✅ Triggered Buildkite CI #93895 for commit |
njhill
enabled auto-merge (squash)
October 9, 2026 18:14
Member
michael-gpu
pushed a commit
to practical-inference/vllm-ampere-extended
that referenced
this pull request
Oct 20, 2026
…hanging (vllm-project#60714) Signed-off-by: Vadim Gimpelson <vadim.gimpelson@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Hui Li <40770106+untrall@users.noreply.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.

Overview
max_num_scheduled_tokens=0passed config validation.LLM.generate()then hung forever with no error. This PR changes the field's bound fromge=0toge=1, so0is now rejected when the config is built.Fixes #60349. The bug was introduced by #44207.
Claims
max_num_scheduled_tokens=0now raises a pydanticValidationError(a subclass ofValueError) at config time, instead of a silent hang. The error names the field and the allowed range.0is rejected on every user route:SchedulerConfig,EngineArgs,EngineArgswith speculative decoding, and--max-num-scheduled-tokenson the CLI.None(the default),1and larger values work as before.Validation
Hardware and software: 1 × NVIDIA B200. vLLM at
43b4aaea3e40e20ef53ce2bfecfa1b72060bf2b5(before), and the same commit plus this patch (after). Python 3.12, pydantic 2.13. The bug was also reproduced in thevllm/vllm-openai:nightlyimage at the same commit.Reproducer (
repro.py):generate()made no progress for ~362 s untiltimeoutkilled it. No exception, no error log.EXIT_CODE=124Before:
After:
Other checks:
pytest tests/test_config.py -k scheduler_config:2 passed, 439 deselected. Withge=0put back, the new test fails withFailed: DID NOT RAISE ValidationError. I ran it with--noconftestbecause my local venv lacks the test dependencies fortests/conftest.py. The test is intests/test_config.py, which Buildkite already runs (.buildkite/test_areas/engine.yaml).pre-commit run --files vllm/config/scheduler.py tests/test_config.py: every hook passed or was skipped, including mypy.Config probe on the patched tree (no GPU work)
With ngram speculative decoding and
None, the value still resolves tomax_num_batched_tokens(2048), as before.Details
Root cause.
SchedulerConfig.max_num_scheduled_tokensisField(default=None, ge=0), so0passes. The only other check,<= 0inVllmConfig._set_max_num_scheduled_tokens(vllm/config/vllm.py), runs only when speculative decoding is on.Scheduler.__init__keeps the value because itis not None. So everyschedule()call starts withtoken_budget = 0. Both scheduling loops (running and waiting) run onlywhile token_budget > 0. No token is ever scheduled, the request stays inwaiting, and nothing raises. A debug run on the old code, with prints added to the scheduler, showed this on every step:Where it came from. Before #44207 (commit
51e0c579b), the field had no bound and the scheduler used a truthiness test, so0quietly fell back tomax_num_batched_tokens. #44207 addedge=0and changed the test tois not None. Its goal was to make the non-speculative path reject bad values the same way the speculative path does. But the speculative path rejects<= 0("does not allow any tokens to be scheduled"), while the new bound rejects only< 0. This off-by-one turned0into a real zero budget.Why a bound and not a scheduler change. The scheduler treats a zero budget as "paused" on purpose, so its behavior is correct. A budget of zero tokens per step can never make progress, on any model or hardware. The sibling fields
max_num_batched_tokensandmax_num_seqsalready usege=1. The issue asks for a config-timeValueError.Not done.
0back to the default. That would silently replace a value the user set. The documented default isNone, and the speculative path rejects0.<= 0check. A user value can no longer reach it, but removing it is cleanup outside this fix.Limitation. Setting the field after construction is not validated, because the
@configdataclass does not validate on assignment. Today only internal code does this (vllm/config/vllm.pysets it tomax_num_batched_tokens, which isge=1), so no user route can reach the scheduler with0.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.