Skip to content

dots3-note W6a: the DENSE vision tower, on a SERVED image request - #2523

Open
localai-org-maint-bot wants to merge 6 commits into
mainfrom
row/MODEL-MM-DOTS3-NOTE-W6A
Open

dots3-note W6a: the DENSE vision tower, on a SERVED image request#2523
localai-org-maint-bot wants to merge 6 commits into
mainfrom
row/MODEL-MM-DOTS3-NOTE-W6A

Conversation

@localai-org-maint-bot

@localai-org-maint-bot localai-org-maint-bot commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

dots3-note's DENSE vision tower, on a SERVED image request.

This is the first brick on MODEL-MM-dots3-note-dots3-note-for-causal-lm whose
output a client can ask for. W3 wrote host maths, W4a/W4b put the two attention
geometries on the decode path and W5 put the MoE layers there; all of them ended
at a ctest binary, because until recently there was no engine path by which a
vision tower could be fed from a production entry point at all. #2398 put the
multimodal input pipeline in the runner and #2481 made the server's chat seam
dispatch on the ARCHITECTURE, so the remaining work was this model's own half.

An image_url chat request now travels the whole chain:
ApiServer::handle_chat_completions -> the architecture-dispatched multimodal
chat seam -> GPUModelRunner::execute_mm_encoder -> ModelRegistry::EmbedMm ->
ModelRegistry::Forward -> Dots3NoteModel::ForwardDevice, which reads
mm->inputs_embeds in place of the embedding lookup. kDots3NoteFactory grew
encode_mm and embed_mm, and supports_multimodal went FALSE -> TRUE with
both halves behind it. mrope_prompt_positions stays null, because upstream's
Dots3NoteForCausalLM is SupportsMultiModal, SupportsPP and not
SupportsMRoPE.

Registering this architecture is a NEW translation unit plus one
REGISTER_VLLM_MM_CHAT line, with zero edits to any shared table. dots3-note is
the second architecture to reach the registry #2481 built, which is what that row
was for.

The arm

patch_embed -> blocks 0-24 -> post_trunk_norm -> the patch_merger adapter
folding 4x1536 to 5120. Each block is a fused attn.qkv with no bias, a per-head
q_norm/k_norm applied BEFORE a 2-D vision RoPE, bidirectional attention, and
the three-tensor SwiGLU routed through layers::MlpGateUpMethodBase by merging
fc1 and fc3 into one [2I, E] operand at load. Beside it is a dots3 image
processor with its own resized_size, its own PER-CHANNEL normalization and its
own pre_pixel_shuffle patch order, none of which the Qwen3-VL processor can
stand in for.

The tower shares no code with qwen3_vl_vision and the reason is in the header:
the two agree on the block outline and on almost nothing below it. What IS shared
is every seam underneath — the vt:: ops, dense_attn's device glue,
dense_loaders' weight readers and MlpGateUpMethodBase.

The released checkpoint still refuses, and that is correct

17 of its 42 vision blocks are pyramid MoE and 1960 of its 2195
vision_encoder.* tensors are W6b's, so the tower refuses at block 25 naming the
brick, the tensors stay a named deferral, and the deferral table's brick moved
W6 -> W6b. Video is refused to W7, audio to W8, the blockwise-FP8 vision arm
to W9. This is W3's polarity applied to the second tower, not a new exception.

One behaviour changed under measurement rather than by design. Refusing only
inside encode_mm throws in the engine's busy loop: it stopped AsyncLLM, and
every later request, TEXT ONES INCLUDED, came back 500. The served-request gate
caught it. The chat FACTORY now asks the same refusal, so the install returns a
REFUSING seam and an image request is HTTP 400 naming the block while text keeps
serving. The gate asserts both halves.

The gate is a CONSISTENCY gate

