metrale_storage/backend/
posix.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The portable file backend: one pinned bounce buffer, a
4//! positional read through `crate::tier::pio` (`pread`/`pwrite` on unix,
5//! `seek_read`/`seek_write` on Windows) and a host-to-device copy per group.
6//! `HighSpeedSwap` uses it on every target but Linux.
7//!
8//! Owner: metrale-storage high-speed swap.
9//! Invariants:
10//! - The bounce buffer is refilled only after the stream sync that follows its
11//!   previous copy.
12
13use anyhow::{Context, Result, bail};
14use std::ffi::c_void;
15
16use super::{ReadRequest, StorageBackend};
17use crate::cuda_min::{PinnedBuffer, copy_h_to_d_async, stream_sync};
18use crate::group::{GroupKey, GroupLayout};
19use crate::layout::Layout;
20
21pub struct PosixBackend {
22    layout: Layout,
23    bounce: PinnedBuffer,
24}
25
26impl PosixBackend {
27    pub fn new(layout: Layout) -> Result<Self> {
28        let bounce = PinnedBuffer::new(layout.group_bytes() as usize)
29            .context("alloc pinned bounce buffer")?;
30        Ok(Self { layout, bounce })
31    }
32    pub fn layout(&self) -> &Layout {
33        &self.layout
34    }
35}
36
37impl StorageBackend for PosixBackend {
38    fn read(&mut self, requests: &[ReadRequest], stream: u64) -> Result<()> {
39        let bytes = self.layout.group_bytes() as usize;
40        let bounce_ptr = self.bounce.ptr;
41        for req in requests {
42            let off = self.layout.offset(req.group);
43            // 2026-09-25: SAFETY: `bounce_ptr` is a pinned allocation of
44            // `group_bytes()` owned by `self.bounce`; the previous copy out of it
45            // finished at the `stream_sync` below.
46            let buf = unsafe { std::slice::from_raw_parts_mut(bounce_ptr as *mut u8, bytes) };
47            crate::tier::pio::read_exact_at(self.layout.file(req.group.layer), buf, off)
48                .with_context(|| format!("read {bytes}@{off}"))?;
49            // 2026-09-25: Every request reuses the one bounce buffer, so the copy
50            // completes before the next read overwrites it.
51            copy_h_to_d_async(req.dst_dev_ptr, bounce_ptr as *const c_void, bytes, stream)?;
52            stream_sync(stream)?;
53        }
54        Ok(())
55    }
56
57    fn write_from_host(&mut self, key: GroupKey, src: &[u8]) -> Result<()> {
58        let bytes = self.layout.group_bytes() as usize;
59        if src.len() != bytes {
60            bail!(
61                "write_from_host: src len {} != group bytes {bytes}",
62                src.len()
63            );
64        }
65        // 2026-09-25: The Linux layer files are O_DIRECT (layout.rs), so `src`
66        // is staged through the pinned bounce buffer.
67        unsafe {
68            std::ptr::copy_nonoverlapping(src.as_ptr(), self.bounce.ptr as *mut u8, bytes);
69        }
70        let off = self.layout.offset(key);
71        // 2026-09-25: SAFETY: pinned, `group_bytes()` long, owned by `self`.
72        let buf = unsafe { std::slice::from_raw_parts(self.bounce.ptr as *const u8, bytes) };
73        crate::tier::pio::write_all_at(self.layout.file(key.layer), buf, off)
74            .with_context(|| format!("write {bytes}@{off}"))?;
75        // 2026-09-25: No fsync: the write is not durable across a crash.
76        Ok(())
77    }
78
79    fn group_layout(&self) -> GroupLayout {
80        self.layout.spec
81    }
82}
83
84impl PosixBackend {
85    /// 2026-09-25: Advise the kernel to drop every layer file's page cache
86    /// (`POSIX_FADV_DONTNEED`), so later reads go to the device. A no-op on
87    /// non-unix targets, where reads may still come from the cache.
88    #[cfg(unix)]
89    pub fn drop_pagecache(&self) {
90        for layer in 0..self.layout.spec.num_layers {
91            let fd = self.layout.fd(layer);
92            unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_DONTNEED) };
93        }
94    }
95
96    #[cfg(not(unix))]
97    pub fn drop_pagecache(&self) {}
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::group::{GroupLayout, KvKind};
104
105    fn tempdir(name: &str) -> std::path::PathBuf {
106        let p =
107            std::env::temp_dir().join(format!("metrale-storage-{}-{}", name, std::process::id()));
108        let _ = std::fs::remove_dir_all(&p);
109        std::fs::create_dir_all(&p).unwrap();
110        p
111    }
112
113    #[test]
114    #[ignore = "requires GPU"]
115    fn write_then_read_round_trip() {
116        // 2026-09-25: `PinnedBuffer::new` needs a current CUDA context.
117        let _ctx = crate::cuda_min::CudaCtx::new(0).expect("cuda init");
118        let dir = tempdir("rt");
119        let spec = GroupLayout::new(1, 2, 1, 16, 128, 2, 4096);
120        let layout = Layout::create(&dir, spec).unwrap();
121        let mut backend = PosixBackend::new(layout).unwrap();
122        let bytes = backend.layout().group_bytes() as usize;
123        let pat: Vec<u8> = (0..bytes).map(|i| (i & 0xFF) as u8).collect();
124        let key = GroupKey::new(0, 1, 0, KvKind::V);
125        backend.write_from_host(key, &pat).unwrap();
126        backend.drop_pagecache();
127
128        let dev = crate::cuda_min::DeviceBuffer::new(bytes).unwrap();
129        let req = ReadRequest {
130            group: key,
131            dst_dev_ptr: dev.ptr,
132        };
133        backend.read(&[req], _ctx.stream).unwrap();
134        let mut host_back = vec![0_u8; bytes];
135        crate::cuda_min::copy_d_to_h_async(
136            host_back.as_mut_ptr() as *mut c_void,
137            dev.ptr,
138            bytes,
139            _ctx.stream,
140        )
141        .unwrap();
142        crate::cuda_min::stream_sync(_ctx.stream).unwrap();
143        assert_eq!(host_back, pat);
144        std::fs::remove_dir_all(&dir).ok();
145    }
146}