metrale_model_layers/weight_map/
ssm_qwen35_more.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: MoE loaders for the MiniMax M2 (`block_sparse_moe`, `w1/w2/w3`) and Gemma-4 (`router`, `moe.experts`) layouts.
4//!
5//! Owner: model-layers (weight loading).
6//! Invariants: none beyond the types.
7
8#![allow(unused_imports)]
9
10use anyhow::{Context, Result, bail, ensure};
11use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
12use metrale_model_weights::weights::{WeightDtype, WeightStore};
13
14use super::*;
15
16/// 2026-09-25: Load a MiniMax M2 MoE block.
17///
18/// Differences from `load_moe_no_shared`:
19///   * Prefix `{layer}.block_sparse_moe` instead of `{layer}.mlp`.
20///   * Experts use `w1/w2/w3`: w1 = gate_proj, w2 = down_proj, w3 = up_proj.
21///   * `e_score_correction_bias` goes into `MoeWeights.correction_bias`,
22///     widened to F32 when it is BF16.
23///
24/// Like `load_moe_no_shared` it builds a zero-filled shared expert (packed
25/// codes, scales and `weight_scale_2` all 0), which contributes 0.
26#[allow(clippy::too_many_arguments)]
27pub fn load_moe_minimax(
28    store: &WeightStore,
29    layer_prefix: &str,
30    num_experts: usize,
31    gpu: &dyn GpuBackend,
32    config: &metrale_config::ModelConfig,
33    variant: Nvfp4Variant,
34    absmax_k: metrale_gpu_runtime::gpu::KernelHandle,
35    quantize_k: metrale_gpu_runtime::gpu::KernelHandle,
36    stream: u64,
37) -> Result<MoeWeights> {
38    let p = format!("{layer_prefix}.block_sparse_moe");
39
40    // 2026-09-25: `dense_f32_safe` converts an F32 gate to BF16.
41    let gate = dense_f32_safe(store, &format!("{p}.gate.weight"), gpu)?;
42    // 2026-09-25: The router kernels read the bias as `const float*`, so a BF16
43    // bias is widened to F32 on the host; any other dtype is passed through.
44    let bias_key = format!("{p}.e_score_correction_bias");
45    let bias_t = store.get(&bias_key)?;
46    let correction_bias = if bias_t.dtype == metrale_model_weights::weights::WeightDtype::BF16 {
47        let n = bias_t.num_elements();
48        let mut bf16_buf = vec![0u8; n * 2];
49        gpu.copy_d2h(bias_t.ptr, &mut bf16_buf)?;
50        let mut f32_buf = vec![0u8; n * 4];
51        for i in 0..n {
52            // 2026-09-25: A BF16 value is the high half of an F32; the low half stays zero.
53            f32_buf[i * 4 + 2] = bf16_buf[i * 2];
54            f32_buf[i * 4 + 3] = bf16_buf[i * 2 + 1];
55        }
56        let ptr = gpu.alloc(f32_buf.len())?;
57        gpu.copy_h2d(&f32_buf, ptr)?;
58        DenseWeight { weight: ptr }
59    } else {
60        dense(store, &bias_key)?
61    };
62
63    // 2026-09-25: The shared-expert buffers round up and hold at least 1 byte,
64    // so a hidden or intermediate size below the group size of 16 does not
65    // request a 0-byte allocation. For sizes that divide evenly the byte counts
66    // equal `load_moe_no_shared`'s.
67    let h = config.hidden_size;
68    let inter = config.moe_intermediate_size;
69    let group_size = 16usize;
70    let ceil_div = |n: usize, d: usize| -> usize { n.div_ceil(d) };
71    let gu_packed_bytes = (inter * h).div_ceil(2).max(1);
72    let gu_scale_bytes = (inter * ceil_div(h, group_size)).max(1);
73    let d_packed_bytes = (h * inter).div_ceil(2).max(1);
74    let d_scale_bytes = (h * ceil_div(inter, group_size)).max(1);
75
76    let alloc_zero = |size: usize| -> Result<DevicePtr> {
77        let sz = size.max(1);
78        let ptr = gpu.alloc(sz)?;
79        gpu.memset(ptr, 0, sz)?;
80        Ok(ptr)
81    };
82    let mk_zero_quant = |packed_sz: usize, scale_sz: usize| -> Result<QuantizedWeight> {
83        Ok(QuantizedWeight {
84            weight: alloc_zero(packed_sz)?,
85            weight_scale: alloc_zero(scale_sz)?,
86            weight_scale_2: 0.0,
87            input_scale: DevicePtr::NULL,
88            weight_scale_2_vec: DevicePtr::NULL,
89            act: metrale_config::Nvfp4Act::Unstamped,
90        })
91    };
92    let shared_expert = ExpertWeight {
93        gate_proj: mk_zero_quant(gu_packed_bytes, gu_scale_bytes)?,
94        up_proj: mk_zero_quant(gu_packed_bytes, gu_scale_bytes)?,
95        down_proj: mk_zero_quant(d_packed_bytes, d_scale_bytes)?,
96    };
97    let shared_expert_gate = DenseWeight {
98        weight: alloc_zero(h * 2)?,
99    };
100
101    // 2026-09-25: Per expert key:
102    //   * pre-quantized NVFP4 → `quantized_auto`, which takes the store's pointers;
103    //   * FP8 with `weight_scale_inv` → `dequant_fp8_blockscaled_to_bf16`, then
104    //     `quantize_to_nvfp4`;
105    //   * anything else → `dense_auto`, then `quantize_to_nvfp4`.
106    // `(n, k)` is the on-disk shape: w1 and w3 are `[moe_intermediate, hidden]`,
107    // w2 is `[hidden, moe_intermediate]`.
108    let quant = |ep: &str, n: usize, k: usize| -> Result<QuantizedWeight> {
109        // 2026-09-25: Pre-quantized means a `weight_packed` key under an NVFP4
110        // variant, or a `weight_scale_2` key under `Standard`.
111        let is_compressed = store.contains(&format!("{ep}.weight_packed"));
112        let is_standard_nvfp4 = matches!(variant, Nvfp4Variant::Standard)
113            && store.contains(&format!("{ep}.weight_scale_2"));
114        if (matches!(
115            variant,
116            Nvfp4Variant::Standard | Nvfp4Variant::CompressedTensors
117        ) && is_compressed)
118            || is_standard_nvfp4
119        {
120            return quantized_auto(store, ep, gpu, variant);
121        }
122        // 2026-09-25: After quantizing, the BF16 dequant is freed, and so are an
123        // FP8 source weight and its `weight_scale_inv`. The store keeps the freed
124        // pointers, and nothing may read these keys afterwards.
125        let wkey = format!("{ep}.weight");
126        let scale_key = format!("{ep}.weight_scale_inv");
127        let (src_ptr, src_is_fp8) = {
128            let t = store.get(&wkey)?;
129            (
130                t.ptr,
131                t.dtype == metrale_model_weights::weights::WeightDtype::FP8E4M3,
132            )
133        };
134        let scale_ptr = if store.contains(&scale_key) {
135            Some(store.get(&scale_key)?.ptr)
136        } else {
137            None
138        };
139        let (dense_w, owned_bf16, need_free_src) = if scale_ptr.is_some() {
140            (dequant_fp8_blockscaled_to_bf16(store, ep, gpu)?, true, true)
141        } else {
142            (dense_auto(store, &wkey, gpu)?, src_is_fp8, src_is_fp8)
143        };
144        let nvfp4 = quantize_to_nvfp4(&dense_w, n, k, gpu, absmax_k, quantize_k, stream)?;
145        if owned_bf16 {
146            gpu.free(dense_w.weight)?;
147        }
148        if need_free_src {
149            gpu.free(src_ptr)?;
150            if let Some(sp) = scale_ptr {
151                gpu.free(sp)?;
152            }
153        }
154        Ok(nvfp4)
155    };
156    let inter_moe = config.moe_intermediate_size;
157    let mut experts = Vec::with_capacity(num_experts);
158    for e in 0..num_experts {
159        if config.is_local_expert(e) {
160            experts.push(ExpertWeight {
161                gate_proj: quant(&format!("{p}.experts.{e}.w1"), inter_moe, h)?,
162                down_proj: quant(&format!("{p}.experts.{e}.w2"), h, inter_moe)?,
163                up_proj: quant(&format!("{p}.experts.{e}.w3"), inter_moe, h)?,
164            });
165        } else {
166            experts.push(ExpertWeight::null());
167        }
168    }
169
170    Ok(MoeWeights {
171        gate,
172        shared_expert,
173        shared_expert_gate,
174        experts,
175        router_pre_norm: None,
176        correction_bias: Some(correction_bias),
177    })
178}
179
180/// 2026-09-25: Load a Gemma-4 MoE block.
181///
182/// - The router is `{lp}.router.proj.weight`, kept unfused as the gate.
183/// - `router.scale` `[H]` becomes `router_pre_norm` (`scale * hidden_size^-0.5`).
184/// - `router.per_expert_scale` `[E]` is folded into each expert's down_proj
185///   `weight_scale_2`.
186/// - Experts are `{lp}.moe.experts.{e}`; the shared expert is zero-filled.
187/// - The gate and both router scales are read as BF16 without a dtype check.
188pub fn load_moe_gemma4(
189    store: &WeightStore,
190    layer_prefix: &str,
191    num_experts: usize,
192    gpu: &dyn GpuBackend,
193    config: &metrale_config::ModelConfig,
194    variant: Nvfp4Variant,
195    qctx: QuantizeCtx,
196) -> Result<MoeWeights> {
197    let h = config.hidden_size;
198    let inter = config.moe_intermediate_size;
199
200    let gate_key = format!("{layer_prefix}.router.proj.weight");
201    let scale_key = format!("{layer_prefix}.router.scale");
202    let per_exp_key = format!("{layer_prefix}.router.per_expert_scale");
203
204    let gate_wt = store.get(&gate_key)?;
205    let gate_bytes = num_experts * h * 2;
206    let mut gate_buf = vec![0u8; gate_bytes];
207    gpu.copy_d2h(gate_wt.ptr, &mut gate_buf)?;
208
209    // 2026-09-25: `MoeLayer::router_input` runs `rms_norm` with this weight
210    // before the gate GEMV, so the gate sees `rms_norm(x) * scale * hidden_size^-0.5`.
211    let router_pre_norm = if store.contains(&scale_key) {
212        let scale_wt = store.get(&scale_key)?;
213        let mut scale_buf = vec![0u8; h * 2];
214        gpu.copy_d2h(scale_wt.ptr, &mut scale_buf)?;
215        let scalar_root = 1.0f32 / (h as f32).sqrt();
216        for dim in 0..h {
217            let bits = u16::from_le_bytes([scale_buf[dim * 2], scale_buf[dim * 2 + 1]]);
218            let f = f32::from_bits((bits as u32) << 16) * scalar_root;
219            let out = (f.to_bits() >> 16) as u16;
220            scale_buf[dim * 2] = out as u8;
221            scale_buf[dim * 2 + 1] = (out >> 8) as u8;
222        }
223        let pre_norm_ptr = gpu.alloc(h * 2)?;
224        gpu.copy_h2d(&scale_buf, pre_norm_ptr)?;
225        tracing::info!("Gemma-4 MoE: router pre-norm weight = scale * hidden_size^(-0.5)");
226        Some(DenseWeight {
227            weight: pre_norm_ptr,
228        })
229    } else {
230        None
231    };
232
233    let gate_ptr = gpu.alloc(gate_bytes)?;
234    gpu.copy_h2d(&gate_buf, gate_ptr)?;
235    let gate = DenseWeight { weight: gate_ptr };
236
237    let group_size = 16usize;
238    let gu_packed = inter * h / 2;
239    let gu_scale = inter * (h / group_size);
240    let d_packed = h * inter / 2;
241    let d_scale = h * (inter / group_size);
242
243    let alloc_zero = |size: usize| -> Result<DevicePtr> {
244        let ptr = gpu.alloc(size)?;
245        gpu.memset(ptr, 0, size)?;
246        Ok(ptr)
247    };
248    let mk_zero = |p: usize, s: usize| -> Result<QuantizedWeight> {
249        Ok(QuantizedWeight {
250            weight: alloc_zero(p)?,
251            weight_scale: alloc_zero(s)?,
252            weight_scale_2: 0.0,
253            input_scale: DevicePtr::NULL,
254            weight_scale_2_vec: DevicePtr::NULL,
255            act: metrale_config::Nvfp4Act::Unstamped,
256        })
257    };
258    let shared_expert = ExpertWeight {
259        gate_proj: mk_zero(gu_packed, gu_scale)?,
260        up_proj: mk_zero(gu_packed, gu_scale)?,
261        down_proj: mk_zero(d_packed, d_scale)?,
262    };
263    let shared_expert_gate = DenseWeight {
264        weight: alloc_zero(h * 2)?,
265    };
266
267    // 2026-09-25: 1.0 for every expert when `router.per_expert_scale` is absent.
268    let per_expert_scales: Vec<f32> = if store.contains(&per_exp_key) {
269        let per_exp_wt = store.get(&per_exp_key)?;
270        let mut buf = vec![0u8; num_experts * 2];
271        gpu.copy_d2h(per_exp_wt.ptr, &mut buf)?;
272        (0..num_experts)
273            .map(|e| {
274                let bits = u16::from_le_bytes([buf[e * 2], buf[e * 2 + 1]]);
275                f32::from_bits((bits as u32) << 16)
276            })
277            .collect()
278    } else {
279        vec![1.0f32; num_experts]
280    };
281
282    let mut experts = Vec::with_capacity(num_experts);
283    for e in 0..num_experts {
284        let ep = format!("{layer_prefix}.moe.experts.{e}");
285        if config.is_local_expert(e) {
286            let mut down = quantized_any(
287                store,
288                &format!("{ep}.down_proj"),
289                h,
290                inter,
291                gpu,
292                variant,
293                qctx,
294            )?;
295            // 2026-09-25: `weight_scale_2` multiplies every dequantized value of
296            // down_proj, so this scales expert e's output by `per_expert_scales[e]`.
297            down.weight_scale_2 *= per_expert_scales[e];
298            experts.push(ExpertWeight {
299                gate_proj: quantized_any(
300                    store,
301                    &format!("{ep}.gate_proj"),
302                    inter,
303                    h,
304                    gpu,
305                    variant,
306                    qctx,
307                )?,
308                up_proj: quantized_any(
309                    store,
310                    &format!("{ep}.up_proj"),
311                    inter,
312                    h,
313                    gpu,
314                    variant,
315                    qctx,
316                )?,
317                down_proj: down,
318            });
319        } else {
320            experts.push(ExpertWeight::null());
321        }
322    }
323
324    Ok(MoeWeights {
325        gate,
326        shared_expert,
327        shared_expert_gate,
328        experts,
329        router_pre_norm,
330        correction_bias: None,
331    })
332}