Spec §6.4 option B stands: the checkpoint is 298.67 GB fp8 against 119-122 GiB
hosts, vLLM cannot be run on it on any hardware this project owns, and no
denominator exists. Correctness is argued against an independent in-test
double-precision reference written from nvidia/vision.py and
nvidia/vision_attention.py, sharing no helper with the implementation. It
establishes that two implementations agree. It does NOT establish that either
matches vLLM, and no performance number is claimed on any axis.

Measured: relative 8.44e-3 against a 2.0e-2 bound, with a qk-norm/rope order swap
reaching 5.9e-2.

Every upstream anchor names the SHA it was read at. dots3_note is BEYOND our
parity pin 5559679229; the sources were read at 9035151d6, and upstream has
already moved — vision_attention.py is 477 lines there and 494 at vLLM main
7a100bb61.

Reachability is held by mutation

Deleting the tower call, deleting the .mm read, or deleting the loader's
materialization call site each reds the served gate. Replacing the tower with a
correctly-SHAPED constant reds EXACTLY ONE case — the two-different-images
logprob one — while status, prompt_tokens and completion_tokens all still
pass, which is the measurement behind having that case at all. Deleting the
production call site leaves the tower's own unit gate green at 8/8 and 4981
assertions, which is why both files exist. Binary hashes and the byte-for-byte
restores are recorded in spec §4.11.6.

Records

Spec §7 assigned "the FP32-scale FP8 formula" to W6 and §8's R5 was filed against
W6. The row's own W2 census contradicts both: the released bf16 checkpoint
carries 37944 BF16 + 62 F32 tensors and no scale tensor at all, and the -fp8
sibling that does ship them is already refused by name as W9. Both moved to W9;
the risk is unchanged and is not waived, only its owner moved.
model-matrix.md:124 still read "W2 LANDED" and omitted W3, W4a, W4b-1/2/3 and
W5, so it is brought current in the same change.

#2512's scope prose says the dense arm ends in "pixel shuffle -> adapter", while
the released vision_config sets adapter_type: "patch_merger" — upstream's own
name for the arm that SKIPS that permutation, because pre_pixel_shuffle: true
moved the 2x2 regrouping into the preprocessor and the RoPE. The issue's own
tensor inventory, adapter.{ln_q, mlp.0, mlp.2}, is PatchMergerAdapter's state
dict and agrees with the committed fixture, so the inventory is right and the
word is loose. Recorded in spec §4.11.1 rather than reconciled silently.

Fresh review repairs (this PR, on top of the reviewed head)

The headline fix was incomplete, and the cascade it removed was still
reachable.
Moving the MoE refusal into the chat factory fixed ONE condition.
The install-time predicate was still a strict SUBSET of the VT_CHECKs
EncodeMmDots3NoteForCausalLM makes on a served request: an ALL-DENSE
checkpoint whose adapter_out_dim is not the text hidden_size, or whose
adapter_merge_size is not spatial_merge_size, installed cleanly, served
text, and then threw inside the engine's busy loop on its first image — after
which AsyncLLM::errored_ is set for the life of the process
(async_llm.cpp:584-601) and every later request, text ones included, is dead.
Upstream keeps those two merge keys independent with independent defaults, so
this is a config a checkpoint can carry rather than a shape the parse rules out.
Dots3NoteVisionRefusal now makes the same two comparisons, from a
text_hidden_size copied off the LANGUAGE config at parse — the value the route
actually compares against, which the dead vision_config.hidden_size field it
replaces was not. Two new served cases gate it, each asserting HTTP 400 on the
image AND HTTP 200 on a text request afterwards on the same server; the second
assertion is the one the pre-repair tree cannot pass.

A refusal that could never fire is gone. adapter_merge_size**2 * adapter_in_dim != merged_dim() compared merged_dim() against its own
definition — x != x. It was also the only row in the refusal table naming no
brick, because it stood for no condition. The adapter_merge_size check above
is the real cross-key check that belongs in its place.

