metrale_model_layers/layers/ops/
norm.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for the RMS norm family: plain, strided, warp-per-row,
4//! with residual save, with residual add, and gated.
5//!
6//! Owner: model-layers ops.
7//! Invariants: none beyond the types.
8
9#![allow(unused_imports)]
10
11use anyhow::Result;
12use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
13use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
14
15use crate::layers::moe;
16use crate::weight_map::{DenseWeight, Fp8DenseWeight, Fp8Weight, QuantizedWeight};
17
18use super::*;
19
20/// 2026-09-25: RMS norm of `num_groups` groups of `rows_per_group` rows in one
21/// launch. Groups start `row_stride` elements apart, and rows inside a group are
22/// packed at `hidden_size`. The multi-sequence q/k head norms use it because each
23/// sequence's heads sit inside that sequence's own interleaved block.
24///
25/// `rms_norm_strided` in `kernels/gb10/common/rms_norm.cu` has the per-row body
26/// of `rms_norm` there; only the row's base address differs.
27#[allow(clippy::too_many_arguments)]
28pub fn rms_norm_strided(
29    gpu: &dyn GpuBackend,
30    kernel: KernelHandle,
31    input: DevicePtr,
32    weight: &DenseWeight,
33    output: DevicePtr,
34    rows_per_group: u32,
35    num_groups: u32,
36    hidden_size: u32,
37    eps: f32,
38    row_stride: u32,
39    stream: u64,
40) -> Result<()> {
41    KernelLaunch::new(gpu, kernel)
42        .grid([rows_per_group, num_groups, 1])
43        .block([hidden_size.min(1024), 1, 1])
44        .arg_ptr(input)
45        .arg_ptr(weight.weight)
46        .arg_ptr(output)
47        .arg_u32(hidden_size)
48        .arg_f32(eps)
49        .arg_u32(row_stride)
50        .launch(stream)
51}
52
53/// 2026-09-25: RMS norm of `num_tokens` packed rows of `hidden_size`, one block
54/// per row. How `weight` scales the result is the kernel's: `rms_norm` in
55/// `kernels/gb10/common/rms_norm.cu` multiplies by `1 + weight`, and the
56/// `rms_norm_vanilla` kernels by `weight`.
57pub fn rms_norm(
58    gpu: &dyn GpuBackend,
59    kernel: KernelHandle,
60    input: DevicePtr,
61    weight: &DenseWeight,
62    output: DevicePtr,
63    num_tokens: u32,
64    hidden_size: u32,
65    eps: f32,
66    stream: u64,
67) -> Result<()> {
68    KernelLaunch::new(gpu, kernel)
69        .grid([num_tokens, 1, 1])
70        .block([hidden_size.min(1024), 1, 1])
71        .arg_ptr(input)
72        .arg_ptr(weight.weight)
73        .arg_ptr(output)
74        .arg_u32(hidden_size)
75        .arg_f32(eps)
76        .launch(stream)
77}
78
79/// 2026-09-25: RMS norm with one warp per row and 8 rows per block, so the
80/// reduction needs no shared memory or block barrier. The prefill per-head
81/// `q_norm`/`k_norm` uses it when [`rms_norm_short_row_eligible`] holds
82/// (`qwen3_attention/prefill/cache_skip.rs`).
83pub fn rms_norm_warp_row(
84    gpu: &dyn GpuBackend,
85    kernel: KernelHandle,
86    input: DevicePtr,
87    weight: &DenseWeight,
88    output: DevicePtr,
89    num_rows: u32,
90    hidden_size: u32,
91    eps: f32,
92    stream: u64,
93) -> Result<()> {
94    const ROWS_PER_BLOCK: u32 = 8;
95    KernelLaunch::new(gpu, kernel)
96        .grid([num_rows.div_ceil(ROWS_PER_BLOCK), 1, 1])
97        .block([32 * ROWS_PER_BLOCK, 1, 1])
98        .arg_ptr(input)
99        .arg_ptr(weight.weight)
100        .arg_ptr(output)
101        .arg_u32(num_rows)
102        .arg_u32(hidden_size)
103        .arg_f32(eps)
104        .launch(stream)
105}
106
107/// 2026-09-25: Gate for [`rms_norm_warp_row`]: `hidden_size <= 256` and even,
108/// and `num_rows >= 1024`. `METRALE_RMS_NORM_WARP_ROW=0` turns it off; the
109/// variable is read once per process.
110pub fn rms_norm_short_row_eligible(num_rows: u32, hidden_size: u32) -> bool {
111    use std::sync::OnceLock;
112    static ON: OnceLock<bool> = OnceLock::new();
113    let on = *ON.get_or_init(|| std::env::var("METRALE_RMS_NORM_WARP_ROW").as_deref() != Ok("0"));
114    on && hidden_size <= 256 && hidden_size.is_multiple_of(2) && num_rows >= 1024
115}
116
117/// 2026-09-25: `output = rms_norm(input)` and `residual = input` in one pass,
118/// one block per token row.
119pub fn rms_norm_residual(
120    gpu: &dyn GpuBackend,
121    kernel: KernelHandle,
122    input: DevicePtr,
123    weight: &DenseWeight,
124    output: DevicePtr,
125    residual: DevicePtr,
126    num_tokens: u32,
127    hidden_size: u32,
128    eps: f32,
129    stream: u64,
130) -> Result<()> {
131    KernelLaunch::new(gpu, kernel)
132        .grid([num_tokens, 1, 1])
133        .block([hidden_size.min(1024), 1, 1])
134        .arg_ptr(input)
135        .arg_ptr(weight.weight)
136        .arg_ptr(output)
137        .arg_ptr(residual)
138        .arg_u32(hidden_size)
139        .arg_f32(eps)
140        .launch(stream)
141}
142
143/// 2026-09-25: `hidden += src`, then `output = rms_norm(hidden)` and
144/// `residual = hidden`, in one kernel, one block per token row.
145#[allow(clippy::too_many_arguments)]
146pub fn residual_add_rms_norm(
147    gpu: &dyn GpuBackend,
148    kernel: KernelHandle,
149    hidden: DevicePtr,
150    src: DevicePtr,
151    weight: &DenseWeight,
152    output: DevicePtr,
153    residual: DevicePtr,
154    num_tokens: u32,
155    hidden_size: u32,
156    eps: f32,
157    stream: u64,
158) -> Result<()> {
159    KernelLaunch::new(gpu, kernel)
160        .grid([num_tokens, 1, 1])
161        .block([hidden_size.min(1024), 1, 1])
162        .arg_ptr(hidden)
163        .arg_ptr(src)
164        .arg_ptr(weight.weight)
165        .arg_ptr(output)
166        .arg_ptr(residual)
167        .arg_u32(hidden_size)
168        .arg_f32(eps)
169        .launch(stream)
170}
171
172/// 2026-09-28: `residual_add_rms_norm_exact` (`residual_add_rms_norm_exact.cu`): the arguments
173/// and launch of [`residual_add_rms_norm`], with the bytes of `bf16_residual_add` followed by
174/// [`rms_norm_residual`]: the sum of squares reads the BF16-rounded sums. The circuit executor
175/// launches it across a layer boundary, with the next layer's input-norm weight.
176#[allow(clippy::too_many_arguments)]
177pub fn residual_add_rms_norm_exact(
178    gpu: &dyn GpuBackend,
179    kernel: KernelHandle,
180    hidden: DevicePtr,
181    src: DevicePtr,
182    weight: &DenseWeight,
183    output: DevicePtr,
184    residual: DevicePtr,
185    num_tokens: u32,
186    hidden_size: u32,
187    eps: f32,
188    stream: u64,
189) -> Result<()> {
190    residual_add_rms_norm(
191        gpu,
192        kernel,
193        hidden,
194        src,
195        weight,
196        output,
197        residual,
198        num_tokens,
199        hidden_size,
200        eps,
201        stream,
202    )
203}
204
205/// 2026-09-25: [`residual_add_rms_norm`] that also writes the normed row in FP32
206/// to `output_f32`, the MoE router input, so routing does not see the BF16
207/// rounding of the norm output. Callers use it when `METRALE_FP32_ROUTING` is
208/// active for the layer's FFN.
209#[allow(clippy::too_many_arguments)]
210pub fn residual_add_rms_norm_gatef32(
211    gpu: &dyn GpuBackend,
212    kernel: KernelHandle,
213    hidden: DevicePtr,
214    src: DevicePtr,
215    weight: &DenseWeight,
216    output: DevicePtr,
217    output_f32: DevicePtr,
218    residual: DevicePtr,
219    num_tokens: u32,
220    hidden_size: u32,
221    eps: f32,
222    stream: u64,
223) -> Result<()> {
224    KernelLaunch::new(gpu, kernel)
225        .grid([num_tokens, 1, 1])
226        .block([hidden_size.min(1024), 1, 1])
227        .arg_ptr(hidden)
228        .arg_ptr(src)
229        .arg_ptr(weight.weight)
230        .arg_ptr(output)
231        .arg_ptr(output_f32)
232        .arg_ptr(residual)
233        .arg_u32(hidden_size)
234        .arg_f32(eps)
235        .launch(stream)
236}
237
238/// 2026-09-25: Gated RMS norm of `num_tokens` rows, one block per row; the gate
239/// row of token `t` starts at `t * gate_stride`. The order of norm and gate is
240/// the resolved kernel's. `gated_rms_norm` and `gated_rms_norm_f32_input` in
241/// `kernels/gb10/common/rms_norm.cu` compute `rms_norm(input) * weight *
242/// silu(gate)` and ignore `group_size`. The per-group shadows, e.g.
243/// `kernels/gb10/minimax-m2-229b/nvfp4/rms_norm.cu`, gate first and normalise
244/// each `group_size` slice.
245pub fn gated_rms_norm(
246    gpu: &dyn GpuBackend,
247    kernel: KernelHandle,
248    input: DevicePtr,
249    gate: DevicePtr,
250    weight: &DenseWeight,
251    output: DevicePtr,
252    num_tokens: u32,
253    hidden_size: u32,
254    gate_stride: u32,
255    eps: f32,
256    group_size: u32,
257    stream: u64,
258) -> Result<()> {
259    KernelLaunch::new(gpu, kernel)
260        .grid([num_tokens, 1, 1])
261        .block([hidden_size.min(1024), 1, 1])
262        .arg_ptr(input)
263        .arg_ptr(gate)
264        .arg_ptr(weight.weight)
265        .arg_ptr(output)
266        .arg_u32(hidden_size)
267        .arg_f32(eps)
268        .arg_u32(gate_stride)
269        .arg_u32(group_size)
270        .launch(stream)
271}
272
273/// 2026-09-25: Gated RMS norm of every `(head, sequence)` row of a multi-sequence
274/// decode in one launch: `heads_per_seq` rows per sequence, `num_seqs`
275/// sequences. Sequence strides count elements of each buffer's own type:
276/// `input_seq_stride` in f32, `gate_seq_stride` and `output_seq_stride` in BF16.
277///
278/// The kernel, `gated_rms_norm_f32_input_strided` in
279/// `kernels/gb10/common/rms_norm.cu`, has the per-row body of
280/// `gated_rms_norm_f32_input`; only the row's base address differs.
281#[allow(clippy::too_many_arguments)]
282pub fn gated_rms_norm_strided(
283    gpu: &dyn GpuBackend,
284    kernel: KernelHandle,
285    input: DevicePtr,
286    gate: DevicePtr,
287    weight: &DenseWeight,
288    output: DevicePtr,
289    heads_per_seq: u32,
290    num_seqs: u32,
291    hidden_size: u32,
292    gate_stride: u32,
293    eps: f32,
294    group_size: u32,
295    input_seq_stride: u32,
296    gate_seq_stride: u32,
297    output_seq_stride: u32,
298    stream: u64,
299) -> Result<()> {
300    KernelLaunch::new(gpu, kernel)
301        .grid([heads_per_seq, num_seqs, 1])
302        .block([hidden_size.min(1024), 1, 1])
303        .arg_ptr(input)
304        .arg_ptr(gate)
305        .arg_ptr(weight.weight)
306        .arg_ptr(output)
307        .arg_u32(hidden_size)
308        .arg_f32(eps)
309        .arg_u32(gate_stride)
310        .arg_u32(group_size)
311        .arg_u32(input_seq_stride)
312        .arg_u32(gate_seq_stride)
313        .arg_u32(output_seq_stride)
314        .launch(stream)
315}
316
317/// 2026-09-25: Gated RMS norm of every `(head, token)` row of a prefill chunk in
318/// one launch. `input` and `output` rows of token `t` start at
319/// `t * input_token_stride`, and gate rows at `t * gate_token_stride`, both in
320/// BF16 elements.
321#[allow(clippy::too_many_arguments)]
322pub fn gated_rms_norm_prefill(
323    gpu: &dyn GpuBackend,
324    kernel: KernelHandle,
325    input: DevicePtr,
326    gate: DevicePtr,
327    weight: &DenseWeight,
328    output: DevicePtr,
329    heads_per_token: u32,
330    head_dim: u32,
331    eps: f32,
332    num_actual_tokens: u32,
333    input_token_stride: u32,
334    gate_token_stride: u32,
335    stream: u64,
336) -> Result<()> {
337    KernelLaunch::new(gpu, kernel)
338        .grid([heads_per_token, num_actual_tokens, 1])
339        .block([head_dim.min(1024), 1, 1])
340        .arg_ptr(input)
341        .arg_ptr(gate)
342        .arg_ptr(weight.weight)
343        .arg_ptr(output)
344        .arg_u32(head_dim)
345        .arg_f32(eps)
346        .arg_u32(input_token_stride)
347        .arg_u32(gate_token_stride)
348        .launch(stream)
349}