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
87pub(crate) fn 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 let dt_name = format!("{p}down_proj.t.weight");
160 let down_t = match model.tensor(&dt_name) {
161 Some(_) => Some(load_matrix(model, &dt_name, force_f32, ov)?),
162 None => None,
163 };
164 if let Some(t) = &down_t
165 && (t.rows() != inter || t.cols() != arch.hidden_size)
166 {
167 return Err(CmfError::Parse(format!(
168 "{p}down_proj.t: [{}, {}] != [{inter}, {}]",
169 t.rows(),
170 t.cols(),
171 arch.hidden_size
172 )));
173 }
174 let mut segs = Vec::new();
180 let mut start = inter;
181 for k in 1.. {
182 let gn = format!("{p}gate_proj.tube{k}.weight");
183 if model.tensor(&gn).is_none() {
184 break;
185 }
186 let gate = load_matrix(model, &gn, force_f32, ov)?;
187 let up = load_matrix(model, &format!("{p}up_proj.tube{k}.weight"), force_f32, ov)?;
188 let down = load_matrix(
189 model,
190 &format!("{p}down_proj.tube{k}.weight"),
191 force_f32,
192 ov,
193 )?;
194 let width = gate.rows();
195 if up.rows() != width || down.cols() != width || down.rows() != arch.hidden_size {
196 return Err(CmfError::Parse(format!(
197 "{p}tube{k}: dims disagree (gate.rows={width}, up.rows={}, \
198 down=[{}, {}], hidden={})",
199 up.rows(),
200 down.rows(),
201 down.cols(),
202 arch.hidden_size
203 )));
204 }
205 segs.push(crate::pipeline::FfnSeg {
206 gate,
207 up,
208 down,
209 start,
210 width,
211 });
212 start += width;
213 }
214 Ok(DenseFfn {
215 gate_proj,
216 up_proj,
217 down_proj,
218 act: crate::pipeline::Act::from_arch_full(arch),
219 down_t,
220 segs,
221 })
222 };
223 let router_name = format!("{prefix}mlp.gate.weight");
224 let resonance_moe = model.tensor(&router_name).is_none()
228 && arch.moe.as_ref().is_some_and(|m| m.router_resonance)
229 && model
230 .tensor(&format!("{prefix}mlp.experts.0.gate_proj.weight"))
231 .is_some();
232 if model.tensor(&router_name).is_none() && !resonance_moe {
233 return Ok(FfnKind::Dense(load_dense(&format!("{prefix}mlp."))?));
234 }
235 let cfg = arch.moe.as_ref().ok_or_else(|| {
236 CmfError::Parse(format!(
237 "{router_name} present but header has no arch.moe block"
238 ))
239 })?;
240 let mut experts = Vec::new();
245 for e in 0..cfg.num_experts {
246 if model
247 .tensor(&format!("{prefix}mlp.experts.{e}.gate_proj.weight"))
248 .is_none()
249 {
250 break;
251 }
252 experts.push(load_dense(&format!("{prefix}mlp.experts.{e}."))?);
253 }
254 if experts.is_empty() {
255 return Err(CmfError::Parse(format!(
256 "{prefix}: router present but no expert tensors"
257 )));
258 }
259 let shared = if model
260 .tensor(&format!("{prefix}mlp.shared_expert.gate_proj.weight"))
261 .is_some()
262 {
263 let gate_name = format!("{prefix}mlp.shared_expert_gate.weight");
264 Some((
265 load_dense(&format!("{prefix}mlp.shared_expert."))?,
266 if model.tensor(&gate_name).is_some() {
267 Some(load_matrix(model, &gate_name, force_f32, ov)?)
268 } else {
269 None
270 },
271 ))
272 } else {
273 None
274 };
275 let bias_name = format!("{prefix}mlp.expert_bias");
278 let expert_bias = if model.tensor(&bias_name).is_some() {
279 Some(load_f32(model, &bias_name, ov).map_err(CmfError::Parse)?)
280 } else {
281 None
282 };
283 let top_k = std::env::var("CMF_MOE_TOPK")
289 .ok()
290 .and_then(|v| v.parse::<usize>().ok())
291 .filter(|&k| k >= 1 && k <= cfg.top_k)
292 .inspect(|k| tracing::info!("MoE top_k override: {} (header {})", k, cfg.top_k))
293 .unwrap_or(cfg.top_k);
294 let route_tau = std::env::var("CMF_MOE_TAU")
296 .ok()
297 .and_then(|v| v.parse::<f32>().ok())
298 .filter(|&t| t > 0.0 && t < 1.0)
299 .inspect(|t| tracing::info!("MoE adaptive routing: tau {t}"));
300 let mask = moe_task_mask(model, &prefix, experts.len());
301 let router = if resonance_moe {
302 QTensor::from_f32(
304 vec![0.0; experts.len() * arch.hidden_size],
305 experts.len(),
306 arch.hidden_size,
307 )
308 } else {
309 load_matrix(model, &router_name, force_f32, ov)?
310 };
311 if router.rows() != experts.len() {
312 return Err(CmfError::Parse(format!(
313 "{router_name}: {} rows != {} experts",
314 router.rows(),
315 experts.len()
316 )));
317 }
318 let top_k = top_k.min(experts.len());
319 let pes_name = format!("{prefix}mlp.per_expert_scale");
323 let per_expert_scale = if model.tensor(&pes_name).is_some() {
324 Some(load_f32(model, &pes_name, ov).map_err(CmfError::Parse)?)
325 } else {
326 None
327 };
328 let router_input_norm = per_expert_scale.is_some();
329 let per_expert = model
334 .tensor(&format!("{prefix}mlp.experts.0.desc.mu"))
335 .is_some();
336 let resonance = if per_expert {
337 let ne_d = experts.len();
338 let hidden = arch.hidden_size;
339 let mut mu = Vec::with_capacity(ne_d * hidden);
340 let mut u = Vec::new();
341 let mut bias = Vec::with_capacity(ne_d);
342 let mut k = 0usize;
343 for e in 0..ne_d {
344 let m = load_f32(model, &format!("{prefix}mlp.experts.{e}.desc.mu"), ov)
345 .map_err(CmfError::Parse)?;
346 if m.len() != hidden {
347 return Err(CmfError::Parse(format!(
348 "{prefix}mlp.experts.{e}.desc.mu: {} != {hidden}",
349 m.len()
350 )));
351 }
352 mu.extend_from_slice(&m);
353 let un = format!("{prefix}mlp.experts.{e}.desc.u");
354 if model.tensor(&un).is_some() {
355 let ue = load_f32(model, &un, ov).map_err(CmfError::Parse)?;
356 let ke = ue.len() / hidden.max(1);
357 if e == 0 {
358 k = ke;
359 }
360 if ke != k {
361 return Err(CmfError::Parse(format!("{un}: rank {ke} != {k}")));
362 }
363 u.extend_from_slice(&ue);
364 }
365 let bn = format!("{prefix}mlp.experts.{e}.desc.bias");
366 bias.push(if model.tensor(&bn).is_some() {
367 load_f32(model, &bn, ov)
368 .map_err(CmfError::Parse)?
369 .first()
370 .copied()
371 .unwrap_or(0.0)
372 } else {
373 0.0
374 });
375 }
376 Some(crate::pipeline::Resonance { mu, u, k, bias })
377 } else if model.tensor(&format!("{prefix}mlp.desc.mu")).is_some() {
378 let mu = load_f32(model, &format!("{prefix}mlp.desc.mu"), ov).map_err(CmfError::Parse)?;
379 let ne_d = experts.len();
380 let hidden = arch.hidden_size;
381 if mu.len() != ne_d * hidden {
382 return Err(CmfError::Parse(format!(
383 "{prefix}mlp.desc.mu: {} != {ne_d}×{hidden}",
384 mu.len()
385 )));
386 }
387 let u_name = format!("{prefix}mlp.desc.u");
388 let (u, k) = if model.tensor(&u_name).is_some() {
389 let u = load_f32(model, &u_name, ov).map_err(CmfError::Parse)?;
390 let k = u.len() / (ne_d * hidden).max(1);
391 (u, k)
392 } else {
393 (Vec::new(), 0)
394 };
395 let b_name = format!("{prefix}mlp.desc.bias");
396 let bias = if model.tensor(&b_name).is_some() {
397 load_f32(model, &b_name, ov).map_err(CmfError::Parse)?
398 } else {
399 vec![0.0; ne_d]
400 };
401 Some(crate::pipeline::Resonance { mu, u, k, bias })
402 } else {
403 None
404 };
405 let moe = MoeFfn {
406 router,
407 experts,
408 top_k,
409 route_tau,
410 norm_topk_prob: cfg.norm_topk_prob,
411 router_sigmoid: cfg.router_sigmoid,
412 expert_bias,
413 routed_scaling: cfg.routed_scaling_factor.unwrap_or(1.0),
414 shared,
415 stats: std::cell::RefCell::new(Vec::new()),
416 act_sq: std::cell::RefCell::new(Vec::new()),
417 act_rows: std::cell::RefCell::new(Vec::new()),
418 mask,
419 per_expert_scale,
420 router_input_norm,
421 resonance,
422 };
423 if model
426 .tensor(&format!("{prefix}mlp.gate_proj.weight"))
427 .is_some()
428 {
429 let norm = |suffix: &str| -> Result<Vec<f32>, CmfError> {
430 load_f32(model, &format!("{prefix}{suffix}.weight"), ov).map_err(CmfError::Parse)
431 };
432 return Ok(FfnKind::DenseMoe(Box::new(crate::pipeline::DenseMoeFfn {
433 dense: load_dense(&format!("{prefix}mlp."))?,
434 moe,
435 post_norm_1: norm("post_feedforward_layernorm_1")?,
436 pre_norm_2: norm("pre_feedforward_layernorm_2")?,
437 post_norm_2: norm("post_feedforward_layernorm_2")?,
438 })));
439 }
440 Ok(FfnKind::Moe(moe))
441}
442
443pub(crate) fn moe_task_mask(
454 _model: &std::sync::Arc<CmfModel>,
455 prefix: &str,
456 ne: usize,
457) -> Option<Vec<bool>> {
458 use std::sync::OnceLock;
459 static CFG: OnceLock<Option<(std::collections::HashMap<usize, Vec<u64>>, f64)>> =
460 OnceLock::new();
461 let cfg = CFG.get_or_init(|| {
462 let path = std::path::PathBuf::from(std::env::var("CMF_MOE_MASK").ok()?);
463 let shown = path.display();
464 let text = std::fs::read_to_string(&path)
465 .map_err(|e| tracing::warn!("CMF_MOE_MASK: cannot read {shown}: {e}"))
466 .ok()?;
467 let map: std::collections::HashMap<String, Vec<u64>> = serde_json::from_str(&text)
468 .map_err(|e| tracing::warn!("CMF_MOE_MASK: bad JSON in {shown}: {e}"))
469 .ok()?;
470 let weighted = map
475 .values()
476 .any(|row| row.iter().copied().sum::<u64>() >= 1_000_000);
477 let cover = std::env::var("CMF_MOE_MASK_COVER")
478 .ok()
479 .and_then(|v| v.parse::<f64>().ok())
480 .filter(|&c| c > 0.0 && c <= 1.0)
481 .unwrap_or(if weighted { 0.925 } else { 0.9 });
482 tracing::info!("MoE task mask: {shown}, cover {cover}");
483 Some((
484 map.into_iter()
485 .filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
486 .collect(),
487 cover,
488 ))
489 });
490 let (stats, cover) = cfg.as_ref()?;
491 let li: usize = prefix
493 .split("layers.")
494 .nth(1)?
495 .split('.')
496 .next()?
497 .parse()
498 .ok()?;
499 let counts = stats.get(&li)?;
500 if counts.len() != ne {
501 tracing::warn!(
502 "CMF_MOE_MASK: layer {li} has {} counts, model has {ne} experts — skipped",
503 counts.len()
504 );
505 return None;
506 }
507 let total: u64 = counts.iter().sum();
508 if total == 0 {
509 return None;
510 }
511 let mut order: Vec<usize> = (0..ne).collect();
512 order.sort_unstable_by_key(|&e| std::cmp::Reverse(counts[e]));
513 let mut mask = vec![false; ne];
514 let mut acc = 0u64;
515 let mut kept = 0usize;
516 for &e in &order {
517 mask[e] = true;
518 acc += counts[e];
519 kept += 1;
520 if (acc as f64) >= cover * (total as f64) {
521 break;
522 }
523 }
524 tracing::info!(
525 "MoE task mask L{li}: {kept}/{ne} experts for {:.0}% mass",
526 cover * 100.0
527 );
528 Some(mask)
529}
530
531pub(crate) fn load_matrix(
532 model: &Arc<CmfModel>,
533 name: &str,
534 force_f32: bool,
535 ov: &Overlay,
536) -> Result<QTensor, CmfError> {
537 if ov.blend_touches(model, name) {
541 if let Overlay::Blend(list) = ov {
542 let entry = model
543 .tensor(name)
544 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
545 let data =
546 blend_f32(model, name, list).map_err(|e| CmfError::Parse(format!("blend: {e}")))?;
547 return Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]));
548 }
549 }
550 let skill = match ov {
551 Overlay::One(s) => Some(*s),
552 _ => None,
553 };
554 let name: &str = &match skill {
557 Some(sid) if model.tensor(&format!("skill.{sid}.{name}")).is_some() => {
558 format!("skill.{sid}.{name}")
559 }
560 _ => name.to_string(),
561 };
562 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
563 if force_f32 {
564 let entry = model
565 .tensor(name)
566 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
567 if entry.shape.len() != 2 {
568 return Err(err(format!("'{name}' is not 2-D")));
569 }
570 let data = load_f32(model, name, &Overlay::None).map_err(err)?;
571 Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]))
572 } else {
573 QTensor::from_model(model, name).map_err(err)
574 }
575}
576
577impl Pipeline {
578 pub fn from_model(
580 model: &Arc<CmfModel>,
581 sampler_config: SamplerConfig,
582 ) -> Result<Self, CmfError> {
583 Self::from_model_with_skill(model, sampler_config, None)
584 }
585
586 pub fn from_model_with_skill(
592 model: &Arc<CmfModel>,
593 sampler_config: SamplerConfig,
594 skill: Option<&str>,
595 ) -> Result<Self, CmfError> {
596 match skill {
597 Some(s) => Self::from_model_with_overlay(model, sampler_config, &Overlay::One(s)),
598 None => Self::from_model_with_overlay(model, sampler_config, &Overlay::None),
599 }
600 }
601
602 pub fn from_model_with_blend(
605 model: &Arc<CmfModel>,
606 sampler_config: SamplerConfig,
607 blend: &[(String, f32)],
608 ) -> Result<Self, CmfError> {
609 Self::from_model_with_overlay(model, sampler_config, &Overlay::Blend(blend))
610 }
611
612 fn skill_file_guard(model: &CmfModel) -> Result<(), CmfError> {
613 if model.required_features & cortiq_core::format::features::SKILL_FILE != 0 {
616 return Err(CmfError::Parse(
617 "this file is a standalone SKILL, not a runnable model — attach it: \
618 cortiq skill apply <base.cmf> <this file> -o specialist.cmf"
619 .into(),
620 ));
621 }
622 Ok(())
623 }
624
625 fn from_model_with_overlay(
626 model: &Arc<CmfModel>,
627 sampler_config: SamplerConfig,
628 ov: &Overlay,
629 ) -> Result<Self, CmfError> {
630 if let Some(dir) = model.path.parent() {
635 crate::gpu::set_cache_dir(dir.to_path_buf());
636 }
637 crate::gpu::graph_unsupported_reset();
640 Self::skill_file_guard(model)?;
641 let skill = match ov {
642 Overlay::One(s) => Some(*s),
643 _ => None,
644 };
645 if let Some(sid) = skill {
646 let known = model.header.skills.iter().any(|s| s.id == sid)
647 || model.skill_tensors(sid).next().is_some();
648 if !known {
649 return Err(CmfError::Parse(format!(
650 "skill '{sid}' not in this container (header.skills: {:?})",
651 model
652 .header
653 .skills
654 .iter()
655 .map(|s| &s.id)
656 .collect::<Vec<_>>()
657 )));
658 }
659 tracing::info!(
660 "skill '{sid}': {} replacement tensors overlaid",
661 model.skill_tensors(sid).count()
662 );
663 }
664 let arch = model.arch().clone();
665 let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
666 if let Some(heads) = &arch.attention_heads_per_layer {
667 if heads.len() != arch.num_layers {
668 return Err(CmfError::Parse(format!(
669 "arch.attention_heads_per_layer has {} entries, expected {}",
670 heads.len(),
671 arch.num_layers
672 )));
673 }
674 if let Some((li, &nh)) = heads
675 .iter()
676 .enumerate()
677 .find(|(_, nh)| **nh == 0 || **nh % arch.num_kv_heads != 0)
678 {
679 return Err(CmfError::Parse(format!(
680 "layer {li} has {nh} Q heads, which must be nonzero and divisible by {} KV heads",
681 arch.num_kv_heads
682 )));
683 }
684 }
685 if arch
686 .layer_types
687 .iter()
688 .any(|t| matches!(t, LayerType::SlidingAttention))
689 && arch.sliding_window.is_none()
690 {
691 return Err(CmfError::Parse(
692 "model has SlidingAttention layers but no arch.sliding_window".into(),
693 ));
694 }
695
696 let heads_masked = model.masks.masks.iter().any(|m| {
706 m.head_masks.iter().any(|row| {
707 let mut bits = 0usize;
708 for &b in row.iter() {
709 bits += b.count_ones() as usize;
710 }
711 !row.is_empty() && bits < arch.num_attention_heads
712 })
713 });
714 let force_f32 = heads_masked; let mut tokenizer = if let Some(vocab_bytes) = &model.vocab {
718 Tokenizer::from_bytes(vocab_bytes)
719 .map_err(|e| CmfError::Parse(format!("embedded tokenizer: {e}")))?
720 } else {
721 let sidecar = model.path.with_file_name("tokenizer.json");
722 if sidecar.exists() {
723 Tokenizer::from_file(&sidecar)
724 .map_err(|e| CmfError::Parse(format!("sidecar tokenizer: {e}")))?
725 } else {
726 tracing::warn!("no tokenizer in file or sidecar — using byte-level fallback");
727 Tokenizer::byte_level()
728 }
729 };
730 if let Some(tc) = &model.header.tokenizer_config {
732 tokenizer.chat_template = tc.chat_template.clone();
733 tokenizer.extra_eos.extend(tc.eos_token_ids.iter().copied());
734 if tokenizer.bos_token_id.is_none() {
735 tokenizer.bos_token_id = tc.bos_token_id;
736 }
737 tracing::info!(
738 "chat bundle: template {} chars, {} stop ids",
739 tc.chat_template.as_deref().map(str::len).unwrap_or(0),
740 tc.eos_token_ids.len()
741 );
742 }
743 if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
747 tokenizer.add_bos = true;
748 }
749
750 let embed_tokens = load_matrix(model, "model.embed_tokens.weight", false, ov)?;
752 let final_norm = if arch.qwen4_exp.is_some() {
753 vec![0.0; arch.hidden_size]
756 } else {
757 load_f32(model, "model.norm.weight", ov).map_err(err)?
758 };
759 let lm_head = if model.tensor("lm_head.weight").is_some() {
760 load_matrix(model, "lm_head.weight", false, ov)?
761 } else if arch.tie_word_embeddings {
762 load_matrix(model, "model.embed_tokens.weight", false, ov)?
764 } else {
765 return Err(CmfError::MissingTensor(
766 "lm_head.weight (and tie_word_embeddings is false)".into(),
767 ));
768 };
769
770 let has_linear = arch
772 .layer_types
773 .iter()
774 .any(|t| matches!(t, LayerType::LinearAttention));
775 let mut vmf_cfg = None;
776 let mut gdn_cfg = None;
777 if has_linear {
778 let lc = arch.linear_core.as_ref().ok_or_else(|| {
779 CmfError::Parse(
780 "model has LinearAttention layers but no arch.linear_core — \
781 reconvert with the current converter"
782 .into(),
783 )
784 })?;
785 let need = |v: Option<usize>, name: &str| {
786 v.ok_or_else(|| CmfError::Parse(format!("linear core needs arch.{name}")))
787 };
788 match lc.kind.as_str() {
789 "vmf_phase" => {
790 vmf_cfg = Some(VmfPhaseCfg {
791 num_heads: lc.num_heads,
792 nphase: need(lc.nphase, "linear_core.nphase")?,
793 value_head_dim: lc.value_head_dim,
794 hidden_size: arch.hidden_size,
795 phase_mass: std::env::var("CMF_PHASE_MASS")
798 .ok()
799 .and_then(|v| v.parse().ok())
800 .unwrap_or(0.0),
801 });
802 }
803 "gated_delta_net" => {
804 gdn_cfg = Some(GdnCfg {
805 num_v_heads: lc.num_heads,
806 num_k_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
807 key_head_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
808 value_head_dim: lc.value_head_dim,
809 conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
810 hidden_size: arch.hidden_size,
811 rms_eps: arch.rms_norm_eps,
812 output_gate_sigmoid: false,
813 });
814 }
815 other => {
816 return Err(CmfError::Parse(format!(
817 "unknown linear core '{other}' (this runtime executes: \
818 gated_delta_net, vmf_phase)"
819 )));
820 }
821 }
822 }
823
824 let has_kda = arch.layer_types.iter().any(|t| matches!(t, LayerType::Kda));
826 let kda_cfg = if has_kda {
827 let need = |v: Option<usize>, name: &str| {
828 v.ok_or_else(|| CmfError::Parse(format!("KDA core needs arch.{name}")))
829 };
830 Some(crate::linear_core::KdaCfg {
831 num_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
832 head_k_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
833 head_v_dim: need(arch.linear_value_head_dim, "linear_value_head_dim")?,
834 conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
835 hidden_size: arch.hidden_size,
836 rms_eps: arch.rms_norm_eps,
837 })
838 } else {
839 None
840 };
841
842 let has_short_conv = arch
844 .layer_types
845 .iter()
846 .any(|t| matches!(t, LayerType::ShortConv));
847 let short_conv_cfg = if has_short_conv {
848 Some(ShortConvCfg {
849 hidden_size: arch.hidden_size,
850 kernel: arch.linear_conv_kernel_dim.ok_or_else(|| {
851 CmfError::Parse(
852 "model has ShortConv layers but no arch.linear_conv_kernel_dim — \
853 reconvert with the current converter"
854 .into(),
855 )
856 })?,
857 })
858 } else {
859 None
860 };
861
862 let load_full_attn = |prefix: &str, layer: Option<usize>| -> Result<AttnKind, CmfError> {
864 let t = |suffix: &str| load_matrix(model, &format!("{prefix}{suffix}"), force_f32, ov);
865 let n = |suffix: &str| -> Option<Vec<f32>> {
866 model
867 .tensor(&format!("{prefix}{suffix}"))
868 .and_then(|_| load_f32(model, &format!("{prefix}{suffix}"), ov).ok())
869 };
870 if let Some(mla) = arch.mla.as_ref() {
872 let (q_proj, q_a, q_a_norm) = if mla.q_lora_rank.is_some() {
874 (
875 t("self_attn.q_b_proj.weight")?,
876 Some(t("self_attn.q_a_proj.weight")?),
877 Some(n("self_attn.q_a_layernorm.weight").ok_or_else(|| {
878 CmfError::Parse(format!("{prefix}: MLA needs q_a_layernorm"))
879 })?),
880 )
881 } else {
882 (t("self_attn.q_proj.weight")?, None, None)
883 };
884 let hd = mla.qk_rope_head_dim + mla.qk_nope_head_dim;
885 let nh = q_proj.rows() / hd;
886 let mut scale = 1.0 / (hd as f32).sqrt();
889 if let Some(y) = arch.yarn.as_ref() {
890 if let Some(m) = y.mscale_all_dim.filter(|&m| m > 0.0) {
891 let ms = 0.1 * m * y.factor.ln() + 1.0;
892 scale *= ms * ms;
893 }
894 }
895 return Ok(AttnKind::Mla(Box::new(crate::pipeline::MlaWeights {
896 q_proj,
897 q_a,
898 q_a_norm,
899 kv_a: t("self_attn.kv_a_proj_with_mqa.weight")?,
900 kv_a_norm: n("self_attn.kv_a_layernorm.weight").ok_or_else(|| {
901 CmfError::Parse(format!("{prefix}: MLA needs kv_a_layernorm"))
902 })?,
903 kv_b: t("self_attn.kv_b_proj.weight")?,
904 o_proj: t("self_attn.o_proj.weight")?,
905 nh,
906 qk_rope: mla.qk_rope_head_dim,
907 qk_nope: mla.qk_nope_head_dim,
908 v_dim: mla.v_head_dim,
909 lora: mla.kv_lora_rank,
910 scale,
911 nope: mla.nope,
912 })));
913 }
914 let wq = t("self_attn.q_proj.weight")?;
915 let nh = layer
916 .and_then(|li| {
917 arch.attention_heads_per_layer
918 .as_ref()
919 .and_then(|v| v.get(li).copied())
920 })
921 .unwrap_or(arch.num_attention_heads);
922 let output_gate = arch.global_head_dim.is_none() && wq.rows() == 2 * nh * arch.head_dim;
926 let is_global_layer = arch.global_head_dim.is_some()
929 && layer.is_some_and(|li| {
930 arch.sliding_window_pattern
931 .is_some_and(|p| p > 0 && (li + 1) % p == 0)
932 });
933 let expect = if is_global_layer {
934 nh * arch.global_head_dim.unwrap_or(arch.head_dim)
935 } else {
936 nh * arch.head_dim
937 };
938 if !output_gate && wq.rows() != expect {
939 return Err(CmfError::Parse(format!(
940 "{prefix}self_attn.q_proj.weight rows={} != heads({nh}) * head_dim({})",
941 wq.rows(),
942 expect / nh.max(1)
943 )));
944 }
945 let gate_name = format!("{prefix}self_attn.g_proj.weight");
946 let softplus_gate = if model.tensor(&gate_name).is_some() {
947 let gate = load_matrix(model, &gate_name, force_f32, ov)?;
948 if gate.cols() != arch.hidden_size {
949 return Err(CmfError::Parse(format!(
950 "{gate_name} cols={} != hidden_size ({})",
951 gate.cols(),
952 arch.hidden_size
953 )));
954 }
955 let per_head = if gate.rows() == nh {
956 true
957 } else if gate.rows() == nh * arch.head_dim {
958 false
959 } else {
960 return Err(CmfError::Parse(format!(
961 "{gate_name} rows={} must equal heads ({nh}) or heads*head_dim ({})",
962 gate.rows(),
963 nh * arch.head_dim
964 )));
965 };
966 Some((gate, per_head))
967 } else {
968 None
969 };
970 let bias = match (
972 n("self_attn.q_proj.bias"),
973 n("self_attn.k_proj.bias"),
974 n("self_attn.v_proj.bias"),
975 ) {
976 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
977 _ => None,
978 };
979 Ok(AttnKind::Full {
980 wq,
981 wk: t("self_attn.k_proj.weight")?,
982 wv: t("self_attn.v_proj.weight")?,
983 wo: t("self_attn.o_proj.weight")?,
984 q_norm: n("self_attn.q_norm.weight"),
985 k_norm: n("self_attn.k_norm.weight"),
986 output_gate,
987 softplus_gate,
988 bias,
989 })
990 };
991
992 let load_linear_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
993 if gdn_cfg.is_some() {
994 let t = |suffix: &str| {
996 load_matrix(
997 model,
998 &format!("{prefix}linear_attn.{suffix}"),
999 force_f32,
1000 ov,
1001 )
1002 };
1003 let f = |suffix: &str| {
1004 load_f32(model, &format!("{prefix}linear_attn.{suffix}"), ov).map_err(err)
1005 };
1006 return Ok(AttnKind::LinearGdn(GdnWeights {
1007 in_proj_qkv: t("in_proj_qkv.weight")?,
1008 in_proj_z: t("in_proj_z.weight")?,
1009 in_proj_a: t("in_proj_a.weight")?,
1010 in_proj_b: t("in_proj_b.weight")?,
1011 conv1d: f("conv1d.weight")?,
1012 a_log: f("A_log")?,
1013 dt_bias: f("dt_bias")?,
1014 norm: f("norm.weight")?,
1015 out_proj: t("out_proj.weight")?,
1016 }));
1017 }
1018 let t = |suffix: &str| {
1019 load_matrix(model, &format!("{prefix}vmf_attn.{suffix}"), force_f32, ov)
1020 };
1021 let a_log = load_f32(model, &format!("{prefix}vmf_attn.A_log"), ov).map_err(err)?;
1022 let k_gate = if model
1026 .tensor(&format!("{prefix}vmf_attn.k_gate.weight"))
1027 .is_some()
1028 {
1029 Some((
1030 t("k_gate.weight")?,
1031 load_f32(model, &format!("{prefix}vmf_attn.k_gate.bias"), ov).map_err(err)?,
1032 ))
1033 } else {
1034 None
1035 };
1036 let conv = if model
1039 .tensor(&format!("{prefix}vmf_attn.conv1d.weight"))
1040 .is_some()
1041 {
1042 Some(load_f32(model, &format!("{prefix}vmf_attn.conv1d.weight"), ov).map_err(err)?)
1043 } else {
1044 None
1045 };
1046 Ok(AttnKind::Linear(VmfPhaseWeights {
1047 thq: t("thq.weight")?,
1048 conv,
1049 thk: t("thk.weight")?,
1050 v_proj: t("v_proj.weight")?,
1051 out_proj: t("out_proj.weight")?,
1052 decay: a_log.iter().map(|&a| (-(a as f64).exp()).exp()).collect(),
1053 k_gate,
1054 }))
1055 };
1056
1057 let load_short_conv = |prefix: &str| -> Result<AttnKind, CmfError> {
1061 let t = |suffix: &str| {
1062 load_matrix(
1063 model,
1064 &format!("{prefix}short_conv.{suffix}"),
1065 force_f32,
1066 ov,
1067 )
1068 };
1069 Ok(AttnKind::ShortConv(ShortConvWeights {
1070 in_proj: t("in_proj.weight")?,
1071 conv: load_f32(model, &format!("{prefix}short_conv.conv.weight"), ov)
1072 .map_err(err)?,
1073 out_proj: t("out_proj.weight")?,
1074 }))
1075 };
1076
1077 let load_kda = |prefix: &str| -> Result<AttnKind, CmfError> {
1081 let t = |suffix: &str| {
1082 load_matrix(model, &format!("{prefix}kda_attn.{suffix}"), force_f32, ov)
1083 };
1084 let f = |suffix: &str| {
1085 load_f32(model, &format!("{prefix}kda_attn.{suffix}"), ov).map_err(err)
1086 };
1087 let gate = if model
1088 .tensor(&format!("{prefix}kda_attn.g_proj.weight"))
1089 .is_some()
1090 {
1091 crate::linear_core::KdaOutGate::Full(t("g_proj.weight")?)
1092 } else {
1093 crate::linear_core::KdaOutGate::LowRank(
1094 t("g_a_proj.weight")?,
1095 t("g_b_proj.weight")?,
1096 )
1097 };
1098 Ok(AttnKind::Kda(Box::new(crate::linear_core::KdaWeights {
1099 q_proj: t("q_proj.weight")?,
1100 k_proj: t("k_proj.weight")?,
1101 v_proj: t("v_proj.weight")?,
1102 conv_q: f("q_conv1d.weight")?,
1103 conv_k: f("k_conv1d.weight")?,
1104 conv_v: f("v_conv1d.weight")?,
1105 f_a: t("f_a_proj.weight")?,
1106 f_b: t("f_b_proj.weight")?,
1107 dt_bias: f("dt_bias")?,
1108 a_log: f("A_log")?,
1109 b_proj: t("b_proj.weight")?,
1110 gate,
1111 o_norm: f("o_norm.weight")?,
1112 o_proj: t("o_proj.weight")?,
1113 gate_lower_bound: arch.kda_gate_lower_bound.map(|v| v as f32),
1114 })))
1115 };
1116
1117 fn anyhow_like(ok: bool) -> Result<(), ()> {
1118 if ok { Ok(()) } else { Err(()) }
1119 }
1120 let mut layers = Vec::with_capacity(arch.num_layers);
1121 let is_g3n = arch.g3n.is_some();
1122 let owns_its_layers = is_g3n
1127 || arch.arch_name == "deepseek_v4"
1128 || arch.arch_name == "deepseek_v41"
1129 || arch.qwen4_exp.is_some();
1130 for li in 0..(if owns_its_layers { 0 } else { arch.num_layers }) {
1131 let prefix = format!("model.layers.{li}.");
1132 let attn = match arch.layer_types.get(li) {
1133 Some(LayerType::LinearAttention) => load_linear_attn(&prefix)?,
1134 Some(LayerType::Kda) => load_kda(&prefix)?,
1135 Some(LayerType::ShortConv) => load_short_conv(&prefix)?,
1136 _ => load_full_attn(&prefix, Some(li))?,
1137 };
1138 let pre_ffn = format!("{prefix}pre_feedforward_layernorm.weight");
1142 let sandwich = model.tensor(&pre_ffn).is_some();
1143 layers.push(LayerWeights {
1144 input_norm: load_f32(model, &format!("{prefix}input_layernorm.weight"), ov)
1145 .map_err(err)?,
1146 post_norm: if sandwich {
1147 load_f32(model, &pre_ffn, ov).map_err(err)?
1148 } else {
1149 load_f32(
1150 model,
1151 &format!("{prefix}post_attention_layernorm.weight"),
1152 ov,
1153 )
1154 .map_err(err)?
1155 },
1156 attn_out_norm: if sandwich {
1157 Some(
1158 load_f32(
1159 model,
1160 &format!("{prefix}post_attention_layernorm.weight"),
1161 ov,
1162 )
1163 .map_err(err)?,
1164 )
1165 } else {
1166 None
1167 },
1168 ffn_out_norm: if sandwich {
1169 Some(
1170 load_f32(
1171 model,
1172 &format!("{prefix}post_feedforward_layernorm.weight"),
1173 ov,
1174 )
1175 .map_err(err)?,
1176 )
1177 } else {
1178 None
1179 },
1180 layer_scale: model
1182 .tensor(&format!("{prefix}layer_scalar"))
1183 .and_then(|_| {
1184 load_f32(model, &format!("{prefix}layer_scalar"), ov)
1185 .ok()
1186 .and_then(|v| v.first().copied())
1187 }),
1188 ffn: build_layer_ffn(model, &arch, li, false, ov)?,
1190 attn,
1191 });
1192 }
1193
1194 let mtp_present = model
1203 .tensor("model.mtp.layers.0.self_attn.q_proj.weight")
1204 .is_some()
1205 || model.tensor("model.mtp.eh_proj.weight").is_some();
1206 let dsv4_mtp = model.tensor("model.mtp.0.main_proj.weight").is_some();
1211 if arch.mtp.is_some() && !mtp_present && !dsv4_mtp {
1212 tracing::info!(
1213 "header declares an MTP head but the file carries none — \
1214 loading without it"
1215 );
1216 }
1217 let mtp = if let Some(cfg) = arch.mtp.as_ref().filter(|_| mtp_present) {
1218 if cfg.num_layers != 1 {
1219 return Err(CmfError::Parse(format!(
1220 "MTP with {} blocks not supported yet (only 1)",
1221 cfg.num_layers
1222 )));
1223 }
1224 let p = "model.mtp.";
1225 let attn = load_full_attn("model.mtp.layers.0.", None)?;
1226 Some(MtpModule {
1227 enorm: load_f32(model, &format!("{p}enorm.weight"), ov).map_err(err)?,
1228 hnorm: load_f32(model, &format!("{p}hnorm.weight"), ov).map_err(err)?,
1229 eh_proj: load_matrix(model, &format!("{p}eh_proj.weight"), false, ov)?,
1230 layer: LayerWeights {
1231 attn_out_norm: None,
1232 ffn_out_norm: None,
1233 layer_scale: None,
1234 input_norm: load_f32(model, &format!("{p}layers.0.input_layernorm.weight"), ov)
1235 .map_err(err)?,
1236 post_norm: load_f32(
1237 model,
1238 &format!("{p}layers.0.post_attention_layernorm.weight"),
1239 ov,
1240 )
1241 .map_err(err)?,
1242 ffn: build_ffn_at(model, &arch, &format!("{p}layers.0."), false, ov)?,
1246 attn,
1247 },
1248 final_norm: load_f32(model, &format!("{p}norm.weight"), ov).map_err(err)?,
1249 kv: LayerKvCache::new(arch.num_kv_heads, arch.head_dim),
1250 })
1251 } else {
1252 None
1253 };
1254
1255 tracing::info!(
1256 "Pipeline loaded: {} | {}L ({} linear) | {:.2}B params | storage: {} | MTP: {}",
1257 arch.arch_name,
1258 arch.num_layers,
1259 arch.layer_types
1260 .iter()
1261 .filter(|t| matches!(t, LayerType::LinearAttention))
1262 .count(),
1263 model.total_param_count() as f64 / 1e9,
1264 if force_f32 {
1265 "f32 (masked)"
1266 } else {
1267 "quantized mmap"
1268 },
1269 if mtp.is_some() { "yes" } else { "no" }
1270 );
1271
1272 let cap = std::env::var("CMF_MAX_SEQ")
1283 .ok()
1284 .and_then(|v| v.parse::<usize>().ok())
1285 .unwrap_or(32_768);
1286 let max_seq_len = arch.max_position_embeddings.min(cap);
1287
1288 let total_layers = arch.num_layers * arch.num_loops;
1290
1291 let mut pipeline = Pipeline::new(
1292 tokenizer,
1293 PipelineWeights {
1294 embed_tokens,
1295 layers,
1296 lm_head,
1297 final_norm,
1298 },
1299 arch.hidden_size,
1300 arch.intermediate_size,
1301 arch.num_attention_heads,
1302 arch.num_kv_heads,
1303 arch.head_dim,
1304 total_layers,
1305 arch.num_layers, arch.loop_final_norm,
1307 arch.vocab_size,
1308 arch.rms_norm_eps,
1309 arch.rope_theta as f32,
1310 arch.norm_style,
1311 max_seq_len,
1312 sampler_config,
1313 );
1314 let rotary = ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2);
1315 pipeline.set_rotary(rotary, arch.rope_theta as f32);
1316 pipeline.attention_heads_per_layer = arch.attention_heads_per_layer.clone();
1317 if let Some(yarn) = &arch.yarn {
1318 pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
1319 rotary,
1320 arch.rope_theta as f32,
1321 yarn.factor,
1322 yarn.original_max_position_embeddings,
1323 yarn.beta_fast,
1324 yarn.beta_slow,
1325 ));
1326 pipeline.rope_scale = yarn.attention_factor;
1327 }
1328 pipeline.embed_multiplier = arch.embed_multiplier;
1332 pipeline.logit_multiplier = arch.logit_multiplier;
1333 if let Some(qpas) = arch.query_pre_attn_scalar {
1334 pipeline.attn_scale = 1.0 / (qpas as f32).sqrt();
1335 }
1336 if let (Some(w), Some(p)) = (arch.sliding_window, arch.sliding_window_pattern) {
1337 pipeline.swa = Some((w, p));
1338 if let Some(base) = arch.rope_local_base_freq {
1339 pipeline.inv_freq_local = Some(std::sync::Arc::new(
1340 crate::attention::rope_inv_freq(rotary, base as f32),
1341 ));
1342 }
1343 }
1344 let explicit_sliding: Vec<bool> = arch
1345 .layer_types
1346 .iter()
1347 .map(|t| matches!(t, cortiq_core::LayerType::SlidingAttention))
1348 .collect();
1349 if explicit_sliding.iter().any(|&v| v) {
1350 pipeline.sliding_layers = Some(explicit_sliding);
1351 if let Some(w) = arch.sliding_window {
1352 pipeline.swa = Some((w, usize::MAX));
1353 }
1354 let local_rotary = ((arch.head_dim as f32
1355 * arch
1356 .local_partial_rotary_factor
1357 .unwrap_or(arch.partial_rotary_factor))
1358 as usize)
1359 .max(2);
1360 pipeline.rotary_dim_local = Some(local_rotary);
1361 if let Some(base) = arch.rope_local_base_freq {
1362 pipeline.inv_freq_local = Some(std::sync::Arc::new(
1363 crate::attention::rope_inv_freq(local_rotary, base as f32),
1364 ));
1365 }
1366 }
1367 if let (Some(ghd), Some(gkv)) = (arch.global_head_dim, arch.num_global_kv_heads) {
1371 pipeline.global_attn = Some((ghd, gkv));
1372 let prf = arch.global_partial_rotary_factor.unwrap_or(1.0);
1373 let half = ghd / 2;
1374 let ra = (((prf * ghd as f32) as usize) / 2).min(half);
1375 let mut f = vec![0.0f32; half];
1376 for (i, slot) in f.iter_mut().enumerate().take(ra) {
1377 *slot = 1.0 / (arch.rope_theta as f32).powf(2.0 * i as f32 / ghd as f32);
1378 }
1379 pipeline.inv_freq_global = Some(std::sync::Arc::new(f));
1380 let global_at = |li: usize| -> bool {
1385 match &pipeline.sliding_layers {
1386 Some(map) => !map.get(li).copied().unwrap_or(false),
1387 None => pipeline
1388 .swa
1389 .map(|(_, p)| p > 0 && p != usize::MAX && (li + 1) % p == 0)
1390 .unwrap_or(false),
1391 }
1392 };
1393 for li in 0..arch.num_layers {
1394 if global_at(li) {
1395 pipeline.kv_cache.layers[li] = crate::kv_cache::LayerKvCache::new(gkv, ghd);
1396 }
1397 }
1398 }
1399 if let Some(mla) = arch.mla.as_ref() {
1402 let hd = mla.qk_rope_head_dim + mla.qk_nope_head_dim;
1403 pipeline.head_dim = hd;
1404 pipeline.num_kv_heads = arch.num_attention_heads;
1405 pipeline.rotary_dim = mla.qk_rope_head_dim;
1406 let half = mla.qk_rope_head_dim / 2;
1407 let mut f = vec![0.0f32; half];
1408 for (i, slot) in f.iter_mut().enumerate() {
1409 *slot = 1.0
1410 / (arch.rope_theta as f32).powf(2.0 * i as f32 / mla.qk_rope_head_dim as f32);
1411 }
1412 pipeline.inv_freq = std::sync::Arc::new(f);
1413 for li in 0..arch.num_layers {
1414 pipeline.kv_cache.layers[li] =
1415 crate::kv_cache::LayerKvCache::new(arch.num_attention_heads, hd);
1416 }
1417 }
1418 if let Some(fac) = &arch.rope_freq_factors {
1422 let mut f = pipeline.inv_freq.as_ref().clone();
1423 for (i, v) in f.iter_mut().enumerate() {
1424 if let Some(&d) = fac.get(i) {
1425 *v /= d as f32;
1426 }
1427 }
1428 pipeline.inv_freq = std::sync::Arc::new(f);
1429 }
1430 pipeline.attn_v_norm = arch.attn_v_norm;
1431 pipeline.qk_norm_after_rope = arch.qk_norm_after_rope;
1432 pipeline.final_softcap = arch.final_logit_softcapping.map(|c| c as f32);
1433 if let Some(ncl) = arch.head_clusters {
1435 let cm = load_f32(model, "lm_head.clusters.weight", ov).map_err(err)?;
1436 if cm.len() != ncl * arch.hidden_size {
1437 return Err(CmfError::Parse(format!(
1438 "lm_head.clusters.weight: {} != {ncl}×{}",
1439 cm.len(),
1440 arch.hidden_size
1441 )));
1442 }
1443 pipeline.head_clusters = Some(std::sync::Arc::new(cm));
1444 }
1445 pipeline.attn_softcap = arch.attn_logit_softcapping.unwrap_or(0.0) as f32;
1446 pipeline.vmf_cfg = vmf_cfg;
1447 pipeline.gdn_cfg = gdn_cfg;
1448 pipeline.kda_cfg = kda_cfg;
1449 if arch.qwen4_exp.is_some() {
1450 let (globals, layers, cfg, state) = crate::qwen4_exp::load(model, &arch)?;
1451 pipeline.qwen4_exp = Some(Box::new((globals, layers, cfg, state)));
1452 }
1453 if let Some(gc) = arch.g3n.as_ref() {
1454 use crate::g3n::{G3nAltUp, G3nGlobals, G3nLaurel, G3nLayer};
1455 anyhow_like(gc.altup_num_inputs == crate::g3n::ALTUP_N).map_err(|_| {
1456 CmfError::Parse(format!(
1457 "g3n: altup_num_inputs {} != supported {}",
1458 gc.altup_num_inputs,
1459 crate::g3n::ALTUP_N
1460 ))
1461 })?;
1462 let t = |name: &str| load_matrix(model, name, force_f32, ov);
1463 let f = |name: &str| load_f32(model, name, ov).map_err(err);
1464 let mut altup_proj = Vec::new();
1465 let mut altup_unembed = Vec::new();
1466 for i in 0..crate::g3n::ALTUP_N - 1 {
1467 altup_proj.push(t(&format!("model.altup_projections.{i}.weight"))?);
1468 altup_unembed.push(t(&format!("model.altup_unembed_projections.{i}.weight"))?);
1469 }
1470 let first_shared = arch.num_layers.saturating_sub(gc.num_kv_shared_layers);
1471 let sliding_of = |li: usize| {
1472 matches!(
1473 arch.layer_types.get(li),
1474 Some(cortiq_core::LayerType::SlidingAttention)
1475 )
1476 };
1477 let mut g3n_layers = Vec::with_capacity(arch.num_layers);
1478 for li in 0..arch.num_layers {
1479 let pfx = format!("model.layers.{li}.");
1480 let shared = li >= first_shared && first_shared > 0;
1481 let share_src = if shared {
1482 let want = sliding_of(li);
1483 (0..first_shared).rev().find(|&j| sliding_of(j) == want)
1484 } else {
1485 None
1486 };
1487 g3n_layers.push(G3nLayer {
1488 altup: G3nAltUp {
1489 router_norm: f(&format!("{pfx}altup.router_norm.weight"))?,
1490 modality_router: t(&format!("{pfx}altup.modality_router.weight"))?,
1491 prediction_coefs: t(&format!("{pfx}altup.prediction_coefs.weight"))?,
1492 correction_coefs: t(&format!("{pfx}altup.correction_coefs.weight"))?,
1493 correct_output_scale: f(&format!("{pfx}altup.correct_output_scale"))?,
1494 },
1495 laurel: G3nLaurel {
1496 left: t(&format!("{pfx}laurel.linear_left.weight"))?,
1497 right: t(&format!("{pfx}laurel.linear_right.weight"))?,
1498 post_norm: f(&format!("{pfx}laurel.post_laurel_norm.weight"))?,
1499 },
1500 input_norm: f(&format!("{pfx}input_layernorm.weight"))?,
1501 post_attn_norm: f(&format!("{pfx}post_attention_layernorm.weight"))?,
1502 pre_ffw_norm: f(&format!("{pfx}pre_feedforward_layernorm.weight"))?,
1503 post_ffw_norm: f(&format!("{pfx}post_feedforward_layernorm.weight"))?,
1504 wq: t(&format!("{pfx}self_attn.q_proj.weight"))?,
1505 wk: if shared {
1506 None
1507 } else {
1508 Some(t(&format!("{pfx}self_attn.k_proj.weight"))?)
1509 },
1510 wv: if shared {
1511 None
1512 } else {
1513 Some(t(&format!("{pfx}self_attn.v_proj.weight"))?)
1514 },
1515 wo: t(&format!("{pfx}self_attn.o_proj.weight"))?,
1516 q_norm: f(&format!("{pfx}self_attn.q_norm.weight"))?,
1517 k_norm: if shared {
1518 None
1519 } else {
1520 Some(f(&format!("{pfx}self_attn.k_norm.weight"))?)
1521 },
1522 kv_share_src: share_src,
1523 sliding: sliding_of(li),
1524 gate: t(&format!("{pfx}mlp.gate_proj.weight"))?,
1525 up: t(&format!("{pfx}mlp.up_proj.weight"))?,
1526 down: t(&format!("{pfx}mlp.down_proj.weight"))?,
1527 sparsity: gc.activation_sparsity.get(li).copied().unwrap_or(0.0),
1528 ple_gate: t(&format!("{pfx}per_layer_input_gate.weight"))?,
1529 ple_proj: t(&format!("{pfx}per_layer_projection.weight"))?,
1530 post_ple_norm: f(&format!("{pfx}post_per_layer_input_norm.weight"))?,
1531 });
1532 }
1533 let hd = arch.head_dim;
1534 let globals = G3nGlobals {
1535 altup_proj,
1536 altup_unembed,
1537 ple_embed: t("model.embed_tokens_per_layer.weight")?,
1538 ple_model_proj: t("model.per_layer_model_projection.weight")?,
1539 ple_norm: f("model.per_layer_projection_norm.weight")?,
1540 ple_vocab: gc.ple_vocab,
1541 ple_dim: gc.ple_dim,
1542 num_layers: arch.num_layers,
1543 hidden: arch.hidden_size,
1544 rms_eps: arch.rms_norm_eps,
1545 inv_freq_local: crate::attention::rope_inv_freq(
1546 hd,
1547 arch.rope_local_base_freq.unwrap_or(10_000.0) as f32,
1548 ),
1549 inv_freq_global: crate::attention::rope_inv_freq(hd, arch.rope_theta as f32),
1550 window: arch.sliding_window.unwrap_or(512),
1551 };
1552 pipeline.g3n = Some(Box::new((globals, g3n_layers)));
1553 }
1554 if arch.arch_name == "deepseek_v41" {
1559 let source = arch.deepseek_v41.as_ref().ok_or_else(|| {
1560 CmfError::Parse("deepseek_v41: missing preserved source config".into())
1561 })?;
1562 let tc = source.get("text_config").unwrap_or(source);
1563 let usize_of = |key: &str, fallback: usize| {
1564 tc.get(key)
1565 .and_then(|v| v.as_u64())
1566 .map(|v| v as usize)
1567 .unwrap_or(fallback)
1568 };
1569 let f32_of = |key: &str, fallback: f32| {
1570 tc.get(key)
1571 .and_then(|v| v.as_f64())
1572 .map(|v| v as f32)
1573 .unwrap_or(fallback)
1574 };
1575 let usize_any = |keys: &[&str], fallback: usize| {
1576 keys.iter()
1577 .find_map(|key| tc.get(key).and_then(|v| v.as_u64()))
1578 .map(|v| v as usize)
1579 .unwrap_or(fallback)
1580 };
1581 let f32_any = |keys: &[&str], fallback: f32| {
1582 keys.iter()
1583 .find_map(|key| tc.get(key).and_then(|v| v.as_f64()))
1584 .map(|v| v as f32)
1585 .unwrap_or(fallback)
1586 };
1587 let bool_of = |key: &str, fallback: bool| {
1588 tc.get(key).and_then(|v| v.as_bool()).unwrap_or(fallback)
1589 };
1590 let array_of = |key: &str| -> Vec<usize> {
1591 tc.get(key)
1592 .and_then(|v| v.as_array())
1593 .map(|a| {
1594 a.iter()
1595 .filter_map(|v| v.as_u64().map(|x| x as usize))
1596 .collect()
1597 })
1598 .unwrap_or_default()
1599 };
1600 let array_alias = |keys: &[&str]| -> Vec<usize> {
1601 keys.iter()
1602 .find_map(|key| {
1603 let values = array_of(key);
1604 (!values.is_empty()).then_some(values)
1605 })
1606 .unwrap_or_default()
1607 };
1608 let dim = usize_any(&["hidden_size", "dim"], arch.hidden_size);
1609 let n_layers = usize_any(&["num_hidden_layers", "n_layers"], arch.num_layers);
1610 let n_heads = usize_any(
1611 &["num_attention_heads", "n_heads"],
1612 arch.num_attention_heads,
1613 );
1614 let head_dim = usize_any(&["head_dim"], arch.head_dim);
1615 let rope_head_dim = usize_any(&["rope_head_dim", "qk_rope_head_dim"], 64.min(head_dim));
1616 let moe_inter = usize_any(
1617 &[
1618 "moe_intermediate_size",
1619 "moe_inter_dim",
1620 "intermediate_size",
1621 ],
1622 arch.intermediate_size,
1623 );
1624 let n_experts = usize_any(
1625 &["n_routed_experts"],
1626 arch.moe.as_ref().map(|m| m.num_experts).unwrap_or(384),
1627 );
1628 let top_k = usize_any(
1629 &["num_experts_per_tok", "n_activated_experts"],
1630 arch.moe.as_ref().map(|m| m.top_k).unwrap_or(6),
1631 );
1632 let mut ratios = array_of("compress_ratios");
1633 if ratios.len() >= n_layers {
1634 ratios.truncate(n_layers);
1635 } else {
1636 ratios = (0..n_layers)
1637 .map(|li| {
1638 if (2..20).contains(&li) {
1639 2
1640 } else if (20..40).contains(&li) {
1641 1
1642 } else {
1643 0
1644 }
1645 })
1646 .collect();
1647 }
1648 let kv_sources = {
1649 let a = array_alias(&["kv_source_layers", "kv_source_layer_ids"]);
1650 if a.is_empty() { vec![2, 8, 14, 20] } else { a }
1651 };
1652 let index_sources = {
1653 let a = array_alias(&["index_source_layers", "index_source_layer_ids"]);
1654 if a.is_empty() {
1655 vec![2, 8, 14, 20, 24, 28, 32, 36]
1656 } else {
1657 a
1658 }
1659 };
1660 let engram_layers = array_of("engram_layer_ids");
1661 let engram_embeddings = array_of("engram_num_embeddings");
1662 let cfg = crate::dsv41::Dsv41Cfg {
1663 dim,
1664 n_heads,
1665 head_dim,
1666 rope_head_dim: rope_head_dim.min(head_dim) & !1,
1667 q_lora_rank: usize_of("q_lora_rank", 1280),
1668 o_lora_rank: usize_of("o_lora_rank", 1024),
1669 o_groups: usize_of("o_groups", 8),
1670 hc_mult: usize_of("hc_mult", 4),
1671 hc_sinkhorn_iters: usize_of("hc_sinkhorn_iters", 20),
1672 hc_eps: f32_of("hc_eps", 1e-6),
1673 norm_eps: f32_any(&["norm_eps", "rms_norm_eps"], arch.rms_norm_eps as f32),
1674 n_routed_experts: n_experts,
1675 top_k,
1676 moe_inter,
1677 gate_temp: f32_of("gate_temp", 1.0),
1678 norm_topk_prob: bool_of("norm_topk_prob", true),
1679 route_scale: f32_any(
1680 &["routed_scaling_factor", "route_scale"],
1681 arch.moe
1682 .as_ref()
1683 .and_then(|m| m.routed_scaling_factor)
1684 .unwrap_or(1.5),
1685 ),
1686 swiglu_limit: f32_of("swiglu_limit", 10.0),
1687 window: usize_any(
1688 &["window_size", "sliding_window"],
1689 arch.sliding_window.unwrap_or(128),
1690 ),
1691 rope_theta: f32_any(&["rope_theta"], arch.rope_theta as f32),
1692 compress_rope_theta: f32_any(&["compress_rope_theta"], 160_000.0),
1693 rope_factor: f32_any(
1694 &["rope_factor"],
1695 tc.get("rope_scaling")
1696 .and_then(|v| v.get("factor"))
1697 .and_then(|v| v.as_f64())
1698 .map(|v| v as f32)
1699 .unwrap_or(16.0),
1700 ),
1701 original_seq_len: usize_any(
1702 &["original_seq_len", "original_max_position_embeddings"],
1703 tc.get("rope_scaling")
1704 .and_then(|v| v.get("original_max_position_embeddings"))
1705 .and_then(|v| v.as_u64())
1706 .map(|v| v as usize)
1707 .unwrap_or(65_536),
1708 ),
1709 beta_fast: f32_any(
1710 &["beta_fast"],
1711 tc.get("rope_scaling")
1712 .and_then(|v| v.get("beta_fast"))
1713 .and_then(|v| v.as_f64())
1714 .map(|v| v as f32)
1715 .unwrap_or(32.0),
1716 ),
1717 beta_slow: f32_any(
1718 &["beta_slow"],
1719 tc.get("rope_scaling")
1720 .and_then(|v| v.get("beta_slow"))
1721 .and_then(|v| v.as_f64())
1722 .map(|v| v as f32)
1723 .unwrap_or(1.0),
1724 ),
1725 index_heads: usize_any(&["index_n_heads", "indexer_n_heads"], 32),
1726 index_head_dim: usize_any(&["index_head_dim", "indexer_head_dim"], 128),
1727 index_topk: usize_any(&["index_topk", "indexer_topk"], 512),
1728 candidate_source: usize_any(
1729 &["candidate_source_layer", "candidate_source_layer_id"],
1730 20,
1731 ),
1732 candidate_topk_blocks: usize_any(&["candidate_topk_blocks"], 2048),
1733 candidate_block_size: usize_any(&["candidate_block_size"], 8),
1734 kv_sources,
1735 index_sources,
1736 compress_ratios: ratios,
1737 engram_layers,
1738 engram_vocab: usize_any(&["engram_vocab_size"], 16_000_000),
1739 engram_embeddings,
1740 engram_max_ngram: usize_any(&["engram_max_ngram_size"], 4),
1741 engram_heads: usize_any(&["engram_n_heads"], 8),
1742 engram_head_dim: usize_any(&["engram_head_dim"], 256),
1743 engram_compressed_vocab: usize_any(&["engram_compressed_vocab_size"], 99_092),
1744 engram_pad_id: usize_any(&["engram_pad_id", "engram_pad_token_id"], 2),
1745 vocab: usize_any(&["vocab_size"], arch.vocab_size),
1746 };
1747 let token_map = crate::dsv41::token_map_from_model(model, cfg.vocab);
1748 let (g, dl, hash) = crate::dsv41::load(model, &cfg, n_layers, token_map)
1749 .map_err(|e| CmfError::Parse(format!("deepseek_v41: {e}")))?;
1750 let st = crate::dsv41::Dsv41State::new(&cfg, hash);
1751 if let Ok(vision_cfg) = crate::dsv41_vision::VisionConfig::from_source(source) {
1756 if vision_cfg.vision_enabled()
1757 && model.tensor("vision.patch_embed.proj.weight").is_some()
1758 {
1759 pipeline.dsv41_vision = Some(
1760 crate::dsv41_vision::VisionModel::from_model(model, vision_cfg)
1761 .map_err(|e| CmfError::Parse(format!("deepseek_v41 vision: {e}")))?,
1762 );
1763 }
1764 }
1765 tracing::info!(
1766 "deepseek_v41: loaded {} layers, {} KV sources, {} index sources, {} Engram layers; experts remain mmap-backed",
1767 dl.len(),
1768 cfg.kv_sources.len(),
1769 cfg.index_sources.len(),
1770 cfg.engram_layers.len()
1771 );
1772 pipeline.dsv41 = Some(Box::new((g, dl, cfg, st)));
1773 }
1774 if arch.arch_name == "deepseek_v4" {
1779 let moe = arch
1780 .moe
1781 .as_ref()
1782 .ok_or_else(|| CmfError::Parse("deepseek_v4: no moe config".into()))?;
1783 let cfg = crate::dsv4::Dsv4Cfg {
1784 dim: arch.hidden_size,
1785 n_heads: arch.num_attention_heads,
1786 head_dim: arch.head_dim,
1787 rope_head_dim: if arch.partial_rotary_factor < 1.0 {
1794 (((arch.head_dim as f32 * arch.partial_rotary_factor) as usize) & !1)
1795 .clamp(2, arch.head_dim)
1796 } else {
1797 64.min(arch.head_dim)
1798 },
1799 q_lora_rank: 0,
1803 o_lora_rank: 0,
1804 o_groups: 8,
1811 hc_mult: 4,
1812 hc_sinkhorn_iters: 20,
1813 hc_eps: 1e-6,
1814 norm_eps: arch.rms_norm_eps as f32,
1815 n_routed_experts: moe.num_experts,
1816 top_k: moe.top_k,
1817 moe_inter: moe.moe_intermediate_size,
1818 route_scale: moe.routed_scaling_factor.unwrap_or(1.0),
1819 swiglu_limit: 10.0,
1825 window: arch.sliding_window.unwrap_or(128),
1826 index_topk: 512,
1827 vocab: arch.vocab_size,
1828 };
1829 let (g, dl) = crate::dsv4::load(model, &cfg, arch.num_layers)
1830 .map_err(|e| CmfError::Parse(format!("deepseek_v4: {e}")))?;
1831 let mut cfg = cfg;
1836 if let Some(l0) = dl.first() {
1837 cfg.q_lora_rank = l0.wq_a.rows();
1838 let attn_width = arch.num_attention_heads * arch.head_dim;
1839 if l0.wo_a.cols() > 0 && attn_width % l0.wo_a.cols() == 0 {
1840 cfg.o_groups = (attn_width / l0.wo_a.cols()).max(1);
1841 }
1842 cfg.o_lora_rank = l0.wo_b.cols() / cfg.o_groups.max(1);
1843 cfg.hc_mult = (l0.hc_attn_fn.len() / l0.hc_attn_base.len().max(1)) / cfg.dim.max(1);
1844 if cfg.hc_mult == 0 {
1845 cfg.hc_mult = 4;
1846 }
1847 }
1848 let (yf, yo, ybf, ybs) = match &arch.yarn {
1861 Some(y) => (
1862 y.factor,
1863 y.original_max_position_embeddings,
1864 y.beta_fast,
1865 y.beta_slow,
1866 ),
1867 None => {
1868 tracing::warn!(
1869 "deepseek_v4: the header carries no YaRN profile — \
1870 falling back to the release's (factor 16, original \
1871 65536, beta 32/1). Re-converting with a build that \
1872 reads rope_scaling.type would make this exact."
1873 );
1874 (16.0, 65536, 32.0, 1.0)
1875 }
1876 };
1877 pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
1878 cfg.rope_head_dim,
1879 arch.rope_theta as f32,
1880 yf,
1881 yo,
1882 ybf,
1883 ybs,
1884 ));
1885 if let Ok(stats) = std::env::var("CMF_MOE_PIN") {
1890 let cover = std::env::var("CMF_MOE_PIN_COVER")
1891 .ok()
1892 .and_then(|v| v.parse::<f64>().ok())
1893 .filter(|&c| c > 0.0 && c <= 1.0)
1894 .unwrap_or(0.95);
1895 let hot = crate::pin::hot_experts(&stats, cover);
1896 let mut names: Vec<String> = Vec::new();
1897 for e in &model.tensors {
1898 let is_expert = e.name.contains(".mlp.experts.");
1899 if !is_expert {
1900 names.push(e.name.clone()); }
1902 }
1903 let mut kept_experts = 0usize;
1904 if let Some(hot) = &hot {
1905 for (li, experts) in hot {
1906 for e in experts {
1907 for w in ["gate_proj", "up_proj", "down_proj"] {
1908 names.push(format!("model.layers.{li}.mlp.experts.{e}.{w}.weight"));
1909 }
1910 kept_experts += 1;
1911 }
1912 }
1913 }
1914 let r = crate::pin::pin_tensors(model, &names);
1915 tracing::info!(
1916 "закреплено {:.1} ГБ ({} тензоров, горячих экспертов {kept_experts}, покрытие {cover}); лимит {}",
1917 r.bytes as f64 / 1e9,
1918 r.tensors,
1919 r.limit
1920 .map(|l| format!("{:.1} ГБ", l as f64 / 1e9))
1921 .unwrap_or_else(|| "неизвестен".into())
1922 );
1923 if r.skipped > 0 {
1924 tracing::warn!("не закреплено тензоров: {}", r.skipped);
1925 }
1926 }
1927 let st = crate::dsv4::Dsv4State::new(arch.num_layers);
1928 let depth = std::env::var("CMF_DSV4_MTP_DEPTH")
1932 .ok()
1933 .and_then(|v| v.parse::<usize>().ok())
1934 .unwrap_or(3);
1935 pipeline.dsv4_mtp = crate::dsv4::load_mtp(model, &cfg, depth);
1936 crate::dsv4::dspark_reserve_note(&pipeline.dsv4_mtp, &cfg, &dl);
1938 pipeline.dsv4 = Some(Box::new((g, dl, cfg, st)));
1939 }
1940 pipeline.short_conv_cfg = short_conv_cfg;
1941 pipeline.mtp = mtp;
1942 pipeline.install_dynamic_routing(model, false);
1943 match ov {
1947 Overlay::One(sid) => {
1948 pipeline.dyn_active = model.header.skills.iter().position(|s| &s.id == sid);
1949 }
1950 Overlay::Blend(_) => pipeline.dyn_blend_loaded = true,
1951 Overlay::None => {}
1952 }
1953 if let Some(c) = &model.header.calibration {
1956 pipeline.set_calib_temp(c.temperature);
1957 }
1958 let o1 = match crate::nystrom::o1_from_env() {
1964 crate::nystrom::O1Env::Off => None,
1965 crate::nystrom::O1Env::On(cfg) => Some(cfg),
1966 crate::nystrom::O1Env::Unset => model
1967 .header
1968 .provenance
1969 .as_ref()
1970 .and_then(|p| p.get("o1_attn"))
1971 .and_then(crate::nystrom::O1Cfg::from_json),
1972 };
1973 if o1.is_some() {
1974 if pipeline.attn_softcap > 0.0 {
1975 return Err(CmfError::Parse(
1976 "--o1 with attention-logit soft-capping (Gemma-2) is not supported: \
1977 the streaming operator has no capped-score form"
1978 .into(),
1979 ));
1980 }
1981 pipeline.set_o1(o1);
1982 }
1983 Ok(pipeline)
1984 }
1985
1986 pub(crate) fn install_dynamic_routing(&mut self, model: &Arc<CmfModel>, force_f32: bool) {
1991 self.model = Some(model.clone());
1992 self.dyn_force_f32 = force_f32;
1993 let mut per_skill = Vec::with_capacity(model.header.skills.len());
1994 for sk in &model.header.skills {
1995 let mut ffn_layers = std::collections::BTreeSet::new();
1996 let mut non_ffn = false;
1997 let prefix = format!("skill.{}.", sk.id);
1998 for t in model.skill_tensors(&sk.id) {
1999 let rel = &t.name[prefix.len()..]; let toks: Vec<&str> = rel.split('.').collect();
2001 if toks.len() >= 5 && toks[0] == "model" && toks[1] == "layers" && toks[3] == "mlp"
2002 {
2003 if let Ok(li) = toks[2].parse::<usize>() {
2004 ffn_layers.insert(li);
2005 continue;
2006 }
2007 }
2008 non_ffn = true; }
2010 if non_ffn {
2011 tracing::warn!(
2012 "skill '{}' replaces non-FFN tensors — excluded from dynamic \
2013 routing (static overlay still works)",
2014 sk.id
2015 );
2016 per_skill.push(None);
2017 } else {
2018 per_skill.push(Some(ffn_layers.into_iter().collect::<Vec<_>>()));
2019 }
2020 }
2021 self.dyn_skill_layers = per_skill;
2022 }
2023
2024 pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
2031 self.reset_session();
2037 if self.dyn_active == idx {
2038 return Ok(());
2039 }
2040 let model = self.model.clone().ok_or_else(|| {
2041 CmfError::Parse("dynamic routing needs a model-backed pipeline".into())
2042 })?;
2043 let mut union: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
2044 if let Some(old) = self.dyn_active {
2045 if let Some(Some(ls)) = self.dyn_skill_layers.get(old) {
2046 union.extend(ls.iter().copied());
2047 }
2048 }
2049 let new_id: Option<String> = match idx {
2050 Some(n) => match self.dyn_skill_layers.get(n) {
2051 Some(Some(ls)) => {
2052 union.extend(ls.iter().copied());
2053 Some(model.header.skills[n].id.clone())
2054 }
2055 _ => {
2056 return Err(CmfError::Parse(format!(
2057 "skill index {n} not dynamic-eligible"
2058 )));
2059 }
2060 },
2061 None => None,
2062 };
2063 let ov = match &new_id {
2064 Some(s) => Overlay::One(s),
2065 None => Overlay::None,
2066 };
2067 let arch = model.arch();
2068 for li in union {
2069 self.weights.layers[li].ffn =
2070 build_layer_ffn(&model, arch, li, self.dyn_force_f32, &ov)?;
2071 }
2072 self.dyn_active = idx;
2073 Ok(())
2074 }
2075}