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