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::panic::AssertUnwindSafe;
23use std::result::Result;
24use std::sync::Arc;
25use std::sync::atomic::AtomicU64;
26use std::sync::atomic::Ordering;
27use std::time::Duration;
28
29use async_trait::async_trait;
30use futures::FutureExt as _;
31use hyperactor::Actor;
32use hyperactor::ActorHandle;
33use hyperactor::ActorId;
34use hyperactor::ActorRef;
35use hyperactor::Context;
36use hyperactor::Endpoint as _;
37use hyperactor::Handler;
38use hyperactor::Instance;
39use hyperactor::PortHandle;
40use hyperactor::actor::ActorError;
41use hyperactor::actor::Referable;
42use hyperactor::actor::RemoteHandles;
43use hyperactor::context::Mailbox;
44use serde::Deserialize;
45use serde::Serialize;
46use tokio::sync::mpsc;
47use tokio_util::sync::CancellationToken;
48use typeuri::Named;
49
50use super::cq_actor::Attach;
51use super::cq_actor::CompletionInbox;
52use super::cq_actor::CompletionQueueActor;
53use super::cq_actor::CompletionResult;
54use super::cq_actor::CompletionRoute;
55use super::cq_actor::CqId;
56use super::cq_actor::Detach;
57use super::cq_actor::DetachedQueuePair;
58use super::cq_actor::IbvCompletionQueue as _;
59use super::cq_actor::PollerWake;
60use super::cq_pool::CqLease;
61use super::domain::IbvDomain;
62use super::domain::IbvDomainImpl;
63use super::manager_actor::CreatePeerQueuePair;
64use super::memory_region::IbvMemoryRegionView;
65use super::memory_region::IbvRemoteMemoryRegionView;
66use super::primitives::Gid;
67use super::primitives::GidScope;
68use super::primitives::GidType;
69use super::primitives::IbvConfig;
70use super::primitives::IbvCq;
71use super::primitives::IbvOperation;
72use super::primitives::IbvQp;
73use super::primitives::IbvQpInfo;
74#[cfg(test)]
75use super::primitives::IbvWc;
76use super::primitives::resolve_qp_type;
77use crate::RdmaOpType;
78use crate::local_memory::KeepaliveLocalMemory;
79
80#[derive(Debug)]
86pub struct WorkRequestError {
87 pub wr_id: u64,
88 pub status: rdmaxcel_sys::ibv_wc_status::Type,
89 pub vendor_err: u32,
90 message: String,
91}
92
93impl std::fmt::Display for WorkRequestError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.write_str(&self.message)
96 }
97}
98
99impl std::error::Error for WorkRequestError {}
100
101impl WorkRequestError {
102 pub fn is_wr_flush_err(&self) -> bool {
106 self.status == rdmaxcel_sys::ibv_wc_status::IBV_WC_WR_FLUSH_ERR
107 }
108
109 pub(super) fn from_status(
112 wr_id: u64,
113 status: rdmaxcel_sys::ibv_wc_status::Type,
114 vendor_err: u32,
115 ) -> Self {
116 Self {
117 wr_id,
118 status,
119 vendor_err,
120 message: format!(
121 "completion failed for wr_id={wr_id}: status={status:?}, vendor_err={vendor_err}"
122 ),
123 }
124 }
125
126 #[cfg(test)]
127 pub(super) fn for_test(wr_id: u64, message: &str) -> Self {
128 Self {
129 wr_id,
130 status: rdmaxcel_sys::ibv_wc_status::IBV_WC_GENERAL_ERR,
131 vendor_err: 0,
132 message: message.to_string(),
133 }
134 }
135}
136
137#[derive(Debug)]
140pub struct PollCompletionError {
141 message: String,
142}
143
144impl std::fmt::Display for PollCompletionError {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.write_str(&self.message)
147 }
148}
149
150impl std::error::Error for PollCompletionError {}
151
152impl PollCompletionError {
153 pub(super) fn new(message: String) -> Self {
155 Self { message }
156 }
157}
158
159#[derive(Debug, Named, Clone, Serialize, Deserialize)]
163pub struct DoorBell {
164 pub src_ptr: usize,
165 pub dst_ptr: usize,
166 pub size: usize,
167}
168wirevalue::register_type!(DoorBell);
169
170#[derive(Debug, Clone, Copy, PartialEq)]
172pub enum PollTarget {
173 Send,
174 Recv,
175}
176
177pub mod legacy;
180
181#[derive(Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize, Named)]
187pub(super) struct QpKey {
188 pub(super) self_device: String,
189 pub(super) other_id: ActorId,
190 pub(super) other_device: String,
191 pub(super) qp_index: usize,
192}
193
194pub trait IbvQueuePair: std::fmt::Debug + Send + Sync + 'static + Sized {
211 unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
227 domain: &IbvDomain<I>,
228 config: IbvConfig,
229 send_cq: Arc<IbvCq>,
230 recv_cq: Arc<IbvCq>,
231 ) -> Result<Self, anyhow::Error>;
232
233 fn connect(&mut self, info: &IbvQpInfo) -> Result<(), anyhow::Error>;
235
236 fn get_qp_info(&mut self) -> Result<IbvQpInfo, anyhow::Error>;
239
240 fn state(&mut self) -> Result<u32, anyhow::Error>;
242
243 fn max_msg_size(&self) -> usize {
249 MAX_RDMA_MSG_SIZE
250 }
251
252 fn put(
256 &mut self,
257 remote_dst: IbvRemoteMemoryRegionView,
258 local_src: IbvMemoryRegionView,
259 ) -> Result<Vec<u64>, anyhow::Error>;
260
261 fn get(
265 &mut self,
266 local_dst: IbvMemoryRegionView,
267 remote_src: IbvRemoteMemoryRegionView,
268 ) -> Result<Vec<u64>, anyhow::Error>;
269}
270
271pub(super) unsafe fn get_qp_info(
279 qp: *mut rdmaxcel_sys::ibv_qp,
280 context: *mut rdmaxcel_sys::ibv_context,
281 config: &IbvConfig,
282 gid: Gid,
283) -> Result<IbvQpInfo, anyhow::Error> {
284 let mut port_attr = rdmaxcel_sys::ibv_port_attr::default();
285 let errno = unsafe {
289 rdmaxcel_sys::ibv_query_port(
290 context,
291 config.port_num,
292 &mut port_attr as *mut rdmaxcel_sys::ibv_port_attr as *mut _,
293 )
294 };
295 if errno != 0 {
296 return Err(anyhow::anyhow!(
297 "failed to query port attributes: {}",
298 Error::from_raw_os_error(errno)
299 ));
300 }
301
302 let qp_num = unsafe { (*qp).qp_num };
304 Ok(IbvQpInfo {
305 qp_num,
306 lid: port_attr.lid,
307 gid: Some(gid),
308 psn: config.psn,
309 })
310}
311
312pub(super) unsafe fn state(qp: *mut rdmaxcel_sys::ibv_qp) -> Result<u32, anyhow::Error> {
318 let mut qp_attr = rdmaxcel_sys::ibv_qp_attr::default();
319 let mut qp_init_attr = rdmaxcel_sys::ibv_qp_init_attr::default();
320 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE;
321 let errno =
324 unsafe { rdmaxcel_sys::ibv_query_qp(qp, &mut qp_attr, mask.0 as i32, &mut qp_init_attr) };
325 if errno != 0 {
326 return Err(anyhow::anyhow!(
327 "failed to query QP state: {}",
328 Error::from_raw_os_error(errno)
329 ));
330 }
331 Ok(qp_attr.qp_state)
332}
333
334pub(super) unsafe fn connect(
343 qp: *mut rdmaxcel_sys::ibv_qp,
344 config: &IbvConfig,
345 access_flags: i32,
346 info: &IbvQpInfo,
347 sgid_index: u8,
348) -> Result<(), anyhow::Error> {
349 let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
351 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_INIT,
352 qp_access_flags: access_flags as u32,
353 pkey_index: config.pkey_index,
354 port_num: config.port_num,
355 ..Default::default()
356 };
357 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
358 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PKEY_INDEX
359 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PORT
360 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_ACCESS_FLAGS;
361 let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
365 if errno != 0 {
366 return Err(anyhow::anyhow!(
367 "failed to transition QP to INIT: {}",
368 Error::from_raw_os_error(errno)
369 ));
370 }
371
372 let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
374 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTR,
375 path_mtu: config.path_mtu,
376 dest_qp_num: info.qp_num,
377 rq_psn: info.psn,
378 max_dest_rd_atomic: config.max_dest_rd_atomic,
379 min_rnr_timer: config.min_rnr_timer,
380 ah_attr: rdmaxcel_sys::ibv_ah_attr {
381 dlid: info.lid,
382 sl: 0,
383 src_path_bits: 0,
384 port_num: config.port_num,
385 grh: Default::default(),
386 ..Default::default()
387 },
388 ..Default::default()
389 };
390 if let Some(gid) = info.gid {
391 qp_attr.ah_attr.is_global = 1;
392 qp_attr.ah_attr.grh.dgid = rdmaxcel_sys::ibv_gid::from(gid);
393 qp_attr.ah_attr.grh.hop_limit = 0xff;
394 qp_attr.ah_attr.grh.sgid_index = sgid_index;
395 } else {
396 qp_attr.ah_attr.is_global = 0;
397 }
398 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
399 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_AV
400 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PATH_MTU
401 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_DEST_QPN
402 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RQ_PSN
403 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MAX_DEST_RD_ATOMIC
404 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MIN_RNR_TIMER;
405 let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
407 if errno != 0 {
408 return Err(anyhow::anyhow!(
409 "failed to transition QP to RTR: {}",
410 Error::from_raw_os_error(errno)
411 ));
412 }
413
414 let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
416 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTS,
417 sq_psn: config.psn,
418 max_rd_atomic: config.max_rd_atomic,
419 retry_cnt: config.retry_cnt,
420 rnr_retry: config.rnr_retry,
421 timeout: config.qp_timeout,
422 ..Default::default()
423 };
424 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
425 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_TIMEOUT
426 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RETRY_CNT
427 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_SQ_PSN
428 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RNR_RETRY
429 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MAX_QP_RD_ATOMIC;
430 let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
432 if errno != 0 {
433 return Err(anyhow::anyhow!(
434 "failed to transition QP to RTS: {}",
435 Error::from_raw_os_error(errno)
436 ));
437 }
438 Ok(())
439}
440
441#[derive(Debug)]
449pub struct RCQueuePair {
450 qp: IbvQp,
451 config: IbvConfig,
452 gid: Gid,
456 access_flags: i32,
459 next_wr_id: u64,
462}
463
464impl RCQueuePair {
465 pub(super) unsafe fn from_qp(
477 qp: IbvQp,
478 config: IbvConfig,
479 gid: Gid,
480 access_flags: i32,
481 ) -> Self {
482 RCQueuePair {
483 qp,
484 config,
485 gid,
486 access_flags,
487 next_wr_id: 0,
488 }
489 }
490
491 fn post_chunked(
495 &mut self,
496 op: IbvOperation,
497 laddr: usize,
498 lkey: u32,
499 raddr: usize,
500 rkey: u32,
501 total_size: usize,
502 ) -> Result<Vec<u64>, anyhow::Error> {
503 let mut remaining = total_size;
504 let mut offset = 0;
505 let mut wr_ids = Vec::new();
506 while remaining > 0 {
507 let chunk = std::cmp::min(remaining, MAX_RDMA_MSG_SIZE);
508 let wr_id = self.next_wr_id;
509 self.next_wr_id += 1;
510 self.post_one(op, laddr + offset, lkey, chunk, raddr + offset, rkey, wr_id)?;
511 wr_ids.push(wr_id);
512 remaining -= chunk;
513 offset += chunk;
514 }
515 Ok(wr_ids)
516 }
517
518 fn post_one(
520 &self,
521 op: IbvOperation,
522 laddr: usize,
523 lkey: u32,
524 length: usize,
525 raddr: usize,
526 rkey: u32,
527 wr_id: u64,
528 ) -> Result<(), anyhow::Error> {
529 let qp = self.qp.as_ptr();
530 let context = self.qp.context().as_ptr();
531 let mut sge = rdmaxcel_sys::ibv_sge {
532 addr: laddr as u64,
533 length: length as u32,
534 lkey,
535 };
536 let mut wr = rdmaxcel_sys::ibv_send_wr {
537 wr_id,
538 next: std::ptr::null_mut(),
539 sg_list: &mut sge as *mut _,
540 num_sge: 1,
541 opcode: op.into(),
542 send_flags: rdmaxcel_sys::ibv_send_flags::IBV_SEND_SIGNALED.0,
543 wr: Default::default(),
544 qp_type: Default::default(),
545 __bindgen_anon_1: Default::default(),
546 __bindgen_anon_2: Default::default(),
547 };
548 wr.wr.rdma.remote_addr = raddr as u64;
551 wr.wr.rdma.rkey = rkey;
552 let mut bad_wr: *mut rdmaxcel_sys::ibv_send_wr = std::ptr::null_mut();
553 let errno = unsafe {
557 let post_send = (*context)
558 .ops
559 .post_send
560 .expect("post_send verb missing from ibv_context ops");
561 post_send(qp, &mut wr as *mut _, &mut bad_wr)
562 };
563 if errno != 0 {
564 return Err(anyhow::anyhow!(
565 "failed to post {:?} request: {}",
566 op,
567 Error::from_raw_os_error(errno)
568 ));
569 }
570 Ok(())
571 }
572}
573
574impl IbvQueuePair for RCQueuePair {
575 unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
576 domain: &IbvDomain<I>,
577 config: IbvConfig,
578 send_cq: Arc<IbvCq>,
579 recv_cq: Arc<IbvCq>,
580 ) -> Result<Self, anyhow::Error> {
581 tracing::debug!("creating an RCQueuePair from config {}", config);
582 let pd = domain.as_ptr();
585 if pd.is_null() {
586 anyhow::bail!("cannot create an RCQueuePair on a null protection domain");
587 }
588
589 let gid = domain.device_info().select_gid(
592 config.port_num,
593 Some(GidScope::Global),
594 Some(GidType::RoCEv2),
595 )?;
596
597 let mut init_attr = rdmaxcel_sys::ibv_qp_init_attr {
599 send_cq: send_cq.as_ptr(),
600 recv_cq: recv_cq.as_ptr(),
601 cap: rdmaxcel_sys::ibv_qp_cap {
602 max_send_wr: config.max_send_wr,
603 max_recv_wr: config.max_recv_wr,
604 max_send_sge: config.max_send_sge,
605 max_recv_sge: config.max_recv_sge,
606 max_inline_data: 0,
607 },
608 qp_type: rdmaxcel_sys::ibv_qp_type::IBV_QPT_RC,
609 sq_sig_all: 0,
610 ..Default::default()
611 };
612 let qp = unsafe { rdmaxcel_sys::ibv_create_qp(pd, &mut init_attr) };
616 if qp.is_null() {
617 anyhow::bail!(
618 "failed to create queue pair (QP): {}",
619 Error::last_os_error()
620 );
621 }
622 let qp = unsafe { IbvQp::from_raw(qp, send_cq, recv_cq, domain.pd().clone()) };
626 let access_flags = domain.access_flags();
627 Ok(unsafe { Self::from_qp(qp, config, gid, access_flags) })
630 }
631
632 fn connect(&mut self, info: &IbvQpInfo) -> Result<(), anyhow::Error> {
633 unsafe {
635 connect(
636 self.qp.as_ptr(),
637 &self.config,
638 self.access_flags,
639 info,
640 self.gid.index(),
641 )
642 }
643 }
644
645 fn get_qp_info(&mut self) -> Result<IbvQpInfo, anyhow::Error> {
646 let context = self.qp.context().as_ptr();
647 unsafe { get_qp_info(self.qp.as_ptr(), context, &self.config, self.gid) }
650 }
651
652 fn state(&mut self) -> Result<u32, anyhow::Error> {
653 unsafe { state(self.qp.as_ptr()) }
655 }
656
657 fn put(
658 &mut self,
659 remote_dst: IbvRemoteMemoryRegionView,
660 local_src: IbvMemoryRegionView,
661 ) -> Result<Vec<u64>, anyhow::Error> {
662 if remote_dst.size < local_src.size {
663 return Err(anyhow::anyhow!(
664 "remote buffer size ({}) is smaller than local buffer size ({})",
665 remote_dst.size,
666 local_src.size
667 ));
668 }
669 self.post_chunked(
670 IbvOperation::Write,
671 local_src.rdma_addr,
672 local_src.lkey,
673 remote_dst.addr,
674 remote_dst.rkey,
675 local_src.size,
676 )
677 }
678
679 fn get(
680 &mut self,
681 local_dst: IbvMemoryRegionView,
682 remote_src: IbvRemoteMemoryRegionView,
683 ) -> Result<Vec<u64>, anyhow::Error> {
684 if local_dst.size < remote_src.size {
685 return Err(anyhow::anyhow!(
686 "local buffer size ({}) is smaller than remote buffer size ({})",
687 local_dst.size,
688 remote_src.size
689 ));
690 }
691 self.post_chunked(
692 IbvOperation::Read,
693 local_dst.rdma_addr,
694 local_dst.lkey,
695 remote_src.addr,
696 remote_src.rkey,
697 remote_src.size,
698 )
699 }
700}
701
702pub(super) trait Manager:
705 Actor + Referable + RemoteHandles<CreatePeerQueuePair<Self>>
706{
707}
708
709impl<T> Manager for T where T: Actor + Referable + RemoteHandles<CreatePeerQueuePair<T>> {}
710
711#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713pub(super) struct StripeId {
714 pub(super) op_idx: usize,
715 pub(super) stripe_idx: usize,
716 pub(super) stripe_count: usize,
717}
718
719impl std::fmt::Display for StripeId {
720 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
721 write!(
722 f,
723 "op_idx={}, stripe {}/{}",
724 self.op_idx, self.stripe_idx, self.stripe_count
725 )
726 }
727}
728
729#[derive(Debug)]
731pub(super) struct StripeResult {
732 pub(super) stripe_id: StripeId,
733 pub(super) result: Result<(), String>,
734}
735
736#[derive(Debug)]
738pub(super) struct QueuePairOp {
739 pub(super) stripe_id: StripeId,
740 pub(super) qp_index: usize,
742 pub(super) op_type: RdmaOpType,
743 pub(super) local_memory: KeepaliveLocalMemory,
744 pub(super) local: IbvMemoryRegionView,
746 pub(super) remote: IbvRemoteMemoryRegionView,
747}
748
749#[derive(Debug)]
755pub(super) struct ProcessOps {
756 pub(super) items: Vec<QueuePairOp>,
757 pub(super) reply: mpsc::UnboundedSender<StripeResult>,
758}
759
760#[derive(Debug)]
762struct PendingOp {
763 op: QueuePairOp,
764 reply: mpsc::UnboundedSender<StripeResult>,
765 wrs: u32,
769}
770
771#[derive(Debug)]
773struct PostedOpEntry {
774 stripe_id: StripeId,
775 pending_wrs: HashSet<u64>,
776 _local_memory: KeepaliveLocalMemory,
778 _local_mrv: IbvMemoryRegionView,
781 reply: mpsc::UnboundedSender<StripeResult>,
782 first_error: Option<String>,
786}
787
788#[derive(Debug)]
790struct QueuePairState<Qp: IbvQueuePair> {
791 qp_key: QpKey,
792 qp: Qp,
793 max_send_wr: u32,
795 queue: VecDeque<PendingOp>,
800 posted: HashMap<u64, PostedOpEntry>,
806 wr_to_op: HashMap<u64, u64>,
808 next_op_id: u64,
809 in_flight: u32,
812 posted_wrs: Arc<AtomicU64>,
815 poller_wake: Option<PollerWake>,
816}
817
818#[derive(Debug)]
819struct DetachedQueuePairResources<Qp> {
820 _qp: Qp,
821 _posted: HashMap<u64, PostedOpEntry>,
822}
823
824#[derive(Debug)]
825struct QueuePairWorkerFailed {
826 error: String,
827}
828
829#[derive(Debug)]
832pub(super) struct QueuePairHandle<M: Manager, Qp: IbvQueuePair> {
833 actor: ActorHandle<QueuePairActor<M, Qp>>,
834 sender: mpsc::UnboundedSender<ProcessOps>,
835}
836
837impl<M: Manager, Qp: IbvQueuePair> Clone for QueuePairHandle<M, Qp> {
838 fn clone(&self) -> Self {
839 Self {
840 actor: self.actor.clone(),
841 sender: self.sender.clone(),
842 }
843 }
844}
845
846impl<M: Manager, Qp: IbvQueuePair> QueuePairHandle<M, Qp> {
847 pub(super) fn new(
848 actor: ActorHandle<QueuePairActor<M, Qp>>,
849 sender: mpsc::UnboundedSender<ProcessOps>,
850 ) -> Self {
851 Self { actor, sender }
852 }
853
854 pub(super) fn send(&self, message: ProcessOps) -> Result<(), anyhow::Error> {
855 self.sender
856 .send(message)
857 .map_err(|_| anyhow::anyhow!("queue pair worker stopped"))
858 }
859
860 pub(super) fn sender(&self) -> mpsc::UnboundedSender<ProcessOps> {
861 self.sender.clone()
862 }
863
864 pub(super) fn drain_and_stop(&self, reason: &str) -> Result<(), ActorError> {
865 self.actor.drain_and_stop(reason)
866 }
867
868 #[cfg(test)]
869 fn actor(&self) -> &ActorHandle<QueuePairActor<M, Qp>> {
870 &self.actor
871 }
872}
873
874#[derive(Debug)]
875struct QueuePairWorker<Qp: IbvQueuePair> {
876 state: Option<QueuePairState<Qp>>,
877 completions: CompletionInbox,
878 completion_route: Option<CompletionRoute>,
879 process_ops: mpsc::UnboundedReceiver<ProcessOps>,
880 _submission_sender: mpsc::UnboundedSender<ProcessOps>,
881 cq_poller: ActorHandle<CompletionQueueActor<IbvCq>>,
882 detach_target: Option<(CqId, u32)>,
883 cq_lease: Option<CqLease>,
884}
885
886#[derive(Debug)]
893pub(super) struct QueuePairActor<M: Manager, Qp: IbvQueuePair> {
894 local_manager: ActorRef<M>,
897 peer_manager: ActorRef<M>,
899 worker: Option<QueuePairWorker<Qp>>,
900 is_loopback: bool,
906 init_timeout: Duration,
907 cancellation: CancellationToken,
908 task: Option<tokio::task::JoinHandle<()>>,
909 #[cfg(test)]
910 completion_sender: mpsc::UnboundedSender<CompletionResult>,
911}
912
913impl<M: Manager, Qp: IbvQueuePair> QueuePairActor<M, Qp> {
914 fn worker_mut(&mut self) -> &mut QueuePairWorker<Qp> {
915 self.worker
916 .as_mut()
917 .expect("the queue pair worker has not started")
918 }
919
920 pub(super) fn new(
921 qp_key: QpKey,
922 local_manager: ActorRef<M>,
923 peer_manager: ActorRef<M>,
924 qp: Qp,
925 cq_poller: ActorHandle<CompletionQueueActor<IbvCq>>,
926 cq_lease: CqLease,
927 is_loopback: bool,
928 max_send_wr: u32,
929 ) -> (Self, mpsc::UnboundedSender<ProcessOps>) {
930 let init_timeout = hyperactor_config::global::get(crate::config::RDMA_QP_INIT_TIMEOUT);
931 let posted_wrs = Arc::new(AtomicU64::new(0));
932 let (completions, completion_route) = CompletionInbox::new();
933 let (sender, process_ops) = mpsc::unbounded_channel();
934 #[cfg(test)]
935 let completion_sender = completion_route.sender_for_test();
936 let state = QueuePairState::new(qp_key, qp, max_send_wr, posted_wrs);
937 (
938 Self {
939 worker: Some(QueuePairWorker {
940 state: Some(state),
941 completions,
942 completion_route: Some(completion_route),
943 process_ops,
944 _submission_sender: sender.clone(),
945 cq_poller,
946 detach_target: None,
947 cq_lease: Some(cq_lease),
948 }),
949 local_manager,
950 peer_manager,
951 is_loopback,
952 init_timeout,
953 cancellation: CancellationToken::new(),
954 task: None,
955 #[cfg(test)]
956 completion_sender,
957 },
958 sender,
959 )
960 }
961}
962
963impl<M: Manager, Qp: IbvQueuePair> Drop for QueuePairActor<M, Qp> {
964 fn drop(&mut self) {
965 self.cancellation.cancel();
966 }
967}
968
969impl<Qp: IbvQueuePair> QueuePairState<Qp> {
970 fn new(qp_key: QpKey, qp: Qp, max_send_wr: u32, posted_wrs: Arc<AtomicU64>) -> Self {
971 Self {
972 qp_key,
973 qp,
974 max_send_wr,
975 queue: VecDeque::new(),
976 posted: HashMap::new(),
977 wr_to_op: HashMap::new(),
978 next_op_id: 0,
979 in_flight: 0,
980 posted_wrs,
981 poller_wake: None,
982 }
983 }
984
985 fn enqueue(&mut self, message: ProcessOps) {
986 let max_msg_size = self.qp.max_msg_size();
987 self.queue
988 .extend(message.items.into_iter().map(|op| PendingOp {
989 wrs: op.local.size.div_ceil(max_msg_size).max(1) as u32,
990 op,
991 reply: message.reply.clone(),
992 }));
993 }
994
995 fn try_post_head(&mut self) -> Result<bool, anyhow::Error> {
1006 let pending = self.queue.pop_front().expect("non-empty queue");
1007 let PendingOp { op, reply, wrs } = pending;
1008 let stripe_id = op.stripe_id;
1009
1010 if wrs > self.max_send_wr {
1012 let err = format!(
1013 "op too large for this QP [{stripe_id}, qp_key={:?}, op_type={:?}, wrs={}, max_send_wr={}, local: {:?}, remote: {:?}]",
1014 self.qp_key, op.op_type, wrs, self.max_send_wr, op.local_memory, op.remote,
1015 );
1016 let _ = reply.send(StripeResult {
1017 stripe_id,
1018 result: Err(err),
1019 });
1020 return Ok(true);
1021 }
1022
1023 if self.in_flight + wrs > self.max_send_wr {
1027 self.queue.push_front(PendingOp { op, reply, wrs });
1028 return Ok(false);
1029 }
1030
1031 let local = op.local;
1033 let post_result = match op.op_type {
1034 RdmaOpType::WriteFromLocal => self.qp.put(op.remote.clone(), local.clone()),
1035 RdmaOpType::ReadIntoLocal => self.qp.get(local.clone(), op.remote.clone()),
1036 };
1037 let wr_ids = post_result.map_err(|e| {
1038 anyhow::anyhow!(
1039 "qp.{} failed [{stripe_id}, qp_key={:?}, local: {:?}, remote: {:?}]: {e}",
1040 if matches!(op.op_type, RdmaOpType::ReadIntoLocal) {
1041 "get"
1042 } else {
1043 "put"
1044 },
1045 self.qp_key,
1046 local,
1047 op.remote,
1048 )
1049 })?;
1050
1051 let op_id = self.next_op_id;
1053 self.next_op_id += 1;
1054 let mut pending_wrs = HashSet::with_capacity(wr_ids.len());
1055 for id in &wr_ids {
1056 self.wr_to_op.insert(*id, op_id);
1057 pending_wrs.insert(*id);
1058 }
1059 self.in_flight += wr_ids.len() as u32;
1060 self.posted.insert(
1061 op_id,
1062 PostedOpEntry {
1063 stripe_id,
1064 pending_wrs,
1065 _local_memory: op.local_memory,
1066 _local_mrv: local,
1067 reply,
1068 first_error: None,
1069 },
1070 );
1071 self.posted_wrs
1074 .fetch_add(wr_ids.len() as u64, Ordering::Relaxed);
1075 self.poller_wake
1076 .as_ref()
1077 .expect("the queue pair attached before posting work")
1078 .notify()
1079 .map_err(|error| {
1080 anyhow::anyhow!(
1081 "could not wake the completion queue poller [qp_key={:?}]: {error}",
1082 self.qp_key,
1083 )
1084 })?;
1085 Ok(true)
1086 }
1087
1088 fn post_ready(&mut self) -> Result<(), anyhow::Error> {
1089 while !self.queue.is_empty() {
1090 if !self.try_post_head()? {
1091 break;
1092 }
1093 }
1094 Ok(())
1095 }
1096
1097 fn complete_wr(&mut self, completion: CompletionResult) -> Result<(), anyhow::Error> {
1098 let (wr_id, wr_error) = match completion {
1099 Ok(wc) => (wc.wr_id(), None),
1100 Err(per_wr) => (per_wr.wr_id, Some(per_wr.to_string())),
1101 };
1102 let op_id = self
1103 .wr_to_op
1104 .remove(&wr_id)
1105 .expect("completed wr_id missing from wr_to_op");
1106 let entry = self
1107 .posted
1108 .get_mut(&op_id)
1109 .expect("op_id missing from posted");
1110 entry.pending_wrs.remove(&wr_id);
1111 self.in_flight -= 1;
1112 if let Some(err) = wr_error
1113 && entry.first_error.is_none()
1114 {
1115 entry.first_error = Some(err);
1116 }
1117 if entry.pending_wrs.is_empty() {
1118 let entry = self.posted.remove(&op_id).expect("just verified");
1119 let result = match entry.first_error {
1120 Some(err) => Err(err),
1121 None => Ok(()),
1122 };
1123 let _ = entry.reply.send(StripeResult {
1124 stripe_id: entry.stripe_id,
1125 result,
1126 });
1127 }
1128 Ok(())
1129 }
1130
1131 fn into_detached(self) -> DetachedQueuePair {
1132 DetachedQueuePair::new(DetachedQueuePairResources {
1133 _qp: self.qp,
1134 _posted: self.posted,
1138 })
1139 }
1140}
1141
1142impl<Qp: IbvQueuePair> QueuePairWorker<Qp> {
1143 fn state(&self) -> &QueuePairState<Qp> {
1144 self.state
1145 .as_ref()
1146 .expect("the queue pair has not been detached")
1147 }
1148
1149 fn state_mut(&mut self) -> &mut QueuePairState<Qp> {
1150 self.state
1151 .as_mut()
1152 .expect("the queue pair has not been detached")
1153 }
1154
1155 async fn run(&mut self, cancellation: CancellationToken) -> Result<(), anyhow::Error> {
1156 if cancellation.is_cancelled() {
1157 return Ok(());
1158 }
1159 let qp_num = self.state_mut().qp.get_qp_info()?.qp_num;
1160 let cq = Arc::clone(
1161 self.cq_lease
1162 .as_ref()
1163 .expect("the queue pair worker starts with a CQ lease")
1164 .cq(),
1165 );
1166 let cq_id = cq.cq_id();
1167 let route = self
1168 .completion_route
1169 .take()
1170 .expect("the queue pair attaches its completion route once");
1171 let posted = Arc::clone(&self.state().posted_wrs);
1172 let (reply, installed) = tokio::sync::oneshot::channel();
1173 self.cq_poller.try_post(
1174 Instance::<()>::self_client(),
1175 Attach {
1176 cq,
1177 qp_num,
1178 route,
1179 posted,
1180 reply,
1181 },
1182 )?;
1183 self.detach_target = Some((cq_id, qp_num));
1184 let poller_wake = tokio::select! {
1185 biased;
1186
1187 _ = cancellation.cancelled() => return Ok(()),
1188 result = installed => result.map_err(|error| {
1189 anyhow::anyhow!("CQ poller did not attach queue pair: {error}")
1190 })?,
1191 };
1192 self.state_mut().poller_wake = Some(poller_wake);
1193
1194 loop {
1195 tokio::select! {
1196 biased;
1197
1198 _ = cancellation.cancelled() => return Ok(()),
1199 completion = self.completions.recv(), if self.state().in_flight > 0 => {
1202 let completion = completion.ok_or_else(|| {
1203 anyhow::anyhow!(
1204 "completion queue poller stopped [qp_key={:?}]",
1205 self.state().qp_key,
1206 )
1207 })?;
1208 self.state_mut().complete_wr(completion)?;
1209 self.state_mut().post_ready()?;
1210 }
1211 message = self.process_ops.recv() => {
1212 let message = message.expect(
1213 "the queue pair worker retains a submission sender while it runs",
1214 );
1215 self.state_mut().enqueue(message);
1216 self.state_mut().post_ready()?;
1217 }
1218 }
1219 }
1220 }
1221}
1222
1223impl<Qp: IbvQueuePair> Drop for QueuePairWorker<Qp> {
1224 fn drop(&mut self) {
1225 let Some((cq_id, qp_num)) = self.detach_target else {
1226 return;
1227 };
1228 let state = self
1229 .state
1230 .take()
1231 .expect("a queue pair with a detach target has state");
1232 let lease = self
1233 .cq_lease
1234 .take()
1235 .expect("a queue pair with a detach target has a CQ lease");
1236 let qp_key = state.qp_key.clone();
1237 if let Err(error) = self.cq_poller.try_post(
1238 Instance::<()>::self_client(),
1239 Detach {
1240 cq_id,
1241 qp_num,
1242 qp: state.into_detached(),
1243 lease,
1244 },
1245 ) {
1246 tracing::warn!(
1247 ?qp_key,
1248 qp_num,
1249 ?cq_id,
1250 %error,
1251 "handing the queue pair to its poller failed",
1252 );
1253 }
1254 }
1255}
1256
1257async fn run_queue_pair_worker<Qp: IbvQueuePair>(
1258 mut worker: QueuePairWorker<Qp>,
1259 cancellation: CancellationToken,
1260 failed: PortHandle<QueuePairWorkerFailed>,
1261) {
1262 let result = AssertUnwindSafe(worker.run(cancellation))
1263 .catch_unwind()
1264 .await;
1265 let error = match result {
1266 Ok(Ok(())) => return,
1267 Ok(Err(error)) => error.to_string(),
1268 Err(_) => "queue pair worker panicked".to_owned(),
1269 };
1270 let error_for_log = error.clone();
1271 if let Err(post_error) = failed.try_post(
1272 Instance::<()>::self_client(),
1273 QueuePairWorkerFailed { error },
1274 ) {
1275 tracing::error!(
1276 error = %error_for_log,
1277 %post_error,
1278 "reporting queue pair worker failure failed",
1279 );
1280 }
1281}
1282
1283#[async_trait]
1284impl<M: Manager, Qp: IbvQueuePair> Actor for QueuePairActor<M, Qp> {
1285 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
1286 this.set_system();
1287 let qp_key = self.worker_mut().state().qp_key.clone();
1288 let local_info = self
1289 .worker_mut()
1290 .state_mut()
1291 .qp
1292 .get_qp_info()
1293 .map_err(|e| {
1294 tracing::error!(?qp_key, error = %e, "QueuePairActor init: get_qp_info failed");
1295 anyhow::anyhow!("could not extract local QP info: {e}")
1296 })?;
1297
1298 let peer_info = if self.is_loopback {
1299 local_info.clone()
1302 } else {
1303 let (reply, rx) = this.mailbox().open_once_port::<Result<IbvQpInfo, String>>();
1304 self.peer_manager.post(
1305 this,
1306 CreatePeerQueuePair {
1307 sender: self.local_manager.clone(),
1308 sender_device: qp_key.self_device.clone(),
1309 receiver_device: qp_key.other_device.clone(),
1310 qp_index: qp_key.qp_index,
1311 sender_info: local_info,
1312 reply: reply.bind(),
1313 },
1314 );
1315 match tokio::time::timeout(self.init_timeout, rx.recv()).await {
1316 Ok(Ok(Ok(info))) => info,
1317 Ok(Ok(Err(e))) => {
1318 tracing::error!(
1319 ?qp_key,
1320 peer_manager = ?self.peer_manager,
1321 error = %e,
1322 "QueuePairActor init: peer manager rejected CreatePeerQueuePair",
1323 );
1324 return Err(anyhow::anyhow!("peer manager rejected QP request: {e}"));
1325 }
1326 Ok(Err(e)) => {
1327 tracing::error!(
1328 ?qp_key,
1329 peer_manager = ?self.peer_manager,
1330 error = %e,
1331 "QueuePairActor init: peer reply port closed",
1332 );
1333 return Err(anyhow::anyhow!("peer reply port closed: {e}"));
1334 }
1335 Err(_) => {
1336 tracing::error!(
1337 ?qp_key,
1338 peer_manager = ?self.peer_manager,
1339 timeout = ?self.init_timeout,
1340 "QueuePairActor init: timed out waiting for peer reply",
1341 );
1342 return Err(anyhow::anyhow!(
1343 "QP initialization timed out after {:?}",
1344 self.init_timeout
1345 ));
1346 }
1347 }
1348 };
1349
1350 self.worker_mut()
1351 .state_mut()
1352 .qp
1353 .connect(&peer_info)
1354 .map_err(|e| {
1355 tracing::error!(
1356 ?qp_key,
1357 peer_info = ?peer_info,
1358 error = %e,
1359 "QueuePairActor init: connect failed",
1360 );
1361 anyhow::anyhow!("could not connect QP to peer: {e}")
1362 })?;
1363
1364 let worker = self
1365 .worker
1366 .take()
1367 .expect("the queue pair worker starts only once");
1368 let failed = this.handle().port::<QueuePairWorkerFailed>();
1369 self.task = Some(crate::rdma_runtime::spawn_on_rdma_runtime(
1370 run_queue_pair_worker(worker, self.cancellation.clone(), failed),
1371 ));
1372 Ok(())
1373 }
1374
1375 async fn cleanup(
1376 &mut self,
1377 _this: &Instance<Self>,
1378 _err: Option<&ActorError>,
1379 ) -> Result<(), anyhow::Error> {
1380 self.cancellation.cancel();
1381 let Some(task) = self.task.take() else {
1382 return Ok(());
1383 };
1384 task.await
1385 .map_err(|error| anyhow::anyhow!("joining queue pair worker failed: {error}"))
1386 }
1387
1388 fn spawn_server_task<F>(future: F) -> tokio::task::JoinHandle<F::Output>
1392 where
1393 F: std::future::Future + Send + 'static,
1394 F::Output: Send + 'static,
1395 {
1396 crate::rdma_runtime::spawn_on_rdma_runtime(future)
1397 }
1398}
1399
1400#[async_trait]
1401impl<M: Manager, Qp: IbvQueuePair> Handler<QueuePairWorkerFailed> for QueuePairActor<M, Qp> {
1402 async fn handle(
1403 &mut self,
1404 _cx: &Context<Self>,
1405 message: QueuePairWorkerFailed,
1406 ) -> Result<(), anyhow::Error> {
1407 Err(anyhow::anyhow!(
1408 "queue pair worker failed: {}",
1409 message.error
1410 ))
1411 }
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416 use std::sync::Arc;
1417 use std::sync::Mutex;
1418 use std::sync::atomic::AtomicBool;
1419 use std::sync::atomic::Ordering;
1420 use std::time::Duration;
1421
1422 use anyhow::Result;
1423 use async_trait::async_trait;
1424 use hyperactor::ActorHandle;
1425 use hyperactor::Context;
1426 use hyperactor::Handler;
1427 use hyperactor::proc::Proc;
1428
1429 use super::super::memory_region::IbvMemoryRegionKeepalive;
1430 use super::*;
1431 use crate::backend::ibverbs::device::IbvDevice;
1432 use crate::backend::ibverbs::device::list_all_devices;
1433 use crate::backend::ibverbs::device_selection::IbvDeviceTarget;
1434 use crate::backend::ibverbs::device_selection::resolve_target;
1435 use crate::backend::ibverbs::mlx_device::MlxDevice;
1436 use crate::backend::ibverbs::primitives::IbvConfig;
1437
1438 #[test]
1439 fn test_create_connection() {
1440 if list_all_devices().is_empty() {
1441 println!("Skipping test: RDMA devices not available");
1442 return;
1443 }
1444
1445 let config = IbvConfig {
1446 use_gpu_direct: false,
1447 ..Default::default()
1448 };
1449 let device_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0))
1450 .expect("resolving cpu:0 should succeed")
1451 .expect("cpu:0 should resolve to a NIC");
1452 let mut device = IbvDevice::<MlxDevice>::try_open(device_info.name(), config.clone())
1453 .expect("resolved device should open");
1454 let domain = device
1455 .get_or_create_domain("test")
1456 .expect("domain creation should succeed");
1457 let queue_pair = legacy::IbvQueuePair::new(domain, config.clone());
1458 assert!(queue_pair.is_ok());
1459 }
1460
1461 #[test]
1462 fn test_loopback_connection() {
1463 if list_all_devices().is_empty() {
1464 println!("Skipping test: RDMA devices not available");
1465 return;
1466 }
1467
1468 let server_config = IbvConfig {
1469 use_gpu_direct: false,
1470 ..Default::default()
1471 };
1472 let client_config = IbvConfig {
1473 use_gpu_direct: false,
1474 ..Default::default()
1475 };
1476
1477 let server_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0))
1478 .expect("resolving cpu:0 should succeed")
1479 .expect("cpu:0 should resolve to a NIC");
1480 let mut server_device =
1481 IbvDevice::<MlxDevice>::try_open(server_info.name(), server_config.clone())
1482 .expect("server device should open");
1483 let server_domain = server_device
1484 .get_or_create_domain("test")
1485 .expect("server domain creation should succeed");
1486 let client_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0))
1487 .expect("resolving cpu:0 should succeed")
1488 .expect("cpu:0 should resolve to a NIC");
1489 let mut client_device =
1490 IbvDevice::<MlxDevice>::try_open(client_info.name(), client_config.clone())
1491 .expect("client device should open");
1492 let client_domain = client_device
1493 .get_or_create_domain("test")
1494 .expect("client domain creation should succeed");
1495
1496 let mut server_qp =
1497 legacy::IbvQueuePair::new(server_domain, server_config.clone()).unwrap();
1498 let mut client_qp =
1499 legacy::IbvQueuePair::new(client_domain, client_config.clone()).unwrap();
1500
1501 let server_connection_info = server_qp.get_qp_info().unwrap();
1502 let client_connection_info = client_qp.get_qp_info().unwrap();
1503
1504 assert!(server_qp.connect(&client_connection_info).is_ok());
1505 assert!(client_qp.connect(&server_connection_info).is_ok());
1506 }
1507
1508 #[derive(Debug, Clone)]
1516 struct CreateCapture {
1517 sender_id: hyperactor::ActorId,
1518 sender_device: String,
1519 receiver_device: String,
1520 qp_index: usize,
1521 sender_qp_num: u32,
1522 }
1523
1524 #[derive(Debug)]
1525 struct QpaMockState {
1526 creates: Vec<CreateCapture>,
1527 response: Option<Result<IbvQpInfo, String>>,
1528 supervision_tx: Option<
1531 tokio::sync::mpsc::UnboundedSender<hyperactor::supervision::ActorSupervisionEvent>,
1532 >,
1533 }
1534
1535 #[derive(Debug)]
1542 #[hyperactor::export(handlers = [CreatePeerQueuePair<QpaMockManager>])]
1543 struct QpaMockManager {
1544 state: Arc<Mutex<QpaMockState>>,
1545 }
1546
1547 #[async_trait]
1548 impl Actor for QpaMockManager {
1549 async fn handle_supervision_event(
1550 &mut self,
1551 _this: &Instance<Self>,
1552 event: &hyperactor::supervision::ActorSupervisionEvent,
1553 ) -> Result<bool> {
1554 let tx = self.state.lock().unwrap().supervision_tx.clone();
1555 let Some(tx) = tx else {
1556 return Ok(!event.is_error());
1557 };
1558 tx.send(event.clone())
1559 .map_err(|e| anyhow::anyhow!("supervision_tx send failed: {e}"))?;
1560 Ok(true)
1561 }
1562 }
1563
1564 #[async_trait]
1565 impl Handler<CreatePeerQueuePair<QpaMockManager>> for QpaMockManager {
1566 async fn handle(
1567 &mut self,
1568 cx: &Context<Self>,
1569 msg: CreatePeerQueuePair<QpaMockManager>,
1570 ) -> Result<()> {
1571 let response = {
1572 let mut state = self.state.lock().unwrap();
1573 state.creates.push(CreateCapture {
1574 sender_id: msg.sender.actor_addr().id().clone(),
1575 sender_device: msg.sender_device.clone(),
1576 receiver_device: msg.receiver_device.clone(),
1577 qp_index: msg.qp_index,
1578 sender_qp_num: msg.sender_info.qp_num,
1579 });
1580 state.response.take()
1581 };
1582 if let Some(response) = response {
1583 msg.reply.post(cx, response);
1584 }
1585 Ok(())
1587 }
1588 }
1589
1590 #[derive(Debug)]
1595 struct SpawnQpaChild {
1596 qp_key: QpKey,
1597 peer_manager: ActorRef<QpaMockManager>,
1598 qp: MockQp,
1599 is_loopback: bool,
1600 max_send_wr: u32,
1601 reply: hyperactor::OncePortHandle<QueuePairHandle<QpaMockManager, MockQp>>,
1602 }
1603
1604 #[async_trait]
1605 impl Handler<SpawnQpaChild> for QpaMockManager {
1606 async fn handle(&mut self, cx: &Context<Self>, msg: SpawnQpaChild) -> Result<()> {
1607 let local_manager = cx.bind::<QpaMockManager>();
1608 let cq_poller = cx.spawn(CompletionQueueActor::<IbvCq>::new());
1609 let test_qp = msg.qp.clone();
1610 let (actor, sender) = QueuePairActor::new(
1611 msg.qp_key,
1612 local_manager,
1613 msg.peer_manager,
1614 msg.qp,
1615 cq_poller,
1616 CqLease::for_test(),
1617 msg.is_loopback,
1618 msg.max_send_wr,
1619 );
1620 test_qp.set_completion_sender(actor.completion_sender.clone());
1621 let handle = QueuePairHandle::new(cx.spawn(actor), sender);
1622 msg.reply.try_post(cx, handle)?;
1623 Ok(())
1624 }
1625 }
1626
1627 #[derive(Debug)]
1630 enum PostedOp {
1631 Put {
1632 remote_dst: IbvRemoteMemoryRegionView,
1633 local_src: IbvMemoryRegionView,
1634 wr_ids: Vec<u64>,
1635 },
1636 Get {
1637 local_dst: IbvMemoryRegionView,
1638 remote_src: IbvRemoteMemoryRegionView,
1639 wr_ids: Vec<u64>,
1640 },
1641 }
1642
1643 #[derive(Debug)]
1644 struct MockQpInner {
1645 connect_calls: Vec<IbvQpInfo>,
1646 next_wr_id: u64,
1647 posted_tx: tokio::sync::mpsc::UnboundedSender<PostedOp>,
1650 completion_sender: Option<mpsc::UnboundedSender<CompletionResult>>,
1651 post_error: Option<String>,
1653 }
1654
1655 #[derive(Debug, Clone)]
1659 struct MockQp {
1660 info: IbvQpInfo,
1661 inner: Arc<Mutex<MockQpInner>>,
1662 }
1663
1664 impl MockQp {
1665 fn new(qp_num: u32, psn: u32) -> (Self, tokio::sync::mpsc::UnboundedReceiver<PostedOp>) {
1669 let (posted_tx, posted_rx) = tokio::sync::mpsc::unbounded_channel();
1670 let qp = Self {
1671 info: IbvQpInfo {
1672 qp_num,
1673 lid: 0,
1674 gid: None,
1675 psn,
1676 },
1677 inner: Arc::new(Mutex::new(MockQpInner {
1678 connect_calls: Vec::new(),
1679 next_wr_id: 0,
1680 posted_tx,
1681 completion_sender: None,
1682 post_error: None,
1683 })),
1684 };
1685 (qp, posted_rx)
1686 }
1687
1688 fn connect_calls(&self) -> Vec<IbvQpInfo> {
1689 self.inner.lock().unwrap().connect_calls.clone()
1690 }
1691
1692 fn set_completion_sender(&self, sender: mpsc::UnboundedSender<CompletionResult>) {
1693 self.inner.lock().unwrap().completion_sender = Some(sender);
1694 }
1695
1696 fn queue_completion(&self, wr_id: u64) {
1697 self.queue_completion_result(Ok(IbvWc::for_test(wr_id, true)));
1698 }
1699
1700 fn queue_per_wr_error(&self, wr_id: u64, message: &str) {
1701 self.queue_completion_result(Err(WorkRequestError::for_test(wr_id, message)));
1702 }
1703
1704 fn queue_completion_result(&self, result: CompletionResult) {
1705 self.inner
1706 .lock()
1707 .unwrap()
1708 .completion_sender
1709 .as_ref()
1710 .expect("the mock queue pair is attached")
1711 .send(result)
1712 .expect("the completion inbox should remain open");
1713 }
1714
1715 fn queue_post_error(&self, message: &str) {
1717 self.inner.lock().unwrap().post_error = Some(message.to_string());
1718 }
1719 }
1720
1721 impl IbvQueuePair for MockQp {
1722 unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
1723 _domain: &IbvDomain<I>,
1724 _config: IbvConfig,
1725 _send_cq: Arc<IbvCq>,
1726 _recv_cq: Arc<IbvCq>,
1727 ) -> Result<Self> {
1728 unreachable!("MockQp is constructed directly, not from a domain")
1731 }
1732
1733 fn connect(&mut self, info: &IbvQpInfo) -> Result<()> {
1734 self.inner.lock().unwrap().connect_calls.push(info.clone());
1735 Ok(())
1736 }
1737
1738 fn get_qp_info(&mut self) -> Result<IbvQpInfo> {
1739 Ok(self.info.clone())
1740 }
1741
1742 fn state(&mut self) -> Result<u32> {
1743 Ok(rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTS)
1744 }
1745
1746 fn put(
1747 &mut self,
1748 remote_dst: IbvRemoteMemoryRegionView,
1749 local_src: IbvMemoryRegionView,
1750 ) -> Result<Vec<u64>> {
1751 let mut inner = self.inner.lock().unwrap();
1752 if let Some(msg) = inner.post_error.take() {
1753 return Err(anyhow::anyhow!(msg));
1754 }
1755 let wrs = local_src.size.div_ceil(MAX_RDMA_MSG_SIZE).max(1);
1756 let mut wr_ids = Vec::with_capacity(wrs);
1757 for _ in 0..wrs {
1758 wr_ids.push(inner.next_wr_id);
1759 inner.next_wr_id += 1;
1760 }
1761 let _ = inner.posted_tx.send(PostedOp::Put {
1762 remote_dst,
1763 local_src,
1764 wr_ids: wr_ids.clone(),
1765 });
1766 Ok(wr_ids)
1767 }
1768
1769 fn get(
1770 &mut self,
1771 local_dst: IbvMemoryRegionView,
1772 remote_src: IbvRemoteMemoryRegionView,
1773 ) -> Result<Vec<u64>> {
1774 let mut inner = self.inner.lock().unwrap();
1775 if let Some(msg) = inner.post_error.take() {
1776 return Err(anyhow::anyhow!(msg));
1777 }
1778 let wrs = local_dst.size.div_ceil(MAX_RDMA_MSG_SIZE).max(1);
1779 let mut wr_ids = Vec::with_capacity(wrs);
1780 for _ in 0..wrs {
1781 wr_ids.push(inner.next_wr_id);
1782 inner.next_wr_id += 1;
1783 }
1784 let _ = inner.posted_tx.send(PostedOp::Get {
1785 local_dst,
1786 remote_src,
1787 wr_ids: wr_ids.clone(),
1788 });
1789 Ok(wr_ids)
1790 }
1791 }
1792
1793 struct QpaHarness {
1794 proc: Proc,
1795 parent: ActorHandle<QpaMockManager>,
1796 peer: ActorHandle<QpaMockManager>,
1797 peer_state: Arc<Mutex<QpaMockState>>,
1798 client: hyperactor::Client,
1799 supervision_rx:
1800 tokio::sync::mpsc::UnboundedReceiver<hyperactor::supervision::ActorSupervisionEvent>,
1801 }
1802
1803 impl QpaHarness {
1804 fn build() -> Result<Self> {
1805 let proc = Proc::anonymous();
1806 let (supervision_tx, supervision_rx) = tokio::sync::mpsc::unbounded_channel();
1807 let parent = proc.spawn_with_label(
1808 "parent",
1809 QpaMockManager {
1810 state: Arc::new(Mutex::new(QpaMockState {
1811 creates: Vec::new(),
1812 response: None,
1813 supervision_tx: Some(supervision_tx),
1814 })),
1815 },
1816 );
1817 let peer_state = Arc::new(Mutex::new(QpaMockState {
1818 creates: Vec::new(),
1819 response: None,
1820 supervision_tx: None,
1821 }));
1822 let peer = proc.spawn_with_label(
1823 "peer",
1824 QpaMockManager {
1825 state: Arc::clone(&peer_state),
1826 },
1827 );
1828 let client = proc.client("client");
1829 Ok(Self {
1830 proc,
1831 parent,
1832 peer,
1833 peer_state,
1834 client,
1835 supervision_rx,
1836 })
1837 }
1838
1839 fn peer_id(&self) -> hyperactor::ActorId {
1840 self.peer.actor_addr().id().clone()
1841 }
1842
1843 fn parent_id(&self) -> hyperactor::ActorId {
1844 self.parent.actor_addr().id().clone()
1845 }
1846
1847 async fn next_supervision_event(
1849 &mut self,
1850 ) -> hyperactor::supervision::ActorSupervisionEvent {
1851 tokio::time::timeout(Duration::from_secs(5), self.supervision_rx.recv())
1852 .await
1853 .expect("timed out waiting for child supervision event")
1854 .expect("supervision channel closed")
1855 }
1856
1857 async fn teardown(mut self) {
1860 self.supervision_rx.close();
1861 self.proc
1862 .destroy_and_wait(Duration::from_secs(30), "test teardown")
1863 .await
1864 .expect("destroy_and_wait failed");
1865 let mut leftover = Vec::new();
1866 while let Some(event) = self.supervision_rx.recv().await {
1867 leftover.push(event);
1868 }
1869 assert!(
1870 leftover.is_empty(),
1871 "unexpected supervision events at teardown: {leftover:?}",
1872 );
1873 }
1874
1875 async fn spawn_actor(
1876 &self,
1877 qp_key: QpKey,
1878 peer_manager: ActorRef<QpaMockManager>,
1879 qp: MockQp,
1880 is_loopback: bool,
1881 ) -> Result<QueuePairHandle<QpaMockManager, MockQp>> {
1882 self.spawn_actor_with_caps(qp_key, peer_manager, qp, is_loopback, 4)
1883 .await
1884 }
1885
1886 async fn spawn_actor_with_caps(
1887 &self,
1888 qp_key: QpKey,
1889 peer_manager: ActorRef<QpaMockManager>,
1890 qp: MockQp,
1891 is_loopback: bool,
1892 max_send_wr: u32,
1893 ) -> Result<QueuePairHandle<QpaMockManager, MockQp>> {
1894 let (reply, rx) = self.client.mailbox().open_once_port();
1895 self.parent.try_post(
1896 &self.client,
1897 SpawnQpaChild {
1898 qp_key,
1899 peer_manager,
1900 qp,
1901 is_loopback,
1902 max_send_wr,
1903 reply,
1904 },
1905 )?;
1906 Ok(rx.recv().await?)
1907 }
1908 }
1909
1910 async fn await_status(
1911 handle: &QueuePairHandle<QpaMockManager, MockQp>,
1912 expected: impl Fn(&hyperactor::actor::ActorStatus) -> bool,
1913 ) -> hyperactor::actor::ActorStatus {
1914 let mut status = handle.actor().status();
1915 status.wait_for(|s| expected(s)).await.unwrap();
1916 status.borrow().clone()
1917 }
1918
1919 #[timed_test::async_timed_test(timeout_secs = 60)]
1920 async fn qpa_init_succeeds_via_peer() -> Result<()> {
1921 let harness = QpaHarness::build()?;
1922 let peer_info = IbvQpInfo {
1923 qp_num: 0xbeef,
1924 lid: 0,
1925 gid: None,
1926 psn: 0xc0ffee,
1927 };
1928 harness.peer_state.lock().unwrap().response = Some(Ok(peer_info.clone()));
1929
1930 let (qp, _posted_rx) = MockQp::new(0x1234, 0xdead);
1931 let qp_key = QpKey {
1932 self_device: "mlx5_0".into(),
1933 other_id: harness.peer_id(),
1934 other_device: "mlx5_1".into(),
1935 qp_index: 3,
1936 };
1937 let handle = harness
1938 .spawn_actor(
1939 qp_key.clone(),
1940 harness.peer.bind::<QpaMockManager>(),
1941 qp.clone(),
1942 false,
1943 )
1944 .await?;
1945
1946 await_status(&handle, |s| {
1947 matches!(s, hyperactor::actor::ActorStatus::Idle)
1948 })
1949 .await;
1950
1951 let creates = harness.peer_state.lock().unwrap().creates.clone();
1952 assert_eq!(creates.len(), 1);
1953 assert_eq!(creates[0].sender_device, "mlx5_0");
1954 assert_eq!(creates[0].receiver_device, "mlx5_1");
1955 assert_eq!(creates[0].qp_index, 3);
1956 assert_eq!(creates[0].sender_qp_num, 0x1234);
1957 assert_eq!(creates[0].sender_id, harness.parent_id());
1960
1961 let connects = qp.connect_calls();
1962 assert_eq!(connects.len(), 1);
1963 assert_eq!(connects[0].qp_num, peer_info.qp_num);
1964 assert_eq!(connects[0].psn, peer_info.psn);
1965 harness.teardown().await;
1966 Ok(())
1967 }
1968
1969 #[timed_test::async_timed_test(timeout_secs = 60)]
1970 async fn qpa_init_loopback_skips_peer_round_trip() -> Result<()> {
1971 let harness = QpaHarness::build()?;
1972
1973 let (qp, _posted_rx) = MockQp::new(0xaaa, 0xbbb);
1974 let qp_key = QpKey {
1975 self_device: "mlx5_0".into(),
1976 other_id: harness.parent_id(),
1977 other_device: "mlx5_0".into(),
1978 qp_index: 0,
1979 };
1980 let handle = harness
1981 .spawn_actor(
1982 qp_key,
1983 harness.peer.bind::<QpaMockManager>(),
1984 qp.clone(),
1985 true,
1986 )
1987 .await?;
1988
1989 await_status(&handle, |s| {
1990 matches!(s, hyperactor::actor::ActorStatus::Idle)
1991 })
1992 .await;
1993 assert!(harness.peer_state.lock().unwrap().creates.is_empty());
1994
1995 let connects = qp.connect_calls();
1996 assert_eq!(connects.len(), 1);
1997 assert_eq!(connects[0].qp_num, 0xaaa);
1998 assert_eq!(connects[0].psn, 0xbbb);
1999 harness.teardown().await;
2000 Ok(())
2001 }
2002
2003 #[timed_test::async_timed_test(timeout_secs = 60)]
2004 async fn qpa_init_peer_error_fails_actor() -> Result<()> {
2005 let mut harness = QpaHarness::build()?;
2006 harness.peer_state.lock().unwrap().response =
2007 Some(Err("peer rejected, no domain on receiver_device".into()));
2008
2009 let qp_key = QpKey {
2010 self_device: "mlx5_0".into(),
2011 other_id: harness.peer_id(),
2012 other_device: "mlx5_99".into(),
2013 qp_index: 0,
2014 };
2015 let (qp, _posted_rx) = MockQp::new(1, 2);
2016 let handle = harness
2017 .spawn_actor(qp_key, harness.peer.bind::<QpaMockManager>(), qp, false)
2018 .await?;
2019
2020 let event = harness.next_supervision_event().await;
2021 assert_eq!(&event.actor_id, handle.actor().actor_addr());
2022 let report = event.failure_report().expect("event should be a failure");
2023 assert!(
2024 report.contains("peer rejected"),
2025 "failure report should surface the peer's error string; got: {report}"
2026 );
2027 await_status(&handle, |s| {
2028 matches!(s, hyperactor::actor::ActorStatus::Failed(_))
2029 })
2030 .await;
2031 harness.teardown().await;
2032 Ok(())
2033 }
2034
2035 #[timed_test::async_timed_test(timeout_secs = 60)]
2036 async fn qpa_init_timeout_fails_actor() -> Result<()> {
2037 let lock = hyperactor_config::global::lock();
2038 let _guard = lock.override_key(
2039 crate::config::RDMA_QP_INIT_TIMEOUT,
2040 Duration::from_millis(100),
2041 );
2042
2043 let mut harness = QpaHarness::build()?;
2044
2045 let qp_key = QpKey {
2046 self_device: "mlx5_0".into(),
2047 other_id: harness.peer_id(),
2048 other_device: "mlx5_1".into(),
2049 qp_index: 0,
2050 };
2051 let (qp, _posted_rx) = MockQp::new(1, 2);
2052 let handle = harness
2053 .spawn_actor(qp_key, harness.peer.bind::<QpaMockManager>(), qp, false)
2054 .await?;
2055
2056 let event = harness.next_supervision_event().await;
2057 assert_eq!(&event.actor_id, handle.actor().actor_addr());
2058 let report = event.failure_report().expect("event should be a failure");
2059 assert!(
2060 report.contains("timed out"),
2061 "failure report should mention timeout; got: {report}"
2062 );
2063 await_status(&handle, |s| {
2064 matches!(s, hyperactor::actor::ActorStatus::Failed(_))
2065 })
2066 .await;
2067 harness.teardown().await;
2068 Ok(())
2069 }
2070
2071 use crate::backend::ibverbs::primitives::IbvMr;
2076 use crate::local_memory::Keepalive;
2077 use crate::local_memory::KeepaliveLocalMemory;
2078
2079 struct FakeKeepalive {
2082 addr: usize,
2083 size: usize,
2084 }
2085 impl Keepalive for FakeKeepalive {
2086 fn addr(&self) -> usize {
2087 self.addr
2088 }
2089 fn size(&self) -> usize {
2090 self.size
2091 }
2092 }
2093
2094 fn fake_local_memory(addr: usize, size: usize) -> KeepaliveLocalMemory {
2096 KeepaliveLocalMemory::try_new(Arc::new(FakeKeepalive { addr, size }))
2097 .expect("a fake host address has a location")
2098 }
2099
2100 struct DropFlagKeepalive {
2103 addr: usize,
2104 size: usize,
2105 dropped: Arc<AtomicBool>,
2106 }
2107 impl Keepalive for DropFlagKeepalive {
2108 fn addr(&self) -> usize {
2109 self.addr
2110 }
2111 fn size(&self) -> usize {
2112 self.size
2113 }
2114 }
2115 impl Drop for DropFlagKeepalive {
2116 fn drop(&mut self) {
2117 self.dropped.store(true, Ordering::SeqCst);
2118 }
2119 }
2120
2121 fn fake_mrv(addr: usize, size: usize) -> IbvMemoryRegionView {
2122 IbvMemoryRegionView::new(
2123 addr,
2124 addr,
2125 size,
2126 0x1234,
2127 0x5678,
2128 SELF_DEVICE.to_string(),
2129 Arc::new(IbvMr::null()),
2131 )
2132 }
2133
2134 const SELF_DEVICE: &str = "mlx5_0";
2136 const PEER_DEVICE: &str = "mlx5_0";
2137
2138 fn make_op(op_idx: usize, op_type: RdmaOpType, addr: usize, size: usize) -> QueuePairOp {
2139 make_stripe(op_idx, 0, 1, op_type, addr, size, 0, size)
2140 }
2141
2142 #[expect(clippy::too_many_arguments, reason = "a test fixture for one stripe")]
2143 fn make_stripe(
2144 op_idx: usize,
2145 stripe_idx: usize,
2146 stripe_count: usize,
2147 op_type: RdmaOpType,
2148 addr: usize,
2149 region_size: usize,
2150 offset: usize,
2151 size: usize,
2152 ) -> QueuePairOp {
2153 let local = fake_mrv(addr, region_size)
2154 .try_slice(offset, size)
2155 .expect("a valid local slice");
2156 let remote = IbvRemoteMemoryRegionView {
2157 rkey: 0,
2158 addr: 0x4000_0000,
2159 size: region_size,
2160 device_name: PEER_DEVICE.to_string(),
2161 }
2162 .try_slice(offset, size)
2163 .expect("a valid remote slice");
2164 QueuePairOp {
2165 stripe_id: StripeId {
2166 op_idx,
2167 stripe_idx,
2168 stripe_count,
2169 },
2170 qp_index: 0,
2171 op_type,
2172 local_memory: fake_local_memory(addr, region_size),
2173 local,
2174 remote,
2175 }
2176 }
2177
2178 #[derive(Debug)]
2181 struct DropFlagMr {
2182 dropped: Arc<AtomicBool>,
2183 }
2184 impl IbvMemoryRegionKeepalive for DropFlagMr {}
2185 impl Drop for DropFlagMr {
2186 fn drop(&mut self) {
2187 self.dropped.store(true, Ordering::SeqCst);
2188 }
2189 }
2190
2191 fn make_op_watching_mr_drop(
2193 op_idx: usize,
2194 op_type: RdmaOpType,
2195 addr: usize,
2196 size: usize,
2197 dropped: Arc<AtomicBool>,
2198 ) -> QueuePairOp {
2199 QueuePairOp {
2200 local: IbvMemoryRegionView::new(
2201 addr,
2202 addr,
2203 size,
2204 0x1234,
2205 0x5678,
2206 SELF_DEVICE.to_string(),
2207 Arc::new(DropFlagMr { dropped }),
2208 ),
2209 ..make_op(op_idx, op_type, addr, size)
2210 }
2211 }
2212
2213 fn make_op_watching_drop(
2215 op_idx: usize,
2216 op_type: RdmaOpType,
2217 addr: usize,
2218 size: usize,
2219 dropped: Arc<AtomicBool>,
2220 ) -> QueuePairOp {
2221 QueuePairOp {
2222 local_memory: KeepaliveLocalMemory::try_new(Arc::new(DropFlagKeepalive {
2223 addr,
2224 size,
2225 dropped,
2226 }))
2227 .expect("a fake host address has a location"),
2228 ..make_op(op_idx, op_type, addr, size)
2229 }
2230 }
2231
2232 async fn await_dropped(flag: &AtomicBool) {
2236 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
2237 while !flag.load(Ordering::SeqCst) {
2238 assert!(
2239 tokio::time::Instant::now() < deadline,
2240 "the pin on the allocation was never released",
2241 );
2242 tokio::time::sleep(Duration::from_millis(10)).await;
2243 }
2244 }
2245
2246 impl QpaHarness {
2247 async fn spawn_ready_actor(
2252 &self,
2253 max_send_wr: u32,
2254 ) -> Result<(
2255 QueuePairHandle<QpaMockManager, MockQp>,
2256 MockQp,
2257 tokio::sync::mpsc::UnboundedReceiver<PostedOp>,
2258 )> {
2259 let (qp, posted_rx) = MockQp::new(0x1, 0x2);
2260 let qp_key = QpKey {
2261 self_device: SELF_DEVICE.into(),
2262 other_id: self.parent_id(),
2263 other_device: PEER_DEVICE.into(),
2264 qp_index: 0,
2265 };
2266 let handle = self
2267 .spawn_actor_with_caps(
2268 qp_key,
2269 self.peer.bind::<QpaMockManager>(),
2270 qp.clone(),
2271 true,
2272 max_send_wr,
2273 )
2274 .await?;
2275 await_status(&handle, |s| {
2276 matches!(s, hyperactor::actor::ActorStatus::Idle)
2277 })
2278 .await;
2279 Ok((handle, qp, posted_rx))
2280 }
2281 }
2282
2283 async fn recv_posted(rx: &mut tokio::sync::mpsc::UnboundedReceiver<PostedOp>) -> PostedOp {
2285 tokio::time::timeout(Duration::from_secs(5), rx.recv())
2286 .await
2287 .expect("timed out waiting for post on MockQp")
2288 .expect("MockQp post channel closed")
2289 }
2290
2291 async fn assert_no_post(
2294 rx: &mut tokio::sync::mpsc::UnboundedReceiver<PostedOp>,
2295 wait: Duration,
2296 ) {
2297 if let Ok(Some(p)) = tokio::time::timeout(wait, rx.recv()).await {
2298 panic!("unexpected post on MockQp: {p:?}");
2299 }
2300 }
2301
2302 fn expect_put(p: PostedOp) -> (IbvMemoryRegionView, IbvRemoteMemoryRegionView, Vec<u64>) {
2303 match p {
2304 PostedOp::Put {
2305 remote_dst,
2306 local_src,
2307 wr_ids,
2308 } => (local_src, remote_dst, wr_ids),
2309 other => panic!("expected Put, got {other:?}"),
2310 }
2311 }
2312
2313 fn expect_get(p: PostedOp) -> (IbvMemoryRegionView, IbvRemoteMemoryRegionView, Vec<u64>) {
2314 match p {
2315 PostedOp::Get {
2316 local_dst,
2317 remote_src,
2318 wr_ids,
2319 } => (local_dst, remote_src, wr_ids),
2320 other => panic!("expected Get, got {other:?}"),
2321 }
2322 }
2323
2324 fn submit_ops(
2327 actor: &QueuePairHandle<QpaMockManager, MockQp>,
2328 items: Vec<QueuePairOp>,
2329 ) -> Result<mpsc::UnboundedReceiver<StripeResult>> {
2330 let (reply, rx) = mpsc::unbounded_channel();
2331 actor.send(ProcessOps { items, reply })?;
2332 Ok(rx)
2333 }
2334
2335 async fn collect_replies(
2338 rx: &mut mpsc::UnboundedReceiver<StripeResult>,
2339 n: usize,
2340 ) -> Vec<(usize, Result<(), String>)> {
2341 let mut out = Vec::with_capacity(n);
2342 for _ in 0..n {
2343 let m = tokio::time::timeout(Duration::from_secs(5), rx.recv())
2344 .await
2345 .expect("timed out waiting for ProcessOps reply")
2346 .expect("ProcessOps result channel closed");
2347 out.push((m.stripe_id.op_idx, m.result));
2348 }
2349 out.sort_by_key(|(i, _)| *i);
2350 out
2351 }
2352
2353 async fn try_recv(
2356 rx: &mut mpsc::UnboundedReceiver<StripeResult>,
2357 wait: Duration,
2358 ) -> Option<(usize, Result<(), String>)> {
2359 match tokio::time::timeout(wait, rx.recv()).await {
2360 Ok(Some(m)) => Some((m.stripe_id.op_idx, m.result)),
2361 Ok(None) => panic!("ProcessOps result channel closed"),
2362 Err(_) => None,
2363 }
2364 }
2365
2366 #[timed_test::async_timed_test(timeout_secs = 60)]
2367 async fn qpa_processes_single_write() -> Result<()> {
2368 let harness = QpaHarness::build()?;
2369 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2370
2371 let items = vec![make_op(7, RdmaOpType::WriteFromLocal, 0x1000, 4096)];
2372 let mut rx = submit_ops(&actor, items)?;
2373
2374 let (lhandle, rhandle, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2376 assert_eq!(wr_ids, vec![0]);
2377 assert_eq!(lhandle.rdma_addr, 0x1000);
2378 assert_eq!(lhandle.size, 4096);
2379 assert_eq!(lhandle.lkey, 0x1234);
2380 assert_eq!(lhandle.rkey, 0x5678);
2381 assert_eq!(lhandle.device_name, SELF_DEVICE);
2382 assert_eq!(rhandle.addr, 0x4000_0000);
2383 assert_eq!(rhandle.size, 4096);
2384 assert_eq!(rhandle.device_name, PEER_DEVICE);
2385
2386 qp.queue_completion(0);
2387 let replies = collect_replies(&mut rx, 1).await;
2388 assert_eq!(replies, vec![(7, Ok(()))]);
2389 harness.teardown().await;
2390 Ok(())
2391 }
2392
2393 #[timed_test::async_timed_test(timeout_secs = 60)]
2394 async fn qpa_posts_only_its_stripe() -> Result<()> {
2395 let harness = QpaHarness::build()?;
2396 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2397
2398 let items = vec![make_stripe(
2399 7,
2400 2,
2401 4,
2402 RdmaOpType::WriteFromLocal,
2403 0x1000,
2404 4096,
2405 2048,
2406 1024,
2407 )];
2408 let mut rx = submit_ops(&actor, items)?;
2409
2410 let (local, remote, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2411 assert_eq!((local.rdma_addr, local.size), (0x1800, 1024));
2412 assert_eq!((remote.addr, remote.size), (0x4000_0800, 1024));
2413
2414 qp.queue_completion(wr_ids[0]);
2415 let reply = tokio::time::timeout(Duration::from_secs(5), rx.recv())
2416 .await
2417 .expect("timed out waiting for the stripe reply")
2418 .expect("reply channel closed");
2419 assert_eq!(
2420 reply.stripe_id,
2421 StripeId {
2422 op_idx: 7,
2423 stripe_idx: 2,
2424 stripe_count: 4,
2425 }
2426 );
2427 assert_eq!(reply.result, Ok(()));
2428 harness.teardown().await;
2429 Ok(())
2430 }
2431
2432 #[timed_test::async_timed_test(timeout_secs = 60)]
2433 async fn qpa_cleanup_joins_worker_and_closes_submission_channel() -> Result<()> {
2434 let mut harness = QpaHarness::build()?;
2435 let (actor, _qp, _posted_rx) = harness.spawn_ready_actor(4).await?;
2436
2437 actor.drain_and_stop("test complete")?;
2438 await_status(&actor, |status| {
2439 matches!(status, hyperactor::actor::ActorStatus::Stopped(_))
2440 })
2441 .await;
2442 let event = harness.next_supervision_event().await;
2443 assert_eq!(&event.actor_id, actor.actor().actor_addr());
2444 assert!(matches!(
2445 event.actor_status,
2446 hyperactor::actor::ActorStatus::Stopped(_)
2447 ));
2448
2449 let (reply, _rx) = mpsc::unbounded_channel();
2450 assert!(
2451 actor
2452 .send(ProcessOps {
2453 items: Vec::new(),
2454 reply,
2455 })
2456 .is_err(),
2457 "the submission receiver must be closed after actor cleanup",
2458 );
2459 harness.teardown().await;
2460 Ok(())
2461 }
2462
2463 #[timed_test::async_timed_test(timeout_secs = 60)]
2464 async fn qpa_stays_alive_after_external_submission_handles_are_dropped() -> Result<()> {
2465 let mut harness = QpaHarness::build()?;
2466 let (actor, _qp, _posted_rx) = harness.spawn_ready_actor(4).await?;
2467 let actor_handle = actor.actor().clone();
2468 let mut status = actor_handle.status();
2469
2470 drop(actor);
2471 assert!(
2472 tokio::time::timeout(
2473 Duration::from_secs(1),
2474 status.wait_for(hyperactor::actor::ActorStatus::is_terminal),
2475 )
2476 .await
2477 .is_err(),
2478 "dropping external submission handles must not stop the worker",
2479 );
2480
2481 actor_handle.drain_and_stop("test complete")?;
2482 let event = harness.next_supervision_event().await;
2483 assert_eq!(&event.actor_id, actor_handle.actor_addr());
2484 assert!(matches!(
2485 event.actor_status,
2486 hyperactor::actor::ActorStatus::Stopped(_)
2487 ));
2488 harness.teardown().await;
2489 Ok(())
2490 }
2491
2492 #[timed_test::async_timed_test(timeout_secs = 60)]
2493 async fn qpa_survives_a_dropped_operation_result_receiver() -> Result<()> {
2494 let harness = QpaHarness::build()?;
2495 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(1).await?;
2496 let abandoned = submit_ops(
2497 &actor,
2498 vec![make_op(0, RdmaOpType::WriteFromLocal, 0x1000, 4096)],
2499 )?;
2500 drop(abandoned);
2501 let (_, _, first_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2502 qp.queue_completion(first_wrs[0]);
2503
2504 let mut reply = submit_ops(
2505 &actor,
2506 vec![make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096)],
2507 )?;
2508 let (_, _, second_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2509 qp.queue_completion(second_wrs[0]);
2510
2511 assert_eq!(collect_replies(&mut reply, 1).await, vec![(1, Ok(()))]);
2512 harness.teardown().await;
2513 Ok(())
2514 }
2515
2516 #[timed_test::async_timed_test(timeout_secs = 60)]
2517 async fn qpa_posted_op_pins_local_memory() -> Result<()> {
2518 let harness = QpaHarness::build()?;
2519 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2520
2521 let dropped = Arc::new(AtomicBool::new(false));
2526 let items = vec![
2527 make_op_watching_drop(
2528 0,
2529 RdmaOpType::WriteFromLocal,
2530 0x1000,
2531 4096,
2532 Arc::clone(&dropped),
2533 ),
2534 make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2535 ];
2536 let mut rx = submit_ops(&actor, items)?;
2537
2538 let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2539 let (_, _, other_wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2540 assert!(
2541 !dropped.load(Ordering::SeqCst),
2542 "the allocation must stay mapped while its WR is in flight: `submit` can \
2543 return before every WR retires, and the caller is then free to drop it",
2544 );
2545
2546 qp.queue_completion(wr_ids[0]);
2550 await_dropped(&dropped).await;
2551
2552 qp.queue_completion(other_wr_ids[0]);
2554 assert_eq!(
2555 collect_replies(&mut rx, 2).await,
2556 vec![(0, Ok(())), (1, Ok(()))]
2557 );
2558 harness.teardown().await;
2559 Ok(())
2560 }
2561
2562 #[timed_test::async_timed_test(timeout_secs = 60)]
2563 async fn qpa_posted_op_pins_local_registration() -> Result<()> {
2564 let harness = QpaHarness::build()?;
2565 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2566
2567 let dropped = Arc::new(AtomicBool::new(false));
2569 let items = vec![
2570 make_op_watching_mr_drop(
2571 0,
2572 RdmaOpType::WriteFromLocal,
2573 0x1000,
2574 4096,
2575 Arc::clone(&dropped),
2576 ),
2577 make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2578 ];
2579 let mut rx = submit_ops(&actor, items)?;
2580
2581 let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2582 let (_, _, other_wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2583 assert!(
2584 !dropped.load(Ordering::SeqCst),
2585 "the registration must stay alive while a WR that touches it is in flight"
2586 );
2587
2588 qp.queue_completion(wr_ids[0]);
2589 await_dropped(&dropped).await;
2590
2591 qp.queue_completion(other_wr_ids[0]);
2592 assert_eq!(
2593 collect_replies(&mut rx, 2).await,
2594 vec![(0, Ok(())), (1, Ok(()))]
2595 );
2596 harness.teardown().await;
2597 Ok(())
2598 }
2599
2600 #[timed_test::async_timed_test(timeout_secs = 60)]
2601 async fn qpa_chunked_op_waits_for_all_wrs() -> Result<()> {
2602 let harness = QpaHarness::build()?;
2603 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8).await?;
2604
2605 let items = vec![make_op(
2608 11,
2609 RdmaOpType::WriteFromLocal,
2610 0x1000,
2611 3 * MAX_RDMA_MSG_SIZE,
2612 )];
2613 let mut rx = submit_ops(&actor, items)?;
2614
2615 let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2616 assert_eq!(wr_ids, vec![0, 1, 2]);
2617
2618 qp.queue_completion(wr_ids[0]);
2620 qp.queue_completion(wr_ids[1]);
2621 assert!(
2622 try_recv(&mut rx, Duration::from_millis(100))
2623 .await
2624 .is_none(),
2625 "op should not be reported until every WR has completed",
2626 );
2627
2628 qp.queue_completion(wr_ids[2]);
2630 let replies = collect_replies(&mut rx, 1).await;
2631 assert_eq!(replies, vec![(11, Ok(()))]);
2632 harness.teardown().await;
2633 Ok(())
2634 }
2635
2636 #[timed_test::async_timed_test(timeout_secs = 60)]
2637 async fn qpa_per_wr_error_held_until_all_wrs_complete() -> Result<()> {
2638 let harness = QpaHarness::build()?;
2639 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8).await?;
2640
2641 let items = vec![make_op(
2645 42,
2646 RdmaOpType::WriteFromLocal,
2647 0x1000,
2648 3 * MAX_RDMA_MSG_SIZE,
2649 )];
2650 let mut rx = submit_ops(&actor, items)?;
2651
2652 let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2653 assert_eq!(wr_ids.len(), 3);
2654
2655 qp.queue_per_wr_error(wr_ids[1], "simulated WR fail");
2657 assert!(
2658 try_recv(&mut rx, Duration::from_millis(100))
2659 .await
2660 .is_none(),
2661 "op error must wait for the other WRs to drain",
2662 );
2663
2664 qp.queue_completion(wr_ids[0]);
2667 qp.queue_completion(wr_ids[2]);
2668
2669 let replies = collect_replies(&mut rx, 1).await;
2670 assert_eq!(replies.len(), 1);
2671 assert_eq!(replies[0].0, 42);
2672 let err = replies[0]
2673 .1
2674 .as_ref()
2675 .expect_err("op should fail because one WR errored");
2676 assert!(
2677 err.contains("simulated WR fail"),
2678 "op error should surface the per-WR error: {err}",
2679 );
2680 harness.teardown().await;
2681 Ok(())
2682 }
2683
2684 #[timed_test::async_timed_test(timeout_secs = 60)]
2685 async fn qpa_per_wr_error_isolates_to_failing_op() -> Result<()> {
2686 let harness = QpaHarness::build()?;
2687 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8).await?;
2688
2689 let items = vec![
2693 make_op(0, RdmaOpType::WriteFromLocal, 0x1000, 3 * MAX_RDMA_MSG_SIZE),
2694 make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2695 ];
2696 let mut rx = submit_ops(&actor, items)?;
2697
2698 let (_, _, big_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2699 let (_, _, small_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2700 assert_eq!(big_wrs, vec![0, 1, 2]);
2701 assert_eq!(small_wrs, vec![3]);
2702
2703 qp.queue_completion(small_wrs[0]);
2705 qp.queue_per_wr_error(big_wrs[1], "isolated per-WR fail");
2707 qp.queue_completion(big_wrs[0]);
2708 qp.queue_completion(big_wrs[2]);
2709
2710 let replies = collect_replies(&mut rx, 2).await;
2711 assert_eq!(replies.len(), 2);
2712 assert_eq!(replies[0].0, 0);
2713 let err = replies[0]
2714 .1
2715 .as_ref()
2716 .expect_err("op_idx 0 should fail because one WR errored");
2717
2718 assert!(
2719 err.contains("isolated per-WR fail"),
2720 "op_idx 0 error should name the per-WR error: {err}",
2721 );
2722 assert_eq!(replies[1], (1usize, Ok(())));
2723 harness.teardown().await;
2724 Ok(())
2725 }
2726
2727 #[timed_test::async_timed_test(timeout_secs = 60)]
2728 async fn qpa_reads_gate_on_send_slots_like_writes() -> Result<()> {
2729 let harness = QpaHarness::build()?;
2730 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(2).await?;
2733
2734 let items = (0..3usize)
2735 .map(|i| make_op(i, RdmaOpType::ReadIntoLocal, 0x1000 + i * 0x1000, 4096))
2736 .collect();
2737 let mut rx = submit_ops(&actor, items)?;
2738
2739 let (_, _, r0_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2741 let (_, _, r1_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2742 assert_eq!(r0_wrs, vec![0]);
2743 assert_eq!(r1_wrs, vec![1]);
2744 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2745
2746 qp.queue_completion(r0_wrs[0]);
2748 let (_, _, r2_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2749 assert_eq!(r2_wrs, vec![2]);
2750
2751 qp.queue_completion(r1_wrs[0]);
2752 qp.queue_completion(r2_wrs[0]);
2753 let replies = collect_replies(&mut rx, 3).await;
2754 assert_eq!(replies, vec![(0, Ok(())), (1, Ok(())), (2, Ok(()))]);
2755 harness.teardown().await;
2756 Ok(())
2757 }
2758
2759 #[timed_test::async_timed_test(timeout_secs = 60)]
2760 async fn qpa_write_slot_gating() -> Result<()> {
2761 let harness = QpaHarness::build()?;
2762 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(2).await?;
2764
2765 let items = (0..4usize)
2766 .map(|i| make_op(i, RdmaOpType::WriteFromLocal, 0x1000 + i * 0x1000, 4096))
2767 .collect();
2768 let mut rx = submit_ops(&actor, items)?;
2769
2770 let (_, _, w0_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2771 let (_, _, w1_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2772 assert_eq!(w0_wrs, vec![0]);
2773 assert_eq!(w1_wrs, vec![1]);
2774 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2775
2776 qp.queue_completion(w0_wrs[0]);
2778 let (_, _, w2_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2779 assert_eq!(w2_wrs, vec![2]);
2780 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2781
2782 qp.queue_completion(w1_wrs[0]);
2784 let (_, _, w3_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2785 assert_eq!(w3_wrs, vec![3]);
2786
2787 qp.queue_completion(w2_wrs[0]);
2788 qp.queue_completion(w3_wrs[0]);
2789 let replies = collect_replies(&mut rx, 4).await;
2790 assert_eq!(
2791 replies,
2792 vec![(0, Ok(())), (1, Ok(())), (2, Ok(())), (3, Ok(()))],
2793 );
2794 harness.teardown().await;
2795 Ok(())
2796 }
2797
2798 #[timed_test::async_timed_test(timeout_secs = 60)]
2801 async fn qpa_reads_and_writes_share_send_slots() -> Result<()> {
2802 let harness = QpaHarness::build()?;
2803 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2804
2805 let items = vec![
2806 make_op(0, RdmaOpType::ReadIntoLocal, 0x1000, 4096),
2807 make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2808 make_op(2, RdmaOpType::WriteFromLocal, 0x3000, 4096),
2809 make_op(3, RdmaOpType::WriteFromLocal, 0x4000, 4096),
2810 make_op(4, RdmaOpType::ReadIntoLocal, 0x5000, 2 * MAX_RDMA_MSG_SIZE),
2811 ];
2812 let mut rx = submit_ops(&actor, items)?;
2813
2814 let (_, _, r0_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2816 let (_, _, w1_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2817 let (_, _, w2_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2818 let (_, _, w3_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2819 assert_eq!(r0_wrs, vec![0]);
2820 assert_eq!(w1_wrs, vec![1]);
2821 assert_eq!(w2_wrs, vec![2]);
2822 assert_eq!(w3_wrs, vec![3]);
2823 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2825
2826 qp.queue_completion(r0_wrs[0]);
2828 assert_eq!(collect_replies(&mut rx, 1).await, vec![(0, Ok(()))]);
2829 assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2830
2831 qp.queue_completion(w1_wrs[0]);
2834 assert_eq!(collect_replies(&mut rx, 1).await, vec![(1, Ok(()))]);
2835 let (_, _, r4_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2836 assert_eq!(r4_wrs, vec![4, 5]);
2837
2838 qp.queue_completion(w2_wrs[0]);
2840 qp.queue_completion(w3_wrs[0]);
2841 for &id in &r4_wrs {
2842 qp.queue_completion(id);
2843 }
2844 let rest = collect_replies(&mut rx, 3).await;
2845 assert_eq!(rest, vec![(2, Ok(())), (3, Ok(())), (4, Ok(()))]);
2846 harness.teardown().await;
2847 Ok(())
2848 }
2849
2850 #[timed_test::async_timed_test(timeout_secs = 60)]
2851 async fn qpa_multiple_batches_share_credit() -> Result<()> {
2852 let harness = QpaHarness::build()?;
2853 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2854
2855 let batch_a = vec![
2857 make_op(10, RdmaOpType::WriteFromLocal, 0x1000, 4096),
2858 make_op(11, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2859 ];
2860 let mut rx_a = submit_ops(&actor, batch_a)?;
2861
2862 let batch_b = vec![
2865 make_op(20, RdmaOpType::WriteFromLocal, 0x3000, 4096),
2866 make_op(21, RdmaOpType::WriteFromLocal, 0x4000, 4096),
2867 ];
2868 let mut rx_b = submit_ops(&actor, batch_b)?;
2869
2870 let mut all_wr_ids = Vec::new();
2872 for _ in 0..4 {
2873 let (_, _, wrs) = expect_put(recv_posted(&mut posted_rx).await);
2874 all_wr_ids.extend(wrs);
2875 }
2876 for &id in &all_wr_ids {
2877 qp.queue_completion(id);
2878 }
2879
2880 let replies_a = collect_replies(&mut rx_a, 2).await;
2881 assert_eq!(replies_a, vec![(10, Ok(())), (11, Ok(()))]);
2882 let replies_b = collect_replies(&mut rx_b, 2).await;
2883 assert_eq!(replies_b, vec![(20, Ok(())), (21, Ok(()))]);
2884 harness.teardown().await;
2885 Ok(())
2886 }
2887
2888 #[timed_test::async_timed_test(timeout_secs = 60)]
2889 async fn qpa_op_too_large_for_qp() -> Result<()> {
2890 let harness = QpaHarness::build()?;
2891 let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(1).await?;
2893
2894 let items = vec![
2895 make_op(0, RdmaOpType::WriteFromLocal, 0x1000, 2 * MAX_RDMA_MSG_SIZE),
2896 make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2897 ];
2898 let mut rx = submit_ops(&actor, items)?;
2899
2900 let (_, _, wrs) = expect_put(recv_posted(&mut posted_rx).await);
2902 assert_eq!(wrs, vec![0]);
2903 qp.queue_completion(0);
2904
2905 let replies = collect_replies(&mut rx, 2).await;
2906 assert_eq!(replies.len(), 2);
2908 assert_eq!(replies[0].0, 0);
2909 let err = replies[0]
2910 .1
2911 .as_ref()
2912 .expect_err("op_idx 0 should fail as too large");
2913 assert!(err.contains("too large"), "expected too-large error: {err}");
2914 assert_eq!(replies[1], (1usize, Ok(())));
2915 harness.teardown().await;
2916 Ok(())
2917 }
2918
2919 #[timed_test::async_timed_test(timeout_secs = 60)]
2920 async fn qpa_post_error_kills_actor_via_supervision() -> Result<()> {
2921 let mut harness = QpaHarness::build()?;
2922 let (actor, qp, _posted_rx) = harness.spawn_ready_actor(4).await?;
2923
2924 qp.queue_post_error("simulated post failure");
2925 let items = vec![make_op(0, RdmaOpType::WriteFromLocal, 0x1000, 4096)];
2926 let _rx = submit_ops(&actor, items)?;
2927
2928 let event = harness.next_supervision_event().await;
2929 assert_eq!(&event.actor_id, actor.actor().actor_addr());
2930 let report = event.failure_report().expect("event should be a failure");
2931 assert!(
2932 report.contains("qp.put failed") && report.contains("simulated post failure"),
2933 "supervision report should name the post failure: {report}",
2934 );
2935 await_status(&actor, |s| {
2936 matches!(s, hyperactor::actor::ActorStatus::Failed(_))
2937 })
2938 .await;
2939 harness.teardown().await;
2940 Ok(())
2941 }
2942}