Skip to content

Commit 6bd296e

Browse files
authored
Merge pull request #373 from AdaWorldAPI/feat/simd-narrow-ladder-20261024
simd: saturating narrows down the 512-bit ladder (16x32 -> 32x16 -> 64x8)
2 parents 628386b + 9dc5862 commit 6bd296e

8 files changed

Lines changed: 494 additions & 269 deletions

File tree

‎crates/simd-masking-parity/src/lib.rs‎

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
//! over every pair of 16 IEEE edge values: NaN, ±0, ±inf, subnormal); `0x73x` / `0x74x` / `0x750` the strided 16-byte register match (all sixteen
3232
//! bytes; strides 16 / 20 / 32 / 512 from first offsets 0..15, care bits in
3333
//! bytes 12..16); `0xFxx` the 16-lane byte vectors `I8x16` / `U8x16` (wrapping
34-
//! `add`/`sub`, signed/unsigned `min`/`max`, the load/store round trips); `0x10xx` the saturating
35-
//! `I32x16 → I8x16 / U8x16` narrows and the `F32x16 ⇄ I32x16` casts on in-range lanes. `main.rs` (native / qemu) and
34+
//! `add`/`sub`, signed/unsigned `min`/`max`, the load/store round trips); `0x10xx`
35+
//! the saturating narrows down the 512-bit ladder (`I32x16 → I16x32 → I8x64 /
36+
//! U8x64`) and the `F32x16` ↔ `I32x16` casts. `main.rs` (native / qemu) and
3637
//! `selfcheck()` (the wasm cdylib export, driven by `run.mjs`) both call
3738
//! [`run`].
3839
@@ -49,7 +50,7 @@ use ndarray::simd::{
4950
ne_i32_to_mask_under, ne_u32_to_mask, ne_u32_to_mask_under, ne_u64_to_mask, ne_u8_to_mask,
5051
ternary_match_strided16_to_mask, ternary_match_strided_to_mask, ternary_match_u32_to_mask,
5152
ternary_match_u32_to_mask_under, ternary_match_u64_to_mask, ternary_match_u64_to_mask_under, ternlog, F32x16,
52-
F64x8, I32x16, I8x16, KeyRunCarry, MortonDir, U32x16, U64x8, U8x16,
53+
F64x8, I16x32, I32x16, I8x16, KeyRunCarry, MortonDir, U32x16, U64x8, U8x16,
5354
};
5455

