metrale_model_layers/layers/ops/
fp8_act_quant.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The per-token FP8 activation quantizers a layer can launch, and
4//! the grid each one needs.
5//!
6//! Owner: model-layers ops.
7//! Invariants:
8//! - [`Fp8ActQuant::pick_with`] takes the kernel handle and the grid from the
9//!   same `twin` flag, so a pick never pairs one kernel with the other's grid.
10//!
11//! The shared kernel (`kernels/gb10/common/per_token_group_quant_fp8.cu`) runs
12//! one 128-thread CTA per 128-element K-group. The Hopper twin
13//! (`kernels/hopper/common/fp8_act_quant_hopper.cu`) gives each group 16
14//! threads that load one `uint4` each, and packs 8 groups into a CTA. Only
15//! `kernels/hopper` carries the twin; on other targets its handle is 0 and the
16//! shared kernel runs. `fp8_act_quant_floor.rs` decides when the twin runs.
17
18use metrale_gpu_runtime::gpu::{GpuBackend, KernelHandle};
19
20use super::Fp8QuantPick;
21
22/// 2026-09-25: Module of the shared quantizer; the file and the kernel share
23/// this name.
24pub const FP8_QUANT_MODULE: &str = "per_token_group_quant_fp8";
25/// 2026-09-25: Entry point of the shared quantizer.
26pub const FP8_QUANT_ENTRY: &str = "per_token_group_quant_fp8";
27/// 2026-09-25: Module (file stem) of the Hopper twin.
28pub const FP8_QUANT_HOPPER_MODULE: &str = "fp8_act_quant_hopper";
29/// 2026-09-25: Entry point of the Hopper twin. It differs from the shared
30/// entry point, so a Hopper image carries both kernels.
31pub const FP8_QUANT_HOPPER_ENTRY: &str = "per_token_group_quant_fp8_hopper";
32
33/// 2026-09-25: K-groups one Hopper CTA covers: 128 threads / 16 threads per
34/// group, as `HQ_GROUPS_PER_CTA` in the `.cu`. The kernel derives its group
35/// span from `gridDim.y`, so this value sets only the grid's Y extent.
36pub const FP8_QUANT_HOPPER_GROUPS_PER_CTA: u32 = 8;
37
38/// 2026-09-25: The FP8 activation quantizer a layer launches: the shared
39/// kernel, plus the Hopper twin when the image has one.
40#[derive(Clone, Copy, Debug)]
41pub struct Fp8ActQuant {
42    /// 2026-09-25: `per_token_group_quant_fp8`, or `KernelHandle(0)` when the
43    /// image lacks it.
44    pub shared: KernelHandle,
45    /// 2026-09-25: `per_token_group_quant_fp8_hopper`, or `KernelHandle(0)`
46    /// when the image lacks it.
47    pub hopper: KernelHandle,
48}
49
50impl Default for Fp8ActQuant {
51    /// 2026-09-25: No quantizer: both handles are 0.
52    fn default() -> Self {
53        Self {
54            shared: KernelHandle(0),
55            hopper: KernelHandle(0),
56        }
57    }
58}
59
60impl Fp8ActQuant {
61    /// 2026-09-25: Probe both kernels; a missing one resolves to
62    /// `KernelHandle(0)`.
63    pub fn resolve(gpu: &dyn GpuBackend) -> Self {
64        Self {
65            shared: crate::layers::try_kernel(gpu, FP8_QUANT_MODULE, FP8_QUANT_ENTRY),
66            hopper: crate::layers::try_target_kernel(
67                gpu,
68                FP8_QUANT_HOPPER_MODULE,
69                FP8_QUANT_HOPPER_ENTRY,
70            ),
71        }
72    }
73
74    /// 2026-09-25: A quantizer with only the shared kernel.
75    pub fn shared_only(shared: KernelHandle) -> Self {
76        Self {
77            shared,
78            hopper: KernelHandle(0),
79        }
80    }
81
82    /// 2026-09-25: Is there a quantizer to launch at all? True when either
83    /// handle is loaded, whatever the width; [`Self::pick`] is the per-launch
84    /// choice.
85    pub fn available(&self) -> bool {
86        self.shared.0 != 0 || self.hopper.0 != 0
87    }
88
89    /// 2026-09-25: Is the twin in this image? Whether a launch uses it is
90    /// [`Fp8QuantPick::twin`].
91    pub fn twin_present(&self) -> bool {
92        self.hopper.0 != 0
93    }
94
95    /// 2026-09-25: Which kernel this `(m, k)` launches, on which grid, and why
96    /// not the other, from the resolved `fp8_act_quant_hopper` lever and the
97    /// compiled target's SM count. [`Self::pick_with`] is the pure form.
98    pub fn pick(&self, m: u32, k: u32) -> Fp8QuantPick {
99        self.pick_with(
100            super::fp8_act_quant_hopper_enabled(),
101            m,
102            k,
103            metrale_kernels::TARGET_SM_COUNT,
104        )
105    }
106
107    /// 2026-09-25: [`Self::pick`] over an explicit lever value and SM count.
108    /// Pure, so CPU tests can grade any `(m, k)`.
109    pub fn pick_with(&self, requested: bool, m: u32, k: u32, sm_count: u32) -> Fp8QuantPick {
110        let reject = super::fp8_act_quant_hopper_reject(
111            requested,
112            self.twin_present(),
113            self.shared.0 != 0,
114            m,
115            k,
116            sm_count,
117        );
118        let twin = reject.is_none();
119        Fp8QuantPick {
120            kernel: if twin { self.hopper } else { self.shared },
121            grid: fp8_quant_grid(twin, m, k),
122            twin,
123            reject,
124            requested,
125        }
126    }
127
128    /// 2026-09-25: The handle this `(m, k)` launches.
129    pub fn kernel(&self, m: u32, k: u32) -> KernelHandle {
130        self.pick(m, k).kernel
131    }
132
133    /// 2026-09-25: The grid for `(m, k)`, from the same pick as
134    /// [`Self::kernel`].
135    pub fn grid(&self, m: u32, k: u32) -> [u32; 3] {
136        self.pick(m, k).grid
137    }
138}
139
140/// 2026-09-25: Grid for one quantizer launch. Pure, so
141/// `fp8_act_quant_tests.rs` checks both arms without a GPU.
142///
143/// `M` is on grid X in both arms: X allows 2^31-1 blocks, Y only 65535.
144///
145/// Shared arm: `(M, K/128, 1)`, one CTA per K-group; the kernel reads the
146/// group from `blockIdx.y`.
147///
148/// Hopper arm: `(M, max(1, ceil(K/128 / 8)), 1)`. The kernel derives its group
149/// span as `ceil(L / gridDim.y)`, so any Y in `1..=L` covers every group
150/// exactly once; the Y extent is a speed choice.
151pub fn fp8_quant_grid(hopper: bool, m: u32, k: u32) -> [u32; 3] {
152    let groups = k / 128;
153    if hopper {
154        [
155            m,
156            groups.div_ceil(FP8_QUANT_HOPPER_GROUPS_PER_CTA).max(1),
157            1,
158        ]
159    } else {
160        [m, groups, 1]
161    }
162}
163
164/// 2026-09-25: The half-open K-group range one Hopper CTA owns, computed as
165/// the `.cu` computes `gpc`/`g0`/`g_end`. `fp8_act_quant_tests.rs` uses it to
166/// check on the host that the grid's Y extent and the kernel's span partition
167/// the groups.
168pub fn fp8_quant_hopper_span(groups: u32, grid_y: u32, block_y: u32) -> (u32, u32) {
169    let gpc = groups.div_ceil(grid_y);
170    let g0 = block_y * gpc;
171    if g0 >= groups {
172        return (groups, groups);
173    }
174    (g0, (g0 + gpc).min(groups))
175}
176
177/// 2026-09-25: The group slot thread `tid` of a Hopper CTA works on, and the
178/// half-open range `[start, end)` of 8 elements it owns within that
179/// 128-element group, as `sub`/`lane` in the `.cu`. `None` when the slot is
180/// past the CTA's span.
181pub fn fp8_quant_hopper_lane(tid: u32, span: u32) -> Option<(u32, u32, u32)> {
182    const LANES: u32 = 16;
183    const ELEMS: u32 = 8;
184    let sub = tid / LANES;
185    let lane = tid % LANES;
186    if sub >= span {
187        return None;
188    }
189    Some((sub, lane * ELEMS, lane * ELEMS + ELEMS))
190}
191
192#[cfg(test)]
193#[path = "fp8_act_quant_tests.rs"]
194mod fp8_act_quant_tests;