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 atlas_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::qwen3_attention::MlaWeights;
41use crate::layers::vision_encoder::VisionEncoder;
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: ATLAS_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("ATLAS_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: ATLAS_NVFP4_MLA=0 — MLA projections stay BF16");
118        }
119        if super::longcat::ffn::bf16_dense_ffn() {
120            tracing::info!("LongCat: ATLAS_LONGCAT_BF16_FFN=1 — per-sublayer dense FFN stays BF16");
121        }
122        let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
123        let quantize_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
124        let stream = gpu.default_stream();
125        let mut yarn_shared = DevicePtr::NULL;
126        let mut layers: Vec<Box<dyn TransformerLayer>> =
127            Vec::with_capacity(config.num_hidden_layers);
128
129        for l in 0..ckpt_layers {
130            let lp = format!("model.layers.{l}");
131            // One carry buffer per checkpoint layer (producer sublayer 0 →
132            // consumer sublayer 1). Allocated per block so two blocks in
133            // flight (chunked prefill) cannot alias.
134            let carry = gpu.alloc(CARRY_TOKENS * h * 2)?;
135
136            for s in 0..2usize {
137                let ap = format!("{lp}.self_attn.{s}");
138                let global_idx = l * 2 + s;
139
140                // ── MLA: name-bound loads + the two LongCat folds ──
141                let wq_a = dense(store, &format!("{ap}.q_a_proj.weight"))?;
142                let wq_b = prep::prep_q_b(
143                    store,
144                    &format!("{ap}.q_b_proj.weight"),
145                    n_heads,
146                    nope,
147                    rope,
148                    q_lora,
149                    scale_q,
150                    padded_hd,
151                    gpu,
152                )?;
153                let q_a_norm = dense(store, &format!("{ap}.q_a_layernorm.weight"))?;
154                let wkv_a = prep::prep_kv_a(
155                    store,
156                    &format!("{ap}.kv_a_proj_with_mqa.weight"),
157                    kv_lora,
158                    rope,
159                    h,
160                    gpu,
161                )?;
162                let kv_a_norm = prep::prep_kv_a_norm(
163                    store,
164                    &format!("{ap}.kv_a_layernorm.weight"),
165                    kv_lora,
166                    scale_kv,
167                    gpu,
168                )?;
169                let wkv_b = prep::prep_kv_b(
170                    store,
171                    &format!("{ap}.kv_b_proj.weight"),
172                    n_heads,
173                    nope,
174                    config.v_head_dim,
175                    rope,
176                    kv_lora,
177                    padded_hd,
178                    gpu,
179                )?;
180                let wo = prep::prep_o_proj(
181                    store,
182                    &format!("{ap}.o_proj.weight"),
183                    h,
184                    n_heads,
185                    config.v_head_dim,
186                    padded_hd,
187                    gpu,
188                )?;
189
190                // ── shared MLA precompute (per-head transpose → absorbed QK
191                //    → block-diagonals), reusing the Mistral phases ──
192                let mut c = mctx::MistralLayerCtx::new(
193                    store, config, gpu, absmax_k, quantize_k, stream, global_idx,
194                );
195                // Everything downstream indexes the PADDED weights.
196                c.hd = padded_hd;
197                c.nope = padded_nope;
198                c.v_dim = padded_hd;
199                c.wq_a_dense = Some(wq_a);
200                c.wq_b = Some(wq_b);
201                c.q_a_norm = Some(q_a_norm);
202                c.wkv_a_dense = Some(wkv_a);
203                c.wkv_a_rope_dense = Some(DenseWeight {
204                    weight: wkv_a.weight.offset(kv_lora * h * 2),
205                });
206                c.wkv_b = Some(wkv_b);
207                c.kv_a_norm = Some(kv_a_norm);
208                c.wq_a_nvfp4 = Some(quantize_to_nvfp4(
209                    &wq_a, q_lora, h, gpu, absmax_k, quantize_k, stream,
210                )?);
211                c.wq_b_nvfp4 = Some(quantize_to_nvfp4(
212                    &wq_b,
213                    n_heads * padded_hd,
214                    q_lora,
215                    gpu,
216                    absmax_k,
217                    quantize_k,
218                    stream,
219                )?);
220                c.wkv_a_nvfp4 = Some(quantize_to_nvfp4(
221                    &wkv_a,
222                    kv_lora + rope,
223                    h,
224                    gpu,
225                    absmax_k,
226                    quantize_k,
227                    stream,
228                )?);
229                phase_per_head::build_per_head_views(&mut c)?;
230                phase_qk_absorbed::build_w_qk_absorbed(&mut c)?;
231                phase_block_diag::build_block_diagonals(&mut c)?;
232                let o_nvfp4 = quantize_to_nvfp4(
233                    &wo,
234                    h,
235                    n_heads * padded_hd,
236                    gpu,
237                    absmax_k,
238                    quantize_k,
239                    stream,
240                )?;
241                let yarn = mctx::ensure_yarn_inv_freq(&mut yarn_shared, config, rope, gpu)?;
242
243                let null = DenseWeight {
244                    weight: DevicePtr::NULL,
245                };
246                let mla = MlaWeights {
247                    wq_a,
248                    wq_a_fp8: None,
249                    wq_a_nvfp4: if disable_nvfp4_mla {
250                        None
251                    } else {
252                        c.wq_a_nvfp4
253                    },
254                    wq_b,
255                    wq_b_fp8: None,
256                    wq_b_nvfp4: if disable_nvfp4_mla {
257                        None
258                    } else {
259                        c.wq_b_nvfp4
260                    },
261                    q_a_norm,
262                    wkv_a,
263                    wkv_a_nvfp4: if disable_nvfp4_mla {
264                        None
265                    } else {
266                        c.wkv_a_nvfp4
267                    },
268                    wkv_b,
269                    kv_a_norm,
270                    wkv_a_rope: c.wkv_a_rope_dense.expect("set above"),
271                    wkv_a_merged: DenseWeight {
272                        weight: wkv_a.weight,
273                    },
274                    wo,
275                    wo_nvfp4: if disable_nvfp4_mla {
276                        None
277                    } else {
278                        Some(o_nvfp4)
279                    },
280                    wo_a: null,
281                    wo_a_nvfp4: None,
282                    wo_b: null,
283                    wo_b_nvfp4: None,
284                    wo_b_fp8: None,
285                    wo_a_fp8: None,
286                    wkv_a_fp8: None,
287                    wq_b_rope: c.wq_b_rope.context("longcat: wq_b_rope")?,
288                    w_uk_t: c.w_uk_t.context("longcat: w_uk_t")?,
289                    w_uv: c.w_uv.context("longcat: w_uv")?,
290                    w_qk_absorbed: c.w_qk_absorbed.context("longcat: w_qk_absorbed")?,
291                    w_uk_block_diag: c.w_uk_block_diag.context("longcat: w_uk_bd")?,
292                    w_uv_block_diag: c.w_uv_block_diag.context("longcat: w_uv_bd")?,
293                    yarn_inv_freq: yarn,
294                    main_inv_freq: yarn,
295                    q_lora_rank: q_lora,
296                    kv_lora_rank: kv_lora,
297                    o_lora_rank: 0,
298                    nope: padded_nope,
299                    rope,
300                    v_dim: padded_hd,
301                    compressor: None,
302                    attn_sink: DevicePtr::NULL,
303                };
304
305                // Dummy attention weights (never read on the MLA path).
306                let o_dummy = QuantizedWeight {
307                    weight: DevicePtr::NULL,
308                    weight_scale: DevicePtr::NULL,
309                    weight_scale_2: 0.0,
310                    input_scale: DevicePtr::NULL,
311                    weight_scale_2_vec: DevicePtr::NULL,
312                };
313                let attn = AttentionWeights {
314                    q_proj: null,
315                    k_proj: null,
316                    v_proj: null,
317                    o_proj: o_dummy,
318                    q_norm: null,
319                    k_norm: null,
320                    q_norm_full: None,
321                    k_norm_full: None,
322                    k_scale: 1.0,
323                    v_scale: 1.0,
324                };
325
326                // ── dense SwiGLU FFN for this sublayer ──
327                let ffn = build_dense_ffn(store, &format!("{lp}.mlps.{s}"), config, gpu)?;
328                let input_norm = dense(store, &format!("{lp}.input_layernorm.{s}.weight"))?;
329                let post_norm = dense(store, &format!("{lp}.post_attention_layernorm.{s}.weight"))?;
330                let kv_dtype = layer_kv_dtypes
331                    .get(global_idx)
332                    .copied()
333                    .unwrap_or(KvCacheDtype::Bf16);
334
335                let mut layer = Qwen3AttentionLayer::new_ungated(
336                    input_norm, attn, post_norm, ffn, global_idx, None, None, None, gpu, kv_dtype,
337                    0, config,
338                )?;
339                layer.set_mla_weights(mla);
340                // Padding widened the head to 256; the scale must remain
341                // 1/sqrt(192), the TRUE qk head width.
342                layer.set_attn_scale_override(attn_scale);
343                // The attention chain strides Q/K/V by `head_dim_override`,
344                // which must be the PADDED width the weights now emit — the
345                // config's 192 would slice every head short.
346                layer.set_dimension_overrides(padded_hd, n_heads, n_heads);
347
348                if s == 0 {
349                    // Sublayer 0 owns the block's shortcut MoE; its output is
350                    // stashed and added at the end of sublayer 1.
351                    let moe = build_shortcut_moe(store, &lp, config, gpu)?;
352                    layer.set_shortcut_moe(moe, carry, CARRY_TOKENS);
353                } else {
354                    layer.set_shortcut_carry_in(carry, CARRY_TOKENS);
355                }
356                layers.push(Box::new(layer));
357            }
358
359            if (l + 1) % 4 == 0 || l == ckpt_layers - 1 {
360                let free = gpu.free_memory().unwrap_or(0);
361                tracing::info!(
362                    "LongCat L{}/{ckpt_layers} — {:.1} GB free",
363                    l + 1,
364                    free as f64 / 1e9
365                );
366            }
367        }
368        Ok(layers)
369    }
370
371    fn load_embedding(
372        &self,
373        store: &WeightStore,
374        _config: &ModelConfig,
375        _gpu: &dyn GpuBackend,
376    ) -> Result<DenseWeight> {
377        dense(store, "model.embed_tokens.weight").context("longcat: embedding")
378    }
379
380    fn load_ngram_embedding(
381        &self,
382        store: &WeightStore,
383        config: &ModelConfig,
384        gpu: &dyn GpuBackend,
385        max_tokens: usize,
386    ) -> Result<Option<crate::layers::ngram_embed::NgramEmbedding>> {
387        ngram::build(store, config, gpu, max_tokens)
388    }
389
390    fn load_final_norm(
391        &self,
392        store: &WeightStore,
393        _config: &ModelConfig,
394        _gpu: &dyn GpuBackend,
395    ) -> Result<DenseWeight> {
396        dense(store, "model.norm.weight").context("longcat: final norm")
397    }
398
399    fn load_lm_head(
400        &self,
401        store: &WeightStore,
402        config: &ModelConfig,
403        _gpu: &dyn GpuBackend,
404    ) -> Result<DenseWeight> {
405        if store.contains("lm_head.weight") {
406            dense(store, "lm_head.weight")
407        } else if config.tie_word_embeddings {
408            dense(store, "model.embed_tokens.weight")
409        } else {
410            anyhow::bail!("longcat: lm_head.weight not found")
411        }
412    }
413
414    fn load_mtp_weights(
415        &self,
416        _store: &WeightStore,
417        _config: &ModelConfig,
418        _gpu: &dyn GpuBackend,
419    ) -> Result<Option<MtpWeights>> {
420        // The checkpoint ships `model.mtp.*`, but the MTP head shape is not
421        // the Qwen-style one Atlas builds. Ignored (matches HF's own
422        // `_keys_to_ignore_on_load_unexpected = [r"model\\.mtp.*"]`).
423        Ok(None)
424    }
425
426    fn load_vision_encoder(
427        &self,
428        _store: &WeightStore,
429        _config: &ModelConfig,
430        _gpu: &dyn GpuBackend,
431    ) -> Result<Option<VisionEncoder>> {
432        Ok(None)
433    }
434}
435
436/// One sublayer's dense SwiGLU FFN (`mlps.{s}`), NVFP4-quantized at load.
437mod ffn;
438use ffn::{build_dense_ffn, build_shortcut_moe};