feat(#810 A2-R): NemotronH's dense weights reach the device, and the attention block that reads them is model-local (#517) - #950
Merged
Conversation
…ttention block that reads them is model-local (#517) A2 was scoped as one unit and came back as a campaign. This is A2a, the first landable piece: the weights the device can actually execute move onto the tree's shared residency seam, and the 6 GQA attention layers compute there. WHY THE LINE FALLS WHERE IT DOES. The released checkpoint is MIXED_PRECISION and only 216 of its tensors are plain bf16 (gated: test_nemotron_h_loader.cpp :254). Everything this change puts on the device comes from that population. Everything it leaves on the host does not: * the 23 Mamba2 blocks are entered through `mixer.in_proj`, FP8 W8A8. The block is NOT splittable -- in_proj produces the fused zxbcdt the conv and the scan both consume -- so it moves as a unit, after the shared FP8 W8A8 linear seam is extracted out of qwen3_5.cpp (ResidentFp8:1456, MatmulFp8CutlassD:1495). That is #940 and is deliberately not done here: its own gate is Qwen3.5 byte-identity, which does not belong in this row. * the 5934 expert projections and lm_head are NVFP4 W4A16 g16 -- 30.19e9 parameters, 15.8 GiB packed and 56.2 GiB dequantized to bf16. There is no device NVFP4->bf16 dequant kernel in vt at all, so a "bf16 everywhere" device forward means a host dequant plus a 56.2 GiB upload. Both gate hosts are unified-memory, so that is a reboot rather than an OOM. nemotron_h_loader.h:36-46 rejected the same design for the load. So 46 of 52 layers still compute on the host, lm_head still computes on the host, and each host layer costs one download plus one upload. That bounce is scaffold, not architecture, and every later unit deletes one pair of it. NO SPEED CLAIM IS MADE OR IMPLIED. NOT dense_attn::AttnBlock (#941). The spec's section 2 names it as this model's seam and that is wrong: it takes Qwen3DenseAttnWeights, it reads `cfg.rms_norm_eps` -- which this config does not ship, so hf_config.cpp:551 defaults it to 0.0 -- and its default path calls vt::RopeNeox unconditionally against kNemotronHAttentionHasNoRope. Routing there would reintroduce the exact defect #810 just removed from the runner. NemotronHAttnBlock follows the tree's model-local idiom (granite.cpp:84, gemma.cpp:42, glm4.cpp:80) and documents its six deltas. NOT a d_dev on NemotronHOwned. That was the smaller diff and it is the parallel path AGENTS.md forbids. The dense weights move to the shared OwnedTensor and upload through dense_attn::ResidentWeight. NemotronHOwned survives for the quantized weights precisely because OwnedTensor carries no form, no group scale and no input_scale, so converting a quantized weight to it would discard the memory format the checkpoint ships. G-SAFE IS FULLY INTACT. All three clauses at nemotron_h_registry.cpp:161-170 -- `attn_kv.empty()`, `gdn_state.empty()`, `num_reqs <= 1` -- are untouched. This arm is non-paged and single-request, so it creates none of the capability the interlock guards and therefore does not narrow it. THE GATE IS NUMERIC, NOT TOKEN-ONLY. A too-wide dtype and a wrongly-applied rotation are both invisible to tokens, so the device attention block is compared against the host reference element by element through ExpectCloseRel (which self-certifies by requiring the band to reject an all-zeros answer), and the no-RoPE property is proven by SEPARATION against a rotated reference with the rotation instrument checked first. Both arms now route the residual add+RMSNorm through vt::FusedChain, so they compose the identical op sequence and differ only in which backend runs it -- check-fusion-consistency caught that the first draft did not, which would have made the equivalence claim a comparison of two different compositions. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
Merged at a PINNED SHA rather than the moving ref: this checkout is shared and origin/main advances under a long-running row, so a diff against the REF would accuse this branch of dropping files it never touched. Overlap is docs/FEATURES.md and docs/USAGE.md only, both KEYED tables. The NemotronH row is the only key this branch edits; every other key is taken from the target wholesale. Also picks up bc6433d (#878), the repair for the #873 checker regression that this row measured as its inherited red baseline. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…on, and bf16 is the checkpoint's dtype
MEASURED coverage hole, found by running the mutation rather than by reasoning
about it. M2 (apply the RoPE this architecture does not have) reded f32 and
stayed GREEN at bf16:
rope separation .................. 2.11e-2 (Thor sm_110, measured)
RelFor(kBF16) .................... 3.0e-2 (band -- ABOVE the separation)
So the bf16 arm accepted a fully rope'd answer, and bf16 is the released
checkpoint's own model dtype. A mutation that stays green is a coverage hole,
not a pass.
The band was wrong because it was the WRONG BAND. RelFor is calibrated for a
forward against an independent f64 reference and has to absorb the whole
accumulated error of the port. Device-vs-host is far tighter: identical vt:: op
sequence, identical dtype, only the backend differs. Measured on Thor with the
band driven to 1e-9:
f32 worst element slack 2.42e-08, whole-output separation 2.29e-06
bf16 passes at 1e-9 outright -- the bf16 store absorbs the f32-level
difference entirely
DevRelFor is therefore f32 1e-5 (~4x over the measured separation) and bf16
4e-3 (about one bf16 ULP, the smallest band that cannot flake on another GPU's
reduction order). Both sit far below 2.11e-2.
And the margin is now ASSERTED rather than remembered: the case checks
`DevRelFor(dt) * 5 < kRopeSeparation`, so a future widening of the band fails
THERE instead of silently disarming the no-RoPE property.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
… watched a test-local copy, and `nk` had no reader at all Review findings on f77dbac. Four of the seven were the same defect wearing different clothes: an assertion that cannot fail reads exactly like one that passes. F1 -- THE MARGIN WAS TWO COMPILE-TIME CONSTANTS, AND THE WRONG DTYPE'S NUMBER. `DevRelFor(dt) * 5 < kRopeSeparation` compared a constant with a constant, so it observed nothing about the running system, and `2.11e-2` was the f32 arm's separation. Measured live, per dtype, on this fixture: f32 a rotation moves this block by 2.10893e-2 band 1e-5 ratio 2109x bf16 a rotation moves this block by 1.97151e-2 band 4e-3 ratio 4.93x So the stored constant was ~6% too large for the bf16 arm, and the invented `*5` factor REDS a bf16 arm that is in fact correctly armed. Lowering 5 to 2 to get green would be widening a scope to turn a gate green. The proxy is gone. The case now builds the rotated counterfactual from the same per-dtype fixture (`RefAttentionRotated`, which carries its own instrument) and asserts the PROPERTY: run the rotated answer through the SAME `ExpectCloseRel` arithmetic that accepted the real one, and require it to come out REJECTED. No stored twin, no safety factor, measured per dtype per run. The host no-RoPE case shares the helper, which deletes 40 lines of inlined attention core. M3B proves the repair is what closes the hole, with NO product mutation at all: widen the band back to `RelFor` and the guard reds at bf16 ONLY (2 assertions), while f32 stays green. That is the historical hole, isolated. F2 -- THE RESIDENCY CASE GATED `OwnT`, NOT `CopyDenseOwned`. The fixture builds its OwnedTensors with the test's own helper, so it can prove the device arm CONSUMES the type and can never see the loader mis-PRODUCE it. Two consequences, both now closed against the real checkpoint: * `host_bytes` was printed and read by nothing. Truncating every production payload to half moved it 470 MiB with 16/16 green and 3/3 oracle goldens still MATCHING -- a shrunk std::vector keeps its buffer, so only the accounting moved. Both totals are now asserted EXACTLY, and the 15324-byte gap between them is accounted for rather than tolerated: -24156 for 6039 f32 scalars read but never stored as payload, +8832 for the 69 f32-by- contract SSM widenings. `host_bytes == source_bytes` would have been wrong. * `nk` had NO READER on this path. `View()` does not propagate it and both arms called `vt::MatmulBT` unconditionally, so `CHECK(aw.q_proj.nk)` recorded an intent. It has a consumer now, in the tree's own idiom (qwen3_5.cpp:3353, :3611, :7638): the two functions that CALL MatmulBT refuse a weight whose recorded orientation is not the [out, in] one MatmulBT reads. The tautological fixture assertions are deleted. F4 -- THE NAMES COLLIDED ON THE GREPPABLE SURFACES. The spec's `A2a` is the PAGED unit, the one that NARROWS the interlock; this one does not narrow it. `A2b` is taken twice (the spec's batching unit and Laguna's "Brick A2b", cuda_laguna.cu:368). Renamed throughout -- test case names, the 35 Greek references, the docs rows and the PR title -- to A2-R residency / A2-Q quantized / A2-P paged / A2-B batching, with the mapping recorded in-tree where a reader greps rather than only in a PR body. F5 the "can never straddle" claim is softened to what is true: two file-local statics with identical predicates, a convention rather than an impossibility. F7 removes one dead device allocation per layer per step; no speed claim. `nemotron_h_registry.cpp` is STILL BYTE-IDENTICAL TO MAIN, deliberately, including its `A2b` spelling: that file carries the G-SAFE interlock and its empty diff is itself evidence a reviewer reads. Gate on Thor (sm_110, fa2/cutlass DISABLED for [110] -- this host's documented profile): test_nemotron_h_forward 16/16, 5716 assertions, SUCCESS; test_nemotron_h_scaffold 14/14, 38302; test_nemotron_h_loader against the real 20.1 GiB checkpoint 2/2, 53 assertions, 3/3 oracle goldens MATCH. Every mutation carries compile exit, error count and a binary sha distinct from baseline, and every restore rebuilds back to the baseline sha byte-for-byte. One mutation attempt did not build (`unused variable 'atol'`) and is reported VOID rather than counted -- its binary sha was the previous mutation's, unchanged. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
Pinned SHA, per #841: `origin/main` moves under a shared checkout, so a diff taken against the moving ref accuses this branch of dropping files it never touched. Merged at an immutable SHA and verified against it. Three files overlap -- CMakeLists.txt, docs/FEATURES.md, docs/USAGE.md -- and all three are keyed. `git diff 4880c57 -- <those three>` after the merge is exactly this row's own edits and NOTHING else: the one NemotronH row in each doc table, and the one source entry in the CMake list. Every other key came from the target byte-for-byte. The merge also caught the one file the A2-R rename had missed. CMakeLists.txt :882 still spelled the unit `A2α`, so a grep for the new name would have found the tests and the model sources but not the build entry that names the file they gate. Renamed here rather than left for the next reader. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…laced Comment only, no assertion changes. The block header said the property is proven "by much more than the band above", which was true of the `* 5` ratio and is not true of what replaced it: the check is now that the band REJECTS the rotated answer outright, with no margin factor. A reader who trusted the old sentence would go looking for a ratio that is deliberately gone. test_nemotron_h_forward rebuilt and re-run: 16/16, SUCCESS. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
… the parser's view Found by hitting it on this row's own pull request. `commit-protocol-tag` reported #950 as missing `Following-Agents-Protocol` and `AI-Assisted` while the body carried both, correctly, in a well-formed final paragraph. The cause is not the trailer block. The repository sets `squash_merge_commit_message = PR_BODY`, so the body IS the landed commit message, and `git interpret-trailers` treats a bare `---` as the end of a commit message -- everything after it is a patch, and is not parsed. Two markdown horizontal rules in the body therefore hid the trailer block completely. Measured, both directions, on the body as stored by GitHub: git interpret-trailers --parse < body -> nothing sed 's/^---*$/***/' body | interpret-trailers -> all three trailers A markdown table's `|---|---|` separator is unaffected: the line is not bare. Recorded in the commit and pull request guide rather than in a checker, because the checker is already CORRECT -- it refused a body whose trailers the parser genuinely cannot see, which is exactly what the landed commit would carry. The gap was that nothing told the author which character did it. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
localai-bot
pushed a commit
that referenced
this pull request
Aug 16, 2026
…W6's own variable shadow (#965, #672) `windows-msvc-cpu` and `windows-msvc-vulkan` fail on every open pull request: server_main.cpp(1315,55): error C2220: the following warning is treated as an error server_main.cpp(1315,55): warning C4456: declaration of 'loaded' hides previous local declaration That is W6's own speech-attach block declaring `loaded` inside the scope of the text engine's `loaded` at `:1025`. It is the ONLY warning in the job, it has been on `main` since W6 landed, and it is fixed here by renaming the inner declaration. Nothing is suppressed and no detector is weakened. WHAT FOUND IT WAS THE MATCHED-ARM CHECK, NOT THE LABEL, and that is the part worth recording. Both jobs are habitually red and habitually attributed to #645 — which is the `M_PI` portability regression in three LTX2 sources: different file, different detector, different failure. A second cause sitting behind a known-red name is invisible for exactly as long as nobody reads the log. Three unrelated open pull requests that touch no speech surface — #956, #950 and #939 — fail with the identical C4456, which is what separates "pre-existing" from "mine". `main` carries no baseline because `windows-msvc-*` are PR-only (#584), so the failure presents to each author in turn as a red their own diff caused. Verified after the rename: 7 of 7 server ctest cases pass, `test_openai_api_server` is 62 cases / 733 assertions, and `vllm-server --speech-model <dir>` with no `--model` starts and serves for real: server: speech/music-only model (family=minimax-music3, 44100 Hz, text-only synthesis, family DETECTED); serving /v1/audio/speech server: listening on http://0.0.0.0:18923 (model 'minimax-music3') Issue: #965 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
This was referenced Aug 16, 2026
localai-bot
pushed a commit
that referenced
this pull request
Aug 16, 2026
…the first, and it came from main (#968, #672) With #965's `C4456 'loaded' shadow` removed, `windows-msvc-cpu` and `windows-msvc-vulkan` failed again on this row's pull request — and on a different cause: include\vector(1461,29): error C2220: the following warning is treated as an error include\vector(1461,29): warning C4244: '=': conversion from 'const double' to 'float', possible loss of data raised from `src/vllm/multimodal/ltx2_video.cpp:203,214`, the two narrowing `positions.assign` calls that `c7cb59fbb` (#964, LTX25-TOKEN-APPEND) landed on `main` while this row was in flight. `StreamState::positions` and `Ltx2LatentState::positions` differ in element type; GCC and Clang narrow silently, MSVC diagnoses and the build treats it as an error. NOT FIXED HERE, deliberately. #964's own comment at `ltx2_video.cpp:129-132` reasons that "double -> float -> double reproduces the bits", so the narrowing is intentional and a silencing `static_cast` would be a claim about that reasoning rather than a formatting repair. It belongs to the lane that owns the round trip. Filed as #968 with the evidence rather than papered over. THE MATCHED ARM SPLITS EXACTLY ON THE MERGE BASE, which is what makes it inherited rather than mine. Grepping each `windows-msvc-cpu` job log for the warning: #966 and #951, both on `c7cb59fbb`, hit it twice each; #967, #956, #950, #939 and #938, all based before it, do not hit it at all. This row's diff touches zero LTX2 files. THE FINDING WORTH CARRYING is not either warning. It is that TWO INDEPENDENT CAUSES WERE STACKED BEHIND ONE HABITUALLY-RED JOB NAME, and the first hid the second — and that neither was #645, the `M_PI` regression both jobs are usually attributed to. A known-red list tells you a job is often red. It never tells you that today's red is the same one. Only reading the log does. Issue: #968 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
Six commits of `main` since the branch's pinned merge of 4880c57. Verified before committing rather than after: .agents/issue-index.md origin/main is a strict PREFIX of the result, so the auto-merge appended rather than interleaving. That is the one check an interleave fails, and this record has produced clean-but-wrong merges repeatedly today. docs/FEATURES.md every row this branch does not own is byte-identical docs/USAGE.md to origin/main. Only the NemotronH row differs. The three gated files that carried the fresh review's PASS -- the forward test, nemotron_h_device.cpp and nemotron_h.cpp -- are untouched by this merge. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
localai-bot
added a commit
that referenced
this pull request
Aug 16, 2026
… heard, and the five keys upstream refuses that we dropped (#672, #953, #965) (#966) feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be heard, and the five keys upstream refuses that we dropped (#672, #953) Developer directive: parity on what upstream supports — "we want to be a good reference" — usage docs for MiniMax-Music3, and in those docs the weights. Then, mid-flight: "we should allow to load only the music model" and "we need to have an e2e test working". FOLLOWING_AGENTS_PROTOCOL ## The upstream surface, enumerated rather than summarized SGLang-Omni `748a0b43` `models/minimax_music3/` and diffusers `c6da9936` `modular_pipelines/minimax_music3/`, read field by field and recorded with `file:line` in spec §10.1 so the next reader re-derives nothing. **Closed here:** the music-only server, the missing example, and five refusals. **Owed and named:** the non-`wav` response formats, request batching and `/v1/audio/speech/batch`, the 32 kHz delivery resample. **Permanently refused rather than owed:** streaming — neither upstream arm has it (`supports_streaming_vocoder=False`). **One place we are ahead of both arms:** `guidance_scale` is a real per-request control here, where diffusers freezes it at 1.7 into the guider component (`denoise.py:180`) and SGLang exposes it only as a serve-time knob. ## `--model` is optional when `--speech-model` is given Serving a 28.5 GB music model also forced loading an unrelated text model, and on this box the smallest text checkpoint is 35B — so the recipe this project documented was effectively unrunnable. Upstream's own is `sgl-omni serve --model MiniMaxAI/MiniMax-Music3`, no text tower anywhere. vllm-server --speech-model /path/to/minimax-music3 Third instance of a shape already in `server_main.cpp`: a pooling checkpoint serves `/v1/embeddings` alone, a Parakeet checkpoint serves `/v1/audio/transcriptions` alone. It mirrors vLLM's task-conditional registration (`api_server.py:255-265`). **Additive, and proved rather than argued.** The only case whose verdict changes is BOTH flags absent, which was an error and remains one with a message naming both options. The route table is gated in both directions over a real socket, because a handler-dispatch test cannot see route registration at all. ## The example the music family did not have `examples/minimax_music3_gen` — a thin client of `include/vllm.h` and nothing else, like `parakeet-transcribe` and `vllm-cli`. Hearing this model previously needed a running server plus a `curl`, or a C ABI caller nobody had written. ## Five keys upstream refuses by name were SILENT here (#953) `temperature`, `top_p`, `top_k`, `repetition_penalty` — refused upstream at `request_builders.py:14-19,109-114`, because this model's autoregressive stage has ONE sampler, a fixed top-50 draw (`encoders.py:48,94-103`). And `max_new_tokens`, upstream's LENGTH spelling in 25 Hz frames rather than seconds (`request_builders.py:56-68`), so a 250-frame request silently became the family's 60 s default. That is the #925 class exactly, in the same file that already carries #925's refusal one paragraph above. Fixed in flow. ## The e2e gate no longer reports a skip wearing a pass It read 5 cases / 5 passed with **`assertions: 0`** whenever the checkpoint was absent — the same shape that fooled this project on `test_qwen3_paged_engine`. Split into a checkpoint-free half that runs unconditionally in CI (request contract, both ceilings, the speech-only route table over a real socket with a stub synthesizer) and the env-gated half, whose HTTP case now drives the real engine over a real socket against the music-only server shape. A coverage-report case prints which arms ran, every run. The full arm was run: `POST /v1/audio/speech -> 200 audio/wav, 12332 bytes in 518 s wall`, 2 AR frames -> 6 latent frames -> 3072 samples/channel, 6144 int16 samples all non-zero, 0 clipped, 2818 of 3072 positions differing between L and R, and `/v1/completions` + `/v1/chat/completions` both 404 from the route table. `checkpoint_arms_run=5`. | arm | cases | assertions | |---|---|---| | `test_minimax_music3_e2e_real`, no env vars | 9 | 37 (was 5 / **0**) | | `test_minimax_music3_e2e_real`, checkpoint only | 9 | 86 | | `test_minimax_music3_e2e_real`, checkpoint + `VLLM_CPP_MUSIC3_DIT=1` | 9 | **582** | | `test_speech_api` | 6 | 67 | | `test_openai_api_server` | 62 | 733 | | `test_openai_conformance` | 23 | 252 | | `test_minimax_h3` (unchanged) | 79 | 57395 | | server flag ctest cases | 7 passed | | ## The weights are documented (porting-a-model.md §2.1) `docs/USAGE.md` gains component-by-component tables: the diffusers arm at `MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec`, **28.5 GB resident** (28 517 617 303 B, measured) out of a 57.4 GB repository and why they differ; the native `.pth` arm we refuse and SGLang-Omni serves; the one implemented GGUF Q4_K artifact with its sha256; and the fourteen third-party quantized repositories in five formats, each marked refused. The revision is verified rather than copied — `condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to that revision's own LFS record. ## A sample a human can hear 2.0 s of 44100 Hz stereo from this engine in 3286 s of wall clock: RMS 0.03169, peak 0.97437 with 0 clipped samples, 84 073 of 88 064 positions differing between left and right. **Its samples are compared to nothing** — §5 withdrew the token gate — so it shows the pipeline runs, not that the music is right. It is not committed: `check-pr-size.py` classifies every path and none takes a `.wav` outside `tests/`, where a file compared to nothing would sit beside the goldens and imply it was one. ## The four asks, answered directly **1. Music-only server.** `vllm-server --speech-model <dir>` with NO `--model` starts and serves, observed live rather than inferred: server: speech/music-only model (family=minimax-music3, 44100 Hz, text-only synthesis, family DETECTED); serving /v1/audio/speech server: listening on http://0.0.0.0:18923 (model 'minimax-music3') `--model` alone and `--model` + `--speech-model` are **byte-identical in behaviour**. The whole change is one new early branch, `if (args.model_dir.empty())`, which loads the speech engine and `return 0`s before reaching a single line of the existing path; nothing downstream of it was touched. The only case whose verdict changes is BOTH flags absent, which was an error and remains one. Server suites: **7 of 7** ctest cases (4 pre-existing + 3 new — neither flag is still an error and now names both options; `--speech-model` alone reaches the speech LOAD; `--speech-family` alone still demands a checkpoint), `test_openai_api_server` **62 cases / 733 assertions** (+1 case / +24 assertions, the speech-only route table over a real socket), `test_openai_conformance` **23 / 252** unchanged. **2. e2e, three arms.** What a bare CI run executes unconditionally: the request contract on the exact body the real case posts, the near-miss and sampling refusals, the duration arithmetic including both ceilings, and the speech-only route table over a real socket with a stub synthesizer. What stays env-gated: everything needing the 28.5 GB checkpoint, plus the two 2.4B-DiT arms behind `VLLM_CPP_MUSIC3_DIT`. | arm | cases | assertions | checkpoint arms run | |---|---|---|---| | no env vars (what CI runs) | 9 | **37** | 0 — was 5 cases / **0 assertions** | | `VLLM_CPP_MUSIC3_CHECKPOINT` | 9 | **86** | 3 | | + `VLLM_CPP_MUSIC3_DIT=1` | 9 | **582** | 5 | **3. The five keys.** All five were **accepted and silently dropped**; all five are **now refused by name**. | key | upstream anchor | why it cannot be honoured | |---|---|---| | `temperature` | `request_builders.py:14-19,109-114` | the AR stage's only sampler is a fixed top-50 draw, `encoders.py:48,94-103` | | `top_p` | same | no nucleus branch exists | | `top_k` | same | `_AR_SAMPLING_TOP_K` is a module constant of 50 | | `repetition_penalty` | same | no penalty is applied anywhere in the loop | | `max_new_tokens` | `request_builders.py:56-68`, `constants.py:4-5` | upstream's LENGTH, in 25 Hz frames not seconds; the refusal names `audio_duration` and the /25 conversion | **4. The weights table** (`docs/USAGE.md`, "MiniMax-Music3: the exact weights"). It carries: repo **and** revision — `MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec`, verified rather than copied, since `condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to `83179c5e…a202c2a4d`, that revision's own LFS record; the Q4_K artifact's sha256 `4c5d41b2…c70cbdd0` at revision `c36aaeed…` with its exact byte count; **28.5 GB resident (28 517 617 303 B, measured) versus 57.4 GB repository**, with the reason they differ; the refused native `.pth` arm (`qwen_7B/`, `flowmatching_vae.pth`, `dav.pth`) and that SGLang-Omni serves it; and all fourteen community quant repositories across five formats, each marked refused and each marked **third-party** rather than first-party. This is the first application of `.agents/porting-a-model.md` §2.1 (landing as #951). ## Two reds stacked behind one habitually-red job name `windows-msvc-cpu`/`windows-msvc-vulkan` are usually attributed to #645 (`M_PI` in three LTX2 sources). **Neither of the two causes here was #645**, and the first hid the second. **#965, fixed in flow.** `C4456: declaration of 'loaded' hides previous local declaration` at `server_main.cpp:1315` — W6's own speech-attach block declaring `loaded` inside the text engine's `loaded`. The only warning in the job, on `main` since W6 landed. Matched arm: #956, #950 and #939, none touching the speech surface, fail identically. Renamed; nothing suppressed. **#968, filed and NOT fixed here.** With the shadow gone the same jobs failed again on `C4244: conversion from 'const double' to 'float'`, raised inside MSVC's `<vector>` from `ltx2_video.cpp:203,214` — two narrowing `positions.assign` calls that `c7cb59fbb` (#964) landed on `main` while this row was in flight. **This branch touches zero LTX2 files.** The matched arm splits exactly on the merge base: #966 and #951 (on `c7cb59fbb`) hit it, #967/#956/ #950/#939/#938 (before it) do not. It is deliberately left to the LTX-2.5 lane — #964's own comment reasons that "double -> float -> double reproduces the bits", so a silencing cast is a claim about that reasoning rather than a formatting fix. **The finding, which outlives both:** a known-red list tells you a job is often red. It never tells you that today's red is the same one. Only reading the log does — and here it took two readings, because removing the first cause is what made the second visible. <!-- kept for the record --> ### The first of the two, in detail (#965) `windows-msvc-cpu`/`windows-msvc-vulkan` failed here, and they are **not** #645 (`M_PI` in three LTX2 sources). They were W6's own `C4456: declaration of 'loaded' hides previous local declaration` at `server_main.cpp:1315` — the only warning in the job, on `main` since W6 landed. The matched-arm check is what separated it from my diff: #956, #950 and #939, all touching no speech surface, fail identically. `main` has no baseline because `windows-msvc-*` are PR-only (#584), so it presents to each author in turn as their own red. Filed and fixed in flow by renaming the inner declaration; nothing suppressed. ## Mutations Four run, four fire: sampling refusal neutered (5 assertions red), `max_new_tokens` refusal neutered (2 red), `--model` made mandatory again (2 ctest cases red), generate routes registered unconditionally (3 cases / 6 assertions red in the api-server suite, 1 / 2 in the e2e suite). Sources restored and verified sha256-identical. Supersedes #954 (untrailered merge commits) and #963 (a `server_main.cpp` commit that owed `docs/USAGE.md` under the per-commit documentation checkpoint). Same tree, linear history, every commit green on `check-doc-checkpoint`, `check-commit-trailers` and `check-commit-style` locally before pushing. Every source file is byte-identical to the one built and gated. Issue: #672 Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code] --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
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.
Implements A2-R of
.agents/specs/nemotron-h-abi-e2e.md, issue #810, parent row #517.A2 was scoped as one unit and came back as a campaign. This is the first landable piece: the weights the device can actually execute move onto the tree's shared residency seam, and the 6 GQA attention layers compute there.
⚠ The unit names, because the obvious ones collide
The spec's §1 "permitted split" (lines 236-241) already defines an
A2aand anA2b, and they are not this campaign's units. The spec'sA2ais the paged unit — the one that narrows the G-SAFE interlock. An earlier revision of this PR also called itselfA2a, so a reader greppingA2alanded here and expected the interlock to have moved. It has not.A2bis taken twice over: the spec's batching unit, and Laguna's unrelated "Brick A2b" (src/vt/cuda/cuda_laguna.cu:368).OwnedTensorseam; embeddings, 52 norms +norm_fand the 6 GQA blocks on the devicenemotron_h.cpp:1023)A2a— single-request paged decodenum_reqs > 1A2b— batchingnum_reqsclauseThe rename is in the tree, not only here: the three
TEST_CASEnames, all 36 former Greek references across the model sources, theCMakeLists.txtsource entry, both doc rows, and this PR's title. The mapping is recorded attests/vllm/models/test_nemotron_h_forward.cpp§8 so it is greppable.src/vllm/model_executor/models/nemotron_h_registry.cppis the one deliberate exception. It still saysA2b, in the spec's own sense, and the file is byte-identical tomain: it carries the G-SAFE interlock, and an empty diff on it is evidence a reviewer reads directly. Touching it to fix a comment would cost that.What runs where, and why the line falls there
norm_f, the 6 GQA attention blocks. The residual stream is device-resident for the whole forward.lm_head.The line is drawn by the memory format the checkpoint ships, not by taste. The released checkpoint is
MIXED_PRECISIONand only 216 of its tensors are plain bf16 (gated:test_nemotron_h_loader.cpp:254). Everything put on the device comes from that population; everything left on the host does not:mixer.in_proj, FP8 W8A8. The block is not splittable —in_projproduces the fusedzxbcdtthat both the conv and the scan consume — so it moves as a unit, after the shared FP8 W8A8 linear seam is extracted out ofqwen3_5.cpp(ResidentFp8:1456,MatmulFp8CutlassD:1495). That is The FP8 W8A8 linear path is not a shared seam: residency and GEMM entry points live inside qwen3_5.cpp, so a second model cannot reach them without hand-rolling a parallel path #940, deliberately not done here: its own gate is Qwen3.5 byte-identity, which does not belong in a NemotronH row.lm_headare NVFP4 W4A16 g16: 30.19e9 parameters, 15.8 GiB packed, 56.2 GiB dequantized to bf16. There is no device NVFP4→bf16 dequant kernel invtat all (the only standalone device dequant isvt::DequantFp8ChannelBf16,fused_ops.h:31, ROCm-only and per-channel FP8), so a "bf16 everywhere" device forward means a host dequant plus a 56.2 GiB upload. Both gate hosts are unified-memory, so that is a reboot, not an OOM.nemotron_h_loader.h:36-46rejected the same design for the load.What this does NOT prove
46 of 52 layers still compute on the host.
lm_headstill computes on the host. Nothing here is paged, nothing batches, and no throughput number is recorded or implied — this arm is slower than the host reference, because each host layer costs one download plus one upload. That bounce is scaffold, not architecture; every later unit deletes one pair of it.Nothing production-facing reaches the new code.
NemotronHDeviceForwardandNemotronHAttnBlockHostIOare called only fromtest_nemotron_h_forward;ForwardNemotronHForCausalLMstill routes to the hostNemotronHForward. That is disclosed rather than hidden, and it is A2-P that must wire the device arm throughModelRegistry::Forwardor the arm stays dead. Owed, named, and tracked on #810.G-SAFE is fully intact
All three clauses at
nemotron_h_registry.cpp:161-170—attn_kv.empty(),gdn_state.empty(),num_reqs <= 1— are untouched. The branch does not modify that file at all. This arm is non-paged and single-request, so it creates none of the capability the interlock guards and therefore does not narrow it.Not
dense_attn::AttnBlock(#941)The spec's §2 names it as this model's seam. That is wrong, and routing there would reintroduce the exact defect #810 removed from the runner — a shared function reading HF-config fields this architecture does not ship. Three measured reasons:
Qwen3DenseAttnWeights(dense_attn_block.h:335), not this model's separate q/k/v/o.cfg.rms_norm_eps. This config shipslayer_norm_epsilonandnorm_epsand norms_norm_eps, whichhf_config.cpp:551defaults to 0.0 — a silent eps=0 normalization.vt::RopeNeoxunconditionally (:496) againstkNemotronHAttentionHasNoRope.NemotronHAttnBlockfollows the tree's model-local idiom (granite.cpp:84,gemma.cpp:42,glm4.cpp:80, none allowlisted) and documents its six deltas.Not a
d_devonNemotronHOwnedThat was the smaller diff and it is the parallel path AGENTS.md forbids. Dense weights move to the shared
OwnedTensorand upload throughdense_attn::ResidentWeight.NemotronHOwnedsurvives for the quantized weights, precisely becauseOwnedTensorcarries noform, no groupscaleand noinput_scale— converting a quantized weight to it would discard the memory format the checkpoint ships.Review repairs (findings on
f77dbac3f)Four of the seven findings were one defect wearing different clothes: an assertion that cannot fail reads exactly like one that passes.
F1 — the "armed margin" observed nothing, and used the wrong dtype's number
DevRelFor(dt) * 5 < kRopeSeparationcompared two compile-time constants, so it measured nothing about the running system. And2.11e-2was the f32 arm's separation. Measured live, per dtype, on this fixture (Thor sm_110):2.10893e-21e-51.97151e-24e-3So the stored constant was ~6% too large for bf16 — and once the real number is read, the invented
* 5factor reds a bf16 arm that is in fact correctly armed. Lowering 5 to 2 to get green would be widening a scope to turn a gate green, which is the one thing AGENTS.md forbids outright.The proxy is gone. The case builds the rotated counterfactual from the same per-dtype fixture (
RefAttentionRotated, which carries its ownq_movedinstrument) and asserts the property: run the rotated answer through the sameExpectCloseRelarithmetic that accepted the real one, and require it to come out rejected. No stored twin, no safety factor, measured per dtype per run. The host no-RoPE case shares the helper, deleting 40 lines of inlined attention core.F2 — the residency case gated a test-local copy, and
nkhad no reader at allThe fixture builds its
OwnedTensors with the test's ownOwnT, not with productionCopyDenseOwned. It can prove the device arm consumes the type; it can never see the loader mis-produce it. Both halves are now closed against the real checkpoint.host_byteswas printed and read by nothing. Truncating every production payload to half moved it 470 MiB with the forward suite 16/16 green and 3/3 oracle goldens still MATCHING — a shrunkstd::vectorkeeps its buffer, soView()reads the same values back and only the accounting moved. Both totals are now asserted exactly, against the content-pinned revision.CHECK(rep.host_bytes == rep.source_bytes)is deliberately NOT in the file, because it is false. The review proposed it on the strength of the printed linehost bytes: 18013 MiB, source 18013 MiB, but MiB resolution hides the difference: measured exactly, they differ by 15324 bytes, so that assertion would have redded on correct code. What is asserted instead is both exact totals and the identity that explains the gap, so the number is accounted for rather than tolerated.weight_scale_2+ 46×2 FP8 + 12 fp8-KV)host_bytes − source_bytesnkhad no consumer on this path, exactly as the review found:View()does not propagate it and both arms calledvt::MatmulBTunconditionally. Rather than delete the intent, it now has a real reader in the tree's own idiom (qwen3_5.cpp:3353,:3611,:7638): the two functions that callMatmulBT—nemotron_h.cpp:317(host) andnemotron_h_device.cpp:261(device) — refuse a weight whose recorded orientation is not the[out, in]oneMatmulBTreads. The tautological fixture assertions are deleted.F3 — the GB10 leg is RUN, not owed: the bands transfer unchanged
The review asked for this to be documented as owed, because
dgx'sgpu.lockwas held. It came free, so the leg was run instead, and F3 is closed by measurement rather than by a note.The concern was specific and correct: the bands were calibrated against Thor's fallback kernels, with
fa2andcutlass-*reportingDISABLED for [110]. On GB10 the same device arm runs FA2 + CUTLASS GEMMs against the same CPU host reference, and the f32 band1e-5sat only 4.4x above Thor's measured2.29e-6. A different attention kernel could plausibly have eaten that.It did not. The separation is identical to Thor's, to six figures.
2.29122e-062.29122e-060(bit-exact)0(bit-exact)DevRelFor1e-5DevRelFor4e-32.10893e-2/1.97151e-2The rope separations matching exactly is expected and is a property of the repair: the counterfactual is built from the fixture in double precision on the host, so it is host-independent by construction. The device-vs-host separations matching is the actual finding — this arm's ops (
vt::Attention,MatmulBT,RmsNorm,Embedding,FusedChain) land on the same answer through both kernel families.The build is the non-degraded one, which is what makes the result admissible. A degraded build here would answer the Thor question twice rather than the GB10 question once, so the script voids rather than fails on any missing fast path. Configure at
121a:cutlass-fp8: ENABLED, so the result stands.BUILD_EXIT=0,compile_errors: 0,enospc_hits: 0,-j 4, 3.0 TB free. Binariesa330e72f…/68eb1e88….test_nemotron_h_forwardtest_nemotron_h_scaffoldtest_nemotron_h_loader(real checkpoint)host bytes exact: 18888922112, source bytes exact: 18888937436— byte-for-byte the Thor values, which is the evidence that the two new exact literals are properties of the pinned checkpoint plus the loader and not of a host.Two honest caveats on this leg.
local-ai-workerwas not parked: there is no systemd unit by that name ondgxin its current state, sosystemctl stopwas a no-op and the./local-ai workerprocess ran throughout. That is inert for this measurement — every number here is a numerical separation, not a timing — and it would have voided a throughput result, which this row does not record. Free memory during the run was 114 GiB of 119, so there was no pressure. Thegpu.lockwas held for the whole run viaflockand is released.F5 — "both arms share one reader" is not literally true
nemotron_h.cpp:56andnemotron_h_device.cpp:126are two file-local function-local statics with byte-identical predicates, latching independently. Nothing straddles today and the duplication is the tree's idiom (qwen3_5.cpp:1699), but the comment claimed impossibility. It now says what is true: a convention, not an impossibility, and hoisting the predicate is a tree-wide change rather than this row's.F7 — one dead device allocation per layer per step
DBuf mixer_out(d, adt, {T, H})was allocated and then move-assigned over on both branches. The mixer output now goes straight intocarry, which is the only thing that reads it; the previouscarryblock returns to the pool at exactly the same statement as before, so the lifetimes are unchanged. No speed claim is made or implied.Gate
Thor (sm_110), FRESH build directory, real checkpoint
Both gate hosts were exercised — Thor below, GB10 in F3 above. Spec §5.4 requires both, and Thor is the portable/fallback path rather than a second CUDA host.
test_nemotron_h_forwardtest_nemotron_h_scaffoldtest_nemotron_h_loader(real 20.1 GiB checkpoint)Baseline binaries:
test_nemotron_h_forward34a49eaf…,test_nemotron_h_loader4cfeb7e9…. Device confirmed in-container (NVIDIA Thor), no skip message emitted. Per case, each with a non-zero case count:The gate was re-run at the pushed head, not only at the head the mutations used. The final commit is comment-only, and the rebuilt binaries hash to
34a49eaf…/4cfeb7e9…— byte-identical to the baseline — so the whole mutation table below is valid at the merged head rather than at a superseded one.Resolved checkpoint directory recorded as evidence:
/w/ckpt/nemotron-3.5-lightning-30b-nvfp4,VT_NEMOTRON35_SNAPSHOTunset.host bytes exact: 18888922112, source bytes exact: 18888937436. Thor disk 338 GiB free before and after.Mutations
Each applied alone to a scratch copy with anchor uniqueness (
count == 1) asserted on every edit, rebuilt, run, then restored — and every restore rebuilds back to the baseline binary sha, which is the control that proves the tree came back byte-for-byte.0, 0 errorsb8394994…0.0200768against band0.004RelForbands with both new guards disabled, plus M20, 0 errors4a42e062…RelFor, no product mutation at all0, 0 errors0c30f4e1…nkfor all four attention projections0, 0 errorse22fb03f…, loaderea0f360b…weight 'mixer.q_proj' is not in the [out, in] torch-Linear orientation vt::MatmulBT consumesCopyDenseOwnedtruncates every payload to half0, 0 errors5f113cce…, loader3771a85f…host_bytes == 18888922112(read18396080000) and on the composition identityM3, first attemptCOMPILE_EXIT=1, 1 error (unused variable 'atol')b8394994…— identical to M2's, i.e. unchangedTwo traps caught by the harness rather than by luck, both worth recording:
COMPILE_EXIT, the error count and the sha beside every result is what caught it.shutil.copy2preserves mtime, andninjakeys on mtime. A restored file older than its object silently keeps the previous mutation's object in the binary — the first M3B run reported a rotated device arm on unmutated source. Fixed by stamping mtime on restore, and the restore-to-baseline-sha control now makes that class of error impossible to miss.M4 and M5 are both caught only by the checkpoint-gated loader suite, because they are defects in how the loader produces a weight and no synthetic fixture runs that code. That is stated plainly rather than presented as CI coverage.
Full gate
cteston the local x86_64 development arm, whole suite: 485 / 485 passed, 0 failed (2 skipped:test_modelopt_mixed_precision_checkpoint,test_voxtral_e2e), 488.96 s.scripts/agent-preflight.sh: 1 gate failed —test_cpu_x86_llamacpp_floor— and it is not this change's, which was verified rather than assumed. The failure isNO_QUIET_WINDOW … exit 4, the harness refusing to measure, not a floor violation, on a box at load average 157 from this session's own 973-target build. Re-run at load average 15 on the same tree:Ran 10 tests in 49.154s / OK, exit 0. So it is a load artefact, and the local gate is green on a quiet box. This diff touches no CPU and no llama.cpp path. The inherited baseline was RE-MEASURED on this head rather than subtracted from a list. That matters, because it shrank:4880c5715— picked up by the pinned merge above — landed #904's LTX-2.5 device use-after-free, andsanitize-cpuis now GREEN on both legs (address,undefined41m17s,thread44m45s). It is therefore no longer inherited, and a red there would have been mine. The seven #873 gates are likewise green, repaired by #878, and are not subtracted.That leaves exactly one inherited red on CI:
windows-msvc-cpuandwindows-msvc-vulkan(#584), which have nomainbaseline and so are red on every PR. Verified rather than assumed to be inherited — both were already failing on the reviewed headf77dbac3f(gh api .../commits/f77dbac3f.../check-runs→windows-msvc-cpu failure,windows-msvc-vulkan failure), and the failing step isBuild and execute the native Windows CPU focused gate. Everything else on the PR passes, includingcuda-fat-build(1h20m35s),build-test-cpu,build-test-cpu-arm64,build-test-vulkan,device-leakage,pr-size,agent-record,documentation-checkpointandcommit-protocol-tag.origin/mainwas merged at the pinned SHA4880c5715per #841. Three files overlap —CMakeLists.txt,docs/FEATURES.md,docs/USAGE.md— and all three are keyed;git diff 4880c5715after the merge is exactly this row's own edits and nothing else, so every other key came from the target byte-for-byte. The merge also caught the one file the rename had missed (CMakeLists.txt:882).Docs:
FEATURES.mdandUSAGE.mdupdated (both owed and both demanded bycheck-doc-checkpoint). No lifecycle change, so noSTATUS/BENCHMARKS/NOWwrite is owed.One trap this PR hit, recorded rather than worked around
commit-protocol-tagreported this body as missingFollowing-Agents-ProtocolandAI-Assistedwhile it carried both, correctly, in a well-formed final paragraph. The cause was two bare---markdown horizontal rules:squash_merge_commit_message = PR_BODYmakes this body the landed commit message, andgit interpret-trailerstreats a bare---as the end of a commit message, so everything below it — trailer block included — is never parsed. Measured both directions on the stored body:--parsereturned nothing;sed 's/^---*$/***/'returned all three trailers. The checker was right and the body was wrong. Written up in.agents/style/commits.mdso the next author is told which character did it. A table's|---|---|is unaffected — the line is not bare.Follow-ups filed and referenced, not folded in: #940 (extract the shared FP8 W8A8 linear seam), #941 (the spec's §2
dense_attn::AttnBlocknaming is wrong).FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]