use super::arch::{self, DnnArch};
use super::vec_portable::{self, fma};
use super::{Activation, MAX_CONV_INPUTS_ALL, MAX_INPUTS, MAX_RNN_NEURONS_ALL};
#[derive(Clone, Copy, Default)]
pub struct LinearLayer<'w> {
pub bias: Option<&'w [f32]>,
pub subias: Option<&'w [f32]>,
pub weights: Option<&'w [i8]>,
pub float_weights: Option<&'w [f32]>,
pub weights_idx: Option<&'w [i32]>,
pub diag: Option<&'w [f32]>,
pub scale: Option<&'w [f32]>,
pub nb_inputs: usize,
pub nb_outputs: usize,
}
#[derive(Clone, Copy, Default)]
pub struct Conv2dLayer<'w> {
pub bias: Option<&'w [f32]>,
pub float_weights: Option<&'w [f32]>,
pub in_channels: usize,
pub out_channels: usize,
pub ktime: usize,
pub kheight: usize,
}
#[derive(Clone, Copy)]
struct Kernels(DnnArch);
fn sparse_blocks(idx: &[i32], rows: usize, cols: usize) -> usize {
assert!(rows.is_multiple_of(8), "a sparse layer needs a multiple of 8 outputs");
let mut p = 0;
let mut total = 0;
for _ in 0..rows / 8 {
let count = idx[p];
assert!(count >= 0, "negative block count in a sparse index table");
p += 1;
for &pos in &idx[p..p + count as usize] {
assert!(
pos >= 0 && pos as usize + 4 <= cols,
"position out of range in a sparse index table"
);
}
p += count as usize;
total += count as usize;
}
total
}
#[inline]
pub(crate) fn check_sparse(idx: &[i32], rows: usize, cols: usize, w_len: usize) {
let blocks = sparse_blocks(idx, rows, cols);
assert!(blocks.checked_mul(32).is_some_and(|n| w_len >= n), "sparse weights shorter than the index table");
}
impl Kernels {
#[inline]
fn new(arch: DnnArch) -> Kernels {
Kernels(arch::usable(arch))
}
fn sgemv(self, out: &mut [f32], w: &[f32], rows: usize, cols: usize, stride: usize, x: &[f32]) {
assert!(out.len() >= rows && x.len() >= cols);
if rows == 0 {
return;
}
if cols == 0 {
out[..rows].fill(0.0);
return;
}
let need = (cols - 1).checked_mul(stride).and_then(|n| n.checked_add(rows));
assert!(stride >= rows && need.is_some_and(|n| w.len() >= n), "float weights shorter than the layer");
match self.0 {
#[cfg(target_arch = "aarch64")]
DnnArch::Neon | DnnArch::NeonDotprod => unsafe {
super::vec_neon::sgemv(out, w, rows, cols, stride, x)
},
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx2Fma | DnnArch::AvxVnni | DnnArch::Avx512Vnni => unsafe {
super::vec_x86::sgemv_avx2(out, w, rows, cols, stride, x)
},
_ => vec_portable::sgemv(out, w, rows, cols, stride, x),
}
}
fn sparse_sgemv8x4(self, out: &mut [f32], w: &[f32], idx: &[i32], rows: usize, x: &[f32]) {
assert!(out.len() >= rows);
match self.0 {
#[cfg(target_arch = "aarch64")]
DnnArch::Neon | DnnArch::NeonDotprod => unsafe {
super::vec_neon::sparse_sgemv8x4(out, w, idx, rows, x)
},
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx2Fma | DnnArch::AvxVnni | DnnArch::Avx512Vnni => unsafe {
super::vec_x86::sparse_sgemv8x4_avx2(out, w, idx, rows, x)
},
_ => {
check_sparse(idx, rows, x.len(), w.len());
vec_portable::sparse_sgemv8x4(out, w, idx, rows, x)
}
}
}
#[allow(clippy::too_many_arguments)]
fn cgemv8x4(self, out: &mut [f32], w: &[i8], scale: &[f32], rows: usize, cols: usize, x: &[f32]) {
assert!(rows.is_multiple_of(8) && cols.is_multiple_of(4), "an int8 layer needs 8·k outputs and 4·k inputs");
assert!(cols <= MAX_INPUTS);
assert!(out.len() >= rows && x.len() >= cols && scale.len() >= rows);
assert!(rows.checked_mul(cols).is_some_and(|n| w.len() >= n), "int8 weights shorter than the layer");
match self.0 {
#[cfg(target_arch = "aarch64")]
DnnArch::Neon => unsafe { super::vec_neon::cgemv8x4(out, w, scale, rows, cols, x) },
#[cfg(target_arch = "aarch64")]
DnnArch::NeonDotprod => unsafe {
super::vec_neon::cgemv8x4_dotprod(out, w, scale, rows, cols, x)
},
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx2Fma => unsafe { super::vec_x86::cgemv8x4_avx2(out, w, scale, rows, cols, x) },
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::AvxVnni => unsafe {
super::vec_x86::cgemv8x4_avxvnni(out, w, scale, rows, cols, x)
},
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx512Vnni => unsafe {
super::vec_x86::cgemv8x4_avx512vnni(out, w, scale, rows, cols, x)
},
_ => vec_portable::cgemv8x4(out, w, scale, rows, cols, x),
}
}
#[allow(clippy::too_many_arguments)]
fn sparse_cgemv8x4(
self,
out: &mut [f32],
w: &[i8],
idx: &[i32],
scale: &[f32],
rows: usize,
cols: usize,
x: &[f32],
) {
assert!(cols <= MAX_INPUTS);
assert!(out.len() >= rows && x.len() >= cols && scale.len() >= rows);
match self.0 {
#[cfg(target_arch = "aarch64")]
DnnArch::Neon => unsafe {
super::vec_neon::sparse_cgemv8x4(out, w, idx, scale, rows, cols, x)
},
#[cfg(target_arch = "aarch64")]
DnnArch::NeonDotprod => unsafe {
super::vec_neon::sparse_cgemv8x4_dotprod(out, w, idx, scale, rows, cols, x)
},
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx2Fma => unsafe {
super::vec_x86::sparse_cgemv8x4_avx2(out, w, idx, scale, rows, cols, x)
},
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::AvxVnni => unsafe {
super::vec_x86::sparse_cgemv8x4_avxvnni(out, w, idx, scale, rows, cols, x)
},
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx512Vnni => unsafe {
super::vec_x86::sparse_cgemv8x4_avx512vnni(out, w, idx, scale, rows, cols, x)
},
_ => {
check_sparse(idx, rows, cols, w.len());
vec_portable::sparse_cgemv8x4(out, w, idx, scale, rows, cols, x)
}
}
}
fn vec_exp(self, buf: &mut [f32]) {
match self.0 {
#[cfg(target_arch = "aarch64")]
DnnArch::Neon | DnnArch::NeonDotprod => unsafe { super::vec_neon::vec_exp(buf) },
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx2Fma | DnnArch::AvxVnni | DnnArch::Avx512Vnni => unsafe {
super::vec_x86::vec_exp_avx2(buf)
},
_ => vec_portable::vec_exp(buf),
}
}
fn vec_tanh(self, buf: &mut [f32]) {
match self.0 {
#[cfg(target_arch = "aarch64")]
DnnArch::Neon | DnnArch::NeonDotprod => unsafe { super::vec_neon::vec_tanh(buf) },
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx2Fma | DnnArch::AvxVnni | DnnArch::Avx512Vnni => unsafe {
super::vec_x86::vec_tanh_avx2(buf)
},
_ => vec_portable::vec_tanh(buf),
}
}
fn vec_sigmoid(self, buf: &mut [f32]) {
match self.0 {
#[cfg(target_arch = "aarch64")]
DnnArch::Neon | DnnArch::NeonDotprod => unsafe { super::vec_neon::vec_sigmoid(buf) },
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
DnnArch::Avx2Fma | DnnArch::AvxVnni | DnnArch::Avx512Vnni => unsafe {
super::vec_x86::vec_sigmoid_avx2(buf)
},
_ => vec_portable::vec_sigmoid(buf),
}
}
fn linear(self, l: &LinearLayer, out: &mut [f32], input: &[f32]) {
let m = l.nb_inputs;
let n = l.nb_outputs;
assert!(input.len() >= m, "input shorter than the layer's nb_inputs");
assert!(out.len() >= n, "output shorter than the layer's nb_outputs");
let input = &input[..m];
let out = &mut out[..n];
if let Some(fw) = l.float_weights {
match l.weights_idx {
Some(idx) => self.sparse_sgemv8x4(out, fw, idx, n, input),
None => self.sgemv(out, fw, n, m, n, input),
}
} else if let Some(w) = l.weights {
let scale = l.scale.expect("an int8 layer needs a scale");
match l.weights_idx {
Some(idx) => self.sparse_cgemv8x4(out, w, idx, scale, n, m, input),
None => self.cgemv8x4(out, w, scale, n, m, input),
}
} else {
out.fill(0.0);
}
if let Some(bias) = l.bias {
for (o, &b) in out.iter_mut().zip(&bias[..n]) {
*o += b;
}
}
if let Some(diag) = l.diag {
assert!(3 * m == n, "a diagonal needs nb_outputs == 3·nb_inputs");
let diag = &diag[..n];
for (i, &x) in input.iter().enumerate() {
out[i] = fma(diag[i], x, out[i]);
out[i + m] = fma(diag[i + m], x, out[i + m]);
out[i + 2 * m] = fma(diag[i + 2 * m], x, out[i + 2 * m]);
}
}
}
fn activation(self, buf: &mut [f32], act: Activation) {
match act {
Activation::Sigmoid => self.vec_sigmoid(buf),
Activation::Tanh => self.vec_tanh(buf),
Activation::Swish => {
const PIECE: usize = 64;
for piece in buf.chunks_mut(PIECE) {
let mut s = [0.0f32; PIECE];
let s = &mut s[..piece.len()];
s.copy_from_slice(piece);
self.vec_sigmoid(s);
for (y, &g) in piece.iter_mut().zip(s.iter()) {
*y *= g;
}
}
}
Activation::Relu => {
for v in buf.iter_mut() {
if *v < 0.0 {
*v = 0.0;
}
}
}
Activation::Softmax => {
self.vec_exp(buf);
let mut sum = 0.0f32;
for &v in buf.iter() {
sum += v;
}
let s = (1.0f64 / (sum as f64 + 1e-30)) as f32;
for v in buf.iter_mut() {
*v *= s;
}
}
Activation::Exp => self.vec_exp(buf),
Activation::Linear => {}
}
}
}
pub fn compute_linear(l: &LinearLayer, out: &mut [f32], input: &[f32], arch: DnnArch) {
Kernels::new(arch).linear(l, out, input);
}
pub fn compute_activation(out: &mut [f32], input: &[f32], act: Activation, arch: DnnArch) {
let out = &mut out[..input.len()];
out.copy_from_slice(input);
Kernels::new(arch).activation(out, act);
}
pub fn compute_activation_in_place(buf: &mut [f32], act: Activation, arch: DnnArch) {
Kernels::new(arch).activation(buf, act);
}
pub fn compute_generic_dense(
l: &LinearLayer,
out: &mut [f32],
input: &[f32],
act: Activation,
arch: DnnArch,
) {
let k = Kernels::new(arch);
k.linear(l, out, input);
k.activation(&mut out[..l.nb_outputs], act);
}
pub fn compute_generic_gru(
input_w: &LinearLayer,
recurrent_w: &LinearLayer,
state: &mut [f32],
input: &[f32],
arch: DnnArch,
) {
let n = recurrent_w.nb_inputs;
assert!(3 * n == recurrent_w.nb_outputs, "a GRU needs recurrent nb_outputs == 3·nb_inputs");
assert!(input_w.nb_outputs == recurrent_w.nb_outputs, "the two GRU layers differ in size");
assert!(n <= MAX_RNN_NEURONS_ALL, "GRU larger than MAX_RNN_NEURONS_ALL");
let k = Kernels::new(arch);
let state = &mut state[..n];
let mut zrh = [0.0f32; 3 * MAX_RNN_NEURONS_ALL];
let mut recur = [0.0f32; 3 * MAX_RNN_NEURONS_ALL];
let zrh = &mut zrh[..3 * n];
let recur = &mut recur[..3 * n];
k.linear(input_w, zrh, input);
k.linear(recurrent_w, recur, state);
for (a, &b) in zrh[..2 * n].iter_mut().zip(&recur[..2 * n]) {
*a += b;
}
k.activation(&mut zrh[..2 * n], Activation::Sigmoid);
let (zr, h) = zrh.split_at_mut(2 * n);
let (z, r) = zr.split_at(n);
for i in 0..n {
h[i] = fma(recur[2 * n + i], r[i], h[i]);
}
k.activation(h, Activation::Tanh);
for i in 0..n {
state[i] = fma(z[i], state[i], (1.0 - z[i]) * h[i]);
}
}
pub fn compute_glu(l: &LinearLayer, out: &mut [f32], input: &[f32], arch: DnnArch) {
let n = l.nb_outputs;
assert!(l.nb_inputs == n, "a GLU needs nb_inputs == nb_outputs");
assert!(n <= MAX_INPUTS);
let k = Kernels::new(arch);
let mut act = [0.0f32; MAX_INPUTS];
let act = &mut act[..n];
k.linear(l, act, input);
k.activation(act, Activation::Sigmoid);
for ((o, &x), &a) in out[..n].iter_mut().zip(&input[..n]).zip(act.iter()) {
*o = x * a;
}
}
pub fn compute_glu_in_place(l: &LinearLayer, buf: &mut [f32], arch: DnnArch) {
let n = l.nb_outputs;
assert!(l.nb_inputs == n, "a GLU needs nb_inputs == nb_outputs");
assert!(n <= MAX_INPUTS);
let k = Kernels::new(arch);
let mut act = [0.0f32; MAX_INPUTS];
let act = &mut act[..n];
k.linear(l, act, buf);
k.activation(act, Activation::Sigmoid);
for (o, &a) in buf[..n].iter_mut().zip(act.iter()) {
*o *= a;
}
}
pub fn compute_generic_conv1d(
l: &LinearLayer,
out: &mut [f32],
mem: &mut [f32],
input: &[f32],
act: Activation,
arch: DnnArch,
) {
compute_generic_conv1d_dilation(l, out, mem, input, 1, act, arch);
}
pub fn compute_generic_conv1d_dilation(
l: &LinearLayer,
out: &mut [f32],
mem: &mut [f32],
input: &[f32],
dilation: usize,
act: Activation,
arch: DnnArch,
) {
let input_size = input.len();
let nb_inputs = l.nb_inputs;
assert!(nb_inputs <= MAX_CONV_INPUTS_ALL, "conv1d larger than MAX_CONV_INPUTS_ALL");
assert!(input_size <= nb_inputs, "conv1d input longer than the layer's nb_inputs");
assert!(dilation >= 1);
let hist = nb_inputs - input_size;
let k = Kernels::new(arch);
let mut tmp = [0.0f32; MAX_CONV_INPUTS_ALL];
let tmp = &mut tmp[..nb_inputs];
if dilation == 1 {
tmp[..hist].copy_from_slice(&mem[..hist]);
} else {
assert!(input_size > 0 && nb_inputs.is_multiple_of(input_size));
let ksize = nb_inputs / input_size;
assert!(mem.len() >= input_size * dilation * (ksize - 1));
for i in 0..ksize - 1 {
let src = i * input_size * dilation;
tmp[i * input_size..(i + 1) * input_size].copy_from_slice(&mem[src..src + input_size]);
}
}
tmp[hist..].copy_from_slice(input);
k.linear(l, out, tmp);
k.activation(&mut out[..l.nb_outputs], act);
if dilation == 1 {
mem[..hist].copy_from_slice(&tmp[input_size..]);
} else {
let len = input_size * dilation * (nb_inputs / input_size - 1);
if len > 0 {
mem.copy_within(input_size..len, 0);
mem[len - input_size..len].copy_from_slice(input);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn compute_conv2d(
c: &Conv2dLayer,
out: &mut [f32],
mem: &mut [f32],
input: &[f32],
height: usize,
hstride: usize,
act: Activation,
arch: DnnArch,
) {
let (cin, cout, ktime, kheight) = (c.in_channels, c.out_channels, c.ktime, c.kheight);
assert!(ktime >= 1 && kheight >= 1);
let in_stride = height + kheight - 1;
let time_stride = cin * in_stride;
let hist = (ktime - 1) * time_stride;
assert!(input.len() >= time_stride && mem.len() >= hist);
if cout > 0 {
assert!(hstride >= height && out.len() >= (cout - 1) * hstride + height);
}
let w = c.float_weights.expect("a conv2d layer needs its weights");
assert!(w.len() >= cout * cin * ktime * kheight);
let k = Kernels::new(arch);
let step = |t: usize| -> &[f32] {
if t + 1 < ktime {
&mem[t * time_stride..(t + 1) * time_stride]
} else {
&input[..time_stride]
}
};
for i in 0..cout {
let o = &mut out[i * hstride..i * hstride + height];
o.fill(0.0);
for m in 0..cin {
let wm = &w[(i * cin + m) * ktime * kheight..(i * cin + m + 1) * ktime * kheight];
if ktime == 3 && kheight == 3 {
let x0 = &step(0)[m * in_stride..(m + 1) * in_stride];
let x1 = &step(1)[m * in_stride..(m + 1) * in_stride];
let x2 = &step(2)[m * in_stride..(m + 1) * in_stride];
for (j, oj) in o.iter_mut().enumerate() {
let mut s = fma(wm[0], x0[j], wm[1] * x0[j + 1]);
s = fma(wm[2], x0[j + 2], s);
s = fma(wm[3], x1[j], s);
s = fma(wm[4], x1[j + 1], s);
s = fma(wm[5], x1[j + 2], s);
s = fma(wm[6], x2[j], s);
s = fma(wm[7], x2[j + 1], s);
s = fma(wm[8], x2[j + 2], s);
*oj += s;
}
} else {
for t in 0..ktime {
let xt = &step(t)[m * in_stride..(m + 1) * in_stride];
for h in 0..kheight {
let wv = wm[t * kheight + h];
for (j, oj) in o.iter_mut().enumerate() {
*oj = fma(wv, xt[j + h], *oj);
}
}
}
}
}
}
if let Some(bias) = c.bias {
for i in 0..cout {
let b = bias[i];
for v in &mut out[i * hstride..i * hstride + height] {
*v += b;
}
}
}
for i in 0..cout {
k.activation(&mut out[i * hstride..i * hstride + height], act);
}
if hist > 0 {
mem.copy_within(time_stride..hist, 0);
mem[hist - time_stride..hist].copy_from_slice(&input[..time_stride]);
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use std::vec;
use std::vec::Vec;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u32 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 >> 16) as u32
}
fn below(&mut self, n: u32) -> u32 {
self.next() % n
}
fn f(&mut self, a: f32) -> f32 {
a * (2.0 * (self.next() as f32 / 4_294_967_296.0) - 1.0)
}
fn w8(&mut self) -> i8 {
(self.below(255) as i32 - 127) as i8
}
}
const EDGE: [f32; 34] = [
0.0,
-0.0,
1.0,
-1.0,
0.5 / 127.0,
1.5 / 127.0,
2.5 / 127.0,
-0.5 / 127.0,
-2.5 / 127.0,
1.003_937,
1.007_874,
1.5,
-1.5,
2.0,
-2.0,
3.0,
88.0,
-88.0,
89.0,
-89.0,
1e10,
-1e10,
1e20,
-1e20,
f32::INFINITY,
f32::NEG_INFINITY,
1e-40,
-1e-40,
7.3e9,
5.4e7,
2.313e9,
2.3132e9,
4.626e9,
4.6263e9,
];
fn input(rng: &mut Rng, n: usize, trial: usize) -> Vec<f32> {
(0..n)
.map(|i| match trial {
0..=2 => EDGE[(i * 7 + trial) % EDGE.len()],
3..=19 => rng.f(1.0),
20..=24 => rng.f(1.6),
_ => rng.f(30.0),
})
.collect()
}
fn same(a: &[f32], b: &[f32]) -> bool {
a.len() == b.len()
&& a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits() || (x.is_nan() && y.is_nan()))
}
struct Owned {
bias: Vec<f32>,
fw: Vec<f32>,
w8: Vec<i8>,
idx: Vec<i32>,
scale: Vec<f32>,
diag: Vec<f32>,
kind: u8,
nin: usize,
nout: usize,
with_bias: bool,
with_diag: bool,
}
fn make(rng: &mut Rng, nin: usize, nout: usize, kind: u8, with_bias: bool, with_diag: bool) -> Owned {
let bias = (0..nout).map(|_| rng.f(0.5)).collect();
let scale = (0..nout).map(|_| (1.0 / 128.0 / 127.0) * (0.5 + rng.below(1000) as f32 / 1000.0)).collect();
let diag = (0..nout).map(|_| rng.f(0.3)).collect();
let mut idx = Vec::new();
let mut blocks = 0;
if kind >= 2 {
for _ in (0..nout).step_by(8) {
let at = idx.len();
idx.push(0);
let mut count = 0;
for c in (0..nin.saturating_sub(3)).step_by(4) {
if rng.below(3) != 0 {
idx.push(c as i32);
count += 1;
}
}
idx[at] = count;
blocks += count as usize;
}
}
let nw = if kind >= 2 { 32 * blocks } else { nin * nout };
let fw = (0..nw).map(|_| rng.f(0.3)).collect();
let w8 = (0..nw).map(|_| rng.w8()).collect();
Owned { bias, fw, w8, idx, scale, diag, kind, nin, nout, with_bias, with_diag }
}
impl Owned {
fn layer(&self) -> LinearLayer<'_> {
let mut l = LinearLayer { nb_inputs: self.nin, nb_outputs: self.nout, ..Default::default() };
if self.with_bias {
l.bias = Some(&self.bias);
}
if self.with_diag {
l.diag = Some(&self.diag);
}
match self.kind {
0 => l.float_weights = Some(&self.fw),
1 => {
l.weights = Some(&self.w8);
l.scale = Some(&self.scale);
}
2 => {
l.weights = Some(&self.w8);
l.scale = Some(&self.scale);
l.weights_idx = Some(&self.idx);
}
_ => {
l.float_weights = Some(&self.fw);
l.weights_idx = Some(&self.idx);
}
}
l
}
}
const SHAPES: [(usize, usize, u8); 64] = [
(88, 64, 1), (64, 64, 1), (288, 64, 1), (64, 192, 1), (57, 128, 0), (192, 20, 0),
(128, 576, 1), (192, 576, 1), (224, 12, 0), (32, 64, 0), (80, 1, 0), (328, 192, 1),
(688, 128, 1), (128, 40, 1), (192, 4, 0), (40, 64, 0), (544, 32, 1), (128, 56, 1),
(544, 128, 2), (96, 64, 2), (480, 64, 2), (64, 96, 2), (26, 96, 0), (50, 128, 0),
(576, 80, 2), (128, 320, 2), (301, 64, 0), (173, 96, 0), (768, 128, 1), (128, 512, 1),
(128, 16, 1), (128, 1, 0), (160, 2, 0), (42, 80, 0), (160, 80, 0), (320, 160, 1),
(342, 128, 0), (384, 128, 1), (256, 120, 1), (42, 120, 0), (240, 120, 0), (128, 3, 0),
(128, 288, 1), (3, 1, 0), (4, 1, 0), (5, 1, 0), (7, 1, 0), (16, 1, 0), (17, 1, 0),
(19, 1, 0), (20, 1, 0), (2, 8, 0), (32, 1, 0), (8, 48, 1), (1, 3, 0), (12, 12, 0),
(64, 96, 3), (160, 24, 3), (2040, 8, 1), (8, 8, 1), (12, 40, 1), (20, 8, 1), (36, 21, 0),
(9, 7, 0),
];
const ODD_SPARSE: [(usize, usize, u8); 3] = [(30, 16, 2), (13, 8, 2), (22, 8, 3)];
#[test]
fn linear_layers_match_portable() {
let mut rng = Rng(0x9E37_79B9_7F4A_7C15);
for &(nin, nout, kind) in SHAPES.iter().chain(ODD_SPARSE.iter()) {
for trial in 0..30 {
let with_diag = trial % 3 == 2 && nout == 3 * nin;
let o = make(&mut rng, nin, nout, kind, trial % 4 != 3, with_diag);
let l = o.layer();
let x = input(&mut rng, nin + trial % 3, trial);
let mut want = vec![0.0; nout];
compute_linear(&l, &mut want, &x, DnnArch::Portable);
for &a in arch::available() {
let mut got = vec![0.0; nout + 2];
compute_linear(&l, &mut got, &x, a);
assert!(same(&got[..nout], &want), "{a:?} {nin}x{nout} kind {kind} trial {trial}");
assert_eq!(&got[nout..], &[0.0, 0.0]);
let mut dense = vec![0.0; nout];
compute_generic_dense(&l, &mut dense, &x, Activation::Tanh, a);
let mut want_t = want.clone();
compute_activation_in_place(&mut want_t, Activation::Tanh, DnnArch::Portable);
assert!(same(&dense, &want_t), "dense {a:?} {nin}x{nout} kind {kind}");
}
}
}
}
#[test]
fn int8_quantization_of_every_float_class_matches() {
let mut x = Vec::new();
for sign in [0u32, 1] {
for e in 0..256u32 {
for f in [0u32, 1, 0x40_0000, 0x7f_ffff, 0x12_3456] {
x.push(f32::from_bits(sign << 31 | e << 23 | f));
}
}
}
for k in -260..=260 {
for d in [-0.5f32, 0.5] {
let v = (k as f32 + d) / 127.0;
x.extend([v, f32::from_bits(v.to_bits() + 1), f32::from_bits(v.to_bits() - 1)]);
}
}
x.truncate(x.len() / 8 * 8);
let mut pieces: Vec<(usize, usize)> = (0..x.len()).step_by(2048).map(|s| (s, (x.len() - s).min(2048))).collect();
pieces.extend([(8, 24), (0, 16), (16, 8)]);
for (start, n) in pieces {
let x = &x[start..start + n];
let mut w = vec![0i8; n * n];
for i in 0..n {
w[(i / 8 * (n / 4) + i / 4) * 32 + 4 * (i % 8) + i % 4] = 1;
}
let scale = vec![1.0f32; n];
let l = LinearLayer { weights: Some(&w), scale: Some(&scale), nb_inputs: n, nb_outputs: n, ..Default::default() };
for &a in arch::available() {
let mut y = vec![0.0f32; n];
compute_linear(&l, &mut y, &x[..n], a);
for i in 0..n {
assert_eq!(y[i], vec_portable::quantize(x[i]) as f32, "{a:?} x = {:e} ({:08x})", x[i], x[i].to_bits());
}
}
}
}
#[test]
fn a_sparse_layer_with_every_block_equals_the_dense_layer() {
let mut rng = Rng(7);
for &(nin, nout) in &[(64usize, 32usize), (96, 64), (8, 8), (544, 128)] {
let dense8: Vec<i8> = (0..nin * nout).map(|_| rng.w8()).collect();
let scale: Vec<f32> = (0..nout).map(|_| rng.f(0.01)).collect();
let mut idx = Vec::new();
for _ in 0..nout / 8 {
idx.push((nin / 4) as i32);
idx.extend((0..nin as i32).step_by(4));
}
let densef: Vec<f32> = (0..nin * nout).map(|_| rng.f(0.3)).collect();
let mut sparsef = Vec::new();
for b in 0..nout / 8 {
for g in 0..nin / 4 {
for c in 0..4 {
for k in 0..8 {
sparsef.push(densef[(4 * g + c) * nout + 8 * b + k]);
}
}
}
}
let x = input(&mut rng, nin, 5);
for &a in arch::available() {
let d = LinearLayer { weights: Some(&dense8), scale: Some(&scale), nb_inputs: nin, nb_outputs: nout, ..Default::default() };
let s = LinearLayer { weights_idx: Some(&idx), ..d };
let (mut yd, mut ys) = (vec![0.0; nout], vec![0.0; nout]);
compute_linear(&d, &mut yd, &x, a);
compute_linear(&s, &mut ys, &x, a);
assert!(same(&yd, &ys), "int8 {a:?} {nin}x{nout}");
let d = LinearLayer { float_weights: Some(&densef), nb_inputs: nin, nb_outputs: nout, ..Default::default() };
let s = LinearLayer { float_weights: Some(&sparsef), weights_idx: Some(&idx), ..d };
compute_linear(&d, &mut yd, &x, a);
compute_linear(&s, &mut ys, &x, a);
assert!(same(&yd, &ys), "float {a:?} {nin}x{nout}");
}
}
}
#[test]
fn activations_match_portable() {
let mut rng = Rng(3);
let acts = [
Activation::Linear,
Activation::Sigmoid,
Activation::Tanh,
Activation::Relu,
Activation::Softmax,
Activation::Swish,
Activation::Exp,
];
for act in acts {
for n in (1..=41).chain([576, 130]) {
for trial in 0..40 {
let x = input(&mut rng, n, trial % 30);
let mut want = vec![0.0; n];
compute_activation(&mut want, &x, act, DnnArch::Portable);
for &a in arch::available() {
let mut got = vec![0.0; n];
compute_activation(&mut got, &x, act, a);
assert!(same(&got, &want), "{act:?} {a:?} n {n} trial {trial}");
let mut inplace = x.clone();
compute_activation_in_place(&mut inplace, act, a);
assert!(same(&inplace, &want), "in place {act:?} {a:?} n {n}");
}
}
}
}
}
#[test]
fn activation_tails_use_the_division_formulas() {
let x = [0.3f32];
let mut y = [0.0];
compute_activation(&mut y, &x, Activation::Tanh, DnnArch::Portable);
assert_eq!(y[0].to_bits(), vec_portable::tanh_tail(0.3).to_bits());
compute_activation(&mut y, &x, Activation::Sigmoid, DnnArch::Portable);
assert_eq!(y[0].to_bits(), vec_portable::sigmoid_tail(0.3).to_bits());
let x = [0.3f32; 5];
let mut y = [0.0; 5];
for &a in arch::available() {
compute_activation(&mut y, &x, Activation::Tanh, a);
assert_eq!(y[3].to_bits(), vec_portable::tanh_approx(0.3).to_bits());
assert_eq!(y[4].to_bits(), vec_portable::tanh_tail(0.3).to_bits());
}
assert_ne!(vec_portable::tanh_tail(0.3).to_bits(), vec_portable::tanh_approx(0.3).to_bits());
let x = [-0.0f32, f32::NAN, -1.0, 2.0];
let mut y = [0.0; 4];
compute_activation(&mut y, &x, Activation::Relu, DnnArch::Portable);
assert_eq!(y[0].to_bits(), (-0.0f32).to_bits());
assert!(y[1].is_nan());
assert_eq!(&y[2..], &[0.0, 2.0]);
}
#[test]
fn gru_glu_and_conv1d_match_portable() {
let mut rng = Rng(11);
let grus = [(128usize, 192usize, 1u8), (192, 192, 1), (64, 64, 1), (272, 160, 1), (64, 32, 2), (160, 128, 0), (57, 4, 0), (8, 16, 1)];
for &(nin, n, kind) in &grus {
let li = make(&mut rng, nin, 3 * n, kind, true, false);
let lr = make(&mut rng, n, 3 * n, if kind == 2 { 1 } else { kind }, true, kind == 0);
let mut want = vec![0.0; n];
let mut states: Vec<Vec<f32>> = arch::available().iter().map(|_| vec![0.0; n]).collect();
for step in 0..30 {
let x = input(&mut rng, nin, 3 + step % 25);
compute_generic_gru(&li.layer(), &lr.layer(), &mut want, &x, DnnArch::Portable);
for (s, &a) in states.iter_mut().zip(arch::available()) {
compute_generic_gru(&li.layer(), &lr.layer(), s, &x, a);
assert!(same(s, &want), "GRU {a:?} {nin}x{n} step {step}");
}
}
}
for &(n, kind) in &[(192usize, 1u8), (160, 1), (128, 1), (64, 2), (24, 0), (20, 0)] {
let l = make(&mut rng, n, n, kind, true, false);
let x = input(&mut rng, n, 7);
let mut want = vec![0.0; n];
compute_glu(&l.layer(), &mut want, &x, DnnArch::Portable);
for &a in arch::available() {
let mut got = vec![0.0; n];
compute_glu(&l.layer(), &mut got, &x, a);
assert!(same(&got, &want), "GLU {a:?} {n}");
let mut inplace = x.clone();
compute_glu_in_place(&l.layer(), &mut inplace, a);
assert!(same(&inplace, &want), "GLU in place {a:?} {n}");
}
}
let convs = [(64usize, 3usize, 32usize, 1u8, 1usize), (64, 2, 64, 1, 2), (32, 4, 64, 2, 2), (173, 1, 96, 0, 1), (16, 3, 8, 0, 3), (40, 2, 16, 3, 1)];
for &(isz, ksize, nout, kind, dil) in &convs {
let l = make(&mut rng, isz * ksize, nout, kind, true, false);
let memlen = isz * dil * (ksize - 1);
let mut want_mem = vec![0.0; memlen];
let mut mems: Vec<Vec<f32>> = arch::available().iter().map(|_| vec![0.0; memlen]).collect();
for step in 0..20 {
let x = input(&mut rng, isz, 3 + step);
let mut want = vec![0.0; nout];
compute_generic_conv1d_dilation(&l.layer(), &mut want, &mut want_mem, &x, dil, Activation::Tanh, DnnArch::Portable);
for (m, &a) in mems.iter_mut().zip(arch::available()) {
let mut got = vec![0.0; nout];
if dil == 1 {
compute_generic_conv1d(&l.layer(), &mut got, m, &x, Activation::Tanh, a);
} else {
compute_generic_conv1d_dilation(&l.layer(), &mut got, m, &x, dil, Activation::Tanh, a);
}
assert!(same(&got, &want) && same(m, &want_mem), "conv1d {a:?} {isz}x{ksize} dil {dil} step {step}");
}
}
}
}
#[test]
fn conv1d_without_memory_is_a_dense_layer() {
let mut rng = Rng(5);
let l = make(&mut rng, 173, 96, 0, true, false);
let x = input(&mut rng, 173, 4);
let (mut a, mut b, mut c) = (vec![0.0; 96], vec![0.0; 96], vec![0.0; 96]);
compute_generic_conv1d(&l.layer(), &mut a, &mut [], &x, Activation::Tanh, arch::detect());
compute_generic_dense(&l.layer(), &mut b, &x, Activation::Tanh, arch::detect());
compute_generic_conv1d_dilation(&l.layer(), &mut c, &mut [], &x, 2, Activation::Tanh, arch::detect());
assert!(same(&a, &b) && same(&c, &b));
}
#[test]
fn conv2d_matches_portable() {
let mut rng = Rng(13);
let shapes = [(1usize, 4usize, 3usize, 3usize, 224usize, 226usize), (4, 1, 3, 3, 224, 224), (2, 3, 2, 5, 17, 20), (3, 2, 1, 1, 9, 9)];
for &(cin, cout, kt, kh, height, hstride) in &shapes {
let w: Vec<f32> = (0..cout * cin * kt * kh).map(|_| rng.f(0.5)).collect();
let bias: Vec<f32> = (0..cout).map(|_| rng.f(0.5)).collect();
let c = Conv2dLayer { bias: Some(&bias), float_weights: Some(&w), in_channels: cin, out_channels: cout, ktime: kt, kheight: kh };
let ts = cin * (height + kh - 1);
let mut want_mem = vec![0.0; (kt - 1) * ts + 3];
let mut mems: Vec<Vec<f32>> = arch::available().iter().map(|_| want_mem.clone()).collect();
for step in 0..8 {
let x = input(&mut rng, ts + 5, 3 + step);
let olen = (cout - 1) * hstride + height + 2;
let mut want = vec![0.0; olen];
compute_conv2d(&c, &mut want, &mut want_mem, &x, height, hstride, Activation::Tanh, DnnArch::Portable);
for (m, &a) in mems.iter_mut().zip(arch::available()) {
let mut got = vec![0.0; olen];
compute_conv2d(&c, &mut got, m, &x, height, hstride, Activation::Tanh, a);
assert!(same(&got, &want) && same(m, &want_mem), "conv2d {a:?} step {step}");
}
}
}
}
}