spark_model/weight_loader/longcat/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! LongCat-Flash(-Lite) weight loader — the backbone behind the n-gram
4//! embeddings (`longcat_flash_ngram`).
5//!
6//! Architecture (HF `modeling_longcat_flash.py`), and how it maps onto Atlas:
7//!
8//! - Each CHECKPOINT layer is a dual-sublayer "shortcut" block: two MLA
9//!   attentions, two dense SwiGLU MLPs, and ONE shortcut MoE whose output is
10//!   computed on sublayer 1's post-attention normed input but added at the END
11//!   of sublayer 2. Atlas serves each SUBLAYER as one `Qwen3AttentionLayer`
12//!   (`num_hidden_layers` is already 2x at parse), with the shortcut carried
13//!   between the pair via `set_shortcut_moe` / `set_shortcut_carry_in`.
14//! - MLA is the DeepSeek-lineage q-LoRA form Atlas already serves; the two
15//!   LongCat deltas (interleaved rope, sqrt LoRA scaling) fold into the
16//!   WEIGHTS at load (see `prep`), so the runtime is unchanged.
17//! - The MoE router is softmax + `e_score_correction_bias` over
18//!   `n_routed + zero_expert_num` logits, with the zero (identity) experts
19//!   folded inside the router kernel (see `moe_topk_softmax_bias.cu`).
20//!
21//! Tensor names are HF-standard under `model.layers.{L}.`:
22//!   `self_attn.{0,1}.{q_a_proj,q_a_layernorm,q_b_proj,kv_a_proj_with_mqa,
23//!                     kv_a_layernorm,kv_b_proj,o_proj}`
24//!   `mlps.{0,1}.{gate,up,down}_proj`, `input_layernorm.{0,1}`,
25//!   `post_attention_layernorm.{0,1}`,
26//!   `mlp.router.{classifier.weight,e_score_correction_bias}`,
27//!   `mlp.experts.{e}.{gate,up,down}_proj`.
28
29mod ngram;
30mod prep;
31
32use anyhow::{Context, Result};
33use avarok_core::config::ModelConfig;
34use spark_runtime::gpu::{DevicePtr, GpuBackend};
35use spark_runtime::kv_cache::KvCacheDtype;
36use spark_runtime::weights::WeightStore;
37
38use crate::layer::TransformerLayer;
39use crate::layers::Qwen3AttentionLayer;
40use crate::layers::VisionTower;
41use crate::layers::qwen3_attention::MlaWeights;
42use crate::mistral_loader::loader_impl::{
43    ctx as mctx, phase_block_diag, phase_per_head, phase_qk_absorbed,
44};
45use crate::weight_loader::ModelWeightLoader;
46use crate::weight_map::{
47    AttentionWeights, DenseWeight, MtpWeights, QuantizedWeight, dense, quantize_to_nvfp4,
48};
49
50pub struct LongcatWeightLoader;
51
52/// Tokens the shortcut-MoE carry buffer must hold: the largest prefill chunk
53/// a sublayer can be handed. Sized from `max_prefill_tokens`' ceiling; the
54/// producer/consumer both `ensure!` against it rather than overrunning.
55const CARRY_TOKENS: usize = 8192;
56
57impl ModelWeightLoader for LongcatWeightLoader {
58    fn supports_tp(&self) -> bool {
59        // MLA TP would need the same wq_b/wkv_b column sharding Mistral does,
60        // plus a per-rank shortcut carry. Not validated — refuse rather than
61        // serve a silently wrong shard split.
62        false
63    }
64
65    fn load_layers(
66        &self,
67        store: &WeightStore,
68        config: &ModelConfig,
69        gpu: &dyn GpuBackend,
70        layer_kv_dtypes: &[KvCacheDtype],
71    ) -> Result<Vec<Box<dyn TransformerLayer>>> {
72        anyhow::ensure!(
73            config.num_hidden_layers.is_multiple_of(2),
74            "longcat: num_hidden_layers ({}) must be even — each checkpoint \
75             layer is TWO engine sublayers",
76            config.num_hidden_layers
77        );
78        let ckpt_layers = config.num_hidden_layers / 2;
79        let h = config.hidden_size;
80        let nope = config.qk_nope_head_dim;
81        let rope = config.qk_rope_head_dim;
82        let q_lora = config.q_lora_rank;
83        let kv_lora = config.kv_lora_rank;
84        let n_heads = config.num_attention_heads;
85        // The reference's mla_scale_{q,kv}_lora flags (both true on Lite).
86        let scale_q = (h as f32 / q_lora as f32).sqrt();
87        let scale_kv = (h as f32 / kv_lora as f32).sqrt();
88        // Head-width padding (see prep.rs §3): LongCat's qk head is 192 and its
89        // v head is 128 — the first MLA model where they differ, and 192 does
90        // not compile. Both are padded to the stock 256 in the WEIGHTS.
91        let true_qk_hd = nope + rope;
92        let padded_hd = 256usize;
93        let padded_nope = padded_hd - rope;
94        // The softmax scale must stay 1/sqrt(TRUE qk head width).
95        let attn_scale = 1.0f32 / (true_qk_hd as f32).sqrt();
96
97        tracing::info!(
98            "LongCat: {ckpt_layers} checkpoint layers → {} engine sublayers \
99             (MLA q_lora={q_lora} kv_lora={kv_lora} nope={nope} rope={rope}; \
100             rope de-interleave + q/kv LoRA scale folded at load: \
101             q×{scale_q:.4}, kv-norm×{scale_kv:.4}); {} routed + {} zero experts",
102            config.num_hidden_layers,
103            config.num_experts,
104            config.zero_expert_num,
105        );
106
107        // Same measurement lever the Mistral MLA loader has: AVAROK_NVFP4_MLA=0
108        // keeps the MLA projections in BF16, which separates "the port's math
109        // is wrong" from "4-bit quantization of these projections is lossy".
110        let disable_nvfp4_mla = std::env::var("AVAROK_NVFP4_MLA")
111            .map(|v| {
112                let v = v.trim().to_ascii_lowercase();
113                matches!(v.as_str(), "0" | "false" | "no" | "off")
114            })
115            .unwrap_or(false);
116        if disable_nvfp4_mla {
117            tracing::info!("LongCat: AVAROK_NVFP4_MLA=0 — MLA projections stay BF16");
118        }
119        if super::longcat::ffn::bf16_dense_ffn() {
120            tracing::info!(
121                "LongCat: AVAROK_LONGCAT_BF16_FFN=1 — per-sublayer dense FFN stays BF16"
122            );
123        }
124        let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
125        let quantize_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
126        let stream = gpu.default_stream();
127        let mut yarn_shared = DevicePtr::NULL;
128        let mut layers: Vec<Box<dyn TransformerLayer>> =
129            Vec::with_capacity(config.num_hidden_layers);
130
131        for l in 0..ckpt_layers {
132            let lp = format!("model.layers.{l}");
133            // One carry buffer per checkpoint layer (producer sublayer 0 →
134            // consumer sublayer 1). Allocated per block so two blocks in
135            // flight (chunked prefill) cannot alias.
136            let carry = gpu.alloc(CARRY_TOKENS * h * 2)?;
137
138            for s in 0..2usize {
139                let ap = format!("{lp}.self_attn.{s}");
140                let global_idx = l * 2 + s;
141
142                // ── MLA: name-bound loads + the two LongCat folds ──
143                let wq_a = dense(store, &format!("{ap}.q_a_proj.weight"))?;
144                let wq_b = prep::prep_q_b(
145                    store,
146                    &format!("{ap}.q_b_proj.weight"),
147                    n_heads,
148                    nope,
149                    rope,
150                    q_lora,
151                    scale_q,
152                    padded_hd,
153                    gpu,
154                )?;
155                let q_a_norm = dense(store, &format!("{ap}.q_a_layernorm.weight"))?;
156                let wkv_a = prep::prep_kv_a(
157                    store,
158                    &format!("{ap}.kv_a_proj_with_mqa.weight"),
159                    kv_lora,
160                    rope,
161                    h,
162                    gpu,
163                )?;
164                let kv_a_norm = prep::prep_kv_a_norm(
165                    store,
166                    &format!("{ap}.kv_a_layernorm.weight"),
167                    kv_lora,
168                    scale_kv,
169                    gpu,
170                )?;
171                let wkv_b = prep::prep_kv_b(
172                    store,
173                    &format!("{ap}.kv_b_proj.weight"),
174                    n_heads,
175                    nope,
176                    config.v_head_dim,
177                    rope,
178                    kv_lora,
179                    padded_hd,
180                    gpu,
181                )?;
182                let wo = prep::prep_o_proj(
183                    store,
184                    &format!("{ap}.o_proj.weight"),
185                    h,
186                    n_heads,
187                    config.v_head_dim,
188                    padded_hd,
189                    gpu,
190                )?;
191
192                // ── shared MLA precompute (per-head transpose → absorbed QK
193                //    → block-diagonals), reusing the Mistral phases ──
194                let mut c = mctx::MistralLayerCtx::new(
195                    store, config, gpu, absmax_k, quantize_k, stream, global_idx,
196                );
197                // Everything downstream indexes the PADDED weights.
198                c.hd = padded_hd;
199                c.nope = padded_nope;
200                c.v_dim = padded_hd;
201                c.wq_a_dense = Some(wq_a);
202                c.wq_b = Some(wq_b);
203                c.q_a_norm = Some(q_a_norm);
204                c.wkv_a_dense = Some(wkv_a);
205                c.wkv_a_rope_dense = Some(DenseWeight {
206                    weight: wkv_a.weight.offset(kv_lora * h * 2),
207                });
208                c.wkv_b = Some(wkv_b);
209                c.kv_a_norm = Some(kv_a_norm);
210                c.wq_a_nvfp4 = Some(quantize_to_nvfp4(
211                    &wq_a, q_lora, h, gpu, absmax_k, quantize_k, stream,
212                )?);
213                c.wq_b_nvfp4 = Some(quantize_to_nvfp4(
214                    &wq_b,
215                    n_heads * padded_hd,
216                    q_lora,
217                    gpu,
218                    absmax_k,
219                    quantize_k,
220                    stream,
221                )?);
222                c.wkv_a_nvfp4 = Some(quantize_to_nvfp4(
223                    &wkv_a,
224                    kv_lora + rope,
225                    h,
226                    gpu,
227                    absmax_k,
228                    quantize_k,
229                    stream,
230                )?);
231                phase_per_head::build_per_head_views(&mut c)?;
232                phase_qk_absorbed::build_w_qk_absorbed(&mut c)?;
233                phase_block_diag::build_block_diagonals(&mut c)?;
234                let o_nvfp4 = quantize_to_nvfp4(
235                    &wo,
236                    h,
237                    n_heads * padded_hd,
238                    gpu,
239                    absmax_k,
240                    quantize_k,
241                    stream,
242                )?;
243                let yarn = mctx::ensure_yarn_inv_freq(&mut yarn_shared, config, rope, gpu)?;
244
245                let null = DenseWeight {
246                    weight: DevicePtr::NULL,
247                };
248                let mla = MlaWeights {
249                    wq_a,
250                    wq_a_fp8: None,
251                    wq_a_nvfp4: if disable_nvfp4_mla {
252                        None
253                    } else {
254                        c.wq_a_nvfp4
255                    },
256                    wq_b,
257                    wq_b_fp8: None,
258                    wq_b_nvfp4: if disable_nvfp4_mla {
259                        None
260                    } else {
261                        c.wq_b_nvfp4
262                    },
263                    q_a_norm,
264                    wkv_a,
265                    wkv_a_nvfp4: if disable_nvfp4_mla {
266                        None
267                    } else {
268                        c.wkv_a_nvfp4
269                    },
270                    wkv_b,
271                    kv_a_norm,
272                    wkv_a_rope: c.wkv_a_rope_dense.expect("set above"),
273                    wkv_a_merged: DenseWeight {
274                        weight: wkv_a.weight,
275                    },
276                    wo,
277                    wo_nvfp4: if disable_nvfp4_mla {
278                        None
279                    } else {
280                        Some(o_nvfp4)
281                    },
282                    wo_a: null,
283                    wo_a_nvfp4: None,
284                    wo_b: null,
285                    wo_b_nvfp4: None,
286                    wo_b_fp8: None,
287                    wo_a_fp8: None,
288                    wkv_a_fp8: None,
289                    wq_b_rope: c.wq_b_rope.context("longcat: wq_b_rope")?,
290                    w_uk_t: c.w_uk_t.context("longcat: w_uk_t")?,
291                    w_uv: c.w_uv.context("longcat: w_uv")?,
292                    w_qk_absorbed: c.w_qk_absorbed.context("longcat: w_qk_absorbed")?,
293                    w_uk_block_diag: c.w_uk_block_diag.context("longcat: w_uk_bd")?,
294                    w_uv_block_diag: c.w_uv_block_diag.context("longcat: w_uv_bd")?,
295                    yarn_inv_freq: yarn,
296                    main_inv_freq: yarn,
297                    q_lora_rank: q_lora,
298                    kv_lora_rank: kv_lora,
299                    o_lora_rank: 0,
300                    nope: padded_nope,
301                    rope,
302                    v_dim: padded_hd,
303                    compressor: None,
304                    attn_sink: DevicePtr::NULL,
305                };
306
307                // Dummy attention weights (never read on the MLA path).
308                let o_dummy = QuantizedWeight {
309                    weight: DevicePtr::NULL,
310                    weight_scale: DevicePtr::NULL,
311                    weight_scale_2: 0.0,
312                    input_scale: DevicePtr::NULL,
313                    weight_scale_2_vec: DevicePtr::NULL,
314                };
315                let attn = AttentionWeights {
316                    q_proj: null,
317                    k_proj: null,
318                    v_proj: null,
319                    o_proj: o_dummy,
320                    q_norm: null,
321                    k_norm: null,
322                    q_norm_full: None,
323                    k_norm_full: None,
324                    k_scale: 1.0,
325                    v_scale: 1.0,
326                };
327
328                // ── dense SwiGLU FFN for this sublayer ──
329                let ffn = build_dense_ffn(store, &format!("{lp}.mlps.{s}"), config, gpu)?;
330                let input_norm = dense(store, &format!("{lp}.input_layernorm.{s}.weight"))?;
331                let post_norm = dense(store, &format!("{lp}.post_attention_layernorm.{s}.weight"))?;
332                let kv_dtype = layer_kv_dtypes
333                    .get(global_idx)
334                    .copied()
335                    .unwrap_or(KvCacheDtype::Bf16);
336
337                let mut layer = Qwen3AttentionLayer::new_ungated(
338                    input_norm, attn, post_norm, ffn, global_idx, None, None, None, gpu, kv_dtype,
339                    0, config,
340                )?;
341                layer.set_mla_weights(mla);
342                // Padding widened the head to 256; the scale must remain
343                // 1/sqrt(192), the TRUE qk head width.
344                layer.set_attn_scale_override(attn_scale);
345                // The attention chain strides Q/K/V by `head_dim_override`,
346                // which must be the PADDED width the weights now emit — the
347                // config's 192 would slice every head short.
348                layer.set_dimension_overrides(padded_hd, n_heads, n_heads);
349
350                if s == 0 {
351                    // Sublayer 0 owns the block's shortcut MoE; its output is
352                    // stashed and added at the end of sublayer 1.
353                    let moe = build_shortcut_moe(store, &lp, config, gpu)?;
354                    layer.set_shortcut_moe(moe, carry, CARRY_TOKENS);
355                } else {
356                    layer.set_shortcut_carry_in(carry, CARRY_TOKENS);
357                }
358                layers.push(Box::new(layer));
359            }
360
361            if (l + 1) % 4 == 0 || l == ckpt_layers - 1 {
362                let free = gpu.free_memory().unwrap_or(0);
363                tracing::info!(
364                    "LongCat L{}/{ckpt_layers} — {:.1} GB free",
365                    l + 1,
366                    free as f64 / 1e9
367                );
368            }
369        }
370        Ok(layers)
371    }
372
373    fn load_embedding(
374        &self,
375        store: &WeightStore,
376        _config: &ModelConfig,
377        _gpu: &dyn GpuBackend,
378    ) -> Result<DenseWeight> {
379        dense(store, "model.embed_tokens.weight").context("longcat: embedding")
380    }
381
382    fn load_ngram_embedding(
383        &self,
384        store: &WeightStore,
385        config: &ModelConfig,
386        gpu: &dyn GpuBackend,
387        max_tokens: usize,
388    ) -> Result<Option<crate::layers::ngram_embed::NgramEmbedding>> {
389        ngram::build(store, config, gpu, max_tokens)
390    }
391
392    fn load_final_norm(
393        &self,
394        store: &WeightStore,
395        _config: &ModelConfig,
396        _gpu: &dyn GpuBackend,
397    ) -> Result<DenseWeight> {
398        dense(store, "model.norm.weight").context("longcat: final norm")
399    }
400
401    fn load_lm_head(
402        &self,
403        store: &WeightStore,
404        config: &ModelConfig,
405        _gpu: &dyn GpuBackend,
406    ) -> Result<DenseWeight> {
407        if store.contains("lm_head.weight") {
408            dense(store, "lm_head.weight")
409        } else if config.tie_word_embeddings {
410            dense(store, "model.embed_tokens.weight")
411        } else {
412            anyhow::bail!("longcat: lm_head.weight not found")
413        }
414    }
415
416    fn load_mtp_weights(
417        &self,
418        _store: &WeightStore,
419        _config: &ModelConfig,
420        _gpu: &dyn GpuBackend,
421    ) -> Result<Option<MtpWeights>> {
422        // The checkpoint ships `model.mtp.*`, but the MTP head shape is not
423        // the Qwen-style one Atlas builds. Ignored (matches HF's own
424        // `_keys_to_ignore_on_load_unexpected = [r"model\\.mtp.*"]`).
425        Ok(None)
426    }
427
428    fn load_vision_encoder(
429        &self,
430        _store: &WeightStore,
431        _config: &ModelConfig,
432        _gpu: &dyn GpuBackend,
433    ) -> Result<Option<VisionTower>> {
434        Ok(None)
435    }
436}
437
438/// One sublayer's dense SwiGLU FFN (`mlps.{s}`), NVFP4-quantized at load.
439mod ffn;
440use ffn::{build_dense_ffn, build_shortcut_moe};