use frink_core::weight_matrix::WeightMatrix;
use frink_gguf::TensorSource;
use crate::loader::{load_f32_vec, load_weight_matrix, LoadError};
pub const SHORTCONV_ARCHITECTURES: &[(&str, &str)] = &[
(
"lfm2",
"lfm2.cpp:9-11 (is_recr), :80-82 (tensors), :139-189 (block)",
),
(
"lfm2moe",
"lfm2moe.cpp:12-14, :66-69; the graph is lfm2's (models.h:1899)",
),
];
pub fn is_shortconv_architecture(arch: &str) -> bool {
SHORTCONV_ARCHITECTURES.iter().any(|(a, _)| *a == arch)
}
pub struct ShortConv {
pub conv: Vec<f32>,
pub l_cache: usize,
pub in_proj: WeightMatrix,
pub out_proj: WeightMatrix,
}
impl ShortConv {
pub fn load(
file: &impl TensorSource,
arch: &str,
layer: usize,
hidden_dim: usize,
) -> Result<Self, LoadError> {
let key = format!("{arch}.shortconv.l_cache");
let l_cache = file
.metadata_u64(&key)
.ok_or_else(|| LoadError::MissingHparam(key.clone()))? as usize;
if l_cache < 2 {
return Err(LoadError::UnsupportedFeature(
key,
format!("{l_cache}: lfm2.cpp:171 asserts a conv of width at least 2"),
));
}
let conv_name = format!("blk.{layer}.shortconv.conv.weight");
let conv = load_f32_vec(file, &conv_name)?;
if conv.len() != l_cache * hidden_dim {
return Err(LoadError::UnsupportedFeature(
conv_name,
format!(
"{} elements; lfm2.cpp:80 sizes the kernel {{l_cache {l_cache}, n_embd \
{hidden_dim}}}",
conv.len()
),
));
}
let in_proj = load_weight_matrix(file, &format!("blk.{layer}.shortconv.in_proj.weight"))?;
let out_proj = load_weight_matrix(file, &format!("blk.{layer}.shortconv.out_proj.weight"))?;
for (name, m, rows) in [
("in_proj", &in_proj, 3 * hidden_dim),
("out_proj", &out_proj, hidden_dim),
] {
if m.rows() != rows || m.cols() != hidden_dim {
return Err(LoadError::UnsupportedFeature(
format!("blk.{layer}.shortconv.{name}.weight"),
format!(
"{}x{}; lfm2.cpp:81-82 size it {rows}x{hidden_dim}",
m.rows(),
m.cols()
),
));
}
}
Ok(Self {
conv,
l_cache,
in_proj,
out_proj,
})
}
pub fn hidden_dim(&self) -> usize {
self.out_proj.rows()
}
pub fn forward_rows(
&self,
normed: &[f32],
rows: usize,
mut history: impl FnMut(&[f32]) -> Vec<f32>,
) -> Vec<f32> {
let n = self.hidden_dim();
assert_eq!(normed.len(), rows * n);
let bcx = if rows == 1 {
self.in_proj.apply(normed)
} else {
self.in_proj.apply_batch(normed, rows)
};
let l = self.l_cache;
let mut y = vec![0.0f32; rows * n];
for r in 0..rows {
let row = &bcx[r * 3 * n..(r + 1) * 3 * n];
let (b, c, x) = (&row[..n], &row[n..2 * n], &row[2 * n..]);
let bx: Vec<f32> = b.iter().zip(x).map(|(b, x)| b * x).collect();
let window = history(&bx);
assert_eq!(window.len(), l * n, "the window is l_cache rows of n_embd");
let out = &mut y[r * n..(r + 1) * n];
for ch in 0..n {
let taps = &self.conv[ch * l..(ch + 1) * l];
let mut acc = 0.0f32;
for (i, tap) in taps.iter().enumerate() {
acc += window[i * n + ch] * tap;
}
out[ch] = c[ch] * acc;
}
}
if rows == 1 {
self.out_proj.apply(&y)
} else {
self.out_proj.apply_batch(&y, rows)
}
}
}
pub fn window_from_history<'a>(
l_cache: usize,
n_embd: usize,
n_rows: usize,
row_at: impl Fn(usize) -> &'a [f32],
) -> Vec<f32> {
let mut window = vec![0.0f32; l_cache * n_embd];
for i in 0..l_cache {
let Some(row) = (n_rows + i).checked_sub(l_cache) else {
continue;
};
window[i * n_embd..(i + 1) * n_embd].copy_from_slice(row_at(row));
}
window
}
#[cfg(test)]
mod tests {
use super::*;
use frink_core::Tensor;
fn identity(n: usize) -> WeightMatrix {
let mut v = vec![0.0f32; n * n];
for i in 0..n {
v[i * n + i] = 1.0;
}
WeightMatrix::F32(Tensor::new(v, vec![n, n]))
}
#[test]
fn window_is_zero_padded_then_the_last_l_rows() {
let hist: Vec<Vec<f32>> = vec![vec![1.0, 10.0], vec![2.0, 20.0], vec![3.0, 30.0]];
let w = window_from_history(3, 2, 1, |i| &hist[i]);
assert_eq!(w, vec![0.0, 0.0, 0.0, 0.0, 1.0, 10.0]);
let w = window_from_history(3, 2, 3, |i| &hist[i]);
assert_eq!(w, vec![1.0, 10.0, 2.0, 20.0, 3.0, 30.0]);
let w = window_from_history(2, 2, 3, |i| &hist[i]);
assert_eq!(w, vec![2.0, 20.0, 3.0, 30.0]);
}
#[test]
fn newest_input_is_on_the_last_tap() {
let n = 2;
let mut in_v = vec![0.0f32; 3 * n * n];
for blk in 0..3 {
for i in 0..n {
in_v[(blk * n + i) * n + i] = 1.0;
}
}
let sc = ShortConv {
conv: vec![1.0, 2.0, 3.0, 0.0, 0.0, 1.0],
l_cache: 3,
in_proj: WeightMatrix::F32(Tensor::new(in_v, vec![3 * n, n])),
out_proj: identity(n),
};
let mut hist: Vec<Vec<f32>> = Vec::new();
let inputs = [[1.0f32, 1.0], [2.0, 1.0], [1.0, 1.0]];
let flat: Vec<f32> = inputs.concat();
let out = sc.forward_rows(&flat, 3, |bx| {
hist.push(bx.to_vec());
window_from_history(3, n, hist.len(), |i| &hist[i])
});
assert_eq!(out, vec![3.0, 1.0, 28.0, 1.0, 12.0, 1.0]);
let mut hist2: Vec<Vec<f32>> = Vec::new();
let mut one_at_a_time = Vec::new();
for row in inputs {
one_at_a_time.extend(sc.forward_rows(&row, 1, |bx| {
hist2.push(bx.to_vec());
window_from_history(3, n, hist2.len(), |i| &hist2[i])
}));
}
assert_eq!(one_at_a_time, out);
}
#[test]
fn the_table_is_the_two_lfm2_graphs() {
assert!(is_shortconv_architecture("lfm2"));
assert!(is_shortconv_architecture("lfm2moe"));
assert!(!is_shortconv_architecture("deci"));
assert!(!is_shortconv_architecture("jamba"));
}
}