use super::mlp_data::{
LAYER0_BIAS, LAYER0_WEIGHTS, LAYER1_BIAS, LAYER1_RECUR_WEIGHTS, LAYER1_WEIGHTS, LAYER2_BIAS,
LAYER2_WEIGHTS,
};
use crate::mathops::mac;
pub const WEIGHTS_SCALE: f32 = 1.0 / 128.0;
pub const MAX_NEURONS: usize = 32;
#[derive(Debug)]
pub struct DenseLayer {
pub bias: &'static [f32],
pub input_weights: &'static [f32],
pub nb_inputs: usize,
pub nb_neurons: usize,
pub sigmoid: bool,
}
#[derive(Debug)]
pub struct GruLayer {
pub bias: &'static [f32],
pub input_weights: &'static [f32],
pub recurrent_weights: &'static [f32],
pub nb_inputs: usize,
pub nb_neurons: usize,
}
const fn to_f32<const N: usize>(w: &[i8; N]) -> [f32; N] {
let mut out = [0.0f32; N];
let mut i = 0;
while i < N {
out[i] = w[i] as f32;
i += 1;
}
out
}
static L0_BIAS: [f32; 32] = to_f32(&LAYER0_BIAS);
static L0_WEIGHTS: [f32; 800] = to_f32(&LAYER0_WEIGHTS);
static L1_BIAS: [f32; 72] = to_f32(&LAYER1_BIAS);
static L1_WEIGHTS: [f32; 2304] = to_f32(&LAYER1_WEIGHTS);
static L1_RECUR_WEIGHTS: [f32; 1728] = to_f32(&LAYER1_RECUR_WEIGHTS);
static L2_BIAS: [f32; 2] = to_f32(&LAYER2_BIAS);
static L2_WEIGHTS: [f32; 48] = to_f32(&LAYER2_WEIGHTS);
pub static LAYER0: DenseLayer = DenseLayer {
bias: &L0_BIAS,
input_weights: &L0_WEIGHTS,
nb_inputs: 25,
nb_neurons: 32,
sigmoid: false,
};
pub static LAYER1: GruLayer = GruLayer {
bias: &L1_BIAS,
input_weights: &L1_WEIGHTS,
recurrent_weights: &L1_RECUR_WEIGHTS,
nb_inputs: 32,
nb_neurons: 24,
};
pub static LAYER2: DenseLayer = DenseLayer {
bias: &L2_BIAS,
input_weights: &L2_WEIGHTS,
nb_inputs: 24,
nb_neurons: 2,
sigmoid: true,
};
#[allow(clippy::excessive_precision)]
pub(super) const TANSIG_N: [f32; 3] = [952.52801514, 96.39235687, 0.60863042];
#[allow(clippy::excessive_precision)]
pub(super) const TANSIG_D: [f32; 3] = [952.72399902, 413.36801147, 11.88600922];
#[inline]
pub fn tansig_approx(x: f32) -> f32 {
let [n0, n1, n2] = TANSIG_N;
let [d0, d1, d2] = TANSIG_D;
let x2 = x * x;
let num = mac(n0, mac(n1, n2, x2), x2);
let den = mac(d0, mac(d1, d2, x2), x2);
let num = num * x / den;
let num = if 1.0 < num { 1.0 } else { num };
if -1.0 > num { -1.0 } else { num }
}
#[inline]
pub fn sigmoid_approx(x: f32) -> f32 {
mac(0.5, 0.5, tansig_approx(0.5 * x))
}
#[inline]
fn gemm_accum(out: &mut [f32], w: &[f32], stride: usize, x: &[f32]) {
match out.len() {
32 => gemm_accum_fixed::<32>(out, w, stride, x),
24 => gemm_accum_fixed::<24>(out, w, stride, x),
2 => gemm_accum_fixed::<2>(out, w, stride, x),
rows => {
for (j, &xj) in x.iter().enumerate() {
let row = &w[j * stride..j * stride + rows];
for (o, &wi) in out.iter_mut().zip(row) {
*o = mac(*o, wi, xj);
}
}
}
}
}
#[inline(always)]
fn gemm_accum_fixed<const R: usize>(out: &mut [f32], w: &[f32], stride: usize, x: &[f32]) {
let out: &mut [f32; R] = out.try_into().expect("R outputs");
let mut acc = *out;
for (j, &xj) in x.iter().enumerate() {
let row: &[f32; R] = w[j * stride..j * stride + R].try_into().expect("R weights");
for i in 0..R {
acc[i] = mac(acc[i], row[i], xj);
}
}
*out = acc;
}
pub fn compute_dense(layer: &DenseLayer, output: &mut [f32], input: &[f32]) {
let n = layer.nb_neurons;
let m = layer.nb_inputs;
let output = &mut output[..n];
output.copy_from_slice(&layer.bias[..n]);
gemm_accum(output, layer.input_weights, n, &input[..m]);
#[cfg(target_arch = "aarch64")]
let output = unsafe { super::neon::scale_and_activate(output, layer.sigmoid) };
#[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
let output = unsafe { super::x86::scale_and_activate(output, layer.sigmoid) };
for o in output.iter_mut() {
*o *= WEIGHTS_SCALE;
}
if layer.sigmoid {
for o in output.iter_mut() {
*o = sigmoid_approx(*o);
}
} else {
for o in output.iter_mut() {
*o = tansig_approx(*o);
}
}
}
pub fn compute_gru(gru: &GruLayer, state: &mut [f32], input: &[f32]) {
#[cfg(target_arch = "aarch64")]
if gru.nb_inputs == super::neon::GRU_INPUTS && gru.nb_neurons == super::neon::GRU_NEURONS {
let state = (&mut state[..super::neon::GRU_NEURONS]).try_into().expect("24 values");
unsafe { super::neon::gru(gru, state, input) };
return;
}
#[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
if gru.nb_inputs == super::x86::GRU_INPUTS && gru.nb_neurons == super::x86::GRU_NEURONS {
let state = (&mut state[..super::x86::GRU_NEURONS]).try_into().expect("24 values");
unsafe { super::x86::gru(gru, state, input) };
return;
}
compute_gru_scalar(gru, state, input);
}
pub fn compute_gru_scalar(gru: &GruLayer, state: &mut [f32], input: &[f32]) {
let n = gru.nb_neurons;
let m = gru.nb_inputs;
let stride = 3 * n;
let input = &input[..m];
let state = &mut state[..n];
let mut z = [0.0f32; MAX_NEURONS];
let mut r = [0.0f32; MAX_NEURONS];
let mut h = [0.0f32; MAX_NEURONS];
let mut tmp = [0.0f32; MAX_NEURONS];
let (z, r, h, tmp) = (&mut z[..n], &mut r[..n], &mut h[..n], &mut tmp[..n]);
z.copy_from_slice(&gru.bias[..n]);
gemm_accum(z, gru.input_weights, stride, input);
gemm_accum(z, gru.recurrent_weights, stride, state);
for v in z.iter_mut() {
*v = sigmoid_approx(WEIGHTS_SCALE * *v);
}
r.copy_from_slice(&gru.bias[n..2 * n]);
gemm_accum(r, &gru.input_weights[n..], stride, input);
gemm_accum(r, &gru.recurrent_weights[n..], stride, state);
for v in r.iter_mut() {
*v = sigmoid_approx(WEIGHTS_SCALE * *v);
}
h.copy_from_slice(&gru.bias[2 * n..3 * n]);
for i in 0..n {
tmp[i] = state[i] * r[i];
}
gemm_accum(h, &gru.input_weights[2 * n..], stride, input);
gemm_accum(h, &gru.recurrent_weights[2 * n..], stride, tmp);
for i in 0..n {
h[i] = mac((1.0 - z[i]) * tansig_approx(WEIGHTS_SCALE * h[i]), z[i], state[i]);
}
state.copy_from_slice(h);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layer_sizes_match_the_tables() {
for l in [&LAYER0, &LAYER2] {
assert_eq!(l.bias.len(), l.nb_neurons);
assert_eq!(l.input_weights.len(), l.nb_inputs * l.nb_neurons);
assert!(l.nb_neurons <= MAX_NEURONS);
}
let g = &LAYER1;
assert_eq!(g.bias.len(), 3 * g.nb_neurons);
assert_eq!(g.input_weights.len(), 3 * g.nb_inputs * g.nb_neurons);
assert_eq!(g.recurrent_weights.len(), 3 * g.nb_neurons * g.nb_neurons);
assert_eq!(LAYER0.nb_neurons, LAYER1.nb_inputs);
assert_eq!(LAYER1.nb_neurons, LAYER2.nb_inputs);
}
#[test]
fn f32_tables_hold_the_int8_values() {
for (a, b) in L1_WEIGHTS.iter().zip(LAYER1_WEIGHTS.iter()) {
assert_eq!(*a, *b as f32);
}
for (a, b) in L0_WEIGHTS.iter().zip(LAYER0_WEIGHTS.iter()) {
assert_eq!(*a, *b as f32);
}
}
#[test]
fn tansig_is_limited_and_odd() {
assert_eq!(tansig_approx(0.0), 0.0);
assert_eq!(tansig_approx(100.0), 1.0);
assert_eq!(tansig_approx(-100.0), -1.0);
assert!(tansig_approx(f32::NAN).is_nan());
for i in 0..1000 {
let x = i as f32 * 0.01;
assert_eq!(tansig_approx(-x), -tansig_approx(x));
}
assert_eq!(sigmoid_approx(0.0), 0.5);
}
}