use crate::pool;
#[cfg(target_arch = "aarch64")]
#[inline(always)]
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe fn neon_exp4(x: std::arch::aarch64::float32x4_t) -> std::arch::aarch64::float32x4_t {
use std::arch::aarch64::*;
let x = vmaxq_f32(x, vdupq_n_f32(-87.3));
let x = vminq_f32(x, vdupq_n_f32(88.7));
let inv_ln2 = vdupq_n_f32(std::f32::consts::LOG2_E);
let ln2_hi = vdupq_n_f32(0.693_145_75);
let ln2_lo = vdupq_n_f32(1.428_606_8e-6);
let n = vrndnq_f32(vmulq_f32(x, inv_ln2));
let r = vfmsq_f32(vfmsq_f32(x, n, ln2_hi), n, ln2_lo);
let c1 = vdupq_n_f32(1.0);
let mut p = vdupq_n_f32(0.001_388_888_9);
p = vfmaq_f32(vdupq_n_f32(0.008_333_334), p, r);
p = vfmaq_f32(vdupq_n_f32(0.041_666_668), p, r);
p = vfmaq_f32(vdupq_n_f32(0.166_666_67), p, r);
p = vfmaq_f32(vdupq_n_f32(0.5), p, r);
p = vfmaq_f32(c1, p, r);
p = vfmaq_f32(c1, p, r);
let ni = vcvtq_s32_f32(n);
vreinterpretq_f32_s32(vaddq_s32(vreinterpretq_s32_f32(p), vshlq_n_s32(ni, 23)))
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2", enable = "fma")]
#[allow(unsafe_op_in_unsafe_fn, clippy::excessive_precision)]
pub unsafe fn avx2_exp8(x: std::arch::x86_64::__m256) -> std::arch::x86_64::__m256 {
use std::arch::x86_64::*;
let x = _mm256_max_ps(x, _mm256_set1_ps(-87.3));
let x = _mm256_min_ps(x, _mm256_set1_ps(88.7));
let inv_ln2 = _mm256_set1_ps(std::f32::consts::LOG2_E);
let ln2_hi = _mm256_set1_ps(0.693145751953125);
let ln2_lo = _mm256_set1_ps(1.428606765330187e-6);
let n = _mm256_round_ps::<{ _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC }>(_mm256_mul_ps(
x, inv_ln2,
));
let r = _mm256_fnmadd_ps(n, ln2_lo, _mm256_fnmadd_ps(n, ln2_hi, x));
let c1 = _mm256_set1_ps(1.0);
let mut p = _mm256_set1_ps(0.001388888888888889);
p = _mm256_fmadd_ps(p, r, _mm256_set1_ps(0.008333333333333333));
p = _mm256_fmadd_ps(p, r, _mm256_set1_ps(0.041666666666666664));
p = _mm256_fmadd_ps(p, r, _mm256_set1_ps(0.16666666666666666));
p = _mm256_fmadd_ps(p, r, _mm256_set1_ps(0.5));
p = _mm256_fmadd_ps(p, r, c1);
p = _mm256_fmadd_ps(p, r, c1);
let ni = _mm256_cvtps_epi32(n);
let shifted = _mm256_slli_epi32::<23>(ni);
_mm256_castsi256_ps(_mm256_add_epi32(_mm256_castps_si256(p), shifted))
}
#[cfg(target_arch = "aarch64")]
pub fn bias_gelu(data: &mut [f32], bias: &[f32], m: usize, n: usize) {
use std::arch::aarch64::*;
let chunks = n / 4;
unsafe {
let half = vdupq_n_f32(0.5);
let one = vdupq_n_f32(1.0);
let inv_sqrt2 = vdupq_n_f32(std::f32::consts::FRAC_1_SQRT_2);
let p = vdupq_n_f32(0.3275911);
let a1 = vdupq_n_f32(0.254_829_6);
let a2 = vdupq_n_f32(-0.284_496_72);
let a3 = vdupq_n_f32(1.421_413_8);
let a4 = vdupq_n_f32(-1.453_152_1);
let a5 = vdupq_n_f32(1.061_405_4);
let neg_one = vdupq_n_f32(-1.0);
let zero = vdupq_n_f32(0.0);
for row in 0..m {
let base = row * n;
for c in 0..chunks {
let off = base + c * 4;
let ptr = data.as_mut_ptr().add(off);
let x = vaddq_f32(vld1q_f32(ptr), vld1q_f32(bias.as_ptr().add(c * 4)));
let erf_arg = vmulq_f32(x, inv_sqrt2);
let xa = vabsq_f32(erf_arg);
let sign = vbslq_f32(vcgeq_f32(erf_arg, zero), one, neg_one);
let denom = vfmaq_f32(one, p, xa);
let t = vdivq_f32(one, denom);
let mut y = a5;
y = vfmaq_f32(a4, y, t);
y = vfmaq_f32(a3, y, t);
y = vfmaq_f32(a2, y, t);
y = vfmaq_f32(a1, y, t);
y = vmulq_f32(y, t);
let exp_val = neon_exp4(vnegq_f32(vmulq_f32(xa, xa)));
let erf_val = vmulq_f32(sign, vfmsq_f32(one, y, exp_val));
vst1q_f32(ptr, vmulq_f32(x, vmulq_f32(half, vaddq_f32(one, erf_val))));
}
for i in (chunks * 4)..n {
let x = data[base + i] + bias[i];
data[base + i] = scalar_gelu(x);
}
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2", enable = "fma")]
#[allow(clippy::excessive_precision)]
unsafe fn bias_gelu_avx2(data: &mut [f32], bias: &[f32], m: usize, n: usize) {
use std::arch::x86_64::*;
let chunks = n / 8;
unsafe {
let half = _mm256_set1_ps(0.5);
let one = _mm256_set1_ps(1.0);
let inv_sqrt2 = _mm256_set1_ps(std::f32::consts::FRAC_1_SQRT_2);
let p = _mm256_set1_ps(0.3275911);
let a1 = _mm256_set1_ps(0.254829592);
let a2 = _mm256_set1_ps(-0.284496736);
let a3 = _mm256_set1_ps(1.421413741);
let a4 = _mm256_set1_ps(-1.453152027);
let a5 = _mm256_set1_ps(1.061405429);
let neg_one = _mm256_set1_ps(-1.0);
let zero = _mm256_set1_ps(0.0);
let abs_mask = _mm256_castsi256_ps(_mm256_set1_epi32(0x7fff_ffff));
for row in 0..m {
let base = row * n;
for c in 0..chunks {
let off = base + c * 8;
let ptr = data.as_mut_ptr().add(off);
let x = _mm256_add_ps(
_mm256_loadu_ps(ptr),
_mm256_loadu_ps(bias.as_ptr().add(c * 8)),
);
let erf_arg = _mm256_mul_ps(x, inv_sqrt2);
let xa = _mm256_and_ps(erf_arg, abs_mask);
let ge0 = _mm256_cmp_ps::<_CMP_GE_OQ>(erf_arg, zero);
let sign = _mm256_blendv_ps(neg_one, one, ge0);
let denom = _mm256_fmadd_ps(p, xa, one);
let t = _mm256_div_ps(one, denom);
let mut y = a5;
y = _mm256_fmadd_ps(y, t, a4);
y = _mm256_fmadd_ps(y, t, a3);
y = _mm256_fmadd_ps(y, t, a2);
y = _mm256_fmadd_ps(y, t, a1);
y = _mm256_mul_ps(y, t);
let exp_val = avx2_exp8(_mm256_sub_ps(zero, _mm256_mul_ps(xa, xa)));
let erf_val = _mm256_mul_ps(sign, _mm256_fnmadd_ps(y, exp_val, one));
_mm256_storeu_ps(
ptr,
_mm256_mul_ps(x, _mm256_mul_ps(half, _mm256_add_ps(one, erf_val))),
);
}
for i in (chunks * 8)..n {
let x = data[base + i] + bias[i];
data[base + i] = scalar_gelu(x);
}
}
}
}
#[cfg(not(target_arch = "aarch64"))]
fn bias_gelu_scalar(data: &mut [f32], bias: &[f32], m: usize, n: usize) {
for row in 0..m {
let base = row * n;
for i in 0..n {
let x = data[base + i] + bias[i];
data[base + i] = scalar_gelu(x);
}
}
}
#[cfg(target_arch = "x86_64")]
pub fn bias_gelu(data: &mut [f32], bias: &[f32], m: usize, n: usize) {
if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") {
unsafe { bias_gelu_avx2(data, bias, m, n) };
return;
}
bias_gelu_scalar(data, bias, m, n);
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
pub fn bias_gelu(data: &mut [f32], bias: &[f32], m: usize, n: usize) {
bias_gelu_scalar(data, bias, m, n);
}
pub fn swiglu_rows(inp: &[f32], out: &mut [f32], outer: usize, n: usize, gate_first: bool) {
debug_assert!(inp.len() >= outer * 2 * n && out.len() >= outer * n);
for o in 0..outer {
let in_row = &inp[o * 2 * n..(o + 1) * 2 * n];
let out_row = &mut out[o * n..(o + 1) * n];
let (gate, up) = if gate_first {
(&in_row[..n], &in_row[n..])
} else {
(&in_row[n..], &in_row[..n])
};
for i in 0..n {
let g = gate[i];
out_row[i] = up[i] * (g / (1.0 + crate::vmath::exp_poly(-g)));
}
}
}
pub fn par_bias_gelu(data: &mut [f32], bias: &[f32], m: usize, n: usize) {
let cfg = crate::config::RuntimeConfig::global();
if m * n < cfg.par_threshold || m < cfg.min_rows_per_thread {
bias_gelu(data, bias, m, n);
return;
}
let data_ptr = data.as_mut_ptr() as usize;
let bias_ptr = bias.as_ptr() as usize;
pool::par_for(m, cfg.min_rows_per_thread, &|off, cnt| unsafe {
let d = std::slice::from_raw_parts_mut((data_ptr as *mut f32).add(off * n), cnt * n);
let b = std::slice::from_raw_parts(bias_ptr as *const f32, n);
bias_gelu(d, b, cnt, n);
});
}
#[cfg(target_arch = "aarch64")]
pub fn silu_inplace(data: &mut [f32]) {
use std::arch::aarch64::*;
let chunks = data.len() / 4;
unsafe {
let one = vdupq_n_f32(1.0);
for c in 0..chunks {
let ptr = data.as_mut_ptr().add(c * 4);
let x = vld1q_f32(ptr);
let exp_neg = neon_exp4(vnegq_f32(x));
let sigmoid = vdivq_f32(one, vaddq_f32(one, exp_neg));
vst1q_f32(ptr, vmulq_f32(x, sigmoid));
}
}
for i in (chunks * 4)..data.len() {
let x = data[i];
data[i] = x / (1.0 + (-x).exp());
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2", enable = "fma")]
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn silu_inplace_avx2(data: &mut [f32]) {
use std::arch::x86_64::*;
let chunks = data.len() / 8;
let one = _mm256_set1_ps(1.0);
let zero = _mm256_set1_ps(0.0);
for c in 0..chunks {
let off = c * 8;
let ptr = data.as_mut_ptr().add(off);
let x = _mm256_loadu_ps(ptr);
let neg_x = _mm256_sub_ps(zero, x);
let denom = _mm256_add_ps(one, avx2_exp8(neg_x));
_mm256_storeu_ps(ptr, _mm256_div_ps(x, denom));
}
for i in (chunks * 8)..data.len() {
let x = data[i];
data[i] = x / (1.0 + (-x).exp());
}
}
#[cfg(target_arch = "x86_64")]
pub fn silu_inplace(data: &mut [f32]) {
if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") {
unsafe { silu_inplace_avx2(data) };
return;
}
for v in data.iter_mut() {
let x = *v;
*v = x / (1.0 + crate::vmath::exp_poly(-x));
}
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
pub fn silu_inplace(data: &mut [f32]) {
for v in data.iter_mut() {
let x = *v;
*v = x / (1.0 + crate::vmath::exp_poly(-x));
}
}
#[cfg(target_arch = "aarch64")]
pub fn layer_norm_row(
input: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
h: usize,
eps: f32,
) {
use std::arch::aarch64::*;
let inv_hf = 1.0 / h as f32;
let chunks = h / 4;
unsafe {
let mut vsum = vdupq_n_f32(0.0);
for c in 0..chunks {
let x = vld1q_f32(input.as_ptr().add(c * 4));
vsum = vaddq_f32(vsum, x);
}
let mut sum = vaddvq_f32(vsum);
for i in (chunks * 4)..h {
sum += input[i];
}
let mean = sum * inv_hf;
let vmean = vdupq_n_f32(mean);
let mut vdev = vdupq_n_f32(0.0);
for c in 0..chunks {
let d = vsubq_f32(vld1q_f32(input.as_ptr().add(c * 4)), vmean);
vdev = vfmaq_f32(vdev, d, d);
}
let mut dev = vaddvq_f32(vdev);
for i in (chunks * 4)..h {
let d = input[i] - mean;
dev += d * d;
}
let var = (dev * inv_hf).max(0.0);
let inv = 1.0 / (var + eps).sqrt();
let vinv = vdupq_n_f32(inv);
for c in 0..chunks {
let off = c * 4;
let x = vld1q_f32(input.as_ptr().add(off));
let norm = vmulq_f32(vsubq_f32(x, vmean), vinv);
vst1q_f32(
output.as_mut_ptr().add(off),
vfmaq_f32(
vld1q_f32(beta.as_ptr().add(off)),
norm,
vld1q_f32(gamma.as_ptr().add(off)),
),
);
}
for i in (chunks * 4)..h {
output[i] = (input[i] - mean) * inv * gamma[i] + beta[i];
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2", enable = "fma")]
unsafe fn layer_norm_row_avx2(
input: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
h: usize,
eps: f32,
) {
use std::arch::x86_64::*;
let inv_hf = 1.0 / h as f32;
let chunks = h / 8;
unsafe {
let mut vsum = _mm256_setzero_ps();
for c in 0..chunks {
vsum = _mm256_add_ps(vsum, _mm256_loadu_ps(input.as_ptr().add(c * 8)));
}
let hsum = {
let lo = _mm256_castps256_ps128(vsum);
let hi = _mm256_extractf128_ps::<1>(vsum);
let s4 = _mm_add_ps(lo, hi);
let s2 = _mm_add_ps(s4, _mm_movehl_ps(s4, s4));
let s1 = _mm_add_ss(s2, _mm_shuffle_ps::<0x55>(s2, s2));
_mm_cvtss_f32(s1)
};
let mut sum = hsum;
for i in (chunks * 8)..h {
sum += input[i];
}
let mean = sum * inv_hf;
let vmean = _mm256_set1_ps(mean);
let mut vdev = _mm256_setzero_ps();
for c in 0..chunks {
let d = _mm256_sub_ps(_mm256_loadu_ps(input.as_ptr().add(c * 8)), vmean);
vdev = _mm256_fmadd_ps(d, d, vdev);
}
let hdev = {
let lo = _mm256_castps256_ps128(vdev);
let hi = _mm256_extractf128_ps::<1>(vdev);
let s4 = _mm_add_ps(lo, hi);
let s2 = _mm_add_ps(s4, _mm_movehl_ps(s4, s4));
let s1 = _mm_add_ss(s2, _mm_shuffle_ps::<0x55>(s2, s2));
_mm_cvtss_f32(s1)
};
let mut dev = hdev;
for i in (chunks * 8)..h {
let d = input[i] - mean;
dev += d * d;
}
let var = (dev * inv_hf).max(0.0);
let inv = 1.0 / (var + eps).sqrt();
let vinv = _mm256_set1_ps(inv);
for c in 0..chunks {
let off = c * 8;
let x = _mm256_loadu_ps(input.as_ptr().add(off));
let norm = _mm256_mul_ps(_mm256_sub_ps(x, vmean), vinv);
let g = _mm256_loadu_ps(gamma.as_ptr().add(off));
let b = _mm256_loadu_ps(beta.as_ptr().add(off));
_mm256_storeu_ps(output.as_mut_ptr().add(off), _mm256_fmadd_ps(norm, g, b));
}
for i in (chunks * 8)..h {
output[i] = (input[i] - mean) * inv * gamma[i] + beta[i];
}
}
}
#[cfg(not(target_arch = "aarch64"))]
fn layer_norm_row_scalar(
input: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
h: usize,
eps: f32,
) {
let inv_hf = 1.0 / h as f32;
let mut sum = 0f32;
for i in 0..h {
sum += input[i];
}
let mean = sum * inv_hf;
let mut dev = 0f32;
for i in 0..h {
let d = input[i] - mean;
dev += d * d;
}
let var = (dev * inv_hf).max(0.0);
let inv = 1.0 / (var + eps).sqrt();
for i in 0..h {
output[i] = (input[i] - mean) * inv * gamma[i] + beta[i];
}
}
#[cfg(target_arch = "x86_64")]
pub fn layer_norm_row(
input: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
h: usize,
eps: f32,
) {
if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") {
unsafe { layer_norm_row_avx2(input, gamma, beta, output, h, eps) };
return;
}
layer_norm_row_scalar(input, gamma, beta, output, h, eps);
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
pub fn layer_norm_row(
input: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
h: usize,
eps: f32,
) {
layer_norm_row_scalar(input, gamma, beta, output, h, eps);
}
pub fn layer_norm_rows(
input: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
rows: usize,
h: usize,
eps: f32,
) {
debug_assert!(input.len() >= rows * h && output.len() >= rows * h);
#[cfg(target_arch = "x86_64")]
{
if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma")
{
for r in 0..rows {
unsafe {
layer_norm_row_avx2(
&input[r * h..(r + 1) * h],
gamma,
beta,
&mut output[r * h..(r + 1) * h],
h,
eps,
)
};
}
return;
}
for r in 0..rows {
layer_norm_row_scalar(
&input[r * h..(r + 1) * h],
gamma,
beta,
&mut output[r * h..(r + 1) * h],
h,
eps,
);
}
}
#[cfg(not(target_arch = "x86_64"))]
for r in 0..rows {
layer_norm_row(
&input[r * h..(r + 1) * h],
gamma,
beta,
&mut output[r * h..(r + 1) * h],
h,
eps,
);
}
}
pub fn batch_norm_inference(
x: &[f32],
gamma: &[f32],
beta: &[f32],
mean: &[f32],
var: &[f32],
out: &mut [f32],
channels: usize,
eps: f32,
) {
let n = x.len() / channels.max(1);
for i in 0..n {
for c in 0..channels {
let idx = i * channels + c;
let inv = 1.0 / (var[c] + eps).sqrt();
let xhat = (x[idx] - mean[c]) * inv;
out[idx] = gamma[c] * xhat + beta[c];
}
}
}
pub fn batch_norm_inference_backward_input(
x: &[f32],
gamma: &[f32],
_mean: &[f32],
var: &[f32],
dy: &[f32],
dx: &mut [f32],
channels: usize,
eps: f32,
) {
let n = x.len() / channels.max(1);
for i in 0..n {
for c in 0..channels {
let idx = i * channels + c;
let inv = 1.0 / (var[c] + eps).sqrt();
dx[idx] = dy[idx] * gamma[c] * inv;
}
}
}
pub fn batch_norm_inference_backward_gamma(
x: &[f32],
mean: &[f32],
var: &[f32],
dy: &[f32],
dgamma: &mut [f32],
channels: usize,
eps: f32,
) {
dgamma.fill(0.0);
let n = x.len() / channels.max(1);
for i in 0..n {
for c in 0..channels {
let idx = i * channels + c;
let inv = 1.0 / (var[c] + eps).sqrt();
let xhat = (x[idx] - mean[c]) * inv;
dgamma[c] += dy[idx] * xhat;
}
}
}
pub fn batch_norm_inference_backward_beta(dy: &[f32], dbeta: &mut [f32], channels: usize) {
dbeta.fill(0.0);
let n = dy.len() / channels.max(1);
for i in 0..n {
for c in 0..channels {
dbeta[c] += dy[i * channels + c];
}
}
}
pub fn residual_bias_layer_norm(
a: &[f32],
b: &[f32],
bias: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
n: usize,
h: usize,
eps: f32,
) {
let mut tmp = vec![0f32; h];
for row in 0..n {
let base = row * h;
for i in 0..h {
tmp[i] = a[base + i] + b[base + i] + bias[i];
}
layer_norm_row(&tmp, gamma, beta, &mut output[base..base + h], h, eps);
}
}
pub fn residual_bias_rms_norm(
a: &[f32],
b: &[f32],
bias: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
n: usize,
h: usize,
eps: f32,
) {
let inv_h = 1.0 / h as f32;
for row in 0..n {
let base = row * h;
let mut sumsq = 0f32;
for i in 0..h {
let v = a[base + i] + b[base + i] + bias[i];
sumsq += v * v;
}
let inv_rms = (sumsq * inv_h + eps).sqrt().recip();
for i in 0..h {
let v = a[base + i] + b[base + i] + bias[i];
output[base + i] = v * inv_rms * gamma[i] + beta[i];
}
}
}
pub fn par_residual_bias_ln(
a: &[f32],
b: &[f32],
bias: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
n: usize,
h: usize,
eps: f32,
) {
let cfg = crate::config::RuntimeConfig::global();
if n * h < cfg.par_threshold || n < cfg.min_rows_per_thread {
residual_bias_layer_norm(a, b, bias, gamma, beta, output, n, h, eps);
return;
}
let a_ptr = a.as_ptr() as usize;
let b_ptr = b.as_ptr() as usize;
let o_ptr = output.as_mut_ptr() as usize;
let bias_ptr = bias.as_ptr() as usize;
let gamma_ptr = gamma.as_ptr() as usize;
let beta_ptr = beta.as_ptr() as usize;
pool::par_for(n, cfg.min_rows_per_thread, &|off, cnt| unsafe {
let a_s = std::slice::from_raw_parts((a_ptr as *const f32).add(off * h), cnt * h);
let b_s = std::slice::from_raw_parts((b_ptr as *const f32).add(off * h), cnt * h);
let o_s = std::slice::from_raw_parts_mut((o_ptr as *mut f32).add(off * h), cnt * h);
let bi = std::slice::from_raw_parts(bias_ptr as *const f32, h);
let g = std::slice::from_raw_parts(gamma_ptr as *const f32, h);
let be = std::slice::from_raw_parts(beta_ptr as *const f32, h);
residual_bias_layer_norm(a_s, b_s, bi, g, be, o_s, cnt, h, eps);
});
}
#[inline]
fn par_softmax_rows<F: Fn(&mut [f32], usize, usize) + Sync>(
data: &mut [f32],
rows: usize,
cols: usize,
kernel: &F,
) {
if rows >= 4 && pool::should_parallelize(rows * cols) {
let base = data.as_mut_ptr() as usize;
pool::par_for(rows, 1, &|off, cnt| {
for r in off..off + cnt {
let row = unsafe {
std::slice::from_raw_parts_mut((base as *mut f32).add(r * cols), cols)
};
kernel(row, 1, cols);
}
});
} else {
kernel(data, rows, cols);
}
}
#[cfg(target_arch = "aarch64")]
fn softmax_rows_neon(data: &mut [f32], rows: usize, cols: usize) {
use std::arch::aarch64::*;
let chunks = cols / 4;
unsafe {
for row in 0..rows {
let base = row * cols;
let ptr = data.as_mut_ptr().add(base);
let mut vmax = vdupq_n_f32(f32::NEG_INFINITY);
for c in 0..chunks {
vmax = vmaxq_f32(vmax, vld1q_f32(ptr.add(c * 4)));
}
let mut max_val = vmaxvq_f32(vmax);
for i in (chunks * 4)..cols {
max_val = max_val.max(*ptr.add(i));
}
let vmx = vdupq_n_f32(max_val);
let mut vsum = vdupq_n_f32(0.0);
for c in 0..chunks {
let off = c * 4;
let e = neon_exp4(vsubq_f32(vld1q_f32(ptr.add(off)), vmx));
vst1q_f32(ptr.add(off), e);
vsum = vaddq_f32(vsum, e);
}
let mut sum = vaddvq_f32(vsum);
for i in (chunks * 4)..cols {
let e = (*ptr.add(i) - max_val).exp();
*ptr.add(i) = e;
sum += e;
}
let vinv = vdupq_n_f32(1.0 / sum);
for c in 0..chunks {
let off = c * 4;
vst1q_f32(ptr.add(off), vmulq_f32(vld1q_f32(ptr.add(off)), vinv));
}
let inv = 1.0 / sum;
for i in (chunks * 4)..cols {
*ptr.add(i) *= inv;
}
}
}
}
#[cfg(target_arch = "aarch64")]
pub fn neon_softmax(data: &mut [f32], rows: usize, cols: usize) {
par_softmax_rows(data, rows, cols, &softmax_rows_neon);
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2", enable = "fma")]
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn softmax_rows_avx2(data: &mut [f32], rows: usize, cols: usize) {
use std::arch::x86_64::*;
let chunks = cols / 8;
for r in 0..rows {
let row = data.as_mut_ptr().add(r * cols);
let mut vmax = _mm256_set1_ps(f32::NEG_INFINITY);
for c in 0..chunks {
vmax = _mm256_max_ps(vmax, _mm256_loadu_ps(row.add(c * 8)));
}
let mut max_v = {
let lo = _mm256_castps256_ps128(vmax);
let hi = _mm256_extractf128_ps::<1>(vmax);
let s4 = _mm_max_ps(lo, hi);
let s2 = _mm_max_ps(s4, _mm_movehl_ps(s4, s4));
let s1 = _mm_max_ss(s2, _mm_shuffle_ps::<0x55>(s2, s2));
_mm_cvtss_f32(s1)
};
for i in (chunks * 8)..cols {
let v = *row.add(i);
if v > max_v {
max_v = v;
}
}
let vmax = _mm256_set1_ps(max_v);
let mut vsum = _mm256_setzero_ps();
for c in 0..chunks {
let off = c * 8;
let e = avx2_exp8(_mm256_sub_ps(_mm256_loadu_ps(row.add(off)), vmax));
_mm256_storeu_ps(row.add(off), e);
vsum = _mm256_add_ps(vsum, e);
}
let mut sum_v = {
let lo = _mm256_castps256_ps128(vsum);
let hi = _mm256_extractf128_ps::<1>(vsum);
let s4 = _mm_add_ps(lo, hi);
let s2 = _mm_add_ps(s4, _mm_movehl_ps(s4, s4));
let s1 = _mm_add_ss(s2, _mm_shuffle_ps::<0x55>(s2, s2));
_mm_cvtss_f32(s1)
};
for i in (chunks * 8)..cols {
let v = (*row.add(i) - max_v).exp();
*row.add(i) = v;
sum_v += v;
}
let vinv = _mm256_set1_ps(1.0 / sum_v);
for c in 0..chunks {
let off = c * 8;
_mm256_storeu_ps(
row.add(off),
_mm256_mul_ps(_mm256_loadu_ps(row.add(off)), vinv),
);
}
let inv_sum = 1.0 / sum_v;
for i in (chunks * 8)..cols {
*row.add(i) *= inv_sum;
}
}
}
#[cfg(target_arch = "x86_64")]
pub fn neon_softmax(data: &mut [f32], rows: usize, cols: usize) {
let avx2 =
std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma");
if avx2 {
par_softmax_rows(data, rows, cols, &|d, r, c| unsafe {
softmax_rows_avx2(d, r, c);
});
} else {
par_softmax_rows(data, rows, cols, &softmax_rows_poly);
}
}
pub fn softmax_rows_poly(data: &mut [f32], rows: usize, cols: usize) {
for r in 0..rows {
softmax_row_poly(&mut data[r * cols..(r + 1) * cols]);
}
}
#[inline]
fn reduce_max4(row: &[f32]) -> f32 {
let mut acc = [f32::NEG_INFINITY; 4];
let mut it = row.chunks_exact(4);
for ch in &mut it {
for i in 0..4 {
acc[i] = acc[i].max(ch[i]);
}
}
let mut m = acc[0].max(acc[1]).max(acc[2].max(acc[3]));
for &v in it.remainder() {
m = m.max(v);
}
m
}
#[inline]
fn reduce_sum4(row: &[f32]) -> f32 {
let mut acc = [0f32; 4];
let mut it = row.chunks_exact(4);
for ch in &mut it {
for i in 0..4 {
acc[i] += ch[i];
}
}
let mut s = (acc[0] + acc[1]) + (acc[2] + acc[3]);
for &v in it.remainder() {
s += v;
}
s
}
#[cfg_attr(target_arch = "aarch64", allow(dead_code))]
fn softmax_row_poly(row: &mut [f32]) {
let max_v = reduce_max4(row);
if !max_v.is_finite() {
let n = row.len();
crate::naive::softmax(row, 1, n);
return;
}
for v in row.iter_mut() {
*v = crate::vmath::exp_poly(*v - max_v);
}
let sum = reduce_sum4(row);
let inv = if sum > 0.0 { 1.0 / sum } else { 0.0 };
for v in row.iter_mut() {
*v *= inv;
}
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
pub fn neon_softmax(data: &mut [f32], rows: usize, cols: usize) {
par_softmax_rows(data, rows, cols, &softmax_rows_poly);
}
#[cfg(target_arch = "aarch64")]
pub fn gelu_inplace(data: &mut [f32]) {
use std::arch::aarch64::*;
let len = data.len();
let chunks = len / 4;
unsafe {
let half = vdupq_n_f32(0.5);
let one = vdupq_n_f32(1.0);
let inv_sqrt2 = vdupq_n_f32(std::f32::consts::FRAC_1_SQRT_2);
let p = vdupq_n_f32(0.3275911);
let a1 = vdupq_n_f32(0.254_829_6);
let a2 = vdupq_n_f32(-0.284_496_72);
let a3 = vdupq_n_f32(1.421_413_8);
let a4 = vdupq_n_f32(-1.453_152_1);
let a5 = vdupq_n_f32(1.061_405_4);
let neg_one = vdupq_n_f32(-1.0);
let zero = vdupq_n_f32(0.0);
for c in 0..chunks {
let ptr = data.as_mut_ptr().add(c * 4);
let x = vld1q_f32(ptr);
let erf_arg = vmulq_f32(x, inv_sqrt2);
let xa = vabsq_f32(erf_arg);
let sign = vbslq_f32(vcgeq_f32(erf_arg, zero), one, neg_one);
let denom = vfmaq_f32(one, p, xa);
let t = vdivq_f32(one, denom);
let mut y = a5;
y = vfmaq_f32(a4, y, t);
y = vfmaq_f32(a3, y, t);
y = vfmaq_f32(a2, y, t);
y = vfmaq_f32(a1, y, t);
y = vmulq_f32(y, t);
let exp_val = neon_exp4(vnegq_f32(vmulq_f32(xa, xa)));
let erf_val = vmulq_f32(sign, vfmsq_f32(one, y, exp_val));
vst1q_f32(ptr, vmulq_f32(x, vmulq_f32(half, vaddq_f32(one, erf_val))));
}
for i in (chunks * 4)..len {
data[i] = scalar_gelu(data[i]);
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2", enable = "fma")]
#[allow(unsafe_op_in_unsafe_fn, clippy::excessive_precision)]
unsafe fn gelu_inplace_avx2(data: &mut [f32]) {
use std::arch::x86_64::*;
let chunks = data.len() / 8;
let half = _mm256_set1_ps(0.5);
let one = _mm256_set1_ps(1.0);
let inv_sqrt2 = _mm256_set1_ps(std::f32::consts::FRAC_1_SQRT_2);
let p = _mm256_set1_ps(0.3275911);
let a1 = _mm256_set1_ps(0.254829592);
let a2 = _mm256_set1_ps(-0.284496736);
let a3 = _mm256_set1_ps(1.421413741);
let a4 = _mm256_set1_ps(-1.453152027);
let a5 = _mm256_set1_ps(1.061405429);
let neg_one = _mm256_set1_ps(-1.0);
let zero = _mm256_set1_ps(0.0);
let abs_mask = _mm256_castsi256_ps(_mm256_set1_epi32(0x7fff_ffff));
for c in 0..chunks {
let off = c * 8;
let ptr = data.as_mut_ptr().add(off);
let x = _mm256_loadu_ps(ptr);
let erf_arg = _mm256_mul_ps(x, inv_sqrt2);
let xa = _mm256_and_ps(erf_arg, abs_mask);
let ge0 = _mm256_cmp_ps::<_CMP_GE_OQ>(erf_arg, zero);
let sign = _mm256_blendv_ps(neg_one, one, ge0);
let denom = _mm256_fmadd_ps(p, xa, one);
let t = _mm256_div_ps(one, denom);
let mut y = a5;
y = _mm256_fmadd_ps(y, t, a4);
y = _mm256_fmadd_ps(y, t, a3);
y = _mm256_fmadd_ps(y, t, a2);
y = _mm256_fmadd_ps(y, t, a1);
y = _mm256_mul_ps(y, t);
let exp_val = avx2_exp8(_mm256_sub_ps(zero, _mm256_mul_ps(xa, xa)));
let erf_val = _mm256_mul_ps(sign, _mm256_fnmadd_ps(y, exp_val, one));
_mm256_storeu_ps(
ptr,
_mm256_mul_ps(x, _mm256_mul_ps(half, _mm256_add_ps(one, erf_val))),
);
}
for i in (chunks * 8)..data.len() {
data[i] = scalar_gelu(data[i]);
}
}
#[cfg(target_arch = "x86_64")]
pub fn gelu_inplace(data: &mut [f32]) {
if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") {
unsafe { gelu_inplace_avx2(data) };
return;
}
for v in data.iter_mut() {
*v = scalar_gelu(*v);
}
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
pub fn gelu_inplace(data: &mut [f32]) {
for v in data.iter_mut() {
*v = scalar_gelu(*v);
}
}
const ACTIVATION_PAR_MIN: usize = 1 << 20;
#[inline]
pub fn scalar_gelu_approx(x: f32) -> f32 {
const C: f32 = 0.797_884_6; const A: f32 = 0.044_715;
0.5 * x * (1.0 + (C * (x + A * x * x * x)).tanh())
}
pub fn gelu_approx_inplace(data: &mut [f32]) {
for v in data.iter_mut() {
*v = scalar_gelu_approx(*v);
}
}
pub fn par_gelu_approx_inplace(data: &mut [f32]) {
let len = data.len();
if len < ACTIVATION_PAR_MIN {
gelu_approx_inplace(data);
return;
}
let cfg = crate::config::RuntimeConfig::global();
let chunk = 512;
let rows = len / chunk;
if rows < 2 {
gelu_approx_inplace(data);
return;
}
let data_ptr = data.as_mut_ptr() as usize;
pool::par_for(rows, cfg.min_rows_per_thread, &|off, cnt| unsafe {
let start = off * chunk;
let end = if off + cnt >= rows {
len
} else {
(off + cnt) * chunk
};
let s = std::slice::from_raw_parts_mut((data_ptr as *mut f32).add(start), end - start);
gelu_approx_inplace(s);
});
}
pub fn gelu_approx_out(src: &[f32], dst: &mut [f32]) {
debug_assert_eq!(src.len(), dst.len());
for (s, d) in src.iter().zip(dst.iter_mut()) {
*d = scalar_gelu_approx(*s);
}
}
pub fn par_gelu_approx_out(src: &[f32], dst: &mut [f32]) {
debug_assert_eq!(src.len(), dst.len());
let len = src.len();
if len < ACTIVATION_PAR_MIN {
gelu_approx_out(src, dst);
return;
}
let cfg = crate::config::RuntimeConfig::global();
let chunk = 512;
let rows = len / chunk;
if rows < 2 {
gelu_approx_out(src, dst);
return;
}
let src_ptr = src.as_ptr() as usize;
let dst_ptr = dst.as_mut_ptr() as usize;
pool::par_for(rows, cfg.min_rows_per_thread, &|off, cnt| unsafe {
let start = off * chunk;
let end = if off + cnt >= rows {
len
} else {
(off + cnt) * chunk
};
let n = end - start;
let s = std::slice::from_raw_parts((src_ptr as *const f32).add(start), n);
let d = std::slice::from_raw_parts_mut((dst_ptr as *mut f32).add(start), n);
gelu_approx_out(s, d);
});
}
pub fn par_gelu_inplace(data: &mut [f32]) {
let len = data.len();
if len < ACTIVATION_PAR_MIN {
gelu_inplace(data);
return;
}
let cfg = crate::config::RuntimeConfig::global();
let chunk = 512;
let rows = len / chunk;
if rows < 2 {
gelu_inplace(data);
return;
}
let data_ptr = data.as_mut_ptr() as usize;
pool::par_for(rows, cfg.min_rows_per_thread, &|off, cnt| unsafe {
let start = off * chunk;
let end = if off + cnt >= rows {
len
} else {
(off + cnt) * chunk
};
let s = std::slice::from_raw_parts_mut((data_ptr as *mut f32).add(start), end - start);
gelu_inplace(s);
});
}
pub fn par_silu_inplace(data: &mut [f32]) {
let len = data.len();
if len < ACTIVATION_PAR_MIN {
silu_inplace(data);
return;
}
let cfg = crate::config::RuntimeConfig::global();
let chunk = 512;
let rows = len / chunk;
if rows < 2 {
silu_inplace(data);
return;
}
let data_ptr = data.as_mut_ptr() as usize;
pool::par_for(rows, cfg.min_rows_per_thread, &|off, cnt| unsafe {
let start = off * chunk;
let end = if off + cnt >= rows {
len
} else {
(off + cnt) * chunk
};
let s = std::slice::from_raw_parts_mut((data_ptr as *mut f32).add(start), end - start);
silu_inplace(s);
});
}
#[cfg(target_arch = "aarch64")]
pub fn neon_sgemm_small(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
use std::arch::aarch64::*;
let n4 = n / 4;
unsafe {
for j4 in 0..n4 {
let j = j4 * 4;
let mut acc = [vdupq_n_f32(0.0); 8];
for kk in 0..k {
let bv = vld1q_f32(b.as_ptr().add(kk * n + j));
for i in 0..m {
let av = vdupq_n_f32(*a.as_ptr().add(i * k + kk));
acc[i] = vfmaq_f32(acc[i], av, bv);
}
}
for i in 0..m {
vst1q_f32(c.as_mut_ptr().add(i * n + j), acc[i]);
}
}
for j in (n4 * 4)..n {
for i in 0..m {
let mut sum = 0f32;
for kk in 0..k {
sum += a[i * k + kk] * b[kk * n + j];
}
c[i * n + j] = sum;
}
}
}
}
#[cfg(not(target_arch = "aarch64"))]
pub fn neon_sgemm_small(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
crate::naive::matmul(a, b, c, m, k, n);
}
#[cfg(target_arch = "aarch64")]
pub fn neon_sgemm_bias_small(
a: &[f32],
b: &[f32],
bias: &[f32],
c: &mut [f32],
m: usize,
k: usize,
n: usize,
) {
neon_sgemm_small(a, b, c, m, k, n);
crate::blas::bias_add(c, bias, m, n);
}
#[cfg(not(target_arch = "aarch64"))]
pub fn neon_sgemm_bias_small(
a: &[f32],
b: &[f32],
bias: &[f32],
c: &mut [f32],
m: usize,
k: usize,
n: usize,
) {
crate::naive::matmul(a, b, c, m, k, n);
crate::naive::bias_add(c, bias, m, n);
}
fn scalar_gelu(x: f32) -> f32 {
x * 0.5 * (1.0 + scalar_erf(x * std::f32::consts::FRAC_1_SQRT_2))
}
fn scalar_erf(x: f32) -> f32 {
let sign = if x >= 0.0 { 1.0f32 } else { -1.0 };
let xa = x.abs();
let t = 1.0 / (1.0 + 0.3275911 * xa);
let y = t
* (0.254_829_6
+ t * (-0.284_496_72 + t * (1.421_413_8 + t * (-1.453_152_1 + t * 1.061_405_4))));
sign * (1.0 - y * crate::vmath::exp_poly(-xa * xa))
}
pub fn layer_norm2d_nchw(
input: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
batch: usize,
channels: usize,
h: usize,
w: usize,
eps: f32,
) {
let spatial = h * w;
for b in 0..batch {
for i in 0..spatial {
let mut mean = 0.0f32;
for c in 0..channels {
mean += input[((b * channels + c) * spatial) + i];
}
mean /= channels as f32;
let mut var = 0.0f32;
for c in 0..channels {
let d = input[((b * channels + c) * spatial) + i] - mean;
var += d * d;
}
var /= channels as f32;
let inv = 1.0 / (var + eps).sqrt();
for c in 0..channels {
let v = (input[((b * channels + c) * spatial) + i] - mean) * inv;
output[((b * channels + c) * spatial) + i] = v * gamma[c] + beta[c];
}
}
}
}
pub fn conv_transpose2d_nchw(
input: &[f32],
weight: &[f32],
output: &mut [f32],
n: usize,
c_in: usize,
h: usize,
w: usize,
c_out: usize,
h_out: usize,
w_out: usize,
kh: usize,
kw: usize,
sh: usize,
sw: usize,
ph: usize,
pw: usize,
dh: usize,
dw: usize,
groups: usize,
) {
output.fill(0.0);
if groups == 0 || c_in < groups {
return;
}
let c_in_per_g = c_in / groups;
let c_out_per_g = c_out / groups;
for ni in 0..n {
for ic in 0..c_in {
let g = ic / c_in_per_g;
let _ic_off = ic % c_in_per_g;
for iy in 0..h {
for ix in 0..w {
let v = input[((ni * c_in + ic) * h + iy) * w + ix];
if v == 0.0 {
continue;
}
for ky in 0..kh {
let oy = iy * sh + ky * dh;
if oy < ph || oy >= h_out + ph {
continue;
}
let oy = oy - ph;
if oy >= h_out {
continue;
}
for kx in 0..kw {
let ox = ix * sw + kx * dw;
if ox < pw || ox >= w_out + pw {
continue;
}
let ox = ox - pw;
if ox >= w_out {
continue;
}
for oc_off in 0..c_out_per_g {
let oc = g * c_out_per_g + oc_off;
let w_idx = ((ic * c_out_per_g + oc_off) * kh + ky) * kw + kx;
let wt = weight[w_idx];
output[((ni * c_out + oc) * h_out + oy) * w_out + ox] += v * wt;
}
}
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn conv_transpose3d_ncdhw(
input: &[f32],
weight: &[f32],
output: &mut [f32],
n: usize,
c_in: usize,
d: usize,
h: usize,
w: usize,
c_out: usize,
d_out: usize,
h_out: usize,
w_out: usize,
kd: usize,
kh: usize,
kw: usize,
sd: usize,
sh: usize,
sw: usize,
pd: usize,
ph: usize,
pw: usize,
dd: usize,
dh: usize,
dw: usize,
groups: usize,
) {
output.fill(0.0);
if groups == 0 || c_in < groups {
return;
}
let c_in_per_g = c_in / groups;
let c_out_per_g = c_out / groups;
for ni in 0..n {
for ic in 0..c_in {
let g = ic / c_in_per_g;
for id in 0..d {
for iy in 0..h {
for ix in 0..w {
let v = input[(((ni * c_in + ic) * d + id) * h + iy) * w + ix];
if v == 0.0 {
continue;
}
for kz in 0..kd {
let oz = id * sd + kz * dd;
if oz < pd || oz >= d_out + pd {
continue;
}
let oz = oz - pd;
if oz >= d_out {
continue;
}
for ky in 0..kh {
let oy = iy * sh + ky * dh;
if oy < ph || oy >= h_out + ph {
continue;
}
let oy = oy - ph;
if oy >= h_out {
continue;
}
for kx in 0..kw {
let ox = ix * sw + kx * dw;
if ox < pw || ox >= w_out + pw {
continue;
}
let ox = ox - pw;
if ox >= w_out {
continue;
}
for oc_off in 0..c_out_per_g {
let oc = g * c_out_per_g + oc_off;
let w_idx = (((ic * c_out_per_g + oc_off) * kd + kz) * kh
+ ky)
* kw
+ kx;
let wt = weight[w_idx];
output[(((ni * c_out + oc) * d_out + oz) * h_out + oy)
* w_out
+ ox] += v * wt;
}
}
}
}
}
}
}
}
}
}
pub fn group_norm_nchw(
input: &[f32],
gamma: &[f32],
beta: &[f32],
output: &mut [f32],
batch: usize,
channels: usize,
h: usize,
w: usize,
num_groups: usize,
eps: f32,
) {
let cpg = channels / num_groups;
let spatial = h * w;
let n = (cpg * spatial) as f32;
for b in 0..batch {
for g in 0..num_groups {
let c0 = g * cpg;
let mut mean = 0.0f32;
for c in 0..cpg {
let plane = &input
[((b * channels + c0 + c) * spatial)..((b * channels + c0 + c + 1) * spatial)];
mean += plane.iter().sum::<f32>();
}
mean /= n;
let mut var = 0.0f32;
for c in 0..cpg {
let plane = &input
[((b * channels + c0 + c) * spatial)..((b * channels + c0 + c + 1) * spatial)];
for &v in plane {
let d = v - mean;
var += d * d;
}
}
var /= n;
let inv = 1.0 / (var + eps).sqrt();
for c in 0..cpg {
let gi = c0 + c;
let gamm = gamma[gi];
let bet = beta[gi];
let src =
&input[((b * channels + gi) * spatial)..((b * channels + gi + 1) * spatial)];
let dst = &mut output
[((b * channels + gi) * spatial)..((b * channels + gi + 1) * spatial)];
for (d, &s) in dst.iter_mut().zip(src) {
*d = (s - mean) * inv * gamm + bet;
}
}
}
}
}
pub fn resize_nearest_2x_nchw(
input: &[f32],
output: &mut [f32],
channels: usize,
h: usize,
w: usize,
) {
let h2 = h * 2;
let w2 = w * 2;
for c in 0..channels {
let plane = &input[c * h * w..(c + 1) * h * w];
let dst = &mut output[c * h2 * w2..(c + 1) * h2 * w2];
for y in 0..h {
for x in 0..w {
let v = plane[y * w + x];
for dy in 0..2 {
for dx in 0..2 {
dst[(y * 2 + dy) * w2 + (x * 2 + dx)] = v;
}
}
}
}
}
}
pub fn interpolate3d_ncdhw(
input: &[f32],
output: &mut [f32],
n: usize,
c: usize,
d_in: usize,
h_in: usize,
w_in: usize,
d_out: usize,
h_out: usize,
w_out: usize,
) {
for bn in 0..n {
for ch in 0..c {
let in_base = (bn * c + ch) * d_in * h_in * w_in;
let out_base = (bn * c + ch) * d_out * h_out * w_out;
for do_ in 0..d_out {
let di = ((do_ * d_in) / d_out).min(d_in.saturating_sub(1));
for ho in 0..h_out {
let hi = ((ho * h_in) / h_out).min(h_in.saturating_sub(1));
for wo in 0..w_out {
let wi = ((wo * w_in) / w_out).min(w_in.saturating_sub(1));
let src = in_base + (di * h_in + hi) * w_in + wi;
let dst = out_base + (do_ * h_out + ho) * w_out + wo;
output[dst] = input[src];
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gelu_correctness() {
let x = 1.5f32;
let g = scalar_gelu(x);
assert!((g - 1.3990).abs() < 0.01, "gelu(1.5) = {g}");
}
#[test]
#[cfg(target_arch = "x86_64")]
fn x86_dispatch_arms_agree() {
if !(std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("fma"))
{
return;
}
let (m, n) = (3usize, 64usize);
let data: Vec<f32> = (0..m * n).map(|i| i as f32 * 0.37 - 35.0).collect();
let bias: Vec<f32> = (0..n).map(|j| j as f32 * 0.11 - 3.5).collect();
let mut simd = data.clone();
let mut scalar = data.clone();
unsafe { bias_gelu_avx2(&mut simd, &bias, m, n) };
bias_gelu_scalar(&mut scalar, &bias, m, n);
let worst = simd
.iter()
.zip(&scalar)
.map(|(a, b)| (a - b).abs())
.fold(0f32, f32::max);
assert!(worst < 2e-6, "bias_gelu arms diverge by {worst}");
let h = 128usize;
let input: Vec<f32> = (0..h)
.map(|i| ((i * 37) % 19) as f32 * 0.25 - 2.0)
.collect();
let gamma: Vec<f32> = (0..h).map(|i| 1.0 + i as f32 * 0.01).collect();
let beta: Vec<f32> = (0..h).map(|i| i as f32 * -0.02).collect();
let mut simd = vec![0f32; h];
let mut scalar = vec![0f32; h];
unsafe { layer_norm_row_avx2(&input, &gamma, &beta, &mut simd, h, 1e-5) };
layer_norm_row_scalar(&input, &gamma, &beta, &mut scalar, h, 1e-5);
let worst = simd
.iter()
.zip(&scalar)
.map(|(a, b)| (a - b).abs())
.fold(0f32, f32::max);
assert!(worst < 4e-6, "layer_norm_row arms diverge by {worst}");
}
#[test]
fn softmax_poly_arm_matches_the_reference() {
let cases: Vec<Vec<f32>> = vec![
(0..64)
.map(|i| ((i * 37) % 23) as f32 * 0.3 - 3.0)
.collect(),
(0..48).map(|i| i as f32 * 4.0 - 90.0).collect(),
vec![1.0; 33],
{
let mut v = vec![-50.0f32; 17];
v[9] = 50.0;
v
},
vec![f32::NEG_INFINITY; 8],
];
for (ci, src) in cases.iter().enumerate() {
let cols = src.len();
let mut poly = src.clone();
let mut want = src.clone();
softmax_rows_poly(&mut poly, 1, cols);
crate::naive::softmax(&mut want, 1, cols);
let worst = poly
.iter()
.zip(&want)
.map(|(a, b)| (a - b).abs())
.fold(0f32, f32::max);
assert!(worst < 1e-6, "case {ci}: softmax poly arm off by {worst:e}");
if src.iter().any(|v| v.is_finite()) {
let sum: f32 = poly.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "case {ci}: rows sum to {sum}");
}
}
}
#[test]
fn swiglu_rows_matches_a_libm_reference() {
for &gate_first in &[true, false] {
let (outer, n) = (5usize, 37usize);
let inp: Vec<f32> = (0..outer * 2 * n)
.map(|i| ((i * 29) % 61) as f32 * 0.4 - 12.0)
.collect();
let mut got = vec![0f32; outer * n];
swiglu_rows(&inp, &mut got, outer, n, gate_first);
let mut want = vec![0f32; outer * n];
for o in 0..outer {
for i in 0..n {
let row = &inp[o * 2 * n..(o + 1) * 2 * n];
let (up, gate) = if gate_first {
(row[n + i], row[i])
} else {
(row[i], row[n + i])
};
want[o * n + i] = up * (gate / (1.0 + (-gate).exp()));
}
}
let worst = got
.iter()
.zip(&want)
.map(|(a, b)| (a - b).abs())
.fold(0f32, f32::max);
println!("swiglu gate_first={gate_first} worst abs diff {worst:e}");
assert!(
worst < 1e-5,
"swiglu off by {worst:e} (gate_first={gate_first})"
);
}
}
#[test]
fn bias_gelu_works() {
let mut data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let bias = vec![0.1, 0.2, 0.3, 0.4];
bias_gelu(&mut data, &bias, 2, 4);
for &v in &data {
assert!(v > 0.0, "bias_gelu produced {v}");
}
}
#[test]
fn batch_norm_inference_roundtrip() {
let c = 4usize;
let x: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let gamma = vec![1.0; c];
let beta = vec![0.0; c];
let mean = vec![2.5, 2.5, 2.5, 2.5];
let var = vec![1.0; c];
let mut y = vec![0.0; 8];
batch_norm_inference(&x, &gamma, &beta, &mean, &var, &mut y, c, 1e-5);
let mut dx = vec![0.0; 8];
let dy = vec![1.0; 8];
let mut dgamma = vec![0.0; c];
let mut dbeta = vec![0.0; c];
batch_norm_inference_backward_input(&x, &gamma, &mean, &var, &dy, &mut dx, c, 1e-5);
batch_norm_inference_backward_gamma(&x, &mean, &var, &dy, &mut dgamma, c, 1e-5);
batch_norm_inference_backward_beta(&dy, &mut dbeta, c);
assert!(y.iter().all(|v| v.is_finite()));
assert!(dx.iter().all(|v| v.is_finite()));
assert!(dgamma.iter().any(|&v| v.abs() > 1e-6));
assert_eq!(dbeta, vec![2.0, 2.0, 2.0, 2.0]);
}
#[test]
fn layer_norm_unit_test() {
let input = vec![1.0, 2.0, 3.0, 4.0];
let gamma = vec![1.0; 4];
let beta = vec![0.0; 4];
let mut output = vec![0.0; 4];
layer_norm_row(&input, &gamma, &beta, &mut output, 4, 1e-5);
assert!((output[0] - -1.342).abs() < 0.01);
assert!((output[3] - 1.342).abs() < 0.01);
let sum: f32 = output.iter().sum();
assert!(sum.abs() < 0.01, "LN sum should be ~0, got {sum}");
}
#[test]
fn par_bias_gelu_matches_sequential() {
let n = 100;
let m = 64;
let mut data_par = vec![0.5f32; n * m];
let mut data_seq = data_par.clone();
let bias = vec![0.1f32; m];
bias_gelu(&mut data_seq, &bias, n, m);
par_bias_gelu(&mut data_par, &bias, n, m);
let max_diff: f32 = data_par
.iter()
.zip(data_seq.iter())
.map(|(a, b)| (a - b).abs())
.fold(0f32, f32::max);
assert!(max_diff < 1e-6, "par vs seq diff: {max_diff}");
}
}
#[cfg(test)]
mod par_activation_tail_tests {
use super::*;
fn assert_matches_serial(len: usize) {
let mk = || -> Vec<f32> { (0..len).map(|i| ((i % 97) as f32 / 12.0) - 4.0).collect() };
for (name, par, serial) in [
(
"gelu",
par_gelu_inplace as fn(&mut [f32]),
gelu_inplace as fn(&mut [f32]),
),
("gelu_approx", par_gelu_approx_inplace, gelu_approx_inplace),
("silu", par_silu_inplace, silu_inplace),
] {
let (mut a, mut b) = (mk(), mk());
par(&mut a);
serial(&mut b);
for (i, (x, y)) in a.iter().zip(&b).enumerate() {
assert!(
(x - y).abs() <= 1e-6,
"{name} len={len} index {i}: parallel {x} != serial {y}"
);
}
}
let (src, mut d_par, mut d_ser) = (mk(), mk(), mk());
par_gelu_approx_out(&src, &mut d_par);
gelu_approx_out(&src, &mut d_ser);
assert_eq!(d_par, d_ser, "gelu_approx_out len={len}");
}
#[test]
fn a_tail_shorter_than_the_chunk_is_not_activated_twice() {
assert_matches_serial(1_065_216);
}
#[test]
fn tail_lengths_around_the_parallel_threshold_all_match_serial() {
for extra in [0usize, 1, 255, 256, 511] {
assert_matches_serial(ACTIVATION_PAR_MIN + extra);
}
}
}