5556
/// Number of check groups [`run`] executes (for the log line only).
@@ -67,7 +68,7 @@ pub fn run() -> u32 {
6768
check_ternlog_all_tables, check_u64x8_algebra, check_i32x16_compare, check_predicates_to_mask,
6869
check_mask_algebra, check_care_match, check_masked_reductions, check_blend, check_morton_shift,
6970
check_predicates_under, check_set_range, check_unsigned_compare_to_mask, check_gather_scatter_group,
70-
check_f64x8_compare, check_i8x16_u8x16_lanes, check_care_match16, check_narrow_and_casts,
71+
check_f64x8_compare, check_i8x16_u8x16_lanes, check_care_match16, check_narrow_ladder,
7172
];
7273
for g in groups {
7374
if let Err(code) = g() {
@@ -1845,59 +1846,93 @@ fn check_gather_scatter_group() -> Result<(), u32> {
18451846
Ok(())
18461847
}
18471848

1848-
/// `I32x16::narrow_saturate_i8` / `narrow_saturate_u8` against `clamp`, and the
1849-
/// `F32x16::cast_i32` / `I32x16::cast_f32` casts against `as` on in-range lanes.
1850-
fn check_narrow_and_casts() -> Result<(), u32> {
1851-
let edges: [i32; 16] =
1852-
[i32::MIN, -1_000_000, -256, -129, -128, -127, -1, 0, 1, 126, 127, 128, 254, 255, 256, i32::MAX];
1853-
let mut rng = SplitMix64(0x0C0D_EC08_0000_0001);
1854-
let mut vecs: Vec<[i32; 16]> = vec![edges];
1855-
for k in 0..512 {
1856-
vecs.push(core::array::from_fn(|_| {
1857-
let r = rng.next();
1858-
// Half the vectors draw from the whole i32 range, half from the saturation band.
1859-
if k % 2 == 0 {
1860-
r as i32
1849+
/// The saturating narrows down the 512-bit ladder (16×32 → 32×16 → 64×8) and the in-range
1850+
/// f32 ↔ i32 casts, against `clamp` / `as` references.
1851+
fn check_narrow_ladder() -> Result<(), u32> {
1852+
let mut rng = SplitMix64(0x1000_0000_0017);
1853+
let edges32 =
1854+
[i32::MIN, i32::MIN + 1, -32769, -32768, -32767, -256, -129, -128, -127, -1, 0, 1, 127, 128, 255, 256];
1855+
let edges16 = [i16::MIN, -32767, -300, -256, -129, -128, -127, -1, 0, 1, 127, 128, 254, 255, 256, i16::MAX];
1856+
let (mut sat, mut kept) = (0usize, 0usize);
1857+
for round in 0..64u32 {
1858+
// Round 0 is the edge set; later rounds mix random words with values near the seams.
1859+
let a: [i32; 32] = core::array::from_fn(|i| match (round, rng.next()) {
1860+
(0, _) => edges32[i % 16].wrapping_add(if i >= 16 { 1 } else { 0 }),
1861+
(_, r) if r % 3 == 0 => r as i32,
1862+
(_, r) => (r as i32) >> (16 + (r >> 40) % 16),
1863+
});
1864+
let lo = I32x16::from_array(a[..16].try_into().unwrap());
1865+
let hi = I32x16::from_array(a[16..].try_into().unwrap());
1866+
let n16 = lo.narrow_saturating_i16(hi).to_array();
1867+
for i in 0..32 {
1868+
let want = a[i].clamp(i16::MIN as i32, i16::MAX as i32) as i16;
1869+
if n16[i] != want {
1870+
return Err(0x1000 + i as u32);
1871+
}
1872+
if want as i32 == a[i] {
1873+
kept += 1
18611874
} else {
1862-
(r % 601) as i32 - 300
1875+
sat += 1
18631876
}
1864-
}));
1865-
}
1866-
let (mut below, mut above) = (0usize, 0usize);
1867-
for v in &vecs {
1868-
let x = I32x16::from_array(*v);
1869-
let want_i8: [i8; 16] = core::array::from_fn(|i| v[i].clamp(-128, 127) as i8);
1870-
let want_u8: [u8; 16] = core::array::from_fn(|i| v[i].clamp(0, 255) as u8);
1871-
if x.narrow_saturate_i8().to_array() != want_i8 {
1872-
return Err(0x1001);
18731877
}
1874-
if x.narrow_saturate_u8().to_array() != want_u8 {
1875-
return Err(0x1002);
1878+
// Chained to 8 bits: two saturating steps equal one clamp to the narrow range.
1879+
let n16v = I16x32::from_array(n16);
1880+
let back: [i16; 32] = core::array::from_fn(|i| n16[31 - i]);
1881+
let (i8s, u8s) = (
1882+
n16v.narrow_saturating_i8(I16x32::from_array(back))
1883+
.to_array(),
1884+
n16v.narrow_saturating_u8(I16x32::from_array(back))
1885+
.to_array(),
1886+
);
1887+
for i in 0..64 {
1888+
let src = if i < 32 {
1889+
a[i]
1890+
} else {
1891+
a[63 - i].clamp(i16::MIN as i32, i16::MAX as i32)
1892+
};
1893+
if i8s[i] != src.clamp(-128, 127) as i8 {
1894+
return Err(0x1040 + i as u32);
1895+
}
1896+
if u8s[i] != src.clamp(0, 255) as u8 {
1897+
return Err(0x1080 + i as u32);
1898+
}
18761899
}
1877-
below += v.iter().filter(|&&a| a < -128).count();
1878-
above += v.iter().filter(|&&a| a > 255).count();
1879-
let want_f: [u32; 16] = core::array::from_fn(|i| (v[i] as f32).to_bits());
1880-
if x.cast_f32().to_array().map(f32::to_bits) != want_f {
1881-
return Err(0x1003);
1900+
}
1901+
// The i16 edge set directly, both halves.
1902+
let e = I16x32::from_array(core::array::from_fn(|i| edges16[i % 16]));
1903+
let f = I16x32::from_array(core::array::from_fn(|i| edges16[15 - i % 16]));
1904+
let (i8s, u8s) = (e.narrow_saturating_i8(f).to_array(), e.narrow_saturating_u8(f).to_array());
1905+
for i in 0..64 {
1906+
let v = if i < 32 {
1907+
edges16[i % 16]
1908+
} else {
1909+
edges16[15 - (i - 32) % 16]
1910+
};
1911+
if i8s[i] != v.clamp(-128, 127) as i8 || u8s[i] != v.clamp(0, 255) as u8 {
1912+
return Err(0x10C0 + i as u32);
18821913
}
18831914
}
1884-
// Anti-vacuity: the corpus must actually saturate in both directions.
1885-
if below < 1000 || above < 1000 {
1886-
return Err(0x10FF);
1915+
// Anti-vacuity: the rounds must both saturate and pass lanes through.
1916+
if sat < 64 || kept < 64 {
1917+
return Err(0x10F0);
18871918
}
1888-
// In-range f32 → i32 truncation, including negative fractions, ±0 and the ±2^31 edges.
1889-
let fedges: [f32; 16] = [
1890-
-0.0, 0.0, 0.5, -0.5, 0.99999994, -0.99999994, 1.5, -1.5, 127.5, -128.5, 255.9, -2147483520.0, 2147483520.0,
1891-
8388607.5, -8388607.5, 1e9,
1892-
];
1893-
let mut fvecs: Vec<[f32; 16]> = vec![fedges];
1894-
for _ in 0..512 {
1895-
fvecs.push(core::array::from_fn(|_| ((rng.next() >> 11) as f64 / (1u64 << 53) as f64 * 4.0e9 - 2.0e9) as f32));
1896-
}
1897-
for v in &fvecs {
1898-
let want: [i32; 16] = core::array::from_fn(|i| v[i] as i32);
1899-
if F32x16::from_array(*v).cast_i32().to_array() != want {
1900-
return Err(0x1004);
1919+
// Casts, in range only (out-of-range and NaN differ by backend by design).
1920+
for _ in 0..64 {
1921+
let v: [i32; 16] = core::array::from_fn(|_| (rng.next() as i32) >> (rng.next() % 31));
1922+
let f = I32x16::from_array(v).cast_f32().to_array();
1923+
for i in 0..16 {
1924+
if f[i].to_bits() != (v[i] as f32).to_bits() {
1925+
return Err(0x10F1);
1926+
}
1927+
}
1928+
let x: [f32; 16] = core::array::from_fn(|_| {
1929+
f32::from_bits((rng.next() as u32 & 0x807F_FFFF) | (((rng.next() % 31) as u32 + 112) << 23))
1930+
});
1931+
let t = F32x16::from_array(x).cast_i32().to_array();
1932+
for i in 0..16 {
1933+
if t[i] != x[i] as i32 {
1934+
return Err(0x10F2);
1935+
}
19011936
}
19021937
}
19031938
Ok(())

‎src/simd.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,10 @@ pub use crate::simd_neon::{
429429
batch_packed_i4_16, i8x16, i8x32, palette_lookup_u8x8, prefetch_read_t0, prefetch_read_t1, prefetch_read_t2, u8x16,
430430
u8x8, I8x16, I8x32, U8x16, U8x8,
431431
};
432+
// The 64×8 and 32×16 signed rungs of the ladder (64×8, 32×16, 16×32). `simd_neon.rs` has
433+
// carried them since #337 but no arm re-exported them, so aarch64 had neither type.
434+
#[cfg(all(target_arch = "aarch64", not(feature = "nightly-simd")))]
435+
pub use crate::simd_neon::{i16x32, i8x64, I16x32, I8x64};
432436
// U16x8 on aarch64 comes from simd_neon (backed by uint16x8_t)
433437
#[cfg(all(target_arch = "aarch64", not(feature = "nightly-simd")))]
434438
pub use crate::simd_neon::{u16x8, U16x8};

‎src/simd_avx2.rs‎

Lines changed: 84 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,67 @@ impl I8x64 {
13891389
}
13901390

13911391
impl I16x32 {
1392+
/// Narrow two 32×i16 vectors to one 64×i8 vector with signed saturation: lanes 0..32
1393+
/// come from `self`, lanes 32..64 from `hi`, each clamped to `-128..=127`.
1394+
///
1395+
/// ```
1396+
/// use ndarray::simd::I16x32;
1397+
/// let mut a = [0i16; 32];
1398+
/// a[..8].copy_from_slice(&[i16::MIN, -129, -128, -1, 1, 127, 128, i16::MAX]);
1399+
/// let v = I16x32::from_array(a).narrow_saturating_i8(I16x32::splat(-300)).to_array();
1400+
/// assert_eq!(v[..8], [-128, -128, -128, -1, 1, 127, 127, 127]);
1401+
/// assert!(v[32..].iter().all(|&x| x == -128));
1402+
/// ```
1403+
#[inline(always)]
1404+
pub fn narrow_saturating_i8(self, hi: Self) -> I8x64 {
1405+
use core::arch::x86_64::*;
1406+
let mut out = [0i8; 64];
1407+
// SAFETY: SSE2 baseline. The loads read 16-byte quarters of the two 64-byte arrays and
1408+
// the stores write 16-byte quarters of the 64-byte `out`.
1409+
unsafe {
1410+
let o = out.as_mut_ptr() as *mut __m128i;
1411+
for (h, src) in [&self.0, &hi.0].into_iter().enumerate() {
1412+
let p = src.as_ptr() as *const __m128i;
1413+
_mm_storeu_si128(o.add(2 * h), _mm_packs_epi16(_mm_loadu_si128(p), _mm_loadu_si128(p.add(1))));
1414+
_mm_storeu_si128(
1415+
o.add(2 * h + 1),
1416+
_mm_packs_epi16(_mm_loadu_si128(p.add(2)), _mm_loadu_si128(p.add(3))),
1417+
);
1418+
}
1419+
}
1420+
I8x64(out)
1421+
}
1422+
1423+
/// Narrow two 32×i16 vectors to one 64×u8 vector with saturation: lanes 0..32 come
1424+
/// from `self`, lanes 32..64 from `hi`, each clamped to `0..=255`.
1425+
///
1426+
/// ```
1427+
/// use ndarray::simd::I16x32;
1428+
/// let mut a = [0i16; 32];
1429+
/// a[..8].copy_from_slice(&[i16::MIN, -1, 0, 1, 254, 255, 256, i16::MAX]);
1430+
/// let v = I16x32::from_array(a).narrow_saturating_u8(I16x32::splat(300)).to_array();
1431+
/// assert_eq!(v[..8], [0, 0, 0, 1, 254, 255, 255, 255]);
1432+
/// assert!(v[32..].iter().all(|&x| x == 255));
1433+
/// ```
1434+
#[inline(always)]
1435+
pub fn narrow_saturating_u8(self, hi: Self) -> U8x64 {
1436+
use core::arch::x86_64::*;
1437+
let mut out = [0u8; 64];
1438+
// SAFETY: as in `narrow_saturating_i8`; `packuswb` saturates signed i16 to 0..=255.
1439+
unsafe {
1440+
let o = out.as_mut_ptr() as *mut __m128i;
1441+
for (h, src) in [&self.0, &hi.0].into_iter().enumerate() {
1442+
let p = src.as_ptr() as *const __m128i;
1443+
_mm_storeu_si128(o.add(2 * h), _mm_packus_epi16(_mm_loadu_si128(p), _mm_loadu_si128(p.add(1))));
1444+
_mm_storeu_si128(
1445+
o.add(2 * h + 1),
1446+
_mm_packus_epi16(_mm_loadu_si128(p.add(2)), _mm_loadu_si128(p.add(3))),
1447+
);
1448+
}
1449+
}
1450+
U8x64(out)
1451+
}
1452+
13921453
#[inline(always)]
13931454
pub fn zero() -> Self {
13941455
Self([0i16; 32])
@@ -2632,65 +2693,35 @@ impl U16x32 {
26322693
}
26332694

26342695
impl I32x16 {
2635-
/// The two saturating `i32 → i16` packs (`packssdw`) of lanes 0..8 and 8..16.
2636-
#[inline(always)]
2637-
fn packs_i16(self) -> (core::arch::x86_64::__m128i, core::arch::x86_64::__m128i) {
2638-
use core::arch::x86_64::*;
2639-
// SAFETY: SSE2 is part of the x86_64 baseline; the four 16-byte unaligned loads
2640-
// read the 64-byte array in bounds.
2641-
unsafe {
2642-
let p = self.0.as_ptr() as *const __m128i;
2643-
(
2644-
_mm_packs_epi32(_mm_loadu_si128(p), _mm_loadu_si128(p.add(1))),
2645-
_mm_packs_epi32(_mm_loadu_si128(p.add(2)), _mm_loadu_si128(p.add(3))),
2646-
)
2647-
}
2648-
}
2649-
2650-
/// Narrow each lane to `i8` with signed saturation: lanes below -128 become -128 and
2651-
/// lanes above 127 become 127; every other lane keeps its value.
2696+
/// Narrow two 16×i32 vectors to one 32×i16 vector with signed saturation: lanes 0..16
2697+
/// come from `self`, lanes 16..32 from `hi`, each clamped to `i16::MIN..=i16::MAX`.
26522698
///
26532699
/// ```
26542700
/// use ndarray::simd::I32x16;
2655-
/// let v = I32x16::from_array([-1000, -129, -128, -1, 0, 1, 127, 128, 1000, 5, -5, 64, -64, 100, -100, i32::MIN]);
2656-
/// assert_eq!(
2657-
/// v.narrow_saturate_i8().to_array(),
2658-
/// [-128, -128, -128, -1, 0, 1, 127, 127, 127, 5, -5, 64, -64, 100, -100, -128]
2659-
/// );
2701+
/// let lo = I32x16::from_array([i32::MIN, -32769, -32768, -1, 0, 1, 32767, 32768, i32::MAX, 5, -5, 7, -7, 9, -9, 0]);
2702+
/// let hi = I32x16::splat(70_000);
2703+
/// let v = lo.narrow_saturating_i16(hi).to_array();
2704+
/// assert_eq!(v[..9], [-32768, -32768, -32768, -1, 0, 1, 32767, 32767, 32767]);
2705+
/// assert!(v[16..].iter().all(|&x| x == 32767));
26602706
/// ```
26612707
#[inline(always)]
2662-
pub fn narrow_saturate_i8(self) -> crate::simd_avx512::I8x16 {
2708+
pub fn narrow_saturating_i16(self, hi: Self) -> I16x32 {
26632709
use core::arch::x86_64::*;
2664-
let (lo, hi) = self.packs_i16();
2665-
let mut out = [0i8; 16];
2666-
// SAFETY: SSE2 baseline; `packsswb` is register-only and the store writes exactly the
2667-
// 16 bytes of `out`.
2668-
unsafe { _mm_storeu_si128(out.as_mut_ptr() as *mut __m128i, _mm_packs_epi16(lo, hi)) };
2669-
crate::simd_avx512::I8x16(out)
2670-
}
2671-
2672-
/// Narrow each lane to `u8` with saturation: lanes below 0 become 0 and lanes above 255
2673-
/// become 255; every other lane keeps its value.
2674-
///
2675-
/// ```
2676-
/// use ndarray::simd::I32x16;
2677-
/// let v = I32x16::from_array([-1000, -1, 0, 1, 127, 128, 255, 256, 1000, 5, 64, 200, 254, i32::MIN, i32::MAX, 0]);
2678-
/// assert_eq!(
2679-
/// v.narrow_saturate_u8().to_array(),
2680-
/// [0, 0, 0, 1, 127, 128, 255, 255, 255, 5, 64, 200, 254, 0, 255, 0]
2681-
/// );
2682-
/// ```
2683-
#[inline(always)]
2684-
pub fn narrow_saturate_u8(self) -> crate::simd_avx512::U8x16 {
2685-
use core::arch::x86_64::*;
2686-
// Saturating to i16 and then to u8 is the same as clamping to 0..=255, because both
2687-
// steps are monotone and 0..=255 lies inside the i16 range.
2688-
let (lo, hi) = self.packs_i16();
2689-
let mut out = [0u8; 16];
2690-
// SAFETY: SSE2 baseline; `packuswb` is register-only and the store writes exactly the
2691-
// 16 bytes of `out`.
2692-
unsafe { _mm_storeu_si128(out.as_mut_ptr() as *mut __m128i, _mm_packus_epi16(lo, hi)) };
2693-
crate::simd_avx512::U8x16(out)
2710+
let mut out = [0i16; 32];
2711+
// SAFETY: SSE2 is part of the x86_64 baseline. The loads read 16-byte quarters of the
2712+
// two 64-byte arrays (k < 4) and the stores write 16-byte quarters of the 64-byte `out`.
2713+
unsafe {
2714+
let o = out.as_mut_ptr() as *mut __m128i;
2715+
for (h, src) in [&self.0, &hi.0].into_iter().enumerate() {
2716+
let p = src.as_ptr() as *const __m128i;
2717+
_mm_storeu_si128(o.add(2 * h), _mm_packs_epi32(_mm_loadu_si128(p), _mm_loadu_si128(p.add(1))));
2718+
_mm_storeu_si128(
2719+
o.add(2 * h + 1),
2720+
_mm_packs_epi32(_mm_loadu_si128(p.add(2)), _mm_loadu_si128(p.add(3))),
2721+
);
2722+
}
2723+
}
2724+
I16x32(out)
26942725
}
26952726

26962727
/// The two 256-bit halves of the 64-byte-aligned array, loaded once.

0 commit comments

Comments
 (0)