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 build_ffn_at(model, arch, &format!("model.layers.{li}."), force_f32, ov)
120}
121
122pub(crate) fn build_ffn_at(
127 model: &Arc<CmfModel>,
128 arch: &ModelArch,
129 prefix: &str,
130 force_f32: bool,
131 ov: &Overlay,
132) -> Result<FfnKind, CmfError> {
133 let prefix = prefix.to_string();
134 let load_dense = |p: &str| -> Result<DenseFfn, CmfError> {
135 let gate_proj = load_matrix(model, &format!("{p}gate_proj.weight"), force_f32, ov)?;
136 let up_proj = load_matrix(model, &format!("{p}up_proj.weight"), force_f32, ov)?;
137 let down_proj = load_matrix(model, &format!("{p}down_proj.weight"), force_f32, ov)?;
138 let inter = gate_proj.rows();
142 if up_proj.rows() != inter || down_proj.cols() != inter {
143 return Err(CmfError::Parse(format!(
144 "{p}: FFN dims disagree (gate.rows={inter}, up.rows={}, \
145 down.cols={}); all three must equal inter'",
146 up_proj.rows(),
147 down_proj.cols()
148 )));
149 }
150 if down_proj.rows() != arch.hidden_size {
151 return Err(CmfError::Parse(format!(
152 "{p}: down_proj.rows={} != hidden_size={}",
153 down_proj.rows(),
154 arch.hidden_size
155 )));
156 }
157 Ok(DenseFfn {
158 gate_proj,
159 up_proj,
160 down_proj,
161 act: crate::pipeline::Act::from_arch_full(arch),
162 })
163 };
164 let router_name = format!("{prefix}mlp.gate.weight");
165 if model.tensor(&router_name).is_none() {
166 return Ok(FfnKind::Dense(load_dense(&format!("{prefix}mlp."))?));
167 }
168 let cfg = arch.moe.as_ref().ok_or_else(|| {
169 CmfError::Parse(format!(
170 "{router_name} present but header has no arch.moe block"
171 ))
172 })?;
173 let mut experts = Vec::new();
178 for e in 0..cfg.num_experts {
179 if model
180 .tensor(&format!("{prefix}mlp.experts.{e}.gate_proj.weight"))
181 .is_none()
182 {
183 break;
184 }
185 experts.push(load_dense(&format!("{prefix}mlp.experts.{e}."))?);
186 }
187 if experts.is_empty() {
188 return Err(CmfError::Parse(format!(
189 "{prefix}: router present but no expert tensors"
190 )));
191 }
192 let shared = if model
193 .tensor(&format!("{prefix}mlp.shared_expert.gate_proj.weight"))
194 .is_some()
195 {
196 let gate_name = format!("{prefix}mlp.shared_expert_gate.weight");
197 Some((
198 load_dense(&format!("{prefix}mlp.shared_expert."))?,
199 if model.tensor(&gate_name).is_some() {
200 Some(load_matrix(model, &gate_name, force_f32, ov)?)
201 } else {
202 None
203 },
204 ))
205 } else {
206 None
207 };
208 let bias_name = format!("{prefix}mlp.expert_bias");
211 let expert_bias = if model.tensor(&bias_name).is_some() {
212 Some(load_f32(model, &bias_name, ov).map_err(CmfError::Parse)?)
213 } else {
214 None
215 };
216 let top_k = std::env::var("CMF_MOE_TOPK")
222 .ok()
223 .and_then(|v| v.parse::<usize>().ok())
224 .filter(|&k| k >= 1 && k <= cfg.top_k)
225 .inspect(|k| tracing::info!("MoE top_k override: {} (header {})", k, cfg.top_k))
226 .unwrap_or(cfg.top_k);
227 let route_tau = std::env::var("CMF_MOE_TAU")
229 .ok()
230 .and_then(|v| v.parse::<f32>().ok())
231 .filter(|&t| t > 0.0 && t < 1.0)
232 .inspect(|t| tracing::info!("MoE adaptive routing: tau {t}"));
233 let mask = moe_task_mask(&prefix, experts.len());
234 let router = load_matrix(model, &router_name, force_f32, ov)?;
235 if router.rows() != experts.len() {
236 return Err(CmfError::Parse(format!(
237 "{router_name}: {} rows != {} experts",
238 router.rows(),
239 experts.len()
240 )));
241 }
242 let top_k = top_k.min(experts.len());
243 let pes_name = format!("{prefix}mlp.per_expert_scale");
247 let per_expert_scale = if model.tensor(&pes_name).is_some() {
248 Some(load_f32(model, &pes_name, ov).map_err(CmfError::Parse)?)
249 } else {
250 None
251 };
252 let router_input_norm = per_expert_scale.is_some();
253 let moe = MoeFfn {
254 router,
255 experts,
256 top_k,
257 route_tau,
258 norm_topk_prob: cfg.norm_topk_prob,
259 router_sigmoid: cfg.router_sigmoid,
260 expert_bias,
261 routed_scaling: cfg.routed_scaling_factor.unwrap_or(1.0),
262 shared,
263 stats: std::cell::RefCell::new(Vec::new()),
264 act_sq: std::cell::RefCell::new(Vec::new()),
265 act_rows: std::cell::RefCell::new(Vec::new()),
266 mask,
267 per_expert_scale,
268 router_input_norm,
269 };
270 if model
273 .tensor(&format!("{prefix}mlp.gate_proj.weight"))
274 .is_some()
275 {
276 let norm = |suffix: &str| -> Result<Vec<f32>, CmfError> {
277 load_f32(model, &format!("{prefix}{suffix}.weight"), ov).map_err(CmfError::Parse)
278 };
279 return Ok(FfnKind::DenseMoe(Box::new(
280 crate::pipeline::DenseMoeFfn {
281 dense: load_dense(&format!("{prefix}mlp."))?,
282 moe,
283 post_norm_1: norm("post_feedforward_layernorm_1")?,
284 pre_norm_2: norm("pre_feedforward_layernorm_2")?,
285 post_norm_2: norm("post_feedforward_layernorm_2")?,
286 },
287 )));
288 }
289 Ok(FfnKind::Moe(moe))
290}
291
292fn moe_task_mask(prefix: &str, ne: usize) -> Option<Vec<bool>> {
300 use std::sync::OnceLock;
301 static CFG: OnceLock<Option<(std::collections::HashMap<usize, Vec<u64>>, f64)>> =
302 OnceLock::new();
303 let cfg = CFG.get_or_init(|| {
304 let path = std::env::var("CMF_MOE_MASK").ok()?;
305 let cover = std::env::var("CMF_MOE_MASK_COVER")
306 .ok()
307 .and_then(|v| v.parse::<f64>().ok())
308 .filter(|&c| c > 0.0 && c <= 1.0)
309 .unwrap_or(0.9);
310 let text = std::fs::read_to_string(&path)
311 .map_err(|e| tracing::warn!("CMF_MOE_MASK: cannot read {path}: {e}"))
312 .ok()?;
313 let map: std::collections::HashMap<String, Vec<u64>> =
314 serde_json::from_str(&text)
315 .map_err(|e| tracing::warn!("CMF_MOE_MASK: bad JSON in {path}: {e}"))
316 .ok()?;
317 tracing::info!("MoE task mask: {path}, cover {cover}");
318 Some((
319 map.into_iter()
320 .filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
321 .collect(),
322 cover,
323 ))
324 });
325 let (stats, cover) = cfg.as_ref()?;
326 let li: usize = prefix
328 .split("layers.")
329 .nth(1)?
330 .split('.')
331 .next()?
332 .parse()
333 .ok()?;
334 let counts = stats.get(&li)?;
335 if counts.len() != ne {
336 tracing::warn!("CMF_MOE_MASK: layer {li} has {} counts, model has {ne} experts — skipped", counts.len());
337 return None;
338 }
339 let total: u64 = counts.iter().sum();
340 if total == 0 {
341 return None;
342 }
343 let mut order: Vec<usize> = (0..ne).collect();
344 order.sort_unstable_by_key(|&e| std::cmp::Reverse(counts[e]));
345 let mut mask = vec![false; ne];
346 let mut acc = 0u64;
347 let mut kept = 0usize;
348 for &e in &order {
349 mask[e] = true;
350 acc += counts[e];
351 kept += 1;
352 if (acc as f64) >= cover * (total as f64) {
353 break;
354 }
355 }
356 tracing::info!("MoE task mask L{li}: {kept}/{ne} experts for {:.0}% mass", cover * 100.0);
357 Some(mask)
358}
359
360fn load_matrix(
361 model: &Arc<CmfModel>,
362 name: &str,
363 force_f32: bool,
364 ov: &Overlay,
365) -> Result<QTensor, CmfError> {
366 if ov.blend_touches(model, name) {
370 if let Overlay::Blend(list) = ov {
371 let entry = model
372 .tensor(name)
373 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
374 let data =
375 blend_f32(model, name, list).map_err(|e| CmfError::Parse(format!("blend: {e}")))?;
376 return Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]));
377 }
378 }
379 let skill = match ov {
380 Overlay::One(s) => Some(*s),
381 _ => None,
382 };
383 let name: &str = &match skill {
386 Some(sid) if model.tensor(&format!("skill.{sid}.{name}")).is_some() => {
387 format!("skill.{sid}.{name}")
388 }
389 _ => name.to_string(),
390 };
391 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
392 if force_f32 {
393 let entry = model
394 .tensor(name)
395 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
396 if entry.shape.len() != 2 {
397 return Err(err(format!("'{name}' is not 2-D")));
398 }
399 let data = load_f32(model, name, &Overlay::None).map_err(err)?;
400 Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]))
401 } else {
402 QTensor::from_model(model, name).map_err(err)
403 }
404}
405
406impl Pipeline {
407 pub fn from_model(
409 model: &Arc<CmfModel>,
410 sampler_config: SamplerConfig,
411 ) -> Result<Self, CmfError> {
412 Self::from_model_with_skill(model, sampler_config, None)
413 }
414
415 pub fn from_model_with_skill(
421 model: &Arc<CmfModel>,
422 sampler_config: SamplerConfig,
423 skill: Option<&str>,
424 ) -> Result<Self, CmfError> {
425 match skill {
426 Some(s) => Self::from_model_with_overlay(model, sampler_config, &Overlay::One(s)),
427 None => Self::from_model_with_overlay(model, sampler_config, &Overlay::None),
428 }
429 }
430
431 pub fn from_model_with_blend(
434 model: &Arc<CmfModel>,
435 sampler_config: SamplerConfig,
436 blend: &[(String, f32)],
437 ) -> Result<Self, CmfError> {
438 Self::from_model_with_overlay(model, sampler_config, &Overlay::Blend(blend))
439 }
440
441 fn from_model_with_overlay(
442 model: &Arc<CmfModel>,
443 sampler_config: SamplerConfig,
444 ov: &Overlay,
445 ) -> Result<Self, CmfError> {
446 let skill = match ov {
447 Overlay::One(s) => Some(*s),
448 _ => None,
449 };
450 if let Some(sid) = skill {
451 let known = model.header.skills.iter().any(|s| s.id == sid)
452 || model.skill_tensors(sid).next().is_some();
453 if !known {
454 return Err(CmfError::Parse(format!(
455 "skill '{sid}' not in this container (header.skills: {:?})",
456 model
457 .header
458 .skills
459 .iter()
460 .map(|s| &s.id)
461 .collect::<Vec<_>>()
462 )));
463 }
464 tracing::info!(
465 "skill '{sid}': {} replacement tensors overlaid",
466 model.skill_tensors(sid).count()
467 );
468 }
469 let arch = model.arch().clone();
470 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
471 if let Some(heads) = &arch.attention_heads_per_layer {
472 if heads.len() != arch.num_layers {
473 return Err(CmfError::Parse(format!(
474 "arch.attention_heads_per_layer has {} entries, expected {}",
475 heads.len(),
476 arch.num_layers
477 )));
478 }
479 if let Some((li, &nh)) = heads
480 .iter()
481 .enumerate()
482 .find(|(_, nh)| **nh == 0 || **nh % arch.num_kv_heads != 0)
483 {
484 return Err(CmfError::Parse(format!(
485 "layer {li} has {nh} Q heads, which must be nonzero and divisible by {} KV heads",
486 arch.num_kv_heads
487 )));
488 }
489 }
490 if arch
491 .layer_types
492 .iter()
493 .any(|t| matches!(t, LayerType::SlidingAttention))
494 && arch.sliding_window.is_none()
495 {
496 return Err(CmfError::Parse(
497 "model has SlidingAttention layers but no arch.sliding_window".into(),
498 ));
499 }
500
501 let masks_present = !model.masks.masks.is_empty();
507 let force_f32 = masks_present; let mut tokenizer = if let Some(vocab_bytes) = &model.vocab {
511 Tokenizer::from_bytes(vocab_bytes)
512 .map_err(|e| CmfError::Parse(format!("embedded tokenizer: {e}")))?
513 } else {
514 let sidecar = model.path.with_file_name("tokenizer.json");
515 if sidecar.exists() {
516 Tokenizer::from_file(&sidecar)
517 .map_err(|e| CmfError::Parse(format!("sidecar tokenizer: {e}")))?
518 } else {
519 tracing::warn!("no tokenizer in file or sidecar — using byte-level fallback");
520 Tokenizer::byte_level()
521 }
522 };
523 if let Some(tc) = &model.header.tokenizer_config {
525 tokenizer.chat_template = tc.chat_template.clone();
526 tokenizer.extra_eos.extend(tc.eos_token_ids.iter().copied());
527 if tokenizer.bos_token_id.is_none() {
528 tokenizer.bos_token_id = tc.bos_token_id;
529 }
530 tracing::info!(
531 "chat bundle: template {} chars, {} stop ids",
532 tc.chat_template.as_deref().map(str::len).unwrap_or(0),
533 tc.eos_token_ids.len()
534 );
535 }
536 if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
540 tokenizer.add_bos = true;
541 }
542
543 let embed_tokens = load_matrix(model, "model.embed_tokens.weight", false, ov)?;
545 let final_norm = load_f32(model, "model.norm.weight", ov).map_err(err)?;
546 let lm_head = if model.tensor("lm_head.weight").is_some() {
547 load_matrix(model, "lm_head.weight", false, ov)?
548 } else if arch.tie_word_embeddings {
549 load_matrix(model, "model.embed_tokens.weight", false, ov)?
551 } else {
552 return Err(CmfError::MissingTensor(
553 "lm_head.weight (and tie_word_embeddings is false)".into(),
554 ));
555 };
556
557 let has_linear = arch
559 .layer_types
560 .iter()
561 .any(|t| matches!(t, LayerType::LinearAttention));
562 let mut vmf_cfg = None;
563 let mut gdn_cfg = None;
564 if has_linear {
565 let lc = arch.linear_core.as_ref().ok_or_else(|| {
566 CmfError::Parse(
567 "model has LinearAttention layers but no arch.linear_core — \
568 reconvert with the current converter"
569 .into(),
570 )
571 })?;
572 let need = |v: Option<usize>, name: &str| {
573 v.ok_or_else(|| CmfError::Parse(format!("linear core needs arch.{name}")))
574 };
575 match lc.kind.as_str() {
576 "vmf_phase" => {
577 vmf_cfg = Some(VmfPhaseCfg {
578 num_heads: lc.num_heads,
579 nphase: need(lc.nphase, "linear_core.nphase")?,
580 value_head_dim: lc.value_head_dim,
581 hidden_size: arch.hidden_size,
582 phase_mass: std::env::var("CMF_PHASE_MASS")
585 .ok()
586 .and_then(|v| v.parse().ok())
587 .unwrap_or(0.0),
588 });
589 }
590 "gated_delta_net" => {
591 gdn_cfg = Some(GdnCfg {
592 num_v_heads: lc.num_heads,
593 num_k_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
594 key_head_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
595 value_head_dim: lc.value_head_dim,
596 conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
597 hidden_size: arch.hidden_size,
598 rms_eps: arch.rms_norm_eps,
599 });
600 }
601 other => {
602 return Err(CmfError::Parse(format!(
603 "unknown linear core '{other}' (this runtime executes: \
604 gated_delta_net, vmf_phase)"
605 )));
606 }
607 }
608 }
609
610 let has_kda = arch
612 .layer_types
613 .iter()
614 .any(|t| matches!(t, LayerType::Kda));
615 let kda_cfg = if has_kda {
616 let need = |v: Option<usize>, name: &str| {
617 v.ok_or_else(|| CmfError::Parse(format!("KDA core needs arch.{name}")))
618 };
619 Some(crate::linear_core::KdaCfg {
620 num_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
621 head_k_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
622 head_v_dim: need(arch.linear_value_head_dim, "linear_value_head_dim")?,
623 conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
624 hidden_size: arch.hidden_size,
625 rms_eps: arch.rms_norm_eps,
626 })
627 } else {
628 None
629 };
630
631 let has_short_conv = arch
633 .layer_types
634 .iter()
635 .any(|t| matches!(t, LayerType::ShortConv));
636 let short_conv_cfg = if has_short_conv {
637 Some(ShortConvCfg {
638 hidden_size: arch.hidden_size,
639 kernel: arch.linear_conv_kernel_dim.ok_or_else(|| {
640 CmfError::Parse(
641 "model has ShortConv layers but no arch.linear_conv_kernel_dim — \
642 reconvert with the current converter"
643 .into(),
644 )
645 })?,
646 })
647 } else {
648 None
649 };
650
651 let load_full_attn = |prefix: &str, layer: Option<usize>| -> Result<AttnKind, CmfError> {
653 let t = |suffix: &str| load_matrix(model, &format!("{prefix}{suffix}"), force_f32, ov);
654 let n = |suffix: &str| -> Option<Vec<f32>> {
655 model
656 .tensor(&format!("{prefix}{suffix}"))
657 .and_then(|_| load_f32(model, &format!("{prefix}{suffix}"), ov).ok())
658 };
659 if let Some(mla) = arch.mla.as_ref() {
661 let (q_proj, q_a, q_a_norm) = if mla.q_lora_rank.is_some() {
663 (
664 t("self_attn.q_b_proj.weight")?,
665 Some(t("self_attn.q_a_proj.weight")?),
666 Some(n("self_attn.q_a_layernorm.weight").ok_or_else(|| {
667 CmfError::Parse(format!("{prefix}: MLA needs q_a_layernorm"))
668 })?),
669 )
670 } else {
671 (t("self_attn.q_proj.weight")?, None, None)
672 };
673 let hd = mla.qk_rope_head_dim + mla.qk_nope_head_dim;
674 let nh = q_proj.rows() / hd;
675 let mut scale = 1.0 / (hd as f32).sqrt();
678 if let Some(y) = arch.yarn.as_ref() {
679 if let Some(m) = y.mscale_all_dim.filter(|&m| m > 0.0) {
680 let ms = 0.1 * m * y.factor.ln() + 1.0;
681 scale *= ms * ms;
682 }
683 }
684 return Ok(AttnKind::Mla(Box::new(crate::pipeline::MlaWeights {
685 q_proj,
686 q_a,
687 q_a_norm,
688 kv_a: t("self_attn.kv_a_proj_with_mqa.weight")?,
689 kv_a_norm: n("self_attn.kv_a_layernorm.weight").ok_or_else(|| {
690 CmfError::Parse(format!("{prefix}: MLA needs kv_a_layernorm"))
691 })?,
692 kv_b: t("self_attn.kv_b_proj.weight")?,
693 o_proj: t("self_attn.o_proj.weight")?,
694 nh,
695 qk_rope: mla.qk_rope_head_dim,
696 qk_nope: mla.qk_nope_head_dim,
697 v_dim: mla.v_head_dim,
698 lora: mla.kv_lora_rank,
699 scale,
700 nope: mla.nope,
701 })));
702 }
703 let wq = t("self_attn.q_proj.weight")?;
704 let nh = layer
705 .and_then(|li| {
706 arch.attention_heads_per_layer
707 .as_ref()
708 .and_then(|v| v.get(li).copied())
709 })
710 .unwrap_or(arch.num_attention_heads);
711 let output_gate = arch.global_head_dim.is_none() && wq.rows() == 2 * nh * arch.head_dim;
715 let is_global_layer = arch.global_head_dim.is_some()
718 && layer.is_some_and(|li| {
719 arch.sliding_window_pattern
720 .is_some_and(|p| p > 0 && (li + 1) % p == 0)
721 });
722 let expect = if is_global_layer {
723 nh * arch.global_head_dim.unwrap_or(arch.head_dim)
724 } else {
725 nh * arch.head_dim
726 };
727 if !output_gate && wq.rows() != expect {
728 return Err(CmfError::Parse(format!(
729 "{prefix}self_attn.q_proj.weight rows={} != heads({nh}) * head_dim({})",
730 wq.rows(),
731 expect / nh.max(1)
732 )));
733 }
734 let gate_name = format!("{prefix}self_attn.g_proj.weight");
735 let softplus_gate = if model.tensor(&gate_name).is_some() {
736 let gate = load_matrix(model, &gate_name, force_f32, ov)?;
737 if gate.cols() != arch.hidden_size {
738 return Err(CmfError::Parse(format!(
739 "{gate_name} cols={} != hidden_size ({})",
740 gate.cols(),
741 arch.hidden_size
742 )));
743 }
744 let per_head = if gate.rows() == nh {
745 true
746 } else if gate.rows() == nh * arch.head_dim {
747 false
748 } else {
749 return Err(CmfError::Parse(format!(
750 "{gate_name} rows={} must equal heads ({nh}) or heads*head_dim ({})",
751 gate.rows(),
752 nh * arch.head_dim
753 )));
754 };
755 Some((gate, per_head))
756 } else {
757 None
758 };
759 let bias = match (
761 n("self_attn.q_proj.bias"),
762 n("self_attn.k_proj.bias"),
763 n("self_attn.v_proj.bias"),
764 ) {
765 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
766 _ => None,
767 };
768 Ok(AttnKind::Full {
769 wq,
770 wk: t("self_attn.k_proj.weight")?,
771 wv: t("self_attn.v_proj.weight")?,
772 wo: t("self_attn.o_proj.weight")?,
773 q_norm: n("self_attn.q_norm.weight"),
774 k_norm: n("self_attn.k_norm.weight"),
775 output_gate,
776 softplus_gate,
777 bias,
778 })
779 };
780
781 let load_linear_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
782 if gdn_cfg.is_some() {
783 let t = |suffix: &str| {
785 load_matrix(
786 model,
787 &format!("{prefix}linear_attn.{suffix}"),
788 force_f32,
789 ov,
790 )
791 };
792 let f = |suffix: &str| {
793 load_f32(model, &format!("{prefix}linear_attn.{suffix}"), ov).map_err(err)
794 };
795 return Ok(AttnKind::LinearGdn(GdnWeights {
796 in_proj_qkv: t("in_proj_qkv.weight")?,
797 in_proj_z: t("in_proj_z.weight")?,
798 in_proj_a: t("in_proj_a.weight")?,
799 in_proj_b: t("in_proj_b.weight")?,
800 conv1d: f("conv1d.weight")?,
801 a_log: f("A_log")?,
802 dt_bias: f("dt_bias")?,
803 norm: f("norm.weight")?,
804 out_proj: t("out_proj.weight")?,
805 }));
806 }
807 let t = |suffix: &str| {
808 load_matrix(model, &format!("{prefix}vmf_attn.{suffix}"), force_f32, ov)
809 };
810 let a_log = load_f32(model, &format!("{prefix}vmf_attn.A_log"), ov).map_err(err)?;
811 let k_gate = if model
815 .tensor(&format!("{prefix}vmf_attn.k_gate.weight"))
816 .is_some()
817 {
818 Some((
819 t("k_gate.weight")?,
820 load_f32(model, &format!("{prefix}vmf_attn.k_gate.bias"), ov).map_err(err)?,
821 ))
822 } else {
823 None
824 };
825 Ok(AttnKind::Linear(VmfPhaseWeights {
826 thq: t("thq.weight")?,
827 thk: t("thk.weight")?,
828 v_proj: t("v_proj.weight")?,
829 out_proj: t("out_proj.weight")?,
830 decay: a_log.iter().map(|&a| (-(a as f64).exp()).exp()).collect(),
831 k_gate,
832 }))
833 };
834
835 let load_short_conv = |prefix: &str| -> Result<AttnKind, CmfError> {
839 let t = |suffix: &str| {
840 load_matrix(
841 model,
842 &format!("{prefix}short_conv.{suffix}"),
843 force_f32,
844 ov,
845 )
846 };
847 Ok(AttnKind::ShortConv(ShortConvWeights {
848 in_proj: t("in_proj.weight")?,
849 conv: load_f32(model, &format!("{prefix}short_conv.conv.weight"), ov)
850 .map_err(err)?,
851 out_proj: t("out_proj.weight")?,
852 }))
853 };
854
855 let load_kda = |prefix: &str| -> Result<AttnKind, CmfError> {
859 let t = |suffix: &str| {
860 load_matrix(model, &format!("{prefix}kda_attn.{suffix}"), force_f32, ov)
861 };
862 let f = |suffix: &str| {
863 load_f32(model, &format!("{prefix}kda_attn.{suffix}"), ov).map_err(err)
864 };
865 let gate = if model
866 .tensor(&format!("{prefix}kda_attn.g_proj.weight"))
867 .is_some()
868 {
869 crate::linear_core::KdaOutGate::Full(t("g_proj.weight")?)
870 } else {
871 crate::linear_core::KdaOutGate::LowRank(
872 t("g_a_proj.weight")?,
873 t("g_b_proj.weight")?,
874 )
875 };
876 Ok(AttnKind::Kda(Box::new(crate::linear_core::KdaWeights {
877 q_proj: t("q_proj.weight")?,
878 k_proj: t("k_proj.weight")?,
879 v_proj: t("v_proj.weight")?,
880 conv_q: f("q_conv1d.weight")?,
881 conv_k: f("k_conv1d.weight")?,
882 conv_v: f("v_conv1d.weight")?,
883 f_a: t("f_a_proj.weight")?,
884 f_b: t("f_b_proj.weight")?,
885 dt_bias: f("dt_bias")?,
886 a_log: f("A_log")?,
887 b_proj: t("b_proj.weight")?,
888 gate,
889 o_norm: f("o_norm.weight")?,
890 o_proj: t("o_proj.weight")?,
891 gate_lower_bound: arch.kda_gate_lower_bound.map(|v| v as f32),
892 })))
893 };
894
895 fn anyhow_like(ok: bool) -> Result<(), ()> {
896 if ok { Ok(()) } else { Err(()) }
897 }
898 let mut layers = Vec::with_capacity(arch.num_layers);
899 let is_g3n = arch.g3n.is_some();
900 for li in 0..(if is_g3n { 0 } else { arch.num_layers }) {
901 let prefix = format!("model.layers.{li}.");
902 let attn = match arch.layer_types.get(li) {
903 Some(LayerType::LinearAttention) => load_linear_attn(&prefix)?,
904 Some(LayerType::Kda) => load_kda(&prefix)?,
905 Some(LayerType::ShortConv) => load_short_conv(&prefix)?,
906 _ => load_full_attn(&prefix, Some(li))?,
907 };
908 let pre_ffn = format!("{prefix}pre_feedforward_layernorm.weight");
912 let sandwich = model.tensor(&pre_ffn).is_some();
913 layers.push(LayerWeights {
914 input_norm: load_f32(model, &format!("{prefix}input_layernorm.weight"), ov)
915 .map_err(err)?,
916 post_norm: if sandwich {
917 load_f32(model, &pre_ffn, ov).map_err(err)?
918 } else {
919 load_f32(
920 model,
921 &format!("{prefix}post_attention_layernorm.weight"),
922 ov,
923 )
924 .map_err(err)?
925 },
926 attn_out_norm: if sandwich {
927 Some(
928 load_f32(
929 model,
930 &format!("{prefix}post_attention_layernorm.weight"),
931 ov,
932 )
933 .map_err(err)?,
934 )
935 } else {
936 None
937 },
938 ffn_out_norm: if sandwich {
939 Some(
940 load_f32(
941 model,
942 &format!("{prefix}post_feedforward_layernorm.weight"),
943 ov,
944 )
945 .map_err(err)?,
946 )
947 } else {
948 None
949 },
950 layer_scale: model
952 .tensor(&format!("{prefix}layer_scalar"))
953 .and_then(|_| {
954 load_f32(model, &format!("{prefix}layer_scalar"), ov)
955 .ok()
956 .and_then(|v| v.first().copied())
957 }),
958 ffn: build_layer_ffn(model, &arch, li, false, ov)?,
960 attn,
961 });
962 }
963
964 let mtp = if let Some(cfg) = &arch.mtp {
966 if cfg.num_layers != 1 {
967 return Err(CmfError::Parse(format!(
968 "MTP with {} blocks not supported yet (only 1)",
969 cfg.num_layers
970 )));
971 }
972 let p = "model.mtp.";
973 let attn = load_full_attn("model.mtp.layers.0.", None)?;
974 Some(MtpModule {
975 enorm: load_f32(model, &format!("{p}enorm.weight"), ov).map_err(err)?,
976 hnorm: load_f32(model, &format!("{p}hnorm.weight"), ov).map_err(err)?,
977 eh_proj: load_matrix(model, &format!("{p}eh_proj.weight"), false, ov)?,
978 layer: LayerWeights {
979 attn_out_norm: None,
980 ffn_out_norm: None,
981 layer_scale: None,
982 input_norm: load_f32(model, &format!("{p}layers.0.input_layernorm.weight"), ov)
983 .map_err(err)?,
984 post_norm: load_f32(
985 model,
986 &format!("{p}layers.0.post_attention_layernorm.weight"),
987 ov,
988 )
989 .map_err(err)?,
990 ffn: build_ffn_at(model, &arch, &format!("{p}layers.0."), false, ov)?,
994 attn,
995 },
996 final_norm: load_f32(model, &format!("{p}norm.weight"), ov).map_err(err)?,
997 kv: LayerKvCache::new(arch.num_kv_heads, arch.head_dim),
998 })
999 } else {
1000 None
1001 };
1002
1003 tracing::info!(
1004 "Pipeline loaded: {} | {}L ({} linear) | {:.2}B params | storage: {} | MTP: {}",
1005 arch.arch_name,
1006 arch.num_layers,
1007 arch.layer_types
1008 .iter()
1009 .filter(|t| matches!(t, LayerType::LinearAttention))
1010 .count(),
1011 model.total_param_count() as f64 / 1e9,
1012 if force_f32 {
1013 "f32 (masked)"
1014 } else {
1015 "quantized mmap"
1016 },
1017 if mtp.is_some() { "yes" } else { "no" }
1018 );
1019
1020 let cap = std::env::var("CMF_MAX_SEQ")
1023 .ok()
1024 .and_then(|v| v.parse::<usize>().ok())
1025 .unwrap_or(8192);
1026 let max_seq_len = arch.max_position_embeddings.min(cap);
1027
1028 let total_layers = arch.num_layers * arch.num_loops;
1030
1031 let mut pipeline = Pipeline::new(
1032 tokenizer,
1033 PipelineWeights {
1034 embed_tokens,
1035 layers,
1036 lm_head,
1037 final_norm,
1038 },
1039 arch.hidden_size,
1040 arch.intermediate_size,
1041 arch.num_attention_heads,
1042 arch.num_kv_heads,
1043 arch.head_dim,
1044 total_layers,
1045 arch.num_layers, arch.loop_final_norm,
1047 arch.vocab_size,
1048 arch.rms_norm_eps,
1049 arch.rope_theta as f32,
1050 arch.norm_style,
1051 max_seq_len,
1052 sampler_config,
1053 );
1054 let rotary = ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2);
1055 pipeline.set_rotary(rotary, arch.rope_theta as f32);
1056 pipeline.attention_heads_per_layer = arch.attention_heads_per_layer.clone();
1057 if let Some(yarn) = &arch.yarn {
1058 pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
1059 rotary,
1060 arch.rope_theta as f32,
1061 yarn.factor,
1062 yarn.original_max_position_embeddings,
1063 yarn.beta_fast,
1064 yarn.beta_slow,
1065 ));
1066 pipeline.rope_scale = yarn.attention_factor;
1067 }
1068 pipeline.embed_multiplier = arch.embed_multiplier;
1072 pipeline.logit_multiplier = arch.logit_multiplier;
1073 if let Some(qpas) = arch.query_pre_attn_scalar {
1074 pipeline.attn_scale = 1.0 / (qpas as f32).sqrt();
1075 }
1076 if let (Some(w), Some(p)) = (arch.sliding_window, arch.sliding_window_pattern) {
1077 pipeline.swa = Some((w, p));
1078 if let Some(base) = arch.rope_local_base_freq {
1079 pipeline.inv_freq_local = Some(std::sync::Arc::new(
1080 crate::attention::rope_inv_freq(rotary, base as f32),
1081 ));
1082 }
1083 }
1084 let explicit_sliding: Vec<bool> = arch
1085 .layer_types
1086 .iter()
1087 .map(|t| matches!(t, cortiq_core::LayerType::SlidingAttention))
1088 .collect();
1089 if explicit_sliding.iter().any(|&v| v) {
1090 pipeline.sliding_layers = Some(explicit_sliding);
1091 if let Some(w) = arch.sliding_window {
1092 pipeline.swa = Some((w, usize::MAX));
1093 }
1094 let local_rotary = ((arch.head_dim as f32
1095 * arch
1096 .local_partial_rotary_factor
1097 .unwrap_or(arch.partial_rotary_factor))
1098 as usize)
1099 .max(2);
1100 pipeline.rotary_dim_local = Some(local_rotary);
1101 if let Some(base) = arch.rope_local_base_freq {
1102 pipeline.inv_freq_local = Some(std::sync::Arc::new(
1103 crate::attention::rope_inv_freq(local_rotary, base as f32),
1104 ));
1105 }
1106 }
1107 if let (Some(ghd), Some(gkv)) = (arch.global_head_dim, arch.num_global_kv_heads) {
1111 pipeline.global_attn = Some((ghd, gkv));
1112 let prf = arch.global_partial_rotary_factor.unwrap_or(1.0);
1113 let half = ghd / 2;
1114 let ra = (((prf * ghd as f32) as usize) / 2).min(half);
1115 let mut f = vec![0.0f32; half];
1116 for (i, slot) in f.iter_mut().enumerate().take(ra) {
1117 *slot = 1.0 / (arch.rope_theta as f32).powf(2.0 * i as f32 / ghd as f32);
1118 }
1119 pipeline.inv_freq_global = Some(std::sync::Arc::new(f));
1120 let global_at = |li: usize| -> bool {
1125 match &pipeline.sliding_layers {
1126 Some(map) => !map.get(li).copied().unwrap_or(false),
1127 None => pipeline
1128 .swa
1129 .map(|(_, p)| p > 0 && p != usize::MAX && (li + 1) % p == 0)
1130 .unwrap_or(false),
1131 }
1132 };
1133 for li in 0..arch.num_layers {
1134 if global_at(li) {
1135 pipeline.kv_cache.layers[li] = crate::kv_cache::LayerKvCache::new(gkv, ghd);
1136 }
1137 }
1138 }
1139 if let Some(mla) = arch.mla.as_ref() {
1142 let hd = mla.qk_rope_head_dim + mla.qk_nope_head_dim;
1143 pipeline.head_dim = hd;
1144 pipeline.num_kv_heads = arch.num_attention_heads;
1145 pipeline.rotary_dim = mla.qk_rope_head_dim;
1146 let half = mla.qk_rope_head_dim / 2;
1147 let mut f = vec![0.0f32; half];
1148 for (i, slot) in f.iter_mut().enumerate() {
1149 *slot = 1.0
1150 / (arch.rope_theta as f32)
1151 .powf(2.0 * i as f32 / mla.qk_rope_head_dim as f32);
1152 }
1153 pipeline.inv_freq = std::sync::Arc::new(f);
1154 for li in 0..arch.num_layers {
1155 pipeline.kv_cache.layers[li] =
1156 crate::kv_cache::LayerKvCache::new(arch.num_attention_heads, hd);
1157 }
1158 }
1159 if let Some(fac) = &arch.rope_freq_factors {
1163 let mut f = pipeline.inv_freq.as_ref().clone();
1164 for (i, v) in f.iter_mut().enumerate() {
1165 if let Some(&d) = fac.get(i) {
1166 *v /= d as f32;
1167 }
1168 }
1169 pipeline.inv_freq = std::sync::Arc::new(f);
1170 }
1171 pipeline.attn_v_norm = arch.attn_v_norm;
1172 pipeline.final_softcap = arch.final_logit_softcapping.map(|c| c as f32);
1173 pipeline.attn_softcap = arch.attn_logit_softcapping.unwrap_or(0.0) as f32;
1174 pipeline.vmf_cfg = vmf_cfg;
1175 pipeline.gdn_cfg = gdn_cfg;
1176 pipeline.kda_cfg = kda_cfg;
1177 if let Some(gc) = arch.g3n.as_ref() {
1178 use crate::g3n::{G3nAltUp, G3nGlobals, G3nLaurel, G3nLayer};
1179 anyhow_like(gc.altup_num_inputs == crate::g3n::ALTUP_N).map_err(|_| {
1180 CmfError::Parse(format!(
1181 "g3n: altup_num_inputs {} != supported {}",
1182 gc.altup_num_inputs,
1183 crate::g3n::ALTUP_N
1184 ))
1185 })?;
1186 let t = |name: &str| load_matrix(model, name, force_f32, ov);
1187 let f = |name: &str| load_f32(model, name, ov).map_err(err);
1188 let mut altup_proj = Vec::new();
1189 let mut altup_unembed = Vec::new();
1190 for i in 0..crate::g3n::ALTUP_N - 1 {
1191 altup_proj.push(t(&format!("model.altup_projections.{i}.weight"))?);
1192 altup_unembed.push(t(&format!("model.altup_unembed_projections.{i}.weight"))?);
1193 }
1194 let first_shared = arch.num_layers.saturating_sub(gc.num_kv_shared_layers);
1195 let sliding_of = |li: usize| {
1196 matches!(
1197 arch.layer_types.get(li),
1198 Some(cortiq_core::LayerType::SlidingAttention)
1199 )
1200 };
1201 let mut g3n_layers = Vec::with_capacity(arch.num_layers);
1202 for li in 0..arch.num_layers {
1203 let pfx = format!("model.layers.{li}.");
1204 let shared = li >= first_shared && first_shared > 0;
1205 let share_src = if shared {
1206 let want = sliding_of(li);
1207 (0..first_shared).rev().find(|&j| sliding_of(j) == want)
1208 } else {
1209 None
1210 };
1211 g3n_layers.push(G3nLayer {
1212 altup: G3nAltUp {
1213 router_norm: f(&format!("{pfx}altup.router_norm.weight"))?,
1214 modality_router: t(&format!("{pfx}altup.modality_router.weight"))?,
1215 prediction_coefs: t(&format!("{pfx}altup.prediction_coefs.weight"))?,
1216 correction_coefs: t(&format!("{pfx}altup.correction_coefs.weight"))?,
1217 correct_output_scale: f(&format!("{pfx}altup.correct_output_scale"))?,
1218 },
1219 laurel: G3nLaurel {
1220 left: t(&format!("{pfx}laurel.linear_left.weight"))?,
1221 right: t(&format!("{pfx}laurel.linear_right.weight"))?,
1222 post_norm: f(&format!("{pfx}laurel.post_laurel_norm.weight"))?,
1223 },
1224 input_norm: f(&format!("{pfx}input_layernorm.weight"))?,
1225 post_attn_norm: f(&format!("{pfx}post_attention_layernorm.weight"))?,
1226 pre_ffw_norm: f(&format!("{pfx}pre_feedforward_layernorm.weight"))?,
1227 post_ffw_norm: f(&format!("{pfx}post_feedforward_layernorm.weight"))?,
1228 wq: t(&format!("{pfx}self_attn.q_proj.weight"))?,
1229 wk: if shared {
1230 None
1231 } else {
1232 Some(t(&format!("{pfx}self_attn.k_proj.weight"))?)
1233 },
1234 wv: if shared {
1235 None
1236 } else {
1237 Some(t(&format!("{pfx}self_attn.v_proj.weight"))?)
1238 },
1239 wo: t(&format!("{pfx}self_attn.o_proj.weight"))?,
1240 q_norm: f(&format!("{pfx}self_attn.q_norm.weight"))?,
1241 k_norm: if shared {
1242 None
1243 } else {
1244 Some(f(&format!("{pfx}self_attn.k_norm.weight"))?)
1245 },
1246 kv_share_src: share_src,
1247 sliding: sliding_of(li),
1248 gate: t(&format!("{pfx}mlp.gate_proj.weight"))?,
1249 up: t(&format!("{pfx}mlp.up_proj.weight"))?,
1250 down: t(&format!("{pfx}mlp.down_proj.weight"))?,
1251 sparsity: gc
1252 .activation_sparsity
1253 .get(li)
1254 .copied()
1255 .unwrap_or(0.0),
1256 ple_gate: t(&format!("{pfx}per_layer_input_gate.weight"))?,
1257 ple_proj: t(&format!("{pfx}per_layer_projection.weight"))?,
1258 post_ple_norm: f(&format!("{pfx}post_per_layer_input_norm.weight"))?,
1259 });
1260 }
1261 let hd = arch.head_dim;
1262 let globals = G3nGlobals {
1263 altup_proj,
1264 altup_unembed,
1265 ple_embed: t("model.embed_tokens_per_layer.weight")?,
1266 ple_model_proj: t("model.per_layer_model_projection.weight")?,
1267 ple_norm: f("model.per_layer_projection_norm.weight")?,
1268 ple_vocab: gc.ple_vocab,
1269 ple_dim: gc.ple_dim,
1270 num_layers: arch.num_layers,
1271 hidden: arch.hidden_size,
1272 rms_eps: arch.rms_norm_eps,
1273 inv_freq_local: crate::attention::rope_inv_freq(
1274 hd,
1275 arch.rope_local_base_freq.unwrap_or(10_000.0) as f32,
1276 ),
1277 inv_freq_global: crate::attention::rope_inv_freq(hd, arch.rope_theta as f32),
1278 window: arch.sliding_window.unwrap_or(512),
1279 };
1280 pipeline.g3n = Some(Box::new((globals, g3n_layers)));
1281 }
1282 pipeline.short_conv_cfg = short_conv_cfg;
1283 pipeline.mtp = mtp;
1284 pipeline.install_dynamic_routing(model, false);
1285 match ov {
1289 Overlay::One(sid) => {
1290 pipeline.dyn_active = model.header.skills.iter().position(|s| &s.id == sid);
1291 }
1292 Overlay::Blend(_) => pipeline.dyn_blend_loaded = true,
1293 Overlay::None => {}
1294 }
1295 if let Some(c) = &model.header.calibration {
1298 pipeline.set_calib_temp(c.temperature);
1299 }
1300 let o1 = match crate::nystrom::o1_from_env() {
1306 crate::nystrom::O1Env::Off => None,
1307 crate::nystrom::O1Env::On(cfg) => Some(cfg),
1308 crate::nystrom::O1Env::Unset => model
1309 .header
1310 .provenance
1311 .as_ref()
1312 .and_then(|p| p.get("o1_attn"))
1313 .and_then(crate::nystrom::O1Cfg::from_json),
1314 };
1315 if o1.is_some() {
1316 if pipeline.attn_softcap > 0.0 {
1317 return Err(CmfError::Parse(
1318 "--o1 with attention-logit soft-capping (Gemma-2) is not supported: \
1319 the streaming operator has no capped-score form"
1320 .into(),
1321 ));
1322 }
1323 pipeline.set_o1(o1);
1324 }
1325 Ok(pipeline)
1326 }
1327
1328 pub(crate) fn install_dynamic_routing(&mut self, model: &Arc<CmfModel>, force_f32: bool) {
1333 self.model = Some(model.clone());
1334 self.dyn_force_f32 = force_f32;
1335 let mut per_skill = Vec::with_capacity(model.header.skills.len());
1336 for sk in &model.header.skills {
1337 let mut ffn_layers = std::collections::BTreeSet::new();
1338 let mut non_ffn = false;
1339 let prefix = format!("skill.{}.", sk.id);
1340 for t in model.skill_tensors(&sk.id) {
1341 let rel = &t.name[prefix.len()..]; let toks: Vec<&str> = rel.split('.').collect();
1343 if toks.len() >= 5 && toks[0] == "model" && toks[1] == "layers" && toks[3] == "mlp"
1344 {
1345 if let Ok(li) = toks[2].parse::<usize>() {
1346 ffn_layers.insert(li);
1347 continue;
1348 }
1349 }
1350 non_ffn = true; }
1352 if non_ffn {
1353 tracing::warn!(
1354 "skill '{}' replaces non-FFN tensors — excluded from dynamic \
1355 routing (static overlay still works)",
1356 sk.id
1357 );
1358 per_skill.push(None);
1359 } else {
1360 per_skill.push(Some(ffn_layers.into_iter().collect::<Vec<_>>()));
1361 }
1362 }
1363 self.dyn_skill_layers = per_skill;
1364 }
1365
1366 pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
1373 self.kv_cache.clear();
1375 self.kv_history.clear();
1376 if self.dyn_active == idx {
1377 return Ok(());
1378 }
1379 let model = self.model.clone().ok_or_else(|| {
1380 CmfError::Parse("dynamic routing needs a model-backed pipeline".into())
1381 })?;
1382 let mut union: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
1383 if let Some(old) = self.dyn_active {
1384 if let Some(Some(ls)) = self.dyn_skill_layers.get(old) {
1385 union.extend(ls.iter().copied());
1386 }
1387 }
1388 let new_id: Option<String> = match idx {
1389 Some(n) => match self.dyn_skill_layers.get(n) {
1390 Some(Some(ls)) => {
1391 union.extend(ls.iter().copied());
1392 Some(model.header.skills[n].id.clone())
1393 }
1394 _ => {
1395 return Err(CmfError::Parse(format!(
1396 "skill index {n} not dynamic-eligible"
1397 )));
1398 }
1399 },
1400 None => None,
1401 };
1402 let ov = match &new_id {
1403 Some(s) => Overlay::One(s),
1404 None => Overlay::None,
1405 };
1406 let arch = model.arch();
1407 for li in union {
1408 self.weights.layers[li].ffn =
1409 build_layer_ffn(&model, arch, li, self.dyn_force_f32, &ov)?;
1410 }
1411 self.dyn_active = idx;
1412 Ok(())
1413 }
1414}