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;