1use std::collections::HashMap;
20use std::collections::hash_map::DefaultHasher;
21use std::fmt::Write as _;
22use std::hash::Hash;
23use std::hash::Hasher;
24use std::sync::OnceLock;
25use std::time::Duration;
26
27use anyhow::Result;
28use async_trait::async_trait;
29use hyperactor::Actor;
30use hyperactor::ActorHandle;
31use hyperactor::ActorRef;
32use hyperactor::Context;
33use hyperactor::Endpoint as _;
34use hyperactor::HandleClient;
35use hyperactor::Handler;
36use hyperactor::Instance;
37use hyperactor::OncePortHandle;
38use hyperactor::OncePortRef;
39use hyperactor::PortHandle;
40use hyperactor::RefClient;
41use hyperactor::actor::Referable;
42use serde::Deserialize;
43use serde::Serialize;
44use typeuri::Named;
45
46use super::IbvBuffer;
47use super::IbvOp;
48use super::device::IbvDevice;
49use super::device::IbvDeviceImpl;
50use super::device_selection::resolve_target;
51use super::device_selection::select_optimal_ibv_devices;
52use super::domain::IbvDomain;
53use super::domain::IbvDomainImpl;
54use super::efa_device::EfaDevice;
55use super::memory_region::IbvMemoryRegionView;
56use super::mlx_device::MlxDevice;
57use super::primitives::IbvConfig;
58use super::primitives::IbvQpInfo;
59use super::primitives::ibverbs_supported;
60use super::queue_pair::IbvQueuePair;
61use super::queue_pair::OpResult;
62use super::queue_pair::ProcessOps;
63use super::queue_pair::QpKey;
64use super::queue_pair::QueuePairActor;
65use super::queue_pair::legacy;
66use crate::RdmaOp;
67use crate::RdmaTransportLevel;
68use crate::backend::RdmaBackend;
69use crate::backend::RdmaConfig;
70use crate::backend::ResolveRemoteBackendContext;
71use crate::device_selection::MemoryLocation;
72use crate::local_memory::KeepaliveLocalMemory;
73use crate::local_memory::is_device_ptr;
74use crate::rdma_components::RdmaRemoteBuffer;
75use crate::rdma_manager_actor::RdmaManagerActor;
76use crate::validate_execution_context;
77
78#[derive(Debug, Serialize, Deserialize, Named)]
83#[serde(bound(serialize = "", deserialize = ""))]
84pub(super) struct CreatePeerQueuePair<M: Referable> {
85 pub(super) sender: ActorRef<M>,
87 pub(super) sender_device: String,
89 pub(super) receiver_device: String,
91 pub(super) sender_info: IbvQpInfo,
93 pub(super) reply: OncePortRef<Result<IbvQpInfo, String>>,
95}
96wirevalue::register_type!(CreatePeerQueuePair<IbvManagerActor<MlxDevice>>);
97wirevalue::register_type!(CreatePeerQueuePair<IbvManagerActor<EfaDevice>>);
98
99pub(super) struct SubmitOps<I: IbvDeviceImpl> {
110 pub(super) ops: Vec<IbvOp<IbvManagerActor<I>>>,
111 pub(super) reply: PortHandle<OpResult>,
112}
113
114pub struct RawQueuePair {
123 pub self_device: String,
124 pub reply: OncePortHandle<Result<legacy::IbvQueuePair, String>>,
125}
126
127#[derive(Handler, HandleClient, RefClient, Debug, Serialize, Deserialize, Named)]
129pub enum IbvManagerMessage {
130 ReleaseBuffer { remote_buf_id: usize },
133}
134wirevalue::register_type!(IbvManagerMessage);
135
136#[derive(Handler, HandleClient, Debug)]
138pub enum IbvManagerLocalMessage {
139 RegisterRemoteBuffer {
147 remote_buf_id: usize,
148 local: KeepaliveLocalMemory,
149 #[reply]
150 reply: OncePortHandle<Result<IbvBuffer, String>>,
151 },
152}
153
154const DEFAULT_DOMAIN: &str = "default";
157
158#[derive(Debug)]
167#[hyperactor::export(
168 handlers = [
169 IbvManagerMessage,
170 CreatePeerQueuePair<IbvManagerActor<I>>,
171 ],
172)]
173pub struct IbvManagerActor<I: IbvDeviceImpl> {
174 owner: OnceLock<ActorHandle<RdmaManagerActor>>,
175
176 qp_handles: HashMap<
181 QpKey,
182 ActorHandle<QueuePairActor<IbvManagerActor<I>, <I::Domain as IbvDomainImpl>::QueuePair>>,
183 >,
184
185 peer_created_qps: HashMap<QpKey, <I::Domain as IbvDomainImpl>::QueuePair>,
191
192 devices: HashMap<String, IbvDevice<I>>,
196
197 config: IbvConfig,
198
199 buffer_registrations: HashMap<usize, IbvMemoryRegionView>,
205}
206
207#[async_trait]
208impl<I: IbvDeviceImpl> Actor for IbvManagerActor<I> {
209 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
210 let owner = if let Some(owner) = this.parent_handle() {
211 owner
212 } else {
213 anyhow::bail!("RdmaManagerActor not found as parent of IbvManagerActor");
214 };
215 self.owner
216 .set(owner)
217 .expect("owner should only be set once during init");
218 Ok(())
219 }
220
221 fn spawn_server_task<F>(future: F) -> tokio::task::JoinHandle<F::Output>
225 where
226 F: std::future::Future + Send + 'static,
227 F::Output: Send + 'static,
228 {
229 crate::rdma_runtime::spawn_on_rdma_runtime(future)
230 }
231}
232
233impl<I: IbvDeviceImpl> Drop for IbvManagerActor<I> {
234 fn drop(&mut self) {
235 for (_key, handle) in self.qp_handles.drain() {
240 let _ = handle.drain_and_stop("IbvManagerActor dropped");
241 }
242
243 }
247}
248
249impl<I: IbvDeviceImpl> IbvManagerActor<I> {
250 pub async fn new(params: Option<IbvConfig>) -> Result<Self, anyhow::Error> {
252 if !ibverbs_supported() {
253 return Err(anyhow::anyhow!(
254 "Cannot create IbvManagerActor because RDMA is not supported on this machine"
255 ));
256 }
257
258 let mut config = match params {
261 Some(config) => config,
262 None => {
263 let mut config = IbvConfig::default();
264 I::apply_config_defaults(&mut config);
265 config
266 }
267 };
268 tracing::debug!("rdma is enabled, config target: {:?}", config.target);
269
270 if config.use_gpu_direct {
272 match validate_execution_context().await {
273 Ok(_) => {
274 tracing::info!("GPU Direct RDMA execution context validated successfully");
275 }
276 Err(e) => {
277 tracing::warn!(
278 "GPU Direct RDMA execution context validation failed: {}. Downgrading to standard ibverbs mode.",
279 e
280 );
281 config.use_gpu_direct = false;
282 }
283 }
284 }
285
286 let actor = Self {
287 owner: OnceLock::new(),
288 qp_handles: HashMap::new(),
289 peer_created_qps: HashMap::new(),
290 devices: HashMap::new(),
291 config,
292 buffer_registrations: HashMap::new(),
293 };
294
295 Ok(actor)
296 }
297
298 fn get_or_create_device_domain(
301 &mut self,
302 device_name: &str,
303 ) -> Result<&IbvDomain<I::Domain>, anyhow::Error> {
304 if !self.devices.contains_key(device_name) {
305 let device =
306 IbvDevice::<I>::open(device_name, self.config.clone()).ok_or_else(|| {
307 anyhow::anyhow!("{} does not advertise {}", I::backend_name(), device_name,)
308 })?;
309 crate::print_device_info_if_debug_enabled(device.context().as_ptr());
311 self.devices.insert(device_name.to_string(), device);
312 }
313 self.devices
314 .get_mut(device_name)
315 .expect("device just inserted or already present")
316 .get_or_create_domain(DEFAULT_DOMAIN)
317 }
318
319 fn resolve_local_mr(
326 &mut self,
327 mem: &KeepaliveLocalMemory,
328 ) -> Result<IbvMemoryRegionView, anyhow::Error> {
329 if let Some(mrv) = mem.mr_slot().get() {
330 return Ok(mrv.clone());
331 }
332 let addr = mem.addr();
333
334 let device_name = if let Some(target) = &self.config.target {
339 resolve_target::<I>(target)
340 .ok_or_else(|| anyhow::anyhow!("configured device target {:?} not found", target))?
341 .name()
342 .clone()
343 } else {
344 let cuda_nic = if is_device_ptr(addr) {
345 let mut device_ordinal: i32 = -1;
346 let err = unsafe {
350 rdmaxcel_sys::rdmaxcel_cuPointerGetAttribute(
351 &mut device_ordinal as *mut _ as *mut std::ffi::c_void,
352 rdmaxcel_sys::CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL,
353 addr as rdmaxcel_sys::CUdeviceptr,
354 )
355 };
356 let ordinal = (err == rdmaxcel_sys::CUDA_SUCCESS)
357 .then_some(device_ordinal)
358 .ok_or_else(|| {
359 anyhow::anyhow!(
360 "could not get CUDA device ordinal for device memory at 0x{:x}: {}",
361 addr,
362 err
363 )
364 })?;
365 assert!(ordinal >= 0, "CUDA device ordinal must be non-negative");
366 Some(
367 super::device_selection::get_cuda_device_to_ibv_device::<I>()
368 .get(ordinal as usize)
369 .and_then(|d| d.clone())
370 .ok_or_else(|| {
371 anyhow::anyhow!(
372 "no RDMA device found for CUDA device ordinal {}",
373 ordinal
374 )
375 })?,
376 )
377 } else {
378 None
379 };
380 match cuda_nic {
381 Some(info) => info.name().clone(),
382 None => {
383 let devices = select_optimal_ibv_devices::<I>(MemoryLocation::Cpu(None));
389 match devices.len() {
390 0 => anyhow::bail!("no RDMA devices found"),
391 n => {
392 let mut hasher = DefaultHasher::new();
393 (mem.addr(), mem.size()).hash(&mut hasher);
394 devices[(hasher.finish() % n as u64) as usize]
395 .name()
396 .clone()
397 }
398 }
399 }
400 }
401 };
402 tracing::debug!(
403 "Using RDMA device: {} for memory at 0x{:x}",
404 device_name,
405 addr
406 );
407
408 let domain = self.get_or_create_device_domain(&device_name)?;
409 let mrv = domain.register_mr(mem)?;
412 Ok(mem.mr_slot().get_or_init(|| mrv).clone())
413 }
414
415 fn create_peer_qp(
421 &mut self,
422 qp_key: &QpKey,
423 sender_info: &IbvQpInfo,
424 ) -> Result<IbvQpInfo, anyhow::Error> {
425 if self.peer_created_qps.contains_key(qp_key) {
426 anyhow::bail!("peer queue pair already exists for {qp_key:?}");
427 }
428 let self_device = &qp_key.self_device;
429 let config = self.config.clone();
430 let domain = self.get_or_create_device_domain(self_device)?;
431 let mut qp = domain
432 .create_queue_pair(&config)
433 .map_err(|e| anyhow::anyhow!("could not create peer IbvQueuePair: {}", e))?;
434 let local_info = qp
435 .get_qp_info()
436 .map_err(|e| anyhow::anyhow!("could not extract peer QP info: {}", e))?;
437 qp.connect(sender_info)
438 .map_err(|e| anyhow::anyhow!("could not connect peer QP: {}", e))?;
439 self.peer_created_qps.insert(qp_key.clone(), qp);
440 Ok(local_info)
441 }
442
443 fn ensure_qp_actor(
448 &mut self,
449 cx: &Context<'_, Self>,
450 qp_key: &QpKey,
451 peer_manager: ActorRef<Self>,
452 ) -> Result<
453 ActorHandle<QueuePairActor<Self, <I::Domain as IbvDomainImpl>::QueuePair>>,
454 anyhow::Error,
455 > {
456 if let Some(h) = self.qp_handles.get(qp_key) {
457 return Ok(h.clone());
458 }
459 let self_device = &qp_key.self_device;
460 let config = self.config.clone();
461 let domain = self.get_or_create_device_domain(self_device)?;
462 let qp = domain
463 .create_queue_pair(&config)
464 .map_err(|e| anyhow::anyhow!("could not create IbvQueuePair for {qp_key:?}: {}", e))?;
465 let local_manager: ActorRef<Self> = cx.bind();
466 let is_loopback = local_manager.actor_addr() == peer_manager.actor_addr()
467 && qp_key.self_device == qp_key.other_device;
468 let actor = cx.spawn(QueuePairActor::new(
469 qp_key.clone(),
470 local_manager,
471 peer_manager,
472 qp,
473 is_loopback,
474 config.max_send_wr,
475 config.max_rd_atomic as u32,
476 ));
477 self.qp_handles.insert(qp_key.clone(), actor.clone());
478 Ok(actor)
479 }
480}
481
482#[async_trait]
483impl<I: IbvDeviceImpl> IbvManagerMessageHandler for IbvManagerActor<I> {
484 async fn release_buffer(
485 &mut self,
486 _cx: &Context<Self>,
487 remote_buf_id: usize,
488 ) -> Result<(), anyhow::Error> {
489 self.buffer_registrations.remove(&remote_buf_id);
493 Ok(())
494 }
495}
496
497#[async_trait]
501impl<I: IbvDeviceImpl> Handler<IbvManagerMessage> for IbvManagerActor<I> {
502 async fn handle(
503 &mut self,
504 cx: &Context<Self>,
505 message: IbvManagerMessage,
506 ) -> Result<(), anyhow::Error> {
507 <Self as IbvManagerMessageHandler>::handle(self, cx, message).await
508 }
509}
510
511#[async_trait]
512impl<I: IbvDeviceImpl> Handler<SubmitOps<I>> for IbvManagerActor<I> {
513 async fn handle(&mut self, cx: &Context<Self>, msg: SubmitOps<I>) -> Result<(), anyhow::Error> {
514 let SubmitOps { ops, reply } = msg;
515
516 for (i, op) in ops.into_iter().enumerate() {
521 let mrv = match self.resolve_local_mr(&op.local_memory) {
522 Ok(mrv) => mrv,
523 Err(e) => {
524 reply.try_post(
525 cx,
526 OpResult {
527 op_idx: i,
528 result: Err(e.to_string()),
529 },
530 )?;
531 continue;
532 }
533 };
534 let qp_key = QpKey {
535 self_device: mrv.device_name.clone(),
536 other_id: op.remote_manager.actor_addr().id().clone(),
537 other_device: op.remote_buffer.device_name.clone(),
538 };
539 let peer_manager = op.remote_manager.clone();
540 let handle = match self.ensure_qp_actor(cx, &qp_key, peer_manager) {
541 Ok(h) => h,
542 Err(e) => {
543 reply.try_post(
544 cx,
545 OpResult {
546 op_idx: i,
547 result: Err(e.to_string()),
548 },
549 )?;
550 continue;
551 }
552 };
553 handle.try_post(
554 cx,
555 ProcessOps {
556 items: vec![(i, op, mrv)],
557 reply: reply.clone(),
558 },
559 )?;
560 }
561 Ok(())
562 }
563}
564
565#[async_trait]
566impl<I: IbvDeviceImpl> Handler<RawQueuePair> for IbvManagerActor<I> {
567 async fn handle(&mut self, cx: &Context<Self>, msg: RawQueuePair) -> Result<(), anyhow::Error> {
568 let RawQueuePair { self_device, reply } = msg;
569 let config = self.config.clone();
572 let result = self
573 .get_or_create_device_domain(&self_device)
574 .and_then(|domain| legacy::IbvQueuePair::new(domain, config))
575 .map_err(|e| e.to_string());
576 let _ = reply.try_post(cx, result);
577 Ok(())
578 }
579}
580
581#[async_trait]
582impl<I: IbvDeviceImpl> Handler<CreatePeerQueuePair<IbvManagerActor<I>>> for IbvManagerActor<I> {
583 async fn handle(
584 &mut self,
585 cx: &Context<Self>,
586 msg: CreatePeerQueuePair<IbvManagerActor<I>>,
587 ) -> Result<(), anyhow::Error> {
588 let CreatePeerQueuePair {
589 sender,
590 sender_device,
591 receiver_device,
592 sender_info,
593 reply,
594 } = msg;
595 let qp_key = QpKey {
596 self_device: receiver_device,
597 other_id: sender.actor_addr().id().clone(),
598 other_device: sender_device,
599 };
600 match self.create_peer_qp(&qp_key, &sender_info) {
601 Ok(local_info) => reply.post(cx, Ok(local_info)),
602 Err(e) => reply.post(cx, Err(e.to_string())),
603 }
604 Ok(())
605 }
606}
607
608#[async_trait]
609impl<I: IbvDeviceImpl> IbvManagerLocalMessageHandler for IbvManagerActor<I> {
610 async fn register_remote_buffer(
611 &mut self,
612 _cx: &Context<Self>,
613 remote_buf_id: usize,
614 local: KeepaliveLocalMemory,
615 ) -> Result<Result<IbvBuffer, String>, anyhow::Error> {
616 if let Some(mrv) = self.buffer_registrations.get(&remote_buf_id) {
617 return Ok(Ok(IbvBuffer::from(mrv)));
618 }
619 let mrv = match self.resolve_local_mr(&local) {
624 Ok(v) => v,
625 Err(e) => return Ok(Err(e.to_string())),
626 };
627 let buf = IbvBuffer::from(&mrv);
628 self.buffer_registrations.insert(remote_buf_id, mrv);
629 Ok(Ok(buf))
630 }
631}
632
633#[async_trait]
636impl<I: IbvDeviceImpl> Handler<IbvManagerLocalMessage> for IbvManagerActor<I> {
637 async fn handle(
638 &mut self,
639 cx: &Context<Self>,
640 message: IbvManagerLocalMessage,
641 ) -> Result<(), anyhow::Error> {
642 <Self as IbvManagerLocalMessageHandler>::handle(self, cx, message).await
643 }
644}
645
646#[derive(Debug)]
651pub struct IbvBackend<I: IbvDeviceImpl>(pub ActorHandle<IbvManagerActor<I>>);
652
653impl<I: IbvDeviceImpl> Clone for IbvBackend<I> {
654 fn clone(&self) -> Self {
655 Self(self.0.clone())
656 }
657}
658
659impl<I: IbvDeviceImpl> std::ops::Deref for IbvBackend<I> {
660 type Target = ActorHandle<IbvManagerActor<I>>;
661 fn deref(&self) -> &Self::Target {
662 &self.0
663 }
664}
665
666#[derive(Serialize, Deserialize, Named)]
669#[serde(bound = "")]
670pub struct IbvRemoteBackendContext<I: IbvDeviceImpl> {
671 pub manager: ActorRef<IbvManagerActor<I>>,
672 pub buffer: IbvBuffer,
673}
674
675impl<I: IbvDeviceImpl> Clone for IbvRemoteBackendContext<I> {
679 fn clone(&self) -> Self {
680 Self {
681 manager: self.manager.clone(),
682 buffer: self.buffer.clone(),
683 }
684 }
685}
686
687impl<I: IbvDeviceImpl> std::fmt::Debug for IbvRemoteBackendContext<I> {
688 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
689 f.debug_struct("IbvRemoteBackendContext")
690 .field("manager", &self.manager)
691 .field("buffer", &self.buffer)
692 .finish()
693 }
694}
695
696#[async_trait]
697impl<I: IbvDeviceImpl> RdmaBackend for IbvBackend<I>
698where
699 RdmaRemoteBuffer: ResolveRemoteBackendContext<IbvBackend<I>>,
700{
701 type RemoteBackendContext = IbvRemoteBackendContext<I>;
702 type TransportInfo = ();
703
704 fn available() -> bool {
705 if IbvDevice::<I>::available() {
706 if hyperactor_config::global::get(crate::config::RDMA_DISABLE_IBVERBS) {
707 tracing::warn!(
708 "ibverbs ({}) is available, but it was disabled by configuration (RDMA_DISABLE_IBVERBS=true)",
709 I::backend_name()
710 );
711 return false;
712 }
713 return true;
714 }
715 false
716 }
717
718 fn transport_level(&self) -> RdmaTransportLevel {
719 RdmaTransportLevel::Nic
720 }
721
722 fn transport_info(&self) -> Option<Self::TransportInfo> {
723 None
724 }
725
726 async fn spawn(
727 cx: &(impl hyperactor::context::Actor + Send + Sync),
728 config: &RdmaConfig,
729 ) -> Result<Self> {
730 let actor = IbvManagerActor::<I>::new(config.ibv.clone()).await?;
731 Ok(IbvBackend(cx.spawn(actor)))
732 }
733
734 async fn register_remote_buffer(
735 &self,
736 cx: &(impl hyperactor::context::Actor + Send + Sync),
737 remote_buf_id: usize,
738 local: KeepaliveLocalMemory,
739 ) -> Result<IbvRemoteBackendContext<I>> {
740 let buffer = self
741 .0
742 .register_remote_buffer(cx, remote_buf_id, local)
743 .await?
744 .map_err(|e| anyhow::anyhow!(e))?;
745 Ok(IbvRemoteBackendContext {
746 manager: self.0.bind(),
747 buffer,
748 })
749 }
750
751 async fn release_buffer(
752 &self,
753 cx: &(impl hyperactor::context::Actor + Send + Sync),
754 remote_buf_id: usize,
755 ) -> Result<()> {
756 self.0.release_buffer(cx, remote_buf_id).await
757 }
758
759 async fn submit(
772 &self,
773 cx: &(impl hyperactor::context::Actor + Send + Sync),
774 ops: Vec<RdmaOp>,
775 timeout: Duration,
776 ) -> Result<(), anyhow::Error> {
777 let mut ibv_ops = Vec::with_capacity(ops.len());
778 for op in ops {
779 let ctx = <RdmaRemoteBuffer as ResolveRemoteBackendContext<IbvBackend<I>>>::resolve(
780 &op.remote,
781 )
782 .expect("op routed to incompatible backend");
783 ibv_ops.push(IbvOp {
784 op_type: op.op_type,
785 local_memory: op.local.clone(),
786 remote_buffer: ctx.buffer,
787 remote_manager: ctx.manager,
788 });
789 }
790 let n = ibv_ops.len();
791
792 let (reply, mut reply_rx) = cx.mailbox().open_port::<OpResult>();
793
794 self.0.try_post(
795 cx,
796 SubmitOps {
797 ops: ibv_ops,
798 reply,
799 },
800 )?;
801
802 let mut failures: Vec<(usize, String)> = Vec::with_capacity(n);
803 let mut received = 0usize;
804 let mut terminal: Option<String> = None;
805 let deadline = tokio::time::Instant::now() + timeout;
806 while received < n {
807 tokio::select! {
808 () = tokio::time::sleep_until(deadline) => {
809 terminal = Some(format!(
810 "submit timed out after {received}/{n} replies with {} failures",
811 failures.len()
812 ));
813 break;
814 }
815 recv = reply_rx.recv() => {
816 match recv {
817 Ok(OpResult { result: Ok(()), .. }) => received += 1,
818 Ok(OpResult { op_idx, result: Err(e) }) => {
819 received += 1;
820 failures.push((op_idx, e));
821 }
822 Err(e) => {
823 terminal = Some(format!(
824 "SubmitOps reply port closed after {received}/{n} replies with {} failures: {e}",
825 failures.len()
826 ));
827 break;
828 }
829 }
830 }
831 }
832 }
833
834 if terminal.is_none() && failures.is_empty() {
835 return Ok(());
836 }
837
838 failures.sort_by_key(|(idx, _)| *idx);
839 let mut msg = terminal.unwrap_or_else(|| format!("{}/{n} ops failed", failures.len()));
840 if !failures.is_empty() {
841 msg.push(':');
842 for (idx, err) in &failures {
843 write!(msg, "\n op {idx}: {err}").expect("infallible String write");
844 }
845 }
846 Err(anyhow::anyhow!(msg))
847 }
848}
849
850#[cfg(test)]
851mod tests {
852 use std::sync::Arc;
867 use std::sync::atomic::AtomicUsize;
868 use std::sync::atomic::Ordering;
869 use std::time::Duration;
870
871 use async_trait::async_trait;
872 use hyperactor::Actor;
873 use hyperactor::ActorRef;
874 use hyperactor::Context;
875 use hyperactor::Handler;
876 use hyperactor::Instance;
877 use hyperactor::Label;
878 use hyperactor::OncePortRef;
879 use hyperactor::Proc;
880 use hyperactor::RefClient;
881 use hyperactor::RemoteSpawn;
882 use hyperactor::Uid;
883 use hyperactor::actor::ActorError;
884 use hyperactor::channel::ChannelAddr;
885 use hyperactor::channel::ChannelTransport;
886 use hyperactor_config::Flattrs;
887 use serde::Deserialize;
888 use serde::Serialize;
889 use typeuri::Named;
890
891 use crate::IbvConfig;
892 use crate::RdmaManagerActor;
893 use crate::RdmaManagerMessageClient;
894 use crate::RdmaOp;
895 use crate::RdmaOpType;
896 use crate::RdmaRemoteBuffer;
897 use crate::backend::RdmaBackendHandle;
898 use crate::backend::cuda_test_utils::CudaAllocation;
899 use crate::backend::cuda_test_utils::CudaAllocator;
900 use crate::backend::ibverbs::device::list_all_devices;
901 use crate::backend::ibverbs::device_selection::IbvDeviceTarget;
902 use crate::backend::ibverbs::primitives::IbvQpType;
903 use crate::local_memory::KeepaliveLocalMemory;
904
905 #[derive(Debug, Clone, Copy, Serialize, Deserialize, Named)]
911 pub enum BufferDevice {
912 Cpu,
913 Cuda(i32),
914 }
915
916 #[derive(Debug, Clone, Serialize, Deserialize, Named)]
921 pub struct BufferHelperOp {
922 op_type: RdmaOpType,
923 local_buf: RdmaRemoteBuffer,
924 remote_buf: RdmaRemoteBuffer,
925 }
926
927 #[hyperactor::export(handlers = [BufferHelperMessage])]
930 #[hyperactor::spawnable]
931 #[derive(Debug)]
932 pub struct BufferHelperActor {
933 rdma_manager: ActorRef<RdmaManagerActor>,
934 cuda_allocs: Vec<CudaAllocation>,
938 }
939
940 #[async_trait]
941 impl Actor for BufferHelperActor {
942 async fn cleanup(
943 &mut self,
944 _this: &Instance<Self>,
945 _err: Option<&ActorError>,
946 ) -> Result<(), anyhow::Error> {
947 for alloc in self.cuda_allocs.drain(..) {
948 alloc.try_free();
949 }
950 Ok(())
951 }
952 }
953
954 #[async_trait]
955 impl RemoteSpawn for BufferHelperActor {
956 type Params = ActorRef<RdmaManagerActor>;
957
958 async fn new(
959 rdma_manager: ActorRef<RdmaManagerActor>,
960 _env: Flattrs,
961 ) -> Result<Self, anyhow::Error> {
962 Ok(Self {
963 rdma_manager,
964 cuda_allocs: Vec::new(),
965 })
966 }
967 }
968
969 #[derive(Handler, RefClient, Named, Serialize, Deserialize, Debug)]
970 pub enum BufferHelperMessage {
971 Allocate {
975 size: usize,
976 device: BufferDevice,
977 pattern: u8,
978 #[reply]
979 reply: OncePortRef<RdmaRemoteBuffer>,
980 },
981 ReadContents {
986 remote: Box<RdmaRemoteBuffer>,
987 offset: usize,
988 len: usize,
989 #[reply]
990 reply: OncePortRef<Vec<u8>>,
991 },
992 Submit {
997 ops: Vec<BufferHelperOp>,
998 timeout_secs: u64,
999 #[reply]
1000 reply: OncePortRef<Result<(), String>>,
1001 },
1002 }
1003
1004 impl BufferHelperActor {
1005 async fn allocate_impl(
1006 &mut self,
1007 cx: &Context<'_, Self>,
1008 size: usize,
1009 device: BufferDevice,
1010 pattern: u8,
1011 ) -> Result<RdmaRemoteBuffer, anyhow::Error> {
1012 let local = match device {
1013 BufferDevice::Cpu => {
1014 let buf: Box<[u8]> = vec![pattern; size].into_boxed_slice();
1015 KeepaliveLocalMemory::new(Arc::new(buf))
1016 }
1017 BufferDevice::Cuda(device_id) => {
1018 let alloc = CudaAllocator::get().allocate(device_id, size, size);
1019 let local = KeepaliveLocalMemory::new(Arc::new(alloc.clone()));
1020 self.cuda_allocs.push(alloc);
1021 let fill = vec![pattern; size];
1022 unsafe { local.write_at(0, &fill) }?;
1025 local
1026 }
1027 };
1028 let handle = self
1029 .rdma_manager
1030 .downcast_handle(cx)
1031 .ok_or_else(|| anyhow::anyhow!("rdma_manager not local to BufferHelperActor"))?;
1032 handle.request_buffer(cx, local).await
1033 }
1034
1035 async fn read_contents_impl(
1036 &mut self,
1037 cx: &Context<'_, Self>,
1038 remote: RdmaRemoteBuffer,
1039 offset: usize,
1040 len: usize,
1041 ) -> Result<Vec<u8>, anyhow::Error> {
1042 let handle = self
1043 .rdma_manager
1044 .downcast_handle(cx)
1045 .ok_or_else(|| anyhow::anyhow!("rdma_manager not local"))?;
1046 let local = handle
1047 .request_local_memory(cx, remote.id)
1048 .await?
1049 .ok_or_else(|| {
1050 anyhow::anyhow!(
1051 "no local memory registered on this side for remote_buf_id={}",
1052 remote.id,
1053 )
1054 })?;
1055 let mut out = vec![0u8; len];
1056 unsafe { local.read_at(offset, &mut out)? };
1060 Ok(out)
1061 }
1062
1063 async fn submit_impl(
1064 &mut self,
1065 cx: &Context<'_, Self>,
1066 ops: Vec<BufferHelperOp>,
1067 timeout_secs: u64,
1068 ) -> Result<Result<(), String>, anyhow::Error> {
1069 let handle = self
1070 .rdma_manager
1071 .downcast_handle(cx)
1072 .ok_or_else(|| anyhow::anyhow!("rdma_manager not local"))?;
1073 let mut rdma_ops = Vec::with_capacity(ops.len());
1074 for (i, op) in ops.into_iter().enumerate() {
1075 let local = handle
1076 .request_local_memory(cx, op.local_buf.id)
1077 .await?
1078 .ok_or_else(|| {
1079 anyhow::anyhow!(
1080 "op {i}: no local memory registered for remote_buf_id={}",
1081 op.local_buf.id,
1082 )
1083 })?;
1084 rdma_ops.push(RdmaOp {
1085 op_type: op.op_type,
1086 local,
1087 remote: op.remote_buf,
1088 });
1089 }
1090 let nic = RdmaManagerActor::local_handle(cx)
1091 .get_backend_handles(cx)
1092 .await?
1093 .into_iter()
1094 .find(|h| !matches!(h, RdmaBackendHandle::Tcp(_)))
1095 .ok_or_else(|| anyhow::anyhow!("no NIC backend on this proc"))?;
1096 let result = nic
1097 .submit(cx, rdma_ops, Duration::from_secs(timeout_secs))
1098 .await;
1099 Ok(result.map_err(|e| format!("{e}")))
1100 }
1101 }
1102
1103 #[async_trait]
1104 #[hyperactor::handle(BufferHelperMessage)]
1105 impl BufferHelperMessageHandler for BufferHelperActor {
1106 async fn allocate(
1107 &mut self,
1108 cx: &Context<Self>,
1109 size: usize,
1110 device: BufferDevice,
1111 pattern: u8,
1112 ) -> Result<RdmaRemoteBuffer, anyhow::Error> {
1113 self.allocate_impl(cx, size, device, pattern).await
1114 }
1115
1116 async fn read_contents(
1117 &mut self,
1118 cx: &Context<Self>,
1119 remote: Box<RdmaRemoteBuffer>,
1120 offset: usize,
1121 len: usize,
1122 ) -> Result<Vec<u8>, anyhow::Error> {
1123 self.read_contents_impl(cx, *remote, offset, len).await
1124 }
1125
1126 async fn submit(
1127 &mut self,
1128 cx: &Context<Self>,
1129 ops: Vec<BufferHelperOp>,
1130 timeout_secs: u64,
1131 ) -> Result<Result<(), String>, anyhow::Error> {
1132 self.submit_impl(cx, ops, timeout_secs).await
1133 }
1134 }
1135
1136 static COUNTER: AtomicUsize = AtomicUsize::new(0);
1141
1142 struct TestEnv {
1148 client: hyperactor::Client,
1149 proc_a: Proc,
1150 helper_a: ActorRef<BufferHelperActor>,
1151 proc_b: Proc,
1152 helper_b: ActorRef<BufferHelperActor>,
1153 }
1154
1155 impl TestEnv {
1156 async fn new(config_a: IbvConfig, config_b: IbvConfig) -> Result<Self, anyhow::Error> {
1158 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1159 let proc_a = Proc::direct(
1160 ChannelAddr::any(ChannelTransport::Unix),
1161 format!("rdma_side_a_{id}"),
1162 )?;
1163 let helper_a = Self::spawn_side(&proc_a, config_a).await?;
1164 let proc_b = Proc::direct(
1165 ChannelAddr::any(ChannelTransport::Unix),
1166 format!("rdma_side_b_{id}"),
1167 )?;
1168 let helper_b = Self::spawn_side(&proc_b, config_b).await?;
1169 let client = proc_b.client("test_client");
1170 Ok(Self {
1171 client,
1172 proc_a,
1173 helper_a,
1174 proc_b,
1175 helper_b,
1176 })
1177 }
1178
1179 async fn same_config(config: IbvConfig) -> Result<Self, anyhow::Error> {
1181 Self::new(config.clone(), config).await
1182 }
1183
1184 async fn spawn_side(
1187 proc: &Proc,
1188 config: IbvConfig,
1189 ) -> Result<ActorRef<BufferHelperActor>, anyhow::Error> {
1190 let rdma_actor = RdmaManagerActor::new(Some(config), Flattrs::default()).await?;
1191 let rdma_handle =
1193 proc.spawn_with_uid(Uid::singleton(Label::strip("rdma_manager")), rdma_actor)?;
1194 let rdma: ActorRef<RdmaManagerActor> = rdma_handle.bind();
1195 let helper_actor = BufferHelperActor::new(rdma, Flattrs::default()).await?;
1196 let helper_handle = proc.spawn_with_label("helper", helper_actor);
1197 Ok(helper_handle.bind())
1198 }
1199
1200 async fn shutdown(mut self) -> Result<(), anyhow::Error> {
1201 let _ = self
1202 .proc_a
1203 .destroy_and_wait(Duration::from_secs(10), "TestEnv shutdown proc_a")
1204 .await?;
1205 let _ = self
1206 .proc_b
1207 .destroy_and_wait(Duration::from_secs(10), "TestEnv shutdown proc_b")
1208 .await?;
1209 Ok(())
1210 }
1211 }
1212
1213 async fn assert_remote_pattern(
1218 helper: &ActorRef<BufferHelperActor>,
1219 cx: &hyperactor::Client,
1220 remote: RdmaRemoteBuffer,
1221 size: usize,
1222 pattern: u8,
1223 ) -> Result<(), anyhow::Error> {
1224 let got = helper.read_contents(cx, Box::new(remote), 0, size).await?;
1225 assert_eq!(got, vec![pattern; size]);
1226 Ok(())
1227 }
1228
1229 async fn run_cross_actor_write(
1232 env: &TestEnv,
1233 src_dev: BufferDevice,
1234 dst_dev: BufferDevice,
1235 size: usize,
1236 pattern: u8,
1237 timeout_secs: u64,
1238 ) -> Result<(), anyhow::Error> {
1239 let src = env
1240 .helper_a
1241 .allocate(&env.client, size, src_dev, pattern)
1242 .await?;
1243 let dst = env.helper_b.allocate(&env.client, size, dst_dev, 0).await?;
1244 env.helper_a
1245 .submit(
1246 &env.client,
1247 vec![BufferHelperOp {
1248 op_type: RdmaOpType::WriteFromLocal,
1249 local_buf: src,
1250 remote_buf: dst.clone(),
1251 }],
1252 timeout_secs,
1253 )
1254 .await?
1255 .map_err(|e| anyhow::anyhow!(e))?;
1256 assert_remote_pattern(&env.helper_b, &env.client, dst, size, pattern).await
1257 }
1258
1259 async fn run_cross_actor_read(
1262 env: &TestEnv,
1263 dst_dev: BufferDevice,
1264 src_dev: BufferDevice,
1265 size: usize,
1266 pattern: u8,
1267 timeout_secs: u64,
1268 ) -> Result<(), anyhow::Error> {
1269 let dst = env.helper_a.allocate(&env.client, size, dst_dev, 0).await?;
1270 let src = env
1271 .helper_b
1272 .allocate(&env.client, size, src_dev, pattern)
1273 .await?;
1274 env.helper_a
1275 .submit(
1276 &env.client,
1277 vec![BufferHelperOp {
1278 op_type: RdmaOpType::ReadIntoLocal,
1279 local_buf: dst.clone(),
1280 remote_buf: src,
1281 }],
1282 timeout_secs,
1283 )
1284 .await?
1285 .map_err(|e| anyhow::anyhow!(e))?;
1286 assert_remote_pattern(&env.helper_a, &env.client, dst, size, pattern).await
1287 }
1288
1289 async fn run_multi_op_same_qp(
1295 env: &TestEnv,
1296 dev_a: BufferDevice,
1297 dev_b: BufferDevice,
1298 size: usize,
1299 timeout_secs: u64,
1300 ) -> Result<(), anyhow::Error> {
1301 const WRITE_PATTERN: u8 = 0xa1;
1302 const READ_PATTERN: u8 = 0xb2;
1303 let write_src = env
1304 .helper_a
1305 .allocate(&env.client, size, dev_a, WRITE_PATTERN)
1306 .await?;
1307 let write_dst = env.helper_b.allocate(&env.client, size, dev_b, 0).await?;
1308 let read_dst = env.helper_a.allocate(&env.client, size, dev_a, 0).await?;
1309 let read_src = env
1310 .helper_b
1311 .allocate(&env.client, size, dev_b, READ_PATTERN)
1312 .await?;
1313 env.helper_a
1314 .submit(
1315 &env.client,
1316 vec![
1317 BufferHelperOp {
1318 op_type: RdmaOpType::WriteFromLocal,
1319 local_buf: write_src,
1320 remote_buf: write_dst.clone(),
1321 },
1322 BufferHelperOp {
1323 op_type: RdmaOpType::ReadIntoLocal,
1324 local_buf: read_dst.clone(),
1325 remote_buf: read_src,
1326 },
1327 ],
1328 timeout_secs,
1329 )
1330 .await?
1331 .map_err(|e| anyhow::anyhow!(e))?;
1332 assert_remote_pattern(&env.helper_b, &env.client, write_dst, size, WRITE_PATTERN).await?;
1333 assert_remote_pattern(&env.helper_a, &env.client, read_dst, size, READ_PATTERN).await
1334 }
1335
1336 async fn run_true_loopback(
1342 env: &TestEnv,
1343 dev: BufferDevice,
1344 size: usize,
1345 ) -> Result<(), anyhow::Error> {
1346 const PATTERN: u8 = 0x5d;
1347 let src = env
1348 .helper_a
1349 .allocate(&env.client, size, dev, PATTERN)
1350 .await?;
1351 let dst = env.helper_a.allocate(&env.client, size, dev, 0).await?;
1352 env.helper_a
1353 .submit(
1354 &env.client,
1355 vec![BufferHelperOp {
1356 op_type: RdmaOpType::WriteFromLocal,
1357 local_buf: src,
1358 remote_buf: dst.clone(),
1359 }],
1360 5,
1361 )
1362 .await?
1363 .map_err(|e| anyhow::anyhow!(e))?;
1364 assert_remote_pattern(&env.helper_a, &env.client, dst, size, PATTERN).await
1365 }
1366
1367 fn require_rdma() {
1372 if list_all_devices().is_empty() {
1373 panic!("SKIPPED: no RDMA devices available");
1374 }
1375 }
1376
1377 fn require_cuda() {
1378 if !crate::is_cuda_available() {
1379 panic!("SKIPPED: CUDA not available");
1380 }
1381 }
1382
1383 #[timed_test::async_timed_test(timeout_secs = 60)]
1392 async fn test_register_remote_buffer_fills_mr_slot() -> Result<(), anyhow::Error> {
1393 require_rdma();
1394 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
1395 let buf: Box<[u8]> = vec![0u8; 1024].into_boxed_slice();
1396 let local = KeepaliveLocalMemory::new(Arc::new(buf));
1397 assert!(
1398 local.mr_slot().get().is_none(),
1399 "MR slot should be empty before registration",
1400 );
1401 RdmaManagerActor::local_handle(&env.client)
1402 .request_buffer(&env.client, local.clone())
1403 .await?;
1404 assert!(
1405 local.mr_slot().get().is_some(),
1406 "registration should populate the MR slot",
1407 );
1408 env.shutdown().await
1409 }
1410
1411 #[timed_test::async_timed_test(timeout_secs = 60)]
1416 async fn test_cross_actor_same_device_write() -> Result<(), anyhow::Error> {
1417 require_rdma();
1418 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
1419 run_cross_actor_write(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, 0xa5, 5).await?;
1420 env.shutdown().await
1421 }
1422
1423 #[timed_test::async_timed_test(timeout_secs = 60)]
1425 async fn test_cross_actor_same_device_read() -> Result<(), anyhow::Error> {
1426 require_rdma();
1427 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
1428 run_cross_actor_read(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, 0x3c, 5).await?;
1429 env.shutdown().await
1430 }
1431
1432 #[timed_test::async_timed_test(timeout_secs = 60)]
1437 async fn test_loopback_write() -> Result<(), anyhow::Error> {
1438 require_rdma();
1439 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
1440 run_true_loopback(&env, BufferDevice::Cpu, 32).await?;
1441 env.shutdown().await
1442 }
1443
1444 #[timed_test::async_timed_test(timeout_secs = 60)]
1446 async fn test_cross_device_write() -> Result<(), anyhow::Error> {
1447 require_rdma();
1448 let env = TestEnv::new(
1449 IbvConfig::targeting(IbvDeviceTarget::cpu(0)),
1450 IbvConfig::targeting(IbvDeviceTarget::cpu(1)),
1451 )
1452 .await?;
1453 run_cross_actor_write(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, 0x77, 5).await?;
1454 env.shutdown().await
1455 }
1456
1457 #[timed_test::async_timed_test(timeout_secs = 60)]
1459 async fn test_cross_device_read() -> Result<(), anyhow::Error> {
1460 require_rdma();
1461 let env = TestEnv::new(
1462 IbvConfig::targeting(IbvDeviceTarget::cpu(0)),
1463 IbvConfig::targeting(IbvDeviceTarget::cpu(1)),
1464 )
1465 .await?;
1466 run_cross_actor_read(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, 0x88, 5).await?;
1467 env.shutdown().await
1468 }
1469
1470 #[timed_test::async_timed_test(timeout_secs = 60)]
1474 async fn test_multi_op_same_qp_cpu() -> Result<(), anyhow::Error> {
1475 require_rdma();
1476 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
1477 run_multi_op_same_qp(&env, BufferDevice::Cpu, BufferDevice::Cpu, 64, 5).await?;
1478 env.shutdown().await
1479 }
1480
1481 #[timed_test::async_timed_test(timeout_secs = 120)]
1485 async fn test_multi_op_same_qp_cuda() -> Result<(), anyhow::Error> {
1486 require_rdma();
1487 require_cuda();
1488 const SIZE: usize = 2 * 1024 * 1024;
1489 let env = TestEnv::new(
1490 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
1491 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
1492 )
1493 .await?;
1494 run_multi_op_same_qp(&env, BufferDevice::Cuda(0), BufferDevice::Cuda(1), SIZE, 10).await?;
1495 env.shutdown().await
1496 }
1497
1498 #[timed_test::async_timed_test(timeout_secs = 60)]
1500 async fn test_cuda_to_cpu_write() -> Result<(), anyhow::Error> {
1501 require_rdma();
1502 require_cuda();
1503 const SIZE: usize = 2 * 1024 * 1024;
1504 let env = TestEnv::new(
1505 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
1506 IbvConfig::targeting(IbvDeviceTarget::cpu(1)),
1507 )
1508 .await?;
1509 run_cross_actor_write(
1510 &env,
1511 BufferDevice::Cuda(0),
1512 BufferDevice::Cpu,
1513 SIZE,
1514 0x9b,
1515 10,
1516 )
1517 .await?;
1518 env.shutdown().await
1519 }
1520
1521 #[timed_test::async_timed_test(timeout_secs = 60)]
1523 async fn test_cuda_to_cpu_read() -> Result<(), anyhow::Error> {
1524 require_rdma();
1525 require_cuda();
1526 const SIZE: usize = 2 * 1024 * 1024;
1527 let env = TestEnv::new(
1528 IbvConfig::targeting(IbvDeviceTarget::cpu(0)),
1529 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
1530 )
1531 .await?;
1532 run_cross_actor_read(
1533 &env,
1534 BufferDevice::Cpu,
1535 BufferDevice::Cuda(1),
1536 SIZE,
1537 0x37,
1538 10,
1539 )
1540 .await?;
1541 env.shutdown().await
1542 }
1543
1544 #[timed_test::async_timed_test(timeout_secs = 60)]
1546 async fn test_cpu_to_cuda_write() -> Result<(), anyhow::Error> {
1547 require_rdma();
1548 require_cuda();
1549 const SIZE: usize = 2 * 1024 * 1024;
1550 let env = TestEnv::new(
1551 IbvConfig::targeting(IbvDeviceTarget::cpu(0)),
1552 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
1553 )
1554 .await?;
1555 run_cross_actor_write(
1556 &env,
1557 BufferDevice::Cpu,
1558 BufferDevice::Cuda(1),
1559 SIZE,
1560 0x5a,
1561 10,
1562 )
1563 .await?;
1564 env.shutdown().await
1565 }
1566
1567 #[timed_test::async_timed_test(timeout_secs = 60)]
1569 async fn test_cpu_to_cuda_read() -> Result<(), anyhow::Error> {
1570 require_rdma();
1571 require_cuda();
1572 const SIZE: usize = 2 * 1024 * 1024;
1573 let env = TestEnv::new(
1574 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
1575 IbvConfig::targeting(IbvDeviceTarget::cpu(1)),
1576 )
1577 .await?;
1578 run_cross_actor_read(
1579 &env,
1580 BufferDevice::Cuda(0),
1581 BufferDevice::Cpu,
1582 SIZE,
1583 0x4e,
1584 10,
1585 )
1586 .await?;
1587 env.shutdown().await
1588 }
1589
1590 #[timed_test::async_timed_test(timeout_secs = 60)]
1592 async fn test_cuda_to_cuda_write() -> Result<(), anyhow::Error> {
1593 require_rdma();
1594 require_cuda();
1595 const SIZE: usize = 2 * 1024 * 1024;
1596 let env = TestEnv::new(
1597 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
1598 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
1599 )
1600 .await?;
1601 run_cross_actor_write(
1602 &env,
1603 BufferDevice::Cuda(0),
1604 BufferDevice::Cuda(1),
1605 SIZE,
1606 0xee,
1607 10,
1608 )
1609 .await?;
1610 env.shutdown().await
1611 }
1612
1613 #[timed_test::async_timed_test(timeout_secs = 60)]
1615 async fn test_cuda_to_cuda_read() -> Result<(), anyhow::Error> {
1616 require_rdma();
1617 require_cuda();
1618 const SIZE: usize = 2 * 1024 * 1024;
1619 let env = TestEnv::new(
1620 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
1621 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
1622 )
1623 .await?;
1624 run_cross_actor_read(
1625 &env,
1626 BufferDevice::Cuda(0),
1627 BufferDevice::Cuda(1),
1628 SIZE,
1629 0x42,
1630 10,
1631 )
1632 .await?;
1633 env.shutdown().await
1634 }
1635
1636 #[timed_test::async_timed_test(timeout_secs = 60)]
1642 async fn test_standard_qp_cuda_dmabuf_fallback() -> Result<(), anyhow::Error> {
1643 require_rdma();
1644 require_cuda();
1645 const SIZE: usize = 16 * 1024 * 1024;
1646 let mut config_a = IbvConfig::targeting(IbvDeviceTarget::gpu(0));
1647 config_a.qp_type = IbvQpType::Standard;
1648 let mut config_b = IbvConfig::targeting(IbvDeviceTarget::gpu(1));
1649 config_b.qp_type = IbvQpType::Standard;
1650 let env = TestEnv::new(config_a, config_b).await?;
1651 run_cross_actor_write(
1652 &env,
1653 BufferDevice::Cuda(0),
1654 BufferDevice::Cuda(1),
1655 SIZE,
1656 0x33,
1657 10,
1658 )
1659 .await?;
1660 env.shutdown().await
1661 }
1662
1663 #[timed_test::async_timed_test(timeout_secs = 60)]
1668 async fn test_multi_batch_same_qp() -> Result<(), anyhow::Error> {
1669 require_rdma();
1670 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
1671 let src1 = env
1672 .helper_a
1673 .allocate(&env.client, 32, BufferDevice::Cpu, 0xa1)
1674 .await?;
1675 let dst1 = env
1676 .helper_b
1677 .allocate(&env.client, 32, BufferDevice::Cpu, 0)
1678 .await?;
1679 env.helper_a
1680 .submit(
1681 &env.client,
1682 vec![BufferHelperOp {
1683 op_type: RdmaOpType::WriteFromLocal,
1684 local_buf: src1,
1685 remote_buf: dst1.clone(),
1686 }],
1687 5,
1688 )
1689 .await?
1690 .map_err(|e| anyhow::anyhow!(e))?;
1691 assert_remote_pattern(&env.helper_b, &env.client, dst1, 32, 0xa1).await?;
1692
1693 let src2 = env
1694 .helper_a
1695 .allocate(&env.client, 32, BufferDevice::Cpu, 0xb2)
1696 .await?;
1697 let dst2 = env
1698 .helper_b
1699 .allocate(&env.client, 32, BufferDevice::Cpu, 0)
1700 .await?;
1701 env.helper_a
1702 .submit(
1703 &env.client,
1704 vec![BufferHelperOp {
1705 op_type: RdmaOpType::WriteFromLocal,
1706 local_buf: src2,
1707 remote_buf: dst2.clone(),
1708 }],
1709 5,
1710 )
1711 .await?
1712 .map_err(|e| anyhow::anyhow!(e))?;
1713 assert_remote_pattern(&env.helper_b, &env.client, dst2, 32, 0xb2).await?;
1714 env.shutdown().await
1715 }
1716
1717 #[timed_test::async_timed_test(timeout_secs = 120)]
1722 async fn test_multi_op_multi_qp() -> Result<(), anyhow::Error> {
1723 require_rdma();
1724 require_cuda();
1725 const SIZE: usize = 2 * 1024 * 1024;
1726
1727 let env = TestEnv::same_config(IbvConfig::default()).await?;
1728
1729 const LOOPBACK_PAT: u8 = 0x11;
1730 let lb_src = env
1731 .helper_a
1732 .allocate(&env.client, SIZE, BufferDevice::Cpu, LOOPBACK_PAT)
1733 .await?;
1734 let lb_dst = env
1735 .helper_a
1736 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
1737 .await?;
1738
1739 const CC_PAT: u8 = 0x22;
1740 let cc_src = env
1741 .helper_a
1742 .allocate(&env.client, SIZE, BufferDevice::Cpu, CC_PAT)
1743 .await?;
1744 let cc_dst = env
1745 .helper_b
1746 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
1747 .await?;
1748
1749 const CG_PAT: u8 = 0x33;
1750 let cg_src = env
1751 .helper_a
1752 .allocate(&env.client, SIZE, BufferDevice::Cpu, CG_PAT)
1753 .await?;
1754 let cg_dst = env
1755 .helper_b
1756 .allocate(&env.client, SIZE, BufferDevice::Cuda(1), 0)
1757 .await?;
1758
1759 const GC_PAT: u8 = 0x44;
1760 let gc_src = env
1761 .helper_a
1762 .allocate(&env.client, SIZE, BufferDevice::Cuda(0), GC_PAT)
1763 .await?;
1764 let gc_dst = env
1765 .helper_b
1766 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
1767 .await?;
1768
1769 const GG_PAT: u8 = 0x55;
1770 let gg_src = env
1771 .helper_b
1772 .allocate(&env.client, SIZE, BufferDevice::Cuda(1), GG_PAT)
1773 .await?;
1774 let gg_dst = env
1775 .helper_a
1776 .allocate(&env.client, SIZE, BufferDevice::Cuda(0), 0)
1777 .await?;
1778
1779 env.helper_a
1780 .submit(
1781 &env.client,
1782 vec![
1783 BufferHelperOp {
1784 op_type: RdmaOpType::WriteFromLocal,
1785 local_buf: lb_src,
1786 remote_buf: lb_dst.clone(),
1787 },
1788 BufferHelperOp {
1789 op_type: RdmaOpType::WriteFromLocal,
1790 local_buf: cc_src,
1791 remote_buf: cc_dst.clone(),
1792 },
1793 BufferHelperOp {
1794 op_type: RdmaOpType::WriteFromLocal,
1795 local_buf: cg_src,
1796 remote_buf: cg_dst.clone(),
1797 },
1798 BufferHelperOp {
1799 op_type: RdmaOpType::WriteFromLocal,
1800 local_buf: gc_src,
1801 remote_buf: gc_dst.clone(),
1802 },
1803 BufferHelperOp {
1804 op_type: RdmaOpType::ReadIntoLocal,
1805 local_buf: gg_dst.clone(),
1806 remote_buf: gg_src,
1807 },
1808 ],
1809 30,
1810 )
1811 .await?
1812 .map_err(|e| anyhow::anyhow!(e))?;
1813
1814 assert_remote_pattern(&env.helper_a, &env.client, lb_dst, SIZE, LOOPBACK_PAT).await?;
1815 assert_remote_pattern(&env.helper_b, &env.client, cc_dst, SIZE, CC_PAT).await?;
1816 assert_remote_pattern(&env.helper_b, &env.client, cg_dst, SIZE, CG_PAT).await?;
1817 assert_remote_pattern(&env.helper_b, &env.client, gc_dst, SIZE, GC_PAT).await?;
1818 assert_remote_pattern(&env.helper_a, &env.client, gg_dst, SIZE, GG_PAT).await?;
1819
1820 env.shutdown().await
1821 }
1822
1823 #[timed_test::async_timed_test(timeout_secs = 60)]
1827 async fn test_submit_timeout() -> Result<(), anyhow::Error> {
1828 require_rdma();
1829 const SIZE: usize = 1024 * 1024;
1830 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
1831 let src = env
1832 .helper_a
1833 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0x77)
1834 .await?;
1835 let dst = env
1836 .helper_b
1837 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
1838 .await?;
1839 let result = env
1840 .helper_a
1841 .submit(
1842 &env.client,
1843 vec![BufferHelperOp {
1844 op_type: RdmaOpType::WriteFromLocal,
1845 local_buf: src,
1846 remote_buf: dst,
1847 }],
1848 0,
1849 )
1850 .await?;
1851 let err = result.expect_err("expected submit to time out");
1852 assert!(
1853 err.contains("submit timed out"),
1854 "unexpected error message: {err}",
1855 );
1856 env.shutdown().await
1857 }
1858
1859 #[timed_test::async_timed_test(timeout_secs = 60)]
1869 async fn test_partial_failure_batch() -> Result<(), anyhow::Error> {
1870 require_rdma();
1871 const SIZE: usize = 32;
1872 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
1873
1874 const GOOD_PAT: u8 = 0xc3;
1875 const POST_FLUSH_PAT: u8 = 0xde;
1876
1877 let good_src_0 = env
1878 .helper_a
1879 .allocate(&env.client, SIZE, BufferDevice::Cpu, GOOD_PAT)
1880 .await?;
1881 let good_dst_0 = env
1882 .helper_b
1883 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
1884 .await?;
1885
1886 let bogus_src = env
1887 .helper_a
1888 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0xee)
1889 .await?;
1890 let real_remote = env
1891 .helper_b
1892 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
1893 .await?;
1894 let mut bogus_remote = real_remote.clone();
1895 let bufs = [
1896 bogus_remote
1897 .backends
1898 .mlx
1899 .as_mut()
1900 .map(|ctx| &mut ctx.buffer),
1901 bogus_remote
1902 .backends
1903 .efa
1904 .as_mut()
1905 .map(|ctx| &mut ctx.buffer),
1906 ];
1907 for buf in bufs.into_iter().flatten() {
1908 buf.rkey = 0xdead_beef;
1909 buf.addr = 0xdead_0000;
1910 }
1911
1912 let post_flush_src = env
1913 .helper_a
1914 .allocate(&env.client, SIZE, BufferDevice::Cpu, POST_FLUSH_PAT)
1915 .await?;
1916 let post_flush_dst = env
1917 .helper_b
1918 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
1919 .await?;
1920
1921 let result = env
1922 .helper_a
1923 .submit(
1924 &env.client,
1925 vec![
1926 BufferHelperOp {
1927 op_type: RdmaOpType::WriteFromLocal,
1928 local_buf: good_src_0,
1929 remote_buf: good_dst_0.clone(),
1930 },
1931 BufferHelperOp {
1932 op_type: RdmaOpType::WriteFromLocal,
1933 local_buf: bogus_src,
1934 remote_buf: bogus_remote,
1935 },
1936 BufferHelperOp {
1937 op_type: RdmaOpType::WriteFromLocal,
1938 local_buf: post_flush_src,
1939 remote_buf: post_flush_dst.clone(),
1940 },
1941 ],
1942 10,
1943 )
1944 .await?;
1945 let err = result.expect_err("expected at least one op to fail");
1946 let rem_access = format!(
1947 "status={:?}",
1948 rdmaxcel_sys::ibv_wc_status::IBV_WC_REM_ACCESS_ERR,
1949 );
1950 let wr_flush = format!(
1951 "status={:?}",
1952 rdmaxcel_sys::ibv_wc_status::IBV_WC_WR_FLUSH_ERR,
1953 );
1954 assert!(
1955 !err.contains("op 0:"),
1956 "op 0 should not appear in error: {err}",
1957 );
1958 let op1 = err
1959 .split("\n ")
1960 .find(|line| line.starts_with("op 1:"))
1961 .unwrap_or_else(|| panic!("expected op 1 line in error: {err}"));
1962 assert!(
1963 op1.contains("completion failed") && op1.contains(&rem_access),
1964 "expected op 1 to fail with REM_ACCESS_ERR: {op1}",
1965 );
1966 let op2 = err
1967 .split("\n ")
1968 .find(|line| line.starts_with("op 2:"))
1969 .unwrap_or_else(|| panic!("expected op 2 line in error: {err}"));
1970 assert!(
1971 op2.contains("completion failed") && op2.contains(&wr_flush),
1972 "expected op 2 to be flushed with WR_FLUSH_ERR: {op2}",
1973 );
1974
1975 assert_remote_pattern(&env.helper_b, &env.client, good_dst_0, SIZE, GOOD_PAT).await?;
1976 assert_remote_pattern(&env.helper_b, &env.client, post_flush_dst, SIZE, 0).await?;
1978
1979 env.shutdown().await
1980 }
1981}