1use anyhow::{Context, Result, bail};
13use io_uring::{IoUring, opcode, types};
14use std::ffi::c_void;
15use std::os::fd::RawFd;
16
17use super::{ReadRequest, StorageBackend};
18use crate::cuda_min::{CudaEvent, PinnedBuffer, copy_h_to_d_async, stream_sync};
19use crate::group::{GroupKey, GroupLayout};
20use crate::layout::Layout;
21
22pub struct IoUringBackend {
23 layout: Layout,
24 ring: IoUring,
25 buffers: Vec<PinnedBuffer>,
26 events: Vec<Option<CudaEvent>>,
29 qd: usize,
30}
31
32impl IoUringBackend {
33 pub fn new(layout: Layout, qd: usize) -> Result<Self> {
34 if qd == 0 {
35 bail!("queue depth must be ≥ 1");
36 }
37 let ring = IoUring::builder()
39 .setup_sqpoll(2_000)
40 .build(qd as u32)
41 .context("io_uring build")?;
42
43 let group_bytes = layout.group_bytes() as usize;
44 let mut buffers = Vec::with_capacity(qd);
45 for _ in 0..qd {
46 buffers.push(PinnedBuffer::new(group_bytes)?);
47 }
48 let iovecs: Vec<libc::iovec> = buffers
51 .iter()
52 .map(|b| libc::iovec {
53 iov_base: b.ptr,
54 iov_len: b.bytes,
55 })
56 .collect();
57 unsafe {
58 ring.submitter()
59 .register_buffers(&iovecs)
60 .context("register_buffers")?;
61 }
62 let events: Vec<Option<CudaEvent>> = (0..qd).map(|_| None).collect();
63 Ok(Self {
64 layout,
65 ring,
66 buffers,
67 events,
68 qd,
69 })
70 }
71
72 pub fn layout(&self) -> &Layout {
73 &self.layout
74 }
75
76 pub fn drop_pagecache(&self) {
79 for layer in 0..self.layout.spec.num_layers {
80 let fd = self.layout.fd(layer);
81 unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_DONTNEED) };
82 }
83 }
84
85 fn wait_buffer_free(&mut self, buf_idx: usize) -> Result<()> {
88 if let Some(ev) = self.events[buf_idx].take() {
89 ev.sync()?;
90 }
91 Ok(())
92 }
93
94 fn submit_read(
97 &mut self,
98 fd: RawFd,
99 offset: u64,
100 bytes: u32,
101 buf_idx: u16,
102 user_data: u64,
103 ) -> Result<()> {
104 let buf_ptr = self.buffers[buf_idx as usize].ptr as *mut u8;
105 let read_e = opcode::ReadFixed::new(types::Fd(fd), buf_ptr, bytes, buf_idx)
106 .offset(offset)
107 .build()
108 .user_data(user_data);
109 unsafe {
110 self.ring
111 .submission()
112 .push(&read_e)
113 .map_err(|_| anyhow::anyhow!("io_uring SQ full"))?;
114 }
115 Ok(())
116 }
117}
118
119impl StorageBackend for IoUringBackend {
120 fn read(&mut self, requests: &[ReadRequest], stream: u64) -> Result<()> {
121 let bytes = self.layout.group_bytes() as u32;
122 if requests.len() > u16::MAX as usize {
124 bail!("io_uring batch too large: {}", requests.len());
125 }
126
127 let mut next_submit = 0;
128 let mut completed = 0;
129 let mut free_bufs: Vec<u16> = (0..self.qd as u16).rev().collect();
132
133 while completed < requests.len() {
134 while next_submit < requests.len() {
135 let Some(&buf_idx) = free_bufs.last() else {
136 break;
137 };
138 self.wait_buffer_free(buf_idx as usize)?;
139 free_bufs.pop();
140 let req = &requests[next_submit];
141 let fd = self.layout.fd(req.group.layer);
142 let off = self.layout.offset(req.group);
143 let user = ((next_submit as u64) << 16) | (buf_idx as u64);
144 self.submit_read(fd, off, bytes, buf_idx, user)?;
145 next_submit += 1;
146 }
147 self.ring
148 .submit_and_wait(1)
149 .context("io_uring submit_and_wait")?;
150 let cq = self.ring.completion();
151 for cqe in cq {
152 let user = cqe.user_data();
153 let buf_idx = (user & 0xFFFF) as u16;
154 let req_idx = (user >> 16) as usize;
155 let result = cqe.result();
156 if result < 0 {
157 bail!("io_uring read failed for req {req_idx}: errno {}", -result);
158 }
159 if result as u32 != bytes {
160 bail!("io_uring short read: req {req_idx} got {result}, expected {bytes}");
161 }
162 let req = &requests[req_idx];
163 let buf = &self.buffers[buf_idx as usize];
164 copy_h_to_d_async(
165 req.dst_dev_ptr,
166 buf.ptr as *const c_void,
167 bytes as usize,
168 stream,
169 )?;
170 let ev = CudaEvent::new()?;
171 ev.record(stream)?;
172 self.events[buf_idx as usize] = Some(ev);
173 free_bufs.push(buf_idx);
174 completed += 1;
175 }
176 }
177 stream_sync(stream)?;
180 for slot in self.events.iter_mut() {
182 *slot = None;
183 }
184 Ok(())
185 }
186
187 fn write_from_host(&mut self, key: GroupKey, src: &[u8]) -> Result<()> {
188 let bytes = self.layout.group_bytes() as usize;
189 if src.len() != bytes {
190 bail!(
191 "write_from_host: src len {} != group bytes {bytes}",
192 src.len()
193 );
194 }
195 self.wait_buffer_free(0)?;
198 unsafe {
199 std::ptr::copy_nonoverlapping(src.as_ptr(), self.buffers[0].ptr as *mut u8, bytes);
200 }
201 let fd = self.layout.fd(key.layer);
202 let off = self.layout.offset(key) as i64;
203 let n = unsafe { libc::pwrite(fd, self.buffers[0].ptr, bytes, off) };
204 if n != bytes as isize {
205 bail!(
206 "pwrite {bytes}@{off} returned {n}, errno {}",
207 std::io::Error::last_os_error()
208 );
209 }
210 Ok(())
211 }
212
213 fn group_layout(&self) -> GroupLayout {
214 self.layout.spec
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::cuda_min::{CudaCtx, DeviceBuffer, copy_d_to_h_async};
222 use crate::group::{GroupKey, GroupLayout, KvKind};
223 use std::path::PathBuf;
224
225 fn tempdir(name: &str) -> PathBuf {
226 let p =
227 std::env::temp_dir().join(format!("metrale-iouring-{}-{}", name, std::process::id()));
228 let _ = std::fs::remove_dir_all(&p);
229 std::fs::create_dir_all(&p).unwrap();
230 p
231 }
232
233 #[test]
234 #[ignore = "requires GPU"]
235 fn write_then_read_round_trip() {
236 let _ctx = CudaCtx::new(0).expect("cuda init");
237 let dir = tempdir("rt");
238 let spec = GroupLayout::new(1, 4, 1, 16, 128, 2, 4096);
239 let layout = Layout::create(&dir, spec).unwrap();
240 let mut backend = IoUringBackend::new(layout, 4).unwrap();
241 let bytes = backend.layout().group_bytes() as usize;
242 let patterns: Vec<(GroupKey, Vec<u8>)> = (0..4u32)
245 .map(|b| {
246 let k = GroupKey::new(0, b, 0, KvKind::K);
247 let pat: Vec<u8> = (0..bytes)
248 .map(|i| ((i + b as usize) & 0xFF) as u8)
249 .collect();
250 (k, pat)
251 })
252 .collect();
253 for (k, p) in &patterns {
254 backend.write_from_host(*k, p).unwrap();
255 }
256 backend.drop_pagecache();
257 let dev: Vec<DeviceBuffer> = patterns
258 .iter()
259 .map(|_| DeviceBuffer::new(bytes).unwrap())
260 .collect();
261 let reqs: Vec<ReadRequest> = patterns
262 .iter()
263 .zip(&dev)
264 .map(|((k, _), d)| ReadRequest {
265 group: *k,
266 dst_dev_ptr: d.ptr,
267 })
268 .collect();
269 backend.read(&reqs, _ctx.stream).unwrap();
270 for ((_, expected), d) in patterns.iter().zip(&dev) {
271 let mut got = vec![0_u8; bytes];
272 copy_d_to_h_async(got.as_mut_ptr() as *mut c_void, d.ptr, bytes, _ctx.stream).unwrap();
273 stream_sync(_ctx.stream).unwrap();
274 assert_eq!(&got, expected);
275 }
276 std::fs::remove_dir_all(&dir).ok();
277 }
278}