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