metrale_model_layers/layers/ops/w4a4_proj/mx_plan.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Which `w4a4_gemv_mx*` entry serves an `m`-row projection, and
4//! with what launch shape: the `METRALE_W4A4_MX_NT` / `METRALE_W4A4_MX_PS`
5//! levers and the pure planning in `mx_plan`.
6//!
7//! Owner: model-layers ops.
8//! Invariants: none beyond the types.
9
10use std::sync::OnceLock;
11
12use metrale_gpu_runtime::gpu::KernelHandle;
13
14use super::{W4A4_MAX_M, W4a4State};
15
16/// 2026-09-25: `METRALE_W4A4_MX_NT` = 4 (default, also unset or empty) | 1; any
17/// other value panics. At 4 the 9..=64-row W4A4 GEMV runs the activation-reuse
18/// twins: 17..=32 rows take four 16-row weight tiles per CTA, 9..=16 and
19/// 33..=64 rows take two, and each warp feeds its activation fragments to
20/// every tile from registers, so the activation matrix is read from L2 that
21/// many times less. The twins keep the one-tile kernels' per-warp chunk order
22/// and warp-order reduction. At 1 every width runs the one-tile kernels.
23pub(super) fn mx_nt() -> u32 {
24 static NT: OnceLock<u32> = OnceLock::new();
25 *NT.get_or_init(
26 || match std::env::var("METRALE_W4A4_MX_NT").ok().as_deref() {
27 None | Some("") | Some("4") => 4,
28 Some("1") => 1,
29 Some(v) => panic!("METRALE_W4A4_MX_NT={v}: expected 1 or 4"),
30 },
31 )
32}
33
34/// 2026-09-25: `METRALE_W4A4_MX_PS` = 1 (default, also unset or empty) | 0; any
35/// other value panics. At 1, a 9..=32-row launch whose whole activation stripe
36/// fits in [`PS_SMEM_MAX`] and whose weight has at least
37/// [`PS_MIN_TILES_PER_SM`] 16-row tiles per SM runs the persistent
38/// activation-staged entry (`w4a4_gemv_mx{16,32}_ps`): one CTA per SM pulls
39/// 16-row tiles from a counter and reads the activations from shared memory.
40/// 0 routes those launches back to the [`mx_nt`] kernels. It acts only when the
41/// tile factor is not 1, so `METRALE_W4A4_MX_NT=1` selects the one-tile kernels
42/// at every width.
43pub(super) fn mx_ps() -> bool {
44 static PS: OnceLock<bool> = OnceLock::new();
45 *PS.get_or_init(
46 || match std::env::var("METRALE_W4A4_MX_PS").ok().as_deref() {
47 None | Some("") | Some("1") => true,
48 Some("0") => false,
49 Some(v) => panic!("METRALE_W4A4_MX_PS={v}: expected 0 or 1"),
50 },
51 )
52}
53
54/// 2026-09-25: Largest dynamic shared memory, in bytes, one CTA may opt in to
55/// on GB10 (sm_121).
56pub const PS_SMEM_MAX: u32 = 101_376;
57/// 2026-09-25: The persistent entries run only when the weight has at least
58/// this many 16-row tiles per SM; below it `mx_plan` uses the tiled kernels.
59pub const PS_MIN_TILES_PER_SM: u32 = 8;
60
61/// 2026-09-25: Pure: 8-token column blocks (`MB`) of the persistent entry
62/// serving `m` rows (`w4a4_gemv_mx16_ps` or `w4a4_gemv_mx32_ps`).
63pub fn ps_column_blocks(m: u32) -> u32 {
64 if m <= 16 { 2 } else { 4 }
65}
66
67/// 2026-09-25: Pure: the host side of the persistent entries' launch contract
68/// (`w4a4_gemv_mx_ps.cuh`): dynamic shared memory for `mb` column blocks with
69/// `sst` k128 chunks staged per warp. 8 warps x sst x mb x (512 B of
70/// fragments + 64 B of scales), plus the reduction buffer of `RJ = 2` 4 KiB
71/// blocks that both entries instantiate.
72pub fn ps_smem_bytes(mb: u32, sst: u32) -> u32 {
73 8 * sst * mb * 576 + 2 * 4096
74}
75
76/// 2026-09-25: Pure: k128 chunks in one warp's stripe (chunks c = warp mod 8).
77pub fn ps_stripe_chunks(k: u32) -> u32 {
78 (k / 128).div_ceil(8)
79}
80
81/// 2026-09-25: How one W4A4 GEMV launch is shaped.
82#[derive(Clone, Copy, Debug)]
83pub(super) enum MxLaunch {
84 /// 2026-09-25: Grid ceil(N / rows_per_cta).
85 Tiles {
86 kernel: KernelHandle,
87 rows_per_cta: u32,
88 },
89 /// 2026-09-25: Grid #SMs, `smem` bytes of dynamic shared memory, `sst`
90 /// staged chunks.
91 Persistent {
92 kernel: KernelHandle,
93 sst: u32,
94 smem: u32,
95 },
96}
97
98/// 2026-09-25: Pure: the launch for an `m`-row `[n, k]` projection.
99pub(super) fn mx_plan(s: &W4a4State, m: u32, n: u32, k: u32, nt: u32, ps: bool) -> MxLaunch {
100 if ps && nt != 1 && m > 8 && m <= W4A4_MAX_M {
101 let kernel = if m <= 16 { s.mx16_ps } else { s.mx32_ps };
102 let sst = ps_stripe_chunks(k);
103 let smem = ps_smem_bytes(ps_column_blocks(m), sst);
104 if smem <= PS_SMEM_MAX && n.div_ceil(16) >= PS_MIN_TILES_PER_SM * s.sms {
105 return MxLaunch::Persistent { kernel, sst, smem };
106 }
107 }
108 let (kernel, rows_per_cta) = mx_pick(s, m, nt);
109 MxLaunch::Tiles {
110 kernel,
111 rows_per_cta,
112 }
113}
114
115/// 2026-09-25: Pure: (kernel, rows per CTA) for an `m`-row launch at tile
116/// factor `nt`.
117pub(super) fn mx_pick(s: &W4a4State, m: u32, nt: u32) -> (KernelHandle, u32) {
118 match (m, nt) {
119 (0..=8, _) => (s.mx8, 16),
120 (9..=16, 1) => (s.mx16, 16),
121 (9..=16, _) => (s.mx16_nt2, 32),
122 (17..=32, 1) => (s.mx32, 16),
123 (17..=32, _) => (s.mx32_nt4, 64),
124 (_, 1) => (s.mx64, 16),
125 _ => (s.mx64_nt2, 32),
126 }
127}