A record the code claimed did not exist. The image processor's resize
refusal said, in its comment and in its runtime message, that it was "Recorded
under ## Owed in .agents/specs/dots3-note.md". That section named it
nowhere and no issue tracked it. It is a capability gap and not a corner:
factor is 28 on the released geometry and upstream ALWAYS resizes, so once W6b
lifts the MoE refusal almost no real image is servable. Now recorded under
## Owed naming W8, tracked by #2537, and cited by both the comment and the
message.

What the consistency gate CANNOT see is now part of its evidence. A uniform
+1% scale error on the tower output reads relative 0.0152 against the 0.02 bound
and PASSES — the detection floor for a systematic multiplicative error is ~1.5%.
Replacing exact-erf GELU with the tanh approximation is BYTE-IDENTICAL to the
baseline, because the bf16 store of fc1 absorbs it, so a named formula choice
is below the gate's resolution. Both are in §4.11.6 beside the 8.44e-3 / 0.02
numbers, which stand. §4.11.6 also stops overstating its mutation proof: a
changed binary sha proves the build was not stale, not that the mutation reached
the code under test — M4 moved the tower binary's sha by relinking while its
behaviour was unchanged. The case counts are the evidence.

Two smaller ones. Dots3NoteVisionParams::pre_pixel_shuffle defaulted to true
against upstream's DotsMoEVitConfig default of False; production always goes
through the parser, which was already correct, but the field selects between two
incompatible token orders and a default-constructed params must not claim the
wrong one. And TinyCheckpoint::value_of returned a static empty vector for an
unknown tensor name, which ref::Tower would then index — an instrument that
fails toward garbage instead of reporting itself. It throws naming the tensor.

§4.11.6 (Evidence) also moved after §4.11.5 (Reachability), which it preceded.

The full gate was rerun on the repaired tree: ninja all 1381 targets
NINJA_RC=0, ctest -j 2 702/702 with 0 failed and 7 skipped for absent
checkpoints, CTEST_RC=0. The four dots3 suites are 9/68, 8/4996, 51/6888 and
26/110835, all 0 failed. scripts/agent-preflight.sh rc 0.

#2537 is FILED, not fixed here: the bicubic resize it asks for is W8's, and the
spec's ## Owed names it.

Closes #2512.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE:claude-opus-5 [claude-code]

…the row's own census contradicts

Commits the spec before the code, as the protocol requires: §4.11 states what
W6a ships, what it refuses by name, what its gate can and cannot claim, and the
one word in #2512's prose that the committed fixture corrects.

The scope is the DENSE half of the vision tower, served end to end. It is
unblocked now because #2398 put the multimodal input pipeline in the runner and
#2481 made the server's chat seam dispatch on the ARCHITECTURE, so the remaining
work is this model's own half. The RELEASED checkpoint still refuses: 17 of its
42 vision blocks are pyramid MoE and 1960 of its 2195 `vision_encoder.*` tensors
belong to W6b. That is W3's polarity applied to the second tower, not a new
exception, and §4.11.3 says so.

§4.11.1 resolves a reading #2512's prose invites. Its scope sentence says the
dense arm ends in "pixel shuffle -> adapter", while the released `vision_config`
sets `adapter_type: "patch_merger"` — upstream's own name for the arm that SKIPS
the pixel-shuffle permutation, because `pre_pixel_shuffle: true` moved the 2x2
regrouping into the preprocessor and the RoPE. The issue's own tensor inventory,
`adapter.{ln_q, mlp.0, mlp.2}`, is PatchMergerAdapter's state dict and agrees
with the fixture, so the inventory is right and the word is loose. No geometry
disagreement was reconciled silently.

The gate is named for what it is. §6.4 option B stands: the checkpoint is
298.67 GB fp8 against 119-122 GiB hosts, vLLM cannot be run on it here, and no
denominator exists. Correctness is argued by an independent in-test
double-precision reference. That is a CONSISTENCY gate — two implementations
agree — and no performance number is claimable on any axis.

