Skip to content

feat(SPEC-DSPARK-QWEN3-ROUTING): give a Qwen3 DSpark draft an explicit route, and refuse the DeepSeek-V4 lane by name - #1248

Merged
localai-bot merged 10 commits into
mainfrom
row/DSPARK-QWEN3-ROUTING-IMPL
Aug 18, 2026
Merged

feat(SPEC-DSPARK-QWEN3-ROUTING): give a Qwen3 DSpark draft an explicit route, and refuse the DeepSeek-V4 lane by name#1248
localai-bot merged 10 commits into
mainfrom
row/DSPARK-QWEN3-ROUTING-IMPL

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

A Qwen3 DSpark draft that declares architectures: ["DSparkDraftModel"] with
model_type: "qwen3" had no route. Upstream forces exactly that pair onto the
DeepSeek-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. IsDsparkDraft had no production caller at all: every reference
outside 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 + qwen3 pair, and refuses the DeepSeek-V4 shape BY NAME
instead 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 leaves
the 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 from
include/vllm.h and 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.h
constructs 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]

mudler added 10 commits August 18, 2026 12:08
…, 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
localai-bot merged commit 10fe7f4 into main Aug 18, 2026
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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants