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