use crate::models::lazy_phi3::{Phi3Config, Phi3Model, Phi3Weights};
#[cfg(test)]
use fuel_ir::Shape;
use fuel_ir::error::Result;
#[cfg(test)]
use fuel_tensor::Device;
use fuel_tensor::{lazy::LayerWeights, lazy::Tensor, lazy::WeightStorage};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct QuantizedPhi3Model {
inner: Phi3Model,
}
impl QuantizedPhi3Model {
pub fn forward(&self, tokens: &[u32], start_pos: usize) -> Result<Tensor> {
self.inner.forward(tokens, start_pos)
}
pub fn forward_embeds(&self, embeds: &Tensor, start_pos: usize) -> Result<Tensor> {
self.inner.forward_embeds(embeds, start_pos)
}
pub fn forward_hidden(&self, tokens: &[u32], start_pos: usize) -> Result<Tensor> {
self.inner.forward_hidden(tokens, start_pos)
}
pub fn forward_hidden_embeds(&self, embeds: &Tensor, start_pos: usize) -> Result<Tensor> {
self.inner.forward_hidden_embeds(embeds, start_pos)
}
pub fn embed_tokens_anchored(&self, anchor: &Tensor, tokens: &[u32]) -> Result<Tensor> {
self.inner.embed_tokens_anchored(anchor, tokens)
}
pub fn forward_with_kv_context(
&self,
tokens: &[u32],
cache: &mut fuel_tensor::inference_context::KvCache,
ctx: &mut fuel_tensor::inference_context::InferenceContext,
) -> Result<Vec<f32>> {
self.inner.forward_with_kv_context(tokens, cache, ctx)
}
pub fn forward_with_kv_context_persistent(
&self,
tokens: &[u32],
cache: &mut fuel_tensor::inference_context::KvCache,
ctx: &mut fuel_tensor::inference_context::InferenceContext,
session: &mut Option<fuel_tensor::inference_context::DecodeSession>,
) -> Result<Vec<f32>> {
self.inner
.forward_with_kv_context_persistent(tokens, cache, ctx, session)
}
pub fn forward_decode_step(
&self,
tokens: &[u32],
cache: &mut fuel_tensor::inference_context::KvCache,
ctx: &mut fuel_tensor::inference_context::InferenceContext,
) -> Result<Vec<f32>> {
self.inner.forward_decode_step(tokens, cache, ctx)
}
pub fn config(&self) -> &Phi3Config {
&self.inner.config
}
pub fn inner(&self) -> &Phi3Model {
&self.inner
}
pub fn load_from_mmapped(
st: &fuel_loaders::safetensors::MmapedSafetensors,
cfg: Phi3Config,
) -> Result<Self> {
let src = Phi3Weights::load_from_mmapped(st, &cfg)?;
Self::from_f32_bake(cfg, src)
}
pub fn from_f32_bake(cfg: Phi3Config, src: Phi3Weights) -> Result<Self> {
let h = cfg.hidden_size;
let i = cfg.intermediate_size;
let head_dim = cfg.head_dim();
let kv = cfg.num_key_value_heads * head_dim;
check_q4_0_divisible(
"hidden_size (attn_q/k/v/o, ffn_gate/up, output in-features)",
h,
)?;
check_q4_0_divisible("intermediate_size (ffn_down in-features)", i)?;
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(
"QuantizedPhi3Model::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!(
"QuantizedPhi3Model::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 mut layers: Vec<LayerWeights> = Vec::with_capacity(cfg.num_hidden_layers);
for (idx, layer) in src.layers.into_iter().enumerate() {
let attn_q =
quantize_linear(&layer.attn_q, h, h).map_err(|e| layer_err(idx, "attn_q", e))?;
let attn_k =
quantize_linear(&layer.attn_k, h, kv).map_err(|e| layer_err(idx, "attn_k", e))?;
let attn_v =
quantize_linear(&layer.attn_v, h, kv).map_err(|e| layer_err(idx, "attn_v", e))?;
let attn_o =
quantize_linear(&layer.attn_o, h, h).map_err(|e| layer_err(idx, "attn_o", e))?;
let ffn_gate = quantize_linear(&layer.ffn_gate, h, i)
.map_err(|e| layer_err(idx, "ffn_gate", e))?;
let ffn_up =
quantize_linear(&layer.ffn_up, h, i).map_err(|e| layer_err(idx, "ffn_up", e))?;
let ffn_down = quantize_linear(&layer.ffn_down, i, h)
.map_err(|e| layer_err(idx, "ffn_down", e))?;
layers.push(LayerWeights {
attn_q,
attn_q_bias: layer.attn_q_bias,
attn_k,
attn_k_bias: layer.attn_k_bias,
attn_v,
attn_v_bias: layer.attn_v_bias,
attn_o,
ffn_gate,
ffn_up,
ffn_down,
attn_norm_gain: layer.attn_norm_gain,
ffn_norm_gain: layer.ffn_norm_gain,
});
}
let output = quantize_linear(&src.output, h, cfg.vocab_size)
.map_err(|e| fuel_ir::error::Error::Msg(format!("output: {e}")).bt())?;
let inner = Phi3Model {
config: cfg,
weights: Phi3Weights {
instance: fuel_tensor::decode_shape::ModelInstanceId::next(),
token_embedding: src.token_embedding,
layers,
final_norm_gain: src.final_norm_gain,
output,
},
};
Ok(Self { inner })
}
pub fn from_gguf<P: AsRef<std::path::Path>>(path: P, cfg: &Phi3Config) -> 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 h = cfg.hidden_size;
let i = cfg.intermediate_size;
let head_dim = cfg.head_dim();
let kv = cfg.num_key_value_heads * head_dim;
let token_embedding = load_f32("token_embd.weight")?;
if token_embedding.len() != cfg.vocab_size * h {
return Err(fuel_ir::error::Error::Msg(format!(
"gguf token_embd.weight: {} elems, expected {}×{}",
token_embedding.len(),
cfg.vocab_size,
h,
))
.bt());
}
let mut layers: Vec<LayerWeights> = Vec::with_capacity(cfg.num_hidden_layers);
for idx in 0..cfg.num_hidden_layers {
let prefix = format!("blk.{idx}");
let attn_q = load_weight(&format!("{prefix}.attn_q.weight"), h, h)?;
let attn_k = load_weight(&format!("{prefix}.attn_k.weight"), kv, h)?;
let attn_v = load_weight(&format!("{prefix}.attn_v.weight"), kv, h)?;
let attn_o = load_weight(&format!("{prefix}.attn_output.weight"), h, h)?;
let ffn_gate = load_weight(&format!("{prefix}.ffn_gate.weight"), i, h)?;
let ffn_up = load_weight(&format!("{prefix}.ffn_up.weight"), i, h)?;
let ffn_down = load_weight(&format!("{prefix}.ffn_down.weight"), h, i)?;
let attn_norm_gain: Arc<[f32]> =
Arc::from(load_f32(&format!("{prefix}.attn_norm.weight"))?);
let ffn_norm_gain: Arc<[f32]> =
Arc::from(load_f32(&format!("{prefix}.ffn_norm.weight"))?);
layers.push(LayerWeights {
attn_q,
attn_q_bias: None,
attn_k,
attn_k_bias: None,
attn_v,
attn_v_bias: None,
attn_o,
ffn_gate,
ffn_up,
ffn_down,
attn_norm_gain,
ffn_norm_gain,
});
}
let final_norm_gain: Arc<[f32]> = Arc::from(load_f32("output_norm.weight")?);
let output = if content.tensor_infos.contains_key("output.weight") {
load_weight("output.weight", cfg.vocab_size, h)?
} else {
let mut f32_in_out = vec![0.0_f32; h * cfg.vocab_size];
for v in 0..cfg.vocab_size {
for j in 0..h {
f32_in_out[j * cfg.vocab_size + v] = token_embedding[v * h + j];
}
}
WeightStorage::F32(Arc::from(f32_in_out))
};
let inner = Phi3Model {
config: cfg.clone(),
weights: Phi3Weights {
instance: fuel_tensor::decode_shape::ModelInstanceId::next(),
token_embedding: Arc::from(token_embedding),
layers,
final_norm_gain,
output,
},
};
Ok(Self { inner })
}
}
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!(
"QuantizedPhi3Model::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::*;
fn test_cfg() -> Phi3Config {
Phi3Config {
vocab_size: 64,
hidden_size: 32,
intermediate_size: 64,
num_hidden_layers: 2,
num_attention_heads: 4,
num_key_value_heads: 4,
max_position_embeddings: 64,
rope_theta: 10_000.0,
rms_norm_eps: 1e-5,
tie_word_embeddings: false,
}
}
fn tiny_weights(cfg: &Phi3Config) -> Phi3Weights {
let mut s: u32 = 13579;
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 h = cfg.hidden_size;
let i = cfg.intermediate_size;
let kv = cfg.num_key_value_heads * cfg.head_dim();
let token_embedding = vec_of(cfg.vocab_size * h);
let layers: Vec<LayerWeights> = (0..cfg.num_hidden_layers)
.map(|_| LayerWeights {
attn_q: WeightStorage::F32(vec_of(h * h)),
attn_q_bias: None,
attn_k: WeightStorage::F32(vec_of(h * kv)),
attn_k_bias: None,
attn_v: WeightStorage::F32(vec_of(h * kv)),
attn_v_bias: None,
attn_o: WeightStorage::F32(vec_of(h * h)),
ffn_gate: WeightStorage::F32(vec_of(h * i)),
ffn_up: WeightStorage::F32(vec_of(h * i)),
ffn_down: WeightStorage::F32(vec_of(i * h)),
attn_norm_gain: Arc::from(vec![1.0_f32; h]),
ffn_norm_gain: Arc::from(vec![1.0_f32; h]),
})
.collect();
let final_norm_gain = Arc::from(vec![1.0_f32; h]);
let output = WeightStorage::F32(vec_of(h * cfg.vocab_size));
Phi3Weights {
instance: fuel_tensor::decode_shape::ModelInstanceId::next(),
token_embedding,
layers,
final_norm_gain,
output,
}
}
#[test]
fn quantized_phi3_decode_matches_quantized_forward() {
use fuel_ir::DType;
use fuel_tensor::Device;
use fuel_tensor::{
inference_context::DecodeSession, inference_context::InferenceContext,
inference_context::KvCache,
};
let cfg = test_cfg();
let model = QuantizedPhi3Model::from_f32_bake(cfg.clone(), tiny_weights(&cfg)).unwrap();
let tokens: Vec<u32> = vec![1, 2, 3, 4, 5, 6];
let prefill = 3;
let dev = Device::cpu();
let mut cache = KvCache::with_capacity(
cfg.num_hidden_layers,
cfg.num_key_value_heads,
cfg.head_dim(),
tokens.len(),
DType::F32,
&dev,
)
.expect("with_capacity");
let mut ctx = InferenceContext::new(dev);
let mut session: Option<DecodeSession> = None;
model
.forward_with_kv_context_persistent(
&tokens[..prefill],
&mut cache,
&mut ctx,
&mut session,
)
.expect("prefill");
for pos in prefill..tokens.len() {
let got = model
.forward_with_kv_context_persistent(
&tokens[pos..=pos],
&mut cache,
&mut ctx,
&mut session,
)
.expect("decode");
let full = model.forward(&tokens[..=pos], 0).unwrap().realize_f32();
let expected = &full[pos * cfg.vocab_size..(pos + 1) * cfg.vocab_size];
let worst = got
.iter()
.zip(expected.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
assert!(
worst < 1e-5,
"quantized Phi3 decode at position {pos} diverged by {worst}"
);
}
assert_eq!(cache.cached_len, tokens.len());
}
#[test]
fn forward_shape_finite_with_q4_0_weights() {
let cfg = test_cfg();
let src = tiny_weights(&cfg);
let model = QuantizedPhi3Model::from_f32_bake(cfg.clone(), src).unwrap();
let l0 = &model.inner().weights.layers[0];
assert!(matches!(l0.attn_q, WeightStorage::Q4_0 { .. }));
assert!(matches!(l0.attn_k, WeightStorage::Q4_0 { .. }));
assert!(matches!(l0.attn_v, WeightStorage::Q4_0 { .. }));
assert!(matches!(l0.attn_o, WeightStorage::Q4_0 { .. }));
assert!(matches!(l0.ffn_gate, WeightStorage::Q4_0 { .. }));
assert!(matches!(l0.ffn_up, WeightStorage::Q4_0 { .. }));
assert!(matches!(l0.ffn_down, WeightStorage::Q4_0 { .. }));
assert!(matches!(
model.inner().weights.output,
WeightStorage::Q4_0 { .. }
));
assert!(l0.attn_q_bias.is_none());
assert!(l0.attn_k_bias.is_none());
assert!(l0.attn_v_bias.is_none());
let logits = model.forward(&[1, 2, 3], 0).unwrap();
assert_eq!(logits.shape().dims(), &[1, 3, cfg.vocab_size]);
for &v in &logits.realize_f32() {
assert!(v.is_finite(), "non-finite logit: {v}");
}
}
#[test]
fn from_f32_bake_rejects_non_divisible_hidden_size() {
let bad_cfg = Phi3Config {
vocab_size: 32,
hidden_size: 30,
intermediate_size: 64,
num_hidden_layers: 1,
num_attention_heads: 2,
num_key_value_heads: 2,
max_position_embeddings: 32,
rope_theta: 10_000.0,
rms_norm_eps: 1e-5,
tie_word_embeddings: false,
};
let h = bad_cfg.hidden_size;
let i = bad_cfg.intermediate_size;
let kv = bad_cfg.num_key_value_heads * bad_cfg.head_dim();
let stub_layer = LayerWeights {
attn_q: WeightStorage::F32(Arc::from(vec![0.0_f32; h * h])),
attn_q_bias: None,
attn_k: WeightStorage::F32(Arc::from(vec![0.0_f32; h * kv])),
attn_k_bias: None,
attn_v: WeightStorage::F32(Arc::from(vec![0.0_f32; h * kv])),
attn_v_bias: None,
attn_o: WeightStorage::F32(Arc::from(vec![0.0_f32; h * h])),
ffn_gate: WeightStorage::F32(Arc::from(vec![0.0_f32; h * i])),
ffn_up: WeightStorage::F32(Arc::from(vec![0.0_f32; h * i])),
ffn_down: WeightStorage::F32(Arc::from(vec![0.0_f32; i * h])),
attn_norm_gain: Arc::from(vec![1.0_f32; h]),
ffn_norm_gain: Arc::from(vec![1.0_f32; h]),
};
let src = Phi3Weights {
instance: fuel_tensor::decode_shape::ModelInstanceId::next(),
token_embedding: Arc::from(vec![0.0_f32; bad_cfg.vocab_size * h]),
layers: vec![stub_layer],
final_norm_gain: Arc::from(vec![1.0_f32; h]),
output: WeightStorage::F32(Arc::from(vec![0.0_f32; h * bad_cfg.vocab_size])),
};
assert!(QuantizedPhi3Model::from_f32_bake(bad_cfg, src).is_err());
}
#[test]
fn forward_embeds_matches_forward_after_token_lookup() {
let cfg = test_cfg();
let src = tiny_weights(&cfg);
let model = QuantizedPhi3Model::from_f32_bake(cfg.clone(), src).unwrap();
let tokens: Vec<u32> = vec![1, 2, 3];
let logits_ref = model.forward(&tokens, 0).unwrap().realize_f32();
let anchor =
Tensor::from_f32(vec![0.0_f32], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
let embeds = model.embed_tokens_anchored(&anchor, &tokens).unwrap();
let logits_via_embeds = model.forward_embeds(&embeds, 0).unwrap().realize_f32();
let max_diff = logits_ref
.iter()
.zip(logits_via_embeds.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff < 1e-4,
"Quantized Phi3 forward vs forward_embeds must agree (max diff {max_diff})"
);
}
}