use fuel_ir::Shape;
use fuel_ir::error::Result;
use fuel_tensor::Device;
use fuel_tensor::{lazy::Tensor, lazy::WeightStorage};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarianActivation {
Relu,
Silu,
Gelu,
GeluPytorchTanh,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MarianConfig {
pub vocab_size: usize,
pub decoder_vocab_size: Option<usize>,
pub max_position_embeddings: usize,
pub d_model: usize,
pub encoder_layers: usize,
pub encoder_ffn_dim: usize,
pub encoder_attention_heads: usize,
pub decoder_layers: usize,
pub decoder_ffn_dim: usize,
pub decoder_attention_heads: usize,
pub scale_embedding: bool,
pub share_encoder_decoder_embeddings: bool,
pub activation_function: MarianActivation,
}
impl MarianConfig {
pub fn target_vocab_size(&self) -> usize {
self.decoder_vocab_size.unwrap_or(self.vocab_size)
}
pub fn encoder_head_dim(&self) -> usize {
self.d_model / self.encoder_attention_heads
}
pub fn decoder_head_dim(&self) -> usize {
self.d_model / self.decoder_attention_heads
}
pub fn opus_mt_tc_big_fr_en() -> Self {
Self {
vocab_size: 53017,
decoder_vocab_size: Some(53017),
max_position_embeddings: 1024,
d_model: 1024,
encoder_layers: 6,
encoder_ffn_dim: 4096,
encoder_attention_heads: 16,
decoder_layers: 6,
decoder_ffn_dim: 4096,
decoder_attention_heads: 16,
scale_embedding: true,
share_encoder_decoder_embeddings: true,
activation_function: MarianActivation::Relu,
}
}
}
#[derive(Debug, Clone)]
pub struct MarianAttentionWeights {
pub q_proj: WeightStorage,
pub q_proj_bias: Arc<[f32]>,
pub k_proj: WeightStorage,
pub k_proj_bias: Arc<[f32]>,
pub v_proj: WeightStorage,
pub v_proj_bias: Arc<[f32]>,
pub out_proj: WeightStorage,
pub out_proj_bias: Arc<[f32]>,
}
#[derive(Debug, Clone)]
pub struct MarianEncoderLayerWeights {
pub self_attn: MarianAttentionWeights,
pub self_attn_ln_gain: Arc<[f32]>,
pub self_attn_ln_bias: Arc<[f32]>,
pub fc1: WeightStorage,
pub fc1_bias: Arc<[f32]>,
pub fc2: WeightStorage,
pub fc2_bias: Arc<[f32]>,
pub final_ln_gain: Arc<[f32]>,
pub final_ln_bias: Arc<[f32]>,
}
#[derive(Debug, Clone)]
pub struct MarianDecoderLayerWeights {
pub self_attn: MarianAttentionWeights,
pub self_attn_ln_gain: Arc<[f32]>,
pub self_attn_ln_bias: Arc<[f32]>,
pub encoder_attn: MarianAttentionWeights,
pub encoder_attn_ln_gain: Arc<[f32]>,
pub encoder_attn_ln_bias: Arc<[f32]>,
pub fc1: WeightStorage,
pub fc1_bias: Arc<[f32]>,
pub fc2: WeightStorage,
pub fc2_bias: Arc<[f32]>,
pub final_ln_gain: Arc<[f32]>,
pub final_ln_bias: Arc<[f32]>,
}
#[derive(Debug, Clone)]
pub struct MarianWeights {
pub shared_embedding: Arc<[f32]>,
pub encoder_layers: Vec<MarianEncoderLayerWeights>,
pub decoder_layers: Vec<MarianDecoderLayerWeights>,
pub final_logits_bias: Arc<[f32]>,
}
#[derive(Debug, Clone)]
pub struct MarianModel {
pub config: MarianConfig,
pub weights: MarianWeights,
}
impl MarianModel {
pub fn forward(&self, src_tokens: &[u32], tgt_tokens: &[u32]) -> Result<Tensor> {
let cfg = &self.config;
assert!(!src_tokens.is_empty(), "src_tokens must be non-empty");
assert!(!tgt_tokens.is_empty(), "tgt_tokens must be non-empty");
assert!(
cfg.share_encoder_decoder_embeddings,
"v1 only supports shared encoder+decoder embeddings",
);
if !cfg.d_model.is_multiple_of(cfg.encoder_attention_heads) {
return Err(fuel_ir::error::Error::Msg(format!(
"MarianConfig: d_model ({}) must be divisible by encoder_attention_heads ({})",
cfg.d_model, cfg.encoder_attention_heads,
)));
}
if !cfg.d_model.is_multiple_of(cfg.decoder_attention_heads) {
return Err(fuel_ir::error::Error::Msg(format!(
"MarianConfig: d_model ({}) must be divisible by decoder_attention_heads ({})",
cfg.d_model, cfg.decoder_attention_heads,
)));
}
let embed = Tensor::from_f32(
self.weights.shared_embedding.clone(),
Shape::from_dims(&[cfg.vocab_size, cfg.d_model]),
&Device::cpu(),
)?;
let pos_table = build_sinusoidal_table(cfg.max_position_embeddings, cfg.d_model)?;
let pos_lt_shape = Shape::from_dims(&[cfg.max_position_embeddings, cfg.d_model]);
let pos_full = embed.const_f32_like(Arc::from(pos_table), pos_lt_shape)?;
let enc_out = self.encode(&embed, &pos_full, src_tokens)?;
let dec_out = self.decode(&embed, &pos_full, tgt_tokens, &enc_out)?;
let lm_head = WeightStorage::F32(self.weights.shared_embedding.clone());
let target_vocab = cfg.target_vocab_size();
let logits = lm_head.apply_linear(&dec_out, cfg.d_model, target_vocab)?;
let bias_t = dec_out.const_f32_like(
Arc::clone(&self.weights.final_logits_bias),
Shape::from_dims(&[target_vocab]),
)?;
logits.broadcast_add(&bias_t)
}
pub fn forward_encoder(&self, src_tokens: &[u32]) -> Result<Tensor> {
let cfg = &self.config;
assert!(!src_tokens.is_empty(), "src_tokens must be non-empty");
assert!(
cfg.share_encoder_decoder_embeddings,
"v1 only supports shared encoder+decoder embeddings",
);
if !cfg.d_model.is_multiple_of(cfg.encoder_attention_heads) {
return Err(fuel_ir::error::Error::Msg(format!(
"MarianConfig: d_model ({}) must be divisible by encoder_attention_heads ({})",
cfg.d_model, cfg.encoder_attention_heads,
)));
}
let embed = Tensor::from_f32(
self.weights.shared_embedding.clone(),
Shape::from_dims(&[cfg.vocab_size, cfg.d_model]),
&Device::cpu(),
)?;
let pos_table = build_sinusoidal_table(cfg.max_position_embeddings, cfg.d_model)?;
let pos_lt_shape = Shape::from_dims(&[cfg.max_position_embeddings, cfg.d_model]);
let pos_full = embed.const_f32_like(Arc::from(pos_table), pos_lt_shape)?;
self.encode(&embed, &pos_full, src_tokens)
}
pub fn forward_encoder_embeds(&self, src_embeds: &Tensor) -> Result<Tensor> {
let cfg = &self.config;
let pos_table = build_sinusoidal_table(cfg.max_position_embeddings, cfg.d_model)?;
let pos_full = src_embeds.const_f32_like(
Arc::from(pos_table),
Shape::from_dims(&[cfg.max_position_embeddings, cfg.d_model]),
)?;
self.encode_from_embeds(src_embeds, &pos_full)
}
pub fn forward_decoder_embeds(&self, tgt_embeds: &Tensor, enc_out: &Tensor) -> Result<Tensor> {
let cfg = &self.config;
let pos_table = build_sinusoidal_table(cfg.max_position_embeddings, cfg.d_model)?;
let pos_full = enc_out.const_f32_like(
Arc::from(pos_table),
Shape::from_dims(&[cfg.max_position_embeddings, cfg.d_model]),
)?;
let dec_out = self.decode_from_embeds(tgt_embeds, &pos_full, enc_out)?;
let lm_head = WeightStorage::F32(self.weights.shared_embedding.clone());
let target_vocab = cfg.target_vocab_size();
let logits = lm_head.apply_linear(&dec_out, cfg.d_model, target_vocab)?;
let bias_t = enc_out.const_f32_like(
Arc::clone(&self.weights.final_logits_bias),
Shape::from_dims(&[target_vocab]),
)?;
logits.broadcast_add(&bias_t)
}
pub fn forward_decoder_hidden_embeds(
&self,
tgt_embeds: &Tensor,
enc_out: &Tensor,
) -> Result<Tensor> {
let cfg = &self.config;
let pos_table = build_sinusoidal_table(cfg.max_position_embeddings, cfg.d_model)?;
let pos_full = enc_out.const_f32_like(
Arc::from(pos_table),
Shape::from_dims(&[cfg.max_position_embeddings, cfg.d_model]),
)?;
self.decode_from_embeds(tgt_embeds, &pos_full, enc_out)
}
pub fn embed_tokens_anchored(&self, anchor: &Tensor, tokens: &[u32]) -> Result<Tensor> {
let cfg = &self.config;
anchor.embed_tokens_anchored(
self.weights.shared_embedding.clone(),
cfg.vocab_size,
cfg.d_model,
tokens,
)
}
pub fn forward_decoder(&self, tgt_tokens: &[u32], enc_out: &Tensor) -> Result<Tensor> {
let cfg = &self.config;
assert!(!tgt_tokens.is_empty(), "tgt_tokens must be non-empty");
assert!(
cfg.share_encoder_decoder_embeddings,
"v1 only supports shared encoder+decoder embeddings",
);
if !cfg.d_model.is_multiple_of(cfg.decoder_attention_heads) {
return Err(fuel_ir::error::Error::Msg(format!(
"MarianConfig: d_model ({}) must be divisible by decoder_attention_heads ({})",
cfg.d_model, cfg.decoder_attention_heads,
)));
}
let embed = enc_out.const_f32_like(
self.weights.shared_embedding.clone(),
Shape::from_dims(&[cfg.vocab_size, cfg.d_model]),
)?;
let pos_table = build_sinusoidal_table(cfg.max_position_embeddings, cfg.d_model)?;
let pos_full = enc_out.const_f32_like(
Arc::from(pos_table),
Shape::from_dims(&[cfg.max_position_embeddings, cfg.d_model]),
)?;
let dec_out = self.decode(&embed, &pos_full, tgt_tokens, enc_out)?;
let lm_head = WeightStorage::F32(self.weights.shared_embedding.clone());
let target_vocab = cfg.target_vocab_size();
let logits = lm_head.apply_linear(&dec_out, cfg.d_model, target_vocab)?;
let bias_t = enc_out.const_f32_like(
Arc::clone(&self.weights.final_logits_bias),
Shape::from_dims(&[target_vocab]),
)?;
logits.broadcast_add(&bias_t)
}
fn encode(&self, embed: &Tensor, pos_full: &Tensor, src_tokens: &[u32]) -> Result<Tensor> {
let cfg = &self.config;
let src_len = src_tokens.len();
let batch = 1;
let ids = embed.const_u32_like(src_tokens.to_vec(), Shape::from_dims(&[src_len]))?;
let src_embeds = embed
.index_select(0_usize, &ids)?
.reshape(Shape::from_dims(&[batch, src_len, cfg.d_model]))?;
self.encode_from_embeds(&src_embeds, pos_full)
}
fn encode_from_embeds(&self, src_embeds: &Tensor, pos_full: &Tensor) -> Result<Tensor> {
let cfg = &self.config;
let dims = src_embeds.shape();
let dims = dims.dims();
if dims.len() != 3 || dims[0] != 1 || dims[2] != cfg.d_model {
return Err(fuel_ir::error::Error::Msg(format!(
"MarianModel::forward_encoder_embeds: expected shape (1, src_len, d_model={}), got {:?}",
cfg.d_model, dims,
)).bt());
}
let src_len = dims[1];
let batch = dims[0];
if src_len == 0 {
return Err(fuel_ir::error::Error::Msg(
"MarianModel::forward_encoder_embeds: src_len must be > 0".into(),
)
.bt());
}
let mut x = src_embeds.clone();
if cfg.scale_embedding {
x = x.mul_scalar((cfg.d_model as f64).sqrt());
}
let pos_slice = pos_full
.slice(0_usize, 0, src_len)?
.reshape(Shape::from_dims(&[1, src_len, cfg.d_model]))?;
x = x.add(&pos_slice.broadcast_to(Shape::from_dims(&[batch, src_len, cfg.d_model]))?)?;
for layer in &self.weights.encoder_layers {
x = self.apply_encoder_layer(&x, layer)?;
}
Ok(x)
}
fn decode(
&self,
embed: &Tensor,
pos_full: &Tensor,
tgt_tokens: &[u32],
enc_out: &Tensor,
) -> Result<Tensor> {
let cfg = &self.config;
let tgt_len = tgt_tokens.len();
let batch = 1;
let ids = embed.const_u32_like(tgt_tokens.to_vec(), Shape::from_dims(&[tgt_len]))?;
let tgt_embeds = embed
.index_select(0_usize, &ids)?
.reshape(Shape::from_dims(&[batch, tgt_len, cfg.d_model]))?;
self.decode_from_embeds(&tgt_embeds, pos_full, enc_out)
}
fn decode_from_embeds(
&self,
tgt_embeds: &Tensor,
pos_full: &Tensor,
enc_out: &Tensor,
) -> Result<Tensor> {
let cfg = &self.config;
let dims = tgt_embeds.shape();
let dims = dims.dims();
if dims.len() != 3 || dims[0] != 1 || dims[2] != cfg.d_model {
return Err(fuel_ir::error::Error::Msg(format!(
"MarianModel::forward_decoder_embeds: expected tgt shape (1, tgt_len, d_model={}), got {:?}",
cfg.d_model, dims,
)).bt());
}
let tgt_len = dims[1];
let batch = dims[0];
if tgt_len == 0 {
return Err(fuel_ir::error::Error::Msg(
"MarianModel::forward_decoder_embeds: tgt_len must be > 0".into(),
)
.bt());
}
let mut x = tgt_embeds.clone();
if cfg.scale_embedding {
x = x.mul_scalar((cfg.d_model as f64).sqrt());
}
let pos_slice = pos_full
.slice(0_usize, 0, tgt_len)?
.reshape(Shape::from_dims(&[1, tgt_len, cfg.d_model]))?;
x = x.add(&pos_slice.broadcast_to(Shape::from_dims(&[batch, tgt_len, cfg.d_model]))?)?;
let mut mask_data = vec![0.0_f32; tgt_len * tgt_len];
for i in 0..tgt_len {
for j in (i + 1)..tgt_len {
mask_data[i * tgt_len + j] = f32::NEG_INFINITY;
}
}
let causal_mask =
x.const_f32_like(mask_data, Shape::from_dims(&[1, 1, tgt_len, tgt_len]))?;
for layer in &self.weights.decoder_layers {
x = self.apply_decoder_layer(&x, layer, enc_out, &causal_mask)?;
}
Ok(x)
}
fn apply_encoder_layer(&self, x: &Tensor, layer: &MarianEncoderLayerWeights) -> Result<Tensor> {
let cfg = &self.config;
let attn = self.attention(
x,
x,
&layer.self_attn,
cfg.encoder_attention_heads,
cfg.encoder_head_dim(),
None,
)?;
let h1 = x.add(&attn)?.layer_norm_affine(
std::sync::Arc::clone(&layer.self_attn_ln_gain),
std::sync::Arc::clone(&layer.self_attn_ln_bias),
1e-5,
)?;
let ffn = self.feed_forward(
&h1,
&layer.fc1,
&layer.fc1_bias,
&layer.fc2,
&layer.fc2_bias,
cfg.encoder_ffn_dim,
)?;
h1.add(&ffn)?.layer_norm_affine(
std::sync::Arc::clone(&layer.final_ln_gain),
std::sync::Arc::clone(&layer.final_ln_bias),
1e-5,
)
}
fn apply_decoder_layer(
&self,
x: &Tensor,
layer: &MarianDecoderLayerWeights,
enc_out: &Tensor,
causal_mask: &Tensor,
) -> Result<Tensor> {
let cfg = &self.config;
let self_attn = self.attention(
x,
x,
&layer.self_attn,
cfg.decoder_attention_heads,
cfg.decoder_head_dim(),
Some(causal_mask),
)?;
let h1 = x.add(&self_attn)?.layer_norm_affine(
std::sync::Arc::clone(&layer.self_attn_ln_gain),
std::sync::Arc::clone(&layer.self_attn_ln_bias),
1e-5,
)?;
let cross_attn = self.attention(
&h1,
enc_out,
&layer.encoder_attn,
cfg.decoder_attention_heads,
cfg.decoder_head_dim(),
None,
)?;
let h2 = h1.add(&cross_attn)?.layer_norm_affine(
std::sync::Arc::clone(&layer.encoder_attn_ln_gain),
std::sync::Arc::clone(&layer.encoder_attn_ln_bias),
1e-5,
)?;
let ffn = self.feed_forward(
&h2,
&layer.fc1,
&layer.fc1_bias,
&layer.fc2,
&layer.fc2_bias,
cfg.decoder_ffn_dim,
)?;
h2.add(&ffn)?.layer_norm_affine(
std::sync::Arc::clone(&layer.final_ln_gain),
std::sync::Arc::clone(&layer.final_ln_bias),
1e-5,
)
}
fn attention(
&self,
q_src: &Tensor,
kv_src: &Tensor,
w: &MarianAttentionWeights,
n_heads: usize,
head_dim: usize,
attn_mask: Option<&Tensor>,
) -> Result<Tensor> {
let d_model = self.config.d_model;
let q_shape = q_src.shape();
let q_dims = q_shape.dims();
let batch = q_dims[0];
let q_len = q_dims[1];
let kv_shape = kv_src.shape();
let kv_dims = kv_shape.dims();
let kv_len = kv_dims[1];
let scaling = (head_dim as f64).powf(-0.5);
let q = add_bias_3d(
w.q_proj.apply_linear(q_src, d_model, d_model)?,
&w.q_proj_bias,
d_model,
)?
.mul_scalar(scaling);
let k = add_bias_3d(
w.k_proj.apply_linear(kv_src, d_model, d_model)?,
&w.k_proj_bias,
d_model,
)?;
let v = add_bias_3d(
w.v_proj.apply_linear(kv_src, d_model, d_model)?,
&w.v_proj_bias,
d_model,
)?;
let _ = (batch, q_len, kv_len);
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 k_t = k.transpose()?;
let scores = q.matmul(&k_t)?;
let scores = match attn_mask {
None => scores,
Some(mask) => scores.broadcast_add(mask)?,
};
let probs = scores.softmax_last_dim()?;
let ctx = probs.matmul(&v)?;
let merged = ctx.merge_heads()?;
let out = w.out_proj.apply_linear(&merged, d_model, d_model)?;
add_bias_3d(out, &w.out_proj_bias, d_model)
}
fn feed_forward(
&self,
x: &Tensor,
fc1_w: &WeightStorage,
fc1_b: &Arc<[f32]>,
fc2_w: &WeightStorage,
fc2_b: &Arc<[f32]>,
ffn_dim: usize,
) -> Result<Tensor> {
let cfg = &self.config;
let fc1 = add_bias_3d(fc1_w.apply_linear(x, cfg.d_model, ffn_dim)?, fc1_b, ffn_dim)?;
let activated = match cfg.activation_function {
MarianActivation::Relu => fc1.relu(),
MarianActivation::Silu => fc1.silu(),
MarianActivation::Gelu => fc1.gelu(),
MarianActivation::GeluPytorchTanh => fc1.gelu_tanh(),
};
let fc2 = add_bias_3d(
fc2_w.apply_linear(&activated, ffn_dim, cfg.d_model)?,
fc2_b,
cfg.d_model,
)?;
Ok(fc2)
}
}
fn build_sinusoidal_table(max_positions: usize, d_model: usize) -> Result<Vec<f32>> {
if !d_model.is_multiple_of(2) {
return Err(fuel_ir::error::Error::Msg(format!(
"build_sinusoidal_table: d_model ({d_model}) must be even"
)));
}
let half = d_model / 2;
let mut out = vec![0.0_f32; max_positions * d_model];
let inv_freq: Vec<f32> = (0..half)
.map(|i| 1.0_f32 / 10_000.0_f32.powf((2 * i) as f32 / d_model as f32))
.collect();
for t in 0..max_positions {
for i in 0..half {
let arg = t as f32 * inv_freq[i];
out[t * d_model + i] = arg.sin();
out[t * d_model + half + i] = arg.cos();
}
}
Ok(out)
}
fn add_bias_3d(x: Tensor, bias: &Arc<[f32]>, n: usize) -> Result<Tensor> {
let _ = n;
x.add_trailing_bias(Arc::clone(bias))
}
impl MarianAttentionWeights {
fn load(
st: &fuel_loaders::safetensors::MmapedSafetensors,
prefix: &str,
d_model: usize,
) -> Result<Self> {
use fuel_tensor::{
lazy::load_tensor_as_f32, lazy::load_transposed_matrix_preserve_dtype as ltm,
};
let q_proj = ltm(st, &format!("{prefix}.q_proj.weight"), d_model, d_model)?;
let q_proj_bias = Arc::from(load_tensor_as_f32(st, &format!("{prefix}.q_proj.bias"))?);
let k_proj = ltm(st, &format!("{prefix}.k_proj.weight"), d_model, d_model)?;
let k_proj_bias = Arc::from(load_tensor_as_f32(st, &format!("{prefix}.k_proj.bias"))?);
let v_proj = ltm(st, &format!("{prefix}.v_proj.weight"), d_model, d_model)?;
let v_proj_bias = Arc::from(load_tensor_as_f32(st, &format!("{prefix}.v_proj.bias"))?);
let out_proj = ltm(st, &format!("{prefix}.out_proj.weight"), d_model, d_model)?;
let out_proj_bias = Arc::from(load_tensor_as_f32(st, &format!("{prefix}.out_proj.bias"))?);
Ok(Self {
q_proj,
q_proj_bias,
k_proj,
k_proj_bias,
v_proj,
v_proj_bias,
out_proj,
out_proj_bias,
})
}
}
impl MarianWeights {
pub fn load_from_mmapped(
st: &fuel_loaders::safetensors::MmapedSafetensors,
cfg: &MarianConfig,
) -> Result<Self> {
use fuel_tensor::{
lazy::load_tensor_as_f32, lazy::load_transposed_matrix_preserve_dtype as ltm,
};
let d = cfg.d_model;
let enc_ffn = cfg.encoder_ffn_dim;
let dec_ffn = cfg.decoder_ffn_dim;
let shared_embedding = Arc::from(load_tensor_as_f32(st, "model.shared.weight")?);
let mut encoder_layers = Vec::with_capacity(cfg.encoder_layers);
for i in 0..cfg.encoder_layers {
let p = format!("model.encoder.layers.{i}");
let self_attn = MarianAttentionWeights::load(st, &format!("{p}.self_attn"), d)?;
let self_attn_ln_gain = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn_layer_norm.weight"),
)?);
let self_attn_ln_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn_layer_norm.bias"),
)?);
let fc1 = ltm(st, &format!("{p}.fc1.weight"), enc_ffn, d)?;
let fc1_bias = Arc::from(load_tensor_as_f32(st, &format!("{p}.fc1.bias"))?);
let fc2 = ltm(st, &format!("{p}.fc2.weight"), d, enc_ffn)?;
let fc2_bias = Arc::from(load_tensor_as_f32(st, &format!("{p}.fc2.bias"))?);
let final_ln_gain = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.final_layer_norm.weight"),
)?);
let final_ln_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.final_layer_norm.bias"),
)?);
encoder_layers.push(MarianEncoderLayerWeights {
self_attn,
self_attn_ln_gain,
self_attn_ln_bias,
fc1,
fc1_bias,
fc2,
fc2_bias,
final_ln_gain,
final_ln_bias,
});
}
let mut decoder_layers = Vec::with_capacity(cfg.decoder_layers);
for i in 0..cfg.decoder_layers {
let p = format!("model.decoder.layers.{i}");
let self_attn = MarianAttentionWeights::load(st, &format!("{p}.self_attn"), d)?;
let self_attn_ln_gain = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn_layer_norm.weight"),
)?);
let self_attn_ln_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn_layer_norm.bias"),
)?);
let encoder_attn = MarianAttentionWeights::load(st, &format!("{p}.encoder_attn"), d)?;
let encoder_attn_ln_gain = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.encoder_attn_layer_norm.weight"),
)?);
let encoder_attn_ln_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.encoder_attn_layer_norm.bias"),
)?);
let fc1 = ltm(st, &format!("{p}.fc1.weight"), dec_ffn, d)?;
let fc1_bias = Arc::from(load_tensor_as_f32(st, &format!("{p}.fc1.bias"))?);
let fc2 = ltm(st, &format!("{p}.fc2.weight"), d, dec_ffn)?;
let fc2_bias = Arc::from(load_tensor_as_f32(st, &format!("{p}.fc2.bias"))?);
let final_ln_gain = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.final_layer_norm.weight"),
)?);
let final_ln_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.final_layer_norm.bias"),
)?);
decoder_layers.push(MarianDecoderLayerWeights {
self_attn,
self_attn_ln_gain,
self_attn_ln_bias,
encoder_attn,
encoder_attn_ln_gain,
encoder_attn_ln_bias,
fc1,
fc1_bias,
fc2,
fc2_bias,
final_ln_gain,
final_ln_bias,
});
}
let target_vocab = cfg.target_vocab_size();
let final_logits_bias = match load_tensor_as_f32(st, "final_logits_bias") {
Ok(v) => Arc::from(v),
Err(_) => Arc::from(vec![0.0_f32; target_vocab]),
};
Ok(Self {
shared_embedding,
encoder_layers,
decoder_layers,
final_logits_bias,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tiny_attention_weights(
d_model: usize,
next_box: &mut Box<dyn FnMut() -> f32>,
) -> MarianAttentionWeights {
let vec_of = |n: usize, next: &mut dyn FnMut() -> f32| -> Arc<[f32]> {
Arc::from((0..n).map(|_| next()).collect::<Vec<_>>())
};
MarianAttentionWeights {
q_proj: WeightStorage::F32(vec_of(d_model * d_model, &mut **next_box)),
q_proj_bias: vec_of(d_model, &mut **next_box),
k_proj: WeightStorage::F32(vec_of(d_model * d_model, &mut **next_box)),
k_proj_bias: vec_of(d_model, &mut **next_box),
v_proj: WeightStorage::F32(vec_of(d_model * d_model, &mut **next_box)),
v_proj_bias: vec_of(d_model, &mut **next_box),
out_proj: WeightStorage::F32(vec_of(d_model * d_model, &mut **next_box)),
out_proj_bias: vec_of(d_model, &mut **next_box),
}
}
fn tiny_weights(cfg: &MarianConfig) -> MarianWeights {
let mut s: u32 = 64646;
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 d = cfg.d_model;
let enc_ffn = cfg.encoder_ffn_dim;
let dec_ffn = cfg.decoder_ffn_dim;
let mut nb: Box<dyn FnMut() -> f32> = Box::new(next);
let vec_of = |n: usize, next: &mut dyn FnMut() -> f32| -> Arc<[f32]> {
Arc::from((0..n).map(|_| next()).collect::<Vec<_>>())
};
let shared_embedding = vec_of(cfg.vocab_size * d, &mut *nb);
let encoder_layers: Vec<MarianEncoderLayerWeights> = (0..cfg.encoder_layers)
.map(|_| MarianEncoderLayerWeights {
self_attn: tiny_attention_weights(d, &mut nb),
self_attn_ln_gain: Arc::from(vec![1.0_f32; d]),
self_attn_ln_bias: Arc::from(vec![0.0_f32; d]),
fc1: WeightStorage::F32(vec_of(d * enc_ffn, &mut *nb)),
fc1_bias: vec_of(enc_ffn, &mut *nb),
fc2: WeightStorage::F32(vec_of(enc_ffn * d, &mut *nb)),
fc2_bias: vec_of(d, &mut *nb),
final_ln_gain: Arc::from(vec![1.0_f32; d]),
final_ln_bias: Arc::from(vec![0.0_f32; d]),
})
.collect();
let decoder_layers: Vec<MarianDecoderLayerWeights> = (0..cfg.decoder_layers)
.map(|_| MarianDecoderLayerWeights {
self_attn: tiny_attention_weights(d, &mut nb),
self_attn_ln_gain: Arc::from(vec![1.0_f32; d]),
self_attn_ln_bias: Arc::from(vec![0.0_f32; d]),
encoder_attn: tiny_attention_weights(d, &mut nb),
encoder_attn_ln_gain: Arc::from(vec![1.0_f32; d]),
encoder_attn_ln_bias: Arc::from(vec![0.0_f32; d]),
fc1: WeightStorage::F32(vec_of(d * dec_ffn, &mut *nb)),
fc1_bias: vec_of(dec_ffn, &mut *nb),
fc2: WeightStorage::F32(vec_of(dec_ffn * d, &mut *nb)),
fc2_bias: vec_of(d, &mut *nb),
final_ln_gain: Arc::from(vec![1.0_f32; d]),
final_ln_bias: Arc::from(vec![0.0_f32; d]),
})
.collect();
let target_vocab = cfg.target_vocab_size();
let final_logits_bias = vec_of(target_vocab, &mut *nb);
MarianWeights {
shared_embedding,
encoder_layers,
decoder_layers,
final_logits_bias,
}
}
fn tiny_config() -> MarianConfig {
MarianConfig {
vocab_size: 16,
decoder_vocab_size: None,
max_position_embeddings: 16,
d_model: 8,
encoder_layers: 2,
encoder_ffn_dim: 16,
encoder_attention_heads: 2,
decoder_layers: 2,
decoder_ffn_dim: 16,
decoder_attention_heads: 2,
scale_embedding: true,
share_encoder_decoder_embeddings: true,
activation_function: MarianActivation::Relu,
}
}
#[test]
fn forward_shape_and_finite() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let src = [1_u32, 2, 3, 4];
let tgt = [5_u32, 6, 7];
let logits = model.forward(&src, &tgt).unwrap();
assert_eq!(
logits.shape().dims(),
&[1, tgt.len(), cfg.target_vocab_size()]
);
for &v in &logits.realize_f32() {
assert!(v.is_finite(), "got non-finite logit {v}");
}
}
#[test]
fn cross_attention_is_wired() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let tgt = [5_u32, 6, 7];
let a = model.forward(&[1, 2, 3, 4], &tgt).unwrap().realize_f32();
let b = model.forward(&[8, 9, 10, 11], &tgt).unwrap().realize_f32();
let mut max_diff = 0.0_f32;
for (x, y) in a.iter().zip(b.iter()) {
max_diff = max_diff.max((x - y).abs());
}
assert!(
max_diff > 1e-6,
"cross-attention must condition on encoder output: changing src must change tgt logits, max_diff = {max_diff}"
);
}
#[test]
fn decoder_causal_mask_is_enforced() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let src = [1_u32, 2, 3];
let tgt_a = [5_u32, 6, 7, 8];
let tgt_b = [5_u32, 6, 7, 15]; let a = model.forward(&src, &tgt_a).unwrap().realize_f32();
let b = model.forward(&src, &tgt_b).unwrap().realize_f32();
let v = cfg.target_vocab_size();
for t in 0..3 {
for col in 0..v {
let i = t * v + col;
assert!(
(a[i] - b[i]).abs() < 1e-5,
"causal mask violated at t={t}, col={col}: {} vs {}",
a[i],
b[i],
);
}
}
}
#[test]
fn sinusoidal_table_position_0() {
let t = build_sinusoidal_table(4, 8).unwrap();
let half = 4;
for i in 0..half {
assert!((t[i]).abs() < 1e-6, "sin[0, {i}] = {} != 0", t[i]);
assert!(
(t[half + i] - 1.0).abs() < 1e-6,
"cos[0, {i}] = {} != 1",
t[half + i]
);
}
}
#[test]
fn presets_construct() {
let c = MarianConfig::opus_mt_tc_big_fr_en();
assert_eq!(c.d_model, 1024);
assert_eq!(c.encoder_attention_heads, 16);
assert_eq!(c.target_vocab_size(), 53017);
}
#[test]
fn forward_encoder_shape_and_finite() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let src = [1_u32, 2, 3, 4, 5];
let enc = model.forward_encoder(&src).unwrap();
assert_eq!(enc.shape().dims(), &[1, src.len(), cfg.d_model]);
for &v in &enc.realize_f32() {
assert!(v.is_finite(), "non-finite encoder output: {v}");
}
}
#[test]
fn forward_encoder_deterministic() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let src = [1_u32, 2, 3, 4];
let a = model.forward_encoder(&src).unwrap().realize_f32();
let b = model.forward_encoder(&src).unwrap().realize_f32();
assert_eq!(a.len(), b.len());
for (x, y) in a.iter().zip(b.iter()) {
assert!(
(x - y).abs() < 1e-7,
"non-deterministic encoder: {x} vs {y}"
);
}
}
#[test]
fn forward_decoder_shape_and_finite() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let enc = model.forward_encoder(&[1_u32, 2, 3, 4]).unwrap();
let tgt = [5_u32, 6, 7];
let logits = model.forward_decoder(&tgt, &enc).unwrap();
assert_eq!(
logits.shape().dims(),
&[1, tgt.len(), cfg.target_vocab_size()]
);
for &v in &logits.realize_f32() {
assert!(v.is_finite(), "non-finite decoder logit: {v}");
}
}
#[test]
fn forward_decoder_matches_full_forward() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let src = [1_u32, 2, 3];
let tgt = [5_u32, 6, 7];
let full = model.forward(&src, &tgt).unwrap().realize_f32();
let enc = model.forward_encoder(&src).unwrap();
let split = model.forward_decoder(&tgt, &enc).unwrap().realize_f32();
assert_eq!(full.len(), split.len());
for (a, b) in full.iter().zip(split.iter()) {
assert!(
(a - b).abs() < 1e-5,
"forward_decoder must match forward: {a} vs {b}"
);
}
}
#[test]
fn forward_encoder_embeds_matches_forward_encoder() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let src = [1_u32, 2, 3];
let enc_ref = model.forward_encoder(&src).unwrap().realize_f32();
let anchor =
Tensor::from_f32(vec![0.0_f32], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
let src_embeds = model.embed_tokens_anchored(&anchor, &src).unwrap();
let enc_via_embeds = model
.forward_encoder_embeds(&src_embeds)
.unwrap()
.realize_f32();
let max_diff = enc_ref
.iter()
.zip(enc_via_embeds.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff < 1e-5,
"Marian forward_encoder vs forward_encoder_embeds must agree (max diff {max_diff})"
);
}
#[test]
fn forward_decoder_embeds_matches_forward_decoder() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let src = [1_u32, 2, 3];
let tgt = [5_u32, 6, 7];
let enc = model.forward_encoder(&src).unwrap();
let dec_ref = model.forward_decoder(&tgt, &enc).unwrap().realize_f32();
let tgt_embeds = model.embed_tokens_anchored(&enc, &tgt).unwrap();
let dec_via_embeds = model
.forward_decoder_embeds(&tgt_embeds, &enc)
.unwrap()
.realize_f32();
let max_diff = dec_ref
.iter()
.zip(dec_via_embeds.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff < 1e-5,
"Marian forward_decoder vs forward_decoder_embeds must agree (max diff {max_diff})"
);
}
#[test]
fn forward_encoder_embeds_rejects_bad_shape() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let bad = Tensor::from_f32(
vec![0.0_f32; 3 * (cfg.d_model + 1)],
Shape::from_dims(&[1, 3, cfg.d_model + 1]),
&Device::cpu(),
)
.unwrap();
assert!(model.forward_encoder_embeds(&bad).is_err());
}
#[test]
fn forward_decoder_hidden_embeds_skips_lm_head() {
let cfg = tiny_config();
let model = MarianModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let src = [1_u32, 2, 3];
let tgt = [5_u32, 6, 7];
let enc = model.forward_encoder(&src).unwrap();
let tgt_embeds = model.embed_tokens_anchored(&enc, &tgt).unwrap();
let hidden = model
.forward_decoder_hidden_embeds(&tgt_embeds, &enc)
.unwrap();
assert_eq!(hidden.shape().dims(), &[1, tgt.len(), cfg.d_model]);
for &v in &hidden.realize_f32() {
assert!(v.is_finite(), "non-finite hidden: {v}");
}
}
#[test]
fn dim_head_mismatch_declines_rather_than_panicking() {
let valid = tiny_config();
let src = [1_u32, 2, 3, 4];
let tgt = [5_u32, 6, 7];
let good = MarianModel {
config: valid.clone(),
weights: tiny_weights(&valid),
};
good.forward(&src, &tgt)
.expect("control: the conforming config must still run");
let mut bad = valid.clone();
bad.encoder_attention_heads = 3; let model = MarianModel {
config: bad.clone(),
weights: tiny_weights(&bad),
};
let err = model
.forward(&src, &tgt)
.expect_err("a non-dividing head count must DECLINE, not panic");
let msg = format!("{err}");
assert!(
msg.contains("d_model") && msg.contains("encoder_attention_heads"),
"the decline must name both operands, got: {msg}"
);
}
#[test]
fn odd_d_model_declines_rather_than_panicking() {
build_sinusoidal_table(4, 8).expect("control: an even d_model must still build");
let err = build_sinusoidal_table(4, 7).expect_err("an odd d_model must DECLINE, not panic");
let msg = format!("{err}");
assert!(
msg.contains("d_model") && msg.contains('7'),
"the decline must name the operand and its value, got: {msg}"
);
}
}