use fuel_ir::Shape;
use fuel_ir::error::Result;
use fuel_tensor::Device;
use fuel_tensor::{lazy::Tensor, lazy::WeightStorage};
use std::sync::Arc;
pub use crate::models::lazy_phi::PhiActivation as MixFormerActivation;
#[derive(Debug, Clone, PartialEq)]
pub struct MixFormerConfig {
pub vocab_size: usize,
pub hidden_size: usize,
pub n_inner: Option<usize>,
pub num_hidden_layers: usize,
pub num_attention_heads: usize,
pub rotary_dim: usize,
pub layer_norm_eps: f64,
pub max_position_embeddings: usize,
pub rope_theta: f64,
pub hidden_activation: MixFormerActivation,
pub tie_word_embeddings: bool,
}
impl MixFormerConfig {
pub fn head_dim(&self) -> usize {
self.hidden_size / self.num_attention_heads
}
pub fn inner_dim(&self) -> usize {
self.n_inner.unwrap_or(4 * self.hidden_size)
}
pub fn effective_rotary_dim(&self) -> usize {
self.rotary_dim & !1
}
pub fn phi_1_5() -> Self {
Self {
vocab_size: 51200,
hidden_size: 2048,
n_inner: None, num_hidden_layers: 24,
num_attention_heads: 32,
rotary_dim: 32,
layer_norm_eps: 1e-5,
max_position_embeddings: 2048,
rope_theta: 10_000.0,
hidden_activation: MixFormerActivation::GeluPytorchTanh, tie_word_embeddings: false,
}
}
}
#[derive(Debug, Clone)]
pub struct MixFormerLayerWeights {
pub ln_gain: Arc<[f32]>,
pub ln_bias: Arc<[f32]>,
pub wqkv: WeightStorage,
pub wqkv_bias: Arc<[f32]>,
pub out_proj: WeightStorage,
pub out_proj_bias: Arc<[f32]>,
pub fc1: WeightStorage,
pub fc1_bias: Arc<[f32]>,
pub fc2: WeightStorage,
pub fc2_bias: Arc<[f32]>,
}
#[derive(Debug, Clone)]
pub struct MixFormerWeights {
pub token_embedding: Arc<[f32]>,
pub layers: Vec<MixFormerLayerWeights>,
pub final_ln_gain: Arc<[f32]>,
pub final_ln_bias: Arc<[f32]>,
pub lm_head: Option<WeightStorage>, pub lm_head_bias: Arc<[f32]>,
}
#[derive(Debug, Clone)]
pub struct MixFormerModel {
pub config: MixFormerConfig,
pub weights: MixFormerWeights,
}
impl MixFormerModel {
pub fn forward(&self, tokens: &[u32], start_pos: usize) -> Result<Tensor> {
let cfg = &self.config;
let weights = &self.weights;
let seq = tokens.len();
let _batch = 1;
assert!(seq > 0, "MixFormerModel: tokens must be non-empty");
let h = Tensor::embed_tokens(
weights.token_embedding.clone(),
cfg.vocab_size,
cfg.hidden_size,
tokens,
&Device::cpu(),
)?;
self.forward_embeds(&h, start_pos)
}
pub fn forward_embeds(&self, embeds: &Tensor, start_pos: usize) -> Result<Tensor> {
let cfg = &self.config;
let weights = &self.weights;
let dims = embeds.shape();
let dims = dims.dims();
assert_eq!(dims.len(), 3, "embeds must be rank 3 [b, seq, hidden]");
let seq = dims[1];
let head_dim = cfg.head_dim();
if cfg.num_attention_heads * head_dim != cfg.hidden_size {
return Err(fuel_ir::error::Error::Msg(
"MixFormerConfig: hidden_size must be divisible by num_attention_heads".into(),
)
.bt());
}
let rotary_dim = cfg.effective_rotary_dim();
assert!(
rotary_dim > 0 && rotary_dim <= head_dim,
"MixFormerConfig: rotary_dim ({rotary_dim}) out of (0, head_dim ({head_dim})]",
);
let mut h = embeds.clone();
let (rope_cos, rope_sin) = h.rope_tables_const(cfg.rope_theta, start_pos, seq, rotary_dim);
for layer in &weights.layers {
h = self.apply_layer(&h, layer, &rope_cos, &rope_sin, head_dim, rotary_dim)?;
}
let h_norm = h.layer_norm_affine(
std::sync::Arc::clone(&weights.final_ln_gain),
std::sync::Arc::clone(&weights.final_ln_bias),
cfg.layer_norm_eps,
)?;
let lm_w = match &weights.lm_head {
Some(w) => w.clone(),
None => WeightStorage::F32(weights.token_embedding.clone()),
};
let logits = lm_w.apply_linear(&h_norm, cfg.hidden_size, cfg.vocab_size)?;
let bias_t = h.const_f32_like(
Arc::clone(&weights.lm_head_bias),
Shape::from_dims(&[cfg.vocab_size]),
)?;
logits.broadcast_add(&bias_t)
}
pub fn forward_hidden(&self, tokens: &[u32], start_pos: usize) -> Result<Tensor> {
let cfg = &self.config;
let weights = &self.weights;
let seq = tokens.len();
let _batch = 1;
assert!(
seq > 0,
"MixFormerModel::forward_hidden: tokens must be non-empty"
);
let h = Tensor::embed_tokens(
weights.token_embedding.clone(),
cfg.vocab_size,
cfg.hidden_size,
tokens,
&Device::cpu(),
)?;
self.forward_hidden_embeds(&h, start_pos)
}
pub fn forward_hidden_embeds(&self, embeds: &Tensor, start_pos: usize) -> Result<Tensor> {
let cfg = &self.config;
let weights = &self.weights;
let dims = embeds.shape();
let dims = dims.dims();
assert_eq!(dims.len(), 3, "embeds must be rank 3 [b, seq, hidden]");
let seq = dims[1];
let head_dim = cfg.head_dim();
let rotary_dim = cfg.effective_rotary_dim();
let mut h = embeds.clone();
let (rope_cos, rope_sin) = h.rope_tables_const(cfg.rope_theta, start_pos, seq, rotary_dim);
for layer in &weights.layers {
h = self.apply_layer(&h, layer, &rope_cos, &rope_sin, head_dim, rotary_dim)?;
}
h.layer_norm_affine(
std::sync::Arc::clone(&weights.final_ln_gain),
std::sync::Arc::clone(&weights.final_ln_bias),
cfg.layer_norm_eps,
)
}
fn apply_layer(
&self,
x: &Tensor,
layer: &MixFormerLayerWeights,
rope_cos: &Tensor,
rope_sin: &Tensor,
head_dim: usize,
rotary_dim: usize,
) -> Result<Tensor> {
let cfg = &self.config;
let x_shape = x.shape();
let dims = x_shape.dims();
let batch = dims[0];
let seq = dims[1];
let h = cfg.hidden_size;
let n_heads = cfg.num_attention_heads;
let x_norm = x.layer_norm_affine(
std::sync::Arc::clone(&layer.ln_gain),
std::sync::Arc::clone(&layer.ln_bias),
cfg.layer_norm_eps,
)?;
let qkv_lin = layer.wqkv.apply_linear(&x_norm, h, 3 * h)?;
let qkv_b_t =
x_norm.const_f32_like(Arc::clone(&layer.wqkv_bias), Shape::from_dims(&[3 * h]))?;
let qkv = qkv_lin.broadcast_add(&qkv_b_t)?;
let q = qkv.slice(2_usize, 0, h)?;
let k = qkv.slice(2_usize, h, h)?;
let v = qkv.slice(2_usize, 2 * h, h)?;
let _ = batch;
let q = q.split_heads(n_heads, head_dim)?;
let k = k.split_heads(n_heads, head_dim)?;
let v = v.split_heads(n_heads, head_dim)?;
let q_r = q.rope_partial(rope_cos, rope_sin, rotary_dim)?;
let k_r = k.rope_partial(rope_cos, rope_sin, rotary_dim)?;
let k_t = k_r.transpose()?;
let scale = 1.0_f64 / (head_dim as f64).sqrt();
let scores = q_r.matmul(&k_t)?;
let scores_scaled = scores.mul_scalar(scale);
let mask = Tensor::additive_causal_mask_like(x, seq)
.reshape(Shape::from_dims(&[1, 1, seq, seq]))?;
let scores_masked = scores_scaled.broadcast_add(&mask)?;
let attn = scores_masked.softmax_last_dim()?;
let attn_v = attn.matmul(&v)?;
let merged = attn_v.merge_heads()?;
let attn_out_lin = layer.out_proj.apply_linear(&merged, h, h)?;
let out_bias_t =
x.const_f32_like(Arc::clone(&layer.out_proj_bias), Shape::from_dims(&[h]))?;
let attn_out = attn_out_lin.broadcast_add(&out_bias_t)?;
let inner = cfg.inner_dim();
let fc1_lin = layer.fc1.apply_linear(&x_norm, h, inner)?;
let fc1_b_t = x.const_f32_like(Arc::clone(&layer.fc1_bias), Shape::from_dims(&[inner]))?;
let fc1_out = fc1_lin.broadcast_add(&fc1_b_t)?;
let activated = match cfg.hidden_activation {
MixFormerActivation::Gelu => fc1_out.gelu(),
MixFormerActivation::GeluPytorchTanh => fc1_out.gelu_tanh(),
};
let fc2_lin = layer.fc2.apply_linear(&activated, inner, h)?;
let fc2_b_t = x.const_f32_like(Arc::clone(&layer.fc2_bias), Shape::from_dims(&[h]))?;
let mlp_out = fc2_lin.broadcast_add(&fc2_b_t)?;
x.add(&attn_out)?.add(&mlp_out)
}
}
impl MixFormerWeights {
pub fn load_from_mmapped(
st: &fuel_loaders::safetensors::MmapedSafetensors,
cfg: &MixFormerConfig,
) -> Result<Self> {
use fuel_tensor::{
lazy::load_tensor_as_f32, lazy::load_transposed_matrix_preserve_dtype as ltm,
};
let h = cfg.hidden_size;
let inter = cfg.inner_dim();
let token_embedding = Arc::from(load_tensor_as_f32(st, "transformer.embd.wte.weight")?);
let mut layers = Vec::with_capacity(cfg.num_hidden_layers);
for i in 0..cfg.num_hidden_layers {
let p = format!("transformer.h.{i}");
let ln_gain = Arc::from(load_tensor_as_f32(st, &format!("{p}.ln.weight"))?);
let ln_bias = Arc::from(load_tensor_as_f32(st, &format!("{p}.ln.bias"))?);
let wqkv = ltm(st, &format!("{p}.mixer.Wqkv.weight"), 3 * h, h)?;
let wqkv_bias = Arc::from(load_tensor_as_f32(st, &format!("{p}.mixer.Wqkv.bias"))?);
let out_proj = ltm(st, &format!("{p}.mixer.out_proj.weight"), h, h)?;
let out_proj_bias =
Arc::from(load_tensor_as_f32(st, &format!("{p}.mixer.out_proj.bias"))?);
let fc1 = ltm(st, &format!("{p}.mlp.fc1.weight"), inter, h)?;
let fc1_bias = Arc::from(load_tensor_as_f32(st, &format!("{p}.mlp.fc1.bias"))?);
let fc2 = ltm(st, &format!("{p}.mlp.fc2.weight"), h, inter)?;
let fc2_bias = Arc::from(load_tensor_as_f32(st, &format!("{p}.mlp.fc2.bias"))?);
layers.push(MixFormerLayerWeights {
ln_gain,
ln_bias,
wqkv,
wqkv_bias,
out_proj,
out_proj_bias,
fc1,
fc1_bias,
fc2,
fc2_bias,
});
}
let final_ln_gain = Arc::from(load_tensor_as_f32(st, "lm_head.ln.weight")?);
let final_ln_bias = Arc::from(load_tensor_as_f32(st, "lm_head.ln.bias")?);
let lm_head = if cfg.tie_word_embeddings {
None
} else {
Some(ltm(st, "lm_head.linear.weight", cfg.vocab_size, h)?)
};
let lm_head_bias = load_tensor_as_f32(st, "lm_head.linear.bias")
.ok()
.map(Arc::from)
.unwrap_or_else(|| Arc::from(vec![0.0_f32; cfg.vocab_size]));
Ok(Self {
token_embedding,
layers,
final_ln_gain,
final_ln_bias,
lm_head,
lm_head_bias,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tiny_weights(cfg: &MixFormerConfig) -> MixFormerWeights {
let mut s: u32 = 42424;
let next = move || -> f32 {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.05
};
let vec_of = |n: usize, next: &mut dyn FnMut() -> f32| -> Arc<[f32]> {
Arc::from((0..n).map(|_| next()).collect::<Vec<_>>())
};
let h = cfg.hidden_size;
let inner = cfg.inner_dim();
let mut nb: Box<dyn FnMut() -> f32> = Box::new(next);
let token_embedding = vec_of(cfg.vocab_size * h, &mut *nb);
let layers: Vec<MixFormerLayerWeights> = (0..cfg.num_hidden_layers)
.map(|_| MixFormerLayerWeights {
ln_gain: Arc::from(vec![1.0_f32; h]),
ln_bias: Arc::from(vec![0.0_f32; h]),
wqkv: WeightStorage::F32(vec_of(h * (3 * h), &mut *nb)),
wqkv_bias: vec_of(3 * h, &mut *nb),
out_proj: WeightStorage::F32(vec_of(h * h, &mut *nb)),
out_proj_bias: vec_of(h, &mut *nb),
fc1: WeightStorage::F32(vec_of(h * inner, &mut *nb)),
fc1_bias: vec_of(inner, &mut *nb),
fc2: WeightStorage::F32(vec_of(inner * h, &mut *nb)),
fc2_bias: vec_of(h, &mut *nb),
})
.collect();
let final_ln_gain = Arc::from(vec![1.0_f32; h]);
let final_ln_bias = Arc::from(vec![0.0_f32; h]);
let lm_head = if cfg.tie_word_embeddings {
None
} else {
Some(WeightStorage::F32(vec_of(h * cfg.vocab_size, &mut *nb)))
};
let lm_head_bias = vec_of(cfg.vocab_size, &mut *nb);
MixFormerWeights {
token_embedding,
layers,
final_ln_gain,
final_ln_bias,
lm_head,
lm_head_bias,
}
}
fn tiny_config() -> MixFormerConfig {
MixFormerConfig {
vocab_size: 32,
hidden_size: 16,
n_inner: Some(32),
num_hidden_layers: 2,
num_attention_heads: 4,
rotary_dim: 2,
layer_norm_eps: 1e-5,
max_position_embeddings: 64,
rope_theta: 10_000.0,
hidden_activation: MixFormerActivation::GeluPytorchTanh,
tie_word_embeddings: false,
}
}
#[test]
fn forward_shape_and_finite() {
let cfg = tiny_config();
let model = MixFormerModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let tokens: Vec<u32> = vec![1, 2, 3, 4, 5];
let logits = model.forward(&tokens, 0).unwrap();
assert_eq!(logits.shape().dims(), &[1, tokens.len(), cfg.vocab_size]);
for &v in &logits.realize_f32() {
assert!(v.is_finite(), "got non-finite logit {v}");
}
}
#[test]
fn n_inner_defaults_to_4x_hidden() {
let cfg = MixFormerConfig {
n_inner: None,
..tiny_config()
};
assert_eq!(cfg.inner_dim(), 4 * cfg.hidden_size);
}
#[test]
fn tied_lm_head() {
let cfg = MixFormerConfig {
tie_word_embeddings: true,
..tiny_config()
};
let weights = tiny_weights(&cfg);
assert!(weights.lm_head.is_none());
let model = MixFormerModel {
config: cfg.clone(),
weights,
};
let logits = model.forward(&[2, 3, 5], 0).unwrap().realize_f32();
assert_eq!(logits.len(), 3 * cfg.vocab_size);
}
#[test]
#[allow(clippy::needless_range_loop)]
fn fused_wqkv_matches_independent_qkv_slicing() {
let cfg = MixFormerConfig {
num_hidden_layers: 1,
..tiny_config()
};
let mut weights = tiny_weights(&cfg);
let h = cfg.hidden_size;
let wqkv_len = match &weights.layers[0].wqkv {
WeightStorage::F32(v) => v.len(),
_ => panic!("expected F32"),
};
assert_eq!(wqkv_len, h * (3 * h));
assert_eq!(weights.layers[0].wqkv_bias.len(), 3 * h);
let baseline = MixFormerModel {
config: cfg.clone(),
weights: weights.clone(),
};
let a = baseline.forward(&[1, 2, 3], 0).unwrap().realize_f32();
let mut wqkv_no_v = match &weights.layers[0].wqkv {
WeightStorage::F32(v) => v.to_vec(),
_ => panic!(),
};
for row in 0..h {
for j in 2 * h..3 * h {
wqkv_no_v[row * (3 * h) + j] = 0.0;
}
}
let mut wqkv_bias_no_v: Vec<f32> = weights.layers[0].wqkv_bias.to_vec();
for j in 2 * h..3 * h {
wqkv_bias_no_v[j] = 0.0;
}
weights.layers[0].wqkv = WeightStorage::F32(Arc::from(wqkv_no_v));
weights.layers[0].wqkv_bias = Arc::from(wqkv_bias_no_v);
let m_no_v = MixFormerModel {
config: cfg.clone(),
weights,
};
let b = m_no_v.forward(&[1, 2, 3], 0).unwrap().realize_f32();
let mut max_diff = 0.0_f32;
for (av, bv) in a.iter().zip(b.iter()) {
max_diff = max_diff.max((av - bv).abs());
}
assert!(
max_diff > 1e-6,
"zeroing V columns of fused Wqkv must change attention output, max_diff = {max_diff}"
);
}
#[test]
fn head_dim_mismatch_declines_rather_than_panicking() {
let valid = tiny_config();
let tokens: Vec<u32> = vec![1, 2, 3];
let good = MixFormerModel {
config: valid.clone(),
weights: tiny_weights(&valid),
};
good.forward(&tokens, 0)
.expect("control: the conforming config must still run");
let mut bad = valid.clone();
bad.num_attention_heads += 1; let model = MixFormerModel {
config: bad,
weights: tiny_weights(&valid),
};
let err = model
.forward(&tokens, 0)
.expect_err("a non-divisible head count must DECLINE, not panic");
let msg = format!("{err}");
assert!(
msg.contains("num_attention_heads") && msg.contains("hidden_size"),
"the decline must name the relation it rejected, got: {msg}"
);
}
}