@@ -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) ]
5280pub 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