metrale_model_arch/weight_loader/longcat/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: LongCat weight loader: each checkpoint layer becomes two engine sublayers.
4//!
5//! Owner: model-arch weight loader (LongCat).
6//! Invariants:
7//! - `num_hidden_layers` is even (the config parser doubles the checkpoint's layer count);
8//!   checkpoint layer `l` becomes sublayers `2l` and `2l + 1`.
9//! - Each sublayer is a `Qwen3AttentionLayer` with MLA weights and its own dense FFN.
10//!   Sublayer 0 of a block holds the shortcut MoE and writes its output to the block's
11//!   carry buffer (`set_shortcut_moe`); sublayer 1 adds it (`set_shortcut_carry_in`).
12//! - The interleaved-RoPE permutation, the LoRA scales and the head padding are folded into
13//!   the weights at load (`prep`).
14//!
15//! Tensor names under `model.layers.{L}.`:
16//!   `self_attn.{0,1}.{q_a_proj,q_a_layernorm,q_b_proj,kv_a_proj_with_mqa,
17//!                     kv_a_layernorm,kv_b_proj,o_proj}`
18//!   `mlps.{0,1}.{gate,up,down}_proj`, `input_layernorm.{0,1}`,
19//!   `post_attention_layernorm.{0,1}`,
20//!   `mlp.router.{classifier.weight,e_score_correction_bias}`,
21//!   `mlp.experts.{e}.{gate,up,down}_proj`.
22
23mod ngram;
24mod prep;
25
26use anyhow::{Context, Result};
27use metrale_cache::kv_cache::KvCacheDtype;
28use metrale_config::ModelConfig;
29use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
30use metrale_model_weights::weights::WeightStore;
31
32use crate::mistral_loader::loader_impl::{
33    ctx as mctx, phase_block_diag, phase_per_head, phase_qk_absorbed,
34};
35use crate::weight_loader::ModelWeightLoader;
36use metrale_model_layers::layer::TransformerLayer;
37use metrale_model_layers::layers::Qwen3AttentionLayer;
38use metrale_model_layers::layers::VisionTower;
39use metrale_model_layers::layers::qwen3_attention::MlaWeights;
40use metrale_model_layers::weight_map::{
41    AttentionWeights, DenseWeight, MtpWeights, QuantizedWeight, dense, quantize_to_nvfp4,
42};
43
44pub struct LongcatWeightLoader;
45
46/// 2026-09-25: Tokens one shortcut-MoE carry buffer holds; a larger chunk is an
47/// error (`set_shortcut_moe`).
48const CARRY_TOKENS: usize = 8192;
49
50impl ModelWeightLoader for LongcatWeightLoader {
51    fn supports_tp(&self) -> bool {
52        // 2026-09-25: Nothing here shards the MLA weights or the shortcut carry.
53        false
54    }
55
56    fn load_layers(
57        &self,
58        store: &WeightStore,
59        config: &ModelConfig,
60        gpu: &dyn GpuBackend,
61        layer_kv_dtypes: &[KvCacheDtype],
62    ) -> Result<Vec<Box<dyn TransformerLayer>>> {
63        anyhow::ensure!(
64            config.num_hidden_layers.is_multiple_of(2),
65            "longcat: num_hidden_layers ({}) must be even — each checkpoint \
66             layer is TWO engine sublayers",
67            config.num_hidden_layers
68        );
69        let ckpt_layers = config.num_hidden_layers / 2;
70        let h = config.hidden_size;
71        let nope = config.qk_nope_head_dim;
72        let rope = config.qk_rope_head_dim;
73        let q_lora = config.q_lora_rank;
74        let kv_lora = config.kv_lora_rank;
75        let n_heads = config.num_attention_heads;
76        // 2026-09-25: The q and kv LoRA scales, `sqrt(hidden / rank)`, folded
77        // into `q_b_proj` and `kv_a_layernorm` by `prep`.
78        let scale_q = (h as f32 / q_lora as f32).sqrt();
79        let scale_kv = (h as f32 / kv_lora as f32).sqrt();
80        // 2026-09-25: The qk and v heads are both padded to 256 in the weights
81        // (`prep`).
82        let true_qk_hd = nope + rope;
83        let padded_hd = 256usize;
84        let padded_nope = padded_hd - rope;
85        // 2026-09-25: The softmax scale stays 1/sqrt(true qk head width).
86        let attn_scale = 1.0f32 / (true_qk_hd as f32).sqrt();
87
88        tracing::info!(
89            "LongCat: {ckpt_layers} checkpoint layers → {} engine sublayers \
90             (MLA q_lora={q_lora} kv_lora={kv_lora} nope={nope} rope={rope}; \
91             rope de-interleave + q/kv LoRA scale folded at load: \
92             q×{scale_q:.4}, kv-norm×{scale_kv:.4}); {} routed + {} zero experts",
93            config.num_hidden_layers,
94            config.num_experts,
95            config.zero_expert_num,
96        );
97
98        // 2026-09-25: `METRALE_NVFP4_MLA` set to 0/false/no/off keeps the MLA
99        // projections in BF16, the same lever the Mistral MLA loader reads.
100        let disable_nvfp4_mla = std::env::var("METRALE_NVFP4_MLA")
101            .map(|v| {
102                let v = v.trim().to_ascii_lowercase();
103                matches!(v.as_str(), "0" | "false" | "no" | "off")
104            })
105            .unwrap_or(false);
106        if disable_nvfp4_mla {
107            tracing::info!("LongCat: METRALE_NVFP4_MLA=0 — MLA projections stay BF16");
108        }
109        if super::longcat::ffn::bf16_dense_ffn() {
110            tracing::info!(
111                "LongCat: METRALE_LONGCAT_BF16_FFN=1 — per-sublayer dense FFN stays BF16"
112            );
113        }
114        let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
115        let quantize_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
116        let stream = gpu.default_stream();
117        let mut yarn_shared = DevicePtr::NULL;
118        let mut layers: Vec<Box<dyn TransformerLayer>> =
119            Vec::with_capacity(config.num_hidden_layers);
120
121        for l in 0..ckpt_layers {
122            let lp = format!("model.layers.{l}");
123            // 2026-09-25: One carry buffer per checkpoint layer, written by
124            // sublayer 0 and read by sublayer 1.
125            let carry = gpu.alloc(CARRY_TOKENS * h * 2)?;
126
127            for s in 0..2usize {
128                let ap = format!("{lp}.self_attn.{s}");
129                let global_idx = l * 2 + s;
130
131                // 2026-09-25: MLA weights, loaded by name through `prep`.
132                let wq_a = dense(store, &format!("{ap}.q_a_proj.weight"))?;
133                let wq_b = prep::prep_q_b(
134                    store,
135                    &format!("{ap}.q_b_proj.weight"),
136                    n_heads,
137                    nope,
138                    rope,
139                    q_lora,
140                    scale_q,
141                    padded_hd,
142                    gpu,
143                )?;
144                let q_a_norm = dense(store, &format!("{ap}.q_a_layernorm.weight"))?;
145                let wkv_a = prep::prep_kv_a(
146                    store,
147                    &format!("{ap}.kv_a_proj_with_mqa.weight"),
148                    kv_lora,
149                    rope,
150                    h,
151                    gpu,
152                )?;
153                let kv_a_norm = prep::prep_kv_a_norm(
154                    store,
155                    &format!("{ap}.kv_a_layernorm.weight"),
156                    kv_lora,
157                    scale_kv,
158                    gpu,
159                )?;
160                let wkv_b = prep::prep_kv_b(
161                    store,
162                    &format!("{ap}.kv_b_proj.weight"),
163                    n_heads,
164                    nope,
165                    config.v_head_dim,
166                    rope,
167                    kv_lora,
168                    padded_hd,
169                    gpu,
170                )?;
171                let wo = prep::prep_o_proj(
172                    store,
173                    &format!("{ap}.o_proj.weight"),
174                    h,
175                    n_heads,
176                    config.v_head_dim,
177                    padded_hd,
178                    gpu,
179                )?;
180
181                // 2026-09-25: MLA precompute through the Mistral loader's phases:
182                // per-head views, absorbed QK, block diagonals.
183                let mut c = mctx::MistralLayerCtx::new(
184                    store, config, gpu, absmax_k, quantize_k, stream, global_idx,
185                );
186                // 2026-09-25: Everything downstream indexes the padded weights.
187                c.hd = padded_hd;
188                c.nope = padded_nope;
189                c.v_dim = padded_hd;
190                c.wq_a_dense = Some(wq_a);
191                c.wq_b = Some(wq_b);
192                c.q_a_norm = Some(q_a_norm);
193                c.wkv_a_dense = Some(wkv_a);
194                c.wkv_a_rope_dense = Some(DenseWeight {
195                    weight: wkv_a.weight.offset(kv_lora * h * 2),
196                });
197                c.wkv_b = Some(wkv_b);
198                c.kv_a_norm = Some(kv_a_norm);
199                c.wq_a_nvfp4 = Some(quantize_to_nvfp4(
200                    &wq_a, q_lora, h, gpu, absmax_k, quantize_k, stream,
201                )?);
202                c.wq_b_nvfp4 = Some(quantize_to_nvfp4(
203                    &wq_b,
204                    n_heads * padded_hd,
205                    q_lora,
206                    gpu,
207                    absmax_k,
208                    quantize_k,
209                    stream,
210                )?);
211                c.wkv_a_nvfp4 = Some(quantize_to_nvfp4(
212                    &wkv_a,
213                    kv_lora + rope,
214                    h,
215                    gpu,
216                    absmax_k,
217                    quantize_k,
218                    stream,
219                )?);
220                phase_per_head::build_per_head_views(&mut c)?;
221                phase_qk_absorbed::build_w_qk_absorbed(&mut c)?;
222                phase_block_diag::build_block_diagonals(&mut c)?;
223                let o_nvfp4 = quantize_to_nvfp4(
224                    &wo,
225                    h,
226                    n_heads * padded_hd,
227                    gpu,
228                    absmax_k,
229                    quantize_k,
230                    stream,
231                )?;
232                let yarn = mctx::ensure_yarn_inv_freq(&mut yarn_shared, config, rope, gpu)?;
233
234                let null = DenseWeight {
235                    weight: DevicePtr::NULL,
236                };
237                let mla = MlaWeights {
238                    wq_a,
239                    wq_a_fp8: None,
240                    wq_a_nvfp4: if disable_nvfp4_mla {
241                        None
242                    } else {
243                        c.wq_a_nvfp4
244                    },
245                    wq_b,
246                    wq_b_fp8: None,
247                    wq_b_nvfp4: if disable_nvfp4_mla {
248                        None
249                    } else {
250                        c.wq_b_nvfp4
251                    },
252                    q_a_norm,
253                    wkv_a,
254                    wkv_a_nvfp4: if disable_nvfp4_mla {
255                        None
256                    } else {
257                        c.wkv_a_nvfp4
258                    },
259                    wkv_b,
260                    kv_a_norm,
261                    wkv_a_rope: c.wkv_a_rope_dense.expect("set above"),
262                    wkv_a_merged: DenseWeight {
263                        weight: wkv_a.weight,
264                    },
265                    wo,
266                    wo_nvfp4: if disable_nvfp4_mla {
267                        None
268                    } else {
269                        Some(o_nvfp4)
270                    },
271                    wo_a: null,
272                    wo_a_nvfp4: None,
273                    wo_b: null,
274                    wo_b_nvfp4: None,
275                    wo_b_fp8: None,
276                    wo_a_fp8: None,
277                    wkv_a_fp8: None,
278                    wq_b_rope: c.wq_b_rope.context("longcat: wq_b_rope")?,
279                    w_uk_t: c.w_uk_t.context("longcat: w_uk_t")?,
280                    w_uv: c.w_uv.context("longcat: w_uv")?,
281                    w_qk_absorbed: c.w_qk_absorbed.context("longcat: w_qk_absorbed")?,
282                    w_uk_block_diag: c.w_uk_block_diag.context("longcat: w_uk_bd")?,
283                    w_uv_block_diag: c.w_uv_block_diag.context("longcat: w_uv_bd")?,
284                    yarn_inv_freq: yarn,
285                    main_inv_freq: yarn,
286                    q_lora_rank: q_lora,
287                    kv_lora_rank: kv_lora,
288                    o_lora_rank: 0,
289                    nope: padded_nope,
290                    rope,
291                    v_dim: padded_hd,
292                    compressor: None,
293                    attn_sink: DevicePtr::NULL,
294                };
295
296                // 2026-09-25: Null attention weights; the layer runs on the MLA
297                // weights installed by `set_mla_weights`.
298                let o_dummy = QuantizedWeight {
299                    weight: DevicePtr::NULL,
300                    weight_scale: DevicePtr::NULL,
301                    weight_scale_2: 0.0,
302                    input_scale: DevicePtr::NULL,
303                    weight_scale_2_vec: DevicePtr::NULL,
304                    act: metrale_config::Nvfp4Act::Unstamped,
305                };
306                let attn = AttentionWeights {
307                    q_proj: null,
308                    k_proj: null,
309                    v_proj: null,
310                    o_proj: o_dummy,
311                    q_norm: null,
312                    k_norm: null,
313                    q_norm_full: None,
314                    k_norm_full: None,
315                    k_scale: 1.0,
316                    v_scale: 1.0,
317                };
318
319                // 2026-09-25: This sublayer's dense SwiGLU FFN.
320                let ffn = build_dense_ffn(store, &format!("{lp}.mlps.{s}"), config, gpu)?;
321                let input_norm = dense(store, &format!("{lp}.input_layernorm.{s}.weight"))?;
322                let post_norm = dense(store, &format!("{lp}.post_attention_layernorm.{s}.weight"))?;
323                let kv_dtype = layer_kv_dtypes
324                    .get(global_idx)
325                    .copied()
326                    .unwrap_or(KvCacheDtype::Bf16);
327
328                let mut layer = Qwen3AttentionLayer::new_ungated(
329                    input_norm, attn, post_norm, ffn, global_idx, None, None, None, gpu, kv_dtype,
330                    0, config,
331                )?;
332                layer.set_mla_weights(mla);
333                // 2026-09-25: Padding widened the head; the scale stays
334                // 1/sqrt(true qk head width).
335                layer.set_attn_scale_override(attn_scale);
336                // 2026-09-25: Q/K/V are strided by the padded head width the
337                // weights emit, not the config's qk head width.
338                layer.set_dimension_overrides(padded_hd, n_heads, n_heads);
339
340                if s == 0 {
341                    // 2026-09-25: Sublayer 0 owns the block's shortcut MoE; its
342                    // output is stored and added at the end of sublayer 1.
343                    let moe = build_shortcut_moe(store, &lp, config, gpu)?;
344                    layer.set_shortcut_moe(moe, carry, CARRY_TOKENS);
345                } else {
346                    layer.set_shortcut_carry_in(carry, CARRY_TOKENS);
347                }
348                layers.push(Box::new(layer));
349            }
350
351            if (l + 1) % 4 == 0 || l == ckpt_layers - 1 {
352                let free = gpu.free_memory().unwrap_or(0);
353                tracing::info!(
354                    "LongCat L{}/{ckpt_layers} — {:.1} GB free",
355                    l + 1,
356                    free as f64 / 1e9
357                );
358            }
359        }
360        Ok(layers)
361    }
362
363    fn load_embedding(
364        &self,
365        store: &WeightStore,
366        _config: &ModelConfig,
367        _gpu: &dyn GpuBackend,
368    ) -> Result<DenseWeight> {
369        dense(store, "model.embed_tokens.weight").context("longcat: embedding")
370    }
371
372    fn load_ngram_embedding(
373        &self,
374        store: &WeightStore,
375        config: &ModelConfig,
376        gpu: &dyn GpuBackend,
377        max_tokens: usize,
378    ) -> Result<Option<metrale_model_layers::layers::ngram_embed::NgramEmbedding>> {
379        ngram::build(store, config, gpu, max_tokens)
380    }
381
382    fn load_final_norm(
383        &self,
384        store: &WeightStore,
385        _config: &ModelConfig,
386        _gpu: &dyn GpuBackend,
387    ) -> Result<DenseWeight> {
388        dense(store, "model.norm.weight").context("longcat: final norm")
389    }
390
391    fn load_lm_head(
392        &self,
393        store: &WeightStore,
394        config: &ModelConfig,
395        _gpu: &dyn GpuBackend,
396    ) -> Result<DenseWeight> {
397        if store.contains("lm_head.weight") {
398            dense(store, "lm_head.weight")
399        } else if config.tie_word_embeddings {
400            dense(store, "model.embed_tokens.weight")
401        } else {
402            anyhow::bail!("longcat: lm_head.weight not found")
403        }
404    }
405
406    fn load_mtp_weights(
407        &self,
408        _store: &WeightStore,
409        _config: &ModelConfig,
410        _gpu: &dyn GpuBackend,
411    ) -> Result<Option<MtpWeights>> {
412        Ok(None)
413    }
414
415    fn load_vision_encoder(
416        &self,
417        _store: &WeightStore,
418        _config: &ModelConfig,
419        _gpu: &dyn GpuBackend,
420    ) -> Result<Option<VisionTower>> {
421        Ok(None)
422    }
423}
424
425mod ffn;
426use ffn::{build_dense_ffn, build_shortcut_moe};