Skip to main content

hrx/fabric/
storage.rs

1//! GPU-published Linux io_uring requests over retained, registered host pages.
2use super::{memory::DeviceUse, *};
3use std::{
4    fs::File,
5    os::fd::{AsRawFd, FromRawFd, OwnedFd},
6    sync::atomic::{AtomicU32, Ordering},
7    time::{Duration, Instant},
8};
9#[allow(
10    non_camel_case_types,
11    non_snake_case,
12    non_upper_case_globals,
13    dead_code,
14    missing_docs,
15    clippy::all
16)]
17#[path = "storage_ffi.rs"]
18mod ffi;
19use ffi::*;
20
21// Stable Linux UAPI operations; the generated layouts carry size/offset asserts.
22const REGISTER_BUFFERS: u32 = 0;
23const REGISTER_FILES: u32 = 2;
24const REGISTER_RESTRICTIONS: u32 = 11;
25const ENABLE_RINGS: u32 = 12;
26const REGISTER_EVENTFD: u32 = 4;
27const SINGLE_ISSUER: u32 = 1 << 12;
28const DEFER_TASKRUN: u32 = 1 << 13;
29const TASKRUN_FLAG: u32 = 1 << 9;
30const SQ_TASKRUN: u32 = 4;
31
32/// Kernel I/O progress service; neither mode relays application requests.
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
34pub enum StorageProgress {
35    /// Kernel polling worker, with explicit idle wakes.
36    #[default]
37    Sqpoll,
38    /// Single-issuer deferred work with completion notifications.
39    Wait,
40}
41
42/// Bounded caller-owned SQ/CQ storage and its kernel SQPOLL service.
43#[derive(Clone, Debug)]
44pub struct StorageOptions {
45    /// Selected kernel progress mechanism.
46    pub progress: StorageProgress,
47    /// Power-of-two SQ capacity, 2 through 4096. CQ capacity is twice this value.
48    pub entries: u32,
49    /// Milliseconds before an idle SQPOLL worker sleeps; waits issue idle wakes.
50    pub idle_milliseconds: u32,
51    /// Shared residency ceiling for ring pages. Payload pages carry their own charge.
52    pub memory_budget: Option<crate::residency::MemoryBudget>,
53}
54impl Default for StorageOptions {
55    fn default() -> Self {
56        Self {
57            progress: StorageProgress::Sqpoll,
58            entries: 8,
59            idle_milliseconds: 1,
60            memory_budget: None,
61        }
62    }
63}
64/// Returned native ring offsets. GPU addresses are obtained from [`StorageRing::memory`].
65/// `host_payload` is a CPU virtual address for Linux fixed-buffer I/O, never a GPU VA.
66#[derive(Clone, Copy, Debug)]
67pub struct StorageLayout {
68    /// Offset of the native 64-byte SQE array.
69    pub submission_entries: usize,
70    /// Offset of the 32-bit SQ publisher tail.
71    pub submission_tail: usize,
72    /// Offset of the native 16-byte CQE array.
73    pub completion_entries: usize,
74    /// Offset of the 32-bit CQ consumer head.
75    pub completion_head: usize,
76    /// Offset of the 32-bit CQ producer tail.
77    pub completion_tail: usize,
78    /// Native SQ entry mask.
79    pub submission_mask: u32,
80    /// Native CQ entry mask.
81    pub completion_mask: u32,
82    /// CPU address of fixed buffer zero.
83    pub host_payload: u64,
84}
85/// One fixed-file table, one fixed payload buffer, and a single GPU SQ/CQ owner.
86/// Ring restrictions permit only fixed-file READ_FIXED and WRITE_FIXED requests.
87/// They are not a sandbox for untrusted native programs.
88pub struct StorageRing {
89    fd: OwnedFd,
90    notification: Option<OwnedFd>,
91    issuer: libc::pid_t,
92    service_calls: AtomicU32,
93    wait_calls: AtomicU32,
94    memory: Buffer,
95    // SQPOLL may update ring controls even before the GPU is submitted. Block
96    // ordinary host buffer access for the entire enabled kernel ring lifetime.
97    _kernel_use: DeviceUse,
98    payload: Buffer,
99    _files: Vec<File>,
100    parameters: io_uring_params,
101    control: usize,
102    layout: StorageLayout,
103}
104fn syscall_result(result: libc::c_long) -> Result<libc::c_long> {
105    if result < 0 {
106        Err(std::io::Error::last_os_error().into())
107    } else {
108        Ok(result)
109    }
110}
111impl StorageRing {
112    /// Register regular files and a payload allocated by `allocate_registered`.
113    /// File order defines fixed-file indices; payload is fixed-buffer index zero.
114    /// Requires NO_MMAP, NO_SQARRAY, restrictions and the selected progress mode. Admission
115    /// failure is explicit; no alternate transport or host request relay is used.
116    pub fn new(
117        device: &Device,
118        files: &[File],
119        payload: &Buffer,
120        options: StorageOptions,
121    ) -> Result<Self> {
122        if device.endpoint().engine() != Engine::Gpu
123            || files.is_empty()
124            || files.len() > 1024
125            || !options.entries.is_power_of_two()
126            || !(2..=4096).contains(&options.entries)
127            || options.idle_milliseconds == 0
128        {
129            return Err(Error::Message(
130                "invalid GPU storage ring configuration".into(),
131            ));
132        }
133        if !payload.owns_registered_pages() {
134            return Err(Error::Unsupported(
135                "storage payload requires owned registered host pages".into(),
136            ));
137        }
138        payload.device_address(device)?;
139        let files = files
140            .iter()
141            .map(|file| {
142                if !file.metadata()?.is_file() {
143                    return Err(Error::Unsupported("storage requires regular files".into()));
144                }
145                Ok(file.try_clone()?)
146            })
147            .collect::<Result<Vec<_>>>()?;
148        let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
149        if page <= 0 {
150            return Err(Error::Unsupported("host page size unavailable".into()));
151        }
152        let page = page as usize;
153        let round = |n: usize| n.div_ceil(page) * page;
154        let control = round(options.entries as usize * 64);
155        let control_bytes = page + round(options.entries as usize * 2 * 16);
156        let memory = device.fabric().allocate_registered(
157            control + control_bytes,
158            std::slice::from_ref(device),
159            options.memory_budget.as_ref(),
160        )?;
161        let mut parameters = io_uring_params {
162            flags: IORING_SETUP_NO_MMAP
163                | IORING_SETUP_NO_SQARRAY
164                | IORING_SETUP_R_DISABLED
165                | match options.progress {
166                    StorageProgress::Sqpoll => IORING_SETUP_SQPOLL,
167                    StorageProgress::Wait => SINGLE_ISSUER | DEFER_TASKRUN | TASKRUN_FLAG,
168                },
169            sq_thread_idle: options.idle_milliseconds,
170            ..Default::default()
171        };
172        parameters.sq_off.user_addr = memory.host_pointer() as u64;
173        parameters.cq_off.user_addr = parameters.sq_off.user_addr + control as u64;
174        let fd =
175            unsafe { libc::syscall(libc::SYS_io_uring_setup, options.entries, &mut parameters) };
176        if fd < 0 {
177            let error = std::io::Error::last_os_error();
178            return if matches!(
179                error.raw_os_error(),
180                Some(libc::ENOSYS | libc::EINVAL | libc::EPERM | libc::EACCES)
181            ) {
182                Err(Error::Unsupported(format!(
183                    "caller-owned io_uring ring unavailable: {error}"
184                )))
185            } else {
186                Err(error.into())
187            };
188        }
189        let fd = unsafe { OwnedFd::from_raw_fd(fd as i32) };
190        if parameters.sq_entries != options.entries
191            || parameters.cq_entries != options.entries * 2
192            || parameters.cq_off.cqes as usize + parameters.cq_entries as usize * 16 > control_bytes
193        {
194            return Err(Error::Unsupported(
195                "unexpected io_uring storage requirements".into(),
196            ));
197        }
198        for offset in [
199            parameters.sq_off.head,
200            parameters.sq_off.tail,
201            parameters.sq_off.flags,
202            parameters.sq_off.dropped,
203            parameters.cq_off.head,
204            parameters.cq_off.tail,
205            parameters.cq_off.overflow,
206        ] {
207            if offset % 4 != 0 || offset as usize + 4 > control_bytes {
208                return Err(Error::Unsupported("invalid native ring offsets".into()));
209            }
210        }
211        let layout = StorageLayout {
212            submission_entries: 0,
213            submission_tail: control + parameters.sq_off.tail as usize,
214            completion_entries: control + parameters.cq_off.cqes as usize,
215            completion_head: control + parameters.cq_off.head as usize,
216            completion_tail: control + parameters.cq_off.tail as usize,
217            submission_mask: parameters.sq_entries - 1,
218            completion_mask: parameters.cq_entries - 1,
219            host_payload: payload.host_pointer() as u64,
220        };
221        let kernel_use = memory.retain_use()?;
222        let notification = if options.progress == StorageProgress::Wait {
223            let fd = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
224            syscall_result(fd as _)?;
225            Some(unsafe { OwnedFd::from_raw_fd(fd) })
226        } else {
227            None
228        };
229        let ring = Self {
230            fd,
231            notification,
232            issuer: unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t },
233            service_calls: AtomicU32::new(0),
234            wait_calls: AtomicU32::new(0),
235            memory,
236            _kernel_use: kernel_use,
237            payload: payload.clone(),
238            _files: files,
239            parameters,
240            control,
241            layout,
242        };
243        let iov = libc::iovec {
244            iov_base: payload.host_pointer().cast(),
245            iov_len: payload.len(),
246        };
247        ring.register(REGISTER_BUFFERS, &iov as *const _ as *const libc::c_void, 1)?;
248        let descriptors: Vec<i32> = ring._files.iter().map(AsRawFd::as_raw_fd).collect();
249        ring.register(
250            REGISTER_FILES,
251            descriptors.as_ptr().cast(),
252            descriptors.len() as u32,
253        )?;
254        if let Some(notification) = &ring.notification {
255            let fd = notification.as_raw_fd();
256            ring.register(REGISTER_EVENTFD, (&fd as *const i32).cast(), 1)?;
257        }
258        // restriction opcode: register-op=0, SQE-op=1, flags-allowed=2, flags-required=3.
259        let restrictions =
260            [(1, 4), (1, 5), (2, 1), (3, 1), (0, ENABLE_RINGS as u8)].map(|(opcode, value)| {
261                io_uring_restriction {
262                    opcode,
263                    __bindgen_anon_1: io_uring_restriction__bindgen_ty_1 { register_op: value },
264                    ..Default::default()
265                }
266            });
267        ring.register(
268            REGISTER_RESTRICTIONS,
269            restrictions.as_ptr().cast(),
270            restrictions.len() as u32,
271        )?;
272        ring.register(ENABLE_RINGS, ptr::null(), 0)?;
273        Ok(ring)
274    }
275    fn register(&self, operation: u32, data: *const libc::c_void, count: u32) -> Result<()> {
276        syscall_result(unsafe {
277            libc::syscall(
278                libc::SYS_io_uring_register,
279                self.fd.as_raw_fd(),
280                operation,
281                data,
282                count,
283            )
284        })?;
285        Ok(())
286    }
287    /// Ring memory for preparing trusted GPU arguments. Ordinary host reads and
288    /// writes return Busy for the entire ring lifetime: SQPOLL owns its controls.
289    pub fn memory(&self) -> &Buffer {
290        &self.memory
291    }
292    /// Retained registered payload backing.
293    pub fn payload(&self) -> &Buffer {
294        &self.payload
295    }
296    /// Native offsets, capacities and fixed-buffer CPU address.
297    pub fn layout(&self) -> StorageLayout {
298        self.layout
299    }
300    fn word(&self, offset: u32) -> u32 {
301        unsafe {
302            (&*self
303                .memory
304                .host_pointer()
305                .add(self.control + offset as usize)
306                .cast::<AtomicU32>())
307                .load(Ordering::Acquire)
308        }
309    }
310    fn wake(&self) -> Result<()> {
311        if let Some(notification) = &self.notification {
312            if unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t } != self.issuer {
313                return Err(Error::Message(
314                    "wait-mode storage must be serviced by its creating thread".into(),
315                ));
316            }
317            let mut value = 0u64;
318            let read =
319                unsafe { libc::read(notification.as_raw_fd(), (&mut value as *mut u64).cast(), 8) };
320            if read < 0 {
321                let error = std::io::Error::last_os_error();
322                if !matches!(error.raw_os_error(), Some(libc::EAGAIN | libc::EINTR)) {
323                    return Err(error.into());
324                }
325            }
326            let completed = self.word(self.parameters.cq_off.tail);
327            let tail = self.word(self.parameters.sq_off.tail);
328            let head = self.word(self.parameters.sq_off.head);
329            if tail == completed {
330                return Ok(());
331            }
332            self.service_calls.fetch_add(1, Ordering::Relaxed);
333            let result = unsafe {
334                libc::syscall(
335                    libc::SYS_io_uring_enter,
336                    self.fd.as_raw_fd(),
337                    tail.wrapping_sub(head),
338                    0u32,
339                    1u32,
340                    ptr::null::<u8>(),
341                    0usize,
342                )
343            };
344            if result < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
345                return Ok(());
346            }
347            syscall_result(result)?;
348            if self.word(self.parameters.cq_off.tail) == completed
349                && self.word(self.parameters.sq_off.flags) & SQ_TASKRUN == 0
350                && self.word(self.parameters.sq_off.head) != completed
351            {
352                let mut fd = libc::pollfd {
353                    fd: notification.as_raw_fd(),
354                    events: libc::POLLIN,
355                    revents: 0,
356                };
357                self.wait_calls.fetch_add(1, Ordering::Relaxed);
358                // Bounded wait preserves the caller's timeout and detects GPU failure.
359                let result = unsafe { libc::poll(&mut fd, 1, 1) };
360                if result < 0 && std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR)
361                {
362                    syscall_result(result as _)?;
363                }
364            }
365            return Ok(());
366        }
367        if self.word(self.parameters.sq_off.flags) & IORING_SQ_NEED_WAKEUP != 0
368            && self.word(self.parameters.sq_off.tail) != self.word(self.parameters.sq_off.head)
369        {
370            self.service_calls.fetch_add(1, Ordering::Relaxed);
371            let result = unsafe {
372                libc::syscall(
373                    libc::SYS_io_uring_enter,
374                    self.fd.as_raw_fd(),
375                    0u32,
376                    0u32,
377                    IORING_ENTER_SQ_WAKEUP,
378                    ptr::null::<u8>(),
379                    0usize,
380                )
381            };
382            if result < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
383                return Ok(());
384            }
385            syscall_result(result)?;
386        }
387        Ok(())
388    }
389    pub(crate) fn service_counts(&self) -> (u32, u32) {
390        (
391            self.service_calls.load(Ordering::Relaxed),
392            self.wait_calls.load(Ordering::Relaxed),
393        )
394    }
395    fn drained(&self) -> bool {
396        let tail = self.word(self.parameters.sq_off.tail);
397        tail == self.word(self.parameters.sq_off.head)
398            && tail == self.word(self.parameters.cq_off.tail)
399    }
400    /// Submit one GPU owner of this initially empty ring and retain all resources.
401    /// # Safety
402    /// `command` must be the sole SQ publisher and CQ consumer, obey native ring
403    /// capacity/release/acquire rules, handle short/error CQEs, and drain accepted
404    /// I/O before returning. All fixed-file accesses must be authorized and ordered
405    /// against external users. GPU and CPU payload addresses must not be confused.
406    pub unsafe fn dispatch(self, command: PreparedGpu) -> Result<StorageExecution> {
407        let uses = vec![self.memory.retain_use()?, self.payload.retain_use()?];
408        let done = unsafe { command.dispatch() }?;
409        Ok(StorageExecution {
410            owners: Some(StorageOwners {
411                ring: self,
412                _command: command,
413                done,
414                uses,
415            }),
416            retired: false,
417        })
418    }
419}
420struct StorageOwners {
421    ring: StorageRing,
422    _command: PreparedGpu,
423    done: Completion,
424    uses: Vec<DeviceUse>,
425}
426/// Live GPU file work. Waits service SQPOLL or deferred work; the host never authors SQEs,
427/// consumes CQEs, or reads/writes application payload to make device progress.
428pub struct StorageExecution {
429    owners: Option<StorageOwners>,
430    retired: bool,
431}
432impl StorageExecution {
433    pub(crate) fn into_ring(mut self) -> Result<StorageRing> {
434        if !self.retired {
435            return Err(Error::Busy("storage execution has not retired".into()));
436        }
437        Ok(self.owners.take().unwrap().ring)
438    }
439    /// Cached combined GPU/kernel-I/O retirement.
440    pub fn is_complete(&self) -> bool {
441        self.retired
442    }
443    /// Service idle wakes and check both completion domains under one deadline.
444    /// Timeout retains files, ring pages, payload and their residency charges.
445    pub fn wait_timeout(&mut self, timeout: Duration) -> Result<bool> {
446        if self.retired {
447            return Ok(true);
448        }
449        let start = Instant::now();
450        let owners = self.owners.as_mut().unwrap();
451        loop {
452            owners.ring.wake()?;
453            let gpu = owners.done.refresh()?;
454            if gpu && owners.ring.drained() {
455                self.retired = true;
456                owners.uses.clear();
457                return Ok(true);
458            }
459            if start.elapsed() >= timeout {
460                return Ok(false);
461            }
462            std::thread::yield_now();
463        }
464    }
465}
466impl Drop for StorageExecution {
467    fn drop(&mut self) {
468        if self.owners.is_some() && !self.wait_timeout(Duration::from_secs(10)).unwrap_or(false) {
469            std::mem::forget(self.owners.take());
470        }
471    }
472}