Skip to content

WASM PR 4: fold the repeated fused-op arms into macros - #2566

Open
czoli1976 wants to merge 7 commits into
sonos:mainfrom
czoli1976:refactor/wasm-fused-arm-macros
Open

WASM PR 4: fold the repeated fused-op arms into macros#2566
czoli1976 wants to merge 7 commits into
sonos:mainfrom
czoli1976:refactor/wasm-fused-arm-macros

Conversation

@czoli1976

Copy link
Copy Markdown
Contributor

Stacked on #2565. This is the point of the series.

Each of the six f32 kernels spelled the same 24 FusedKerSpec arms out once per accumulator, so 8x8 needed 535 lines to say what 4x1 says in 86, and widening a kernel meant copying every arm again. Of 2342 kernel lines, 2005 were those arms and only 337 were the AddMatMul loops that actually differ.

The arms that apply one operation uniformly across the accumulators now expand from wasm/fuse.rs. Each macro emits the same statements in the same order, so this is a source-level change only. Per kernel the 24 arms become 24 lines.

The two layouts need different primitives, and which one an arm reaches for is visible on the arm's own line rather than hidden behind a flag: in the GEMV kernels a per-row operand is a vector load and a per-column one is a splat, and in the GEMM kernels it is the other way round. 8x8 holds each row as a low/high pair that takes one splat between them.

Five arms stay written out per kernel. AddMatMul and AddRowColProducts both carry the fused-versus-separate multiply-add choice, and keeping them adjacent and explicit is what makes that choice checkable by eye — #2565 added the test that enforces it. AddUnicast and Store walk the C tile by stride differently in each kernel. LoadTile differs too: 4x1 uses an unaligned v128_load where the others dereference, so a shared macro would have changed the alignment immediate for no gain.

The last commit corrects documentation that had drifted: WASM_RELAXED_SIMD.md said all six f32 kernels switch to fused multiply-add under +relaxed-simd, when only 32x1 and 8x8 do and the quoted 1.40–1.55x applies only to those; the header on wasm_i32_4x4 claimed bit-identity with generic_i32_4x4 without qualification, which holds only without +relaxed-simd, since the relaxed dot takes an i7 second operand; and the module gate moves from target_family = "wasm" to target_arch = "wasm32", matching what the kernels require.

Evidence

Same gate as #2563, and it is what makes this reviewable: the emitted body of all eight kernels is byte-identical, under +simd128, under +simd128,+relaxed-simd, and in the shipped release profile with lto = true. So the 1153 deleted lines can be read as deleted repetition rather than as a rewrite of six hand-tuned SIMD kernels.

Benchmarks are a guard here rather than the claim, since the instructions are unchanged. Best-of-two runs of benches/wasm.rs across 24 shapes: median +0.5%, range −1.4% to +2.1%.

Tests: 2195 under +simd128, 2208 under +simd128,+relaxed-simd, with tract-core and test-unit-core green in both. No new clippy lints.

wasm_i32_4x4 is deliberately left alone — it already loops over a scalar [[i32; 4]; 4] at about 7 lines an arm, so it never had this problem, and collapsing it would mean a second macro family for scalar accumulators.

Across the four PRs wasm.rs goes from 3159 lines in one file to 2196 across eight.

🍍

czoli1976 and others added 7 commits August 3, 2026 10:07
…nel and madd macros

wasm.rs was a single 3159-line file, while every other linalg backend is a
module root plus a subdirectory. Move the three parts that carry no f32 MMM
kernel code into wasm/madd.rs, wasm/mmm_i32.rs and wasm/act.rs, and re-export
them from the root so every existing path still resolves. Pure relocation: the
emitted kernel bodies are unchanged under both +simd128 and +simd128,+relaxed-simd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… wasm/ module tree

Splits the six f32 kernels by accumulator layout: wasm/mmm_f32_gemm.rs holds the
two that pack columns into lanes (4x4, 8x8), wasm/mmm_f32_gemv.rs the four that
pack rows and fix nr=1 (4x1, 8x1, 16x1, 32x1) — the same split the mmm_f32 and
mmv_f32 dispatch callbacks make. The dispatch assertions move to
wasm/dispatch_tests.rs. Pure relocation: the emitted kernel bodies are unchanged
under both +simd128 and +simd128,+relaxed-simd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… target

Three timing modules lived in src behind #[cfg(test)] + #[ignore], which no
other backend does. benches/wasm.rs already supersedes two of them — its own
header records that looping four kernels back-to-back biased the in-src
version — so those are dropped, and the activation bench moves across. The
numerical-consistency check between 16x1 and 32x1 is an assertion rather than
a timer, so it stays in src alongside the dispatch tests. The four copies of
the kernel-lookup helper in the bench collapse to one; the timing loops keep
their own warmup and repetition counts, which differ on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… against relaxed-simd

The backend picks its multiply-add form, its int8 packing and its sigmoid/tanh
kernels at compile time on +relaxed-simd, so the single +simd128 CI leg left
half of it untested; add a second leg. AddRowColProducts and AddMatMul with k=1
compute the same product and so must agree on whether that multiply-add is
fused, which nothing checked — the existing coverage uses operands whose
products are exact, where fused and separate agree trivially. Also record why
wasm_f32_4x4 sits at TargetOptimized and is unreachable through dispatch, and
assert that it stays that way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… macros

Each of the four GEMV kernels spelled the same 24 FusedKerSpec arms out once
per accumulator, so widening a kernel meant copying every arm again. The arms
that apply one operation uniformly across the accumulators now expand from
wasm/fuse.rs, which emits the same statements in the same order; the emitted
kernel bodies are unchanged under both +simd128 and +simd128,+relaxed-simd.
AddMatMul, AddRowColProducts, AddUnicast and Store stay written out: the first
two must agree on whether the multiply-add is fused, and the last two walk the
C tile by stride differently in each kernel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… macros

Same treatment as the GEMV kernels, with the two primitives this layout needs:
here a per-row operand is splatted across accumulators and a per-column one is
loaded as a vector, the opposite of the GEMV kernels, and the 8x8 tile holds
each row as a low/high pair that takes one splat between them. The emitted
kernel bodies are unchanged under both +simd128 and +simd128,+relaxed-simd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WASM_RELAXED_SIMD.md said all six f32 kernels switch to fused multiply-add
under +relaxed-simd; only 32x1 and 8x8 do, and the 1.40-1.55x figure applies
only to those. The int8 kernel switches too, on instruction rather than
rounding, which the document did not mention. The header on wasm_i32_4x4
claimed bit-identity with generic_i32_4x4 without qualification, which holds
only without +relaxed-simd — the relaxed dot takes an i7 second operand.
The module header still named wasm32-wasi, renamed to wasm32-wasip1.

The module gate moves from target_family = "wasm" to target_arch = "wasm32",
matching what the kernels actually require (std::arch::wasm32). This only
narrows wasm64, which cannot compile these kernels anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant