Skip to content

Commit 8aef362

Browse files
authored
Merge pull request #254 from AdaWorldAPI/claude/happy-hamilton-0azlw4
simd: wasm32 SIMD128 (v128) arm for matmul_i8_to_i32 (byte-parity vs scalar)
2 parents a8a1fea + 64c91d4 commit 8aef362

2 files changed

Lines changed: 400 additions & 2 deletions

File tree

src/simd_runtime/matmul.rs

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,178 @@ pub fn matmul_f32(
4646
crate::hpc::amx_matmul::matmul_f32(lhs, rhs, out)
4747
}
4848

49-
/// i8 × i8 → i32 matmul. Runtime-dispatched to AMX TDPBUSD → VPDPBUSD-zmm →
50-
/// VPDPBUSD-ymm → scalar with the sign-shift bias trick.
49+
/// i8 × i8 → i32 matmul. Runtime-dispatched.
50+
///
51+
/// - **x86_64**: delegates unchanged to
52+
/// [`crate::hpc::amx_matmul::matmul_i8_to_i32`] — AMX TDPBUSD →
53+
/// VPDPBUSD-zmm → VPDPBUSD-ymm → scalar, with the sign-shift bias trick
54+
/// documented there.
55+
/// - **wasm32 + simd128**: dispatches to
56+
/// `crate::simd_wasm::wasm32_simd::matmul_i8_to_i32_wasm` — a v128
57+
/// i16-widen + i32-pairwise-reduce dot product. No sign-shift bias
58+
/// trick is needed there: signed×signed multiply is native to that
59+
/// widening path (unlike AMX/VPDPBUSD, which are unsigned×signed).
60+
/// (Plain backtick, not an intra-doc link: that function only exists
61+
/// under wasm32+simd128, narrower than this doc comment's own
62+
/// `not(target_arch = "x86_64")` gate, so a `[...]` link would be
63+
/// "broken" on e.g. aarch64 or plain wasm32 builds.)
64+
/// - **Everywhere else off x86_64** (wasm32 without simd128, other
65+
/// non-x86_64 targets): a portable scalar reference, bit-identical to
66+
/// the x86_64 tier's scalar fallback.
67+
///
68+
/// `crate::hpc::amx_matmul` is `#[cfg(target_arch = "x86_64")]`-gated in
69+
/// its entirety (it carries `std::arch::asm!` AMX tile code that only
70+
/// makes sense there), so its `MatmulError` type is unreachable off
71+
/// x86_64. The portable path below returns a distinct, same-shaped
72+
/// `MatmulError` defined in this module instead — the two never collide
73+
/// because exactly one of the two `matmul_i8_to_i32` definitions here is
74+
/// ever compiled for a given target. (Plain backtick, not a link: that
75+
/// `MatmulError` is itself `#[cfg(not(target_arch = "x86_64"))]`-gated,
76+
/// so it doesn't exist in the x86_64 compilation this very doc comment
77+
/// belongs to — a `[...]` link here would be "broken" on x86_64.)
78+
#[cfg(target_arch = "x86_64")]
5179
#[inline(always)]
5280
pub fn matmul_i8_to_i32(
5381
lhs: ArrayView2<'_, i8>, rhs: ArrayView2<'_, i8>, out: ArrayViewMut2<'_, i32>,
5482
) -> Result<(), crate::hpc::amx_matmul::MatmulError> {
5583
crate::hpc::amx_matmul::matmul_i8_to_i32(lhs, rhs, out)
5684
}
5785

86+
/// Portable (off-x86_64) error type for [`matmul_i8_to_i32`]. Mirrors the
87+
/// shape of `crate::hpc::amx_matmul::MatmulError` (unreachable here — see
88+
/// that function's doc comment) minus the x86_64-only `AmxUnavailable`
89+
/// variant, which has no meaning off x86_64.
90+
#[cfg(not(target_arch = "x86_64"))]
91+
#[derive(Debug, Clone, PartialEq, Eq)]
92+
pub enum MatmulError {
93+
/// Shapes don't satisfy `lhs:(M,K), rhs:(K,N), out:(M,N)`.
94+
ShapeMismatch {
95+
/// Shape of the LHS view, `(rows, cols)`.
96+
lhs: (usize, usize),
97+
/// Shape of the RHS view, `(rows, cols)`.
98+
rhs: (usize, usize),
99+
/// Shape of the output view, `(rows, cols)`.
100+
out: (usize, usize),
101+
},
102+
/// Output tensor is not row-contiguous (column stride != 1).
103+
NonContiguousOutput,
104+
}
105+
106+
#[cfg(not(target_arch = "x86_64"))]
107+
impl std::fmt::Display for MatmulError {
108+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109+
match self {
110+
MatmulError::ShapeMismatch { lhs, rhs, out } => write!(
111+
f,
112+
"shape mismatch: lhs={:?} rhs={:?} out={:?}; expected lhs:(M,K), rhs:(K,N), out:(M,N)",
113+
lhs, rhs, out
114+
),
115+
MatmulError::NonContiguousOutput => f.write_str("output must be row-contiguous (col stride = 1)"),
116+
}
117+
}
118+
}
119+
120+
#[cfg(not(target_arch = "x86_64"))]
121+
impl std::error::Error for MatmulError {}
122+
123+
/// Validate `lhs:(M,K) × rhs:(K,N) → out:(M,N)` and that `out` is
124+
/// row-contiguous. Portable mirror of the private `check_shapes` helper
125+
/// in `hpc::amx_matmul` (that module is x86_64-only).
126+
#[cfg(not(target_arch = "x86_64"))]
127+
fn check_shapes_portable(
128+
lhs: &ArrayView2<'_, i8>, rhs: &ArrayView2<'_, i8>, out: &ArrayViewMut2<'_, i32>,
129+
) -> Result<(usize, usize, usize), MatmulError> {
130+
let (m, k) = lhs.dim();
131+
let (kr, n) = rhs.dim();
132+
let (mo, no) = out.dim();
133+
if k != kr || m != mo || n != no {
134+
return Err(MatmulError::ShapeMismatch {
135+
lhs: (m, k),
136+
rhs: (kr, n),
137+
out: (mo, no),
138+
});
139+
}
140+
// Output must be row-stride-1 (writes are linear per row).
141+
let strides = out.strides();
142+
if strides[1] != 1 {
143+
return Err(MatmulError::NonContiguousOutput);
144+
}
145+
Ok((m, n, k))
146+
}
147+
148+
/// Copy a possibly-strided 2-D `i8` view into a contiguous row-major Vec.
149+
/// Portable mirror of the private `pack_contig` helper in `hpc::amx_matmul`.
150+
#[cfg(not(target_arch = "x86_64"))]
151+
fn pack_contig_i8(view: &ArrayView2<'_, i8>) -> Vec<i8> {
152+
let (rows, cols) = view.dim();
153+
let mut buf = Vec::with_capacity(rows * cols);
154+
for r in 0..rows {
155+
for c in 0..cols {
156+
buf.push(view[[r, c]]);
157+
}
158+
}
159+
buf
160+
}
161+
162+
/// Scalar i8×i8→i32 GEMM reference — bit-identical math to
163+
/// `hpc::amx_matmul::matmul_i8_to_i32`'s tier-4 scalar fallback (plain
164+
/// `i32` accumulation, no sign-shift bias). That fallback is inline
165+
/// inside an x86_64-only function, so this is a portable copy of the
166+
/// same loop rather than a cross-module call.
167+
#[cfg(all(
168+
not(target_arch = "x86_64"),
169+
not(all(target_arch = "wasm32", target_feature = "simd128"))
170+
))]
171+
fn matmul_i8_scalar_reference(a: &[i8], b: &[i8], c: &mut [i32], m: usize, n: usize, k: usize) {
172+
for i in 0..m {
173+
for p in 0..k {
174+
let av = a[i * k + p] as i32;
175+
for j in 0..n {
176+
c[i * n + j] += av * b[p * n + j] as i32;
177+
}
178+
}
179+
}
180+
}
181+
182+
/// i8 × i8 → i32 matmul — portable (off-x86_64) path. wasm32+simd128
183+
/// dispatches to the v128 dot-product kernel in
184+
/// `crate::simd_wasm::wasm32_simd::matmul_i8_to_i32_wasm`; everywhere
185+
/// else (wasm32 without simd128, other non-x86_64 targets) falls through
186+
/// to `matmul_i8_scalar_reference`. Both are plain backtick references,
187+
/// not intra-doc links: each is `#[cfg]`-gated narrower than this
188+
/// function itself, so a `[...]` link would be "broken" whichever one
189+
/// isn't compiled for the current target. See the x86_64 sibling
190+
/// definition's doc comment for the full tier rationale.
191+
#[cfg(not(target_arch = "x86_64"))]
192+
#[inline(always)]
193+
pub fn matmul_i8_to_i32(
194+
lhs: ArrayView2<'_, i8>, rhs: ArrayView2<'_, i8>, mut out: ArrayViewMut2<'_, i32>,
195+
) -> Result<(), MatmulError> {
196+
let (m, n, k) = check_shapes_portable(&lhs, &rhs, &out)?;
197+
let a = pack_contig_i8(&lhs);
198+
let b = pack_contig_i8(&rhs);
199+
let mut c = vec![0i32; m * n];
200+
201+
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
202+
{
203+
crate::simd_wasm::wasm32_simd::matmul_i8_to_i32_wasm(&a, &b, &mut c, m, n, k);
204+
}
205+
#[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
206+
{
207+
matmul_i8_scalar_reference(&a, &b, &mut c, m, n, k);
208+
}
209+
210+
// Write i32 result back into the (possibly strided) output.
211+
let (rows, cols) = out.dim();
212+
debug_assert_eq!(c.len(), rows * cols);
213+
for r in 0..rows {
214+
for col in 0..cols {
215+
out[[r, col]] = c[r * cols + col];
216+
}
217+
}
218+
Ok(())
219+
}
220+
58221
/// `C = A · B` where A is M×K u8, B is K×N i8, C is M×N i32 (overwrite).
59222
///
60223
/// Delegates to [`crate::simd_int_ops::gemm_u8_i8`]. Tier 0 (runtime
@@ -98,4 +261,35 @@ mod tests {
98261
let expected_c0: i32 = (0..k).map(|kk| a[kk] as i32 * b[kk * n] as i32).sum();
99262
assert_eq!(c[0], expected_c0, "c[0] mismatch");
100263
}
264+
265+
/// Portable-path parity test for `matmul_i8_to_i32`: on wasm32+simd128
266+
/// this exercises `crate::simd_wasm::wasm32_simd::matmul_i8_to_i32_wasm`
267+
/// end-to-end through the public `ArrayView2` entry point (the
268+
/// self-contained kernel-level parity test lives in
269+
/// `simd_wasm.rs::wasm32_simd::tests::wasm_matmul_i8_matches_scalar`;
270+
/// this one instead proves the trampoline wiring itself). `K = 20` is
271+
/// deliberately not a multiple of 16, exercising the scalar tail.
272+
#[cfg(not(target_arch = "x86_64"))]
273+
#[test]
274+
fn matmul_i8_to_i32_portable_matches_scalar() {
275+
let m = 5;
276+
let n = 4;
277+
let k = 20;
278+
let a = Array2::<i8>::from_shape_fn((m, k), |(i, j)| (((i * 7 + j * 3) as i32 % 13) - 6) as i8);
279+
let b = Array2::<i8>::from_shape_fn((k, n), |(i, j)| (((i * 5 + j * 11) as i32 % 17) - 8) as i8);
280+
let mut out = Array2::<i32>::zeros((m, n));
281+
matmul_i8_to_i32(a.view(), b.view(), out.view_mut()).expect("portable i8 matmul");
282+
283+
// Scalar reference — identical math to `hpc::amx_matmul`'s tier-4.
284+
let mut expect = Array2::<i32>::zeros((m, n));
285+
for i in 0..m {
286+
for p in 0..k {
287+
let av = a[[i, p]] as i32;
288+
for j in 0..n {
289+
expect[[i, j]] += av * b[[p, j]] as i32;
290+
}
291+
}
292+
}
293+
assert_eq!(out, expect);
294+
}
101295
}

0 commit comments

Comments
 (0)