Skip to content

HMMA with an f16 accumulator rounds once per 8 products, as Ampere does (chain change) - #1897

Closed
Know-It-All-Brother wants to merge 1 commit into
seedds:mainfrom
Know-It-All-Brother:hmma-round-once
Closed

Know-It-All-Brother wants to merge 1 commit into
seedds:mainfrom
Know-It-All-Brother:hmma-round-once

Conversation

@Know-It-All-Brother

Copy link
Copy Markdown
Contributor

What and why

The translator's HMMA helper accumulates on simdgroup_half8x8, which rounds the f16 accumulator after every product. Ampere's HMMA with an f16 accumulator adds 8 products to C and rounds once. So an HMMA.16816 is k 0–7 rounded to f16, then k 8–15 on top of that, rounded again.

I checked this against the 5,000 recorded A100 fp16 cases in Khattak and Mikaitis's MATLAB-tensor-core toolbox (model_validation/A100/fp16, C rounded to f16 as HMMA's accumulator is):

Model Cases matching the A100
Per-product f16 rounding (main) 2,420 of 5,000
8 products summed in f32, rounded once 5,000 of 5,000

The change: HMMA_BODY accumulates each block of 8 into a simdgroup_float8x8 from the half operands and a half C (simdgroup_multiply_accumulate takes mixed types), then rounds it to f16 per lane through thread_elements().

It's a chain change, so every Switch 2 game's HMMA shaders rebuild: agl's NDSR upscaler and the NDVE movie networks among them.

Reference entry: HMMA rounds its f16 accumulator once per 8 products.

Checked

  • Tests:

    • test_dmsh_hmma.py with DMSH_TEST_XLATE: 12 tests pass. Its GPU runs now compare every output bit for bit with the two-block reference instead of within a tolerance. main's body fails that on 21,461 outputs.
    • docs_symptoms.py --check and test_docs pass.
    • lint_python.py --against origin/main: no new findings in the changed files. The 3 new pyright findings are in dm/tools/unity_bucket.py, from main.
  • Games run, with title ID and version: none in-game yet. Ocarina of Time's (0400486024DEC000, v131072) NDSR shaders ran isolated on synthetic inputs, and their outputs change in 1.5% (fa3033d05c0a41d6) and 0.8% (d99fbeeb53d38847) of halves.

  • Devices and OS: MacBook Pro M5 Max (64 GB), macOS 27.0.1, Xcode 27.2.

  • Measurements, isolated, best of 200 runs on the M5 Max:

    Shader main This PR
    NDSR fa3033d05c0a41d6 0.276 ms 0.316 ms
    NDSR d99fbeeb53d38847 0.393 ms 0.446 ms

    Rounding through a multiply-accumulate with zero A and B gives the same bits but took about twice as long, so I round per lane.

  • Not checked: regress.py check on the other Switch 2 games, in-game picture and frame time, iOS.

Related

Docs

  • The finding is in docs/reference/shaders.md ("HMMA rounds its f16 accumulator once per 8 products"), and python3 dm/tools/docs_symptoms.py was run
  • A chain tool changed: dmsh_chain.py --header was run and dm/gfx/nvn/dmsh_chain.h is part of the change
  • No game files, keys or data extracted from a game in the change or the attachments

@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders labels Nov 9, 2026
…es, instead of after every product (chain change)

HMMA_BODY accumulated on simdgroup_half8x8, whose multiply-accumulate rounds the f16 accumulator after every product. Ampere's HMMA adds 8 products to C and rounds once (Khattak and Mikaitis's A100 model, N_FMA 8): an HMMA.16816 is k 0-7 rounded to f16, then k 8-15 on top, rounded again. Against the 5,000 recorded A100 fp16 cases of their MATLAB-tensor-core toolbox (C as f16), per-product rounding matches 2,420 and the f32 block sum rounded once matches all 5,000.

Each block now accumulates into a simdgroup_float8x8 from the half operands and a half C, and is rounded to f16 per lane through thread_elements(). test_dmsh_hmma.py's GPU runs compare every output bit for bit with that two-block reference instead of within a tolerance (main's body differs in 21,461 outputs). On an M5 Max, NDSR's fa3033d05c0a41d6 takes 0.316 ms against 0.276 isolated, d99fbeeb53d38847 0.446 against 0.393.
@fxd0h

fxd0h commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Measured the same thing on real Ampere on Nov 7, a Jetson Orin (sm_87): mma.sync.aligned.m16n8k16.row.col.f16.f16.f16.f16 through inline PTX, 3 seeds × 4,096 warps, inputs uniform in [-1, 1].

main's body matched the Orin bit for bit on 34.9% of outputs, and 26.6% for two chained HMMAs. Summing each block of 8 in float and rounding once matched 99.97% in both cases. The 0.03% left were near-cancellations around zero, which looked like my numpy model rather than the hardware. So the Orin agrees with your A100 numbers.

@Know-It-All-Brother

Copy link
Copy Markdown
Contributor Author

@fxd0h I have Orin Nanos, that's a great idea to validate that way.
I wonder if I could get it running on one?

@fxd0h

fxd0h commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

yeah it's a tiny cuda kernel, calls the mma through inline ptx with random halves and dumps the bits, then I just diff against metal on the mac: https://gist.github.com/fxd0h/ee97de9e608d5d93932f7855a73973d4

seedds added a commit that referenced this pull request Nov 9, 2026
…es, instead of after every product: the translated helper and the host's NDVE network sum each block of 8 in f32 and round it to f16, which matches 5,000 of 5,000 recorded A100 cases (per-product rounding 2,420) and 99.97 % of a Jetson Orin's outputs (34.9 %); chain 0c434f1b68df (PR #1897)

HMMA_BODY accumulated on simdgroup_half8x8, whose multiply-accumulate rounds the f16 accumulator after every product. Each block now accumulates into a simdgroup_float8x8 from the half operands and a half C and is rounded to f16 per lane through thread_elements(). test_dmsh_hmma.py's GPU runs compare every output bit for bit with the two-block reference (main's body differs in 21,461 outputs on an M5 Max and on an M6).

ndve_engine.metal's hmma() (#1894, bit for bit with the helper) rounds the same way, into a new matrix: written into its accumulator arrays' own thread_elements(), its layers ran 15 to 50 times slower on an M6. On the M6 the network's seven layer kernels take 2.94 ms against 2.85; isolated on an M5 Max, NDSR's fa3033d05c0a41d6 takes 0.316 ms against 0.276. Not measured in a game.

Co-authored-by: Know-It-All-Brother <354264050+Know-It-All-Brother@users.noreply.github.com>
@seedds

seedds commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Landed as 356a713, with you as co-author. Thanks, and thanks to fxd0h for the Orin run; its numbers are in the reference entry.

What changed on top of the PR:

  • The host's NDVE network rounds the same way. Run nn::ndve's movie network as host Metal kernels behind DM_NDVE_ENGINE=1 #1894 landed before this, and test_ndve_engine.py checks that its kernels match the helper bit for bit, so ndve_engine.metal's hmma() now sums each block of 8 in f32 and rounds once too. It rounds into a new matrix: writing the rounded halves into the accumulator arrays' own thread_elements(), as HMMA_BODY does, made its layers 15 to 50 times slower on an M6. With a new matrix, the seven layer kernels take 2.94 ms against 2.85. In the translated helper both forms cost the same, so HMMA_BODY is yours unchanged.
  • Rebased onto the current chain (now 0c434f1b68df), with the code comment about the zero multiply-accumulate reworded, and a line added to Ocarina of Time's note.

Checked on an M6: test_dmsh_hmma.py passes, and main's body differs in 21,461 outputs there, the same count as on your M5 Max. test_ndve_engine.py, test_docs and test_nsp_chain pass too. No in-game run yet.

@seedds seedds closed this Nov 9, 2026
Know-It-All-Brother added a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…es, instead of after every product: the translated helper and the host's NDVE network sum each block of 8 in f32 and round it to f16, which matches 5,000 of 5,000 recorded A100 cases (per-product rounding 2,420) and 99.97 % of a Jetson Orin's outputs (34.9 %); chain 0c434f1b68df (PR seedds#1897)

HMMA_BODY accumulated on simdgroup_half8x8, whose multiply-accumulate rounds the f16 accumulator after every product. Each block now accumulates into a simdgroup_float8x8 from the half operands and a half C and is rounded to f16 per lane through thread_elements(). test_dmsh_hmma.py's GPU runs compare every output bit for bit with the two-block reference (main's body differs in 21,461 outputs on an M5 Max and on an M6).

ndve_engine.metal's hmma() (seedds#1894, bit for bit with the helper) rounds the same way, into a new matrix: written into its accumulator arrays' own thread_elements(), its layers ran 15 to 50 times slower on an M6. On the M6 the network's seven layer kernels take 2.94 ms against 2.85; isolated on an M5 Max, NDSR's fa3033d05c0a41d6 takes 0.316 ms against 0.276. Not measured in a game.

Co-authored-by: Know-It-All-Brother <354264050+Know-It-All-Brother@users.noreply.github.com>
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…nds once to half), seedds#1909 (fragment uniform reads bound to the binding length), seedds#1901 (dispatch_overlap on for Ocarina), seedds#1459 (MetalFX takes agl_ndsr_vr, no-curve variants linear), seedds#1903 (render scale stubs for Ocarina; replaces the seedds#1497 ceiling we carried)

Hand-resolved: the mesh draw keeps the task isolation of seedds#1842 and the copy paths our nvn_tex_level_range / DM_SHARPEN; seedds#1497's stub, test and BINDING entry go; the build-keyed rule names the render scale function (main+0x31d3200). Chain e906c7467690.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1459 by PromethOps (open): DM_UPSCALER=metalfx takes agl's agl_ndsr_vr.pck networks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's
- seedds#1497 by givethesourceplox (open): Add a render-scale ceiling for Ocarina v131072
- seedds#1842 by ycarogabriel-lgtm (open): Ocarina of Time: opt-in task isolation (DM_NVN_ISOLATE_TASK), the Hi-Z task a5288 in command buffers of its own, a workaround for its GPU hangs on an M1
- seedds#1897 by Know-It-All-Brother (closed): HMMA with an f16 accumulator rounds once per 8 products, as Ampere does (chain change)
- seedds#1901 by FifteenPen (open): The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms
- seedds#1902 by fxd0h (open): HFMA2 rounds once to half, as Ampere does (chain change)
- seedds#1903 by FifteenPen (open): Ocarina of Time takes the launcher's Render scale: the 3D views at that fraction of the game's own size, the dynamic resolution held at its top level (Kokiri Forest docked at 0.75: GPU p50 15.77 → 14.31 ms, 60 fps held)
- seedds#1909 by oz-rinkliff (open): Bound translated fragment uniform reads to the NVN binding length
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…1909 seedds#1897 seedds#1917 seedds#1920 seedds#1936) and seedds#1902, ahead of merging main

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1897 by Know-It-All-Brother (closed): HMMA with an f16 accumulator rounds once per 8 products, as Ampere does (chain change)
- seedds#1902 by fxd0h (open): HFMA2 rounds once to half, as Ampere does (chain change)
- seedds#1909 by oz-rinkliff (open): Bound translated fragment uniform reads to the NVN binding length
- seedds#1917 by FifteenPen (closed): A draw that writes depth and samples its own depth attachment reads a copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone
- seedds#1920 by nawangwe (closed): A GPU-written span's sliver inside a texture does not make it the GPU's: Ocarina of Time's terrain tiles no longer vanish or show another tile's surface (seedds#1566)
- seedds#1936 by TheGoldfish11 (closed): Write resolution-family image textures uncompressed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants