feat(#810 A2-Q1): NemotronH's 23 Mamba2 blocks reach the device on the FP8 W8A8 seam (#517) - #1288
Closed
localai-bot wants to merge 3 commits into
Closed
feat(#810 A2-Q1): NemotronH's 23 Mamba2 blocks reach the device on the FP8 W8A8 seam (#517)#1288localai-bot wants to merge 3 commits into
localai-bot wants to merge 3 commits into
Conversation
…e FP8 W8A8 seam (#517) The 23 Mamba2 layers were the last host bounce of a decode step, and they were the expensive one. The host arm reaches its two projections through `Linear(..., const NemotronHOwned&)`, whose `DenseFor` calls `NemotronHOwned::DenseBf16()` -- a FULL dequant of the fp8 tower into a fresh bf16 buffer ON EVERY CALL. That is 23 x (10304x2688 + 2688x4096) = 890e6 elements re-expanded per token, plus a download of the normed hidden and an upload of the mixer output per layer. The block moves whole or not at all: `mixer.in_proj` produces the fused `zxbcdt` that the causal conv and the SSD scan both consume (mamba_mixer2.py:550, split :692-696), so there is no intermediate landing in which the conv is on the device and `in_proj` is not. That is why the shared FP8 W8A8 linear seam had to be extracted first (#940, `dense_fp8_gemm.h`), and this change routes through it rather than re-typing its entry points. `NemotronHMamba2MixerDevice` mirrors the host arm statement for statement -- same vt:: ops, same order, same dtypes, different backend, which is the property A2-R established and the numeric gate reads: in_proj (FP8 W8A8) -> QkvSplit(z | xBC | dt) -> CausalConv1dFwd(silu) -> QkvSplit(x | B | C) -> Mamba2ChunkScan -> RmsNormGatedGroup(n_groups) -> out_proj (FP8 W8A8) The one substitution is the split: the host arm copies columns with `SliceCols` because `vt::Mamba2ChunkScan` validates every operand contiguous, and `vt::QkvSplit` is exactly that copy on the device. Both forwards select it at runtime, never by preprocessor: `MambaIsFp8` names only `NemotronHWeightForm` and `vt::OpRegistered` is the op table's own answer, so a dense NemotronH or a device without the fp8 pair keeps the host bounce. The paged arm hands the mixer the recurrent rows `vt::GdnStateGather` already gathered and zeroed and it advances them IN PLACE, so the two downloads, the host mixer and the two uploads per layer are gone; A2-P's gather/scatter pair and its `has_initial = true` reasoning are unchanged. The `ssm_dtype == f32` term in the paged selection is not decoration. The gather widens the page into an f32 working buffer by op contract and the host arm then narrows it back to `ssm_dtype`, so on a checkpoint whose `mamba_ssm_cache_dtype` is not f32 the two arms would round differently and the per-block numeric gate would be comparing two different computations. The device residency is built on first use and keyed on a `ResidentSlot` the weights own, never on an address (#237): across two engine builds in one process an address-keyed cache returns the previous engine's device pointers, which is plausible wrong values rather than a crash. The e4m3 staging copy is released as soon as `ResidentFp8` has uploaded it, so the conversion peaks at one projection rather than at the whole 890 MB tower. `dense_fp8::ResidentFp8` still does not account its own upload -- that is #974, and this change does not touch the shared header -- so A2-Q1 accounts what IT uploads at the site that causes it, through the same `load_stats::AddDeviceUpload` `ResidentWeight` and `ResidentNvfp4` call. That counter is also the instrument the new residency case reads: an arm that re-uploaded the tower every step returns identical numbers to one that uploads it once. G-SAFE is untouched. All three clauses of the interlock stand, this change creates no paging, no carried state and no batching that A2-P did not already create, and `num_reqs <= 1` remains A2-B's to remove. `tests/vllm/models/test_nemotron_h_mamba_device.cpp` is the cheap arm in front of the real-checkpoint gate, for the same reason A2-Q2a's device MoE file exists: `BuildTiny` is all-`kDense`, so nothing in the existing device suite could reach the FP8 W8A8 arm at all. The two arms are NOT bit-comparable by construction -- the host reference is W8A16 and the device arm is W8A8 as vLLM is -- so every band is measured in the run against a defect the fixture separates, and the element count is asserted against the geometry so a maximum over zero elements cannot read as agreement. `scripts/nemotron-h-a2q1-dgx-gate.sh` records the GB10 recipe rather than leaving it to be retyped, including the three environment facts that have each voided a run before: the CUDA lane is `sbsa`, `nvcc --version` is not a sufficient postcondition, and `/workspace` is CIFS and refuses symlinks. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
… than inherit it silently `ResidentIn` locks only the slot's creation and the build runs outside that lock, which is the shape `NemotronHMoeBlockDevice` already has for its Marlin arena: two threads entering one layer for the first time would both build it. Nothing in this tree drives one model's forward from two threads, and diverging from the arena's idiom here would be an unrelated change, so the property is written down at the call site instead of being left for a reviewer to rediscover. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
Collaborator
Author
|
Superseded by #1289, which carries the identical tree on a linear branch off This branch's merge commit Closed rather than force-pushed: AGENTS.md says never force-push and never add a force option, without carving out task branches. #1289 is the same change with one commit off the current |
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.
feat(#810 A2-Q1): NemotronH's 23 Mamba2 blocks reach the device on the FP8 W8A8 seam (#517)
The 23 Mamba2 layers were the last host bounce of a decode step, and they were
the expensive one. The host arm reaches its two projections through
Linear(..., const NemotronHOwned&), whoseDenseForcallsNemotronHOwned::DenseBf16()-- a FULL dequant of the fp8 tower into a freshbf16 buffer ON EVERY CALL. That is 23 x (10304x2688 + 2688x4096) = 890e6
elements re-expanded per token, plus a download of the normed hidden and an
upload of the mixer output per layer.
The block moves whole or not at all:
mixer.in_projproduces the fusedzxbcdtthat the causal conv and the SSD scan both consume (
mamba_mixer2.py:550, split:692-696), so there is no intermediate landing in which the conv is on thedevice and
in_projis not. That is why the shared FP8 W8A8 linear seam had tobe extracted first (#940,
dense_fp8_gemm.h), and this change routes through itrather than re-typing its entry points.
What it does
NemotronHMamba2MixerDevicemirrors the host arm statement for statement --same vt:: ops, same order, same dtypes, different backend:
The one substitution is the split: the host arm copies columns with
SliceColsbecause
vt::Mamba2ChunkScanvalidates every operand contiguous, andvt::QkvSplitis exactly that copy on the device.Both forwards select it at runtime, never by preprocessor.
MambaIsFp8namesonly
NemotronHWeightFormandvt::OpRegisteredis the op table's own answer,so a dense NemotronH or a device without the fp8 pair keeps the host bounce.
The paged arm hands the mixer the recurrent rows
vt::GdnStateGatheralreadygathered and zeroed and it advances them IN PLACE, so the two downloads, the
host mixer and the two uploads per layer are gone; A2-P's gather/scatter pair
and its
has_initial = truereasoning are unchanged.The
ssm_dtype == f32term in the paged selection is not decoration. The gatherwidens the page into an f32 working buffer by op contract and the host arm then
narrows it back to
ssm_dtype, so on a checkpoint whosemamba_ssm_cache_dtypeis not f32 the two arms would round differently and the per-block numeric gate
would compare two different computations.
Three points where this differs from the spec, deliberately
Fp8Weightpair is built ON FIRST DEVICE USE in aResidentSlottheweights own, not by the loader as spec §4.1/§4.2 proposed. That is A2-Q2a's
newer idiom and it is better here: it does not double the 890 MB tower in
host memory at load, it uploads nothing on a host-only run, and it leaves
rep.host_bytes(the literal18888922112pinned attest_nemotron_h_loader.cpp:310) untouched, so §4.2's re-derivationobligation does not arise. Keying on the slot rather than an address is
CUDA BF16 MoE resident cache reuses stale pointers after engine teardown #237: across two engine builds in one process an address-keyed cache returns
the previous engine's device pointers, which is plausible wrong values.
dense_fp8::ResidentFp8still does not callload_stats::AddDeviceUpload--that is FP8 W8A8 weights are uploaded without load accounting or the post-upload residency step, unlike every other resident weight in the same file #974, unchanged, and the shared header is not touched -- so A2-Q1
accounts what IT uploads at the site that causes it, as
ResidentWeightandResidentNvfp4do.ResidentFp8has uploaded it,so the conversion peaks at one projection rather than at the whole tower.
The comparison is not bit-comparable, and that is the point
The host reference is W8A16:
DenseBf16says outright thatinput_scaleiscarried and not applied. The device arm is W8A8, which is what vLLM does. The
difference between them is the e4m3 activation quantization, so every band in
tests/vllm/models/test_nemotron_h_mamba_device.cppis measured in the runagainst a defect the fixture separates, and every element count is asserted
against the geometry -- a maximum over zero elements is also 0.0.
The new file is the cheap arm in front of the real-checkpoint gate, for the same
reason A2-Q2a's device MoE file exists:
BuildTinyis all-kDense, so nothingin the existing device suite could reach the FP8 W8A8 arm at all.
G-SAFE
Untouched. All three clauses of the interlock at
nemotron_h_registry.cpp:162stand. This change creates no paging, no carriedstate and no batching that A2-P did not already create, and
num_reqs <= 1remains A2-B's to remove.
What is NOT proved here
The §5.1 per-block numeric gate on the real 20.1 GiB checkpoint, the A3
96/96 mode=decode STRICT PASSre-run, the §5.3 mutation pass and the GPU busyfraction are the unit's acceptance test and they need a GB10 lease.
scripts/nemotron-h-a2q1-dgx-gate.shrecords the recipe, including the threeenvironment facts that have each voided a run before: the CUDA lane is
sbsa,nvcc --versionis not a sufficient postcondition, and/workspaceis CIFS andrefuses symlinks. The spec's
## Nowand## Owedsay so, anddocs/BENCHMARKS.mdrecords the measurement as PENDING rather than as silence.Issue: #810 (parent row #517). Spec:
.agents/specs/nemotron-h-a2q1-fp8-mamba.md.FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]