Two record repairs ride here because the change that makes them stale is the
same one. §7 assigned "the FP32-scale FP8 formula" to W6 and §8's R5 was filed
against W6; the row's own W2 census contradicts both. The released bf16
checkpoint carries 37944 BF16 + 62 F32 tensors and no scale tensor at all, and
the `-fp8` sibling that does ship them is already refused by name as W9. Upstream
CASTS bf16 experts to block-FP8 at load (`vision.py:226-268` @ `9035151d6`),
which is a quantized path this arm does not take. The risk is unchanged and is
not waived; only its owner moved. `model-matrix.md:124` still read "W2 LANDED"
and omitted W3, W4a, W4b-1/2/3 and W5, so it is brought current in the same
breath.

Every upstream anchor names the SHA it was read at. `dots3_note` is BEYOND our
parity pin `5559679229` and upstream has already moved: `vision_attention.py` is
477 lines at `9035151d6` and 494 at vLLM `main` `7a100bb61`.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE:claude-opus-5 [claude-code]
…SERVED image request

The first brick on this row whose output a client can ask for. Every brick
before it ended at a `ctest` binary; this one enters through
`ApiServer::handle_chat_completions` on the server's default configuration.

An `image_url` chat request now travels the whole chain: the
architecture-dispatched multimodal chat seam (a `REGISTER_VLLM_MM_CHAT`
translation unit of its own, so no shared table is edited), then
`GPUModelRunner::execute_mm_encoder` -> `ModelRegistry::EmbedMm` ->
`ModelRegistry::Forward` -> `Dots3NoteModel::ForwardDevice`, which reads
`mm->inputs_embeds` in place of the embedding lookup. `kDots3NoteFactory` grew
`encode_mm` and `embed_mm`, and `supports_multimodal` went FALSE -> TRUE with
both halves behind it. `mrope_prompt_positions` stays null: upstream's
`Dots3NoteForCausalLM` is `SupportsMultiModal, SupportsPP` and not
`SupportsMRoPE`, so the ordinary 1-D positions are what this forward reads.

The arm is `patch_embed` -> blocks 0-24 -> `post_trunk_norm` -> the
`patch_merger` adapter. Each block is a fused `attn.qkv` with no bias, a per-head
`q_norm`/`k_norm` applied BEFORE a 2-D vision RoPE, bidirectional attention, and
the three-tensor SwiGLU routed through `layers::MlpGateUpMethodBase` by merging
`fc1` and `fc3` at load. Beside it: a dots3 image processor with its own
`resized_size`, its own per-channel normalization and its own
`pre_pixel_shuffle` patch order, none of which the Qwen3-VL processor can stand
in for.

THE RELEASED CHECKPOINT STILL REFUSES, and that is correct. 17 of its 42 vision
blocks are pyramid MoE and 1960 of its 2195 `vision_encoder.*` tensors are W6b's,
so the tower refuses at block 25 naming the brick, the tensors stay a named
deferral, and the deferral table's brick moved `W6` -> `W6b`. Video is refused to
W7, audio to W8, the blockwise-FP8 vision arm to W9. This is W3's polarity
applied to the second tower, not a new exception.

One behaviour changed under measurement rather than by design. Refusing only
inside `encode_mm` throws in the engine's busy loop: it stopped `AsyncLLM`, and
every later request, TEXT ONES INCLUDED, came back 500. The served-request gate
caught it. The chat FACTORY now asks the same refusal, so the install returns a
REFUSING seam and an image request is HTTP 400 naming the block while text keeps
serving; the encoder check stays as defence in depth. The gate asserts both
halves.

