1use std::collections::HashMap;
20use std::fmt::Write as _;
21use std::num::NonZeroUsize;
22use std::sync::Arc;
23use std::sync::OnceLock;
24use std::time::Duration;
25
26use anyhow::Result;
27use async_trait::async_trait;
28use dashmap::DashMap;
29use hyperactor::Actor;
30use hyperactor::ActorHandle;
31use hyperactor::ActorId;
32use hyperactor::ActorRef;
33use hyperactor::Context;
34use hyperactor::Endpoint as _;
35use hyperactor::HandleClient;
36use hyperactor::Handler;
37use hyperactor::Instance;
38use hyperactor::OncePortHandle;
39use hyperactor::OncePortRef;
40use hyperactor::actor::Referable;
41use rand::seq::SliceRandom;
42use serde::Deserialize;
43use serde::Serialize;
44use tokio::sync::mpsc;
45use typeuri::Named;
46
47use super::IbvOp;
48use super::cq_actor::CompletionQueueActor;
49use super::device::IbvDevice;
50use super::device::IbvDeviceImpl;
51use super::device_selection::PeerDeviceAffinityPolicy;
52use super::device_selection::configured_peer_device_affinity;
53use super::device_selection::resolve_target;
54use super::device_selection::select_optimal_ibv_devices;
55use super::domain::IbvDomain;
56use super::domain::IbvDomainImpl;
57use super::efa_device::EfaDevice;
58use super::memory_region::IbvMemoryRegionView;
59use super::memory_region::IbvRemoteMemoryRegionView;
60use super::mlx_device::MlxDevice;
61use super::primitives::IbvConfig;
62use super::primitives::IbvCq;
63use super::primitives::IbvQpInfo;
64use super::primitives::ibverbs_supported;
65use super::queue_pair::IbvQueuePair;
66use super::queue_pair::ProcessOps;
67use super::queue_pair::QpKey;
68use super::queue_pair::QueuePairActor;
69use super::queue_pair::QueuePairHandle;
70use super::queue_pair::QueuePairOp;
71use super::queue_pair::StripeId;
72use super::queue_pair::StripeResult;
73use super::queue_pair::legacy;
74use crate::RdmaOp;
75use crate::RdmaOpType;
76use crate::RdmaTransportLevel;
77use crate::backend::RdmaBackend;
78use crate::backend::RdmaConfig;
79use crate::backend::ResolveRemoteBackendContext;
80use crate::local_memory::KeepaliveLocalMemory;
81use crate::rdma_components::RdmaRemoteBuffer;
82use crate::rdma_manager_actor::RdmaManagerActor;
83use crate::validate_execution_context;
84
85const KIB: usize = 1024;
86const STRIPE_ALIGNMENT: usize = 64;
89
90fn configured_min_stripe_size() -> NonZeroUsize {
91 let kilobytes = hyperactor_config::global::get(crate::config::RDMA_MIN_STRIPE_SIZE_KB).get();
92 NonZeroUsize::new(
93 kilobytes
94 .checked_mul(KIB)
95 .expect("RDMA_MIN_STRIPE_SIZE_KB fits in bytes"),
96 )
97 .expect("a non-zero KiB count produces a non-zero byte count")
98}
99
100#[derive(Debug, Serialize, Deserialize, Named)]
105#[serde(bound(serialize = "", deserialize = ""))]
106pub(super) struct CreatePeerQueuePair<M: Referable> {
107 pub(super) sender: ActorRef<M>,
109 pub(super) sender_device: String,
111 pub(super) receiver_device: String,
113 pub(super) qp_index: usize,
115 pub(super) sender_info: IbvQpInfo,
117 pub(super) reply: OncePortRef<Result<IbvQpInfo, String>>,
119}
120wirevalue::register_type!(CreatePeerQueuePair<IbvManagerActor<MlxDevice>>);
121wirevalue::register_type!(CreatePeerQueuePair<IbvManagerActor<EfaDevice>>);
122
123pub(super) struct SubmitOps<I: IbvDeviceImpl> {
133 ops: Vec<ManagerOp<I>>,
134 reply: mpsc::UnboundedSender<StripeResult>,
135}
136
137enum ManagerOp<I: IbvDeviceImpl> {
138 NeedsMemReg {
139 op_idx: usize,
140 op: IbvOp<IbvManagerActor<I>>,
141 },
142 NeedsQueuePair {
143 peer_manager: ActorRef<IbvManagerActor<I>>,
144 ops: Vec<QueuePairOp>,
145 },
146}
147
148#[derive(Clone, Debug, Eq, Hash, PartialEq)]
149struct QpRoute {
150 self_device: String,
151 other_id: ActorId,
152 other_device: String,
153}
154
155#[derive(Debug)]
164struct QueuePairRouter {
165 peer_device_affinity: PeerDeviceAffinityPolicy,
166 qps_per_peer: NonZeroUsize,
167 next_qp_indices: DashMap<QpRoute, usize>,
168 queue_pairs: DashMap<QpKey, mpsc::UnboundedSender<ProcessOps>>,
169}
170
171impl QueuePairRouter {
172 fn new(peer_device_affinity: PeerDeviceAffinityPolicy, qps_per_peer: NonZeroUsize) -> Self {
173 Self {
174 peer_device_affinity,
175 qps_per_peer,
176 next_qp_indices: DashMap::new(),
177 queue_pairs: DashMap::new(),
178 }
179 }
180
181 fn next_qp_index(&self, route: QpRoute) -> usize {
182 let mut next = self.next_qp_indices.entry(route).or_insert(0);
183 let current = *next;
184 *next = (current + 1) % self.qps_per_peer.get();
185 current
186 }
187
188 fn pair_registrations<'a>(
190 &self,
191 local: &'a [IbvMemoryRegionView],
192 remote: &'a [IbvRemoteMemoryRegionView],
193 ) -> Result<Vec<(&'a IbvMemoryRegionView, &'a IbvRemoteMemoryRegionView)>, anyhow::Error> {
194 let mut local: Vec<&IbvMemoryRegionView> = local.iter().collect();
198 local.sort_by(|a, b| a.device_name.cmp(&b.device_name));
199 let mut remote: Vec<&IbvRemoteMemoryRegionView> = remote.iter().collect();
200 remote.sort_by(|a, b| a.device_name.cmp(&b.device_name));
201
202 let local_names: Vec<String> = local.iter().map(|mr| mr.device_name.clone()).collect();
203 let remote_names: Vec<String> = remote.iter().map(|mr| mr.device_name.clone()).collect();
204 let pairs = self
205 .peer_device_affinity
206 .pairs(&local_names, &remote_names)
207 .into_iter()
208 .enumerate()
209 .filter_map(|(i, peer)| peer.map(|j| (local[i], remote[j])))
210 .collect::<Vec<_>>();
211 anyhow::ensure!(
212 !pairs.is_empty(),
213 "no NIC of {local_names:?} pairs with a peer NIC of {remote_names:?} under {:?}",
214 self.peer_device_affinity,
215 );
216 Ok(pairs)
217 }
218
219 fn plan_ops<I: IbvDeviceImpl>(
229 &self,
230 op_idx: usize,
231 op: &IbvOp<IbvManagerActor<I>>,
232 ) -> Result<Vec<QueuePairOp>, anyhow::Error> {
233 let local_mrs = op.local_memory.registered_mrs::<I>();
234 let mut pairs = self.pair_registrations(&local_mrs, &op.remote_buffers)?;
235 let other_id = op.remote_manager.actor_addr().id().clone();
236
237 let size = match op.op_type {
238 RdmaOpType::WriteFromLocal => op.local_memory.size(),
239 RdmaOpType::ReadIntoLocal => op
240 .remote_buffers
241 .first()
242 .map(|remote| remote.size)
243 .unwrap_or(0),
244 };
245 let max_stripes = (size / configured_min_stripe_size().get()).max(1);
246 let stripe_count = if max_stripes < pairs.len() {
247 pairs.shuffle(&mut rand::rng());
248 max_stripes
249 } else {
250 let wanted_per_pair = self.qps_per_peer.get().div_ceil(pairs.len());
253 let allowed_per_pair = max_stripes / pairs.len();
254 pairs.len() * wanted_per_pair.min(allowed_per_pair)
255 };
256
257 let stripe_size = size / stripe_count / STRIPE_ALIGNMENT * STRIPE_ALIGNMENT;
258 let mut offset = 0;
259 (0..stripe_count)
260 .map(|stripe_idx| {
261 let (local, remote) = pairs[stripe_idx % pairs.len()];
262 let size = if stripe_idx + 1 == stripe_count {
263 size - offset
264 } else {
265 stripe_size
266 };
267 let route = QpRoute {
268 self_device: local.device_name.clone(),
269 other_id: other_id.clone(),
270 other_device: remote.device_name.clone(),
271 };
272 let op = QueuePairOp {
273 stripe_id: StripeId {
274 op_idx,
275 stripe_idx,
276 stripe_count,
277 },
278 qp_index: self.next_qp_index(route),
279 op_type: op.op_type,
280 local_memory: op.local_memory.clone(),
281 local: local.try_slice(offset, size)?,
282 remote: remote.try_slice(offset, size)?,
283 };
284 offset += size;
285 Ok(op)
286 })
287 .collect()
288 }
289
290 fn try_dispatch<I: IbvDeviceImpl>(
291 &self,
292 op_idx: usize,
293 op: IbvOp<IbvManagerActor<I>>,
294 reply: &mpsc::UnboundedSender<StripeResult>,
295 ) -> Option<ManagerOp<I>> {
296 if op.local_memory.registered_mrs::<I>().is_empty() {
297 return Some(ManagerOp::NeedsMemReg { op_idx, op });
298 }
299 let planned = match self.plan_ops(op_idx, &op) {
300 Ok(planned) => planned,
301 Err(error) => {
302 let _ = reply.send(StripeResult {
303 stripe_id: StripeId {
304 op_idx,
305 stripe_idx: 0,
306 stripe_count: 1,
307 },
308 result: Err(error.to_string()),
309 });
310 return None;
311 }
312 };
313 let senders = planned
314 .iter()
315 .map(|planned| {
316 let key = QpKey {
317 self_device: planned.local.device_name.clone(),
318 other_id: op.remote_manager.actor_addr().id().clone(),
319 other_device: planned.remote.device_name.clone(),
320 qp_index: planned.qp_index,
321 };
322 self.queue_pairs
323 .get(&key)
324 .map(|sender| sender.value().clone())
325 })
326 .collect::<Option<Vec<_>>>();
327 let Some(senders) = senders else {
328 return Some(ManagerOp::NeedsQueuePair {
329 peer_manager: op.remote_manager,
330 ops: planned,
331 });
332 };
333
334 for (sender, planned) in senders.into_iter().zip(planned) {
335 let stripe_id = planned.stripe_id;
336 if sender
337 .send(ProcessOps {
338 items: vec![planned],
339 reply: reply.clone(),
340 })
341 .is_err()
342 {
343 let _ = reply.send(StripeResult {
344 stripe_id,
345 result: Err("queue pair worker stopped".to_owned()),
346 });
347 }
348 }
349 None
350 }
351}
352
353pub struct RawQueuePair {
362 pub self_device: String,
363 pub reply: OncePortHandle<Result<legacy::IbvQueuePair, String>>,
364}
365
366#[derive(Handler, HandleClient, Debug)]
368pub enum IbvManagerLocalMessage {
369 RegisterRemoteBuffer {
377 local: KeepaliveLocalMemory,
378 #[reply]
379 reply: OncePortHandle<Result<Vec<IbvRemoteMemoryRegionView>, String>>,
380 },
381}
382
383const DEFAULT_DOMAIN: &str = "default";
386
387#[derive(Debug)]
396#[hyperactor::export(
397 handlers = [
398 CreatePeerQueuePair<IbvManagerActor<I>>,
399 ],
400)]
401pub struct IbvManagerActor<I: IbvDeviceImpl> {
402 owner: OnceLock<ActorHandle<RdmaManagerActor>>,
403
404 qp_handles: HashMap<
409 QpKey,
410 QueuePairHandle<IbvManagerActor<I>, <I::Domain as IbvDomainImpl>::QueuePair>,
411 >,
412
413 peer_created_qps: HashMap<QpKey, <I::Domain as IbvDomainImpl>::QueuePair>,
419
420 devices: HashMap<String, IbvDevice<I>>,
424
425 peer_device_affinity: PeerDeviceAffinityPolicy,
429
430 queue_pair_router: Arc<QueuePairRouter>,
433
434 cq_pollers: HashMap<String, ActorHandle<CompletionQueueActor<IbvCq>>>,
437
438 config: IbvConfig,
439}
440
441#[async_trait]
442impl<I: IbvDeviceImpl> Actor for IbvManagerActor<I> {
443 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
444 this.set_system();
445 let owner = if let Some(owner) = this.parent_handle() {
446 owner
447 } else {
448 anyhow::bail!("RdmaManagerActor not found as parent of IbvManagerActor");
449 };
450 self.owner
451 .set(owner)
452 .expect("owner should only be set once during init");
453 Ok(())
454 }
455
456 fn spawn_server_task<F>(future: F) -> tokio::task::JoinHandle<F::Output>
460 where
461 F: std::future::Future + Send + 'static,
462 F::Output: Send + 'static,
463 {
464 crate::rdma_runtime::spawn_on_rdma_runtime(future)
465 }
466}
467
468impl<I: IbvDeviceImpl> Drop for IbvManagerActor<I> {
469 fn drop(&mut self) {
470 self.queue_pair_router.queue_pairs.clear();
471
472 for (_key, handle) in self.qp_handles.drain() {
475 let _ = handle.drain_and_stop("IbvManagerActor dropped");
476 }
477
478 for (_key, poller) in self.cq_pollers.drain() {
479 let _ = poller.drain_and_stop("IbvManagerActor dropped");
480 }
481
482 }
485}
486
487impl<I: IbvDeviceImpl> IbvManagerActor<I> {
488 pub async fn new(params: Option<IbvConfig>) -> Result<Self, anyhow::Error> {
490 if !ibverbs_supported() {
491 return Err(anyhow::anyhow!(
492 "Cannot create IbvManagerActor because RDMA is not supported on this machine"
493 ));
494 }
495
496 let mut config = match params {
499 Some(config) => config,
500 None => {
501 let mut config = IbvConfig::default();
502 I::apply_config_defaults(&mut config);
503 config
504 }
505 };
506
507 if config.target.is_none() {
510 config.target = super::device_selection::configured_ibverbs_target()?;
511 }
512 tracing::debug!("rdma is enabled, config target: {:?}", config.target);
513
514 if config.use_gpu_direct {
516 match validate_execution_context().await {
517 Ok(_) => {
518 tracing::info!("GPU Direct RDMA execution context validated successfully");
519 }
520 Err(e) => {
521 tracing::warn!(
522 "GPU Direct RDMA execution context validation failed: {}. Downgrading to standard ibverbs mode.",
523 e
524 );
525 config.use_gpu_direct = false;
526 }
527 }
528 }
529
530 let peer_device_affinity = configured_peer_device_affinity()?;
531 let actor = Self {
532 owner: OnceLock::new(),
533 qp_handles: HashMap::new(),
534 peer_created_qps: HashMap::new(),
535 devices: HashMap::new(),
536 peer_device_affinity: peer_device_affinity.clone(),
537 queue_pair_router: Arc::new(QueuePairRouter::new(
538 peer_device_affinity,
539 hyperactor_config::global::get(crate::config::RDMA_QPS_PER_PEER).into_std(),
540 )),
541 cq_pollers: HashMap::new(),
542 config,
543 };
544
545 Ok(actor)
546 }
547
548 fn queue_pair_router(&self) -> Arc<QueuePairRouter> {
549 Arc::clone(&self.queue_pair_router)
550 }
551
552 fn get_or_create_device_domain(
555 &mut self,
556 device_name: &str,
557 ) -> Result<&IbvDomain<I::Domain>, anyhow::Error> {
558 self.get_or_create_device(device_name)?
559 .get_or_create_domain(DEFAULT_DOMAIN)
560 }
561
562 fn get_or_create_device(
564 &mut self,
565 device_name: &str,
566 ) -> Result<&mut IbvDevice<I>, anyhow::Error> {
567 if !self.devices.contains_key(device_name) {
568 let device = IbvDevice::<I>::try_open(device_name, self.config.clone())?;
569 crate::print_device_info_if_debug_enabled(device.context().as_ptr());
571 self.devices.insert(device_name.to_string(), device);
572 }
573 Ok(self
574 .devices
575 .get_mut(device_name)
576 .expect("device just inserted or already present"))
577 }
578
579 fn resolve_local_mrs(
590 &mut self,
591 mem: &KeepaliveLocalMemory,
592 ) -> Result<Vec<IbvMemoryRegionView>, anyhow::Error> {
593 let already_serving = mem.registered_mrs::<I>();
594 if !already_serving.is_empty() {
595 return Ok(already_serving);
596 }
597
598 let device_names = match &self.config.target {
599 Some(target) => vec![
600 resolve_target::<I>(target)?
601 .ok_or_else(|| {
602 anyhow::anyhow!("configured device target {:?} not found", target)
603 })?
604 .name()
605 .clone(),
606 ],
607 None => self.pick_optimal_devices(mem)?,
608 };
609
610 let mut views = Vec::with_capacity(device_names.len());
611 let mut failure: Option<anyhow::Error> = None;
612 for device_name in &device_names {
613 match self.resolve_local_mr_on(mem, device_name) {
614 Ok(view) => views.push(view),
615 Err(error) => {
616 tracing::warn!(
617 "not serving [{:#x}, {:#x}) from {device_name}: {error:#}",
618 mem.addr(),
619 mem.addr() + mem.size(),
620 );
621 failure.get_or_insert(error);
622 }
623 }
624 }
625 if views.is_empty() {
626 return Err(failure.expect("`device_names` is never empty, so one of them failed"));
627 }
628 Ok(views)
629 }
630
631 fn resolve_local_mr_on(
641 &mut self,
642 mem: &KeepaliveLocalMemory,
643 device_name: &str,
644 ) -> Result<IbvMemoryRegionView, anyhow::Error> {
645 if let Some(mrv) = mem.registered_mr::<I>(device_name)? {
646 return Ok(mrv);
647 }
648 tracing::debug!(
649 "Using RDMA device: {} for memory at 0x{:x}",
650 device_name,
651 mem.addr()
652 );
653
654 let registered = self
657 .get_or_create_device_domain(device_name)
658 .and_then(|domain| domain.register_mr(mem));
659 match registered {
660 Ok(mrv) => mem.install_mr::<I>(mrv),
661 Err(error) => {
662 mem.record_mr_failure::<I>(device_name, &error);
663 Err(error)
664 }
665 }
666 }
667
668 fn pick_optimal_devices(
673 &self,
674 mem: &KeepaliveLocalMemory,
675 ) -> Result<Vec<String>, anyhow::Error> {
676 let location = mem.location();
677 let devices = select_optimal_ibv_devices::<I>(location)?;
678 anyhow::ensure!(
679 !devices.is_empty(),
680 "no {} RDMA device has a path to {location:?}",
681 I::backend_name(),
682 );
683 let names: Vec<String> = devices.iter().map(|device| device.name().clone()).collect();
684 let max = hyperactor_config::global::get(crate::config::RDMA_MAX_NICS_PER_BUFFER);
685 Ok(self
689 .peer_device_affinity
690 .choose(&names, max.map(hyperactor_config::NonZeroUsize::into_std)))
691 }
692
693 fn create_peer_qp(
699 &mut self,
700 qp_key: &QpKey,
701 sender_info: &IbvQpInfo,
702 ) -> Result<IbvQpInfo, anyhow::Error> {
703 if self.peer_created_qps.contains_key(qp_key) {
704 anyhow::bail!("peer queue pair already exists for {qp_key:?}");
705 }
706 let self_device = qp_key.self_device.clone();
707 let config = self.config.clone();
708 let mut qp = self
709 .get_or_create_device(&self_device)?
710 .create_non_posting_queue_pair(DEFAULT_DOMAIN, &config)
711 .map_err(|e| anyhow::anyhow!("could not create peer IbvQueuePair: {}", e))?;
712 let local_info = qp
713 .get_qp_info()
714 .map_err(|e| anyhow::anyhow!("could not extract peer QP info: {}", e))?;
715 qp.connect(sender_info)
716 .map_err(|e| anyhow::anyhow!("could not connect peer QP: {}", e))?;
717 self.peer_created_qps.insert(qp_key.clone(), qp);
718 Ok(local_info)
719 }
720
721 fn ensure_cq_poller(
723 &mut self,
724 cx: &Context<'_, Self>,
725 device: &str,
726 ) -> ActorHandle<CompletionQueueActor<IbvCq>> {
727 let key = if hyperactor_config::global::get(crate::config::RDMA_CQ_POLLER_PER_DEVICE) {
728 device.to_owned()
729 } else {
730 String::new()
731 };
732 self.cq_pollers
733 .entry(key)
734 .or_insert_with(|| cx.spawn(CompletionQueueActor::new()))
735 .clone()
736 }
737
738 fn ensure_qp_actor(
743 &mut self,
744 cx: &Context<'_, Self>,
745 qp_key: &QpKey,
746 peer_manager: ActorRef<Self>,
747 ) -> Result<QueuePairHandle<Self, <I::Domain as IbvDomainImpl>::QueuePair>, anyhow::Error> {
748 if let Some(h) = self.qp_handles.get(qp_key) {
749 return Ok(h.clone());
750 }
751 let self_device = qp_key.self_device.clone();
752 let config = self.config.clone();
753 let cq_poller = self.ensure_cq_poller(cx, &self_device);
754 let (qp, cq_lease) = self
755 .get_or_create_device(&self_device)?
756 .create_queue_pair(DEFAULT_DOMAIN, &config)
757 .map_err(|e| anyhow::anyhow!("could not create IbvQueuePair for {qp_key:?}: {}", e))?;
758 let local_manager: ActorRef<Self> = cx.bind();
759 let is_loopback = local_manager.actor_addr() == peer_manager.actor_addr()
760 && qp_key.self_device == qp_key.other_device;
761 let (actor, sender) = QueuePairActor::new(
762 qp_key.clone(),
763 local_manager,
764 peer_manager,
765 qp,
766 cq_poller,
767 cq_lease,
768 is_loopback,
769 config.max_send_wr,
770 );
771 let handle = QueuePairHandle::new(cx.spawn(actor), sender);
772 self.queue_pair_router
773 .queue_pairs
774 .insert(qp_key.clone(), handle.sender());
775 self.qp_handles.insert(qp_key.clone(), handle.clone());
776 Ok(handle)
777 }
778
779 fn dispatch_ops(
780 &mut self,
781 cx: &Context<'_, Self>,
782 peer_manager: ActorRef<Self>,
783 ops: Vec<QueuePairOp>,
784 reply: &mpsc::UnboundedSender<StripeResult>,
785 ) {
786 for op in ops {
787 let stripe_id = op.stripe_id;
788 let qp_key = QpKey {
789 self_device: op.local.device_name.clone(),
790 other_id: peer_manager.actor_addr().id().clone(),
791 other_device: op.remote.device_name.clone(),
792 qp_index: op.qp_index,
793 };
794 let handle = match self.ensure_qp_actor(cx, &qp_key, peer_manager.clone()) {
795 Ok(handle) => handle,
796 Err(error) => {
797 let _ = reply.send(StripeResult {
798 stripe_id,
799 result: Err(error.to_string()),
800 });
801 continue;
802 }
803 };
804 if let Err(error) = handle.send(ProcessOps {
805 items: vec![op],
806 reply: reply.clone(),
807 }) {
808 let _ = reply.send(StripeResult {
809 stripe_id,
810 result: Err(error.to_string()),
811 });
812 }
813 }
814 }
815}
816
817#[async_trait]
818impl<I: IbvDeviceImpl> Handler<SubmitOps<I>> for IbvManagerActor<I> {
819 async fn handle(&mut self, cx: &Context<Self>, msg: SubmitOps<I>) -> Result<(), anyhow::Error> {
820 let SubmitOps { ops, reply } = msg;
821
822 for op in ops {
826 let (peer_manager, planned) = match op {
827 ManagerOp::NeedsQueuePair { peer_manager, ops } => (peer_manager, ops),
828 ManagerOp::NeedsMemReg { op_idx, op } => {
829 let whole_op = StripeId {
830 op_idx,
831 stripe_idx: 0,
832 stripe_count: 1,
833 };
834 if let Err(error) = self.resolve_local_mrs(&op.local_memory) {
835 let _ = reply.send(StripeResult {
836 stripe_id: whole_op,
837 result: Err(error.to_string()),
838 });
839 continue;
840 }
841 let planned = match self.queue_pair_router.plan_ops(op_idx, &op) {
842 Ok(planned) => planned,
843 Err(error) => {
844 let _ = reply.send(StripeResult {
845 stripe_id: whole_op,
846 result: Err(error.to_string()),
847 });
848 continue;
849 }
850 };
851 (op.remote_manager, planned)
852 }
853 };
854 self.dispatch_ops(cx, peer_manager, planned, &reply);
855 }
856 Ok(())
857 }
858}
859
860#[async_trait]
861impl<I: IbvDeviceImpl> Handler<RawQueuePair> for IbvManagerActor<I> {
862 async fn handle(&mut self, cx: &Context<Self>, msg: RawQueuePair) -> Result<(), anyhow::Error> {
863 let RawQueuePair { self_device, reply } = msg;
864 let config = self.config.clone();
867 let result = self
868 .get_or_create_device_domain(&self_device)
869 .and_then(|domain| legacy::IbvQueuePair::new(domain, config))
870 .map_err(|e| e.to_string());
871 let _ = reply.try_post(cx, result);
872 Ok(())
873 }
874}
875
876#[async_trait]
877impl<I: IbvDeviceImpl> Handler<CreatePeerQueuePair<IbvManagerActor<I>>> for IbvManagerActor<I> {
878 async fn handle(
879 &mut self,
880 cx: &Context<Self>,
881 msg: CreatePeerQueuePair<IbvManagerActor<I>>,
882 ) -> Result<(), anyhow::Error> {
883 let CreatePeerQueuePair {
884 sender,
885 sender_device,
886 receiver_device,
887 qp_index,
888 sender_info,
889 reply,
890 } = msg;
891 let qp_key = QpKey {
892 self_device: receiver_device,
893 other_id: sender.actor_addr().id().clone(),
894 other_device: sender_device,
895 qp_index,
896 };
897 match self.create_peer_qp(&qp_key, &sender_info) {
898 Ok(local_info) => reply.post(cx, Ok(local_info)),
899 Err(e) => reply.post(cx, Err(e.to_string())),
900 }
901 Ok(())
902 }
903}
904
905#[async_trait]
906impl<I: IbvDeviceImpl> IbvManagerLocalMessageHandler for IbvManagerActor<I> {
907 async fn register_remote_buffer(
908 &mut self,
909 _cx: &Context<Self>,
910 local: KeepaliveLocalMemory,
911 ) -> Result<Result<Vec<IbvRemoteMemoryRegionView>, String>, anyhow::Error> {
912 Ok(self
916 .resolve_local_mrs(&local)
917 .map(|mrs| mrs.iter().map(IbvRemoteMemoryRegionView::from).collect())
918 .map_err(|error| format!("{error:#}")))
919 }
920}
921
922#[async_trait]
926impl<I: IbvDeviceImpl> Handler<IbvManagerLocalMessage> for IbvManagerActor<I> {
927 async fn handle(
928 &mut self,
929 cx: &Context<Self>,
930 message: IbvManagerLocalMessage,
931 ) -> Result<(), anyhow::Error> {
932 <Self as IbvManagerLocalMessageHandler>::handle(self, cx, message).await
933 }
934}
935
936#[derive(Debug)]
941pub struct IbvBackend<I: IbvDeviceImpl> {
942 manager: ActorHandle<IbvManagerActor<I>>,
943 queue_pair_router: Arc<QueuePairRouter>,
944}
945
946impl<I: IbvDeviceImpl> Clone for IbvBackend<I> {
947 fn clone(&self) -> Self {
948 Self {
949 manager: self.manager.clone(),
950 queue_pair_router: Arc::clone(&self.queue_pair_router),
951 }
952 }
953}
954
955impl<I: IbvDeviceImpl> std::ops::Deref for IbvBackend<I> {
956 type Target = ActorHandle<IbvManagerActor<I>>;
957 fn deref(&self) -> &Self::Target {
958 &self.manager
959 }
960}
961
962#[derive(Serialize, Deserialize, Named)]
966#[serde(bound = "")]
967pub struct IbvRemoteBackendContext<I: IbvDeviceImpl> {
968 pub manager: ActorRef<IbvManagerActor<I>>,
969 pub buffers: Vec<IbvRemoteMemoryRegionView>,
970}
971
972impl<I: IbvDeviceImpl> Clone for IbvRemoteBackendContext<I> {
976 fn clone(&self) -> Self {
977 Self {
978 manager: self.manager.clone(),
979 buffers: self.buffers.clone(),
980 }
981 }
982}
983
984impl<I: IbvDeviceImpl> std::fmt::Debug for IbvRemoteBackendContext<I> {
985 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
986 f.debug_struct("IbvRemoteBackendContext")
987 .field("manager", &self.manager)
988 .field("buffers", &self.buffers)
989 .finish()
990 }
991}
992
993#[async_trait]
994impl<I: IbvDeviceImpl> RdmaBackend for IbvBackend<I>
995where
996 RdmaRemoteBuffer: ResolveRemoteBackendContext<IbvBackend<I>>,
997{
998 type RemoteBackendContext = IbvRemoteBackendContext<I>;
999 type TransportInfo = ();
1000
1001 fn available() -> bool {
1002 if IbvDevice::<I>::available() {
1003 if hyperactor_config::global::get(crate::config::RDMA_DISABLE_IBVERBS) {
1004 tracing::warn!(
1005 "ibverbs ({}) is available, but it was disabled by configuration (RDMA_DISABLE_IBVERBS=true)",
1006 I::backend_name()
1007 );
1008 return false;
1009 }
1010 return true;
1011 }
1012 false
1013 }
1014
1015 fn transport_level(&self) -> RdmaTransportLevel {
1016 RdmaTransportLevel::Nic
1017 }
1018
1019 fn transport_info(&self) -> Option<Self::TransportInfo> {
1020 None
1021 }
1022
1023 async fn spawn(
1024 cx: &(impl hyperactor::context::Actor + Send + Sync),
1025 config: &RdmaConfig,
1026 ) -> Result<Self> {
1027 let actor = IbvManagerActor::<I>::new(config.ibv.clone()).await?;
1028 let queue_pair_router = actor.queue_pair_router();
1029 Ok(Self {
1030 manager: cx.spawn(actor),
1031 queue_pair_router,
1032 })
1033 }
1034
1035 async fn register_remote_buffer(
1036 &self,
1037 cx: &(impl hyperactor::context::Actor + Send + Sync),
1038 _remote_buf_id: usize,
1039 local: KeepaliveLocalMemory,
1040 ) -> Result<IbvRemoteBackendContext<I>> {
1041 let buffers = self
1042 .manager
1043 .register_remote_buffer(cx, local)
1044 .await?
1045 .map_err(|e| anyhow::anyhow!(e))?;
1046 Ok(IbvRemoteBackendContext {
1047 manager: self.manager.bind(),
1048 buffers,
1049 })
1050 }
1051
1052 async fn release_buffer(
1056 &self,
1057 _cx: &(impl hyperactor::context::Actor + Send + Sync),
1058 _remote_buf_id: usize,
1059 ) -> Result<()> {
1060 Ok(())
1061 }
1062
1063 async fn submit(
1074 &self,
1075 cx: &(impl hyperactor::context::Actor + Send + Sync),
1076 ops: Vec<RdmaOp>,
1077 timeout: Duration,
1078 ) -> Result<(), anyhow::Error> {
1079 let mut ibv_ops = Vec::with_capacity(ops.len());
1080 for op in ops {
1081 let ctx = <RdmaRemoteBuffer as ResolveRemoteBackendContext<IbvBackend<I>>>::resolve(
1082 &op.remote,
1083 )
1084 .expect("op routed to incompatible backend");
1085 ibv_ops.push(IbvOp {
1086 op_type: op.op_type,
1087 local_memory: op.local.clone(),
1088 remote_buffers: ctx.buffers,
1089 remote_manager: ctx.manager,
1090 });
1091 }
1092 let n = ibv_ops.len();
1093 let (reply, mut reply_rx) = mpsc::unbounded_channel();
1094
1095 let mut manager_ops = Vec::new();
1096 for (op_idx, op) in ibv_ops.into_iter().enumerate() {
1097 if let Some(op) = self.queue_pair_router.try_dispatch::<I>(op_idx, op, &reply) {
1098 manager_ops.push(op);
1099 }
1100 }
1101 if !manager_ops.is_empty() {
1102 self.manager.try_post(
1103 cx,
1104 SubmitOps {
1105 ops: manager_ops,
1106 reply,
1107 },
1108 )?;
1109 }
1110
1111 let mut outstanding: HashMap<usize, usize> = HashMap::with_capacity(n);
1112 let mut failures: HashMap<usize, Vec<(usize, String)>> = HashMap::new();
1115 let mut completed = 0usize;
1116 let mut received_stripes = 0usize;
1117 let mut terminal: Option<String> = None;
1118 let deadline = tokio::time::Instant::now() + timeout;
1119 while completed < n {
1120 tokio::select! {
1121 () = tokio::time::sleep_until(deadline) => {
1122 terminal = Some(format!(
1123 "submit timed out after {completed}/{n} ops ({received_stripes} stripe replies) with {} failed ops",
1124 failures.len()
1125 ));
1126 break;
1127 }
1128 recv = reply_rx.recv() => {
1129 match recv {
1130 Some(StripeResult { stripe_id, result }) => {
1131 received_stripes += 1;
1132 if let Err(error) = result {
1133 failures
1134 .entry(stripe_id.op_idx)
1135 .or_default()
1136 .push((stripe_id.stripe_idx, error));
1137 }
1138 let remaining = outstanding
1139 .entry(stripe_id.op_idx)
1140 .or_insert(stripe_id.stripe_count);
1141 *remaining = remaining
1142 .checked_sub(1)
1143 .expect("an operation reports no more than its stripe count");
1144 if *remaining == 0 {
1145 outstanding.remove(&stripe_id.op_idx);
1146 completed += 1;
1147 }
1148 }
1149 None => {
1150 terminal = Some(format!(
1151 "operation result channel closed after {completed}/{n} ops ({received_stripes} stripe replies) with {} failed ops: \
1152 some built-in RDMA actor likely stopped or failed, see logs for more details",
1153 failures.len()
1154 ));
1155 break;
1156 }
1157 }
1158 }
1159 }
1160 }
1161
1162 if terminal.is_none() && failures.is_empty() {
1163 return Ok(());
1164 }
1165
1166 let mut failures: Vec<_> = failures.into_iter().collect();
1167 failures.sort_by_key(|(idx, _)| *idx);
1168 let mut msg = terminal.unwrap_or_else(|| format!("{}/{n} ops failed", failures.len()));
1169 if !failures.is_empty() {
1170 msg.push(':');
1171 for (idx, op_failures) in &mut failures {
1172 op_failures.sort_by_key(|(stripe_idx, _)| *stripe_idx);
1173 write!(msg, "\n op {idx}:").expect("infallible String write");
1174 for (stripe_idx, err) in op_failures {
1175 write!(msg, "\n stripe {stripe_idx}: {err}")
1176 .expect("infallible String write");
1177 }
1178 }
1179 }
1180 Err(anyhow::anyhow!(msg))
1181 }
1182}
1183
1184#[cfg(test)]
1185mod tests {
1186 use std::collections::BTreeMap;
1201 use std::collections::BTreeSet;
1202 use std::num::NonZeroUsize;
1203 use std::sync::Arc;
1204 use std::sync::atomic::AtomicUsize;
1205 use std::sync::atomic::Ordering;
1206 use std::time::Duration;
1207
1208 use async_trait::async_trait;
1209 use hyperactor::Actor;
1210 use hyperactor::ActorAddr;
1211 use hyperactor::ActorEnvironment;
1212 use hyperactor::ActorRef;
1213 use hyperactor::Context;
1214 use hyperactor::Handler;
1215 use hyperactor::Instance;
1216 use hyperactor::Label;
1217 use hyperactor::OncePortRef;
1218 use hyperactor::Proc;
1219 use hyperactor::RefClient;
1220 use hyperactor::RemoteSpawn;
1221 use hyperactor::Uid;
1222 use hyperactor::actor::ActorError;
1223 use hyperactor::channel::ChannelAddr;
1224 use hyperactor::channel::ChannelTransport;
1225 use serde::Deserialize;
1226 use serde::Serialize;
1227 use typeuri::Named;
1228
1229 use super::IbvManagerActor;
1230 use super::IbvOp;
1231 use super::KIB;
1232 use super::QpRoute;
1233 use super::QueuePairRouter;
1234 use crate::IbvConfig;
1235 use crate::RdmaManagerActor;
1236 use crate::RdmaManagerMessageClient;
1237 use crate::RdmaOp;
1238 use crate::RdmaOpType;
1239 use crate::RdmaRemoteBuffer;
1240 use crate::backend::RdmaBackendHandle;
1241 use crate::backend::cuda_test_utils::CudaAllocation;
1242 use crate::backend::cuda_test_utils::CudaAllocator;
1243 use crate::backend::ibverbs::device::list_all_devices;
1244 use crate::backend::ibverbs::device_selection::IbvDeviceTarget;
1245 use crate::backend::ibverbs::device_selection::PeerDeviceAffinityPolicy;
1246 use crate::backend::ibverbs::device_selection::resolve_target;
1247 use crate::backend::ibverbs::device_selection::select_optimal_ibv_devices;
1248 use crate::backend::ibverbs::memory_region::IbvMemoryRegionView;
1249 use crate::backend::ibverbs::memory_region::IbvRemoteMemoryRegionView;
1250 use crate::backend::ibverbs::mlx_device::MlxDevice;
1251 use crate::backend::ibverbs::primitives::IbvQpType;
1252 use crate::device_selection::MemoryLocation;
1253 use crate::local_memory::KeepaliveLocalMemory;
1254
1255 #[derive(Debug, Clone, Copy, Serialize, Deserialize, Named)]
1261 pub enum BufferDevice {
1262 Cpu,
1263 Cuda(i32),
1264 }
1265
1266 #[derive(Debug, Clone, Serialize, Deserialize, Named)]
1271 pub struct BufferHelperOp {
1272 op_type: RdmaOpType,
1273 local_buf: RdmaRemoteBuffer,
1274 remote_buf: RdmaRemoteBuffer,
1275 }
1276
1277 #[hyperactor::export(handlers = [BufferHelperMessage])]
1280 #[hyperactor::spawnable]
1281 #[derive(Debug)]
1282 pub struct BufferHelperActor {
1283 rdma_manager: ActorRef<RdmaManagerActor>,
1284 cuda_allocs: Vec<CudaAllocation>,
1288 }
1289
1290 #[async_trait]
1291 impl Actor for BufferHelperActor {
1292 async fn cleanup(
1293 &mut self,
1294 _this: &Instance<Self>,
1295 _err: Option<&ActorError>,
1296 ) -> Result<(), anyhow::Error> {
1297 for alloc in self.cuda_allocs.drain(..) {
1298 alloc.try_free();
1299 }
1300 Ok(())
1301 }
1302 }
1303
1304 #[async_trait]
1305 impl RemoteSpawn for BufferHelperActor {
1306 type Params = ActorRef<RdmaManagerActor>;
1307
1308 async fn new(
1309 rdma_manager: ActorRef<RdmaManagerActor>,
1310 _env: &ActorEnvironment,
1311 ) -> Result<Self, anyhow::Error> {
1312 Ok(Self {
1313 rdma_manager,
1314 cuda_allocs: Vec::new(),
1315 })
1316 }
1317 }
1318
1319 #[derive(Handler, RefClient, Named, Serialize, Deserialize, Debug)]
1320 pub enum BufferHelperMessage {
1321 Allocate {
1325 size: usize,
1326 device: BufferDevice,
1327 pattern: u8,
1328 #[reply]
1329 reply: OncePortRef<RdmaRemoteBuffer>,
1330 },
1331 AllocatePositional {
1333 size: usize,
1334 device: BufferDevice,
1335 #[reply]
1336 reply: OncePortRef<RdmaRemoteBuffer>,
1337 },
1338 ReadContents {
1343 remote: Box<RdmaRemoteBuffer>,
1344 offset: usize,
1345 len: usize,
1346 #[reply]
1347 reply: OncePortRef<Vec<u8>>,
1348 },
1349 Submit {
1354 ops: Vec<BufferHelperOp>,
1355 timeout_secs: u64,
1356 #[reply]
1357 reply: OncePortRef<Result<(), String>>,
1358 },
1359 }
1360
1361 impl BufferHelperActor {
1362 async fn allocate_impl(
1363 &mut self,
1364 cx: &Context<'_, Self>,
1365 device: BufferDevice,
1366 contents: Vec<u8>,
1367 ) -> Result<RdmaRemoteBuffer, anyhow::Error> {
1368 let size = contents.len();
1369 let local = match device {
1370 BufferDevice::Cpu => {
1371 KeepaliveLocalMemory::try_new(Arc::new(contents.into_boxed_slice()))?
1372 }
1373 BufferDevice::Cuda(device_id) => {
1374 let alloc = CudaAllocator::get().allocate(device_id, size, size);
1375 let local = KeepaliveLocalMemory::try_new(Arc::new(alloc.clone()))?;
1376 self.cuda_allocs.push(alloc);
1377 unsafe { local.write_at(0, &contents) }?;
1380 local
1381 }
1382 };
1383 let handle = self
1384 .rdma_manager
1385 .downcast_handle(cx)
1386 .ok_or_else(|| anyhow::anyhow!("rdma_manager not local to BufferHelperActor"))?;
1387 handle.request_buffer(cx, local).await
1388 }
1389
1390 async fn read_contents_impl(
1391 &mut self,
1392 cx: &Context<'_, Self>,
1393 remote: RdmaRemoteBuffer,
1394 offset: usize,
1395 len: usize,
1396 ) -> Result<Vec<u8>, anyhow::Error> {
1397 let handle = self
1398 .rdma_manager
1399 .downcast_handle(cx)
1400 .ok_or_else(|| anyhow::anyhow!("rdma_manager not local"))?;
1401 let local = handle
1402 .request_local_memory(cx, remote.id)
1403 .await?
1404 .ok_or_else(|| {
1405 anyhow::anyhow!(
1406 "no local memory registered on this side for remote_buf_id={}",
1407 remote.id,
1408 )
1409 })?;
1410 let mut out = vec![0u8; len];
1411 unsafe { local.read_at(offset, &mut out)? };
1415 Ok(out)
1416 }
1417
1418 async fn submit_impl(
1419 &mut self,
1420 cx: &Context<'_, Self>,
1421 ops: Vec<BufferHelperOp>,
1422 timeout_secs: u64,
1423 ) -> Result<Result<(), String>, anyhow::Error> {
1424 let handle = self
1425 .rdma_manager
1426 .downcast_handle(cx)
1427 .ok_or_else(|| anyhow::anyhow!("rdma_manager not local"))?;
1428 let mut rdma_ops = Vec::with_capacity(ops.len());
1429 for (i, op) in ops.into_iter().enumerate() {
1430 let local = handle
1431 .request_local_memory(cx, op.local_buf.id)
1432 .await?
1433 .ok_or_else(|| {
1434 anyhow::anyhow!(
1435 "op {i}: no local memory registered for remote_buf_id={}",
1436 op.local_buf.id,
1437 )
1438 })?;
1439 rdma_ops.push(RdmaOp {
1440 op_type: op.op_type,
1441 local,
1442 remote: op.remote_buf,
1443 });
1444 }
1445 let nic = RdmaManagerActor::local_handle(cx)
1446 .get_backend_handles(cx)
1447 .await?
1448 .into_iter()
1449 .find(|h| !matches!(h, RdmaBackendHandle::Tcp(_)))
1450 .ok_or_else(|| anyhow::anyhow!("no NIC backend on this proc"))?;
1451 let result = nic
1452 .submit(cx, rdma_ops, Duration::from_secs(timeout_secs))
1453 .await;
1454 Ok(result.map_err(|e| format!("{e}")))
1455 }
1456 }
1457
1458 #[async_trait]
1459 #[hyperactor::handle(BufferHelperMessage)]
1460 impl BufferHelperMessageHandler for BufferHelperActor {
1461 async fn allocate(
1462 &mut self,
1463 cx: &Context<Self>,
1464 size: usize,
1465 device: BufferDevice,
1466 pattern: u8,
1467 ) -> Result<RdmaRemoteBuffer, anyhow::Error> {
1468 self.allocate_impl(cx, device, vec![pattern; size]).await
1469 }
1470
1471 async fn allocate_positional(
1472 &mut self,
1473 cx: &Context<Self>,
1474 size: usize,
1475 device: BufferDevice,
1476 ) -> Result<RdmaRemoteBuffer, anyhow::Error> {
1477 self.allocate_impl(cx, device, positional_contents(size))
1478 .await
1479 }
1480
1481 async fn read_contents(
1482 &mut self,
1483 cx: &Context<Self>,
1484 remote: Box<RdmaRemoteBuffer>,
1485 offset: usize,
1486 len: usize,
1487 ) -> Result<Vec<u8>, anyhow::Error> {
1488 self.read_contents_impl(cx, *remote, offset, len).await
1489 }
1490
1491 async fn submit(
1492 &mut self,
1493 cx: &Context<Self>,
1494 ops: Vec<BufferHelperOp>,
1495 timeout_secs: u64,
1496 ) -> Result<Result<(), String>, anyhow::Error> {
1497 self.submit_impl(cx, ops, timeout_secs).await
1498 }
1499 }
1500
1501 static COUNTER: AtomicUsize = AtomicUsize::new(0);
1506
1507 struct TestEnv {
1513 client: hyperactor::Client,
1514 proc_a: Proc,
1515 helper_a: ActorRef<BufferHelperActor>,
1516 proc_b: Proc,
1517 helper_b: ActorRef<BufferHelperActor>,
1518 }
1519
1520 impl TestEnv {
1521 async fn new(config_a: IbvConfig, config_b: IbvConfig) -> Result<Self, anyhow::Error> {
1523 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1524 let proc_a = Proc::direct(
1525 ChannelAddr::any(ChannelTransport::Unix),
1526 format!("rdma_side_a_{id}"),
1527 )?;
1528 let helper_a = Self::spawn_side(&proc_a, config_a).await?;
1529 let proc_b = Proc::direct(
1530 ChannelAddr::any(ChannelTransport::Unix),
1531 format!("rdma_side_b_{id}"),
1532 )?;
1533 let helper_b = Self::spawn_side(&proc_b, config_b).await?;
1534 let client = proc_b.client("test_client");
1535 Ok(Self {
1536 client,
1537 proc_a,
1538 helper_a,
1539 proc_b,
1540 helper_b,
1541 })
1542 }
1543
1544 async fn same_config(config: IbvConfig) -> Result<Self, anyhow::Error> {
1546 Self::new(config.clone(), config).await
1547 }
1548
1549 async fn spawn_side(
1552 proc: &Proc,
1553 config: IbvConfig,
1554 ) -> Result<ActorRef<BufferHelperActor>, anyhow::Error> {
1555 let rdma_actor =
1556 RdmaManagerActor::new(Some(config), &ActorEnvironment::default()).await?;
1557 let rdma_handle =
1559 proc.spawn_with_uid(Uid::singleton(Label::strip("rdma_manager")), rdma_actor)?;
1560 let rdma: ActorRef<RdmaManagerActor> = rdma_handle.bind();
1561 let helper_actor = BufferHelperActor::new(rdma, &ActorEnvironment::default()).await?;
1562 let helper_handle = proc.spawn_with_label("helper", helper_actor);
1563 Ok(helper_handle.bind())
1564 }
1565
1566 async fn shutdown(mut self) -> Result<(), anyhow::Error> {
1567 let _ = self
1568 .proc_a
1569 .destroy_and_wait(Duration::from_secs(10), "TestEnv shutdown proc_a")
1570 .await?;
1571 let _ = self
1572 .proc_b
1573 .destroy_and_wait(Duration::from_secs(10), "TestEnv shutdown proc_b")
1574 .await?;
1575 Ok(())
1576 }
1577 }
1578
1579 async fn assert_remote_pattern(
1584 helper: &ActorRef<BufferHelperActor>,
1585 cx: &hyperactor::Client,
1586 remote: RdmaRemoteBuffer,
1587 size: usize,
1588 pattern: u8,
1589 ) -> Result<(), anyhow::Error> {
1590 let got = helper.read_contents(cx, Box::new(remote), 0, size).await?;
1591 assert_eq!(got, vec![pattern; size]);
1592 Ok(())
1593 }
1594
1595 fn positional_byte(offset: usize) -> u8 {
1599 (offset % 251) as u8
1600 }
1601
1602 fn positional_contents(size: usize) -> Vec<u8> {
1603 (0..size).map(positional_byte).collect()
1604 }
1605
1606 fn assert_positional(got: &[u8], size: usize, what: &str) {
1609 assert_eq!(got.len(), size, "{what}: read back the wrong length");
1610 if let Some(offset) = (0..size).find(|&i| got[i] != positional_byte(i)) {
1611 panic!(
1612 "{what}: byte {offset} of {size} is {:#04x}, expected {:#04x}",
1613 got[offset],
1614 positional_byte(offset),
1615 );
1616 }
1617 }
1618
1619 async fn run_positional_write_and_read(
1623 env: &TestEnv,
1624 size: usize,
1625 timeout_secs: u64,
1626 ) -> Result<(), anyhow::Error> {
1627 let write_src = env
1628 .helper_a
1629 .allocate_positional(&env.client, size, BufferDevice::Cpu)
1630 .await?;
1631 let write_dst = env
1632 .helper_b
1633 .allocate(&env.client, size, BufferDevice::Cpu, 0)
1634 .await?;
1635 env.helper_a
1636 .submit(
1637 &env.client,
1638 vec![BufferHelperOp {
1639 op_type: RdmaOpType::WriteFromLocal,
1640 local_buf: write_src,
1641 remote_buf: write_dst.clone(),
1642 }],
1643 timeout_secs,
1644 )
1645 .await?
1646 .map_err(|e| anyhow::anyhow!(e))?;
1647 let got = env
1648 .helper_b
1649 .read_contents(&env.client, Box::new(write_dst), 0, size)
1650 .await?;
1651 assert_positional(&got, size, "write");
1652
1653 let read_dst = env
1654 .helper_a
1655 .allocate(&env.client, size, BufferDevice::Cpu, 0)
1656 .await?;
1657 let read_src = env
1658 .helper_b
1659 .allocate_positional(&env.client, size, BufferDevice::Cpu)
1660 .await?;
1661 env.helper_a
1662 .submit(
1663 &env.client,
1664 vec![BufferHelperOp {
1665 op_type: RdmaOpType::ReadIntoLocal,
1666 local_buf: read_dst.clone(),
1667 remote_buf: read_src,
1668 }],
1669 timeout_secs,
1670 )
1671 .await?
1672 .map_err(|e| anyhow::anyhow!(e))?;
1673 let got = env
1674 .helper_a
1675 .read_contents(&env.client, Box::new(read_dst), 0, size)
1676 .await?;
1677 assert_positional(&got, size, "read");
1678 Ok(())
1679 }
1680
1681 async fn run_cross_actor_write(
1684 env: &TestEnv,
1685 src_dev: BufferDevice,
1686 dst_dev: BufferDevice,
1687 size: usize,
1688 pattern: u8,
1689 timeout_secs: u64,
1690 ) -> Result<(), anyhow::Error> {
1691 let src = env
1692 .helper_a
1693 .allocate(&env.client, size, src_dev, pattern)
1694 .await?;
1695 let dst = env.helper_b.allocate(&env.client, size, dst_dev, 0).await?;
1696 env.helper_a
1697 .submit(
1698 &env.client,
1699 vec![BufferHelperOp {
1700 op_type: RdmaOpType::WriteFromLocal,
1701 local_buf: src,
1702 remote_buf: dst.clone(),
1703 }],
1704 timeout_secs,
1705 )
1706 .await?
1707 .map_err(|e| anyhow::anyhow!(e))?;
1708 assert_remote_pattern(&env.helper_b, &env.client, dst, size, pattern).await
1709 }
1710
1711 async fn run_cross_actor_read(
1714 env: &TestEnv,
1715 dst_dev: BufferDevice,
1716 src_dev: BufferDevice,
1717 size: usize,
1718 pattern: u8,
1719 timeout_secs: u64,
1720 ) -> Result<(), anyhow::Error> {
1721 let dst = env.helper_a.allocate(&env.client, size, dst_dev, 0).await?;
1722 let src = env
1723 .helper_b
1724 .allocate(&env.client, size, src_dev, pattern)
1725 .await?;
1726 env.helper_a
1727 .submit(
1728 &env.client,
1729 vec![BufferHelperOp {
1730 op_type: RdmaOpType::ReadIntoLocal,
1731 local_buf: dst.clone(),
1732 remote_buf: src,
1733 }],
1734 timeout_secs,
1735 )
1736 .await?
1737 .map_err(|e| anyhow::anyhow!(e))?;
1738 assert_remote_pattern(&env.helper_a, &env.client, dst, size, pattern).await
1739 }
1740
1741 async fn run_multi_op_same_qp(
1747 env: &TestEnv,
1748 dev_a: BufferDevice,
1749 dev_b: BufferDevice,
1750 size: usize,
1751 timeout_secs: u64,
1752 ) -> Result<(), anyhow::Error> {
1753 const WRITE_PATTERN: u8 = 0xa1;
1754 const READ_PATTERN: u8 = 0xb2;
1755 let write_src = env
1756 .helper_a
1757 .allocate(&env.client, size, dev_a, WRITE_PATTERN)
1758 .await?;
1759 let write_dst = env.helper_b.allocate(&env.client, size, dev_b, 0).await?;
1760 let read_dst = env.helper_a.allocate(&env.client, size, dev_a, 0).await?;
1761 let read_src = env
1762 .helper_b
1763 .allocate(&env.client, size, dev_b, READ_PATTERN)
1764 .await?;
1765 env.helper_a
1766 .submit(
1767 &env.client,
1768 vec![
1769 BufferHelperOp {
1770 op_type: RdmaOpType::WriteFromLocal,
1771 local_buf: write_src,
1772 remote_buf: write_dst.clone(),
1773 },
1774 BufferHelperOp {
1775 op_type: RdmaOpType::ReadIntoLocal,
1776 local_buf: read_dst.clone(),
1777 remote_buf: read_src,
1778 },
1779 ],
1780 timeout_secs,
1781 )
1782 .await?
1783 .map_err(|e| anyhow::anyhow!(e))?;
1784 assert_remote_pattern(&env.helper_b, &env.client, write_dst, size, WRITE_PATTERN).await?;
1785 assert_remote_pattern(&env.helper_a, &env.client, read_dst, size, READ_PATTERN).await
1786 }
1787
1788 async fn run_true_loopback(
1794 env: &TestEnv,
1795 dev: BufferDevice,
1796 size: usize,
1797 ) -> Result<(), anyhow::Error> {
1798 const PATTERN: u8 = 0x5d;
1799 let src = env
1800 .helper_a
1801 .allocate(&env.client, size, dev, PATTERN)
1802 .await?;
1803 let dst = env.helper_a.allocate(&env.client, size, dev, 0).await?;
1804 env.helper_a
1805 .submit(
1806 &env.client,
1807 vec![BufferHelperOp {
1808 op_type: RdmaOpType::WriteFromLocal,
1809 local_buf: src,
1810 remote_buf: dst.clone(),
1811 }],
1812 5,
1813 )
1814 .await?
1815 .map_err(|e| anyhow::anyhow!(e))?;
1816 assert_remote_pattern(&env.helper_a, &env.client, dst, size, PATTERN).await
1817 }
1818
1819 fn require_rdma() {
1824 if list_all_devices().is_empty() {
1825 panic!("SKIPPED: no RDMA devices available");
1826 }
1827 }
1828
1829 fn require_cuda() {
1830 if !crate::is_cuda_available() {
1831 panic!("SKIPPED: CUDA not available");
1832 }
1833 }
1834
1835 fn make_plan_op(size: usize, pair_count: usize) -> IbvOp<IbvManagerActor<MlxDevice>> {
1836 let allocation: Box<[u8]> = vec![0; size.max(1)].into_boxed_slice();
1837 let local_memory =
1838 KeepaliveLocalMemory::try_new(Arc::new(allocation)).expect("valid CPU allocation");
1839 let mut remote_buffers = Vec::with_capacity(pair_count);
1840 for pair in 0..pair_count {
1841 let device = format!("mlx5_{pair}");
1842 let mut local = IbvMemoryRegionView::for_test(&device, pair as u32);
1843 local.virtual_addr = local_memory.addr();
1844 local.rdma_addr = local_memory.addr();
1845 local.size = local_memory.size();
1846 local_memory
1847 .install_mr::<MlxDevice>(local)
1848 .expect("install test registration");
1849 remote_buffers.push(IbvRemoteMemoryRegionView {
1850 rkey: pair as u32,
1851 addr: 0x1000,
1852 size,
1853 device_name: device,
1854 });
1855 }
1856 let remote_manager = ActorRef::attest(
1857 "manager.proc@inproc://0"
1858 .parse::<ActorAddr>()
1859 .expect("valid test actor address"),
1860 );
1861 IbvOp {
1862 op_type: RdmaOpType::ReadIntoLocal,
1863 local_memory,
1864 remote_buffers,
1865 remote_manager,
1866 }
1867 }
1868
1869 fn plan_ranges(
1870 size: usize,
1871 pair_count: usize,
1872 min_stripe_kb: usize,
1873 qps_per_peer: usize,
1874 ) -> Vec<(usize, usize)> {
1875 let op = make_plan_op(size, pair_count);
1876 let local_addr = op.local_memory.addr();
1877 let lock = hyperactor_config::global::lock();
1878 let _stripe_guard = lock.override_key(
1879 crate::config::RDMA_MIN_STRIPE_SIZE_KB,
1880 hyperactor_config::NonZeroUsize::new(min_stripe_kb)
1881 .expect("minimum stripe size is non-zero"),
1882 );
1883 let router = QueuePairRouter::new(
1884 PeerDeviceAffinityPolicy::Any,
1885 NonZeroUsize::new(qps_per_peer).expect("qps_per_peer is non-zero"),
1886 );
1887 let mut ranges: Vec<_> = router
1888 .plan_ops::<MlxDevice>(0, &op)
1889 .expect("plan test operation")
1890 .into_iter()
1891 .map(|op| (op.local.virtual_addr - local_addr, op.local.size))
1892 .collect();
1893 ranges.sort_unstable();
1894 ranges
1895 }
1896
1897 #[test]
1898 fn plan_ops_balances_stripes_without_an_undersized_tail() {
1899 assert_eq!(plan_ranges(511 * KIB, 4, 512, 1), vec![(0, 511 * KIB)]);
1900 assert_eq!(plan_ranges(513 * KIB, 4, 512, 1), vec![(0, 513 * KIB)]);
1901 assert_eq!(
1902 plan_ranges(12 * KIB, 3, 1, 1),
1903 vec![(0, 4 * KIB), (4 * KIB, 4 * KIB), (8 * KIB, 4 * KIB)]
1904 );
1905 assert_eq!(
1906 plan_ranges(15 * KIB, 8, 4, 1),
1907 vec![(0, 5 * KIB), (5 * KIB, 5 * KIB), (10 * KIB, 5 * KIB)]
1908 );
1909 assert_eq!(
1910 plan_ranges(10 * KIB, 3, 1, 1),
1911 vec![(0, 3392), (3392, 3392), (6784, 3456)]
1912 );
1913 assert_eq!(
1914 plan_ranges(32_000_000, 4, 8192, 1),
1915 vec![
1916 (0, 10_666_624),
1917 (10_666_624, 10_666_624),
1918 (21_333_248, 10_666_752),
1919 ]
1920 );
1921 assert_eq!(plan_ranges(0, 4, 1, 1), vec![(0, 0)]);
1922 }
1923
1924 #[test]
1925 fn plan_ops_stripes_across_queue_pairs() {
1926 let even =
1927 |count: usize, size: usize| (0..count).map(|i| (i * size, size)).collect::<Vec<_>>();
1928 assert_eq!(plan_ranges(4 * KIB, 1, 1, 4), even(4, KIB));
1930 assert_eq!(plan_ranges(2 * KIB, 1, 1, 4), even(2, KIB));
1933 assert_eq!(plan_ranges(16 * KIB, 1, 1, 4), even(4, 4 * KIB));
1935 assert_eq!(plan_ranges(16 * KIB, 2, 1, 4), even(4, 4 * KIB));
1937 assert_eq!(plan_ranges(12 * KIB, 3, 1, 4), even(6, 2 * KIB));
1939 assert_eq!(plan_ranges(16 * KIB, 8, 1, 4), even(8, 2 * KIB));
1941 assert_eq!(plan_ranges(3 * KIB, 2, 1, 4), even(2, 1536));
1943 assert_eq!(plan_ranges(KIB - 1, 2, 1, 4), vec![(0, KIB - 1)]);
1945 }
1946
1947 fn plan_queue_pairs(
1949 router: &QueuePairRouter,
1950 op_idx: usize,
1951 op: &IbvOp<IbvManagerActor<MlxDevice>>,
1952 ) -> Vec<(String, usize)> {
1953 let mut planned = router
1954 .plan_ops::<MlxDevice>(op_idx, op)
1955 .expect("plan test operation");
1956 planned.sort_unstable_by_key(|planned| planned.stripe_id.stripe_idx);
1957 planned
1958 .into_iter()
1959 .map(|planned| (planned.local.device_name, planned.qp_index))
1960 .collect()
1961 }
1962
1963 #[test]
1964 fn plan_ops_spreads_one_op_over_distinct_queue_pairs() {
1965 let lock = hyperactor_config::global::lock();
1966 let _stripe_guard = lock.override_key(
1967 crate::config::RDMA_MIN_STRIPE_SIZE_KB,
1968 hyperactor_config::NonZeroUsize::new(1).expect("1 is non-zero"),
1969 );
1970
1971 let router = QueuePairRouter::new(
1972 PeerDeviceAffinityPolicy::Any,
1973 NonZeroUsize::new(3).expect("3 is non-zero"),
1974 );
1975 let qp_indices: Vec<usize> = plan_queue_pairs(&router, 0, &make_plan_op(3 * KIB, 1))
1976 .into_iter()
1977 .map(|(_, qp_index)| qp_index)
1978 .collect();
1979 assert_eq!(qp_indices, vec![0, 1, 2], "one stripe per queue pair");
1980
1981 let router = QueuePairRouter::new(
1984 PeerDeviceAffinityPolicy::Any,
1985 NonZeroUsize::new(4).expect("4 is non-zero"),
1986 );
1987 assert_eq!(
1988 queue_pairs_by_device(&router, &make_plan_op(12 * KIB, 3)),
1989 vec![vec![0, 1]; 3]
1990 );
1991
1992 let router = QueuePairRouter::new(
1995 PeerDeviceAffinityPolicy::Any,
1996 NonZeroUsize::new(4).expect("4 is non-zero"),
1997 );
1998 assert_eq!(
1999 queue_pairs_by_device(&router, &make_plan_op(3 * KIB, 2)),
2000 vec![vec![0]; 2]
2001 );
2002 }
2003
2004 #[test]
2005 fn plan_ops_assigns_stripes_to_pairs_in_name_order() {
2006 let lock = hyperactor_config::global::lock();
2007 let _stripe_guard = lock.override_key(
2008 crate::config::RDMA_MIN_STRIPE_SIZE_KB,
2009 hyperactor_config::NonZeroUsize::new(1).expect("1 is non-zero"),
2010 );
2011 let router = QueuePairRouter::new(
2012 PeerDeviceAffinityPolicy::Any,
2013 NonZeroUsize::new(4).expect("4 is non-zero"),
2014 );
2015
2016 for op_idx in 0..3 {
2018 let devices: Vec<String> = plan_queue_pairs(&router, op_idx, &make_plan_op(4 * KIB, 2))
2019 .into_iter()
2020 .map(|(device, _)| device)
2021 .collect();
2022 assert_eq!(devices, vec!["mlx5_0", "mlx5_1", "mlx5_0", "mlx5_1"]);
2023 }
2024 }
2025
2026 fn queue_pairs_by_device(
2029 router: &QueuePairRouter,
2030 op: &IbvOp<IbvManagerActor<MlxDevice>>,
2031 ) -> Vec<Vec<usize>> {
2032 let mut by_device: BTreeMap<String, Vec<usize>> = BTreeMap::new();
2033 for (device, qp_index) in plan_queue_pairs(router, 0, op) {
2034 by_device.entry(device).or_default().push(qp_index);
2035 }
2036 by_device.into_values().collect()
2037 }
2038
2039 #[test]
2040 fn plan_ops_continues_the_rotation_across_ops() {
2041 let lock = hyperactor_config::global::lock();
2042 let _stripe_guard = lock.override_key(
2043 crate::config::RDMA_MIN_STRIPE_SIZE_KB,
2044 hyperactor_config::NonZeroUsize::new(1).expect("1 is non-zero"),
2045 );
2046 let router = QueuePairRouter::new(
2047 PeerDeviceAffinityPolicy::Any,
2048 NonZeroUsize::new(3).expect("3 is non-zero"),
2049 );
2050 let op = make_plan_op(2 * KIB, 1);
2051
2052 let qp_indices: Vec<Vec<usize>> = (0..3)
2054 .map(|op_idx| {
2055 plan_queue_pairs(&router, op_idx, &op)
2056 .into_iter()
2057 .map(|(_, qp_index)| qp_index)
2058 .collect()
2059 })
2060 .collect();
2061 assert_eq!(qp_indices, vec![vec![0, 1], vec![2, 0], vec![1, 2]]);
2062 }
2063
2064 #[test]
2065 fn queue_pair_indices_advance_independently_per_route() {
2066 let router = QueuePairRouter::new(
2067 PeerDeviceAffinityPolicy::Any,
2068 NonZeroUsize::new(3).expect("3 is non-zero"),
2069 );
2070 let other_id = "manager.proc@inproc://0"
2071 .parse::<ActorAddr>()
2072 .expect("valid test actor address")
2073 .id()
2074 .clone();
2075 let route_a = QpRoute {
2076 self_device: "mlx5_0".into(),
2077 other_id: other_id.clone(),
2078 other_device: "mlx5_1".into(),
2079 };
2080 let route_b = QpRoute {
2081 self_device: "mlx5_2".into(),
2082 other_id,
2083 other_device: "mlx5_3".into(),
2084 };
2085
2086 assert_eq!(
2087 (0..4)
2088 .map(|_| router.next_qp_index(route_a.clone()))
2089 .collect::<Vec<_>>(),
2090 vec![0, 1, 2, 0],
2091 );
2092 assert_eq!(router.next_qp_index(route_b), 0);
2093 assert_eq!(router.next_qp_index(route_a), 1);
2094 }
2095
2096 #[test]
2097 fn plan_ops_round_robins_queue_pair_indices() {
2098 let router = QueuePairRouter::new(
2099 PeerDeviceAffinityPolicy::Any,
2100 NonZeroUsize::new(3).expect("3 is non-zero"),
2101 );
2102 let op = make_plan_op(1, 1);
2103
2104 let qp_indices = (0..4)
2105 .map(|op_idx| {
2106 let planned = router
2107 .plan_ops::<MlxDevice>(op_idx, &op)
2108 .expect("plan test operation");
2109 let [planned] = planned.as_slice() else {
2110 panic!("one NIC pair should produce one stripe");
2111 };
2112 planned.qp_index
2113 })
2114 .collect::<Vec<_>>();
2115
2116 assert_eq!(qp_indices, vec![0, 1, 2, 0]);
2117 }
2118
2119 #[timed_test::async_timed_test(timeout_secs = 60)]
2128 async fn test_host_buffers_use_per_allocation_mrs_by_default() -> Result<(), anyhow::Error> {
2129 require_rdma();
2130 let lock = hyperactor_config::global::lock();
2131 let _guard = lock.override_key(crate::config::RDMA_HOST_ODP, false);
2132 let target = IbvDeviceTarget::cpu(0);
2133 let device = resolve_target::<MlxDevice>(&target)?
2134 .expect("cpu:0 should resolve to a NIC")
2135 .name()
2136 .clone();
2137 let env = TestEnv::same_config(IbvConfig::targeting(target)).await?;
2138 let buf: Box<[u8]> = vec![0u8; 1024].into_boxed_slice();
2139 let local = KeepaliveLocalMemory::try_new(Arc::new(buf))?;
2140 assert!(
2141 local.registered_mr::<MlxDevice>(&device)?.is_none(),
2142 "the region should have no registration before it is registered",
2143 );
2144 RdmaManagerActor::local_handle(&env.client)
2145 .request_buffer(&env.client, local.clone())
2146 .await?;
2147 let first = local
2148 .registered_mr::<MlxDevice>(&device)?
2149 .expect("registration should be recorded under the pinned device");
2150
2151 let second_buf: Box<[u8]> = vec![0u8; 1024].into_boxed_slice();
2152 let second_local = KeepaliveLocalMemory::try_new(Arc::new(second_buf))?;
2153 RdmaManagerActor::local_handle(&env.client)
2154 .request_buffer(&env.client, second_local.clone())
2155 .await?;
2156 let second = second_local
2157 .registered_mr::<MlxDevice>(&device)?
2158 .expect("second registration should be recorded under the pinned device");
2159 assert_ne!(
2160 (first.lkey, first.rkey),
2161 (second.lkey, second.rkey),
2162 "the default path should retain per-allocation registrations"
2163 );
2164 env.shutdown().await
2165 }
2166
2167 #[timed_test::async_timed_test(timeout_secs = 60)]
2168 async fn test_host_odp_shares_a_key_and_advises_destinations() -> Result<(), anyhow::Error> {
2169 require_rdma();
2170 let lock = hyperactor_config::global::lock();
2171 let _guard = lock.override_key(crate::config::RDMA_HOST_ODP, true);
2172 let target = IbvDeviceTarget::cpu(0);
2173 let device = resolve_target::<MlxDevice>(&target)?
2174 .expect("cpu:0 should resolve to a NIC")
2175 .name()
2176 .clone();
2177 let env = TestEnv::same_config(IbvConfig::targeting(target)).await?;
2178
2179 let first_buf: Box<[u8]> = vec![0u8; 1024].into_boxed_slice();
2180 let first_local = KeepaliveLocalMemory::try_new(Arc::new(first_buf))?;
2181 RdmaManagerActor::local_handle(&env.client)
2182 .request_buffer(&env.client, first_local.clone())
2183 .await?;
2184 let first = first_local
2185 .registered_mr::<MlxDevice>(&device)?
2186 .expect("first ODP registration should be recorded");
2187
2188 let second_buf: Box<[u8]> = vec![0u8; 1024].into_boxed_slice();
2189 let second_local = KeepaliveLocalMemory::try_new(Arc::new(second_buf))?;
2190 RdmaManagerActor::local_handle(&env.client)
2191 .request_buffer(&env.client, second_local.clone())
2192 .await?;
2193 let second = second_local
2194 .registered_mr::<MlxDevice>(&device)?
2195 .expect("second ODP registration should be recorded");
2196
2197 assert_eq!(
2198 (first.lkey, first.rkey),
2199 (second.lkey, second.rkey),
2200 "host buffers on one device should share its implicit ODP MR"
2201 );
2202 run_cross_actor_write(
2203 &env,
2204 BufferDevice::Cpu,
2205 BufferDevice::Cpu,
2206 1024 * 1024,
2207 0xa5,
2208 10,
2209 )
2210 .await?;
2211 run_cross_actor_read(
2212 &env,
2213 BufferDevice::Cpu,
2214 BufferDevice::Cpu,
2215 1024 * 1024,
2216 0x5a,
2217 10,
2218 )
2219 .await?;
2220 env.shutdown().await
2221 }
2222
2223 #[timed_test::async_timed_test(timeout_secs = 120)]
2229 async fn test_match_name_nic_selection() -> Result<(), anyhow::Error> {
2230 require_rdma();
2231 const MAX_NICS: usize = 4;
2232 let lock = hyperactor_config::global::lock();
2233 let _max_guard = lock.override_key(
2234 crate::config::RDMA_MAX_NICS_PER_BUFFER,
2235 Some(hyperactor_config::NonZeroUsize::new(MAX_NICS).expect("MAX_NICS is 4")),
2236 );
2237 let _policy_guard = lock.override_key(
2238 crate::config::RDMA_PEER_DEVICE_AFFINITY,
2239 "match_name".to_string(),
2240 );
2241 let expected: BTreeSet<String> =
2242 select_optimal_ibv_devices::<MlxDevice>(MemoryLocation::Cpu(None))?
2243 .iter()
2244 .take(MAX_NICS)
2245 .map(|nic| nic.name().clone())
2246 .collect();
2247
2248 let env = TestEnv::same_config(IbvConfig::default()).await?;
2249 let buffer = env
2250 .helper_a
2251 .allocate(&env.client, 32, BufferDevice::Cpu, 0)
2252 .await?;
2253 let served_by: BTreeSet<String> = buffer
2254 .resolve_mlx()
2255 .expect("the buffer is registered on a Mellanox NIC")
2256 .buffers
2257 .iter()
2258 .map(|mr| mr.device_name.clone())
2259 .collect();
2260 assert_eq!(served_by, expected);
2261
2262 for pattern in 0..2 * MAX_NICS as u8 {
2263 run_cross_actor_write(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, pattern, 5)
2264 .await?;
2265 }
2266 env.shutdown().await
2267 }
2268
2269 #[timed_test::async_timed_test(timeout_secs = 120)]
2270 async fn test_striped_transfers_land_every_byte() -> Result<(), anyhow::Error> {
2271 require_rdma();
2272 const MAX_NICS: usize = 4;
2273 const MIN_STRIPE_KB: usize = 512;
2274 const SIZE: usize = MAX_NICS * MIN_STRIPE_KB * KIB + 17;
2276 let tied = select_optimal_ibv_devices::<MlxDevice>(MemoryLocation::Cpu(None))?.len();
2277 if tied < 2 {
2278 panic!("SKIPPED: this host has fewer than two NICs tied for host memory");
2279 }
2280
2281 let lock = hyperactor_config::global::lock();
2282 let _max_guard = lock.override_key(
2283 crate::config::RDMA_MAX_NICS_PER_BUFFER,
2284 Some(hyperactor_config::NonZeroUsize::new(MAX_NICS).expect("MAX_NICS is non-zero")),
2285 );
2286 let _stripe_guard = lock.override_key(
2287 crate::config::RDMA_MIN_STRIPE_SIZE_KB,
2288 hyperactor_config::NonZeroUsize::new(MIN_STRIPE_KB).expect("MIN_STRIPE_KB is non-zero"),
2289 );
2290 let _policy_guard =
2291 lock.override_key(crate::config::RDMA_PEER_DEVICE_AFFINITY, "any".to_string());
2292
2293 let env = TestEnv::same_config(IbvConfig::default()).await?;
2294 run_positional_write_and_read(&env, SIZE, 20).await?;
2295 env.shutdown().await
2296 }
2297
2298 #[timed_test::async_timed_test(timeout_secs = 120)]
2300 async fn test_queue_pair_stripes_land_every_byte() -> Result<(), anyhow::Error> {
2301 require_rdma();
2302 const QPS_PER_PEER: usize = 4;
2303 const MIN_STRIPE_KB: usize = 512;
2304 const SIZE: usize = QPS_PER_PEER * MIN_STRIPE_KB * KIB + 17;
2306
2307 let lock = hyperactor_config::global::lock();
2308 let _qps_guard = lock.override_key(
2309 crate::config::RDMA_QPS_PER_PEER,
2310 hyperactor_config::NonZeroUsize::new(QPS_PER_PEER).expect("QPS_PER_PEER is non-zero"),
2311 );
2312 let _stripe_guard = lock.override_key(
2313 crate::config::RDMA_MIN_STRIPE_SIZE_KB,
2314 hyperactor_config::NonZeroUsize::new(MIN_STRIPE_KB).expect("MIN_STRIPE_KB is non-zero"),
2315 );
2316
2317 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
2318 run_positional_write_and_read(&env, SIZE, 20).await?;
2319 env.shutdown().await
2320 }
2321
2322 #[timed_test::async_timed_test(timeout_secs = 60)]
2326 async fn test_any_nic_selection() -> Result<(), anyhow::Error> {
2327 require_rdma();
2328 let lock = hyperactor_config::global::lock();
2329 let _max_guard = lock.override_key(
2330 crate::config::RDMA_MAX_NICS_PER_BUFFER,
2331 Some(hyperactor_config::NonZeroUsize::MIN),
2332 );
2333 let _policy_guard =
2334 lock.override_key(crate::config::RDMA_PEER_DEVICE_AFFINITY, "any".to_string());
2335 let tied = select_optimal_ibv_devices::<MlxDevice>(MemoryLocation::Cpu(None))?.len();
2336
2337 let env = TestEnv::same_config(IbvConfig::default()).await?;
2338 let mut served_by: BTreeSet<String> = BTreeSet::new();
2339 for _ in 0..16 {
2340 let buffer = env
2341 .helper_a
2342 .allocate(&env.client, 32, BufferDevice::Cpu, 0)
2343 .await?;
2344 let registrations = buffer
2345 .resolve_mlx()
2346 .expect("the buffer is registered on a Mellanox NIC")
2347 .buffers;
2348 let [mr] = registrations.as_slice() else {
2349 panic!("one NIC serves a buffer here, got {registrations:?}");
2350 };
2351 served_by.insert(mr.device_name.clone());
2352 }
2353 assert_eq!(
2356 served_by.len() > 1,
2357 tied > 1,
2358 "{tied} NICs tie for host memory, but the buffers landed on {served_by:?}",
2359 );
2360 env.shutdown().await
2361 }
2362
2363 #[timed_test::async_timed_test(timeout_secs = 60)]
2368 async fn test_cross_actor_same_device_write() -> Result<(), anyhow::Error> {
2369 require_rdma();
2370 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
2371 run_cross_actor_write(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, 0xa5, 5).await?;
2372 env.shutdown().await
2373 }
2374
2375 #[timed_test::async_timed_test(timeout_secs = 60)]
2377 async fn test_cross_actor_same_device_read() -> Result<(), anyhow::Error> {
2378 require_rdma();
2379 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
2380 run_cross_actor_read(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, 0x3c, 5).await?;
2381 env.shutdown().await
2382 }
2383
2384 #[timed_test::async_timed_test(timeout_secs = 60)]
2389 async fn test_loopback_write() -> Result<(), anyhow::Error> {
2390 require_rdma();
2391 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
2392 run_true_loopback(&env, BufferDevice::Cpu, 32).await?;
2393 env.shutdown().await
2394 }
2395
2396 #[timed_test::async_timed_test(timeout_secs = 60)]
2398 async fn test_cross_device_write() -> Result<(), anyhow::Error> {
2399 require_rdma();
2400 let env = TestEnv::new(
2401 IbvConfig::targeting(IbvDeviceTarget::cpu(0)),
2402 IbvConfig::targeting(IbvDeviceTarget::cpu(1)),
2403 )
2404 .await?;
2405 run_cross_actor_write(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, 0x77, 5).await?;
2406 env.shutdown().await
2407 }
2408
2409 #[timed_test::async_timed_test(timeout_secs = 60)]
2411 async fn test_cross_device_read() -> Result<(), anyhow::Error> {
2412 require_rdma();
2413 let env = TestEnv::new(
2414 IbvConfig::targeting(IbvDeviceTarget::cpu(0)),
2415 IbvConfig::targeting(IbvDeviceTarget::cpu(1)),
2416 )
2417 .await?;
2418 run_cross_actor_read(&env, BufferDevice::Cpu, BufferDevice::Cpu, 32, 0x88, 5).await?;
2419 env.shutdown().await
2420 }
2421
2422 #[timed_test::async_timed_test(timeout_secs = 60)]
2426 async fn test_multi_op_same_qp_cpu() -> Result<(), anyhow::Error> {
2427 require_rdma();
2428 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
2429 run_multi_op_same_qp(&env, BufferDevice::Cpu, BufferDevice::Cpu, 64, 5).await?;
2430 env.shutdown().await
2431 }
2432
2433 #[timed_test::async_timed_test(timeout_secs = 120)]
2437 async fn test_multi_op_same_qp_cuda() -> Result<(), anyhow::Error> {
2438 require_rdma();
2439 require_cuda();
2440 const SIZE: usize = 2 * 1024 * 1024;
2441 let env = TestEnv::new(
2442 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
2443 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
2444 )
2445 .await?;
2446 run_multi_op_same_qp(&env, BufferDevice::Cuda(0), BufferDevice::Cuda(1), SIZE, 10).await?;
2447 env.shutdown().await
2448 }
2449
2450 #[timed_test::async_timed_test(timeout_secs = 60)]
2452 async fn test_cuda_to_cpu_write() -> Result<(), anyhow::Error> {
2453 require_rdma();
2454 require_cuda();
2455 const SIZE: usize = 2 * 1024 * 1024;
2456 let env = TestEnv::new(
2457 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
2458 IbvConfig::targeting(IbvDeviceTarget::cpu(1)),
2459 )
2460 .await?;
2461 run_cross_actor_write(
2462 &env,
2463 BufferDevice::Cuda(0),
2464 BufferDevice::Cpu,
2465 SIZE,
2466 0x9b,
2467 10,
2468 )
2469 .await?;
2470 env.shutdown().await
2471 }
2472
2473 #[timed_test::async_timed_test(timeout_secs = 60)]
2475 async fn test_cuda_to_cpu_read() -> Result<(), anyhow::Error> {
2476 require_rdma();
2477 require_cuda();
2478 const SIZE: usize = 2 * 1024 * 1024;
2479 let env = TestEnv::new(
2480 IbvConfig::targeting(IbvDeviceTarget::cpu(0)),
2481 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
2482 )
2483 .await?;
2484 run_cross_actor_read(
2485 &env,
2486 BufferDevice::Cpu,
2487 BufferDevice::Cuda(1),
2488 SIZE,
2489 0x37,
2490 10,
2491 )
2492 .await?;
2493 env.shutdown().await
2494 }
2495
2496 #[timed_test::async_timed_test(timeout_secs = 60)]
2498 async fn test_cpu_to_cuda_write() -> Result<(), anyhow::Error> {
2499 require_rdma();
2500 require_cuda();
2501 const SIZE: usize = 2 * 1024 * 1024;
2502 let env = TestEnv::new(
2503 IbvConfig::targeting(IbvDeviceTarget::cpu(0)),
2504 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
2505 )
2506 .await?;
2507 run_cross_actor_write(
2508 &env,
2509 BufferDevice::Cpu,
2510 BufferDevice::Cuda(1),
2511 SIZE,
2512 0x5a,
2513 10,
2514 )
2515 .await?;
2516 env.shutdown().await
2517 }
2518
2519 #[timed_test::async_timed_test(timeout_secs = 60)]
2521 async fn test_cpu_to_cuda_read() -> Result<(), anyhow::Error> {
2522 require_rdma();
2523 require_cuda();
2524 const SIZE: usize = 2 * 1024 * 1024;
2525 let env = TestEnv::new(
2526 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
2527 IbvConfig::targeting(IbvDeviceTarget::cpu(1)),
2528 )
2529 .await?;
2530 run_cross_actor_read(
2531 &env,
2532 BufferDevice::Cuda(0),
2533 BufferDevice::Cpu,
2534 SIZE,
2535 0x4e,
2536 10,
2537 )
2538 .await?;
2539 env.shutdown().await
2540 }
2541
2542 #[timed_test::async_timed_test(timeout_secs = 60)]
2544 async fn test_cuda_to_cuda_write() -> Result<(), anyhow::Error> {
2545 require_rdma();
2546 require_cuda();
2547 const SIZE: usize = 2 * 1024 * 1024;
2548 let env = TestEnv::new(
2549 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
2550 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
2551 )
2552 .await?;
2553 run_cross_actor_write(
2554 &env,
2555 BufferDevice::Cuda(0),
2556 BufferDevice::Cuda(1),
2557 SIZE,
2558 0xee,
2559 10,
2560 )
2561 .await?;
2562 env.shutdown().await
2563 }
2564
2565 #[timed_test::async_timed_test(timeout_secs = 60)]
2567 async fn test_cuda_to_cuda_read() -> Result<(), anyhow::Error> {
2568 require_rdma();
2569 require_cuda();
2570 const SIZE: usize = 2 * 1024 * 1024;
2571 let env = TestEnv::new(
2572 IbvConfig::targeting(IbvDeviceTarget::gpu(0)),
2573 IbvConfig::targeting(IbvDeviceTarget::gpu(1)),
2574 )
2575 .await?;
2576 run_cross_actor_read(
2577 &env,
2578 BufferDevice::Cuda(0),
2579 BufferDevice::Cuda(1),
2580 SIZE,
2581 0x42,
2582 10,
2583 )
2584 .await?;
2585 env.shutdown().await
2586 }
2587
2588 #[timed_test::async_timed_test(timeout_secs = 60)]
2594 async fn test_standard_qp_cuda_dmabuf_fallback() -> Result<(), anyhow::Error> {
2595 require_rdma();
2596 require_cuda();
2597 const SIZE: usize = 16 * 1024 * 1024;
2598 let mut config_a = IbvConfig::targeting(IbvDeviceTarget::gpu(0));
2599 config_a.qp_type = IbvQpType::Standard;
2600 let mut config_b = IbvConfig::targeting(IbvDeviceTarget::gpu(1));
2601 config_b.qp_type = IbvQpType::Standard;
2602 let env = TestEnv::new(config_a, config_b).await?;
2603 run_cross_actor_write(
2604 &env,
2605 BufferDevice::Cuda(0),
2606 BufferDevice::Cuda(1),
2607 SIZE,
2608 0x33,
2609 10,
2610 )
2611 .await?;
2612 env.shutdown().await
2613 }
2614
2615 #[timed_test::async_timed_test(timeout_secs = 60)]
2620 async fn test_multi_batch_same_qp() -> Result<(), anyhow::Error> {
2621 require_rdma();
2622 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
2623 let src1 = env
2624 .helper_a
2625 .allocate(&env.client, 32, BufferDevice::Cpu, 0xa1)
2626 .await?;
2627 let dst1 = env
2628 .helper_b
2629 .allocate(&env.client, 32, BufferDevice::Cpu, 0)
2630 .await?;
2631 env.helper_a
2632 .submit(
2633 &env.client,
2634 vec![BufferHelperOp {
2635 op_type: RdmaOpType::WriteFromLocal,
2636 local_buf: src1,
2637 remote_buf: dst1.clone(),
2638 }],
2639 5,
2640 )
2641 .await?
2642 .map_err(|e| anyhow::anyhow!(e))?;
2643 assert_remote_pattern(&env.helper_b, &env.client, dst1, 32, 0xa1).await?;
2644
2645 let src2 = env
2646 .helper_a
2647 .allocate(&env.client, 32, BufferDevice::Cpu, 0xb2)
2648 .await?;
2649 let dst2 = env
2650 .helper_b
2651 .allocate(&env.client, 32, BufferDevice::Cpu, 0)
2652 .await?;
2653 env.helper_a
2654 .submit(
2655 &env.client,
2656 vec![BufferHelperOp {
2657 op_type: RdmaOpType::WriteFromLocal,
2658 local_buf: src2,
2659 remote_buf: dst2.clone(),
2660 }],
2661 5,
2662 )
2663 .await?
2664 .map_err(|e| anyhow::anyhow!(e))?;
2665 assert_remote_pattern(&env.helper_b, &env.client, dst2, 32, 0xb2).await?;
2666 env.shutdown().await
2667 }
2668
2669 #[timed_test::async_timed_test(timeout_secs = 120)]
2674 async fn test_multi_op_multi_qp() -> Result<(), anyhow::Error> {
2675 require_rdma();
2676 require_cuda();
2677 const SIZE: usize = 2 * 1024 * 1024;
2678
2679 let env = TestEnv::same_config(IbvConfig::default()).await?;
2680
2681 const LOOPBACK_PAT: u8 = 0x11;
2682 let lb_src = env
2683 .helper_a
2684 .allocate(&env.client, SIZE, BufferDevice::Cpu, LOOPBACK_PAT)
2685 .await?;
2686 let lb_dst = env
2687 .helper_a
2688 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
2689 .await?;
2690
2691 const CC_PAT: u8 = 0x22;
2692 let cc_src = env
2693 .helper_a
2694 .allocate(&env.client, SIZE, BufferDevice::Cpu, CC_PAT)
2695 .await?;
2696 let cc_dst = env
2697 .helper_b
2698 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
2699 .await?;
2700
2701 const CG_PAT: u8 = 0x33;
2702 let cg_src = env
2703 .helper_a
2704 .allocate(&env.client, SIZE, BufferDevice::Cpu, CG_PAT)
2705 .await?;
2706 let cg_dst = env
2707 .helper_b
2708 .allocate(&env.client, SIZE, BufferDevice::Cuda(1), 0)
2709 .await?;
2710
2711 const GC_PAT: u8 = 0x44;
2712 let gc_src = env
2713 .helper_a
2714 .allocate(&env.client, SIZE, BufferDevice::Cuda(0), GC_PAT)
2715 .await?;
2716 let gc_dst = env
2717 .helper_b
2718 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
2719 .await?;
2720
2721 const GG_PAT: u8 = 0x55;
2722 let gg_src = env
2723 .helper_b
2724 .allocate(&env.client, SIZE, BufferDevice::Cuda(1), GG_PAT)
2725 .await?;
2726 let gg_dst = env
2727 .helper_a
2728 .allocate(&env.client, SIZE, BufferDevice::Cuda(0), 0)
2729 .await?;
2730
2731 env.helper_a
2732 .submit(
2733 &env.client,
2734 vec![
2735 BufferHelperOp {
2736 op_type: RdmaOpType::WriteFromLocal,
2737 local_buf: lb_src,
2738 remote_buf: lb_dst.clone(),
2739 },
2740 BufferHelperOp {
2741 op_type: RdmaOpType::WriteFromLocal,
2742 local_buf: cc_src,
2743 remote_buf: cc_dst.clone(),
2744 },
2745 BufferHelperOp {
2746 op_type: RdmaOpType::WriteFromLocal,
2747 local_buf: cg_src,
2748 remote_buf: cg_dst.clone(),
2749 },
2750 BufferHelperOp {
2751 op_type: RdmaOpType::WriteFromLocal,
2752 local_buf: gc_src,
2753 remote_buf: gc_dst.clone(),
2754 },
2755 BufferHelperOp {
2756 op_type: RdmaOpType::ReadIntoLocal,
2757 local_buf: gg_dst.clone(),
2758 remote_buf: gg_src,
2759 },
2760 ],
2761 30,
2762 )
2763 .await?
2764 .map_err(|e| anyhow::anyhow!(e))?;
2765
2766 assert_remote_pattern(&env.helper_a, &env.client, lb_dst, SIZE, LOOPBACK_PAT).await?;
2767 assert_remote_pattern(&env.helper_b, &env.client, cc_dst, SIZE, CC_PAT).await?;
2768 assert_remote_pattern(&env.helper_b, &env.client, cg_dst, SIZE, CG_PAT).await?;
2769 assert_remote_pattern(&env.helper_b, &env.client, gc_dst, SIZE, GC_PAT).await?;
2770 assert_remote_pattern(&env.helper_a, &env.client, gg_dst, SIZE, GG_PAT).await?;
2771
2772 env.shutdown().await
2773 }
2774
2775 #[timed_test::async_timed_test(timeout_secs = 60)]
2779 async fn test_submit_timeout() -> Result<(), anyhow::Error> {
2780 require_rdma();
2781 const SIZE: usize = 1024 * 1024;
2782 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
2783 let src = env
2784 .helper_a
2785 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0x77)
2786 .await?;
2787 let dst = env
2788 .helper_b
2789 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
2790 .await?;
2791 let result = env
2792 .helper_a
2793 .submit(
2794 &env.client,
2795 vec![BufferHelperOp {
2796 op_type: RdmaOpType::WriteFromLocal,
2797 local_buf: src,
2798 remote_buf: dst,
2799 }],
2800 0,
2801 )
2802 .await?;
2803 let err = result.expect_err("expected submit to time out");
2804 assert!(
2805 err.contains("submit timed out"),
2806 "unexpected error message: {err}",
2807 );
2808 env.shutdown().await
2809 }
2810
2811 #[timed_test::async_timed_test(timeout_secs = 60)]
2821 async fn test_partial_failure_batch() -> Result<(), anyhow::Error> {
2822 require_rdma();
2823 let lock = hyperactor_config::global::lock();
2824 let _guard = lock.override_key(
2825 crate::config::RDMA_QPS_PER_PEER,
2826 hyperactor_config::NonZeroUsize::new(1).expect("1 is non-zero"),
2827 );
2828 const SIZE: usize = 32;
2829 let env = TestEnv::same_config(IbvConfig::targeting(IbvDeviceTarget::cpu(0))).await?;
2830
2831 const GOOD_PAT: u8 = 0xc3;
2832 const POST_FLUSH_PAT: u8 = 0xde;
2833
2834 let good_src_0 = env
2835 .helper_a
2836 .allocate(&env.client, SIZE, BufferDevice::Cpu, GOOD_PAT)
2837 .await?;
2838 let good_dst_0 = env
2839 .helper_b
2840 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
2841 .await?;
2842
2843 let bogus_src = env
2844 .helper_a
2845 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0xee)
2846 .await?;
2847 let real_remote = env
2848 .helper_b
2849 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
2850 .await?;
2851 let mut bogus_remote = real_remote.clone();
2852 let bufs = [
2853 bogus_remote
2854 .backends
2855 .mlx
2856 .as_mut()
2857 .map(|ctx| &mut ctx.buffers),
2858 bogus_remote
2859 .backends
2860 .efa
2861 .as_mut()
2862 .map(|ctx| &mut ctx.buffers),
2863 ];
2864 for buf in bufs.into_iter().flatten().flatten() {
2865 buf.rkey = 0xdead_beef;
2866 buf.addr = 0xdead_0000;
2867 }
2868
2869 let post_flush_src = env
2870 .helper_a
2871 .allocate(&env.client, SIZE, BufferDevice::Cpu, POST_FLUSH_PAT)
2872 .await?;
2873 let post_flush_dst = env
2874 .helper_b
2875 .allocate(&env.client, SIZE, BufferDevice::Cpu, 0)
2876 .await?;
2877
2878 let result = env
2879 .helper_a
2880 .submit(
2881 &env.client,
2882 vec![
2883 BufferHelperOp {
2884 op_type: RdmaOpType::WriteFromLocal,
2885 local_buf: good_src_0,
2886 remote_buf: good_dst_0.clone(),
2887 },
2888 BufferHelperOp {
2889 op_type: RdmaOpType::WriteFromLocal,
2890 local_buf: bogus_src,
2891 remote_buf: bogus_remote,
2892 },
2893 BufferHelperOp {
2894 op_type: RdmaOpType::WriteFromLocal,
2895 local_buf: post_flush_src,
2896 remote_buf: post_flush_dst.clone(),
2897 },
2898 ],
2899 10,
2900 )
2901 .await?;
2902 let err = result.expect_err("expected at least one op to fail");
2903 let rem_access = format!(
2904 "status={:?}",
2905 rdmaxcel_sys::ibv_wc_status::IBV_WC_REM_ACCESS_ERR,
2906 );
2907 let wr_flush = format!(
2908 "status={:?}",
2909 rdmaxcel_sys::ibv_wc_status::IBV_WC_WR_FLUSH_ERR,
2910 );
2911 fn op_block(err: &str, op_idx: usize) -> &str {
2914 let header = format!("\n op {op_idx}:\n");
2915 let start = err
2916 .find(&header)
2917 .unwrap_or_else(|| panic!("expected op {op_idx} in error: {err}"));
2918 let block = &err[start + header.len()..];
2919 &block[..block.find("\n op ").unwrap_or(block.len())]
2920 }
2921
2922 assert!(
2923 !err.contains("op 0:"),
2924 "op 0 should not appear in error: {err}",
2925 );
2926 let op1 = op_block(&err, 1);
2927 assert!(
2928 op1.lines().all(|line| line.starts_with(" stripe ")),
2929 "expected op 1 to list one line per failed stripe: {op1}",
2930 );
2931 assert!(
2932 op1.contains("completion failed") && op1.contains(&rem_access),
2933 "expected op 1 to fail with REM_ACCESS_ERR: {op1}",
2934 );
2935 let op2 = op_block(&err, 2);
2936 assert!(
2937 op2.contains("completion failed") && op2.contains(&wr_flush),
2938 "expected op 2 to be flushed with WR_FLUSH_ERR: {op2}",
2939 );
2940
2941 assert_remote_pattern(&env.helper_b, &env.client, good_dst_0, SIZE, GOOD_PAT).await?;
2942 assert_remote_pattern(&env.helper_b, &env.client, post_flush_dst, SIZE, 0).await?;
2944
2945 env.shutdown().await
2946 }
2947}