1use crate::kv_cache::LayerKvCache;
15use crate::linear_core::{
16 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights,
17};
18use crate::pipeline::{
19 AttnKind, DenseFfn, FfnKind, LayerWeights, MoeFfn, MtpModule, Pipeline, PipelineWeights,
20};
21use crate::qtensor::QTensor;
22use crate::sampler::SamplerConfig;
23use crate::tokenizer::Tokenizer;
24use cortiq_core::quant::dequant_tensor;
25use cortiq_core::{CmfError, CmfModel, LayerType, ModelArch};
26use std::sync::Arc;
27
28pub enum Overlay<'a> {
31 None,
32 One(&'a str),
33 Blend(&'a [(String, f32)]),
35}
36
37impl Overlay<'_> {
38 fn blend_touches(&self, model: &CmfModel, name: &str) -> bool {
39 match self {
40 Overlay::Blend(list) => list
41 .iter()
42 .any(|(sid, _)| model.tensor(&format!("skill.{sid}.{name}")).is_some()),
43 _ => false,
44 }
45 }
46}
47
48fn dequant_by_name(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
49 let entry = model
50 .tensor(name)
51 .ok_or_else(|| format!("tensor '{name}' not found in CMF directory"))?;
52 let mut out = vec![0.0f32; entry.n_elems()];
53 dequant_tensor(entry, model.entry_bytes(entry), &mut out)?;
54 Ok(out)
55}
56
57fn blend_f32(model: &CmfModel, name: &str, list: &[(String, f32)]) -> Result<Vec<f32>, String> {
60 let mut acc: Option<Vec<f32>> = None;
61 for (sid, w) in list {
62 let sname = format!("skill.{sid}.{name}");
63 let src = if model.tensor(&sname).is_some() {
64 &sname
65 } else {
66 name
67 };
68 let t = dequant_by_name(model, src)?;
69 match &mut acc {
70 None => {
71 let mut t = t;
72 for v in t.iter_mut() {
73 *v *= w;
74 }
75 acc = Some(t);
76 }
77 Some(a) => {
78 for (av, tv) in a.iter_mut().zip(&t) {
79 *av += w * tv;
80 }
81 }
82 }
83 }
84 acc.ok_or_else(|| "empty blend".into())
85}
86
87pub(crate) fn load_f32(model: &CmfModel, name: &str, ov: &Overlay) -> Result<Vec<f32>, String> {
89 if ov.blend_touches(model, name) {
90 if let Overlay::Blend(list) = ov {
91 return blend_f32(model, name, list);
92 }
93 }
94 let skill = match ov {
95 Overlay::One(s) => Some(*s),
96 _ => None,
97 };
98 let entry = model
99 .resolve_tensor(name, skill)
100 .ok_or_else(|| format!("tensor '{name}' not found in CMF directory"))?;
101 let bytes = model.entry_bytes(entry);
102 let mut out = vec![0.0f32; entry.n_elems()];
103 dequant_tensor(entry, bytes, &mut out)?;
104 Ok(out)
105}
106
107pub(crate) fn build_layer_ffn(
113 model: &Arc<CmfModel>,
114 arch: &ModelArch,
115 li: usize,
116 force_f32: bool,
117 ov: &Overlay,
118) -> Result<FfnKind, CmfError> {
119 build_ffn_at(model, arch, &format!("model.layers.{li}."), force_f32, ov)
120}
121
122pub(crate) fn build_ffn_at(
127 model: &Arc<CmfModel>,
128 arch: &ModelArch,
129 prefix: &str,
130 force_f32: bool,
131 ov: &Overlay,
132) -> Result<FfnKind, CmfError> {
133 let prefix = prefix.to_string();
134 let load_dense = |p: &str| -> Result<DenseFfn, CmfError> {
135 let gate_proj = load_matrix(model, &format!("{p}gate_proj.weight"), force_f32, ov)?;
136 let up_proj = load_matrix(model, &format!("{p}up_proj.weight"), force_f32, ov)?;
137 let down_proj = load_matrix(model, &format!("{p}down_proj.weight"), force_f32, ov)?;
138 let inter = gate_proj.rows();
142 if up_proj.rows() != inter || down_proj.cols() != inter {
143 return Err(CmfError::Parse(format!(
144 "{p}: FFN dims disagree (gate.rows={inter}, up.rows={}, \
145 down.cols={}); all three must equal inter'",
146 up_proj.rows(),
147 down_proj.cols()
148 )));
149 }
150 if down_proj.rows() != arch.hidden_size {
151 return Err(CmfError::Parse(format!(
152 "{p}: down_proj.rows={} != hidden_size={}",
153 down_proj.rows(),
154 arch.hidden_size
155 )));
156 }
157 let dt_name = format!("{p}down_proj.t.weight");
160 let down_t = match model.tensor(&dt_name) {
161 Some(_) => Some(load_matrix(model, &dt_name, force_f32, ov)?),
162 None => None,
163 };
164 if let Some(t) = &down_t
165 && (t.rows() != inter || t.cols() != arch.hidden_size)
166 {
167 return Err(CmfError::Parse(format!(
168 "{p}down_proj.t: [{}, {}] != [{inter}, {}]",
169 t.rows(),
170 t.cols(),
171 arch.hidden_size
172 )));
173 }
174 let mut segs = Vec::new();
180 let mut start = inter;
181 for k in 1.. {
182 let gn = format!("{p}gate_proj.tube{k}.weight");
183 if model.tensor(&gn).is_none() {
184 break;
185 }
186 let gate = load_matrix(model, &gn, force_f32, ov)?;
187 let up = load_matrix(model, &format!("{p}up_proj.tube{k}.weight"), force_f32, ov)?;
188 let down = load_matrix(model, &format!("{p}down_proj.tube{k}.weight"), force_f32, ov)?;
189 let width = gate.rows();
190 if up.rows() != width || down.cols() != width || down.rows() != arch.hidden_size {
191 return Err(CmfError::Parse(format!(
192 "{p}tube{k}: dims disagree (gate.rows={width}, up.rows={}, \
193 down=[{}, {}], hidden={})",
194 up.rows(),
195 down.rows(),
196 down.cols(),
197 arch.hidden_size
198 )));
199 }
200 segs.push(crate::pipeline::FfnSeg {
201 gate,
202 up,
203 down,
204 start,
205 width,
206 });
207 start += width;
208 }
209 Ok(DenseFfn {
210 gate_proj,
211 up_proj,
212 down_proj,
213 act: crate::pipeline::Act::from_arch_full(arch),
214 down_t,
215 segs,
216 })
217 };
218 let router_name = format!("{prefix}mlp.gate.weight");
219 let resonance_moe = model.tensor(&router_name).is_none()
223 && arch.moe.as_ref().is_some_and(|m| m.router_resonance)
224 && model
225 .tensor(&format!("{prefix}mlp.experts.0.gate_proj.weight"))
226 .is_some();
227 if model.tensor(&router_name).is_none() && !resonance_moe {
228 return Ok(FfnKind::Dense(load_dense(&format!("{prefix}mlp."))?));
229 }
230 let cfg = arch.moe.as_ref().ok_or_else(|| {
231 CmfError::Parse(format!(
232 "{router_name} present but header has no arch.moe block"
233 ))
234 })?;
235 let mut experts = Vec::new();
240 for e in 0..cfg.num_experts {
241 if model
242 .tensor(&format!("{prefix}mlp.experts.{e}.gate_proj.weight"))
243 .is_none()
244 {
245 break;
246 }
247 experts.push(load_dense(&format!("{prefix}mlp.experts.{e}."))?);
248 }
249 if experts.is_empty() {
250 return Err(CmfError::Parse(format!(
251 "{prefix}: router present but no expert tensors"
252 )));
253 }
254 let shared = if model
255 .tensor(&format!("{prefix}mlp.shared_expert.gate_proj.weight"))
256 .is_some()
257 {
258 let gate_name = format!("{prefix}mlp.shared_expert_gate.weight");
259 Some((
260 load_dense(&format!("{prefix}mlp.shared_expert."))?,
261 if model.tensor(&gate_name).is_some() {
262 Some(load_matrix(model, &gate_name, force_f32, ov)?)
263 } else {
264 None
265 },
266 ))
267 } else {
268 None
269 };
270 let bias_name = format!("{prefix}mlp.expert_bias");
273 let expert_bias = if model.tensor(&bias_name).is_some() {
274 Some(load_f32(model, &bias_name, ov).map_err(CmfError::Parse)?)
275 } else {
276 None
277 };
278 let top_k = std::env::var("CMF_MOE_TOPK")
284 .ok()
285 .and_then(|v| v.parse::<usize>().ok())
286 .filter(|&k| k >= 1 && k <= cfg.top_k)
287 .inspect(|k| tracing::info!("MoE top_k override: {} (header {})", k, cfg.top_k))
288 .unwrap_or(cfg.top_k);
289 let route_tau = std::env::var("CMF_MOE_TAU")
291 .ok()
292 .and_then(|v| v.parse::<f32>().ok())
293 .filter(|&t| t > 0.0 && t < 1.0)
294 .inspect(|t| tracing::info!("MoE adaptive routing: tau {t}"));
295 let mask = moe_task_mask(model, &prefix, experts.len());
296 let router = if resonance_moe {
297 QTensor::from_f32(vec![0.0; experts.len() * arch.hidden_size], experts.len(), arch.hidden_size)
299 } else {
300 load_matrix(model, &router_name, force_f32, ov)?
301 };
302 if router.rows() != experts.len() {
303 return Err(CmfError::Parse(format!(
304 "{router_name}: {} rows != {} experts",
305 router.rows(),
306 experts.len()
307 )));
308 }
309 let top_k = top_k.min(experts.len());
310 let pes_name = format!("{prefix}mlp.per_expert_scale");
314 let per_expert_scale = if model.tensor(&pes_name).is_some() {
315 Some(load_f32(model, &pes_name, ov).map_err(CmfError::Parse)?)
316 } else {
317 None
318 };
319 let router_input_norm = per_expert_scale.is_some();
320 let per_expert = model.tensor(&format!("{prefix}mlp.experts.0.desc.mu")).is_some();
325 let resonance = if per_expert {
326 let ne_d = experts.len();
327 let hidden = arch.hidden_size;
328 let mut mu = Vec::with_capacity(ne_d * hidden);
329 let mut u = Vec::new();
330 let mut bias = Vec::with_capacity(ne_d);
331 let mut k = 0usize;
332 for e in 0..ne_d {
333 let m = load_f32(model, &format!("{prefix}mlp.experts.{e}.desc.mu"), ov).map_err(CmfError::Parse)?;
334 if m.len() != hidden {
335 return Err(CmfError::Parse(format!("{prefix}mlp.experts.{e}.desc.mu: {} != {hidden}", m.len())));
336 }
337 mu.extend_from_slice(&m);
338 let un = format!("{prefix}mlp.experts.{e}.desc.u");
339 if model.tensor(&un).is_some() {
340 let ue = load_f32(model, &un, ov).map_err(CmfError::Parse)?;
341 let ke = ue.len() / hidden.max(1);
342 if e == 0 {
343 k = ke;
344 }
345 if ke != k {
346 return Err(CmfError::Parse(format!("{un}: rank {ke} != {k}")));
347 }
348 u.extend_from_slice(&ue);
349 }
350 let bn = format!("{prefix}mlp.experts.{e}.desc.bias");
351 bias.push(if model.tensor(&bn).is_some() {
352 load_f32(model, &bn, ov).map_err(CmfError::Parse)?.first().copied().unwrap_or(0.0)
353 } else {
354 0.0
355 });
356 }
357 Some(crate::pipeline::Resonance { mu, u, k, bias })
358 } else if model.tensor(&format!("{prefix}mlp.desc.mu")).is_some() {
359 let mu = load_f32(model, &format!("{prefix}mlp.desc.mu"), ov).map_err(CmfError::Parse)?;
360 let ne_d = experts.len();
361 let hidden = arch.hidden_size;
362 if mu.len() != ne_d * hidden {
363 return Err(CmfError::Parse(format!("{prefix}mlp.desc.mu: {} != {ne_d}×{hidden}", mu.len())));
364 }
365 let u_name = format!("{prefix}mlp.desc.u");
366 let (u, k) = if model.tensor(&u_name).is_some() {
367 let u = load_f32(model, &u_name, ov).map_err(CmfError::Parse)?;
368 let k = u.len() / (ne_d * hidden).max(1);
369 (u, k)
370 } else {
371 (Vec::new(), 0)
372 };
373 let b_name = format!("{prefix}mlp.desc.bias");
374 let bias = if model.tensor(&b_name).is_some() {
375 load_f32(model, &b_name, ov).map_err(CmfError::Parse)?
376 } else {
377 vec![0.0; ne_d]
378 };
379 Some(crate::pipeline::Resonance { mu, u, k, bias })
380 } else {
381 None
382 };
383 let moe = MoeFfn {
384 router,
385 experts,
386 top_k,
387 route_tau,
388 norm_topk_prob: cfg.norm_topk_prob,
389 router_sigmoid: cfg.router_sigmoid,
390 expert_bias,
391 routed_scaling: cfg.routed_scaling_factor.unwrap_or(1.0),
392 shared,
393 stats: std::cell::RefCell::new(Vec::new()),
394 act_sq: std::cell::RefCell::new(Vec::new()),
395 act_rows: std::cell::RefCell::new(Vec::new()),
396 mask,
397 per_expert_scale,
398 router_input_norm,
399 resonance,
400 };
401 if model
404 .tensor(&format!("{prefix}mlp.gate_proj.weight"))
405 .is_some()
406 {
407 let norm = |suffix: &str| -> Result<Vec<f32>, CmfError> {
408 load_f32(model, &format!("{prefix}{suffix}.weight"), ov).map_err(CmfError::Parse)
409 };
410 return Ok(FfnKind::DenseMoe(Box::new(crate::pipeline::DenseMoeFfn {
411 dense: load_dense(&format!("{prefix}mlp."))?,
412 moe,
413 post_norm_1: norm("post_feedforward_layernorm_1")?,
414 pre_norm_2: norm("pre_feedforward_layernorm_2")?,
415 post_norm_2: norm("post_feedforward_layernorm_2")?,
416 })));
417 }
418 Ok(FfnKind::Moe(moe))
419}
420
421pub(crate) fn moe_task_mask(
432 model: &std::sync::Arc<CmfModel>,
433 prefix: &str,
434 ne: usize,
435) -> Option<Vec<bool>> {
436 use std::sync::OnceLock;
437 static CFG: OnceLock<Option<(std::collections::HashMap<usize, Vec<u64>>, f64)>> =
438 OnceLock::new();
439 let cfg = CFG.get_or_init(|| {
440 let (path, sidecar) = if let Ok(path) = std::env::var("CMF_MOE_MASK") {
441 (std::path::PathBuf::from(path), false)
442 } else {
443 let mut name = model.path.as_os_str().to_os_string();
444 name.push(".moe-mass.json");
445 let path = std::path::PathBuf::from(name);
446 path.is_file().then_some((path, true))?
447 };
448 let shown = path.display();
449 let text = std::fs::read_to_string(&path)
450 .map_err(|e| tracing::warn!("CMF_MOE_MASK: cannot read {shown}: {e}"))
451 .ok()?;
452 let map: std::collections::HashMap<String, Vec<u64>> = serde_json::from_str(&text)
453 .map_err(|e| tracing::warn!("CMF_MOE_MASK: bad JSON in {shown}: {e}"))
454 .ok()?;
455 let weighted = sidecar
460 || map
461 .values()
462 .any(|row| row.iter().copied().sum::<u64>() >= 1_000_000);
463 let cover = std::env::var("CMF_MOE_MASK_COVER")
464 .ok()
465 .and_then(|v| v.parse::<f64>().ok())
466 .filter(|&c| c > 0.0 && c <= 1.0)
467 .unwrap_or(if weighted { 0.925 } else { 0.9 });
468 tracing::info!("MoE task mask: {shown}, cover {cover}");
469 Some((
470 map.into_iter()
471 .filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
472 .collect(),
473 cover,
474 ))
475 });
476 let (stats, cover) = cfg.as_ref()?;
477 let li: usize = prefix
479 .split("layers.")
480 .nth(1)?
481 .split('.')
482 .next()?
483 .parse()
484 .ok()?;
485 let counts = stats.get(&li)?;
486 if counts.len() != ne {
487 tracing::warn!(
488 "CMF_MOE_MASK: layer {li} has {} counts, model has {ne} experts — skipped",
489 counts.len()
490 );
491 return None;
492 }
493 let total: u64 = counts.iter().sum();
494 if total == 0 {
495 return None;
496 }
497 let mut order: Vec<usize> = (0..ne).collect();
498 order.sort_unstable_by_key(|&e| std::cmp::Reverse(counts[e]));
499 let mut mask = vec![false; ne];
500 let mut acc = 0u64;
501 let mut kept = 0usize;
502 for &e in &order {
503 mask[e] = true;
504 acc += counts[e];
505 kept += 1;
506 if (acc as f64) >= cover * (total as f64) {
507 break;
508 }
509 }
510 tracing::info!(
511 "MoE task mask L{li}: {kept}/{ne} experts for {:.0}% mass",
512 cover * 100.0
513 );
514 Some(mask)
515}
516
517fn load_matrix(
518 model: &Arc<CmfModel>,
519 name: &str,
520 force_f32: bool,
521 ov: &Overlay,
522) -> Result<QTensor, CmfError> {
523 if ov.blend_touches(model, name) {
527 if let Overlay::Blend(list) = ov {
528 let entry = model
529 .tensor(name)
530 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
531 let data =
532 blend_f32(model, name, list).map_err(|e| CmfError::Parse(format!("blend: {e}")))?;
533 return Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]));
534 }
535 }
536 let skill = match ov {
537 Overlay::One(s) => Some(*s),
538 _ => None,
539 };
540 let name: &str = &match skill {
543 Some(sid) if model.tensor(&format!("skill.{sid}.{name}")).is_some() => {
544 format!("skill.{sid}.{name}")
545 }
546 _ => name.to_string(),
547 };
548 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
549 if force_f32 {
550 let entry = model
551 .tensor(name)
552 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
553 if entry.shape.len() != 2 {
554 return Err(err(format!("'{name}' is not 2-D")));
555 }
556 let data = load_f32(model, name, &Overlay::None).map_err(err)?;
557 Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]))
558 } else {
559 QTensor::from_model(model, name).map_err(err)
560 }
561}
562
563impl Pipeline {
564 pub fn from_model(
566 model: &Arc<CmfModel>,
567 sampler_config: SamplerConfig,
568 ) -> Result<Self, CmfError> {
569 Self::from_model_with_skill(model, sampler_config, None)
570 }
571
572 pub fn from_model_with_skill(
578 model: &Arc<CmfModel>,
579 sampler_config: SamplerConfig,
580 skill: Option<&str>,
581 ) -> Result<Self, CmfError> {
582 match skill {
583 Some(s) => Self::from_model_with_overlay(model, sampler_config, &Overlay::One(s)),
584 None => Self::from_model_with_overlay(model, sampler_config, &Overlay::None),
585 }
586 }
587
588 pub fn from_model_with_blend(
591 model: &Arc<CmfModel>,
592 sampler_config: SamplerConfig,
593 blend: &[(String, f32)],
594 ) -> Result<Self, CmfError> {
595 Self::from_model_with_overlay(model, sampler_config, &Overlay::Blend(blend))
596 }
597
598 fn skill_file_guard(model: &CmfModel) -> Result<(), CmfError> {
599 if model.required_features & cortiq_core::format::features::SKILL_FILE != 0 {
602 return Err(CmfError::Parse(
603 "this file is a standalone SKILL, not a runnable model — attach it: \
604 cortiq skill apply <base.cmf> <this file> -o specialist.cmf"
605 .into(),
606 ));
607 }
608 Ok(())
609 }
610
611 fn from_model_with_overlay(
612 model: &Arc<CmfModel>,
613 sampler_config: SamplerConfig,
614 ov: &Overlay,
615 ) -> Result<Self, CmfError> {
616 if let Some(dir) = model.path.parent() {
621 crate::gpu::set_cache_dir(dir.to_path_buf());
622 }
623 crate::gpu::graph_unsupported_reset();
626 Self::skill_file_guard(model)?;
627 let skill = match ov {
628 Overlay::One(s) => Some(*s),
629 _ => None,
630 };
631 if let Some(sid) = skill {
632 let known = model.header.skills.iter().any(|s| s.id == sid)
633 || model.skill_tensors(sid).next().is_some();
634 if !known {
635 return Err(CmfError::Parse(format!(
636 "skill '{sid}' not in this container (header.skills: {:?})",
637 model
638 .header
639 .skills
640 .iter()
641 .map(|s| &s.id)
642 .collect::<Vec<_>>()
643 )));
644 }
645 tracing::info!(
646 "skill '{sid}': {} replacement tensors overlaid",
647 model.skill_tensors(sid).count()
648 );
649 }
650 let arch = model.arch().clone();
651 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
652 if let Some(heads) = &arch.attention_heads_per_layer {
653 if heads.len() != arch.num_layers {
654 return Err(CmfError::Parse(format!(
655 "arch.attention_heads_per_layer has {} entries, expected {}",
656 heads.len(),
657 arch.num_layers
658 )));
659 }
660 if let Some((li, &nh)) = heads
661 .iter()
662 .enumerate()
663 .find(|(_, nh)| **nh == 0 || **nh % arch.num_kv_heads != 0)
664 {
665 return Err(CmfError::Parse(format!(
666 "layer {li} has {nh} Q heads, which must be nonzero and divisible by {} KV heads",
667 arch.num_kv_heads
668 )));
669 }
670 }
671 if arch
672 .layer_types
673 .iter()
674 .any(|t| matches!(t, LayerType::SlidingAttention))
675 && arch.sliding_window.is_none()
676 {
677 return Err(CmfError::Parse(
678 "model has SlidingAttention layers but no arch.sliding_window".into(),
679 ));
680 }
681
682 let heads_masked = model.masks.masks.iter().any(|m| {
692 m.head_masks.iter().any(|row| {
693 let mut bits = 0usize;
694 for &b in row.iter() {
695 bits += b.count_ones() as usize;
696 }
697 !row.is_empty() && bits < arch.num_attention_heads
698 })
699 });
700 let force_f32 = heads_masked; let mut tokenizer = if let Some(vocab_bytes) = &model.vocab {
704 Tokenizer::from_bytes(vocab_bytes)
705 .map_err(|e| CmfError::Parse(format!("embedded tokenizer: {e}")))?
706 } else {
707 let sidecar = model.path.with_file_name("tokenizer.json");
708 if sidecar.exists() {
709 Tokenizer::from_file(&sidecar)
710 .map_err(|e| CmfError::Parse(format!("sidecar tokenizer: {e}")))?
711 } else {
712 tracing::warn!("no tokenizer in file or sidecar — using byte-level fallback");
713 Tokenizer::byte_level()
714 }
715 };
716 if let Some(tc) = &model.header.tokenizer_config {
718 tokenizer.chat_template = tc.chat_template.clone();
719 tokenizer.extra_eos.extend(tc.eos_token_ids.iter().copied());
720 if tokenizer.bos_token_id.is_none() {
721 tokenizer.bos_token_id = tc.bos_token_id;
722 }
723 tracing::info!(
724 "chat bundle: template {} chars, {} stop ids",
725 tc.chat_template.as_deref().map(str::len).unwrap_or(0),
726 tc.eos_token_ids.len()
727 );
728 }
729 if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
733 tokenizer.add_bos = true;
734 }
735
736 let embed_tokens = load_matrix(model, "model.embed_tokens.weight", false, ov)?;
738 let final_norm = load_f32(model, "model.norm.weight", ov).map_err(err)?;
739 let lm_head = if model.tensor("lm_head.weight").is_some() {
740 load_matrix(model, "lm_head.weight", false, ov)?
741 } else if arch.tie_word_embeddings {
742 load_matrix(model, "model.embed_tokens.weight", false, ov)?
744 } else {
745 return Err(CmfError::MissingTensor(
746 "lm_head.weight (and tie_word_embeddings is false)".into(),
747 ));
748 };
749
750 let has_linear = arch
752 .layer_types
753 .iter()
754 .any(|t| matches!(t, LayerType::LinearAttention));
755 let mut vmf_cfg = None;
756 let mut gdn_cfg = None;
757 if has_linear {
758 let lc = arch.linear_core.as_ref().ok_or_else(|| {
759 CmfError::Parse(
760 "model has LinearAttention layers but no arch.linear_core — \
761 reconvert with the current converter"
762 .into(),
763 )
764 })?;
765 let need = |v: Option<usize>, name: &str| {
766 v.ok_or_else(|| CmfError::Parse(format!("linear core needs arch.{name}")))
767 };
768 match lc.kind.as_str() {
769 "vmf_phase" => {
770 vmf_cfg = Some(VmfPhaseCfg {
771 num_heads: lc.num_heads,
772 nphase: need(lc.nphase, "linear_core.nphase")?,
773 value_head_dim: lc.value_head_dim,
774 hidden_size: arch.hidden_size,
775 phase_mass: std::env::var("CMF_PHASE_MASS")
778 .ok()
779 .and_then(|v| v.parse().ok())
780 .unwrap_or(0.0),
781 });
782 }
783 "gated_delta_net" => {
784 gdn_cfg = Some(GdnCfg {
785 num_v_heads: lc.num_heads,
786 num_k_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
787 key_head_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
788 value_head_dim: lc.value_head_dim,
789 conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
790 hidden_size: arch.hidden_size,
791 rms_eps: arch.rms_norm_eps,
792 });
793 }
794 other => {
795 return Err(CmfError::Parse(format!(
796 "unknown linear core '{other}' (this runtime executes: \
797 gated_delta_net, vmf_phase)"
798 )));
799 }
800 }
801 }
802
803 let has_kda = arch.layer_types.iter().any(|t| matches!(t, LayerType::Kda));
805 let kda_cfg = if has_kda {
806 let need = |v: Option<usize>, name: &str| {
807 v.ok_or_else(|| CmfError::Parse(format!("KDA core needs arch.{name}")))
808 };
809 Some(crate::linear_core::KdaCfg {
810 num_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
811 head_k_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
812 head_v_dim: need(arch.linear_value_head_dim, "linear_value_head_dim")?,
813 conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
814 hidden_size: arch.hidden_size,
815 rms_eps: arch.rms_norm_eps,
816 })
817 } else {
818 None
819 };
820
821 let has_short_conv = arch
823 .layer_types
824 .iter()
825 .any(|t| matches!(t, LayerType::ShortConv));
826 let short_conv_cfg = if has_short_conv {
827 Some(ShortConvCfg {
828 hidden_size: arch.hidden_size,
829 kernel: arch.linear_conv_kernel_dim.ok_or_else(|| {
830 CmfError::Parse(
831 "model has ShortConv layers but no arch.linear_conv_kernel_dim — \
832 reconvert with the current converter"
833 .into(),
834 )
835 })?,
836 })
837 } else {
838 None
839 };
840
841 let load_full_attn = |prefix: &str, layer: Option<usize>| -> Result<AttnKind, CmfError> {
843 let t = |suffix: &str| load_matrix(model, &format!("{prefix}{suffix}"), force_f32, ov);
844 let n = |suffix: &str| -> Option<Vec<f32>> {
845 model
846 .tensor(&format!("{prefix}{suffix}"))
847 .and_then(|_| load_f32(model, &format!("{prefix}{suffix}"), ov).ok())
848 };
849 if let Some(mla) = arch.mla.as_ref() {
851 let (q_proj, q_a, q_a_norm) = if mla.q_lora_rank.is_some() {
853 (
854 t("self_attn.q_b_proj.weight")?,
855 Some(t("self_attn.q_a_proj.weight")?),
856 Some(n("self_attn.q_a_layernorm.weight").ok_or_else(|| {
857 CmfError::Parse(format!("{prefix}: MLA needs q_a_layernorm"))
858 })?),
859 )
860 } else {
861 (t("self_attn.q_proj.weight")?, None, None)
862 };
863 let hd = mla.qk_rope_head_dim + mla.qk_nope_head_dim;
864 let nh = q_proj.rows() / hd;
865 let mut scale = 1.0 / (hd as f32).sqrt();
868 if let Some(y) = arch.yarn.as_ref() {
869 if let Some(m) = y.mscale_all_dim.filter(|&m| m > 0.0) {
870 let ms = 0.1 * m * y.factor.ln() + 1.0;
871 scale *= ms * ms;
872 }
873 }
874 return Ok(AttnKind::Mla(Box::new(crate::pipeline::MlaWeights {
875 q_proj,
876 q_a,
877 q_a_norm,
878 kv_a: t("self_attn.kv_a_proj_with_mqa.weight")?,
879 kv_a_norm: n("self_attn.kv_a_layernorm.weight").ok_or_else(|| {
880 CmfError::Parse(format!("{prefix}: MLA needs kv_a_layernorm"))
881 })?,
882 kv_b: t("self_attn.kv_b_proj.weight")?,
883 o_proj: t("self_attn.o_proj.weight")?,
884 nh,
885 qk_rope: mla.qk_rope_head_dim,
886 qk_nope: mla.qk_nope_head_dim,
887 v_dim: mla.v_head_dim,
888 lora: mla.kv_lora_rank,
889 scale,
890 nope: mla.nope,
891 })));
892 }
893 let wq = t("self_attn.q_proj.weight")?;
894 let nh = layer
895 .and_then(|li| {
896 arch.attention_heads_per_layer
897 .as_ref()
898 .and_then(|v| v.get(li).copied())
899 })
900 .unwrap_or(arch.num_attention_heads);
901 let output_gate = arch.global_head_dim.is_none() && wq.rows() == 2 * nh * arch.head_dim;
905 let is_global_layer = arch.global_head_dim.is_some()
908 && layer.is_some_and(|li| {
909 arch.sliding_window_pattern
910 .is_some_and(|p| p > 0 && (li + 1) % p == 0)
911 });
912 let expect = if is_global_layer {
913 nh * arch.global_head_dim.unwrap_or(arch.head_dim)
914 } else {
915 nh * arch.head_dim
916 };
917 if !output_gate && wq.rows() != expect {
918 return Err(CmfError::Parse(format!(
919 "{prefix}self_attn.q_proj.weight rows={} != heads({nh}) * head_dim({})",
920 wq.rows(),
921 expect / nh.max(1)
922 )));
923 }
924 let gate_name = format!("{prefix}self_attn.g_proj.weight");
925 let softplus_gate = if model.tensor(&gate_name).is_some() {
926 let gate = load_matrix(model, &gate_name, force_f32, ov)?;
927 if gate.cols() != arch.hidden_size {
928 return Err(CmfError::Parse(format!(
929 "{gate_name} cols={} != hidden_size ({})",
930 gate.cols(),
931 arch.hidden_size
932 )));
933 }
934 let per_head = if gate.rows() == nh {
935 true
936 } else if gate.rows() == nh * arch.head_dim {
937 false
938 } else {
939 return Err(CmfError::Parse(format!(
940 "{gate_name} rows={} must equal heads ({nh}) or heads*head_dim ({})",
941 gate.rows(),
942 nh * arch.head_dim
943 )));
944 };
945 Some((gate, per_head))
946 } else {
947 None
948 };
949 let bias = match (
951 n("self_attn.q_proj.bias"),
952 n("self_attn.k_proj.bias"),
953 n("self_attn.v_proj.bias"),
954 ) {
955 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
956 _ => None,
957 };
958 Ok(AttnKind::Full {
959 wq,
960 wk: t("self_attn.k_proj.weight")?,
961 wv: t("self_attn.v_proj.weight")?,
962 wo: t("self_attn.o_proj.weight")?,
963 q_norm: n("self_attn.q_norm.weight"),
964 k_norm: n("self_attn.k_norm.weight"),
965 output_gate,
966 softplus_gate,
967 bias,
968 })
969 };
970
971 let load_linear_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
972 if gdn_cfg.is_some() {
973 let t = |suffix: &str| {
975 load_matrix(
976 model,
977 &format!("{prefix}linear_attn.{suffix}"),
978 force_f32,
979 ov,
980 )
981 };
982 let f = |suffix: &str| {
983 load_f32(model, &format!("{prefix}linear_attn.{suffix}"), ov).map_err(err)
984 };
985 return Ok(AttnKind::LinearGdn(GdnWeights {
986 in_proj_qkv: t("in_proj_qkv.weight")?,
987 in_proj_z: t("in_proj_z.weight")?,
988 in_proj_a: t("in_proj_a.weight")?,
989 in_proj_b: t("in_proj_b.weight")?,
990 conv1d: f("conv1d.weight")?,
991 a_log: f("A_log")?,
992 dt_bias: f("dt_bias")?,
993 norm: f("norm.weight")?,
994 out_proj: t("out_proj.weight")?,
995 }));
996 }
997 let t = |suffix: &str| {
998 load_matrix(model, &format!("{prefix}vmf_attn.{suffix}"), force_f32, ov)
999 };
1000 let a_log = load_f32(model, &format!("{prefix}vmf_attn.A_log"), ov).map_err(err)?;
1001 let k_gate = if model
1005 .tensor(&format!("{prefix}vmf_attn.k_gate.weight"))
1006 .is_some()
1007 {
1008 Some((
1009 t("k_gate.weight")?,
1010 load_f32(model, &format!("{prefix}vmf_attn.k_gate.bias"), ov).map_err(err)?,
1011 ))
1012 } else {
1013 None
1014 };
1015 Ok(AttnKind::Linear(VmfPhaseWeights {
1016 thq: t("thq.weight")?,
1017 thk: t("thk.weight")?,
1018 v_proj: t("v_proj.weight")?,
1019 out_proj: t("out_proj.weight")?,
1020 decay: a_log.iter().map(|&a| (-(a as f64).exp()).exp()).collect(),
1021 k_gate,
1022 }))
1023 };
1024
1025 let load_short_conv = |prefix: &str| -> Result<AttnKind, CmfError> {
1029 let t = |suffix: &str| {
1030 load_matrix(
1031 model,
1032 &format!("{prefix}short_conv.{suffix}"),
1033 force_f32,
1034 ov,
1035 )
1036 };
1037 Ok(AttnKind::ShortConv(ShortConvWeights {
1038 in_proj: t("in_proj.weight")?,
1039 conv: load_f32(model, &format!("{prefix}short_conv.conv.weight"), ov)
1040 .map_err(err)?,
1041 out_proj: t("out_proj.weight")?,
1042 }))
1043 };
1044
1045 let load_kda = |prefix: &str| -> Result<AttnKind, CmfError> {
1049 let t = |suffix: &str| {
1050 load_matrix(model, &format!("{prefix}kda_attn.{suffix}"), force_f32, ov)
1051 };
1052 let f = |suffix: &str| {
1053 load_f32(model, &format!("{prefix}kda_attn.{suffix}"), ov).map_err(err)
1054 };
1055 let gate = if model
1056 .tensor(&format!("{prefix}kda_attn.g_proj.weight"))
1057 .is_some()
1058 {
1059 crate::linear_core::KdaOutGate::Full(t("g_proj.weight")?)
1060 } else {
1061 crate::linear_core::KdaOutGate::LowRank(
1062 t("g_a_proj.weight")?,
1063 t("g_b_proj.weight")?,
1064 )
1065 };
1066 Ok(AttnKind::Kda(Box::new(crate::linear_core::KdaWeights {
1067 q_proj: t("q_proj.weight")?,
1068 k_proj: t("k_proj.weight")?,
1069 v_proj: t("v_proj.weight")?,
1070 conv_q: f("q_conv1d.weight")?,
1071 conv_k: f("k_conv1d.weight")?,
1072 conv_v: f("v_conv1d.weight")?,
1073 f_a: t("f_a_proj.weight")?,
1074 f_b: t("f_b_proj.weight")?,
1075 dt_bias: f("dt_bias")?,
1076 a_log: f("A_log")?,
1077 b_proj: t("b_proj.weight")?,
1078 gate,
1079 o_norm: f("o_norm.weight")?,
1080 o_proj: t("o_proj.weight")?,
1081 gate_lower_bound: arch.kda_gate_lower_bound.map(|v| v as f32),
1082 })))
1083 };
1084
1085 fn anyhow_like(ok: bool) -> Result<(), ()> {
1086 if ok { Ok(()) } else { Err(()) }
1087 }
1088 let mut layers = Vec::with_capacity(arch.num_layers);
1089 let is_g3n = arch.g3n.is_some();
1090 let owns_its_layers = is_g3n || arch.arch_name == "deepseek_v4";
1095 for li in 0..(if owns_its_layers { 0 } else { arch.num_layers }) {
1096 let prefix = format!("model.layers.{li}.");
1097 let attn = match arch.layer_types.get(li) {
1098 Some(LayerType::LinearAttention) => load_linear_attn(&prefix)?,
1099 Some(LayerType::Kda) => load_kda(&prefix)?,
1100 Some(LayerType::ShortConv) => load_short_conv(&prefix)?,
1101 _ => load_full_attn(&prefix, Some(li))?,
1102 };
1103 let pre_ffn = format!("{prefix}pre_feedforward_layernorm.weight");
1107 let sandwich = model.tensor(&pre_ffn).is_some();
1108 layers.push(LayerWeights {
1109 input_norm: load_f32(model, &format!("{prefix}input_layernorm.weight"), ov)
1110 .map_err(err)?,
1111 post_norm: if sandwich {
1112 load_f32(model, &pre_ffn, ov).map_err(err)?
1113 } else {
1114 load_f32(
1115 model,
1116 &format!("{prefix}post_attention_layernorm.weight"),
1117 ov,
1118 )
1119 .map_err(err)?
1120 },
1121 attn_out_norm: if sandwich {
1122 Some(
1123 load_f32(
1124 model,
1125 &format!("{prefix}post_attention_layernorm.weight"),
1126 ov,
1127 )
1128 .map_err(err)?,
1129 )
1130 } else {
1131 None
1132 },
1133 ffn_out_norm: if sandwich {
1134 Some(
1135 load_f32(
1136 model,
1137 &format!("{prefix}post_feedforward_layernorm.weight"),
1138 ov,
1139 )
1140 .map_err(err)?,
1141 )
1142 } else {
1143 None
1144 },
1145 layer_scale: model
1147 .tensor(&format!("{prefix}layer_scalar"))
1148 .and_then(|_| {
1149 load_f32(model, &format!("{prefix}layer_scalar"), ov)
1150 .ok()
1151 .and_then(|v| v.first().copied())
1152 }),
1153 ffn: build_layer_ffn(model, &arch, li, false, ov)?,
1155 attn,
1156 });
1157 }
1158
1159 let mtp_present = model
1168 .tensor("model.mtp.layers.0.self_attn.q_proj.weight")
1169 .is_some()
1170 || model.tensor("model.mtp.eh_proj.weight").is_some();
1171 let dsv4_mtp = model.tensor("model.mtp.0.main_proj.weight").is_some();
1176 if arch.mtp.is_some() && !mtp_present && !dsv4_mtp {
1177 tracing::info!(
1178 "header declares an MTP head but the file carries none — \
1179 loading without it"
1180 );
1181 }
1182 let mtp = if let Some(cfg) = arch.mtp.as_ref().filter(|_| mtp_present) {
1183 if cfg.num_layers != 1 {
1184 return Err(CmfError::Parse(format!(
1185 "MTP with {} blocks not supported yet (only 1)",
1186 cfg.num_layers
1187 )));
1188 }
1189 let p = "model.mtp.";
1190 let attn = load_full_attn("model.mtp.layers.0.", None)?;
1191 Some(MtpModule {
1192 enorm: load_f32(model, &format!("{p}enorm.weight"), ov).map_err(err)?,
1193 hnorm: load_f32(model, &format!("{p}hnorm.weight"), ov).map_err(err)?,
1194 eh_proj: load_matrix(model, &format!("{p}eh_proj.weight"), false, ov)?,
1195 layer: LayerWeights {
1196 attn_out_norm: None,
1197 ffn_out_norm: None,
1198 layer_scale: None,
1199 input_norm: load_f32(model, &format!("{p}layers.0.input_layernorm.weight"), ov)
1200 .map_err(err)?,
1201 post_norm: load_f32(
1202 model,
1203 &format!("{p}layers.0.post_attention_layernorm.weight"),
1204 ov,
1205 )
1206 .map_err(err)?,
1207 ffn: build_ffn_at(model, &arch, &format!("{p}layers.0."), false, ov)?,
1211 attn,
1212 },
1213 final_norm: load_f32(model, &format!("{p}norm.weight"), ov).map_err(err)?,
1214 kv: LayerKvCache::new(arch.num_kv_heads, arch.head_dim),
1215 })
1216 } else {
1217 None
1218 };
1219
1220 tracing::info!(
1221 "Pipeline loaded: {} | {}L ({} linear) | {:.2}B params | storage: {} | MTP: {}",
1222 arch.arch_name,
1223 arch.num_layers,
1224 arch.layer_types
1225 .iter()
1226 .filter(|t| matches!(t, LayerType::LinearAttention))
1227 .count(),
1228 model.total_param_count() as f64 / 1e9,
1229 if force_f32 {
1230 "f32 (masked)"
1231 } else {
1232 "quantized mmap"
1233 },
1234 if mtp.is_some() { "yes" } else { "no" }
1235 );
1236
1237 let cap = std::env::var("CMF_MAX_SEQ")
1248 .ok()
1249 .and_then(|v| v.parse::<usize>().ok())
1250 .unwrap_or(32_768);
1251 let max_seq_len = arch.max_position_embeddings.min(cap);
1252
1253 let total_layers = arch.num_layers * arch.num_loops;
1255
1256 let mut pipeline = Pipeline::new(
1257 tokenizer,
1258 PipelineWeights {
1259 embed_tokens,
1260 layers,
1261 lm_head,
1262 final_norm,
1263 },
1264 arch.hidden_size,
1265 arch.intermediate_size,
1266 arch.num_attention_heads,
1267 arch.num_kv_heads,
1268 arch.head_dim,
1269 total_layers,
1270 arch.num_layers, arch.loop_final_norm,
1272 arch.vocab_size,
1273 arch.rms_norm_eps,
1274 arch.rope_theta as f32,
1275 arch.norm_style,
1276 max_seq_len,
1277 sampler_config,
1278 );
1279 let rotary = ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2);
1280 pipeline.set_rotary(rotary, arch.rope_theta as f32);
1281 pipeline.attention_heads_per_layer = arch.attention_heads_per_layer.clone();
1282 if let Some(yarn) = &arch.yarn {
1283 pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
1284 rotary,
1285 arch.rope_theta as f32,
1286 yarn.factor,
1287 yarn.original_max_position_embeddings,
1288 yarn.beta_fast,
1289 yarn.beta_slow,
1290 ));
1291 pipeline.rope_scale = yarn.attention_factor;
1292 }
1293 pipeline.embed_multiplier = arch.embed_multiplier;
1297 pipeline.logit_multiplier = arch.logit_multiplier;
1298 if let Some(qpas) = arch.query_pre_attn_scalar {
1299 pipeline.attn_scale = 1.0 / (qpas as f32).sqrt();
1300 }
1301 if let (Some(w), Some(p)) = (arch.sliding_window, arch.sliding_window_pattern) {
1302 pipeline.swa = Some((w, p));
1303 if let Some(base) = arch.rope_local_base_freq {
1304 pipeline.inv_freq_local = Some(std::sync::Arc::new(
1305 crate::attention::rope_inv_freq(rotary, base as f32),
1306 ));
1307 }
1308 }
1309 let explicit_sliding: Vec<bool> = arch
1310 .layer_types
1311 .iter()
1312 .map(|t| matches!(t, cortiq_core::LayerType::SlidingAttention))
1313 .collect();
1314 if explicit_sliding.iter().any(|&v| v) {
1315 pipeline.sliding_layers = Some(explicit_sliding);
1316 if let Some(w) = arch.sliding_window {
1317 pipeline.swa = Some((w, usize::MAX));
1318 }
1319 let local_rotary = ((arch.head_dim as f32
1320 * arch
1321 .local_partial_rotary_factor
1322 .unwrap_or(arch.partial_rotary_factor))
1323 as usize)
1324 .max(2);
1325 pipeline.rotary_dim_local = Some(local_rotary);
1326 if let Some(base) = arch.rope_local_base_freq {
1327 pipeline.inv_freq_local = Some(std::sync::Arc::new(
1328 crate::attention::rope_inv_freq(local_rotary, base as f32),
1329 ));
1330 }
1331 }
1332 if let (Some(ghd), Some(gkv)) = (arch.global_head_dim, arch.num_global_kv_heads) {
1336 pipeline.global_attn = Some((ghd, gkv));
1337 let prf = arch.global_partial_rotary_factor.unwrap_or(1.0);
1338 let half = ghd / 2;
1339 let ra = (((prf * ghd as f32) as usize) / 2).min(half);
1340 let mut f = vec![0.0f32; half];
1341 for (i, slot) in f.iter_mut().enumerate().take(ra) {
1342 *slot = 1.0 / (arch.rope_theta as f32).powf(2.0 * i as f32 / ghd as f32);
1343 }
1344 pipeline.inv_freq_global = Some(std::sync::Arc::new(f));
1345 let global_at = |li: usize| -> bool {
1350 match &pipeline.sliding_layers {
1351 Some(map) => !map.get(li).copied().unwrap_or(false),
1352 None => pipeline
1353 .swa
1354 .map(|(_, p)| p > 0 && p != usize::MAX && (li + 1) % p == 0)
1355 .unwrap_or(false),
1356 }
1357 };
1358 for li in 0..arch.num_layers {
1359 if global_at(li) {
1360 pipeline.kv_cache.layers[li] = crate::kv_cache::LayerKvCache::new(gkv, ghd);
1361 }
1362 }
1363 }
1364 if let Some(mla) = arch.mla.as_ref() {
1367 let hd = mla.qk_rope_head_dim + mla.qk_nope_head_dim;
1368 pipeline.head_dim = hd;
1369 pipeline.num_kv_heads = arch.num_attention_heads;
1370 pipeline.rotary_dim = mla.qk_rope_head_dim;
1371 let half = mla.qk_rope_head_dim / 2;
1372 let mut f = vec![0.0f32; half];
1373 for (i, slot) in f.iter_mut().enumerate() {
1374 *slot = 1.0
1375 / (arch.rope_theta as f32).powf(2.0 * i as f32 / mla.qk_rope_head_dim as f32);
1376 }
1377 pipeline.inv_freq = std::sync::Arc::new(f);
1378 for li in 0..arch.num_layers {
1379 pipeline.kv_cache.layers[li] =
1380 crate::kv_cache::LayerKvCache::new(arch.num_attention_heads, hd);
1381 }
1382 }
1383 if let Some(fac) = &arch.rope_freq_factors {
1387 let mut f = pipeline.inv_freq.as_ref().clone();
1388 for (i, v) in f.iter_mut().enumerate() {
1389 if let Some(&d) = fac.get(i) {
1390 *v /= d as f32;
1391 }
1392 }
1393 pipeline.inv_freq = std::sync::Arc::new(f);
1394 }
1395 pipeline.attn_v_norm = arch.attn_v_norm;
1396 pipeline.final_softcap = arch.final_logit_softcapping.map(|c| c as f32);
1397 if let Some(ncl) = arch.head_clusters {
1399 let cm = load_f32(model, "lm_head.clusters.weight", ov).map_err(err)?;
1400 if cm.len() != ncl * arch.hidden_size {
1401 return Err(CmfError::Parse(format!(
1402 "lm_head.clusters.weight: {} != {ncl}×{}",
1403 cm.len(),
1404 arch.hidden_size
1405 )));
1406 }
1407 pipeline.head_clusters = Some(std::sync::Arc::new(cm));
1408 }
1409 pipeline.attn_softcap = arch.attn_logit_softcapping.unwrap_or(0.0) as f32;
1410 pipeline.vmf_cfg = vmf_cfg;
1411 pipeline.gdn_cfg = gdn_cfg;
1412 pipeline.kda_cfg = kda_cfg;
1413 if let Some(gc) = arch.g3n.as_ref() {
1414 use crate::g3n::{G3nAltUp, G3nGlobals, G3nLaurel, G3nLayer};
1415 anyhow_like(gc.altup_num_inputs == crate::g3n::ALTUP_N).map_err(|_| {
1416 CmfError::Parse(format!(
1417 "g3n: altup_num_inputs {} != supported {}",
1418 gc.altup_num_inputs,
1419 crate::g3n::ALTUP_N
1420 ))
1421 })?;
1422 let t = |name: &str| load_matrix(model, name, force_f32, ov);
1423 let f = |name: &str| load_f32(model, name, ov).map_err(err);
1424 let mut altup_proj = Vec::new();
1425 let mut altup_unembed = Vec::new();
1426 for i in 0..crate::g3n::ALTUP_N - 1 {
1427 altup_proj.push(t(&format!("model.altup_projections.{i}.weight"))?);
1428 altup_unembed.push(t(&format!("model.altup_unembed_projections.{i}.weight"))?);
1429 }
1430 let first_shared = arch.num_layers.saturating_sub(gc.num_kv_shared_layers);
1431 let sliding_of = |li: usize| {
1432 matches!(
1433 arch.layer_types.get(li),
1434 Some(cortiq_core::LayerType::SlidingAttention)
1435 )
1436 };
1437 let mut g3n_layers = Vec::with_capacity(arch.num_layers);
1438 for li in 0..arch.num_layers {
1439 let pfx = format!("model.layers.{li}.");
1440 let shared = li >= first_shared && first_shared > 0;
1441 let share_src = if shared {
1442 let want = sliding_of(li);
1443 (0..first_shared).rev().find(|&j| sliding_of(j) == want)
1444 } else {
1445 None
1446 };
1447 g3n_layers.push(G3nLayer {
1448 altup: G3nAltUp {
1449 router_norm: f(&format!("{pfx}altup.router_norm.weight"))?,
1450 modality_router: t(&format!("{pfx}altup.modality_router.weight"))?,
1451 prediction_coefs: t(&format!("{pfx}altup.prediction_coefs.weight"))?,
1452 correction_coefs: t(&format!("{pfx}altup.correction_coefs.weight"))?,
1453 correct_output_scale: f(&format!("{pfx}altup.correct_output_scale"))?,
1454 },
1455 laurel: G3nLaurel {
1456 left: t(&format!("{pfx}laurel.linear_left.weight"))?,
1457 right: t(&format!("{pfx}laurel.linear_right.weight"))?,
1458 post_norm: f(&format!("{pfx}laurel.post_laurel_norm.weight"))?,
1459 },
1460 input_norm: f(&format!("{pfx}input_layernorm.weight"))?,
1461 post_attn_norm: f(&format!("{pfx}post_attention_layernorm.weight"))?,
1462 pre_ffw_norm: f(&format!("{pfx}pre_feedforward_layernorm.weight"))?,
1463 post_ffw_norm: f(&format!("{pfx}post_feedforward_layernorm.weight"))?,
1464 wq: t(&format!("{pfx}self_attn.q_proj.weight"))?,
1465 wk: if shared {
1466 None
1467 } else {
1468 Some(t(&format!("{pfx}self_attn.k_proj.weight"))?)
1469 },
1470 wv: if shared {
1471 None
1472 } else {
1473 Some(t(&format!("{pfx}self_attn.v_proj.weight"))?)
1474 },
1475 wo: t(&format!("{pfx}self_attn.o_proj.weight"))?,
1476 q_norm: f(&format!("{pfx}self_attn.q_norm.weight"))?,
1477 k_norm: if shared {
1478 None
1479 } else {
1480 Some(f(&format!("{pfx}self_attn.k_norm.weight"))?)
1481 },
1482 kv_share_src: share_src,
1483 sliding: sliding_of(li),
1484 gate: t(&format!("{pfx}mlp.gate_proj.weight"))?,
1485 up: t(&format!("{pfx}mlp.up_proj.weight"))?,
1486 down: t(&format!("{pfx}mlp.down_proj.weight"))?,
1487 sparsity: gc.activation_sparsity.get(li).copied().unwrap_or(0.0),
1488 ple_gate: t(&format!("{pfx}per_layer_input_gate.weight"))?,
1489 ple_proj: t(&format!("{pfx}per_layer_projection.weight"))?,
1490 post_ple_norm: f(&format!("{pfx}post_per_layer_input_norm.weight"))?,
1491 });
1492 }
1493 let hd = arch.head_dim;
1494 let globals = G3nGlobals {
1495 altup_proj,
1496 altup_unembed,
1497 ple_embed: t("model.embed_tokens_per_layer.weight")?,
1498 ple_model_proj: t("model.per_layer_model_projection.weight")?,
1499 ple_norm: f("model.per_layer_projection_norm.weight")?,
1500 ple_vocab: gc.ple_vocab,
1501 ple_dim: gc.ple_dim,
1502 num_layers: arch.num_layers,
1503 hidden: arch.hidden_size,
1504 rms_eps: arch.rms_norm_eps,
1505 inv_freq_local: crate::attention::rope_inv_freq(
1506 hd,
1507 arch.rope_local_base_freq.unwrap_or(10_000.0) as f32,
1508 ),
1509 inv_freq_global: crate::attention::rope_inv_freq(hd, arch.rope_theta as f32),
1510 window: arch.sliding_window.unwrap_or(512),
1511 };
1512 pipeline.g3n = Some(Box::new((globals, g3n_layers)));
1513 }
1514 if arch.arch_name == "deepseek_v4" {
1519 let moe = arch
1520 .moe
1521 .as_ref()
1522 .ok_or_else(|| CmfError::Parse("deepseek_v4: no moe config".into()))?;
1523 let cfg = crate::dsv4::Dsv4Cfg {
1524 dim: arch.hidden_size,
1525 n_heads: arch.num_attention_heads,
1526 head_dim: arch.head_dim,
1527 rope_head_dim: if arch.partial_rotary_factor < 1.0 {
1534 (((arch.head_dim as f32 * arch.partial_rotary_factor) as usize) & !1)
1535 .clamp(2, arch.head_dim)
1536 } else {
1537 64.min(arch.head_dim)
1538 },
1539 q_lora_rank: 0,
1543 o_lora_rank: 0,
1544 o_groups: 8,
1551 hc_mult: 4,
1552 hc_sinkhorn_iters: 20,
1553 hc_eps: 1e-6,
1554 norm_eps: arch.rms_norm_eps as f32,
1555 n_routed_experts: moe.num_experts,
1556 top_k: moe.top_k,
1557 moe_inter: moe.moe_intermediate_size,
1558 route_scale: moe.routed_scaling_factor.unwrap_or(1.0),
1559 swiglu_limit: 10.0,
1565 window: arch.sliding_window.unwrap_or(128),
1566 index_topk: 512,
1567 vocab: arch.vocab_size,
1568 };
1569 let (g, dl) = crate::dsv4::load(model, &cfg, arch.num_layers)
1570 .map_err(|e| CmfError::Parse(format!("deepseek_v4: {e}")))?;
1571 let mut cfg = cfg;
1576 if let Some(l0) = dl.first() {
1577 cfg.q_lora_rank = l0.wq_a.rows();
1578 let attn_width = arch.num_attention_heads * arch.head_dim;
1579 if l0.wo_a.cols() > 0 && attn_width % l0.wo_a.cols() == 0 {
1580 cfg.o_groups = (attn_width / l0.wo_a.cols()).max(1);
1581 }
1582 cfg.o_lora_rank = l0.wo_b.cols() / cfg.o_groups.max(1);
1583 cfg.hc_mult = (l0.hc_attn_fn.len() / l0.hc_attn_base.len().max(1)) / cfg.dim.max(1);
1584 if cfg.hc_mult == 0 {
1585 cfg.hc_mult = 4;
1586 }
1587 }
1588 let (yf, yo, ybf, ybs) = match &arch.yarn {
1601 Some(y) => (
1602 y.factor,
1603 y.original_max_position_embeddings,
1604 y.beta_fast,
1605 y.beta_slow,
1606 ),
1607 None => {
1608 tracing::warn!(
1609 "deepseek_v4: the header carries no YaRN profile — \
1610 falling back to the release's (factor 16, original \
1611 65536, beta 32/1). Re-converting with a build that \
1612 reads rope_scaling.type would make this exact."
1613 );
1614 (16.0, 65536, 32.0, 1.0)
1615 }
1616 };
1617 pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
1618 cfg.rope_head_dim,
1619 arch.rope_theta as f32,
1620 yf,
1621 yo,
1622 ybf,
1623 ybs,
1624 ));
1625 if let Ok(stats) = std::env::var("CMF_MOE_PIN") {
1630 let cover = std::env::var("CMF_MOE_PIN_COVER")
1631 .ok()
1632 .and_then(|v| v.parse::<f64>().ok())
1633 .filter(|&c| c > 0.0 && c <= 1.0)
1634 .unwrap_or(0.95);
1635 let hot = crate::pin::hot_experts(&stats, cover);
1636 let mut names: Vec<String> = Vec::new();
1637 for e in &model.tensors {
1638 let is_expert = e.name.contains(".mlp.experts.");
1639 if !is_expert {
1640 names.push(e.name.clone()); }
1642 }
1643 let mut kept_experts = 0usize;
1644 if let Some(hot) = &hot {
1645 for (li, experts) in hot {
1646 for e in experts {
1647 for w in ["gate_proj", "up_proj", "down_proj"] {
1648 names.push(format!("model.layers.{li}.mlp.experts.{e}.{w}.weight"));
1649 }
1650 kept_experts += 1;
1651 }
1652 }
1653 }
1654 let r = crate::pin::pin_tensors(model, &names);
1655 tracing::info!(
1656 "закреплено {:.1} ГБ ({} тензоров, горячих экспертов {kept_experts}, покрытие {cover}); лимит {}",
1657 r.bytes as f64 / 1e9,
1658 r.tensors,
1659 r.limit
1660 .map(|l| format!("{:.1} ГБ", l as f64 / 1e9))
1661 .unwrap_or_else(|| "неизвестен".into())
1662 );
1663 if r.skipped > 0 {
1664 tracing::warn!("не закреплено тензоров: {}", r.skipped);
1665 }
1666 }
1667 let st = crate::dsv4::Dsv4State::new(arch.num_layers);
1668 let depth = std::env::var("CMF_DSV4_MTP_DEPTH")
1672 .ok()
1673 .and_then(|v| v.parse::<usize>().ok())
1674 .unwrap_or(3);
1675 pipeline.dsv4_mtp = crate::dsv4::load_mtp(model, &cfg, depth);
1676 crate::dsv4::dspark_reserve_note(&pipeline.dsv4_mtp, &cfg, &dl);
1678 pipeline.dsv4 = Some(Box::new((g, dl, cfg, st)));
1679 }
1680 pipeline.short_conv_cfg = short_conv_cfg;
1681 pipeline.mtp = mtp;
1682 pipeline.install_dynamic_routing(model, false);
1683 match ov {
1687 Overlay::One(sid) => {
1688 pipeline.dyn_active = model.header.skills.iter().position(|s| &s.id == sid);
1689 }
1690 Overlay::Blend(_) => pipeline.dyn_blend_loaded = true,
1691 Overlay::None => {}
1692 }
1693 if let Some(c) = &model.header.calibration {
1696 pipeline.set_calib_temp(c.temperature);
1697 }
1698 let o1 = match crate::nystrom::o1_from_env() {
1704 crate::nystrom::O1Env::Off => None,
1705 crate::nystrom::O1Env::On(cfg) => Some(cfg),
1706 crate::nystrom::O1Env::Unset => model
1707 .header
1708 .provenance
1709 .as_ref()
1710 .and_then(|p| p.get("o1_attn"))
1711 .and_then(crate::nystrom::O1Cfg::from_json),
1712 };
1713 if o1.is_some() {
1714 if pipeline.attn_softcap > 0.0 {
1715 return Err(CmfError::Parse(
1716 "--o1 with attention-logit soft-capping (Gemma-2) is not supported: \
1717 the streaming operator has no capped-score form"
1718 .into(),
1719 ));
1720 }
1721 pipeline.set_o1(o1);
1722 }
1723 Ok(pipeline)
1724 }
1725
1726 pub(crate) fn install_dynamic_routing(&mut self, model: &Arc<CmfModel>, force_f32: bool) {
1731 self.model = Some(model.clone());
1732 self.dyn_force_f32 = force_f32;
1733 let mut per_skill = Vec::with_capacity(model.header.skills.len());
1734 for sk in &model.header.skills {
1735 let mut ffn_layers = std::collections::BTreeSet::new();
1736 let mut non_ffn = false;
1737 let prefix = format!("skill.{}.", sk.id);
1738 for t in model.skill_tensors(&sk.id) {
1739 let rel = &t.name[prefix.len()..]; let toks: Vec<&str> = rel.split('.').collect();
1741 if toks.len() >= 5 && toks[0] == "model" && toks[1] == "layers" && toks[3] == "mlp"
1742 {
1743 if let Ok(li) = toks[2].parse::<usize>() {
1744 ffn_layers.insert(li);
1745 continue;
1746 }
1747 }
1748 non_ffn = true; }
1750 if non_ffn {
1751 tracing::warn!(
1752 "skill '{}' replaces non-FFN tensors — excluded from dynamic \
1753 routing (static overlay still works)",
1754 sk.id
1755 );
1756 per_skill.push(None);
1757 } else {
1758 per_skill.push(Some(ffn_layers.into_iter().collect::<Vec<_>>()));
1759 }
1760 }
1761 self.dyn_skill_layers = per_skill;
1762 }
1763
1764 pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
1771 self.kv_cache.clear();
1773 self.kv_history.clear();
1774 if self.dyn_active == idx {
1775 return Ok(());
1776 }
1777 let model = self.model.clone().ok_or_else(|| {
1778 CmfError::Parse("dynamic routing needs a model-backed pipeline".into())
1779 })?;
1780 let mut union: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
1781 if let Some(old) = self.dyn_active {
1782 if let Some(Some(ls)) = self.dyn_skill_layers.get(old) {
1783 union.extend(ls.iter().copied());
1784 }
1785 }
1786 let new_id: Option<String> = match idx {
1787 Some(n) => match self.dyn_skill_layers.get(n) {
1788 Some(Some(ls)) => {
1789 union.extend(ls.iter().copied());
1790 Some(model.header.skills[n].id.clone())
1791 }
1792 _ => {
1793 return Err(CmfError::Parse(format!(
1794 "skill index {n} not dynamic-eligible"
1795 )));
1796 }
1797 },
1798 None => None,
1799 };
1800 let ov = match &new_id {
1801 Some(s) => Overlay::One(s),
1802 None => Overlay::None,
1803 };
1804 let arch = model.arch();
1805 for li in union {
1806 self.weights.layers[li].ffn =
1807 build_layer_ffn(&model, arch, li, self.dyn_force_f32, &ov)?;
1808 }
1809 self.dyn_active = idx;
1810 Ok(())
1811 }
1812}