metrale_storage/high_speed_swap/
impl_more.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `HighSpeedSwap` offload and attention. `offload_block*` writes a
4//! KV block to the tier, one K and one V group per kv_head, and can project its
5//! K for the predictor; `attend_layer*` streams a sequence's blocks through the
6//! scratch pool into tiled attention, one tile of `resident_blocks` at a time.
7//!
8//! Owner: metrale-storage high-speed swap.
9//! Invariants:
10//! - A successful offload removes the block's scratch-pool copy, so a later
11//!   attend reads the block from the tier.
12
13use anyhow::Result;
14use std::ffi::c_void;
15
16use super::HighSpeedSwap;
17use crate::backend::{ReadRequest, StorageBackend};
18use crate::config::HighSpeedSwapConfig;
19use crate::cuda_min::{CudaCtx, copy_d_to_h_async, copy_h_to_d_async, stream_sync};
20use crate::group::{GroupKey, KvKind};
21use crate::predictor::Predictor;
22use crate::scratch_pool::{ResidentKey, ScratchPool};
23
24impl HighSpeedSwap {
25    /// 2026-09-25: Write a KV block to the tier and project its K for the
26    /// predictor. `k_block_dev`, `k_block_host` and `v_block_host` are BF16
27    /// `[block_size, num_kv_heads, head_dim]`; the device K feeds the projection,
28    /// the host copies the per-kv_head groups.
29    pub fn offload_block(
30        &mut self,
31        ctx: &CudaCtx,
32        layer: u32,
33        block: u32,
34        k_block_dev: u64,
35        k_block_host: &[half::bf16],
36        v_block_host: &[half::bf16],
37    ) -> Result<()> {
38        self.offload_block_on_stream(
39            ctx.stream,
40            layer,
41            block,
42            k_block_dev,
43            k_block_host,
44            v_block_host,
45        )
46    }
47
48    /// 2026-09-25: `offload_block` on `stream`, which must belong to the current
49    /// thread's CUDA context.
50    pub fn offload_block_on_stream(
51        &mut self,
52        stream: u64,
53        layer: u32,
54        block: u32,
55        k_block_dev: u64,
56        k_block_host: &[half::bf16],
57        v_block_host: &[half::bf16],
58    ) -> Result<()> {
59        // 2026-09-25: `true` runs the projection, which reads `k_block_dev` as
60        // BF16; callers with another K layout use
61        // `offload_block_no_predict_on_stream`.
62        self.offload_block_inner_on_stream(
63            stream,
64            layer,
65            block,
66            k_block_dev,
67            k_block_host,
68            v_block_host,
69            true,
70        )
71    }
72
73    /// 2026-09-25: `offload_block_on_stream` without the predictor projection,
74    /// for callers whose device K is not BF16. The host buffers are still BF16.
75    pub fn offload_block_no_predict_on_stream(
76        &mut self,
77        stream: u64,
78        layer: u32,
79        block: u32,
80        k_block_host: &[half::bf16],
81        v_block_host: &[half::bf16],
82    ) -> Result<()> {
83        self.offload_block_inner_on_stream(
84            stream,
85            layer,
86            block,
87            0,
88            k_block_host,
89            v_block_host,
90            false,
91        )
92    }
93
94    #[allow(clippy::too_many_arguments)]
95    fn offload_block_inner_on_stream(
96        &mut self,
97        stream: u64,
98        layer: u32,
99        block: u32,
100        k_block_dev: u64,
101        k_block_host: &[half::bf16],
102        v_block_host: &[half::bf16],
103        do_predict: bool,
104    ) -> Result<()> {
105        if do_predict {
106            self.predictor.project_kv_block_on_stream(
107                stream,
108                layer as usize,
109                block as usize,
110                k_block_dev,
111            )?;
112        }
113        let bs = self.model.block_size as usize;
114        let nkv = self.model.num_kv_heads as usize;
115        let hd = self.model.head_dim as usize;
116        if k_block_host.len() != bs * nkv * hd || v_block_host.len() != bs * nkv * hd {
117            anyhow::bail!(
118                "offload_block: host buffers must be {} BF16 elements",
119                bs * nkv * hd
120            );
121        }
122        for kh in 0..nkv {
123            let mut k_stripe = Vec::with_capacity(bs * hd * 2);
124            let mut v_stripe = Vec::with_capacity(bs * hd * 2);
125            for tok in 0..bs {
126                let base = (tok * nkv + kh) * hd;
127                for x in &k_block_host[base..base + hd] {
128                    k_stripe.extend_from_slice(&x.to_le_bytes());
129                }
130                for x in &v_block_host[base..base + hd] {
131                    v_stripe.extend_from_slice(&x.to_le_bytes());
132                }
133            }
134            self.backend
135                .write_from_host(GroupKey::new(layer, block, kh as u16, KvKind::K), &k_stripe)?;
136            self.backend
137                .write_from_host(GroupKey::new(layer, block, kh as u16, KvKind::V), &v_stripe)?;
138        }
139        // 2026-09-25: The tier copy was just rewritten; drop any scratch-pool copy
140        // so attention does not read a stale slot.
141        self.pool.invalidate(ResidentKey { layer, block });
142        Ok(())
143    }
144
145    /// 2026-09-25: Streaming attention for one (layer, sequence). `q_dev` is the
146    /// step's BF16 query `[num_q_heads × head_dim]`, `seq_block_ids` the
147    /// sequence's blocks, and `output_dev` receives the BF16 output
148    /// `[num_q_heads × head_dim]`.
149    pub fn attend_layer(
150        &mut self,
151        ctx: &CudaCtx,
152        layer: u32,
153        seq_block_ids: &[u32],
154        q_dev: u64,
155        output_dev: u64,
156    ) -> Result<()> {
157        self.attend_layer_on_stream(ctx.stream, layer, seq_block_ids, q_dev, output_dev)
158    }
159
160    /// 2026-09-25: `attend_layer` on `stream`, which must belong to the current
161    /// thread's CUDA context, with no causal mask: every slot of the last block
162    /// is attended. A query that must not see later tokens of its own block uses
163    /// `attend_layer_on_stream_with_q_pos`.
164    pub fn attend_layer_on_stream(
165        &mut self,
166        stream: u64,
167        layer: u32,
168        seq_block_ids: &[u32],
169        q_dev: u64,
170        output_dev: u64,
171    ) -> Result<()> {
172        let bs = self.model.block_size as i32;
173        self.attend_layer_on_stream_with_q_pos(stream, layer, seq_block_ids, q_dev, output_dev, bs)
174    }
175
176    /// 2026-09-25: Only the first `last_block_valid_slots` token slots of the
177    /// last block in `seq_block_ids` are attended; for a query at position
178    /// `q_pos`, pass `(q_pos % block_size) + 1`.
179    pub fn attend_layer_on_stream_with_q_pos(
180        &mut self,
181        stream: u64,
182        layer: u32,
183        seq_block_ids: &[u32],
184        q_dev: u64,
185        output_dev: u64,
186        last_block_valid_slots: i32,
187    ) -> Result<()> {
188        // 2026-09-25: All `max_blocks_per_layer` blocks of the layer are scored;
189        // only the sequence's blocks are used.
190        self.predictor
191            .project_q_on_stream(stream, q_dev, self.q_proj.ptr)?;
192        let m = &self.model;
193        let layer_a_g = self.predictor.a_g_dev_ptr()
194            + (layer as u64)
195                * (m.max_blocks_per_layer as u64)
196                * (m.num_kv_heads as u64)
197                * (m.block_size as u64)
198                * (self.cfg.rank as u64)
199                * 2;
200        self.predictor.score_blocks_on_stream(
201            stream,
202            self.q_proj.ptr,
203            layer_a_g,
204            self.block_scores_dev.ptr,
205            m.max_blocks_per_layer as usize,
206        )?;
207        copy_d_to_h_async(
208            self.score_host_buf.as_mut_ptr() as *mut c_void,
209            self.block_scores_dev.ptr,
210            self.score_host_buf.len() * 4,
211            stream,
212        )?;
213        stream_sync(stream)?;
214
215        self.attn.begin_step_on_stream(stream, 1)?;
216        let tile_cap = self.cfg.resident_blocks as usize;
217        let mut tile_idx = 0;
218        while tile_idx < seq_block_ids.len() {
219            let tile_end = (tile_idx + tile_cap).min(seq_block_ids.len());
220            let tile = &seq_block_ids[tile_idx..tile_end];
221
222            let mut block_table = vec![0_i32; tile_cap];
223            let mut pinned: Vec<u32> = Vec::new();
224            let mut missing: Vec<u32> = Vec::new();
225            for (i, &blk) in tile.iter().enumerate() {
226                let key = ResidentKey { layer, block: blk };
227                if let Some(slot) = self.pool.lookup(key) {
228                    block_table[i] = slot as i32;
229                    pinned.push(slot);
230                    self.eviction.touch(slot);
231                } else {
232                    missing.push(blk);
233                }
234            }
235            let mut reqs: Vec<ReadRequest> = Vec::new();
236            for &blk in &missing {
237                let key = ResidentKey { layer, block: blk };
238                let candidates = self.eviction.rank(&pinned);
239                let slot = self.pool.assign(key, &candidates)?;
240                pinned.push(slot);
241                self.eviction.touch(slot);
242                self.eviction
243                    .record_score(slot, self.score_host_buf[blk as usize]);
244                let idx = tile.iter().position(|&x| x == blk).unwrap();
245                block_table[idx] = slot as i32;
246                for kh in 0..self.model.num_kv_heads {
247                    reqs.push(ReadRequest {
248                        group: GroupKey::new(layer, blk, kh, KvKind::K),
249                        dst_dev_ptr: self.pool.slot_k_ptr(slot, kh),
250                    });
251                    reqs.push(ReadRequest {
252                        group: GroupKey::new(layer, blk, kh, KvKind::V),
253                        dst_dev_ptr: self.pool.slot_v_ptr(slot, kh),
254                    });
255                }
256            }
257            self.backend.read(&reqs, stream)?;
258
259            let counts = [(tile.len()) as i32];
260            copy_h_to_d_async(
261                self.block_table_dev.ptr,
262                block_table.as_ptr() as *const c_void,
263                tile_cap * 4,
264                stream,
265            )?;
266            copy_h_to_d_async(
267                self.counts_dev.ptr,
268                counts.as_ptr() as *const c_void,
269                4,
270                stream,
271            )?;
272            let (s_blk, s_tok, s_kvh) = self.attn.scratch_pool_strides();
273            let v_off = (self.model.num_kv_heads as u64)
274                * (self.model.block_size as u64)
275                * (self.model.head_dim as u64)
276                * 2;
277            // 2026-09-25: The mask applies only to the tile that holds the
278            // sequence's last block.
279            let lbvs = if tile_end == seq_block_ids.len() {
280                last_block_valid_slots
281            } else {
282                self.model.block_size as i32
283            };
284            self.attn.step_tile_on_stream(
285                stream,
286                q_dev,
287                self.pool.pool_dev_ptr(),
288                self.pool.pool_dev_ptr() + v_off,
289                self.block_table_dev.ptr,
290                self.counts_dev.ptr,
291                1,
292                s_blk,
293                s_tok,
294                s_kvh,
295                lbvs,
296            )?;
297            tile_idx = tile_end;
298        }
299        self.attn.finalize_on_stream(stream, output_dev, 1)?;
300        Ok(())
301    }
302
303    pub fn pool(&self) -> &ScratchPool {
304        &self.pool
305    }
306    pub fn predictor(&self) -> &Predictor {
307        &self.predictor
308    }
309    pub fn config(&self) -> &HighSpeedSwapConfig {
310        &self.cfg
311    }
312}