spark_storage/high_speed_swap/
impl_more.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//
3//! Additional `HighSpeedSwap` methods (offload + attention orchestration).
4
5use anyhow::Result;
6use std::ffi::c_void;
7
8use super::HighSpeedSwap;
9use crate::backend::{ReadRequest, StorageBackend};
10use crate::config::HighSpeedSwapConfig;
11use crate::cuda_min::{CudaCtx, copy_d_to_h_async, copy_h_to_d_async, stream_sync};
12use crate::group::{GroupKey, KvKind};
13use crate::predictor::Predictor;
14use crate::scratch_pool::{ResidentKey, ScratchPool};
15
16impl HighSpeedSwap {
17    /// Persist a freshly-written KV block to disk and update the predictor's
18    /// per-block K_lr. K block layout is `[block_size, num_kv_heads, head_dim]`
19    /// BF16 in both `*_dev` (used for projection) and `*_host` (used for the
20    /// per-(kv_head) disk stripe).
21    pub fn offload_block(
22        &mut self,
23        ctx: &CudaCtx,
24        layer: u32,
25        block: u32,
26        k_block_dev: u64,
27        k_block_host: &[half::bf16],
28        v_block_host: &[half::bf16],
29    ) -> Result<()> {
30        self.offload_block_on_stream(
31            ctx.stream,
32            layer,
33            block,
34            k_block_dev,
35            k_block_host,
36            v_block_host,
37        )
38    }
39
40    /// Stream-only variant for production callers (spark-model decode path).
41    /// `stream` must already be bound to the current thread's CUDA context.
42    pub fn offload_block_on_stream(
43        &mut self,
44        stream: u64,
45        layer: u32,
46        block: u32,
47        k_block_dev: u64,
48        k_block_host: &[half::bf16],
49        v_block_host: &[half::bf16],
50    ) -> Result<()> {
51        // True when the production HBM buffer at `k_block_dev` is BF16-laid-out;
52        // the predictor's project_kv_block kernel reads it as BF16. Non-BF16
53        // callers must use `offload_block_no_predict_on_stream`.
54        self.offload_block_inner_on_stream(
55            stream,
56            layer,
57            block,
58            k_block_dev,
59            k_block_host,
60            v_block_host,
61            true,
62        )
63    }
64
65    /// FP8/quantized callers: identical to `offload_block_on_stream` but skips
66    /// the predictor's per-block K projection (since `k_block_dev` is not
67    /// BF16-laid-out — running the BF16 kernel on it would OOB-read into
68    /// adjacent blocks). Eviction policy degrades to LRU-only for these
69    /// blocks; correctness is preserved.
70    pub fn offload_block_no_predict_on_stream(
71        &mut self,
72        stream: u64,
73        layer: u32,
74        block: u32,
75        k_block_host: &[half::bf16],
76        v_block_host: &[half::bf16],
77    ) -> Result<()> {
78        self.offload_block_inner_on_stream(
79            stream,
80            layer,
81            block,
82            0,
83            k_block_host,
84            v_block_host,
85            false,
86        )
87    }
88
89    #[allow(clippy::too_many_arguments)]
90    fn offload_block_inner_on_stream(
91        &mut self,
92        stream: u64,
93        layer: u32,
94        block: u32,
95        k_block_dev: u64,
96        k_block_host: &[half::bf16],
97        v_block_host: &[half::bf16],
98        do_predict: bool,
99    ) -> Result<()> {
100        if do_predict {
101            self.predictor.project_kv_block_on_stream(
102                stream,
103                layer as usize,
104                block as usize,
105                k_block_dev,
106            )?;
107        }
108        let bs = self.model.block_size as usize;
109        let nkv = self.model.num_kv_heads as usize;
110        let hd = self.model.head_dim as usize;
111        if k_block_host.len() != bs * nkv * hd || v_block_host.len() != bs * nkv * hd {
112            anyhow::bail!(
113                "offload_block: host buffers must be {} BF16 elements",
114                bs * nkv * hd
115            );
116        }
117        for kh in 0..nkv {
118            let mut k_stripe = Vec::with_capacity(bs * hd * 2);
119            let mut v_stripe = Vec::with_capacity(bs * hd * 2);
120            for tok in 0..bs {
121                let base = (tok * nkv + kh) * hd;
122                for x in &k_block_host[base..base + hd] {
123                    k_stripe.extend_from_slice(&x.to_le_bytes());
124                }
125                for x in &v_block_host[base..base + hd] {
126                    v_stripe.extend_from_slice(&x.to_le_bytes());
127                }
128            }
129            self.backend
130                .write_from_host(GroupKey::new(layer, block, kh as u16, KvKind::K), &k_stripe)?;
131            self.backend
132                .write_from_host(GroupKey::new(layer, block, kh as u16, KvKind::V), &v_stripe)?;
133        }
134        // Drop the resident-cache copy (if any). The on-disk image was just
135        // overwritten; without invalidation, attend_layer_on_stream would
136        // keep serving the stale slot. Critical for decode where the active
137        // block is re-offloaded every step with new slots filled.
138        self.pool.invalidate(ResidentKey { layer, block });
139        Ok(())
140    }
141
142    /// Run streaming attention for one (layer, sequence). `q_dev` is the
143    /// full [num_q_heads × head_dim] BF16 query for this step;
144    /// `seq_block_ids` is the sequence's full block list; `output_dev`
145    /// receives the [num_q_heads × head_dim] BF16 attention output.
146    pub fn attend_layer(
147        &mut self,
148        ctx: &CudaCtx,
149        layer: u32,
150        seq_block_ids: &[u32],
151        q_dev: u64,
152        output_dev: u64,
153    ) -> Result<()> {
154        self.attend_layer_on_stream(ctx.stream, layer, seq_block_ids, q_dev, output_dev)
155    }
156
157    /// Stream-only variant for production callers (spark-model decode path).
158    /// `stream` must already be bound to the current thread's CUDA context.
159    ///
160    /// Backwards-compat: defaults `last_block_valid_slots` to `block_size`,
161    /// i.e. no causal masking — appropriate for decode where the active
162    /// block's stale slots are zero-init from `zero_block`. For prefill,
163    /// callers MUST use `attend_layer_on_stream_with_q_pos` to pass the
164    /// query's absolute position, otherwise future tokens within the
165    /// active block leak into past queries.
166    pub fn attend_layer_on_stream(
167        &mut self,
168        stream: u64,
169        layer: u32,
170        seq_block_ids: &[u32],
171        q_dev: u64,
172        output_dev: u64,
173    ) -> Result<()> {
174        let bs = self.model.block_size as i32;
175        self.attend_layer_on_stream_with_q_pos(stream, layer, seq_block_ids, q_dev, output_dev, bs)
176    }
177
178    /// Causal-masking variant: `last_block_valid_slots` controls how many
179    /// slots of the LAST block in `seq_block_ids` are consumed by the
180    /// attention kernel. For prefill query at absolute position `q_pos`,
181    /// pass `(q_pos % block_size) + 1` to mask out future positions in
182    /// the active block.
183    pub fn attend_layer_on_stream_with_q_pos(
184        &mut self,
185        stream: u64,
186        layer: u32,
187        seq_block_ids: &[u32],
188        q_dev: u64,
189        output_dev: u64,
190        last_block_valid_slots: i32,
191    ) -> Result<()> {
192        // 1. Project Q. 2. Score every block at this layer (only seq subset
193        //    is consumed; the rest is wasted compute but score_blocks is µs).
194        self.predictor
195            .project_q_on_stream(stream, q_dev, self.q_proj.ptr)?;
196        let m = &self.model;
197        let layer_a_g = self.predictor.a_g_dev_ptr()
198            + (layer as u64)
199                * (m.max_blocks_per_layer as u64)
200                * (m.num_kv_heads as u64)
201                * (m.block_size as u64)
202                * (self.cfg.rank as u64)
203                * 2;
204        self.predictor.score_blocks_on_stream(
205            stream,
206            self.q_proj.ptr,
207            layer_a_g,
208            self.block_scores_dev.ptr,
209            m.max_blocks_per_layer as usize,
210        )?;
211        copy_d_to_h_async(
212            self.score_host_buf.as_mut_ptr() as *mut c_void,
213            self.block_scores_dev.ptr,
214            self.score_host_buf.len() * 4,
215            stream,
216        )?;
217        stream_sync(stream)?;
218
219        // 3. Tile loop.
220        self.attn.begin_step_on_stream(stream, 1)?;
221        let tile_cap = self.cfg.resident_blocks as usize;
222        let mut tile_idx = 0;
223        while tile_idx < seq_block_ids.len() {
224            let tile_end = (tile_idx + tile_cap).min(seq_block_ids.len());
225            let tile = &seq_block_ids[tile_idx..tile_end];
226
227            // Pin slots already resident for tile blocks; mark them touched.
228            let mut block_table = vec![0_i32; tile_cap];
229            let mut pinned: Vec<u32> = Vec::new();
230            // First pass: identify which tile blocks are missing.
231            let mut missing: Vec<u32> = Vec::new();
232            for (i, &blk) in tile.iter().enumerate() {
233                let key = ResidentKey { layer, block: blk };
234                if let Some(slot) = self.pool.lookup(key) {
235                    block_table[i] = slot as i32;
236                    pinned.push(slot);
237                    self.eviction.touch(slot);
238                } else {
239                    missing.push(blk);
240                }
241            }
242            // Second pass: assign + read missing blocks.
243            let mut reqs: Vec<ReadRequest> = Vec::new();
244            for &blk in &missing {
245                let key = ResidentKey { layer, block: blk };
246                let candidates = self.eviction.rank(&pinned);
247                let slot = self.pool.assign(key, &candidates)?;
248                pinned.push(slot);
249                self.eviction.touch(slot);
250                self.eviction
251                    .record_score(slot, self.score_host_buf[blk as usize]);
252                // Find this block's index in the tile so the block_table is right.
253                let idx = tile.iter().position(|&x| x == blk).unwrap();
254                block_table[idx] = slot as i32;
255                for kh in 0..self.model.num_kv_heads {
256                    reqs.push(ReadRequest {
257                        group: GroupKey::new(layer, blk, kh, KvKind::K),
258                        dst_dev_ptr: self.pool.slot_k_ptr(slot, kh),
259                    });
260                    reqs.push(ReadRequest {
261                        group: GroupKey::new(layer, blk, kh, KvKind::V),
262                        dst_dev_ptr: self.pool.slot_v_ptr(slot, kh),
263                    });
264                }
265            }
266            self.backend.read(&reqs, stream)?;
267
268            // 4. Tiled attention launch.
269            let counts = [(tile.len()) as i32];
270            copy_h_to_d_async(
271                self.block_table_dev.ptr,
272                block_table.as_ptr() as *const c_void,
273                tile_cap * 4,
274                stream,
275            )?;
276            copy_h_to_d_async(
277                self.counts_dev.ptr,
278                counts.as_ptr() as *const c_void,
279                4,
280                stream,
281            )?;
282            let (s_blk, s_tok, s_kvh) = self.attn.scratch_pool_strides();
283            let v_off = (self.model.num_kv_heads as u64)
284                * (self.model.block_size as u64)
285                * (self.model.head_dim as u64)
286                * 2;
287            // Causal mask: only apply on the FINAL tile of the seq's block
288            // list. Earlier tiles are full blocks of historical K/V.
289            let lbvs = if tile_end == seq_block_ids.len() {
290                last_block_valid_slots
291            } else {
292                self.model.block_size as i32
293            };
294            self.attn.step_tile_on_stream(
295                stream,
296                q_dev,
297                self.pool.pool_dev_ptr(),
298                self.pool.pool_dev_ptr() + v_off,
299                self.block_table_dev.ptr,
300                self.counts_dev.ptr,
301                1,
302                s_blk,
303                s_tok,
304                s_kvh,
305                lbvs,
306            )?;
307            tile_idx = tile_end;
308        }
309        self.attn.finalize_on_stream(stream, output_dev, 1)?;
310        Ok(())
311    }
312
313    /// Test/diag accessors.
314    pub fn pool(&self) -> &ScratchPool {
315        &self.pool
316    }
317    pub fn predictor(&self) -> &Predictor {
318        &self.predictor
319    }
320    pub fn config(&self) -> &HighSpeedSwapConfig {
321        &self.cfg
322    }
323}