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).
88pub(crate) fn 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    build_ffn_at(model, arch, &format!("model.layers.{li}."), force_f32, ov)
120}
121
122/// The FFN under an arbitrary prefix. Split out of `build_layer_ffn` so the
123/// MTP block can reuse it: Qwen3.6's MTP layer carries a full MoE mlp
124/// (router + 256 experts + shared expert), not the dense one the head was
125/// first written against.
126pub(crate) fn build_ffn_at(
127    model: &Arc<CmfModel>,
128    arch: &ModelArch,
129    prefix: &str,
130    force_f32: bool,
131    ov: &Overlay,
132) -> Result<FfnKind, CmfError> {
133    let prefix = prefix.to_string();
134    let load_dense = |p: &str| -> Result<DenseFfn, CmfError> {
135        let gate_proj = load_matrix(model, &format!("{p}gate_proj.weight"), force_f32, ov)?;
136        let up_proj = load_matrix(model, &format!("{p}up_proj.weight"), force_f32, ov)?;
137        let down_proj = load_matrix(model, &format!("{p}down_proj.weight"), force_f32, ov)?;
138        // FFN triple invariant (holds for dense and each MoE expert;
139        // enforced loudly so a malformed defrag/repack — spec §11 — fails
140        // at load instead of silently mis-computing). inter' is per-layer.
141        let inter = gate_proj.rows();
142        if up_proj.rows() != inter || down_proj.cols() != inter {
143            return Err(CmfError::Parse(format!(
144                "{p}: FFN dims disagree (gate.rows={inter}, up.rows={}, \
145                 down.cols={}); all three must equal inter'",
146                up_proj.rows(),
147                down_proj.cols()
148            )));
149        }
150        if down_proj.rows() != arch.hidden_size {
151            return Err(CmfError::Parse(format!(
152                "{p}: down_proj.rows={} != hidden_size={}",
153                down_proj.rows(),
154                arch.hidden_size
155            )));
156        }
157        // The transposed down, when the file carries one: the per-token
158        // sparse path needs a neuron's down weights contiguous.
159        let dt_name = format!("{p}down_proj.t.weight");
160        let down_t = match model.tensor(&dt_name) {
161            Some(_) => Some(load_matrix(model, &dt_name, force_f32, ov)?),
162            None => None,
163        };
164        if let Some(t) = &down_t
165            && (t.rows() != inter || t.cols() != arch.hidden_size)
166        {
167            return Err(CmfError::Parse(format!(
168                "{p}down_proj.t: [{}, {}] != [{inter}, {}]",
169                t.rows(),
170                t.cols(),
171                arch.hidden_size
172            )));
173        }
174        // Task tubes (`…gate_proj.tube1.weight`, …): the neurons only
175        // some tasks compute, each stored as its own triple so every
176        // kernel runs it unchanged and an inactive tube's bytes are
177        // never touched. Numbering is dense from 1; the first gap ends
178        // the tube list.
179        let mut segs = Vec::new();
180        let mut start = inter;
181        for k in 1.. {
182            let gn = format!("{p}gate_proj.tube{k}.weight");
183            if model.tensor(&gn).is_none() {
184                break;
185            }
186            let gate = load_matrix(model, &gn, force_f32, ov)?;
187            let up = load_matrix(model, &format!("{p}up_proj.tube{k}.weight"), force_f32, ov)?;
188            let down = load_matrix(model, &format!("{p}down_proj.tube{k}.weight"), force_f32, ov)?;
189            let width = gate.rows();
190            if up.rows() != width || down.cols() != width || down.rows() != arch.hidden_size {
191                return Err(CmfError::Parse(format!(
192                    "{p}tube{k}: dims disagree (gate.rows={width}, up.rows={}, \
193                     down=[{}, {}], hidden={})",
194                    up.rows(),
195                    down.rows(),
196                    down.cols(),
197                    arch.hidden_size
198                )));
199            }
200            segs.push(crate::pipeline::FfnSeg {
201                gate,
202                up,
203                down,
204                start,
205                width,
206            });
207            start += width;
208        }
209        Ok(DenseFfn {
210            gate_proj,
211            up_proj,
212            down_proj,
213            act: crate::pipeline::Act::from_arch_full(arch),
214            down_t,
215            segs,
216        })
217    };
218    let router_name = format!("{prefix}mlp.gate.weight");
219    // Cortiq Embryo: resonance-routed experts carry NO gate tensor — the
220    // MoE is keyed on the first expert + the arch flag (a grown genome
221    // appends `mlp.experts.{E}.*` records without rewriting anything).
222    let resonance_moe = model.tensor(&router_name).is_none()
223        && arch.moe.as_ref().is_some_and(|m| m.router_resonance)
224        && model
225            .tensor(&format!("{prefix}mlp.experts.0.gate_proj.weight"))
226            .is_some();
227    if model.tensor(&router_name).is_none() && !resonance_moe {
228        return Ok(FfnKind::Dense(load_dense(&format!("{prefix}mlp."))?));
229    }
230    let cfg = arch.moe.as_ref().ok_or_else(|| {
231        CmfError::Parse(format!(
232            "{router_name} present but header has no arch.moe block"
233        ))
234    })?;
235    // Experts enumerate by TENSOR PRESENCE up to the header count — a
236    // moe-defrag'd specialist keeps a per-layer contiguous prefix of
237    // renumbered experts (fewer than arch.moe.num_experts), with the
238    // router rows sliced to match.
239    let mut experts = Vec::new();
240    for e in 0..cfg.num_experts {
241        if model
242            .tensor(&format!("{prefix}mlp.experts.{e}.gate_proj.weight"))
243            .is_none()
244        {
245            break;
246        }
247        experts.push(load_dense(&format!("{prefix}mlp.experts.{e}."))?);
248    }
249    if experts.is_empty() {
250        return Err(CmfError::Parse(format!(
251            "{prefix}: router present but no expert tensors"
252        )));
253    }
254    let shared = if model
255        .tensor(&format!("{prefix}mlp.shared_expert.gate_proj.weight"))
256        .is_some()
257    {
258        let gate_name = format!("{prefix}mlp.shared_expert_gate.weight");
259        Some((
260            load_dense(&format!("{prefix}mlp.shared_expert."))?,
261            if model.tensor(&gate_name).is_some() {
262                Some(load_matrix(model, &gate_name, force_f32, ov)?)
263            } else {
264                None
265            },
266        ))
267    } else {
268        None
269    };
270    // LFM2-MoE selection bias (`mlp.expert_bias`): present iff the model
271    // routes with a bias; loaded by tensor presence.
272    let bias_name = format!("{prefix}mlp.expert_bias");
273    let expert_bias = if model.tensor(&bias_name).is_some() {
274        Some(load_f32(model, &bias_name, ov).map_err(CmfError::Parse)?)
275    } else {
276        None
277    };
278    // CMF_MOE_TOPK=N (opt-in): route to fewer experts than the header
279    // asks. MoE decode is memory-bound — every selected expert streams
280    // its three matrices per token — so halving k halves that traffic;
281    // the renormalized top-k keeps the mixture a proper average.
282    // Quality is the experiment — measure ppl before trusting.
283    let top_k = std::env::var("CMF_MOE_TOPK")
284        .ok()
285        .and_then(|v| v.parse::<usize>().ok())
286        .filter(|&k| k >= 1 && k <= cfg.top_k)
287        .inspect(|k| tracing::info!("MoE top_k override: {} (header {})", k, cfg.top_k))
288        .unwrap_or(cfg.top_k);
289    // CMF_MOE_TAU=0.x (opt-in): adaptive routing — see MoeFfn::route_tau.
290    let route_tau = std::env::var("CMF_MOE_TAU")
291        .ok()
292        .and_then(|v| v.parse::<f32>().ok())
293        .filter(|&t| t > 0.0 && t < 1.0)
294        .inspect(|t| tracing::info!("MoE adaptive routing: tau {t}"));
295    let mask = moe_task_mask(model, &prefix, experts.len());
296    let router = if resonance_moe {
297        // never read (selection is by descriptors); zero placeholder in RAM
298        QTensor::from_f32(vec![0.0; experts.len() * arch.hidden_size], experts.len(), arch.hidden_size)
299    } else {
300        load_matrix(model, &router_name, force_f32, ov)?
301    };
302    if router.rows() != experts.len() {
303        return Err(CmfError::Parse(format!(
304            "{router_name}: {} rows != {} experts",
305            router.rows(),
306            experts.len()
307        )));
308    }
309    let top_k = top_k.min(experts.len());
310    // Gemma-4: per-expert weight scale after the top-k renorm; its
311    // presence also marks the scale-less-rms router input (the folded
312    // router gain — see the converter).
313    let pes_name = format!("{prefix}mlp.per_expert_scale");
314    let per_expert_scale = if model.tensor(&pes_name).is_some() {
315        Some(load_f32(model, &pes_name, ov).map_err(CmfError::Parse)?)
316    } else {
317        None
318    };
319    let router_input_norm = per_expert_scale.is_some();
320    // Cortiq Embryo resonance descriptors: per-expert records
321    // `mlp.experts.{e}.desc.{mu,u,bias}` (append-only growth) or the legacy
322    // per-layer `mlp.desc.{mu,u,bias}` [E, ...]. Present → the router is a
323    // placeholder and selection is by reconstruction error.
324    let per_expert = model.tensor(&format!("{prefix}mlp.experts.0.desc.mu")).is_some();
325    let resonance = if per_expert {
326        let ne_d = experts.len();
327        let hidden = arch.hidden_size;
328        let mut mu = Vec::with_capacity(ne_d * hidden);
329        let mut u = Vec::new();
330        let mut bias = Vec::with_capacity(ne_d);
331        let mut k = 0usize;
332        for e in 0..ne_d {
333            let m = load_f32(model, &format!("{prefix}mlp.experts.{e}.desc.mu"), ov).map_err(CmfError::Parse)?;
334            if m.len() != hidden {
335                return Err(CmfError::Parse(format!("{prefix}mlp.experts.{e}.desc.mu: {} != {hidden}", m.len())));
336            }
337            mu.extend_from_slice(&m);
338            let un = format!("{prefix}mlp.experts.{e}.desc.u");
339            if model.tensor(&un).is_some() {
340                let ue = load_f32(model, &un, ov).map_err(CmfError::Parse)?;
341                let ke = ue.len() / hidden.max(1);
342                if e == 0 {
343                    k = ke;
344                }
345                if ke != k {
346                    return Err(CmfError::Parse(format!("{un}: rank {ke} != {k}")));
347                }
348                u.extend_from_slice(&ue);
349            }
350            let bn = format!("{prefix}mlp.experts.{e}.desc.bias");
351            bias.push(if model.tensor(&bn).is_some() {
352                load_f32(model, &bn, ov).map_err(CmfError::Parse)?.first().copied().unwrap_or(0.0)
353            } else {
354                0.0
355            });
356        }
357        Some(crate::pipeline::Resonance { mu, u, k, bias })
358    } else if model.tensor(&format!("{prefix}mlp.desc.mu")).is_some() {
359        let mu = load_f32(model, &format!("{prefix}mlp.desc.mu"), ov).map_err(CmfError::Parse)?;
360        let ne_d = experts.len();
361        let hidden = arch.hidden_size;
362        if mu.len() != ne_d * hidden {
363            return Err(CmfError::Parse(format!("{prefix}mlp.desc.mu: {} != {ne_d}×{hidden}", mu.len())));
364        }
365        let u_name = format!("{prefix}mlp.desc.u");
366        let (u, k) = if model.tensor(&u_name).is_some() {
367            let u = load_f32(model, &u_name, ov).map_err(CmfError::Parse)?;
368            let k = u.len() / (ne_d * hidden).max(1);
369            (u, k)
370        } else {
371            (Vec::new(), 0)
372        };
373        let b_name = format!("{prefix}mlp.desc.bias");
374        let bias = if model.tensor(&b_name).is_some() {
375            load_f32(model, &b_name, ov).map_err(CmfError::Parse)?
376        } else {
377            vec![0.0; ne_d]
378        };
379        Some(crate::pipeline::Resonance { mu, u, k, bias })
380    } else {
381        None
382    };
383    let moe = MoeFfn {
384        router,
385        experts,
386        top_k,
387        route_tau,
388        norm_topk_prob: cfg.norm_topk_prob,
389        router_sigmoid: cfg.router_sigmoid,
390        expert_bias,
391        routed_scaling: cfg.routed_scaling_factor.unwrap_or(1.0),
392        shared,
393        stats: std::cell::RefCell::new(Vec::new()),
394        act_sq: std::cell::RefCell::new(Vec::new()),
395        act_rows: std::cell::RefCell::new(Vec::new()),
396        mask,
397        per_expert_scale,
398        router_input_norm,
399        resonance,
400    };
401    // Gemma-4 dual-branch layer: a dense MLP coexists with the routed
402    // experts, each branch inside its own norm sandwich.
403    if model
404        .tensor(&format!("{prefix}mlp.gate_proj.weight"))
405        .is_some()
406    {
407        let norm = |suffix: &str| -> Result<Vec<f32>, CmfError> {
408            load_f32(model, &format!("{prefix}{suffix}.weight"), ov).map_err(CmfError::Parse)
409        };
410        return Ok(FfnKind::DenseMoe(Box::new(crate::pipeline::DenseMoeFfn {
411            dense: load_dense(&format!("{prefix}mlp."))?,
412            moe,
413            post_norm_1: norm("post_feedforward_layernorm_1")?,
414            pre_norm_2: norm("pre_feedforward_layernorm_2")?,
415            post_norm_2: norm("post_feedforward_layernorm_2")?,
416        })));
417    }
418    Ok(FfnKind::Moe(moe))
419}
420
421/// Task mask over routed experts: DTG-MA applied to MoE.
422///
423/// A model may ship a calibrated `<model>.moe-mass.json` sidecar, which is
424/// loaded automatically. `CMF_MOE_MASK=<stats.json>` remains the diagnostic
425/// override and also accepts legacy selection-count files. The weighted
426/// format defaults to 92.5% routing-mass coverage (the measured q4tp A40
427/// balance); legacy count files keep their historical 90% default. In both
428/// cases `CMF_MOE_MASK_COVER` can override the choice for an A/B. Selection
429/// happens over the allowed set only and softmax renormalizes, so every
430/// sidecar must still pass a held-out perplexity gate for its intended task.
431pub(crate) fn moe_task_mask(
432    model: &std::sync::Arc<CmfModel>,
433    prefix: &str,
434    ne: usize,
435) -> Option<Vec<bool>> {
436    use std::sync::OnceLock;
437    static CFG: OnceLock<Option<(std::collections::HashMap<usize, Vec<u64>>, f64)>> =
438        OnceLock::new();
439    let cfg = CFG.get_or_init(|| {
440        let (path, sidecar) = if let Ok(path) = std::env::var("CMF_MOE_MASK") {
441            (std::path::PathBuf::from(path), false)
442        } else {
443            let mut name = model.path.as_os_str().to_os_string();
444            name.push(".moe-mass.json");
445            let path = std::path::PathBuf::from(name);
446            path.is_file().then_some((path, true))?
447        };
448        let shown = path.display();
449        let text = std::fs::read_to_string(&path)
450            .map_err(|e| tracing::warn!("CMF_MOE_MASK: cannot read {shown}: {e}"))
451            .ok()?;
452        let map: std::collections::HashMap<String, Vec<u64>> = serde_json::from_str(&text)
453            .map_err(|e| tracing::warn!("CMF_MOE_MASK: bad JSON in {shown}: {e}"))
454            .ok()?;
455        // Weighted dumps store normalized route mass in fixed-point units
456        // and therefore have per-layer totals many orders above the old
457        // top-k vote counts. The filename is an explicit stronger signal;
458        // the magnitude check keeps manually named weighted dumps automatic.
459        let weighted = sidecar
460            || map
461                .values()
462                .any(|row| row.iter().copied().sum::<u64>() >= 1_000_000);
463        let cover = std::env::var("CMF_MOE_MASK_COVER")
464            .ok()
465            .and_then(|v| v.parse::<f64>().ok())
466            .filter(|&c| c > 0.0 && c <= 1.0)
467            .unwrap_or(if weighted { 0.925 } else { 0.9 });
468        tracing::info!("MoE task mask: {shown}, cover {cover}");
469        Some((
470            map.into_iter()
471                .filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
472                .collect(),
473            cover,
474        ))
475    });
476    let (stats, cover) = cfg.as_ref()?;
477    // The layer index rides in the tensor prefix ("model.layers.N.").
478    let li: usize = prefix
479        .split("layers.")
480        .nth(1)?
481        .split('.')
482        .next()?
483        .parse()
484        .ok()?;
485    let counts = stats.get(&li)?;
486    if counts.len() != ne {
487        tracing::warn!(
488            "CMF_MOE_MASK: layer {li} has {} counts, model has {ne} experts — skipped",
489            counts.len()
490        );
491        return None;
492    }
493    let total: u64 = counts.iter().sum();
494    if total == 0 {
495        return None;
496    }
497    let mut order: Vec<usize> = (0..ne).collect();
498    order.sort_unstable_by_key(|&e| std::cmp::Reverse(counts[e]));
499    let mut mask = vec![false; ne];
500    let mut acc = 0u64;
501    let mut kept = 0usize;
502    for &e in &order {
503        mask[e] = true;
504        acc += counts[e];
505        kept += 1;
506        if (acc as f64) >= cover * (total as f64) {
507            break;
508        }
509    }
510    tracing::info!(
511        "MoE task mask L{li}: {kept}/{ne} experts for {:.0}% mass",
512        cover * 100.0
513    );
514    Some(mask)
515}
516
517fn load_matrix(
518    model: &Arc<CmfModel>,
519    name: &str,
520    force_f32: bool,
521    ov: &Overlay,
522) -> Result<QTensor, CmfError> {
523    // Claim 14: a blended working tensor is materialized in f32 and
524    // held resident (the overlay-cache slot); single skills stay
525    // zero-copy pointers into the mmap.
526    if ov.blend_touches(model, name) {
527        if let Overlay::Blend(list) = ov {
528            let entry = model
529                .tensor(name)
530                .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
531            let data =
532                blend_f32(model, name, list).map_err(|e| CmfError::Parse(format!("blend: {e}")))?;
533            return Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]));
534        }
535    }
536    let skill = match ov {
537        Overlay::One(s) => Some(*s),
538        _ => None,
539    };
540    // Tensor-source indirection (spec §9): the skill's replacement is
541    // read in place of the backbone tensor — either/or, never a sum.
542    let name: &str = &match skill {
543        Some(sid) if model.tensor(&format!("skill.{sid}.{name}")).is_some() => {
544            format!("skill.{sid}.{name}")
545        }
546        _ => name.to_string(),
547    };
548    let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
549    if force_f32 {
550        let entry = model
551            .tensor(name)
552            .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
553        if entry.shape.len() != 2 {
554            return Err(err(format!("'{name}' is not 2-D")));
555        }
556        let data = load_f32(model, name, &Overlay::None).map_err(err)?;
557        Ok(QTensor::from_f32(data, entry.shape[0], entry.shape[1]))
558    } else {
559        QTensor::from_model(model, name).map_err(err)
560    }
561}
562
563impl Pipeline {
564    /// Build a runnable pipeline from an opened CMF model.
565    pub fn from_model(
566        model: &Arc<CmfModel>,
567        sampler_config: SamplerConfig,
568    ) -> Result<Self, CmfError> {
569        Self::from_model_with_skill(model, sampler_config, None)
570    }
571
572    /// Same, with a skill overlaid (spec §9): every layer tensor is
573    /// resolved through tensor-source indirection — the skill's
574    /// full-shape replacement is read in place of the backbone tensor.
575    /// No per-skill model is ever assembled: Mapped tensors are
576    /// pointers into the one shared mmap.
577    pub fn from_model_with_skill(
578        model: &Arc<CmfModel>,
579        sampler_config: SamplerConfig,
580        skill: Option<&str>,
581    ) -> Result<Self, CmfError> {
582        match skill {
583            Some(s) => Self::from_model_with_overlay(model, sampler_config, &Overlay::One(s)),
584            None => Self::from_model_with_overlay(model, sampler_config, &Overlay::None),
585        }
586    }
587
588    /// Soft superposition (claim 14): working tensors accumulated from
589    /// the given (skill, weight) list — softmax(−E/T) upstream.
590    pub fn from_model_with_blend(
591        model: &Arc<CmfModel>,
592        sampler_config: SamplerConfig,
593        blend: &[(String, f32)],
594    ) -> Result<Self, CmfError> {
595        Self::from_model_with_overlay(model, sampler_config, &Overlay::Blend(blend))
596    }
597
598    fn skill_file_guard(model: &CmfModel) -> Result<(), CmfError> {
599        // A standalone skill file carries a PARTIAL tensor set cut against
600        // a base; running it would be half a network answering questions.
601        if model.required_features & cortiq_core::format::features::SKILL_FILE != 0 {
602            return Err(CmfError::Parse(
603                "this file is a standalone SKILL, not a runnable model — attach it: \
604                 cortiq skill apply <base.cmf> <this file> -o specialist.cmf"
605                    .into(),
606            ));
607        }
608        Ok(())
609    }
610
611    fn from_model_with_overlay(
612        model: &Arc<CmfModel>,
613        sampler_config: SamplerConfig,
614        ov: &Overlay,
615    ) -> Result<Self, CmfError> {
616        // Small device caches (probe verdicts, compiled pipelines) go
617        // beside the model: it is a directory the caller demonstrably
618        // writes to, which `std::env::temp_dir()` is not inside an
619        // Android app sandbox.
620        if let Some(dir) = model.path.parent() {
621            crate::gpu::set_cache_dir(dir.to_path_buf());
622        }
623        // A new model gets a fresh verdict on whether the token graph can
624        // be built: the refusal is remembered per model, not per process.
625        crate::gpu::graph_unsupported_reset();
626        Self::skill_file_guard(model)?;
627        let skill = match ov {
628            Overlay::One(s) => Some(*s),
629            _ => None,
630        };
631        if let Some(sid) = skill {
632            let known = model.header.skills.iter().any(|s| s.id == sid)
633                || model.skill_tensors(sid).next().is_some();
634            if !known {
635                return Err(CmfError::Parse(format!(
636                    "skill '{sid}' not in this container (header.skills: {:?})",
637                    model
638                        .header
639                        .skills
640                        .iter()
641                        .map(|s| &s.id)
642                        .collect::<Vec<_>>()
643                )));
644            }
645            tracing::info!(
646                "skill '{sid}': {} replacement tensors overlaid",
647                model.skill_tensors(sid).count()
648            );
649        }
650        let arch = model.arch().clone();
651        let err = |e: String| CmfError::Parse(format!("weight loading: {e}"));
652        if let Some(heads) = &arch.attention_heads_per_layer {
653            if heads.len() != arch.num_layers {
654                return Err(CmfError::Parse(format!(
655                    "arch.attention_heads_per_layer has {} entries, expected {}",
656                    heads.len(),
657                    arch.num_layers
658                )));
659            }
660            if let Some((li, &nh)) = heads
661                .iter()
662                .enumerate()
663                .find(|(_, nh)| **nh == 0 || **nh % arch.num_kv_heads != 0)
664            {
665                return Err(CmfError::Parse(format!(
666                    "layer {li} has {nh} Q heads, which must be nonzero and divisible by {} KV heads",
667                    arch.num_kv_heads
668                )));
669            }
670        }
671        if arch
672            .layer_types
673            .iter()
674            .any(|t| matches!(t, LayerType::SlidingAttention))
675            && arch.sliding_window.is_none()
676        {
677            return Err(CmfError::Parse(
678                "model has SlidingAttention layers but no arch.sliding_window".into(),
679            ));
680        }
681
682        // Masks × quantized mmap: only the HEAD-mask path needs f32
683        // slices, so f32 is forced only when some mask actually restricts
684        // attention heads. FFN masks run sparse directly on the quant
685        // bytes (sparse_ffn_quant), and embed/lm_head are never masked.
686        // The old condition forced f32 for ANY mask: a 4.17 B file whose
687        // mask touched only the FFN dequantized to 16.7 GB at load and
688        // ran the bare-f32 GEMMs — 0.0 tok/s on a laptop that swapped,
689        // and a 30× crawl on a 48-core server. A mask with no head rows
690        // costs nothing now.
691        let heads_masked = model.masks.masks.iter().any(|m| {
692            m.head_masks.iter().any(|row| {
693                let mut bits = 0usize;
694                for &b in row.iter() {
695                    bits += b.count_ones() as usize;
696                }
697                !row.is_empty() && bits < arch.num_attention_heads
698            })
699        });
700        let force_f32 = heads_masked; // attention only (head masks)
701
702        // ── Tokenizer: embedded → sidecar → byte-level fallback ──
703        let mut tokenizer = if let Some(vocab_bytes) = &model.vocab {
704            Tokenizer::from_bytes(vocab_bytes)
705                .map_err(|e| CmfError::Parse(format!("embedded tokenizer: {e}")))?
706        } else {
707            let sidecar = model.path.with_file_name("tokenizer.json");
708            if sidecar.exists() {
709                Tokenizer::from_file(&sidecar)
710                    .map_err(|e| CmfError::Parse(format!("sidecar tokenizer: {e}")))?
711            } else {
712                tracing::warn!("no tokenizer in file or sidecar — using byte-level fallback");
713                Tokenizer::byte_level()
714            }
715        };
716        // Chat/eos bundle (spec §6.1): the FILE defines chat behavior.
717        if let Some(tc) = &model.header.tokenizer_config {
718            tokenizer.chat_template = tc.chat_template.clone();
719            tokenizer.extra_eos.extend(tc.eos_token_ids.iter().copied());
720            if tokenizer.bos_token_id.is_none() {
721                tokenizer.bos_token_id = tc.bos_token_id;
722            }
723            tracing::info!(
724                "chat bundle: template {} chars, {} stop ids",
725                tc.chat_template.as_deref().map(str::len).unwrap_or(0),
726                tc.eos_token_ids.len()
727            );
728        }
729        // Gemma's contract requires <bos> at sequence start, but its
730        // tokenizer.json post-processor does not add it (the chat
731        // template does). Raw prompts need it too — word salad without.
732        if arch.arch_name.to_lowercase().contains("gemma") && tokenizer.bos_token_id.is_some() {
733            tokenizer.add_bos = true;
734        }
735
736        // ── Top-level weights (never masked → always quantized) ──
737        let embed_tokens = load_matrix(model, "model.embed_tokens.weight", false, ov)?;
738        let final_norm = load_f32(model, "model.norm.weight", ov).map_err(err)?;
739        let lm_head = if model.tensor("lm_head.weight").is_some() {
740            load_matrix(model, "lm_head.weight", false, ov)?
741        } else if arch.tie_word_embeddings {
742            // Tied: reuse the embedding matrix (re-open, cheap for Mapped).
743            load_matrix(model, "model.embed_tokens.weight", false, ov)?
744        } else {
745            return Err(CmfError::MissingTensor(
746                "lm_head.weight (and tie_word_embeddings is false)".into(),
747            ));
748        };
749
750        // ── Linear-core geometry (required if any linear layer exists) ──
751        let has_linear = arch
752            .layer_types
753            .iter()
754            .any(|t| matches!(t, LayerType::LinearAttention));
755        let mut vmf_cfg = None;
756        let mut gdn_cfg = None;
757        if has_linear {
758            let lc = arch.linear_core.as_ref().ok_or_else(|| {
759                CmfError::Parse(
760                    "model has LinearAttention layers but no arch.linear_core — \
761                     reconvert with the current converter"
762                        .into(),
763                )
764            })?;
765            let need = |v: Option<usize>, name: &str| {
766                v.ok_or_else(|| CmfError::Parse(format!("linear core needs arch.{name}")))
767            };
768            match lc.kind.as_str() {
769                "vmf_phase" => {
770                    vmf_cfg = Some(VmfPhaseCfg {
771                        num_heads: lc.num_heads,
772                        nphase: need(lc.nphase, "linear_core.nphase")?,
773                        value_head_dim: lc.value_head_dim,
774                        hidden_size: arch.hidden_size,
775                        // θ-mass (η′): default 0 (massless); CMF_PHASE_MASS
776                        // widens the phase kernel for folded-unhealed models.
777                        phase_mass: std::env::var("CMF_PHASE_MASS")
778                            .ok()
779                            .and_then(|v| v.parse().ok())
780                            .unwrap_or(0.0),
781                    });
782                }
783                "gated_delta_net" => {
784                    gdn_cfg = Some(GdnCfg {
785                        num_v_heads: lc.num_heads,
786                        num_k_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
787                        key_head_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
788                        value_head_dim: lc.value_head_dim,
789                        conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
790                        hidden_size: arch.hidden_size,
791                        rms_eps: arch.rms_norm_eps,
792                    });
793                }
794                other => {
795                    return Err(CmfError::Parse(format!(
796                        "unknown linear core '{other}' (this runtime executes: \
797                         gated_delta_net, vmf_phase)"
798                    )));
799                }
800            }
801        }
802
803        // ── KDA geometry (Kimi Linear / Kimi-K3 delta-attention layers) ──
804        let has_kda = arch.layer_types.iter().any(|t| matches!(t, LayerType::Kda));
805        let kda_cfg = if has_kda {
806            let need = |v: Option<usize>, name: &str| {
807                v.ok_or_else(|| CmfError::Parse(format!("KDA core needs arch.{name}")))
808            };
809            Some(crate::linear_core::KdaCfg {
810                num_heads: need(arch.linear_num_key_heads, "linear_num_key_heads")?,
811                head_k_dim: need(arch.linear_key_head_dim, "linear_key_head_dim")?,
812                head_v_dim: need(arch.linear_value_head_dim, "linear_value_head_dim")?,
813                conv_kernel: need(arch.linear_conv_kernel_dim, "linear_conv_kernel_dim")?,
814                hidden_size: arch.hidden_size,
815                rms_eps: arch.rms_norm_eps,
816            })
817        } else {
818            None
819        };
820
821        // ── Short-convolution geometry (LFM2 conv mixer layers) ──
822        let has_short_conv = arch
823            .layer_types
824            .iter()
825            .any(|t| matches!(t, LayerType::ShortConv));
826        let short_conv_cfg = if has_short_conv {
827            Some(ShortConvCfg {
828                hidden_size: arch.hidden_size,
829                kernel: arch.linear_conv_kernel_dim.ok_or_else(|| {
830                    CmfError::Parse(
831                        "model has ShortConv layers but no arch.linear_conv_kernel_dim — \
832                         reconvert with the current converter"
833                            .into(),
834                    )
835                })?,
836            })
837        } else {
838            None
839        };
840
841        // ── Layers ──
842        let load_full_attn = |prefix: &str, layer: Option<usize>| -> Result<AttnKind, CmfError> {
843            let t = |suffix: &str| load_matrix(model, &format!("{prefix}{suffix}"), force_f32, ov);
844            let n = |suffix: &str| -> Option<Vec<f32>> {
845                model
846                    .tensor(&format!("{prefix}{suffix}"))
847                    .and_then(|_| load_f32(model, &format!("{prefix}{suffix}"), ov).ok())
848            };
849            // DeepSeek-V2 MLA: the latent projections replace the k/v pair.
850            if let Some(mla) = arch.mla.as_ref() {
851                // Compressed q (K3/V3): q_a → rms → q_b; direct otherwise.
852                let (q_proj, q_a, q_a_norm) = if mla.q_lora_rank.is_some() {
853                    (
854                        t("self_attn.q_b_proj.weight")?,
855                        Some(t("self_attn.q_a_proj.weight")?),
856                        Some(n("self_attn.q_a_layernorm.weight").ok_or_else(|| {
857                            CmfError::Parse(format!("{prefix}: MLA needs q_a_layernorm"))
858                        })?),
859                    )
860                } else {
861                    (t("self_attn.q_proj.weight")?, None, None)
862                };
863                let hd = mla.qk_rope_head_dim + mla.qk_nope_head_dim;
864                let nh = q_proj.rows() / hd;
865                // YaRN mscale²: DeepSeek corrects the softmax scale by
866                // (0.1·mscale_all_dim·ln(factor)+1)².
867                let mut scale = 1.0 / (hd as f32).sqrt();
868                if let Some(y) = arch.yarn.as_ref() {
869                    if let Some(m) = y.mscale_all_dim.filter(|&m| m > 0.0) {
870                        let ms = 0.1 * m * y.factor.ln() + 1.0;
871                        scale *= ms * ms;
872                    }
873                }
874                return Ok(AttnKind::Mla(Box::new(crate::pipeline::MlaWeights {
875                    q_proj,
876                    q_a,
877                    q_a_norm,
878                    kv_a: t("self_attn.kv_a_proj_with_mqa.weight")?,
879                    kv_a_norm: n("self_attn.kv_a_layernorm.weight").ok_or_else(|| {
880                        CmfError::Parse(format!("{prefix}: MLA needs kv_a_layernorm"))
881                    })?,
882                    kv_b: t("self_attn.kv_b_proj.weight")?,
883                    o_proj: t("self_attn.o_proj.weight")?,
884                    nh,
885                    qk_rope: mla.qk_rope_head_dim,
886                    qk_nope: mla.qk_nope_head_dim,
887                    v_dim: mla.v_head_dim,
888                    lora: mla.kv_lora_rank,
889                    scale,
890                    nope: mla.nope,
891                })));
892            }
893            let wq = t("self_attn.q_proj.weight")?;
894            let nh = layer
895                .and_then(|li| {
896                    arch.attention_heads_per_layer
897                        .as_ref()
898                        .and_then(|v| v.get(li).copied())
899                })
900                .unwrap_or(arch.num_attention_heads);
901            // Qwen3.5 output gate: q_proj rows = 2·nh·hd (per-head [q; gate]).
902            // Gemma-4 global layers legitimately have nh·global_head_dim
903            // rows (which can equal 2·nh·hd) — never gated.
904            let output_gate = arch.global_head_dim.is_none() && wq.rows() == 2 * nh * arch.head_dim;
905            // Gemma-4 global layers run MQA at global_head_dim — their
906            // q_proj legitimately carries nh·ghd rows.
907            let is_global_layer = arch.global_head_dim.is_some()
908                && layer.is_some_and(|li| {
909                    arch.sliding_window_pattern
910                        .is_some_and(|p| p > 0 && (li + 1) % p == 0)
911                });
912            let expect = if is_global_layer {
913                nh * arch.global_head_dim.unwrap_or(arch.head_dim)
914            } else {
915                nh * arch.head_dim
916            };
917            if !output_gate && wq.rows() != expect {
918                return Err(CmfError::Parse(format!(
919                    "{prefix}self_attn.q_proj.weight rows={} != heads({nh}) * head_dim({})",
920                    wq.rows(),
921                    expect / nh.max(1)
922                )));
923            }
924            let gate_name = format!("{prefix}self_attn.g_proj.weight");
925            let softplus_gate = if model.tensor(&gate_name).is_some() {
926                let gate = load_matrix(model, &gate_name, force_f32, ov)?;
927                if gate.cols() != arch.hidden_size {
928                    return Err(CmfError::Parse(format!(
929                        "{gate_name} cols={} != hidden_size ({})",
930                        gate.cols(),
931                        arch.hidden_size
932                    )));
933                }
934                let per_head = if gate.rows() == nh {
935                    true
936                } else if gate.rows() == nh * arch.head_dim {
937                    false
938                } else {
939                    return Err(CmfError::Parse(format!(
940                        "{gate_name} rows={} must equal heads ({nh}) or heads*head_dim ({})",
941                        gate.rows(),
942                        nh * arch.head_dim
943                    )));
944                };
945                Some((gate, per_head))
946            } else {
947                None
948            };
949            // Qwen2-family projection biases (by tensor presence).
950            let bias = match (
951                n("self_attn.q_proj.bias"),
952                n("self_attn.k_proj.bias"),
953                n("self_attn.v_proj.bias"),
954            ) {
955                (Some(a), Some(b), Some(c)) => Some((a, b, c)),
956                _ => None,
957            };
958            Ok(AttnKind::Full {
959                wq,
960                wk: t("self_attn.k_proj.weight")?,
961                wv: t("self_attn.v_proj.weight")?,
962                wo: t("self_attn.o_proj.weight")?,
963                q_norm: n("self_attn.q_norm.weight"),
964                k_norm: n("self_attn.k_norm.weight"),
965                output_gate,
966                softplus_gate,
967                bias,
968            })
969        };
970
971        let load_linear_attn = |prefix: &str| -> Result<AttnKind, CmfError> {
972            if gdn_cfg.is_some() {
973                // Faithful vendor operator: tensor names 1:1 with the source.
974                let t = |suffix: &str| {
975                    load_matrix(
976                        model,
977                        &format!("{prefix}linear_attn.{suffix}"),
978                        force_f32,
979                        ov,
980                    )
981                };
982                let f = |suffix: &str| {
983                    load_f32(model, &format!("{prefix}linear_attn.{suffix}"), ov).map_err(err)
984                };
985                return Ok(AttnKind::LinearGdn(GdnWeights {
986                    in_proj_qkv: t("in_proj_qkv.weight")?,
987                    in_proj_z: t("in_proj_z.weight")?,
988                    in_proj_a: t("in_proj_a.weight")?,
989                    in_proj_b: t("in_proj_b.weight")?,
990                    conv1d: f("conv1d.weight")?,
991                    a_log: f("A_log")?,
992                    dt_bias: f("dt_bias")?,
993                    norm: f("norm.weight")?,
994                    out_proj: t("out_proj.weight")?,
995                }));
996            }
997            let t = |suffix: &str| {
998                load_matrix(model, &format!("{prefix}vmf_attn.{suffix}"), force_f32, ov)
999            };
1000            let a_log = load_f32(model, &format!("{prefix}vmf_attn.A_log"), ov).map_err(err)?;
1001            // Selective-write gate κ (hybrid_k core): optional by tensor
1002            // presence — files without it run the classic phase kernel
1003            // bit-identically.
1004            let k_gate = if model
1005                .tensor(&format!("{prefix}vmf_attn.k_gate.weight"))
1006                .is_some()
1007            {
1008                Some((
1009                    t("k_gate.weight")?,
1010                    load_f32(model, &format!("{prefix}vmf_attn.k_gate.bias"), ov).map_err(err)?,
1011                ))
1012            } else {
1013                None
1014            };
1015            Ok(AttnKind::Linear(VmfPhaseWeights {
1016                thq: t("thq.weight")?,
1017                thk: t("thk.weight")?,
1018                v_proj: t("v_proj.weight")?,
1019                out_proj: t("out_proj.weight")?,
1020                decay: a_log.iter().map(|&a| (-(a as f64).exp()).exp()).collect(),
1021                k_gate,
1022            }))
1023        };
1024
1025        // LFM2 short-conv mixer: in_proj [3·hidden, hidden], a depthwise
1026        // conv (stored f16 as `[hidden, 1, kernel]` → flattened taps), and
1027        // out_proj [hidden, hidden]. Names canonicalized at convert time.
1028        let load_short_conv = |prefix: &str| -> Result<AttnKind, CmfError> {
1029            let t = |suffix: &str| {
1030                load_matrix(
1031                    model,
1032                    &format!("{prefix}short_conv.{suffix}"),
1033                    force_f32,
1034                    ov,
1035                )
1036            };
1037            Ok(AttnKind::ShortConv(ShortConvWeights {
1038                in_proj: t("in_proj.weight")?,
1039                conv: load_f32(model, &format!("{prefix}short_conv.conv.weight"), ov)
1040                    .map_err(err)?,
1041                out_proj: t("out_proj.weight")?,
1042            }))
1043        };
1044
1045        // KDA layer (Kimi Linear / Kimi-K3): faithful vendor tensors under
1046        // the `kda_attn.` canonical prefix. The output gate is full-rank
1047        // (g_proj, K3) or low-rank (g_a/g_b, Kimi-Linear-48B) by presence.
1048        let load_kda = |prefix: &str| -> Result<AttnKind, CmfError> {
1049            let t = |suffix: &str| {
1050                load_matrix(model, &format!("{prefix}kda_attn.{suffix}"), force_f32, ov)
1051            };
1052            let f = |suffix: &str| {
1053                load_f32(model, &format!("{prefix}kda_attn.{suffix}"), ov).map_err(err)
1054            };
1055            let gate = if model
1056                .tensor(&format!("{prefix}kda_attn.g_proj.weight"))
1057                .is_some()
1058            {
1059                crate::linear_core::KdaOutGate::Full(t("g_proj.weight")?)
1060            } else {
1061                crate::linear_core::KdaOutGate::LowRank(
1062                    t("g_a_proj.weight")?,
1063                    t("g_b_proj.weight")?,
1064                )
1065            };
1066            Ok(AttnKind::Kda(Box::new(crate::linear_core::KdaWeights {
1067                q_proj: t("q_proj.weight")?,
1068                k_proj: t("k_proj.weight")?,
1069                v_proj: t("v_proj.weight")?,
1070                conv_q: f("q_conv1d.weight")?,
1071                conv_k: f("k_conv1d.weight")?,
1072                conv_v: f("v_conv1d.weight")?,
1073                f_a: t("f_a_proj.weight")?,
1074                f_b: t("f_b_proj.weight")?,
1075                dt_bias: f("dt_bias")?,
1076                a_log: f("A_log")?,
1077                b_proj: t("b_proj.weight")?,
1078                gate,
1079                o_norm: f("o_norm.weight")?,
1080                o_proj: t("o_proj.weight")?,
1081                gate_lower_bound: arch.kda_gate_lower_bound.map(|v| v as f32),
1082            })))
1083        };
1084
1085        fn anyhow_like(ok: bool) -> Result<(), ()> {
1086            if ok { Ok(()) } else { Err(()) }
1087        }
1088        let mut layers = Vec::with_capacity(arch.num_layers);
1089        let is_g3n = arch.g3n.is_some();
1090        // Architectures that load their own layer stack below. DeepSeek-V4
1091        // has none of the canonical projections — no q/k/v/o_proj, no
1092        // per-layer gate_proj — so the generic loop would demand
1093        // `self_attn.q_proj.weight` and fail before its own loader ever ran.
1094        let owns_its_layers = is_g3n || arch.arch_name == "deepseek_v4";
1095        for li in 0..(if owns_its_layers { 0 } else { arch.num_layers }) {
1096            let prefix = format!("model.layers.{li}.");
1097            let attn = match arch.layer_types.get(li) {
1098                Some(LayerType::LinearAttention) => load_linear_attn(&prefix)?,
1099                Some(LayerType::Kda) => load_kda(&prefix)?,
1100                Some(LayerType::ShortConv) => load_short_conv(&prefix)?,
1101                _ => load_full_attn(&prefix, Some(li))?,
1102            };
1103            // Gemma-2/3 sandwich: `pre_feedforward_layernorm` present →
1104            // it is the pre-FFN norm, and post_attention/post_feedforward
1105            // норms apply to the branch OUTPUTS before their residuals.
1106            let pre_ffn = format!("{prefix}pre_feedforward_layernorm.weight");
1107            let sandwich = model.tensor(&pre_ffn).is_some();
1108            layers.push(LayerWeights {
1109                input_norm: load_f32(model, &format!("{prefix}input_layernorm.weight"), ov)
1110                    .map_err(err)?,
1111                post_norm: if sandwich {
1112                    load_f32(model, &pre_ffn, ov).map_err(err)?
1113                } else {
1114                    load_f32(
1115                        model,
1116                        &format!("{prefix}post_attention_layernorm.weight"),
1117                        ov,
1118                    )
1119                    .map_err(err)?
1120                },
1121                attn_out_norm: if sandwich {
1122                    Some(
1123                        load_f32(
1124                            model,
1125                            &format!("{prefix}post_attention_layernorm.weight"),
1126                            ov,
1127                        )
1128                        .map_err(err)?,
1129                    )
1130                } else {
1131                    None
1132                },
1133                ffn_out_norm: if sandwich {
1134                    Some(
1135                        load_f32(
1136                            model,
1137                            &format!("{prefix}post_feedforward_layernorm.weight"),
1138                            ov,
1139                        )
1140                        .map_err(err)?,
1141                    )
1142                } else {
1143                    None
1144                },
1145                // Gemma-4: learned scalar multiplying the layer output.
1146                layer_scale: model
1147                    .tensor(&format!("{prefix}layer_scalar"))
1148                    .and_then(|_| {
1149                        load_f32(model, &format!("{prefix}layer_scalar"), ov)
1150                            .ok()
1151                            .and_then(|v| v.first().copied())
1152                    }),
1153                // FFN always quantized — masks run sparse on quant bytes.
1154                ffn: build_layer_ffn(model, &arch, li, false, ov)?,
1155                attn,
1156            });
1157        }
1158
1159        // ── MTP head (optional, spec §2.1) ──
1160        //
1161        // The header declaring an MTP head is not the same as the file
1162        // carrying one. DeepSeek-V4's config announces a next-token predictor
1163        // whose weights the converter does not map (they are spelled `mtp.N.*`
1164        // and have none of the canonical projections), so demanding
1165        // `model.mtp.layers.0.self_attn.q_proj.weight` failed a model that is
1166        // otherwise complete. Presence in the directory decides.
1167        let mtp_present = model
1168            .tensor("model.mtp.layers.0.self_attn.q_proj.weight")
1169            .is_some()
1170            || model.tensor("model.mtp.eh_proj.weight").is_some();
1171        // DeepSeek-V4 writes its own stack under `model.mtp.N.*` — three full
1172        // layers, not a V3-style single block — so it cannot go through the
1173        // path below and is loaded by the dsv4 arm instead. Saying the file
1174        // "carries none" was a false negative worth six gigabytes.
1175        let dsv4_mtp = model.tensor("model.mtp.0.main_proj.weight").is_some();
1176        if arch.mtp.is_some() && !mtp_present && !dsv4_mtp {
1177            tracing::info!(
1178                "header declares an MTP head but the file carries none — \
1179                 loading without it"
1180            );
1181        }
1182        let mtp = if let Some(cfg) = arch.mtp.as_ref().filter(|_| mtp_present) {
1183            if cfg.num_layers != 1 {
1184                return Err(CmfError::Parse(format!(
1185                    "MTP with {} blocks not supported yet (only 1)",
1186                    cfg.num_layers
1187                )));
1188            }
1189            let p = "model.mtp.";
1190            let attn = load_full_attn("model.mtp.layers.0.", None)?;
1191            Some(MtpModule {
1192                enorm: load_f32(model, &format!("{p}enorm.weight"), ov).map_err(err)?,
1193                hnorm: load_f32(model, &format!("{p}hnorm.weight"), ov).map_err(err)?,
1194                eh_proj: load_matrix(model, &format!("{p}eh_proj.weight"), false, ov)?,
1195                layer: LayerWeights {
1196                    attn_out_norm: None,
1197                    ffn_out_norm: None,
1198                    layer_scale: None,
1199                    input_norm: load_f32(model, &format!("{p}layers.0.input_layernorm.weight"), ov)
1200                        .map_err(err)?,
1201                    post_norm: load_f32(
1202                        model,
1203                        &format!("{p}layers.0.post_attention_layernorm.weight"),
1204                        ov,
1205                    )
1206                    .map_err(err)?,
1207                    // Whatever the block actually carries: DeepSeek's MTP
1208                    // layer is dense, Qwen3.6's is a full MoE (router + 256
1209                    // experts + shared). Same builder as a backbone layer.
1210                    ffn: build_ffn_at(model, &arch, &format!("{p}layers.0."), false, ov)?,
1211                    attn,
1212                },
1213                final_norm: load_f32(model, &format!("{p}norm.weight"), ov).map_err(err)?,
1214                kv: LayerKvCache::new(arch.num_kv_heads, arch.head_dim),
1215            })
1216        } else {
1217            None
1218        };
1219
1220        tracing::info!(
1221            "Pipeline loaded: {} | {}L ({} linear) | {:.2}B params | storage: {} | MTP: {}",
1222            arch.arch_name,
1223            arch.num_layers,
1224            arch.layer_types
1225                .iter()
1226                .filter(|t| matches!(t, LayerType::LinearAttention))
1227                .count(),
1228            model.total_param_count() as f64 / 1e9,
1229            if force_f32 {
1230                "f32 (masked)"
1231            } else {
1232                "quantized mmap"
1233            },
1234            if mtp.is_some() { "yes" } else { "no" }
1235        );
1236
1237        // KV window: the descriptor's max, capped for dev-box safety;
1238        // CMF_MAX_SEQ overrides the cap (long-context runs).
1239        // 8192 was the silent quality cliff of the Qwen3.8 bring-up: at
1240        // the cap the wgpu token graph declines, the host evicts half the
1241        // KV, and a GDN hybrid's recurrent state goes stale — the model
1242        // stays fluent and loses its mind (Django internals, a Turkish
1243        // essay, an em-dash loop; one failure, three costumes). 32768
1244        // covers every long-form run we actually ship while keeping the
1245        // graph's device KV mirror affordable beside the weights;
1246        // CMF_MAX_SEQ still overrides in either direction.
1247        let cap = std::env::var("CMF_MAX_SEQ")
1248            .ok()
1249            .and_then(|v| v.parse::<usize>().ok())
1250            .unwrap_or(32_768);
1251        let max_seq_len = arch.max_position_embeddings.min(cap);
1252
1253        // Looped Transformer: total virtual layers = physical × num_loops.
1254        let total_layers = arch.num_layers * arch.num_loops;
1255
1256        let mut pipeline = Pipeline::new(
1257            tokenizer,
1258            PipelineWeights {
1259                embed_tokens,
1260                layers,
1261                lm_head,
1262                final_norm,
1263            },
1264            arch.hidden_size,
1265            arch.intermediate_size,
1266            arch.num_attention_heads,
1267            arch.num_kv_heads,
1268            arch.head_dim,
1269            total_layers,
1270            arch.num_layers, // physical layers in weights
1271            arch.loop_final_norm,
1272            arch.vocab_size,
1273            arch.rms_norm_eps,
1274            arch.rope_theta as f32,
1275            arch.norm_style,
1276            max_seq_len,
1277            sampler_config,
1278        );
1279        let rotary = ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2);
1280        pipeline.set_rotary(rotary, arch.rope_theta as f32);
1281        pipeline.attention_heads_per_layer = arch.attention_heads_per_layer.clone();
1282        if let Some(yarn) = &arch.yarn {
1283            pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
1284                rotary,
1285                arch.rope_theta as f32,
1286                yarn.factor,
1287                yarn.original_max_position_embeddings,
1288                yarn.beta_fast,
1289                yarn.beta_slow,
1290            ));
1291            pipeline.rope_scale = yarn.attention_factor;
1292        }
1293        // Gemma-family extras: embedding scale, attention-scale
1294        // override, and (Gemma-3) sliding-window layers with their own
1295        // local RoPE base.
1296        pipeline.embed_multiplier = arch.embed_multiplier;
1297        pipeline.logit_multiplier = arch.logit_multiplier;
1298        if let Some(qpas) = arch.query_pre_attn_scalar {
1299            pipeline.attn_scale = 1.0 / (qpas as f32).sqrt();
1300        }
1301        if let (Some(w), Some(p)) = (arch.sliding_window, arch.sliding_window_pattern) {
1302            pipeline.swa = Some((w, p));
1303            if let Some(base) = arch.rope_local_base_freq {
1304                pipeline.inv_freq_local = Some(std::sync::Arc::new(
1305                    crate::attention::rope_inv_freq(rotary, base as f32),
1306                ));
1307            }
1308        }
1309        let explicit_sliding: Vec<bool> = arch
1310            .layer_types
1311            .iter()
1312            .map(|t| matches!(t, cortiq_core::LayerType::SlidingAttention))
1313            .collect();
1314        if explicit_sliding.iter().any(|&v| v) {
1315            pipeline.sliding_layers = Some(explicit_sliding);
1316            if let Some(w) = arch.sliding_window {
1317                pipeline.swa = Some((w, usize::MAX));
1318            }
1319            let local_rotary = ((arch.head_dim as f32
1320                * arch
1321                    .local_partial_rotary_factor
1322                    .unwrap_or(arch.partial_rotary_factor))
1323                as usize)
1324                .max(2);
1325            pipeline.rotary_dim_local = Some(local_rotary);
1326            if let Some(base) = arch.rope_local_base_freq {
1327                pipeline.inv_freq_local = Some(std::sync::Arc::new(
1328                    crate::attention::rope_inv_freq(local_rotary, base as f32),
1329                ));
1330            }
1331        }
1332        // Gemma-4: global layers run their own geometry (MQA at
1333        // global_head_dim) with a proportional RoPE — the first
1334        // factor·head_dim dims rotate, the zero-padded tail is identity.
1335        if let (Some(ghd), Some(gkv)) = (arch.global_head_dim, arch.num_global_kv_heads) {
1336            pipeline.global_attn = Some((ghd, gkv));
1337            let prf = arch.global_partial_rotary_factor.unwrap_or(1.0);
1338            let half = ghd / 2;
1339            let ra = (((prf * ghd as f32) as usize) / 2).min(half);
1340            let mut f = vec![0.0f32; half];
1341            for (i, slot) in f.iter_mut().enumerate().take(ra) {
1342                *slot = 1.0 / (arch.rope_theta as f32).powf(2.0 * i as f32 / ghd as f32);
1343            }
1344            pipeline.inv_freq_global = Some(std::sync::Arc::new(f));
1345            // Re-shape the global layers' KV storage to their geometry.
1346            // An explicit layer_types map wins over the numeric pattern
1347            // (explicit tags set swa's pattern to usize::MAX, which
1348            // would otherwise leave every global cache mis-shaped).
1349            let global_at = |li: usize| -> bool {
1350                match &pipeline.sliding_layers {
1351                    Some(map) => !map.get(li).copied().unwrap_or(false),
1352                    None => pipeline
1353                        .swa
1354                        .map(|(_, p)| p > 0 && p != usize::MAX && (li + 1) % p == 0)
1355                        .unwrap_or(false),
1356                }
1357            };
1358            for li in 0..arch.num_layers {
1359                if global_at(li) {
1360                    pipeline.kv_cache.layers[li] = crate::kv_cache::LayerKvCache::new(gkv, ghd);
1361                }
1362            }
1363        }
1364        // MLA (DeepSeek-V2): the expand-to-MHA cache holds nh heads of
1365        // rope+nope dims; rotary covers the rope prefix.
1366        if let Some(mla) = arch.mla.as_ref() {
1367            let hd = mla.qk_rope_head_dim + mla.qk_nope_head_dim;
1368            pipeline.head_dim = hd;
1369            pipeline.num_kv_heads = arch.num_attention_heads;
1370            pipeline.rotary_dim = mla.qk_rope_head_dim;
1371            let half = mla.qk_rope_head_dim / 2;
1372            let mut f = vec![0.0f32; half];
1373            for (i, slot) in f.iter_mut().enumerate() {
1374                *slot = 1.0
1375                    / (arch.rope_theta as f32).powf(2.0 * i as f32 / mla.qk_rope_head_dim as f32);
1376            }
1377            pipeline.inv_freq = std::sync::Arc::new(f);
1378            for li in 0..arch.num_layers {
1379                pipeline.kv_cache.layers[li] =
1380                    crate::kv_cache::LayerKvCache::new(arch.num_attention_heads, hd);
1381            }
1382        }
1383        // Per-frequency rope divisors (MiniCPM3 longrope short_factor):
1384        // served at the native window with the trained per-dim factors.
1385        // Applied after every inv_freq build (plain, YaRN, MLA).
1386        if let Some(fac) = &arch.rope_freq_factors {
1387            let mut f = pipeline.inv_freq.as_ref().clone();
1388            for (i, v) in f.iter_mut().enumerate() {
1389                if let Some(&d) = fac.get(i) {
1390                    *v /= d as f32;
1391                }
1392            }
1393            pipeline.inv_freq = std::sync::Arc::new(f);
1394        }
1395        pipeline.attn_v_norm = arch.attn_v_norm;
1396        pipeline.final_softcap = arch.final_logit_softcapping.map(|c| c as f32);
1397        // Cortiq Embryo hierarchical head: cluster matrix → two-level log-probs.
1398        if let Some(ncl) = arch.head_clusters {
1399            let cm = load_f32(model, "lm_head.clusters.weight", ov).map_err(err)?;
1400            if cm.len() != ncl * arch.hidden_size {
1401                return Err(CmfError::Parse(format!(
1402                    "lm_head.clusters.weight: {} != {ncl}×{}",
1403                    cm.len(),
1404                    arch.hidden_size
1405                )));
1406            }
1407            pipeline.head_clusters = Some(std::sync::Arc::new(cm));
1408        }
1409        pipeline.attn_softcap = arch.attn_logit_softcapping.unwrap_or(0.0) as f32;
1410        pipeline.vmf_cfg = vmf_cfg;
1411        pipeline.gdn_cfg = gdn_cfg;
1412        pipeline.kda_cfg = kda_cfg;
1413        if let Some(gc) = arch.g3n.as_ref() {
1414            use crate::g3n::{G3nAltUp, G3nGlobals, G3nLaurel, G3nLayer};
1415            anyhow_like(gc.altup_num_inputs == crate::g3n::ALTUP_N).map_err(|_| {
1416                CmfError::Parse(format!(
1417                    "g3n: altup_num_inputs {} != supported {}",
1418                    gc.altup_num_inputs,
1419                    crate::g3n::ALTUP_N
1420                ))
1421            })?;
1422            let t = |name: &str| load_matrix(model, name, force_f32, ov);
1423            let f = |name: &str| load_f32(model, name, ov).map_err(err);
1424            let mut altup_proj = Vec::new();
1425            let mut altup_unembed = Vec::new();
1426            for i in 0..crate::g3n::ALTUP_N - 1 {
1427                altup_proj.push(t(&format!("model.altup_projections.{i}.weight"))?);
1428                altup_unembed.push(t(&format!("model.altup_unembed_projections.{i}.weight"))?);
1429            }
1430            let first_shared = arch.num_layers.saturating_sub(gc.num_kv_shared_layers);
1431            let sliding_of = |li: usize| {
1432                matches!(
1433                    arch.layer_types.get(li),
1434                    Some(cortiq_core::LayerType::SlidingAttention)
1435                )
1436            };
1437            let mut g3n_layers = Vec::with_capacity(arch.num_layers);
1438            for li in 0..arch.num_layers {
1439                let pfx = format!("model.layers.{li}.");
1440                let shared = li >= first_shared && first_shared > 0;
1441                let share_src = if shared {
1442                    let want = sliding_of(li);
1443                    (0..first_shared).rev().find(|&j| sliding_of(j) == want)
1444                } else {
1445                    None
1446                };
1447                g3n_layers.push(G3nLayer {
1448                    altup: G3nAltUp {
1449                        router_norm: f(&format!("{pfx}altup.router_norm.weight"))?,
1450                        modality_router: t(&format!("{pfx}altup.modality_router.weight"))?,
1451                        prediction_coefs: t(&format!("{pfx}altup.prediction_coefs.weight"))?,
1452                        correction_coefs: t(&format!("{pfx}altup.correction_coefs.weight"))?,
1453                        correct_output_scale: f(&format!("{pfx}altup.correct_output_scale"))?,
1454                    },
1455                    laurel: G3nLaurel {
1456                        left: t(&format!("{pfx}laurel.linear_left.weight"))?,
1457                        right: t(&format!("{pfx}laurel.linear_right.weight"))?,
1458                        post_norm: f(&format!("{pfx}laurel.post_laurel_norm.weight"))?,
1459                    },
1460                    input_norm: f(&format!("{pfx}input_layernorm.weight"))?,
1461                    post_attn_norm: f(&format!("{pfx}post_attention_layernorm.weight"))?,
1462                    pre_ffw_norm: f(&format!("{pfx}pre_feedforward_layernorm.weight"))?,
1463                    post_ffw_norm: f(&format!("{pfx}post_feedforward_layernorm.weight"))?,
1464                    wq: t(&format!("{pfx}self_attn.q_proj.weight"))?,
1465                    wk: if shared {
1466                        None
1467                    } else {
1468                        Some(t(&format!("{pfx}self_attn.k_proj.weight"))?)
1469                    },
1470                    wv: if shared {
1471                        None
1472                    } else {
1473                        Some(t(&format!("{pfx}self_attn.v_proj.weight"))?)
1474                    },
1475                    wo: t(&format!("{pfx}self_attn.o_proj.weight"))?,
1476                    q_norm: f(&format!("{pfx}self_attn.q_norm.weight"))?,
1477                    k_norm: if shared {
1478                        None
1479                    } else {
1480                        Some(f(&format!("{pfx}self_attn.k_norm.weight"))?)
1481                    },
1482                    kv_share_src: share_src,
1483                    sliding: sliding_of(li),
1484                    gate: t(&format!("{pfx}mlp.gate_proj.weight"))?,
1485                    up: t(&format!("{pfx}mlp.up_proj.weight"))?,
1486                    down: t(&format!("{pfx}mlp.down_proj.weight"))?,
1487                    sparsity: gc.activation_sparsity.get(li).copied().unwrap_or(0.0),
1488                    ple_gate: t(&format!("{pfx}per_layer_input_gate.weight"))?,
1489                    ple_proj: t(&format!("{pfx}per_layer_projection.weight"))?,
1490                    post_ple_norm: f(&format!("{pfx}post_per_layer_input_norm.weight"))?,
1491                });
1492            }
1493            let hd = arch.head_dim;
1494            let globals = G3nGlobals {
1495                altup_proj,
1496                altup_unembed,
1497                ple_embed: t("model.embed_tokens_per_layer.weight")?,
1498                ple_model_proj: t("model.per_layer_model_projection.weight")?,
1499                ple_norm: f("model.per_layer_projection_norm.weight")?,
1500                ple_vocab: gc.ple_vocab,
1501                ple_dim: gc.ple_dim,
1502                num_layers: arch.num_layers,
1503                hidden: arch.hidden_size,
1504                rms_eps: arch.rms_norm_eps,
1505                inv_freq_local: crate::attention::rope_inv_freq(
1506                    hd,
1507                    arch.rope_local_base_freq.unwrap_or(10_000.0) as f32,
1508                ),
1509                inv_freq_global: crate::attention::rope_inv_freq(hd, arch.rope_theta as f32),
1510                window: arch.sliding_window.unwrap_or(512),
1511            };
1512            pipeline.g3n = Some(Box::new((globals, g3n_layers)));
1513        }
1514        // DeepSeek-V4: its own stack, selected by the arch name the
1515        // converter wrote. Loading failure is fatal rather than a silent
1516        // fallback — the generic loop cannot represent this model at all,
1517        // so a fallback would decode noise.
1518        if arch.arch_name == "deepseek_v4" {
1519            let moe = arch
1520                .moe
1521                .as_ref()
1522                .ok_or_else(|| CmfError::Parse("deepseek_v4: no moe config".into()))?;
1523            let cfg = crate::dsv4::Dsv4Cfg {
1524                dim: arch.hidden_size,
1525                n_heads: arch.num_attention_heads,
1526                head_dim: arch.head_dim,
1527                // The rope tail: `partial_rotary_factor` carries it when the
1528                // conversion recorded it (rd/head_dim), which the tensors
1529                // cannot reveal. Files converted before that carry 1.0,
1530                // meaning "unset" here rather than "rotate everything" —
1531                // for those the release's 64 stands in, which is what they
1532                // were converted from.
1533                rope_head_dim: if arch.partial_rotary_factor < 1.0 {
1534                    (((arch.head_dim as f32 * arch.partial_rotary_factor) as usize) & !1)
1535                        .clamp(2, arch.head_dim)
1536                } else {
1537                    64.min(arch.head_dim)
1538                },
1539                // The LoRA ranks and the group count ARE visible in the
1540                // weights, and reading them there means a re-tuned
1541                // checkpoint loads without touching this code.
1542                q_lora_rank: 0,
1543                o_lora_rank: 0,
1544                // Derived below from wo_a's shape — the attention output is
1545                // n_heads*head_dim wide and wo_a takes one group of it per
1546                // row block, so groups = width / wo_a.cols(). A pinned 8 is
1547                // right for the release and wrong for anything else, which
1548                // is exactly what made a toy checkpoint impossible to
1549                // compare against the reference.
1550                o_groups: 8,
1551                hc_mult: 4,
1552                hc_sinkhorn_iters: 20,
1553                hc_eps: 1e-6,
1554                norm_eps: arch.rms_norm_eps as f32,
1555                n_routed_experts: moe.num_experts,
1556                top_k: moe.top_k,
1557                moe_inter: moe.moe_intermediate_size,
1558                route_scale: moe.routed_scaling_factor.unwrap_or(1.0),
1559                // config.json's `swiglu_limit`, which the header has no
1560                // field for. The release ships 10.0; a checkpoint that
1561                // retunes it would need this read from the config, so it
1562                // sits next to the other pinned constants rather than
1563                // hiding inside the expert.
1564                swiglu_limit: 10.0,
1565                window: arch.sliding_window.unwrap_or(128),
1566                index_topk: 512,
1567                vocab: arch.vocab_size,
1568            };
1569            let (g, dl) = crate::dsv4::load(model, &cfg, arch.num_layers)
1570                .map_err(|e| CmfError::Parse(format!("deepseek_v4: {e}")))?;
1571            // Read the ranks off the weights that define them: wq_a's
1572            // rows ARE q_lora_rank, and wo_b's columns are groups x
1573            // o_lora_rank. A header field could disagree with the file;
1574            // these cannot.
1575            let mut cfg = cfg;
1576            if let Some(l0) = dl.first() {
1577                cfg.q_lora_rank = l0.wq_a.rows();
1578                let attn_width = arch.num_attention_heads * arch.head_dim;
1579                if l0.wo_a.cols() > 0 && attn_width % l0.wo_a.cols() == 0 {
1580                    cfg.o_groups = (attn_width / l0.wo_a.cols()).max(1);
1581                }
1582                cfg.o_lora_rank = l0.wo_b.cols() / cfg.o_groups.max(1);
1583                cfg.hc_mult = (l0.hc_attn_fn.len() / l0.hc_attn_base.len().max(1)) / cfg.dim.max(1);
1584                if cfg.hc_mult == 0 {
1585                    cfg.hc_mult = 4;
1586                }
1587            }
1588            // RoPE rides only the last `rope_head_dim` of each head, and the
1589            // reference builds its frequencies over THAT width — not over
1590            // head_dim, which is 512 here. The generic path above sized them
1591            // by head_dim, giving 1/base^(2i/512) where 1/base^(2i/64) is
1592            // wanted: every position rotated by the wrong angle.
1593            //
1594            // YaRN is applied unconditionally by the reference (its guard is
1595            // `original_seq_len > 0`, not the sequence length), so it belongs
1596            // in these frequencies too. Older configs spell the key `type`
1597            // rather than `rope_type`; when the header carries no profile the
1598            // release's own numbers stand in, which is better than silently
1599            // decoding with unscaled frequencies.
1600            let (yf, yo, ybf, ybs) = match &arch.yarn {
1601                Some(y) => (
1602                    y.factor,
1603                    y.original_max_position_embeddings,
1604                    y.beta_fast,
1605                    y.beta_slow,
1606                ),
1607                None => {
1608                    tracing::warn!(
1609                        "deepseek_v4: the header carries no YaRN profile — \
1610                         falling back to the release's (factor 16, original \
1611                         65536, beta 32/1). Re-converting with a build that \
1612                         reads rope_scaling.type would make this exact."
1613                    );
1614                    (16.0, 65536, 32.0, 1.0)
1615                }
1616            };
1617            pipeline.inv_freq = std::sync::Arc::new(crate::attention::yarn_inv_freq(
1618                cfg.rope_head_dim,
1619                arch.rope_theta as f32,
1620                yf,
1621                yo,
1622                ybf,
1623                ybs,
1624            ));
1625            // Keep the working set resident. Everything but the routed
1626            // experts is touched by every token, and of the experts only the
1627            // ones the task actually routes to — the page cache cannot know
1628            // that and evicts by age instead.
1629            if let Ok(stats) = std::env::var("CMF_MOE_PIN") {
1630                let cover = std::env::var("CMF_MOE_PIN_COVER")
1631                    .ok()
1632                    .and_then(|v| v.parse::<f64>().ok())
1633                    .filter(|&c| c > 0.0 && c <= 1.0)
1634                    .unwrap_or(0.95);
1635                let hot = crate::pin::hot_experts(&stats, cover);
1636                let mut names: Vec<String> = Vec::new();
1637                for e in &model.tensors {
1638                    let is_expert = e.name.contains(".mlp.experts.");
1639                    if !is_expert {
1640                        names.push(e.name.clone()); // skeleton: always hot
1641                    }
1642                }
1643                let mut kept_experts = 0usize;
1644                if let Some(hot) = &hot {
1645                    for (li, experts) in hot {
1646                        for e in experts {
1647                            for w in ["gate_proj", "up_proj", "down_proj"] {
1648                                names.push(format!("model.layers.{li}.mlp.experts.{e}.{w}.weight"));
1649                            }
1650                            kept_experts += 1;
1651                        }
1652                    }
1653                }
1654                let r = crate::pin::pin_tensors(model, &names);
1655                tracing::info!(
1656                    "закреплено {:.1} ГБ ({} тензоров, горячих экспертов {kept_experts},                      покрытие {cover}); лимит {}",
1657                    r.bytes as f64 / 1e9,
1658                    r.tensors,
1659                    r.limit
1660                        .map(|l| format!("{:.1} ГБ", l as f64 / 1e9))
1661                        .unwrap_or_else(|| "неизвестен".into())
1662                );
1663                if r.skipped > 0 {
1664                    tracing::warn!("не закреплено тензоров: {}", r.skipped);
1665                }
1666            }
1667            let st = crate::dsv4::Dsv4State::new(arch.num_layers);
1668            // The speculation stack, if the file carries one. Reading it is
1669            // metadata only — the expert weights stay in the mapping until a
1670            // draft actually runs — so it costs nothing to know it is there.
1671            let depth = std::env::var("CMF_DSV4_MTP_DEPTH")
1672                .ok()
1673                .and_then(|v| v.parse::<usize>().ok())
1674                .unwrap_or(3);
1675            pipeline.dsv4_mtp = crate::dsv4::load_mtp(model, &cfg, depth);
1676            // Before any trunk pack is built: leave the draft its VRAM.
1677            crate::dsv4::dspark_reserve_note(&pipeline.dsv4_mtp, &cfg, &dl);
1678            pipeline.dsv4 = Some(Box::new((g, dl, cfg, st)));
1679        }
1680        pipeline.short_conv_cfg = short_conv_cfg;
1681        pipeline.mtp = mtp;
1682        pipeline.install_dynamic_routing(model, false);
1683        // Record the load-time overlay so a later set_active_skill(None)
1684        // correctly reverts it (the union-diff assumes dyn_active mirrors
1685        // the live overlay). Blend loads have no single index to revert.
1686        match ov {
1687            Overlay::One(sid) => {
1688                pipeline.dyn_active = model.header.skills.iter().position(|s| &s.id == sid);
1689            }
1690            Overlay::Blend(_) => pipeline.dyn_blend_loaded = true,
1691            Overlay::None => {}
1692        }
1693        // B1: apply the measured confidence-calibration temperature, if the
1694        // file carries one (softmax(logits / T) for reported Born mass).
1695        if let Some(c) = &model.header.calibration {
1696            pipeline.set_calib_temp(c.temperature);
1697        }
1698        // O(1) Nyström attention (runtime-level, no format change):
1699        // env CMF_O1 decides; unset falls through to the converter hint
1700        // in header.provenance.o1_attn (`cortiq convert --o1`), and
1701        // CMF_O1=off force-disables even the hint. CLI flags override
1702        // later via set_o1().
1703        let o1 = match crate::nystrom::o1_from_env() {
1704            crate::nystrom::O1Env::Off => None,
1705            crate::nystrom::O1Env::On(cfg) => Some(cfg),
1706            crate::nystrom::O1Env::Unset => model
1707                .header
1708                .provenance
1709                .as_ref()
1710                .and_then(|p| p.get("o1_attn"))
1711                .and_then(crate::nystrom::O1Cfg::from_json),
1712        };
1713        if o1.is_some() {
1714            if pipeline.attn_softcap > 0.0 {
1715                return Err(CmfError::Parse(
1716                    "--o1 with attention-logit soft-capping (Gemma-2) is not supported: \
1717                     the streaming operator has no capped-score form"
1718                        .into(),
1719                ));
1720            }
1721            pipeline.set_o1(o1);
1722        }
1723        Ok(pipeline)
1724    }
1725
1726    /// Record per-skill dynamic-routing metadata: which FFN layers each
1727    /// skill actually replaces (derived from the tensors present, not
1728    /// the meta `layers` field), and whether the skill is eligible for
1729    /// cheap dynamic switching (FFN-only). Called once at load.
1730    pub(crate) fn install_dynamic_routing(&mut self, model: &Arc<CmfModel>, force_f32: bool) {
1731        self.model = Some(model.clone());
1732        self.dyn_force_f32 = force_f32;
1733        let mut per_skill = Vec::with_capacity(model.header.skills.len());
1734        for sk in &model.header.skills {
1735            let mut ffn_layers = std::collections::BTreeSet::new();
1736            let mut non_ffn = false;
1737            let prefix = format!("skill.{}.", sk.id);
1738            for t in model.skill_tensors(&sk.id) {
1739                let rel = &t.name[prefix.len()..]; // e.g. model.layers.20.mlp.down_proj.weight
1740                let toks: Vec<&str> = rel.split('.').collect();
1741                if toks.len() >= 5 && toks[0] == "model" && toks[1] == "layers" && toks[3] == "mlp"
1742                {
1743                    if let Ok(li) = toks[2].parse::<usize>() {
1744                        ffn_layers.insert(li);
1745                        continue;
1746                    }
1747                }
1748                non_ffn = true; // replaces attention / embed / lm_head
1749            }
1750            if non_ffn {
1751                tracing::warn!(
1752                    "skill '{}' replaces non-FFN tensors — excluded from dynamic \
1753                     routing (static overlay still works)",
1754                    sk.id
1755                );
1756                per_skill.push(None);
1757            } else {
1758                per_skill.push(Some(ffn_layers.into_iter().collect::<Vec<_>>()));
1759            }
1760        }
1761        self.dyn_skill_layers = per_skill;
1762    }
1763
1764    /// Switch the overlaid skill for subsequent forwards (dynamic
1765    /// routing). `idx` = index into model.header.skills; None = backbone.
1766    /// Rebuilds the FFN of the union of the old and new skill's touched
1767    /// layers with the new overlay — tensor-source indirection made
1768    /// dynamic. Cheap: Mapped tensors are re-resolved mmap pointers.
1769    /// Result is bit-identical to loading the pipeline with that skill.
1770    pub fn set_active_skill(&mut self, idx: Option<usize>) -> Result<(), CmfError> {
1771        // Overlay swap changes weights → every cached K/V is stale.
1772        self.kv_cache.clear();
1773        self.kv_history.clear();
1774        if self.dyn_active == idx {
1775            return Ok(());
1776        }
1777        let model = self.model.clone().ok_or_else(|| {
1778            CmfError::Parse("dynamic routing needs a model-backed pipeline".into())
1779        })?;
1780        let mut union: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
1781        if let Some(old) = self.dyn_active {
1782            if let Some(Some(ls)) = self.dyn_skill_layers.get(old) {
1783                union.extend(ls.iter().copied());
1784            }
1785        }
1786        let new_id: Option<String> = match idx {
1787            Some(n) => match self.dyn_skill_layers.get(n) {
1788                Some(Some(ls)) => {
1789                    union.extend(ls.iter().copied());
1790                    Some(model.header.skills[n].id.clone())
1791                }
1792                _ => {
1793                    return Err(CmfError::Parse(format!(
1794                        "skill index {n} not dynamic-eligible"
1795                    )));
1796                }
1797            },
1798            None => None,
1799        };
1800        let ov = match &new_id {
1801            Some(s) => Overlay::One(s),
1802            None => Overlay::None,
1803        };
1804        let arch = model.arch();
1805        for li in union {
1806            self.weights.layers[li].ffn =
1807                build_layer_ffn(&model, arch, li, self.dyn_force_f32, &ov)?;
1808        }
1809        self.dyn_active = idx;
1810        Ok(())
1811    }
1812}