spark_storage/backend/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//
3// Storage backend trait + impls for the high-speed-swap path.
4//
5// SBIO contract: tiled-attention / scratch-pool code never opens a file or
6// issues a syscall. Every NVMe-touching operation flows through a
7// `StorageBackend` impl, so the predictor / scratch / kernel layers can be
8// tested with the deterministic POSIX backend and swap in the io_uring
9// production backend transparently.
10
11use anyhow::Result;
12
13use crate::group::{GroupKey, GroupLayout, KvKind};
14
15// io_uring is a Linux kernel interface with no analogue elsewhere; the
16// portable `posix` backend (positional read/write via atlas_tier::pio) is what
17// non-Linux builds use.
18#[cfg(target_os = "linux")]
19pub mod io_uring;
20pub mod posix;
21
22#[cfg(target_os = "linux")]
23pub use self::io_uring::IoUringBackend;
24pub use posix::PosixBackend;
25
26/// One read request: pull `group` from disk, land it at `dst_dev_ptr`.
27#[derive(Clone, Copy, Debug)]
28pub struct ReadRequest {
29    pub group: GroupKey,
30    pub dst_dev_ptr: u64,
31}
32
33/// One block-granular read: land the whole block (all kv-heads' K then V,
34/// `block_bytes`) into the device slot based at `dst_dev_ptr`
35/// (== `ScratchPool::slot_dev_ptr(slot)`). `base_key` carries `kv_head = 0,
36/// kind = K` by convention; only its `layer` and `block` are load-bearing.
37#[derive(Clone, Copy, Debug)]
38pub struct BlockReadRequest {
39    pub base_key: GroupKey,
40    pub dst_dev_ptr: u64,
41}
42
43/// Expand each `BlockReadRequest` into the exact `2·nkv` per-head `ReadRequest`s
44/// the un-coalesced path issues, in the SAME order the caller loops emit
45/// (interleaved `K(kh), V(kh)` for `kh` in `0..nkv`) with device destinations
46/// at `dst + kh·gs` (K) and `dst + (nkv+kh)·gs` (V).
47///
48/// This is the SINGLE source of the per-head fan-out: the default `read_blocks`
49/// / `write_block_from_host` trait impls AND the unit tests consume it, so the
50/// RDMA/Cascade backends (which inherit the default) can never drift from the
51/// caller-side per-head layout, and byte-identity is pinned host-side.
52pub fn expand_blocks_to_groups(spec: &GroupLayout, reqs: &[BlockReadRequest]) -> Vec<ReadRequest> {
53    let nkv = spec.num_kv_heads;
54    let gs = spec.group_stride;
55    let mut out = Vec::with_capacity(reqs.len() * 2 * nkv as usize);
56    for r in reqs {
57        let layer = r.base_key.layer;
58        let block = r.base_key.block;
59        for kh in 0..nkv {
60            out.push(ReadRequest {
61                group: GroupKey::new(layer, block, kh, KvKind::K),
62                dst_dev_ptr: r.dst_dev_ptr + (kh as u64) * gs,
63            });
64            out.push(ReadRequest {
65                group: GroupKey::new(layer, block, kh, KvKind::V),
66                dst_dev_ptr: r.dst_dev_ptr + (nkv as u64 + kh as u64) * gs,
67            });
68        }
69    }
70    out
71}
72
73pub trait StorageBackend: Send + Sync {
74    /// Synchronously fulfil all `requests`, returning when the corresponding
75    /// HBM destinations are populated and visible on `stream`. The backend
76    /// chooses how to schedule (blocking POSIX `pread`, batched `io_uring`,
77    /// etc.). At return, the `stream` has been synchronised so the caller
78    /// can issue subsequent kernels that depend on the data.
79    fn read(&mut self, requests: &[ReadRequest], stream: u64) -> Result<()>;
80
81    /// Async variant of `read`: enqueue the tier read + H2D on `stream` and
82    /// return WITHOUT a terminal host `stream_sync`. Default = the synchronous
83    /// `read`, so file backends need no change and the on-demand path stays
84    /// byte-identical.
85    fn read_async(&mut self, requests: &[ReadRequest], stream: u64) -> Result<()> {
86        self.read(requests, stream)
87    }
88
89    /// One-shot sequential write — used at offload time to populate disk
90    /// from a host-side K/V buffer.
91    fn write_from_host(&mut self, key: GroupKey, src: &[u8]) -> Result<()>;
92
93    /// Immutable disk/device geometry. The default block methods below use it to
94    /// fan a block op back out to the per-head path; io_uring/posix return their
95    /// layout spec, Cascade delegates to its backing, RDMA returns its layout.
96    fn group_layout(&self) -> GroupLayout;
97
98    /// Block-granular read: fulfil each request with ONE contiguous
99    /// `block_bytes` op instead of `2·nkv` per-head reads. Same stream contract
100    /// as `read`. The DEFAULT fans out to `read` via `expand_blocks_to_groups`,
101    /// so posix/RDMA/Cascade stay correct (just un-coalesced) with no change.
102    fn read_blocks(&mut self, requests: &[BlockReadRequest], stream: u64) -> Result<()> {
103        let groups = expand_blocks_to_groups(&self.group_layout(), requests);
104        self.read(&groups, stream)
105    }
106
107    /// Async block-granular read — the coalesced twin of `read_async` for the
108    /// prefetch path. DEFAULT fans out to `read_async`.
109    fn read_blocks_async(&mut self, requests: &[BlockReadRequest], stream: u64) -> Result<()> {
110        let groups = expand_blocks_to_groups(&self.group_layout(), requests);
111        self.read_async(&groups, stream)
112    }
113
114    /// Block-granular write: ONE contiguous `block_bytes` op. `src` is exactly
115    /// `block_bytes` laid out `[K0,K1,…,K(nkv-1),V0,…,V(nkv-1)]` at `group_stride`
116    /// pitch. `base_key` carries the block identity (kv_head/kind ignored).
117    /// DEFAULT splits `src` back into the `2·nkv` per-head `group_stride` stripes
118    /// and calls `write_from_host` per head — byte-identical on-disk image.
119    fn write_block_from_host(&mut self, base_key: GroupKey, src: &[u8]) -> Result<()> {
120        let spec = self.group_layout();
121        let nkv = spec.num_kv_heads as usize;
122        let gs = spec.group_stride as usize;
123        let expect = 2 * nkv * gs;
124        if src.len() != expect {
125            anyhow::bail!(
126                "write_block_from_host: src len {} != block bytes {expect}",
127                src.len()
128            );
129        }
130        let layer = base_key.layer;
131        let block = base_key.block;
132        for kh in 0..nkv {
133            let k_off = kh * gs;
134            let v_off = (nkv + kh) * gs;
135            self.write_from_host(
136                GroupKey::new(layer, block, kh as u16, KvKind::K),
137                &src[k_off..k_off + gs],
138            )?;
139            self.write_from_host(
140                GroupKey::new(layer, block, kh as u16, KvKind::V),
141                &src[v_off..v_off + gs],
142            )?;
143        }
144        Ok(())
145    }
146
147    /// Write a run of `run_len` strictly-consecutive same-layer blocks in ONE
148    /// contiguous op. `base_key` carries the run's FIRST block; `src` is exactly
149    /// `run_len · block_bytes`. DEFAULT fans out to `run_len`
150    /// `write_block_from_host` calls — byte- AND op-identical to the
151    /// un-coalesced path (and `run_len == 1` is exactly one call).
152    fn write_blocks_run(&mut self, base_key: GroupKey, run_len: usize, src: &[u8]) -> Result<()> {
153        let spec = self.group_layout();
154        let block_bytes = spec.block_bytes() as usize;
155        let expect = run_len * block_bytes;
156        if src.len() != expect {
157            anyhow::bail!(
158                "write_blocks_run: src len {} != run bytes {expect} ({run_len} × {block_bytes})",
159                src.len()
160            );
161        }
162        for i in 0..run_len {
163            let off = i * block_bytes;
164            self.write_block_from_host(
165                GroupKey::new(base_key.layer, base_key.block + i as u32, 0, KvKind::K),
166                &src[off..off + block_bytes],
167            )?;
168        }
169        Ok(())
170    }
171
172    /// Whether this backend can service `write_blocks_run` as a single wide op.
173    /// DEFAULT `false`: RDMA/Cascade keep the per-block fan-out, and the caller
174    /// stays on the per-block write path.
175    fn supports_write_run_coalescing(&self) -> bool {
176        false
177    }
178
179    /// Optionally pre-register `[base, base+len)` as the read-landing region.
180    /// The RDMA backend registers it as ONE MR (per rail) so zero-copy restore
181    /// reuses that lkey for every slot within it. No-op for the file backends.
182    fn register_landing_region(&mut self, base: u64, len: usize) -> Result<()> {
183        let _ = (base, len);
184        Ok(())
185    }
186}
187
188#[cfg(test)]
189#[path = "mod_tests.rs"]
190mod coalesce_tests;