metrale_model_layers/layers/ops/gemm_quant_gemv.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for the dense GEMVs: BF16 for one row, two rows and M
4//! rows, and FP8-weight for one row.
5//!
6//! Owner: model-layers ops.
7//! Invariants:
8//! - [`dense_gemv_batchm`] returns an error, without launching, when `m` is
9//! outside `1..=DENSE_GEMV_BATCHM_MAX_M`.
10
11use super::*;
12
13/// 2026-09-25: Dense BF16 GEMV for one row, `C = A @ B^T`: A `[1, K]`, B
14/// `[N, K]`, C `[1, N]`. Each 256-thread block computes 4 outputs, 64 threads
15/// per output (kernel `dense_gemv_bf16(A, B, C, N, K)`).
16pub fn dense_gemv(
17 gpu: &dyn GpuBackend,
18 kernel: KernelHandle,
19 input: DevicePtr,
20 weight: &DenseWeight,
21 output: DevicePtr,
22 n: u32,
23 k: u32,
24 stream: u64,
25) -> Result<()> {
26 KernelLaunch::new(gpu, kernel)
27 .grid([div_ceil(n, 4), 1, 1])
28 .block([256, 1, 1])
29 .arg_ptr(input)
30 .arg_ptr(weight.weight)
31 .arg_ptr(output)
32 .arg_u32(n)
33 .arg_u32(k)
34 .launch(stream)
35}
36
37/// 2026-09-25: Dense BF16 GEMV for two rows in one pass over the weight. Each
38/// row follows `dense_gemv_bf16`'s K order and reduction (gb10 kernels).
39///
40/// `input`: `[2, K]` BF16, contiguous; `output`: two rows at
41/// `output + t * out_stride` (BF16 elements). The batched SSM decode uses it
42/// for a BF16 `in_proj_qkvz` (`qwen3_ssm/trait_decode_batched.rs`).
43///
44/// Kernel: `dense_gemv_bf16_batch2(A, B, C, N, K, out_stride)`
45#[allow(clippy::too_many_arguments)]
46pub fn dense_gemv_batch2(
47 gpu: &dyn GpuBackend,
48 kernel: KernelHandle,
49 input: DevicePtr,
50 weight: &DenseWeight,
51 output: DevicePtr,
52 n: u32,
53 k: u32,
54 out_stride: u32,
55 stream: u64,
56) -> Result<()> {
57 KernelLaunch::new(gpu, kernel)
58 .grid([div_ceil(n, 4), 1, 1])
59 .block([256, 1, 1])
60 .arg_ptr(input)
61 .arg_ptr(weight.weight)
62 .arg_ptr(output)
63 .arg_u32(n)
64 .arg_u32(k)
65 .arg_u32(out_stride)
66 .launch(stream)
67}
68
69#[allow(clippy::too_many_arguments)]
70/// 2026-09-25: Rows `dense_gemv_bf16_batchm` computes: `MAX_M` in
71/// `kernels/gb10/common/dense_gemv_bf16_batchm.cu`. The kernel clamps a
72/// larger M, so [`dense_gemv_batchm`] refuses above this value. Each row's
73/// accumulation does not depend on M, so a row's result is the same at every
74/// batch width.
75///
76/// Other readers: the row thresholds of the GLM-5.3 DSA, KDA and MLP dispatch
77/// (`glm5next_dsa/layer.rs`, `glm5next_kda/mod.rs`, `glm5next_mlp/forward.rs`,
78/// `glm5next_layer/levers.rs`), the `verify_k` workspace size
79/// (`glm5_next_load/loader.rs`) and the clamp in
80/// `target_defaults::resolve_batchm_max`.
81pub const DENSE_GEMV_BATCHM_MAX_M: u32 = 16;
82
83/// 2026-09-25: The widest batch that the MTP row dispatch
84/// (`mtp_head/row_dispatch.rs`) and the BF16 arm of the model's
85/// `lm_head_batched` (`impl_a3_lm_head.rs`) hand to `dense_gemv_bf16_batchm`;
86/// `target_defaults::BASELINE_BATCHM_MAX` is set from it. Above it those paths
87/// choose other kernels, some of which accumulate in a different order, so
88/// this value decides which bits a decode of a given width produces.
89pub const DENSE_GEMV_BATCHM_DECODE_MAX_M: u32 = 8;
90
91/// 2026-09-25: Dense BF16 GEMV for M rows, `C[t] = A[t] @ B^T` for `t` in
92/// `[0, M)`, reading the weight once for all rows. Each row follows
93/// `dense_gemv_bf16`'s K order and reduction (gb10 kernels).
94///
95/// `input`: `[M, K]` BF16, contiguous; `output`: M rows at
96/// `output + t * out_stride` (BF16 elements). Refuses `m` outside
97/// `1..=DENSE_GEMV_BATCHM_MAX_M`.
98///
99/// Kernel: `dense_gemv_bf16_batchm(A, B, C, M, N, K, out_stride)`
100pub fn dense_gemv_batchm(
101 gpu: &dyn GpuBackend,
102 kernel: KernelHandle,
103 input: DevicePtr,
104 weight: &DenseWeight,
105 output: DevicePtr,
106 m: u32,
107 n: u32,
108 k: u32,
109 out_stride: u32,
110 stream: u64,
111) -> Result<()> {
112 // 2026-09-25: The kernel clamps M to its MAX_M instead of failing, which
113 // would leave the rows past it unwritten.
114 ensure!(
115 (1..=DENSE_GEMV_BATCHM_MAX_M).contains(&m),
116 "dense_gemv_batchm: m={m} outside 1..={DENSE_GEMV_BATCHM_MAX_M} \
117 (kernel MAX_M clamps silently; use dense_gemm_tc for wider batches)"
118 );
119 KernelLaunch::new(gpu, kernel)
120 .grid([div_ceil(n, 4), 1, 1])
121 .block([256, 1, 1])
122 .arg_ptr(input)
123 .arg_ptr(weight.weight)
124 .arg_ptr(output)
125 .arg_u32(m)
126 .arg_u32(n)
127 .arg_u32(k)
128 .arg_u32(out_stride)
129 .launch(stream)
130}
131
132/// 2026-09-27: [`dense_gemv_batchm`] over `m` rows split into `y_blocks` block
133/// rows of `ceil(m / y_blocks)` rows each (at most `DENSE_GEMV_BATCHM_MAX_M`):
134/// more blocks for a narrow weight such as the MoE router, and one launch past
135/// 16 rows. Each row's result is the same as in any other split.
136#[allow(clippy::too_many_arguments)]
137pub fn dense_gemv_batchm_split(
138 gpu: &dyn GpuBackend,
139 kernel: KernelHandle,
140 input: DevicePtr,
141 weight: &DenseWeight,
142 output: DevicePtr,
143 m: u32,
144 y_blocks: u32,
145 n: u32,
146 k: u32,
147 out_stride: u32,
148 stream: u64,
149) -> Result<()> {
150 ensure!(
151 m >= 1 && (1..=m).contains(&y_blocks) && m.div_ceil(y_blocks) <= DENSE_GEMV_BATCHM_MAX_M,
152 "dense_gemv_batchm_split: m={m} over y_blocks={y_blocks} must give 1..={DENSE_GEMV_BATCHM_MAX_M} rows per block row"
153 );
154 KernelLaunch::new(gpu, kernel)
155 .grid([div_ceil(n, 4), y_blocks, 1])
156 .block([256, 1, 1])
157 .arg_ptr(input)
158 .arg_ptr(weight.weight)
159 .arg_ptr(output)
160 .arg_u32(m)
161 .arg_u32(n)
162 .arg_u32(k)
163 .arg_u32(out_stride)
164 .launch(stream)
165}
166
167/// 2026-09-25: FP8-weight GEMV for one row,
168/// `C = A @ (dequant(B_fp8) * row_scale)^T`: A `[1, K]` BF16, B `[N, K]` FP8
169/// E4M3, row_scale `[N]` f32, C `[1, N]` BF16. One byte per weight, against
170/// two for [`dense_gemv`]. Each 256-thread block computes 4 outputs, 64
171/// threads per output (kernel `dense_gemv_fp8w(A, B, row_scale, C, N, K)`).
172pub fn dense_gemv_fp8w(
173 gpu: &dyn GpuBackend,
174 kernel: KernelHandle,
175 input: DevicePtr,
176 weight: &Fp8DenseWeight,
177 output: DevicePtr,
178 n: u32,
179 k: u32,
180 stream: u64,
181) -> Result<()> {
182 KernelLaunch::new(gpu, kernel)
183 .grid([div_ceil(n, 4), 1, 1])
184 .block([256, 1, 1])
185 .arg_ptr(input)
186 .arg_ptr(weight.weight)
187 .arg_ptr(weight.row_scale)
188 .arg_ptr(output)
189 .arg_u32(n)
190 .arg_u32(k)
191 .launch(stream)
192}