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 experts = (0..cfg.num_experts)
160 .map(|e| load_dense(&format!("{prefix}mlp.experts.{e}.")))
161 .collect::<Result<Vec<_>, _>>()?;
162 let shared = if model
163 .tensor(&format!("{prefix}mlp.shared_expert.gate_proj.weight"))
164 .is_some()
165 {
166 let gate_name = format!("{prefix}mlp.shared_expert_gate.weight");
167 Some((
168 load_dense(&format!("{prefix}mlp.shared_expert."))?,
169 if model.tensor(&gate_name).is_some() {
170 Some(load_matrix(model, &gate_name, force_f32, ov)?)
171 } else {
172 None
173 },
174 ))
175 } else {
176 None
177 };
178 let bias_name = format!("{prefix}mlp.expert_bias");
181 let expert_bias = if model.tensor(&bias_name).is_some() {
182 Some(load_f32(model, &bias_name, ov).map_err(CmfError::Parse)?)
183 } else {
184 None
185 };
186 let top_k = std::env::var("CMF_MOE_TOPK")
192 .ok()
193 .and_then(|v| v.parse::<usize>().ok())
194 .filter(|&k| k >= 1 && k <= cfg.top_k)
195 .inspect(|k| tracing::info!("MoE top_k override: {} (header {})", k, cfg.top_k))
196 .unwrap_or(cfg.top_k);
197 let route_tau = std::env::var("CMF_MOE_TAU")
199 .ok()
200 .and_then(|v| v.parse::<f32>().ok())
201 .filter(|&t| t > 0.0 && t < 1.0)
202 .inspect(|t| tracing::info!("MoE adaptive routing: tau {t}"));
203 Ok(FfnKind::Moe(MoeFfn {
204 router: load_matrix(model, &router_name, force_f32, ov)?,
205 experts,
206 top_k,
207 route_tau,
208 norm_topk_prob: cfg.norm_topk_prob,
209 router_sigmoid: cfg.router_sigmoid,
210 expert_bias,
211 routed_scaling: cfg.routed_scaling_factor.unwrap_or(1.0),
212 shared,
213 stats: std::cell::RefCell::new(Vec::new()),
214 }))
215}
216
217fn load_matrix(
218 model: &Arc<CmfModel>,
219 name: &str,
220 force_f32: bool,
221 ov: &Overlay,
222) -> Result<QTensor, CmfError> {
223 if ov.blend_touches(model, name) {
227 if let Overlay::Blend(list) = ov {
228 let entry = model
229 .tensor(name)
230 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
231 let data =
232 blend_f32(model, name, list).map_err(|e| CmfError::Parse(format!("blend: {e}")))?;
233 return Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]));
234 }
235 }
236 let skill = match ov {
237 Overlay::One(s) => Some(*s),
238 _ => None,
239 };
240 let name: &str = &match skill {
243 Some(sid) if model.tensor(&format!("skill.{sid}.{name}")).is_some() => {
244 format!("skill.{sid}.{name}")
245 }
246 _ => name.to_string(),
247 };
248 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
249 if force_f32 {
250 let entry = model
251 .tensor(name)
252 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
253 if entry.shape.len() != 2 {
254 return Err(err(format!("'{name}' is not 2-D")));
255 }
256 let data = load_f32(model, name, &Overlay::None).map_err(err)?;
257 Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]))
258 } else {
259 QTensor::from_model(model, name).map_err(err)
260 }
261}
262
263impl Pipeline {
264 pub fn from_model(
266 model: &Arc<CmfModel>,
267 sampler_config: SamplerConfig,
268 ) -> Result<Self, CmfError> {
269 Self::from_model_with_skill(model, sampler_config, None)
270 }
271
272 pub fn from_model_with_skill(
278 model: &Arc<CmfModel>,
279 sampler_config: SamplerConfig,
280 skill: Option<&str>,
281 ) -> Result<Self, CmfError> {
282 match skill {
283 Some(s) => Self::from_model_with_overlay(model, sampler_config, &Overlay::One(s)),
284 None => Self::from_model_with_overlay(model, sampler_config, &Overlay::None),
285 }
286 }
287
288 pub fn from_model_with_blend(
291 model: &Arc<CmfModel>,
292 sampler_config: SamplerConfig,
293 blend: &[(String, f32)],
294 ) -> Result<Self, CmfError> {
295 Self::from_model_with_overlay(model, sampler_config, &Overlay::Blend(blend))
296 }
297
298 fn from_model_with_overlay(
299 model: &Arc<CmfModel>,
300 sampler_config: SamplerConfig,
301 ov: &Overlay,
302 ) -> Result<Self, CmfError> {
303 let skill = match ov {
304 Overlay::One(s) => Some(*s),
305 _ => None,
306 };
307 if let Some(sid) = skill {
308 let known = model.header.skills.iter().any(|s| s.id == sid)
309 || model.skill_tensors(sid).next().is_some();
310 if !known {
311 return Err(CmfError::Parse(format!(
312 "skill '{sid}' not in this container (header.skills: {:?})",
313 model
314 .header
315 .skills
316 .iter()
317 .map(|s| &s.id)
318 .collect::<Vec<_>>()
319 )));
320 }
321 tracing::info!(
322 "skill '{sid}': {} replacement tensors overlaid",
323 model.skill_tensors(sid).count()
324 );
325 }
326 let arch = model.arch().clone();
327 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
328 if let Some(heads) = &arch.attention_heads_per_layer {
329 if heads.len() != arch.num_layers {
330 return Err(CmfError::Parse(format!(
331 "arch.attention_heads_per_layer has {} entries, expected {}",
332 heads.len(),
333 arch.num_layers
334 )));
335 }
336 if let Some((li, &nh)) = heads
337 .iter()
338 .enumerate()
339 .find(|(_, nh)| **nh == 0 || **nh % arch.num_kv_heads != 0)
340 {
341 return Err(CmfError::Parse(format!(
342 "layer {li} has {nh} Q heads, which must be nonzero and divisible by {} KV heads",
343 arch.num_kv_heads
344 )));
345 }
346 }
347 if arch
348 .layer_types
349 .iter()
350 .any(|t| matches!(t, LayerType::SlidingAttention))
351 && arch.sliding_window.is_none()
352 {
353 return Err(CmfError::Parse(
354 "model has SlidingAttention layers but no arch.sliding_window".into(),
355 ));
356 }
357
358 let masks_present = !model.masks.masks.is_empty();
364 let force_f32 = masks_present; let mut tokenizer = if let Some(vocab_bytes) = &model.vocab {
368 Tokenizer::from_bytes(vocab_bytes)
369 .map_err(|e| CmfError::Parse(format!("embedded tokenizer: {e}")))?
370 } else {
371 let sidecar = model.path.with_file_name("tokenizer.json");
372 if sidecar.exists() {
373 Tokenizer::from_file(&sidecar)
374 .map_err(|e| CmfError::Parse(format!("sidecar tokenizer: {e}")))?
375 } else {
376 tracing::warn!("no tokenizer in file or sidecar — using byte-level fallback");
377 Tokenizer::byte_level()
378 }
379 };
380 if let Some(tc) = &model.header.tokenizer_config {
382 tokenizer.chat_template = tc.chat_template.clone();
383 tokenizer.extra_eos.extend(tc.eos_token_ids.iter().copied());
384 if tokenizer.bos_token_id.is_none() {
385 tokenizer.bos_token_id = tc.bos_token_id;
386 }
387 tracing::info!(
388 "chat bundle: template {} chars, {} stop ids",
389 tc.chat_template.as_deref().map(str::len).unwrap_or(0),
390 tc.eos_token_ids.len()
391 );
392 }
393 if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
397 tokenizer.add_bos = true;
398 }
399
400 let embed_tokens = load_matrix(model, "model.embed_tokens.weight", false, ov)?;
402 let final_norm = load_f32(model, "model.norm.weight", ov).map_err(err)?;
403 let lm_head = if model.tensor("lm_head.weight").is_some() {
404 load_matrix(model, "lm_head.weight", false, ov)?
405 } else if arch.tie_word_embeddings {
406 load_matrix(model, "model.embed_tokens.weight", false, ov)?
408 } else {
409 return Err(CmfError::MissingTensor(
410 "lm_head.weight (and tie_word_embeddings is false)".into(),
411 ));
412 };
413
414 let has_linear = arch
416 .layer_types
417 .iter()
418 .any(|t| matches!(t, LayerType::LinearAttention));
419 let mut vmf_cfg = None;
420 let mut gdn_cfg = None;
421 if has_linear {
422 let lc = arch.linear_core.as_ref().ok_or_else(|| {
423 CmfError::Parse(
424 "model has LinearAttention layers but no arch.linear_core — \
425 reconvert with the current converter"
426 .into(),
427 )
428 })?;
429 let need = |v: Option<usize>, name: &str| {
430 v.ok_or_else(|| CmfError::Parse(format!("linear core needs arch.{name}")))
431 };
432 match lc.kind.as_str() {
433 "vmf_phase" => {
434 vmf_cfg = Some(VmfPhaseCfg {
435 num_heads: lc.num_heads,
436 nphase: need(lc.nphase, "linear_core.nphase")?,
437 value_head_dim: lc.value_head_dim,
438 hidden_size: arch.hidden_size,
439 phase_mass: std::env::var("CMF_PHASE_MASS")
442 .ok()
443 .and_then(|v| v.parse().ok())
444 .unwrap_or(0.0),
445 });
446 }
447 "gated_delta_net" => {
448 gdn_cfg = Some(GdnCfg {
449 num_v_heads: lc.num_heads,
450 num_k_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
451 key_head_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
452 value_head_dim: lc.value_head_dim,
453 conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
454 hidden_size: arch.hidden_size,
455 rms_eps: arch.rms_norm_eps,
456 });
457 }
458 other => {
459 return Err(CmfError::Parse(format!(
460 "unknown linear core '{other}' (this runtime executes: \
461 gated_delta_net, vmf_phase)"
462 )));
463 }
464 }
465 }
466
467 let has_short_conv = arch
469 .layer_types
470 .iter()
471 .any(|t| matches!(t, LayerType::ShortConv));
472 let short_conv_cfg = if has_short_conv {
473 Some(ShortConvCfg {
474 hidden_size: arch.hidden_size,
475 kernel: arch.linear_conv_kernel_dim.ok_or_else(|| {
476 CmfError::Parse(
477 "model has ShortConv layers but no arch.linear_conv_kernel_dim — \
478 reconvert with the current converter"
479 .into(),
480 )
481 })?,
482 })
483 } else {
484 None
485 };
486
487 let load_full_attn = |prefix: &str, layer: Option<usize>| -> Result<AttnKind, CmfError> {
489 let t = |suffix: &str| load_matrix(model, &format!("{prefix}{suffix}"), force_f32, ov);
490 let n = |suffix: &str| -> Option<Vec<f32>> {
491 model
492 .tensor(&format!("{prefix}{suffix}"))
493 .and_then(|_| load_f32(model, &format!("{prefix}{suffix}"), ov).ok())
494 };
495 let wq = t("self_attn.q_proj.weight")?;
496 let nh = layer
497 .and_then(|li| {
498 arch.attention_heads_per_layer
499 .as_ref()
500 .and_then(|v| v.get(li).copied())
501 })
502 .unwrap_or(arch.num_attention_heads);
503 let output_gate = arch.global_head_dim.is_none() && wq.rows() == 2 * nh * arch.head_dim;
507 if !output_gate && wq.rows() != nh * arch.head_dim {
508 return Err(CmfError::Parse(format!(
509 "{prefix}self_attn.q_proj.weight rows={} != heads({nh}) * head_dim({})",
510 wq.rows(),
511 arch.head_dim
512 )));
513 }
514 let gate_name = format!("{prefix}self_attn.g_proj.weight");
515 let softplus_gate = if model.tensor(&gate_name).is_some() {
516 let gate = load_matrix(model, &gate_name, force_f32, ov)?;
517 if gate.cols() != arch.hidden_size {
518 return Err(CmfError::Parse(format!(
519 "{gate_name} cols={} != hidden_size ({})",
520 gate.cols(),
521 arch.hidden_size
522 )));
523 }
524 let per_head = if gate.rows() == nh {
525 true
526 } else if gate.rows() == nh * arch.head_dim {
527 false
528 } else {
529 return Err(CmfError::Parse(format!(
530 "{gate_name} rows={} must equal heads ({nh}) or heads*head_dim ({})",
531 gate.rows(),
532 nh * arch.head_dim
533 )));
534 };
535 Some((gate, per_head))
536 } else {
537 None
538 };
539 let bias = match (
541 n("self_attn.q_proj.bias"),
542 n("self_attn.k_proj.bias"),
543 n("self_attn.v_proj.bias"),
544 ) {
545 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
546 _ => None,
547 };
548 Ok(AttnKind::Full {
549 wq,
550 wk: t("self_attn.k_proj.weight")?,
551 wv: t("self_attn.v_proj.weight")?,
552 wo: t("self_attn.o_proj.weight")?,
553 q_norm: n("self_attn.q_norm.weight"),
554 k_norm: n("self_attn.k_norm.weight"),
555 output_gate,
556 softplus_gate,
557 bias,
558 })
559 };
560
561 let load_linear_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
562 if gdn_cfg.is_some() {
563 let t = |suffix: &str| {
565 load_matrix(
566 model,
567 &format!("{prefix}linear_attn.{suffix}"),
568 force_f32,
569 ov,
570 )
571 };
572 let f = |suffix: &str| {
573 load_f32(model, &format!("{prefix}linear_attn.{suffix}"), ov).map_err(err)
574 };
575 return Ok(AttnKind::LinearGdn(GdnWeights {
576 in_proj_qkv: t("in_proj_qkv.weight")?,
577 in_proj_z: t("in_proj_z.weight")?,
578 in_proj_a: t("in_proj_a.weight")?,
579 in_proj_b: t("in_proj_b.weight")?,
580 conv1d: f("conv1d.weight")?,
581 a_log: f("A_log")?,
582 dt_bias: f("dt_bias")?,
583 norm: f("norm.weight")?,
584 out_proj: t("out_proj.weight")?,
585 }));
586 }
587 let t = |suffix: &str| {
588 load_matrix(model, &format!("{prefix}vmf_attn.{suffix}"), force_f32, ov)
589 };
590 let a_log = load_f32(model, &format!("{prefix}vmf_attn.A_log"), ov).map_err(err)?;
591 let k_gate = if model
595 .tensor(&format!("{prefix}vmf_attn.k_gate.weight"))
596 .is_some()
597 {
598 Some((
599 t("k_gate.weight")?,
600 load_f32(model, &format!("{prefix}vmf_attn.k_gate.bias"), ov).map_err(err)?,
601 ))
602 } else {
603 None
604 };
605 Ok(AttnKind::Linear(VmfPhaseWeights {
606 thq: t("thq.weight")?,
607 thk: t("thk.weight")?,
608 v_proj: t("v_proj.weight")?,
609 out_proj: t("out_proj.weight")?,
610 decay: a_log.iter().map(|&a| (-(a as f64).exp()).exp()).collect(),
611 k_gate,
612 }))
613 };
614
615 let load_short_conv = |prefix: &str| -> Result<AttnKind, CmfError> {
619 let t = |suffix: &str| {
620 load_matrix(
621 model,
622 &format!("{prefix}short_conv.{suffix}"),
623 force_f32,
624 ov,
625 )
626 };
627 Ok(AttnKind::ShortConv(ShortConvWeights {
628 in_proj: t("in_proj.weight")?,
629 conv: load_f32(model, &format!("{prefix}short_conv.conv.weight"), ov)
630 .map_err(err)?,
631 out_proj: t("out_proj.weight")?,
632 }))
633 };
634
635 let mut layers = Vec::with_capacity(arch.num_layers);
636 for li in 0..arch.num_layers {
637 let prefix = format!("model.layers.{li}.");
638 let attn = match arch.layer_types.get(li) {
639 Some(LayerType::LinearAttention) => load_linear_attn(&prefix)?,
640 Some(LayerType::ShortConv) => load_short_conv(&prefix)?,
641 _ => load_full_attn(&prefix, Some(li))?,
642 };
643 let pre_ffn = format!("{prefix}pre_feedforward_layernorm.weight");
647 let sandwich = model.tensor(&pre_ffn).is_some();
648 layers.push(LayerWeights {
649 input_norm: load_f32(model, &format!("{prefix}input_layernorm.weight"), ov)
650 .map_err(err)?,
651 post_norm: if sandwich {
652 load_f32(model, &pre_ffn, ov).map_err(err)?
653 } else {
654 load_f32(
655 model,
656 &format!("{prefix}post_attention_layernorm.weight"),
657 ov,
658 )
659 .map_err(err)?
660 },
661 attn_out_norm: if sandwich {
662 Some(
663 load_f32(
664 model,
665 &format!("{prefix}post_attention_layernorm.weight"),
666 ov,
667 )
668 .map_err(err)?,
669 )
670 } else {
671 None
672 },
673 ffn_out_norm: if sandwich {
674 Some(
675 load_f32(
676 model,
677 &format!("{prefix}post_feedforward_layernorm.weight"),
678 ov,
679 )
680 .map_err(err)?,
681 )
682 } else {
683 None
684 },
685 layer_scale: model
687 .tensor(&format!("{prefix}layer_scalar"))
688 .and_then(|_| {
689 load_f32(model, &format!("{prefix}layer_scalar"), ov)
690 .ok()
691 .and_then(|v| v.first().copied())
692 }),
693 ffn: build_layer_ffn(model, &arch, li, false, ov)?,
695 attn,
696 });
697 }
698
699 let mtp = if let Some(cfg) = &arch.mtp {
701 if cfg.num_layers != 1 {
702 return Err(CmfError::Parse(format!(
703 "MTP with {} blocks not supported yet (only 1)",
704 cfg.num_layers
705 )));
706 }
707 let p = "model.mtp.";
708 let attn = load_full_attn("model.mtp.layers.0.", None)?;
709 Some(MtpModule {
710 enorm: load_f32(model, &format!("{p}enorm.weight"), ov).map_err(err)?,
711 hnorm: load_f32(model, &format!("{p}hnorm.weight"), ov).map_err(err)?,
712 eh_proj: load_matrix(model, &format!("{p}eh_proj.weight"), false, ov)?,
713 layer: LayerWeights {
714 attn_out_norm: None,
715 ffn_out_norm: None,
716 layer_scale: None,
717 input_norm: load_f32(model, &format!("{p}layers.0.input_layernorm.weight"), ov)
718 .map_err(err)?,
719 post_norm: load_f32(
720 model,
721 &format!("{p}layers.0.post_attention_layernorm.weight"),
722 ov,
723 )
724 .map_err(err)?,
725 ffn: FfnKind::Dense(DenseFfn {
726 gate_proj: load_matrix(
727 model,
728 &format!("{p}layers.0.mlp.gate_proj.weight"),
729 false,
730 ov,
731 )?,
732 up_proj: load_matrix(
733 model,
734 &format!("{p}layers.0.mlp.up_proj.weight"),
735 false,
736 ov,
737 )?,
738 down_proj: load_matrix(
739 model,
740 &format!("{p}layers.0.mlp.down_proj.weight"),
741 false,
742 ov,
743 )?,
744 act: crate::pipeline::Act::from_arch(&arch.hidden_act),
745 }),
746 attn,
747 },
748 final_norm: load_f32(model, &format!("{p}norm.weight"), ov).map_err(err)?,
749 kv: LayerKvCache::new(arch.num_kv_heads, arch.head_dim),
750 })
751 } else {
752 None
753 };
754
755 tracing::info!(
756 "Pipeline loaded: {} | {}L ({} linear) | {:.2}B params | storage: {} | MTP: {}",
757 arch.arch_name,
758 arch.num_layers,
759 arch.layer_types
760 .iter()
761 .filter(|t| matches!(t, LayerType::LinearAttention))
762 .count(),
763 model.total_param_count() as f64 / 1e9,
764 if force_f32 {
765 "f32 (masked)"
766 } else {
767 "quantized mmap"
768 },
769 if mtp.is_some() { "yes" } else { "no" }
770 );
771
772 let cap = std::env::var("CMF_MAX_SEQ")
775 .ok()
776 .and_then(|v| v.parse::<usize>().ok())
777 .unwrap_or(8192);
778 let max_seq_len = arch.max_position_embeddings.min(cap);
779
780 let total_layers = arch.num_layers * arch.num_loops;
782
783 let mut pipeline = Pipeline::new(
784 tokenizer,
785 PipelineWeights {
786 embed_tokens,
787 layers,
788 lm_head,
789 final_norm,
790 },
791 arch.hidden_size,
792 arch.intermediate_size,
793 arch.num_attention_heads,
794 arch.num_kv_heads,
795 arch.head_dim,
796 total_layers,
797 arch.num_layers, arch.loop_final_norm,
799 arch.vocab_size,
800 arch.rms_norm_eps,
801 arch.rope_theta as f32,
802 arch.norm_style,
803 max_seq_len,
804 sampler_config,
805 );
806 let rotary = ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2);
807 pipeline.set_rotary(rotary, arch.rope_theta as f32);
808 pipeline.attention_heads_per_layer = arch.attention_heads_per_layer.clone();
809 if let Some(yarn) = &arch.yarn {
810 pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
811 rotary,
812 arch.rope_theta as f32,
813 yarn.factor,
814 yarn.original_max_position_embeddings,
815 yarn.beta_fast,
816 yarn.beta_slow,
817 ));
818 pipeline.rope_scale = yarn.attention_factor;
819 }
820 pipeline.embed_multiplier = arch.embed_multiplier;
824 if let Some(qpas) = arch.query_pre_attn_scalar {
825 pipeline.attn_scale = 1.0 / (qpas as f32).sqrt();
826 }
827 if let (Some(w), Some(p)) = (arch.sliding_window, arch.sliding_window_pattern) {
828 pipeline.swa = Some((w, p));
829 if let Some(base) = arch.rope_local_base_freq {
830 pipeline.inv_freq_local = Some(std::sync::Arc::new(
831 crate::attention::rope_inv_freq(rotary, base as f32),
832 ));
833 }
834 }
835 let explicit_sliding: Vec<bool> = arch
836 .layer_types
837 .iter()
838 .map(|t| matches!(t, cortiq_core::LayerType::SlidingAttention))
839 .collect();
840 if explicit_sliding.iter().any(|&v| v) {
841 pipeline.sliding_layers = Some(explicit_sliding);
842 if let Some(w) = arch.sliding_window {
843 pipeline.swa = Some((w, usize::MAX));
844 }
845 let local_rotary = ((arch.head_dim as f32
846 * arch
847 .local_partial_rotary_factor
848 .unwrap_or(arch.partial_rotary_factor))
849 as usize)
850 .max(2);
851 pipeline.rotary_dim_local = Some(local_rotary);
852 if let Some(base) = arch.rope_local_base_freq {
853 pipeline.inv_freq_local = Some(std::sync::Arc::new(
854 crate::attention::rope_inv_freq(local_rotary, base as f32),
855 ));
856 }
857 }
858 if let (Some(ghd), Some(gkv)) = (arch.global_head_dim, arch.num_global_kv_heads) {
862 pipeline.global_attn = Some((ghd, gkv));
863 let prf = arch.global_partial_rotary_factor.unwrap_or(1.0);
864 let half = ghd / 2;
865 let ra = (((prf * ghd as f32) as usize) / 2).min(half);
866 let mut f = vec![0.0f32; half];
867 for (i, slot) in f.iter_mut().enumerate().take(ra) {
868 *slot = 1.0 / (arch.rope_theta as f32).powf(2.0 * i as f32 / ghd as f32);
869 }
870 pipeline.inv_freq_global = Some(std::sync::Arc::new(f));
871 if let Some((_, p)) = pipeline.swa {
873 for li in 0..arch.num_layers {
874 if (li + 1) % p.max(1) == 0 {
875 pipeline.kv_cache.layers[li] = crate::kv_cache::LayerKvCache::new(gkv, ghd);
876 }
877 }
878 }
879 }
880 pipeline.attn_v_norm = arch.attn_v_norm;
881 pipeline.final_softcap = arch.final_logit_softcapping.map(|c| c as f32);
882 pipeline.vmf_cfg = vmf_cfg;
883 pipeline.gdn_cfg = gdn_cfg;
884 pipeline.short_conv_cfg = short_conv_cfg;
885 pipeline.mtp = mtp;
886 pipeline.install_dynamic_routing(model, false);
887 match ov {
891 Overlay::One(sid) => {
892 pipeline.dyn_active = model.header.skills.iter().position(|s| &s.id == sid);
893 }
894 Overlay::Blend(_) => pipeline.dyn_blend_loaded = true,
895 Overlay::None => {}
896 }
897 if let Some(c) = &model.header.calibration {
900 pipeline.set_calib_temp(c.temperature);
901 }
902 let o1 = match crate::nystrom::o1_from_env() {
908 crate::nystrom::O1Env::Off => None,
909 crate::nystrom::O1Env::On(cfg) => Some(cfg),
910 crate::nystrom::O1Env::Unset => model
911 .header
912 .provenance
913 .as_ref()
914 .and_then(|p| p.get("o1_attn"))
915 .and_then(crate::nystrom::O1Cfg::from_json),
916 };
917 if o1.is_some() {
918 pipeline.set_o1(o1);
919 }
920 Ok(pipeline)
921 }
922
923 pub(crate) fn install_dynamic_routing(&mut self, model: &Arc<CmfModel>, force_f32: bool) {
928 self.model = Some(model.clone());
929 self.dyn_force_f32 = force_f32;
930 let mut per_skill = Vec::with_capacity(model.header.skills.len());
931 for sk in &model.header.skills {
932 let mut ffn_layers = std::collections::BTreeSet::new();
933 let mut non_ffn = false;
934 let prefix = format!("skill.{}.", sk.id);
935 for t in model.skill_tensors(&sk.id) {
936 let rel = &t.name[prefix.len()..]; let toks: Vec<&str> = rel.split('.').collect();
938 if toks.len() >= 5 && toks[0] == "model" && toks[1] == "layers" && toks[3] == "mlp"
939 {
940 if let Ok(li) = toks[2].parse::<usize>() {
941 ffn_layers.insert(li);
942 continue;
943 }
944 }
945 non_ffn = true; }
947 if non_ffn {
948 tracing::warn!(
949 "skill '{}' replaces non-FFN tensors — excluded from dynamic \
950 routing (static overlay still works)",
951 sk.id
952 );
953 per_skill.push(None);
954 } else {
955 per_skill.push(Some(ffn_layers.into_iter().collect::<Vec<_>>()));
956 }
957 }
958 self.dyn_skill_layers = per_skill;
959 }
960
961 pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
968 if self.dyn_active == idx {
969 return Ok(());
970 }
971 let model = self.model.clone().ok_or_else(|| {
972 CmfError::Parse("dynamic routing needs a model-backed pipeline".into())
973 })?;
974 let mut union: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
975 if let Some(old) = self.dyn_active {
976 if let Some(Some(ls)) = self.dyn_skill_layers.get(old) {
977 union.extend(ls.iter().copied());
978 }
979 }
980 let new_id: Option<String> = match idx {
981 Some(n) => match self.dyn_skill_layers.get(n) {
982 Some(Some(ls)) => {
983 union.extend(ls.iter().copied());
984 Some(model.header.skills[n].id.clone())
985 }
986 _ => {
987 return Err(CmfError::Parse(format!(
988 "skill index {n} not dynamic-eligible"
989 )));
990 }
991 },
992 None => None,
993 };
994 let ov = match &new_id {
995 Some(s) => Overlay::One(s),
996 None => Overlay::None,
997 };
998 let arch = model.arch();
999 for li in union {
1000 self.weights.layers[li].ffn =
1001 build_layer_ffn(&model, arch, li, self.dyn_force_f32, &ov)?;
1002 }
1003 self.dyn_active = idx;
1004 Ok(())
1005 }
1006}