The gate is a CONSISTENCY gate and says so in its own header. Spec §6.4 option B
records that no oracle for this model runs on any hardware this project owns, so
correctness is argued against an independent in-test double-precision reference
written from `nvidia/vision.py` and `nvidia/vision_attention.py` at `9035151d6`,
sharing no helper with the implementation. It establishes that two
implementations agree; it does not establish that either matches vLLM, and no
performance number is claimed on any axis. Measured: relative 8.44e-3 against a
2.0e-2 bound, with the qk-norm/rope order swap reaching 5.9e-2.

Reachability is held by mutation, not by argument. Deleting the tower call, the
`.mm` read, or the loader's materialization call site each reds the served gate;
replacing the tower with a correctly-SHAPED constant reds EXACTLY ONE case, the
two-different-images logprob one, while status, `prompt_tokens` and
`completion_tokens` all still pass. Deleting the production call site leaves the
tower's own unit gate green at 8/8 and 4981 assertions, which is why both files
exist. Evidence, binary hashes and the byte-for-byte restores are in spec
§4.11.6.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE:claude-opus-5 [claude-code]
… which W6a's flag flip moved dots3-note back into

`registry_model_property` partitions every registration into hybrid,
multimodal-non-hybrid and text-only, and asserts `supports_multimodal` per
branch. W5 moved `Dots3NoteForCausalLM` into the text-only branch because the
flag had become a claim this port could not honour; W6a backs it again, so the
architecture belongs in the multimodal branch and the gate said so by going red.

Found by the full `ctest` run rather than by reading: 701 of 702 passed and this
was the one. The comment records the round trip rather than erasing it, because
the true -> false -> true trail is what says what this port could back at each
point. It also says what the flag does NOT mean: the RELEASED checkpoint's vision
tower still refuses at its first pyramid MoE block, and a `_ModelInfo` flag is a
statement about the ARCHITECTURE, not a promise about every checkpoint of it.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE:claude-opus-5 [claude-code]
…vidence the two suite counts above it are not

§4.11.6 listed five suites and their assertion counts. Five green suites are a
statement about five suites; the row's evidence table has to say what the whole
tree does, because that is where the registry-gate regression this change caused
actually surfaced.

Records `ninja` over all 1375 targets followed by `ctest -j 2`: 702/702, 0
failed, 7 skipped for absent checkpoints, `CTEST_RC=0` in 931 s, plus
`agent-preflight.sh` and the two commit checkers at rc 0. And it names what the
run found — `test_model_registry`'s multimodal branch — rather than presenting a
clean number as though nothing had to be repaired to get it.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE:claude-opus-5 [claude-code]
…edicate, because the subset still reaches the engine

W6a moved the MoE refusal into the chat factory to stop a throw inside the
engine's busy loop from killing `AsyncLLM` for the life of the process. It fixed
one condition. The install-time predicate was still a strict SUBSET of the
`VT_CHECK`s `EncodeMmDots3NoteForCausalLM` makes on a served request, and three
of them were reachable from an all-dense config `Dots3NoteVisionRefusal`
accepted: `adapter_out_dim` against the TEXT `hidden_size`
(`dots3_note_registry.cpp`), the emitted row count against the placeholder span,
and `L % merge_unit` inside the tower. Upstream keeps `adapter_merge_size` and
`spatial_merge_size` as independent keys with independent defaults, so a
checkpoint can carry them disagreeing; the parse cannot rule it out.

Measured before the fix, on the served gate with the two new cases' `REQUIRE`s
downgraded so the request runs to the end: the image request returns 500 naming
`dots3_note_registry.cpp:202`, `async-llm` logs "output handler saw engine
death", and the TEXT request sent afterwards on the same server returns 500
`request submitted to a stopped AsyncLLM`. `errored_` is set at
`async_llm.cpp:584-601` and nothing clears it.

The refusal now asks both comparisons. It reads a `text_hidden_size` copied off
the LANGUAGE config at parse, which is the value the encoder compares against.
It replaces `vision_config.hidden_size`, a field this port parsed, never read,
and documented as "the TEXT width the adapter lands in" — a second copy of the
number, and reading it instead of `config.hidden_size` would have left the two
predicates answering different questions.

