metrale_model_layers/layers/
w4a16_gemv_tiers.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: SSOT for the narrow `w4a16_gemv_batch{M}` tier family (M = 4..8).
4//!
5//! Owner: model-layers (NVFP4 GEMV dispatch).
6//! Invariants:
7//! - `select_tier` never returns a tier narrower than `m`, nor one that the
8//!   loaded target did not resolve.
9//!
10//! `w4a16_gemv_batchm_impl<MAX_M>` sizes `acc[]` and `s_vl[]` by `MAX_M` and
11//! unrolls its row loop over `MAX_M`, so `MAX_M` also sizes the code. The
12//! `t >= M` guard skips a dead row's work at run time but not its
13//! instructions, so M=5 on the `MAX_M=8` tier carries three unused rows; the
14//! exact-M tiers 5, 6 and 7 avoid that.
15//!
16//! The width decision lives here once, as a pure function (`select_tier`)
17//! over which tiers the loaded target resolved; every consumer holds a
18//! `W4a16BatchmTiers` and calls `kernel(m)`.
19//!
20//! `METRALE_NO_GEMV_EXACT_M_TIERS` (presence-checked: any value, including
21//! `0`, disables) hides widths 5/6/7 from the decision, leaving widths 4 and
22//! 8. It does not unload the kernels, so an A/B needs no rebuild.
23
24use metrale_gpu_runtime::gpu::{GpuBackend, KernelHandle};
25
26/// 2026-09-25: Tier widths in this family, narrowest first. Parallel to the `handles`
27/// field of [`W4a16BatchmTiers`] and to the `present` array of
28/// [`select_tier`]. `w4a16_gemv_batch16`/`_batch32` are not in this table:
29/// they are held separately (`wide`, `wide32`) and handed out only above 8
30/// rows, within the row edge ([`W4a16BatchmTiers::edge`]).
31pub const W4A16_BATCHM_WIDTHS: [u32; 5] = [4, 5, 6, 7, 8];
32
33/// 2026-09-25: Index into [`W4A16_BATCHM_WIDTHS`] of width 5, the first tier
34/// `METRALE_NO_GEMV_EXACT_M_TIERS` hides.
35const FIRST_EXACT_M: usize = 1;
36
37/// 2026-09-25: Index of width 7, the last tier `METRALE_NO_GEMV_EXACT_M_TIERS`
38/// hides; width 8 stays.
39const EXACT_M_LAST: usize = 3;
40
41/// 2026-09-25: Are the exact-M tiers (5/6/7) allowed in the dispatch decision?
42///
43/// A presence check, read once per process: `kernel` consults it on every
44/// launch.
45pub fn exact_m_tiers_enabled() -> bool {
46    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
47    *ON.get_or_init(|| std::env::var_os("METRALE_NO_GEMV_EXACT_M_TIERS").is_none())
48}
49
50/// 2026-09-25: Pure tier decision: index into [`W4A16_BATCHM_WIDTHS`] of the
51/// narrowest tier that covers `m` rows and is present in the loaded target,
52/// or `None` when no such tier exists (or `m` is 0).
53///
54/// `present[i]` is whether `W4A16_BATCHM_WIDTHS[i]` resolved. `exact_m` is
55/// [`exact_m_tiers_enabled`], passed in so both values are testable without
56/// the process environment or the latched `OnceLock`.
57///
58/// Narrowest-that-covers, not exact-only: a target missing a tier still
59/// dispatches on the next wider one.
60pub fn select_tier(
61    m: u32,
62    present: [bool; W4A16_BATCHM_WIDTHS.len()],
63    exact_m: bool,
64) -> Option<usize> {
65    if m == 0 {
66        return None;
67    }
68    W4A16_BATCHM_WIDTHS
69        .iter()
70        .enumerate()
71        .find(|&(i, &w)| {
72            w >= m && present[i] && (exact_m || !(FIRST_EXACT_M..=EXACT_M_LAST).contains(&i))
73        })
74        .map(|(i, _)| i)
75}
76
77/// 2026-09-25: Resolved handles for the narrow `w4a16_gemv_batch{M}` family.
78///
79/// A zero handle means "this target did not load that tier"; consumers check
80/// `.0 != 0`, and [`Self::kernel`] returns a zero handle rather than panicking
81/// when nothing in the family covers `m`.
82#[derive(Clone, Copy, Debug)]
83pub struct W4a16BatchmTiers {
84    /// 2026-09-25: Parallel to [`W4A16_BATCHM_WIDTHS`].
85    handles: [KernelHandle; W4A16_BATCHM_WIDTHS.len()],
86    /// 2026-09-25: `w4a16_gemv_batch16`, handed out for 9..=16 rows when the
87    /// row edge exceeds 8: under `gemv_tc::wide_rows_enabled()`, or for a weight
88    /// that declares FP4 activations. `ops::w4a16_gemv_batchm` launches the
89    /// tensor-core kernel instead when `gemv_tc::tc_kernel` returns one.
90    wide: KernelHandle,
91    /// 2026-09-25: `w4a16_gemv_batch32`, handed out above 16 rows within the edge.
92    wide32: KernelHandle,
93    /// 2026-09-28: `w4a4_proj::max_rows` for this backend: the widest the W4A4 kernels serve
94    /// (0 when they are absent or the published tier does not prepare them).
95    a4_rows: u32,
96}
97
98/// 2026-09-25: The "no NVFP4 kernels" state: an all-zero table, for which `kernel`
99/// returns a zero handle at every `m`.
100impl Default for W4a16BatchmTiers {
101    fn default() -> Self {
102        Self {
103            handles: [KernelHandle(0); W4A16_BATCHM_WIDTHS.len()],
104            wide: KernelHandle(0),
105            wide32: KernelHandle(0),
106            a4_rows: 0,
107        }
108    }
109}
110
111impl W4a16BatchmTiers {
112    /// 2026-09-25: Resolve every tier in the family. A missing kernel is a zero
113    /// handle, and `select_tier` skips it.
114    pub fn resolve(gpu: &dyn GpuBackend) -> Self {
115        let mut handles = [KernelHandle(0); W4A16_BATCHM_WIDTHS.len()];
116        for (h, w) in handles.iter_mut().zip(W4A16_BATCHM_WIDTHS) {
117            // 2026-09-25: Width 8 resolves through `batch8_kernel`, which prefers
118            // `w4a16_gemv_batch8_rt2` unless `METRALE_NO_BATCH8_RT=1`.
119            *h = if w == 8 {
120                super::batch8_kernel(gpu)
121            } else {
122                super::try_kernel(gpu, "w4a16_gemv", &format!("w4a16_gemv_batch{w}"))
123            };
124        }
125        let wide = super::try_kernel(gpu, "w4a16_gemv", "w4a16_gemv_batch16");
126        // 2026-09-25: The only call of `w4a4_proj::prepare`, which allocates the
127        // W4A4 activation scratch; `resolve` runs while layers are built.
128        if let Err(e) = crate::layers::ops::w4a4_proj::prepare(gpu) {
129            tracing::warn!("w4a4 scratch/kernels unavailable, projections stay W4A16: {e:#}");
130        }
131        // 2026-09-28: `w4a16_gemv_batch32` is handed out only within a W4A4 edge above 16
132        // rows, so it is resolved only when the published tier can take the W4A4 path.
133        let wide32 = if crate::layers::weight_quantization().uses_w4a4_decode() {
134            super::try_kernel(gpu, "w4a16_gemv", "w4a16_gemv_batch32")
135        } else {
136            KernelHandle(0)
137        };
138        Self {
139            handles,
140            wide,
141            wide32,
142            a4_rows: crate::layers::ops::w4a4_proj::max_rows(gpu),
143        }
144    }
145
146    /// 2026-09-28: Row edge of the narrow projection arms for a weight stamped `act`: the
147    /// W4A4 edge the published tier gives it (`WeightQuantTier::w4a4_rows`), else the W4A16
148    /// edge `gemv_tc::narrow_gemv_max_rows`.
149    fn edge_of(&self, act: metrale_config::Nvfp4Act) -> u32 {
150        let rows = crate::layers::weight_quantization().w4a4_rows(
151            act,
152            super::ops::w4a4_proj::W4A4_MAX_M,
153            super::ops::w4a4_proj::W4A4_WIDE_MAX_M,
154            self.a4_rows,
155        );
156        if rows > 0 {
157            rows
158        } else {
159            super::ops::gemv_tc::narrow_gemv_max_rows()
160        }
161    }
162
163    /// 2026-09-28: The row edge for `w`, from its activation stamp (`edge_of`).
164    pub fn edge(&self, w: &crate::weight_map::QuantizedWeight) -> u32 {
165        self.edge_of(w.act)
166    }
167
168    /// 2026-09-28: `w` declares FP4 activations and this backend has the W4A4 kernels. The
169    /// published tier decides what that means for a dense FFN's narrow steps
170    /// (`WeightQuantTier::ffn_small_batch_w4a4`).
171    pub fn declares_a4(&self, w: &crate::weight_map::QuantizedWeight) -> bool {
172        w.act == metrale_config::Nvfp4Act::A4 && self.a4_rows > 0
173    }
174
175    /// 2026-09-28: [`Self::edge`] capped at `w4a4_proj::W4A4_MAX_M`: the dense FFN's
176    /// narrow arms stop at 32 rows, and 33..=64 go to `forward_prefill`.
177    pub fn ffn_edge(&self, w: &crate::weight_map::QuantizedWeight) -> u32 {
178        self.edge(w).min(super::ops::w4a4_proj::W4A4_MAX_M)
179    }
180
181    /// 2026-09-28: [`Self::kernel`] for a projection over `w`, within [`Self::edge`].
182    pub fn kernel_for(&self, m: u32, w: &crate::weight_map::QuantizedWeight) -> KernelHandle {
183        self.kernel_within(m, self.edge(w))
184    }
185
186    /// 2026-09-25: Which tiers this target resolved: the `present` argument of
187    /// [`select_tier`].
188    fn present(&self) -> [bool; W4A16_BATCHM_WIDTHS.len()] {
189        self.handles.map(|h| h.0 != 0)
190    }
191
192    /// 2026-09-25: Narrowest resolved tier covering `m` rows, or `KernelHandle(0)` when
193    /// this family cannot serve `m`, for a projection without a policy stamp (the lm_head,
194    /// the MTP head): within the edge of an unstamped weight (2026-09-28: the W4A16 edge,
195    /// or under `nvfp4` with `--w4a4-downcast` the lever's edge, as before the tiers).
196    pub fn kernel(&self, m: u32) -> KernelHandle {
197        self.kernel_within(m, self.edge_of(metrale_config::Nvfp4Act::Unstamped))
198    }
199
200    fn kernel_within(&self, m: u32, edge: u32) -> KernelHandle {
201        if m > W4A16_BATCHM_WIDTHS[W4A16_BATCHM_WIDTHS.len() - 1] && m <= edge {
202            // 2026-09-25: 9..=16 → batch16, 17.. → batch32. With the 64-row W4A4
203            // edge this also covers 33..=64, which the W4A4 path serves;
204            // `ops::w4a16_gemv_batchm` refuses more than 32 rows.
205            return if m <= 16 { self.wide } else { self.wide32 };
206        }
207        select_tier(m, self.present(), exact_m_tiers_enabled())
208            .map_or(KernelHandle(0), |i| self.handles[i])
209    }
210
211    /// 2026-09-25: Width of the tier `select_tier` picks for `m` (the narrow
212    /// family only; `kernel` hands out `wide`/`wide32` above 8 rows).
213    pub fn width(&self, m: u32) -> Option<u32> {
214        select_tier(m, self.present(), exact_m_tiers_enabled()).map(|i| W4A16_BATCHM_WIDTHS[i])
215    }
216
217    /// 2026-09-25: Is the base `w4a16_gemv_batch4` tier resolved? Capability probes
218    /// for NVFP4 batched decode ask this one.
219    pub fn has_base(&self) -> bool {
220        self.handles[0].0 != 0
221    }
222}
223
224#[cfg(test)]
225#[path = "w4a16_gemv_tiers_tests.rs"]
226mod w4a16_gemv_tiers_tests;