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