Skip to content

Commit a503291

Browse files
committed
simd(neon): native U32x16 = [U32x4;4] ARX lane + fix aarch64 stable compile
Native NEON U32x16 (the ChaCha20/BLAKE ARX lane), mirroring the wasm `[U32x4; 4]` shape so one ChaCha20 backend source compiles unchanged on every tier: - U32x4 gains `bitxor` (veorq_u32) + `rotate_left` (shift-or via the variable-count vshlq_u32; n%32 early-return). Rotate amount is a public ARX constant. - New U32x16([U32x4;4]) with Add / BitXor / rotate_left / splat / from_array / to_array, fanned over the 4 native lanes. - aarch64_simd F32x16::to_bits/from_bits switch to U32x16::from_array / to_array (native is [U32x4;4], not a [u32;16] tuple). - simd.rs aarch64 arm re-exports the native u32x16/U32x16 from simd_neon instead of the scalar fallback (drops them from the scalar list). Also fixes the pre-existing aarch64 *stable* compile breakage (the target never built on stable — no aarch64 CI caught it), a prerequisite for the cross-parity CI job: - Missing `pub type u16x8 = U16x8;` alias (simd.rs imported a symbol that did not exist). - `dot_i8x16_neon` / `codebook_gather_i8_dotprod` used the nightly-only `vdotq_s32` intrinsic (issue #117224). Rewritten with stable widening NEON (vmull_s8 + vpaddlq_s16 reduce; vmovl_s8 + vaddw_s16 accumulate) — bit-identical result, runs on all aarch64, no `dotprod` feature needed. Also fixes a latent bug in the gather (acc1..3 were never written, v1 aliased v0). Host ARX parity test green; aarch64 lib now `cargo check`-clean on stable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6pT32kk6pnuAAqR3JiYqu
1 parent 729599e commit a503291

3 files changed

Lines changed: 187 additions & 22 deletions

File tree

.claude/blackboard.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,3 +630,27 @@ is superseded by the matryoshka once the fork lands.
630630
4. **Matryoshka execution:** fork `chacha20`, clone `avx2.rs` backend → rewire over
631631
`ndarray::simd::U32x16`, `[patch]` into the encryption stack (transitive accel),
632632
gate vs RustCrypto `soft`, retire `simd_crypto.rs`. Full plan in the doc above.
633+
634+
---
635+
636+
## 2026-07-12 — PR #240 CI fully green (no_std/MSRV fix chain closed)
637+
638+
Tip `5a914c37`. All 20 checks pass; the three previously-red failures are resolved:
639+
640+
- **261f736a**`simd_crypto` dispatcher: `is_x86_feature_detected!` → compile-time
641+
`#[cfg(all(target_arch="x86_64", target_feature="avx512f"))]` (no runtime detection;
642+
the workspace SIMD-dispatch rule). Unblocked `blas-msrv` + `nostd/thumbv6m`.
643+
- **5a914c37** — three no_std/bare-`--no-default-features` test-build fixes:
644+
- `simd_crypto` tests: `vec![[0u8;64]; n]` → fixed arrays `[[0u8;64]; 40]` / `[[0u8;64]; 16]`
645+
(no `vec!` under no_std).
646+
- `tests/chacha20_rustcrypto_parity.rs`: added `#![cfg(feature = "std")]` (imports
647+
std-gated `ndarray::simd`; no-ops under `--no-default-features`).
648+
- `src/tri.rs`: `Array2::<i32>::zeros(...)` (serde_json dep-drift added
649+
`impl PartialEq<Value> for i32`, making bare `Array2::zeros` element-type ambiguous).
650+
651+
Green jobs of note: `tests/{stable,beta,1.95.0}`, `blas-msrv`, `nostd/thumbv6m-none-eabi`,
652+
`clippy/1.95.0`, `format/stable`, `native-backend/stable`, `tier4-avx512-check`,
653+
`wasm-simd/parity-node` (new gate), `hpc-stream-parallel/rayon`, CodeRabbit.
654+
655+
Deferred (task #30, token-limit call): native neon `U32x16=[U32x4;4]` + aarch64 cross
656+
parity CI + the matryoshka chacha20 fork. Plan in `.claude/CHACHA20_MATRYOSHKA_PLAN.md`.

src/simd.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,14 @@ pub use crate::simd_neon::{
383383
// U16x8 on aarch64 comes from simd_neon (backed by uint16x8_t)
384384
#[cfg(all(target_arch = "aarch64", not(feature = "nightly-simd")))]
385385
pub use crate::simd_neon::{u16x8, U16x8};
386+
// U32x16 (native `[U32x4; 4]`) — the ARX lane the ChaCha20 backend rides. Comes
387+
// from simd_neon, not the scalar fallback, so it carries Add/BitXor/rotate_left.
388+
#[cfg(all(target_arch = "aarch64", not(feature = "nightly-simd")))]
389+
pub use crate::simd_neon::{u32x16, U32x16};
386390
#[cfg(all(target_arch = "aarch64", not(feature = "nightly-simd")))]
387391
pub use scalar::{
388-
f32x8, f64x4, i32x16, i32x8, i64x4, i64x8, u16x16, u32x16, u32x8, u64x4, u64x8, u8x64, F32x8, F64x4, I32x16, I32x8,
389-
I64x4, I64x8, U16x16, U16x32, U32x16, U32x8, U64x4, U64x8, U8x64,
392+
f32x8, f64x4, i32x16, i32x8, i64x4, i64x8, u16x16, u32x8, u64x4, u64x8, u8x64, F32x8, F64x4, I32x16, I32x8, I64x4,
393+
I64x8, U16x16, U16x32, U32x8, U64x4, U64x8, U8x64,
390394
};
391395

392396
// wasm32 + simd128: the native v128 float hot path (F32x16 / F64x8 + masks)

src/simd_neon.rs

Lines changed: 157 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
191196
pub 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)]
204214
pub 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

Comments
 (0)