A refusal that could never fire goes with it. `adapter_merge_size**2 *
adapter_in_dim != merged_dim()` compared `merged_dim()` against its own
definition, so it was `x != x`; it was also the only refusal naming no brick,
because it stood for no condition. The `adapter_merge_size` check is the real
cross-key check that belongs where it was.

Two instruments are repaired beside it. `Dots3NoteVisionParams::pre_pixel_shuffle`
defaulted to `true` against upstream's `DotsMoEVitConfig` default of `False`;
production always goes through the parser, which was already correct, but the
field selects between two incompatible token orders, so a default-constructed
params must not claim the wrong one. And `TinyCheckpoint::value_of` returned a
static empty vector for an unknown tensor name, which `ref::Tower` then indexes
— an instrument that fails toward garbage instead of reporting itself. It throws
naming the tensor.

The image processor's resize refusal now cites the issue that tracks it, #2537,
and names the brick that owes it. Its comment and its message both claimed the
debt was "Recorded under `## Owed` in `.agents/specs/dots3-note.md`" while that
section named it nowhere.

RED: 9 cases, 2 failed, 11 of 4996 tower assertions failed, on served-gate
`05848d1f4226e416…` / tower `2d2dc86de602c004…`. GREEN: 9/9 with 68 assertions
and 8/8 with 4996, on `c8d9573ef3d605a7…` / `8f2b81436dcdac81…`.

Repairs the fresh review of #2523.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE:claude-opus-5 [claude-code]
…ed, and what the gate cannot see

Three record repairs the fresh review of #2523 found, none of which the code
change beside them carries.

The image processor refuses any image whose resized size differs from its input
size, and both its comment and its runtime message said that was "Recorded under
`## Owed` in `.agents/specs/dots3-note.md`". That section named it nowhere and
no issue tracked it. It is now an `## Owed` entry naming W8, tracked by #2537.
It is a capability gap rather than a corner: `factor` is `patch_size *
merge_size`, which is 28 on the released checkpoint, and upstream always resizes
(`processor.py:174`, `Image.Resampling.BICUBIC`), so once W6b lifts the MoE ViT
refusal almost no real image clears it. The entry says what closing it needs and
why refusing remains the right interim behaviour.

§4.11.6 gains what the consistency gate CANNOT see, because on a row with no
oracle that is part of the evidence and not a caveat outside it. A uniform +1%
scale error on the tower output reads relative 0.0152 against the 0.02 bound and
PASSES, so the detection floor for a systematic multiplicative error is ~1.5%.
Replacing the exact-erf GELU with the tanh approximation is byte-identical to
the baseline — the same printed digits, max |diff| 0.0533141 — because the bf16
store of `fc1` absorbs it, so a named formula choice is below the gate's
resolution. The 8.44e-3 agreement and M5's 5.9e-2 red both stand; what these
bound is the class of defect the gate detects.

The same section stops overstating its mutation proof. A changed binary sha256
proves the build was not stale, which is the trap it was chosen for; it does not
prove the mutation reached the code under test. M4 moved the tower gate's sha by
relinking a translation unit that gate does not exercise while its behaviour was
unchanged, which the paragraph after the table already recorded without drawing
the conclusion. The case counts are the evidence.

The full-gate row is rerun on this tree rather than carried over: `ninja` all
1381 targets `NINJA_RC=0`, then `ctest -j 2` 702/702 with 0 failed, 7 skipped
for absent checkpoints, `CTEST_RC=0`, 158.06 s.

§4.11.3's refusal table gains the two conditions the widened predicate covers,
with why they name no brick, and records the tautology that left with them.
§4.11.6 (Evidence) also moved after §4.11.5 (Reachability), which it preceded.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE: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.

dots3-note W6a: the dense vision tower arm, served end to end through the architecture-dispatched chat seam

2 participants