Conversation
|
Added a second commit: [BI] Warn when a decode step can escape the cudagraph capture set. Pinning The new commit warns at config time when that combination is requested. It only Placement matters: the check sits after Verified: Separately, the root cause behind this whole issue has since been found, and This PR is still worth having on its own: it is the defence-in-depth layer, and |
|
Pushed Once Getting that measurement required stepping over this very pin, so the run carries So what changed is the justification, not the behaviour:
Reviewers who want the graph back on a model they have audited themselves now Measured cost of keeping it: none detectable on our workload — BI=1 warm median |
|
Pushed Owner decision, and the measurement supports it: with the indexer predicate fixed What went with the pin, and why each had to:
What stayed, because none of it depends on the pin: the capture-set escape Tests: One gap a reviewer should not inherit. The soak that justifies this ran with |
|
Retracting the "Not claimed" paragraph in That premise is wrong, and reading the kernel settles it without a GPU. Both if (seq_len <= TopK) { row_output[i] = (i < seq_len) ? i : -1; }which is exactly what It also means the two fixes are equivalent on this deployment The probe that motivated the caveat measured 321-361 bf16 ULP under a random Nothing in the code or tests changes — the retraction is of a caveat, not of a |
|
Dropped the multimodal encoder and Gemma4 dispatcher changes ( Both dropped changes existed because of the pin. The pin promised that no What is left is two changes to models nobody on this side has ever loaded. The Kept: microbatching (the only one that never consults Verification after the change: |
Piecewise cudagraph replay is not numerically equivalent to full-graph replay or to eager, and it is not batch-invariant on its own either. Since FULL_AND_PIECEWISE picks the per-step mode from batch properties (is the step a uniform decode? does it fit in max_cudagraph_capture_size?), a request's output ends up depending on its neighbours. Measured on DeepSeek-V4-Flash-Base TP4/EP4 with the batch composition pinned by a hand-driven LLMEngine (no HTTP races), victim token-1 logprob across neighbour counts: eager 19/19 identical; FULL 17/17 identical and bit-equal to eager; PIECEWISE not invariant; FULL_AND_PIECEWISE flipping between the FULL and PIECEWISE values step by step. Online this showed up as 29/75 checker rounds with a differing logprob, and 0/22 with --enforce-eager. Ruled out first, each with a single-variable experiment: cudagraph batch padding (dense capture sizes [1..128] still reproduce), the tile-scheduler plan family (graphs plus a forced skeleton plan still leaks at the same rate), and scheduling differences (eager is ~3.5x slower and changes co-scheduling, hence the pinned-composition harness). Pin cudagraph_mode to FULL under VLLM_BATCH_INVARIANT and stop the mixed-mode downgrade from handing piecewise back; it uses FULL_DECODE_ONLY instead. That keeps the decode-side graph win rather than forcing eager everywhere. Dynamic speculative decoding on the v1 runner needs piecewise, so that combination now raises instead of being silently overridden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 09b2ec8bd1359bda9f4b0f7162678b1ce7dbbccb)
Pinning cudagraph_mode to FULL only preserves batch invariance while every decode step stays inside the capture set. Above max_cudagraph_capture_size a step falls back to eager, and eager is not bit-identical to graph replay, so which side of that boundary a request lands on -- a function of how many requests share the step -- changes its output. The default capture sizes are min(max_num_seqs * decode_query_len * 2, 512 or 1024), i.e. 2x headroom, so this only fires when the capture set is overridden explicitly. Warn rather than raise: the remedy (a larger capture set) costs capture time and memory that the user should opt into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pinning cudagraph_mode=FULL under VLLM_BATCH_INVARIANT only guarded the mixed-mode downgrade. Two later branches in resolve_cudagraph_mode_and_sizes re-resolve to PIECEWISE without consulting the flag, so an unsupported backend or spec-decode query length silently reinstated the per-step mode choice the pin exists to remove. Both now drop to NONE under batch invariance: correct but eager, rather than fast and batch-dependent. A KV connector that requires piecewise for layerwise async operations hit the same problem from the other side -- it sets PIECEWISE before the pin runs, and the pin overrode a hard requirement. Refuse that combination instead. Also correct the capture-set warning. It ignored an explicit scheduler_config.max_num_scheduled_tokens, so a deployment that caps the token budget below max_num_batched_tokens could get a bogus warning. The comment claimed full cudagraphs and eager agree bit-for-bit; that is a per-model observation, not something config validation establishes, and it contradicted the warning's own premise. Say what the check actually covers. tests/test_config.py: parametrized regression over both re-resolution paths. Container run: pytest tests/test_config.py -k batch_invariant -> 7 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 2 of the audit found the pin still overrode two more downgrades. Pooling models and, on ROCm, decode/prefill context parallel all set PIECEWISE because full cudagraphs genuinely do not work there; pinning FULL afterwards reinstated an unsupported mode. Generalize the KV-connector check into _full_cudagraph_unsupported_reason() and refuse the whole class. The capture-set warning was also wrong in the other direction: it compared against the decode fan-out alone, but the defaults themselves cap at 512 (1024 on Blackwell), so a stock max_num_seqs=1024 deployment on H100 got a warning telling it to fix a configuration it never chose. Compare against what this deployment would capture by default instead, so only an actual shrink warns. Last bit-for-bit claim in compilation.py removed for the same reason as the one in vllm.py: whether graph and eager agree is a per-model property. tests/test_config.py: the quiet-path test is parametrized over max_num_seqs and pins the capability family so it does not depend on the host GPU. Negative control with the old criterion: max_num_seqs=1024 fails, 64 passes. 8 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 3 found the refusal added last round was still reachable around: pooling, KV connectors and the ROCm context-parallel hooks all downgrade to PIECEWISE, but set_splitting_ops_for_v1() runs before the batch-invariance pin and can raise that back to FULL (sequence parallelism, fuse_attn_quant). A check gated on "the mode still reads as piecewise" then sees nothing to refuse and the run proceeds on a mode the configuration cannot support. Check unconditionally. The capture-set warning gets narrowed rather than made cleverer. Three rounds of re-deriving "what the default would have been" produced three different wrong answers: it ignored the platform cap, then model-level overrides (GPT-OSS defaults to 1024 on non-Blackwell), then speculative decoding's query-length alignment. Record whether the deployment actually chose a capture envelope and only check explicit ones; a config that never chose has nothing to fix. The message now says what it can and flags the spec-decode caveat instead of pretending the number is exact. tests/test_config.py: refusal regression for the pooling case. 9 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 4: the flag distinguishing a deployment's capture configuration from a default was sampled inside _set_cudagraph_sizes, which runs after try_verify_and_update_config(). A model is allowed to write its own capture defaults there -- gpt-oss sets 1024 when the user set neither field -- so a stock gpt-oss deployment read as "explicitly configured" and got a warning about a configuration nobody wrote. Snapshot at the top of __post_init__. The refusal regression was also not discriminating: its input was already PIECEWISE so the pooling downgrade never happened, and enable_sp is disabled at tensor_parallel_size=1, so the mode never got raised back and the old piecewise-gated check would have passed it too. Use attention-quant fusion instead, which does raise the pooling downgrade back to FULL before the pin. Negative control against the piecewise-gated version: 1 failed. tests/test_config.py: the flag itself is covered directly, with capture sizes wide enough not to consume the warning the next test asserts on. 11 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The warning test's docstring still described the "below what this deployment would capture by default" criterion, which was replaced two commits ago because re-deriving the default kept getting it wrong. Say what the check does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 5: the flag distinguishing a deployment's capture configuration from a default lived on VllmConfig, which breaks twice. with_hf_config() rebuilds the config through replace(), which re-runs __post_init__ against already-resolved fields, so the rebuild read its own defaults as a choice; and vllm's replace() walks __dict__ expecting every entry to be a field, so a dynamic attribute there raises outright. Store it on the compilation config, which passes through replace() by reference, and only record it the first time. tests/test_config.py: the flag is asserted after a with_hf_config() rebuild, and a monkeypatched hook that writes capture sizes during try_verify_and_update_config stands in for gpt-oss so the ordering is pinned without needing that model. Negative control without the first-time guard: 1 failed. 12 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 5: the dynamic-speculative-decoding refusal was still nested inside the "mode still reads as piecewise" branch, so it was reachable around by exactly the path the other refusals were hoisted out of -- dynamic SD downgrades to PIECEWISE, attention-quant fusion or sequence parallelism raises it back to FULL, and by the pin there is no piecewise left to see. Fold it into _full_cudagraph_unsupported_reason() with the rest. The pin's comment also still asserted piecewise is not numerically equivalent to full or eager. On this model it is, once the indexer predicate is fixed. State the argument that survives: the per-step mode is chosen from batch properties, which is only harmless while every selectable path agrees, and nothing checks that here. 13 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 6 enumerated every cudagraph_mode writer and every "X cannot use full cudagraphs" rule, and found three the pin still walks past: The speculative-decoding proposers pick PIECEWISE whenever the target model's mixed mode is PIECEWISE *or FULL*, so pinning the target to FULL handed the draft path a piecewise dispatcher -- which then chooses graph or eager per step from the token count. That is the batch-dependent numeric path this whole change exists to remove, reintroduced one model down. Force NONE for the proposer under batch invariance. PCP rejects full cudagraphs in the worker on every platform, not just ROCm; the helper only checked it under is_rocm(), so CUDA MLA PCP was pinned to FULL and then crashed at worker init. Only the DCP downgrade is ROCm-specific. Voxtral Realtime asserts against any full mode at model construction. Explicit PIECEWISE worked before; batch invariance pinned it to FULL and the assert fired. Refuse it in the helper instead. tests/test_config.py: PCP regression through the helper. 14 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 7 found a second dispatcher the pin never reaches. The encoder batches this step's multimodal items together, picks a graph from their combined token count, greedy-packs to a graph size and falls back to eager over budget -- the same batch-dependent path selection, one level down and outside cudagraph_mode's reach. It is opt-in (cudagraph_mm_encoder defaults to False), so refuse the combination rather than disabling it behind the user's back. Gemma4's self-managed centroid graph is the remaining one of this shape and is recorded as a known gap; it is not reachable from config alone. 15 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 8 enumerated every scheduling point that picks a graph per step. Three were still outside the pin's reach: Gemma4's MTP proposer captures centroid graphs at fixed sizes at load time and replays whichever fits the step's token count, eager above 64. It never consults cudagraph_mode, so the proposer PIECEWISE ban did not reach it. Skip the capture under batch invariance -- lighter than refusing the model. Microbatching decides per step whether to split and which graph to replay. Opt-in (enable_dbo / ubatch_size), so refuse. The multimodal encoder refusal added last round was too broad: the flag is inert on a text-only model, so it now only fires for multimodal ones. tests/test_config.py: both refusals through the helper, plus the text-only case that must stay quiet. 15 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 9: the multimodal-encoder and microbatching refusals were added inside _full_cudagraph_unsupported_reason(), which only runs when the main cudagraph_mode is not NONE. But neither of those consults cudagraph_mode at all -- the encoder manager keys on cudagraph_mm_encoder and multimodal support, and microbatching on enable_dbo / ubatch_size. So under --enforce-eager both kept scheduling their own graphs while the refusals never ran, and the message's "run with --enforce-eager" was the wrong remedy for exactly those two. Check them up front instead, ungated. Skipping Gemma4's centroid capture also changed behaviour rather than just disabling a graph: with no sizes captured, _greedy_sample fell through to the base class and its full-vocab argmax instead of the model's own centroid top-k. Keep the eager centroid path -- the same computation, no graph -- and route the over-the-largest-size case through it too, which was the pre-existing fallback. tests/test_config.py: the encoder refusal is now asserted with cudagraph_mode NONE as well, which is the case that was slipping through. 15 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The config-time refusal for cudagraph_mm_encoder rejected every multimodal model, but the dispatcher is only created for models that also implement SupportsEncoderCudaGraph -- Llava is multimodal and does not, so a configuration where the flag was inert now failed to start. That capability is not knowable until the model is loaded. Handle it where it is: _create_encoder_cudagraph_manager returns None under batch invariance, with a warning naming the reason. Same treatment as Gemma4's centroid graphs, and for the same reason. Microbatching keeps its config-time refusal -- enable_dbo / ubatch_size is an exact condition, and it is equally ungated on cudagraph_mode. tests/test_config.py: microbatching refused under both FULL and NONE, and the encoder flag asserted *not* to be a config-time error. 15 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The batch-invariance check ran before supports_encoder_cudagraph(), so a multimodal model that does not implement the protocol -- and would never have got this manager -- was still told its encoder cudagraphs were being disabled. Move it after, so only deployments that would really have had one hear about it. tests/v1/determinism/test_batch_invariant_dispatchers.py: the decision points for the three dispatchers that schedule graphs outside cudagraph_mode's reach, as unit tests over the branches themselves -- no weights, no GPU. Covers the encoder ordering (with and without the capability), Gemma4's centroid capture including that skipping it keeps the eager centroid path rather than the base class's argmax, and microbatching's refusal under both FULL and NONE. 7 passed; test_config.py -k batch_invariant still 15 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The B200 batch-invariance job lists its test files one by one; source_file_dependencies only decides whether the job runs, it does not discover new files. This one was never executed by any job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…anges None of the six files this PR changes are under the paths the B200 Batch Invariance job watches. The job runs here only because the PR adds a test file; a later change to the cudagraph-mode resolution, to a spec-decode proposer, or to the encoder manager would not run the tests written for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The warning said piecewise cudagraphs break batch invariance. On DSv4 they no longer do: once the indexer's short-context predicate is bounded on a static length, a 50-round soak under FULL_AND_PIECEWISE is bitwise clean, where the same soak leaked at a 38.7% per-round rate before. So the pin is a policy, not a measured necessity, and the message should say the hazard instead of asserting a breakage that has been measured away. What makes a piecewise mode unsafe is that a request's numeric path follows the token count of the batch it lands in, which is only harmless while every selectable path is bit-identical -- and that holds here because no host-side value read inside a captured region is frozen against the capture-time dummy batch. Nothing in this function can check that for the next model or the next patch, so the pin stays. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The pin dropped any piecewise mode to FULL under VLLM_BATCH_INVARIANT, on the premise that a per-step mode chosen from batch properties cannot be invariant. That premise was measured and does not hold here: with the indexer's short-context predicate no longer frozen against the capture-time dummy batch (upstream vllm-project#52492), a 50-round soak of the full DeepSeek-V4-Flash-Base under FULL_AND_PIECEWISE is bitwise clean, where the same soak leaked in 38.7% of rounds before. Keeping the pin now costs the mixed-step graph for a guarantee it was not providing. So the mode is left alone and the warning carries what the pin used to: a piecewise mode is invariant only while every path it can select is bitwise equal for that model, which is verified for DeepSeek-V4 and unknown elsewhere. Removed with it: - the refusal for configurations that can use neither piecewise nor full, which existed only because the pin would otherwise undo their downgrades; - `_full_cudagraph_unsupported_reason`, whose only caller that refusal was. vLLM's own downgrades to piecewise now stand unopposed, which is what they were written to do; - the batch-invariant branches in `resolve_cudagraph_mode_and_sizes`, so those fallbacks resolve identically with and without invariance. Two tests now assert exactly that, rather than asserting the divergence. Kept: every check that does not depend on the pin -- the capture-set escape warning, and the refusals for microbatching, prefill context parallel, the multimodal encoder dispatcher and the speculative-decode dispatchers. Those close paths where a graph is chosen per step without consulting cudagraph_mode, which no mode setting can fix. Tested: tests/test_config.py -k the cudagraph/batch-invariant selection, 13 passed. Negative control: re-inserting the pin fails exactly the two mode-survives cases and nothing else. Not claimed: the soak that justifies this ran with the predicate bounded on a static length, so both paths did full scoring. vllm-project#52492 instead keeps the eager shortcut, and graph and eager then agree only while "candidates <= topk" makes the shortcut's set equal to the real top-k -- an equal set is not an equal order, and the order sensitivity of FlashMLA sparse decode is unresolved in our measurements. A deployment enabling piecewise on top of vllm-project#52492 should re-run that soak rather than inherit this result. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Both were found while closing every way to escape the FULL pin: the pin promised that no numeric path is selected from the batch, so every dispatcher that schedules graphs of its own had to be shut too. The pin is gone -- a 50-round soak showed DSv4's piecewise paths are bitwise equal -- and with it that obligation. What replaced the pin is a warning, which makes no promise on the deployment's behalf. That leaves two changes to models nobody here has ever loaded. The Gemma4 one is not a one-liner either: skipping the centroid capture requires an eager centroid path of its own, or the base class silently substitutes a full-vocab argmax. Changing numerics on unrun models is not something the submitter can defend, so remove them; the analysis is kept as a draft for whoever has the hardware. Kept: microbatching (never consults cudagraph_mode, and --enforce-eager does not reach it either), the speculative-decoding proposer, the capture-envelope warning, and the piecewise warning. tests/v1/determinism/test_batch_invariant_dispatchers.py keeps the microbatching case; the two removed decision points take their tests with them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6494fe8 to
b518bcc
Compare
|
Rebased onto Two things upstream did for us in that window:
Post-rebase verification: |
Four issues from an external review of this branch. Microbatching: the refusal fired on `use_ubatching`, which is the configured flag, not whether a step can be split. `should_ubatch` stays False unless data_parallel_size > 1, and check_ubatch_thresholds needs a step at least as large as one of the two thresholds -- unreachable once both exceed the token budget. Both cases were a startup failure for a configuration that could never microbatch. Refuse only when it can actually run. Capture envelope: `num_speculative_tokens` is the global maximum, but dynamic speculative decoding pairs the large draft counts with small batches, so max_num_seqs times the global maximum is a step the schedule forbids -- and the warning then fired on an envelope that already covered every real step. Take the largest step the schedule allows, in a helper so the arithmetic is testable without a draft model. Multimodal encoder: it greedy-packs the images scheduled this step and picks a captured budget from their combined token count, consulting neither cudagraph_mode nor --enforce-eager, so nothing here reached it. Warn. Speculative decoding: the two draft-side dispatchers this branch switches off are the legacy ones; the V2 speculators build cudagraph managers of their own that this check does not reach. Say so, rather than let partial coverage read as coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The _largest_uniform_decode_step helper was added between @config(config=ConfigDict(...)) and class VllmConfig, so the decorator wrapped the function and the class was never registered: importing vllm.config died in pydantic with "'function' object has no attribute '__bases__'". Move the helper above the decorator and assert VllmConfig is still a dataclass, since nothing else in the file fails in a way that names the cause. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VllmConfig has its own, earlier assertion that microbatching needs a DeepEP/nixl all2all backend. The refusal test passed only because the batch invariance check fired first, and the new "does not refuse what cannot run" test hit that assertion instead of exercising the check at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What
Batch invariance is an absolute claim: a request's output must not depend on
which other requests share its engine step. Several components schedule
cudagraphs of their own, keyed on what the step happens to carry, and
cudagraph_modedoes not reach them. This closes the ones whose condition isexact, and warns where the answer is a per-model property config validation
cannot establish.
graph to replay, and never consults
cudagraph_mode— so unlike everythingelse here it is not gated on the mode, and
--enforce-eagerdoes not switchit off either. Opt-in (
enable_dbo/ubatch_size), and the condition isexact, so refuse rather than silently disable.
NONE. They pick PIECEWISEwhenever the target's mixed mode is PIECEWISE or FULL, which puts a
per-step graph/eager choice back on the draft path.
sets
max_cudagraph_capture_sizebelow its own largest decode step, batchesabove it fall back to eager, so which numeric path a request takes depends on
how many requests share its step. Only an explicit choice is checked — the
defaults come from the platform, the model (some override the cap) and, under
speculative decoding, from query-length alignment, so re-deriving "what the
default would have been" only produces false alarms on stock configs. The
answer is snapshotted before
try_verify_and_update_config()can let a modelwrite its own capture defaults, and stored so it survives
replace().from batch properties, so a request's decode step runs under a path its
neighbours chose. That is harmless exactly while every selectable path is
bit-identical for this model — verified on DeepSeek-V4, unknown elsewhere.
cudagraph_mode=FULLremoves the choice.Why the mode is no longer pinned
An earlier version of this PR pinned
cudagraph_modetoFULLunderVLLM_BATCH_INVARIANT. The leak that motivated it turned out to be aDeepSeek-V4 model bug — a host-side indexer predicate evaluated once against
the capture-time dummy batch and baked into the graph — fixed upstream in
vllm-project#52492. With that fixed, a 50-round soak of the full 43-layer model under
FULL_AND_PIECEWISEis bitwise clean, where the same soak leaked in 38.7% ofrounds before. Pinning would now cost the mixed-step graph for nothing, so the
mode is left alone and the burden moves to the warning: a deployment that has
not checked its own model should not read silence as safety.
Tests
tests/test_config.py -k batch_invariant→ 13 passed, covering: therequested piecewise mode survives; the warning fires;
resolve_cudagraph_mode_ and_sizesis no longer special-cased (BI on and off return the same mode);microbatching is refused under both
FULLandNONE; the capture-envelopewarning fires on an explicit undersized choice and stays quiet on defaults; and
a model-written capture default is not mistaken for a deployment choice.
tests/v1/determinism/test_batch_invariant_dispatchers.py→ 1 passed: themicrobatching decision point as a unit test over the branch itself, no weights,
no GPU.
Negative control: with the old pinned build in place, 6 of those 13 fail — the
assertions are testing the removal, not restating it.
Non-BI behaviour unchanged, checked explicitly: with the flag off, both
piecewise-bearing modes survive and the mixed-mode downgrade still picks
FULL_AND_PIECEWISE.CI: the determinism suite now also runs when
vllm/config/,vllm/v1/spec_decode/orgpu_model_runner.pychange — previously it onlytriggered on kernel changes, so config-level regressions here were untested.
Model evaluation
Not applicable to the current contents: nothing here changes numerics on any
configuration we run (microbatching off, speculative decoding off, capture
sizes default). The piecewise measurements that justify not pinning are in
the soak referenced above — full 43-layer DeepSeek-V4-Flash-Base TP4/EP4,
50/50 rounds with 0 differing cases under
FULL_AND_PIECEWISE, and GSM8K1319/1319 per-question agreement across two independent re-runs at concurrency
64.
Scope, and what was deliberately dropped
Two more dispatchers of the same shape were found and then removed from this
PR (2026-08-18): the multimodal encoder's cudagraph manager, and Gemma4's
self-managed centroid graphs. Both were motivated by the pin — it promised no
numeric path is selected from the batch, so every escape had to be closed. With
the pin gone that obligation goes too, and both would change numerics on models
the submitter has never loaded (the Gemma4 one is not a one-liner: skipping the
capture needs an eager centroid path of its own, or the base class silently
substitutes a full-vocab argmax). They are recorded for whoever has that
hardware.
Review follow-up
An external review (codex, gpt-5.6) raised five points on this branch; each
was checked against the source before acting.
use_ubatching, theconfigured flag, not on whether a step can actually be split.
should_ubatchstays False unless
data_parallel_size > 1, andcheck_ubatch_thresholdsneeds a step at least as large as one of the two DBO thresholds — unreachable
once both exceed the token budget. Either case was a startup failure for a
configuration that could never microbatch. Now refused only when it can run.
max_num_seqsbythe global
num_speculative_tokens. Dynamic speculative decoding pairs thelarge draft counts with small batches, so that product is a step the schedule
forbids, and the warning fired on an envelope that already covered every real
step. The estimate now follows the schedule, extracted into
_largest_uniform_decode_stepso the arithmetic is testable without a draftmodel.
cudagraph_mm_encodergreedy-packs the images scheduledthis step and picks a captured budget from their combined token count,
consulting neither
cudagraph_modenor--enforce-eager. Warn rather thanrefuse: it is opt-in and no model exercised here has an encoder, so a refusal
would be a promise about a deployment that was never tested.
legacy ones; the V2 speculators (
AutoRegressiveSpeculator,DFlashSpeculator) build cudagraph managers of their own that this checknever reaches, and upstream [Feature]: Batch Invariant Feature and Performance Optimization vllm-project/vllm#27433 records that batch invariance under
speculative decoding is unfinished. Said so, rather than let partial coverage
read as coverage.
of gap, and skipping their capture needs an eager centroid path or the base
class silently substitutes a full-vocab argmax. Changing numerics on a model
nobody here has run is not something the submitter can defend; the analysis is
kept as a draft.
The review also confirmed clean:
_capture_sizes_user_specifiedis invisible todataclass equality and
compute_hash(), surviveswith_hf_config()and theengine's deepcopy/cloudpickle round-trips, and the snapshot ordering does
separate a CLI choice from GPT-OSS's model hook.
Notes
this is about dispatchers that schedule graphs outside
cudagraph_mode'sreach.
vllm/config/vllm.py,vllm/v1/spec_decode/*and the two test files only).🤖 Generated with Claude Code