metrale_model_layers/layers/ops/
fp8_gemv_batch.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for the batched FP8-weight GEMVs: the row-scaled
4//! `dense_gemv_fp8w_batch2` and `fp8_gemv_rowscale_batch{8,16}_rt2`, and the
5//! block-scaled `w8a16_gemv_batch*` family.
6//!
7//! Owner: model-layers ops.
8//! Invariants:
9//! - The `fp8_gemv_rowscale_batch*_rt2` and `w8a16_gemv_batch{4,16}*`
10//!   launchers return an error, without launching, when `m` is outside
11//!   `1..=MAX_M` of their kernel.
12
13use anyhow::{Result, ensure};
14use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
15use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
16
17use crate::weight_map::Fp8DenseWeight;
18
19/// 2026-09-25: Register-tiled row-scaled FP8 GEMV for up to 8 rows, two
20/// outputs per 64-lane group (kernel `fp8_gemv_rowscale_batch8_rt2`, module
21/// `fp8_gemv_rt`). `input` `[M, K]` BF16, `output` `[M, N]` BF16; the kernel
22/// applies `row_scale[n]` at write-out. The DFlash drafter's FP8 path calls it
23/// (`dflash_head/forward_block.rs`). Refuses `m` outside `1..=8` and a `k`
24/// that is not a multiple of 16.
25#[allow(clippy::too_many_arguments)]
26pub fn fp8_gemv_rowscale_batch8_rt2(
27    gpu: &dyn GpuBackend,
28    kernel: KernelHandle,
29    input: DevicePtr,
30    weight: &Fp8DenseWeight,
31    output: DevicePtr,
32    m: u32,
33    n: u32,
34    k: u32,
35    stream: u64,
36) -> Result<()> {
37    ensure!(
38        (1..=8).contains(&m),
39        "fp8_gemv_rowscale_batch8_rt2: m={m} outside 1..=8 (kernel MAX_M)"
40    );
41    ensure!(
42        k.is_multiple_of(16),
43        "fp8_gemv_rowscale_batch8_rt2: K={k} not a multiple of 16"
44    );
45    KernelLaunch::new(gpu, kernel)
46        .grid([div_ceil(n, 8), 1, 1])
47        .block([256, 1, 1])
48        .arg_ptr(input)
49        .arg_ptr(weight.weight)
50        .arg_ptr(weight.row_scale)
51        .arg_ptr(output)
52        .arg_u32(m)
53        .arg_u32(n)
54        .arg_u32(k)
55        .launch(stream)
56}
57
58/// 2026-09-25: The 16-row instantiation of the same kernel template
59/// (`fp8_gemv_rowscale_batch16_rt2`, module `fp8_gemv_rt`), with the launch
60/// geometry of [`fp8_gemv_rowscale_batch8_rt2`]. Refuses `m` outside
61/// `1..=16` and a `k` that is not a multiple of 16.
62#[allow(clippy::too_many_arguments)]
63pub fn fp8_gemv_rowscale_batch16_rt2(
64    gpu: &dyn GpuBackend,
65    kernel: KernelHandle,
66    input: DevicePtr,
67    weight: &Fp8DenseWeight,
68    output: DevicePtr,
69    m: u32,
70    n: u32,
71    k: u32,
72    stream: u64,
73) -> Result<()> {
74    ensure!(
75        (1..=16).contains(&m),
76        "fp8_gemv_rowscale_batch16_rt2: m={m} outside 1..=16 (kernel MAX_M)"
77    );
78    ensure!(
79        k.is_multiple_of(16),
80        "fp8_gemv_rowscale_batch16_rt2: K={k} not a multiple of 16"
81    );
82    KernelLaunch::new(gpu, kernel)
83        .grid([div_ceil(n, 8), 1, 1])
84        .block([256, 1, 1])
85        .arg_ptr(input)
86        .arg_ptr(weight.weight)
87        .arg_ptr(weight.row_scale)
88        .arg_ptr(output)
89        .arg_u32(m)
90        .arg_u32(n)
91        .arg_u32(k)
92        .launch(stream)
93}
94
95/// 2026-09-25: FP8-weight GEMV for two rows in one pass over the weight:
96/// `input` `[2, K]` BF16, `output` `[2, N]` BF16, scaled by
97/// `weight.row_scale`. Not bit-identical to two `dense_gemv_fp8w` launches:
98/// the kernel adds four products to the accumulator at once, where
99/// `dense_gemv_fp8w` adds each product separately.
100pub fn dense_gemv_fp8w_batch2(
101    gpu: &dyn GpuBackend,
102    kernel: KernelHandle,
103    input: DevicePtr,
104    weight: &Fp8DenseWeight,
105    output: DevicePtr,
106    n: u32,
107    k: u32,
108    stream: u64,
109) -> Result<()> {
110    KernelLaunch::new(gpu, kernel)
111        .grid([div_ceil(n, 4), 1, 1])
112        .block([256, 1, 1])
113        .arg_ptr(input)
114        .arg_ptr(weight.weight)
115        .arg_ptr(weight.row_scale)
116        .arg_ptr(output)
117        .arg_u32(n)
118        .arg_u32(k)
119        .launch(stream)
120}
121
122/// 2026-09-25: The signature of the contiguous batched GEMV launchers, such as
123/// [`w8a16_gemv_batch4`] and [`w8a16_gemv_batch16`], so a caller that picks a
124/// tier by row count can hold the launcher and its handle as one pair.
125pub type ContiguousBatchGemv = fn(
126    &dyn GpuBackend,
127    KernelHandle,
128    DevicePtr,
129    DevicePtr,
130    DevicePtr,
131    DevicePtr,
132    u32,
133    u32,
134    u32,
135    u64,
136) -> Result<()>;
137
138/// 2026-09-25: Block-scaled FP8 GEMV for up to 4 rows
139/// (`w8a16_gemv_batchm_impl<4>`); one pass over the weight serves every row.
140/// `input` `[M, K]` BF16, `output` `[M, N]` BF16; `weight` `[N, K]` FP8 E4M3
141/// and `block_scale` `[N/128, K/128]` FP32, as for `w8a16_gemv`.
142///
143/// Per row, the K order and the separate FP32 additions match `w8a16_gemv`
144/// (gb10 kernels); `w8a16_batch_bitparity_microtest` compares the two byte
145/// for byte.
146///
147/// Refuses `m > 4`: the kernel computes only rows below its MAX_M and never
148/// writes the rest. [`w8a16_gemv_batch16`] takes the same arguments for
149/// 5..=16 rows.
150#[allow(clippy::too_many_arguments)]
151pub fn w8a16_gemv_batch4(
152    gpu: &dyn GpuBackend,
153    kernel: KernelHandle,
154    input: DevicePtr,
155    weight: DevicePtr,
156    block_scale: DevicePtr,
157    output: DevicePtr,
158    m: u32,
159    n: u32,
160    k: u32,
161    stream: u64,
162) -> Result<()> {
163    ensure!(
164        (1..=4).contains(&m),
165        "w8a16_gemv_batch4: m={m} outside 1..=4 (kernel MAX_M; use w8a16_gemv_batch16)"
166    );
167    contiguous_batch_launch(
168        gpu,
169        kernel,
170        input,
171        weight,
172        block_scale,
173        output,
174        m,
175        n,
176        k,
177        stream,
178    )
179}
180
181/// 2026-09-25: The 16-row instantiation of the same template (kernel
182/// `w8a16_gemv_batch16`, module `w8a16_gemv_batch4`), with the launch
183/// geometry and per-row numerics of [`w8a16_gemv_batch4`].
184#[allow(clippy::too_many_arguments)]
185pub fn w8a16_gemv_batch16(
186    gpu: &dyn GpuBackend,
187    kernel: KernelHandle,
188    input: DevicePtr,
189    weight: DevicePtr,
190    block_scale: DevicePtr,
191    output: DevicePtr,
192    m: u32,
193    n: u32,
194    k: u32,
195    stream: u64,
196) -> Result<()> {
197    ensure!(
198        (1..=16).contains(&m),
199        "w8a16_gemv_batch16: m={m} outside 1..=16 (kernel MAX_M)"
200    );
201    contiguous_batch_launch(
202        gpu,
203        kernel,
204        input,
205        weight,
206        block_scale,
207        output,
208        m,
209        n,
210        k,
211        stream,
212    )
213}
214
215/// 2026-09-25: Launch body of the two contiguous entry points; they differ
216/// only in the MAX_M bound they check.
217#[allow(clippy::too_many_arguments)]
218fn contiguous_batch_launch(
219    gpu: &dyn GpuBackend,
220    kernel: KernelHandle,
221    input: DevicePtr,
222    weight: DevicePtr,
223    block_scale: DevicePtr,
224    output: DevicePtr,
225    m: u32,
226    n: u32,
227    k: u32,
228    stream: u64,
229) -> Result<()> {
230    KernelLaunch::new(gpu, kernel)
231        .grid([div_ceil(n, 4), 1, 1])
232        .block([256, 1, 1])
233        .arg_ptr(input)
234        .arg_ptr(weight)
235        .arg_ptr(block_scale)
236        .arg_ptr(output)
237        .arg_u32(m)
238        .arg_u32(n)
239        .arg_u32(k)
240        .launch(stream)
241}
242
243/// 2026-09-25: Launches a batch-2 block-scaled FP8 GEMV. No kernel under
244/// `kernels/` defines a `w8a16_gemv_batch2` entry point.
245#[allow(clippy::too_many_arguments)]
246pub fn w8a16_gemv_batch2(
247    gpu: &dyn GpuBackend,
248    kernel: KernelHandle,
249    input: DevicePtr,
250    weight: DevicePtr,
251    block_scale: DevicePtr,
252    output: DevicePtr,
253    n: u32,
254    k: u32,
255    stream: u64,
256) -> Result<()> {
257    KernelLaunch::new(gpu, kernel)
258        .grid([div_ceil(n, 4), 1, 1])
259        .block([256, 1, 1])
260        .arg_ptr(input)
261        .arg_ptr(weight)
262        .arg_ptr(block_scale)
263        .arg_ptr(output)
264        .arg_u32(n)
265        .arg_u32(k)
266        .launch(stream)
267}
268
269/// 2026-09-25: Strided variant of [`w8a16_gemv_batch4`] (up to 4 rows). It
270/// reads and writes rows at caller-given pitches, so one projection of the
271/// multi-sequence decode QKV buffer `[n, per_seq_qkv]` is written for every
272/// row in one launch.
273///
274/// Layout: `input` `[M, a_row_stride]` BF16, of which the first `k` elements
275/// of each row are read; `weight`/`block_scale` as for `w8a16_gemv`
276/// (`[N, K]` FP8 E4M3 and `[N/128, K/128]` FP32); `output` `[M, c_row_stride]`
277/// BF16, of which the first `n` elements of each row are written. Strides are
278/// in elements. The launcher refuses pitches that do not cover `k` and `n`,
279/// and an `a_row_stride` that is not a multiple of 8 (the kernel loads
280/// activations as 16-byte `uint4`).
281///
282/// Per-row numerics as [`w8a16_gemv_batch4`]; `native_fp8_qkv_batch_microtest`
283/// compares it with `w8a16_gemv`.
284///
285/// Kernel: `w8a16_gemv_batch4_strided` (module `w8a16_gemv_batch4`).
286#[allow(clippy::too_many_arguments)]
287pub fn w8a16_gemv_batch4_strided(
288    gpu: &dyn GpuBackend,
289    kernel: KernelHandle,
290    input: DevicePtr,
291    weight: DevicePtr,
292    block_scale: DevicePtr,
293    output: DevicePtr,
294    m: u32,
295    n: u32,
296    k: u32,
297    a_row_stride: u32,
298    c_row_stride: u32,
299    stream: u64,
300) -> Result<()> {
301    ensure!(
302        (1..=4).contains(&m),
303        "w8a16_gemv_batch4_strided: m={m} outside 1..=4 (kernel MAX_M)"
304    );
305    strided_batch_launch(
306        gpu,
307        kernel,
308        input,
309        weight,
310        block_scale,
311        output,
312        m,
313        n,
314        k,
315        a_row_stride,
316        c_row_stride,
317        stream,
318    )
319}
320
321/// 2026-09-25: The 16-row variant of [`w8a16_gemv_batch4_strided`] (kernel
322/// `w8a16_gemv_batch16_strided`, module `w8a16_gemv_batch4`), with the same
323/// launch geometry.
324#[allow(clippy::too_many_arguments)]
325pub fn w8a16_gemv_batch16_strided(
326    gpu: &dyn GpuBackend,
327    kernel: KernelHandle,
328    input: DevicePtr,
329    weight: DevicePtr,
330    block_scale: DevicePtr,
331    output: DevicePtr,
332    m: u32,
333    n: u32,
334    k: u32,
335    a_row_stride: u32,
336    c_row_stride: u32,
337    stream: u64,
338) -> Result<()> {
339    ensure!(
340        (1..=16).contains(&m),
341        "w8a16_gemv_batch16_strided: m={m} outside 1..=16 (kernel MAX_M)"
342    );
343    strided_batch_launch(
344        gpu,
345        kernel,
346        input,
347        weight,
348        block_scale,
349        output,
350        m,
351        n,
352        k,
353        a_row_stride,
354        c_row_stride,
355        stream,
356    )
357}
358
359/// 2026-09-25: Launch body of the two strided entry points; they differ only
360/// in the MAX_M bound they check.
361#[allow(clippy::too_many_arguments)]
362fn strided_batch_launch(
363    gpu: &dyn GpuBackend,
364    kernel: KernelHandle,
365    input: DevicePtr,
366    weight: DevicePtr,
367    block_scale: DevicePtr,
368    output: DevicePtr,
369    m: u32,
370    n: u32,
371    k: u32,
372    a_row_stride: u32,
373    c_row_stride: u32,
374    stream: u64,
375) -> Result<()> {
376    ensure!(
377        a_row_stride >= k && c_row_stride >= n,
378        "w8a16_gemv batch strided: row pitches (a={a_row_stride}, c={c_row_stride}) \
379         must cover the used extents (k={k}, n={n})"
380    );
381    ensure!(
382        a_row_stride.is_multiple_of(8),
383        "w8a16_gemv batch strided: a_row_stride={a_row_stride} must keep rows \
384         16B-aligned (uint4 activation loads)"
385    );
386    KernelLaunch::new(gpu, kernel)
387        .grid([div_ceil(n, 4), 1, 1])
388        .block([256, 1, 1])
389        .arg_ptr(input)
390        .arg_ptr(weight)
391        .arg_ptr(block_scale)
392        .arg_ptr(output)
393        .arg_u32(m)
394        .arg_u32(n)
395        .arg_u32(k)
396        .arg_u32(a_row_stride)
397        .arg_u32(c_row_stride)
398        .launch(stream)
399}