use crate::models::lazy_t5::{
T5AttentionWeights, T5Config, T5DecoderLayerWeights, T5EncoderLayerWeights, T5FfnWeights,
T5Model, T5Weights,
};
#[cfg(test)]
use fuel_ir::Shape;
use fuel_ir::error::Result;
#[cfg(test)]
use fuel_tensor::Device;
use fuel_tensor::lazy::WeightStorage;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct QuantizedT5Model {
inner: T5Model,
}
impl QuantizedT5Model {
pub fn forward(
&self,
src_tokens: &[u32],
tgt_tokens: &[u32],
) -> Result<fuel_tensor::lazy::Tensor> {
self.inner.forward(src_tokens, tgt_tokens)
}
pub fn forward_encoder(&self, src_tokens: &[u32]) -> Result<fuel_tensor::lazy::Tensor> {
self.inner.forward_encoder(src_tokens)
}
pub fn forward_encoder_embeds(
&self,
src_embeds: &fuel_tensor::lazy::Tensor,
) -> Result<fuel_tensor::lazy::Tensor> {
self.inner.forward_encoder_embeds(src_embeds)
}
pub fn forward_decoder(
&self,
tgt_tokens: &[u32],
encoder_out: &fuel_tensor::lazy::Tensor,
) -> Result<fuel_tensor::lazy::Tensor> {
self.inner.forward_decoder(tgt_tokens, encoder_out)
}
pub fn forward_decoder_embeds(
&self,
tgt_embeds: &fuel_tensor::lazy::Tensor,
encoder_out: &fuel_tensor::lazy::Tensor,
) -> Result<fuel_tensor::lazy::Tensor> {
self.inner.forward_decoder_embeds(tgt_embeds, encoder_out)
}
pub fn forward_decoder_hidden_embeds(
&self,
tgt_embeds: &fuel_tensor::lazy::Tensor,
encoder_out: &fuel_tensor::lazy::Tensor,
) -> Result<fuel_tensor::lazy::Tensor> {
self.inner
.forward_decoder_hidden_embeds(tgt_embeds, encoder_out)
}
pub fn embed_tokens_anchored(
&self,
anchor: &fuel_tensor::lazy::Tensor,
tokens: &[u32],
) -> Result<fuel_tensor::lazy::Tensor> {
self.inner.embed_tokens_anchored(anchor, tokens)
}
pub fn config(&self) -> &T5Config {
&self.inner.config
}
pub fn inner(&self) -> &T5Model {
&self.inner
}
pub fn load_from_mmapped(
st: &fuel_loaders::safetensors::MmapedSafetensors,
cfg: T5Config,
) -> Result<Self> {
let src = T5Weights::load_from_mmapped(st, &cfg)?;
Self::from_f32_bake(cfg, src)
}
pub fn from_f32_bake(cfg: T5Config, src: T5Weights) -> Result<Self> {
let d = cfg.d_model;
let inner = cfg.inner_dim();
let d_ff = cfg.d_ff;
check_q4_0_divisible("d_model", d)?;
check_q4_0_divisible("inner_dim (= num_heads * d_kv)", inner)?;
check_q4_0_divisible("d_ff", d_ff)?;
let quantize_linear =
|w: &WeightStorage, in_features: usize, out_features: usize| -> Result<WeightStorage> {
let f32_in_out = match w {
WeightStorage::F32(a) => a.to_vec(),
_ => return Err(fuel_ir::error::Error::Msg(
"QuantizedT5Model::from_f32_bake: source weights must be WeightStorage::F32"
.into(),
)
.bt()),
};
if f32_in_out.len() != in_features * out_features {
return Err(fuel_ir::error::Error::Msg(format!(
"QuantizedT5Model::from_f32_bake: weight has {} elems, expected {}×{}",
f32_in_out.len(),
in_features,
out_features,
))
.bt());
}
quantize_in_out_to_q4_0(&f32_in_out, in_features, out_features)
};
let quantize_attn = |a: &T5AttentionWeights, name: &str| -> Result<T5AttentionWeights> {
let q = quantize_linear(&a.q, d, inner)
.map_err(|e| fuel_ir::error::Error::Msg(format!("{name}.q: {e}")).bt())?;
let k = quantize_linear(&a.k, d, inner)
.map_err(|e| fuel_ir::error::Error::Msg(format!("{name}.k: {e}")).bt())?;
let v = quantize_linear(&a.v, d, inner)
.map_err(|e| fuel_ir::error::Error::Msg(format!("{name}.v: {e}")).bt())?;
let o = quantize_linear(&a.o, inner, d)
.map_err(|e| fuel_ir::error::Error::Msg(format!("{name}.o: {e}")).bt())?;
Ok(T5AttentionWeights { q, k, v, o })
};
let quantize_ffn = |f: &T5FfnWeights, name: &str| -> Result<T5FfnWeights> {
match f {
T5FfnWeights::Dense { wi, wo } => {
let wi = quantize_linear(wi, d, d_ff)
.map_err(|e| fuel_ir::error::Error::Msg(format!("{name}.wi: {e}")).bt())?;
let wo = quantize_linear(wo, d_ff, d)
.map_err(|e| fuel_ir::error::Error::Msg(format!("{name}.wo: {e}")).bt())?;
Ok(T5FfnWeights::Dense { wi, wo })
}
T5FfnWeights::Gated { wi_0, wi_1, wo } => {
let wi_0 = quantize_linear(wi_0, d, d_ff).map_err(|e| {
fuel_ir::error::Error::Msg(format!("{name}.wi_0: {e}")).bt()
})?;
let wi_1 = quantize_linear(wi_1, d, d_ff).map_err(|e| {
fuel_ir::error::Error::Msg(format!("{name}.wi_1: {e}")).bt()
})?;
let wo = quantize_linear(wo, d_ff, d)
.map_err(|e| fuel_ir::error::Error::Msg(format!("{name}.wo: {e}")).bt())?;
Ok(T5FfnWeights::Gated { wi_0, wi_1, wo })
}
}
};
let mut encoder_layers: Vec<T5EncoderLayerWeights> =
Vec::with_capacity(src.encoder_layers.len());
for (idx, layer) in src.encoder_layers.into_iter().enumerate() {
let self_attn = quantize_attn(&layer.self_attn, "self_attn")
.map_err(|e| layer_err(idx, "encoder.self_attn", e))?;
let ffn =
quantize_ffn(&layer.ffn, "ffn").map_err(|e| layer_err(idx, "encoder.ffn", e))?;
encoder_layers.push(T5EncoderLayerWeights {
self_attn_norm_gain: layer.self_attn_norm_gain,
self_attn,
ffn_norm_gain: layer.ffn_norm_gain,
ffn,
});
}
let mut decoder_layers: Vec<T5DecoderLayerWeights> =
Vec::with_capacity(src.decoder_layers.len());
for (idx, layer) in src.decoder_layers.into_iter().enumerate() {
let self_attn = quantize_attn(&layer.self_attn, "self_attn")
.map_err(|e| layer_err(idx, "decoder.self_attn", e))?;
let cross_attn = quantize_attn(&layer.cross_attn, "cross_attn")
.map_err(|e| layer_err(idx, "decoder.cross_attn", e))?;
let ffn =
quantize_ffn(&layer.ffn, "ffn").map_err(|e| layer_err(idx, "decoder.ffn", e))?;
decoder_layers.push(T5DecoderLayerWeights {
self_attn_norm_gain: layer.self_attn_norm_gain,
self_attn,
cross_attn_norm_gain: layer.cross_attn_norm_gain,
cross_attn,
ffn_norm_gain: layer.ffn_norm_gain,
ffn,
});
}
let lm_head = match src.lm_head {
Some(w) => Some(
quantize_linear(&w, d, cfg.vocab_size)
.map_err(|e| fuel_ir::error::Error::Msg(format!("lm_head: {e}")).bt())?,
),
None => None,
};
let inner = T5Model {
config: cfg,
weights: T5Weights {
shared_embedding: src.shared_embedding,
encoder_rel_bias: src.encoder_rel_bias,
decoder_rel_bias: src.decoder_rel_bias,
encoder_layers,
decoder_layers,
encoder_final_norm_gain: src.encoder_final_norm_gain,
decoder_final_norm_gain: src.decoder_final_norm_gain,
lm_head,
},
};
Ok(Self { inner })
}
pub fn from_gguf<P: AsRef<std::path::Path>>(path: P, cfg: &T5Config) -> Result<Self> {
use fuel_loaders::quantized::gguf_mmap::MmapedContent;
let mc = MmapedContent::from_path(path)?;
let content = mc.content();
let mmap_arc = mc.mmap();
let mmap_bytes: &[u8] = &mmap_arc[..];
let data_off = content.tensor_data_offset as usize;
let get_tensor_bytes =
|name: &str| -> Result<(&[u8], fuel_loaders::quantized::GgmlDType, Vec<usize>)> {
let info = content.tensor_infos.get(name).ok_or_else(|| {
fuel_ir::error::Error::Msg(format!("gguf: missing tensor {name:?}"))
})?;
let elems = info.shape.elem_count();
let block_size = info.ggml_dtype.block_size();
let bytes_len = elems / block_size * info.ggml_dtype.type_size();
let start = data_off + info.offset as usize;
Ok((
&mmap_bytes[start..start + bytes_len],
info.ggml_dtype,
info.shape.dims().to_vec(),
))
};
let load_f32 = |name: &str| -> Result<Vec<f32>> {
let (bytes, dt, _) = get_tensor_bytes(name)?;
dequant_bytes_to_f32(bytes, dt, name)
};
let load_weight = |name: &str,
out_features: usize,
in_features: usize|
-> Result<WeightStorage> {
let (bytes, dt, dims) = get_tensor_bytes(name)?;
let expected = out_features * in_features;
let actual: usize = dims.iter().product();
if actual != expected {
return Err(fuel_ir::error::Error::Msg(format!(
"gguf: tensor {name:?} has {actual} elements, expected {expected} for [{out_features}, {in_features}]",
)).bt());
}
match dt {
fuel_loaders::quantized::GgmlDType::Q4_0 => Ok(WeightStorage::Q4_0 {
words: bytes_to_u32_arc(bytes),
bytes_len: bytes.len(),
in_features,
out_features,
}),
_ => {
let f32_out_in = dequant_bytes_to_f32(bytes, dt, name)?;
let mut f32_in_out = vec![0.0_f32; expected];
for o in 0..out_features {
for j in 0..in_features {
f32_in_out[j * out_features + o] = f32_out_in[o * in_features + j];
}
}
Ok(WeightStorage::F32(Arc::from(f32_in_out)))
}
}
};
let d = cfg.d_model;
let inner = cfg.inner_dim();
let d_ff = cfg.d_ff;
let n_enc = cfg.num_layers;
let n_dec = cfg.num_decoder_layers_resolved();
let gated = cfg.gated_ffn;
let shared_embedding = load_f32("token_embd.weight")?;
if shared_embedding.len() != cfg.vocab_size * d {
return Err(fuel_ir::error::Error::Msg(format!(
"gguf token_embd.weight: {} elems, expected {}×{}",
shared_embedding.len(),
cfg.vocab_size,
d,
))
.bt());
}
let encoder_rel_bias = load_f32("enc.blk.0.attn_rel_b.weight")?;
let decoder_rel_bias = load_f32("dec.blk.0.attn_rel_b.weight")?;
let expected_rel = cfg.relative_attention_num_buckets * cfg.num_heads;
if encoder_rel_bias.len() != expected_rel {
return Err(fuel_ir::error::Error::Msg(format!(
"gguf enc.blk.0.attn_rel_b.weight: {} elems, expected {}×{}",
encoder_rel_bias.len(),
cfg.relative_attention_num_buckets,
cfg.num_heads,
))
.bt());
}
if decoder_rel_bias.len() != expected_rel {
return Err(fuel_ir::error::Error::Msg(format!(
"gguf dec.blk.0.attn_rel_b.weight: {} elems, expected {}×{}",
decoder_rel_bias.len(),
cfg.relative_attention_num_buckets,
cfg.num_heads,
))
.bt());
}
let load_attn = |prefix: &str,
q_name: &str,
k_name: &str,
v_name: &str,
o_name: &str|
-> Result<T5AttentionWeights> {
let q = load_weight(&format!("{prefix}.{q_name}.weight"), inner, d)?;
let k = load_weight(&format!("{prefix}.{k_name}.weight"), inner, d)?;
let v = load_weight(&format!("{prefix}.{v_name}.weight"), inner, d)?;
let o = load_weight(&format!("{prefix}.{o_name}.weight"), d, inner)?;
Ok(T5AttentionWeights { q, k, v, o })
};
let load_ffn = |prefix: &str| -> Result<T5FfnWeights> {
if gated {
let wi_0 = load_weight(&format!("{prefix}.ffn_gate.weight"), d_ff, d)?;
let wi_1 = load_weight(&format!("{prefix}.ffn_up.weight"), d_ff, d)?;
let wo = load_weight(&format!("{prefix}.ffn_down.weight"), d, d_ff)?;
Ok(T5FfnWeights::Gated { wi_0, wi_1, wo })
} else {
let wi = load_weight(&format!("{prefix}.ffn_up.weight"), d_ff, d)?;
let wo = load_weight(&format!("{prefix}.ffn_down.weight"), d, d_ff)?;
Ok(T5FfnWeights::Dense { wi, wo })
}
};
let mut encoder_layers: Vec<T5EncoderLayerWeights> = Vec::with_capacity(n_enc);
for i in 0..n_enc {
let prefix = format!("enc.blk.{i}");
let self_attn_norm_gain: Arc<[f32]> =
Arc::from(load_f32(&format!("{prefix}.attn_norm.weight"))?);
let self_attn = load_attn(&prefix, "attn_q", "attn_k", "attn_v", "attn_o")?;
let ffn_norm_gain: Arc<[f32]> =
Arc::from(load_f32(&format!("{prefix}.ffn_norm.weight"))?);
let ffn = load_ffn(&prefix)?;
encoder_layers.push(T5EncoderLayerWeights {
self_attn_norm_gain,
self_attn,
ffn_norm_gain,
ffn,
});
}
let mut decoder_layers: Vec<T5DecoderLayerWeights> = Vec::with_capacity(n_dec);
for i in 0..n_dec {
let prefix = format!("dec.blk.{i}");
let self_attn_norm_gain: Arc<[f32]> =
Arc::from(load_f32(&format!("{prefix}.attn_norm.weight"))?);
let self_attn = load_attn(&prefix, "attn_q", "attn_k", "attn_v", "attn_o")?;
let cross_attn_norm_gain: Arc<[f32]> =
Arc::from(load_f32(&format!("{prefix}.cross_attn_norm.weight"))?);
let cross_attn = load_attn(
&prefix,
"cross_attn_q",
"cross_attn_k",
"cross_attn_v",
"cross_attn_o",
)?;
let ffn_norm_gain: Arc<[f32]> =
Arc::from(load_f32(&format!("{prefix}.ffn_norm.weight"))?);
let ffn = load_ffn(&prefix)?;
decoder_layers.push(T5DecoderLayerWeights {
self_attn_norm_gain,
self_attn,
cross_attn_norm_gain,
cross_attn,
ffn_norm_gain,
ffn,
});
}
let encoder_final_norm_gain: Arc<[f32]> = Arc::from(load_f32("enc.output_norm.weight")?);
let decoder_final_norm_gain: Arc<[f32]> = Arc::from(load_f32("dec.output_norm.weight")?);
let lm_head = if content.tensor_infos.contains_key("output.weight") {
Some(load_weight("output.weight", cfg.vocab_size, d)?)
} else if cfg.tie_word_embeddings {
None
} else {
return Err(fuel_ir::error::Error::Msg(
"gguf: output.weight absent and config has tie_word_embeddings=false".into(),
)
.bt());
};
let inner_model = T5Model {
config: cfg.clone(),
weights: T5Weights {
shared_embedding: Arc::from(shared_embedding),
encoder_rel_bias: Arc::from(encoder_rel_bias),
decoder_rel_bias: Arc::from(decoder_rel_bias),
encoder_layers,
decoder_layers,
encoder_final_norm_gain,
decoder_final_norm_gain,
lm_head,
},
};
Ok(Self { inner: inner_model })
}
}
fn layer_err(idx: usize, name: &str, e: fuel_ir::error::Error) -> fuel_ir::error::Error {
fuel_ir::error::Error::Msg(format!("layer {idx} {name}: {e}")).bt()
}
fn check_q4_0_divisible(name: &str, n: usize) -> Result<()> {
const QK4_0: usize = 32;
if !n.is_multiple_of(QK4_0) {
return Err(fuel_ir::error::Error::Msg(format!(
"QuantizedT5Model::from_f32_bake: {name} ({n}) must be divisible by Q4_0 block size ({QK4_0})"
)).bt());
}
Ok(())
}
fn quantize_in_out_to_q4_0(
f32_in_out: &[f32],
in_features: usize,
out_features: usize,
) -> Result<WeightStorage> {
use fuel_quantized::{BlockQ4_0, GgmlType};
const QK4_0: usize = 32;
if !in_features.is_multiple_of(QK4_0) {
return Err(fuel_ir::error::Error::Msg(format!(
"Q4_0 quantize: in_features ({in_features}) must be divisible by {QK4_0}"
))
.bt());
}
let mut f32_out_in = vec![0.0_f32; out_features * in_features];
for o in 0..out_features {
for j in 0..in_features {
f32_out_in[o * in_features + j] = f32_in_out[j * out_features + o];
}
}
let n_blocks = out_features * in_features / QK4_0;
let mut blocks: Vec<BlockQ4_0> = vec![BlockQ4_0::zeros(); n_blocks];
BlockQ4_0::from_float(&f32_out_in, &mut blocks);
let bytes_len = n_blocks * std::mem::size_of::<BlockQ4_0>();
let byte_slice: &[u8] = fuel_quantized::blocks_as_bytes(&blocks);
let padded_len = bytes_len.div_ceil(4) * 4;
let mut padded = vec![0_u8; padded_len];
padded[..bytes_len].copy_from_slice(byte_slice);
let words: Vec<u32> = padded
.as_chunks::<4>()
.0
.iter()
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
Ok(WeightStorage::Q4_0 {
words: Arc::from(words),
bytes_len,
in_features,
out_features,
})
}
fn bytes_to_u32_arc(bytes: &[u8]) -> Arc<[u32]> {
let padded_len = bytes.len().div_ceil(4) * 4;
let mut padded = vec![0_u8; padded_len];
padded[..bytes.len()].copy_from_slice(bytes);
let words: Vec<u32> = padded
.as_chunks::<4>()
.0
.iter()
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
Arc::from(words)
}
fn dequant_bytes_to_f32(
bytes: &[u8],
dt: fuel_loaders::quantized::GgmlDType,
name: &str,
) -> Result<Vec<f32>> {
fuel_quantized::dequant_ggml_bytes(bytes, dt, name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::lazy_t5::T5Activation;
use fuel_tensor::lazy::Tensor;
fn test_cfg() -> T5Config {
T5Config {
vocab_size: 32,
d_model: 32,
d_kv: 8,
d_ff: 64,
num_layers: 2,
num_decoder_layers: None,
num_heads: 4,
relative_attention_num_buckets: 8,
relative_attention_max_distance: 32,
layer_norm_epsilon: 1e-6,
activation: T5Activation::Relu,
gated_ffn: false,
tie_word_embeddings: true,
}
}
fn tiny_weights(cfg: &T5Config) -> T5Weights {
let mut s: u32 = 75757;
let mut next = move || -> f32 {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.05
};
let mut vec_of =
|n: usize| -> Arc<[f32]> { Arc::from((0..n).map(|_| next()).collect::<Vec<_>>()) };
let d = cfg.d_model;
let inner = cfg.inner_dim();
let d_ff = cfg.d_ff;
let mk_attn = |vec_of: &mut dyn FnMut(usize) -> Arc<[f32]>| T5AttentionWeights {
q: WeightStorage::F32(vec_of(d * inner)),
k: WeightStorage::F32(vec_of(d * inner)),
v: WeightStorage::F32(vec_of(d * inner)),
o: WeightStorage::F32(vec_of(inner * d)),
};
let mk_ffn = |vec_of: &mut dyn FnMut(usize) -> Arc<[f32]>| -> T5FfnWeights {
if cfg.gated_ffn {
T5FfnWeights::Gated {
wi_0: WeightStorage::F32(vec_of(d * d_ff)),
wi_1: WeightStorage::F32(vec_of(d * d_ff)),
wo: WeightStorage::F32(vec_of(d_ff * d)),
}
} else {
T5FfnWeights::Dense {
wi: WeightStorage::F32(vec_of(d * d_ff)),
wo: WeightStorage::F32(vec_of(d_ff * d)),
}
}
};
let shared_embedding = vec_of(cfg.vocab_size * d);
let encoder_rel_bias = vec_of(cfg.relative_attention_num_buckets * cfg.num_heads);
let decoder_rel_bias = vec_of(cfg.relative_attention_num_buckets * cfg.num_heads);
let encoder_layers: Vec<T5EncoderLayerWeights> = (0..cfg.num_layers)
.map(|_| T5EncoderLayerWeights {
self_attn_norm_gain: Arc::from(vec![1.0_f32; d]),
self_attn: mk_attn(&mut vec_of),
ffn_norm_gain: Arc::from(vec![1.0_f32; d]),
ffn: mk_ffn(&mut vec_of),
})
.collect();
let decoder_layers: Vec<T5DecoderLayerWeights> = (0..cfg.num_decoder_layers_resolved())
.map(|_| T5DecoderLayerWeights {
self_attn_norm_gain: Arc::from(vec![1.0_f32; d]),
self_attn: mk_attn(&mut vec_of),
cross_attn_norm_gain: Arc::from(vec![1.0_f32; d]),
cross_attn: mk_attn(&mut vec_of),
ffn_norm_gain: Arc::from(vec![1.0_f32; d]),
ffn: mk_ffn(&mut vec_of),
})
.collect();
let encoder_final_norm_gain = Arc::from(vec![1.0_f32; d]);
let decoder_final_norm_gain = Arc::from(vec![1.0_f32; d]);
let lm_head = if cfg.tie_word_embeddings {
None
} else {
Some(WeightStorage::F32(vec_of(d * cfg.vocab_size)))
};
T5Weights {
shared_embedding,
encoder_rel_bias,
decoder_rel_bias,
encoder_layers,
decoder_layers,
encoder_final_norm_gain,
decoder_final_norm_gain,
lm_head,
}
}
#[test]
fn forward_shape_finite_with_q4_0_weights() {
let cfg = test_cfg();
let src = tiny_weights(&cfg);
let model = QuantizedT5Model::from_f32_bake(cfg.clone(), src).unwrap();
let e0 = &model.inner().weights.encoder_layers[0];
assert!(matches!(e0.self_attn.q, WeightStorage::Q4_0 { .. }));
assert!(matches!(e0.self_attn.k, WeightStorage::Q4_0 { .. }));
assert!(matches!(e0.self_attn.v, WeightStorage::Q4_0 { .. }));
assert!(matches!(e0.self_attn.o, WeightStorage::Q4_0 { .. }));
match &e0.ffn {
T5FfnWeights::Dense { wi, wo } => {
assert!(matches!(wi, WeightStorage::Q4_0 { .. }));
assert!(matches!(wo, WeightStorage::Q4_0 { .. }));
}
_ => panic!("expected dense FFN"),
}
let d0 = &model.inner().weights.decoder_layers[0];
assert!(matches!(d0.self_attn.q, WeightStorage::Q4_0 { .. }));
assert!(matches!(d0.cross_attn.q, WeightStorage::Q4_0 { .. }));
let src_tokens = [1_u32, 2, 3, 4];
let tgt_tokens = [5_u32, 6, 7];
let logits = model.forward(&src_tokens, &tgt_tokens).unwrap();
assert_eq!(
logits.shape().dims(),
&[1, tgt_tokens.len(), cfg.vocab_size]
);
for &v in &logits.realize_f32() {
assert!(v.is_finite(), "non-finite logit: {v}");
}
}
#[test]
fn gated_ffn_quantizes_three_matrices() {
let cfg = T5Config {
gated_ffn: true,
activation: T5Activation::GeluPytorchTanh,
..test_cfg()
};
let src = tiny_weights(&cfg);
let model = QuantizedT5Model::from_f32_bake(cfg.clone(), src).unwrap();
let e0 = &model.inner().weights.encoder_layers[0];
match &e0.ffn {
T5FfnWeights::Gated { wi_0, wi_1, wo } => {
assert!(matches!(wi_0, WeightStorage::Q4_0 { .. }));
assert!(matches!(wi_1, WeightStorage::Q4_0 { .. }));
assert!(matches!(wo, WeightStorage::Q4_0 { .. }));
}
_ => panic!("expected gated FFN"),
}
let src_tokens = [1_u32, 2, 3];
let tgt_tokens = [4_u32, 5];
let logits = model.forward(&src_tokens, &tgt_tokens).unwrap();
assert_eq!(
logits.shape().dims(),
&[1, tgt_tokens.len(), cfg.vocab_size]
);
}
#[test]
fn separate_lm_head_quantized_when_untied() {
let cfg = T5Config {
tie_word_embeddings: false,
..test_cfg()
};
let src = tiny_weights(&cfg);
let model = QuantizedT5Model::from_f32_bake(cfg.clone(), src).unwrap();
let lm_head = model
.inner()
.weights
.lm_head
.as_ref()
.expect("lm_head present");
assert!(matches!(lm_head, WeightStorage::Q4_0 { .. }));
}
#[test]
fn rejects_non_q4_0_divisible_d_model() {
let cfg = T5Config {
d_model: 30,
..test_cfg()
};
let src = tiny_weights(&cfg);
assert!(QuantizedT5Model::from_f32_bake(cfg, src).is_err());
}
#[test]
fn forward_encoder_embeds_matches_forward_encoder() {
let cfg = test_cfg();
let src = tiny_weights(&cfg);
let model = QuantizedT5Model::from_f32_bake(cfg.clone(), src).unwrap();
let src_tokens = [1_u32, 2, 3];
let enc_ref = model.forward_encoder(&src_tokens).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_tokens).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-4,
"Quantized T5 forward_encoder vs forward_encoder_embeds must agree (max diff {max_diff})"
);
}
}