metrale_storage/
tiled_attention.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Host side of the tiled decode attention
4//! (`kernels/paged_decode_attn_tiled.cu`, `kernels/attention_finalize.cu` in
5//! this crate). A decode step is `begin_step`, which sets the FP32 running
6//! state (m, l, o) to (-inf, 0, 0); one `step_tile` per tile of KV blocks; and
7//! `finalize`, which writes `o / l` (0 where `l` is 0) as BF16.
8//!
9//! Owner: storage (high-speed swap).
10//! Invariants: the (m, l, o) buffers are allocated once in `new` for
11//! `dims.max_seqs` sequences and reused by every step.
12
13use anyhow::{Context, Result, bail};
14use std::ffi::c_void;
15
16use crate::cuda_min::{CudaCtx, CudaModule, DeviceBuffer, launch_kernel};
17
18include!(concat!(env!("OUT_DIR"), "/storage_ptx.rs"));
19
20unsafe extern "C" {
21    fn cuMemsetD32Async(dst: u64, value: u32, count: usize, stream: u64) -> i32;
22}
23
24#[derive(Clone, Copy, Debug)]
25pub struct TiledAttentionDims {
26    pub max_seqs: usize,
27    pub num_q_heads: usize,
28    pub num_kv_heads: usize,
29    pub head_dim: usize,
30    pub block_size: usize,
31    pub tile_capacity: usize,
32}
33
34impl TiledAttentionDims {
35    pub fn validate(&self) -> Result<()> {
36        if !self.num_q_heads.is_multiple_of(self.num_kv_heads) {
37            bail!(
38                "num_q_heads ({}) must divide num_kv_heads ({})",
39                self.num_q_heads,
40                self.num_kv_heads
41            );
42        }
43        if self.head_dim > 256 {
44            bail!("head_dim {} exceeds MAX_HEAD_DIM=256", self.head_dim);
45        }
46        Ok(())
47    }
48    pub fn gqa_ratio(&self) -> i32 {
49        (self.num_q_heads / self.num_kv_heads) as i32
50    }
51    fn n_q_slots(&self) -> usize {
52        self.max_seqs * self.num_q_heads
53    }
54    pub fn m_bytes(&self) -> usize {
55        self.n_q_slots() * 4
56    }
57    pub fn l_bytes(&self) -> usize {
58        self.n_q_slots() * 4
59    }
60    pub fn o_bytes(&self) -> usize {
61        self.n_q_slots() * self.head_dim * 4
62    }
63    pub fn output_bytes(&self) -> usize {
64        self.n_q_slots() * self.head_dim * 2
65    }
66}
67
68pub struct TiledAttention {
69    dims: TiledAttentionDims,
70    _modules: Vec<CudaModule>,
71    f_step: u64,
72    f_finalize: u64,
73    pub m_state: DeviceBuffer,
74    pub l_state: DeviceBuffer,
75    pub o_state: DeviceBuffer,
76}
77
78const NEG_INF_F32_BITS: u32 = 0xFF800000;
79
80impl TiledAttention {
81    pub fn new(dims: TiledAttentionDims) -> Result<Self> {
82        dims.validate()?;
83        let mut modules = Vec::new();
84        let mut f_step = 0u64;
85        let mut f_finalize = 0u64;
86        for entry in STORAGE_PTX.iter() {
87            match entry.name {
88                "paged_decode_attn_tiled" => {
89                    let m = CudaModule::from_ptx(entry.ptx)
90                        .with_context(|| format!("load {}", entry.name))?;
91                    f_step = m.function("paged_decode_attn_tiled")?;
92                    modules.push(m);
93                }
94                "attention_finalize" => {
95                    let m = CudaModule::from_ptx(entry.ptx)
96                        .with_context(|| format!("load {}", entry.name))?;
97                    f_finalize = m.function("attention_finalize")?;
98                    modules.push(m);
99                }
100                _ => {}
101            }
102        }
103        if f_step == 0 || f_finalize == 0 {
104            bail!("tiled-attention PTX modules missing");
105        }
106        let m_state = DeviceBuffer::new(dims.m_bytes())?;
107        let l_state = DeviceBuffer::new(dims.l_bytes())?;
108        let o_state = DeviceBuffer::new(dims.o_bytes())?;
109        Ok(Self {
110            dims,
111            _modules: modules,
112            f_step,
113            f_finalize,
114            m_state,
115            l_state,
116            o_state,
117        })
118    }
119
120    /// 2026-09-25: Set (m, l, o) of the first `num_seqs` sequences to
121    /// (-inf, 0, 0) on `ctx.stream`, before the step's first `step_tile`.
122    ///
123    /// # Errors
124    /// `num_seqs > max_seqs`, or a `cuMemsetD32Async` fails.
125    pub fn begin_step(&self, ctx: &CudaCtx, num_seqs: usize) -> Result<()> {
126        self.begin_step_on_stream(ctx.stream, num_seqs)
127    }
128
129    pub fn begin_step_on_stream(&self, stream: u64, num_seqs: usize) -> Result<()> {
130        if num_seqs > self.dims.max_seqs {
131            bail!(
132                "begin_step num_seqs {} > max_seqs {}",
133                num_seqs,
134                self.dims.max_seqs
135            );
136        }
137        let n_q = num_seqs * self.dims.num_q_heads;
138        let n_o = n_q * self.dims.head_dim;
139        unsafe {
140            let s = cuMemsetD32Async(self.m_state.ptr, NEG_INF_F32_BITS, n_q, stream);
141            if s != 0 {
142                bail!("cuMemsetD32Async m_state failed: {s}");
143            }
144            let s = cuMemsetD32Async(self.l_state.ptr, 0, n_q, stream);
145            if s != 0 {
146                bail!("cuMemsetD32Async l_state failed: {s}");
147            }
148            let s = cuMemsetD32Async(self.o_state.ptr, 0, n_o, stream);
149            if s != 0 {
150                bail!("cuMemsetD32Async o_state failed: {s}");
151            }
152        }
153        Ok(())
154    }
155
156    /// 2026-09-25: `(blk, tok, kvh)` strides, in BF16 elements, of the paged
157    /// K/V layout `[num_blocks, block_size, num_kv_heads, head_dim]`.
158    pub fn paged_strides(&self) -> (i64, i64, i64) {
159        let blk = (self.dims.block_size * self.dims.num_kv_heads * self.dims.head_dim) as i64;
160        let tok = (self.dims.num_kv_heads * self.dims.head_dim) as i64;
161        let kvh = self.dims.head_dim as i64;
162        (blk, tok, kvh)
163    }
164
165    /// 2026-09-25: `(blk, tok, kvh)` strides, in BF16 elements, of the scratch
166    /// pool slot layout `[slot, K|V, kv_head, block_size, head_dim]`.
167    pub fn scratch_pool_strides(&self) -> (i64, i64, i64) {
168        let kv_stripe = (self.dims.num_kv_heads * self.dims.block_size * self.dims.head_dim) as i64;
169        let blk = 2 * kv_stripe;
170        let tok = self.dims.head_dim as i64;
171        let kvh = (self.dims.block_size * self.dims.head_dim) as i64;
172        (blk, tok, kvh)
173    }
174
175    /// 2026-09-25: One tile of KV blocks for `num_seqs` sequences, folded into
176    /// the running state. The strides select the K/V layout; see
177    /// [`paged_strides`](Self::paged_strides) and
178    /// [`scratch_pool_strides`](Self::scratch_pool_strides).
179    #[allow(clippy::too_many_arguments)]
180    pub fn step_tile(
181        &self,
182        ctx: &CudaCtx,
183        q: u64,
184        k_pool: u64,
185        v_pool: u64,
186        tile_blocks: u64,
187        tile_block_counts: u64,
188        num_seqs: usize,
189        blk_stride: i64,
190        tok_stride: i64,
191        kvh_stride: i64,
192        last_block_valid_slots: i32,
193    ) -> Result<()> {
194        self.step_tile_on_stream(
195            ctx.stream,
196            q,
197            k_pool,
198            v_pool,
199            tile_blocks,
200            tile_block_counts,
201            num_seqs,
202            blk_stride,
203            tok_stride,
204            kvh_stride,
205            last_block_valid_slots,
206        )
207    }
208
209    #[allow(clippy::too_many_arguments)]
210    pub fn step_tile_on_stream(
211        &self,
212        stream: u64,
213        q: u64,
214        k_pool: u64,
215        v_pool: u64,
216        tile_blocks: u64,
217        tile_block_counts: u64,
218        num_seqs: usize,
219        blk_stride: i64,
220        tok_stride: i64,
221        kvh_stride: i64,
222        last_block_valid_slots: i32,
223    ) -> Result<()> {
224        let mut q_v = q;
225        let mut k_v = k_pool;
226        let mut v_v = v_pool;
227        let mut tb = tile_blocks;
228        let mut tc = tile_block_counts;
229        let mut m_v = self.m_state.ptr;
230        let mut l_v = self.l_state.ptr;
231        let mut o_v = self.o_state.ptr;
232        let mut nq = self.dims.num_q_heads as i32;
233        let mut nk = self.dims.num_kv_heads as i32;
234        let mut hd = self.dims.head_dim as i32;
235        let mut bs = self.dims.block_size as i32;
236        let mut tcap = self.dims.tile_capacity as i32;
237        let mut gqa = self.dims.gqa_ratio();
238        let mut blk_s = blk_stride;
239        let mut tok_s = tok_stride;
240        let mut kvh_s = kvh_stride;
241        let mut lbvs = last_block_valid_slots;
242        let mut params = [
243            &mut q_v as *mut _ as *mut c_void,
244            &mut k_v as *mut _ as *mut c_void,
245            &mut v_v as *mut _ as *mut c_void,
246            &mut tb as *mut _ as *mut c_void,
247            &mut tc as *mut _ as *mut c_void,
248            &mut m_v as *mut _ as *mut c_void,
249            &mut l_v as *mut _ as *mut c_void,
250            &mut o_v as *mut _ as *mut c_void,
251            &mut nq as *mut _ as *mut c_void,
252            &mut nk as *mut _ as *mut c_void,
253            &mut hd as *mut _ as *mut c_void,
254            &mut bs as *mut _ as *mut c_void,
255            &mut tcap as *mut _ as *mut c_void,
256            &mut gqa as *mut _ as *mut c_void,
257            &mut blk_s as *mut _ as *mut c_void,
258            &mut tok_s as *mut _ as *mut c_void,
259            &mut kvh_s as *mut _ as *mut c_void,
260            &mut lbvs as *mut _ as *mut c_void,
261        ];
262        launch_kernel(
263            self.f_step,
264            (num_seqs as u32, self.dims.num_q_heads as u32, 1),
265            (self.dims.head_dim as u32, 1, 1),
266            0,
267            stream,
268            &mut params,
269        )
270    }
271
272    /// 2026-09-25: Write `o_state / l_state` for `num_seqs` sequences to
273    /// `output` as BF16, 0 where `l` is 0.
274    pub fn finalize(&self, ctx: &CudaCtx, output: u64, num_seqs: usize) -> Result<()> {
275        self.finalize_on_stream(ctx.stream, output, num_seqs)
276    }
277
278    pub fn finalize_on_stream(&self, stream: u64, output: u64, num_seqs: usize) -> Result<()> {
279        let mut l_v = self.l_state.ptr;
280        let mut o_v = self.o_state.ptr;
281        let mut out_v = output;
282        let mut nq = self.dims.num_q_heads as i32;
283        let mut hd = self.dims.head_dim as i32;
284        let mut params = [
285            &mut l_v as *mut _ as *mut c_void,
286            &mut o_v as *mut _ as *mut c_void,
287            &mut out_v as *mut _ as *mut c_void,
288            &mut nq as *mut _ as *mut c_void,
289            &mut hd as *mut _ as *mut c_void,
290        ];
291        launch_kernel(
292            self.f_finalize,
293            (num_seqs as u32, self.dims.num_q_heads as u32, 1),
294            (self.dims.head_dim as u32, 1, 1),
295            0,
296            stream,
297            &mut params,
298        )
299    }
300
301    pub fn dims(&self) -> TiledAttentionDims {
302        self.dims
303    }
304}