Repository navigation
Unify the spec-decode drafter toggle across all 27 vLLM composes - #1048
Merged
Merged
Conversation
Turning the drafter off is the documented mitigation for vllm#50021, but it
did not work as documented on any compose that ships one. The knob had
accreted across three generations and was never reconciled:
hardcoded (10) no runtime escape at all -- the header told you to delete
a line from the YAML. Includes vllm/dual, the blessed
(qwen3.6-27b, vllm, dual) default.
SPEC only (5) SPEC_N=0 silently ignored
SPEC_N only (9) SPEC=off silently ignored
n=0 passthru (3) flag still sent, with num_speculative_tokens:0
Plus SPEC_N_MAX (3 Gemma) and NUM_SPEC_TOKENS (Nemotron) as further names.
Applying the wrong variable produced a healthy-looking server with the
drafter still running -- the failure was silent, which is what made this a
safety bug rather than an inconsistency.
One contract now, on all 27: SPEC_N=<n> sets depth, SPEC_N=0 disables,
SPEC=off remains an alias. Every compose keeps its own measured default
depth (asserted per-file before/after; none moved). Legacy alias names still
resolve. A non-numeric SPEC_N is a hard boot error, never a silent "off".
scripts/tests/test-spec-toggle-contract.sh enforces it by extracting each
entrypoint, filtering the env exactly as docker does, running it with vllm
stubbed, and parsing the resulting --speculative-config as JSON. Seven
self-tests -- one per shape this repo actually shipped -- run before the
real scan and fail as "the gate itself is broken".
spec-sweep.sh uses one variable for every arm including n=0, retiring the
two-variable dance and the SPEC-OFF-IGNORED workaround it carried for this
exact bug. swap_apply.py recovers the drafter from the sibling entrypoint
now that command: no longer carries it, patching that script in place so
NVLink detection and patch installs survive in brought-weights composes.
Verified live on 2x 3090: vllm/dual with SPEC_N=0 propagates into the
container, emits zero --speculative-config in the engine argv, and serves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test-spec-sweep asserted the literal old two-variable plan (SPEC=off for the n=0 arm, SPEC_N_MAX=<n> for the rest). These are hand-written fixtures, not registry-derived, so the suite could not catch the drift itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 13, 2026
A1RM4X
pushed a commit
to A1RM4X/club-3090
that referenced
this pull request
Sep 14, 2026
vllm/gemma-26ba4b-single defaults to SPEC_N=4 and needs an external MTP drafter that setup.sh fetches only under WITH_ASSISTANT_DRAFT=1 — a flag that existed in the model profile, in setup.sh's implementation and in a test, and in no user-facing surface at all, while its siblings WITH_DFLASH_DRAFT and WITH_VISION were at least in the file's header comment. A user who did not set it got a bare missing-config.json out of vLLM naming neither the cause nor either fix, and reasonably concluded the download should have been automatic. There was already a working escape hatch — SPEC_N=0, standardised across every vLLM compose by noonghunna#1048 — and nothing in the failure path mentioned it either. Three surfaces, working outward from the failure: - The compose refuses to boot when SPEC_N>0 and the drafter directory has no config.json, and its message names BOTH routes: re-run setup with the flag, or boot drafter-less with SPEC_N=0. Same fail-loud contract the deepseek moecache composes carry for their external GGUF (noonghunna#1054). The header's Drafter line now says how to obtain the file, which it previously described without sourcing. - `setup.sh --help` grows an "Optional extra downloads" section. The header comment was never the right surface — reading it means already having opened the script. WITH_PRISM_EAGLE3 was undocumented in both places and is added too; the new gate found it. - The header comment gains the two flags it was missing. test-setup-optin-flags-documented asserts the general form: every WITH_* flag setup.sh reads in code must be named in --help. Comment-only mentions are excluded, so a flag another script owns cannot satisfy it. test-compose-drafter-preflight executes the entrypoint with vllm stubbed and asserts three things: a missing drafter refuses and names both routes; the check is inert when the drafter is present; and SPEC_N=0 still boots with nothing on disk — the escape hatch must not require the download it exists to avoid. test-spec-toggle-contract needed one change to be able to see any of this: it runs entrypoints in a sandbox, and a fail-loud check on a path embedded in the entrypoint (rather than in `command:`, which it already stubs) made the compose exit before emitting argv. It now redirects the container HF cache into the sandbox and materialises the model dirs the body names. Verified: both new gates fail against the pre-fix tree — the flags gate names all four flags as undocumented, and the preflight gate shows --speculative-config reaching vLLM pointing at a directory that does not exist. The spec-toggle change is load-bearing, not cosmetic: the old gate reds against the new compose. Not boot-tested — no weights for this model here, and the entrypoint was exercised with a stub rather than a container. Fixes noonghunna#1304 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.
The problem
Turning the speculative-decoding drafter off is the documented mitigation for vllm#50021 — the GDN spec-decode wild write that kills workers under sustained multi-turn traffic. It is referenced as such in
DUAL_CARD.md,MULTI_CARD.md,FAQ.mdand a dozen compose headers.On every one of the 27 composes that ship a drafter, that mitigation did not work as documented. The knob had accreted across three generations and was never reconciled:
SPEConlySPEC_N=0silently ignoredSPEC_NonlySPEC=offsilently ignoredn=0passthroughnum_speculative_tokens:0Two more names were in play beyond those:
SPEC_N_MAX(3 Gemma composes) andNUM_SPEC_TOKENS(Nemotron).The silent half is what makes this a safety bug rather than an annoyance. A user who learns
SPEC=offon one slug, applies it to another, and sees a healthy server has not mitigated anything. Qwen3.8 is where it surfaced because it is the one model whose slugs span two conventions.Worst case is
vllm/dual— the blessed(qwen3.6-27b, vllm, dual)default. Its own header read:That is not scriptable, not
.env-able, lost ongit pull, and it dirties the working tree so the next pull conflicts.The change
One contract, on all 27:
n=4wheren=4was measured (the ⚡ Qwen3.8-27B part 2 — the fast tier lands at 105 tok/s, plus NVFP4 #1024 knee),n=3/n=2elsewhere. Asserted per-file before/after; no default moved.SPEC_N_MAX/NUM_SPEC_TOKENSstill resolve, so nothing anyone has pinned breaks.SPEC_Nis a hard boot error, not a silent fall back to "off". A typo can never quietly leave the drafter running, nor quietly disable it.environment:declaration, so the two cannot drift.scripts/spec-sweep.shnow uses one variable for every arm includingn=0, replacing the two-variable dance and theSPEC-OFF-IGNOREDworkaround it carried for exactly this bug.swap_apply.py(brought-weights / c3 ② Serve) recovers the drafter from the sibling's entrypoint now thatcommand:no longer carries it. It patches that script in place rather than rebuilding it — the naive version would have replaced the entrypoint wholesale and silently dropped NVLink detection and patch installs from every brought-weights compose. Verified:vllm/dual→mtp/n=3 andvllm/qwen38-27b-dual-fast→mtp/n=4, both retainingdetect_nvlinkand theirinstall.shcalls.The gate:
test-spec-toggle-contract.sh(suite 103 → 104)It does not grep. For each compose it extracts the entrypoint and
command:, applies compose interpolation, filters the env exactly as docker does, runs the script withvllmstubbed, and inspects the argv the engine would receive — then parses--speculative-configas JSON. No GPU, no weights, no image.Seven self-tests run before the real scan, each a shape this repo actually shipped: hardcoded,
SPEC-only,SPEC_N-only,n=0passthrough, undeclared env, malformed payload, plus a passing control. They fail as "the gate itself is broken".Two bugs the process caught, worth recording
1. A green gate over a dead feature. My first version passed 27/27 while every compose was still broken — it applied env directly to bash, but docker only forwards variables declared under
environment:, and 21 composes declared neither knob. A live boot ofvllm/dualwithSPEC_N=0loggeddrafter ON n=3and exposed it. The gate now reproduces docker's filtering, and a self-test covers it.2. A payload that looked right and wasn't. 16 composes briefly carried a stray list-item prefix (
"- '{...}") that kept a readable token count under a regex while being unparseable JSON — vLLM would have failed at startup. The gate nowjson.loadsthe payload and checks its keys.Both are the same lesson: a check that only ever passes is indistinguishable from no check. Each fix has a negative control that was confirmed to fail before it was trusted.
Verification
test-spec-toggle-contract— self-test 7/7, 27/27 composes, and confirmed to fail when the bug is reintroduced (all three classes tested individually).vllm/dualon 2× 3090 — the compose that previously had no escape:SPEC_N=0→[spec] drafter OFF, no--speculative-configin engine argv, serves[spec] drafter ON - mtp n=3, drafter present, servesNot covered
The llama.cpp family has the same bug and is untouched here. No llama.cpp compose gates on
SPEC, sospec-sweep.sh'sn=0arm for that engine has always booted spec-ON (its acceptance guard flags it). Those use a different mechanism (MTP_DRAFT_N_MAX/--spec-draft-model) and I could not verify a change without booting them, so it is documented in place rather than guessed at. Tracked as #1049.The
n=3vsn=4question —n=4is measured on the AutoRound tier,n=3on fp8/nvfp4 is inherited and unswept — is a separate measurement job, not touched here.