metrale_model_layers/weight_map/
moe.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Weight structs for MoE layers, whole transformer layers and the MTP head.
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: Weights of one MoE block.
17pub struct MoeWeights {
18    /// 2026-09-25: Router gate, BF16, one row of `hidden_size` per router logit.
19    pub gate: DenseWeight,
20    pub shared_expert: ExpertWeight,
21    /// 2026-09-25: Shared-expert gate `[1, hidden_size]` BF16: the shared
22    /// expert's output is scaled by `sigmoid(dot(input, gate))`, or by 1 when
23    /// this pointer is NULL.
24    pub shared_expert_gate: DenseWeight,
25    /// 2026-09-25: One entry per expert id; experts this rank does not hold are
26    /// `ExpertWeight::null()`.
27    pub experts: Vec<ExpertWeight>,
28    /// 2026-09-25: RMSNorm weight applied to the router input before the gate
29    /// GEMV (`MoeLayer::router_input`). The Gemma-4 loader sets it to the BF16
30    /// vector `router.scale * hidden_size^-0.5`; `None` feeds the gate the
31    /// input unchanged.
32    pub router_pre_norm: Option<DenseWeight>,
33    /// 2026-09-25: Per-expert F32 bias `[num_experts]` added to the router
34    /// scores for top-k selection.
35    pub correction_bias: Option<DenseWeight>,
36}
37
38impl MoeWeights {
39    /// 2026-09-25: `num_experts` experts with NULL pointers, for tests.
40    #[cfg(test)]
41    pub fn empty(num_experts: usize) -> Self {
42        let null_dense = DenseWeight {
43            weight: DevicePtr::NULL,
44        };
45        let null_quant = QuantizedWeight {
46            weight: DevicePtr::NULL,
47            weight_scale: DevicePtr::NULL,
48            weight_scale_2: 1.0,
49            input_scale: DevicePtr::NULL,
50            weight_scale_2_vec: DevicePtr::NULL,
51            act: metrale_config::Nvfp4Act::Unstamped,
52        };
53        let null_expert = ExpertWeight {
54            gate_proj: null_quant,
55            up_proj: null_quant,
56            down_proj: null_quant,
57        };
58        Self {
59            gate: null_dense,
60            shared_expert: null_expert,
61            shared_expert_gate: null_dense,
62            experts: vec![null_expert; num_experts],
63            router_pre_norm: None,
64            correction_bias: None,
65        }
66    }
67}
68
69/// 2026-09-25: All weights of one attention or linear-attention layer.
70pub enum LayerWeights {
71    FullAttention {
72        input_norm: DenseWeight,
73        attn: AttentionWeights,
74        post_attn_norm: DenseWeight,
75        moe: MoeWeights,
76    },
77    LinearAttention {
78        input_norm: DenseWeight,
79        ssm: SsmWeights,
80        post_attn_norm: DenseWeight,
81        moe: MoeWeights,
82    },
83}
84
85/// 2026-09-25: BF16 gate, up and down projections of one expert.
86#[derive(Debug, Clone, Copy)]
87pub struct DenseExpertWeight {
88    pub gate_proj: DenseWeight,
89    pub up_proj: DenseWeight,
90    pub down_proj: DenseWeight,
91}
92
93/// 2026-09-25: MTP (multi-token prediction) head weights: one decoder layer
94/// plus the concat projection `fc`.
95///
96/// Every `DenseWeight` here is BF16 (`load_mtp` dequantizes FP8 or NVFP4
97/// projections). `MtpHead::new` quantizes the projections when it builds the head.
98pub struct MtpWeights {
99    /// 2026-09-25: RMSNorm weight for the token embedding before the concat.
100    pub pre_fc_norm_embedding: DenseWeight,
101    /// 2026-09-25: RMSNorm weight for the target hidden state before the concat.
102    pub pre_fc_norm_hidden: DenseWeight,
103    /// 2026-09-25: Concat projection `[hidden_size, 2*hidden_size]` BF16.
104    pub fc: DenseWeight,
105    /// 2026-09-25: RMSNorm weight before attention.
106    pub input_layernorm: DenseWeight,
107    /// 2026-09-25: Attention projections, BF16.
108    pub q_proj: DenseWeight,
109    pub k_proj: DenseWeight,
110    pub v_proj: DenseWeight,
111    pub o_proj: DenseWeight,
112    pub q_norm: DenseWeight,
113    pub k_norm: DenseWeight,
114    /// 2026-09-25: RMSNorm weight after attention (fused with the residual add).
115    pub post_attn_layernorm: DenseWeight,
116    /// 2026-09-25: MoE router gate, BF16. NULL when `dense_ffn` is `Some`.
117    pub moe_gate: DenseWeight,
118    /// 2026-09-25: Shared expert, BF16. NULL fields when `dense_ffn` is `Some`.
119    pub shared_expert: DenseExpertWeight,
120    /// 2026-09-25: Shared-expert gate `[1, hidden_size]` BF16. NULL when `dense_ffn` is `Some`.
121    pub shared_expert_gate: DenseWeight,
122    /// 2026-09-25: One BF16 entry per expert. Empty when `dense_ffn` is `Some`.
123    pub experts: Vec<DenseExpertWeight>,
124    /// 2026-09-25: Dense FFN (`gate_proj`, `up_proj`, `down_proj`) of an MTP
125    /// head without a router. When `Some`, `MtpHead::new` builds no MoE and
126    /// the MoE fields above are unused.
127    pub dense_ffn: Option<DenseExpertWeight>,
128    /// 2026-09-25: Final RMSNorm weight.
129    pub norm: DenseWeight,
130    /// 2026-09-25: The routed and shared experts as the checkpoint's own FP8
131    /// block-scaled tables (`mtp.layers.0.mlp.experts.{e}.*_proj.{weight,weight_scale_inv}`).
132    /// When `Some`, `experts` and `shared_expert` above are fresh BF16 dequants
133    /// of the same tensors; a head that builds its MoE from these tables calls
134    /// [`Self::release_bf16_expert_dequants`]. `None` when the experts are not
135    /// FP8 on disk, are stacked, or the variant is `Bf16Raw`.
136    pub fp8_experts: Option<MtpFp8Experts>,
137}
138
139/// 2026-09-25: Native FP8 block-scaled MTP MoE tables: `experts[e]` is routed
140/// expert `e` (all `num_experts` are loaded), plus the shared expert. The FP8
141/// bytes belong to the weight store; only the FP32 block scales are owned
142/// allocations.
143pub struct MtpFp8Experts {
144    pub experts: Vec<Fp8ExpertWeight>,
145    pub shared_expert: Fp8ExpertWeight,
146}
147
148impl MtpWeights {
149    /// 2026-09-25: Free the BF16 dequants of the routed and shared experts once
150    /// an FP8 `MoeLayer` serves them. Returns an error when
151    /// [`Self::fp8_experts`] is `None`. `load_mtp` attaches the tables only for
152    /// unstacked FP8 experts and a variant other than `Bf16Raw`; every expert
153    /// entry is then a fresh dequant allocation, never a store pointer. Leaves
154    /// `experts` empty and `shared_expert` NULL.
155    pub fn release_bf16_expert_dequants(&mut self, gpu: &dyn GpuBackend) -> Result<()> {
156        ensure!(
157            self.fp8_experts.is_some(),
158            "release_bf16_expert_dequants: no FP8 tables — the BF16 experts are the only copy"
159        );
160        let mut released = 0usize;
161        for de in self.experts.drain(..) {
162            for w in [de.gate_proj, de.up_proj, de.down_proj] {
163                gpu.free(w.weight)?;
164                released += 1;
165            }
166        }
167        let null = DenseWeight {
168            weight: DevicePtr::NULL,
169        };
170        let shared = std::mem::replace(
171            &mut self.shared_expert,
172            DenseExpertWeight {
173                gate_proj: null,
174                up_proj: null,
175                down_proj: null,
176            },
177        );
178        for w in [shared.gate_proj, shared.up_proj, shared.down_proj] {
179            if !w.weight.is_null() {
180                gpu.free(w.weight)?;
181                released += 1;
182            }
183        }
184        tracing::info!(
185            "MTP head: released {released} BF16 expert dequants (native FP8 tables serve the MoE)"
186        );
187        Ok(())
188    }
189}