use crate::simd::{SimdF32, Vf32, LANES};
use crate::weights::Conv3x3;
const BLOCK_CHANNELS: usize = 16;
const CV: usize = BLOCK_CHANNELS / LANES;
const PX: usize = if cfg!(target_arch = "aarch64") || LANES == 8 {
4
} else {
2
};
#[inline]
pub(crate) const fn bordered_len(width: usize, channels: usize) -> usize {
(width + 2) * channels
}
pub(crate) fn conv_relu_row(layer: &Conv3x3, rows: [&[f32]; 3], width: usize, out: &mut [f32]) {
let (cin, cout) = (layer.cin, layer.cout);
assert_eq!(cout % BLOCK_CHANNELS, 0);
assert!(rows.iter().all(|row| row.len() >= bordered_len(width, cin)));
assert!(out.len() >= bordered_len(width, cout));
let full = width / PX * PX;
for co0 in (0..cout).step_by(BLOCK_CHANNELS) {
let mut x = 0;
while x < full {
unsafe { block::<PX>(layer, rows, x, co0, out) };
x += PX;
}
while x < width {
unsafe { block::<1>(layer, rows, x, co0, out) };
x += 1;
}
}
}
#[inline(always)]
unsafe fn block<const P: usize>(
layer: &Conv3x3,
rows: [&[f32]; 3],
x: usize,
co0: usize,
out: &mut [f32],
) {
let (cin, cout) = (layer.cin, layer.cout);
let weights = layer.weights.as_ptr();
let mut acc = [[Vf32::splat(0.0); CV]; P];
for v in 0..CV {
let bias = Vf32::load(layer.bias.as_ptr().add(co0 + v * LANES));
for pixel in acc.iter_mut() {
pixel[v] = bias;
}
}
for (dy, row) in rows.iter().enumerate() {
for dx in 0..3 {
let samples = row.as_ptr().add((x + dx) * cin);
let taps = weights.add(((dy * 3 + dx) * cin) * cout + co0);
for ci in 0..cin {
let mut w = [Vf32::splat(0.0); CV];
for (v, lane) in w.iter_mut().enumerate() {
*lane = Vf32::load(taps.add(ci * cout + v * LANES));
}
for (p, pixel) in acc.iter_mut().enumerate() {
let sample = Vf32::splat(*samples.add(p * cin + ci));
for v in 0..CV {
pixel[v] = pixel[v].mul_add(sample, w[v]);
}
}
}
}
}
let zero = Vf32::splat(0.0);
for (p, pixel) in acc.iter().enumerate() {
let target = out.as_mut_ptr().add((x + 1 + p) * cout + co0);
for (v, lane) in pixel.iter().enumerate() {
lane.max(zero).store(target.add(v * LANES));
}
}
}
pub(crate) fn conv_logit_row(layer: &Conv3x3, rows: [&[f32]; 3], width: usize, out: &mut [f32]) {
let cin = layer.cin;
assert_eq!(layer.cout, 1);
assert_eq!(cin % LANES, 0);
assert!(rows.iter().all(|row| row.len() >= bordered_len(width, cin)));
assert!(out.len() >= width);
let bias = layer.bias[0];
for (x, logit) in out.iter_mut().enumerate().take(width) {
let mut acc = Vf32::splat(0.0);
for (dy, row) in rows.iter().enumerate() {
for dx in 0..3 {
let samples = &row[(x + dx) * cin..][..cin];
let taps = &layer.weights[(dy * 3 + dx) * cin..][..cin];
for v in (0..cin).step_by(LANES) {
unsafe {
acc = acc.mul_add(
Vf32::load(samples.as_ptr().add(v)),
Vf32::load(taps.as_ptr().add(v)),
);
}
}
}
}
*logit = acc.reduce_sum() + bias;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn layer(cin: usize, cout: usize) -> Conv3x3 {
let mut weights = vec![0.0; 9 * cin * cout];
for tap in 0..9 {
for ci in 0..cin {
for co in 0..cout {
weights[(tap * cin + ci) * cout + co] = (tap * 100 + ci * 10 + co) as f32;
}
}
}
Conv3x3 {
cin,
cout,
weights,
bias: (0..cout).map(|co| co as f32 * 0.5).collect(),
}
}
fn reference(layer: &Conv3x3, rows: [&[f32]; 3], x: usize, co: usize) -> f32 {
let mut total = layer.bias[co];
for (dy, row) in rows.iter().enumerate() {
for dx in 0..3 {
for ci in 0..layer.cin {
let sample = row[(x + dx) * layer.cin + ci];
let weight = layer.weights[((dy * 3 + dx) * layer.cin + ci) * layer.cout + co];
total += sample * weight;
}
}
}
total
}
fn bordered_rows(width: usize, cin: usize, seed: f32) -> [Vec<f32>; 3] {
std::array::from_fn(|dy| {
let mut row = vec![0.0; bordered_len(width, cin)];
for x in 1..=width {
for ci in 0..cin {
let v = ((x * 7 + ci * 3 + dy * 5) % 11) as f32 - 5.0 + seed;
row[x * cin + ci] = v;
}
}
row
})
}
#[test]
fn a_hidden_row_matches_the_definition_with_relu() {
for width in [1, 2, 3, 5, 8, 13] {
let layer = layer(7, 32);
let rows = bordered_rows(width, 7, 0.25);
let mut out = vec![7.0; bordered_len(width, 32)];
conv_relu_row(&layer, [&rows[0], &rows[1], &rows[2]], width, &mut out);
for x in 0..width {
for co in 0..32 {
let expected =
reference(&layer, [&rows[0], &rows[1], &rows[2]], x, co).max(0.0);
let actual = out[(x + 1) * 32 + co];
assert!(
(actual - expected).abs() <= expected.abs() * 1e-5,
"width {width} x {x} co {co}: {actual} vs {expected}"
);
}
}
assert!(out[..32].iter().all(|v| *v == 7.0));
assert!(out[(width + 1) * 32..].iter().all(|v| *v == 7.0));
}
}
#[test]
fn the_logit_row_matches_the_definition() {
let layer = layer(16, 1);
let width = 9;
let rows = bordered_rows(width, 16, -0.5);
let mut out = vec![0.0; width];
conv_logit_row(&layer, [&rows[0], &rows[1], &rows[2]], width, &mut out);
for (x, actual) in out.iter().enumerate() {
let expected = reference(&layer, [&rows[0], &rows[1], &rows[2]], x, 0);
assert!(
(actual - expected).abs() <= expected.abs() * 1e-5,
"x {x}: {actual} vs {expected}"
);
}
}
}