1use std::ffi::c_void;
17
18use anyhow::{Context, Result};
19
20use crate::backend::{ReadRequest, StorageBackend};
21use crate::cascade_policy::SlotCache;
22use crate::cuda_min::{CudaEvent, PinnedBuffer, copy_h_to_d_async, stream_sync};
23use crate::group::{GroupKey, GroupLayout};
24
25struct PinnedStore {
28 buf: PinnedBuffer,
29 group_bytes: usize,
30}
31
32impl PinnedStore {
33 fn new(cap_slots: u32, group_bytes: usize) -> Result<Self> {
34 let bytes = (cap_slots as usize)
35 .checked_mul(group_bytes)
36 .context("CascadeBackend T1 size overflow")?;
37 Ok(Self {
38 buf: PinnedBuffer::new(bytes).context("alloc T1 pinned store")?,
39 group_bytes,
40 })
41 }
42 #[inline]
43 fn slot_host_ptr(&self, slot: u32) -> *const c_void {
44 unsafe {
47 (self.buf.ptr as *const u8).add(slot as usize * self.group_bytes) as *const c_void
48 }
49 }
50 fn slot_bytes(&self, slot: u32) -> Vec<u8> {
53 unsafe {
55 std::slice::from_raw_parts(
56 (self.buf.ptr as *const u8).add(slot as usize * self.group_bytes),
57 self.group_bytes,
58 )
59 .to_vec()
60 }
61 }
62 fn write_slot(&mut self, slot: u32, src: &[u8]) {
63 debug_assert_eq!(src.len(), self.group_bytes);
64 unsafe {
67 std::ptr::copy_nonoverlapping(
68 src.as_ptr(),
69 (self.buf.ptr as *mut u8).add(slot as usize * self.group_bytes),
70 self.group_bytes,
71 );
72 }
73 }
74}
75
76pub struct CascadeBackend {
77 hot: SlotCache,
78 store: PinnedStore,
79 backing: Box<dyn StorageBackend>,
80 group_bytes: usize,
81 last_read_event: Option<CudaEvent>,
85}
86
87unsafe impl Sync for CascadeBackend {}
91
92impl CascadeBackend {
93 pub fn new(
94 backing: Box<dyn StorageBackend>,
95 layout: GroupLayout,
96 cap_slots: u32,
97 ) -> Result<Self> {
98 let group_bytes = layout.group_bytes() as usize;
99 tracing::info!(
100 "high-speed-swap: T1 cascade cache = {cap_slots} slots × {group_bytes} B = {:.1} GiB local pinned RAM, backing below",
101 (cap_slots as f64 * group_bytes as f64) / (1024.0 * 1024.0 * 1024.0),
102 );
103 Ok(Self {
104 hot: SlotCache::new(cap_slots),
105 store: PinnedStore::new(cap_slots, group_bytes)?,
106 backing,
107 group_bytes,
108 last_read_event: None,
109 })
110 }
111
112 fn flush_all(&mut self) -> Result<()> {
114 for (key, slot) in self.hot.residents() {
115 let bytes = self.store.slot_bytes(slot);
116 self.backing.write_from_host(key, &bytes)?;
117 }
118 Ok(())
119 }
120
121 fn read_common(&mut self, requests: &[ReadRequest], stream: u64, is_async: bool) -> Result<()> {
126 let keys: Vec<GroupKey> = requests.iter().map(|r| r.group).collect();
127 let (hits, misses) = self.hot.plan_read(&keys);
128 for (i, slot) in &hits {
129 let src = self.store.slot_host_ptr(*slot);
130 copy_h_to_d_async(requests[*i].dst_dev_ptr, src, self.group_bytes, stream)?;
131 self.hot.touch(*slot);
132 }
133 if is_async && !hits.is_empty() {
134 let ev = CudaEvent::new()?;
135 ev.record(stream)?;
136 self.last_read_event = Some(ev);
137 }
138 if !misses.is_empty() {
141 let miss_reqs: Vec<ReadRequest> = misses.iter().map(|&i| requests[i]).collect();
142 if is_async {
143 self.backing.read_async(&miss_reqs, stream)?;
144 } else {
145 self.backing.read(&miss_reqs, stream)?;
146 }
147 }
148 if !is_async {
149 stream_sync(stream)?;
150 }
151 Ok(())
152 }
153}
154
155impl StorageBackend for CascadeBackend {
156 fn write_from_host(&mut self, key: GroupKey, src: &[u8]) -> Result<()> {
157 let plan = self.hot.plan_write(key);
158 if let Some((victim_key, victim_slot)) = plan.flush_victim {
161 let victim_bytes = self.store.slot_bytes(victim_slot);
162 self.backing
163 .write_from_host(victim_key, &victim_bytes)
164 .context("cascade: flush T1 victim to backing")?;
165 }
166 if let Some(ev) = self.last_read_event.take() {
168 ev.sync()?;
169 }
170 self.store.write_slot(plan.slot, src);
171 Ok(())
172 }
173
174 fn read(&mut self, requests: &[ReadRequest], stream: u64) -> Result<()> {
175 self.read_common(requests, stream, false)
176 }
177
178 fn read_async(&mut self, requests: &[ReadRequest], stream: u64) -> Result<()> {
179 self.read_common(requests, stream, true)
180 }
181
182 fn register_landing_region(&mut self, base: u64, len: usize) -> Result<()> {
183 self.backing.register_landing_region(base, len)
186 }
187
188 fn group_layout(&self) -> GroupLayout {
189 self.backing.group_layout()
192 }
193}
194
195impl Drop for CascadeBackend {
196 fn drop(&mut self) {
197 let _ = self.flush_all();
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::backend::PosixBackend;
207 use crate::cuda_min::{CudaCtx, DeviceBuffer, copy_d_to_h_async, stream_sync};
208 use crate::group::KvKind;
209 use crate::layout::Layout;
210
211 fn tempdir(name: &str) -> std::path::PathBuf {
212 let p = std::env::temp_dir().join(format!("metrale-cascade-{name}-{}", std::process::id()));
213 let _ = std::fs::remove_dir_all(&p);
214 std::fs::create_dir_all(&p).unwrap();
215 p
216 }
217
218 #[test]
224 #[ignore = "requires GPU"]
225 fn read_async_eviction_no_corruption() {
226 let _ctx = CudaCtx::new(0).expect("cuda init");
227 let dir = tempdir("evict");
228 let spec = GroupLayout::new(1, 4, 1, 16, 128, 2, 4096);
229 let layout = Layout::create(&dir, spec).unwrap();
230 let bytes = spec.group_bytes() as usize;
231 let backing = Box::new(PosixBackend::new(layout).unwrap());
232 let mut cascade = CascadeBackend::new(backing, spec, 2).unwrap();
233
234 let keys: Vec<GroupKey> = (0..4u32)
235 .map(|b| GroupKey::new(0, b, 0, KvKind::K))
236 .collect();
237 let pat = |b: usize| -> Vec<u8> {
238 (0..bytes)
239 .map(|i| ((i * 7 + b * 11) & 0xFF) as u8)
240 .collect()
241 };
242
243 cascade.write_from_host(keys[0], &pat(0)).unwrap();
244 cascade.write_from_host(keys[1], &pat(1)).unwrap();
245
246 let d0 = DeviceBuffer::new(bytes).unwrap();
247 let d1 = DeviceBuffer::new(bytes).unwrap();
248 cascade
249 .read_async(
250 &[
251 ReadRequest {
252 group: keys[0],
253 dst_dev_ptr: d0.ptr,
254 },
255 ReadRequest {
256 group: keys[1],
257 dst_dev_ptr: d1.ptr,
258 },
259 ],
260 _ctx.stream,
261 )
262 .unwrap();
263 cascade.write_from_host(keys[2], &pat(2)).unwrap();
264 cascade.write_from_host(keys[3], &pat(3)).unwrap();
265 stream_sync(_ctx.stream).unwrap();
266 let readback = |d: &DeviceBuffer, want: &[u8]| {
267 let mut got = vec![0u8; bytes];
268 copy_d_to_h_async(got.as_mut_ptr() as *mut c_void, d.ptr, bytes, _ctx.stream).unwrap();
269 stream_sync(_ctx.stream).unwrap();
270 assert_eq!(&got, want, "cascade async restore corrupted");
271 };
272 readback(&d0, &pat(0));
273 readback(&d1, &pat(1));
274
275 let devs: Vec<DeviceBuffer> = (0..4).map(|_| DeviceBuffer::new(bytes).unwrap()).collect();
276 let reqs: Vec<ReadRequest> = keys
277 .iter()
278 .zip(&devs)
279 .map(|(k, d)| ReadRequest {
280 group: *k,
281 dst_dev_ptr: d.ptr,
282 })
283 .collect();
284 cascade.read(&reqs, _ctx.stream).unwrap();
285 for (b, d) in devs.iter().enumerate() {
286 readback(d, &pat(b));
287 }
288 drop(cascade);
289 std::fs::remove_dir_all(&dir).ok();
290 }
291}