Skip to content

Commit 84addbd

Browse files
authored
Merge pull request #375 from AdaWorldAPI/claude/requant-fixedpoint
simd: TFLite/gemmlowp fixed-point requantization, bit-exact on every backend
2 parents a78de7b + 22c54b5 commit 84addbd

14 files changed

Lines changed: 3663 additions & 3 deletions

File tree

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

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
//! the saturating narrows down the 512-bit ladder (`I32x16 → I16x32 → I8x64 /
3636
//! U8x64`) and the `F32x16` ↔ `I32x16` casts; `0x11xx` `F32x16` / `F64x8`
3737
//! `mul_add` rounding once, bit for bit against an exact integer reference (no
38-
//! `mul_add`, so no libm in the binary). `main.rs` (native / qemu) and
38+
//! `mul_add`, so no libm in the binary); `0x12xx` the `I32x16` gemmlowp requantization
39+
//! methods against an independent `i128` reference. `main.rs` (native / qemu) and
3940
//! `selfcheck()` (the wasm cdylib export, driven by `run.mjs`) both call
4041
//! [`run`].
4142
@@ -56,7 +57,7 @@ use ndarray::simd::{
5657
};
5758

5859
/// Number of check groups [`run`] executes (for the log line only).
59-
pub const CHECKS: usize = 18;
60+
pub const CHECKS: usize = 19;
6061

