use serde_json::json;
use super::*;
const D: usize = 8;
const HEADS: usize = 2;
const LAYERS: usize = 2;
const FFN: usize = 16;
const VOCAB: usize = 20;
const POSITIONS: usize = 32;
const EOS: usize = 2;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> f32 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 >> 40) as f32 / (1u64 << 23) as f32 - 1.0
}
fn fill(&mut self, n: usize) -> Vec<f32> {
(0..n).map(|_| self.next()).collect()
}
}
fn safetensors(tensors: &[(String, Vec<usize>, Vec<f32>)]) -> Vec<u8> {
let mut head = serde_json::Map::new();
let mut data = Vec::new();
for (name, shape, values) in tensors {
let start = data.len();
for v in values {
data.extend_from_slice(&v.to_le_bytes());
}
head.insert(
name.clone(),
json!({"dtype": "F32", "shape": shape, "data_offsets": [start, data.len()]}),
);
}
let head = serde_json::to_vec(&head).unwrap();
let mut out = (head.len() as u64).to_le_bytes().to_vec();
out.extend_from_slice(&head);
out.extend_from_slice(&data);
out
}
fn tiny_tensors(seed: u64) -> Vec<(String, Vec<usize>, Vec<f32>)> {
let mut r = Rng(seed);
let mut t = Vec::new();
let mut add = |name: String, shape: Vec<usize>, r: &mut Rng| {
let n = shape.iter().product();
t.push((name, shape, r.fill(n)));
};
add(
"model.decoder.embed_tokens.weight".into(),
vec![VOCAB, D],
&mut r,
);
add(
"model.decoder.embed_positions.weight".into(),
vec![POSITIONS + POSITION_OFFSET, D],
&mut r,
);
add("lm_head.weight".into(), vec![VOCAB, D], &mut r);
let norms = [
"model.decoder.layernorm_embedding".to_string(),
"model.decoder.layer_norm".to_string(),
];
let mut norm_names: Vec<String> = norms.to_vec();
for l in 0..LAYERS {
let p = format!("model.decoder.layers.{l}");
for attn in ["self_attn", "encoder_attn"] {
for proj in ["q_proj", "k_proj", "v_proj", "out_proj"] {
add(format!("{p}.{attn}.{proj}.weight"), vec![D, D], &mut r);
add(format!("{p}.{attn}.{proj}.bias"), vec![D], &mut r);
}
}
add(format!("{p}.fc1.weight"), vec![FFN, D], &mut r);
add(format!("{p}.fc1.bias"), vec![FFN], &mut r);
add(format!("{p}.fc2.weight"), vec![D, FFN], &mut r);
add(format!("{p}.fc2.bias"), vec![D], &mut r);
for n in [
"self_attn_layer_norm",
"encoder_attn_layer_norm",
"final_layer_norm",
] {
norm_names.push(format!("{p}.{n}"));
}
}
for n in norm_names {
add(format!("{n}.weight"), vec![D], &mut r);
add(format!("{n}.bias"), vec![D], &mut r);
}
t
}
fn build(tensors: &[(String, Vec<usize>, Vec<f32>)], max_length: usize) -> Decoder {
let w = Weights::from_bytes(&safetensors(tensors)).unwrap();
let text = json!({
"d_model": D, "decoder_attention_heads": HEADS, "decoder_layers": LAYERS,
"activation_function": "gelu", "scale_embedding": true
});
let generation = json!({
"decoder_start_token_id": EOS, "eos_token_id": EOS, "forced_eos_token_id": EOS,
"max_length": max_length
});
Decoder::from_weights(&w, &text, &generation).unwrap()
}
fn tiny(seed: u64) -> Decoder {
build(&tiny_tensors(seed), POSITIONS)
}
fn memory(seed: u64, l: usize) -> Tensor {
Tensor::new(vec![l, D], Rng(seed).fill(l * D))
}
fn full_logits(dec: &Decoder, mem: &Tensor, prefix: &[u32]) -> Vec<f32> {
let mut c = dec.new_cache(mem).unwrap();
let h = dec.forward(prefix, &mut c).unwrap();
dec.logits(&h).unwrap()
}
#[test]
fn cached_steps_match_full_recompute() {
for seed in [1u64, 7, 12345] {
let dec = tiny(seed);
let mem = memory(seed + 1, 9);
let mut cache = dec.new_cache(&mem).unwrap();
let mut seq = vec![dec.start];
for step in 0..POSITIONS - 1 {
let h = dec.forward(&[*seq.last().unwrap()], &mut cache).unwrap();
let a = dec.logits(&h).unwrap();
let b = full_logits(&dec, &mem, &seq);
for (x, y) in a.iter().zip(&b) {
assert!((x - y).abs() < 1e-4, "第 {step} 步:{x} 对 {y}");
}
assert_eq!(argmax(&a), argmax(&b), "第 {step} 步选的 token 不同");
seq.push(argmax(&a));
}
}
}
#[test]
fn self_attention_is_causal() {
let dec = tiny(3);
let mem = memory(4, 5);
let run = |ids: &[u32]| {
let mut c = dec.new_cache(&mem).unwrap();
dec.forward(ids, &mut c).unwrap()
};
let (a, b) = (run(&[2, 5, 6, 7]), run(&[2, 5, 6, 8]));
assert_eq!(a.data[..3 * D], b.data[..3 * D]);
assert_ne!(a.data[3 * D..], b.data[3 * D..]);
}
#[test]
fn generate_matches_naive_greedy() {
let dec = tiny(11);
let mem = memory(12, 7);
let got = dec.generate(&mem, POSITIONS).unwrap();
let mut seq = vec![dec.start];
let mut want = Vec::new();
while seq.len() < POSITIONS {
let next = if seq.len() + 1 == POSITIONS {
dec.eos
} else {
argmax(&full_logits(&dec, &mem, &seq))
};
if next == dec.eos {
break;
}
want.push(next);
seq.push(next);
}
assert_eq!(got, want);
}
#[test]
fn position_offset_is_two() {
let base = tiny_tensors(21);
let mem = memory(22, 4);
let logits = |tensors: &[(String, Vec<usize>, Vec<f32>)]| {
let dec = build(tensors, POSITIONS);
full_logits(&dec, &mem, &[2])
};
let perturb = |row: usize| {
let mut t = base.clone();
let e = t
.iter_mut()
.find(|e| e.0 == "model.decoder.embed_positions.weight")
.unwrap();
for v in &mut e.2[row * D..(row + 1) * D] {
*v += 1.0;
}
t
};
let plain = logits(&base);
assert_eq!(plain, logits(&perturb(0)));
assert_eq!(plain, logits(&perturb(1)));
assert_ne!(plain, logits(&perturb(POSITION_OFFSET)));
}
fn biased(seed: u64, token: usize) -> Decoder {
let mut dec = tiny(seed);
let mut bias = vec![0f32; VOCAB];
bias[token] = 1e6;
dec.lm_head.bias = Some(bias);
dec
}
#[test]
fn stops_at_eos() {
let dec = biased(31, EOS);
assert!(dec.generate(&memory(32, 4), POSITIONS).unwrap().is_empty());
}
#[test]
fn forces_eos_at_max_length() {
let dec = biased(33, 5);
let mem = memory(34, 4);
assert_eq!(dec.generate(&mem, 6).unwrap(), vec![5; 4]);
assert_eq!(dec.generate(&mem, usize::MAX).unwrap().len(), POSITIONS - 2);
assert!(dec.generate(&mem, 1).unwrap().is_empty());
assert!(dec.generate(&mem, 0).unwrap().is_empty());
}
#[test]
fn rejects_bad_input() {
let dec = tiny(41);
assert!(dec.generate(&Tensor::zeros(vec![4, D + 1]), 8).is_err());
assert!(dec.generate(&Tensor::zeros(vec![0, D]), 8).is_err());
let nan = Tensor::new(vec![1, D], vec![f32::NAN; D]);
assert!(dec.generate(&nan, 8).is_err());
let mut c = dec.new_cache(&memory(42, 2)).unwrap();
assert!(dec.forward(&[VOCAB as u32], &mut c).is_err());
}
#[test]
#[ignore = "要读 727 MB 权重"]
fn real_weights_random_memory() {
let dir = crate::models::cache_root()
.unwrap()
.join("PP-FormulaNet_plus-L_safetensors");
let dec = Decoder::load(&dir).unwrap();
assert_eq!(dec.max_length(), 1537);
let mut r = Rng(99);
let mem = Tensor::new(vec![144, 512], r.fill(144 * 512));
let t = std::time::Instant::now();
let ids = dec.generate(&mem, 64).unwrap();
println!(
"{} 个 token,{:.1} ms/token",
ids.len(),
t.elapsed().as_secs_f64() * 1e3 / (ids.len() + 1) as f64
);
assert!(ids.len() <= 62);
assert!(!ids.contains(&dec.eos));
}