use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
use crate::region::wrap_anonymous;
const INT4_LINEAR_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
pub fn linear_4bit(
x: &str,
w_packed: &str,
b: &str,
out: &str,
in_dim: u32,
out_dim: u32,
) -> Result<Program, String> {
if in_dim == 0 {
return Err("Fix: linear_4bit in_dim=0 is invalid: empty reduction".to_string());
}
if out_dim == 0 {
return Err("Fix: linear_4bit out_dim=0 is invalid: empty output".to_string());
}
if in_dim % 8 != 0 {
return Err(format!(
"Fix: linear_4bit in_dim={in_dim} is not divisible by 8; pad weights to a multiple of 8."
));
}
let u32s_per_col = in_dim / 8;
let total_u32s = u32s_per_col.checked_mul(out_dim).ok_or_else(|| {
"Fix: linear_4bit in_dim/8 * out_dim overflows u32; reduce dimensions.".to_string()
})?;
let i = Expr::var("i");
let k = Expr::var("k");
let packed_idx = Expr::add(
Expr::mul(Expr::div(k.clone(), Expr::u32(8)), Expr::u32(out_dim)),
i.clone(),
);
let shift = Expr::mul(Expr::rem(k.clone(), Expr::u32(8)), Expr::u32(4));
let nibble = Expr::bitand(
Expr::shr(Expr::load(w_packed, packed_idx), shift),
Expr::u32(0xF),
);
let weight_f32 = Expr::cast(DataType::F32, nibble);
let body = vec![
Node::let_bind("i", Expr::InvocationId { axis: 0 }),
Node::if_then(
Expr::lt(i.clone(), Expr::u32(out_dim)),
vec![
Node::let_bind("acc", Expr::load(b, i.clone())),
Node::loop_for(
"k",
Expr::u32(0),
Expr::u32(in_dim),
vec![Node::assign(
"acc",
Expr::add(
Expr::var("acc"),
Expr::mul(Expr::load(x, k.clone()), weight_f32.clone()),
),
)],
),
Node::Store {
buffer: out.into(),
index: i,
value: Expr::var("acc"),
},
],
),
];
Ok(Program::wrapped(
vec![
BufferDecl::storage(x, 0, BufferAccess::ReadOnly, DataType::F32).with_count(in_dim),
BufferDecl::storage(w_packed, 1, BufferAccess::ReadOnly, DataType::U32)
.with_count(total_u32s),
BufferDecl::storage(b, 2, BufferAccess::ReadOnly, DataType::F32).with_count(out_dim),
BufferDecl::output(out, 3, DataType::F32).with_count(out_dim),
],
INT4_LINEAR_WORKGROUP_SIZE,
vec![wrap_anonymous("vyre-libs::nn::linear_4bit", body)],
))
}
#[cfg(test)]
mod tests {
use vyre_reference::value::Value;
use super::linear_4bit;
use crate::fixture_bytes::{f32_bytes, u32_bytes};
#[test]
fn linear_4bit_matches_unpack_then_linear() {
let x = f32_bytes(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
let col0 = 0x8765_4321u32;
let col1 = 0x0000_0000u32;
let w = u32_bytes(&[col0, col1]);
let b = f32_bytes(&[0.0, 0.0]);
let out_size = 2usize * 4;
let program = linear_4bit("x", "w", "b", "out", 8, 2).unwrap();
let outputs = vyre_reference::reference_eval(
&program,
&[
Value::from(x),
Value::from(w),
Value::from(b),
Value::from(vec![0u8; out_size]),
],
)
.expect("Fix: reference eval must succeed");
let out_vals: Vec<f32> =
vyre_primitives::wire::decode_f32_le_bytes_all(&outputs[0].to_bytes());
assert!(
(out_vals[0] - 204.0).abs() < 1e-4,
"expected 204.0, got {}",
out_vals[0]
);
assert!(
(out_vals[1] - 0.0).abs() < 1e-4,
"expected 0.0, got {}",
out_vals[1]
);
}
#[test]
fn linear_4bit_rejects_indivisible_in_dim() {
let err = linear_4bit("x", "w", "b", "out", 7, 4).unwrap_err();
assert!(
err.contains("divisible by 8"),
"error must mention divisibility: {err}"
);
}
}