use fuel_ir::Shape;
use fuel_ir::error::Result;
use fuel_tensor::Device;
use fuel_tensor::{lazy::LayerNormPair, lazy::Tensor, lazy::WeightStorage};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub struct PersimmonConfig {
pub vocab_size: usize,
pub hidden_size: usize,
pub intermediate_size: usize,
pub num_hidden_layers: usize,
pub num_attention_heads: usize,
pub num_key_value_heads: usize,
pub head_dim: usize,
pub layer_norm_eps: f64,
pub rope_theta: f64,
pub max_position_embeddings: usize,
pub partial_rotary_factor: f64,
pub qk_layernorm: bool,
}
impl PersimmonConfig {
pub fn rope_dim(&self) -> usize {
let rd = (self.head_dim as f64 * self.partial_rotary_factor) as usize;
(rd / 2) * 2
}
}
#[derive(Debug, Clone, serde::Deserialize)]
struct PersimmonConfigRaw {
vocab_size: usize,
hidden_size: usize,
intermediate_size: usize,
num_hidden_layers: usize,
num_attention_heads: usize,
#[serde(default)]
num_key_value_heads: Option<usize>,
#[serde(default)]
head_dim: Option<usize>,
#[serde(default = "default_persimmon_layer_norm_eps")]
layer_norm_eps: f64,
#[serde(default = "default_persimmon_rope_theta")]
rope_theta: f64,
max_position_embeddings: usize,
#[serde(default = "default_persimmon_partial_rotary_factor")]
partial_rotary_factor: f64,
#[serde(default = "default_persimmon_qk_layernorm")]
qk_layernorm: bool,
}
fn default_persimmon_layer_norm_eps() -> f64 {
1e-5
}
fn default_persimmon_rope_theta() -> f64 {
25_000.0
}
fn default_persimmon_partial_rotary_factor() -> f64 {
0.5
}
fn default_persimmon_qk_layernorm() -> bool {
true
}
impl PersimmonConfigRaw {
fn from_json_str(json: &str) -> fuel_ir::error::Result<Self> {
serde_json::from_str(json)
.map_err(|e| fuel_ir::error::Error::Msg(format!("parsing Persimmon config.json: {e}")))
}
fn resolve(self) -> Result<PersimmonConfig> {
Ok(PersimmonConfig {
vocab_size: self.vocab_size,
hidden_size: self.hidden_size,
intermediate_size: self.intermediate_size,
num_hidden_layers: self.num_hidden_layers,
num_attention_heads: self.num_attention_heads,
num_key_value_heads: fuel_loaders::hf_config::num_key_value_heads(
self.num_key_value_heads,
self.num_attention_heads,
)?,
head_dim: fuel_loaders::hf_config::head_dim(
self.head_dim,
self.hidden_size,
self.num_attention_heads,
)?,
layer_norm_eps: self.layer_norm_eps,
rope_theta: self.rope_theta,
max_position_embeddings: self.max_position_embeddings,
partial_rotary_factor: self.partial_rotary_factor,
qk_layernorm: self.qk_layernorm,
})
}
}
impl PersimmonConfig {
pub fn from_hf_json_str(json: &str) -> fuel_ir::error::Result<Self> {
PersimmonConfigRaw::from_json_str(json)?.resolve()
}
}
#[derive(Debug, Clone)]
pub struct PersimmonLayerWeights {
pub input_ln_gain: Arc<[f32]>,
pub input_ln_bias: Arc<[f32]>,
pub post_attn_ln_gain: Arc<[f32]>,
pub post_attn_ln_bias: Arc<[f32]>,
pub attn_q: WeightStorage,
pub attn_q_bias: Arc<[f32]>,
pub attn_k: WeightStorage,
pub attn_k_bias: Arc<[f32]>,
pub attn_v: WeightStorage,
pub attn_v_bias: Arc<[f32]>,
pub attn_o: WeightStorage,
pub attn_o_bias: Arc<[f32]>,
pub q_norm: Option<LayerNormPair>,
pub k_norm: Option<LayerNormPair>,
pub mlp_up: WeightStorage,
pub mlp_up_bias: Arc<[f32]>,
pub mlp_down: WeightStorage,
pub mlp_down_bias: Arc<[f32]>,
}
#[derive(Debug, Clone)]
pub struct PersimmonWeights {
pub token_embedding: Arc<[f32]>,
pub layers: Vec<PersimmonLayerWeights>,
pub final_ln_gain: Arc<[f32]>,
pub final_ln_bias: Arc<[f32]>,
pub output: WeightStorage,
}
#[derive(Debug, Clone)]
pub struct PersimmonModel {
pub config: PersimmonConfig,
pub weights: PersimmonWeights,
}
impl PersimmonModel {
pub fn forward(&self, tokens: &[u32], start_pos: usize) -> Result<Tensor> {
let h_norm = self.run_backbone(tokens, start_pos)?;
self.apply_lm_head(&h_norm)
}
pub fn forward_hidden(&self, tokens: &[u32], start_pos: usize) -> Result<Tensor> {
self.run_backbone(tokens, start_pos)
}
pub fn forward_embeds(&self, embeds: &Tensor, start_pos: usize) -> Result<Tensor> {
let h_norm = self.run_backbone_embeds(embeds, start_pos)?;
self.apply_lm_head(&h_norm)
}
pub fn forward_hidden_embeds(&self, embeds: &Tensor, start_pos: usize) -> Result<Tensor> {
self.run_backbone_embeds(embeds, start_pos)
}
pub fn embed_tokens_anchored(&self, anchor: &Tensor, tokens: &[u32]) -> Result<Tensor> {
let cfg = &self.config;
anchor.embed_tokens_anchored(
self.weights.token_embedding.clone(),
cfg.vocab_size,
cfg.hidden_size,
tokens,
)
}
fn apply_lm_head(&self, h_norm: &Tensor) -> Result<Tensor> {
let cfg = &self.config;
self.weights
.output
.apply_linear(h_norm, cfg.hidden_size, cfg.vocab_size)
}
fn run_backbone(&self, tokens: &[u32], start_pos: usize) -> Result<Tensor> {
let cfg = &self.config;
let weights = &self.weights;
let seq = tokens.len();
assert!(seq > 0);
let h = Tensor::embed_tokens(
weights.token_embedding.clone(),
cfg.vocab_size,
cfg.hidden_size,
tokens,
&Device::cpu(),
)?;
self.run_backbone_embeds(&h, start_pos)
}
fn run_backbone_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();
if dims.len() != 3 || dims[0] != 1 || dims[2] != cfg.hidden_size {
return Err(fuel_ir::error::Error::Msg(format!(
"PersimmonModel::forward_embeds: expected embeds shape (1, seq, hidden_size={}), got {:?}",
cfg.hidden_size, dims,
)).bt());
}
let seq = dims[1];
if seq == 0 {
return Err(fuel_ir::error::Error::Msg(
"PersimmonModel::forward_embeds: seq must be > 0".into(),
)
.bt());
}
if cfg.num_attention_heads * cfg.head_dim != cfg.hidden_size {
return Err(fuel_ir::error::Error::Msg(
"PersimmonConfig: num_attention_heads * head_dim must equal hidden_size".into(),
)
.bt());
}
let mut h = embeds.clone();
let rope_dim = cfg.rope_dim();
let (rope_cos, rope_sin) = h.rope_tables_const(cfg.rope_theta, start_pos, seq, rope_dim);
for layer in &weights.layers {
h = self.apply_layer(&h, layer, &rope_cos, &rope_sin)?;
}
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: &PersimmonLayerWeights,
rope_cos: &Tensor,
rope_sin: &Tensor,
) -> Result<Tensor> {
let cfg = &self.config;
let head_dim = cfg.head_dim;
let x_shape = x.shape();
let dims = x_shape.dims();
let batch = dims[0];
let seq = dims[1];
let kv_dim = cfg.num_key_value_heads * head_dim;
let x_norm = x.layer_norm_affine(
std::sync::Arc::clone(&layer.input_ln_gain),
std::sync::Arc::clone(&layer.input_ln_bias),
cfg.layer_norm_eps,
)?;
let q = layer.attn_q.apply_linear_with_bias(
&x_norm,
cfg.hidden_size,
cfg.hidden_size,
std::sync::Arc::clone(&layer.attn_q_bias),
)?;
let k = layer.attn_k.apply_linear_with_bias(
&x_norm,
cfg.hidden_size,
kv_dim,
std::sync::Arc::clone(&layer.attn_k_bias),
)?;
let v = layer.attn_v.apply_linear_with_bias(
&x_norm,
cfg.hidden_size,
kv_dim,
std::sync::Arc::clone(&layer.attn_v_bias),
)?;
let (q, k) = match (&layer.q_norm, &layer.k_norm) {
(Some((qg, qb)), Some((kg, kb))) => {
let q = q.layer_norm_affine(
std::sync::Arc::clone(qg),
std::sync::Arc::clone(qb),
cfg.layer_norm_eps,
)?;
let k = k.layer_norm_affine(
std::sync::Arc::clone(kg),
std::sync::Arc::clone(kb),
cfg.layer_norm_eps,
)?;
(q, k)
}
_ => (q, k),
};
let _ = (batch, seq);
let q = q.split_heads(cfg.num_attention_heads, head_dim)?;
let k = k.split_heads(cfg.num_key_value_heads, head_dim)?;
let v = v.split_heads(cfg.num_key_value_heads, head_dim)?;
let q_r = q.rope_partial(rope_cos, rope_sin, cfg.rope_dim())?;
let k_r = k.rope_partial(rope_cos, rope_sin, cfg.rope_dim())?;
let n_rep = cfg.num_attention_heads / cfg.num_key_value_heads;
let k_full = k_r.repeat_interleave(1_usize, n_rep)?;
let v_full = v.repeat_interleave(1_usize, n_rep)?;
let k_t = k_full.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_full)?;
let merged = attn_v.merge_heads()?;
let attn_out = layer.attn_o.apply_linear_with_bias(
&merged,
cfg.hidden_size,
cfg.hidden_size,
std::sync::Arc::clone(&layer.attn_o_bias),
)?;
let h1 = x.add(&attn_out)?;
let h1_norm = h1.layer_norm_affine(
std::sync::Arc::clone(&layer.post_attn_ln_gain),
std::sync::Arc::clone(&layer.post_attn_ln_bias),
cfg.layer_norm_eps,
)?;
let up = layer.mlp_up.apply_linear_with_bias(
&h1_norm,
cfg.hidden_size,
cfg.intermediate_size,
std::sync::Arc::clone(&layer.mlp_up_bias),
)?;
let up_act = up.relu();
let ffn_out = layer.mlp_down.apply_linear_with_bias(
&up_act,
cfg.intermediate_size,
cfg.hidden_size,
std::sync::Arc::clone(&layer.mlp_down_bias),
)?;
h1.add(&ffn_out)
}
}
impl PersimmonWeights {
pub fn load_from_mmapped(
st: &fuel_loaders::safetensors::MmapedSafetensors,
cfg: &PersimmonConfig,
) -> 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.intermediate_size;
let q_dim = cfg.num_attention_heads * cfg.head_dim;
let kv_dim = cfg.num_key_value_heads * cfg.head_dim;
let token_embedding = Arc::from(load_tensor_as_f32(st, "model.embed_tokens.weight")?);
let mut layers = Vec::with_capacity(cfg.num_hidden_layers);
for i in 0..cfg.num_hidden_layers {
let p = format!("model.layers.{i}");
let input_ln_gain = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.input_layernorm.weight"),
)?);
let input_ln_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.input_layernorm.bias"),
)?);
let post_attn_ln_gain = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.post_attention_layernorm.weight"),
)?);
let post_attn_ln_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.post_attention_layernorm.bias"),
)?);
let attn_q = ltm(st, &format!("{p}.self_attn.q_proj.weight"), q_dim, h)?;
let attn_q_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn.q_proj.bias"),
)?);
let attn_k = ltm(st, &format!("{p}.self_attn.k_proj.weight"), kv_dim, h)?;
let attn_k_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn.k_proj.bias"),
)?);
let attn_v = ltm(st, &format!("{p}.self_attn.v_proj.weight"), kv_dim, h)?;
let attn_v_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn.v_proj.bias"),
)?);
let attn_o = ltm(st, &format!("{p}.self_attn.dense.weight"), h, q_dim)?;
let attn_o_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn.dense.bias"),
)?);
let q_norm = if cfg.qk_layernorm {
let g = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn.q_layernorm.weight"),
)?);
let b = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn.q_layernorm.bias"),
)?);
Some((g, b))
} else {
None
};
let k_norm = if cfg.qk_layernorm {
let g = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn.k_layernorm.weight"),
)?);
let b = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.self_attn.k_layernorm.bias"),
)?);
Some((g, b))
} else {
None
};
let mlp_up = ltm(st, &format!("{p}.mlp.dense_h_to_4h.weight"), inter, h)?;
let mlp_up_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.mlp.dense_h_to_4h.bias"),
)?);
let mlp_down = ltm(st, &format!("{p}.mlp.dense_4h_to_h.weight"), h, inter)?;
let mlp_down_bias = Arc::from(load_tensor_as_f32(
st,
&format!("{p}.mlp.dense_4h_to_h.bias"),
)?);
layers.push(PersimmonLayerWeights {
input_ln_gain,
input_ln_bias,
post_attn_ln_gain,
post_attn_ln_bias,
attn_q,
attn_q_bias,
attn_k,
attn_k_bias,
attn_v,
attn_v_bias,
attn_o,
attn_o_bias,
q_norm,
k_norm,
mlp_up,
mlp_up_bias,
mlp_down,
mlp_down_bias,
});
}
let final_ln_gain = Arc::from(load_tensor_as_f32(st, "model.final_layernorm.weight")?);
let final_ln_bias = Arc::from(load_tensor_as_f32(st, "model.final_layernorm.bias")?);
let output = ltm(st, "lm_head.weight", cfg.vocab_size, h)?;
Ok(Self {
token_embedding,
layers,
final_ln_gain,
final_ln_bias,
output,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const PERSIMMON_8B_CONFIG_JSON: &str = r#"{
"architectures": ["PersimmonForCausalLM"],
"model_type": "persimmon",
"vocab_size": 262144,
"hidden_size": 4096,
"intermediate_size": 16384,
"num_hidden_layers": 36,
"num_attention_heads": 64,
"num_key_value_heads": 64,
"max_position_embeddings": 16384,
"layer_norm_eps": 1e-05,
"rms_norm_eps": 1e-06,
"rope_theta": 25000.0,
"qk_layernorm": true
}"#;
#[test]
fn persimmon_config_from_hf_json_parses_the_artifact() {
let cfg = PersimmonConfig::from_hf_json_str(PERSIMMON_8B_CONFIG_JSON).unwrap();
assert_eq!(cfg.hidden_size, 4096);
assert_eq!(cfg.num_hidden_layers, 36);
assert_eq!(cfg.num_attention_heads, 64);
assert_eq!(cfg.vocab_size, 262_144);
assert_eq!(cfg.intermediate_size, 16_384);
assert_eq!(cfg.num_key_value_heads, 64);
assert_eq!(cfg.head_dim, 64);
assert_eq!(cfg.layer_norm_eps, 1e-5);
assert_eq!(cfg.rope_theta, 25_000.0);
assert!(cfg.qk_layernorm); assert_eq!(cfg.partial_rotary_factor, 0.5);
}
#[test]
fn persimmon_config_reads_a_second_distinct_config_with_explicit_head_dim() {
let json = r#"{
"model_type": "persimmon",
"vocab_size": 50000,
"hidden_size": 1024,
"intermediate_size": 4096,
"num_hidden_layers": 8,
"num_attention_heads": 16,
"num_key_value_heads": 4,
"head_dim": 80,
"max_position_embeddings": 2048
}"#;
let cfg = PersimmonConfig::from_hf_json_str(json).unwrap();
assert_eq!(cfg.num_key_value_heads, 4);
assert_eq!(cfg.head_dim, 80);
assert_ne!(cfg.head_dim, 1024 / 16);
assert_eq!(cfg.layer_norm_eps, 1e-5);
assert_eq!(cfg.rope_theta, 25_000.0);
assert_eq!(cfg.partial_rotary_factor, 0.5);
assert!(cfg.qk_layernorm); assert_ne!(cfg.hidden_size, 4096);
}
#[test]
fn persimmon_config_gqa_defaults_to_num_heads_when_absent() {
let json = r#"{
"model_type": "persimmon",
"vocab_size": 1000,
"hidden_size": 64,
"intermediate_size": 128,
"num_hidden_layers": 2,
"num_attention_heads": 8,
"max_position_embeddings": 128
}"#;
let cfg = PersimmonConfig::from_hf_json_str(json).unwrap();
assert_eq!(cfg.num_key_value_heads, 8);
}
#[test]
fn persimmon_config_preserves_true_mqa() {
let json = r#"{
"model_type": "persimmon",
"vocab_size": 1000,
"hidden_size": 64,
"intermediate_size": 128,
"num_hidden_layers": 2,
"num_attention_heads": 8,
"num_key_value_heads": 1,
"max_position_embeddings": 128
}"#;
let cfg = PersimmonConfig::from_hf_json_str(json).unwrap();
assert_eq!(cfg.num_key_value_heads, 1);
}
fn tiny_weights(cfg: &PersimmonConfig) -> PersimmonWeights {
let mut s: u32 = 77777;
let next = || -> 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 i = cfg.intermediate_size;
let kv = cfg.num_key_value_heads * cfg.head_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<PersimmonLayerWeights> = (0..cfg.num_hidden_layers)
.map(|_| PersimmonLayerWeights {
input_ln_gain: Arc::from(vec![1.0_f32; h]),
input_ln_bias: Arc::from(vec![0.0_f32; h]),
post_attn_ln_gain: Arc::from(vec![1.0_f32; h]),
post_attn_ln_bias: Arc::from(vec![0.0_f32; h]),
attn_q: WeightStorage::F32(vec_of(h * h, &mut *nb)),
attn_q_bias: vec_of(h, &mut *nb),
attn_k: WeightStorage::F32(vec_of(h * kv, &mut *nb)),
attn_k_bias: vec_of(kv, &mut *nb),
attn_v: WeightStorage::F32(vec_of(h * kv, &mut *nb)),
attn_v_bias: vec_of(kv, &mut *nb),
attn_o: WeightStorage::F32(vec_of(h * h, &mut *nb)),
attn_o_bias: vec_of(h, &mut *nb),
q_norm: if cfg.qk_layernorm {
Some((Arc::from(vec![1.0_f32; h]), Arc::from(vec![0.0_f32; h])))
} else {
None
},
k_norm: if cfg.qk_layernorm {
Some((Arc::from(vec![1.0_f32; kv]), Arc::from(vec![0.0_f32; kv])))
} else {
None
},
mlp_up: WeightStorage::F32(vec_of(h * i, &mut *nb)),
mlp_up_bias: vec_of(i, &mut *nb),
mlp_down: WeightStorage::F32(vec_of(i * h, &mut *nb)),
mlp_down_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 output = WeightStorage::F32(vec_of(h * cfg.vocab_size, &mut *nb));
PersimmonWeights {
token_embedding,
layers,
final_ln_gain,
final_ln_bias,
output,
}
}
#[test]
fn forward_with_qk_layernorm_and_partial_rotary() {
let cfg = PersimmonConfig {
vocab_size: 32,
hidden_size: 16,
intermediate_size: 32,
num_hidden_layers: 2,
num_attention_heads: 4,
num_key_value_heads: 4,
head_dim: 4,
layer_norm_eps: 1e-5,
rope_theta: 25_000.0,
max_position_embeddings: 64,
partial_rotary_factor: 1.0,
qk_layernorm: true,
};
let model = PersimmonModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
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());
}
}
#[test]
fn forward_hidden_shape_and_finite() {
let cfg = PersimmonConfig {
vocab_size: 32,
hidden_size: 16,
intermediate_size: 32,
num_hidden_layers: 2,
num_attention_heads: 4,
num_key_value_heads: 4,
head_dim: 4,
layer_norm_eps: 1e-5,
rope_theta: 25_000.0,
max_position_embeddings: 64,
partial_rotary_factor: 1.0,
qk_layernorm: true,
};
let model = PersimmonModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let tokens: Vec<u32> = vec![1, 2, 3, 4];
let hidden = model.forward_hidden(&tokens, 0).unwrap();
assert_eq!(hidden.shape().dims(), &[1, tokens.len(), cfg.hidden_size]);
for &v in &hidden.realize_f32() {
assert!(v.is_finite(), "non-finite hidden: {v}");
}
}
fn forward_embeds_test_cfg() -> PersimmonConfig {
PersimmonConfig {
vocab_size: 32,
hidden_size: 16,
intermediate_size: 32,
num_hidden_layers: 2,
num_attention_heads: 4,
num_key_value_heads: 4,
head_dim: 4,
layer_norm_eps: 1e-5,
rope_theta: 25_000.0,
max_position_embeddings: 64,
partial_rotary_factor: 1.0,
qk_layernorm: true,
}
}
#[test]
fn forward_embeds_matches_forward_after_token_lookup() {
let cfg = forward_embeds_test_cfg();
let model = PersimmonModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
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-5,
"Persimmon forward vs forward_embeds must agree (max diff {max_diff})"
);
}
#[test]
fn forward_embeds_rejects_bad_shape() {
let cfg = forward_embeds_test_cfg();
let model = PersimmonModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let bad = Tensor::from_f32(
vec![0.0_f32; 3 * (cfg.hidden_size + 1)],
Shape::from_dims(&[1, 3, cfg.hidden_size + 1]),
&Device::cpu(),
)
.unwrap();
assert!(model.forward_embeds(&bad, 0).is_err());
}
#[test]
fn forward_hidden_embeds_matches_forward_hidden() {
let cfg = forward_embeds_test_cfg();
let model = PersimmonModel {
config: cfg.clone(),
weights: tiny_weights(&cfg),
};
let tokens: Vec<u32> = vec![5, 7];
let h_ref = model.forward_hidden(&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 h_via_embeds = model
.forward_hidden_embeds(&embeds, 0)
.unwrap()
.realize_f32();
let max_diff = h_ref
.iter()
.zip(h_via_embeds.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff < 1e-5,
"Persimmon forward_hidden vs forward_hidden_embeds must agree (max diff {max_diff})"
);
}
}