dots3-note W6a: the DENSE vision tower, on a SERVED image request - #2523
Open
localai-org-maint-bot wants to merge 6 commits into
Open
dots3-note W6a: the DENSE vision tower, on a SERVED image request#2523localai-org-maint-bot wants to merge 6 commits into
localai-org-maint-bot wants to merge 6 commits into
Conversation
…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]
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.
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-lmwhoseoutput 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
ctestbinary, because until recently there was no engine path by which avision 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_urlchat request now travels the whole chain:ApiServer::handle_chat_completions-> the architecture-dispatched multimodalchat seam ->
GPUModelRunner::execute_mm_encoder->ModelRegistry::EmbedMm->ModelRegistry::Forward->Dots3NoteModel::ForwardDevice, which readsmm->inputs_embedsin place of the embedding lookup.kDots3NoteFactorygrewencode_mmandembed_mm, andsupports_multimodalwent FALSE -> TRUE withboth halves behind it.
mrope_prompt_positionsstays null, because upstream'sDots3NoteForCausalLMisSupportsMultiModal, SupportsPPand notSupportsMRoPE.Registering this architecture is a NEW translation unit plus one
REGISTER_VLLM_MM_CHATline, with zero edits to any shared table. dots3-note isthe 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-> thepatch_mergeradapterfolding 4x1536 to 5120. Each block is a fused
attn.qkvwith no bias, a per-headq_norm/k_normapplied BEFORE a 2-D vision RoPE, bidirectional attention, andthe three-tensor SwiGLU routed through
layers::MlpGateUpMethodBaseby mergingfc1andfc3into one[2I, E]operand at load. Beside it is a dots3 imageprocessor with its own
resized_size, its own PER-CHANNEL normalization and itsown
pre_pixel_shufflepatch order, none of which the Qwen3-VL processor canstand in for.
The tower shares no code with
qwen3_vl_visionand 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 andMlpGateUpMethodBase.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 thebrick, 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 armto 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_mmthrows in the engine's busy loop: it stoppedAsyncLLM, andevery 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.pyandnvidia/vision_attention.py, sharing no helper with the implementation. Itestablishes 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_noteis BEYOND ourparity pin
5559679229; the sources were read at9035151d6, and upstream hasalready moved —
vision_attention.pyis 477 lines there and 494 at vLLMmain7a100bb61.Reachability is held by mutation
Deleting the tower call, deleting the
.mmread, or deleting the loader'smaterialization 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_tokensandcompletion_tokensall stillpass, 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
-fp8sibling 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:124still read "W2 LANDED" and omitted W3, W4a, W4b-1/2/3 andW5, 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_configsetsadapter_type: "patch_merger"— upstream's ownname for the arm that SKIPS that permutation, because
pre_pixel_shuffle: truemoved 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 statedict 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_CHECKsEncodeMmDots3NoteForCausalLMmakes on a served request: an ALL-DENSEcheckpoint whose
adapter_out_dimis not the texthidden_size, or whoseadapter_merge_sizeis notspatial_merge_size, installed cleanly, servedtext, 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.
Dots3NoteVisionRefusalnow makes the same two comparisons, from atext_hidden_sizecopied off the LANGUAGE config at parse — the value the routeactually compares against, which the dead
vision_config.hidden_sizefield itreplaces 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()comparedmerged_dim()against its owndefinition —
x != x. It was also the only row in the refusal table naming nobrick, because it stood for no condition. The
adapter_merge_sizecheck aboveis 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
## Owedin.agents/specs/dots3-note.md". That section named itnowhere and no issue tracked it. It is a capability gap and not a corner:
factoris 28 on the released geometry and upstream ALWAYS resizes, so once W6blifts the MoE refusal almost no real image is servable. Now recorded under
## Owednaming W8, tracked by #2537, and cited by both the comment and themessage.
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
fc1absorbs it, so a named formula choiceis 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_shuffledefaulted totrueagainst upstream's
DotsMoEVitConfigdefault ofFalse; production always goesthrough 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_ofreturned a static empty vector for anunknown tensor name, which
ref::Towerwould then index — an instrument thatfails 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:
ninjaall 1381 targetsNINJA_RC=0,ctest -j 2702/702 with 0 failed and 7 skipped for absentcheckpoints,
CTEST_RC=0. The four dots3 suites are 9/68, 8/4996, 51/6888 and26/110835, all 0 failed.
scripts/agent-preflight.shrc 0.#2537 is FILED, not fixed here: the bicubic resize it asks for is W8's, and the
spec's
## Owednames it.Closes #2512.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE:claude-opus-5 [claude-code]