1use super::ring::IoUringState;
11use crate::PipelineError;
12use core::marker::PhantomData;
13use core::sync::atomic::{AtomicU32, Ordering};
14
15#[repr(C)]
17#[derive(Debug, Clone, Copy)]
18pub struct Iovec {
19 pub iov_base: *mut core::ffi::c_void,
21 pub iov_len: usize,
23}
24
25pub const IORING_OP_READV: u8 = 1;
27pub const IORING_OP_READ_FIXED: u8 = 22;
29pub const IORING_OP_URING_CMD: u8 = 46;
31
32pub struct GpuMappedBuffer<'a> {
38 ptr: *mut u8,
39 len: usize,
40 _owner: PhantomData<&'a mut [u8]>,
41}
42
43unsafe impl Send for GpuMappedBuffer<'_> {}
48unsafe impl Sync for GpuMappedBuffer<'_> {}
49
50macro_rules! define_mapped_owner_constructor {
51 ($name:ident, $ptr:ident, $doc:expr) => {
52 #[doc = $doc]
53 pub unsafe fn $name<O: ?Sized>(_owner: &'a mut O, $ptr: *mut u8, len: usize) -> Self {
54 Self {
55 ptr: $ptr,
56 len,
57 _owner: PhantomData,
58 }
59 }
60 };
61}
62
63impl<'a> GpuMappedBuffer<'a> {
64 pub unsafe fn from_host_visible_slice(slice: &'a mut [u8]) -> Self {
74 Self {
75 ptr: slice.as_mut_ptr(),
76 len: slice.len(),
77 _owner: PhantomData,
78 }
79 }
80
81 define_mapped_owner_constructor!(
82 from_host_visible_owner,
83 ptr,
84 concat!(
85 "Construct from a raw pointer plus an explicit owner anchor.\n\n",
86 "The borrow on `owner` forces the mapped region to outlive every derived ",
87 "[`AsyncUringStream`].\n\n",
88 "# Safety\n\n",
89 "The caller must ensure that `ptr` names a `len`-byte host-visible GPU ",
90 "allocation owned by `owner`, and that no other code accesses the region ",
91 "while the returned handle is alive."
92 )
93 );
94
95 pub unsafe fn duplicate(&self) -> Self {
103 Self {
104 ptr: self.ptr,
105 len: self.len,
106 _owner: PhantomData,
107 }
108 }
109
110 pub fn sub_region(&self, offset: usize, len: usize) -> Result<Self, crate::PipelineError> {
121 let _end = vyre_driver::accounting::checked_usize_byte_range_end_lazy(
122 offset,
123 len,
124 self.len,
125 || {
126 crate::PipelineError::QueueFull {
127 queue: "submission",
128 fix: "GpuMappedBuffer::sub_region offset + len overflows usize; reduce slot size or enlarge the staging buffer",
129 }
130 },
131 |_| {
132 crate::PipelineError::QueueFull {
133 queue: "submission",
134 fix: "GpuMappedBuffer::sub_region exceeds the mapped allocation; reduce slot size or enlarge the staging buffer",
135 }
136 },
137 )?;
138 Ok(Self {
139 ptr: self.ptr.wrapping_add(offset),
140 len,
141 _owner: PhantomData,
142 })
143 }
144
145 #[must_use]
147 pub fn len(&self) -> usize {
148 self.len
149 }
150
151 #[must_use]
153 pub fn is_empty(&self) -> bool {
154 self.len == 0
155 }
156
157 pub(crate) fn as_ptr(&self) -> *mut u8 {
159 self.ptr
160 }
161
162 pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
169 unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) }
171 }
172
173 define_mapped_owner_constructor!(
174 from_bar1_peer_with_owner,
175 peer_ptr,
176 concat!(
177 "Construct from a PCIe peer-memory pointer for direct storage DMA.\n\n",
178 "# Safety\n\n",
179 "The caller must ensure that `peer_ptr` names a GPU allocation suitable ",
180 "for peer DMA, that the allocation outlives the handle, and that the ",
181 "io_uring kernel and storage driver both support DMA mapping."
182 )
183 );
184}
185
186pub struct AsyncUringStream<'a> {
189 pub(crate) ring_state: IoUringState,
190 pub(crate) gpu_buffer: GpuMappedBuffer<'a>,
191 pub(crate) megakernel_tail: &'a AtomicU32,
192 pub(crate) inflight: u32,
193 pub(crate) pending_submissions: u32,
194}
195
196unsafe impl Send for AsyncUringStream<'_> {}
199unsafe impl Sync for AsyncUringStream<'_> {}
200
201impl<'a> AsyncUringStream<'a> {
202 pub fn new(
205 ring_state: IoUringState,
206 gpu_buffer: GpuMappedBuffer<'a>,
207 megakernel_tail: &'a AtomicU32,
208 ) -> Self {
209 Self {
210 ring_state,
211 gpu_buffer,
212 megakernel_tail,
213 inflight: 0,
214 pending_submissions: 0,
215 }
216 }
217
218 pub fn replace_buffer(&mut self, gpu_buffer: GpuMappedBuffer<'a>) {
220 self.gpu_buffer = gpu_buffer;
221 }
222
223 pub unsafe fn submit_read_to_gpu(
238 &mut self,
239 fd: i32,
240 offset: u64,
241 len: u32,
242 chunk_idx: usize,
243 iovs_storage: &mut [Iovec],
244 ) -> Result<(), PipelineError> {
245 if iovs_storage.is_empty() {
246 return Err(PipelineError::QueueFull {
247 queue: "submission",
248 fix: "caller supplied empty iovs_storage; pass at least one slot",
249 });
250 }
251 let target_offset = checked_chunk_target_offset(chunk_idx, len)?;
252 unsafe { self.submit_read_to_gpu_at(fd, offset, len, target_offset, iovs_storage) }
254 }
255
256 pub unsafe fn submit_read_to_gpu_at(
272 &mut self,
273 fd: i32,
274 offset: u64,
275 len: u32,
276 target_offset: u64,
277 iovs_storage: &mut [Iovec],
278 ) -> Result<(), PipelineError> {
279 unsafe {
282 self.submit_read_to_gpu_at_with_user_data(
283 fd,
284 offset,
285 len,
286 target_offset,
287 target_offset,
288 iovs_storage,
289 )
290 }
291 }
292
293 pub unsafe fn submit_read_to_gpu_at_with_user_data(
305 &mut self,
306 fd: i32,
307 offset: u64,
308 len: u32,
309 target_offset: u64,
310 user_data: u64,
311 iovs_storage: &mut [Iovec],
312 ) -> Result<(), PipelineError> {
313 if iovs_storage.is_empty() {
314 return Err(PipelineError::QueueFull {
315 queue: "submission",
316 fix: "caller supplied empty iovs_storage; pass at least one slot",
317 });
318 }
319 let end = checked_target_end(target_offset, len)?;
320 let gpu_len = usize_to_u64(self.gpu_buffer.len(), "mapped GPU buffer length")?;
321 if end > gpu_len {
322 return Err(PipelineError::QueueFull {
323 queue: "submission",
324 fix: "target_offset + len exceeds GpuMappedBuffer length; enlarge the buffer or reduce the read size",
325 });
326 }
327
328 let Some(sqe) = self.ring_state.get_sqe() else {
329 return Err(PipelineError::QueueFull {
330 queue: "submission",
331 fix: "SQ full; call AsyncUringStream::poll to drain completions then retry",
332 });
333 };
334
335 let target_addr = unsafe {
338 self.gpu_buffer
339 .as_ptr()
340 .add(u64_to_usize(target_offset, "target offset")?)
341 };
342
343 iovs_storage[0] = Iovec {
344 iov_base: target_addr.cast::<core::ffi::c_void>(),
345 iov_len: u32_to_usize(len, "read length")?,
346 };
347
348 sqe.opcode = IORING_OP_READV;
349 sqe.fd = fd;
350 sqe.user_data_or_off = offset;
351 sqe.addr = pointer_addr_u64(iovs_storage.as_ptr(), "readv iovec pointer")?;
352 sqe.len = 1;
353 sqe.user_data = user_data;
354
355 self.ring_state.commit_sqe();
356 increment_queue_counter(&mut self.inflight, "inflight SQE count")?;
357 increment_queue_counter(&mut self.pending_submissions, "pending submission count")?;
358
359 Ok(())
360 }
361
362 pub fn flush_submissions(&mut self) -> Result<(), PipelineError> {
368 if self.pending_submissions == 0 {
369 return Ok(());
370 }
371 if self.ring_state.uses_sqpoll() {
372 if self.ring_state.sq_needs_wakeup() {
373 self.ring_state.wake_sqpoll()?;
374 }
375 } else {
376 self.ring_state.enter(self.pending_submissions, 0, 0)?;
377 }
378 self.pending_submissions = 0;
379 Ok(())
380 }
381
382 pub fn poll(&mut self) -> Result<u32, PipelineError> {
393 self.flush_submissions()?;
394 let mut completed: u32 = 0;
395 let mut first_error: Option<PipelineError> = None;
396
397 while let Some(cqe) = self.ring_state.peek_cqe() {
398 let res = cqe.res;
399 self.ring_state.advance_cq();
400 decrement_queue_counter(&mut self.inflight, "inflight SQE count")?;
401
402 if res < 0 {
403 if first_error.is_none() {
404 first_error = Some(PipelineError::IoUringSyscall {
405 syscall: "io_uring_cqe",
406 errno: -res,
407 fix: "inspect user_data to identify the failed SQE; common causes: EIO on disk, EFAULT on bad iovec, EINVAL on misaligned offset",
408 });
409 }
410 continue;
411 }
412
413 completed = vyre_driver::accounting::checked_add_u32_value(
417 completed,
418 1,
419 PipelineError::QueueFull {
420 queue: "completion",
421 fix: "io_uring completion count overflowed u32; drain completions more frequently",
422 },
423 )?;
424 }
425
426 if completed != 0 {
427 self.megakernel_tail.fetch_add(completed, Ordering::Release);
428 }
429
430 match first_error {
431 Some(err) => Err(err),
432 None => Ok(completed),
433 }
434 }
435
436 pub fn wait_for_completion(&mut self) -> Result<(), PipelineError> {
443 if self.inflight > 0 {
444 self.flush_submissions()?;
445 self.ring_state.enter(0, 1, 1)?;
446 self.poll()?;
447 }
448 Ok(())
449 }
450
451 #[must_use]
453 pub fn inflight(&self) -> u32 {
454 self.inflight
455 }
456
457 #[cfg(feature = "uring-cmd-nvme")]
483 pub unsafe fn submit_nvme_passthrough(
484 &mut self,
485 fd: i32,
486 user_data: u64,
487 nvme_sqe_bytes: &[u8],
488 ) -> Result<(), PipelineError> {
489 if nvme_sqe_bytes.len() != 64 {
490 return Err(PipelineError::QueueFull {
491 queue: "submission",
492 fix: "NVMe passthrough SQE must be exactly 64 bytes; see linux/nvme_ioctl.h",
493 });
494 }
495
496 let Some(sqe) = self.ring_state.get_sqe() else {
497 return Err(PipelineError::QueueFull {
498 queue: "submission",
499 fix: "SQ full; call AsyncUringStream::poll to drain completions then retry",
500 });
501 };
502
503 let nvme_ptr = nvme_sqe_bytes.as_ptr();
508 sqe.opcode = IORING_OP_URING_CMD;
509 sqe.fd = fd;
510 sqe.user_data_or_off = 0;
511 sqe.addr = pointer_addr_u64(nvme_ptr, "NVMe command pointer")?;
513 sqe.len = 64;
514 sqe.user_data = user_data;
515 sqe.addr3 = pointer_addr_u64(nvme_ptr, "NVMe command addr3 pointer")?;
518
519 self.ring_state.commit_sqe();
520 increment_queue_counter(&mut self.inflight, "inflight SQE count")?;
521 increment_queue_counter(&mut self.pending_submissions, "pending submission count")?;
522
523 Ok(())
524 }
525
526 pub unsafe fn submit_read_fixed(
546 &mut self,
547 fd: i32,
548 offset: u64,
549 len: u32,
550 chunk_idx: usize,
551 buf_index: u16,
552 ) -> Result<(), PipelineError> {
553 let target_offset = checked_chunk_target_offset(chunk_idx, len)?;
554 unsafe {
556 self.submit_read_fixed_at(
557 fd,
558 offset,
559 len,
560 target_offset,
561 buf_index,
562 usize_to_u64(chunk_idx, "chunk index")?,
563 )
564 }
565 }
566
567 pub unsafe fn submit_read_fixed_at(
586 &mut self,
587 fd: i32,
588 offset: u64,
589 len: u32,
590 target_offset: u64,
591 buf_index: u16,
592 user_data: u64,
593 ) -> Result<(), PipelineError> {
594 let end = checked_target_end(target_offset, len)?;
595 let gpu_len = usize_to_u64(self.gpu_buffer.len(), "mapped GPU buffer length")?;
596 if end > gpu_len {
597 return Err(PipelineError::QueueFull {
598 queue: "submission",
599 fix: "chunk_idx * len exceeds GpuMappedBuffer length",
600 });
601 }
602
603 let Some(sqe) = self.ring_state.get_sqe() else {
604 return Err(PipelineError::QueueFull {
605 queue: "submission",
606 fix: "SQ full; call AsyncUringStream::poll to drain completions then retry",
607 });
608 };
609
610 let target_addr = unsafe {
613 self.gpu_buffer
614 .as_ptr()
615 .add(u64_to_usize(target_offset, "target offset")?)
616 };
617
618 sqe.opcode = IORING_OP_READ_FIXED;
619 sqe.fd = fd;
620 sqe.user_data_or_off = offset;
621 sqe.addr = pointer_addr_u64(target_addr, "fixed-read target pointer")?;
622 sqe.len = len;
623 sqe.buf_index = buf_index;
624 sqe.user_data = user_data;
625
626 self.ring_state.commit_sqe();
627 increment_queue_counter(&mut self.inflight, "inflight SQE count")?;
628 increment_queue_counter(&mut self.pending_submissions, "pending submission count")?;
629
630 Ok(())
631 }
632
633 pub unsafe fn submit_read_to_gpu_fixed_file(
648 &mut self,
649 file_index: i32,
650 offset: u64,
651 len: u32,
652 chunk_idx: usize,
653 iovs_storage: &mut [Iovec],
654 ) -> Result<(), PipelineError> {
655 if iovs_storage.is_empty() {
656 return Err(PipelineError::QueueFull {
657 queue: "submission",
658 fix: "caller supplied empty iovs_storage; pass at least one slot",
659 });
660 }
661 let target_offset = checked_chunk_target_offset(chunk_idx, len)?;
662 let end = checked_target_end(target_offset, len)?;
663 let gpu_len = usize_to_u64(self.gpu_buffer.len(), "mapped GPU buffer length")?;
664 if end > gpu_len {
665 return Err(PipelineError::QueueFull {
666 queue: "submission",
667 fix: "chunk_idx * len exceeds GpuMappedBuffer length",
668 });
669 }
670
671 let Some(sqe) = self.ring_state.get_sqe() else {
672 return Err(PipelineError::QueueFull {
673 queue: "submission",
674 fix: "SQ full; call AsyncUringStream::poll to drain completions then retry",
675 });
676 };
677
678 let target_addr = unsafe {
681 self.gpu_buffer
682 .as_ptr()
683 .add(u64_to_usize(target_offset, "target offset")?)
684 };
685 iovs_storage[0] = Iovec {
686 iov_base: target_addr.cast::<core::ffi::c_void>(),
687 iov_len: u32_to_usize(len, "read length")?,
688 };
689
690 sqe.opcode = IORING_OP_READV;
691 sqe.flags = super::ring::IOSQE_FIXED_FILE;
692 sqe.fd = file_index;
693 sqe.user_data_or_off = offset;
694 sqe.addr = pointer_addr_u64(iovs_storage.as_ptr(), "fixed-file readv iovec pointer")?;
695 sqe.len = 1;
696 sqe.user_data = usize_to_u64(chunk_idx, "chunk index")?;
697
698 self.ring_state.commit_sqe();
699 increment_queue_counter(&mut self.inflight, "inflight SQE count")?;
700 increment_queue_counter(&mut self.pending_submissions, "pending submission count")?;
701
702 Ok(())
703 }
704
705 #[cfg(not(feature = "uring-cmd-nvme"))]
709 #[allow(clippy::unused_self, clippy::missing_safety_doc)]
710 pub unsafe fn submit_nvme_passthrough(
711 &mut self,
712 _fd: i32,
713 _user_data: u64,
714 _nvme_sqe_bytes: &[u8],
715 ) -> Result<(), PipelineError> {
716 Err(PipelineError::NvmePassthroughDisabled)
717 }
718}
719
720fn checked_chunk_target_offset(chunk_idx: usize, len: u32) -> Result<u64, PipelineError> {
721 let chunk_idx = usize_to_u64(chunk_idx, "chunk index")?;
722 vyre_driver::accounting::checked_mul_u64_lazy(chunk_idx, u64::from(len), || {
723 PipelineError::QueueFull {
724 queue: "submission",
725 fix: "chunk_idx * len overflows u64; split the IO batch before submission",
726 }
727 })
728}
729
730fn checked_target_end(target_offset: u64, len: u32) -> Result<u64, PipelineError> {
731 vyre_driver::accounting::checked_add_u64_lazy(target_offset, u64::from(len), || {
732 PipelineError::QueueFull {
733 queue: "submission",
734 fix: "target_offset + len overflows u64; split the IO batch before submission",
735 }
736 })
737}
738
739fn increment_queue_counter(counter: &mut u32, label: &'static str) -> Result<(), PipelineError> {
740 *counter = vyre_driver::accounting::checked_add_u32_value(
741 *counter,
742 1,
743 PipelineError::QueueFull {
744 queue: "submission",
745 fix: match label {
746 "inflight SQE count" => {
747 "inflight SQE count overflowed u32; poll completions before submitting more work"
748 }
749 "pending submission count" => {
750 "pending submission count overflowed u32; flush submissions before queuing more work"
751 }
752 _ => {
753 "io_uring queue counter overflowed u32; drain the queue before submitting more work"
754 }
755 },
756 },
757 )?;
758 Ok(())
759}
760
761fn decrement_queue_counter(counter: &mut u32, label: &'static str) -> Result<(), PipelineError> {
762 *counter = counter.checked_sub(1).ok_or(PipelineError::QueueFull {
763 queue: "completion",
764 fix: match label {
765 "inflight SQE count" => {
766 "io_uring completion arrived with no inflight SQE; rebuild the stream state"
767 }
768 _ => "io_uring queue counter underflowed; rebuild the stream state",
769 },
770 })?;
771 Ok(())
772}
773
774fn usize_to_u64(value: usize, label: &'static str) -> Result<u64, PipelineError> {
775 u64::try_from(value).map_err(|_| PipelineError::QueueFull {
776 queue: "submission",
777 fix: match label {
778 "chunk index" => "chunk index cannot fit u64; split the IO batch before submission",
779 "mapped GPU buffer length" => {
780 "mapped GPU buffer length cannot fit u64; split the staging allocation"
781 }
782 _ => "host usize value cannot fit u64; split the IO batch before submission",
783 },
784 })
785}
786
787fn pointer_addr_u64<T>(ptr: *const T, label: &'static str) -> Result<u64, PipelineError> {
788 usize_to_u64(ptr.addr(), label)
789}
790
791fn u64_to_usize(value: u64, label: &'static str) -> Result<usize, PipelineError> {
792 usize::try_from(value).map_err(|_| PipelineError::QueueFull {
793 queue: "submission",
794 fix: match label {
795 "target offset" => {
796 "target offset cannot fit usize; split the IO batch before submission"
797 }
798 _ => "u64 value cannot fit usize; split the IO batch before submission",
799 },
800 })
801}
802
803fn u32_to_usize(value: u32, label: &'static str) -> Result<usize, PipelineError> {
804 usize::try_from(value).map_err(|_| PipelineError::QueueFull {
805 queue: "submission",
806 fix: match label {
807 "read length" => "read length cannot fit usize; split the IO request before submission",
808 _ => "u32 value cannot fit usize; split the IO request before submission",
809 },
810 })
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816
817 #[test]
818 fn mapped_slice_roundtrip_is_miri_clean() {
819 let mut backing = [1_u8, 2, 3, 4];
820 let mut mapped = unsafe { GpuMappedBuffer::from_host_visible_slice(&mut backing) };
822 let slice = unsafe { mapped.as_mut_slice() };
824 slice[0] = 9;
825 slice[3] = 7;
826 assert_eq!(backing, [9, 2, 3, 7]);
827 }
828}