6162
/// The wasm export: identical to [`run`], `extern "C"` so `run.mjs` can call it.
6263
#[no_mangle]
@@ -71,7 +72,7 @@ pub fn run() -> u32 {
7172
check_mask_algebra, check_care_match, check_masked_reductions, check_blend, check_morton_shift,
7273
check_predicates_under, check_set_range, check_unsigned_compare_to_mask, check_gather_scatter_group,
7374
check_f64x8_compare, check_i8x16_u8x16_lanes, check_care_match16, check_narrow_ladder,
74-
check_mul_add_single_rounding,
75+
check_mul_add_single_rounding, check_fixedpoint_requant,
7576
];
7677
for g in groups {
7778
if let Err(code) = g() {
@@ -2077,6 +2078,161 @@ fn check_mul_add_single_rounding() -> Result<(), u32> {
20772078
Ok(())
20782079
}
20792080

2081+
/// `I32x16::saturating_rounding_doubling_high_mul`, `rounding_divide_by_pot` and
2082+
/// `multiply_by_quantized_multiplier` against an independent `i128` reference
2083+
/// written from the definitions (no call into the scalar functions of
2084+
/// `simd_fixedpoint`): srdhm is `ab / 2^31` rounded half toward +inf with the
2085+
/// `MIN * MIN` saturation, rdpot is `x / 2^e` rounded half away from zero, and
2086+
/// the multiplier is their composition. Anti-vacuity: the rounding must differ
2087+
/// from truncation on some lanes, the saturating case must be hit, and exact ties
2088+
/// of both signs must occur.
2089+
fn check_fixedpoint_requant() -> Result<(), u32> {
2090+
fn o_srdhm(a: i32, b: i32) -> i32 {
2091+
let p = a as i128 * b as i128;
2092+
let q = (p + (1i128 << 30)).div_euclid(1i128 << 31);
2093+
q.clamp(i32::MIN as i128, i32::MAX as i128) as i32
2094+
}
2095+
fn o_rdpot(x: i32, e: u32) -> i32 {
2096+
let d = 1i128 << e;
2097+
let m = (x as i128).abs();
2098+
let r = (2 * m + d).div_euclid(2 * d);
2099+
(if x < 0 { -r } else { r }) as i32
2100+
}
2101+
fn o_mbqm(x: i32, m: i32, s: i32) -> i32 {
2102+
let left = s.max(0) as u32;
2103+
let right = (-s).max(0) as u32;
2104+
let xs = ((x as i128) << left) as i32; // wraps like the realization's contract
2105+
o_rdpot(o_srdhm(xs, m), right)
2106+
}
2107+
2108+
let mut rng = SplitMix64(0x1200_0000_0019);
2109+
const EDGE: [i32; 12] =
2110+
[i32::MIN, i32::MIN + 1, -1, 0, 1, i32::MAX - 1, i32::MAX, 1 << 30, -(1 << 30), (1 << 30) + 1, -(1 << 30) - 1, 1 << 15];
2111+
const EXPS: [u32; 4] = [0, 1, 30, 31];
2112+
let (mut trunc_differs_srdhm, mut trunc_differs_rdpot) = (0usize, 0usize);
2113+
let (mut min_min, mut tie_pos, mut tie_neg) = (0usize, 0usize, 0usize);
2114+
2115+
for round in 0..512u32 {
2116+
// ---- M1: srdhm ----
2117+
let mut a = [0i32; 16];
2118+
let mut b = [0i32; 16];
2119+
for i in 0..16 {
2120+
match (round as usize + i) % 4 {
2121+
0 => {
2122+
a[i] = rng.next() as i32;
2123+
b[i] = rng.next() as i32;
2124+
}
2125+
1 => {
2126+
a[i] = EDGE[(rng.next() % 12) as usize];
2127+
b[i] = EDGE[(rng.next() % 12) as usize];
2128+
}
2129+
2 => {
2130+
// exact tie: odd * odd * 2^30
2131+
let f = |r: &mut SplitMix64| {
2132+
let m = ((r.next() % 0x7FFF) as i32 | 1) << 15;
2133+
if r.next() & 1 == 0 { m } else { -m }
2134+
};
2135+
a[i] = f(&mut rng);
2136+
b[i] = f(&mut rng);
2137+
}
2138+
_ => {
2139+
a[i] = if i % 2 == 0 { i32::MIN } else { rng.next() as i32 };
2140+
b[i] = if i % 4 == 0 { i32::MIN } else { rng.next() as i32 };
2141+
}
2142+
}
2143+
}
2144+
let got = I32x16::from_array(a).saturating_rounding_doubling_high_mul(I32x16::from_array(b)).to_array();
2145+
for i in 0..16 {
2146+
let want = o_srdhm(a[i], b[i]);
2147+
if got[i] != want {
2148+
return Err(0x1200 + i as u32);
2149+
}
2150+
let p = a[i] as i128 * b[i] as i128;
2151+
if want as i128 != p / (1i128 << 31) {
2152+
trunc_differs_srdhm += 1;
2153+
}
2154+
if a[i] == i32::MIN && b[i] == i32::MIN {
2155+
min_min += 1;
2156+
}
2157+
if p.rem_euclid(1i128 << 31) == 1i128 << 30 {
2158+
if p >= 0 { tie_pos += 1 } else { tie_neg += 1 }
2159+
}
2160+
}
2161+
2162+
// ---- M2: rounding_divide_by_pot ----
2163+
let mut x = [0i32; 16];
2164+
let mut e = [0i32; 16];
2165+
for i in 0..16 {
2166+
let ex = match (round as usize + i) % 4 {
2167+
0 => (rng.next() % 32) as u32,
2168+
1 => EXPS[(rng.next() % 4) as usize],
2169+
2 => EXPS[i % 4],
2170+
_ => 1 + (rng.next() % 31) as u32,
2171+
};
2172+
let r = rng.next() as i32;
2173+
x[i] = match (round as usize + i) % 3 {
2174+
0 => r,
2175+
1 if ex >= 1 => ((r >> ex) << ex) | (1i32 << (ex - 1)), // exact tie
2176+
1 => r,
2177+
_ => EDGE[(rng.next() % 12) as usize],
2178+
};
2179+
e[i] = ex as i32;
2180+
}
2181+
let got = I32x16::from_array(x).rounding_divide_by_pot(I32x16::from_array(e)).to_array();
2182+
for i in 0..16 {
2183+
let want = o_rdpot(x[i], e[i] as u32);
2184+
if got[i] != want {
2185+
return Err(0x1210 + i as u32);
2186+
}
2187+
if want as i128 != (x[i] as i128) / (1i128 << e[i]) {
2188+
trunc_differs_rdpot += 1;
2189+
}
2190+
}
2191+
2192+
// ---- M3: multiply_by_quantized_multiplier ----
2193+
let mut mx = [0i32; 16];
2194+
let mut mm = [0i32; 16];
2195+
let mut ms = [0i32; 16];
2196+
for i in 0..16 {
2197+
let s = match (round as usize + i) % 4 {
2198+
0 => (rng.next() % 62) as i32 - 31,
2199+
1 => -31 + (rng.next() % 4) as i32,
2200+
2 => 27 + (rng.next() % 4) as i32,
2201+
_ => (rng.next() % 5) as i32 - 2,
2202+
};
2203+
ms[i] = s;
2204+
mm[i] = match (round as usize + i) % 3 {
2205+
0 => (rng.next() >> 33) as i32 | (1 << 30), // normalised, as quantize_multiplier yields
2206+
1 => (rng.next() >> 33) as i32,
2207+
_ => [0, 1, 1 << 30, i32::MAX][(rng.next() % 4) as usize],
2208+
};
2209+
// keep x << max(s, 0) inside i32 (the documented contract)
2210+
mx[i] = (rng.next() as i32) >> (s.max(0) + 1);
2211+
}
2212+
let got = I32x16::from_array(mx)
2213+
.multiply_by_quantized_multiplier(I32x16::from_array(mm), I32x16::from_array(ms))
2214+
.to_array();
2215+
for i in 0..16 {
2216+
if got[i] != o_mbqm(mx[i], mm[i], ms[i]) {
2217+
return Err(0x1220 + i as u32);
2218+
}
2219+
}
2220+
}
2221+
if trunc_differs_srdhm < 256 {
2222+
return Err(0x12F0);
2223+
}
2224+
if trunc_differs_rdpot < 256 {
2225+
return Err(0x12F1);
2226+
}
2227+
if min_min == 0 {
2228+
return Err(0x12F2);
2229+
}
2230+
if tie_pos == 0 || tie_neg == 0 {
2231+
return Err(0x12F3);
2232+
}
2233+
Ok(())
2234+
}
2235+
20802236
/// Exact `a * b + c`, rounded once to nearest-even: the reference for the
20812237
/// `0x11xx` group. It holds the exact sum in a 4608-bit integer (bit 0 is
20822238
/// `2^ORACLE_BASE`, below every product of two subnormals), so it shares no

‎src/lib.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,10 @@ pub mod simd_half;
490490
#[cfg(feature = "std")]
491491
pub(crate) mod simd_fma;
492492

493+
/// TFLite / gemmlowp fixed-point requantization (scalar reference + batch epilogue).
494+
#[cfg(feature = "std")]
495+
pub(crate) mod simd_fixedpoint;
496+
493497
/// Runtime SIMD dispatch — release-binary distribution path.
494498
///
495499
/// Gated under `--features runtime-dispatch`. Mutually exclusive with

‎src/simd.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,17 @@ pub use crate::simd_ops::{
997997
scale_f32_inplace, sub_f32, sub_f32_inplace,
998998
};
999999

1000+
// Fixed-point requantization, bit-exact to TFLite-micro / gemmlowp (double rounding,
1001+
// TFLM's default build). The `I32x16` methods `saturating_rounding_doubling_high_mul`,
1002+
// `rounding_divide_by_pot` and `multiply_by_quantized_multiplier` are the vector form.
1003+
pub use crate::simd_fixedpoint::{
1004+
multiply_by_quantized_multiplier_i32, multiply_by_quantized_multiplier_i64,
1005+
multiply_by_quantized_multiplier_single_rounding_i32, requantize_i32_to_i8, rounding_divide_by_pot_i32, srdhm_i16,
1006+
srdhm_i32,
1007+
};
1008+
#[cfg(feature = "std")]
1009+
pub use crate::simd_fixedpoint::{quantize_multiplier, quantize_multiplier_single_rounding};
1010+
10001011
// ChaCha20 keystream is NO LONGER an `ndarray::simd` surface: the AdaWorldAPI
10011012
// `chacha20` fork (`vendor/chacha20/`) carries the accelerated backend, riding
10021013
// the `U32x16` ARX lane above, and is `[patch]`ed transitively under the

‎src/simd_avx.rs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,32 @@ impl Shr<Self> for U64x8 {
271271
// ---------------------------------------------------------------------------
272272

273273
impl I32x16 {
274+
/// `SaturatingRoundingDoublingHighMul` per lane (TFLM `fixedpoint.h`).
275+
/// AVX without AVX2 has no 256-bit integer multiply, so this arm goes
276+
/// lane by lane through [`crate::simd_fixedpoint::srdhm_i32`].
277+
#[inline]
278+
pub fn saturating_rounding_doubling_high_mul(self, b: Self) -> Self {
279+
let (a, b) = (self.to_array(), b.to_array());
280+
Self::from_array(core::array::from_fn(|i| crate::simd_fixedpoint::srdhm_i32(a[i], b[i])))
281+
}
282+
283+
/// `RoundingDivideByPOT` per lane, per-lane exponent in `0..=31`.
284+
#[inline]
285+
pub fn rounding_divide_by_pot(self, exponent: Self) -> Self {
286+
let (x, e) = (self.to_array(), exponent.to_array());
287+
Self::from_array(core::array::from_fn(|i| crate::simd_fixedpoint::rounding_divide_by_pot_i32(x[i], e[i])))
288+
}
289+
290+
/// `MultiplyByQuantizedMultiplier` (double rounding) per lane, with
291+
/// per-lane multiplier and shift.
292+
#[inline]
293+
pub fn multiply_by_quantized_multiplier(self, multiplier: Self, shift: Self) -> Self {
294+
let (x, m, s) = (self.to_array(), multiplier.to_array(), shift.to_array());
295+
Self::from_array(core::array::from_fn(|i| {
296+
crate::simd_fixedpoint::multiply_by_quantized_multiplier_i32(x[i], m[i], s[i])
297+
}))
298+
}
299+
274300
/// Four 128-bit quarters of the 16 lanes.
275301
#[inline(always)]
276302
fn quarters(self) -> [__m128i; 4] {

‎src/simd_avx2.rs‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2682,6 +2682,140 @@ impl U16x32 {
26822682
}
26832683
}
26842684

2685+
#[cfg(not(all(target_feature = "avx", not(target_feature = "avx2"))))]
2686+
impl I32x16 {
2687+
/// Store two 256-bit halves back into a fresh array (see `avx2_halves`).
2688+
#[inline(always)]
2689+
fn from_avx2_halves(lo: __m256i, hi: __m256i) -> Self {
2690+
let mut o = [0i32; 16];
2691+
// SAFETY: two 32-byte `storeu` stores into the 64-byte local array; no alignment needed.
2692+
unsafe {
2693+
let p = o.as_mut_ptr() as *mut __m256i;
2694+
_mm256_storeu_si256(p, lo);
2695+
_mm256_storeu_si256(p.add(1), hi);
2696+
}
2697+
Self(o)
2698+
}
2699+
2700+
/// `SaturatingRoundingDoublingHighMul` on one 256-bit half (exactness argument: see
2701+
/// [`Self::saturating_rounding_doubling_high_mul`]).
2702+
#[inline(always)]
2703+
fn srdhm_half(a: __m256i, b: __m256i) -> __m256i {
2704+
// SAFETY: AVX2 is a run-time precondition of this arm (see `avx2_halves`); register-only.
2705+
unsafe {
2706+
let nudge = _mm256_set1_epi64x(1 << 30);
2707+
let even = _mm256_srli_epi64(_mm256_add_epi64(_mm256_mul_epi32(a, b), nudge), 31);
2708+
let odd = _mm256_slli_epi64(
2709+
_mm256_add_epi64(_mm256_mul_epi32(_mm256_srli_epi64(a, 32), _mm256_srli_epi64(b, 32)), nudge),
2710+
1,
2711+
);
2712+
let r = _mm256_blend_epi32::<0xAA>(even, odd);
2713+
let min = _mm256_set1_epi32(i32::MIN);
2714+
let ovf = _mm256_and_si256(_mm256_cmpeq_epi32(a, min), _mm256_cmpeq_epi32(b, min));
2715+
_mm256_blendv_epi8(r, _mm256_set1_epi32(i32::MAX), ovf)
2716+
}
2717+
}
2718+
2719+
/// `RoundingDivideByPOT` on one 256-bit half, per-lane exponent.
2720+
#[inline(always)]
2721+
fn rdpot_half(x: __m256i, e: __m256i) -> __m256i {
2722+
// SAFETY: AVX2 run-time precondition (see `avx2_halves`); register-only. `vpsllvd` /
2723+
// `vpsravd` define all counts, so an out-of-contract exponent is unspecified, not UB.
2724+
unsafe {
2725+
let one = _mm256_set1_epi32(1);
2726+
let mask = _mm256_sub_epi32(_mm256_sllv_epi32(one, e), one);
2727+
let rem = _mm256_and_si256(x, mask);
2728+
let thr = _mm256_add_epi32(_mm256_srli_epi32(mask, 1), _mm256_srli_epi32(x, 31));
2729+
let q = _mm256_srav_epi32(x, e);
2730+
// `cmpgt` lanes are -1 where rem > thr, so subtracting adds 1 there.
2731+
_mm256_sub_epi32(q, _mm256_cmpgt_epi32(rem, thr))
2732+
}
2733+
}
2734+
2735+
/// Saturating rounding doubling high multiply (gemmlowp / TFLM
2736+
/// `SaturatingRoundingDoublingHighMul<i32>`): per lane, `round(a * b / 2^31)`
2737+
/// with the half-way case rounded toward `+inf`, i.e. the C++ definition
2738+
/// `((a*b + nudge) / 2^31)` with `nudge = 1<<30` (product `>= 0`) or `1 - (1<<30)`
2739+
/// (product `< 0`) and **truncating** division. `i32::MIN * i32::MIN` saturates to
2740+
/// `i32::MAX`.
2741+
///
2742+
/// ```
2743+
/// use ndarray::simd::I32x16;
2744+
/// let a = I32x16::from_array([3, -3, -1, 1, 0, i32::MIN, i32::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
2745+
/// let r = a.saturating_rounding_doubling_high_mul(I32x16::splat(1 << 30)).to_array();
2746+
/// // 1.5 -> 2, -1.5 -> -1 (tie toward +inf), -0.5 -> 0, 0.5 -> 1.
2747+
/// assert_eq!(r[..7], [2, -1, 0, 1, 0, -1_073_741_824, 1_073_741_824]);
2748+
/// let m = I32x16::splat(i32::MIN).saturating_rounding_doubling_high_mul(I32x16::splat(i32::MIN));
2749+
/// assert!(m.to_array().iter().all(|&x| x == i32::MAX));
2750+
/// ```
2751+
#[inline(always)]
2752+
pub fn saturating_rounding_doubling_high_mul(self, b: Self) -> Self {
2753+
// Exact for both product signs: C++ truncating division after the sign-dependent nudge
2754+
// equals the flooring shift of `a*b + 2^30` (taking bits 31..=62 as the i32), computed
2755+
// with `vpmuldq` on even lanes and on odd lanes after a 32-bit shift down; `MIN*MIN`
2756+
// (2^31, not representable) is blended to `MAX`.
2757+
let (alo, ahi) = self.avx2_halves();
2758+
let (blo, bhi) = b.avx2_halves();
2759+
Self::from_avx2_halves(Self::srdhm_half(alo, blo), Self::srdhm_half(ahi, bhi))
2760+
}
2761+
2762+
/// Rounding arithmetic right shift (gemmlowp / TFLM `RoundingDivideByPOT`) with a
2763+
/// per-lane exponent: `x / 2^e` rounded to nearest, **ties away from zero**.
2764+
/// `exponent` lanes must be in `0..=31`; other values are a contract violation
2765+
/// (the result is unspecified, but no undefined behaviour).
2766+
///
2767+
/// ```
2768+
/// use ndarray::simd::I32x16;
2769+
/// let x = I32x16::from_array([5, -5, 3, -3, 2, -2, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0]);
2770+
/// let r = x.rounding_divide_by_pot(I32x16::splat(1)).to_array();
2771+
/// assert_eq!(r[..8], [3, -3, 2, -2, 1, -1, 1, -1]); // negative ties go away from zero
2772+
/// let e = I32x16::from_array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31]);
2773+
/// let r = I32x16::splat(-10).rounding_divide_by_pot(e).to_array();
2774+
/// assert_eq!(r[..6], [-10, -5, -3, -1, -1, 0]);
2775+
/// assert_eq!(r[15], 0);
2776+
/// let m = I32x16::splat(i32::MIN).rounding_divide_by_pot(I32x16::splat(31)).to_array();
2777+
/// assert_eq!(m[0], -1);
2778+
/// ```
2779+
#[inline(always)]
2780+
pub fn rounding_divide_by_pot(self, exponent: Self) -> Self {
2781+
let (xlo, xhi) = self.avx2_halves();
2782+
let (elo, ehi) = exponent.avx2_halves();
2783+
Self::from_avx2_halves(Self::rdpot_half(xlo, elo), Self::rdpot_half(xhi, ehi))
2784+
}
2785+
2786+
/// Per-lane fixed-point requantization (gemmlowp / TFLM `MultiplyByQuantizedMultiplier`,
2787+
/// double rounding): with `left = max(shift, 0)`, `right = max(-shift, 0)`,
2788+
/// `rounding_divide_by_pot(srdhm(x << left, multiplier), right)`.
2789+
///
2790+
/// Contract: `multiplier >= 0`, `-31 <= shift <= 30`, and `x << left` must not
2791+
/// overflow (TFLM asserts; this wraps).
2792+
///
2793+
/// ```
2794+
/// use ndarray::simd::I32x16;
2795+
/// let x = I32x16::from_array([100, -100, 7, -7, 7, -7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
2796+
/// let s = I32x16::from_array([0, 0, 0, 0, -1, -1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
2797+
/// let r = x.multiply_by_quantized_multiplier(I32x16::splat(1 << 30), s).to_array();
2798+
/// assert_eq!(r[..7], [50, -50, 4, -3, 2, -2, 6]);
2799+
/// ```
2800+
#[inline(always)]
2801+
pub fn multiply_by_quantized_multiplier(self, multiplier: Self, shift: Self) -> Self {
2802+
let (xlo, xhi) = self.avx2_halves();
2803+
let (mlo, mhi) = multiplier.avx2_halves();
2804+
let (slo, shi) = shift.avx2_halves();
2805+
// SAFETY: AVX2 run-time precondition (see `avx2_halves`); register-only (`vpsllvd` wraps,
2806+
// like the scalar `wrapping_mul(1 << left)`).
2807+
unsafe {
2808+
let zero = _mm256_setzero_si256();
2809+
let part = |x: __m256i, m: __m256i, s: __m256i| {
2810+
let left = _mm256_max_epi32(s, zero);
2811+
let right = _mm256_max_epi32(_mm256_sub_epi32(zero, s), zero);
2812+
Self::rdpot_half(Self::srdhm_half(_mm256_sllv_epi32(x, left), m), right)
2813+
};
2814+
Self::from_avx2_halves(part(xlo, mlo, slo), part(xhi, mhi, shi))
2815+
}
2816+
}
2817+
}
2818+
26852819
impl I32x16 {
26862820
/// Narrow two 16×i32 vectors to one 32×i16 vector with signed saturation: lanes 0..16
26872821
/// come from `self`, lanes 16..32 from `hi`, each clamped to `i16::MIN..=i16::MAX`.

0 commit comments

Comments
 (0)