Conversation
5b5ffcf to
8082193
Compare
…el_len
The waiting path pads a resumed decode request out to 1 + num_spec_tokens input
positions to preserve a full cudagraph, guarded by
num_computed_tokens + num_new_tokens > self.max_model_len
which permits num_computed_tokens + num_new_tokens == max_model_len. The step
then samples up to 1 + num_spec_tokens tokens, so _bookkeeping_sync's
assert end_idx <= self.max_model_len
fails by exactly one and takes down the engine:
RuntimeError: Sampled token IDs exceed the max model length.
Total number of tokens: 262145 > max_model_len: 262144
The running-request path already reserves that slot via
`- self.num_sampled_tokens_per_step`, so the two paths disagree by one token.
Writing C for num_computed_tokens and K for num_spec_tokens:
guard admits : C + 1 + K <= max_model_len
runner needs : C + 2 + K <= max_model_len
so the fault fires at exactly C == max_model_len - 1 - K.
Reserve the sampled token here too. On failure fall back to an un-padded step
rather than `break`: unlike the token-budget case this condition does not clear
on a later step, because num_computed_tokens cannot advance while the request is
unscheduled, so breaking would starve the request instead of letting it emit its
final token and stop on length. The cost is one cudagraph miss on the last step
of a request that is about to finish.
Reaching it needs speculative decoding plus a resumed request holding a single
uncomputed token at that exact position, which prefix caching or a KV-offload
tier makes routine on long-context serving.
Signed-off-by: Yifan Jiang <19356972+yifjiang@users.noreply.github.com>
8082193 to
56e7169
Compare
|
In case you have not seen it, njhill opened #53962 for the same bug and says there that it replaces this PR and #50342. It has the |
|
This pull request has merge conflicts that must be resolved before it can be |
|
Closing — superseded by #53962, which merged on 2026-08-27 and carries the same fix. Confirmed on Thanks @njhill for picking it up, and for folding in the One coverage note in case it is useful later: #53962's regression test asserts the async-scheduling path (negative |
Purpose
Fixes an engine-killing assertion when speculative decoding meets the context limit:
Seen twice in one day on a long-context production deployment (
max_model_len262144, MTP withnum_speculative_tokens: 1, prefix caching + CPU KV offload), under two different configurations.Root cause
Two paths size a decode step, and they disagree by one token.
The running path reserves room for the token the step will sample —
scheduler.py#L593-L600:The waiting path, padding a resumed decode request out to spec width for a full cudagraph,
does not —
scheduler.py#L967-L974:pad_spec_decodeschedules real spec slots(
#L1161-L1164), so the step samples up to1 + Ktokens and the assertion in_bookkeeping_syncfires(
gpu_model_runner.py#L3903-L3909).With
C = num_computed_tokensandK = num_spec_tokens:One apart, so it fires at exactly
C == max_model_len - 1 - Kand always overruns by one.Reaching it needs speculative decoding, plus a resumed request holding a single uncomputed token
(preempted and brought back with its prefix recovered — routine with prefix caching or a KV-offload
tier), sitting at that one position.
Fix
Reserve
num_sampled_tokens_per_stephere too, so both paths agree.On failure, fall back to an un-padded step rather than
break. Unlike the token-budget case, thiscondition never clears on a later step —
num_computed_tokenscannot advance while the request isunscheduled — so a bare guard fix would convert the crash into an indefinitely starved request. The
cost is one cudagraph miss on the final step of a request that is about to stop on length.
Test
test_resumed_decode_padded_to_spec_width_respects_max_model_len, parametrized overnum_spec_tokens∈ {1, 3}. Scheduler-only, no model execution. Places the request at the derivedboundary and asserts both that the step cannot overrun and that it is still scheduled.
Measured before/after (
max_model_len2048):Existing
tests/v1/core/test_scheduler.py: 129 passed. Run on v0.26.0, where this hunk is identicalto
main; I have not runmain's full suite locally.Separate from #52807 (offloading connector) — different subsystem, different failure.