metrale_gpu_runtime/cublaslt/
scale_layout.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Index math for the FP32 scale tensors of the block-scaled
4//! FP8 GEMM ([`super::fp8_gemm_act_weight_t_blkscaled`]): the activation
5//! scales in the layout it passes to cuBLASLt, in the quantizer's layout, and
6//! the weight-scale stride check.
7//!
8//! In cuBLASLt's terms the GEMM is `D[N,M] = opT(weightᶜ[K,N]) ·
9//! opN(actᶜ[K,M])`, so the library's M is the weight's N and its N is the
10//! token count. The activation (B, `VEC128_32F`) scales are laid out
11//! `[L, m_pad]`, token index contiguous, with `L = ceil(K/128)`; the
12//! quantizer `per_token_group_quant_fp8` writes `[M, L]`, K group
13//! contiguous. The weight (A, `BLK128x128_32F`) scales are the checkpoint's
14//! `[ceil(N/128), L]` grid, used as they are when `L` is a multiple of 4.
15//! The layouts follow the cuBLAS manual section "128-element 1D and 128x128
16//! 2D Block Scaling For FP8 Data Types".
17//!
18//! Measured 2026-09-11 on 1xH100 (`native_fp8_ffn_w8a8_microtest`, cuBLASLt
19//! 13.1): with the quantizer's `[M, L]` scales passed as B, the GEMM
20//! disagreed with the in-tree `fp8_gemm_t_blockscaled` on the same inputs,
21//! rel_rms 1.1e-2 at M=64 and 7.7e-2 to 8.8e-2 at M=1193.
22//!
23//! The metal build compiles this file too (`cublaslt_metal_stub.rs`).
24//!
25//! Owner: gpu-runtime (cuBLASLt wrapper).
26//! Invariants: none beyond the types.
27
28/// 2026-09-25: Number of 128-wide K groups in `k`: `L = ceil(k / 128)`.
29#[must_use]
30pub fn k_groups(k: usize) -> usize {
31    k.div_ceil(128)
32}
33
34/// 2026-09-25: Offset of the scale for `(token, k_group)` in the B-operand
35/// layout `[L, m_pad]`, token index contiguous. The CUDA adapter
36/// `fp8_act_scale_to_kmajor` (`kernels/gb10/common/fp8_scale_transpose.cu`)
37/// writes the same index.
38#[must_use]
39pub const fn vec128_b_index(m_pad: usize, token: usize, k_group: usize) -> usize {
40    k_group * m_pad + token
41}
42
43/// 2026-09-25: Offset of the same `(token, k_group)` scale in the layout
44/// `per_token_group_quant_fp8` writes: row-major `[M, L]`, K group
45/// contiguous. `fp8_gemm_t_blockscaled` reads this layout.
46#[must_use]
47pub const fn rowmajor_index(l: usize, token: usize, k_group: usize) -> usize {
48    token * l + k_group
49}
50
51/// 2026-09-25: FP32 element count of the B-operand scale tensor:
52/// `m_pad * L`.
53#[must_use]
54pub fn vec128_b_elems(m_pad: usize, k: usize) -> usize {
55    m_pad * k_groups(k)
56}
57
58/// 2026-09-25: Whether `L = ceil(k / 128)` is a multiple of 4, the condition
59/// under which the checkpoint's row-major weight-scale grid is passed to the
60/// `BLK128x128_32F` operand unchanged. The cuBLASLt dispatch gates check it
61/// (`dense_ffn_w8a8_prefill.rs`, `dispatch_proj_decode.rs`).
62#[must_use]
63pub fn blk128x128_stride_ok(k: usize) -> bool {
64    k_groups(k).is_multiple_of(4)
65}
66
67/// 2026-09-25: CPU reference for the `fp8_act_scale_to_kmajor` kernel: read
68/// row-major `[m, l]` scales and write `[l, m_pad]`, with the pad tokens
69/// `m..m_pad` set to 0.0, as the kernel does.
70#[must_use]
71pub fn act_scale_rowmajor_to_kmajor(src: &[f32], m: usize, m_pad: usize, l: usize) -> Vec<f32> {
72    debug_assert!(m_pad >= m, "pad cannot shrink the token extent");
73    debug_assert!(src.len() >= m * l, "source holds fewer than m x l scales");
74    let mut dst = vec![0.0f32; vec128_b_elems(m_pad, l * 128)];
75    for token in 0..m {
76        for kg in 0..l {
77            dst[vec128_b_index(m_pad, token, kg)] = src[rowmajor_index(l, token, kg)];
78        }
79    }
80    dst
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    /// 2026-09-25: Hand-placed values: `src` is row-major `[M=3, L=2]`, and
88    /// the output groups tokens by K group, with one zero pad token each.
89    #[test]
90    fn kmajor_adapter_transposes_and_zero_fills_the_pad() {
91        let src = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
92        let dst = act_scale_rowmajor_to_kmajor(&src, 3, 4, 2);
93        assert_eq!(dst, vec![1.0, 3.0, 5.0, 0.0, 2.0, 4.0, 6.0, 0.0]);
94    }
95
96    #[test]
97    fn adapter_is_identity_free_only_when_one_dimension_is_trivial() {
98        // 2026-09-25: With one K group, or one token, the two layouts
99        // coincide.
100        let src = [7.0f32, 8.0, 9.0];
101        assert_eq!(
102            act_scale_rowmajor_to_kmajor(&src, 3, 3, 1),
103            vec![7.0, 8.0, 9.0]
104        );
105        assert_eq!(
106            act_scale_rowmajor_to_kmajor(&src, 1, 1, 3),
107            vec![7.0, 8.0, 9.0]
108        );
109    }
110
111    #[test]
112    fn the_two_readings_disagree_at_the_measured_shape() {
113        // 2026-09-25: At M=64, K=5120 (L=40) the two offsets agree only where
114        // 63*kg == 39*token, i.e. at 4 of the 2560 scales.
115        let (m_pad, l) = (64usize, 40usize);
116        let same = (0..m_pad)
117            .flat_map(|token| (0..l).map(move |kg| (token, kg)))
118            .filter(|(token, kg)| {
119                vec128_b_index(m_pad, *token, *kg) == rowmajor_index(l, *token, *kg)
120            })
121            .count();
122        assert_eq!(same, 4, "only (0,0), (21,13), (42,26), (63,39) coincide");
123        assert_eq!(m_pad * l - same, 2556);
124    }
125
126    #[test]
127    fn documented_extents_match_the_shapes_we_serve() {
128        // 2026-09-25: K = 5120 and 17408, the `H` and `INTER` of
129        // `dense_ffn_w8a8_prefill_tests.rs`.
130        assert_eq!(k_groups(5120), 40);
131        assert_eq!(k_groups(17408), 136);
132        assert!(blk128x128_stride_ok(5120));
133        assert!(blk128x128_stride_ok(17408));
134        assert!(!blk128x128_stride_ok(128 * 5));
135        assert_eq!(vec128_b_elems(1200, 5120), 1200 * 40);
136    }
137}