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