1use super::gguf::{Gguf, MetaValue};
13pub use crate::tensor_recognize::qwen::Window;
14
15#[allow(clippy::type_complexity)]
19fn qwen4exp_parts<S: SpecSource + ?Sized>(
20 k: &Keys<'_, S>,
21 layers: &[Layer],
22) -> Result<(Option<HyperConnections>, Option<Ple>, Option<Indexer>), SpecError> {
23 let hyper = HyperConnections { lanes: k.usize("hyper_connection.count")?, rank: k.usize("hyper_connection.low_rank")? };
24 if hyper.lanes < 2 || hyper.rank == 0 {
25 return Err(SpecError::Inconsistent(format!("hyper-connections of {} lanes over a rank of {}", hyper.lanes, hyper.rank)));
26 }
27 let ints = |suffix: &str| -> Result<Vec<u64>, SpecError> {
28 k.get(suffix)
29 .and_then(MetaValue::as_array)
30 .and_then(|a| a.iter().map(MetaValue::as_u64).collect::<Option<Vec<u64>>>())
31 .ok_or_else(|| SpecError::MissingKey(k.key(suffix)))
32 };
33 let ratios = ints("attention.compress_ratios")?;
34 if ratios.len() < layers.len() {
35 return Err(SpecError::Inconsistent(format!("{} compression ratios for {} layers", ratios.len(), layers.len())));
36 }
37 let mut block = None;
38 let mut selecting = Vec::new();
39 for (l, &r) in ratios.iter().enumerate() {
40 let r = r as usize;
41 if r == 0 {
42 continue;
43 }
44 if *block.get_or_insert(r) != r {
45 return Err(SpecError::Inconsistent(format!("QSA's blocks of {r} on block {l}, of {} before it", block.unwrap())));
46 }
47 match layers.get(l).map(|layer| &layer.mixer) {
48 Some(Mixer::Attention(_)) => selecting.push(l),
49 Some(_) => return Err(SpecError::Inconsistent(format!("layer {l} is not attention but compresses by {r}"))),
50 None => {}
52 }
53 }
54 let indexer = Indexer {
55 heads: k.usize("attention.indexer.head_count")?,
56 head_dim: k.usize("attention.indexer.key_length")?,
57 budget: k.usize("attention.indexer.top_k")?,
58 block: block.ok_or_else(|| SpecError::Inconsistent("QSA's indexer with no block that selects".into()))?,
59 layers: selecting,
60 };
61 if indexer.block < 2 || indexer.budget % indexer.block != 0 {
62 return Err(SpecError::Inconsistent(format!("QSA's budget of {} in blocks of {}", indexer.budget, indexer.block)));
63 }
64 let ple = match k.get("ple.layers").and_then(MetaValue::as_array) {
65 None => None,
66 Some(_) => {
67 let layer = match ints("ple.layers")?[..] {
68 [l] => l as usize,
69 ref other => return Err(SpecError::Inconsistent(format!("the per-layer embedding on layers {other:?}: one table, one layer"))),
70 };
71 if !matches!(layers.get(layer).map(|l| &l.mixer), Some(Mixer::GatedDelta(_))) {
72 return Err(SpecError::Inconsistent(format!("the per-layer embedding on layer {layer}, not a Gated DeltaNet layer")));
73 }
74 let ple = Ple {
75 layer,
76 ngram: k.usize("ple.ngram_size")?,
77 heads_per_ngram: k.usize("ple.heads_per_ngram")?,
78 head_dim: k.usize("embedding_length_per_layer_input")?,
79 conv_taps: k.usize("ple.conv_kernel")?,
80 eos: k.usize("ple.eos_token_id")? as u32,
81 multipliers: ints("ple.layer_multipliers")?,
82 head_offsets: ints("ple.head_offsets")?,
83 head_vocab: ints("ple.head_vocab_sizes")?,
84 };
85 let heads = (ple.ngram.max(1) - 1) * ple.heads_per_ngram;
86 if ple.ngram < 2 || ple.multipliers.len() != ple.ngram || ple.head_offsets.len() != heads || ple.head_vocab.len() != heads {
87 return Err(SpecError::Inconsistent(format!(
88 "n-grams of {} over {heads} heads with {} multipliers, {} offsets and {} moduli",
89 ple.ngram,
90 ple.multipliers.len(),
91 ple.head_offsets.len(),
92 ple.head_vocab.len()
93 )));
94 }
95 Some(ple)
96 }
97 };
98 Ok((Some(hyper), ple, Some(indexer)))
99}
100
101#[derive(Clone, Debug, PartialEq)]
103pub struct ModelSpec {
104 pub arch: String,
106 pub d: usize,
108 pub vocab: usize,
110 pub ctx_len: usize,
112 pub rms_eps: f64,
114 pub embedding_scale: Option<f64>,
116 pub tied_output: bool,
118 pub final_logit_softcap: Option<f64>,
120 pub layers: Vec<Layer>,
121 pub mtp_blocks: usize,
123 pub ffn_norm: &'static str,
126 pub hyper: Option<HyperConnections>,
128 pub ple: Option<Ple>,
130 pub indexer: Option<Indexer>,
132}
133
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub struct HyperConnections {
140 pub lanes: usize,
141 pub rank: usize,
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct Ple {
151 pub layer: usize,
153 pub ngram: usize,
155 pub heads_per_ngram: usize,
156 pub head_dim: usize,
158 pub conv_taps: usize,
159 pub eos: u32,
161 pub multipliers: Vec<u64>,
163 pub head_offsets: Vec<u64>,
165 pub head_vocab: Vec<u64>,
167}
168
169#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct Indexer {
176 pub heads: usize,
177 pub head_dim: usize,
178 pub budget: usize,
179 pub block: usize,
180 pub layers: Vec<usize>,
181}
182
183#[derive(Clone, Debug, PartialEq)]
185pub struct Layer {
186 pub mixer: Mixer,
187 pub ffn: Ffn,
188 pub post_mixer_norm: bool,
191 pub post_ffn_norm: bool,
193}
194
195#[derive(Clone, Debug, PartialEq)]
197pub enum Mixer {
198 Attention(Attention),
199 GatedDelta(GatedDelta),
200 Mla(Mla),
201}
202
203#[derive(Clone, Debug, PartialEq)]
208pub struct Mla {
209 pub heads: usize,
210 pub q_lora_rank: Option<usize>,
211 pub kv_lora_rank: usize,
212 pub qk_nope: usize,
214 pub qk_rope: usize,
216 pub v_dim: usize,
217 pub rope: Rope,
218 pub scale: f64,
220 pub kv_b_split: bool,
224}
225
226#[derive(Clone, Debug, PartialEq)]
228pub struct Attention {
229 pub heads: usize,
230 pub kv_heads: usize,
231 pub head_dim: usize,
233 pub value_dim: usize,
235 pub rope: Option<Rope>,
237 pub qk_norm: bool,
239 pub output_gate: bool,
242 pub fused_qkv: bool,
244 pub bias: bool,
246 pub output_bias: bool,
248 pub window: Option<Window>,
251 pub sinks: bool,
253 pub softcap: Option<f64>,
255 pub scale: f64,
257 pub temperature: Option<Temperature>,
259 pub qk_unit_norm: bool,
262 pub qk_norm_whole: bool,
265}
266
267#[derive(Clone, Copy, Debug, PartialEq)]
270pub struct Temperature {
271 pub floor: f64,
272 pub scale: f64,
273}
274
275impl Temperature {
276 pub fn at(self, p: usize) -> f64 {
278 1.0 + self.scale * (((p as f64 + 1.0) / self.floor).floor() + 1.0).ln()
279 }
280}
281
282#[derive(Clone, Debug, PartialEq)]
284pub struct Rope {
285 pub dims: usize,
287 pub theta: f64,
288 pub neox: bool,
290 pub scaling: RopeScaling,
291 pub sections: Option<[usize; 4]>,
295}
296
297impl Rope {
298 pub fn rotation(&self, i: usize, pos: usize, factor: Option<f64>) -> (f64, f64) {
303 let mut freq = self.theta.powf(-2.0 * i as f64 / self.dims as f64);
304 if let Some(f) = factor {
305 freq /= f;
306 }
307 let extrapolated = self.text_position(i, pos) as f64 * freq;
308 let (angle, scale) = match self.scaling {
309 RopeScaling::Linear { factor } => (extrapolated / factor, 1.0),
310 RopeScaling::Yarn { factor, original_ctx, beta_fast, beta_slow, magnitude } => {
311 let dims = self.dims as f64;
312 let corr = |beta: f64| {
313 dims * (original_ctx as f64 / (beta * 2.0 * std::f64::consts::PI)).ln() / (2.0 * self.theta.ln())
314 };
315 let low = corr(beta_fast).floor().max(0.0);
316 let high = corr(beta_slow).ceil().min(dims - 1.0);
317 let ramp = 1.0 - ((i as f64 - low) / (high - low).max(0.001)).clamp(0.0, 1.0);
318 (extrapolated / factor * (1.0 - ramp) + extrapolated * ramp, magnitude)
319 }
320 RopeScaling::None | RopeScaling::Factors => (extrapolated, 1.0),
321 };
322 let (s, c) = angle.sin_cos();
323 (c * scale, s * scale)
324 }
325
326 pub fn text_position(&self, i: usize, pos: usize) -> usize {
328 let Some(sec) = self.sections else { return pos };
329 let total: usize = sec.iter().sum();
330 let sector = i % total;
331 let streams = [pos, pos, pos, 0];
332 let stream = if sector % 3 == 1 && sector < 3 * sec[1] {
333 1
334 } else if sector % 3 == 2 && sector < 3 * sec[2] {
335 2
336 } else if sector % 3 == 0 && sector < 3 * sec[0] {
337 0
338 } else {
339 3
340 };
341 streams[stream]
342 }
343}
344
345#[derive(Clone, Debug, PartialEq)]
347pub enum RopeScaling {
348 None,
349 Factors,
351 Linear { factor: f64 },
352 Yarn {
357 factor: f64,
358 original_ctx: usize,
359 beta_fast: f64,
360 beta_slow: f64,
361 magnitude: f64,
362 },
363}
364
365#[derive(Clone, Debug, PartialEq)]
367pub struct GatedDelta {
368 pub k_heads: usize,
369 pub v_heads: usize,
370 pub head_dim: usize,
371 pub conv_taps: usize,
372 pub gate: GateActivation,
374}
375
376#[derive(Clone, Copy, Debug, PartialEq, Eq)]
378pub enum GateActivation {
379 Silu,
380 Sigmoid,
381}
382
383#[derive(Clone, Debug, PartialEq)]
385pub enum Ffn {
386 Dense {
387 dff: usize,
388 act: Act,
389 },
390 Moe(Moe),
391}
392
393#[derive(Clone, Debug, PartialEq)]
395pub struct Moe {
396 pub experts: usize,
397 pub top_k: usize,
398 pub expert_dff: usize,
399 pub act: Act,
400 pub gating: Gating,
401 pub norm_top_k: bool,
403 pub weights_scale: f64,
405 pub shared_experts: usize,
406 pub shared_dff: usize,
407 pub shared_gate: bool,
409 pub biases: bool,
411 pub groups: Option<(usize, usize)>,
414}
415
416#[derive(Clone, Debug, PartialEq)]
418pub enum Act {
419 Silu,
420 GeluTanh,
421 SwigluOai { alpha: f64, limit: f64 },
423}
424
425#[derive(Clone, Debug, PartialEq)]
428pub enum Gating {
429 Softmax,
431 SoftmaxOfTopK,
433 Sigmoid { bias: bool },
435 SigmoidScaledInput,
438}
439
440#[derive(Clone, Debug, PartialEq)]
442pub enum SpecError {
443 UnknownArchitecture(String),
445 MissingKey(String),
447 MissingTensor(String),
449 Inconsistent(String),
451}
452
453impl std::fmt::Display for SpecError {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 match self {
456 SpecError::UnknownArchitecture(a) => write!(f, "architecture `{a}` is not described yet"),
457 SpecError::MissingKey(k) => write!(f, "metadata key `{k}` is missing"),
458 SpecError::MissingTensor(t) => write!(f, "tensor `{t}` is missing"),
459 SpecError::Inconsistent(why) => write!(f, "the checkpoint is inconsistent: {why}"),
460 }
461 }
462}
463
464fn sliding_layers<S: SpecSource + ?Sized>(k: &Keys<'_, S>, n_layers: usize, default: usize, window: usize) -> Result<Vec<bool>, SpecError> {
471 Ok(match k.get("attention.sliding_window_pattern") {
472 Some(MetaValue::Array { items: a, .. }) => a
473 .iter()
474 .take(n_layers)
475 .map(|v| v.as_bool().or_else(|| v.as_u64().map(|x| x != 0)))
476 .collect::<Option<_>>()
477 .ok_or_else(|| SpecError::MissingKey(k.key("attention.sliding_window_pattern")))?,
478 other => {
479 let every = other.and_then(MetaValue::as_u64).map_or(default, |v| v as usize);
480 (0..n_layers).map(|l| window > 0 && (every == 0 || l % every < every - 1)).collect()
481 }
482 })
483}
484
485fn decimal(v: &MetaValue) -> Option<f64> {
486 match *v {
487 MetaValue::F32(f) => f.to_string().parse().ok(),
488 _ => v.as_f64(),
489 }
490}
491
492pub trait SpecSource {
495 fn arch(&self) -> &str;
497 fn arch_meta(&self, suffix: &str) -> Option<&MetaValue>;
499 fn dims(&self, name: &str) -> Option<&[u64]>;
501 fn tensor_names(&self) -> Vec<&str>;
503 fn has(&self, name: &str) -> bool {
504 self.dims(name).is_some()
505 }
506}
507
508impl SpecSource for Gguf {
509 fn arch(&self) -> &str {
510 &self.arch.arch
511 }
512 fn arch_meta(&self, suffix: &str) -> Option<&MetaValue> {
513 Gguf::arch_meta(self, suffix)
514 }
515 fn dims(&self, name: &str) -> Option<&[u64]> {
516 Gguf::dims(self, name)
517 }
518 fn tensor_names(&self) -> Vec<&str> {
519 self.tensors().iter().map(|t| t.name.as_str()).collect()
520 }
521 fn has(&self, name: &str) -> bool {
522 Gguf::has(self, name)
523 }
524}
525
526struct Keys<'a, S: SpecSource + ?Sized> {
528 g: &'a S,
529}
530
531impl<S: SpecSource + ?Sized> Keys<'_, S> {
532 fn key(&self, suffix: &str) -> String {
533 format!("{}.{suffix}", self.g.arch())
534 }
535 fn get(&self, suffix: &str) -> Option<&MetaValue> {
536 self.g.arch_meta(suffix)
537 }
538 fn usize(&self, suffix: &str) -> Result<usize, SpecError> {
539 self.get(suffix)
540 .and_then(MetaValue::as_u64)
541 .map(|v| v as usize)
542 .ok_or_else(|| SpecError::MissingKey(self.key(suffix)))
543 }
544 fn usize_or(&self, suffix: &str, default: usize) -> usize {
545 self.get(suffix)
546 .and_then(MetaValue::as_u64)
547 .map_or(default, |v| v as usize)
548 }
549 fn f64(&self, suffix: &str) -> Result<f64, SpecError> {
550 self.get(suffix)
551 .and_then(decimal)
552 .ok_or_else(|| SpecError::MissingKey(self.key(suffix)))
553 }
554 fn f64_or(&self, suffix: &str, default: f64) -> f64 {
555 self.get(suffix).and_then(decimal).unwrap_or(default)
556 }
557}
558
559impl ModelSpec {
560 pub fn from_gguf(g: &Gguf) -> Result<ModelSpec, SpecError> {
562 ModelSpec::from_source(g)
563 }
564
565 pub fn from_source<S: SpecSource + ?Sized>(g: &S) -> Result<ModelSpec, SpecError> {
567 let k = Keys { g };
568 let arch = g.arch().to_string();
569 let d = k.usize("embedding_length")?;
570 let vocab = g
571 .dims("token_embd.weight")
572 .and_then(|dims| dims.get(1).copied())
573 .ok_or_else(|| SpecError::MissingTensor("token_embd.weight".into()))? as usize;
574 let mtp_blocks = k.usize_or("nextn_predict_layers", 0);
575 let n_layers = k.usize("block_count")? - mtp_blocks;
576 let heads = k.usize("attention.head_count")?;
577 let kv_heads = k.usize_or("attention.head_count_kv", heads);
578 let head_dim = k.usize_or("attention.key_length", d / heads);
579 let value_dim = k.usize_or("attention.value_length", head_dim);
580 let sections = match k.get("rope.dimension_sections").and_then(MetaValue::as_array) {
581 None => None,
582 Some(a) => {
583 let v: Vec<usize> = a.iter().filter_map(MetaValue::as_u64).map(|x| x as usize).collect();
584 match v[..] {
585 [a, b, c, d] if a + b + c + d > 0 => Some([a, b, c, d]),
586 _ => return Err(SpecError::MissingKey(k.key("rope.dimension_sections"))),
587 }
588 }
589 };
590 let rope = |neox: bool| -> Result<Rope, SpecError> {
591 Ok(Rope {
592 dims: k.usize_or("rope.dimension_count", head_dim),
593 theta: k.f64_or("rope.freq_base", 10000.0),
594 neox,
595 scaling: rope_scaling(&k, g)?,
596 sections,
597 })
598 };
599 let attention = |neox: bool, output_gate: bool, at: usize| -> Result<Attention, SpecError> {
602 Ok(Attention {
603 heads,
604 kv_heads,
605 head_dim,
606 value_dim,
607 rope: Some(rope(neox)?),
608 qk_norm: g.has(&format!("blk.{at}.attn_q_norm.weight")),
609 output_gate,
610 fused_qkv: g.has(&format!("blk.{at}.attn_qkv.weight")),
611 bias: g.has(&format!("blk.{at}.attn_q.bias")),
612 output_bias: g.has(&format!("blk.{at}.attn_output.bias")),
613 window: None,
614 sinks: false,
615 softcap: None,
616 scale: 1.0 / (head_dim as f64).sqrt(),
617 temperature: None,
618 qk_unit_norm: false,
619 qk_norm_whole: false,
620 })
621 };
622 let dense = |act: Act| -> Result<Ffn, SpecError> {
623 Ok(Ffn::Dense {
624 dff: k.usize("feed_forward_length")?,
625 act,
626 })
627 };
628 let routed = |gating: usize| -> Result<Ffn, SpecError> {
635 let gating = match k.usize_or("expert_gating_func", gating) {
636 2 => Gating::Sigmoid { bias: g.has("blk.1.exp_probs_b.bias") || has_any(g, "exp_probs_b.bias") },
637 _ => Gating::Softmax,
638 };
639 let shared = k.usize_or("expert_shared_count", 0);
640 let expert_dff = k.usize("expert_feed_forward_length")?;
641 let groups = match (k.usize_or("expert_group_count", 0), k.usize_or("expert_group_used_count", 0)) {
642 (n, used) if n > 1 => Some((n, used)),
643 _ => None,
644 };
645 Ok(Ffn::Moe(Moe {
646 experts: k.usize("expert_count")?,
647 top_k: k.usize("expert_used_count")?,
648 expert_dff,
649 act: Act::Silu,
650 gating,
651 norm_top_k: k.get("expert_weights_norm").and_then(MetaValue::as_bool).unwrap_or(false),
652 weights_scale: k.f64_or("expert_weights_scale", 1.0),
653 shared_experts: shared,
654 shared_dff: expert_dff * shared,
655 shared_gate: false,
656 biases: false,
657 groups,
658 }))
659 };
660 let qwen_mixture = |renormalized: bool| -> Result<Ffn, SpecError> {
666 let first = |suffix: &str| {
667 k.get(suffix)
668 .and_then(|v| v.as_u64().or_else(|| v.as_array().and_then(|a| a.first()).and_then(MetaValue::as_u64)))
669 .filter(|&w| w > 0)
670 .map(|w| w as usize)
671 };
672 let experts = k.usize("expert_count")?;
673 let top_k = k.usize("expert_used_count")?;
674 if experts == 0 || top_k == 0 {
675 return Err(SpecError::Inconsistent(format!("{experts} experts, {top_k} a token: {arch} routes to at least one")));
676 }
677 let ff = || k.usize("feed_forward_length");
678 Ok(Ffn::Moe(Moe {
679 experts,
680 top_k,
681 expert_dff: match first("expert_feed_forward_length") {
682 Some(w) => w,
683 None => ff()? / top_k,
684 },
685 act: Act::Silu,
686 gating: Gating::Softmax,
687 norm_top_k: renormalized,
688 weights_scale: 1.0,
689 shared_experts: 1,
690 shared_dff: match first("expert_shared_feed_forward_length") {
691 Some(w) => w,
692 None => ff()?,
693 },
694 shared_gate: true,
695 biases: false,
696 groups: None,
697 }))
698 };
699 let layers: Vec<Layer> = match arch.as_str() {
700 "qwen3" | "qwen2" | "llama" => {
706 let mixer = Mixer::Attention(attention(arch != "llama", false, 0)?);
707 let ffn = match (arch.as_str(), k.usize_or("expert_count", 0)) {
708 ("llama", experts) if experts > 0 => Ffn::Moe(Moe {
709 experts,
710 top_k: k.usize("expert_used_count")?,
711 expert_dff: match k.get("expert_feed_forward_length").and_then(MetaValue::as_u64) {
712 Some(dff) => dff as usize,
713 None => k.usize("feed_forward_length")?,
714 },
715 act: Act::Silu,
716 gating: Gating::Softmax,
717 norm_top_k: true,
718 weights_scale: 1.0,
719 shared_experts: 0,
720 shared_dff: 0,
721 shared_gate: false,
722 biases: false,
723 groups: None,
724 }),
725 _ => dense(Act::Silu)?,
726 };
727 (0..n_layers)
728 .map(|_| Layer {
729 mixer: mixer.clone(),
730 ffn: ffn.clone(),
731 post_mixer_norm: false,
732 post_ffn_norm: false,
733 })
734 .collect()
735 }
736 "minimax-m2" => {
741 let mut attn = attention(true, false, 0)?;
742 let width = |name: &str| g.dims(&format!("blk.0.{name}.weight")).and_then(|d| d.first().copied()).map(|w| w as usize);
743 attn.qk_norm_whole = attn.qk_norm && width("attn_q_norm") == Some(heads * head_dim);
744 if attn.qk_norm && !attn.qk_norm_whole && width("attn_q_norm") != Some(head_dim) {
745 return Err(SpecError::Inconsistent(format!(
746 "attn_q_norm is {:?} wide, neither a head of {head_dim} nor the {heads} heads' {}",
747 width("attn_q_norm"),
748 heads * head_dim
749 )));
750 }
751 let gating = match k.usize_or("expert_gating_func", 2) {
752 1 => Gating::Softmax,
753 2 => Gating::Sigmoid { bias: has_any(g, "exp_probs_b.bias") },
754 other => {
755 return Err(SpecError::Inconsistent(format!("expert_gating_func {other} is neither a softmax (1) nor a sigmoid (2)")))
756 }
757 };
758 let ffn = Ffn::Moe(Moe {
759 experts: k.usize("expert_count")?,
760 top_k: k.usize("expert_used_count")?,
761 expert_dff: k.usize("expert_feed_forward_length")?,
762 act: Act::Silu,
763 gating,
764 norm_top_k: true,
765 weights_scale: 1.0,
766 shared_experts: 0,
767 shared_dff: 0,
768 shared_gate: false,
769 biases: false,
770 groups: None,
771 });
772 (0..n_layers)
773 .map(|_| Layer {
774 mixer: Mixer::Attention(attn.clone()),
775 ffn: ffn.clone(),
776 post_mixer_norm: false,
777 post_ffn_norm: false,
778 })
779 .collect()
780 }
781 "glm4moe" => {
788 if has_any(g, "post_attention_norm.weight") && !has_any(g, "ffn_norm.weight") {
789 return Err(SpecError::Inconsistent(
790 "a glm4moe checkpoint read raw: its FFN's norm is `post_attention_norm`; `model::open_checkpoint` \
791 presents it as `ffn_norm`"
792 .into(),
793 ));
794 }
795 if sections.is_some() {
796 return Err(SpecError::Inconsistent("GLM-4.5V's multimodal rotation (`rope.dimension_sections`) is not read yet".into()));
797 }
798 let attn = attention(true, false, 0)?;
799 let moe = routed(2)?;
800 let dense_lead = k.usize_or("leading_dense_block_count", 0);
801 let dense_ffn = dense(Act::Silu)?;
802 (0..n_layers)
803 .map(|l| Layer {
804 mixer: Mixer::Attention(attn.clone()),
805 ffn: if l < dense_lead { dense_ffn.clone() } else { moe.clone() },
806 post_mixer_norm: false,
807 post_ffn_norm: false,
808 })
809 .collect()
810 }
811 "phi3" => {
817 if has_any(g, "ffn_up.weight") && !has_any(g, "ffn_gate.weight") {
818 return Err(SpecError::Inconsistent(
819 "a phi3 checkpoint read raw: its gate and up projections are one `ffn_up`; `model::open_checkpoint` \
820 presents them apart"
821 .into(),
822 ));
823 }
824 if g.has("rope_factors_long.weight") || g.has("rope_factors_short.weight") {
825 return Err(SpecError::Inconsistent(
826 "LongRoPE's factor tables (`rope_factors_long`, `rope_factors_short`) are not read yet".into(),
827 ));
828 }
829 let mut attn = attention(true, false, 0)?;
830 let window = k.usize_or("attention.sliding_window", 0);
831 if window > 0 && window < k.usize_or("context_length", usize::MAX) {
832 attn.window = Some(Window::Sliding(window));
833 }
834 let ffn = dense(Act::Silu)?;
835 (0..n_layers)
836 .map(|_| Layer {
837 mixer: Mixer::Attention(attn.clone()),
838 ffn: ffn.clone(),
839 post_mixer_norm: false,
840 post_ffn_norm: false,
841 })
842 .collect()
843 }
844 "glm4" => {
850 if has_any(g, "ffn_up.weight") && !has_any(g, "ffn_gate.weight") {
851 return Err(SpecError::Inconsistent(
852 "a glm4 checkpoint read raw: its gate and up projections are one `ffn_up`; `model::open_checkpoint` \
853 presents them apart"
854 .into(),
855 ));
856 }
857 if sections.is_some() {
858 return Err(SpecError::Inconsistent("GLM-4V's multimodal rotation (`rope.dimension_sections`) is not read yet".into()));
859 }
860 let attn = attention(false, false, 0)?;
861 let ffn = dense(Act::Silu)?;
862 let (post_mixer_norm, post_ffn_norm) = (has_any(g, "post_attention_norm.weight"), has_any(g, "post_ffw_norm.weight"));
863 (0..n_layers)
864 .map(|_| Layer { mixer: Mixer::Attention(attn.clone()), ffn: ffn.clone(), post_mixer_norm, post_ffn_norm })
865 .collect()
866 }
867 "qwen3moe" => {
869 let mixer = Mixer::Attention(attention(true, false, 0)?);
870 let ffn = Ffn::Moe(Moe {
871 experts: k.usize("expert_count")?,
872 top_k: k.usize("expert_used_count")?,
873 expert_dff: k.usize("expert_feed_forward_length")?,
874 act: Act::Silu,
875 gating: Gating::Softmax,
876 norm_top_k: true,
877 weights_scale: 1.0,
878 shared_experts: 0,
879 shared_dff: 0,
880 shared_gate: false,
881 biases: false,
882 groups: None,
883 });
884 (0..n_layers)
885 .map(|_| Layer {
886 mixer: mixer.clone(),
887 ffn: ffn.clone(),
888 post_mixer_norm: false,
889 post_ffn_norm: false,
890 })
891 .collect()
892 }
893 "qwen2moe" => {
896 let mixer = Mixer::Attention(attention(true, false, 0)?);
897 let ffn = qwen_mixture(false)?;
898 (0..n_layers)
899 .map(|_| Layer {
900 mixer: mixer.clone(),
901 ffn: ffn.clone(),
902 post_mixer_norm: false,
903 post_ffn_norm: false,
904 })
905 .collect()
906 }
907 "qwen35" | "qwen35moe" | "qwen3next" | "qwen4exp" => {
916 if arch == "qwen3next" && has_any(g, "ssm_ba.weight") {
917 return Err(SpecError::Inconsistent(
918 "a Qwen3-Next checkpoint read raw: its value heads are grouped by key head and its betas and alphas \
919 one tensor (`ssm_ba`); `model::open_checkpoint` presents it in Qwen3.5's tiled layout"
920 .into(),
921 ));
922 }
923 let recurrent: Vec<bool> = match k.get("attention.recurrent_layers").and_then(MetaValue::as_array) {
924 Some(a) => a
925 .iter()
926 .take(n_layers)
927 .map(|v| v.as_bool().or_else(|| v.as_u64().map(|x| x != 0)))
928 .collect::<Option<_>>()
929 .ok_or_else(|| SpecError::MissingKey(k.key("attention.recurrent_layers")))?,
930 None => {
931 let every = k.usize_or("full_attention_interval", 4);
932 (0..n_layers).map(|l| (l + 1) % every != 0).collect()
933 }
934 };
935 let gdn = GatedDelta {
936 k_heads: k.usize("ssm.group_count")?,
937 v_heads: k.usize("ssm.time_step_rank")?,
938 head_dim: k.usize("ssm.state_size")?,
939 conv_taps: k.usize("ssm.conv_kernel")?,
940 gate: if arch == "qwen4exp" { GateActivation::Sigmoid } else { GateActivation::Silu },
941 };
942 let inner = k.usize("ssm.inner_size")?;
943 if inner != gdn.v_heads * gdn.head_dim {
944 return Err(SpecError::Inconsistent(format!(
945 "ssm.inner_size {inner} is not {} value heads of {}",
946 gdn.v_heads, gdn.head_dim
947 )));
948 }
949 let at = recurrent.iter().position(|r| !r).unwrap_or(0);
950 let attn = attention(true, true, at)?;
951 let ffn = if arch == "qwen35" { dense(Act::Silu)? } else { qwen_mixture(true)? };
952 recurrent
953 .iter()
954 .map(|&r| Layer {
955 mixer: if r { Mixer::GatedDelta(gdn.clone()) } else { Mixer::Attention(attn.clone()) },
956 ffn: ffn.clone(),
957 post_mixer_norm: false,
958 post_ffn_norm: false,
959 })
960 .collect()
961 }
962 "gemma3" => {
966 let window = k.usize_or("attention.sliding_window", 0);
967 let swa = sliding_layers(&k, n_layers, 6, window)?;
968 let global = attention(true, false, 0)?;
969 let scale = if n_layers == 62 { 1.0 / ((d / heads) as f64).sqrt() } else { 1.0 / (head_dim as f64).sqrt() };
970 let local = Attention {
971 window: Some(Window::Sliding(window)),
972 rope: Some(Rope {
973 theta: k.f64_or("rope.freq_base_swa", 10000.0),
974 scaling: RopeScaling::None,
975 ..global.rope.clone().expect("gemma3 rotates")
976 }),
977 scale,
978 ..global.clone()
979 };
980 let global = Attention { scale, ..global };
981 let ffn = dense(Act::GeluTanh)?;
982 swa.iter()
983 .map(|&s| Layer {
984 mixer: Mixer::Attention(if s { local.clone() } else { global.clone() }),
985 ffn: ffn.clone(),
986 post_mixer_norm: true,
987 post_ffn_norm: true,
988 })
989 .collect()
990 }
991 "gemma" => {
994 let attn = attention(true, false, 0)?;
995 let ffn = dense(Act::GeluTanh)?;
996 (0..n_layers)
997 .map(|_| Layer { mixer: Mixer::Attention(attn.clone()), ffn: ffn.clone(), post_mixer_norm: false, post_ffn_norm: false })
998 .collect()
999 }
1000 "gemma2" => {
1006 let window = k.usize_or("attention.sliding_window", 4096);
1007 let swa = sliding_layers(&k, n_layers, 2, window)?;
1008 let scale = if n_layers == 46 { 1.0 / ((d / heads) as f64).sqrt() } else { 1.0 / (head_dim as f64).sqrt() };
1009 let softcap = Some(k.f64_or("attn_logit_softcapping", 50.0));
1010 let global = Attention { scale, softcap, ..attention(true, false, 0)? };
1011 let local = Attention { window: Some(Window::Sliding(window)), ..global.clone() };
1012 let ffn = dense(Act::GeluTanh)?;
1013 swa.iter()
1014 .map(|&s| Layer {
1015 mixer: Mixer::Attention(if s { local.clone() } else { global.clone() }),
1016 ffn: ffn.clone(),
1017 post_mixer_norm: true,
1018 post_ffn_norm: true,
1019 })
1020 .collect()
1021 }
1022 "gpt-oss" => {
1026 let window = k.usize("attention.sliding_window")?;
1027 let every = k.usize_or("attention.sliding_window_pattern", 2);
1028 let attn = Attention { sinks: true, ..attention(true, false, 0)? };
1029 let ffn = Ffn::Moe(Moe {
1030 experts: k.usize("expert_count")?,
1031 top_k: k.usize("expert_used_count")?,
1032 expert_dff: k.usize("expert_feed_forward_length")?,
1033 act: Act::SwigluOai { alpha: 1.702, limit: 7.0 },
1034 gating: Gating::SoftmaxOfTopK,
1035 norm_top_k: false,
1036 weights_scale: 1.0,
1037 shared_experts: 0,
1038 shared_dff: 0,
1039 shared_gate: false,
1040 biases: g.has("blk.0.ffn_gate_inp.bias"),
1041 groups: None,
1042 });
1043 (0..n_layers)
1044 .map(|l| Layer {
1045 mixer: Mixer::Attention(Attention {
1046 window: (every == 0 || l % every < every - 1).then_some(Window::Sliding(window)),
1047 ..attn.clone()
1048 }),
1049 ffn: ffn.clone(),
1050 post_mixer_norm: false,
1051 post_ffn_norm: false,
1052 })
1053 .collect()
1054 }
1055 "deepseek2" => {
1060 let rope_dims = k.usize("rope.dimension_count")?;
1061 let key = k.usize_or("attention.key_length_mla", head_dim);
1062 let rope = Rope {
1063 dims: rope_dims,
1064 theta: k.f64_or("rope.freq_base", 10000.0),
1065 neox: false,
1066 scaling: rope_scaling(&k, g)?,
1067 sections: None,
1068 };
1069 let mscale = match rope.scaling {
1071 RopeScaling::Yarn { factor, magnitude, .. } => {
1072 let log_mul = k.f64_or("rope.scaling.yarn_log_multiplier", 0.0) / 0.1;
1073 magnitude * (1.0 + 0.1 * log_mul * factor.ln())
1074 }
1075 _ => 1.0,
1076 };
1077 let mla = Mla {
1078 heads,
1079 q_lora_rank: k.get("attention.q_lora_rank").and_then(MetaValue::as_u64).map(|v| v as usize),
1080 kv_lora_rank: k.usize("attention.kv_lora_rank")?,
1081 qk_nope: key - rope_dims,
1082 qk_rope: rope_dims,
1083 v_dim: k.usize_or("attention.value_length_mla", value_dim),
1084 rope,
1085 scale: mscale * mscale / (key as f64).sqrt(),
1086 kv_b_split: g.has("blk.0.attn_k_b.weight"),
1087 };
1088 let moe = routed(1)?;
1089 let dense_lead = k.usize_or("leading_dense_block_count", 0);
1090 let dense_ffn = dense(Act::Silu)?;
1091 (0..n_layers)
1092 .map(|l| Layer {
1093 mixer: Mixer::Mla(mla.clone()),
1094 ffn: if l < dense_lead { dense_ffn.clone() } else { moe.clone() },
1095 post_mixer_norm: false,
1096 post_ffn_norm: false,
1097 })
1098 .collect()
1099 }
1100 "llama4" => {
1108 let experts = k.usize("expert_count")?;
1109 let chunked = k.get("attention.sliding_window").and_then(MetaValue::as_u64) != Some(0);
1110 let step = k.usize("interleave_moe_layer_step")?;
1111 let expert_dff = k.usize("expert_feed_forward_length")?;
1112 let base = attention(false, false, 0)?;
1113 let rotated = Attention {
1114 window: chunked.then_some(Window::Chunked(8192)),
1115 qk_unit_norm: experts != 128,
1116 qk_norm_whole: false,
1117 ..base.clone()
1118 };
1119 let full = Attention {
1120 rope: None,
1121 temperature: chunked.then_some(Temperature { floor: 8192.0, scale: 0.1 }),
1122 ..base.clone()
1123 };
1124 let no_rope_step = if chunked { 4 } else { n_layers };
1125 let moe = Ffn::Moe(Moe {
1126 experts,
1127 top_k: k.usize("expert_used_count")?,
1128 expert_dff,
1129 act: Act::Silu,
1130 gating: Gating::SigmoidScaledInput,
1131 norm_top_k: false,
1132 weights_scale: 1.0,
1133 shared_experts: 1,
1134 shared_dff: expert_dff,
1135 shared_gate: false,
1136 biases: false,
1137 groups: None,
1138 });
1139 let dense_ffn = dense(Act::Silu)?;
1140 (0..n_layers)
1141 .map(|l| Layer {
1142 mixer: Mixer::Attention(if (l + 1) % no_rope_step == 0 { full.clone() } else { rotated.clone() }),
1143 ffn: if step > 0 && (l + 1) % step == 0 { moe.clone() } else { dense_ffn.clone() },
1144 post_mixer_norm: false,
1145 post_ffn_norm: false,
1146 })
1147 .collect()
1148 }
1149 other => return Err(SpecError::UnknownArchitecture(other.to_string())),
1150 };
1151 let ffn_norm =
1152 if matches!(arch.as_str(), "qwen35" | "qwen35moe" | "qwen3next" | "gpt-oss") { "post_attention_norm" } else { "ffn_norm" };
1153 let (hyper, ple, indexer) = if arch == "qwen4exp" { qwen4exp_parts(&k, &layers)? } else { (None, None, None) };
1154 let embedding_scale = matches!(arch.as_str(), "gemma" | "gemma2" | "gemma3").then(|| (d as f64).sqrt());
1155 let final_logit_softcap = match k.get("final_logit_softcapping").and_then(decimal) {
1157 None if arch == "gemma2" => Some(30.0),
1158 cap => cap,
1159 };
1160 Ok(ModelSpec {
1161 arch,
1162 d,
1163 vocab,
1164 ctx_len: k.usize_or("context_length", 0),
1165 rms_eps: k.f64_or("attention.layer_norm_rms_epsilon", 1e-6),
1166 embedding_scale,
1167 tied_output: !g.has("output.weight"),
1168 final_logit_softcap,
1169 layers,
1170 mtp_blocks,
1171 ffn_norm,
1172 hyper,
1173 ple,
1174 indexer,
1175 })
1176 }
1177
1178 pub fn attention_layers(&self) -> Vec<usize> {
1180 self.layers
1181 .iter()
1182 .enumerate()
1183 .filter(|(_, l)| matches!(l.mixer, Mixer::Attention(_)))
1184 .map(|(i, _)| i)
1185 .collect()
1186 }
1187}
1188
1189fn has_any<S: SpecSource + ?Sized>(g: &S, suffix: &str) -> bool {
1191 g.tensor_names().iter().any(|t| t.starts_with("blk.") && t.ends_with(suffix))
1192}
1193
1194fn rope_scaling<S: SpecSource + ?Sized>(k: &Keys<S>, g: &S) -> Result<RopeScaling, SpecError> {
1196 if g.has("rope_freqs.weight") {
1197 return Ok(RopeScaling::Factors);
1198 }
1199 let kind = k.get("rope.scaling.type").and_then(MetaValue::as_str).unwrap_or("none");
1200 Ok(match kind {
1201 "none" => RopeScaling::None,
1202 "linear" => RopeScaling::Linear {
1203 factor: k.f64("rope.scaling.factor")?,
1204 },
1205 "yarn" => {
1206 let factor = k.f64("rope.scaling.factor")?;
1207 let get_mscale = |m: f64| if factor <= 1.0 { 1.0 } else { 0.1 * m * factor.ln() + 1.0 };
1210 let log_mul = k.f64_or("rope.scaling.yarn_log_multiplier", 0.0);
1211 let ratio = if log_mul != 0.0 {
1212 let m = if k.g.arch() == "deepseek2" && log_mul != 1.0 { log_mul } else { 1.0 };
1213 get_mscale(m) / get_mscale(log_mul)
1214 } else {
1215 get_mscale(1.0)
1216 };
1217 RopeScaling::Yarn {
1218 factor,
1219 original_ctx: k.usize("rope.scaling.original_context_length")?,
1220 beta_fast: k.f64_or("rope.scaling.yarn_beta_fast", 32.0),
1221 beta_slow: k.f64_or("rope.scaling.yarn_beta_slow", 1.0),
1222 magnitude: ratio * k.f64_or("rope.scaling.attn_factor", 1.0),
1223 }
1224 }
1225 other => return Err(SpecError::MissingKey(format!("rope.scaling.type `{other}` is not described yet"))),
1226 })
1227}