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
87fn 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 let prefix = format!("model.layers.{li}.");
120 let load_dense = |p: &str| -> Result<DenseFfn, CmfError> {
121 let gate_proj = load_matrix(model, &format!("{p}gate_proj.weight"), force_f32, ov)?;
122 let up_proj = load_matrix(model, &format!("{p}up_proj.weight"), force_f32, ov)?;
123 let down_proj = load_matrix(model, &format!("{p}down_proj.weight"), force_f32, ov)?;
124 let inter = gate_proj.rows();
128 if up_proj.rows() != inter || down_proj.cols() != inter {
129 return Err(CmfError::Parse(format!(
130 "{p}: FFN dims disagree (gate.rows={inter}, up.rows={}, \
131 down.cols={}); all three must equal inter'",
132 up_proj.rows(),
133 down_proj.cols()
134 )));
135 }
136 if down_proj.rows() != arch.hidden_size {
137 return Err(CmfError::Parse(format!(
138 "{p}: down_proj.rows={} != hidden_size={}",
139 down_proj.rows(),
140 arch.hidden_size
141 )));
142 }
143 Ok(DenseFfn {
144 gate_proj,
145 up_proj,
146 down_proj,
147 act: crate::pipeline::Act::from_arch(&arch.hidden_act),
148 })
149 };
150 let router_name = format!("{prefix}mlp.gate.weight");
151 if model.tensor(&router_name).is_none() {
152 return Ok(FfnKind::Dense(load_dense(&format!("{prefix}mlp."))?));
153 }
154 let cfg = arch.moe.as_ref().ok_or_else(|| {
155 CmfError::Parse(format!(
156 "{router_name} present but header has no arch.moe block"
157 ))
158 })?;
159 let mut experts = Vec::new();
164 for e in 0..cfg.num_experts {
165 if model
166 .tensor(&format!("{prefix}mlp.experts.{e}.gate_proj.weight"))
167 .is_none()
168 {
169 break;
170 }
171 experts.push(load_dense(&format!("{prefix}mlp.experts.{e}."))?);
172 }
173 if experts.is_empty() {
174 return Err(CmfError::Parse(format!(
175 "{prefix}: router present but no expert tensors"
176 )));
177 }
178 let shared = if model
179 .tensor(&format!("{prefix}mlp.shared_expert.gate_proj.weight"))
180 .is_some()
181 {
182 let gate_name = format!("{prefix}mlp.shared_expert_gate.weight");
183 Some((
184 load_dense(&format!("{prefix}mlp.shared_expert."))?,
185 if model.tensor(&gate_name).is_some() {
186 Some(load_matrix(model, &gate_name, force_f32, ov)?)
187 } else {
188 None
189 },
190 ))
191 } else {
192 None
193 };
194 let bias_name = format!("{prefix}mlp.expert_bias");
197 let expert_bias = if model.tensor(&bias_name).is_some() {
198 Some(load_f32(model, &bias_name, ov).map_err(CmfError::Parse)?)
199 } else {
200 None
201 };
202 let top_k = std::env::var("CMF_MOE_TOPK")
208 .ok()
209 .and_then(|v| v.parse::<usize>().ok())
210 .filter(|&k| k >= 1 && k <= cfg.top_k)
211 .inspect(|k| tracing::info!("MoE top_k override: {} (header {})", k, cfg.top_k))
212 .unwrap_or(cfg.top_k);
213 let route_tau = std::env::var("CMF_MOE_TAU")
215 .ok()
216 .and_then(|v| v.parse::<f32>().ok())
217 .filter(|&t| t > 0.0 && t < 1.0)
218 .inspect(|t| tracing::info!("MoE adaptive routing: tau {t}"));
219 let mask = moe_task_mask(&prefix, experts.len());
220 let router = load_matrix(model, &router_name, force_f32, ov)?;
221 if router.rows() != experts.len() {
222 return Err(CmfError::Parse(format!(
223 "{router_name}: {} rows != {} experts",
224 router.rows(),
225 experts.len()
226 )));
227 }
228 let top_k = top_k.min(experts.len());
229 Ok(FfnKind::Moe(MoeFfn {
230 router,
231 experts,
232 top_k,
233 route_tau,
234 norm_topk_prob: cfg.norm_topk_prob,
235 router_sigmoid: cfg.router_sigmoid,
236 expert_bias,
237 routed_scaling: cfg.routed_scaling_factor.unwrap_or(1.0),
238 shared,
239 stats: std::cell::RefCell::new(Vec::new()),
240 mask,
241 }))
242}
243
244fn moe_task_mask(prefix: &str, ne: usize) -> Option<Vec<bool>> {
252 use std::sync::OnceLock;
253 static CFG: OnceLock<Option<(std::collections::HashMap<usize, Vec<u64>>, f64)>> =
254 OnceLock::new();
255 let cfg = CFG.get_or_init(|| {
256 let path = std::env::var("CMF_MOE_MASK").ok()?;
257 let cover = std::env::var("CMF_MOE_MASK_COVER")
258 .ok()
259 .and_then(|v| v.parse::<f64>().ok())
260 .filter(|&c| c > 0.0 && c <= 1.0)
261 .unwrap_or(0.9);
262 let text = std::fs::read_to_string(&path)
263 .map_err(|e| tracing::warn!("CMF_MOE_MASK: cannot read {path}: {e}"))
264 .ok()?;
265 let map: std::collections::HashMap<String, Vec<u64>> =
266 serde_json::from_str(&text)
267 .map_err(|e| tracing::warn!("CMF_MOE_MASK: bad JSON in {path}: {e}"))
268 .ok()?;
269 tracing::info!("MoE task mask: {path}, cover {cover}");
270 Some((
271 map.into_iter()
272 .filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
273 .collect(),
274 cover,
275 ))
276 });
277 let (stats, cover) = cfg.as_ref()?;
278 let li: usize = prefix
280 .split("layers.")
281 .nth(1)?
282 .split('.')
283 .next()?
284 .parse()
285 .ok()?;
286 let counts = stats.get(&li)?;
287 if counts.len() != ne {
288 tracing::warn!("CMF_MOE_MASK: layer {li} has {} counts, model has {ne} experts — skipped", counts.len());
289 return None;
290 }
291 let total: u64 = counts.iter().sum();
292 if total == 0 {
293 return None;
294 }
295 let mut order: Vec<usize> = (0..ne).collect();
296 order.sort_unstable_by_key(|&e| std::cmp::Reverse(counts[e]));
297 let mut mask = vec![false; ne];
298 let mut acc = 0u64;
299 let mut kept = 0usize;
300 for &e in &order {
301 mask[e] = true;
302 acc += counts[e];
303 kept += 1;
304 if (acc as f64) >= cover * (total as f64) {
305 break;
306 }
307 }
308 tracing::info!("MoE task mask L{li}: {kept}/{ne} experts for {:.0}% mass", cover * 100.0);
309 Some(mask)
310}
311
312fn load_matrix(
313 model: &Arc<CmfModel>,
314 name: &str,
315 force_f32: bool,
316 ov: &Overlay,
317) -> Result<QTensor, CmfError> {
318 if ov.blend_touches(model, name) {
322 if let Overlay::Blend(list) = ov {
323 let entry = model
324 .tensor(name)
325 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
326 let data =
327 blend_f32(model, name, list).map_err(|e| CmfError::Parse(format!("blend: {e}")))?;
328 return Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]));
329 }
330 }
331 let skill = match ov {
332 Overlay::One(s) => Some(*s),
333 _ => None,
334 };
335 let name: &str = &match skill {
338 Some(sid) if model.tensor(&format!("skill.{sid}.{name}")).is_some() => {
339 format!("skill.{sid}.{name}")
340 }
341 _ => name.to_string(),
342 };
343 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
344 if force_f32 {
345 let entry = model
346 .tensor(name)
347 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
348 if entry.shape.len() != 2 {
349 return Err(err(format!("'{name}' is not 2-D")));
350 }
351 let data = load_f32(model, name, &Overlay::None).map_err(err)?;
352 Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]))
353 } else {
354 QTensor::from_model(model, name).map_err(err)
355 }
356}
357
358impl Pipeline {
359 pub fn from_model(
361 model: &Arc<CmfModel>,
362 sampler_config: SamplerConfig,
363 ) -> Result<Self, CmfError> {
364 Self::from_model_with_skill(model, sampler_config, None)
365 }
366
367 pub fn from_model_with_skill(
373 model: &Arc<CmfModel>,
374 sampler_config: SamplerConfig,
375 skill: Option<&str>,
376 ) -> Result<Self, CmfError> {
377 match skill {
378 Some(s) => Self::from_model_with_overlay(model, sampler_config, &Overlay::One(s)),
379 None => Self::from_model_with_overlay(model, sampler_config, &Overlay::None),
380 }
381 }
382
383 pub fn from_model_with_blend(
386 model: &Arc<CmfModel>,
387 sampler_config: SamplerConfig,
388 blend: &[(String, f32)],
389 ) -> Result<Self, CmfError> {
390 Self::from_model_with_overlay(model, sampler_config, &Overlay::Blend(blend))
391 }
392
393 fn from_model_with_overlay(
394 model: &Arc<CmfModel>,
395 sampler_config: SamplerConfig,
396 ov: &Overlay,
397 ) -> Result<Self, CmfError> {
398 let skill = match ov {
399 Overlay::One(s) => Some(*s),
400 _ => None,
401 };
402 if let Some(sid) = skill {
403 let known = model.header.skills.iter().any(|s| s.id == sid)
404 || model.skill_tensors(sid).next().is_some();
405 if !known {
406 return Err(CmfError::Parse(format!(
407 "skill '{sid}' not in this container (header.skills: {:?})",
408 model
409 .header
410 .skills
411 .iter()
412 .map(|s| &s.id)
413 .collect::<Vec<_>>()
414 )));
415 }
416 tracing::info!(
417 "skill '{sid}': {} replacement tensors overlaid",
418 model.skill_tensors(sid).count()
419 );
420 }
421 let arch = model.arch().clone();
422 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
423 if let Some(heads) = &arch.attention_heads_per_layer {
424 if heads.len() != arch.num_layers {
425 return Err(CmfError::Parse(format!(
426 "arch.attention_heads_per_layer has {} entries, expected {}",
427 heads.len(),
428 arch.num_layers
429 )));
430 }
431 if let Some((li, &nh)) = heads
432 .iter()
433 .enumerate()
434 .find(|(_, nh)| **nh == 0 || **nh % arch.num_kv_heads != 0)
435 {
436 return Err(CmfError::Parse(format!(
437 "layer {li} has {nh} Q heads, which must be nonzero and divisible by {} KV heads",
438 arch.num_kv_heads
439 )));
440 }
441 }
442 if arch
443 .layer_types
444 .iter()
445 .any(|t| matches!(t, LayerType::SlidingAttention))
446 && arch.sliding_window.is_none()
447 {
448 return Err(CmfError::Parse(
449 "model has SlidingAttention layers but no arch.sliding_window".into(),
450 ));
451 }
452
453 let masks_present = !model.masks.masks.is_empty();
459 let force_f32 = masks_present; let mut tokenizer = if let Some(vocab_bytes) = &model.vocab {
463 Tokenizer::from_bytes(vocab_bytes)
464 .map_err(|e| CmfError::Parse(format!("embedded tokenizer: {e}")))?
465 } else {
466 let sidecar = model.path.with_file_name("tokenizer.json");
467 if sidecar.exists() {
468 Tokenizer::from_file(&sidecar)
469 .map_err(|e| CmfError::Parse(format!("sidecar tokenizer: {e}")))?
470 } else {
471 tracing::warn!("no tokenizer in file or sidecar — using byte-level fallback");
472 Tokenizer::byte_level()
473 }
474 };
475 if let Some(tc) = &model.header.tokenizer_config {
477 tokenizer.chat_template = tc.chat_template.clone();
478 tokenizer.extra_eos.extend(tc.eos_token_ids.iter().copied());
479 if tokenizer.bos_token_id.is_none() {
480 tokenizer.bos_token_id = tc.bos_token_id;
481 }
482 tracing::info!(
483 "chat bundle: template {} chars, {} stop ids",
484 tc.chat_template.as_deref().map(str::len).unwrap_or(0),
485 tc.eos_token_ids.len()
486 );
487 }
488 if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
492 tokenizer.add_bos = true;
493 }
494
495 let embed_tokens = load_matrix(model, "model.embed_tokens.weight", false, ov)?;
497 let final_norm = load_f32(model, "model.norm.weight", ov).map_err(err)?;
498 let lm_head = if model.tensor("lm_head.weight").is_some() {
499 load_matrix(model, "lm_head.weight", false, ov)?
500 } else if arch.tie_word_embeddings {
501 load_matrix(model, "model.embed_tokens.weight", false, ov)?
503 } else {
504 return Err(CmfError::MissingTensor(
505 "lm_head.weight (and tie_word_embeddings is false)".into(),
506 ));
507 };
508
509 let has_linear = arch
511 .layer_types
512 .iter()
513 .any(|t| matches!(t, LayerType::LinearAttention));
514 let mut vmf_cfg = None;
515 let mut gdn_cfg = None;
516 if has_linear {
517 let lc = arch.linear_core.as_ref().ok_or_else(|| {
518 CmfError::Parse(
519 "model has LinearAttention layers but no arch.linear_core — \
520 reconvert with the current converter"
521 .into(),
522 )
523 })?;
524 let need = |v: Option<usize>, name: &str| {
525 v.ok_or_else(|| CmfError::Parse(format!("linear core needs arch.{name}")))
526 };
527 match lc.kind.as_str() {
528 "vmf_phase" => {
529 vmf_cfg = Some(VmfPhaseCfg {
530 num_heads: lc.num_heads,
531 nphase: need(lc.nphase, "linear_core.nphase")?,
532 value_head_dim: lc.value_head_dim,
533 hidden_size: arch.hidden_size,
534 phase_mass: std::env::var("CMF_PHASE_MASS")
537 .ok()
538 .and_then(|v| v.parse().ok())
539 .unwrap_or(0.0),
540 });
541 }
542 "gated_delta_net" => {
543 gdn_cfg = Some(GdnCfg {
544 num_v_heads: lc.num_heads,
545 num_k_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
546 key_head_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
547 value_head_dim: lc.value_head_dim,
548 conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
549 hidden_size: arch.hidden_size,
550 rms_eps: arch.rms_norm_eps,
551 });
552 }
553 other => {
554 return Err(CmfError::Parse(format!(
555 "unknown linear core '{other}' (this runtime executes: \
556 gated_delta_net, vmf_phase)"
557 )));
558 }
559 }
560 }
561
562 let has_short_conv = arch
564 .layer_types
565 .iter()
566 .any(|t| matches!(t, LayerType::ShortConv));
567 let short_conv_cfg = if has_short_conv {
568 Some(ShortConvCfg {
569 hidden_size: arch.hidden_size,
570 kernel: arch.linear_conv_kernel_dim.ok_or_else(|| {
571 CmfError::Parse(
572 "model has ShortConv layers but no arch.linear_conv_kernel_dim — \
573 reconvert with the current converter"
574 .into(),
575 )
576 })?,
577 })
578 } else {
579 None
580 };
581
582 let load_full_attn = |prefix: &str, layer: Option<usize>| -> Result<AttnKind, CmfError> {
584 let t = |suffix: &str| load_matrix(model, &format!("{prefix}{suffix}"), force_f32, ov);
585 let n = |suffix: &str| -> Option<Vec<f32>> {
586 model
587 .tensor(&format!("{prefix}{suffix}"))
588 .and_then(|_| load_f32(model, &format!("{prefix}{suffix}"), ov).ok())
589 };
590 let wq = t("self_attn.q_proj.weight")?;
591 let nh = layer
592 .and_then(|li| {
593 arch.attention_heads_per_layer
594 .as_ref()
595 .and_then(|v| v.get(li).copied())
596 })
597 .unwrap_or(arch.num_attention_heads);
598 let output_gate = arch.global_head_dim.is_none() && wq.rows() == 2 * nh * arch.head_dim;
602 if !output_gate && wq.rows() != nh * arch.head_dim {
603 return Err(CmfError::Parse(format!(
604 "{prefix}self_attn.q_proj.weight rows={} != heads({nh}) * head_dim({})",
605 wq.rows(),
606 arch.head_dim
607 )));
608 }
609 let gate_name = format!("{prefix}self_attn.g_proj.weight");
610 let softplus_gate = if model.tensor(&gate_name).is_some() {
611 let gate = load_matrix(model, &gate_name, force_f32, ov)?;
612 if gate.cols() != arch.hidden_size {
613 return Err(CmfError::Parse(format!(
614 "{gate_name} cols={} != hidden_size ({})",
615 gate.cols(),
616 arch.hidden_size
617 )));
618 }
619 let per_head = if gate.rows() == nh {
620 true
621 } else if gate.rows() == nh * arch.head_dim {
622 false
623 } else {
624 return Err(CmfError::Parse(format!(
625 "{gate_name} rows={} must equal heads ({nh}) or heads*head_dim ({})",
626 gate.rows(),
627 nh * arch.head_dim
628 )));
629 };
630 Some((gate, per_head))
631 } else {
632 None
633 };
634 let bias = match (
636 n("self_attn.q_proj.bias"),
637 n("self_attn.k_proj.bias"),
638 n("self_attn.v_proj.bias"),
639 ) {
640 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
641 _ => None,
642 };
643 Ok(AttnKind::Full {
644 wq,
645 wk: t("self_attn.k_proj.weight")?,
646 wv: t("self_attn.v_proj.weight")?,
647 wo: t("self_attn.o_proj.weight")?,
648 q_norm: n("self_attn.q_norm.weight"),
649 k_norm: n("self_attn.k_norm.weight"),
650 output_gate,
651 softplus_gate,
652 bias,
653 })
654 };
655
656 let load_linear_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
657 if gdn_cfg.is_some() {
658 let t = |suffix: &str| {
660 load_matrix(
661 model,
662 &format!("{prefix}linear_attn.{suffix}"),
663 force_f32,
664 ov,
665 )
666 };
667 let f = |suffix: &str| {
668 load_f32(model, &format!("{prefix}linear_attn.{suffix}"), ov).map_err(err)
669 };
670 return Ok(AttnKind::LinearGdn(GdnWeights {
671 in_proj_qkv: t("in_proj_qkv.weight")?,
672 in_proj_z: t("in_proj_z.weight")?,
673 in_proj_a: t("in_proj_a.weight")?,
674 in_proj_b: t("in_proj_b.weight")?,
675 conv1d: f("conv1d.weight")?,
676 a_log: f("A_log")?,
677 dt_bias: f("dt_bias")?,
678 norm: f("norm.weight")?,
679 out_proj: t("out_proj.weight")?,
680 }));
681 }
682 let t = |suffix: &str| {
683 load_matrix(model, &format!("{prefix}vmf_attn.{suffix}"), force_f32, ov)
684 };
685 let a_log = load_f32(model, &format!("{prefix}vmf_attn.A_log"), ov).map_err(err)?;
686 let k_gate = if model
690 .tensor(&format!("{prefix}vmf_attn.k_gate.weight"))
691 .is_some()
692 {
693 Some((
694 t("k_gate.weight")?,
695 load_f32(model, &format!("{prefix}vmf_attn.k_gate.bias"), ov).map_err(err)?,
696 ))
697 } else {
698 None
699 };
700 Ok(AttnKind::Linear(VmfPhaseWeights {
701 thq: t("thq.weight")?,
702 thk: t("thk.weight")?,
703 v_proj: t("v_proj.weight")?,
704 out_proj: t("out_proj.weight")?,
705 decay: a_log.iter().map(|&a| (-(a as f64).exp()).exp()).collect(),
706 k_gate,
707 }))
708 };
709
710 let load_short_conv = |prefix: &str| -> Result<AttnKind, CmfError> {
714 let t = |suffix: &str| {
715 load_matrix(
716 model,
717 &format!("{prefix}short_conv.{suffix}"),
718 force_f32,
719 ov,
720 )
721 };
722 Ok(AttnKind::ShortConv(ShortConvWeights {
723 in_proj: t("in_proj.weight")?,
724 conv: load_f32(model, &format!("{prefix}short_conv.conv.weight"), ov)
725 .map_err(err)?,
726 out_proj: t("out_proj.weight")?,
727 }))
728 };
729
730 let mut layers = Vec::with_capacity(arch.num_layers);
731 for li in 0..arch.num_layers {
732 let prefix = format!("model.layers.{li}.");
733 let attn = match arch.layer_types.get(li) {
734 Some(LayerType::LinearAttention) => load_linear_attn(&prefix)?,
735 Some(LayerType::ShortConv) => load_short_conv(&prefix)?,
736 _ => load_full_attn(&prefix, Some(li))?,
737 };
738 let pre_ffn = format!("{prefix}pre_feedforward_layernorm.weight");
742 let sandwich = model.tensor(&pre_ffn).is_some();
743 layers.push(LayerWeights {
744 input_norm: load_f32(model, &format!("{prefix}input_layernorm.weight"), ov)
745 .map_err(err)?,
746 post_norm: if sandwich {
747 load_f32(model, &pre_ffn, ov).map_err(err)?
748 } else {
749 load_f32(
750 model,
751 &format!("{prefix}post_attention_layernorm.weight"),
752 ov,
753 )
754 .map_err(err)?
755 },
756 attn_out_norm: if sandwich {
757 Some(
758 load_f32(
759 model,
760 &format!("{prefix}post_attention_layernorm.weight"),
761 ov,
762 )
763 .map_err(err)?,
764 )
765 } else {
766 None
767 },
768 ffn_out_norm: if sandwich {
769 Some(
770 load_f32(
771 model,
772 &format!("{prefix}post_feedforward_layernorm.weight"),
773 ov,
774 )
775 .map_err(err)?,
776 )
777 } else {
778 None
779 },
780 layer_scale: model
782 .tensor(&format!("{prefix}layer_scalar"))
783 .and_then(|_| {
784 load_f32(model, &format!("{prefix}layer_scalar"), ov)
785 .ok()
786 .and_then(|v| v.first().copied())
787 }),
788 ffn: build_layer_ffn(model, &arch, li, false, ov)?,
790 attn,
791 });
792 }
793
794 let mtp = if let Some(cfg) = &arch.mtp {
796 if cfg.num_layers != 1 {
797 return Err(CmfError::Parse(format!(
798 "MTP with {} blocks not supported yet (only 1)",
799 cfg.num_layers
800 )));
801 }
802 let p = "model.mtp.";
803 let attn = load_full_attn("model.mtp.layers.0.", None)?;
804 Some(MtpModule {
805 enorm: load_f32(model, &format!("{p}enorm.weight"), ov).map_err(err)?,
806 hnorm: load_f32(model, &format!("{p}hnorm.weight"), ov).map_err(err)?,
807 eh_proj: load_matrix(model, &format!("{p}eh_proj.weight"), false, ov)?,
808 layer: LayerWeights {
809 attn_out_norm: None,
810 ffn_out_norm: None,
811 layer_scale: None,
812 input_norm: load_f32(model, &format!("{p}layers.0.input_layernorm.weight"), ov)
813 .map_err(err)?,
814 post_norm: load_f32(
815 model,
816 &format!("{p}layers.0.post_attention_layernorm.weight"),
817 ov,
818 )
819 .map_err(err)?,
820 ffn: FfnKind::Dense(DenseFfn {
821 gate_proj: load_matrix(
822 model,
823 &format!("{p}layers.0.mlp.gate_proj.weight"),
824 false,
825 ov,
826 )?,
827 up_proj: load_matrix(
828 model,
829 &format!("{p}layers.0.mlp.up_proj.weight"),
830 false,
831 ov,
832 )?,
833 down_proj: load_matrix(
834 model,
835 &format!("{p}layers.0.mlp.down_proj.weight"),
836 false,
837 ov,
838 )?,
839 act: crate::pipeline::Act::from_arch(&arch.hidden_act),
840 }),
841 attn,
842 },
843 final_norm: load_f32(model, &format!("{p}norm.weight"), ov).map_err(err)?,
844 kv: LayerKvCache::new(arch.num_kv_heads, arch.head_dim),
845 })
846 } else {
847 None
848 };
849
850 tracing::info!(
851 "Pipeline loaded: {} | {}L ({} linear) | {:.2}B params | storage: {} | MTP: {}",
852 arch.arch_name,
853 arch.num_layers,
854 arch.layer_types
855 .iter()
856 .filter(|t| matches!(t, LayerType::LinearAttention))
857 .count(),
858 model.total_param_count() as f64 / 1e9,
859 if force_f32 {
860 "f32 (masked)"
861 } else {
862 "quantized mmap"
863 },
864 if mtp.is_some() { "yes" } else { "no" }
865 );
866
867 let cap = std::env::var("CMF_MAX_SEQ")
870 .ok()
871 .and_then(|v| v.parse::<usize>().ok())
872 .unwrap_or(8192);
873 let max_seq_len = arch.max_position_embeddings.min(cap);
874
875 let total_layers = arch.num_layers * arch.num_loops;
877
878 let mut pipeline = Pipeline::new(
879 tokenizer,
880 PipelineWeights {
881 embed_tokens,
882 layers,
883 lm_head,
884 final_norm,
885 },
886 arch.hidden_size,
887 arch.intermediate_size,
888 arch.num_attention_heads,
889 arch.num_kv_heads,
890 arch.head_dim,
891 total_layers,
892 arch.num_layers, arch.loop_final_norm,
894 arch.vocab_size,
895 arch.rms_norm_eps,
896 arch.rope_theta as f32,
897 arch.norm_style,
898 max_seq_len,
899 sampler_config,
900 );
901 let rotary = ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2);
902 pipeline.set_rotary(rotary, arch.rope_theta as f32);
903 pipeline.attention_heads_per_layer = arch.attention_heads_per_layer.clone();
904 if let Some(yarn) = &arch.yarn {
905 pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
906 rotary,
907 arch.rope_theta as f32,
908 yarn.factor,
909 yarn.original_max_position_embeddings,
910 yarn.beta_fast,
911 yarn.beta_slow,
912 ));
913 pipeline.rope_scale = yarn.attention_factor;
914 }
915 pipeline.embed_multiplier = arch.embed_multiplier;
919 if let Some(qpas) = arch.query_pre_attn_scalar {
920 pipeline.attn_scale = 1.0 / (qpas as f32).sqrt();
921 }
922 if let (Some(w), Some(p)) = (arch.sliding_window, arch.sliding_window_pattern) {
923 pipeline.swa = Some((w, p));
924 if let Some(base) = arch.rope_local_base_freq {
925 pipeline.inv_freq_local = Some(std::sync::Arc::new(
926 crate::attention::rope_inv_freq(rotary, base as f32),
927 ));
928 }
929 }
930 let explicit_sliding: Vec<bool> = arch
931 .layer_types
932 .iter()
933 .map(|t| matches!(t, cortiq_core::LayerType::SlidingAttention))
934 .collect();
935 if explicit_sliding.iter().any(|&v| v) {
936 pipeline.sliding_layers = Some(explicit_sliding);
937 if let Some(w) = arch.sliding_window {
938 pipeline.swa = Some((w, usize::MAX));
939 }
940 let local_rotary = ((arch.head_dim as f32
941 * arch
942 .local_partial_rotary_factor
943 .unwrap_or(arch.partial_rotary_factor))
944 as usize)
945 .max(2);
946 pipeline.rotary_dim_local = Some(local_rotary);
947 if let Some(base) = arch.rope_local_base_freq {
948 pipeline.inv_freq_local = Some(std::sync::Arc::new(
949 crate::attention::rope_inv_freq(local_rotary, base as f32),
950 ));
951 }
952 }
953 if let (Some(ghd), Some(gkv)) = (arch.global_head_dim, arch.num_global_kv_heads) {
957 pipeline.global_attn = Some((ghd, gkv));
958 let prf = arch.global_partial_rotary_factor.unwrap_or(1.0);
959 let half = ghd / 2;
960 let ra = (((prf * ghd as f32) as usize) / 2).min(half);
961 let mut f = vec![0.0f32; half];
962 for (i, slot) in f.iter_mut().enumerate().take(ra) {
963 *slot = 1.0 / (arch.rope_theta as f32).powf(2.0 * i as f32 / ghd as f32);
964 }
965 pipeline.inv_freq_global = Some(std::sync::Arc::new(f));
966 if let Some((_, p)) = pipeline.swa {
968 for li in 0..arch.num_layers {
969 if (li + 1) % p.max(1) == 0 {
970 pipeline.kv_cache.layers[li] = crate::kv_cache::LayerKvCache::new(gkv, ghd);
971 }
972 }
973 }
974 }
975 pipeline.attn_v_norm = arch.attn_v_norm;
976 pipeline.final_softcap = arch.final_logit_softcapping.map(|c| c as f32);
977 pipeline.vmf_cfg = vmf_cfg;
978 pipeline.gdn_cfg = gdn_cfg;
979 pipeline.short_conv_cfg = short_conv_cfg;
980 pipeline.mtp = mtp;
981 pipeline.install_dynamic_routing(model, false);
982 match ov {
986 Overlay::One(sid) => {
987 pipeline.dyn_active = model.header.skills.iter().position(|s| &s.id == sid);
988 }
989 Overlay::Blend(_) => pipeline.dyn_blend_loaded = true,
990 Overlay::None => {}
991 }
992 if let Some(c) = &model.header.calibration {
995 pipeline.set_calib_temp(c.temperature);
996 }
997 let o1 = match crate::nystrom::o1_from_env() {
1003 crate::nystrom::O1Env::Off => None,
1004 crate::nystrom::O1Env::On(cfg) => Some(cfg),
1005 crate::nystrom::O1Env::Unset => model
1006 .header
1007 .provenance
1008 .as_ref()
1009 .and_then(|p| p.get("o1_attn"))
1010 .and_then(crate::nystrom::O1Cfg::from_json),
1011 };
1012 if o1.is_some() {
1013 pipeline.set_o1(o1);
1014 }
1015 Ok(pipeline)
1016 }
1017
1018 pub(crate) fn install_dynamic_routing(&mut self, model: &Arc<CmfModel>, force_f32: bool) {
1023 self.model = Some(model.clone());
1024 self.dyn_force_f32 = force_f32;
1025 let mut per_skill = Vec::with_capacity(model.header.skills.len());
1026 for sk in &model.header.skills {
1027 let mut ffn_layers = std::collections::BTreeSet::new();
1028 let mut non_ffn = false;
1029 let prefix = format!("skill.{}.", sk.id);
1030 for t in model.skill_tensors(&sk.id) {
1031 let rel = &t.name[prefix.len()..]; let toks: Vec<&str> = rel.split('.').collect();
1033 if toks.len() >= 5 && toks[0] == "model" && toks[1] == "layers" && toks[3] == "mlp"
1034 {
1035 if let Ok(li) = toks[2].parse::<usize>() {
1036 ffn_layers.insert(li);
1037 continue;
1038 }
1039 }
1040 non_ffn = true; }
1042 if non_ffn {
1043 tracing::warn!(
1044 "skill '{}' replaces non-FFN tensors — excluded from dynamic \
1045 routing (static overlay still works)",
1046 sk.id
1047 );
1048 per_skill.push(None);
1049 } else {
1050 per_skill.push(Some(ffn_layers.into_iter().collect::<Vec<_>>()));
1051 }
1052 }
1053 self.dyn_skill_layers = per_skill;
1054 }
1055
1056 pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
1063 if self.dyn_active == idx {
1064 return Ok(());
1065 }
1066 let model = self.model.clone().ok_or_else(|| {
1067 CmfError::Parse("dynamic routing needs a model-backed pipeline".into())
1068 })?;
1069 let mut union: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
1070 if let Some(old) = self.dyn_active {
1071 if let Some(Some(ls)) = self.dyn_skill_layers.get(old) {
1072 union.extend(ls.iter().copied());
1073 }
1074 }
1075 let new_id: Option<String> = match idx {
1076 Some(n) => match self.dyn_skill_layers.get(n) {
1077 Some(Some(ls)) => {
1078 union.extend(ls.iter().copied());
1079 Some(model.header.skills[n].id.clone())
1080 }
1081 _ => {
1082 return Err(CmfError::Parse(format!(
1083 "skill index {n} not dynamic-eligible"
1084 )));
1085 }
1086 },
1087 None => None,
1088 };
1089 let ov = match &new_id {
1090 Some(s) => Overlay::One(s),
1091 None => Overlay::None,
1092 };
1093 let arch = model.arch();
1094 for li in union {
1095 self.weights.layers[li].ffn =
1096 build_layer_ffn(&model, arch, li, self.dyn_force_f32, &ov)?;
1097 }
1098 self.dyn_active = idx;
1099 Ok(())
1100 }
1101}