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() { &sname } else { name };
64 let t = dequant_by_name(model, src)?;
65 match &mut acc {
66 None => {
67 let mut t = t;
68 for v in t.iter_mut() {
69 *v *= w;
70 }
71 acc = Some(t);
72 }
73 Some(a) => {
74 for (av, tv) in a.iter_mut().zip(&t) {
75 *av += w * tv;
76 }
77 }
78 }
79 }
80 acc.ok_or_else(|| "empty blend".into())
81}
82
83fn load_f32(model: &CmfModel, name: &str, ov: &Overlay) -> Result<Vec<f32>, String> {
85 if ov.blend_touches(model, name) {
86 if let Overlay::Blend(list) = ov {
87 return blend_f32(model, name, list);
88 }
89 }
90 let skill = match ov {
91 Overlay::One(s) => Some(*s),
92 _ => None,
93 };
94 let entry = model
95 .resolve_tensor(name, skill)
96 .ok_or_else(|| format!("tensor '{name}' not found in CMF directory"))?;
97 let bytes = model.entry_bytes(entry);
98 let mut out = vec![0.0f32; entry.n_elems()];
99 dequant_tensor(entry, bytes, &mut out)?;
100 Ok(out)
101}
102
103pub(crate) fn build_layer_ffn(
109 model: &Arc<CmfModel>,
110 arch: &ModelArch,
111 li: usize,
112 force_f32: bool,
113 ov: &Overlay,
114) -> Result<FfnKind, CmfError> {
115 let prefix = format!("model.layers.{li}.");
116 let load_dense = |p: &str| -> Result<DenseFfn, CmfError> {
117 let gate_proj = load_matrix(model, &format!("{p}gate_proj.weight"), force_f32, ov)?;
118 let up_proj = load_matrix(model, &format!("{p}up_proj.weight"), force_f32, ov)?;
119 let down_proj = load_matrix(model, &format!("{p}down_proj.weight"), force_f32, ov)?;
120 let inter = gate_proj.rows();
124 if up_proj.rows() != inter || down_proj.cols() != inter {
125 return Err(CmfError::Parse(format!(
126 "{p}: FFN dims disagree (gate.rows={inter}, up.rows={}, \
127 down.cols={}); all three must equal inter'",
128 up_proj.rows(),
129 down_proj.cols()
130 )));
131 }
132 if down_proj.rows() != arch.hidden_size {
133 return Err(CmfError::Parse(format!(
134 "{p}: down_proj.rows={} != hidden_size={}",
135 down_proj.rows(),
136 arch.hidden_size
137 )));
138 }
139 Ok(DenseFfn {
140 gate_proj,
141 up_proj,
142 down_proj,
143 act: crate::pipeline::Act::from_arch(&arch.hidden_act),
144 })
145 };
146 let router_name = format!("{prefix}mlp.gate.weight");
147 if model.tensor(&router_name).is_none() {
148 return Ok(FfnKind::Dense(load_dense(&format!("{prefix}mlp."))?));
149 }
150 let cfg = arch.moe.as_ref().ok_or_else(|| {
151 CmfError::Parse(format!("{router_name} present but header has no arch.moe block"))
152 })?;
153 let experts = (0..cfg.num_experts)
154 .map(|e| load_dense(&format!("{prefix}mlp.experts.{e}.")))
155 .collect::<Result<Vec<_>, _>>()?;
156 let shared = if model
157 .tensor(&format!("{prefix}mlp.shared_expert.gate_proj.weight"))
158 .is_some()
159 {
160 Some((
161 load_dense(&format!("{prefix}mlp.shared_expert."))?,
162 load_matrix(model, &format!("{prefix}mlp.shared_expert_gate.weight"), force_f32, ov)?,
163 ))
164 } else {
165 None
166 };
167 let bias_name = format!("{prefix}mlp.expert_bias");
170 let expert_bias = if model.tensor(&bias_name).is_some() {
171 Some(load_f32(model, &bias_name, ov).map_err(CmfError::Parse)?)
172 } else {
173 None
174 };
175 Ok(FfnKind::Moe(MoeFfn {
176 router: load_matrix(model, &router_name, force_f32, ov)?,
177 experts,
178 top_k: cfg.top_k,
179 norm_topk_prob: cfg.norm_topk_prob,
180 router_sigmoid: cfg.router_sigmoid,
181 expert_bias,
182 routed_scaling: cfg.routed_scaling_factor.unwrap_or(1.0),
183 shared,
184 stats: std::cell::RefCell::new(Vec::new()),
185 }))
186}
187
188fn load_matrix(
189 model: &Arc<CmfModel>,
190 name: &str,
191 force_f32: bool,
192 ov: &Overlay,
193) -> Result<QTensor, CmfError> {
194 if ov.blend_touches(model, name) {
198 if let Overlay::Blend(list) = ov {
199 let entry = model
200 .tensor(name)
201 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
202 let data = blend_f32(model, name, list)
203 .map_err(|e| CmfError::Parse(format!("blend: {e}")))?;
204 return Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]));
205 }
206 }
207 let skill = match ov {
208 Overlay::One(s) => Some(*s),
209 _ => None,
210 };
211 let name: &str = &match skill {
214 Some(sid) if model.tensor(&format!("skill.{sid}.{name}")).is_some() => {
215 format!("skill.{sid}.{name}")
216 }
217 _ => name.to_string(),
218 };
219 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
220 if force_f32 {
221 let entry = model
222 .tensor(name)
223 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
224 if entry.shape.len() != 2 {
225 return Err(err(format!("'{name}' is not 2-D")));
226 }
227 let data = load_f32(model, name, &Overlay::None).map_err(err)?;
228 Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]))
229 } else {
230 QTensor::from_model(model, name).map_err(err)
231 }
232}
233
234impl Pipeline {
235 pub fn from_model(model: &Arc<CmfModel>, sampler_config: SamplerConfig) -> Result<Self, CmfError> {
237 Self::from_model_with_skill(model, sampler_config, None)
238 }
239
240 pub fn from_model_with_skill(
246 model: &Arc<CmfModel>,
247 sampler_config: SamplerConfig,
248 skill: Option<&str>,
249 ) -> Result<Self, CmfError> {
250 match skill {
251 Some(s) => Self::from_model_with_overlay(model, sampler_config, &Overlay::One(s)),
252 None => Self::from_model_with_overlay(model, sampler_config, &Overlay::None),
253 }
254 }
255
256 pub fn from_model_with_blend(
259 model: &Arc<CmfModel>,
260 sampler_config: SamplerConfig,
261 blend: &[(String, f32)],
262 ) -> Result<Self, CmfError> {
263 Self::from_model_with_overlay(model, sampler_config, &Overlay::Blend(blend))
264 }
265
266 fn from_model_with_overlay(
267 model: &Arc<CmfModel>,
268 sampler_config: SamplerConfig,
269 ov: &Overlay,
270 ) -> Result<Self, CmfError> {
271 let skill = match ov {
272 Overlay::One(s) => Some(*s),
273 _ => None,
274 };
275 if let Some(sid) = skill {
276 let known = model.header.skills.iter().any(|s| s.id == sid)
277 || model.skill_tensors(sid).next().is_some();
278 if !known {
279 return Err(CmfError::Parse(format!(
280 "skill '{sid}' not in this container (header.skills: {:?})",
281 model.header.skills.iter().map(|s| &s.id).collect::<Vec<_>>()
282 )));
283 }
284 tracing::info!(
285 "skill '{sid}': {} replacement tensors overlaid",
286 model.skill_tensors(sid).count()
287 );
288 }
289 let arch = model.arch().clone();
290 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
291
292 let masks_present = !model.masks.masks.is_empty();
298 let force_f32 = masks_present; let mut tokenizer = if let Some(vocab_bytes) = &model.vocab {
302 Tokenizer::from_bytes(vocab_bytes)
303 .map_err(|e| CmfError::Parse(format!("embedded tokenizer: {e}")))?
304 } else {
305 let sidecar = model.path.with_file_name("tokenizer.json");
306 if sidecar.exists() {
307 Tokenizer::from_file(&sidecar)
308 .map_err(|e| CmfError::Parse(format!("sidecar tokenizer: {e}")))?
309 } else {
310 tracing::warn!("no tokenizer in file or sidecar — using byte-level fallback");
311 Tokenizer::byte_level()
312 }
313 };
314 if let Some(tc) = &model.header.tokenizer_config {
316 tokenizer.chat_template = tc.chat_template.clone();
317 tokenizer.extra_eos.extend(tc.eos_token_ids.iter().copied());
318 if tokenizer.bos_token_id.is_none() {
319 tokenizer.bos_token_id = tc.bos_token_id;
320 }
321 tracing::info!(
322 "chat bundle: template {} chars, {} stop ids",
323 tc.chat_template.as_deref().map(str::len).unwrap_or(0),
324 tc.eos_token_ids.len()
325 );
326 }
327 if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
331 tokenizer.add_bos = true;
332 }
333
334 let embed_tokens = load_matrix(model, "model.embed_tokens.weight", false, ov)?;
336 let final_norm = load_f32(model, "model.norm.weight", ov).map_err(err)?;
337 let lm_head = if model.tensor("lm_head.weight").is_some() {
338 load_matrix(model, "lm_head.weight", false, ov)?
339 } else if arch.tie_word_embeddings {
340 load_matrix(model, "model.embed_tokens.weight", false, ov)?
342 } else {
343 return Err(CmfError::MissingTensor(
344 "lm_head.weight (and tie_word_embeddings is false)".into(),
345 ));
346 };
347
348 let has_linear = arch
350 .layer_types
351 .iter()
352 .any(|t| matches!(t, LayerType::LinearAttention));
353 let mut vmf_cfg = None;
354 let mut gdn_cfg = None;
355 if has_linear {
356 let lc = arch.linear_core.as_ref().ok_or_else(|| {
357 CmfError::Parse(
358 "model has LinearAttention layers but no arch.linear_core — \
359 reconvert with the current converter"
360 .into(),
361 )
362 })?;
363 let need = |v: Option<usize>, name: &str| {
364 v.ok_or_else(|| CmfError::Parse(format!("linear core needs arch.{name}")))
365 };
366 match lc.kind.as_str() {
367 "vmf_phase" => {
368 vmf_cfg = Some(VmfPhaseCfg {
369 num_heads: lc.num_heads,
370 nphase: need(lc.nphase, "linear_core.nphase")?,
371 value_head_dim: lc.value_head_dim,
372 hidden_size: arch.hidden_size,
373 phase_mass: std::env::var("CMF_PHASE_MASS")
376 .ok()
377 .and_then(|v| v.parse().ok())
378 .unwrap_or(0.0),
379 });
380 }
381 "gated_delta_net" => {
382 gdn_cfg = Some(GdnCfg {
383 num_v_heads: lc.num_heads,
384 num_k_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
385 key_head_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
386 value_head_dim: lc.value_head_dim,
387 conv_kernel: need(
388 arch.linear_conv_kernel_dim,
389 "linear_conv_kernel_dim",
390 )?,
391 hidden_size: arch.hidden_size,
392 rms_eps: arch.rms_norm_eps as f64,
393 });
394 }
395 other => {
396 return Err(CmfError::Parse(format!(
397 "unknown linear core '{other}' (this runtime executes: \
398 gated_delta_net, vmf_phase)"
399 )));
400 }
401 }
402 }
403
404 let has_short_conv =
406 arch.layer_types.iter().any(|t| matches!(t, LayerType::ShortConv));
407 let short_conv_cfg = if has_short_conv {
408 Some(ShortConvCfg {
409 hidden_size: arch.hidden_size,
410 kernel: arch.linear_conv_kernel_dim.ok_or_else(|| {
411 CmfError::Parse(
412 "model has ShortConv layers but no arch.linear_conv_kernel_dim — \
413 reconvert with the current converter"
414 .into(),
415 )
416 })?,
417 })
418 } else {
419 None
420 };
421
422 let load_full_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
424 let t = |suffix: &str| load_matrix(model, &format!("{prefix}{suffix}"), force_f32, ov);
425 let n = |suffix: &str| -> Option<Vec<f32>> {
426 model
427 .tensor(&format!("{prefix}{suffix}"))
428 .and_then(|_| load_f32(model, &format!("{prefix}{suffix}"), ov).ok())
429 };
430 let wq = t("self_attn.q_proj.weight")?;
431 let output_gate = arch.global_head_dim.is_none()
435 && wq.rows() == 2 * arch.num_attention_heads * arch.head_dim;
436 let bias = match (
438 n("self_attn.q_proj.bias"),
439 n("self_attn.k_proj.bias"),
440 n("self_attn.v_proj.bias"),
441 ) {
442 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
443 _ => None,
444 };
445 Ok(AttnKind::Full {
446 wq,
447 wk: t("self_attn.k_proj.weight")?,
448 wv: t("self_attn.v_proj.weight")?,
449 wo: t("self_attn.o_proj.weight")?,
450 q_norm: n("self_attn.q_norm.weight"),
451 k_norm: n("self_attn.k_norm.weight"),
452 output_gate,
453 bias,
454 })
455 };
456
457 let load_linear_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
458 if gdn_cfg.is_some() {
459 let t = |suffix: &str| {
461 load_matrix(model, &format!("{prefix}linear_attn.{suffix}"), force_f32, ov)
462 };
463 let f = |suffix: &str| {
464 load_f32(model, &format!("{prefix}linear_attn.{suffix}"), ov).map_err(err)
465 };
466 return Ok(AttnKind::LinearGdn(GdnWeights {
467 in_proj_qkv: t("in_proj_qkv.weight")?,
468 in_proj_z: t("in_proj_z.weight")?,
469 in_proj_a: t("in_proj_a.weight")?,
470 in_proj_b: t("in_proj_b.weight")?,
471 conv1d: f("conv1d.weight")?,
472 a_log: f("A_log")?,
473 dt_bias: f("dt_bias")?,
474 norm: f("norm.weight")?,
475 out_proj: t("out_proj.weight")?,
476 }));
477 }
478 let t = |suffix: &str| load_matrix(model, &format!("{prefix}vmf_attn.{suffix}"), force_f32, ov);
479 let a_log = load_f32(model, &format!("{prefix}vmf_attn.A_log"), ov).map_err(err)?;
480 let k_gate = if model
484 .tensor(&format!("{prefix}vmf_attn.k_gate.weight"))
485 .is_some()
486 {
487 Some((
488 t("k_gate.weight")?,
489 load_f32(model, &format!("{prefix}vmf_attn.k_gate.bias"), ov).map_err(err)?,
490 ))
491 } else {
492 None
493 };
494 Ok(AttnKind::Linear(VmfPhaseWeights {
495 thq: t("thq.weight")?,
496 thk: t("thk.weight")?,
497 v_proj: t("v_proj.weight")?,
498 out_proj: t("out_proj.weight")?,
499 decay: a_log
500 .iter()
501 .map(|&a| (-(a as f64).exp()).exp())
502 .collect(),
503 k_gate,
504 }))
505 };
506
507 let load_short_conv = |prefix: &str| -> Result<AttnKind, CmfError> {
511 let t = |suffix: &str| {
512 load_matrix(model, &format!("{prefix}short_conv.{suffix}"), force_f32, ov)
513 };
514 Ok(AttnKind::ShortConv(ShortConvWeights {
515 in_proj: t("in_proj.weight")?,
516 conv: load_f32(model, &format!("{prefix}short_conv.conv.weight"), ov)
517 .map_err(err)?,
518 out_proj: t("out_proj.weight")?,
519 }))
520 };
521
522 let mut layers = Vec::with_capacity(arch.num_layers);
523 for li in 0..arch.num_layers {
524 let prefix = format!("model.layers.{li}.");
525 let attn = match arch.layer_types.get(li) {
526 Some(LayerType::LinearAttention) => load_linear_attn(&prefix)?,
527 Some(LayerType::ShortConv) => load_short_conv(&prefix)?,
528 _ => load_full_attn(&prefix)?,
529 };
530 let pre_ffn = format!("{prefix}pre_feedforward_layernorm.weight");
534 let sandwich = model.tensor(&pre_ffn).is_some();
535 layers.push(LayerWeights {
536 input_norm: load_f32(model, &format!("{prefix}input_layernorm.weight"), ov).map_err(err)?,
537 post_norm: if sandwich {
538 load_f32(model, &pre_ffn, ov).map_err(err)?
539 } else {
540 load_f32(model, &format!("{prefix}post_attention_layernorm.weight"), ov)
541 .map_err(err)?
542 },
543 attn_out_norm: if sandwich {
544 Some(
545 load_f32(model, &format!("{prefix}post_attention_layernorm.weight"), ov)
546 .map_err(err)?,
547 )
548 } else {
549 None
550 },
551 ffn_out_norm: if sandwich {
552 Some(
553 load_f32(model, &format!("{prefix}post_feedforward_layernorm.weight"), ov)
554 .map_err(err)?,
555 )
556 } else {
557 None
558 },
559 layer_scale: model
561 .tensor(&format!("{prefix}layer_scalar"))
562 .and_then(|_| {
563 load_f32(model, &format!("{prefix}layer_scalar"), ov)
564 .ok()
565 .and_then(|v| v.first().copied())
566 }),
567 ffn: build_layer_ffn(model, &arch, li, false, ov)?,
569 attn,
570 });
571 }
572
573 let mtp = if let Some(cfg) = &arch.mtp {
575 if cfg.num_layers != 1 {
576 return Err(CmfError::Parse(format!(
577 "MTP with {} blocks not supported yet (only 1)",
578 cfg.num_layers
579 )));
580 }
581 let p = "model.mtp.";
582 let attn = load_full_attn("model.mtp.layers.0.")?;
583 Some(MtpModule {
584 enorm: load_f32(model, &format!("{p}enorm.weight"), ov).map_err(err)?,
585 hnorm: load_f32(model, &format!("{p}hnorm.weight"), ov).map_err(err)?,
586 eh_proj: load_matrix(model, &format!("{p}eh_proj.weight"), false, ov)?,
587 layer: LayerWeights {
588 attn_out_norm: None,
589 ffn_out_norm: None,
590 layer_scale: None,
591 input_norm: load_f32(model, &format!("{p}layers.0.input_layernorm.weight"), ov)
592 .map_err(err)?,
593 post_norm: load_f32(
594 model,
595 &format!("{p}layers.0.post_attention_layernorm.weight"),
596 ov,
597 )
598 .map_err(err)?,
599 ffn: FfnKind::Dense(DenseFfn {
600 gate_proj: load_matrix(model, &format!("{p}layers.0.mlp.gate_proj.weight"), false, ov)?,
601 up_proj: load_matrix(model, &format!("{p}layers.0.mlp.up_proj.weight"), false, ov)?,
602 down_proj: load_matrix(model, &format!("{p}layers.0.mlp.down_proj.weight"), false, ov)?,
603 act: crate::pipeline::Act::from_arch(&arch.hidden_act),
604 }),
605 attn,
606 },
607 final_norm: load_f32(model, &format!("{p}norm.weight"), ov).map_err(err)?,
608 kv: LayerKvCache::new(arch.num_kv_heads, arch.head_dim),
609 })
610 } else {
611 None
612 };
613
614 tracing::info!(
615 "Pipeline loaded: {} | {}L ({} linear) | {:.2}B params | storage: {} | MTP: {}",
616 arch.arch_name,
617 arch.num_layers,
618 arch.layer_types
619 .iter()
620 .filter(|t| matches!(t, LayerType::LinearAttention))
621 .count(),
622 model.total_param_count() as f64 / 1e9,
623 if force_f32 { "f32 (masked)" } else { "quantized mmap" },
624 if mtp.is_some() { "yes" } else { "no" }
625 );
626
627 let cap = std::env::var("CMF_MAX_SEQ")
630 .ok()
631 .and_then(|v| v.parse::<usize>().ok())
632 .unwrap_or(8192);
633 let max_seq_len = arch.max_position_embeddings.min(cap);
634
635 let mut pipeline = Pipeline::new(
636 tokenizer,
637 PipelineWeights {
638 embed_tokens,
639 layers,
640 lm_head,
641 final_norm,
642 },
643 arch.hidden_size,
644 arch.intermediate_size,
645 arch.num_attention_heads,
646 arch.num_kv_heads,
647 arch.head_dim,
648 arch.num_layers,
649 arch.vocab_size,
650 arch.rms_norm_eps,
651 arch.rope_theta as f32,
652 arch.norm_style,
653 max_seq_len,
654 sampler_config,
655 );
656 let rotary = ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2);
657 pipeline.set_rotary(rotary, arch.rope_theta as f32);
658 pipeline.embed_multiplier = arch.embed_multiplier;
662 if let Some(qpas) = arch.query_pre_attn_scalar {
663 pipeline.attn_scale = 1.0 / (qpas as f32).sqrt();
664 }
665 if let (Some(w), Some(p)) = (arch.sliding_window, arch.sliding_window_pattern) {
666 pipeline.swa = Some((w, p));
667 if let Some(base) = arch.rope_local_base_freq {
668 pipeline.inv_freq_local = Some(std::sync::Arc::new(
669 crate::attention::rope_inv_freq(rotary, base as f32),
670 ));
671 }
672 }
673 if let (Some(ghd), Some(gkv)) = (arch.global_head_dim, arch.num_global_kv_heads) {
677 pipeline.global_attn = Some((ghd, gkv));
678 let prf = arch.global_partial_rotary_factor.unwrap_or(1.0);
679 let half = ghd / 2;
680 let ra = (((prf * ghd as f32) as usize) / 2).min(half);
681 let mut f = vec![0.0f32; half];
682 for (i, slot) in f.iter_mut().enumerate().take(ra) {
683 *slot = 1.0 / (arch.rope_theta as f32).powf(2.0 * i as f32 / ghd as f32);
684 }
685 pipeline.inv_freq_global = Some(std::sync::Arc::new(f));
686 if let Some((_, p)) = pipeline.swa {
688 for li in 0..arch.num_layers {
689 if (li + 1) % p.max(1) == 0 {
690 pipeline.kv_cache.layers[li] =
691 crate::kv_cache::LayerKvCache::new(gkv, ghd);
692 }
693 }
694 }
695 }
696 pipeline.attn_v_norm = arch.attn_v_norm;
697 pipeline.final_softcap = arch.final_logit_softcapping.map(|c| c as f32);
698 pipeline.vmf_cfg = vmf_cfg;
699 pipeline.gdn_cfg = gdn_cfg;
700 pipeline.short_conv_cfg = short_conv_cfg;
701 pipeline.mtp = mtp;
702 pipeline.install_dynamic_routing(model, false);
703 match ov {
707 Overlay::One(sid) => {
708 pipeline.dyn_active = model.header.skills.iter().position(|s| &s.id == sid);
709 }
710 Overlay::Blend(_) => pipeline.dyn_blend_loaded = true,
711 Overlay::None => {}
712 }
713 if let Some(c) = &model.header.calibration {
716 pipeline.set_calib_temp(c.temperature);
717 }
718 let o1 = match crate::nystrom::o1_from_env() {
724 crate::nystrom::O1Env::Off => None,
725 crate::nystrom::O1Env::On(cfg) => Some(cfg),
726 crate::nystrom::O1Env::Unset => model
727 .header
728 .provenance
729 .as_ref()
730 .and_then(|p| p.get("o1_attn"))
731 .and_then(crate::nystrom::O1Cfg::from_json),
732 };
733 if o1.is_some() {
734 pipeline.set_o1(o1);
735 }
736 Ok(pipeline)
737 }
738
739 pub(crate) fn install_dynamic_routing(
744 &mut self,
745 model: &Arc<CmfModel>,
746 force_f32: bool,
747 ) {
748 self.model = Some(model.clone());
749 self.dyn_force_f32 = force_f32;
750 let mut per_skill = Vec::with_capacity(model.header.skills.len());
751 for sk in &model.header.skills {
752 let mut ffn_layers = std::collections::BTreeSet::new();
753 let mut non_ffn = false;
754 let prefix = format!("skill.{}.", sk.id);
755 for t in model.skill_tensors(&sk.id) {
756 let rel = &t.name[prefix.len()..]; let toks: Vec<&str> = rel.split('.').collect();
758 if toks.len() >= 5
759 && toks[0] == "model"
760 && toks[1] == "layers"
761 && toks[3] == "mlp"
762 {
763 if let Ok(li) = toks[2].parse::<usize>() {
764 ffn_layers.insert(li);
765 continue;
766 }
767 }
768 non_ffn = true; }
770 if non_ffn {
771 tracing::warn!(
772 "skill '{}' replaces non-FFN tensors — excluded from dynamic \
773 routing (static overlay still works)",
774 sk.id
775 );
776 per_skill.push(None);
777 } else {
778 per_skill.push(Some(ffn_layers.into_iter().collect::<Vec<_>>()));
779 }
780 }
781 self.dyn_skill_layers = per_skill;
782 }
783
784 pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
791 if self.dyn_active == idx {
792 return Ok(());
793 }
794 let model = self
795 .model
796 .clone()
797 .ok_or_else(|| CmfError::Parse("dynamic routing needs a model-backed pipeline".into()))?;
798 let mut union: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
799 if let Some(old) = self.dyn_active {
800 if let Some(Some(ls)) = self.dyn_skill_layers.get(old) {
801 union.extend(ls.iter().copied());
802 }
803 }
804 let new_id: Option<String> = match idx {
805 Some(n) => match self.dyn_skill_layers.get(n) {
806 Some(Some(ls)) => {
807 union.extend(ls.iter().copied());
808 Some(model.header.skills[n].id.clone())
809 }
810 _ => {
811 return Err(CmfError::Parse(format!(
812 "skill index {n} not dynamic-eligible"
813 )))
814 }
815 },
816 None => None,
817 };
818 let ov = match &new_id {
819 Some(s) => Overlay::One(s),
820 None => Overlay::None,
821 };
822 let arch = model.arch();
823 for li in union {
824 self.weights.layers[li].ffn =
825 build_layer_ffn(&model, arch, li, self.dyn_force_f32, &ov)?;
826 }
827 self.dyn_active = idx;
828 Ok(())
829 }
830}