metrale_model_layers/layers/ops/
ssm_gdn_a3.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The chunked GDN prefill launcher (three kernels) and the guard
4//! that decides whether its tensor-core state spine runs.
5//!
6//! Owner: model-layers ops (GDN).
7//! Invariants: none beyond the types.
8#![allow(unused_imports)]
9
10use anyhow::Result;
11use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
12use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
13
14use crate::layers::moe;
15use crate::weight_map::{DenseWeight, Fp8DenseWeight, Fp8Weight, QuantizedWeight};
16
17use super::*;
18
19/// 2026-09-25: The tensor-core spine's compile-time tile: `K_DIM == V_DIM` in
20/// `kernels/hopper/common/gated_delta_rule_chunk_tc.cu`.
21pub(crate) const GDN_TC_DIM: u32 = 128;
22/// 2026-09-25: That kernel's `CHUNK`.
23pub(crate) const GDN_TC_CHUNK: u32 = 64;
24/// 2026-09-25: Mirror of `TCF_SMEM` in the same file:
25/// `St[128][136] + Wp[64][136] + Up[64][136] + ducT[128][72]` bf16 plus
26/// `dec[65]` f32, `34816 + 17408 + 17408 + 18432 + 260 = 88 324` bytes.
27/// The padded 136/72 row strides make the MMA fragment reads
28/// bank-conflict-free. Under-sizing this reads a tile out of bounds, so the
29/// launcher and the kernel must agree on it.
30pub(crate) const GDN_TC_SMEM: u32 = GDN_TC_DIM * 136 * 2
31    + 2 * (GDN_TC_CHUNK * 136 * 2)
32    + GDN_TC_DIM * 72 * 2
33    + (GDN_TC_CHUNK + 1) * 4;
34
35/// 2026-09-25: Why the tensor-core state spine is not running; `None` means it
36/// is.
37///
38/// Pure, so the grammar is testable without a GPU or the process environment.
39/// Each refusal names its guard, so a run that asked for the spine and fell
40/// back says which guard refused.
41///
42/// The tile guards are required, not defensive: the kernel's shared-memory
43/// layout and fragment maps are compile-time 128/128/64, and its K staging
44/// reads 16 bytes at a time, so a narrower head or a `qk_stride` that is not a
45/// multiple of 8 would load the wrong columns.
46pub(crate) fn gdn_tc_spine_reject(
47    requested: bool,
48    kernel_present: bool,
49    k_dim: u32,
50    v_dim: u32,
51    chunk: u32,
52    qk_stride: u32,
53) -> Option<&'static str> {
54    if !requested {
55        Some("not requested")
56    } else if !kernel_present {
57        Some("kernel absent from this image")
58    } else if k_dim != GDN_TC_DIM || v_dim != GDN_TC_DIM || chunk != GDN_TC_CHUNK {
59        Some("head/chunk differs from the compile-time tile (K_DIM=V_DIM=128, CHUNK=64)")
60    } else if !qk_stride.is_multiple_of(8) {
61        Some("qk_stride is not a multiple of 8 (the K staging uses 16-byte vector loads)")
62    } else {
63        None
64    }
65}
66
67#[cfg(test)]
68#[path = "ssm_gdn_tc_tests.rs"]
69mod ssm_gdn_tc_tests;
70
71/// 2026-09-25: Chunked GDN prefill in three launches, ordered by `stream`:
72///   1. `recompute_wu`, or its Hopper twin: per chunk, W and U (bf16) and the
73///      cumulative gate `gc` (f32);
74///   2. a state spine, chosen below: walks the chunks in order, writes each
75///      chunk's entry state `S` and `uc` (bf16), and updates `h_state` in place;
76///   3. `chunk_fwd_o`, or its Hopper twin: the bf16 output.
77///
78/// `w_out`, `u_out`, `s_out`, `uc_out` and `gc_out` are caller-sized scratch.
79#[allow(clippy::too_many_arguments)]
80pub fn gdn_prefill_fla(
81    gpu: &dyn GpuBackend,
82    k_recompute_wu: KernelHandle,
83    // 2026-09-25: Hopper twins of kernels 1 and 3, `KernelHandle(0)` on every
84    // other target. `gdn_hopper_remnants` chooses between twin and parent.
85    k_recompute_wu_hopper: KernelHandle,
86    k_chunk_fwd_o_hopper: KernelHandle,
87    k_chunk_delta_h: KernelHandle,
88    // 2026-09-25: The DV-block-split spine
89    // (`gated_delta_rule_chunk_delta_h_tc_vblock`). It runs only when the fused
90    // spine does not, the handle is non-zero and `METRALE_GDN_TC_VBLOCK=1`.
91    k_chunk_delta_h_tc_vblock: KernelHandle,
92    // 2026-09-25: The tensor-core spine (`GDN_TC_SPINE_ENTRY`). `KernelHandle(0)`
93    // unless the resolved `gdn_prefill_tc` lever is on and the target compiles
94    // the kernel. Same arguments as the fused spine, launched on grid
95    // [nv, batch] with 256 threads and `GDN_TC_SMEM` bytes of shared memory.
96    k_chunk_delta_h_tcfuse: KernelHandle,
97    k_chunk_delta_h_fused: KernelHandle,
98    k_chunk_delta_h_tma: KernelHandle,
99    k_chunk_fwd_o: KernelHandle,
100    h_state: DevicePtr,
101    query: DevicePtr,
102    key: DevicePtr,
103    value: DevicePtr,
104    gate: DevicePtr,
105    beta: DevicePtr,
106    output: DevicePtr,
107    w_out: DevicePtr,
108    u_out: DevicePtr,
109    s_out: DevicePtr,
110    uc_out: DevicePtr,
111    gc_out: DevicePtr,
112    batch_size: u32,
113    seq_len: u32,
114    num_chunks: u32,
115    num_k_heads: u32,
116    num_v_heads: u32,
117    k_dim: u32,
118    v_dim: u32,
119    qk_stride: u32,
120    v_stride: u32,
121    gb_stride: u32,
122    // 2026-09-25: true: `h_state` is a device table of per-sequence state
123    // pointers, each `[nv, kd, vd]`. false: one contiguous base.
124    h_state_is_table: bool,
125    // 2026-09-25: true: streams of different lengths. `cu_seqlens` holds
126    // `batch + 1` device token offsets, and `num_chunks` must be the maximum over
127    // the streams (it is grid x). The kernels derive each stream's chunk offset
128    // from `cu_seqlens` and do not read `cu_chunks`. false: pass NULL for both.
129    cu_seqlens: DevicePtr,
130    cu_chunks: DevicePtr,
131    is_varlen: bool,
132    profile: bool,
133    stream: u64,
134) -> Result<()> {
135    const C: u32 = 64; // 2026-09-25: the kernels' `CHUNK`.
136    let (kd, vd) = (k_dim, v_dim);
137    // 2026-09-25: Shared-memory bytes per kernel. In `recompute_wu`, L aliases
138    // the kk Gram (they use disjoint triangles), so one C*C*4 buffer holds both.
139    let smem_wu = C * kd * 2 + C * C * 4 + C * 4;
140    let smem_dh = 2 * (C * (2 * kd + vd) * 2) + 2 * C * 4 + 2 * (C + 1) * 4;
141    let smem_fo = C * kd * 2 + C * kd * 2 + C * C * 4 + C * vd * 2 + kd * vd * 2 + 2 * C * 4;
142
143    let mut t0: Option<std::time::Instant> = if profile {
144        gpu.synchronize(stream)?;
145        Some(std::time::Instant::now())
146    } else {
147        None
148    };
149
150    macro_rules! prof {
151        ($label:expr, $t0:expr) => {
152            if let Some(t0) = $t0.take() {
153                gpu.synchronize(stream)?;
154                let elapsed = t0.elapsed().as_micros();
155                tracing::info!("  SSM prefill [{}] N={}: {}µs", $label, seq_len, elapsed);
156                *$t0 = Some(std::time::Instant::now());
157            }
158        };
159    }
160
161    // 2026-09-25: The tensor-core prefill lever, read once for the three kernels
162    // it selects: the two twins here and the state spine below. It is the
163    // target's `[defaults] gdn_prefill_tc`, overridden by the value of
164    // `METRALE_GDN_PREFILL_TC` (`=0` turns it off).
165    // `METRALE_NO_GDN_PREFILL_TC_REMNANTS=1` keeps the spine and pins the twins
166    // to their parents.
167    let tc_requested = super::target_defaults::resolved().gdn_prefill_tc.value;
168    let (wu, fo) = gdn_hopper_remnants(
169        tc_requested,
170        k_recompute_wu,
171        smem_wu,
172        k_chunk_fwd_o,
173        smem_fo,
174        k_recompute_wu_hopper,
175        k_chunk_fwd_o_hopper,
176        kd,
177        vd,
178        C,
179    );
180
181    // 2026-09-28: Kernel 3 without a Hopper twin: the bit-identical 8-warp twin when present.
182    let fo = super::gdn_fwd_o_mma8(gpu, fo, k_chunk_fwd_o, kd, vd)?;
183
184    // 2026-09-25: Kernel 1: recompute_wu, or its Hopper twin.
185    KernelLaunch::new(gpu, wu.kernel)
186        .grid([num_chunks, num_v_heads, batch_size])
187        .block([wu.block, 1, 1])
188        .shared_mem(wu.smem)
189        .arg_ptr(key)
190        .arg_ptr(value)
191        .arg_ptr(gate)
192        .arg_ptr(beta)
193        .arg_ptr(w_out)
194        .arg_ptr(u_out)
195        .arg_ptr(gc_out)
196        .arg_u32(batch_size)
197        .arg_u32(seq_len)
198        .arg_u32(num_chunks)
199        .arg_u32(num_k_heads)
200        .arg_u32(num_v_heads)
201        .arg_u32(kd)
202        .arg_u32(vd)
203        .arg_u32(qk_stride)
204        .arg_u32(v_stride)
205        .arg_u32(gb_stride)
206        .arg_ptr(cu_seqlens)
207        .arg_ptr(cu_chunks)
208        .arg_u32(is_varlen as u32)
209        .launch(stream)?;
210    prof!("gdn_fla_recompute_wu", &mut t0);
211
212    // 2026-09-25: Kernel 2, the state spine. The non-TMA candidates take the
213    // same arguments; grid y, block size and shared memory differ. In order of
214    // precedence: TMA and the tensor-core spine (both below); the fused spine
215    // that `init_kernels::fused_spine_kernel` loaded (`gdn_scalar_spine`:
216    // 2026-09-28 `_pipe` by default, `_vtile` under `METRALE_GDN_VTILE=1`,
217    // `_vfused` under `METRALE_GDN_PIPE=0`), unless its handle is 0 or
218    // `METRALE_GDN_VTILE=0`; `_tc_vblock` under `METRALE_GDN_TC_VBLOCK=1`; else
219    // `_ksplit`. The fused spine folds the two per-chunk passes into one, so at
220    // kd = vd = 128 `_vfused` / `_vtile` take 49,412 B against ksplit's 99,336 B
221    // (`_pipe` double-buffers, so it takes ksplit's).
222    let use_fused = k_chunk_delta_h_fused.0 != 0
223        && std::env::var("METRALE_GDN_VTILE").ok().as_deref() != Some("0");
224    let use_tcvb = !use_fused
225        && k_chunk_delta_h_tc_vblock.0 != 0
226        && std::env::var("METRALE_GDN_TC_VBLOCK").ok().as_deref() == Some("1");
227    const DV_BLK: u32 = 64; // 2026-09-25: the kernel's compile-time `DV_BLK`.
228    let num_dv_blk = (vd / DV_BLK).max(1);
229    // 2026-09-25: tc_vblock smem: St[DV_BLK*kd] + ws[C*DV_BLK] f32
230    // + buf[2][C*kd + C*DV_BLK] + gcb[2][C] f32 + decb[2][C+1] f32.
231    let smem_tcvb = DV_BLK * kd * 2
232        + C * DV_BLK * 4
233        + 2 * (C * kd + C * DV_BLK) * 2
234        + 2 * C * 4
235        + 2 * (C + 1) * 4;
236    // 2026-09-25: `_vfused` and `_vtile` stage W, K and U single-buffered plus one
237    // decay row and do not split the DV axis, so grid y stays `batch_size`; they
238    // differ only in thread count. `_pipe` double-buffers W, K and U, so it needs
239    // `smem_dh`, the ksplit footprint. The env reads here must select the same
240    // build that `init_kernels::fused_spine_kernel` loaded, or the second buffer
241    // is read out of bounds.
242    let spine = super::gdn_scalar_spine();
243    let pipe = spine == super::GdnScalarSpine::Pipe;
244    let smem_fused = if pipe {
245        smem_dh
246    } else {
247        C * kd * 2 + C * kd * 2 + C * vd * 2 + (C + 1) * 4
248    };
249    let fused_block = if spine == super::GdnScalarSpine::Vtile {
250        512u32
251    } else {
252        256u32
253    };
254    // 2026-09-25: The tensor-core spine puts both per-chunk products on
255    // `mma.sync.m16n8k16`: bf16 operands (S_c and duc as two bf16 limbs each
256    // in the `GDN_TC_SPINE_ENTRY` build), f32 accumulate. The recurrent state
257    // stays in the f32 accumulator. It runs when the resolved `gdn_prefill_tc`
258    // lever is on (only `kernels/hopper` declares it true) and every guard in
259    // `gdn_tc_spine_reject` passes; a refused request is logged with its guard.
260    let smem_tcfuse = GDN_TC_SMEM;
261    let tc_reject = gdn_tc_spine_reject(
262        tc_requested,
263        k_chunk_delta_h_tcfuse.0 != 0,
264        kd,
265        vd,
266        C,
267        qk_stride,
268    );
269    if tc_requested && let Some(why) = tc_reject {
270        tracing::warn!("METRALE_GDN_PREFILL_TC set but tensor-core spine is NOT running: {why}");
271    }
272    let tc_ok = tc_reject.is_none();
273    if tc_ok {
274        // 2026-09-25: Built from `GDN_TC_SPINE_ENTRY`, the constant
275        // `init_kernels` binds the handle with.
276        tracing::info!(
277            "{}",
278            gdn_tc_spine_route_line(num_v_heads, batch_size, smem_tcfuse)
279        );
280    }
281    // 2026-09-25: TMA state spine (`METRALE_GDN_TMA=1`). Every precondition is
282    // checked here. The descriptors encode the compile-time tile
283    // (K_DIM = V_DIM = 128, CHUNK = 64), so a narrower head would load the wrong
284    // columns.
285    let tma_requested = std::env::var("METRALE_GDN_TMA").ok().as_deref() == Some("1");
286    let tma_reject: Option<&str> = if !tma_requested {
287        Some("not requested")
288    } else if tc_ok {
289        // 2026-09-25: With both levers set, TMA yields to the tensor-core spine
290        // and logs that it did.
291        Some("METRALE_GDN_PREFILL_TC is active and takes precedence")
292    } else if k_chunk_delta_h_tma.0 == 0 {
293        Some("kernel absent from this image")
294    } else if is_varlen {
295        Some(
296            "varlen: choff comes from cu_chunks, so the descriptor's flat row count is unknown host-side",
297        )
298    } else if kd != 128 || vd != 128 || C != 64 {
299        Some("head/chunk differs from the compile-time tile the descriptors encode")
300    } else if !qk_stride.is_multiple_of(8) {
301        Some("qk_stride is not a multiple of 8 (bf16 row pitch must be 16-byte aligned)")
302    } else {
303        None
304    };
305    if tma_requested && let Some(why) = tma_reject {
306        tracing::warn!("METRALE_GDN_TMA=1 but the TMA spine is NOT running: {why}");
307    }
308    let tma_ok = tma_reject.is_none();
309    if tma_ok {
310        tracing::info!("GDN state spine: gated_delta_rule_chunk_delta_h_tma");
311    }
312    // 2026-09-25: `cuda_backend`, and with it `TensorMap`, exists only under the
313    // `cuda` feature. Without it the TMA handle is absent and `tma_ok` is false,
314    // but the `use` below is resolved at compile time, so it needs the `cfg`.
315    #[cfg(feature = "cuda")]
316    if tma_ok {
317        use metrale_gpu_runtime::cuda_backend::tensormap::TensorMap;
318        // 2026-09-25: W/U are [total_blocks][CHUNK][tile] flattened; as a 2-D
319        // tensor that is (total_blocks * CHUNK) rows of `tile` columns, contiguous.
320        let blocks = (batch_size as u64) * (num_chunks as u64) * (num_v_heads as u64);
321        let w_map =
322            TensorMap::tiled_2d_bf16(w_out, blocks * C as u64, kd as u64, kd as u64, C, kd)?;
323        let u_map =
324            TensorMap::tiled_2d_bf16(u_out, blocks * C as u64, vd as u64, vd as u64, C, vd)?;
325        // 2026-09-25: K is a view into the packed conv output: rows are tokens
326        // at a `qk_stride` pitch, and the kernel supplies `kh * K_DIM` as the
327        // column origin.
328        let k_map = TensorMap::tiled_2d_bf16(
329            key,
330            (batch_size as u64) * (seq_len as u64),
331            qk_stride as u64,
332            qk_stride as u64,
333            C,
334            kd,
335        )?;
336        KernelLaunch::new(gpu, k_chunk_delta_h_tma)
337            .grid([num_v_heads, batch_size, 1])
338            .block([256, 1, 1])
339            .shared_mem(smem_dh)
340            .arg_ptr(h_state)
341            .arg_tensormap(w_map.bytes())
342            .arg_tensormap(u_map.bytes())
343            .arg_tensormap(k_map.bytes())
344            .arg_ptr(gc_out)
345            .arg_ptr(s_out)
346            .arg_ptr(uc_out)
347            .arg_u32(batch_size)
348            .arg_u32(seq_len)
349            .arg_u32(num_chunks)
350            .arg_u32(num_k_heads)
351            .arg_u32(num_v_heads)
352            .arg_u32(vd)
353            .arg_u32(h_state_is_table as u32)
354            .arg_ptr(cu_seqlens)
355            .arg_ptr(cu_chunks)
356            .arg_u32(is_varlen as u32)
357            .launch(stream)?;
358        prof!("gdn_fla_chunk_delta_h", &mut t0);
359    }
360
361    // 2026-09-25: Kernel 2 without TMA. Both paths write s_out/uc_out, and
362    // kernel 3 is the same either way.
363    if !tma_ok {
364        let (k_cdh, cdh_grid_y, cdh_smem, cdh_block) = if tc_ok {
365            (k_chunk_delta_h_tcfuse, batch_size, smem_tcfuse, 256u32)
366        } else if use_fused {
367            (k_chunk_delta_h_fused, batch_size, smem_fused, fused_block)
368        } else if use_tcvb {
369            (
370                k_chunk_delta_h_tc_vblock,
371                batch_size * num_dv_blk,
372                smem_tcvb,
373                256u32,
374            )
375        } else {
376            (k_chunk_delta_h, batch_size, smem_dh, 256u32)
377        };
378        KernelLaunch::new(gpu, k_cdh)
379            .grid([num_v_heads, cdh_grid_y, 1])
380            .block([cdh_block, 1, 1])
381            .shared_mem(cdh_smem)
382            .arg_ptr(h_state)
383            .arg_ptr(w_out)
384            .arg_ptr(u_out)
385            .arg_ptr(key)
386            .arg_ptr(gate)
387            .arg_ptr(gc_out)
388            .arg_ptr(s_out)
389            .arg_ptr(uc_out)
390            .arg_u32(batch_size)
391            .arg_u32(seq_len)
392            .arg_u32(num_chunks)
393            .arg_u32(num_k_heads)
394            .arg_u32(num_v_heads)
395            .arg_u32(kd)
396            .arg_u32(vd)
397            .arg_u32(qk_stride)
398            .arg_u32(gb_stride)
399            .arg_u32(h_state_is_table as u32)
400            .arg_ptr(cu_seqlens)
401            .arg_ptr(cu_chunks)
402            .arg_u32(is_varlen as u32)
403            .launch(stream)?;
404        prof!("gdn_fla_chunk_delta_h", &mut t0);
405    }
406
407    // 2026-09-25: Kernel 3: chunk_fwd_o, or its Hopper twin.
408    KernelLaunch::new(gpu, fo.kernel)
409        .grid([num_chunks, num_v_heads, batch_size])
410        .block([fo.block, 1, 1])
411        .shared_mem(fo.smem)
412        .arg_ptr(query)
413        .arg_ptr(key)
414        .arg_ptr(gate)
415        .arg_ptr(gc_out)
416        .arg_ptr(s_out)
417        .arg_ptr(uc_out)
418        .arg_ptr(output)
419        .arg_u32(batch_size)
420        .arg_u32(seq_len)
421        .arg_u32(num_chunks)
422        .arg_u32(num_k_heads)
423        .arg_u32(num_v_heads)
424        .arg_u32(kd)
425        .arg_u32(vd)
426        .arg_u32(qk_stride)
427        .arg_u32(gb_stride)
428        .arg_ptr(cu_seqlens)
429        .arg_ptr(cu_chunks)
430        .arg_u32(is_varlen as u32)
431        .launch(stream)?;
432    if let Some(t0) = t0 {
433        gpu.synchronize(stream)?;
434        let elapsed = t0.elapsed().as_micros();
435        tracing::info!(
436            "  SSM prefill [gdn_fla_chunk_fwd_o] N={}: {}µs",
437            seq_len,
438            elapsed
439        );
440    }
441    Ok(())
442}