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() { &sname } else { name };
64        let t = dequant_by_name(model, src)?;
65        match &mut acc {
66            None => {
67                let mut t = t;
68                for v in t.iter_mut() {
69                    *v *= w;
70                }
71                acc = Some(t);
72            }
73            Some(a) => {
74                for (av, tv) in a.iter_mut().zip(&t) {
75                    *av += w * tv;
76                }
77            }
78        }
79    }
80    acc.ok_or_else(|| "empty blend".into())
81}
82
83/// Dequantize a tensor fully into f32 (norms, masked models).
84fn load_f32(model: &CmfModel, name: &str, ov: &Overlay) -> Result<Vec<f32>, String> {
85    if ov.blend_touches(model, name) {
86        if let Overlay::Blend(list) = ov {
87            return blend_f32(model, name, list);
88        }
89    }
90    let skill = match ov {
91        Overlay::One(s) => Some(*s),
92        _ => None,
93    };
94    let entry = model
95        .resolve_tensor(name, skill)
96        .ok_or_else(|| format!("tensor '{name}' not found in CMF directory"))?;
97    let bytes = model.entry_bytes(entry);
98    let mut out = vec![0.0f32; entry.n_elems()];
99    dequant_tensor(entry, bytes, &mut out)?;
100    Ok(out)
101}
102
103/// Build one layer's FFN (dense or MoE) under a given overlay. Shared
104/// by the static loader AND dynamic per-token skill switching
105/// (`Pipeline::set_active_skill`): switching skills = rebuilding the
106/// FFN of the touched layers, cheap because Mapped tensors are just
107/// re-resolved mmap pointers (no dequant, no copy).
108pub(crate) fn build_layer_ffn(
109    model: &Arc<CmfModel>,
110    arch: &ModelArch,
111    li: usize,
112    force_f32: bool,
113    ov: &Overlay,
114) -> Result<FfnKind, CmfError> {
115    let prefix = format!("model.layers.{li}.");
116    let load_dense = |p: &str| -> Result<DenseFfn, CmfError> {
117        let gate_proj = load_matrix(model, &format!("{p}gate_proj.weight"), force_f32, ov)?;
118        let up_proj = load_matrix(model, &format!("{p}up_proj.weight"), force_f32, ov)?;
119        let down_proj = load_matrix(model, &format!("{p}down_proj.weight"), force_f32, ov)?;
120        // FFN triple invariant (holds for dense and each MoE expert;
121        // enforced loudly so a malformed defrag/repack — spec §11 — fails
122        // at load instead of silently mis-computing). inter' is per-layer.
123        let inter = gate_proj.rows();
124        if up_proj.rows() != inter || down_proj.cols() != inter {
125            return Err(CmfError::Parse(format!(
126                "{p}: FFN dims disagree (gate.rows={inter}, up.rows={}, \
127                 down.cols={}); all three must equal inter'",
128                up_proj.rows(),
129                down_proj.cols()
130            )));
131        }
132        if down_proj.rows() != arch.hidden_size {
133            return Err(CmfError::Parse(format!(
134                "{p}: down_proj.rows={} != hidden_size={}",
135                down_proj.rows(),
136                arch.hidden_size
137            )));
138        }
139        Ok(DenseFfn {
140            gate_proj,
141            up_proj,
142            down_proj,
143            act: crate::pipeline::Act::from_arch(&arch.hidden_act),
144        })
145    };
146    let router_name = format!("{prefix}mlp.gate.weight");
147    if model.tensor(&router_name).is_none() {
148        return Ok(FfnKind::Dense(load_dense(&format!("{prefix}mlp."))?));
149    }
150    let cfg = arch.moe.as_ref().ok_or_else(|| {
151        CmfError::Parse(format!("{router_name} present but header has no arch.moe block"))
152    })?;
153    let experts = (0..cfg.num_experts)
154        .map(|e| load_dense(&format!("{prefix}mlp.experts.{e}.")))
155        .collect::<Result<Vec<_>, _>>()?;
156    let shared = if model
157        .tensor(&format!("{prefix}mlp.shared_expert.gate_proj.weight"))
158        .is_some()
159    {
160        Some((
161            load_dense(&format!("{prefix}mlp.shared_expert."))?,
162            load_matrix(model, &format!("{prefix}mlp.shared_expert_gate.weight"), force_f32, ov)?,
163        ))
164    } else {
165        None
166    };
167    // LFM2-MoE selection bias (`mlp.expert_bias`): present iff the model
168    // routes with a bias; loaded by tensor presence.
169    let bias_name = format!("{prefix}mlp.expert_bias");
170    let expert_bias = if model.tensor(&bias_name).is_some() {
171        Some(load_f32(model, &bias_name, ov).map_err(CmfError::Parse)?)
172    } else {
173        None
174    };
175    Ok(FfnKind::Moe(MoeFfn {
176        router: load_matrix(model, &router_name, force_f32, ov)?,
177        experts,
178        top_k: cfg.top_k,
179        norm_topk_prob: cfg.norm_topk_prob,
180        router_sigmoid: cfg.router_sigmoid,
181        expert_bias,
182        routed_scaling: cfg.routed_scaling_factor.unwrap_or(1.0),
183        shared,
184        stats: std::cell::RefCell::new(Vec::new()),
185    }))
186}
187
188fn load_matrix(
189    model: &Arc<CmfModel>,
190    name: &str,
191    force_f32: bool,
192    ov: &Overlay,
193) -> Result<QTensor, CmfError> {
194    // Claim 14: a blended working tensor is materialized in f32 and
195    // held resident (the overlay-cache slot); single skills stay
196    // zero-copy pointers into the mmap.
197    if ov.blend_touches(model, name) {
198        if let Overlay::Blend(list) = ov {
199            let entry = model
200                .tensor(name)
201                .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
202            let data = blend_f32(model, name, list)
203                .map_err(|e| CmfError::Parse(format!("blend: {e}")))?;
204            return Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]));
205        }
206    }
207    let skill = match ov {
208        Overlay::One(s) => Some(*s),
209        _ => None,
210    };
211    // Tensor-source indirection (spec §9): the skill's replacement is
212    // read in place of the backbone tensor — either/or, never a sum.
213    let name: &str = &match skill {
214        Some(sid) if model.tensor(&format!("skill.{sid}.{name}")).is_some() => {
215            format!("skill.{sid}.{name}")
216        }
217        _ => name.to_string(),
218    };
219    let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
220    if force_f32 {
221        let entry = model
222            .tensor(name)
223            .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
224        if entry.shape.len() != 2 {
225            return Err(err(format!("'{name}' is not 2-D")));
226        }
227        let data = load_f32(model, name, &Overlay::None).map_err(err)?;
228        Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]))
229    } else {
230        QTensor::from_model(model, name).map_err(err)
231    }
232}
233
234impl Pipeline {
235    /// Build a runnable pipeline from an opened CMF model.
236    pub fn from_model(model: &Arc<CmfModel>, sampler_config: SamplerConfig) -> Result<Self, CmfError> {
237        Self::from_model_with_skill(model, sampler_config, None)
238    }
239
240    /// Same, with a skill overlaid (spec §9): every layer tensor is
241    /// resolved through tensor-source indirection — the skill's
242    /// full-shape replacement is read in place of the backbone tensor.
243    /// No per-skill model is ever assembled: Mapped tensors are
244    /// pointers into the one shared mmap.
245    pub fn from_model_with_skill(
246        model: &Arc<CmfModel>,
247        sampler_config: SamplerConfig,
248        skill: Option<&str>,
249    ) -> Result<Self, CmfError> {
250        match skill {
251            Some(s) => Self::from_model_with_overlay(model, sampler_config, &Overlay::One(s)),
252            None => Self::from_model_with_overlay(model, sampler_config, &Overlay::None),
253        }
254    }
255
256    /// Soft superposition (claim 14): working tensors accumulated from
257    /// the given (skill, weight) list — softmax(−E/T) upstream.
258    pub fn from_model_with_blend(
259        model: &Arc<CmfModel>,
260        sampler_config: SamplerConfig,
261        blend: &[(String, f32)],
262    ) -> Result<Self, CmfError> {
263        Self::from_model_with_overlay(model, sampler_config, &Overlay::Blend(blend))
264    }
265
266    fn from_model_with_overlay(
267        model: &Arc<CmfModel>,
268        sampler_config: SamplerConfig,
269        ov: &Overlay,
270    ) -> Result<Self, CmfError> {
271        let skill = match ov {
272            Overlay::One(s) => Some(*s),
273            _ => None,
274        };
275        if let Some(sid) = skill {
276            let known = model.header.skills.iter().any(|s| s.id == sid)
277                || model.skill_tensors(sid).next().is_some();
278            if !known {
279                return Err(CmfError::Parse(format!(
280                    "skill '{sid}' not in this container (header.skills: {:?})",
281                    model.header.skills.iter().map(|s| &s.id).collect::<Vec<_>>()
282                )));
283            }
284            tracing::info!(
285                "skill '{sid}': {} replacement tensors overlaid",
286                model.skill_tensors(sid).count()
287            );
288        }
289        let arch = model.arch().clone();
290        let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
291
292        // Masks × quantized mmap: only ATTENTION keeps f32 (the head-mask
293        // path needs f32 slices). FFN masks now run sparse directly on the
294        // quant bytes (sparse_ffn_quant), and embed/lm_head are never
295        // masked — so a masked model runs at quantized RSS, not the old
296        // whole-model-f32 blowup.
297        let masks_present = !model.masks.masks.is_empty();
298        let force_f32 = masks_present; // attention only (head masks)
299
300        // ── Tokenizer: embedded → sidecar → byte-level fallback ──
301        let mut tokenizer = if let Some(vocab_bytes) = &model.vocab {
302            Tokenizer::from_bytes(vocab_bytes)
303                .map_err(|e| CmfError::Parse(format!("embedded tokenizer: {e}")))?
304        } else {
305            let sidecar = model.path.with_file_name("tokenizer.json");
306            if sidecar.exists() {
307                Tokenizer::from_file(&sidecar)
308                    .map_err(|e| CmfError::Parse(format!("sidecar tokenizer: {e}")))?
309            } else {
310                tracing::warn!("no tokenizer in file or sidecar — using byte-level fallback");
311                Tokenizer::byte_level()
312            }
313        };
314        // Chat/eos bundle (spec §6.1): the FILE defines chat behavior.
315        if let Some(tc) = &model.header.tokenizer_config {
316            tokenizer.chat_template = tc.chat_template.clone();
317            tokenizer.extra_eos.extend(tc.eos_token_ids.iter().copied());
318            if tokenizer.bos_token_id.is_none() {
319                tokenizer.bos_token_id = tc.bos_token_id;
320            }
321            tracing::info!(
322                "chat bundle: template {} chars, {} stop ids",
323                tc.chat_template.as_deref().map(str::len).unwrap_or(0),
324                tc.eos_token_ids.len()
325            );
326        }
327        // Gemma's contract requires <bos> at sequence start, but its
328        // tokenizer.json post-processor does not add it (the chat
329        // template does). Raw prompts need it too — word salad without.
330        if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
331            tokenizer.add_bos = true;
332        }
333
334        // ── Top-level weights (never masked → always quantized) ──
335        let embed_tokens = load_matrix(model, "model.embed_tokens.weight", false, ov)?;
336        let final_norm = load_f32(model, "model.norm.weight", ov).map_err(err)?;
337        let lm_head = if model.tensor("lm_head.weight").is_some() {
338            load_matrix(model, "lm_head.weight", false, ov)?
339        } else if arch.tie_word_embeddings {
340            // Tied: reuse the embedding matrix (re-open, cheap for Mapped).
341            load_matrix(model, "model.embed_tokens.weight", false, ov)?
342        } else {
343            return Err(CmfError::MissingTensor(
344                "lm_head.weight (and tie_word_embeddings is false)".into(),
345            ));
346        };
347
348        // ── Linear-core geometry (required if any linear layer exists) ──
349        let has_linear = arch
350            .layer_types
351            .iter()
352            .any(|t| matches!(t, LayerType::LinearAttention));
353        let mut vmf_cfg = None;
354        let mut gdn_cfg = None;
355        if has_linear {
356            let lc = arch.linear_core.as_ref().ok_or_else(|| {
357                CmfError::Parse(
358                    "model has LinearAttention layers but no arch.linear_core — \
359                     reconvert with the current converter"
360                        .into(),
361                )
362            })?;
363            let need = |v: Option<usize>, name: &str| {
364                v.ok_or_else(|| CmfError::Parse(format!("linear core needs arch.{name}")))
365            };
366            match lc.kind.as_str() {
367                "vmf_phase" => {
368                    vmf_cfg = Some(VmfPhaseCfg {
369                        num_heads: lc.num_heads,
370                        nphase: need(lc.nphase, "linear_core.nphase")?,
371                        value_head_dim: lc.value_head_dim,
372                        hidden_size: arch.hidden_size,
373                        // θ-mass (η′): default 0 (massless); CMF_PHASE_MASS
374                        // widens the phase kernel for folded-unhealed models.
375                        phase_mass: std::env::var("CMF_PHASE_MASS")
376                            .ok()
377                            .and_then(|v| v.parse().ok())
378                            .unwrap_or(0.0),
379                    });
380                }
381                "gated_delta_net" => {
382                    gdn_cfg = Some(GdnCfg {
383                        num_v_heads: lc.num_heads,
384                        num_k_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
385                        key_head_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
386                        value_head_dim: lc.value_head_dim,
387                        conv_kernel: need(
388                            arch.linear_conv_kernel_dim,
389                            "linear_conv_kernel_dim",
390                        )?,
391                        hidden_size: arch.hidden_size,
392                        rms_eps: arch.rms_norm_eps as f64,
393                    });
394                }
395                other => {
396                    return Err(CmfError::Parse(format!(
397                        "unknown linear core '{other}' (this runtime executes: \
398                         gated_delta_net, vmf_phase)"
399                    )));
400                }
401            }
402        }
403
404        // ── Short-convolution geometry (LFM2 conv mixer layers) ──
405        let has_short_conv =
406            arch.layer_types.iter().any(|t| matches!(t, LayerType::ShortConv));
407        let short_conv_cfg = if has_short_conv {
408            Some(ShortConvCfg {
409                hidden_size: arch.hidden_size,
410                kernel: arch.linear_conv_kernel_dim.ok_or_else(|| {
411                    CmfError::Parse(
412                        "model has ShortConv layers but no arch.linear_conv_kernel_dim — \
413                         reconvert with the current converter"
414                            .into(),
415                    )
416                })?,
417            })
418        } else {
419            None
420        };
421
422        // ── Layers ──
423        let load_full_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
424            let t = |suffix: &str| load_matrix(model, &format!("{prefix}{suffix}"), force_f32, ov);
425            let n = |suffix: &str| -> Option<Vec<f32>> {
426                model
427                    .tensor(&format!("{prefix}{suffix}"))
428                    .and_then(|_| load_f32(model, &format!("{prefix}{suffix}"), ov).ok())
429            };
430            let wq = t("self_attn.q_proj.weight")?;
431            // Qwen3.5 output gate: q_proj rows = 2·nh·hd (per-head [q; gate]).
432            // Gemma-4 global layers legitimately have nh·global_head_dim
433            // rows (which can equal 2·nh·hd) — never gated.
434            let output_gate = arch.global_head_dim.is_none()
435                && wq.rows() == 2 * arch.num_attention_heads * arch.head_dim;
436            // Qwen2-family projection biases (by tensor presence).
437            let bias = match (
438                n("self_attn.q_proj.bias"),
439                n("self_attn.k_proj.bias"),
440                n("self_attn.v_proj.bias"),
441            ) {
442                (Some(a), Some(b), Some(c)) => Some((a, b, c)),
443                _ => None,
444            };
445            Ok(AttnKind::Full {
446                wq,
447                wk: t("self_attn.k_proj.weight")?,
448                wv: t("self_attn.v_proj.weight")?,
449                wo: t("self_attn.o_proj.weight")?,
450                q_norm: n("self_attn.q_norm.weight"),
451                k_norm: n("self_attn.k_norm.weight"),
452                output_gate,
453                bias,
454            })
455        };
456
457        let load_linear_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
458            if gdn_cfg.is_some() {
459                // Faithful vendor operator: tensor names 1:1 with the source.
460                let t = |suffix: &str| {
461                    load_matrix(model, &format!("{prefix}linear_attn.{suffix}"), force_f32, ov)
462                };
463                let f = |suffix: &str| {
464                    load_f32(model, &format!("{prefix}linear_attn.{suffix}"), ov).map_err(err)
465                };
466                return Ok(AttnKind::LinearGdn(GdnWeights {
467                    in_proj_qkv: t("in_proj_qkv.weight")?,
468                    in_proj_z: t("in_proj_z.weight")?,
469                    in_proj_a: t("in_proj_a.weight")?,
470                    in_proj_b: t("in_proj_b.weight")?,
471                    conv1d: f("conv1d.weight")?,
472                    a_log: f("A_log")?,
473                    dt_bias: f("dt_bias")?,
474                    norm: f("norm.weight")?,
475                    out_proj: t("out_proj.weight")?,
476                }));
477            }
478            let t = |suffix: &str| load_matrix(model, &format!("{prefix}vmf_attn.{suffix}"), force_f32, ov);
479            let a_log = load_f32(model, &format!("{prefix}vmf_attn.A_log"), ov).map_err(err)?;
480            // Selective-write gate κ (hybrid_k core): optional by tensor
481            // presence — files without it run the classic phase kernel
482            // bit-identically.
483            let k_gate = if model
484                .tensor(&format!("{prefix}vmf_attn.k_gate.weight"))
485                .is_some()
486            {
487                Some((
488                    t("k_gate.weight")?,
489                    load_f32(model, &format!("{prefix}vmf_attn.k_gate.bias"), ov).map_err(err)?,
490                ))
491            } else {
492                None
493            };
494            Ok(AttnKind::Linear(VmfPhaseWeights {
495                thq: t("thq.weight")?,
496                thk: t("thk.weight")?,
497                v_proj: t("v_proj.weight")?,
498                out_proj: t("out_proj.weight")?,
499                decay: a_log
500                    .iter()
501                    .map(|&a| (-(a as f64).exp()).exp())
502                    .collect(),
503                k_gate,
504            }))
505        };
506
507        // LFM2 short-conv mixer: in_proj [3·hidden, hidden], a depthwise
508        // conv (stored f16 as `[hidden, 1, kernel]` → flattened taps), and
509        // out_proj [hidden, hidden]. Names canonicalized at convert time.
510        let load_short_conv = |prefix: &str| -> Result<AttnKind, CmfError> {
511            let t = |suffix: &str| {
512                load_matrix(model, &format!("{prefix}short_conv.{suffix}"), force_f32, ov)
513            };
514            Ok(AttnKind::ShortConv(ShortConvWeights {
515                in_proj: t("in_proj.weight")?,
516                conv: load_f32(model, &format!("{prefix}short_conv.conv.weight"), ov)
517                    .map_err(err)?,
518                out_proj: t("out_proj.weight")?,
519            }))
520        };
521
522        let mut layers = Vec::with_capacity(arch.num_layers);
523        for li in 0..arch.num_layers {
524            let prefix = format!("model.layers.{li}.");
525            let attn = match arch.layer_types.get(li) {
526                Some(LayerType::LinearAttention) => load_linear_attn(&prefix)?,
527                Some(LayerType::ShortConv) => load_short_conv(&prefix)?,
528                _ => load_full_attn(&prefix)?,
529            };
530            // Gemma-2/3 sandwich: `pre_feedforward_layernorm` present →
531            // it is the pre-FFN norm, and post_attention/post_feedforward
532            // норms apply to the branch OUTPUTS before their residuals.
533            let pre_ffn = format!("{prefix}pre_feedforward_layernorm.weight");
534            let sandwich = model.tensor(&pre_ffn).is_some();
535            layers.push(LayerWeights {
536                input_norm: load_f32(model, &format!("{prefix}input_layernorm.weight"), ov).map_err(err)?,
537                post_norm: if sandwich {
538                    load_f32(model, &pre_ffn, ov).map_err(err)?
539                } else {
540                    load_f32(model, &format!("{prefix}post_attention_layernorm.weight"), ov)
541                        .map_err(err)?
542                },
543                attn_out_norm: if sandwich {
544                    Some(
545                        load_f32(model, &format!("{prefix}post_attention_layernorm.weight"), ov)
546                            .map_err(err)?,
547                    )
548                } else {
549                    None
550                },
551                ffn_out_norm: if sandwich {
552                    Some(
553                        load_f32(model, &format!("{prefix}post_feedforward_layernorm.weight"), ov)
554                            .map_err(err)?,
555                    )
556                } else {
557                    None
558                },
559                // Gemma-4: learned scalar multiplying the layer output.
560                layer_scale: model
561                    .tensor(&format!("{prefix}layer_scalar"))
562                    .and_then(|_| {
563                        load_f32(model, &format!("{prefix}layer_scalar"), ov)
564                            .ok()
565                            .and_then(|v| v.first().copied())
566                    }),
567                // FFN always quantized — masks run sparse on quant bytes.
568                ffn: build_layer_ffn(model, &arch, li, false, ov)?,
569                attn,
570            });
571        }
572
573        // ── MTP head (optional, spec §2.1) ──
574        let mtp = if let Some(cfg) = &arch.mtp {
575            if cfg.num_layers != 1 {
576                return Err(CmfError::Parse(format!(
577                    "MTP with {} blocks not supported yet (only 1)",
578                    cfg.num_layers
579                )));
580            }
581            let p = "model.mtp.";
582            let attn = load_full_attn("model.mtp.layers.0.")?;
583            Some(MtpModule {
584                enorm: load_f32(model, &format!("{p}enorm.weight"), ov).map_err(err)?,
585                hnorm: load_f32(model, &format!("{p}hnorm.weight"), ov).map_err(err)?,
586                eh_proj: load_matrix(model, &format!("{p}eh_proj.weight"), false, ov)?,
587                layer: LayerWeights {
588                    attn_out_norm: None,
589                    ffn_out_norm: None,
590                    layer_scale: None,
591                    input_norm: load_f32(model, &format!("{p}layers.0.input_layernorm.weight"), ov)
592                        .map_err(err)?,
593                    post_norm: load_f32(
594                        model,
595                        &format!("{p}layers.0.post_attention_layernorm.weight"),
596                        ov,
597                    )
598                    .map_err(err)?,
599                    ffn: FfnKind::Dense(DenseFfn {
600                        gate_proj: load_matrix(model, &format!("{p}layers.0.mlp.gate_proj.weight"), false, ov)?,
601                        up_proj: load_matrix(model, &format!("{p}layers.0.mlp.up_proj.weight"), false, ov)?,
602                        down_proj: load_matrix(model, &format!("{p}layers.0.mlp.down_proj.weight"), false, ov)?,
603                        act: crate::pipeline::Act::from_arch(&arch.hidden_act),
604                    }),
605                    attn,
606                },
607                final_norm: load_f32(model, &format!("{p}norm.weight"), ov).map_err(err)?,
608                kv: LayerKvCache::new(arch.num_kv_heads, arch.head_dim),
609            })
610        } else {
611            None
612        };
613
614        tracing::info!(
615            "Pipeline loaded: {} | {}L ({} linear) | {:.2}B params | storage: {} | MTP: {}",
616            arch.arch_name,
617            arch.num_layers,
618            arch.layer_types
619                .iter()
620                .filter(|t| matches!(t, LayerType::LinearAttention))
621                .count(),
622            model.total_param_count() as f64 / 1e9,
623            if force_f32 { "f32 (masked)" } else { "quantized mmap" },
624            if mtp.is_some() { "yes" } else { "no" }
625        );
626
627        // KV window: the descriptor's max, capped for dev-box safety;
628        // CMF_MAX_SEQ overrides the cap (long-context runs).
629        let cap = std::env::var("CMF_MAX_SEQ")
630            .ok()
631            .and_then(|v| v.parse::<usize>().ok())
632            .unwrap_or(8192);
633        let max_seq_len = arch.max_position_embeddings.min(cap);
634
635        let mut pipeline = Pipeline::new(
636            tokenizer,
637            PipelineWeights {
638                embed_tokens,
639                layers,
640                lm_head,
641                final_norm,
642            },
643            arch.hidden_size,
644            arch.intermediate_size,
645            arch.num_attention_heads,
646            arch.num_kv_heads,
647            arch.head_dim,
648            arch.num_layers,
649            arch.vocab_size,
650            arch.rms_norm_eps,
651            arch.rope_theta as f32,
652            arch.norm_style,
653            max_seq_len,
654            sampler_config,
655        );
656        let rotary = ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2);
657        pipeline.set_rotary(rotary, arch.rope_theta as f32);
658        // Gemma-family extras: embedding scale, attention-scale
659        // override, and (Gemma-3) sliding-window layers with their own
660        // local RoPE base.
661        pipeline.embed_multiplier = arch.embed_multiplier;
662        if let Some(qpas) = arch.query_pre_attn_scalar {
663            pipeline.attn_scale = 1.0 / (qpas as f32).sqrt();
664        }
665        if let (Some(w), Some(p)) = (arch.sliding_window, arch.sliding_window_pattern) {
666            pipeline.swa = Some((w, p));
667            if let Some(base) = arch.rope_local_base_freq {
668                pipeline.inv_freq_local = Some(std::sync::Arc::new(
669                    crate::attention::rope_inv_freq(rotary, base as f32),
670                ));
671            }
672        }
673        // Gemma-4: global layers run their own geometry (MQA at
674        // global_head_dim) with a proportional RoPE — the first
675        // factor·head_dim dims rotate, the zero-padded tail is identity.
676        if let (Some(ghd), Some(gkv)) = (arch.global_head_dim, arch.num_global_kv_heads) {
677            pipeline.global_attn = Some((ghd, gkv));
678            let prf = arch.global_partial_rotary_factor.unwrap_or(1.0);
679            let half = ghd / 2;
680            let ra = (((prf * ghd as f32) as usize) / 2).min(half);
681            let mut f = vec![0.0f32; half];
682            for (i, slot) in f.iter_mut().enumerate().take(ra) {
683                *slot = 1.0 / (arch.rope_theta as f32).powf(2.0 * i as f32 / ghd as f32);
684            }
685            pipeline.inv_freq_global = Some(std::sync::Arc::new(f));
686            // Re-shape the global layers' KV storage to their geometry.
687            if let Some((_, p)) = pipeline.swa {
688                for li in 0..arch.num_layers {
689                    if (li + 1) % p.max(1) == 0 {
690                        pipeline.kv_cache.layers[li] =
691                            crate::kv_cache::LayerKvCache::new(gkv, ghd);
692                    }
693                }
694            }
695        }
696        pipeline.attn_v_norm = arch.attn_v_norm;
697        pipeline.final_softcap = arch.final_logit_softcapping.map(|c| c as f32);
698        pipeline.vmf_cfg = vmf_cfg;
699        pipeline.gdn_cfg = gdn_cfg;
700        pipeline.short_conv_cfg = short_conv_cfg;
701        pipeline.mtp = mtp;
702        pipeline.install_dynamic_routing(model, false);
703        // Record the load-time overlay so a later set_active_skill(None)
704        // correctly reverts it (the union-diff assumes dyn_active mirrors
705        // the live overlay). Blend loads have no single index to revert.
706        match ov {
707            Overlay::One(sid) => {
708                pipeline.dyn_active = model.header.skills.iter().position(|s| &s.id == sid);
709            }
710            Overlay::Blend(_) => pipeline.dyn_blend_loaded = true,
711            Overlay::None => {}
712        }
713        // B1: apply the measured confidence-calibration temperature, if the
714        // file carries one (softmax(logits / T) for reported Born mass).
715        if let Some(c) = &model.header.calibration {
716            pipeline.set_calib_temp(c.temperature);
717        }
718        // O(1) Nyström attention (runtime-level, no format change):
719        // env CMF_O1 decides; unset falls through to the converter hint
720        // in header.provenance.o1_attn (`cortiq convert --o1`), and
721        // CMF_O1=off force-disables even the hint. CLI flags override
722        // later via set_o1().
723        let o1 = match crate::nystrom::o1_from_env() {
724            crate::nystrom::O1Env::Off => None,
725            crate::nystrom::O1Env::On(cfg) => Some(cfg),
726            crate::nystrom::O1Env::Unset => model
727                .header
728                .provenance
729                .as_ref()
730                .and_then(|p| p.get("o1_attn"))
731                .and_then(crate::nystrom::O1Cfg::from_json),
732        };
733        if o1.is_some() {
734            pipeline.set_o1(o1);
735        }
736        Ok(pipeline)
737    }
738
739    /// Record per-skill dynamic-routing metadata: which FFN layers each
740    /// skill actually replaces (derived from the tensors present, not
741    /// the meta `layers` field), and whether the skill is eligible for
742    /// cheap dynamic switching (FFN-only). Called once at load.
743    pub(crate) fn install_dynamic_routing(
744        &mut self,
745        model: &Arc<CmfModel>,
746        force_f32: bool,
747    ) {
748        self.model = Some(model.clone());
749        self.dyn_force_f32 = force_f32;
750        let mut per_skill = Vec::with_capacity(model.header.skills.len());
751        for sk in &model.header.skills {
752            let mut ffn_layers = std::collections::BTreeSet::new();
753            let mut non_ffn = false;
754            let prefix = format!("skill.{}.", sk.id);
755            for t in model.skill_tensors(&sk.id) {
756                let rel = &t.name[prefix.len()..]; // e.g. model.layers.20.mlp.down_proj.weight
757                let toks: Vec<&str> = rel.split('.').collect();
758                if toks.len() >= 5
759                    && toks[0] == "model"
760                    && toks[1] == "layers"
761                    && toks[3] == "mlp"
762                {
763                    if let Ok(li) = toks[2].parse::<usize>() {
764                        ffn_layers.insert(li);
765                        continue;
766                    }
767                }
768                non_ffn = true; // replaces attention / embed / lm_head
769            }
770            if non_ffn {
771                tracing::warn!(
772                    "skill '{}' replaces non-FFN tensors — excluded from dynamic \
773                     routing (static overlay still works)",
774                    sk.id
775                );
776                per_skill.push(None);
777            } else {
778                per_skill.push(Some(ffn_layers.into_iter().collect::<Vec<_>>()));
779            }
780        }
781        self.dyn_skill_layers = per_skill;
782    }
783
784    /// Switch the overlaid skill for subsequent forwards (dynamic
785    /// routing). `idx` = index into model.header.skills; None = backbone.
786    /// Rebuilds the FFN of the union of the old and new skill's touched
787    /// layers with the new overlay — tensor-source indirection made
788    /// dynamic. Cheap: Mapped tensors are re-resolved mmap pointers.
789    /// Result is bit-identical to loading the pipeline with that skill.
790    pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
791        if self.dyn_active == idx {
792            return Ok(());
793        }
794        let model = self
795            .model
796            .clone()
797            .ok_or_else(|| CmfError::Parse("dynamic routing needs a model-backed pipeline".into()))?;
798        let mut union: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
799        if let Some(old) = self.dyn_active {
800            if let Some(Some(ls)) = self.dyn_skill_layers.get(old) {
801                union.extend(ls.iter().copied());
802            }
803        }
804        let new_id: Option<String> = match idx {
805            Some(n) => match self.dyn_skill_layers.get(n) {
806                Some(Some(ls)) => {
807                    union.extend(ls.iter().copied());
808                    Some(model.header.skills[n].id.clone())
809                }
810                _ => {
811                    return Err(CmfError::Parse(format!(
812                        "skill index {n} not dynamic-eligible"
813                    )))
814                }
815            },
816            None => None,
817        };
818        let ov = match &new_id {
819            Some(s) => Overlay::One(s),
820            None => Overlay::None,
821        };
822        let arch = model.arch();
823        for li in union {
824            self.weights.layers[li].ffn =
825                build_layer_ffn(&model, arch, li, self.dyn_force_f32, &ov)?;
826        }
827        self.dyn_active = idx;
828        Ok(())
829    }
830}