metrale_model_layers/layers/ops/
kv_cache.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for paged KV-cache writes (BF16 and FP8, and the fused K norm + RoPE writes), plus the BF16-K/Turbo-V and MLA submodules.
4//!
5//! Owner: model-layers ops.
6//! Invariants:
7//! - `slot_mapping` is an `i64` array with one entry per token, `block * block_size + offset`;
8//!   every cache-write kernel launched here skips a token whose slot is negative
9//!   (kernels/gb10/common/reshape_and_cache.cu, fused_k_norm_rope_cache.cu,
10//!   reshape_and_cache_fused_k_fp8.cu).
11//! - The paged cache layout is `[num_blocks, block_size, num_kv_heads, head_dim]`.
12
13#![allow(unused_imports)]
14
15use anyhow::Result;
16use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
17use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
18
19use crate::layers::moe;
20use crate::weight_map::{DenseWeight, Fp8DenseWeight, Fp8Weight, QuantizedWeight};
21
22use super::*;
23#[path = "kv_cache_bf16k_turbov.rs"]
24mod bf16k_turbov;
25pub use bf16k_turbov::{
26    paged_decode_attn_bf16k_turbo2v, paged_decode_attn_bf16k_turbo3v,
27    paged_decode_attn_bf16k_turbo4v, reshape_and_cache_bf16k_turbo2v,
28    reshape_and_cache_bf16k_turbo3v, reshape_and_cache_bf16k_turbo4v,
29};
30#[path = "kv_cache_mla.rs"]
31mod mla;
32pub use mla::{
33    mla_batched_gemv, mla_cache_assemble, mla_paged_decode_fp8, mla_paged_decode_nvfp4,
34    mla_q_rope_scatter, mla_q_rope_writeback,
35};
36
37/// 2026-09-25: Fill `count` slot-mapping entries on the device from a block table:
38/// `slots[i] = block_table[(start_pos + i) / block_size] * block_size + (start_pos + i) %
39/// block_size`. `count == 0` launches nothing.
40pub fn fill_slots_from_block_table(
41    gpu: &dyn GpuBackend,
42    kernel: KernelHandle,
43    slots: DevicePtr,
44    block_table: DevicePtr,
45    start_pos: u32,
46    count: u32,
47    block_size: u32,
48    stream: u64,
49) -> Result<()> {
50    if count == 0 {
51        return Ok(());
52    }
53    KernelLaunch::new(gpu, kernel)
54        .grid([div_ceil(count, 256), 1, 1])
55        .block([256, 1, 1])
56        .arg_ptr(slots)
57        .arg_ptr(block_table)
58        .arg_u32(start_pos)
59        .arg_u32(count)
60        .arg_u32(block_size)
61        .launch(stream)
62}
63
64/// 2026-09-25: Copy BF16 K and V rows into the paged BF16 cache at `slot_mapping` (kernel
65/// `reshape_and_cache_flash`), one block per token. `key_stride` / `value_stride` are the row
66/// strides in elements. `_cache_stride` is not passed to the kernel, which derives the block
67/// stride from `block_size`.
68#[allow(clippy::too_many_arguments)]
69pub fn reshape_and_cache(
70    gpu: &dyn GpuBackend,
71    kernel: KernelHandle,
72    key: DevicePtr,
73    value: DevicePtr,
74    k_cache: DevicePtr,
75    v_cache: DevicePtr,
76    slot_mapping: DevicePtr,
77    num_tokens: u32,
78    num_kv_heads: u32,
79    head_dim: u32,
80    block_size: u32,
81    key_stride: u32,
82    value_stride: u32,
83    _cache_stride: u64,
84    stream: u64,
85) -> Result<()> {
86    KernelLaunch::new(gpu, kernel)
87        .grid([num_tokens, 1, 1])
88        .block([256, 1, 1])
89        .arg_ptr(key)
90        .arg_ptr(value)
91        .arg_ptr(k_cache)
92        .arg_ptr(v_cache)
93        .arg_ptr(slot_mapping)
94        .arg_u32(num_kv_heads)
95        .arg_u32(head_dim)
96        .arg_u32(block_size)
97        .arg_u32(key_stride)
98        .arg_u32(value_stride)
99        .launch(stream)
100}
101
102/// 2026-09-25: Copy only V into the paged BF16 cache (kernel `reshape_and_cache_flash_v_only`),
103/// for callers whose K is written by a `fused_k_norm_rope_cache_write_*` launch; a
104/// [`reshape_and_cache`] there would overwrite that K.
105#[allow(clippy::too_many_arguments)]
106pub fn reshape_and_cache_flash_v_only(
107    gpu: &dyn GpuBackend,
108    kernel: KernelHandle,
109    value: DevicePtr,
110    v_cache: DevicePtr,
111    slot_mapping: DevicePtr,
112    num_tokens: u32,
113    num_kv_heads: u32,
114    head_dim: u32,
115    block_size: u32,
116    value_stride: u32,
117    stream: u64,
118) -> Result<()> {
119    KernelLaunch::new(gpu, kernel)
120        .grid([num_tokens, 1, 1])
121        .block([256, 1, 1])
122        .arg_ptr(value)
123        .arg_ptr(v_cache)
124        .arg_ptr(slot_mapping)
125        .arg_u32(num_kv_heads)
126        .arg_u32(head_dim)
127        .arg_u32(block_size)
128        .arg_u32(value_stride)
129        .launch(stream)
130}
131
132/// 2026-09-25: K norm (`x * rsqrt(mean(x^2) + eps) * (1 + w)`), RoPE and the paged BF16 cache
133/// write in one kernel. K stays in FP32 from the BF16 load to the single BF16 rounding at the
134/// store. One block per (token, KV head) with `head_dim` threads; the kernel supports
135/// `head_dim <= 256`.
136#[allow(clippy::too_many_arguments)]
137pub fn fused_k_norm_rope_cache_write_bf16(
138    gpu: &dyn GpuBackend,
139    kernel: KernelHandle,
140    k_in: DevicePtr,
141    k_norm_weight: DevicePtr,
142    positions: DevicePtr,
143    k_cache: DevicePtr,
144    slot_mapping: DevicePtr,
145    num_tokens: u32,
146    num_kv_heads: u32,
147    head_dim: u32,
148    rotary_dim: u32,
149    block_size: u32,
150    rms_eps: f32,
151    theta: f32,
152    stream: u64,
153) -> Result<()> {
154    KernelLaunch::new(gpu, kernel)
155        .grid([num_tokens, num_kv_heads, 1])
156        .block([head_dim, 1, 1])
157        .arg_ptr(k_in)
158        .arg_ptr(k_norm_weight)
159        .arg_ptr(positions)
160        .arg_ptr(k_cache)
161        .arg_ptr(slot_mapping)
162        .arg_u32(num_kv_heads)
163        .arg_u32(head_dim)
164        .arg_u32(rotary_dim)
165        .arg_u32(block_size)
166        .arg_f32(rms_eps)
167        .arg_f32(theta)
168        .launch(stream)
169}
170
171/// 2026-09-25: The interleaved-MRoPE variant (kernel `fused_k_norm_rope_mrope_cache_write_bf16`):
172/// rotation pair `i` takes its position from `pos_t`, `pos_h` or `pos_w` by `i % 3`. The rest of
173/// the kernel is the scalar-position one, so equal position streams give its results.
174#[allow(clippy::too_many_arguments)]
175pub fn fused_k_norm_rope_cache_write_bf16_mrope(
176    gpu: &dyn GpuBackend,
177    kernel: KernelHandle,
178    k_in: DevicePtr,
179    k_norm_weight: DevicePtr,
180    pos_t: DevicePtr,
181    pos_h: DevicePtr,
182    pos_w: DevicePtr,
183    k_cache: DevicePtr,
184    slot_mapping: DevicePtr,
185    num_tokens: u32,
186    num_kv_heads: u32,
187    head_dim: u32,
188    rotary_dim: u32,
189    block_size: u32,
190    rms_eps: f32,
191    theta: f32,
192    stream: u64,
193) -> Result<()> {
194    KernelLaunch::new(gpu, kernel)
195        .grid([num_tokens, num_kv_heads, 1])
196        .block([head_dim, 1, 1])
197        .arg_ptr(k_in)
198        .arg_ptr(k_norm_weight)
199        .arg_ptr(pos_t)
200        .arg_ptr(pos_h)
201        .arg_ptr(pos_w)
202        .arg_ptr(k_cache)
203        .arg_ptr(slot_mapping)
204        .arg_u32(num_kv_heads)
205        .arg_u32(head_dim)
206        .arg_u32(rotary_dim)
207        .arg_u32(block_size)
208        .arg_f32(rms_eps)
209        .arg_f32(theta)
210        .launch(stream)
211}
212
213/// 2026-09-25: [`fused_k_norm_rope_cache_write_bf16`] writing an FP8 E4M3 cache. The FP32 result
214/// times `inv_scale` (`1 / k_scale`) goes through one saturating FP8 cast, with no BF16 rounding.
215#[allow(clippy::too_many_arguments)]
216pub fn fused_k_norm_rope_cache_write_fp8(
217    gpu: &dyn GpuBackend,
218    kernel: KernelHandle,
219    k_in: DevicePtr,
220    k_norm_weight: DevicePtr,
221    positions: DevicePtr,
222    k_cache_fp8: DevicePtr,
223    slot_mapping: DevicePtr,
224    num_tokens: u32,
225    num_kv_heads: u32,
226    head_dim: u32,
227    rotary_dim: u32,
228    block_size: u32,
229    rms_eps: f32,
230    theta: f32,
231    inv_scale: f32,
232    stream: u64,
233) -> Result<()> {
234    KernelLaunch::new(gpu, kernel)
235        .grid([num_tokens, num_kv_heads, 1])
236        .block([head_dim, 1, 1])
237        .arg_ptr(k_in)
238        .arg_ptr(k_norm_weight)
239        .arg_ptr(positions)
240        .arg_ptr(k_cache_fp8)
241        .arg_ptr(slot_mapping)
242        .arg_u32(num_kv_heads)
243        .arg_u32(head_dim)
244        .arg_u32(rotary_dim)
245        .arg_u32(block_size)
246        .arg_f32(rms_eps)
247        .arg_f32(theta)
248        .arg_f32(inv_scale)
249        .launch(stream)
250}
251
252/// 2026-09-25: Decode-path fusion of `rms_norm` + `rope_forward` + `reshape_and_cache_flash_fp8`
253/// for an FP8 KV cache: writes K (normed, rotated, quantized) and V (quantized) in one launch,
254/// leaving `rope_forward` for Q only.
255///
256/// The kernel rounds to BF16 at each point where that unfused chain stores BF16, rather than
257/// keeping FP32 intermediates, so that it writes the chain's cache bytes (see the header of
258/// kernels/gb10/common/reshape_and_cache_fused_k_fp8.cu). Removing a rounding step would change
259/// those bytes.
260///
261/// `k_scale` / `v_scale` are dequant scales; the kernel computes `1.0f / scale` itself, as
262/// `reshape_and_cache_flash_fp8` does. `head_dim` threads per block, one block per (token, KV
263/// head).
264#[allow(clippy::too_many_arguments)]
265pub fn fused_k_norm_rope_cache_write_fp8_kv(
266    gpu: &dyn GpuBackend,
267    kernel: KernelHandle,
268    k_in: DevicePtr,
269    value: DevicePtr,
270    k_norm_weight: DevicePtr,
271    positions: DevicePtr,
272    k_cache: DevicePtr,
273    v_cache: DevicePtr,
274    slot_mapping: DevicePtr,
275    num_tokens: u32,
276    num_kv_heads: u32,
277    head_dim: u32,
278    rotary_dim: u32,
279    block_size: u32,
280    k_scale: f32,
281    v_scale: f32,
282    key_stride: u32,
283    value_stride: u32,
284    cache_stride: u64,
285    rms_eps: f32,
286    theta: f32,
287    stream: u64,
288) -> Result<()> {
289    KernelLaunch::new(gpu, kernel)
290        .grid([num_tokens, num_kv_heads, 1])
291        .block([head_dim, 1, 1])
292        .arg_ptr(k_in)
293        .arg_ptr(value)
294        .arg_ptr(k_norm_weight)
295        .arg_ptr(positions)
296        .arg_ptr(k_cache)
297        .arg_ptr(v_cache)
298        .arg_ptr(slot_mapping)
299        .arg_u32(num_kv_heads)
300        .arg_u32(head_dim)
301        .arg_u32(rotary_dim)
302        .arg_u32(block_size)
303        .arg_f32(k_scale)
304        .arg_f32(v_scale)
305        .arg_u32(key_stride)
306        .arg_u32(value_stride)
307        .arg_u64(cache_stride)
308        .arg_f32(rms_eps)
309        .arg_f32(theta)
310        .launch(stream)
311}
312
313/// 2026-09-25: Quantize BF16 K and V rows into the paged FP8 cache (kernel
314/// `reshape_and_cache_flash_fp8`): `fp8 = sat(bf16 / scale)` with the dequant scales
315/// `k_scale` / `v_scale`. `k_cache` / `v_cache` are the pool base pointers, and `cache_stride` is
316/// the stride between cache blocks in elements (normally `block_size * num_kv_heads * head_dim`).
317pub fn reshape_and_cache_fp8(
318    gpu: &dyn GpuBackend,
319    kernel: KernelHandle,
320    key: DevicePtr,
321    value: DevicePtr,
322    k_cache: DevicePtr,
323    v_cache: DevicePtr,
324    slot_mapping: DevicePtr,
325    num_tokens: u32,
326    num_kv_heads: u32,
327    head_dim: u32,
328    block_size: u32,
329    k_scale: f32,
330    v_scale: f32,
331    key_stride: u32,
332    value_stride: u32,
333    cache_stride: u64,
334    stream: u64,
335) -> Result<()> {
336    KernelLaunch::new(gpu, kernel)
337        .grid([num_tokens, 1, 1])
338        .block([256, 1, 1])
339        .arg_ptr(key)
340        .arg_ptr(value)
341        .arg_ptr(k_cache)
342        .arg_ptr(v_cache)
343        .arg_ptr(slot_mapping)
344        .arg_u32(num_kv_heads)
345        .arg_u32(head_dim)
346        .arg_u32(block_size)
347        .arg_f32(k_scale)
348        .arg_f32(v_scale)
349        .arg_u32(key_stride)
350        .arg_u32(value_stride)
351        .arg_u64(cache_stride)
352        .launch(stream)
353}