1const MAX_RDMA_MSG_SIZE: usize = 1024 * 1024 * 1024;
17
18use std::collections::HashMap;
19use std::collections::HashSet;
20use std::collections::VecDeque;
21use std::io::Error;
22use std::result::Result;
23use std::sync::Arc;
24use std::time::Duration;
25use std::time::Instant;
26
27use async_trait::async_trait;
28use backoff::ExponentialBackoff;
29use backoff::ExponentialBackoffBuilder;
30use backoff::backoff::Backoff;
31use hyperactor::Actor;
32use hyperactor::ActorId;
33use hyperactor::ActorRef;
34use hyperactor::Context;
35use hyperactor::Endpoint as _;
36use hyperactor::Handler;
37use hyperactor::Instance;
38use hyperactor::PortHandle;
39use hyperactor::actor::Referable;
40use hyperactor::actor::RemoteHandles;
41use hyperactor::context::Mailbox;
42use serde::Deserialize;
43use serde::Serialize;
44use typeuri::Named;
45
46use super::IbvBuffer;
47use super::IbvOp;
48use super::domain::IbvDomain;
49use super::domain::IbvDomainImpl;
50use super::manager_actor::CreatePeerQueuePair;
51use super::memory_region::IbvMemoryRegionView;
52use super::primitives::Gid;
53use super::primitives::GidScope;
54use super::primitives::GidType;
55use super::primitives::IbvConfig;
56use super::primitives::IbvCq;
57use super::primitives::IbvOperation;
58use super::primitives::IbvQp;
59use super::primitives::IbvQpInfo;
60use super::primitives::IbvWc;
61use super::primitives::resolve_qp_type;
62use crate::RdmaOpType;
63
64#[derive(Debug)]
70pub struct WorkRequestError {
71 pub wr_id: u64,
72 pub status: rdmaxcel_sys::ibv_wc_status::Type,
73 pub vendor_err: u32,
74 message: String,
75}
76
77impl std::fmt::Display for WorkRequestError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.write_str(&self.message)
80 }
81}
82
83impl std::error::Error for WorkRequestError {}
84
85impl WorkRequestError {
86 pub fn is_wr_flush_err(&self) -> bool {
90 self.status == rdmaxcel_sys::ibv_wc_status::IBV_WC_WR_FLUSH_ERR
91 }
92
93 #[cfg(test)]
94 pub(super) fn for_test(wr_id: u64, message: &str) -> Self {
95 Self {
96 wr_id,
97 status: rdmaxcel_sys::ibv_wc_status::IBV_WC_GENERAL_ERR,
98 vendor_err: 0,
99 message: message.to_string(),
100 }
101 }
102}
103
104#[derive(Debug)]
108pub struct PollCompletionError {
109 message: String,
110}
111
112impl std::fmt::Display for PollCompletionError {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.write_str(&self.message)
115 }
116}
117
118impl std::error::Error for PollCompletionError {}
119
120impl PollCompletionError {
121 #[cfg(test)]
122 pub(super) fn for_test(message: &str) -> Self {
123 Self {
124 message: message.to_string(),
125 }
126 }
127}
128
129#[derive(Debug, Named, Clone, Serialize, Deserialize)]
133pub struct DoorBell {
134 pub src_ptr: usize,
135 pub dst_ptr: usize,
136 pub size: usize,
137}
138wirevalue::register_type!(DoorBell);
139
140#[derive(Debug, Clone, Copy, PartialEq)]
142pub enum PollTarget {
143 Send,
144 Recv,
145}
146
147pub mod legacy;
150
151#[derive(Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize, Named)]
157pub(super) struct QpKey {
158 pub(super) self_device: String,
159 pub(super) other_id: ActorId,
160 pub(super) other_device: String,
161}
162
163pub trait IbvQueuePair: std::fmt::Debug + Send + Sync + 'static + Sized {
180 unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
190 domain: &IbvDomain<I>,
191 config: IbvConfig,
192 ) -> Result<Self, anyhow::Error>;
193
194 fn connect(&mut self, info: &IbvQpInfo) -> Result<(), anyhow::Error>;
196
197 fn get_qp_info(&mut self) -> Result<IbvQpInfo, anyhow::Error>;
200
201 fn state(&mut self) -> Result<u32, anyhow::Error>;
203
204 fn put(
208 &mut self,
209 remote_dst: IbvBuffer,
210 local_src: IbvBuffer,
211 ) -> Result<Vec<u64>, anyhow::Error>;
212
213 fn get(
217 &mut self,
218 local_dst: IbvBuffer,
219 remote_src: IbvBuffer,
220 ) -> Result<Vec<u64>, anyhow::Error>;
221
222 fn poll_completion(
233 &mut self,
234 target: PollTarget,
235 ) -> Result<Option<Result<IbvWc, WorkRequestError>>, PollCompletionError>;
236}
237
238impl IbvQueuePair for legacy::IbvQueuePair {
239 unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
240 domain: &IbvDomain<I>,
241 config: IbvConfig,
242 ) -> Result<Self, anyhow::Error> {
243 legacy::IbvQueuePair::new(domain, config)
244 }
245
246 fn connect(&mut self, info: &IbvQpInfo) -> Result<(), anyhow::Error> {
247 legacy::IbvQueuePair::connect(self, info)
248 }
249
250 fn get_qp_info(&mut self) -> Result<IbvQpInfo, anyhow::Error> {
251 legacy::IbvQueuePair::get_qp_info(self)
252 }
253
254 fn state(&mut self) -> Result<u32, anyhow::Error> {
255 legacy::IbvQueuePair::state(self)
256 }
257
258 fn put(
259 &mut self,
260 remote_dst: IbvBuffer,
261 local_src: IbvBuffer,
262 ) -> Result<Vec<u64>, anyhow::Error> {
263 legacy::IbvQueuePair::put(self, local_src, remote_dst)
264 }
265
266 fn get(
267 &mut self,
268 local_dst: IbvBuffer,
269 remote_src: IbvBuffer,
270 ) -> Result<Vec<u64>, anyhow::Error> {
271 legacy::IbvQueuePair::get(self, local_dst, remote_src)
272 }
273
274 fn poll_completion(
275 &mut self,
276 target: PollTarget,
277 ) -> Result<Option<Result<IbvWc, WorkRequestError>>, PollCompletionError> {
278 legacy::IbvQueuePair::poll_completion(self, target)
279 }
280}
281
282pub(super) unsafe fn get_qp_info(
290 qp: *mut rdmaxcel_sys::ibv_qp,
291 context: *mut rdmaxcel_sys::ibv_context,
292 config: &IbvConfig,
293 gid: Gid,
294) -> Result<IbvQpInfo, anyhow::Error> {
295 let mut port_attr = rdmaxcel_sys::ibv_port_attr::default();
296 let errno = unsafe {
300 rdmaxcel_sys::ibv_query_port(
301 context,
302 config.port_num,
303 &mut port_attr as *mut rdmaxcel_sys::ibv_port_attr as *mut _,
304 )
305 };
306 if errno != 0 {
307 return Err(anyhow::anyhow!(
308 "failed to query port attributes: {}",
309 Error::from_raw_os_error(errno)
310 ));
311 }
312
313 let qp_num = unsafe { (*qp).qp_num };
315 Ok(IbvQpInfo {
316 qp_num,
317 lid: port_attr.lid,
318 gid: Some(gid),
319 psn: config.psn,
320 })
321}
322
323unsafe fn state(qp: *mut rdmaxcel_sys::ibv_qp) -> Result<u32, anyhow::Error> {
329 let mut qp_attr = rdmaxcel_sys::ibv_qp_attr::default();
330 let mut qp_init_attr = rdmaxcel_sys::ibv_qp_init_attr::default();
331 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE;
332 let errno =
335 unsafe { rdmaxcel_sys::ibv_query_qp(qp, &mut qp_attr, mask.0 as i32, &mut qp_init_attr) };
336 if errno != 0 {
337 return Err(anyhow::anyhow!(
338 "failed to query QP state: {}",
339 Error::from_raw_os_error(errno)
340 ));
341 }
342 Ok(qp_attr.qp_state)
343}
344
345pub(super) unsafe fn connect(
354 qp: *mut rdmaxcel_sys::ibv_qp,
355 config: &IbvConfig,
356 access_flags: i32,
357 info: &IbvQpInfo,
358 sgid_index: u8,
359) -> Result<(), anyhow::Error> {
360 let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
362 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_INIT,
363 qp_access_flags: access_flags as u32,
364 pkey_index: config.pkey_index,
365 port_num: config.port_num,
366 ..Default::default()
367 };
368 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
369 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PKEY_INDEX
370 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PORT
371 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_ACCESS_FLAGS;
372 let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
376 if errno != 0 {
377 return Err(anyhow::anyhow!(
378 "failed to transition QP to INIT: {}",
379 Error::from_raw_os_error(errno)
380 ));
381 }
382
383 let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
385 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTR,
386 path_mtu: config.path_mtu,
387 dest_qp_num: info.qp_num,
388 rq_psn: info.psn,
389 max_dest_rd_atomic: config.max_dest_rd_atomic,
390 min_rnr_timer: config.min_rnr_timer,
391 ah_attr: rdmaxcel_sys::ibv_ah_attr {
392 dlid: info.lid,
393 sl: 0,
394 src_path_bits: 0,
395 port_num: config.port_num,
396 grh: Default::default(),
397 ..Default::default()
398 },
399 ..Default::default()
400 };
401 if let Some(gid) = info.gid {
402 qp_attr.ah_attr.is_global = 1;
403 qp_attr.ah_attr.grh.dgid = rdmaxcel_sys::ibv_gid::from(gid);
404 qp_attr.ah_attr.grh.hop_limit = 0xff;
405 qp_attr.ah_attr.grh.sgid_index = sgid_index;
406 } else {
407 qp_attr.ah_attr.is_global = 0;
408 }
409 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
410 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_AV
411 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PATH_MTU
412 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_DEST_QPN
413 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RQ_PSN
414 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MAX_DEST_RD_ATOMIC
415 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MIN_RNR_TIMER;
416 let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
418 if errno != 0 {
419 return Err(anyhow::anyhow!(
420 "failed to transition QP to RTR: {}",
421 Error::from_raw_os_error(errno)
422 ));
423 }
424
425 let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
427 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTS,
428 sq_psn: config.psn,
429 max_rd_atomic: config.max_rd_atomic,
430 retry_cnt: config.retry_cnt,
431 rnr_retry: config.rnr_retry,
432 timeout: config.qp_timeout,
433 ..Default::default()
434 };
435 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
436 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_TIMEOUT
437 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RETRY_CNT
438 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_SQ_PSN
439 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RNR_RETRY
440 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MAX_QP_RD_ATOMIC;
441 let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
443 if errno != 0 {
444 return Err(anyhow::anyhow!(
445 "failed to transition QP to RTS: {}",
446 Error::from_raw_os_error(errno)
447 ));
448 }
449 Ok(())
450}
451
452#[derive(Debug)]
460pub struct RCQueuePair {
461 qp: IbvQp,
462 config: IbvConfig,
463 gid: Gid,
467 access_flags: i32,
470 next_wr_id: u64,
473}
474
475impl RCQueuePair {
476 pub(super) unsafe fn from_qp(
488 qp: IbvQp,
489 config: IbvConfig,
490 gid: Gid,
491 access_flags: i32,
492 ) -> Self {
493 RCQueuePair {
494 qp,
495 config,
496 gid,
497 access_flags,
498 next_wr_id: 0,
499 }
500 }
501
502 fn post_chunked(
506 &mut self,
507 op: IbvOperation,
508 laddr: usize,
509 lkey: u32,
510 raddr: usize,
511 rkey: u32,
512 total_size: usize,
513 ) -> Result<Vec<u64>, anyhow::Error> {
514 let mut remaining = total_size;
515 let mut offset = 0;
516 let mut wr_ids = Vec::new();
517 while remaining > 0 {
518 let chunk = std::cmp::min(remaining, MAX_RDMA_MSG_SIZE);
519 let wr_id = self.next_wr_id;
520 self.next_wr_id += 1;
521 self.post_one(op, laddr + offset, lkey, chunk, raddr + offset, rkey, wr_id)?;
522 wr_ids.push(wr_id);
523 remaining -= chunk;
524 offset += chunk;
525 }
526 Ok(wr_ids)
527 }
528
529 fn post_one(
531 &self,
532 op: IbvOperation,
533 laddr: usize,
534 lkey: u32,
535 length: usize,
536 raddr: usize,
537 rkey: u32,
538 wr_id: u64,
539 ) -> Result<(), anyhow::Error> {
540 let qp = self.qp.as_ptr();
541 let context = self.qp.context().as_ptr();
542 let mut sge = rdmaxcel_sys::ibv_sge {
543 addr: laddr as u64,
544 length: length as u32,
545 lkey,
546 };
547 let mut wr = rdmaxcel_sys::ibv_send_wr {
548 wr_id,
549 next: std::ptr::null_mut(),
550 sg_list: &mut sge as *mut _,
551 num_sge: 1,
552 opcode: op.into(),
553 send_flags: rdmaxcel_sys::ibv_send_flags::IBV_SEND_SIGNALED.0,
554 wr: Default::default(),
555 qp_type: Default::default(),
556 __bindgen_anon_1: Default::default(),
557 __bindgen_anon_2: Default::default(),
558 };
559 wr.wr.rdma.remote_addr = raddr as u64;
562 wr.wr.rdma.rkey = rkey;
563 let mut bad_wr: *mut rdmaxcel_sys::ibv_send_wr = std::ptr::null_mut();
564 let errno = unsafe {
568 let post_send = (*context)
569 .ops
570 .post_send
571 .expect("post_send verb missing from ibv_context ops");
572 post_send(qp, &mut wr as *mut _, &mut bad_wr)
573 };
574 if errno != 0 {
575 return Err(anyhow::anyhow!(
576 "failed to post {:?} request: {}",
577 op,
578 Error::from_raw_os_error(errno)
579 ));
580 }
581 Ok(())
582 }
583}
584
585impl IbvQueuePair for RCQueuePair {
586 unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
587 domain: &IbvDomain<I>,
588 config: IbvConfig,
589 ) -> Result<Self, anyhow::Error> {
590 tracing::debug!("creating an RCQueuePair from config {}", config);
591 let pd = domain.as_ptr();
595 if pd.is_null() {
596 anyhow::bail!("cannot create an RCQueuePair on a null protection domain");
597 }
598
599 let gid = domain.device_info().select_gid(
602 config.port_num,
603 Some(GidScope::Global),
604 Some(GidType::RoCEv2),
605 )?;
606
607 let send_cq = unsafe { IbvCq::create(domain.context().clone(), config.cq_entries) }?;
612 let recv_cq = unsafe { IbvCq::create(domain.context().clone(), config.cq_entries) }?;
614
615 let mut init_attr = rdmaxcel_sys::ibv_qp_init_attr {
617 send_cq: send_cq.as_ptr(),
618 recv_cq: recv_cq.as_ptr(),
619 cap: rdmaxcel_sys::ibv_qp_cap {
620 max_send_wr: config.max_send_wr,
621 max_recv_wr: config.max_recv_wr,
622 max_send_sge: config.max_send_sge,
623 max_recv_sge: config.max_recv_sge,
624 max_inline_data: 0,
625 },
626 qp_type: rdmaxcel_sys::ibv_qp_type::IBV_QPT_RC,
627 sq_sig_all: 0,
628 ..Default::default()
629 };
630 let qp = unsafe { rdmaxcel_sys::ibv_create_qp(pd, &mut init_attr) };
634 if qp.is_null() {
635 anyhow::bail!(
637 "failed to create queue pair (QP): {}",
638 Error::last_os_error()
639 );
640 }
641 let qp = unsafe { IbvQp::from_raw(qp, send_cq, recv_cq, domain.pd().clone()) };
645 let access_flags = domain.access_flags();
646 Ok(unsafe { Self::from_qp(qp, config, gid, access_flags) })
649 }
650
651 fn connect(&mut self, info: &IbvQpInfo) -> Result<(), anyhow::Error> {
652 unsafe {
654 connect(
655 self.qp.as_ptr(),
656 &self.config,
657 self.access_flags,
658 info,
659 self.gid.index(),
660 )
661 }
662 }
663
664 fn get_qp_info(&mut self) -> Result<IbvQpInfo, anyhow::Error> {
665 let context = self.qp.context().as_ptr();
666 unsafe { get_qp_info(self.qp.as_ptr(), context, &self.config, self.gid) }
669 }
670
671 fn state(&mut self) -> Result<u32, anyhow::Error> {
672 unsafe { state(self.qp.as_ptr()) }
674 }
675
676 fn put(
677 &mut self,
678 remote_dst: IbvBuffer,
679 local_src: IbvBuffer,
680 ) -> Result<Vec<u64>, anyhow::Error> {
681 if remote_dst.size < local_src.size {
682 return Err(anyhow::anyhow!(
683 "remote buffer size ({}) is smaller than local buffer size ({})",
684 remote_dst.size,
685 local_src.size
686 ));
687 }
688 self.post_chunked(
689 IbvOperation::Write,
690 local_src.addr,
691 local_src.lkey,
692 remote_dst.addr,
693 remote_dst.rkey,
694 local_src.size,
695 )
696 }
697
698 fn get(
699 &mut self,
700 local_dst: IbvBuffer,
701 remote_src: IbvBuffer,
702 ) -> Result<Vec<u64>, anyhow::Error> {
703 if local_dst.size < remote_src.size {
704 return Err(anyhow::anyhow!(
705 "local buffer size ({}) is smaller than remote buffer size ({})",
706 local_dst.size,
707 remote_src.size
708 ));
709 }
710 self.post_chunked(
711 IbvOperation::Read,
712 local_dst.addr,
713 local_dst.lkey,
714 remote_src.addr,
715 remote_src.rkey,
716 remote_src.size,
717 )
718 }
719
720 fn poll_completion(
721 &mut self,
722 target: PollTarget,
723 ) -> Result<Option<Result<IbvWc, WorkRequestError>>, PollCompletionError> {
724 let (cq, cq_type) = match target {
725 PollTarget::Send => (self.qp.send_cq().as_ptr(), "send"),
726 PollTarget::Recv => (self.qp.recv_cq().as_ptr(), "recv"),
727 };
728 let context = self.qp.context().as_ptr();
729 let poll_cq = unsafe {
732 (*context)
733 .ops
734 .poll_cq
735 .expect("poll_cq verb missing from ibv_context ops")
736 };
737 let mut wc = rdmaxcel_sys::ibv_wc::default();
738 let ret = unsafe { poll_cq(cq, 1, &mut wc) };
742
743 if ret < 0 {
744 return Err(PollCompletionError {
745 message: format!("{} CQ poll failed (ibv_poll_cq returned {})", cq_type, ret),
746 });
747 }
748 if ret == 0 {
749 return Ok(None);
750 }
751
752 if let Some((status, vendor_err)) = wc.error() {
755 return Ok(Some(Err(WorkRequestError {
756 wr_id: wc.wr_id(),
757 status,
758 vendor_err,
759 message: format!(
760 "{} completion failed for wr_id={}: status={:?}, vendor_err={}",
761 cq_type,
762 wc.wr_id(),
763 status,
764 vendor_err,
765 ),
766 })));
767 }
768 Ok(Some(Ok(IbvWc::from(wc))))
769 }
770}
771
772#[derive(Debug)]
788struct PollSleepPolicy {
789 yield_window: Option<Duration>,
790 started_at: Option<Instant>,
791 backoff: Option<ExponentialBackoff>,
792}
793
794impl PollSleepPolicy {
795 fn new() -> Self {
796 let yield_window = hyperactor_config::global::get(crate::config::RDMA_CQ_BUSY_POLL_WINDOW);
797 Self {
798 yield_window,
799 started_at: None,
800 backoff: None,
801 }
802 }
803
804 fn reset(&mut self) {
807 self.started_at = None;
808 self.backoff = None;
809 }
810
811 fn next_interval(&mut self) -> Duration {
814 let Some(window) = self.yield_window else {
815 return Duration::ZERO;
816 };
817 let started = *self.started_at.get_or_insert_with(Instant::now);
818 if started.elapsed() < window {
819 return Duration::ZERO;
820 }
821 let backoff = self.backoff.get_or_insert_with(|| {
822 ExponentialBackoffBuilder::new()
823 .with_initial_interval(Duration::from_millis(1))
824 .with_max_interval(Duration::from_millis(10))
825 .with_multiplier(2.0)
826 .with_randomization_factor(0.0)
827 .with_max_elapsed_time(None)
828 .build()
829 });
830 backoff.next_backoff().unwrap_or(Duration::ZERO)
831 }
832}
833
834pub(super) trait Manager:
837 Actor + Referable + RemoteHandles<CreatePeerQueuePair<Self>>
838{
839}
840
841impl<T> Manager for T where T: Actor + Referable + RemoteHandles<CreatePeerQueuePair<T>> {}
842
843#[allow(dead_code)] #[derive(Debug)]
851pub(super) struct OpResult {
852 pub(super) op_idx: usize,
853 pub(super) result: Result<(), String>,
854}
855
856#[derive(Debug)]
866pub(super) struct ProcessOps<M: Referable> {
867 pub(super) items: Vec<(usize, IbvOp<M>, IbvMemoryRegionView)>,
868 pub(super) reply: PortHandle<OpResult>,
869}
870
871#[derive(Debug)]
873struct Tick;
874
875#[derive(Debug)]
877struct PendingOp<M: Referable> {
878 op_idx: usize,
879 op: IbvOp<M>,
880 mrv: IbvMemoryRegionView,
881 reply: PortHandle<OpResult>,
882 wrs: u32,
886 is_read: bool,
887}
888
889#[derive(Debug)]
891struct PostedOpEntry {
892 op_idx: usize,
893 pending_wrs: HashSet<u64>,
894 is_read: bool,
895 _mrv: IbvMemoryRegionView,
898 reply: PortHandle<OpResult>,
899 first_error: Option<String>,
903}
904
905#[derive(Debug)]
913pub(super) struct QueuePairActor<M: Manager, Qp: IbvQueuePair> {
914 qp_key: QpKey,
915 local_manager: ActorRef<M>,
918 peer_manager: ActorRef<M>,
920 qp: Qp,
921 is_loopback: bool,
927 init_timeout: Duration,
928 max_send_wr: u32,
930 max_rd_atomic: u32,
935 queue: VecDeque<PendingOp<M>>,
940 posted: HashMap<u64, PostedOpEntry>,
946 wr_to_op: HashMap<u64, u64>,
948 next_op_id: u64,
949 in_flight_reads: u32,
950 in_flight_writes: u32,
951 tick_armed: bool,
954 poll_policy: PollSleepPolicy,
955}
956
957impl<M: Manager, Qp: IbvQueuePair> QueuePairActor<M, Qp> {
958 pub(super) fn new(
959 qp_key: QpKey,
960 local_manager: ActorRef<M>,
961 peer_manager: ActorRef<M>,
962 qp: Qp,
963 is_loopback: bool,
964 max_send_wr: u32,
965 max_rd_atomic: u32,
966 ) -> Self {
967 let init_timeout = hyperactor_config::global::get(crate::config::RDMA_QP_INIT_TIMEOUT);
968 let max_rd_atomic = if max_rd_atomic == 0 {
972 max_send_wr
973 } else {
974 max_rd_atomic
975 };
976 Self {
977 qp_key,
978 local_manager,
979 peer_manager,
980 qp,
981 is_loopback,
982 init_timeout,
983 max_send_wr,
984 max_rd_atomic,
985 queue: VecDeque::new(),
986 posted: HashMap::new(),
987 wr_to_op: HashMap::new(),
988 next_op_id: 0,
989 in_flight_reads: 0,
990 in_flight_writes: 0,
991 tick_armed: false,
992 poll_policy: PollSleepPolicy::new(),
993 }
994 }
995
996 fn wr_count(local_size: usize) -> u32 {
1001 local_size.div_ceil(MAX_RDMA_MSG_SIZE).max(1) as u32
1002 }
1003
1004 fn try_post_head(&mut self, cx: &Instance<Self>) -> Result<bool, anyhow::Error> {
1016 let pending = self.queue.pop_front().expect("non-empty queue");
1017 let PendingOp {
1018 op_idx,
1019 op,
1020 mrv,
1021 reply,
1022 wrs,
1023 is_read,
1024 } = pending;
1025
1026 let local_buf = IbvBuffer {
1027 lkey: mrv.lkey,
1028 rkey: mrv.rkey,
1029 addr: mrv.rdma_addr,
1030 size: mrv.size,
1031 device_name: mrv.device_name.clone(),
1032 };
1033
1034 if wrs > self.max_send_wr {
1036 let err = format!(
1037 "op too large for this QP [op_idx={}, qp_key={:?}, op_type={:?}, wrs={}, max_send_wr={}, local: {:?}, remote: {:?}]",
1038 op_idx, self.qp_key, op.op_type, wrs, self.max_send_wr, local_buf, op.remote_buffer,
1039 );
1040 reply.try_post(
1041 cx,
1042 OpResult {
1043 op_idx,
1044 result: Err(err),
1045 },
1046 )?;
1047 return Ok(true);
1048 }
1049 if is_read && wrs > self.max_rd_atomic {
1050 let err = format!(
1051 "read op too large for this QP [op_idx={}, qp_key={:?}, wrs={}, max_rd_atomic={}, local: {:?}, remote: {:?}]",
1052 op_idx, self.qp_key, wrs, self.max_rd_atomic, local_buf, op.remote_buffer,
1053 );
1054 reply.try_post(
1055 cx,
1056 OpResult {
1057 op_idx,
1058 result: Err(err),
1059 },
1060 )?;
1061 return Ok(true);
1062 }
1063
1064 let projected_total = self.in_flight_reads + self.in_flight_writes + wrs;
1068 if projected_total > self.max_send_wr
1069 || (is_read && self.in_flight_reads + wrs > self.max_rd_atomic)
1070 {
1071 self.queue.push_front(PendingOp {
1072 op_idx,
1073 op,
1074 mrv,
1075 reply,
1076 wrs,
1077 is_read,
1078 });
1079 return Ok(false);
1080 }
1081
1082 let post_result = match op.op_type {
1084 RdmaOpType::WriteFromLocal => self.qp.put(op.remote_buffer.clone(), local_buf.clone()),
1085 RdmaOpType::ReadIntoLocal => self.qp.get(local_buf.clone(), op.remote_buffer.clone()),
1086 };
1087 let wr_ids = post_result.map_err(|e| {
1088 anyhow::anyhow!(
1089 "qp.{} failed [op_idx={}, qp_key={:?}, local: {:?}, remote: {:?}]: {e}",
1090 if is_read { "get" } else { "put" },
1091 op_idx,
1092 self.qp_key,
1093 local_buf,
1094 op.remote_buffer,
1095 )
1096 })?;
1097
1098 let op_id = self.next_op_id;
1100 self.next_op_id += 1;
1101 let mut pending_wrs = HashSet::with_capacity(wr_ids.len());
1102 for id in &wr_ids {
1103 self.wr_to_op.insert(*id, op_id);
1104 pending_wrs.insert(*id);
1105 }
1106 if is_read {
1107 self.in_flight_reads += wr_ids.len() as u32;
1108 } else {
1109 self.in_flight_writes += wr_ids.len() as u32;
1110 }
1111 self.posted.insert(
1112 op_id,
1113 PostedOpEntry {
1114 op_idx,
1115 pending_wrs,
1116 is_read,
1117 _mrv: mrv,
1118 reply,
1119 first_error: None,
1120 },
1121 );
1122 Ok(true)
1123 }
1124
1125 fn advance(&mut self, cx: &Instance<Self>) -> Result<(), anyhow::Error> {
1131 while !self.queue.is_empty() {
1134 if !self.try_post_head(cx)? {
1135 break;
1136 }
1137 }
1138
1139 let mut progressed = false;
1146 while !self.wr_to_op.is_empty() {
1147 let completion = self
1148 .qp
1149 .poll_completion(PollTarget::Send)
1150 .map_err(|e| anyhow::anyhow!("CQ poll failed for qp_key={:?}: {e}", self.qp_key))?;
1151 let Some(wc_result) = completion else {
1152 break;
1153 };
1154 progressed = true;
1155
1156 let (wr_id, wr_error) = match wc_result {
1157 Ok(wc) => (wc.wr_id(), None),
1158 Err(per_wr) => (per_wr.wr_id, Some(per_wr.to_string())),
1159 };
1160
1161 let op_id = self
1162 .wr_to_op
1163 .remove(&wr_id)
1164 .expect("completed wr_id missing from wr_to_op");
1165 let entry = self
1166 .posted
1167 .get_mut(&op_id)
1168 .expect("op_id missing from posted");
1169 entry.pending_wrs.remove(&wr_id);
1170 if entry.is_read {
1171 self.in_flight_reads -= 1;
1172 } else {
1173 self.in_flight_writes -= 1;
1174 }
1175 if let Some(err) = wr_error
1176 && entry.first_error.is_none()
1177 {
1178 entry.first_error = Some(err);
1179 }
1180 if entry.pending_wrs.is_empty() {
1181 let entry = self.posted.remove(&op_id).expect("just verified");
1182 let result = match entry.first_error {
1183 Some(err) => Err(err),
1184 None => Ok(()),
1185 };
1186 entry.reply.try_post(
1187 cx,
1188 OpResult {
1189 op_idx: entry.op_idx,
1190 result,
1191 },
1192 )?;
1193 }
1194 }
1195 if progressed {
1196 self.poll_policy.reset();
1197 }
1198
1199 let pending_work = !self.queue.is_empty() || !self.posted.is_empty();
1201 if pending_work && !self.tick_armed {
1202 self.tick_armed = true;
1203 let interval = self.poll_policy.next_interval();
1204 if interval.is_zero() {
1205 cx.handle().try_post(cx, Tick)?;
1206 } else {
1207 cx.post_after(cx, Tick, interval);
1208 }
1209 }
1210 Ok(())
1211 }
1212}
1213
1214#[async_trait]
1215impl<M: Manager, Qp: IbvQueuePair> Actor for QueuePairActor<M, Qp> {
1216 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
1217 let local_info = self.qp.get_qp_info().map_err(|e| {
1218 tracing::error!(qp_key = ?self.qp_key, error = %e, "QueuePairActor init: get_qp_info failed");
1219 anyhow::anyhow!("could not extract local QP info: {e}")
1220 })?;
1221
1222 let peer_info = if self.is_loopback {
1223 local_info.clone()
1226 } else {
1227 let (reply, rx) = this.mailbox().open_once_port::<Result<IbvQpInfo, String>>();
1228 self.peer_manager.post(
1229 this,
1230 CreatePeerQueuePair {
1231 sender: self.local_manager.clone(),
1232 sender_device: self.qp_key.self_device.clone(),
1233 receiver_device: self.qp_key.other_device.clone(),
1234 sender_info: local_info,
1235 reply: reply.bind(),
1236 },
1237 );
1238 match tokio::time::timeout(self.init_timeout, rx.recv()).await {
1239 Ok(Ok(Ok(info))) => info,
1240 Ok(Ok(Err(e))) => {
1241 tracing::error!(
1242 qp_key = ?self.qp_key,
1243 peer_manager = ?self.peer_manager,
1244 error = %e,
1245 "QueuePairActor init: peer manager rejected CreatePeerQueuePair",
1246 );
1247 return Err(anyhow::anyhow!("peer manager rejected QP request: {e}"));
1248 }
1249 Ok(Err(e)) => {
1250 tracing::error!(
1251 qp_key = ?self.qp_key,
1252 peer_manager = ?self.peer_manager,
1253 error = %e,
1254 "QueuePairActor init: peer reply port closed",
1255 );
1256 return Err(anyhow::anyhow!("peer reply port closed: {e}"));
1257 }
1258 Err(_) => {
1259 tracing::error!(
1260 qp_key = ?self.qp_key,
1261 peer_manager = ?self.peer_manager,
1262 timeout = ?self.init_timeout,
1263 "QueuePairActor init: timed out waiting for peer reply",
1264 );
1265 return Err(anyhow::anyhow!(
1266 "QP initialization timed out after {:?}",
1267 self.init_timeout
1268 ));
1269 }
1270 }
1271 };
1272
1273 self.qp.connect(&peer_info).map_err(|e| {
1274 tracing::error!(
1275 qp_key = ?self.qp_key,
1276 peer_info = ?peer_info,
1277 error = %e,
1278 "QueuePairActor init: connect failed",
1279 );
1280 anyhow::anyhow!("could not connect QP to peer: {e}")
1281 })?;
1282 Ok(())
1283 }
1284
1285 fn spawn_server_task<F>(future: F) -> tokio::task::JoinHandle<F::Output>
1289 where
1290 F: std::future::Future + Send + 'static,
1291 F::Output: Send + 'static,
1292 {
1293 crate::rdma_runtime::spawn_on_rdma_runtime(future)
1294 }
1295}
1296
1297#[async_trait]
1298impl<M: Manager, Qp: IbvQueuePair> Handler<ProcessOps<M>> for QueuePairActor<M, Qp> {
1299 async fn handle(
1300 &mut self,
1301 cx: &Context<Self>,
1302 msg: ProcessOps<M>,
1303 ) -> Result<(), anyhow::Error> {
1304 for (op_idx, op, mrv) in msg.items.into_iter() {
1305 let wrs = Self::wr_count(op.local_memory.size());
1306 let is_read = matches!(op.op_type, RdmaOpType::ReadIntoLocal);
1307 self.queue.push_back(PendingOp {
1308 op_idx,
1309 op,
1310 mrv,
1311 reply: msg.reply.clone(),
1312 wrs,
1313 is_read,
1314 });
1315 }
1316 if !self.tick_armed {
1319 self.advance(cx)?;
1320 }
1321 Ok(())
1322 }
1323}
1324
1325#[async_trait]
1326impl<M: Manager, Qp: IbvQueuePair> Handler<Tick> for QueuePairActor<M, Qp> {
1327 async fn handle(&mut self, cx: &Context<Self>, _msg: Tick) -> Result<(), anyhow::Error> {
1328 self.tick_armed = false;
1329 self.advance(cx)?;
1330 Ok(())
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use std::sync::Arc;
1337 use std::sync::Mutex;
1338 use std::time::Duration;
1339
1340 use anyhow::Result;
1341 use async_trait::async_trait;
1342 use hyperactor::ActorHandle;
1343 use hyperactor::Context;
1344 use hyperactor::Handler;
1345 use hyperactor::proc::Proc;
1346
1347 use super::*;
1348 use crate::backend::ibverbs::device::IbvDevice;
1349 use crate::backend::ibverbs::device::list_all_devices;
1350 use crate::backend::ibverbs::device_selection::IbvDeviceTarget;
1351 use crate::backend::ibverbs::device_selection::resolve_target;
1352 use crate::backend::ibverbs::mlx_device::MlxDevice;
1353 use crate::backend::ibverbs::primitives::IbvConfig;
1354
1355 #[test]
1356 fn test_create_connection() {
1357 if list_all_devices().is_empty() {
1358 println!("Skipping test: RDMA devices not available");
1359 return;
1360 }
1361
1362 let config = IbvConfig {
1363 use_gpu_direct: false,
1364 ..Default::default()
1365 };
1366 let device_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0)).unwrap();
1367 let mut device = IbvDevice::<MlxDevice>::open(device_info.name(), config.clone())
1368 .expect("resolved device should open");
1369 let domain = device
1370 .get_or_create_domain("test")
1371 .expect("domain creation should succeed");
1372 let queue_pair = legacy::IbvQueuePair::new(domain, config.clone());
1373 assert!(queue_pair.is_ok());
1374 }
1375
1376 #[test]
1377 fn test_loopback_connection() {
1378 if list_all_devices().is_empty() {
1379 println!("Skipping test: RDMA devices not available");
1380 return;
1381 }
1382
1383 let server_config = IbvConfig {
1384 use_gpu_direct: false,
1385 ..Default::default()
1386 };
1387 let client_config = IbvConfig {
1388 use_gpu_direct: false,
1389 ..Default::default()
1390 };
1391
1392 let server_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0)).unwrap();
1393 let mut server_device =
1394 IbvDevice::<MlxDevice>::open(server_info.name(), server_config.clone())
1395 .expect("server device should open");
1396 let server_domain = server_device
1397 .get_or_create_domain("test")
1398 .expect("server domain creation should succeed");
1399 let client_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0)).unwrap();
1400 let mut client_device =
1401 IbvDevice::<MlxDevice>::open(client_info.name(), client_config.clone())
1402 .expect("client device should open");
1403 let client_domain = client_device
1404 .get_or_create_domain("test")
1405 .expect("client domain creation should succeed");
1406
1407 let mut server_qp =
1408 legacy::IbvQueuePair::new(server_domain, server_config.clone()).unwrap();
1409 let mut client_qp =
1410 legacy::IbvQueuePair::new(client_domain, client_config.clone()).unwrap();
1411
1412 let server_connection_info = server_qp.get_qp_info().unwrap();
1413 let client_connection_info = client_qp.get_qp_info().unwrap();
1414
1415 assert!(server_qp.connect(&client_connection_info).is_ok());
1416 assert!(client_qp.connect(&server_connection_info).is_ok());
1417 }
1418
1419 #[derive(Debug, Clone)]
1427 struct CreateCapture {
1428 sender_id: hyperactor::ActorId,
1429 sender_device: String,
1430 receiver_device: String,
1431 sender_qp_num: u32,
1432 }
1433
1434 #[derive(Debug)]
1435 struct QpaMockState {
1436 creates: Vec<CreateCapture>,
1437 response: Option<Result<IbvQpInfo, String>>,
1438 supervision_tx: Option<
1441 tokio::sync::mpsc::UnboundedSender<hyperactor::supervision::ActorSupervisionEvent>,
1442 >,
1443 }
1444
1445 #[derive(Debug)]
1452 #[hyperactor::export(handlers = [CreatePeerQueuePair<QpaMockManager>])]
1453 struct QpaMockManager {
1454 state: Arc<Mutex<QpaMockState>>,
1455 }
1456
1457 #[async_trait]
1458 impl Actor for QpaMockManager {
1459 async fn handle_supervision_event(
1460 &mut self,
1461 _this: &Instance<Self>,
1462 event: &hyperactor::supervision::ActorSupervisionEvent,
1463 ) -> Result<bool> {
1464 let tx = self.state.lock().unwrap().supervision_tx.clone();
1465 let Some(tx) = tx else {
1466 return Ok(!event.is_error());
1467 };
1468 tx.send(event.clone())
1469 .map_err(|e| anyhow::anyhow!("supervision_tx send failed: {e}"))?;
1470 Ok(true)
1471 }
1472 }
1473
1474 #[async_trait]
1475 impl Handler<CreatePeerQueuePair<QpaMockManager>> for QpaMockManager {
1476 async fn handle(
1477 &mut self,
1478 cx: &Context<Self>,
1479 msg: CreatePeerQueuePair<QpaMockManager>,
1480 ) -> Result<()> {
1481 let response = {
1482 let mut state = self.state.lock().unwrap();
1483 state.creates.push(CreateCapture {
1484 sender_id: msg.sender.actor_addr().id().clone(),
1485 sender_device: msg.sender_device.clone(),
1486 receiver_device: msg.receiver_device.clone(),
1487 sender_qp_num: msg.sender_info.qp_num,
1488 });
1489 state.response.take()
1490 };
1491 if let Some(response) = response {
1492 msg.reply.post(cx, response);
1493 }
1494 Ok(())
1496 }
1497 }
1498
1499 #[derive(Debug)]
1504 struct SpawnQpaChild {
1505 qp_key: QpKey,
1506 peer_manager: ActorRef<QpaMockManager>,
1507 qp: MockQp,
1508 is_loopback: bool,
1509 max_send_wr: u32,
1510 max_rd_atomic: u32,
1511 reply: hyperactor::OncePortHandle<ActorHandle<QueuePairActor<QpaMockManager, MockQp>>>,
1512 }
1513
1514 #[async_trait]
1515 impl Handler<SpawnQpaChild> for QpaMockManager {
1516 async fn handle(&mut self, cx: &Context<Self>, msg: SpawnQpaChild) -> Result<()> {
1517 let local_manager = cx.bind::<QpaMockManager>();
1518 let actor = QueuePairActor::new(
1519 msg.qp_key,
1520 local_manager,
1521 msg.peer_manager,
1522 msg.qp,
1523 msg.is_loopback,
1524 msg.max_send_wr,
1525 msg.max_rd_atomic,
1526 );
1527 let handle = cx.spawn(actor);
1528 msg.reply.try_post(cx, handle)?;
1529 Ok(())
1530 }
1531 }
1532
1533 #[derive(Debug)]
1536 enum PostedOp {
1537 Put {
1538 remote_dst: IbvBuffer,
1539 local_src: IbvBuffer,
1540 wr_ids: Vec<u64>,
1541 },
1542 Get {
1543 local_dst: IbvBuffer,
1544 remote_src: IbvBuffer,
1545 wr_ids: Vec<u64>,
1546 },
1547 }
1548
1549 #[derive(Debug)]
1550 struct MockQpInner {
1551 connect_calls: Vec<IbvQpInfo>,
1552 next_wr_id: u64,
1553 posted_tx: tokio::sync::mpsc::UnboundedSender<PostedOp>,
1556 pending_completions: VecDeque<std::result::Result<IbvWc, WorkRequestError>>,
1560 poll_error: Option<PollCompletionError>,
1562 post_error: Option<String>,
1564 }
1565
1566 #[derive(Debug, Clone)]
1570 struct MockQp {
1571 info: IbvQpInfo,
1572 inner: Arc<Mutex<MockQpInner>>,
1573 }
1574
1575 impl MockQp {
1576 fn new(qp_num: u32, psn: u32) -> (Self, tokio::sync::mpsc::UnboundedReceiver<PostedOp>) {
1580 let (posted_tx, posted_rx) = tokio::sync::mpsc::unbounded_channel();
1581 let qp = Self {
1582 info: IbvQpInfo {
1583 qp_num,
1584 lid: 0,
1585 gid: None,
1586 psn,
1587 },
1588 inner: Arc::new(Mutex::new(MockQpInner {
1589 connect_calls: Vec::new(),
1590 next_wr_id: 0,
1591 posted_tx,
1592 pending_completions: VecDeque::new(),
1593 poll_error: None,
1594 post_error: None,
1595 })),
1596 };
1597 (qp, posted_rx)
1598 }
1599
1600 fn connect_calls(&self) -> Vec<IbvQpInfo> {
1601 self.inner.lock().unwrap().connect_calls.clone()
1602 }
1603
1604 fn queue_completion(&self, wr_id: u64) {
1607 self.inner
1608 .lock()
1609 .unwrap()
1610 .pending_completions
1611 .push_back(Ok(IbvWc::for_test(wr_id, true)));
1612 }
1613
1614 fn queue_per_wr_error(&self, wr_id: u64, message: &str) {
1618 self.inner
1619 .lock()
1620 .unwrap()
1621 .pending_completions
1622 .push_back(Err(WorkRequestError::for_test(wr_id, message)));
1623 }
1624
1625 fn queue_poll_error(&self, err: PollCompletionError) {
1628 self.inner.lock().unwrap().poll_error = Some(err);
1629 }
1630
1631 fn queue_post_error(&self, message: &str) {
1633 self.inner.lock().unwrap().post_error = Some(message.to_string());
1634 }
1635 }
1636
1637 impl IbvQueuePair for MockQp {
1638 unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
1639 _domain: &IbvDomain<I>,
1640 _config: IbvConfig,
1641 ) -> Result<Self> {
1642 unreachable!("MockQp is constructed directly, not from a domain")
1645 }
1646
1647 fn connect(&mut self, info: &IbvQpInfo) -> Result<()> {
1648 self.inner.lock().unwrap().connect_calls.push(info.clone());
1649 Ok(())
1650 }
1651
1652 fn get_qp_info(&mut self) -> Result<IbvQpInfo> {
1653 Ok(self.info.clone())
1654 }
1655
1656 fn state(&mut self) -> Result<u32> {
1657 Ok(rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTS)
1658 }
1659
1660 fn put(&mut self, remote_dst: IbvBuffer, local_src: IbvBuffer) -> Result<Vec<u64>> {
1661 let mut inner = self.inner.lock().unwrap();
1662 if let Some(msg) = inner.post_error.take() {
1663 return Err(anyhow::anyhow!(msg));
1664 }
1665 let wrs = local_src.size.div_ceil(MAX_RDMA_MSG_SIZE).max(1);
1666 let mut wr_ids = Vec::with_capacity(wrs);
1667 for _ in 0..wrs {
1668 wr_ids.push(inner.next_wr_id);
1669 inner.next_wr_id += 1;
1670 }
1671 let _ = inner.posted_tx.send(PostedOp::Put {
1672 remote_dst,
1673 local_src,
1674 wr_ids: wr_ids.clone(),
1675 });
1676 Ok(wr_ids)
1677 }
1678
1679 fn get(&mut self, local_dst: IbvBuffer, remote_src: IbvBuffer) -> Result<Vec<u64>> {
1680 let mut inner = self.inner.lock().unwrap();
1681 if let Some(msg) = inner.post_error.take() {
1682 return Err(anyhow::anyhow!(msg));
1683 }
1684 let wrs = local_dst.size.div_ceil(MAX_RDMA_MSG_SIZE).max(1);
1685 let mut wr_ids = Vec::with_capacity(wrs);
1686 for _ in 0..wrs {
1687 wr_ids.push(inner.next_wr_id);
1688 inner.next_wr_id += 1;
1689 }
1690 let _ = inner.posted_tx.send(PostedOp::Get {
1691 local_dst,
1692 remote_src,
1693 wr_ids: wr_ids.clone(),
1694 });
1695 Ok(wr_ids)
1696 }
1697
1698 fn poll_completion(
1699 &mut self,
1700 _target: PollTarget,
1701 ) -> std::result::Result<
1702 Option<std::result::Result<IbvWc, WorkRequestError>>,
1703 PollCompletionError,
1704 > {
1705 let mut inner = self.inner.lock().unwrap();
1706 if let Some(err) = inner.poll_error.take() {
1707 return Err(err);
1708 }
1709 Ok(inner.pending_completions.pop_front())
1710 }
1711 }
1712
1713 struct QpaHarness {
1714 proc: Proc,
1715 parent: ActorHandle<QpaMockManager>,
1716 peer: ActorHandle<QpaMockManager>,
1717 peer_state: Arc<Mutex<QpaMockState>>,
1718 client: hyperactor::Client,
1719 supervision_rx:
1720 tokio::sync::mpsc::UnboundedReceiver<hyperactor::supervision::ActorSupervisionEvent>,
1721 }
1722
1723 impl QpaHarness {
1724 fn build() -> Result<Self> {
1725 let proc = Proc::anonymous();
1726 let (supervision_tx, supervision_rx) = tokio::sync::mpsc::unbounded_channel();
1727 let parent = proc.spawn_with_label(
1728 "parent",
1729 QpaMockManager {
1730 state: Arc::new(Mutex::new(QpaMockState {
1731 creates: Vec::new(),
1732 response: None,
1733 supervision_tx: Some(supervision_tx),
1734 })),
1735 },
1736 );
1737 let peer_state = Arc::new(Mutex::new(QpaMockState {
1738 creates: Vec::new(),
1739 response: None,
1740 supervision_tx: None,
1741 }));
1742 let peer = proc.spawn_with_label(
1743 "peer",
1744 QpaMockManager {
1745 state: Arc::clone(&peer_state),
1746 },
1747 );
1748 let client = proc.client("client");
1749 Ok(Self {
1750 proc,
1751 parent,
1752 peer,
1753 peer_state,
1754 client,
1755 supervision_rx,
1756 })
1757 }
1758
1759 fn peer_id(&self) -> hyperactor::ActorId {
1760 self.peer.actor_addr().id().clone()
1761 }
1762
1763 fn parent_id(&self) -> hyperactor::ActorId {
1764 self.parent.actor_addr().id().clone()
1765 }
1766
1767 async fn next_supervision_failure(
1769 &mut self,
1770 ) -> hyperactor::supervision::ActorSupervisionEvent {
1771 tokio::time::timeout(Duration::from_secs(5), self.supervision_rx.recv())
1772 .await
1773 .expect("timed out waiting for child failure event")
1774 .expect("supervision channel closed")
1775 }
1776
1777 async fn teardown(mut self) {
1780 self.supervision_rx.close();
1781 self.proc
1782 .destroy_and_wait(Duration::from_secs(30), "test teardown")
1783 .await
1784 .expect("destroy_and_wait failed");
1785 let mut leftover = Vec::new();
1786 while let Some(event) = self.supervision_rx.recv().await {
1787 leftover.push(event);
1788 }
1789 assert!(
1790 leftover.is_empty(),
1791 "unexpected supervision events at teardown: {leftover:?}",
1792 );
1793 }
1794
1795 async fn spawn_actor(
1796 &self,
1797 qp_key: QpKey,
1798 peer_manager: ActorRef<QpaMockManager>,
1799 qp: MockQp,
1800 is_loopback: bool,
1801 ) -> Result<ActorHandle<QueuePairActor<QpaMockManager, MockQp>>> {
1802 self.spawn_actor_with_caps(qp_key, peer_manager, qp, is_loopback, 4, 2)
1803 .await
1804 }
1805
1806 async fn spawn_actor_with_caps(
1807 &self,
1808 qp_key: QpKey,
1809 peer_manager: ActorRef<QpaMockManager>,
1810 qp: MockQp,
1811 is_loopback: bool,
1812 max_send_wr: u32,
1813 max_rd_atomic: u32,
1814 ) -> Result<ActorHandle<QueuePairActor<QpaMockManager, MockQp>>> {
1815 let (reply, rx) = self.client.mailbox().open_once_port();
1816 self.parent.try_post(
1817 &self.client,
1818 SpawnQpaChild {
1819 qp_key,
1820 peer_manager,
1821 qp,
1822 is_loopback,
1823 max_send_wr,
1824 max_rd_atomic,
1825 reply,
1826 },
1827 )?;
1828 Ok(rx.recv().await?)
1829 }
1830 }
1831
1832 async fn await_status(
1833 handle: &ActorHandle<QueuePairActor<QpaMockManager, MockQp>>,
1834 expected: impl Fn(&hyperactor::actor::ActorStatus) -> bool,
1835 ) -> hyperactor::actor::ActorStatus {
1836 let mut status = handle.status();
1837 status.wait_for(|s| expected(s)).await.unwrap();
1838 status.borrow().clone()
1839 }
1840
1841 #[timed_test::async_timed_test(timeout_secs = 60)]
1842 async fn qpa_init_succeeds_via_peer() -> Result<()> {
1843 let harness = QpaHarness::build()?;
1844 let peer_info = IbvQpInfo {
1845 qp_num: 0xbeef,
1846 lid: 0,
1847 gid: None,
1848 psn: 0xc0ffee,
1849 };
1850 harness.peer_state.lock().unwrap().response = Some(Ok(peer_info.clone()));
1851
1852 let (qp, _posted_rx) = MockQp::new(0x1234, 0xdead);
1853 let qp_key = QpKey {
1854 self_device: "mlx5_0".into(),
1855 other_id: harness.peer_id(),
1856 other_device: "mlx5_1".into(),
1857 };
1858 let handle = harness
1859 .spawn_actor(
1860 qp_key.clone(),
1861 harness.peer.bind::<QpaMockManager>(),
1862 qp.clone(),
1863 false,
1864 )
1865 .await?;
1866
1867 await_status(&handle, |s| {
1868 matches!(s, hyperactor::actor::ActorStatus::Idle)
1869 })
1870 .await;
1871
1872 let creates = harness.peer_state.lock().unwrap().creates.clone();
1873 assert_eq!(creates.len(), 1);
1874 assert_eq!(creates[0].sender_device, "mlx5_0");
1875 assert_eq!(creates[0].receiver_device, "mlx5_1");
1876 assert_eq!(creates[0].sender_qp_num, 0x1234);
1877 assert_eq!(creates[0].sender_id, harness.parent_id());
1880
1881 let connects = qp.connect_calls();
1882 assert_eq!(connects.len(), 1);
1883 assert_eq!(connects[0].qp_num, peer_info.qp_num);
1884 assert_eq!(connects[0].psn, peer_info.psn);
1885 harness.teardown().await;
1886 Ok(())
1887 }
1888
1889 #[timed_test::async_timed_test(timeout_secs = 60)]
1890 async fn qpa_init_loopback_skips_peer_round_trip() -> Result<()> {
1891 let harness = QpaHarness::build()?;
1892
1893 let (qp, _posted_rx) = MockQp::new(0xaaa, 0xbbb);
1894 let qp_key = QpKey {
1895 self_device: "mlx5_0".into(),
1896 other_id: harness.parent_id(),
1897 other_device: "mlx5_0".into(),
1898 };
1899 let handle = harness
1900 .spawn_actor(
1901 qp_key,
1902 harness.peer.bind::<QpaMockManager>(),
1903 qp.clone(),
1904 true,
1905 )
1906 .await?;
1907
1908 await_status(&handle, |s| {
1909 matches!(s, hyperactor::actor::ActorStatus::Idle)
1910 })
1911 .await;
1912 assert!(harness.peer_state.lock().unwrap().creates.is_empty());
1913
1914 let connects = qp.connect_calls();
1915 assert_eq!(connects.len(), 1);
1916 assert_eq!(connects[0].qp_num, 0xaaa);
1917 assert_eq!(connects[0].psn, 0xbbb);
1918 harness.teardown().await;
1919 Ok(())
1920 }
1921
1922 #[timed_test::async_timed_test(timeout_secs = 60)]
1923 async fn qpa_init_peer_error_fails_actor() -> Result<()> {
1924 let mut harness = QpaHarness::build()?;
1925 harness.peer_state.lock().unwrap().response =
1926 Some(Err("peer rejected, no domain on receiver_device".into()));
1927
1928 let qp_key = QpKey {
1929 self_device: "mlx5_0".into(),
1930 other_id: harness.peer_id(),
1931 other_device: "mlx5_99".into(),
1932 };
1933 let (qp, _posted_rx) = MockQp::new(1, 2);
1934 let handle = harness
1935 .spawn_actor(qp_key, harness.peer.bind::<QpaMockManager>(), qp, false)
1936 .await?;
1937
1938 let event = harness.next_supervision_failure().await;
1939 assert_eq!(&event.actor_id, handle.actor_addr());
1940 let report = event.failure_report().expect("event should be a failure");
1941 assert!(
1942 report.contains("peer rejected"),
1943 "failure report should surface the peer's error string; got: {report}"
1944 );
1945 await_status(&handle, |s| {
1946 matches!(s, hyperactor::actor::ActorStatus::Failed(_))
1947 })
1948 .await;
1949 harness.teardown().await;
1950 Ok(())
1951 }
1952
1953 #[timed_test::async_timed_test(timeout_secs = 60)]
1954 async fn qpa_init_timeout_fails_actor() -> Result<()> {
1955 let lock = hyperactor_config::global::lock();
1956 let _guard = lock.override_key(
1957 crate::config::RDMA_QP_INIT_TIMEOUT,
1958 Duration::from_millis(100),
1959 );
1960
1961 let mut harness = QpaHarness::build()?;
1962
1963 let qp_key = QpKey {
1964 self_device: "mlx5_0".into(),
1965 other_id: harness.peer_id(),
1966 other_device: "mlx5_1".into(),
1967 };
1968 let (qp, _posted_rx) = MockQp::new(1, 2);
1969 let handle = harness
1970 .spawn_actor(qp_key, harness.peer.bind::<QpaMockManager>(), qp, false)
1971 .await?;
1972
1973 let event = harness.next_supervision_failure().await;
1974 assert_eq!(&event.actor_id, handle.actor_addr());
1975 let report = event.failure_report().expect("event should be a failure");
1976 assert!(
1977 report.contains("timed out"),
1978 "failure report should mention timeout; got: {report}"
1979 );
1980 await_status(&handle, |s| {
1981 matches!(s, hyperactor::actor::ActorStatus::Failed(_))
1982 })
1983 .await;
1984 harness.teardown().await;
1985 Ok(())
1986 }
1987
1988 use crate::backend::ibverbs::primitives::IbvMr;
1993 use crate::local_memory::Keepalive;
1994 use crate::local_memory::KeepaliveLocalMemory;
1995
1996 struct FakeKeepalive {
1999 addr: usize,
2000 size: usize,
2001 }
2002 impl Keepalive for FakeKeepalive {
2003 fn addr(&self) -> usize {
2004 self.addr
2005 }
2006 fn size(&self) -> usize {
2007 self.size
2008 }
2009 }
2010
2011 fn fake_local_memory(addr: usize, size: usize) -> KeepaliveLocalMemory {
2012 KeepaliveLocalMemory::new(Arc::new(FakeKeepalive { addr, size }))
2013 }
2014
2015 fn fake_mrv(addr: usize, size: usize) -> IbvMemoryRegionView {
2016 IbvMemoryRegionView::new(
2017 addr,
2018 addr,
2019 size,
2020 0x1234,
2021 0x5678,
2022 "dev0".to_string(),
2023 Arc::new(IbvMr::null()),
2025 )
2026 }
2027
2028 fn fake_remote_ref() -> ActorRef<QpaMockManager> {
2033 let proc_id = hyperactor::id::ProcId::new(
2034 hyperactor::id::Uid::Instance(0xc0ffee, None),
2035 Some(hyperactor::id::Label::new("remote").unwrap()),
2036 );
2037 let proc_addr =
2038 hyperactor::ProcAddr::new(proc_id, hyperactor::channel::ChannelAddr::Local(1).into());
2039 ActorRef::attest(proc_addr.actor_addr("remote-mgr"))
2040 }
2041
2042 fn make_op(op_type: RdmaOpType, addr: usize, size: usize) -> IbvOp<QpaMockManager> {
2043 IbvOp {
2044 op_type,
2045 local_memory: fake_local_memory(addr, size),
2046 remote_buffer: IbvBuffer {
2047 lkey: 0,
2048 rkey: 0,
2049 addr: 0x4000_0000,
2050 size,
2051 device_name: "remote_dev".to_string(),
2052 },
2053 remote_manager: fake_remote_ref(),
2054 }
2055 }
2056
2057 impl QpaHarness {
2058 async fn spawn_ready_actor(
2063 &self,
2064 max_send_wr: u32,
2065 max_rd_atomic: u32,
2066 ) -> Result<(
2067 ActorHandle<QueuePairActor<QpaMockManager, MockQp>>,
2068 MockQp,
2069 tokio::sync::mpsc::UnboundedReceiver<PostedOp>,
2070 )> {
2071 let (qp, posted_rx) = MockQp::new(0x1, 0x2);
2072 let qp_key = QpKey {
2073 self_device: "mlx5_0".into(),
2074 other_id: self.parent_id(),
2075 other_device: "mlx5_0".into(),
2076 };
2077 let handle = self
2078 .spawn_actor_with_caps(
2079 qp_key,
2080 self.peer.bind::<QpaMockManager>(),
2081 qp.clone(),
2082 true,
2083 max_send_wr,
2084 max_rd_atomic,
2085 )
2086 .await?;
2087 await_status(&handle, |s| {
2088 matches!(s, hyperactor::actor::ActorStatus::Idle)
2089 })
2090 .await;
2091 Ok((handle, qp, posted_rx))
2092 }
2093 }
2094
2095 async fn recv_posted(rx: &mut tokio::sync::mpsc::UnboundedReceiver<PostedOp>) -> PostedOp {
2097 tokio::time::timeout(Duration::from_secs(5), rx.recv())
2098 .await
2099 .expect("timed out waiting for post on MockQp")
2100 .expect("MockQp post channel closed")
2101 }
2102
2103 async fn assert_no_post(
2106 rx: &mut tokio::sync::mpsc::UnboundedReceiver<PostedOp>,
2107 wait: Duration,
2108 ) {
2109 if let Ok(Some(p)) = tokio::time::timeout(wait, rx.recv()).await {
2110 panic!("unexpected post on MockQp: {p:?}");
2111 }
2112 }
2113
2114 fn expect_put(p: PostedOp) -> (IbvBuffer, IbvBuffer, Vec<u64>) {
2115 match p {
2116 PostedOp::Put {
2117 remote_dst,
2118 local_src,
2119 wr_ids,
2120 } => (local_src, remote_dst, wr_ids),
2121 other => panic!("expected Put, got {other:?}"),
2122 }
2123 }
2124
2125 fn expect_get(p: PostedOp) -> (IbvBuffer, IbvBuffer, Vec<u64>) {
2126 match p {
2127 PostedOp::Get {
2128 local_dst,
2129 remote_src,
2130 wr_ids,
2131 } => (local_dst, remote_src, wr_ids),
2132 other => panic!("expected Get, got {other:?}"),
2133 }
2134 }
2135
2136 fn submit_ops(
2139 harness: &QpaHarness,
2140 actor: &ActorHandle<QueuePairActor<QpaMockManager, MockQp>>,
2141 items: Vec<(usize, IbvOp<QpaMockManager>, IbvMemoryRegionView)>,
2142 ) -> Result<hyperactor::mailbox::PortReceiver<OpResult>> {
2143 let (reply, rx) = harness.client.mailbox().open_port::<OpResult>();
2144 actor.try_post(&harness.client, ProcessOps { items, reply })?;
2145 Ok(rx)
2146 }
2147
2148 async fn collect_replies(
2151 rx: &mut hyperactor::mailbox::PortReceiver<OpResult>,
2152 n: usize,
2153 ) -> Vec<(usize, Result<(), String>)> {
2154 let mut out = Vec::with_capacity(n);
2155 for _ in 0..n {
2156 let m = tokio::time::timeout(Duration::from_secs(5), rx.recv())
2157 .await
2158 .expect("timed out waiting for ProcessOps reply")
2159 .expect("ProcessOps reply port closed");
2160 out.push((m.op_idx, m.result));
2161 }
2162 out.sort_by_key(|(i, _)| *i);
2163 out
2164 }
2165
2166 async fn try_recv(
2169 rx: &mut hyperactor::mailbox::PortReceiver<OpResult>,
2170 wait: Duration,
2171 ) -> Option<(usize, Result<(), String>)> {
2172 match tokio::time::timeout(wait, rx.recv()).await {
2173 Ok(Ok(m)) => Some((m.op_idx, m.result)),
2174 Ok(Err(e)) => panic!("ProcessOps reply port closed: {e}"),
2175 Err(_) => None,
2176 }
2177 }
2178
2179 #[timed_test::async_timed_test(timeout_secs = 60)]
2180 async fn qpa_processes_single_write() -> Result<()> {
2181 let harness = QpaHarness::build()?;
2182 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4, 2).await?;
2183
2184 let items = vec![(
2185 7usize,
2186 make_op(RdmaOpType::WriteFromLocal, 0x1000, 4096),
2187 fake_mrv(0x1000, 4096),
2188 )];
2189 let mut rx = submit_ops(&harness, &actor, items)?;
2190
2191 let (lhandle, rhandle, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2193 assert_eq!(wr_ids, vec![0]);
2194 assert_eq!(lhandle.addr, 0x1000);
2195 assert_eq!(lhandle.size, 4096);
2196 assert_eq!(lhandle.lkey, 0x1234);
2197 assert_eq!(lhandle.rkey, 0x5678);
2198 assert_eq!(lhandle.device_name, "dev0");
2199 assert_eq!(rhandle.addr, 0x4000_0000);
2200 assert_eq!(rhandle.size, 4096);
2201 assert_eq!(rhandle.device_name, "remote_dev");
2202
2203 qp.queue_completion(0);
2204 let replies = collect_replies(&mut rx, 1).await;
2205 assert_eq!(replies, vec![(7, Ok(()))]);
2206 harness.teardown().await;
2207 Ok(())
2208 }
2209
2210 #[timed_test::async_timed_test(timeout_secs = 60)]
2211 async fn qpa_chunked_op_waits_for_all_wrs() -> Result<()> {
2212 let harness = QpaHarness::build()?;
2213 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8, 4).await?;
2214
2215 let items = vec![(
2218 11usize,
2219 make_op(RdmaOpType::WriteFromLocal, 0x1000, 3 * MAX_RDMA_MSG_SIZE),
2220 fake_mrv(0x1000, 3 * MAX_RDMA_MSG_SIZE),
2221 )];
2222 let mut rx = submit_ops(&harness, &actor, items)?;
2223
2224 let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2225 assert_eq!(wr_ids, vec![0, 1, 2]);
2226
2227 qp.queue_completion(wr_ids[0]);
2229 qp.queue_completion(wr_ids[1]);
2230 assert!(
2231 try_recv(&mut rx, Duration::from_millis(100))
2232 .await
2233 .is_none(),
2234 "op should not be reported until every WR has completed",
2235 );
2236
2237 qp.queue_completion(wr_ids[2]);
2239 let replies = collect_replies(&mut rx, 1).await;
2240 assert_eq!(replies, vec![(11, Ok(()))]);
2241 harness.teardown().await;
2242 Ok(())
2243 }
2244
2245 #[timed_test::async_timed_test(timeout_secs = 60)]
2246 async fn qpa_per_wr_error_held_until_all_wrs_complete() -> Result<()> {
2247 let harness = QpaHarness::build()?;
2248 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8, 4).await?;
2249
2250 let items = vec![(
2254 42usize,
2255 make_op(RdmaOpType::WriteFromLocal, 0x1000, 3 * MAX_RDMA_MSG_SIZE),
2256 fake_mrv(0x1000, 3 * MAX_RDMA_MSG_SIZE),
2257 )];
2258 let mut rx = submit_ops(&harness, &actor, items)?;
2259
2260 let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2261 assert_eq!(wr_ids.len(), 3);
2262
2263 qp.queue_per_wr_error(wr_ids[1], "simulated WR fail");
2265 assert!(
2266 try_recv(&mut rx, Duration::from_millis(100))
2267 .await
2268 .is_none(),
2269 "op error must wait for the other WRs to drain",
2270 );
2271
2272 qp.queue_completion(wr_ids[0]);
2275 qp.queue_completion(wr_ids[2]);
2276
2277 let replies = collect_replies(&mut rx, 1).await;
2278 assert_eq!(replies.len(), 1);
2279 assert_eq!(replies[0].0, 42);
2280 let err = replies[0]
2281 .1
2282 .as_ref()
2283 .expect_err("op should fail because one WR errored");
2284 assert!(
2285 err.contains("simulated WR fail"),
2286 "op error should surface the per-WR error: {err}",
2287 );
2288 harness.teardown().await;
2289 Ok(())
2290 }
2291
2292 #[timed_test::async_timed_test(timeout_secs = 60)]
2293 async fn qpa_per_wr_error_isolates_to_failing_op() -> Result<()> {
2294 let harness = QpaHarness::build()?;
2295 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8, 4).await?;
2296
2297 let items = vec![
2301 (
2302 0usize,
2303 make_op(RdmaOpType::WriteFromLocal, 0x1000, 3 * MAX_RDMA_MSG_SIZE),
2304 fake_mrv(0x1000, 3 * MAX_RDMA_MSG_SIZE),
2305 ),
2306 (
2307 1usize,
2308 make_op(RdmaOpType::WriteFromLocal, 0x2000, 4096),
2309 fake_mrv(0x2000, 4096),
2310 ),
2311 ];
2312 let mut rx = submit_ops(&harness, &actor, items)?;
2313
2314 let (_, _, big_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2315 let (_, _, small_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2316 assert_eq!(big_wrs, vec![0, 1, 2]);
2317 assert_eq!(small_wrs, vec![3]);
2318
2319 qp.queue_completion(small_wrs[0]);
2321 qp.queue_per_wr_error(big_wrs[1], "isolated per-WR fail");
2323 qp.queue_completion(big_wrs[0]);
2324 qp.queue_completion(big_wrs[2]);
2325
2326 let replies = collect_replies(&mut rx, 2).await;
2327 assert_eq!(replies.len(), 2);
2328 assert_eq!(replies[0].0, 0);
2329 let err = replies[0]
2330 .1
2331 .as_ref()
2332 .expect_err("op_idx 0 should fail because one WR errored");
2333
2334 assert!(
2335 err.contains("isolated per-WR fail"),
2336 "op_idx 0 error should name the per-WR error: {err}",
2337 );
2338 assert_eq!(replies[1], (1usize, Ok(())));
2339 harness.teardown().await;
2340 Ok(())
2341 }
2342
2343 #[timed_test::async_timed_test(timeout_secs = 60)]
2344 async fn qpa_read_credit_gating() -> Result<()> {
2345 let harness = QpaHarness::build()?;
2346 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8, 2).await?;
2348
2349 let items = (0..3usize)
2350 .map(|i| {
2351 (
2352 i,
2353 make_op(RdmaOpType::ReadIntoLocal, 0x1000 + i * 0x1000, 4096),
2354 fake_mrv(0x1000 + i * 0x1000, 4096),
2355 )
2356 })
2357 .collect();
2358 let mut rx = submit_ops(&harness, &actor, items)?;
2359
2360 let (_, _, r0_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2363 let (_, _, r1_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2364 assert_eq!(r0_wrs, vec![0]);
2365 assert_eq!(r1_wrs, vec![1]);
2366 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2367
2368 qp.queue_completion(r0_wrs[0]);
2370 let (_, _, r2_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2371 assert_eq!(r2_wrs, vec![2]);
2372
2373 qp.queue_completion(r1_wrs[0]);
2374 qp.queue_completion(r2_wrs[0]);
2375 let replies = collect_replies(&mut rx, 3).await;
2376 assert_eq!(replies, vec![(0, Ok(())), (1, Ok(())), (2, Ok(()))]);
2377 harness.teardown().await;
2378 Ok(())
2379 }
2380
2381 #[timed_test::async_timed_test(timeout_secs = 60)]
2382 async fn qpa_zero_max_rd_atomic_uses_send_wr() -> Result<()> {
2383 let harness = QpaHarness::build()?;
2384 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(2, 0).await?;
2387
2388 let items = (0..3usize)
2389 .map(|i| {
2390 (
2391 i,
2392 make_op(RdmaOpType::ReadIntoLocal, 0x1000 + i * 0x1000, 4096),
2393 fake_mrv(0x1000 + i * 0x1000, 4096),
2394 )
2395 })
2396 .collect();
2397 let mut rx = submit_ops(&harness, &actor, items)?;
2398
2399 let (_, _, r0_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2402 let (_, _, r1_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2403 assert_eq!(r0_wrs, vec![0]);
2404 assert_eq!(r1_wrs, vec![1]);
2405 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2406
2407 qp.queue_completion(r0_wrs[0]);
2409 let (_, _, r2_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2410 assert_eq!(r2_wrs, vec![2]);
2411
2412 qp.queue_completion(r1_wrs[0]);
2413 qp.queue_completion(r2_wrs[0]);
2414 let replies = collect_replies(&mut rx, 3).await;
2415 assert_eq!(replies, vec![(0, Ok(())), (1, Ok(())), (2, Ok(()))]);
2416 harness.teardown().await;
2417 Ok(())
2418 }
2419
2420 #[timed_test::async_timed_test(timeout_secs = 60)]
2421 async fn qpa_write_slot_gating() -> Result<()> {
2422 let harness = QpaHarness::build()?;
2423 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(2, 8).await?;
2425
2426 let items = (0..4usize)
2427 .map(|i| {
2428 (
2429 i,
2430 make_op(RdmaOpType::WriteFromLocal, 0x1000 + i * 0x1000, 4096),
2431 fake_mrv(0x1000 + i * 0x1000, 4096),
2432 )
2433 })
2434 .collect();
2435 let mut rx = submit_ops(&harness, &actor, items)?;
2436
2437 let (_, _, w0_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2438 let (_, _, w1_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2439 assert_eq!(w0_wrs, vec![0]);
2440 assert_eq!(w1_wrs, vec![1]);
2441 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2442
2443 qp.queue_completion(w0_wrs[0]);
2445 let (_, _, w2_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2446 assert_eq!(w2_wrs, vec![2]);
2447 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2448
2449 qp.queue_completion(w1_wrs[0]);
2451 let (_, _, w3_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2452 assert_eq!(w3_wrs, vec![3]);
2453
2454 qp.queue_completion(w2_wrs[0]);
2455 qp.queue_completion(w3_wrs[0]);
2456 let replies = collect_replies(&mut rx, 4).await;
2457 assert_eq!(
2458 replies,
2459 vec![(0, Ok(())), (1, Ok(())), (2, Ok(())), (3, Ok(()))],
2460 );
2461 harness.teardown().await;
2462 Ok(())
2463 }
2464
2465 #[timed_test::async_timed_test(timeout_secs = 60)]
2466 async fn qpa_blocked_until_one_read_and_one_write_complete() -> Result<()> {
2467 let harness = QpaHarness::build()?;
2468 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4, 2).await?;
2473
2474 let items = vec![
2475 (
2476 0usize,
2477 make_op(RdmaOpType::ReadIntoLocal, 0x1000, 4096),
2478 fake_mrv(0x1000, 4096),
2479 ),
2480 (
2481 1usize,
2482 make_op(RdmaOpType::WriteFromLocal, 0x2000, 4096),
2483 fake_mrv(0x2000, 4096),
2484 ),
2485 (
2486 2usize,
2487 make_op(RdmaOpType::WriteFromLocal, 0x3000, 4096),
2488 fake_mrv(0x3000, 4096),
2489 ),
2490 (
2491 3usize,
2492 make_op(RdmaOpType::WriteFromLocal, 0x4000, 4096),
2493 fake_mrv(0x4000, 4096),
2494 ),
2495 (
2496 4usize,
2497 make_op(RdmaOpType::ReadIntoLocal, 0x5000, 2 * MAX_RDMA_MSG_SIZE),
2498 fake_mrv(0x5000, 2 * MAX_RDMA_MSG_SIZE),
2499 ),
2500 ];
2501 let mut rx = submit_ops(&harness, &actor, items)?;
2502
2503 let (_, _, r0_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2505 let (_, _, w1_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2506 let (_, _, w2_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2507 let (_, _, w3_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2508 assert_eq!(r0_wrs, vec![0]);
2509 assert_eq!(w1_wrs, vec![1]);
2510 assert_eq!(w2_wrs, vec![2]);
2511 assert_eq!(w3_wrs, vec![3]);
2512 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2514
2515 qp.queue_completion(r0_wrs[0]);
2518 let first_reply = collect_replies(&mut rx, 1).await;
2519 assert_eq!(first_reply, vec![(0, Ok(()))]);
2520 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2521
2522 qp.queue_completion(w1_wrs[0]);
2524 let second_reply = collect_replies(&mut rx, 1).await;
2525 assert_eq!(second_reply, vec![(1, Ok(()))]);
2526 let (_, _, r4_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2527 assert_eq!(r4_wrs, vec![4, 5]);
2528
2529 qp.queue_completion(w2_wrs[0]);
2531 qp.queue_completion(w3_wrs[0]);
2532 for &id in &r4_wrs {
2533 qp.queue_completion(id);
2534 }
2535 let rest = collect_replies(&mut rx, 3).await;
2536 assert_eq!(rest, vec![(2, Ok(())), (3, Ok(())), (4, Ok(()))]);
2537 harness.teardown().await;
2538 Ok(())
2539 }
2540
2541 #[timed_test::async_timed_test(timeout_secs = 60)]
2542 async fn qpa_multiple_batches_share_credit() -> Result<()> {
2543 let harness = QpaHarness::build()?;
2544 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4, 2).await?;
2545
2546 let batch_a = vec![
2548 (
2549 10usize,
2550 make_op(RdmaOpType::WriteFromLocal, 0x1000, 4096),
2551 fake_mrv(0x1000, 4096),
2552 ),
2553 (
2554 11usize,
2555 make_op(RdmaOpType::WriteFromLocal, 0x2000, 4096),
2556 fake_mrv(0x2000, 4096),
2557 ),
2558 ];
2559 let mut rx_a = submit_ops(&harness, &actor, batch_a)?;
2560
2561 let batch_b = vec![
2564 (
2565 20usize,
2566 make_op(RdmaOpType::WriteFromLocal, 0x3000, 4096),
2567 fake_mrv(0x3000, 4096),
2568 ),
2569 (
2570 21usize,
2571 make_op(RdmaOpType::WriteFromLocal, 0x4000, 4096),
2572 fake_mrv(0x4000, 4096),
2573 ),
2574 ];
2575 let mut rx_b = submit_ops(&harness, &actor, batch_b)?;
2576
2577 let mut all_wr_ids = Vec::new();
2579 for _ in 0..4 {
2580 let (_, _, wrs) = expect_put(recv_posted(&mut posted_rx).await);
2581 all_wr_ids.extend(wrs);
2582 }
2583 for &id in &all_wr_ids {
2584 qp.queue_completion(id);
2585 }
2586
2587 let replies_a = collect_replies(&mut rx_a, 2).await;
2588 assert_eq!(replies_a, vec![(10, Ok(())), (11, Ok(()))]);
2589 let replies_b = collect_replies(&mut rx_b, 2).await;
2590 assert_eq!(replies_b, vec![(20, Ok(())), (21, Ok(()))]);
2591 harness.teardown().await;
2592 Ok(())
2593 }
2594
2595 #[timed_test::async_timed_test(timeout_secs = 60)]
2596 async fn qpa_op_too_large_for_qp() -> Result<()> {
2597 let harness = QpaHarness::build()?;
2598 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(1, 1).await?;
2600
2601 let items = vec![
2602 (
2603 0usize,
2604 make_op(RdmaOpType::WriteFromLocal, 0x1000, 2 * MAX_RDMA_MSG_SIZE),
2605 fake_mrv(0x1000, 2 * MAX_RDMA_MSG_SIZE),
2606 ),
2607 (
2608 1usize,
2609 make_op(RdmaOpType::WriteFromLocal, 0x2000, 4096),
2610 fake_mrv(0x2000, 4096),
2611 ),
2612 ];
2613 let mut rx = submit_ops(&harness, &actor, items)?;
2614
2615 let (_, _, wrs) = expect_put(recv_posted(&mut posted_rx).await);
2617 assert_eq!(wrs, vec![0]);
2618 qp.queue_completion(0);
2619
2620 let replies = collect_replies(&mut rx, 2).await;
2621 assert_eq!(replies.len(), 2);
2623 assert_eq!(replies[0].0, 0);
2624 let err = replies[0]
2625 .1
2626 .as_ref()
2627 .expect_err("op_idx 0 should fail as too large");
2628 assert!(err.contains("too large"), "expected too-large error: {err}");
2629 assert_eq!(replies[1], (1usize, Ok(())));
2630 harness.teardown().await;
2631 Ok(())
2632 }
2633
2634 #[timed_test::async_timed_test(timeout_secs = 60)]
2635 async fn qpa_poll_error_kills_actor_via_supervision() -> Result<()> {
2636 let mut harness = QpaHarness::build()?;
2637 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4, 2).await?;
2638
2639 let items = vec![(
2641 0usize,
2642 make_op(RdmaOpType::WriteFromLocal, 0x1000, 4096),
2643 fake_mrv(0x1000, 4096),
2644 )];
2645 let _rx = submit_ops(&harness, &actor, items)?;
2646 let _ = recv_posted(&mut posted_rx).await;
2647 qp.queue_poll_error(PollCompletionError::for_test("simulated CQ poison"));
2648
2649 let event = harness.next_supervision_failure().await;
2650 assert_eq!(&event.actor_id, actor.actor_addr());
2651 let report = event.failure_report().expect("event should be a failure");
2652 assert!(
2653 report.contains("CQ poll failed") && report.contains("simulated CQ poison"),
2654 "supervision report should name the poll failure: {report}",
2655 );
2656 await_status(&actor, |s| {
2657 matches!(s, hyperactor::actor::ActorStatus::Failed(_))
2658 })
2659 .await;
2660 harness.teardown().await;
2661 Ok(())
2662 }
2663
2664 #[timed_test::async_timed_test(timeout_secs = 60)]
2665 async fn qpa_post_error_kills_actor_via_supervision() -> Result<()> {
2666 let mut harness = QpaHarness::build()?;
2667 let (actor, qp, _posted_rx) = harness.spawn_ready_actor(4, 2).await?;
2668
2669 qp.queue_post_error("simulated post failure");
2670 let items = vec![(
2671 0usize,
2672 make_op(RdmaOpType::WriteFromLocal, 0x1000, 4096),
2673 fake_mrv(0x1000, 4096),
2674 )];
2675 let _rx = submit_ops(&harness, &actor, items)?;
2676
2677 let event = harness.next_supervision_failure().await;
2678 assert_eq!(&event.actor_id, actor.actor_addr());
2679 let report = event.failure_report().expect("event should be a failure");
2680 assert!(
2681 report.contains("qp.put failed") && report.contains("simulated post failure"),
2682 "supervision report should name the post failure: {report}",
2683 );
2684 await_status(&actor, |s| {
2685 matches!(s, hyperactor::actor::ActorStatus::Failed(_))
2686 })
2687 .await;
2688 harness.teardown().await;
2689 Ok(())
2690 }
2691}