feat(SPEC-DSPARK-QWEN3-ROUTING): give a Qwen3 DSpark draft an explicit route, and refuse the DeepSeek-V4 lane by name - #1248
Merged
Conversation
…, and name the DeepSeek-V4 refusal A DSpark draft whose config declares `architectures=["DSparkDraftModel"]` with `model_type` `qwen3` had no route. It loaded as a Qwen3 draft because the pinned rewrite at `vllm/config/speculative.py:934-944` was never ported, not because anything decided it. `SpeculativeConfig::IsDsparkDraft` had no production caller either: every reference outside its own header was a test, and `ResolveSpecConfig` branched on the CLI method string alone. The architecture string therefore had two possible destinations and the loader read neither. Three parts. `IsDsparkDraft` takes the draft's `model_type` and accepts the `DSparkDraftModel` + `qwen3` pair. A new `ResolveDsparkArchitecture` ports the normalization upstream performs before the draft loads. `ResolveSpecConfig` reads the draft's own `config.json` and calls both, so the loader classifies the checkpoint instead of trusting the flag. Both are mirrored AHEAD of the pin `555967922`, marked BEYOND-PIN, from vllm-project/vllm#52197 merged 2026-08-17 at `7075ddac28c25d4fd2b84bc2a9a6c5ffde0345c8`. The pinned behavior is wrong for a checkpoint that is published and loads here today, so porting it faithfully would break a working load. `.agents/specs/dspark-qwen3-routing.md` §2 carries that decision and its three model-matrix precedents. ONE TRACKED DIVERGENCE. Upstream's fallback rewrites any other DSpark draft onto `model_type` `deepseek_v4` and lets the DeepSeek-V4 path take it. This engine carries only a `DeepseekV4Model` stub for that path, and the lane needs two Sparks, so mirroring the rewrite would fail on an internal check instead of naming the missing arm. Both classification arms therefore REFUSE by name, as AGENTS.md requires of an unimplemented arm. §7 R2 records the decision. RED first. `tests/vllm/entrypoints/test_dspark_draft_routing.cpp` enters through the `LoadedEngine` constructor, which is where `ResolveSpecConfig` is called, because a unit test on the predicate proves the function works and never that anything reaches it. Its two refusal cases failed on an ABSENT refusal before this change, and `ResolveDsparkArchitecture` did not compile because it did not exist. Deleting the classification block again turns both refusal cases red, and reverting the architecture pair turns the routing case red on both files, so neither test measures a class. The draft directory in the reachability test deliberately carries no "dspark" substring. With it, the model-id arm of the predicate answers first and the architecture arm is never consulted, which is the mute switch this test set had to avoid. `docs/FEATURES.md` gains a routing row rather than a clause on the DSpark one, because that cell is already at the table's 220-character limit and a longer one fails `check-public-doc-tables`. `RadixArk/Qwen3.8-27B-DSpark` @ `85ef153be924f17ce4bf62726954eeaa4a73e854` is the checkpoint that declares the pair, and `docs/USAGE.md` now pins its revision, size and sha256. It has NOT been run here. The token-exact gate needs a 2.53 GiB download and GPU time, both pending developer authority, so the routing is gated on CPU and the decode stays owed. Issue: #1193 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…routing change The branch was cut at 5073df6 and origin/main has moved. Merging keeps the row's gate honest: the trailer gates refuse to run while the branch is behind, and the focused gate has to be rerun over the merged tree rather than over the base it was written against. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…ared ProcessId helper The reachability test was written before `tests/support/process_id.h` landed on main, so it seeded its temporary directory name from `std::random_device`. That works, but it is a second spelling of a problem this tree has now solved once. The helper's own header says the class comes back whenever a new loader test copies the temp-directory idiom from the last one, and this file is exactly that copy. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
… no architecture The classification as first written refused on the ABSENCE of evidence. A draft whose `config.json` carries no `architectures` key reached `ResolveDsparkArchitecture` with an empty list, which is upstream's DeepSeek-V4 catch-all, so it was refused by name. Upstream can afford that: it reads the key off a HuggingFace `ModelConfig` where an absent key is `[]`, and it has the DeepSeek-V4 path to fall into. Here it is a regression risk against a lane that already runs. The native `deepseek-ai/dspark_qwen3_*_block7` drafts have not been read on this host, and their repo ids all contain "dspark", so the identity check passes on the name and the architecture normalization is the only thing between them and a refusal. Refusing a checkpoint whose contents nobody has looked at is not mirroring upstream, it is guessing. So the loader now classifies only a config that DECLARES an architecture. `ResolveDsparkArchitecture` keeps upstream's catch-all unchanged, because that function is the port and its unit cases pin it. The narrowing is the loader's, it is gated by a case that goes red when the guard is removed, and it is recorded under `## Owed` in the spec together with the reading that would settle it. The same section now records a second residual found while writing this: on the `FromModelDir` path `maybe_load_dflash()` runs before the engine constructor, so `LoadDsparkDraft` reports its own missing-key error before the named refusal is reached. Closing that needs a second call site and a real draft checkpoint, so it waits on the authority G5 waits on. Issue: #1193 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…routing change The branch was two commits behind when the review finished, and one of them is `GATE-SYMBOL-ANCHORS` (c20018f), a NEW checker that gates every cited symbol against the tree. This change cites many — `IsDsparkDraft`, `ResolveDsparkArchitecture`, `ResolveSpecConfig`, `LoadDsparkDraft`, `maybe_load_dflash`, `FromModelDir` — so the checker has to run over the merged tree rather than over the base the row was written against, and the repairs that follow have to be written against whatever it says. `FIX-GATE-COMMANDS-PROSE-PIN` (7271639) rides along; it touches no file this row owns. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…, and say what the change actually reaches A fresh review found four things, and the first one is the load-bearing one. ## The gate could not see its own subject The accept-set of `ResolveDsparkArchitecture` is a strict SUBSET of `IsDsparkDraft`'s true-set, so the identity check can never change accept versus refuse — it changes only WHICH refusal the user reads. Both refusals contain "DeepSeek-V4" and "not implemented", and those two substrings were all the tests asserted. So the reviewer deleted the entire `IsDsparkDraft` call and both suites stayed green; making the predicate return constant `true` also left the routing suite green. The commit body, the spec, and this row's whole justification rest on that call being load-bearing, and nothing measured it. The repair is in the tests, not the code: each case now asserts the wording of the arm it is supposed to reach — "does not identify as" for the identity refusal, "routes to the DeepSeek-V4 DSpark lane" for the normalization one. Both mutations were rerun in a scratch copy and both are now RED, with `compile_rc=0` so neither is a build failure wearing a pass. A third mutation was added for the arm this commit changes: deleting the `ResolveDsparkArchitecture` call turns the routing suite red too, so the call that stays is measured rather than assumed. ## A branch nothing could enter `ResolveDsparkArchitecture` is total: it answers "Qwen3DSparkModel" or it throws. The `lane != "Qwen3DSparkModel"` guard at the call site was therefore unreachable, and the reviewer's mutation deleting it left both suites green. `AGENTS.md` permits landing an unreached slice only when the commit body, the pull request body and the spec's `## Owed` all name it, and this branch was named in a source comment instead. It is DELETED rather than disclosed, because the staged-slice exception is for work a named row will wire and no row will wire this: the branch becomes live only when a second draft lane exists, and the change that adds that lane is the change that should add its dispatch. The call stays, for its refusal. ## The records said the opposite of the tree `.agents/engine-matrix.md` still stated, in the present tense, that `IsDsparkDraft` has no production caller and that no code reads a draft config's `architectures` key. Both stopped being true when W1-W4 landed. The row now reads before-and-after, carries its code and evidence anchors instead of two dashes, and moves `READY` -> `ACTIVE`, which is what a row with W1-W4 landed and W5/W6 owed actually is. The lifecycle move brings what `AGENTS.md` `## Public documents` requires with it: `docs/STATUS.md`, a `PENDING` row in `docs/BENCHMARKS.md` that says there is no number and why, the spec's `## Now`, the summary counts in the matrix, and a claim file for the `ACTIVE` owner. ## The disclosure was wrong in kind, not in degree The `## Owed` entry said the named refusal "only leads on the in-memory constructor path", which reads as one of two production paths. It is not a production path at all: the in-memory `LoadedEngine` constructors have no caller in `src/`, `include/` or `examples/`, and the one production constructor, `FromModelDir`, runs `LoadDsparkDraft` BEFORE the member-init that calls `ResolveSpecConfig`. So for the DeepSeek-V4 shape the user still reads `LoadDsparkDraft`'s missing-key error. The entry now says that plainly. The second call site stays owed with G5, which is what §3's port map scoped. ## Also in this commit The header claimed ONE tracked divergence. There are two: the DeepSeek-V4 refusal, and the collapse of upstream's Gemma4 branch onto `Qwen3DSparkModel` — upstream leaves `Gemma4DSparkModel` in place because it has a class to dispatch to, and this engine has one DSpark draft lane. That collapse is exactly what made the deleted branch unreachable, so leaving it unlabelled hid the reason. `docs/FEATURES.md` said vLLM ✅ for this routing. True of upstream head, false at our own pin — which is this row's entire point — so the cell now says both. The uncited SGLang ◐ becomes "not assessed". A Speculators-layout draft now has a loader case. That layout declares no top-level `architectures` and is translated to `["Qwen3DSparkModel"]` before the key is read, so it is the one shape where the no-architecture narrowing and the classification disagree and the ORDER decides. It classifies normally, and now something proves it. The spec gains the `## Outcome` section §3 and §7 R3 owe: the draft config is read TWICE rather than hoisted, because the two reads sit on opposite sides of the engine constructor and a shared read would need a wider public seam — the growth §9 names as a stop condition — for a five-layer config parsed twice on a path that then reads a 2.53 GiB shard. `docs/USAGE.md` gains the same correction in the user's own terms: point the server or the C API at a DeepSeek-V4 draft today and the message is the draft loader's missing-key error, not the named refusal, because the draft loads before the speculative config resolves. A user who reads a page that promises one message and gets another has been told the wrong thing. The branch name `row/DSPARK-QWEN3-ROUTING-IMPL` does not match the `row/<ID>` form. Recorded in the spec rather than renamed, because renaming mid-flight strands the review anchored to it. Issue: #1193 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…repaired routing change `origin/main` moved twice while the review findings were being repaired, and a branch that is behind is a branch whose gate did not run: `agent-preflight.sh` SKIPS both trailer gates in that state and reports nothing about the tree. This merge exists so the full gate has something honest to run over. Neither incoming commit touches a file this row owns — `MODEL-FP8-BLOCK-WEIGHT` (0959710) and the MUSIC3 stage instrumentation (aba8d5f) — and `merge-tree --write-tree` reported no conflict before the merge was taken. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…s the box rather than the branch G4 was the last gate row left as "owed" because a gate you have not run is not a gate, and the branch was behind `origin/main` twice while the review findings were being repaired. It has now run over the merged tree. Every gate reports `ok` except `test_cpu_x86_llamacpp_floor`, which refuses to measure while the machine is loaded and exits `NO_QUIET_WINDOW` (4) where the case expects a contended-leg discard (2). That is the harness protecting a measurement, not a defect this branch introduced: the file's last change is `0305b909f` on `main`, this branch touches no benchmark harness, and the same suite reported `ok` on the same sources one preflight run earlier, when the load average was lower. Recording which red is environmental is the point — an unexplained red in a gate row is indistinguishable from a real one to the next reader. Issue: #1193 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…st makes this row's refusal LEAD `SPEC-DSPARK-BLOCK-SIZE-GUARD` (#1225) landed on `main` as `b626be75a` and moved the DSpark resolution to the top of `LoadedEngine::FromModelDir`. That is the change this branch had been waiting on without knowing it: `ResolveSpecConfig` now runs BEFORE `LoadDsparkDraft`, so this row's classification and its named DeepSeek-V4 refusal reach a user through `include/vllm.h` and the server instead of losing the race to the draft loader's missing-key message. Five conflicts, all resolved by keeping both sides. `src/vllm/entrypoints/model_loader.cpp` is pure adjacency: both branches inserted a helper immediately after `MakeDsparkDraftConfig`, so `ReadDsparkDraftKeys` (#1225) and `ReadDsparkDraftIdentity` (this row) now sit side by side and the merged file carries both. #1225's guard is intact and is the ONLY resolution left: `grep -c "ResolveDspark("` is 1, and that one call passes all four arguments (`keys.n_predict, keys.block_floor, cli.num_speculative_tokens, keys.block_floor_key`). `test_dspark_block_size_guard` is 14/14 with 39 assertions, which #1225's own mutation binds. The other four are records. `.agents/engine-matrix.md` keeps this row's `ACTIVE` text and gains #1225's new row; `docs/BENCHMARKS.md`, `docs/STATUS.md` and `docs/USAGE.md` each keep both entries, the last by re-inserting this row's draft-classification sentences at their original position inside the `--speculative-config` cell that #1225 also edited. Three statements became false AT this merge and are re-derived rather than deleted, because AGENTS.md judges reachability at the merge commit and a later reader would have no way to know they had expired. The spec's `## Owed` said the refusal "leads on NO production path yet". It leads now. Two of the resolution's own messages still precede it and are named: #1225's missing-`k` refusal, and the GGUF branch's refusal above the hoist. The second call site inside `LoadDsparkDraft` that the entry owed is therefore no longer owed, and `docs/USAGE.md`'s "Which refusal you actually get today" paragraph, which promised the user the draft loader's message, now says what the merged control flow actually produces. The same entry claimed the in-memory `LoadedEngine` constructors have no caller in `src/`, `include/` or `examples/`. Re-run, the grep hits 23 files and there are three construction sites, not two: `examples/bench/bench_core.h:570` calls the in-memory constructor directly. The conclusion survives on the right reason — an example's internals are not a production entry point, and that call sets no `params.speculative_config`, so `ResolveSpecConfig` returns `nullopt` at its first line. `## Outcome`'s R3 argued against hoisting the config read because the two reads "sit on opposite sides of the engine constructor". After #1225 they are adjacent statements in one function, so that reason is gone. The decision stands on the two that survive — the cost is unmeasurable and the third reader is still on the far side of the target load — and the count is now three parses, not two. The engine-matrix summary was wrong on both branches. Re-derived from the merged file: 163 rows, `READY` 11, `ACTIVE` 37 total, and 24 rows with `READY` 0 and `ACTIVE` 8 under Speculative decoding. `check-agent-record.py` validates the row total and, through `check_engine_summary`, the per-state columns too. Branch 3 of `ResolveDsparkArchitecture` — the Gemma4 collapse onto the one implemented lane — had only a hand-called unit case, which is the shape "nothing lands dead" excludes. It gains a loader-level case beside the other `DraftDir` fixtures. Narrowing its guard to `!has_qwen3` reds exactly that case. The first attempt at that mutation did not compile, `-Werror` on the now-unused `has_gemma4`, and the stale binary reported SUCCESS; it was rewritten to compile and only then measured. Focused suites on this merge, CPU only: `test_dspark_draft_routing` 7 cases / 19 assertions (was 6/17), `test_speculative_dspark` 12/40, `test_dspark_block_size_guard` 14/39, all rc=0. G5 (token-exact run gate) and G6 (spec-off) stay OWED and are not claimed. Issue: #1193 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
Taken so the branch gates run in place and the landing is a fast-forward of current main. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:Opus-5 [Claude Code] # Conflicts: # docs/USAGE.md
localai-bot
pushed a commit
that referenced
this pull request
Aug 18, 2026
…sted `origin/main` advanced again (#1248) while this branch was being pushed, and this time it DID touch three of the same files: `.agents/model-matrix.md`, `docs/BENCHMARKS.md` and `docs/STATUS.md`. Git auto-merged all three, and AGENTS.md `## Records` says never to accept that for a keyed record. So the auto-merge was checked rather than trusted. `git diff` against `10fe7f475` returns 13 changed lines in `.agents/model-matrix.md` and exactly one each in `docs/BENCHMARKS.md` and `docs/STATUS.md`, and every one of them is this branch's own scoped edit: the NemotronH row, the rollup, the two prose counts, the checklist entry, the open-gap row and the status clause. #1248's `MODEL-SPEC-deepseek-v4-dspark-...` row at `:546`, its DSpark benchmark row and its DSpark status paragraph are byte-for-byte equal to main's. No unrelated key moved. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
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.
A Qwen3 DSpark draft that declares
architectures: ["DSparkDraftModel"]withmodel_type: "qwen3"had no route. Upstream forces exactly that pair onto theDeepSeek-V4 lane, and we had never ported that rewrite, so the checkpoint loaded
by omission rather than by decision and the divergence from the pin was silent
and unrecorded.
IsDsparkDrafthad no production caller at all: every referenceoutside its header was a test, and the loader branched on the CLI method string.
The loader now classifies the draft. It reads the draft's own config, admits the
DSparkDraftModel+qwen3pair, and refuses the DeepSeek-V4 shape BY NAMEinstead of silently rewriting it as upstream does, because we do not implement
that lane and a faithful rewrite would fail later on an internal check rather
than naming the missing arm. That is one of two recorded divergences; the second
is that a Gemma4 draft collapses onto
Qwen3DSparkModel, where upstream leavesthe architecture alone and normalizes only keys. Both are argued in the spec.
Mirrors vllm#52197, marked BEYOND-PIN.
The first review found the new call decision-inert: the architecture resolver's
accept-set is a strict subset of the predicate's true-set, so the guard could
only change which message appeared, and both messages shared the only asserted
substrings. Deleting the whole call left every suite green. The two refusals now
carry disjoint wording and the tests assert the arm-specific text, so deleting
the call, making the predicate constant, or collapsing either message onto the
other each reds exactly the intended case.
An unreachable branch was deleted rather than disclosed. The staged-slice
exception exists for work a named row will wire; no row will wire this one,
because it only becomes live when a second lane exists and that change should
carry its own dispatch.
Landing after the block-floor row changes what this row can claim, and the
records are re-derived rather than carried over. The DSpark resolution is now
hoisted to the top of
FromModelDir, so the named refusal LEADS frominclude/vllm.hand from the server: the spec's "leads on no production path"and the USAGE paragraph describing which refusal a user gets were both true at
the reviewed head and false here. The owed second call site is retired, since
the ordering it existed to fix is fixed. A grep claim asserting the in-memory
constructors have no caller in
examples/was wrong —examples/bench/bench_core.hconstructs one — and is corrected; the conclusion holds because an example's
internals are not a production entry point and that call sets no speculative
config.
The run gate needs a 2.53 GiB draft and GPU time, and the spec-off arm needs the
same lease. Both remain OWED. Nothing here is measured.
Refs #1193, #1225.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:Opus-5 [Claude Code]