use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use candle_core::{DType, Device, Result, Tensor, D};
use candle_nn::{LayerNorm, Linear, Module, VarBuilder};
use crate::config::AxiomConfig;
#[derive(Debug, Clone)]
pub struct MlpState {
pub w1: Tensor,
pub w2: Tensor,
}
impl MlpState {
pub fn init(d_model: usize, hidden: usize, device: &Device) -> Result<Self> {
if d_model == 0 || hidden == 0 {
candle_core::bail!("MlpState dimensions must be non-zero (d_model={d_model}, hidden={hidden})");
}
let n = d_model.max(hidden);
let eye = Tensor::eye(n, DType::F32, device)?;
let w1 = eye.narrow(0, 0, hidden)?.narrow(1, 0, d_model)?.contiguous()?;
let w2 = eye.narrow(0, 0, d_model)?.narrow(1, 0, hidden)?.contiguous()?;
Ok(Self { w1, w2 })
}
}
pub struct NativeTTTMlpBlock {
w_q: Linear,
w_k: Linear,
w_v: Linear,
layer_norm: LayerNorm,
hidden: usize,
d_model: usize,
inner_lr: Arc<AtomicU32>,
}
impl NativeTTTMlpBlock {
#[allow(dead_code)]
pub fn new(vs: VarBuilder, config: AxiomConfig, hidden: usize) -> Result<Self> {
let inner_lr = Arc::new(AtomicU32::new(config.lr_inner.to_bits()));
Self::new_with_shared_lr(vs, config, hidden, inner_lr)
}
pub fn new_with_shared_lr(
vs: VarBuilder,
config: AxiomConfig,
hidden: usize,
inner_lr: Arc<AtomicU32>,
) -> Result<Self> {
let d = config.d_model;
Ok(Self {
w_q: candle_nn::linear_no_bias(d, d, vs.pp("w_q"))?,
w_k: candle_nn::linear_no_bias(d, d, vs.pp("w_k"))?,
w_v: candle_nn::linear_no_bias(d, d, vs.pp("w_v"))?,
layer_norm: candle_nn::layer_norm_no_bias(d, config.norm_eps as f64, vs.pp("layer_norm"))?,
hidden,
d_model: d,
inner_lr,
})
}
pub fn hidden(&self) -> usize {
self.hidden
}
pub fn init_state(&self, device: &Device) -> Result<MlpState> {
MlpState::init(self.d_model, self.hidden, device)
}
pub fn reconstruction_error(&self, x: &Tensor, state: &MlpState) -> Result<f32> {
let k = self.w_k.forward(x)?.squeeze(0)?;
let v = self.w_v.forward(x)?.squeeze(0)?;
let z = state.w1.matmul(&k.unsqueeze(1)?)?.squeeze(D::Minus1)?;
let a = z.tanh()?;
let pred = state.w2.matmul(&a.unsqueeze(1)?)?.squeeze(D::Minus1)?;
pred.sub(&v)?.sqr()?.sum_all()?.to_scalar::<f32>()
}
pub fn forward_native(&self, x: &Tensor, state: &mut MlpState) -> Result<Tensor> {
let q = self.w_q.forward(x)?; let k = self.w_k.forward(x)?; let v = self.w_v.forward(x)?;
let k_vec = k.squeeze(0)?; let v_vec = v.squeeze(0)?;
let k_col = k_vec.unsqueeze(1)?; let z = state.w1.matmul(&k_col)?.squeeze(D::Minus1)?; let a = z.tanh()?; let a_col = a.unsqueeze(1)?; let pred = state.w2.matmul(&a_col)?.squeeze(D::Minus1)?; let e = pred.sub(&v_vec)?;
let eta = f32::from_bits(self.inner_lr.load(Ordering::Relaxed));
let lr = Tensor::new(eta, x.device())?;
let dw2 = e.unsqueeze(1)?.matmul(&a.unsqueeze(0)?)?;
let da = state.w2.t()?.matmul(&e.unsqueeze(1)?)?.squeeze(D::Minus1)?;
let one_minus_a2 = a.sqr()?.affine(-1.0, 1.0)?; let dz = da.mul(&one_minus_a2)?; let dw1 = dz.unsqueeze(1)?.matmul(&k_vec.unsqueeze(0)?)?;
state.w2 = state.w2.sub(&dw2.broadcast_mul(&lr)?)?.detach();
state.w1 = state.w1.sub(&dw1.broadcast_mul(&lr)?)?.detach();
let q_col = q.squeeze(0)?.unsqueeze(1)?; let zq = state.w1.matmul(&q_col)?.squeeze(D::Minus1)?; let aq = zq.tanh()?.unsqueeze(1)?; let out = state.w2.matmul(&aq)?.squeeze(D::Minus1)?.unsqueeze(0)?;
self.layer_norm.forward(&out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use candle_nn::{VarBuilder, VarMap};
fn make_block(d_model: usize, hidden: usize) -> (NativeTTTMlpBlock, Device) {
let device = Device::Cpu;
let config = AxiomConfig {
d_model,
n_layers: 1,
vocab_size: 16,
lr_inner: 1e-2,
norm_eps: 1e-6,
};
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, DType::F32, &device);
let block = NativeTTTMlpBlock::new(vb.pp("mlp"), config, hidden).unwrap();
(block, device)
}
#[test]
fn state_init_has_expected_shapes() {
let st = MlpState::init(8, 12, &Device::Cpu).unwrap();
assert_eq!(st.w1.dims(), &[12, 8]);
assert_eq!(st.w2.dims(), &[8, 12]);
}
#[test]
fn state_init_rejects_zero_dimensions() {
assert!(MlpState::init(0, 4, &Device::Cpu).is_err());
assert!(MlpState::init(4, 0, &Device::Cpu).is_err());
assert!(MlpState::init(4, 4, &Device::Cpu).is_ok());
}
#[test]
fn forward_output_shape_and_finite() {
let d = 16usize;
let (block, device) = make_block(d, d);
let mut st = block.init_state(&device).unwrap();
let x = Tensor::randn(0f32, 1f32, (1usize, d), &device).unwrap();
let out = block.forward_native(&x, &mut st).unwrap();
assert_eq!(out.dims(), &[1, d]);
let ov: Vec<f32> = out.flatten_all().unwrap().to_vec1::<f32>().unwrap();
assert!(ov.iter().all(|v| v.is_finite()));
}
#[test]
fn both_layers_update() {
let d = 12usize;
let (block, device) = make_block(d, d);
let mut st = block.init_state(&device).unwrap();
let w1_before = st.w1.clone();
let w2_before = st.w2.clone();
let x = Tensor::randn(0f32, 1f32, (1usize, d), &device).unwrap();
let _ = block.forward_native(&x, &mut st).unwrap();
let d1 = st.w1.sub(&w1_before).unwrap().sqr().unwrap().sum_all().unwrap()
.to_scalar::<f32>().unwrap();
let d2 = st.w2.sub(&w2_before).unwrap().sqr().unwrap().sum_all().unwrap()
.to_scalar::<f32>().unwrap();
assert!(d1 > 0.0, "W1 must update");
assert!(d2 > 0.0, "W2 must update");
}
#[test]
fn rectangular_hidden_is_supported() {
let d = 8usize;
let (block, device) = make_block(d, 20);
assert_eq!(block.hidden(), 20);
let mut st = block.init_state(&device).unwrap();
let x = Tensor::randn(0f32, 1f32, (1usize, d), &device).unwrap();
let out = block.forward_native(&x, &mut st).unwrap();
assert_eq!(out.dims(), &[1, d]);
}
#[test]
fn repeated_token_drives_reconstruction_error_down() {
let d = 16usize;
let (block, device) = make_block(d, d);
let mut st = block.init_state(&device).unwrap();
let x = Tensor::randn(0f32, 1f32, (1usize, d), &device).unwrap();
let recon_err = |block: &NativeTTTMlpBlock, st: &MlpState| -> f32 {
let k = block.w_k.forward(&x).unwrap().squeeze(0).unwrap();
let v = block.w_v.forward(&x).unwrap().squeeze(0).unwrap();
let z = st.w1.matmul(&k.unsqueeze(1).unwrap()).unwrap().squeeze(D::Minus1).unwrap();
let a = z.tanh().unwrap();
let pred = st.w2.matmul(&a.unsqueeze(1).unwrap()).unwrap().squeeze(D::Minus1).unwrap();
pred.sub(&v).unwrap().sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap()
};
let before = recon_err(&block, &st);
for _ in 0..40 {
let _ = block.forward_native(&x, &mut st).unwrap();
}
let after = recon_err(&block, &st);
assert!(
after < before,
"MLP state must reduce reconstruction error (before {before}, after {after})"
);
}
}