const MAGIC: f32 = 12_582_912.0;
#[inline(always)]
#[must_use]
pub fn round_ties_even_fast(x: f32) -> f32 {
(x + MAGIC) - MAGIC
}
#[inline(always)]
fn exp2_unchecked(x: f32) -> f32 {
let n = round_ties_even_fast(x);
let f = x - n; const L1: f32 = std::f32::consts::LN_2;
const L2: f32 = L1 * L1 / 2.0;
const L3: f32 = L1 * L1 * L1 / 6.0;
const L4: f32 = L1 * L1 * L1 * L1 / 24.0;
const L5: f32 = L1 * L1 * L1 * L1 * L1 / 120.0;
let p = 1.0 + f * (L1 + f * (L2 + f * (L3 + f * (L4 + f * L5))));
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let scale = f32::from_bits((((n as i32) + 127) as u32) << 23);
p * scale
}
#[inline(always)]
#[must_use]
pub fn exp(x: f32) -> f32 {
exp2_unchecked((x * std::f32::consts::LOG2_E).clamp(-125.0, 125.0))
}
#[inline(always)]
#[must_use]
pub fn exp2(x: f32) -> f32 {
exp2_unchecked(x.clamp(-125.0, 125.0))
}
#[inline(always)]
#[must_use]
pub fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + exp(-x))
}
#[inline(always)]
#[must_use]
pub fn tanh(x: f32) -> f32 {
if x.abs() < 0.02 {
return x * (1.0 - x * x * (1.0 / 3.0));
}
1.0 - 2.0 / (exp(2.0 * x) + 1.0)
}
#[inline(always)]
#[must_use]
pub fn gelu_tanh(x: f32) -> f32 {
const SQRT_2_OVER_PI: f32 = 0.797_884_56;
let z = SQRT_2_OVER_PI * x * (1.0 + 0.044_715 * x * x);
x / (1.0 + exp(-2.0 * z))
}
#[inline(always)]
#[must_use]
pub fn silu(x: f32) -> f32 {
x / (1.0 + exp(-x))
}
#[inline(always)]
#[must_use]
pub fn erf(x: f32) -> f32 {
const P: f32 = 0.327_591_1;
const A1: f32 = 0.254_829_59;
const A2: f32 = -0.284_496_74;
const A3: f32 = 1.421_413_7;
const A4: f32 = -1.453_152_1;
const A5: f32 = 1.061_405_4;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let ax = x.abs();
let t = 1.0 / P.mul_add(ax, 1.0);
let poly = t * (A1 + t * (A2 + t * (A3 + t * (A4 + t * A5))));
sign * (1.0 - poly * exp(-ax * ax))
}
#[inline(always)]
#[must_use]
pub fn gelu_erf(x: f32) -> f32 {
const INV_SQRT_2: f32 = std::f32::consts::FRAC_1_SQRT_2;
0.5 * x * (1.0 + erf(x * INV_SQRT_2))
}
#[inline(always)]
#[must_use]
#[allow(clippy::many_single_char_names)]
pub fn ln(x: f32) -> f32 {
if x <= 0.0 {
return if x == 0.0 {
f32::NEG_INFINITY
} else {
f32::NAN
};
}
let bits = x.to_bits();
#[allow(clippy::cast_possible_wrap)]
let e = ((bits >> 23) & 0xff) as i32 - 127;
let m = f32::from_bits((bits & 0x007f_ffff) | 0x3f80_0000);
let (m, e) = if m > std::f32::consts::SQRT_2 {
(m * 0.5, e + 1)
} else {
(m, e)
};
let s = (m - 1.0) / (m + 1.0);
let s2 = s * s;
let p = 2.0
* s
* (1.0 + s2 * (0.333_333_34 + s2 * (0.2 + s2 * (0.142_857_15 + s2 * 0.111_111_11))));
(e as f32).mul_add(std::f32::consts::LN_2, p)
}
#[inline(always)]
#[must_use]
pub fn log10(x: f32) -> f32 {
ln(x) * std::f32::consts::LOG10_E
}
#[cfg(test)]
mod exp_sub_sum_tests {
use super::*;
#[test]
fn vector_twins_match_the_scalar_oracle() {
for &n in &[0usize, 1, 3, 4, 7, 8, 9, 15, 16, 31, 33, 64, 1024, 1031] {
let src: Vec<f32> = (0..n)
.map(|i| ((i * 37 % 211) as f32 - 105.0) * 0.15)
.collect();
let max = src.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let mut want = src.clone();
let want_sum = exp_sub_sum_scalar(&mut want, if n == 0 { 0.0 } else { max });
let mut got = src.clone();
let got_sum = exp_sub_sum_inplace(&mut got, if n == 0 { 0.0 } else { max });
for (i, (a, b)) in want.iter().zip(got.iter()).enumerate() {
let err = (a - b).abs() / a.abs().max(1e-30);
assert!(err < 1e-5, "n={n} i={i}: {a} vs {b} (rel {err:e})");
}
let serr = (want_sum - got_sum).abs() / want_sum.abs().max(1e-30);
assert!(
serr < 1e-5,
"n={n} sum {want_sum} vs {got_sum} (rel {serr:e})"
);
}
}
#[test]
fn max_twins_are_exact() {
for &n in &[0usize, 1, 3, 4, 7, 8, 9, 15, 31, 33, 1024, 1031] {
let xs: Vec<f32> = (0..n)
.map(|i| ((i * 89 % 401) as f32 - 200.0) * 0.37)
.collect();
assert_eq!(max_f32_scalar(&xs), max_f32(&xs), "n={n}");
}
assert_eq!(max_f32(&[]), f32::NEG_INFINITY);
}
#[test]
fn gelu_twins_match_the_scalar_oracle() {
for &n in &[0usize, 1, 3, 4, 7, 8, 9, 15, 33, 4096, 4099] {
let src: Vec<f32> = (0..n)
.map(|i| ((i * 53 % 601) as f32 - 300.0) * 0.09)
.collect();
let mut want = src.clone();
for v in &mut want {
*v = gelu_tanh(*v);
}
let mut got = src.clone();
gelu_tanh_inplace(&mut got);
for (i, (a, b)) in want.iter().zip(got.iter()).enumerate() {
let err = (a - b).abs() / a.abs().max(1e-6);
assert!(
err < 1e-5,
"n={n} i={i} x={}: {a} vs {b} (rel {err:e})",
src[i]
);
}
}
}
#[test]
fn normalises_to_one() {
let mut row: Vec<f32> = (0..1024).map(|i| ((i % 97) as f32) * 0.11 - 5.0).collect();
let max = row.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let sum = exp_sub_sum_inplace(&mut row, max);
let total: f32 = row.iter().map(|v| v / sum).sum();
assert!((total - 1.0).abs() < 1e-4, "sums to {total}");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sweep(lo: f32, hi: f32, f: impl Fn(f32) -> f32, oracle: impl Fn(f32) -> f32) -> (f32, f32) {
let n = 200_001;
let mut worst_rel = 0.0f32;
let mut at = lo;
for i in 0..n {
let x = lo + (hi - lo) * (i as f32 / (n - 1) as f32);
let (a, b) = (f(x), oracle(x));
let abs = (a - b).abs();
let rel = if b.abs() > f32::MIN_POSITIVE {
(abs / b.abs()).min(abs)
} else {
abs
};
if rel > worst_rel {
worst_rel = rel;
at = x;
}
}
(worst_rel, at)
}
#[test]
fn exp_tracks_libm() {
let (rel, at) = sweep(-20.0, 20.0, exp, f32::exp);
eprintln!("exp: worst rel {rel:.3e} at x = {at}");
assert!(rel < 1e-5, "exp worst rel {rel:.3e} at {at}");
}
#[test]
fn exp_saturates_rather_than_producing_garbage() {
assert!(exp(200.0).is_finite(), "exp(200) must saturate, not wrap");
assert!(exp(200.0) > 1e30, "exp(200) should be very large");
assert!(
exp(-200.0) < 1e-30 && exp(-200.0) >= 0.0,
"exp(-200) = {} should saturate tiny, not wrap",
exp(-200.0)
);
assert!((exp(0.0) - 1.0).abs() < 1e-7, "exp(0) = {}", exp(0.0));
}
#[test]
fn the_magic_rounding_is_round_ties_even() {
for (x, want) in [
(0.5f32, 0.0f32),
(1.5, 2.0),
(2.5, 2.0),
(-0.5, -0.0),
(-1.5, -2.0),
(-2.5, -2.0),
(3.2, 3.0),
(-3.7, -4.0),
] {
let got = round_ties_even_fast(x);
assert_eq!(got, want, "round_ties_even_fast({x}) = {got}, want {want}");
}
for i in -1000..1000 {
let x = i as f32 / 7.0;
assert_eq!(round_ties_even_fast(x), x.round_ties_even(), "at {x}");
}
}
#[test]
fn tanh_tracks_libm_and_saturates() {
let (rel, at) = sweep(-8.0, 8.0, tanh, f32::tanh);
eprintln!("tanh: worst rel {rel:.3e} at x = {at}");
assert!(rel < 1e-5, "tanh worst rel {rel:.3e} at {at}");
assert!((tanh(0.0)).abs() < 1e-7);
assert!((tanh(20.0) - 1.0).abs() < 1e-6);
assert!((tanh(-20.0) + 1.0).abs() < 1e-6);
}
#[test]
fn sigmoid_and_silu_track_libm() {
let (rel, at) = sweep(-15.0, 15.0, sigmoid, |x| 1.0 / (1.0 + (-x).exp()));
assert!(rel < 1e-5, "sigmoid worst rel {rel:.3e} at {at}");
let (rel, at) = sweep(-15.0, 15.0, silu, |x| x / (1.0 + (-x).exp()));
assert!(rel < 1e-5, "silu worst rel {rel:.3e} at {at}");
assert_eq!(silu(0.0), 0.0, "silu(0) must be exactly 0");
}
#[test]
fn gelu_tracks_libm_and_keeps_its_dip() {
let oracle =
|x: f32| 0.5 * x * (1.0 + (0.797_884_56 * x * (1.0 + 0.044_715 * x * x)).tanh());
let (rel, at) = sweep(-10.0, 10.0, gelu_tanh, oracle);
eprintln!("gelu: worst rel {rel:.3e} at x = {at}");
assert!(rel < 1e-5, "gelu worst rel {rel:.3e} at {at}");
assert!(
(-0.18..-0.15).contains(&gelu_tanh(-0.75)),
"gelu(-0.75) = {} should sit near the -0.17 minimum",
gelu_tanh(-0.75)
);
assert_eq!(gelu_tanh(0.0), 0.0, "gelu(0) must be exactly 0");
assert!((gelu_tanh(10.0) - 10.0).abs() < 1e-4);
assert!(gelu_tanh(-10.0).abs() < 1e-5);
}
#[test]
fn erf_and_gelu_erf_track_a_reference() {
let oracle = |x: f32| -> f32 {
let x = f64::from(x);
if x.abs() < 4.0 {
let mut term = x;
let mut sum = x;
for n in 1..90 {
term *= -x * x / f64::from(n);
sum += term / f64::from(2 * n + 1);
}
(sum * 2.0 / std::f64::consts::PI.sqrt()) as f32
} else if x > 0.0 {
1.0
} else {
-1.0
}
};
let (rel, at) = sweep(-3.0, 3.0, erf, oracle);
eprintln!("erf: worst {rel:.3e} at x = {at}");
assert!(rel < 5e-6, "erf worst {rel:.3e} at {at}");
assert!(erf(0.0).abs() < 3e-7, "erf(0) = {}", erf(0.0));
assert_eq!(gelu_erf(0.0), 0.0, "gelu_erf(0) must be exactly 0");
assert!((erf(4.0) - 1.0).abs() < 1e-6);
assert!((erf(-4.0) + 1.0).abs() < 1e-6);
let worst = (-40..40)
.map(|i| {
let x = i as f32 / 10.0;
(gelu_erf(x) - gelu_tanh(x)).abs()
})
.fold(0.0f32, f32::max);
assert!(
worst > 1e-4,
"gelu_erf and gelu_tanh differ by only {worst:.3e} — are they aliased?"
);
}
#[test]
fn ln_and_log10_track_libm() {
let (rel, at) = sweep(1e-6, 1e6, ln, f32::ln);
eprintln!("ln: worst rel {rel:.3e} at x = {at}");
assert!(rel < 1e-5, "ln worst rel {rel:.3e} at {at}");
let (rel, at) = sweep(1e-6, 1e6, log10, f32::log10);
assert!(rel < 1e-5, "log10 worst rel {rel:.3e} at {at}");
assert!((log10(1.0)).abs() < 1e-6, "log10(1) = {}", log10(1.0));
assert!((log10(10.0) - 1.0).abs() < 1e-5);
assert!((log10(1e-10) + 10.0).abs() < 1e-4);
}
#[test]
fn ln_matches_std_at_the_edges() {
assert_eq!(ln(0.0), f32::NEG_INFINITY);
assert!(ln(-1.0).is_nan());
assert!((ln(std::f32::consts::E) - 1.0).abs() < 1e-6);
}
#[test]
fn exp_and_ln_round_trip() {
for i in -60..60 {
let x = i as f32 / 6.0;
let back = ln(exp(x));
assert!((back - x).abs() < 1e-4, "ln(exp({x})) = {back}");
}
}
}
#[must_use]
pub fn exp_sub_sum_inplace(row: &mut [f32], max: f32) -> f32 {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
return unsafe { exp_sub_sum_avx2(row, max) };
}
}
#[cfg(target_arch = "aarch64")]
{
return unsafe { exp_sub_sum_neon(row, max) };
}
#[cfg(target_arch = "wasm32")]
{
return unsafe { exp_sub_sum_simd128(row, max) };
}
#[allow(unreachable_code)]
exp_sub_sum_scalar(row, max)
}
#[must_use]
pub fn exp_sub_sum_scalar(row: &mut [f32], max: f32) -> f32 {
let mut sum = 0.0f32;
for v in row.iter_mut() {
let e = exp(*v - max);
*v = e;
sum += e;
}
sum
}
#[allow(clippy::many_single_char_names)]
#[allow(clippy::wildcard_imports)]
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn exp_sub_sum_avx2(row: &mut [f32], max: f32) -> f32 {
unsafe {
use std::arch::x86_64::*;
const L1: f32 = std::f32::consts::LN_2;
const L2: f32 = L1 * L1 / 2.0;
const L3: f32 = L1 * L1 * L1 / 6.0;
const L4: f32 = L1 * L1 * L1 * L1 / 24.0;
const L5: f32 = L1 * L1 * L1 * L1 * L1 / 120.0;
let vmax = _mm256_set1_ps(max);
let log2e = _mm256_set1_ps(std::f32::consts::LOG2_E);
let magic = _mm256_set1_ps(MAGIC);
let lo = _mm256_set1_ps(-125.0);
let hi = _mm256_set1_ps(125.0);
let c1 = _mm256_set1_ps(L1);
let c2 = _mm256_set1_ps(L2);
let c3 = _mm256_set1_ps(L3);
let c4 = _mm256_set1_ps(L4);
let c5 = _mm256_set1_ps(L5);
let one = _mm256_set1_ps(1.0);
let bias = _mm256_set1_epi32(127);
let mut acc = _mm256_setzero_ps();
let n = row.len();
let mut i = 0;
while i + 8 <= n {
let x = _mm256_loadu_ps(row.as_ptr().add(i));
let t = _mm256_mul_ps(_mm256_sub_ps(x, vmax), log2e);
let t = _mm256_min_ps(_mm256_max_ps(t, lo), hi);
let r = _mm256_sub_ps(_mm256_add_ps(t, magic), magic);
let f = _mm256_sub_ps(t, r);
let p = _mm256_fmadd_ps(f, c5, c4);
let p = _mm256_fmadd_ps(f, p, c3);
let p = _mm256_fmadd_ps(f, p, c2);
let p = _mm256_fmadd_ps(f, p, c1);
let p = _mm256_fmadd_ps(f, p, one);
let e = _mm256_cvtps_epi32(r);
let e = _mm256_slli_epi32::<23>(_mm256_add_epi32(e, bias));
let out = _mm256_mul_ps(p, _mm256_castsi256_ps(e));
_mm256_storeu_ps(row.as_mut_ptr().add(i), out);
acc = _mm256_add_ps(acc, out);
i += 8;
}
let mut lanes = [0f32; 8];
_mm256_storeu_ps(lanes.as_mut_ptr(), acc);
let mut sum = ((lanes[0] + lanes[1]) + (lanes[2] + lanes[3]))
+ ((lanes[4] + lanes[5]) + (lanes[6] + lanes[7]));
while i < n {
let e = exp(row[i] - max);
row[i] = e;
sum += e;
i += 1;
}
sum
}
}
#[allow(clippy::many_single_char_names)]
#[allow(clippy::wildcard_imports)]
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn exp_sub_sum_neon(row: &mut [f32], max: f32) -> f32 {
unsafe {
use core::arch::aarch64::*;
const L1: f32 = core::f32::consts::LN_2;
const L2: f32 = L1 * L1 / 2.0;
const L3: f32 = L1 * L1 * L1 / 6.0;
const L4: f32 = L1 * L1 * L1 * L1 / 24.0;
const L5: f32 = L1 * L1 * L1 * L1 * L1 / 120.0;
let vmax = vdupq_n_f32(max);
let log2e = vdupq_n_f32(core::f32::consts::LOG2_E);
let magic = vdupq_n_f32(MAGIC);
let lo = vdupq_n_f32(-125.0);
let hi = vdupq_n_f32(125.0);
let c1 = vdupq_n_f32(L1);
let c2 = vdupq_n_f32(L2);
let c3 = vdupq_n_f32(L3);
let c4 = vdupq_n_f32(L4);
let c5 = vdupq_n_f32(L5);
let one = vdupq_n_f32(1.0);
let bias = vdupq_n_s32(127);
let mut acc = vdupq_n_f32(0.0);
let n = row.len();
let mut i = 0;
while i + 4 <= n {
let x = vld1q_f32(row.as_ptr().add(i));
let t = vmulq_f32(vsubq_f32(x, vmax), log2e);
let t = vminq_f32(vmaxq_f32(t, lo), hi);
let r = vsubq_f32(vaddq_f32(t, magic), magic);
let f = vsubq_f32(t, r);
let p = vfmaq_f32(c4, f, c5);
let p = vfmaq_f32(c3, f, p);
let p = vfmaq_f32(c2, f, p);
let p = vfmaq_f32(c1, f, p);
let p = vfmaq_f32(one, f, p);
let e = vshlq_n_s32::<23>(vaddq_s32(vcvtq_s32_f32(r), bias));
let out = vmulq_f32(p, vreinterpretq_f32_s32(e));
vst1q_f32(row.as_mut_ptr().add(i), out);
acc = vaddq_f32(acc, out);
i += 4;
}
let mut sum = (vgetq_lane_f32::<0>(acc) + vgetq_lane_f32::<1>(acc))
+ (vgetq_lane_f32::<2>(acc) + vgetq_lane_f32::<3>(acc));
while i < n {
let e = exp(row[i] - max);
row[i] = e;
sum += e;
i += 1;
}
sum
}
}
#[allow(clippy::many_single_char_names)]
#[allow(clippy::wildcard_imports)]
#[cfg(target_arch = "wasm32")]
#[target_feature(enable = "simd128")]
unsafe fn exp_sub_sum_simd128(row: &mut [f32], max: f32) -> f32 {
unsafe {
use core::arch::wasm32::*;
const L1: f32 = core::f32::consts::LN_2;
const L2: f32 = L1 * L1 / 2.0;
const L3: f32 = L1 * L1 * L1 / 6.0;
const L4: f32 = L1 * L1 * L1 * L1 / 24.0;
const L5: f32 = L1 * L1 * L1 * L1 * L1 / 120.0;
let vmax = f32x4_splat(max);
let log2e = f32x4_splat(core::f32::consts::LOG2_E);
let magic = f32x4_splat(MAGIC);
let lo = f32x4_splat(-125.0);
let hi = f32x4_splat(125.0);
let c1 = f32x4_splat(L1);
let c2 = f32x4_splat(L2);
let c3 = f32x4_splat(L3);
let c4 = f32x4_splat(L4);
let c5 = f32x4_splat(L5);
let one = f32x4_splat(1.0);
let bias = i32x4_splat(127);
let mut acc = f32x4_splat(0.0);
let n = row.len();
let mut i = 0;
while i + 4 <= n {
let x = v128_load(row.as_ptr().add(i).cast());
let t = f32x4_mul(f32x4_sub(x, vmax), log2e);
let t = f32x4_pmin(hi, f32x4_pmax(lo, t));
let r = f32x4_sub(f32x4_add(t, magic), magic);
let f = f32x4_sub(t, r);
let p = f32x4_add(f32x4_mul(f, c5), c4);
let p = f32x4_add(f32x4_mul(f, p), c3);
let p = f32x4_add(f32x4_mul(f, p), c2);
let p = f32x4_add(f32x4_mul(f, p), c1);
let p = f32x4_add(f32x4_mul(f, p), one);
let e = i32x4_trunc_sat_f32x4(r);
let e = i32x4_shl(i32x4_add(e, bias), 23);
let out = f32x4_mul(p, e);
v128_store(row.as_mut_ptr().add(i).cast(), out);
acc = f32x4_add(acc, out);
i += 4;
}
let mut sum = (f32x4_extract_lane::<0>(acc) + f32x4_extract_lane::<1>(acc))
+ (f32x4_extract_lane::<2>(acc) + f32x4_extract_lane::<3>(acc));
while i < n {
let e = exp(row[i] - max);
row[i] = e;
sum += e;
i += 1;
}
sum
}
}
#[must_use]
pub fn max_f32(xs: &[f32]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") {
return unsafe { max_f32_avx2(xs) };
}
}
#[cfg(target_arch = "aarch64")]
{
return unsafe { max_f32_neon(xs) };
}
#[cfg(target_arch = "wasm32")]
{
return unsafe { max_f32_simd128(xs) };
}
#[allow(unreachable_code)]
max_f32_scalar(xs)
}
#[must_use]
pub fn max_f32_scalar(xs: &[f32]) -> f32 {
let mut m = f32::NEG_INFINITY;
for &v in xs {
m = m.max(v);
}
m
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn max_f32_avx2(xs: &[f32]) -> f32 {
unsafe {
use std::arch::x86_64::{_mm256_loadu_ps, _mm256_max_ps, _mm256_storeu_ps};
let n = xs.len();
if n < 8 {
return max_f32_scalar(xs);
}
let mut acc = _mm256_loadu_ps(xs.as_ptr());
let mut i = 8;
while i + 8 <= n {
acc = _mm256_max_ps(acc, _mm256_loadu_ps(xs.as_ptr().add(i)));
i += 8;
}
let mut lanes = [0f32; 8];
_mm256_storeu_ps(lanes.as_mut_ptr(), acc);
let mut m = lanes[0];
for &v in &lanes[1..] {
m = m.max(v);
}
while i < n {
m = m.max(xs[i]);
i += 1;
}
m
}
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn max_f32_neon(xs: &[f32]) -> f32 {
unsafe {
use core::arch::aarch64::{vld1q_f32, vmaxq_f32, vmaxvq_f32};
let n = xs.len();
if n < 4 {
return max_f32_scalar(xs);
}
let mut acc = vld1q_f32(xs.as_ptr());
let mut i = 4;
while i + 4 <= n {
acc = vmaxq_f32(acc, vld1q_f32(xs.as_ptr().add(i)));
i += 4;
}
let mut m = vmaxvq_f32(acc);
while i < n {
m = m.max(xs[i]);
i += 1;
}
m
}
}
#[cfg(target_arch = "wasm32")]
#[target_feature(enable = "simd128")]
unsafe fn max_f32_simd128(xs: &[f32]) -> f32 {
unsafe {
use core::arch::wasm32::{f32x4_extract_lane, f32x4_pmax, v128_load};
let n = xs.len();
if n < 4 {
return max_f32_scalar(xs);
}
let mut acc = v128_load(xs.as_ptr().cast());
let mut i = 4;
while i + 4 <= n {
acc = f32x4_pmax(acc, v128_load(xs.as_ptr().add(i).cast()));
i += 4;
}
let mut m = f32x4_extract_lane::<0>(acc);
m = m.max(f32x4_extract_lane::<1>(acc));
m = m.max(f32x4_extract_lane::<2>(acc));
m = m.max(f32x4_extract_lane::<3>(acc));
while i < n {
m = m.max(xs[i]);
i += 1;
}
m
}
}
pub fn gelu_tanh_inplace(xs: &mut [f32]) {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
unsafe { gelu_tanh_avx2(xs) };
return;
}
}
#[cfg(target_arch = "aarch64")]
{
unsafe { gelu_tanh_neon(xs) };
return;
}
#[cfg(target_arch = "wasm32")]
{
unsafe { gelu_tanh_simd128(xs) };
return;
}
#[allow(unreachable_code)]
for v in xs.iter_mut() {
*v = gelu_tanh(*v);
}
}
#[allow(clippy::excessive_precision)]
#[allow(clippy::many_single_char_names)]
#[allow(clippy::wildcard_imports)]
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn gelu_tanh_avx2(xs: &mut [f32]) {
unsafe {
use std::arch::x86_64::*;
const L1: f32 = std::f32::consts::LN_2;
const L2: f32 = L1 * L1 / 2.0;
const L3: f32 = L1 * L1 * L1 / 6.0;
const L4: f32 = L1 * L1 * L1 * L1 / 24.0;
const L5: f32 = L1 * L1 * L1 * L1 * L1 / 120.0;
let sq2pi = _mm256_set1_ps(0.797_884_56);
let k = _mm256_set1_ps(0.044_715);
let one = _mm256_set1_ps(1.0);
let m2 = _mm256_set1_ps(-2.0);
let log2e = _mm256_set1_ps(std::f32::consts::LOG2_E);
let magic = _mm256_set1_ps(MAGIC);
let lo = _mm256_set1_ps(-125.0);
let hi = _mm256_set1_ps(125.0);
let (c1, c2, c3, c4, c5) = (
_mm256_set1_ps(L1),
_mm256_set1_ps(L2),
_mm256_set1_ps(L3),
_mm256_set1_ps(L4),
_mm256_set1_ps(L5),
);
let bias = _mm256_set1_epi32(127);
let n = xs.len();
let mut i = 0;
while i + 8 <= n {
let x = _mm256_loadu_ps(xs.as_ptr().add(i));
let x2 = _mm256_mul_ps(x, x);
let z = _mm256_mul_ps(_mm256_mul_ps(sq2pi, x), _mm256_fmadd_ps(k, x2, one));
let t = _mm256_mul_ps(_mm256_mul_ps(m2, z), log2e);
let t = _mm256_min_ps(_mm256_max_ps(t, lo), hi);
let r = _mm256_sub_ps(_mm256_add_ps(t, magic), magic);
let f = _mm256_sub_ps(t, r);
let p = _mm256_fmadd_ps(f, c5, c4);
let p = _mm256_fmadd_ps(f, p, c3);
let p = _mm256_fmadd_ps(f, p, c2);
let p = _mm256_fmadd_ps(f, p, c1);
let p = _mm256_fmadd_ps(f, p, one);
let e = _mm256_slli_epi32::<23>(_mm256_add_epi32(_mm256_cvtps_epi32(r), bias));
let ex = _mm256_mul_ps(p, _mm256_castsi256_ps(e));
_mm256_storeu_ps(
xs.as_mut_ptr().add(i),
_mm256_div_ps(x, _mm256_add_ps(one, ex)),
);
i += 8;
}
while i < n {
xs[i] = gelu_tanh(xs[i]);
i += 1;
}
}
}
#[allow(clippy::excessive_precision)]
#[allow(clippy::many_single_char_names)]
#[allow(clippy::wildcard_imports)]
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn gelu_tanh_neon(xs: &mut [f32]) {
unsafe {
use core::arch::aarch64::*;
const L1: f32 = core::f32::consts::LN_2;
const L2: f32 = L1 * L1 / 2.0;
const L3: f32 = L1 * L1 * L1 / 6.0;
const L4: f32 = L1 * L1 * L1 * L1 / 24.0;
const L5: f32 = L1 * L1 * L1 * L1 * L1 / 120.0;
let sq2pi = vdupq_n_f32(0.797_884_56);
let k = vdupq_n_f32(0.044_715);
let one = vdupq_n_f32(1.0);
let m2 = vdupq_n_f32(-2.0);
let log2e = vdupq_n_f32(core::f32::consts::LOG2_E);
let magic = vdupq_n_f32(MAGIC);
let lo = vdupq_n_f32(-125.0);
let hi = vdupq_n_f32(125.0);
let (c1, c2, c3, c4, c5) = (
vdupq_n_f32(L1),
vdupq_n_f32(L2),
vdupq_n_f32(L3),
vdupq_n_f32(L4),
vdupq_n_f32(L5),
);
let bias = vdupq_n_s32(127);
let n = xs.len();
let mut i = 0;
while i + 4 <= n {
let x = vld1q_f32(xs.as_ptr().add(i));
let x2 = vmulq_f32(x, x);
let z = vmulq_f32(vmulq_f32(sq2pi, x), vfmaq_f32(one, k, x2));
let t = vmulq_f32(vmulq_f32(m2, z), log2e);
let t = vminq_f32(vmaxq_f32(t, lo), hi);
let r = vsubq_f32(vaddq_f32(t, magic), magic);
let f = vsubq_f32(t, r);
let p = vfmaq_f32(c4, f, c5);
let p = vfmaq_f32(c3, f, p);
let p = vfmaq_f32(c2, f, p);
let p = vfmaq_f32(c1, f, p);
let p = vfmaq_f32(one, f, p);
let e = vshlq_n_s32::<23>(vaddq_s32(vcvtq_s32_f32(r), bias));
let ex = vmulq_f32(p, vreinterpretq_f32_s32(e));
vst1q_f32(xs.as_mut_ptr().add(i), vdivq_f32(x, vaddq_f32(one, ex)));
i += 4;
}
while i < n {
xs[i] = gelu_tanh(xs[i]);
i += 1;
}
}
}
#[allow(clippy::excessive_precision)]
#[allow(clippy::many_single_char_names)]
#[allow(clippy::wildcard_imports)]
#[cfg(target_arch = "wasm32")]
#[target_feature(enable = "simd128")]
unsafe fn gelu_tanh_simd128(xs: &mut [f32]) {
unsafe {
use core::arch::wasm32::*;
const L1: f32 = core::f32::consts::LN_2;
const L2: f32 = L1 * L1 / 2.0;
const L3: f32 = L1 * L1 * L1 / 6.0;
const L4: f32 = L1 * L1 * L1 * L1 / 24.0;
const L5: f32 = L1 * L1 * L1 * L1 * L1 / 120.0;
let sq2pi = f32x4_splat(0.797_884_56);
let k = f32x4_splat(0.044_715);
let one = f32x4_splat(1.0);
let m2 = f32x4_splat(-2.0);
let log2e = f32x4_splat(core::f32::consts::LOG2_E);
let magic = f32x4_splat(MAGIC);
let lo = f32x4_splat(-125.0);
let hi = f32x4_splat(125.0);
let (c1, c2, c3, c4, c5) = (
f32x4_splat(L1),
f32x4_splat(L2),
f32x4_splat(L3),
f32x4_splat(L4),
f32x4_splat(L5),
);
let bias = i32x4_splat(127);
let n = xs.len();
let mut i = 0;
while i + 4 <= n {
let x = v128_load(xs.as_ptr().add(i).cast());
let x2 = f32x4_mul(x, x);
let z = f32x4_mul(f32x4_mul(sq2pi, x), f32x4_add(f32x4_mul(k, x2), one));
let t = f32x4_mul(f32x4_mul(m2, z), log2e);
let t = f32x4_pmin(hi, f32x4_pmax(lo, t));
let r = f32x4_sub(f32x4_add(t, magic), magic);
let f = f32x4_sub(t, r);
let p = f32x4_add(f32x4_mul(f, c5), c4);
let p = f32x4_add(f32x4_mul(f, p), c3);
let p = f32x4_add(f32x4_mul(f, p), c2);
let p = f32x4_add(f32x4_mul(f, p), c1);
let p = f32x4_add(f32x4_mul(f, p), one);
let e = i32x4_shl(i32x4_add(i32x4_trunc_sat_f32x4(r), bias), 23);
let ex = f32x4_mul(p, e);
v128_store(
xs.as_mut_ptr().add(i).cast(),
f32x4_div(x, f32x4_add(one, ex)),
);
i += 4;
}
while i < n {
xs[i] = gelu_tanh(xs[i]);
i += 1;
}
}
}