metrale_model_arch/
nemotron_moe.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Nemotron-H standalone MoE FFN layer, in two shapes:
4//!   - direct (`moe_latent_size == 0`): the routed experts work on the hidden
5//!     vector;
6//!   - LatentMoE (`moe_latent_size > 0`): the routed experts work in a latent
7//!     space of `moe_latent_size`, with the fc1 / fc2 latent projections
8//!     between hidden and latent.
9//!
10//! Forward: RMS norm, gate, sigmoid top-k routing, (fc1 if latent), routed
11//! up, relu² and down, weighted sum, (fc2 if latent), plus the shared expert
12//! (up, relu², down), summed and added to `hidden`. Experts are selected on
13//! the device through pointer tables; routing reads nothing back to the host.
14//!
15//! Owner: model-arch (Nemotron-H).
16//! Invariants:
17//! - `new` refuses a `top_k` outside `1..=num_experts` or over the routing
18//!   kernels' bounds (`MOE_TOPK_SIGMOID_MAX_TOP_K`,
19//!   `MOE_TOPK_SIGMOID_MAX_EXPERTS`).
20
21use anyhow::Result;
22use metrale_cache::kv_cache::PagedKvCache;
23use metrale_config::ModelConfig;
24use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
25
26use metrale_model_layers::layer::{EmptyLayerState, ForwardContext, LayerState, TransformerLayer};
27use metrale_model_layers::layer::{
28    LayerAuxState, LayerCapabilities, LayerGraphHooks, LayerSplitPrefill, LayerWeightSetup,
29    LayerWriteOnAccept,
30};
31use metrale_model_layers::layers::ops;
32use metrale_model_layers::weight_map::{DenseWeight, NemotronMoeWeights, QuantizedWeight};
33
34/// 2026-09-25: Device-side pointer table for one projection across all experts.
35struct ExpertPtrTable {
36    packed_ptrs: DevicePtr,
37    scale_ptrs: DevicePtr,
38    scale2_vals: DevicePtr,
39}
40
41/// 2026-09-25: Nemotron-H standalone MoE FFN layer.
42pub struct NemotronMoeLayer {
43    weights: NemotronMoeWeights,
44    input_norm: DenseWeight,
45    /// 2026-09-25: LatentMoE dimension (0 = direct, >0 = latent).
46    moe_latent_size: usize,
47    /// 2026-09-25: Routed expert intermediate size for this layer: the
48    /// constructor's `moe_inter`, or `config.moe_intermediate_size` when it is 0.
49    moe_inter: usize,
50    /// 2026-09-25: Experts per token for this layer: the constructor's `top_k`,
51    /// or `config.num_experts_per_tok` when it is 0.
52    top_k: usize,
53    rms_norm_residual_k: KernelHandle,
54    dense_gemv_k: KernelHandle,
55    topk_sigmoid_k: KernelHandle,
56    moe_expert_gemv_k: KernelHandle,
57    w4a16_gemv_k: KernelHandle,
58    /// 2026-09-25: Single-warp `w4a16_gemv_sw`; `KernelHandle(0)` when the
59    /// kernel is missing, and then decode uses the base GEMV.
60    w4a16_gemv_sw_k: KernelHandle,
61    /// 2026-09-25: Native-FP8 GEMV for the shared expert's up_proj and
62    /// down_proj (`NemotronMoeWeights::shared_up_fp8`, `shared_down_fp8`). 0
63    /// when unavailable.
64    w8a16_gemv_k: KernelHandle,
65    /// 2026-09-25: Native-FP8 prefill GEMM for the shared expert. 0 when unavailable.
66    w8a16_gemm_k: KernelHandle,
67    w8a16_gemm_pipelined_k: KernelHandle,
68    relu2_down_shared_k: KernelHandle,
69    weighted_sum_scale_k: KernelHandle,
70    residual_add_k: KernelHandle,
71    dense_gemm_k: KernelHandle,
72    /// 2026-09-25: `dense_gemm_bf16_pipelined`, which `dense_gemm_prefill`
73    /// prefers over `dense_gemm_k`; 0 when the target lacks it.
74    dense_gemm_pipelined_k: KernelHandle,
75    w4a16_gemm_k: KernelHandle,
76    topk_sigmoid_batched_k: KernelHandle,
77    moe_up_prefill_k: KernelHandle,
78    moe_relu2_down_prefill_k: KernelHandle,
79    moe_weighted_sum_prefill_k: KernelHandle,
80    moe_sort_k: KernelHandle,
81    moe_grouped_gemm_k: KernelHandle,
82    moe_relu2_elementwise_k: KernelHandle,
83    moe_grouped_gemm_relu2_k: KernelHandle,
84    moe_w4a4_grouped_k: KernelHandle,
85    moe_unpermute_reduce_k: KernelHandle,
86    moe_grouped_gemm_n128_k: KernelHandle,
87    up_ptrs: ExpertPtrTable,
88    down_ptrs: ExpertPtrTable,
89    // 2026-09-25: Transposed expert pointer tables, for `moe_grouped_gemm_n128_k`.
90    up_ptrs_t: Option<ExpertPtrTable>,
91    down_ptrs_t: Option<ExpertPtrTable>,
92    // 2026-09-25: Transposed shared-expert weights, for the `w4a16_gemm_t` prefill arms.
93    shared_up_t: Option<QuantizedWeight>,
94    shared_down_t: Option<QuantizedWeight>,
95    // 2026-09-25: Pre-dequantized FP8 E4M3 [N, K] copies of the shared-expert
96    // projections, for `fp8_gemm_t_m128_mfast`.
97    shared_up_pd_fp8: Option<DevicePtr>,
98    shared_down_pd_fp8: Option<DevicePtr>,
99    // 2026-09-25: FP8 E4M3 copies of the BF16 latent projections, so prefill
100    // runs `fp8_gemm_t_m128_mfast` for fc1 / fc2 instead of `dense_gemm_prefill`.
101    fc1_pd_fp8: Option<DevicePtr>,
102    fc2_pd_fp8: Option<DevicePtr>,
103    // 2026-09-25: The shared expert's transposed-NVFP4, pre-dequant-FP8 and W4A4
104    // prefill kernels.
105    w4a16_gemm_t_k: KernelHandle,
106    w4a16_gemm_t_m128_k: KernelHandle,
107    fp8_gemm_m128_k: KernelHandle,
108    w4a4_gemm_k: KernelHandle,
109    quantize_nvfp4_k: KernelHandle,
110}
111
112impl NemotronMoeLayer {
113    pub fn new(
114        weights: NemotronMoeWeights,
115        input_norm: DenseWeight,
116        config: &ModelConfig,
117        gpu: &dyn GpuBackend,
118        moe_inter: usize,
119        top_k: usize,
120    ) -> Result<Self> {
121        let up_ptrs = build_ptr_table(&weights.experts, |e| &e.up_proj, gpu)?;
122        let down_ptrs = build_ptr_table(&weights.experts, |e| &e.down_proj, gpu)?;
123        let moe_inter = if moe_inter > 0 {
124            moe_inter
125        } else {
126            config.moe_intermediate_size
127        };
128        let top_k = if top_k > 0 {
129            top_k
130        } else {
131            config.num_experts_per_tok
132        };
133        // 2026-09-25: The loader passes each layer's own `top_k`
134        // (`num_experts_per_tok_for`), so the routing kernels' bounds are
135        // checked per MoE layer.
136        let num_experts = weights.experts.len();
137        anyhow::ensure!(
138            top_k > 0
139                && top_k <= num_experts
140                && top_k <= metrale_model_layers::layers::ops::MOE_TOPK_SIGMOID_MAX_TOP_K
141                && num_experts <= metrale_model_layers::layers::ops::MOE_TOPK_SIGMOID_MAX_EXPERTS,
142            "Nemotron MoE config invalid: top_k={} must be in 1..={} and within \
143             the routing kernels' bounds (top_k max {}, num_experts={} max {})",
144            top_k,
145            num_experts,
146            metrale_model_layers::layers::ops::MOE_TOPK_SIGMOID_MAX_TOP_K,
147            num_experts,
148            metrale_model_layers::layers::ops::MOE_TOPK_SIGMOID_MAX_EXPERTS,
149        );
150
151        Ok(Self {
152            weights,
153            input_norm,
154            moe_latent_size: config.moe_latent_size,
155            moe_inter,
156            top_k,
157            rms_norm_residual_k: gpu.kernel("norm", "rms_norm_residual")?,
158            dense_gemv_k: gpu.kernel("gemv", "dense_gemv_bf16")?,
159            topk_sigmoid_k: gpu.kernel("moe_topk_sig", "moe_topk_sigmoid")?,
160            moe_expert_gemv_k: gpu.kernel("moe_expert_gemv", "moe_expert_gemv")?,
161            w4a16_gemv_k: gpu.kernel("w4a16_gemv", "w4a16_gemv")?,
162            w4a16_gemv_sw_k: metrale_model_layers::layers::try_kernel(
163                gpu,
164                "w4a16_gemv",
165                "w4a16_gemv_sw",
166            ),
167            w8a16_gemv_k: metrale_model_layers::layers::try_kernel(gpu, "w8a16_gemv", "w8a16_gemv"),
168            w8a16_gemm_k: metrale_model_layers::layers::try_kernel(gpu, "w8a16_gemm", "w8a16_gemm"),
169            w8a16_gemm_pipelined_k: metrale_model_layers::layers::try_kernel(
170                gpu,
171                "w8a16_gemm_pipelined",
172                "w8a16_gemm_pipelined",
173            ),
174            relu2_down_shared_k: gpu.kernel("moe_relu2_fused", "moe_expert_relu2_down_shared")?,
175            weighted_sum_scale_k: gpu.kernel("relu2", "moe_weighted_sum_scale")?,
176            residual_add_k: gpu.kernel("residual_add", "bf16_residual_add")?,
177            dense_gemm_k: gpu.kernel("gemm", "dense_gemm_bf16")?,
178            dense_gemm_pipelined_k: metrale_model_layers::layers::try_kernel(
179                gpu,
180                "gemm",
181                "dense_gemm_bf16_pipelined",
182            ),
183            w4a16_gemm_k: gpu.kernel("w4a16", "w4a16_gemm")?,
184            topk_sigmoid_batched_k: metrale_model_layers::layers::try_kernel(
185                gpu,
186                "nemotron_moe_prefill",
187                "nemotron_moe_topk_sigmoid_batched",
188            ),
189            moe_up_prefill_k: metrale_model_layers::layers::try_kernel(
190                gpu,
191                "nemotron_moe_prefill",
192                "nemotron_moe_up_prefill",
193            ),
194            moe_relu2_down_prefill_k: metrale_model_layers::layers::try_kernel(
195                gpu,
196                "nemotron_moe_prefill",
197                "nemotron_moe_relu2_down_prefill",
198            ),
199            moe_weighted_sum_prefill_k: metrale_model_layers::layers::try_kernel(
200                gpu,
201                "nemotron_moe_prefill",
202                "nemotron_moe_weighted_sum_prefill",
203            ),
204            moe_sort_k: metrale_model_layers::layers::try_kernel(gpu, "moe", "moe_sort_by_expert"),
205            moe_grouped_gemm_k: metrale_model_layers::layers::try_kernel(
206                gpu,
207                "moe_w4a16",
208                "moe_w4a16_grouped_gemm_ptrtable",
209            ),
210            moe_relu2_elementwise_k: metrale_model_layers::layers::try_kernel(
211                gpu,
212                "relu2",
213                "relu_squared_inplace",
214            ),
215            moe_grouped_gemm_relu2_k: metrale_model_layers::layers::try_kernel(
216                gpu,
217                "moe_w4a16",
218                "moe_w4a16_grouped_gemm_ptrtable_relu2",
219            ),
220            moe_w4a4_grouped_k: metrale_model_layers::layers::try_kernel(
221                gpu,
222                "moe_w4a4",
223                "moe_w4a4_grouped_gemm_relu2",
224            ),
225            moe_unpermute_reduce_k: metrale_model_layers::layers::try_kernel(
226                gpu,
227                "moe",
228                "moe_unpermute_reduce_indexed",
229            ),
230            moe_grouped_gemm_n128_k: metrale_model_layers::layers::try_kernel(
231                gpu,
232                "moe_w4a16",
233                "moe_w4a16_grouped_gemm_ptrtable_t",
234            ),
235            up_ptrs,
236            down_ptrs,
237            up_ptrs_t: None,
238            down_ptrs_t: None,
239            shared_up_t: None,
240            shared_down_t: None,
241            shared_up_pd_fp8: None,
242            shared_down_pd_fp8: None,
243            fc1_pd_fp8: None,
244            fc2_pd_fp8: None,
245            w4a16_gemm_t_k: metrale_model_layers::layers::try_kernel(gpu, "w4a16", "w4a16_gemm_t"),
246            w4a16_gemm_t_m128_k: metrale_model_layers::layers::try_kernel(
247                gpu,
248                "w4a16",
249                "w4a16_gemm_t_m128",
250            ),
251            fp8_gemm_m128_k: metrale_model_layers::layers::try_kernel(
252                gpu,
253                "w4a16",
254                "fp8_gemm_t_m128_mfast",
255            ),
256            w4a4_gemm_k: metrale_model_layers::layers::try_kernel(gpu, "w4a4", "w4a4_gemm_mfast"),
257            quantize_nvfp4_k: metrale_model_layers::layers::try_kernel(
258                gpu,
259                "quantize_nvfp4",
260                "quantize_bf16_to_nvfp4",
261            ),
262        })
263    }
264}
265
266mod decode_helpers;
267mod prefill_fallback;
268mod prefill_shared_up;
269mod prefill_sorted;
270mod prefill_weights;
271mod ptr_tables;
272mod shared_e4m3;
273pub use shared_e4m3::{set_shared_expert_e4m3_from_cli, shared_expert_e4m3};
274
275use prefill_sorted::SortedPrefillCtx;
276use ptr_tables::{build_ptr_table, build_ptr_table_from_weights};
277
278impl TransformerLayer for NemotronMoeLayer {
279    fn decode(
280        &self,
281        hidden: DevicePtr,
282        residual: DevicePtr,
283        _state: &mut dyn LayerState,
284        _kv_cache: &mut PagedKvCache,
285        _seq_len: usize,
286        _block_table: &mut Vec<u32>,
287        _disk_block_ids: &mut Vec<u32>,
288        _disk_last_offloaded_per_layer: &mut Vec<u32>,
289        ctx: &ForwardContext,
290        stream: u64,
291    ) -> Result<()> {
292        self.decode_inner(hidden, residual, ctx, stream)
293    }
294
295    /// 2026-09-25: Prefill over all tokens: batched RMS norm, gate GEMM, shared
296    /// expert up GEMM and (LatentMoE) fc1 GEMM, then the sorted grouped-GEMM
297    /// expert path (`prefill_sorted.rs`) or the per-token fallback
298    /// (`prefill_fallback.rs`).
299    #[allow(clippy::overly_complex_bool_expr)]
300    fn prefill(
301        &self,
302        hidden: DevicePtr,
303        residual: DevicePtr,
304        num_tokens: usize,
305        _state: &mut dyn LayerState,
306        _kv_cache: &mut PagedKvCache,
307        _seq_len_start: usize,
308        _block_table: &mut Vec<u32>,
309        _disk_block_ids: &mut Vec<u32>,
310        _disk_last_offloaded_per_layer: &mut Vec<u32>,
311        _kv_write_start: usize,
312        ctx: &ForwardContext,
313        stream: u64,
314    ) -> Result<()> {
315        let h = ctx.config.hidden_size;
316        let inter = self.moe_inter as u32;
317        let shared_inter = ctx.config.shared_expert_intermediate_size as u32;
318        let num_experts = ctx.config.num_experts as u32;
319        let top_k = self.top_k as u32;
320        let eps = ctx.config.rms_norm_eps as f32;
321        let scale = ctx.config.routed_scaling_factor as f32;
322        let n = num_tokens as u32;
323
324        let normed = ctx.buffers.norm_output();
325        ops::rms_norm_residual(
326            ctx.gpu,
327            self.rms_norm_residual_k,
328            hidden,
329            &self.input_norm,
330            normed,
331            residual,
332            n,
333            h as u32,
334            eps,
335            stream,
336        )?;
337
338        let gate_logits = ctx.buffers.gate_logits();
339        self.dense_gemm_prefill(
340            ctx.gpu,
341            normed,
342            &self.weights.gate,
343            gate_logits,
344            n,
345            num_experts,
346            h as u32,
347            stream,
348        )?;
349
350        let has_batched = self.topk_sigmoid_batched_k.0 != 0
351            && self.moe_up_prefill_k.0 != 0
352            && self.moe_relu2_down_prefill_k.0 != 0
353            && self.moe_weighted_sum_prefill_k.0 != 0;
354
355        let shared_up_out_base = ctx.buffers.ssm_qkvz();
356        let use_batched_moe = has_batched && num_tokens > 1;
357        // 2026-09-25: The shared expert's up projection for every token, into
358        // `ssm_qkvz`; both expert paths read it. Arms in `prefill_shared_up.rs`.
359        self.prefill_shared_up(normed, shared_up_out_base, n, h, shared_inter, ctx, stream)?;
360
361        // 2026-09-25: LatentMoE: fc1 into `attn_output`. `moe_output` is not
362        // used here because the expert paths write their routed output there.
363        let latent = self.moe_latent_size as u32;
364        let latent_base = if latent > 0 {
365            let latent_buf = ctx.buffers.attn_output();
366            if let Some(w_fp8) = self.fc1_pd_fp8 {
367                ops::fp8_gemm_m128_mfast(
368                    ctx.gpu,
369                    self.fp8_gemm_m128_k,
370                    normed,
371                    w_fp8,
372                    latent_buf,
373                    n,
374                    latent,
375                    h as u32,
376                    stream,
377                )?;
378            } else {
379                let fc1 = self.weights.fc1_latent_proj.as_ref().unwrap();
380                self.dense_gemm_prefill(
381                    ctx.gpu, normed, fc1, latent_buf, n, latent, h as u32, stream,
382                )?;
383            }
384            Some(latent_buf)
385        } else {
386            None
387        };
388
389        let scratch = ctx.buffers.scratch();
390        let indices_dev = scratch;
391        let weights_dev = scratch.offset(n as usize * top_k as usize * 4);
392
393        // 2026-09-25: The sorted path (sort the routed rows by expert, then grouped
394        // GEMMs) needs more than one token, the four `nemotron_moe_prefill`
395        // kernels (`has_batched`), and the sort, grouped-GEMM and unpermute kernels.
396        let use_sorted = use_batched_moe
397            && self.moe_sort_k.0 != 0
398            && self.moe_grouped_gemm_k.0 != 0
399            && self.moe_unpermute_reduce_k.0 != 0;
400
401        let p = SortedPrefillCtx {
402            n,
403            num_tokens,
404            h,
405            inter,
406            shared_inter,
407            num_experts,
408            top_k,
409            scale,
410            latent,
411            gate_logits,
412            indices_dev,
413            weights_dev,
414            normed,
415            hidden,
416            latent_base,
417            shared_up_out_base,
418        };
419        if use_sorted {
420            self.prefill_sorted_path(&p, ctx, stream)?;
421        } else {
422            self.prefill_fallback_path(&p, ctx, stream)?;
423        }
424
425        Ok(())
426    }
427
428    fn alloc_state(&self, _gpu: &dyn GpuBackend) -> Result<Box<dyn LayerState>> {
429        Ok(Box::new(EmptyLayerState))
430    }
431}
432
433impl LayerCapabilities for NemotronMoeLayer {}
434impl LayerWeightSetup for NemotronMoeLayer {}
435impl LayerWriteOnAccept for NemotronMoeLayer {}
436impl LayerGraphHooks for NemotronMoeLayer {}
437impl LayerAuxState for NemotronMoeLayer {}
438impl LayerSplitPrefill for NemotronMoeLayer {}
439impl metrale_model_layers::circuit_exec::CircuitBindings for NemotronMoeLayer {}