Skip to content

spec(#810 A2-Q): split the quantized arms into A2-Q1 and A2-Q2, because their blockers differ (#517) - #988

Merged
localai-bot merged 2 commits into
mainfrom
row/MODEL-NEMOTRON-H-ABI-A2Q-SPEC
Aug 16, 2026
Merged

spec(#810 A2-Q): split the quantized arms into A2-Q1 and A2-Q2, because their blockers differ (#517)#988
localai-bot merged 2 commits into
mainfrom
row/MODEL-NEMOTRON-H-ABI-A2Q-SPEC

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Splits A2-Q of .agents/specs/nemotron-h-abi-e2e.md into two units with their own specs. Issue #810, parent row #517. Records only: no product code, no lifecycle change.

Per the governing spec's §1.4, the implementation is a separate PR by a different agent and the reviewer is a third. This spec is written by the agent that scoped the work and will not implement either half — a fresh implementer is not fresh if it authored the spec it works from.

Why split, and why it is not about size

A2-Q was briefed as one unit covering the mamba, MoE and lm_head device arms. Scoping measured three independent quantization mechanisms with three independent failure modes — but the decisive reason is that the two halves have different blockers:

Unit Spec Blocker
A2-Q1 — FP8 W8A8 mamba nemotron-h-a2q1-fp8-mamba.md #960, declared as a base. Cannot run on Thor at all until it lands
A2-Q2 — NVFP4 MoE + lm_head nemotron-h-a2q2-nvfp4-moe-lmhead.md none for the build; the Thor leg is PENDING #962

Bundling them makes the NVFP4 half wait on a build-file fix it does not need.

The Thor finding, measured

Recorded in full at A2-Q1 §3 because it inverted twice during scoping. Thor has half an fp8 arm, denied by a build accident rather than by silicon.

The GEMM half works. A standalone probe mirroring BuildFp8Plan's exact key (src/vt/cuda/cuda_matmul.cu:533) — A = e4m3 op T, B = e4m3 op N, CUBLAS_COMPUTE_32F, scale CUDA_R_32F, epilogue DEFAULT — on real sm_110 silicon (NVIDIA Thor, driver 13020, cublasLt 130002):

-- D=bf16 --
  m=1     n=10304  k=2688   status=0 returned=1  -> FP8 PLAN AVAILABLE
  m=8     n=10304  k=2688   status=0 returned=1  -> FP8 PLAN AVAILABLE
  m=512   n=10304  k=2688   status=0 returned=1  -> FP8 PLAN AVAILABLE
  m=1     n=2688   k=4096   status=0 returned=1  -> FP8 PLAN AVAILABLE
  m=512   n=2688   k=4096   status=0 returned=1  -> FP8 PLAN AVAILABLE
  m=64    n=4096   k=4096   status=0 returned=1  -> FP8 PLAN AVAILABLE
-- D=f32 --   (same six shapes)                  -> FP8 PLAN AVAILABLE x6
RESULT: 0 of 12 probes had NO fp8 plan

The six shapes are NemotronH's own in_proj and out_proj at decode, small-batch and prefill widths, plus a square control.

The activation-quant half is missing:

kQuantFp8Static registered ONLY at    cuda_matmul_fp8_cutlass.cu:376
that TU compiled only if              VT_CUTLASS_FP8_ARCHS   (CMakeLists.txt:1668-1669)
CUTLASS refs in the kernel body:      0                      (:353-370)
kMatmulFp8CublasLt registered         cuda_matmul.cu:920 -- unconditional

MatmulFp8CutlassD's guard (dense_fp8_gemm.h:130) keys on kMatmulFp8CublasLt, which IS registered, so the guard passes on Thor and the code crashes one call later on the portable reference tier — which is what #960 measured as a SIGSEGV. The relocation unblocks every non-CUTLASS CUDA arch, so it is dispatched as its own unit; A2-Q1 declares it a base rather than carrying it.

The dependency is written against the issue, not a branch name. git ls-remote --heads origin on 2026-08-16 showed no ref for the dispatched row — it is working locally, pre-push. A base has to be something git can reach.

Two corrections to the brief, recorded rather than quietly applied

The MoE arm does not take a per-expert [K,N] pointer array. That is kMoeGroupedGemmBf16 (include/vt/ops.h:1642). MoeGroupedGemmNvfp4Marlin (:1685) takes a rank-3 strided arena [E, K/16, N*8/pack], validated at src/vt/ops.cpp:884, with processed scales and global scales. That needs a load-time repack, which the pointer array does not — measured at ~16.5 GB device-resident across the 23 MoE layers, with the per-slab arithmetic in A2-Q2 §3.

PrepareNemotronHForCausalLM is a pure no-op (nemotron_h_registry.cpp:113-118, three (void) casts) and must be implemented to host that repack pre-capture, as PrepareQwen3_5Dense (qwen3_5_dense.cpp:104-115) does. That makes it a shared-seam change, not a model-local detail.

The lm_head residency is now a decision, not a default

There are two functions named MarlinDenseResidentFor, different safety, selected by translation unit:

dense_nvfp4_gemm.h:379   static std::unordered_map<const Nvfp4Weight*, MarlinDenseResident> cache;   ADDRESS-KEYED
qwen3_5.cpp:2429         return ResidentIn<MarlinDenseResident>(w->resident_marlin);                 SLOT-KEYED, safe

Each is used by the MatmulNvfp4W4A16D in its own TU (:730 and :2521). The shared seam holds the unsafe one, which inverts the incentive the seam rule creates: following the seam gets you the defect, hand-rolling gets you the fix, and the safe behaviour is not discoverable from the header. Filed as #984. A2-Q2 is exactly the second-consumer condition an address key cannot survive — lm_head plus 5935 expert projections through one header — so A2-Q2 §4.3 requires the choice to be argued in the commit rather than defaulted.

G-SAFE and reachability

Both specs keep all three clauses at src/vllm/model_executor/models/nemotron_h_registry.cpp:162attn_kv.empty() && gdn_state.empty() && num_reqs <= 1 — fully intact. Neither unit creates paging, carried state or batching. A2-P is where it narrows.

Both inherit A2-R's unreached posture: NemotronHDeviceForward is called only from test_nemotron_h_forward.cpp:1805, and A2-P owns wiring it through ModelRegistry::Forward. Each spec states this under ## Owed rather than leaving it to be discovered. A2-Q2 adds the nuance that its Prepare hook is reached from production load even while the forward that consumes it is not.

Gates

scripts/agent-preflight.sh green on this branch. Baseline re-measured at the pinned base 0e1bee42f in a clean detached worktree: all 26 record gates green including the seven #873 ones, Release build 0 warnings 1441/1441, ctest -R "nemotron_h|ops_mamba2|ops_fp8|linear_method" 13/13.

Not touched

.agents/issue-index.md is unchanged deliberately. #810 already has a row, and it is linked from both new specs and from this body, so the three places agree. #960, #962 and #984 were filed or dispatched by others; appending rows for them here risks the clean-but-wrong duplicate that append-only union merges produce when two branches record the same intent. They are instead listed under each spec's ## Owed, which is what check-agent-record.py reads.

Closes nothing. Unblocks the dispatch of both implementations to fresh agents.

FOLLOWING_AGENTS_PROTOCOL

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

mudler added 2 commits August 16, 2026 07:02
…se their blockers differ (#517)

A2-Q was briefed as one unit covering NemotronH's mamba, MoE and lm_head
device arms. Scoping it against the tree measured three independent
quantization mechanisms with three independent failure modes, and the
reason to split is not size.

The two halves have DIFFERENT blockers. A2-Q1 (FP8 W8A8 mamba) cannot run
on Thor at all until #960 lands. A2-Q2 (NVFP4 MoE + lm_head) builds and
runs there today, but cannot yet be trusted to report a number (#962).
Bundling them makes the NVFP4 half wait on a build-file fix it does not
need, so this commit gives each half its own spec and its own declared
dependency.

The measured Thor finding is recorded in full in A2-Q1 §3, because it
inverted twice during scoping and cannot be re-derived cheaply. Thor has
HALF an fp8 arm, denied by a build accident rather than by silicon:

  * The GEMM half works. A standalone probe mirroring BuildFp8Plan's exact
    key (cuda_matmul.cu:533) on real sm_110 silicon returns an fp8-e4m3 TN
    heuristic for all six NemotronH mamba shapes at both bf16 and f32 D --
    12 of 12 plans available.
  * The activation-quant half is missing. kQuantFp8Static's only CUDA
    registration is cuda_matmul_fp8_cutlass.cu:376, and CMakeLists.txt
    :1668-1669 compiles that TU only when VT_CUTLASS_FP8_ARCHS is
    non-empty -- empty on sm_110. The kernel body (:353-370) is a plain
    elementwise scale-and-convert with zero CUTLASS references. It is
    trapped in a CUTLASS-gated TU while its GEMM partner is available on
    the same arch.

MatmulFp8CutlassD's guard keys on kMatmulFp8CublasLt, which IS registered,
so the guard passes on Thor and the code crashes one call later on the
portable reference tier -- which is what #960 measured. The relocation
unblocks every non-CUTLASS CUDA arch, not just Thor and not just this
model, so it is dispatched as its own unit and A2-Q1 declares it a base
rather than carrying it. The dependency is written against the ISSUE: on
2026-08-16 `git ls-remote --heads origin` showed no ref for the dispatched
row, and a base has to be something git can reach.

Two corrections to the brief are recorded rather than quietly applied. The
MoE arm does NOT take a per-expert [K,N] pointer array -- that is
kMoeGroupedGemmBf16. MoeGroupedGemmNvfp4Marlin takes a rank-3 strided
arena [E, K/16, N*8/pack], validated at src/vt/ops.cpp:884, needing a
load-time repack measured at ~16.5 GB device-resident across the 23 MoE
layers. And PrepareNemotronHForCausalLM (nemotron_h_registry.cpp:113-118)
is a pure no-op that must be implemented to host that repack pre-capture,
which makes it a shared-seam change rather than a model-local detail.

A2-Q2 §4.3 turns the lm_head residency into an explicit decision. There
are two functions named MarlinDenseResidentFor with different safety,
selected by translation unit: dense_nvfp4_gemm.h:379 keys a process-static
cache on the weight's ADDRESS, while qwen3_5.cpp:2429 uses the
resident_marlin ResidentSlot that #237 added to fix exactly that. The
shared seam holds the unsafe one, so following the seam gets you the
defect and hand-rolling gets you the fix. Filed as #984; A2-Q2 is the
second-consumer condition an address key cannot survive, so it must choose
deliberately instead of defaulting.

Both specs keep all three G-SAFE clauses at nemotron_h_registry.cpp:162
intact, and both inherit A2-R's unreached posture -- the device forward
still has no production caller until A2-P, which is stated in each spec's
Owed section rather than left to be discovered.

No product code and no lifecycle change. Per the governing spec's §1.4 the
implementation is a separate PR by a different agent and the reviewer is a
third, which is why this spec is written by the agent that scoped the work
and will not implement it.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
Concurrent landing. #985 touches .agents/specs/expert-streaming.md and this
branch touches only the three NemotronH spec files, so the trees are
disjoint. Re-gated after the merge rather than read from the diff: a clean
merge is not a merge that builds the behaviour either side had (#818).

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
@localai-bot
localai-bot merged commit 4496ef1 into main Aug 16, 2026
22 of 24 checks passed
localai-bot pushed a commit that referenced this pull request Aug 16, 2026
`origin/main` advanced to `4496ef196` (#988). It touches three NemotronH spec
files and nothing else — no source, no test, no build file and no keyed record —
so this merge changes nothing this row builds or gates, and the full gate above
still describes the tree.

Stated rather than assumed: `git diff --name-only e770a39 4496ef1` returns
`.agents/specs/nemotron-h-a2q1-fp8-mamba.md`,
`.agents/specs/nemotron-h-a2q2-nvfp4-moe-lmhead.md` and
`.agents/specs/nemotron-h-abi-e2e.md`, and no fourth path.

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
…# Now`

The `## Now` line said "landed on `main`", which it is not: it is open as
#991, measured on both gate hosts, awaiting a fresh review and an operator
merge. A `## Now` that anticipates its own landing is exactly the record
that cannot be trusted afterwards -- the whole point of the line is that a
reader learns the position without asking anyone.

It also names the only red lanes and why they are not this row's:
`windows-msvc-cpu`/`windows-msvc-vulkan` fail at the identical step on
#988 and #982, which share no code with this change, so the arm is
matched rather than assumed; the cause is #968 under #584's PR-only lane
and a fix is already open as #983.

Issue #960.

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
Pinned SHA, per #841. main moved four commits while this row was
measuring and re-landing: #993, #982, #988 and #992. One of them is
directly relevant -- #988 split #810's A2-Q into A2-Q1 and A2-Q2, and
A2-Q1 is the unit this row exists to unblock -- and none of them touch
the fp8 quant registration, the CUDA source list, or either checker this
branch edits.

`.agents/issue-index.md` is the only keyed record both sides wrote.
Verified afterwards that origin/main's copy is a strict PREFIX of the
merged file and that only this branch's own two rows follow it, which is
what append-only means and what a clean-but-wrong interleave would break.

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
…nostic

"Known-red" is not a measurement, and this file was carrying it as one.
The two `windows-msvc-*` jobs now carry the diagnostic that failed them:
`warning C4244: '=': conversion from 'const double' to 'float'`, raised
inside MSVC's own `<vector>` while compiling
`src/vllm/multimodal/ltx2_video.cpp`, promoted to `error C2220` by `/WX`.
That is #968, its fix is already open as #983, and `main` carries no
baseline for those jobs because #584 makes them PR-only. The matched arm
is recorded too: #988 and #982 fail at the identical step and share no
file with this row.

Also records the post-merge full gate: 491/491 rather than 489/489,
because `3ce1cf7c7` adds two suites. Quoting the old number after a merge
that changed the denominator would be a stale measurement presented as a
current one.

Issue #960.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants