Repository navigation
HMMA with an f16 accumulator rounds once per 8 products, as Ampere does (chain change) - #1897
Know-It-All-Brother wants to merge 1 commit into
Conversation
…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.
bbb92fe to
e526814
Compare
|
Measured the same thing on real Ampere on Nov 7, a Jetson Orin (sm_87): 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. |
|
@fxd0h I have Orin Nanos, that's a great idea to validate that way. |
|
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 |
…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>
|
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:
Checked on an M6: |
…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>
…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
…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
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):The change:
HMMA_BODYaccumulates each block of 8 into asimdgroup_float8x8from the half operands and a half C (simdgroup_multiply_accumulatetakes mixed types), then rounds it to f16 per lane throughthread_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.pywithDMSH_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 --checkandtest_docspass.lint_python.py --against origin/main: no new findings in the changed files. The 3 new pyright findings are indm/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:
fa3033d05c0a41d6d99fbeeb53d38847Rounding 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 checkon the other Switch 2 games, in-game picture and frame time, iOS.Related
matmul2don Apple10 GPUs. That path would need the same once-per-8 rounding.Docs
docs/reference/shaders.md("HMMA rounds its f16 accumulator once per 8 products"), andpython3 dm/tools/docs_symptoms.pywas rundmsh_chain.py --headerwas run anddm/gfx/nvn/dmsh_chain.his part of the change