Skip to main content

cortiq_engine/
loader.rs

1//! Weight loader: CMF tensor directory → Pipeline.
2//!
3//! Storage rule: models WITH task masks are dequantized to f32 (masked
4//! execution needs f32 row access; skill files are small by design).
5//! Models without masks keep quantized matrices zero-copy from the mmap
6//! (`QTensor::Mapped`) — this is what lets a 15B file run in a few GB
7//! of RSS instead of 60 GB of f32.
8//!
9//! Layer kinds come from `arch.layer_types`: FullAttention loads
10//! `self_attn.*` (with auto-detected Qwen3.5 extras: per-head qk-norm by
11//! tensor presence, output gate by q_proj row count); LinearAttention
12//! loads the canonical core `vmf_attn.*` (folded at convert time).
13
14use 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
28/// Tensor source selector (spec §9): backbone, one skill's overlay, or
29/// a soft superposition of top-m skills (claim 14 working tensors).
30pub enum Overlay<'a> {
31    None,
32    One(&'a str),
33    /// (skill_id, weight); weights sum to 1 (softmax(−E/T) upstream).
34    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
57/// Weighted working tensor (claim 14): Σ wᵢ·Tᵢ, where Tᵢ is the
58/// skill's replacement when present, else the backbone tensor.
59fn 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
87/// Dequantize a tensor fully into f32 (norms, masked models).
88fn 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
107/// Build one layer's FFN (dense or MoE) under a given overlay. Shared
108/// by the static loader AND dynamic per-token skill switching
109/// (`Pipeline::set_active_skill`): switching skills = rebuilding the
110/// FFN of the touched layers, cheap because Mapped tensors are just
111/// re-resolved mmap pointers (no dequant, no copy).
112pub(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        // FFN triple invariant (holds for dense and each MoE expert;
125        // enforced loudly so a malformed defrag/repack — spec §11 — fails
126        // at load instead of silently mis-computing). inter' is per-layer.
127        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    // Experts enumerate by TENSOR PRESENCE up to the header count — a
160    // moe-defrag'd specialist keeps a per-layer contiguous prefix of
161    // renumbered experts (fewer than arch.moe.num_experts), with the
162    // router rows sliced to match.
163    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    // LFM2-MoE selection bias (`mlp.expert_bias`): present iff the model
195    // routes with a bias; loaded by tensor presence.
196    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    // CMF_MOE_TOPK=N (opt-in): route to fewer experts than the header
203    // asks. MoE decode is memory-bound — every selected expert streams
204    // its three matrices per token — so halving k halves that traffic;
205    // the renormalized top-k keeps the mixture a proper average.
206    // Quality is the experiment — measure ppl before trusting.
207    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    // CMF_MOE_TAU=0.x (opt-in): adaptive routing — see MoeFfn::route_tau.
214    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    // Gemma-4: per-expert weight scale after the top-k renorm; its
230    // presence also marks the scale-less-rms router input (the folded
231    // router gain — see the converter).
232    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    // Gemma-4 dual-branch layer: a dense MLP coexists with the routed
257    // experts, each branch inside its own norm sandwich.
258    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
278/// Task mask over routed experts (opt-in, experimental): DTG-MA applied
279/// to MoE. `CMF_MOE_MASK=<stats.json>` points at a claim-12 B-field dump
280/// (`CMF_MOE_STATS` output — per-layer expert-selection counts from a
281/// task-representative run); `CMF_MOE_MASK_COVER` (default 0.9) keeps,
282/// per layer, the smallest top set of experts reaching that fraction of
283/// the recorded routing mass. Selection then happens over the allowed
284/// set only (softmax renormalizes). Gate any real use on a ppl A/B.
285fn 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    // The layer index rides in the tensor prefix ("model.layers.N.").
313    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    // Claim 14: a blended working tensor is materialized in f32 and
353    // held resident (the overlay-cache slot); single skills stay
354    // zero-copy pointers into the mmap.
355    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    // Tensor-source indirection (spec §9): the skill's replacement is
370    // read in place of the backbone tensor — either/or, never a sum.
371    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    /// Build a runnable pipeline from an opened CMF model.
394    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    /// Same, with a skill overlaid (spec §9): every layer tensor is
402    /// resolved through tensor-source indirection — the skill's
403    /// full-shape replacement is read in place of the backbone tensor.
404    /// No per-skill model is ever assembled: Mapped tensors are
405    /// pointers into the one shared mmap.
406    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    /// Soft superposition (claim 14): working tensors accumulated from
418    /// the given (skill, weight) list — softmax(−E/T) upstream.
419    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        // Masks × quantized mmap: only ATTENTION keeps f32 (the head-mask
488        // path needs f32 slices). FFN masks now run sparse directly on the
489        // quant bytes (sparse_ffn_quant), and embed/lm_head are never
490        // masked — so a masked model runs at quantized RSS, not the old
491        // whole-model-f32 blowup.
492        let masks_present = !model.masks.masks.is_empty();
493        let force_f32 = masks_present; // attention only (head masks)
494
495        // ── Tokenizer: embedded → sidecar → byte-level fallback ──
496        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        // Chat/eos bundle (spec §6.1): the FILE defines chat behavior.
510        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        // Gemma's contract requires <bos> at sequence start, but its
523        // tokenizer.json post-processor does not add it (the chat
524        // template does). Raw prompts need it too — word salad without.
525        if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
526            tokenizer.add_bos = true;
527        }
528
529        // ── Top-level weights (never masked → always quantized) ──
530        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            // Tied: reuse the embedding matrix (re-open, cheap for Mapped).
536            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        // ── Linear-core geometry (required if any linear layer exists) ──
544        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                        // θ-mass (η′): default 0 (massless); CMF_PHASE_MASS
569                        // widens the phase kernel for folded-unhealed models.
570                        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        // ── KDA geometry (Kimi Linear / Kimi-K3 delta-attention layers) ──
597        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        // ── Short-convolution geometry (LFM2 conv mixer layers) ──
618        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        // ── Layers ──
638        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            // DeepSeek-V2 MLA: the latent projections replace the k/v pair.
646            if let Some(mla) = arch.mla.as_ref() {
647                // Compressed q (K3/V3): q_a → rms → q_b; direct otherwise.
648                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                // YaRN mscale²: DeepSeek corrects the softmax scale by
662                // (0.1·mscale_all_dim·ln(factor)+1)².
663                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            // Qwen3.5 output gate: q_proj rows = 2·nh·hd (per-head [q; gate]).
698            // Gemma-4 global layers legitimately have nh·global_head_dim
699            // rows (which can equal 2·nh·hd) — never gated.
700            let output_gate = arch.global_head_dim.is_none() && wq.rows() == 2 * nh * arch.head_dim;
701            // Gemma-4 global layers run MQA at global_head_dim — their
702            // q_proj legitimately carries nh·ghd rows.
703            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            // Qwen2-family projection biases (by tensor presence).
746            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                // Faithful vendor operator: tensor names 1:1 with the source.
770                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            // Selective-write gate κ (hybrid_k core): optional by tensor
798            // presence — files without it run the classic phase kernel
799            // bit-identically.
800            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        // LFM2 short-conv mixer: in_proj [3·hidden, hidden], a depthwise
822        // conv (stored f16 as `[hidden, 1, kernel]` → flattened taps), and
823        // out_proj [hidden, hidden]. Names canonicalized at convert time.
824        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        // KDA layer (Kimi Linear / Kimi-K3): faithful vendor tensors under
842        // the `kda_attn.` canonical prefix. The output gate is full-rank
843        // (g_proj, K3) or low-rank (g_a/g_b, Kimi-Linear-48B) by presence.
844        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            // Gemma-2/3 sandwich: `pre_feedforward_layernorm` present →
895            // it is the pre-FFN norm, and post_attention/post_feedforward
896            // норms apply to the branch OUTPUTS before their residuals.
897            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                // Gemma-4: learned scalar multiplying the layer output.
937                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 always quantized — masks run sparse on quant bytes.
945                ffn: build_layer_ffn(model, &arch, li, false, ov)?,
946                attn,
947            });
948        }
949
950        // ── MTP head (optional, spec §2.1) ──
951        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        // KV window: the descriptor's max, capped for dev-box safety;
1024        // CMF_MAX_SEQ overrides the cap (long-context runs).
1025        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        // Looped Transformer: total virtual layers = physical × num_loops.
1032        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, // physical layers in weights
1049            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        // Gemma-family extras: embedding scale, attention-scale
1072        // override, and (Gemma-3) sliding-window layers with their own
1073        // local RoPE base.
1074        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        // Gemma-4: global layers run their own geometry (MQA at
1111        // global_head_dim) with a proportional RoPE — the first
1112        // factor·head_dim dims rotate, the zero-padded tail is identity.
1113        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            // Re-shape the global layers' KV storage to their geometry.
1124            // An explicit layer_types map wins over the numeric pattern
1125            // (explicit tags set swa's pattern to usize::MAX, which
1126            // would otherwise leave every global cache mis-shaped).
1127            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        // MLA (DeepSeek-V2): the expand-to-MHA cache holds nh heads of
1143        // rope+nope dims; rotary covers the rope prefix.
1144        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        // Per-frequency rope divisors (MiniCPM3 longrope short_factor):
1163        // served at the native window with the trained per-dim factors.
1164        // Applied after every inv_freq build (plain, YaRN, MLA).
1165        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        // Record the load-time overlay so a later set_active_skill(None)
1289        // correctly reverts it (the union-diff assumes dyn_active mirrors
1290        // the live overlay). Blend loads have no single index to revert.
1291        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        // B1: apply the measured confidence-calibration temperature, if the
1299        // file carries one (softmax(logits / T) for reported Born mass).
1300        if let Some(c) = &model.header.calibration {
1301            pipeline.set_calib_temp(c.temperature);
1302        }
1303        // O(1) Nyström attention (runtime-level, no format change):
1304        // env CMF_O1 decides; unset falls through to the converter hint
1305        // in header.provenance.o1_attn (`cortiq convert --o1`), and
1306        // CMF_O1=off force-disables even the hint. CLI flags override
1307        // later via set_o1().
1308        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    /// Record per-skill dynamic-routing metadata: which FFN layers each
1332    /// skill actually replaces (derived from the tensors present, not
1333    /// the meta `layers` field), and whether the skill is eligible for
1334    /// cheap dynamic switching (FFN-only). Called once at load.
1335    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()..]; // e.g. model.layers.20.mlp.down_proj.weight
1345                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; // replaces attention / embed / lm_head
1354            }
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    /// Switch the overlaid skill for subsequent forwards (dynamic
1370    /// routing). `idx` = index into model.header.skills; None = backbone.
1371    /// Rebuilds the FFN of the union of the old and new skill's touched
1372    /// layers with the new overlay — tensor-source indirection made
1373    /// dynamic. Cheap: Mapped tensors are re-resolved mmap pointers.
1374    /// Result is bit-identical to loading the pipeline with that skill.
1375    pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
1376        // Overlay swap changes weights → every cached K/V is stale.
1377        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}