1use ferrox_gguf::TensorSource;
14use ferrox_moe::GatingFunction;
15
16use crate::config::{MlaConfig, MlaRopeConfig};
17use crate::engine::{
18 MlaDenseFfn, MlaEngine, MlaLayerFfn, MlaLayerWeights, MlaMoeFfn, MlaMoeRuntime,
19};
20use crate::loader::LoadError;
21use crate::loader::{load_f32_vec, load_weight_matrix, split_expert_tensor};
22use crate::mla::MlaAttnWeights;
23
24#[derive(Debug, Clone)]
26pub struct Deepseek2Hparams {
27 pub arch: String,
28 pub n_layer: usize,
29 pub hidden_dim: usize,
30 pub ffn_dim: usize,
31 pub n_heads: usize,
32 pub q_lora_rank: usize,
33 pub kv_lora_rank: usize,
34 pub qk_nope_head_dim: usize,
35 pub qk_rope_head_dim: usize,
36 pub v_head_dim: usize,
37 pub rms_norm_eps: f32,
38 pub rope_theta: f32,
39 pub leading_dense_block_count: usize,
41 pub n_expert: usize,
42 pub n_expert_used: usize,
43 pub n_shared_experts: usize,
44 pub expert_ffn_dim: usize,
45 pub gating: GatingFunction,
46 pub norm_topk_prob: bool,
47 pub expert_weights_scale: f32,
48}
49
50fn meta_u64(file: &impl TensorSource, key: &str) -> Result<u64, LoadError> {
51 file.metadata_u64(key)
52 .ok_or_else(|| LoadError::MissingHparam(key.to_string()))
53}
54
55fn meta_f32(file: &impl TensorSource, key: &str, default: f32) -> f32 {
56 file.metadata_f32(key).unwrap_or(default)
57}
58
59pub fn read_deepseek2_hparams(file: &impl TensorSource) -> Result<Deepseek2Hparams, LoadError> {
61 let arch = file
62 .metadata_str("general.architecture")
63 .ok_or_else(|| LoadError::MissingHparam("general.architecture".into()))?
64 .to_string();
65 if arch != "deepseek2" && arch != "mistral4" {
66 return Err(LoadError::UnsupportedArchitecture(arch));
67 }
68 let p = |suffix: &str| format!("{arch}.{suffix}");
69 let n_layer = meta_u64(file, &p("block_count"))? as usize;
70 let hidden_dim = meta_u64(file, &p("embedding_length"))? as usize;
71 let ffn_dim = meta_u64(file, &p("feed_forward_length"))? as usize;
72 let n_heads = meta_u64(file, &p("attention.head_count"))? as usize;
73 let q_lora_rank = meta_u64(file, &p("attention.q_lora_rank"))? as usize;
74 let kv_lora_rank = meta_u64(file, &p("attention.kv_lora_rank"))? as usize;
75 let qk_rope_head_dim = meta_u64(file, &p("rope.dimension_count"))
101 .or_else(|_| meta_u64(file, &p("attention.qk_rope_head_dim")))?
102 as usize;
103 let qk_nope_head_dim = match meta_u64(file, &p("attention.key_length_mla")) {
104 Ok(k_mla) => (k_mla as usize)
105 .checked_sub(qk_rope_head_dim)
106 .filter(|&nope| nope >= 1)
107 .ok_or_else(|| {
108 LoadError::MissingHparam(format!(
109 "{arch}.attention.key_length_mla ({k_mla}) must exceed \
110 rope.dimension_count ({qk_rope_head_dim})"
111 ))
112 })?,
113 Err(_) => meta_u64(file, &p("attention.qk_nope_head_dim"))? as usize,
114 };
115 let v_head_dim = meta_u64(file, &p("attention.value_length_mla"))
116 .or_else(|_| meta_u64(file, &p("attention.v_head_dim")))
117 .unwrap_or(qk_nope_head_dim as u64) as usize;
118 let leading_dense = file
119 .metadata_u64(&p("leading_dense_block_count"))
120 .unwrap_or(n_layer as u64) as usize;
121 let n_expert = file.metadata_u64(&p("expert_count")).unwrap_or(0) as usize;
122 let n_expert_used = file
123 .metadata_u64(&p("expert_used_count"))
124 .unwrap_or(if n_expert > 0 { 6 } else { 0 }) as usize;
125 let n_shared_experts = file.metadata_u64(&p("expert_shared_count")).unwrap_or(1) as usize;
126 let expert_ffn_dim = file
127 .metadata_u64(&p("expert_feed_forward_length"))
128 .unwrap_or(ffn_dim as u64) as usize;
129 let rms_norm_eps = meta_f32(file, &p("attention.layer_norm_rms_epsilon"), 1e-6);
130 let rope_theta = meta_f32(file, &p("rope.freq_base"), 10000.0);
131 let gating = match file.metadata_u64(&p("expert_gating_func")) {
134 Some(2) => GatingFunction::Sigmoid,
135 Some(1) => GatingFunction::Softmax,
136 _ if (n_layer == 47 || n_layer == 48)
137 && file
138 .find_tensor("token_embd.weight")
139 .map(|t| t.shape.last().copied().unwrap_or(0) == 154880)
140 .unwrap_or(false) =>
141 {
142 GatingFunction::Sigmoid
143 }
144 _ => GatingFunction::Softmax,
145 };
146 let norm_topk_prob = file
147 .metadata_u64(&p("expert_weights_norm"))
148 .map(|v| v != 0)
149 .unwrap_or(true);
150 let expert_weights_scale = meta_f32(file, &p("expert_weights_scale"), 1.0);
151 Ok(Deepseek2Hparams {
152 arch,
153 n_layer,
154 hidden_dim,
155 ffn_dim,
156 n_heads,
157 q_lora_rank,
158 kv_lora_rank,
159 qk_nope_head_dim,
160 qk_rope_head_dim,
161 v_head_dim,
162 rms_norm_eps,
163 rope_theta,
164 leading_dense_block_count: leading_dense.min(n_layer),
165 n_expert,
166 n_expert_used: n_expert_used.min(n_expert.max(1)),
167 n_shared_experts: n_shared_experts.max(1),
168 expert_ffn_dim,
169 gating,
170 norm_topk_prob,
171 expert_weights_scale,
172 })
173}
174
175fn load_f32_vec_optional(
176 file: &impl TensorSource,
177 name: &str,
178) -> Result<Option<Vec<f32>>, LoadError> {
179 if file.find_tensor(name).is_none() {
180 return Ok(None);
181 }
182 Ok(Some(load_f32_vec(file, name)?))
183}
184
185fn load_mla_attn(
186 file: &impl TensorSource,
187 layer_idx: usize,
188 hp: &Deepseek2Hparams,
189) -> Result<MlaAttnWeights, LoadError> {
190 let l = layer_idx;
191 let q_head_dim = hp.qk_nope_head_dim + hp.qk_rope_head_dim;
192 let q_a_proj = load_weight_matrix(file, &format!("blk.{l}.attn_q_a.weight"))?;
193 let q_b_proj = load_weight_matrix(file, &format!("blk.{l}.attn_q_b.weight"))?;
194 let kv_a = load_weight_matrix(file, &format!("blk.{l}.attn_kv_a_mqa.weight"))?;
195 let o_proj = load_weight_matrix(file, &format!("blk.{l}.attn_output.weight"))?;
196
197 let kv_b_proj = if file
199 .find_tensor(&format!("blk.{l}.attn_kv_b.weight"))
200 .is_some()
201 {
202 load_weight_matrix(file, &format!("blk.{l}.attn_kv_b.weight"))?
203 } else {
204 return Err(LoadError::Gguf(ferrox_gguf::GgufError::TensorNotFound(
205 format!(
206 "blk.{l}.attn_kv_b.weight (split attn_k_b/attn_v_b not wired for MlaEngine yet)"
207 ),
208 )));
209 };
210
211 let q_a_ln = load_f32_vec_optional(file, &format!("blk.{l}.attn_q_a_norm.weight"))?
212 .unwrap_or_else(|| vec![1.0; hp.q_lora_rank]);
213 let kv_a_ln = load_f32_vec_optional(file, &format!("blk.{l}.attn_kv_a_norm.weight"))?
214 .unwrap_or_else(|| vec![1.0; hp.kv_lora_rank]);
215
216 let _ = (q_head_dim,);
217 Ok(MlaAttnWeights {
218 q_a_proj,
219 q_a_layernorm: q_a_ln,
220 q_b_proj,
221 kv_a_proj_with_mqa: kv_a,
222 kv_a_layernorm: kv_a_ln,
223 kv_b_proj,
224 o_proj,
225 g_proj: None,
226 })
227}
228
229fn require_tensor(file: &impl TensorSource, name: &str) -> Result<(), LoadError> {
230 if file.find_tensor(name).is_none() {
231 return Err(LoadError::Gguf(ferrox_gguf::GgufError::TensorNotFound(
232 name.to_string(),
233 )));
234 }
235 Ok(())
236}
237
238fn load_dense_ffn(file: &impl TensorSource, layer_idx: usize) -> Result<MlaDenseFfn, LoadError> {
239 let l = layer_idx;
240 Ok(MlaDenseFfn {
241 gate: load_weight_matrix(file, &format!("blk.{l}.ffn_gate.weight"))?,
242 up: load_weight_matrix(file, &format!("blk.{l}.ffn_up.weight"))?,
243 down: load_weight_matrix(file, &format!("blk.{l}.ffn_down.weight"))?,
244 })
245}
246
247fn load_moe_ffn(
248 file: &impl TensorSource,
249 layer_idx: usize,
250 hp: &Deepseek2Hparams,
251) -> Result<MlaMoeFfn, LoadError> {
252 let l = layer_idx;
253 for name in [
255 format!("blk.{l}.ffn_gate_inp.weight"),
256 format!("blk.{l}.ffn_gate_exps.weight"),
257 format!("blk.{l}.ffn_up_exps.weight"),
258 format!("blk.{l}.ffn_down_exps.weight"),
259 format!("blk.{l}.ffn_gate_shexp.weight"),
260 format!("blk.{l}.ffn_up_shexp.weight"),
261 format!("blk.{l}.ffn_down_shexp.weight"),
262 ] {
263 require_tensor(file, &name)?;
264 }
265 let gate_exps =
266 split_expert_tensor(file, &format!("blk.{l}.ffn_gate_exps.weight"), hp.n_expert)?;
267 let up_exps = split_expert_tensor(file, &format!("blk.{l}.ffn_up_exps.weight"), hp.n_expert)?;
268 let down_exps =
269 split_expert_tensor(file, &format!("blk.{l}.ffn_down_exps.weight"), hp.n_expert)?;
270 let experts = gate_exps
271 .into_iter()
272 .zip(up_exps)
273 .zip(down_exps)
274 .map(|((gate, up), down)| ferrox_moe::ExpertWeights { gate, up, down })
275 .collect();
276 let shared_expert = ferrox_moe::ExpertWeights {
277 gate: load_weight_matrix(file, &format!("blk.{l}.ffn_gate_shexp.weight"))?,
278 up: load_weight_matrix(file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
279 down: load_weight_matrix(file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
280 };
281 let exp_probs_bias = load_f32_vec_optional(file, &format!("blk.{l}.exp_probs_b.bias"))?;
288 Ok(MlaMoeFfn {
289 router: load_weight_matrix(file, &format!("blk.{l}.ffn_gate_inp.weight"))?,
290 experts,
291 shared_expert,
292 exp_probs_bias,
293 })
294}
295
296fn load_layer(
297 file: &impl TensorSource,
298 layer_idx: usize,
299 hp: &Deepseek2Hparams,
300) -> Result<MlaLayerWeights, LoadError> {
301 let l = layer_idx;
302 let ffn = if layer_idx < hp.leading_dense_block_count || hp.n_expert == 0 {
303 MlaLayerFfn::Dense(load_dense_ffn(file, layer_idx)?)
304 } else {
305 MlaLayerFfn::Moe(load_moe_ffn(file, layer_idx, hp)?)
306 };
307 Ok(MlaLayerWeights {
308 attn_norm: load_f32_vec(file, &format!("blk.{l}.attn_norm.weight"))?,
309 attn: load_mla_attn(file, layer_idx, hp)?,
310 ffn_norm: load_f32_vec(file, &format!("blk.{l}.ffn_norm.weight"))?,
311 ffn,
312 })
313}
314
315pub fn load_mla_engine(file: &impl TensorSource) -> Result<MlaEngine, LoadError> {
317 let hp = read_deepseek2_hparams(file)?;
318 if hp.n_expert > 0 && hp.leading_dense_block_count >= hp.n_layer {
319 } else if hp.n_expert > 0 && hp.n_expert_used == 0 {
321 return Err(LoadError::UnsupportedArchitecture(format!(
322 "{}: expert_count={} but expert_used_count is 0",
323 hp.arch, hp.n_expert
324 )));
325 }
326 if hp.n_layer == 0 {
327 return Err(LoadError::UnsupportedArchitecture(format!(
328 "{}: no layers to load",
329 hp.arch
330 )));
331 }
332
333 let embedding = if file.find_tensor("token_embd.weight").is_some() {
334 load_weight_matrix(file, "token_embd.weight")?
335 } else {
336 return Err(LoadError::Gguf(ferrox_gguf::GgufError::TensorNotFound(
337 "token_embd.weight".into(),
338 )));
339 };
340 let final_norm = load_f32_vec(file, "output_norm.weight")?;
341 let output_head = match load_weight_matrix(file, "output.weight") {
342 Ok(w) => w,
343 Err(_) => load_weight_matrix(file, "token_embd.weight")?,
344 };
345
346 let mut layers = Vec::with_capacity(hp.n_layer);
347 for i in 0..hp.n_layer {
348 layers.push(load_layer(file, i, &hp)?);
349 }
350 let has_moe = layers.iter().any(|l| matches!(l.ffn, MlaLayerFfn::Moe(_)));
351 let moe = if has_moe {
352 Some(MlaMoeRuntime {
353 n_experts_active: hp.n_expert_used,
354 gating: hp.gating,
355 norm_topk_prob: hp.norm_topk_prob,
356 expert_weights_scale: hp.expert_weights_scale,
357 })
358 } else {
359 None
360 };
361
362 Ok(MlaEngine {
363 embedding,
364 layers,
365 final_norm,
366 output_head,
367 mla_cfg: MlaConfig {
368 num_heads: hp.n_heads,
369 q_lora_rank: hp.q_lora_rank,
370 kv_lora_rank: hp.kv_lora_rank,
371 qk_nope_head_dim: hp.qk_nope_head_dim,
372 qk_rope_head_dim: hp.qk_rope_head_dim,
373 v_head_dim: hp.v_head_dim,
374 use_output_gate: false,
375 rope: Some(MlaRopeConfig {
376 theta: hp.rope_theta,
377 }),
378 },
379 rms_norm_eps: hp.rms_norm_eps,
380 hidden_dim: hp.hidden_dim,
381 moe,
382 })
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::engine::Engine;
389 use byteorder::{LittleEndian, WriteBytesExt};
390 use ferrox_gguf::GgufFile;
391 use std::io::Write;
392
393 struct FixtureTensor {
394 name: String,
395 shape: Vec<u64>,
396 bytes: Vec<u8>,
397 }
398
399 fn f32_bytes(v: &[f32]) -> Vec<u8> {
400 let mut b = Vec::with_capacity(v.len() * 4);
401 for x in v {
402 b.write_f32::<LittleEndian>(*x).unwrap();
403 }
404 b
405 }
406
407 fn f32_tensor(name: &str, shape: Vec<u64>, values: Vec<f32>) -> FixtureTensor {
408 FixtureTensor {
409 name: name.into(),
410 shape,
411 bytes: f32_bytes(&values),
412 }
413 }
414
415 fn build_gguf(
416 arch: &str,
417 kv: &[(&str, u64)],
418 fkv: &[(&str, f32)],
419 tensors: &[FixtureTensor],
420 ) -> Vec<u8> {
421 let mut buf = Vec::new();
422 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
423 .unwrap();
424 buf.write_u32::<LittleEndian>(3).unwrap();
425 buf.write_u64::<LittleEndian>(tensors.len() as u64).unwrap();
426 let kv_count = 1 + kv.len() + fkv.len();
428 buf.write_u64::<LittleEndian>(kv_count as u64).unwrap();
429
430 let write_string = |buf: &mut Vec<u8>, s: &str| {
431 buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
432 buf.write_all(s.as_bytes()).unwrap();
433 };
434 write_string(&mut buf, "general.architecture");
435 buf.write_u32::<LittleEndian>(8).unwrap();
436 write_string(&mut buf, arch);
437 for &(k, v) in kv {
438 write_string(&mut buf, k);
439 buf.write_u32::<LittleEndian>(10).unwrap(); buf.write_u64::<LittleEndian>(v).unwrap();
441 }
442 for &(k, v) in fkv {
443 write_string(&mut buf, k);
444 buf.write_u32::<LittleEndian>(6).unwrap(); buf.write_f32::<LittleEndian>(v).unwrap();
446 }
447
448 let mut offset = 0u64;
449 let mut offsets = Vec::with_capacity(tensors.len());
450 for t in tensors {
451 write_string(&mut buf, &t.name);
452 buf.write_u32::<LittleEndian>(t.shape.len() as u32).unwrap();
453 for &d in t.shape.iter().rev() {
454 buf.write_u64::<LittleEndian>(d).unwrap();
455 }
456 buf.write_u32::<LittleEndian>(0).unwrap();
457 offsets.push(offset);
458 buf.write_u64::<LittleEndian>(offset).unwrap();
459 offset += (t.bytes.len().div_ceil(32) * 32) as u64;
460 }
461 while buf.len() % 32 != 0 {
462 buf.push(0);
463 }
464 let data_start = buf.len();
465 for (t, &off) in tensors.iter().zip(offsets.iter()) {
466 while buf.len() < data_start + off as usize {
467 buf.push(0);
468 }
469 buf.extend_from_slice(&t.bytes);
470 while buf.len() % 32 != 0 {
471 buf.push(0);
472 }
473 }
474 buf
475 }
476
477 #[test]
478 fn load_synthetic_deepseek2_dense_and_forward() {
479 let h = 16usize;
480 let n_heads = 2usize;
481 let q_lora = 8usize;
482 let kv_lora = 4usize;
483 let qk_nope = 4usize;
484 let qk_rope = 2usize;
485 let v_dim = 4usize;
486 let ffn = 32usize;
487 let vocab = 8usize;
488 let q_head = qk_nope + qk_rope;
489 let arch = "deepseek2";
490
491 let mut tensors = vec![
492 f32_tensor(
493 "token_embd.weight",
494 vec![vocab as u64, h as u64],
495 vec![0.01; h * vocab],
496 ),
497 f32_tensor("output_norm.weight", vec![h as u64], vec![1.0; h]),
498 f32_tensor(
499 "output.weight",
500 vec![vocab as u64, h as u64],
501 vec![0.02; h * vocab],
502 ),
503 ];
504 for l in 0..2usize {
505 tensors.push(f32_tensor(
506 &format!("blk.{l}.attn_norm.weight"),
507 vec![h as u64],
508 vec![1.0; h],
509 ));
510 tensors.push(f32_tensor(
511 &format!("blk.{l}.ffn_norm.weight"),
512 vec![h as u64],
513 vec![1.0; h],
514 ));
515 tensors.push(f32_tensor(
516 &format!("blk.{l}.attn_q_a.weight"),
517 vec![q_lora as u64, h as u64],
518 vec![0.01; h * q_lora],
519 ));
520 tensors.push(f32_tensor(
521 &format!("blk.{l}.attn_q_b.weight"),
522 vec![(n_heads * q_head) as u64, q_lora as u64],
523 vec![0.01; q_lora * n_heads * q_head],
524 ));
525 tensors.push(f32_tensor(
526 &format!("blk.{l}.attn_kv_a_mqa.weight"),
527 vec![(kv_lora + qk_rope) as u64, h as u64],
528 vec![0.01; h * (kv_lora + qk_rope)],
529 ));
530 tensors.push(f32_tensor(
531 &format!("blk.{l}.attn_kv_b.weight"),
532 vec![(n_heads * (qk_nope + v_dim)) as u64, kv_lora as u64],
533 vec![0.01; kv_lora * n_heads * (qk_nope + v_dim)],
534 ));
535 tensors.push(f32_tensor(
536 &format!("blk.{l}.attn_output.weight"),
537 vec![h as u64, (n_heads * v_dim) as u64],
538 vec![0.01; n_heads * v_dim * h],
539 ));
540 tensors.push(f32_tensor(
541 &format!("blk.{l}.ffn_gate.weight"),
542 vec![ffn as u64, h as u64],
543 vec![0.01; h * ffn],
544 ));
545 tensors.push(f32_tensor(
546 &format!("blk.{l}.ffn_up.weight"),
547 vec![ffn as u64, h as u64],
548 vec![0.01; h * ffn],
549 ));
550 tensors.push(f32_tensor(
551 &format!("blk.{l}.ffn_down.weight"),
552 vec![h as u64, ffn as u64],
553 vec![0.01; ffn * h],
554 ));
555 }
556
557 let kv = [
558 ("deepseek2.block_count", 2u64),
559 ("deepseek2.embedding_length", h as u64),
560 ("deepseek2.feed_forward_length", ffn as u64),
561 ("deepseek2.attention.head_count", n_heads as u64),
562 ("deepseek2.attention.q_lora_rank", q_lora as u64),
563 ("deepseek2.attention.kv_lora_rank", kv_lora as u64),
564 ("deepseek2.attention.qk_nope_head_dim", qk_nope as u64),
565 ("deepseek2.attention.qk_rope_head_dim", qk_rope as u64),
566 ("deepseek2.attention.v_head_dim", v_dim as u64),
567 ("deepseek2.leading_dense_block_count", 2u64),
568 ("deepseek2.expert_count", 0u64),
569 ];
570 let fkv = [
571 ("deepseek2.attention.layer_norm_rms_epsilon", 1e-5f32),
572 ("deepseek2.rope.freq_base", 10000.0f32),
573 ];
574 let bytes = build_gguf(arch, &kv, &fkv, &tensors);
575 let path =
576 std::env::temp_dir().join(format!("ferrox_mla_gguf_{}.gguf", std::process::id()));
577 std::fs::write(&path, &bytes).unwrap();
578 let file = GgufFile::open(&path).unwrap();
579 let engine = load_mla_engine(&file).expect("load mla");
580 assert_eq!(engine.layers.len(), 2);
581 assert_eq!(engine.vocab_size(), vocab);
582 let mut state = engine.new_state();
583 let logits = engine.forward_token(0, 0, &mut state);
584 assert_eq!(logits.len(), vocab);
585 assert!(logits.iter().all(|x| x.is_finite()));
586 let _ = std::fs::remove_file(&path);
587 }
588
589 #[allow(clippy::too_many_arguments)] fn push_mla_attn_tensors(
591 tensors: &mut Vec<FixtureTensor>,
592 l: usize,
593 h: usize,
594 n_heads: usize,
595 q_lora: usize,
596 kv_lora: usize,
597 qk_nope: usize,
598 qk_rope: usize,
599 v_dim: usize,
600 ) {
601 let q_head = qk_nope + qk_rope;
602 tensors.push(f32_tensor(
603 &format!("blk.{l}.attn_norm.weight"),
604 vec![h as u64],
605 vec![1.0; h],
606 ));
607 tensors.push(f32_tensor(
608 &format!("blk.{l}.ffn_norm.weight"),
609 vec![h as u64],
610 vec![1.0; h],
611 ));
612 tensors.push(f32_tensor(
613 &format!("blk.{l}.attn_q_a.weight"),
614 vec![q_lora as u64, h as u64],
615 vec![0.01; h * q_lora],
616 ));
617 tensors.push(f32_tensor(
618 &format!("blk.{l}.attn_q_b.weight"),
619 vec![(n_heads * q_head) as u64, q_lora as u64],
620 vec![0.01; q_lora * n_heads * q_head],
621 ));
622 tensors.push(f32_tensor(
623 &format!("blk.{l}.attn_kv_a_mqa.weight"),
624 vec![(kv_lora + qk_rope) as u64, h as u64],
625 vec![0.01; h * (kv_lora + qk_rope)],
626 ));
627 tensors.push(f32_tensor(
628 &format!("blk.{l}.attn_kv_b.weight"),
629 vec![(n_heads * (qk_nope + v_dim)) as u64, kv_lora as u64],
630 vec![0.01; kv_lora * n_heads * (qk_nope + v_dim)],
631 ));
632 tensors.push(f32_tensor(
633 &format!("blk.{l}.attn_output.weight"),
634 vec![h as u64, (n_heads * v_dim) as u64],
635 vec![0.01; n_heads * v_dim * h],
636 ));
637 }
638
639 #[test]
640 fn load_synthetic_deepseek2_moe_after_dense_and_forward() {
641 let h = 16usize;
642 let n_heads = 2usize;
643 let q_lora = 8usize;
644 let kv_lora = 4usize;
645 let qk_nope = 4usize;
646 let qk_rope = 2usize;
647 let v_dim = 4usize;
648 let ffn = 32usize;
649 let exp_ff = 16usize;
650 let n_exp = 4usize;
651 let vocab = 8usize;
652 let arch = "deepseek2";
653
654 let mut tensors = vec![
655 f32_tensor(
656 "token_embd.weight",
657 vec![vocab as u64, h as u64],
658 vec![0.01; h * vocab],
659 ),
660 f32_tensor("output_norm.weight", vec![h as u64], vec![1.0; h]),
661 f32_tensor(
662 "output.weight",
663 vec![vocab as u64, h as u64],
664 vec![0.02; h * vocab],
665 ),
666 ];
667 push_mla_attn_tensors(
669 &mut tensors,
670 0,
671 h,
672 n_heads,
673 q_lora,
674 kv_lora,
675 qk_nope,
676 qk_rope,
677 v_dim,
678 );
679 tensors.push(f32_tensor(
680 "blk.0.ffn_gate.weight",
681 vec![ffn as u64, h as u64],
682 vec![0.01; h * ffn],
683 ));
684 tensors.push(f32_tensor(
685 "blk.0.ffn_up.weight",
686 vec![ffn as u64, h as u64],
687 vec![0.01; h * ffn],
688 ));
689 tensors.push(f32_tensor(
690 "blk.0.ffn_down.weight",
691 vec![h as u64, ffn as u64],
692 vec![0.01; ffn * h],
693 ));
694 push_mla_attn_tensors(
696 &mut tensors,
697 1,
698 h,
699 n_heads,
700 q_lora,
701 kv_lora,
702 qk_nope,
703 qk_rope,
704 v_dim,
705 );
706 tensors.push(f32_tensor(
707 "blk.1.ffn_gate_inp.weight",
708 vec![n_exp as u64, h as u64],
709 vec![0.01; h * n_exp],
710 ));
711 tensors.push(f32_tensor(
714 "blk.1.ffn_gate_exps.weight",
715 vec![n_exp as u64, exp_ff as u64, h as u64],
716 vec![0.01; n_exp * exp_ff * h],
717 ));
718 tensors.push(f32_tensor(
719 "blk.1.ffn_up_exps.weight",
720 vec![n_exp as u64, exp_ff as u64, h as u64],
721 vec![0.01; n_exp * exp_ff * h],
722 ));
723 tensors.push(f32_tensor(
724 "blk.1.ffn_down_exps.weight",
725 vec![n_exp as u64, h as u64, exp_ff as u64],
726 vec![0.01; n_exp * h * exp_ff],
727 ));
728 tensors.push(f32_tensor(
729 "blk.1.ffn_gate_shexp.weight",
730 vec![exp_ff as u64, h as u64],
731 vec![0.01; h * exp_ff],
732 ));
733 tensors.push(f32_tensor(
734 "blk.1.ffn_up_shexp.weight",
735 vec![exp_ff as u64, h as u64],
736 vec![0.01; h * exp_ff],
737 ));
738 tensors.push(f32_tensor(
739 "blk.1.ffn_down_shexp.weight",
740 vec![h as u64, exp_ff as u64],
741 vec![0.01; exp_ff * h],
742 ));
743
744 let kv = [
745 ("deepseek2.block_count", 2u64),
746 ("deepseek2.embedding_length", h as u64),
747 ("deepseek2.feed_forward_length", ffn as u64),
748 ("deepseek2.attention.head_count", n_heads as u64),
749 ("deepseek2.attention.q_lora_rank", q_lora as u64),
750 ("deepseek2.attention.kv_lora_rank", kv_lora as u64),
751 ("deepseek2.attention.qk_nope_head_dim", qk_nope as u64),
752 ("deepseek2.attention.qk_rope_head_dim", qk_rope as u64),
753 ("deepseek2.attention.v_head_dim", v_dim as u64),
754 ("deepseek2.leading_dense_block_count", 1u64),
755 ("deepseek2.expert_count", n_exp as u64),
756 ("deepseek2.expert_used_count", 2u64),
757 ("deepseek2.expert_shared_count", 1u64),
758 ("deepseek2.expert_feed_forward_length", exp_ff as u64),
759 ("deepseek2.expert_gating_func", 1u64), ];
761 let fkv = [
762 ("deepseek2.attention.layer_norm_rms_epsilon", 1e-5f32),
763 ("deepseek2.rope.freq_base", 10000.0f32),
764 ("deepseek2.expert_weights_scale", 1.0f32),
765 ];
766 let bytes = build_gguf(arch, &kv, &fkv, &tensors);
767 let path =
768 std::env::temp_dir().join(format!("ferrox_mla_moe_gguf_{}.gguf", std::process::id()));
769 std::fs::write(&path, &bytes).unwrap();
770 let file = GgufFile::open(&path).unwrap();
771 let engine = load_mla_engine(&file).expect("load mla moe");
772 assert_eq!(engine.layers.len(), 2);
773 assert!(matches!(
774 engine.layers[0].ffn,
775 crate::engine::MlaLayerFfn::Dense(_)
776 ));
777 assert!(matches!(
778 engine.layers[1].ffn,
779 crate::engine::MlaLayerFfn::Moe(_)
780 ));
781 assert!(engine.moe.is_some());
782 let mut state = engine.new_state();
783 let logits = engine.forward_token(0, 0, &mut state);
784 assert_eq!(logits.len(), vocab);
785 assert!(logits.iter().all(|x| x.is_finite()));
786 let _ = std::fs::remove_file(&path);
787 }
788
789 #[test]
790 fn moe_after_dense_fails_closed_without_expert_tensors() {
791 let h = 16usize;
792 let n_heads = 2usize;
793 let q_lora = 8usize;
794 let kv_lora = 4usize;
795 let qk_nope = 4usize;
796 let qk_rope = 2usize;
797 let v_dim = 4usize;
798 let ffn = 32usize;
799 let vocab = 8usize;
800 let arch = "deepseek2";
801
802 let mut tensors = vec![
803 f32_tensor(
804 "token_embd.weight",
805 vec![vocab as u64, h as u64],
806 vec![0.01; h * vocab],
807 ),
808 f32_tensor("output_norm.weight", vec![h as u64], vec![1.0; h]),
809 f32_tensor(
810 "output.weight",
811 vec![vocab as u64, h as u64],
812 vec![0.02; h * vocab],
813 ),
814 ];
815 for l in 0..2usize {
816 push_mla_attn_tensors(
817 &mut tensors,
818 l,
819 h,
820 n_heads,
821 q_lora,
822 kv_lora,
823 qk_nope,
824 qk_rope,
825 v_dim,
826 );
827 tensors.push(f32_tensor(
829 &format!("blk.{l}.ffn_gate.weight"),
830 vec![ffn as u64, h as u64],
831 vec![0.01; h * ffn],
832 ));
833 tensors.push(f32_tensor(
834 &format!("blk.{l}.ffn_up.weight"),
835 vec![ffn as u64, h as u64],
836 vec![0.01; h * ffn],
837 ));
838 tensors.push(f32_tensor(
839 &format!("blk.{l}.ffn_down.weight"),
840 vec![h as u64, ffn as u64],
841 vec![0.01; ffn * h],
842 ));
843 }
844 let kv = [
845 ("deepseek2.block_count", 2u64),
846 ("deepseek2.embedding_length", h as u64),
847 ("deepseek2.feed_forward_length", ffn as u64),
848 ("deepseek2.attention.head_count", n_heads as u64),
849 ("deepseek2.attention.q_lora_rank", q_lora as u64),
850 ("deepseek2.attention.kv_lora_rank", kv_lora as u64),
851 ("deepseek2.attention.qk_nope_head_dim", qk_nope as u64),
852 ("deepseek2.attention.qk_rope_head_dim", qk_rope as u64),
853 ("deepseek2.attention.v_head_dim", v_dim as u64),
854 ("deepseek2.leading_dense_block_count", 1u64),
855 ("deepseek2.expert_count", 4u64),
856 ("deepseek2.expert_used_count", 2u64),
857 ];
858 let fkv = [
859 ("deepseek2.attention.layer_norm_rms_epsilon", 1e-5f32),
860 ("deepseek2.rope.freq_base", 10000.0f32),
861 ];
862 let bytes = build_gguf(arch, &kv, &fkv, &tensors);
863 let path = std::env::temp_dir().join(format!(
864 "ferrox_mla_moe_missing_{}.gguf",
865 std::process::id()
866 ));
867 std::fs::write(&path, &bytes).unwrap();
868 let file = GgufFile::open(&path).unwrap();
869 let err = match load_mla_engine(&file) {
870 Err(e) => e,
871 Ok(_) => panic!("expected missing MoE tensors to fail closed"),
872 };
873 let msg = format!("{err}");
874 assert!(
875 msg.contains("ffn_gate_inp") || msg.contains("TensorNotFound"),
876 "unexpected error: {msg}"
877 );
878 let _ = std::fs::remove_file(&path);
879 }
880}