Repository navigation
HMMA chains keep their accumulators and A in the SIMD-group's matrices, and A loads where its LDSM read it: Ocarina of Time's movie networks take 18.9 ms of compute a frame instead of 26.7 on an M4 - #1456
Conversation
…s, and A loads where its LDSM read it: Ocarina of Time's movie networks take 18.9 ms of compute a frame instead of 26.7 on an M4 Each dmsh_hmma16816_f16 call took every operand through the SIMD-group's scratch block: the lanes stored A, B and C, the matrix hardware loaded them, and D went back to the registers the same way, with three SIMD-group barriers. In the NDVE networks that Ocarina of Time's title and opening movies run (eleven compute shaders, 2,217 HMMAs), 1,890 HMMAs continue an accumulation whose D nothing else reads, four calls in a row share A, and A comes from an LDSM whose rows are a strided tile in shared memory, at four shuffles a lane. TensorPass.Prepare now gives each HMMA flags from the optimised IR of its block: an accumulator slot for a chain, A kept from the previous call, C = 0, and A read where an LDSM read it (its row address, the four matrices' offsets and the row stride). The last needs the address to be the same in every lane but for a part known for each of the 32 lanes, so a divergence analysis of the function and an evaluation of that part come with it. An LDSM in a compute shader becomes one MatrixLoad a matrix, lowered to the same per-lane code afterwards. The translator's own body still reads only the registers, so each flag is a promise about values it computes anyway. dmsh_build.py's hmma_msl gives each calling function its accumulator and A matrices, passed by reference so the inlined helper keeps them in registers, and the caller's shared memory array for A; the LDSM's per-lane code is then unused. Measured on the title screen, median over 321 profiled frames of the frame's compute (DM_NVN_GPUPROF=every:5, DM_NVN_DISPATCH_OWN=1), M4 MacBook Air with 8 GPU cores: 26.65 ms before, 18.85 after; the title screen went from 35.5 to 48.7 fps. test_dmsh_hmma.py runs a sequence of flagged calls on the GPU against a CPU reference and checks the flags the translator gives two chained HMMAs on an LDSM's matrices. DMSH_TENSOR_TRACE=1 prints why an HMMA's A is not loaded where its LDSM read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, and A loads where its LDSM read it: Ocarina of Time's title movie network takes 11.4 ms of compute a frame instead of 14.8 on an M6 (26.7 to 18.9 on an M4; chain 23a1ea1db092, PR #1456) Each dmsh_hmma16816_f16 call took A, B and C through the SIMD-group's scratch block and D back through it, three barriers a call, although most of these HMMAs continue an accumulation nothing else reads, share A with the call before, and take A straight from an LDSM. TensorPass.Prepare now gives each HMMA flags (an accumulator slot for a chain, A kept, C = 0, A loaded where its LDSM read it), each a promise about values the translator's own body computes anyway; dmsh_build.py's hmma_msl keeps the slots and A's matrices in each calling function's registers and loads A from the caller's shared memory. An LDSM in a compute shader is a MatrixLoad a matrix until then. The divergence analysis that decides whether an LDSM's row address is the same in every lane plus a known per-lane part also treats a loop header's phis as divergent when the loop has several back edges and any branch in it depends on the lane: lanes that took different back edges bring the header different values. It changes none of Ocarina of Time's 71 HMMA shaders. Checked: test_dmsh_hmma.py (GPU against a CPU reference, and the translator's flags), the graphics tool tests; Ocarina of Time's 42,854 shaders build as on main (7 + 2 known failures), title A/B on an M6 (compute 14.78 to 11.41 ms, GPU span 15.63 to 11.87), title and opening movie clean on both hosts; Donkey Kong Bananza's 23,638 shaders build, its opening, cutscenes and first punch render cleanly with no red line (its NDSR upscaler takes the chain flags). Not run: gameplay A/Bs, the iPhone. Co-authored-by: yangyidong <63278156+sheepyd@users.noreply.github.com>
|
Landed as bd9f422, with you as co-author. Thank you, this is a clean piece of work: flags that are promises about values the translator's own body computes anyway kept every fallback correct. What changed on the way in:
Checked here on an M6 (12 GPU cores): the title screen's compute 14.78 → 11.41 ms a frame and the GPU span 15.63 → 11.87 ms, main against this change. The title and opening movie are clean on both. OoT's 42,854 shaders build as on main. Donkey Kong Bananza's 23,638 shaders build and its opening and first play render cleanly. agl's NDSR upscaler (in both games) takes the chain flags, but none of its A loads directly: its LDSM rows are addresses read from a table in shared memory, so A goes through the scratch there. That's noted in the entry. |
|
On Ocarina of Time's gameplay upscaler (agl_ndsr_vr.pck) this rewrite draws regular dotted rows across the whole 3D scene on an M3 Max, from the first frame in Kokiri Forest; the title and opening movie are fine, which is what the commit checked. Bisected on one app down to the 89 network shaders rebuilt with |
…s, and A loads where its LDSM read it: Ocarina of Time's title movie network takes 11.4 ms of compute a frame instead of 14.8 on an M6 (26.7 to 18.9 on an M4; chain 23a1ea1db092, PR seedds#1456)
…s, and A loads where its LDSM read it: Ocarina of Time's title movie network takes 11.4 ms of compute a frame instead of 14.8 on an M6 (26.7 to 18.9 on an M4; chain 23a1ea1db092, PR seedds#1456) Each dmsh_hmma16816_f16 call took A, B and C through the SIMD-group's scratch block and D back through it, three barriers a call, although most of these HMMAs continue an accumulation nothing else reads, share A with the call before, and take A straight from an LDSM. TensorPass.Prepare now gives each HMMA flags (an accumulator slot for a chain, A kept, C = 0, A loaded where its LDSM read it), each a promise about values the translator's own body computes anyway; dmsh_build.py's hmma_msl keeps the slots and A's matrices in each calling function's registers and loads A from the caller's shared memory. An LDSM in a compute shader is a MatrixLoad a matrix until then. The divergence analysis that decides whether an LDSM's row address is the same in every lane plus a known per-lane part also treats a loop header's phis as divergent when the loop has several back edges and any branch in it depends on the lane: lanes that took different back edges bring the header different values. It changes none of Ocarina of Time's 71 HMMA shaders. Checked: test_dmsh_hmma.py (GPU against a CPU reference, and the translator's flags), the graphics tool tests; Ocarina of Time's 42,854 shaders build as on main (7 + 2 known failures), title A/B on an M6 (compute 14.78 to 11.41 ms, GPU span 15.63 to 11.87), title and opening movie clean on both hosts; Donkey Kong Bananza's 23,638 shaders build, its opening, cutscenes and first punch render cleanly with no red line (its NDSR upscaler takes the chain flags). Not run: gameplay A/Bs, the iPhone. Co-authored-by: yangyidong <63278156+sheepyd@users.noreply.github.com>
What and why
This continues the open item of Ampere HMMA through simdgroup_matrix: it follows LDSM → HMMA → HMMA chains for the accumulator and for A. B and MOVM are not covered.
Symptom. Ocarina of Time's title and opening movies run the NDVE network. It is 11 compute shaders in
cs_movie.binwith 2,217 HMMA.16816.F16, and they take 27 ms of GPU a frame on an M4 MacBook Air with 8 GPU cores. The title screen runs at 35 fps. The OoT profile is in #1299; I ran it on main plus #1299, #1300 and #1312.Cause. Each
dmsh_hmma16816_f16call took every operand through the SIMD-group's scratch block. The lanes stored A, B and C,simdgroup_loadread them back, and D returned to the registers the same way. That is three SIMD-group barriers per call. In these networks:Measured on the 4 frames from 1200 to 1203, with wrong pictures:
Change.
TensorPass.Prepareworks on the optimised IR of each block and appends flags to every HMMA, as a constant argument after C:FlagCSlot,FlagDSlot).FlagAReuse).FlagCZero).FlagADirect;FlagATransfor .MT88). The call also gets the LDSM's row address, each matrix's offset from lane 0's row, and the row stride.FlagADirectneeds the row address to be U + f(lane), where U is the same in every lane and f is known for all 32 lanes. A divergence analysis over the function decides U:MatrixLoadper matrix, lowered to the old per-lane code afterPrepare. There is one destination each becauseOptimizer.RemoveNodetreats an operation as dead once one of its destinations loses its last use.dmsh_build.py'shmma_msl:FlagADirectis ignored and A goes through the block;FlagADirect, loads A bysimdgroup_loadat lane 0's row (simd_broadcast) plus the matrix's offset. The LDSM's per-lane code is then unused and the Metal compiler drops it.DMSH_TENSOR_TRACE=1on admsh_xlatejob prints why an HMMA's A is not loaded where its LDSM read it.The new reference entry is HMMA chains keep their matrices in the SIMD-group. This is a chain change.
Size: about 1,180 added lines. 250 of them are the test.
TensorPass.csis 888 lines, of which about 400 are code; the rest are blank lines, braces and the 75 lines of the uniformity whitelist. That is over the roughly 1,000 lines in CONTRIBUTING.md, so tell me if you would rather have it split, for example the divergence analysis andFlagADirectas a second PR.Checked
Tests:
dm/gfx/tests/test_*.pyfiles pass withDMSH_TEST_XLATE(385 tests together withtest_nsp_chain,test_docsandtest_xlate_anycpu).test_dmsh_hmma.pynow runs four flagged calls on the GPU against a CPU reference: A from shared memory, plain and transposed; A kept; C = 0; two accumulator slots. Each call gets the registers the translator's body would read, including a stale A where A comes from shared memory. The largest error is 0.014 for two chained HMMAs with an f16 accumulator between them. The single-call check still has a largest error of 0.009.DMSH_TEST_XLATE,test_dmsh_hmma.pyalso checks the flags the translator gives two chained HMMAs on an LDSM's matrices.make -C dm/gfx/testswas not run.Games run:
0400486024DEC000, v131072 NUP, base v0) on the Mac, on a branch of main plus [WIP] The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, v0, SDK 22.2.8) runs on the Mac through the title movie into Kokiri Forest #1299, Ampere F2IP and a task shader's own payload read: Ocarina of Time's untranslated shaders translate, all but one that is data #1300 and Zelda: Ocarina of Time — Restore missing mesh and texture effects on Mac #1312.dmsh_hmma16816_f16body and the flags are identical to what this branch builds from main.Devices and OS: MacBook Air M4 (8 GPU cores, 16 GB), macOS 27. Not run on an iPhone.
Regression check:
scripts/regress.py check --gameswas not run. The only local game with this engine is OoT, and Donkey Kong Bananza (the other HMMA user) is not available here. Everything this change does is limited to compute shaders that contain an LDSM or an HMMA. Their MSL changes in two ways:Bananza's upscaler (
7629d857a0a93833) should be the first thing to check.Measurements: median over 321 profiled frames (frames 1000 to 2600,
DM_NVN_GPUPROF=every:5,DM_NVN_DISPATCH_OWN=1) of the frame's compute on the title screen:Of the 2,217 HMMAs, 540 load A where its LDSM read it and 1,249 keep the previous call's A. Of the other 428, 427 have a shared memory store between the LDSM and the HMMA. Most of those are in
d15370a382630d4e.What is left is B's trip through the block. B is the weights: LDSM.MT88 reads them before the tile loop, they are kept in registers, and the shared memory is reused for activations. With B loaded from shared memory as well (a fake address, measured only), the frame took 12.6 ms. Keeping B as matrices in registers took it to 17.4 ms, but slowed
0a56002cdc7e1fce(4 SIMD-groups, 144 matrices) by 13 %, so it is not part of this PR. Removing the barrier at the end of a mid-chain call changed nothing (18.84 against 18.88 ms).Docs
docs/reference/<subsystem>.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index). It is not in a game note yet, because OoT's note comes with [WIP] The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, v0, SDK 22.2.8) runs on the Mac through the title movie into Kokiri Forest #1299.dmsh_chain.py --headerwas run anddm/gfx/nvn/dmsh_chain.his part of the change.🤖 Generated with Claude Code