@@ -183,49 +183,62 @@ pub unsafe fn codebook_gather_f32x4_a72(centroids: &[f32], indices: &[u8], dim:
183183// Tier 3: A76 DotProd + FP16 (Pi 5, Orange Pi 5)
184184// ═══════════════════════════════════════════════════════════════════════════
185185
186- /// SDOT: 4×(4×i8 · 4×i8) → 4×i32 in ONE instruction.
187- /// ARMv8.2+ dotprod. 4× throughput vs manual widening multiply.
188- /// Core of int8 quantized codebook inference on Pi 5.
186+ /// int8 dot product of two 16-lane chunks → i32.
187+ ///
188+ /// Stable-Rust widening path: `vmull_s8` (8×(i8·i8)→i16 per half) then
189+ /// `vpaddlq_s16` (pairwise widen i16→i32) and a horizontal `vaddvq_s32`.
190+ /// Bit-identical result to the ARMv8.2 `SDOT` instruction, but compiles on
191+ /// stable (the `vdotq_s32` intrinsic is nightly-only, issue #117224) and runs
192+ /// on **all** aarch64 — no `dotprod` feature required. Max |product| = 16384,
193+ /// pairwise sums stay well inside i32, so no overflow for any i8 input.
189194#[ cfg( target_arch = "aarch64" ) ]
190- #[ target_feature ( enable = "dotprod" ) ]
195+ #[ inline ( always ) ]
191196pub unsafe fn dot_i8x16_neon ( a : & [ i8 ; 16 ] , b : & [ i8 ; 16 ] ) -> i32 {
192197 let va = vld1q_s8 ( a. as_ptr ( ) ) ;
193198 let vb = vld1q_s8 ( b. as_ptr ( ) ) ;
194- let acc = vdupq_n_s32 ( 0 ) ;
195- let result = vdotq_s32 ( acc, va, vb) ;
196- // Horizontal sum of 4×i32
197- vaddvq_s32 ( result)
199+ // 8×i16 products for the low and high halves.
200+ let plo = vmull_s8 ( vget_low_s8 ( va) , vget_low_s8 ( vb) ) ;
201+ let phi = vmull_s8 ( vget_high_s8 ( va) , vget_high_s8 ( vb) ) ;
202+ // Widen i16→i32 (pairwise) so the accumulation never overflows, then reduce.
203+ vaddvq_s32 ( vaddq_s32 ( vpaddlq_s16 ( plo) , vpaddlq_s16 ( phi) ) )
198204}
199205
200- /// Quantized codebook gather via SDOT (Pi 5 only).
201- /// Centroids stored as i8, accumulated as i32. 4× throughput vs f32 path.
206+ /// Quantized codebook gather: element-wise widen-accumulate the selected i8
207+ /// centroids into an i32 output of length `dim` (`dim` a multiple of 16).
208+ ///
209+ /// The i32 counterpart of [`codebook_gather_f32x4_neon`]: for each output lane
210+ /// `k`, `output_i32[k] = Σ_idx centroids_i8[idx*dim + k]`, widened i8→i32 via
211+ /// `vmovl_s8` + `vaddw_s16`. Stable on all aarch64 (no `dotprod` intrinsic).
202212#[ cfg( target_arch = "aarch64" ) ]
203- #[ target_feature ( enable = "dotprod" ) ]
213+ #[ inline ( always ) ]
204214pub unsafe fn codebook_gather_i8_dotprod (
205215 centroids_i8 : & [ i8 ] , // quantized centroids: N × dim (i8)
206216 indices : & [ u8 ] ,
207217 dim : usize , // must be multiple of 16
208218 output_i32 : & mut [ i32 ] , // accumulated i32 (dequantize later)
209219) {
210220 debug_assert ! ( dim % 16 == 0 ) ;
221+ debug_assert ! ( output_i32. len( ) >= dim) ;
211222 let chunks = dim / 16 ;
212223
213224 for c in 0 ..chunks {
225+ // Four i32x4 accumulators cover the 16 i8 lanes of this chunk.
214226 let mut acc0 = vdupq_n_s32 ( 0 ) ;
215227 let mut acc1 = vdupq_n_s32 ( 0 ) ;
216228 let mut acc2 = vdupq_n_s32 ( 0 ) ;
217229 let mut acc3 = vdupq_n_s32 ( 0 ) ;
218230
219231 for & idx in indices {
220232 let base = idx as usize * dim + c * 16 ;
221- let v0 = vld1q_s8 ( centroids_i8[ base..] . as_ptr ( ) ) ;
222- let v1 = vld1q_s8 ( centroids_i8[ base..] . as_ptr ( ) ) ;
223- // dotprod: each vdotq_s32 does 4×(4×i8·4×i8)→4×i32
224- let ones = vdupq_n_s8 ( 1 ) ; // identity for accumulation
225- acc0 = vdotq_s32 ( acc0, v0, ones) ;
233+ let v = vld1q_s8 ( centroids_i8[ base..] . as_ptr ( ) ) ; // 16 i8
234+ let lo = vmovl_s8 ( vget_low_s8 ( v) ) ; // lanes 0..8 → i16
235+ let hi = vmovl_s8 ( vget_high_s8 ( v) ) ; // lanes 8..16 → i16
236+ acc0 = vaddw_s16 ( acc0, vget_low_s16 ( lo) ) ;
237+ acc1 = vaddw_s16 ( acc1, vget_high_s16 ( lo) ) ;
238+ acc2 = vaddw_s16 ( acc2, vget_low_s16 ( hi) ) ;
239+ acc3 = vaddw_s16 ( acc3, vget_high_s16 ( hi) ) ;
226240 }
227241
228- // Store 4 i32 results
229242 vst1q_s32 ( output_i32[ c * 16 ..] . as_mut_ptr ( ) , acc0) ;
230243 vst1q_s32 ( output_i32[ c * 16 + 4 ..] . as_mut_ptr ( ) , acc1) ;
231244 vst1q_s32 ( output_i32[ c * 16 + 8 ..] . as_mut_ptr ( ) , acc2) ;
@@ -467,7 +480,11 @@ pub mod aarch64_simd {
467480
468481 // Integer types come from the scalar fallback in simd.rs — they aren't on
469482 // the perf-critical f32 BLAS-1 / VML path that this module accelerates.
470- pub use crate :: simd:: scalar:: { I32x16 , U32x16 , U64x8 } ;
483+ // `U32x16` is the exception: it carries the ARX vocabulary (Add/BitXor/
484+ // rotate_left) the ChaCha20 lane needs, so it is the native `[U32x4; 4]`
485+ // defined at the top of this file (mirroring `simd_wasm::wasm32_simd`).
486+ pub use super :: U32x16 ;
487+ pub use crate :: simd:: scalar:: { I32x16 , U64x8 } ;
471488
472489 /// 16×f32 backed by 4× NEON `float32x4_t` registers (paired loads).
473490 #[ derive( Copy , Clone ) ]
@@ -674,13 +691,14 @@ pub mod aarch64_simd {
674691 for i in 0 ..16 {
675692 o[ i] = a[ i] . to_bits ( ) ;
676693 }
677- U32x16 ( o)
694+ U32x16 :: from_array ( o)
678695 }
679696 #[ inline( always) ]
680697 pub fn from_bits ( bits : U32x16 ) -> Self {
698+ let b = bits. to_array ( ) ;
681699 let mut o = [ 0.0f32 ; 16 ] ;
682700 for i in 0 ..16 {
683- o[ i] = f32:: from_bits ( bits . 0 [ i] ) ;
701+ o[ i] = f32:: from_bits ( b [ i] ) ;
684702 }
685703 Self :: from_array ( o)
686704 }
@@ -1490,6 +1508,11 @@ impl U16x8 {
14901508 }
14911509}
14921510
1511+ // Lowercase alias (consumer-API parity — `simd.rs` re-exports `u16x8`).
1512+ #[ cfg( target_arch = "aarch64" ) ]
1513+ #[ allow( non_camel_case_types) ]
1514+ pub type u16x8 = U16x8 ;
1515+
14931516#[ cfg( target_arch = "aarch64" ) ]
14941517#[ derive( Copy , Clone ) ]
14951518#[ repr( transparent) ]
@@ -1542,8 +1565,122 @@ impl U32x4 {
15421565 pub fn max ( self , other : Self ) -> Self {
15431566 Self ( unsafe { vmaxq_u32 ( self . 0 , other. 0 ) } )
15441567 }
1568+ /// Lane-wise XOR — the ARX `⊕` (`veorq_u32`).
1569+ #[ inline( always) ]
1570+ pub fn bitxor ( self , other : Self ) -> Self {
1571+ Self ( unsafe { veorq_u32 ( self . 0 , other. 0 ) } )
1572+ }
1573+ /// Lane-wise left-rotate by `n` bits — the ARX rotate (matches
1574+ /// `u32::rotate_left`). NEON has no rotate op, so this is the shift-or via
1575+ /// the variable-count `vshlq_u32` (a signed per-lane count: `+n` shifts
1576+ /// left by `n`, `n-32 < 0` shifts logical-right by `32-n`). The `n % 32 == 0`
1577+ /// early-return avoids the ambiguous full-width shift. Rotate amount is a
1578+ /// public ARX constant.
1579+ #[ inline( always) ]
1580+ pub fn rotate_left ( self , n : u32 ) -> Self {
1581+ let n = n % 32 ;
1582+ if n == 0 {
1583+ return self ;
1584+ }
1585+ unsafe {
1586+ let l = vshlq_u32 ( self . 0 , vdupq_n_s32 ( n as i32 ) ) ;
1587+ let r = vshlq_u32 ( self . 0 , vdupq_n_s32 ( n as i32 - 32 ) ) ;
1588+ Self ( vorrq_u32 ( l, r) )
1589+ }
1590+ }
15451591}
15461592
1593+ /// 16×u32 as `[U32x4; 4]` — the NEON-native 16-wide ARX lane (ChaCha20 /
1594+ /// BLAKE). `U32x4` is the dispatched native unit (`uint32x4_t`); `U32x16` fans
1595+ /// each op over the 4 sub-lanes. Consumer API (`Add` / `BitXor` / `rotate_left`)
1596+ /// matches `simd_avx512::U32x16` exactly, so the ChaCha20 backend compiles
1597+ /// unchanged on every tier (the wasm arm uses the identical `[U32x4; 4]` shape).
1598+ #[ cfg( target_arch = "aarch64" ) ]
1599+ #[ derive( Copy , Clone ) ]
1600+ #[ repr( align( 64 ) ) ]
1601+ pub struct U32x16 ( pub [ U32x4 ; 4 ] ) ;
1602+
1603+ #[ cfg( target_arch = "aarch64" ) ]
1604+ impl U32x16 {
1605+ pub const LANES : usize = 16 ;
1606+
1607+ #[ inline( always) ]
1608+ pub fn splat ( v : u32 ) -> Self {
1609+ Self ( [ U32x4 :: splat ( v) ; 4 ] )
1610+ }
1611+
1612+ #[ inline( always) ]
1613+ pub fn from_array ( a : [ u32 ; 16 ] ) -> Self {
1614+ Self ( [
1615+ U32x4 :: from_array ( [ a[ 0 ] , a[ 1 ] , a[ 2 ] , a[ 3 ] ] ) ,
1616+ U32x4 :: from_array ( [ a[ 4 ] , a[ 5 ] , a[ 6 ] , a[ 7 ] ] ) ,
1617+ U32x4 :: from_array ( [ a[ 8 ] , a[ 9 ] , a[ 10 ] , a[ 11 ] ] ) ,
1618+ U32x4 :: from_array ( [ a[ 12 ] , a[ 13 ] , a[ 14 ] , a[ 15 ] ] ) ,
1619+ ] )
1620+ }
1621+
1622+ #[ inline( always) ]
1623+ pub fn to_array ( self ) -> [ u32 ; 16 ] {
1624+ let mut o = [ 0u32 ; 16 ] ;
1625+ for i in 0 ..4 {
1626+ o[ i * 4 ..i * 4 + 4 ] . copy_from_slice ( & self . 0 [ i] . to_array ( ) ) ;
1627+ }
1628+ o
1629+ }
1630+
1631+ /// Lane-wise left-rotate by `n` bits (ARX rotate), fanned over 4 lanes.
1632+ #[ inline( always) ]
1633+ pub fn rotate_left ( self , n : u32 ) -> Self {
1634+ Self ( [
1635+ self . 0 [ 0 ] . rotate_left ( n) ,
1636+ self . 0 [ 1 ] . rotate_left ( n) ,
1637+ self . 0 [ 2 ] . rotate_left ( n) ,
1638+ self . 0 [ 3 ] . rotate_left ( n) ,
1639+ ] )
1640+ }
1641+ }
1642+
1643+ #[ cfg( target_arch = "aarch64" ) ]
1644+ impl core:: ops:: Add for U32x16 {
1645+ type Output = Self ;
1646+ #[ inline( always) ]
1647+ fn add ( self , r : Self ) -> Self {
1648+ Self ( [ self . 0 [ 0 ] . add ( r. 0 [ 0 ] ) , self . 0 [ 1 ] . add ( r. 0 [ 1 ] ) , self . 0 [ 2 ] . add ( r. 0 [ 2 ] ) , self . 0 [ 3 ] . add ( r. 0 [ 3 ] ) ] )
1649+ }
1650+ }
1651+
1652+ #[ cfg( target_arch = "aarch64" ) ]
1653+ impl core:: ops:: BitXor for U32x16 {
1654+ type Output = Self ;
1655+ #[ inline( always) ]
1656+ fn bitxor ( self , r : Self ) -> Self {
1657+ Self ( [
1658+ self . 0 [ 0 ] . bitxor ( r. 0 [ 0 ] ) ,
1659+ self . 0 [ 1 ] . bitxor ( r. 0 [ 1 ] ) ,
1660+ self . 0 [ 2 ] . bitxor ( r. 0 [ 2 ] ) ,
1661+ self . 0 [ 3 ] . bitxor ( r. 0 [ 3 ] ) ,
1662+ ] )
1663+ }
1664+ }
1665+
1666+ #[ cfg( target_arch = "aarch64" ) ]
1667+ impl core:: fmt:: Debug for U32x16 {
1668+ fn fmt ( & self , f : & mut core:: fmt:: Formatter < ' _ > ) -> core:: fmt:: Result {
1669+ write ! ( f, "U32x16({:?})" , self . to_array( ) )
1670+ }
1671+ }
1672+
1673+ #[ cfg( target_arch = "aarch64" ) ]
1674+ impl PartialEq for U32x16 {
1675+ fn eq ( & self , other : & Self ) -> bool {
1676+ self . to_array ( ) == other. to_array ( )
1677+ }
1678+ }
1679+
1680+ #[ cfg( target_arch = "aarch64" ) ]
1681+ #[ allow( non_camel_case_types) ]
1682+ pub type u32x16 = U32x16 ;
1683+
15471684#[ cfg( target_arch = "aarch64" ) ]
15481685#[ derive( Copy , Clone ) ]
15491686#[ repr( transparent) ]
0 commit comments