1use std::collections::HashMap;
16use std::sync::Arc;
17use std::sync::OnceLock;
18use std::time::Duration;
19use std::time::Instant;
20
21use anyhow::Result;
22use async_trait::async_trait;
23use bytes::Bytes;
24use bytes::BytesMut;
25use dashmap::DashMap;
26use hyperactor::Actor;
27use hyperactor::ActorHandle;
28use hyperactor::ActorRef;
29use hyperactor::Context;
30use hyperactor::Endpoint as _;
31use hyperactor::HandleClient;
32use hyperactor::Handler;
33use hyperactor::Instance;
34use hyperactor::OncePortHandle;
35use hyperactor::OncePortRef;
36use hyperactor::PortHandle;
37use hyperactor::RefClient;
38use hyperactor::actor::ActorError;
39use hyperactor::channel;
40use hyperactor::channel::ChannelAddr;
41use hyperactor::channel::ChannelTx;
42use hyperactor::channel::Rx;
43use hyperactor::channel::Tx;
44use hyperactor::context;
45use hyperactor::context::Actor as _;
46use hyperactor_mesh::transport::default_bind_spec;
47use serde::Deserialize;
48use serde::Serialize;
49use serde_multipart::Part;
50use tokio::time::timeout as tokio_timeout;
51use tokio_util::sync::CancellationToken;
52use typeuri::Named;
53
54use super::TcpOp;
55use crate::RdmaOp;
56use crate::RdmaOpType;
57use crate::RdmaTransportLevel;
58use crate::backend::RdmaBackend;
59use crate::backend::RdmaConfig;
60use crate::local_memory::KeepaliveLocalMemory;
61use crate::rdma_manager_actor::GetTcpActorRefClient;
62use crate::rdma_manager_actor::RdmaManagerActor;
63use crate::rdma_manager_actor::RdmaManagerMessageClient;
64
65#[derive(Debug, Clone, Serialize, Deserialize, Named)]
70pub struct TcpChunk(Part);
71wirevalue::register_type!(TcpChunk);
72
73#[derive(Debug, Clone, Serialize, Deserialize, Named)]
75struct TcpDataChunk {
76 transfer_id: usize,
78 offset: usize,
80 data: Part,
81}
82wirevalue::register_type!(TcpDataChunk);
83
84#[derive(Debug)]
89struct TransferState {
90 local_memory: KeepaliveLocalMemory,
92
93 chunks_received: usize,
95
96 total_chunks: usize,
98
99 done: OncePortRef<Result<(), String>>,
102}
103
104impl TransferState {
105 fn new(
106 total_chunks: usize,
107 local_memory: KeepaliveLocalMemory,
108 done: OncePortRef<Result<(), String>>,
109 ) -> Self {
110 Self {
111 local_memory,
112 chunks_received: 0,
113 total_chunks,
114 done,
115 }
116 }
117}
118
119#[derive(Debug, Serialize, Deserialize, Named)]
129struct SendTransferResult {
130 done: OncePortRef<Result<(), String>>,
131 result: Result<(), String>,
132}
133
134#[derive(Debug, Serialize, Deserialize, Named)]
139struct TransferError {
140 message: String,
141}
142
143#[derive(Debug)]
146struct RegisterTransferLocal {
147 local_memory: KeepaliveLocalMemory,
148 total_chunks: usize,
149 done: OncePortRef<Result<(), String>>,
150 reply: OncePortHandle<usize>,
152}
153
154#[derive(Debug)]
157struct ExecuteTransferLocal {
158 transfer_id: usize,
159 local_memory: KeepaliveLocalMemory,
160 chunk_size: usize,
161 dest_addr: ChannelAddr,
162}
163
164#[derive(Handler, HandleClient, RefClient, Debug, Serialize, Deserialize, Named)]
169enum TcpManagerMessage {
170 WriteChunk {
172 buf_id: usize,
173 offset: usize,
174 data: Part,
175 #[reply]
176 reply: OncePortRef<Result<(), String>>,
177 },
178 ReadChunk {
180 buf_id: usize,
181 offset: usize,
182 size: usize,
183 #[reply]
184 reply: OncePortRef<Result<TcpChunk, String>>,
185 },
186 GetChannelAddress {
189 #[reply]
190 reply: OncePortRef<Option<ChannelAddr>>,
191 },
192 RegisterTransferRemote {
195 buf_id: usize,
196 total_chunks: usize,
197 done: OncePortRef<Result<(), String>>,
198 #[reply]
199 reply: OncePortRef<Result<usize, String>>,
200 },
201 ExecuteTransferRemote {
204 transfer_id: usize,
205 buf_id: usize,
206 chunk_size: usize,
207 dest_addr: ChannelAddr,
208 #[reply]
209 reply: OncePortRef<Result<(), String>>,
210 },
211}
212wirevalue::register_type!(TcpManagerMessage);
213
214#[derive(Debug)]
219#[hyperactor::export(
220 handlers = [TcpManagerMessage],
221)]
222pub struct TcpManagerActor {
223 owner: OnceLock<ActorHandle<RdmaManagerActor>>,
224 next_transfer_id: usize,
225 transfers: Arc<DashMap<usize, TransferState>>,
226 channel_addr: Option<ChannelAddr>,
229 outbound: HashMap<ChannelAddr, Vec<Arc<ChannelTx<TcpDataChunk>>>>,
231 cancel: CancellationToken,
233 receiver_done: Option<tokio::sync::oneshot::Receiver<()>>,
235}
236
237impl TcpManagerActor {
238 pub fn new() -> Self {
239 Self {
240 owner: OnceLock::new(),
241 next_transfer_id: 0,
242 transfers: Arc::new(DashMap::new()),
243 channel_addr: None,
244 outbound: HashMap::new(),
245 cancel: CancellationToken::new(),
246 receiver_done: None,
247 }
248 }
249
250 fn register_transfer(
251 &mut self,
252 local_memory: KeepaliveLocalMemory,
253 total_chunks: usize,
254 done: OncePortRef<Result<(), String>>,
255 ) -> usize {
256 let transfer_id = self.next_transfer_id;
257 self.next_transfer_id += 1;
258 self.transfers.insert(
259 transfer_id,
260 TransferState::new(total_chunks, local_memory, done),
261 );
262 transfer_id
263 }
264
265 fn execute_transfer(
266 &mut self,
267 cx: &Context<Self>,
268 transfer_id: usize,
269 local_memory: KeepaliveLocalMemory,
270 chunk_size: usize,
271 dest_addr: ChannelAddr,
272 ) -> Result<()> {
273 let parallelism =
274 hyperactor_config::global::get(crate::config::RDMA_TCP_FALLBACK_PARALLELISM);
275
276 if !self.outbound.contains_key(&dest_addr) {
277 let conns = (0..parallelism)
278 .map(|_| {
279 channel::dial::<TcpDataChunk>(dest_addr.clone())
280 .map(Arc::new)
281 .map_err(anyhow::Error::from)
282 })
283 .collect::<Result<Vec<_>>>()?;
284 self.outbound.insert(dest_addr.clone(), conns);
285 }
286 let conns = self.outbound.get(&dest_addr).unwrap();
287
288 let size = local_memory.size();
289 let total_chunks = size.div_ceil(chunk_size);
290
291 let chunk_index = Arc::new(std::sync::atomic::AtomicUsize::new(0));
292 let error_port: PortHandle<TransferError> = cx.port();
293 let proc = cx.instance().proc().clone();
294 let cancel = self.cancel.clone();
295
296 for conn in conns.clone() {
297 let mem = local_memory.clone();
298 let chunk_index = chunk_index.clone();
299 let error_port = error_port.clone();
300 let proc = proc.clone();
301 let cancel = cancel.clone();
302
303 tokio::spawn(async move {
304 let sender_name = format!(
305 "tcp_chunk_sender_{}",
306 hyperactor_mesh::shortuuid::ShortUuid::generate()
307 );
308 let instance = proc.client(&sender_name);
309
310 loop {
311 if cancel.is_cancelled() {
312 return;
313 }
314
315 let idx = chunk_index.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
316 if idx >= total_chunks {
317 break;
318 }
319
320 let offset = idx * chunk_size;
321 let len = std::cmp::min(chunk_size, size - offset);
322 let mut buf = BytesMut::zeroed(len);
323 if let Err(e) = unsafe { mem.read_at(offset, &mut buf) } {
326 error_port.post(
327 &instance,
328 TransferError {
329 message: format!("read_at failed at offset {offset}: {e}"),
330 },
331 );
332 return;
333 }
334
335 let chunk = TcpDataChunk {
336 transfer_id,
337 offset,
338 data: Part::from(buf.freeze()),
339 };
340
341 if let Err(e) = conn.send(chunk).await {
342 error_port.post(
343 &instance,
344 TransferError {
345 message: format!("failed to send chunk at offset {offset}: {e}"),
346 },
347 );
348 return;
349 }
350 }
351 });
352 }
353
354 Ok(())
355 }
356
357 pub async fn local_handle(
360 client: &(impl context::Actor + Send + Sync),
361 ) -> Result<ActorHandle<Self>, anyhow::Error> {
362 let rdma_handle = RdmaManagerActor::local_handle(client);
363 let tcp_ref = rdma_handle.get_tcp_actor_ref(client).await?;
364 tcp_ref
365 .downcast_handle(client)
366 .ok_or_else(|| anyhow::anyhow!("TcpManagerActor is not in the local process"))
367 }
368}
369
370#[async_trait]
371impl Actor for TcpManagerActor {
372 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
373 this.set_system();
374 let owner = this.parent_handle().ok_or_else(|| {
375 anyhow::anyhow!("RdmaManagerActor not found as parent of TcpManagerActor")
376 })?;
377 self.owner
378 .set(owner)
379 .map_err(|_| anyhow::anyhow!("TcpManagerActor owner already set"))?;
380
381 let parallelism =
382 hyperactor_config::global::get(crate::config::RDMA_TCP_FALLBACK_PARALLELISM);
383 if parallelism > 1 {
384 let addr = match default_bind_spec() {
385 channel::BindSpec::Any(transport) => ChannelAddr::any(transport),
386 channel::BindSpec::Addr(addr) => addr,
387 };
388 let (bound_addr, mut rx) = channel::serve::<TcpDataChunk>(addr)?;
389 self.channel_addr = Some(bound_addr);
390
391 let transfers = self.transfers.clone();
392 let proc = this.proc().clone();
393 let result_port: PortHandle<SendTransferResult> = this.port();
394 let error_port: PortHandle<TransferError> = this.port();
395 let cancel = self.cancel.clone();
396 let receiver_name = format!(
397 "tcp_chunk_receiver_{}",
398 hyperactor_mesh::shortuuid::ShortUuid::generate()
399 );
400
401 let (done_tx, done_rx) = tokio::sync::oneshot::channel::<()>();
402 self.receiver_done = Some(done_rx);
403
404 tokio::spawn(async move {
405 let instance = proc.client(&receiver_name.to_string());
406
407 loop {
408 let chunk = tokio::select! {
409 _ = cancel.cancelled() => break,
410 result = rx.recv() => match result {
411 Ok(chunk) => chunk,
412 Err(e) => {
413 error_port
414 .post(
415 &instance,
416 TransferError {
417 message: format!(
418 "parallel channel receive error: {e}"
419 ),
420 },
421 );
422 break;
423 }
424 },
425 };
426
427 let mut entry = match transfers.get_mut(&chunk.transfer_id) {
428 Some(entry) => entry,
429 None => {
430 tracing::warn!(
431 "received chunk for unknown transfer {:?}",
432 chunk.transfer_id,
433 );
434 continue;
435 }
436 };
437
438 let mut write_offset = chunk.offset;
439 let fragments = chunk.data.into_fragments();
440 let write_err = fragments.iter().find_map(|fragment| {
441 let result = unsafe { entry.local_memory.write_at(write_offset, fragment) };
444 write_offset += fragment.len();
445 result.err()
446 });
447 if let Some(e) = write_err {
448 let transfer_id = chunk.transfer_id;
449 drop(entry);
450 let (_, state) = transfers.remove(&transfer_id).unwrap();
451 result_port.post(
452 &instance,
453 SendTransferResult {
454 done: state.done,
455 result: Err(e.to_string()),
456 },
457 );
458 continue;
459 }
460
461 entry.chunks_received += 1;
462 if entry.chunks_received == entry.total_chunks {
463 let transfer_id = chunk.transfer_id;
464 drop(entry);
465 let (_, state) = transfers.remove(&transfer_id).unwrap();
466 result_port.post(
467 &instance,
468 SendTransferResult {
469 done: state.done,
470 result: Ok(()),
471 },
472 );
473 }
474 }
475 rx.join().await;
476 done_tx.send(()).unwrap();
477 });
478 }
479
480 Ok(())
481 }
482
483 async fn cleanup(
484 &mut self,
485 _this: &Instance<Self>,
486 _err: Option<&ActorError>,
487 ) -> Result<(), anyhow::Error> {
488 self.cancel.cancel();
489 if let Some(done_rx) = self.receiver_done.take() {
490 done_rx.await?;
491 }
492 Ok(())
493 }
494}
495
496#[async_trait]
497#[hyperactor::handle(TcpManagerMessage)]
498impl TcpManagerMessageHandler for TcpManagerActor {
499 async fn write_chunk(
500 &mut self,
501 cx: &Context<Self>,
502 buf_id: usize,
503 offset: usize,
504 data: Part,
505 ) -> Result<Result<(), String>, anyhow::Error> {
506 let owner = self.owner.get().expect("TcpManagerActor owner not set");
507 let mem = match owner.request_local_memory(cx, buf_id).await {
508 Ok(Some(mem)) => mem,
509 Ok(None) => return Ok(Err(format!("buffer {buf_id} not found"))),
510 Err(e) => return Ok(Err(e.to_string())),
511 };
512
513 let bytes = data.into_bytes();
514 if let Err(e) = unsafe { mem.write_at(offset, &bytes) } {
519 return Ok(Err(e.to_string()));
520 }
521
522 Ok(Ok(()))
523 }
524
525 async fn read_chunk(
526 &mut self,
527 cx: &Context<Self>,
528 buf_id: usize,
529 offset: usize,
530 size: usize,
531 ) -> Result<Result<TcpChunk, String>, anyhow::Error> {
532 let owner = self.owner.get().expect("TcpManagerActor owner not set");
533 let mem = match owner.request_local_memory(cx, buf_id).await {
534 Ok(Some(mem)) => mem,
535 Ok(None) => return Ok(Err(format!("buffer {buf_id} not found"))),
536 Err(e) => return Ok(Err(e.to_string())),
537 };
538
539 let mut buf = BytesMut::zeroed(size);
540 if let Err(e) = unsafe { mem.read_at(offset, &mut buf) } {
545 return Ok(Err(e.to_string()));
546 }
547 Ok(Ok(TcpChunk(Part::from(buf.freeze()))))
548 }
549
550 async fn get_channel_address(
551 &mut self,
552 _cx: &Context<Self>,
553 ) -> Result<Option<ChannelAddr>, anyhow::Error> {
554 Ok(self.channel_addr.clone())
555 }
556
557 async fn register_transfer_remote(
558 &mut self,
559 cx: &Context<Self>,
560 buf_id: usize,
561 total_chunks: usize,
562 done: OncePortRef<Result<(), String>>,
563 ) -> Result<Result<usize, String>, anyhow::Error> {
564 let owner = self.owner.get().expect("TcpManagerActor owner not set");
565 let mem = match owner.request_local_memory(cx, buf_id).await {
566 Ok(Some(mem)) => mem,
567 Ok(None) => return Ok(Err(format!("buffer {buf_id} not found"))),
568 Err(e) => return Ok(Err(e.to_string())),
569 };
570 let transfer_id = self.register_transfer(mem, total_chunks, done);
571 Ok(Ok(transfer_id))
572 }
573
574 async fn execute_transfer_remote(
575 &mut self,
576 cx: &Context<Self>,
577 transfer_id: usize,
578 buf_id: usize,
579 chunk_size: usize,
580 dest_addr: ChannelAddr,
581 ) -> Result<Result<(), String>, anyhow::Error> {
582 let owner = self.owner.get().expect("TcpManagerActor owner not set");
583 let mem = match owner.request_local_memory(cx, buf_id).await {
584 Ok(Some(mem)) => mem,
585 Ok(None) => return Ok(Err(format!("buffer {buf_id} not found"))),
586 Err(e) => return Ok(Err(e.to_string())),
587 };
588 self.execute_transfer(cx, transfer_id, mem, chunk_size, dest_addr)?;
589 Ok(Ok(()))
590 }
591}
592
593#[async_trait]
594impl Handler<RegisterTransferLocal> for TcpManagerActor {
595 async fn handle(
596 &mut self,
597 cx: &Context<Self>,
598 message: RegisterTransferLocal,
599 ) -> Result<(), anyhow::Error> {
600 let transfer_id =
601 self.register_transfer(message.local_memory, message.total_chunks, message.done);
602 message.reply.post(cx, transfer_id);
603 Ok(())
604 }
605}
606
607#[async_trait]
608impl Handler<ExecuteTransferLocal> for TcpManagerActor {
609 async fn handle(
610 &mut self,
611 cx: &Context<Self>,
612 message: ExecuteTransferLocal,
613 ) -> Result<(), anyhow::Error> {
614 self.execute_transfer(
615 cx,
616 message.transfer_id,
617 message.local_memory,
618 message.chunk_size,
619 message.dest_addr,
620 )
621 }
622}
623
624#[async_trait]
625impl Handler<SendTransferResult> for TcpManagerActor {
626 async fn handle(
627 &mut self,
628 cx: &Context<Self>,
629 message: SendTransferResult,
630 ) -> Result<(), anyhow::Error> {
631 message.done.post(cx, message.result);
632 Ok(())
633 }
634}
635
636#[async_trait]
637impl Handler<TransferError> for TcpManagerActor {
638 async fn handle(
639 &mut self,
640 _cx: &Context<Self>,
641 message: TransferError,
642 ) -> Result<(), anyhow::Error> {
643 tracing::error!("fatal transfer error: {}", message.message);
644 Err(anyhow::anyhow!(message.message))
645 }
646}
647
648#[derive(Debug, Clone)]
656pub struct TcpBackend(pub ActorHandle<TcpManagerActor>);
657
658impl std::ops::Deref for TcpBackend {
659 type Target = ActorHandle<TcpManagerActor>;
660 fn deref(&self) -> &Self::Target {
661 &self.0
662 }
663}
664
665impl TcpBackend {
666 async fn execute_parallel_write(
669 &self,
670 cx: &(impl context::Actor + Send + Sync),
671 op: &TcpOp,
672 chunk_size: usize,
673 deadline: Instant,
674 ) -> Result<()> {
675 if op.local_memory.size() > op.remote_size {
678 anyhow::bail!(
679 "remote buffer size ({}) is smaller than local buffer size ({})",
680 op.remote_size,
681 op.local_memory.size(),
682 );
683 }
684 let size = op.local_memory.size();
685 let total_chunks = size.div_ceil(chunk_size);
686
687 let (done_handle, done_rx) = hyperactor::mailbox::open_once_port::<Result<(), String>>(cx);
688 let done_ref = done_handle.bind();
689
690 let remaining = deadline.saturating_duration_since(Instant::now());
691 let transfer_id = tokio_timeout(
692 remaining,
693 op.remote_tcp_manager.register_transfer_remote(
694 cx,
695 op.remote_buf_id,
696 total_chunks,
697 done_ref,
698 ),
699 )
700 .await
701 .map_err(|_| anyhow::anyhow!("register_transfer_remote timed out"))??
702 .map_err(|e| anyhow::anyhow!(e))?;
703
704 let dest_addr = tokio_timeout(
705 deadline.saturating_duration_since(Instant::now()),
706 op.remote_tcp_manager.get_channel_address(cx),
707 )
708 .await
709 .map_err(|_| anyhow::anyhow!("get_channel_address timed out"))??
710 .ok_or_else(|| anyhow::anyhow!("remote does not have parallel channels enabled"))?;
711
712 self.0.post(
713 cx,
714 ExecuteTransferLocal {
715 transfer_id,
716 local_memory: op.local_memory.clone(),
717 chunk_size,
718 dest_addr,
719 },
720 );
721
722 let remaining = deadline.saturating_duration_since(Instant::now());
723 let result = tokio_timeout(remaining, done_rx.recv())
724 .await
725 .map_err(|_| anyhow::anyhow!("parallel write timed out"))?
726 .map_err(|e| anyhow::anyhow!(e))?;
727 result.map_err(|e| anyhow::anyhow!(e))
728 }
729
730 async fn execute_parallel_read(
733 &self,
734 cx: &(impl context::Actor + Send + Sync),
735 op: &TcpOp,
736 chunk_size: usize,
737 deadline: Instant,
738 ) -> Result<()> {
739 if op.remote_size > op.local_memory.size() {
742 anyhow::bail!(
743 "remote buffer size ({}) is larger than local buffer size ({})",
744 op.remote_size,
745 op.local_memory.size(),
746 );
747 }
748 let size = op.remote_size;
749 let total_chunks = size.div_ceil(chunk_size);
750
751 let (done_handle, done_rx) = hyperactor::mailbox::open_once_port::<Result<(), String>>(cx);
752 let done_ref = done_handle.bind();
753
754 let (id_handle, id_rx) = hyperactor::mailbox::open_once_port::<usize>(cx);
755
756 self.0.post(
757 cx,
758 RegisterTransferLocal {
759 local_memory: op.local_memory.clone(),
760 total_chunks,
761 done: done_ref,
762 reply: id_handle,
763 },
764 );
765
766 let transfer_id = id_rx
767 .recv()
768 .await
769 .map_err(|e| anyhow::anyhow!("failed to receive transfer id: {e}"))?;
770
771 let my_channel_addr = self
772 .0
773 .get_channel_address(cx)
774 .await?
775 .ok_or_else(|| anyhow::anyhow!("local parallel channels not enabled"))?;
776
777 let remaining = deadline.saturating_duration_since(Instant::now());
778 tokio_timeout(
779 remaining,
780 op.remote_tcp_manager.execute_transfer_remote(
781 cx,
782 transfer_id,
783 op.remote_buf_id,
784 chunk_size,
785 my_channel_addr,
786 ),
787 )
788 .await
789 .map_err(|_| anyhow::anyhow!("execute_transfer_remote timed out"))??
790 .map_err(|e| anyhow::anyhow!(e))?;
791
792 let remaining = deadline.saturating_duration_since(Instant::now());
793 let result = tokio_timeout(remaining, done_rx.recv())
794 .await
795 .map_err(|_| anyhow::anyhow!("parallel read timed out"))?
796 .map_err(|e| anyhow::anyhow!(e))?;
797 result.map_err(|e| anyhow::anyhow!(e))
798 }
799
800 async fn execute_write(
803 &self,
804 cx: &(impl context::Actor + Send + Sync),
805 op: &TcpOp,
806 chunk_size: usize,
807 deadline: Instant,
808 ) -> Result<()> {
809 if op.local_memory.size() > op.remote_size {
812 anyhow::bail!(
813 "remote buffer size ({}) is smaller than local buffer size ({})",
814 op.remote_size,
815 op.local_memory.size(),
816 );
817 }
818 let size = op.local_memory.size();
819 let mut offset = 0;
820
821 while offset < size {
822 let remaining = deadline.saturating_duration_since(Instant::now());
823 if remaining.is_zero() {
824 anyhow::bail!("tcp write timed out");
825 }
826
827 let len = std::cmp::min(chunk_size, size - offset);
828
829 let mut buf = vec![0u8; len];
830 unsafe { op.local_memory.read_at(offset, &mut buf) }?;
834 let data = Part::from(Bytes::from(buf));
835
836 tokio_timeout(
837 remaining,
838 op.remote_tcp_manager
839 .write_chunk(cx, op.remote_buf_id, offset, data),
840 )
841 .await
842 .map_err(|_| anyhow::anyhow!("tcp write chunk timed out"))??
843 .map_err(|e| anyhow::anyhow!(e))?;
844
845 offset += len;
846 }
847
848 Ok(())
849 }
850
851 async fn execute_read(
854 &self,
855 cx: &(impl context::Actor + Send + Sync),
856 op: &TcpOp,
857 chunk_size: usize,
858 deadline: Instant,
859 ) -> Result<()> {
860 if op.remote_size > op.local_memory.size() {
863 anyhow::bail!(
864 "remote buffer size ({}) is larger than local buffer size ({})",
865 op.remote_size,
866 op.local_memory.size(),
867 );
868 }
869 let size = op.remote_size;
870 let mut offset = 0;
871
872 while offset < size {
873 let remaining = deadline.saturating_duration_since(Instant::now());
874 if remaining.is_zero() {
875 anyhow::bail!("tcp read timed out");
876 }
877
878 let len = std::cmp::min(chunk_size, size - offset);
879
880 let chunk = tokio_timeout(
881 remaining,
882 op.remote_tcp_manager
883 .read_chunk(cx, op.remote_buf_id, offset, len),
884 )
885 .await
886 .map_err(|_| anyhow::anyhow!("tcp read chunk timed out"))??
887 .map_err(|e| anyhow::anyhow!(e))?;
888 let data = chunk.0.into_bytes();
889
890 anyhow::ensure!(
891 data.len() == len,
892 "tcp read chunk size mismatch: expected {len}, got {}",
893 data.len()
894 );
895
896 unsafe { op.local_memory.write_at(offset, &data) }?;
901
902 offset += len;
903 }
904
905 Ok(())
906 }
907}
908
909#[async_trait]
910impl RdmaBackend for TcpBackend {
911 type RemoteBackendContext = ActorRef<TcpManagerActor>;
912 type TransportInfo = ();
913
914 fn available() -> bool {
916 hyperactor_config::global::get(crate::config::RDMA_ALLOW_TCP_FALLBACK)
917 }
918
919 fn transport_level(&self) -> RdmaTransportLevel {
920 RdmaTransportLevel::Tcp
921 }
922
923 fn transport_info(&self) -> Option<Self::TransportInfo> {
924 None
925 }
926
927 async fn spawn(cx: &(impl context::Actor + Send + Sync), _config: &RdmaConfig) -> Result<Self> {
928 Ok(TcpBackend(cx.spawn(TcpManagerActor::new())))
929 }
930
931 async fn register_remote_buffer(
933 &self,
934 _cx: &(impl context::Actor + Send + Sync),
935 _remote_buf_id: usize,
936 _local: KeepaliveLocalMemory,
937 ) -> Result<ActorRef<TcpManagerActor>> {
938 Ok(self.0.bind())
939 }
940
941 async fn release_buffer(
942 &self,
943 _cx: &(impl context::Actor + Send + Sync),
944 _remote_buf_id: usize,
945 ) -> Result<()> {
946 Ok(())
947 }
948
949 async fn submit(
954 &self,
955 cx: &(impl context::Actor + Send + Sync),
956 ops: Vec<RdmaOp>,
957 timeout: Duration,
958 ) -> Result<()> {
959 let chunk_size =
960 hyperactor_config::global::get(crate::config::RDMA_MAX_CHUNK_SIZE_MB) * 1024 * 1024;
961 let parallelism =
962 hyperactor_config::global::get(crate::config::RDMA_TCP_FALLBACK_PARALLELISM);
963 let deadline = Instant::now() + timeout;
964
965 for op in ops {
966 let remaining = deadline.saturating_duration_since(Instant::now());
967 if remaining.is_zero() {
968 anyhow::bail!("tcp submit timed out");
969 }
970
971 let remote_tcp_manager = op
972 .remote
973 .resolve_tcp()
974 .expect("op routed to incompatible backend");
975 let tcp_op = TcpOp {
976 op_type: op.op_type,
977 remote_buf_id: op.remote.id,
978 remote_size: op.remote.size,
979 local_memory: op.local,
980 remote_tcp_manager,
981 };
982
983 if parallelism > 1 {
984 match tcp_op.op_type {
985 RdmaOpType::WriteFromLocal => {
986 self.execute_parallel_write(cx, &tcp_op, chunk_size, deadline)
987 .await?;
988 }
989 RdmaOpType::ReadIntoLocal => {
990 self.execute_parallel_read(cx, &tcp_op, chunk_size, deadline)
991 .await?;
992 }
993 }
994 } else {
995 match tcp_op.op_type {
996 RdmaOpType::WriteFromLocal => {
997 self.execute_write(cx, &tcp_op, chunk_size, deadline)
998 .await?;
999 }
1000 RdmaOpType::ReadIntoLocal => {
1001 self.execute_read(cx, &tcp_op, chunk_size, deadline).await?;
1002 }
1003 }
1004 }
1005 }
1006
1007 Ok(())
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use std::sync::Arc;
1014 use std::sync::atomic::AtomicUsize;
1015 use std::sync::atomic::Ordering;
1016 use std::time::Duration;
1017
1018 use hyperactor::ActorEnvironment;
1019 use hyperactor::ActorHandle;
1020 use hyperactor::Proc;
1021 use hyperactor::RemoteSpawn;
1022 use hyperactor::channel::ChannelAddr;
1023
1024 use super::TcpBackend;
1025 use super::TcpManagerActor;
1026 use crate::RdmaManagerMessageClient;
1027 use crate::RdmaOp;
1028 use crate::RdmaOpType;
1029 use crate::backend::RdmaBackend;
1030 use crate::local_memory::KeepaliveLocalMemory;
1031 use crate::rdma_manager_actor::GetTcpActorRefClient;
1032 use crate::rdma_manager_actor::RdmaManagerActor;
1033
1034 static COUNTER: AtomicUsize = AtomicUsize::new(0);
1035
1036 struct TcpTestProcEnv {
1037 proc: Proc,
1038 rdma_handle: ActorHandle<RdmaManagerActor>,
1039 instance: hyperactor::Client,
1040 tcp_backend: TcpBackend,
1041 rdma_remote_buf: crate::RdmaRemoteBuffer,
1042 local_memory: KeepaliveLocalMemory,
1043 }
1044
1045 impl Drop for TcpTestProcEnv {
1046 fn drop(&mut self) {
1047 use crate::rdma_manager_actor::ReleaseBufferClient;
1048 tokio::task::block_in_place(|| {
1051 tokio::runtime::Handle::current()
1052 .block_on(
1053 self.rdma_remote_buf
1054 .owner
1055 .release_buffer(&self.instance, self.rdma_remote_buf.id),
1056 )
1057 .expect("failed to release buffer in TcpTestProcEnv drop");
1058 });
1059 }
1060 }
1061
1062 impl TcpTestProcEnv {
1063 async fn new(buffer_size: usize) -> anyhow::Result<Self> {
1065 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1066 let proc = Proc::direct(
1067 ChannelAddr::any(hyperactor::channel::ChannelTransport::Unix),
1068 format!("tcp_test_{id}"),
1069 )?;
1070 let instance = proc.client("client");
1071
1072 let rdma_actor = RdmaManagerActor::new(None, &ActorEnvironment::default()).await?;
1073 let rdma_handle = proc.spawn(rdma_actor);
1074
1075 let tcp_ref = rdma_handle.get_tcp_actor_ref(&instance).await?;
1076 let tcp_backend = TcpBackend(
1077 tcp_ref
1078 .downcast_handle(&instance)
1079 .ok_or_else(|| anyhow::anyhow!("tcp actor not local"))?,
1080 );
1081
1082 let (local_memory, rdma_remote_buf) =
1083 Self::alloc_cpu_buffer(&instance, &rdma_handle, buffer_size).await?;
1084
1085 Ok(Self {
1086 proc,
1087 rdma_handle,
1088 instance,
1089 tcp_backend,
1090 rdma_remote_buf,
1091 local_memory,
1092 })
1093 }
1094
1095 async fn on_proc(
1097 proc: &Proc,
1098 rdma_handle: &ActorHandle<RdmaManagerActor>,
1099 tcp_backend: TcpBackend,
1100 buffer_size: usize,
1101 ) -> anyhow::Result<Self> {
1102 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1103 let instance = proc.client(&format!("client_{id}"));
1104
1105 let (local_memory, rdma_remote_buf) =
1106 Self::alloc_cpu_buffer(&instance, rdma_handle, buffer_size).await?;
1107
1108 Ok(Self {
1109 proc: proc.clone(),
1110 rdma_handle: rdma_handle.clone(),
1111 instance,
1112 tcp_backend,
1113 rdma_remote_buf,
1114 local_memory,
1115 })
1116 }
1117
1118 async fn alloc_cpu_buffer(
1119 instance: &hyperactor::Client,
1120 rdma_handle: &ActorHandle<RdmaManagerActor>,
1121 buffer_size: usize,
1122 ) -> anyhow::Result<(KeepaliveLocalMemory, crate::RdmaRemoteBuffer)> {
1123 let cpu_buf = vec![0u8; buffer_size].into_boxed_slice();
1124 let local_memory = KeepaliveLocalMemory::try_new(Arc::new(cpu_buf))?;
1125 let rdma_remote_buf = rdma_handle
1126 .request_buffer(instance, local_memory.clone())
1127 .await?;
1128 Ok((local_memory, rdma_remote_buf))
1129 }
1130 }
1131
1132 async fn setup_tcp_env(buf_size: usize) -> anyhow::Result<Vec<TcpTestProcEnv>> {
1134 Ok(vec![
1135 TcpTestProcEnv::new(buf_size).await?,
1136 TcpTestProcEnv::new(buf_size).await?,
1137 ])
1138 }
1139
1140 async fn setup_same_proc_tcp_env(buf_size: usize) -> anyhow::Result<Vec<TcpTestProcEnv>> {
1142 let first = TcpTestProcEnv::new(buf_size).await?;
1143 let second = TcpTestProcEnv::on_proc(
1144 &first.proc,
1145 &first.rdma_handle,
1146 first.tcp_backend.clone(),
1147 buf_size,
1148 )
1149 .await?;
1150 Ok(vec![first, second])
1151 }
1152
1153 async fn setup_tcp_env_pairs(buf_size: usize) -> anyhow::Result<Vec<TcpTestProcEnv>> {
1156 let e0 = TcpTestProcEnv::new(buf_size).await?;
1157 let e1 = TcpTestProcEnv::new(buf_size).await?;
1158 let e2 =
1159 TcpTestProcEnv::on_proc(&e0.proc, &e0.rdma_handle, e0.tcp_backend.clone(), buf_size)
1160 .await?;
1161 let e3 =
1162 TcpTestProcEnv::on_proc(&e1.proc, &e1.rdma_handle, e1.tcp_backend.clone(), buf_size)
1163 .await?;
1164 Ok(vec![e0, e1, e2, e3])
1165 }
1166
1167 fn test_write(mem: &KeepaliveLocalMemory, offset: usize, src: &[u8]) -> anyhow::Result<()> {
1176 unsafe { mem.write_at(offset, src) }
1178 }
1179
1180 fn test_read(mem: &KeepaliveLocalMemory, offset: usize, dst: &mut [u8]) -> anyhow::Result<()> {
1183 unsafe { mem.read_at(offset, dst) }
1185 }
1186
1187 async fn do_write_test(
1189 envs: &mut [TcpTestProcEnv],
1190 buf_size: usize,
1191 timeout: Duration,
1192 ) -> anyhow::Result<()> {
1193 let mut src = vec![0u8; buf_size];
1194 for (i, byte) in src.iter_mut().enumerate() {
1195 *byte = (i % 256) as u8;
1196 }
1197 test_write(&envs[0].local_memory, 0, &src)?;
1198
1199 let remote = envs[1].rdma_remote_buf.clone();
1200 let env = &mut envs[0];
1201 env.tcp_backend
1202 .submit(
1203 &env.instance,
1204 vec![RdmaOp {
1205 op_type: RdmaOpType::WriteFromLocal,
1206 local: env.local_memory.clone(),
1207 remote,
1208 }],
1209 timeout,
1210 )
1211 .await?;
1212
1213 let mut dst = vec![0u8; buf_size];
1214 test_read(&envs[1].local_memory, 0, &mut dst)?;
1215 for (i, byte) in dst.iter().enumerate() {
1216 assert_eq!(*byte, (i % 256) as u8, "mismatch at offset {i} after write");
1217 }
1218 Ok(())
1219 }
1220
1221 async fn do_read_test(
1223 envs: &mut [TcpTestProcEnv],
1224 buf_size: usize,
1225 timeout: Duration,
1226 ) -> anyhow::Result<()> {
1227 let mut src = vec![0u8; buf_size];
1228 for (i, byte) in src.iter_mut().enumerate() {
1229 *byte = ((i * 7 + 3) % 256) as u8;
1230 }
1231 test_write(&envs[1].local_memory, 0, &src)?;
1232
1233 let remote = envs[1].rdma_remote_buf.clone();
1234 let env = &mut envs[0];
1235 env.tcp_backend
1236 .submit(
1237 &env.instance,
1238 vec![RdmaOp {
1239 op_type: RdmaOpType::ReadIntoLocal,
1240 local: env.local_memory.clone(),
1241 remote,
1242 }],
1243 timeout,
1244 )
1245 .await?;
1246
1247 let mut dst = vec![0u8; buf_size];
1248 test_read(&envs[0].local_memory, 0, &mut dst)?;
1249 for (i, byte) in dst.iter().enumerate() {
1250 assert_eq!(
1251 *byte,
1252 ((i * 7 + 3) % 256) as u8,
1253 "mismatch at offset {i} after read"
1254 );
1255 }
1256 Ok(())
1257 }
1258
1259 async fn do_round_trip_test(
1261 envs: &mut [TcpTestProcEnv],
1262 buf_size: usize,
1263 timeout: Duration,
1264 ) -> anyhow::Result<()> {
1265 let mut src = vec![0u8; buf_size];
1266 for (i, byte) in src.iter_mut().enumerate() {
1267 *byte = ((i * 13 + 5) % 256) as u8;
1268 }
1269 test_write(&envs[0].local_memory, 0, &src)?;
1270
1271 let remote = envs[1].rdma_remote_buf.clone();
1272 let env = &mut envs[0];
1273 env.tcp_backend
1274 .submit(
1275 &env.instance,
1276 vec![RdmaOp {
1277 op_type: RdmaOpType::WriteFromLocal,
1278 local: env.local_memory.clone(),
1279 remote: remote.clone(),
1280 }],
1281 timeout,
1282 )
1283 .await?;
1284
1285 test_write(&envs[0].local_memory, 0, &vec![0u8; buf_size])?;
1286
1287 let env = &mut envs[0];
1288 env.tcp_backend
1289 .submit(
1290 &env.instance,
1291 vec![RdmaOp {
1292 op_type: RdmaOpType::ReadIntoLocal,
1293 local: env.local_memory.clone(),
1294 remote,
1295 }],
1296 timeout,
1297 )
1298 .await?;
1299
1300 let mut dst = vec![0u8; buf_size];
1301 test_read(&envs[0].local_memory, 0, &mut dst)?;
1302 for (i, byte) in dst.iter().enumerate() {
1303 assert_eq!(
1304 *byte,
1305 ((i * 13 + 5) % 256) as u8,
1306 "mismatch at offset {i} after round-trip"
1307 );
1308 }
1309 Ok(())
1310 }
1311
1312 #[timed_test::async_timed_test(timeout_secs = 30)]
1316 async fn test_tcp_write_from_local() -> anyhow::Result<()> {
1317 let config = hyperactor_config::global::lock();
1318 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1319
1320 let mut envs = setup_tcp_env(4096).await?;
1321 do_write_test(&mut envs, 4096, Duration::from_secs(30)).await
1322 }
1323
1324 #[timed_test::async_timed_test(timeout_secs = 30)]
1326 async fn test_tcp_read_into_local() -> anyhow::Result<()> {
1327 let config = hyperactor_config::global::lock();
1328 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1329
1330 let mut envs = setup_tcp_env(2048).await?;
1331 do_read_test(&mut envs, 2048, Duration::from_secs(30)).await
1332 }
1333
1334 #[timed_test::async_timed_test(timeout_secs = 30)]
1336 async fn test_tcp_write_then_read_back() -> anyhow::Result<()> {
1337 let config = hyperactor_config::global::lock();
1338 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1339
1340 let mut envs = setup_tcp_env(4096).await?;
1341 do_round_trip_test(&mut envs, 4096, Duration::from_secs(30)).await
1342 }
1343
1344 #[timed_test::async_timed_test(timeout_secs = 30)]
1346 async fn test_tcp_multi_chunk_write() -> anyhow::Result<()> {
1347 let config = hyperactor_config::global::lock();
1348 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1349 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1350
1351 let buf_size = 3 * 1024 * 512;
1352 let mut envs = setup_tcp_env(buf_size).await?;
1353
1354 let mut src = vec![0u8; buf_size];
1355 for (i, byte) in src.iter_mut().enumerate() {
1356 *byte = (i % 251) as u8;
1357 }
1358 test_write(&envs[0].local_memory, 0, &src)?;
1359
1360 let remote = envs[1].rdma_remote_buf.clone();
1361 let env = &mut envs[0];
1362 env.tcp_backend
1363 .submit(
1364 &env.instance,
1365 vec![RdmaOp {
1366 op_type: RdmaOpType::WriteFromLocal,
1367 local: env.local_memory.clone(),
1368 remote,
1369 }],
1370 Duration::from_secs(30),
1371 )
1372 .await?;
1373
1374 let mut dst = vec![0u8; buf_size];
1375 test_read(&envs[1].local_memory, 0, &mut dst)?;
1376 for (i, byte) in dst.iter().enumerate() {
1377 assert_eq!(*byte, (i % 251) as u8, "mismatch at offset {i}");
1378 }
1379
1380 Ok(())
1381 }
1382
1383 #[timed_test::async_timed_test(timeout_secs = 30)]
1385 async fn test_tcp_multi_chunk_read() -> anyhow::Result<()> {
1386 let config = hyperactor_config::global::lock();
1387 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1388 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1389
1390 let buf_size = 3 * 1024 * 512;
1391 let mut envs = setup_tcp_env(buf_size).await?;
1392
1393 let mut src = vec![0u8; buf_size];
1394 for (i, byte) in src.iter_mut().enumerate() {
1395 *byte = ((i * 3 + 17) % 256) as u8;
1396 }
1397 test_write(&envs[1].local_memory, 0, &src)?;
1398
1399 let remote = envs[1].rdma_remote_buf.clone();
1400 let env = &mut envs[0];
1401 env.tcp_backend
1402 .submit(
1403 &env.instance,
1404 vec![RdmaOp {
1405 op_type: RdmaOpType::ReadIntoLocal,
1406 local: env.local_memory.clone(),
1407 remote,
1408 }],
1409 Duration::from_secs(30),
1410 )
1411 .await?;
1412
1413 let mut dst = vec![0u8; buf_size];
1414 test_read(&envs[0].local_memory, 0, &mut dst)?;
1415 for (i, byte) in dst.iter().enumerate() {
1416 assert_eq!(*byte, ((i * 3 + 17) % 256) as u8, "mismatch at offset {i}");
1417 }
1418
1419 Ok(())
1420 }
1421
1422 #[timed_test::async_timed_test(timeout_secs = 30)]
1424 async fn test_tcp_multi_chunk_round_trip() -> anyhow::Result<()> {
1425 let config = hyperactor_config::global::lock();
1426 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1427 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1428
1429 let buf_size = 5 * 1024 * 512;
1430 let mut envs = setup_tcp_env(buf_size).await?;
1431
1432 let mut src = vec![0u8; buf_size];
1433 for (i, byte) in src.iter_mut().enumerate() {
1434 *byte = ((i * 41 + 7) % 256) as u8;
1435 }
1436 test_write(&envs[0].local_memory, 0, &src)?;
1437
1438 let remote = envs[1].rdma_remote_buf.clone();
1439 let env = &mut envs[0];
1440 env.tcp_backend
1441 .submit(
1442 &env.instance,
1443 vec![RdmaOp {
1444 op_type: RdmaOpType::WriteFromLocal,
1445 local: env.local_memory.clone(),
1446 remote: remote.clone(),
1447 }],
1448 Duration::from_secs(30),
1449 )
1450 .await?;
1451
1452 test_write(&envs[0].local_memory, 0, &vec![0u8; buf_size])?;
1453
1454 let env = &mut envs[0];
1455 env.tcp_backend
1456 .submit(
1457 &env.instance,
1458 vec![RdmaOp {
1459 op_type: RdmaOpType::ReadIntoLocal,
1460 local: env.local_memory.clone(),
1461 remote,
1462 }],
1463 Duration::from_secs(30),
1464 )
1465 .await?;
1466
1467 let mut dst = vec![0u8; buf_size];
1468 test_read(&envs[0].local_memory, 0, &mut dst)?;
1469 for (i, byte) in dst.iter().enumerate() {
1470 assert_eq!(
1471 *byte,
1472 ((i * 41 + 7) % 256) as u8,
1473 "mismatch at offset {i} after multi-chunk round-trip"
1474 );
1475 }
1476
1477 Ok(())
1478 }
1479
1480 #[timed_test::async_timed_test(timeout_secs = 30)]
1482 async fn test_tcp_resolve_tcp() -> anyhow::Result<()> {
1483 let config = hyperactor_config::global::lock();
1484 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1485
1486 let envs = setup_tcp_env(64).await?;
1487
1488 for (i, env) in envs.iter().enumerate() {
1489 let tcp_ref = env
1490 .rdma_remote_buf
1491 .resolve_tcp()
1492 .unwrap_or_else(|| panic!("tcp backend not found for env {i}"));
1493 let expected: hyperactor::ActorRef<TcpManagerActor> = env.tcp_backend.bind();
1494 assert_eq!(tcp_ref.actor_addr(), expected.actor_addr());
1495 }
1496
1497 Ok(())
1498 }
1499
1500 #[timed_test::async_timed_test(timeout_secs = 30)]
1502 async fn test_tcp_write_to_released_buffer() -> anyhow::Result<()> {
1503 let config = hyperactor_config::global::lock();
1504 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1505
1506 let buf_size = 64;
1507 let mut envs = setup_tcp_env(buf_size).await?;
1508
1509 let mut src = vec![0u8; buf_size];
1510 for (i, byte) in src.iter_mut().enumerate() {
1511 *byte = (i % 256) as u8;
1512 }
1513 test_write(&envs[0].local_memory, 0, &src)?;
1514
1515 let remote = envs[1].rdma_remote_buf.clone();
1517 let env = &mut envs[0];
1518 env.tcp_backend
1519 .submit(
1520 &env.instance,
1521 vec![RdmaOp {
1522 op_type: RdmaOpType::WriteFromLocal,
1523 local: env.local_memory.clone(),
1524 remote: remote.clone(),
1525 }],
1526 Duration::from_secs(10),
1527 )
1528 .await?;
1529
1530 use crate::rdma_manager_actor::ReleaseBufferClient;
1532 let owner_ref = envs[1].rdma_remote_buf.owner.clone();
1533 owner_ref
1534 .release_buffer(&envs[0].instance, envs[1].rdma_remote_buf.id)
1535 .await?;
1536
1537 let env = &mut envs[0];
1539 let result = env
1540 .tcp_backend
1541 .submit(
1542 &env.instance,
1543 vec![RdmaOp {
1544 op_type: RdmaOpType::WriteFromLocal,
1545 local: env.local_memory.clone(),
1546 remote: remote.clone(),
1547 }],
1548 Duration::from_secs(10),
1549 )
1550 .await;
1551 assert!(result.is_err(), "expected error writing to released buffer");
1552
1553 Ok(())
1554 }
1555
1556 #[timed_test::async_timed_test(timeout_secs = 30)]
1558 async fn test_tcp_read_from_released_buffer() -> anyhow::Result<()> {
1559 let config = hyperactor_config::global::lock();
1560 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1561
1562 let buf_size = 64;
1563 let mut envs = setup_tcp_env(buf_size).await?;
1564
1565 use crate::rdma_manager_actor::ReleaseBufferClient;
1567 let owner_ref = envs[1].rdma_remote_buf.owner.clone();
1568 owner_ref
1569 .release_buffer(&envs[0].instance, envs[1].rdma_remote_buf.id)
1570 .await?;
1571
1572 let remote = envs[1].rdma_remote_buf.clone();
1574 let env = &mut envs[0];
1575 let result = env
1576 .tcp_backend
1577 .submit(
1578 &env.instance,
1579 vec![RdmaOp {
1580 op_type: RdmaOpType::ReadIntoLocal,
1581 local: env.local_memory.clone(),
1582 remote,
1583 }],
1584 Duration::from_secs(10),
1585 )
1586 .await;
1587 assert!(
1588 result.is_err(),
1589 "expected error reading from released buffer"
1590 );
1591
1592 Ok(())
1593 }
1594
1595 #[timed_test::async_timed_test(timeout_secs = 30)]
1599 async fn test_tcp_same_process_write() -> anyhow::Result<()> {
1600 let config = hyperactor_config::global::lock();
1601 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1602
1603 let mut envs = setup_same_proc_tcp_env(4096).await?;
1604 do_write_test(&mut envs, 4096, Duration::from_secs(10)).await
1605 }
1606
1607 #[timed_test::async_timed_test(timeout_secs = 30)]
1609 async fn test_tcp_same_process_read() -> anyhow::Result<()> {
1610 let config = hyperactor_config::global::lock();
1611 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1612
1613 let mut envs = setup_same_proc_tcp_env(2048).await?;
1614 do_read_test(&mut envs, 2048, Duration::from_secs(10)).await
1615 }
1616
1617 #[timed_test::async_timed_test(timeout_secs = 30)]
1619 async fn test_tcp_same_process_round_trip() -> anyhow::Result<()> {
1620 let config = hyperactor_config::global::lock();
1621 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1622
1623 let mut envs = setup_same_proc_tcp_env(4096).await?;
1624 do_round_trip_test(&mut envs, 4096, Duration::from_secs(10)).await
1625 }
1626
1627 use crate::backend::cuda_test_utils::CudaAllocator;
1630 use crate::backend::cuda_test_utils::cuda_device_count;
1631
1632 impl TcpTestProcEnv {
1633 async fn new_gpu(device: i32, buffer_size: usize) -> anyhow::Result<Self> {
1635 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1636 let proc = Proc::direct(
1637 ChannelAddr::any(hyperactor::channel::ChannelTransport::Unix),
1638 format!("tcp_gpu_test_{id}"),
1639 )?;
1640 let instance = proc.client("client");
1641
1642 let rdma_actor = RdmaManagerActor::new(None, &ActorEnvironment::default()).await?;
1643 let rdma_handle = proc.spawn(rdma_actor);
1644
1645 let tcp_ref = rdma_handle.get_tcp_actor_ref(&instance).await?;
1646 let tcp_backend = TcpBackend(
1647 tcp_ref
1648 .downcast_handle(&instance)
1649 .ok_or_else(|| anyhow::anyhow!("tcp actor not local"))?,
1650 );
1651
1652 let alloc = CudaAllocator::get().allocate(device, buffer_size, buffer_size);
1653 let local_memory = KeepaliveLocalMemory::try_new(Arc::new(alloc))?;
1654 let rdma_remote_buf = rdma_handle
1655 .request_buffer(&instance, local_memory.clone())
1656 .await?;
1657
1658 Ok(Self {
1659 proc,
1660 rdma_handle,
1661 instance,
1662 tcp_backend,
1663 rdma_remote_buf,
1664 local_memory,
1665 })
1666 }
1667 }
1668
1669 #[timed_test::async_timed_test(timeout_secs = 60)]
1671 async fn test_tcp_write_multi_gpu() -> anyhow::Result<()> {
1672 if cuda_device_count() < 2 {
1673 println!("Skipping: need at least 2 CUDA devices");
1674 return Ok(());
1675 }
1676
1677 let config = hyperactor_config::global::lock();
1678 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1679
1680 let buf_size = 2 * 1024 * 1024;
1681 let mut envs = vec![
1682 TcpTestProcEnv::new_gpu(0, buf_size).await?,
1683 TcpTestProcEnv::new_gpu(1, buf_size).await?,
1684 ];
1685 do_write_test(&mut envs, buf_size, Duration::from_secs(30)).await
1686 }
1687
1688 #[timed_test::async_timed_test(timeout_secs = 60)]
1690 async fn test_tcp_read_multi_gpu() -> anyhow::Result<()> {
1691 if cuda_device_count() < 2 {
1692 println!("Skipping: need at least 2 CUDA devices");
1693 return Ok(());
1694 }
1695
1696 let config = hyperactor_config::global::lock();
1697 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1698
1699 let buf_size = 2 * 1024 * 1024;
1700 let mut envs = vec![
1701 TcpTestProcEnv::new_gpu(0, buf_size).await?,
1702 TcpTestProcEnv::new_gpu(1, buf_size).await?,
1703 ];
1704 do_read_test(&mut envs, buf_size, Duration::from_secs(30)).await
1705 }
1706
1707 #[timed_test::async_timed_test(timeout_secs = 60)]
1709 async fn test_tcp_round_trip_multi_gpu() -> anyhow::Result<()> {
1710 if cuda_device_count() < 2 {
1711 println!("Skipping: need at least 2 CUDA devices");
1712 return Ok(());
1713 }
1714
1715 let config = hyperactor_config::global::lock();
1716 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1717
1718 let buf_size = 2 * 1024 * 1024;
1719 let mut envs = vec![
1720 TcpTestProcEnv::new_gpu(0, buf_size).await?,
1721 TcpTestProcEnv::new_gpu(1, buf_size).await?,
1722 ];
1723 do_round_trip_test(&mut envs, buf_size, Duration::from_secs(30)).await
1724 }
1725
1726 #[timed_test::async_timed_test(timeout_secs = 30)]
1729 async fn test_tcp_parallel_clean_shutdown() -> anyhow::Result<()> {
1730 let config = hyperactor_config::global::lock();
1731 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1732 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1733 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1734
1735 let buf_size = 3 * 1024 * 1024;
1736 let mut envs = setup_tcp_env(buf_size).await?;
1737
1738 let mut src = vec![0u8; buf_size];
1740 for (i, byte) in src.iter_mut().enumerate() {
1741 *byte = (i % 256) as u8;
1742 }
1743 test_write(&envs[0].local_memory, 0, &src)?;
1744 let remote = envs[1].rdma_remote_buf.clone();
1745 let env = &mut envs[0];
1746 env.tcp_backend
1747 .submit(
1748 &env.instance,
1749 vec![RdmaOp {
1750 op_type: RdmaOpType::WriteFromLocal,
1751 local: env.local_memory.clone(),
1752 remote: remote.clone(),
1753 }],
1754 Duration::from_secs(30),
1755 )
1756 .await?;
1757
1758 envs[0].rdma_handle.drain_and_stop("test")?;
1761 envs[0].rdma_handle.clone().await;
1762 envs[1].rdma_handle.drain_and_stop("test")?;
1763 envs[1].rdma_handle.clone().await;
1764
1765 Ok(())
1766 }
1767
1768 #[timed_test::async_timed_test(timeout_secs = 30)]
1772 async fn test_tcp_parallel_write() -> anyhow::Result<()> {
1773 let config = hyperactor_config::global::lock();
1774 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1775 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1776 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1777
1778 let buf_size = 3 * 1024 * 1024;
1780 let mut envs = setup_tcp_env(buf_size).await?;
1781 do_write_test(&mut envs, buf_size, Duration::from_secs(30)).await
1782 }
1783
1784 #[timed_test::async_timed_test(timeout_secs = 30)]
1786 async fn test_tcp_parallel_read() -> anyhow::Result<()> {
1787 let config = hyperactor_config::global::lock();
1788 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1789 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1790 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1791
1792 let buf_size = 3 * 1024 * 1024;
1793 let mut envs = setup_tcp_env(buf_size).await?;
1794 do_read_test(&mut envs, buf_size, Duration::from_secs(30)).await
1795 }
1796
1797 #[timed_test::async_timed_test(timeout_secs = 30)]
1799 async fn test_tcp_parallel_round_trip() -> anyhow::Result<()> {
1800 let config = hyperactor_config::global::lock();
1801 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1802 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1803 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1804
1805 let buf_size = 3 * 1024 * 1024;
1806 let mut envs = setup_tcp_env(buf_size).await?;
1807 do_round_trip_test(&mut envs, buf_size, Duration::from_secs(30)).await
1808 }
1809
1810 #[timed_test::async_timed_test(timeout_secs = 30)]
1812 async fn test_tcp_parallel_same_process_write() -> anyhow::Result<()> {
1813 let config = hyperactor_config::global::lock();
1814 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1815 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1816 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1817
1818 let buf_size = 3 * 1024 * 1024;
1819 let mut envs = setup_same_proc_tcp_env(buf_size).await?;
1820 do_write_test(&mut envs, buf_size, Duration::from_secs(10)).await
1821 }
1822
1823 #[timed_test::async_timed_test(timeout_secs = 30)]
1825 async fn test_tcp_parallel_same_process_read() -> anyhow::Result<()> {
1826 let config = hyperactor_config::global::lock();
1827 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1828 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1829 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1830
1831 let buf_size = 3 * 1024 * 1024;
1832 let mut envs = setup_same_proc_tcp_env(buf_size).await?;
1833 do_read_test(&mut envs, buf_size, Duration::from_secs(10)).await
1834 }
1835
1836 #[timed_test::async_timed_test(timeout_secs = 30)]
1840 async fn test_tcp_parallel_concurrent_writes() -> anyhow::Result<()> {
1841 let config = hyperactor_config::global::lock();
1842 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1843 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1844 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1845
1846 let buf_size = 3 * 1024 * 1024;
1847 let envs = setup_tcp_env_pairs(buf_size).await?;
1848
1849 let mut src0 = vec![0u8; buf_size];
1851 for (i, byte) in src0.iter_mut().enumerate() {
1852 *byte = (i % 256) as u8;
1853 }
1854 test_write(&envs[0].local_memory, 0, &src0)?;
1855 let mut src2 = vec![0u8; buf_size];
1856 for (i, byte) in src2.iter_mut().enumerate() {
1857 *byte = ((i * 3 + 7) % 256) as u8;
1858 }
1859 test_write(&envs[2].local_memory, 0, &src2)?;
1860
1861 let remote_1 = envs[1].rdma_remote_buf.clone();
1863 let remote_3 = envs[3].rdma_remote_buf.clone();
1864 let h0 = envs[0].tcp_backend.clone();
1865 let h2 = envs[2].tcp_backend.clone();
1866 let inst_0 = &envs[0].instance;
1867 let inst_2 = &envs[2].instance;
1868 let mem_0 = envs[0].local_memory.clone();
1869 let mem_2 = envs[2].local_memory.clone();
1870 let (r1, r2) = tokio::join!(
1871 h0.submit(
1872 inst_0,
1873 vec![RdmaOp {
1874 op_type: RdmaOpType::WriteFromLocal,
1875 local: mem_0,
1876 remote: remote_1,
1877 }],
1878 Duration::from_secs(30),
1879 ),
1880 h2.submit(
1881 inst_2,
1882 vec![RdmaOp {
1883 op_type: RdmaOpType::WriteFromLocal,
1884 local: mem_2,
1885 remote: remote_3,
1886 }],
1887 Duration::from_secs(30),
1888 ),
1889 );
1890 r1?;
1891 r2?;
1892
1893 let mut dst1 = vec![0u8; buf_size];
1894 test_read(&envs[1].local_memory, 0, &mut dst1)?;
1895 for (i, byte) in dst1.iter().enumerate() {
1896 assert_eq!(*byte, (i % 256) as u8, "pair 1 mismatch at offset {i}");
1897 }
1898 let mut dst3 = vec![0u8; buf_size];
1899 test_read(&envs[3].local_memory, 0, &mut dst3)?;
1900 for (i, byte) in dst3.iter().enumerate() {
1901 assert_eq!(
1902 *byte,
1903 ((i * 3 + 7) % 256) as u8,
1904 "pair 2 mismatch at offset {i}"
1905 );
1906 }
1907
1908 Ok(())
1909 }
1910
1911 #[timed_test::async_timed_test(timeout_secs = 30)]
1913 async fn test_tcp_parallel_concurrent_reads() -> anyhow::Result<()> {
1914 let config = hyperactor_config::global::lock();
1915 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1916 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1917 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1918
1919 let buf_size = 3 * 1024 * 1024;
1920 let envs = setup_tcp_env_pairs(buf_size).await?;
1921
1922 let mut src1 = vec![0u8; buf_size];
1924 for (i, byte) in src1.iter_mut().enumerate() {
1925 *byte = ((i * 11 + 3) % 256) as u8;
1926 }
1927 test_write(&envs[1].local_memory, 0, &src1)?;
1928 let mut src3 = vec![0u8; buf_size];
1929 for (i, byte) in src3.iter_mut().enumerate() {
1930 *byte = ((i * 5 + 13) % 256) as u8;
1931 }
1932 test_write(&envs[3].local_memory, 0, &src3)?;
1933
1934 let remote_1 = envs[1].rdma_remote_buf.clone();
1936 let remote_3 = envs[3].rdma_remote_buf.clone();
1937 let h0 = envs[0].tcp_backend.clone();
1938 let h2 = envs[2].tcp_backend.clone();
1939 let inst_0 = &envs[0].instance;
1940 let inst_2 = &envs[2].instance;
1941 let mem_0 = envs[0].local_memory.clone();
1942 let mem_2 = envs[2].local_memory.clone();
1943 let (r1, r2) = tokio::join!(
1944 h0.submit(
1945 inst_0,
1946 vec![RdmaOp {
1947 op_type: RdmaOpType::ReadIntoLocal,
1948 local: mem_0,
1949 remote: remote_1,
1950 }],
1951 Duration::from_secs(30),
1952 ),
1953 h2.submit(
1954 inst_2,
1955 vec![RdmaOp {
1956 op_type: RdmaOpType::ReadIntoLocal,
1957 local: mem_2,
1958 remote: remote_3,
1959 }],
1960 Duration::from_secs(30),
1961 ),
1962 );
1963 r1?;
1964 r2?;
1965
1966 let mut dst0 = vec![0u8; buf_size];
1967 test_read(&envs[0].local_memory, 0, &mut dst0)?;
1968 for (i, byte) in dst0.iter().enumerate() {
1969 assert_eq!(
1970 *byte,
1971 ((i * 11 + 3) % 256) as u8,
1972 "pair 1 mismatch at offset {i}"
1973 );
1974 }
1975 let mut dst2 = vec![0u8; buf_size];
1976 test_read(&envs[2].local_memory, 0, &mut dst2)?;
1977 for (i, byte) in dst2.iter().enumerate() {
1978 assert_eq!(
1979 *byte,
1980 ((i * 5 + 13) % 256) as u8,
1981 "pair 2 mismatch at offset {i}"
1982 );
1983 }
1984
1985 Ok(())
1986 }
1987
1988 #[timed_test::async_timed_test(timeout_secs = 30)]
1990 async fn test_tcp_parallel_concurrent_write_and_read() -> anyhow::Result<()> {
1991 let config = hyperactor_config::global::lock();
1992 let _guard = config.override_key(crate::config::RDMA_ALLOW_TCP_FALLBACK, true);
1993 let _par_guard = config.override_key(crate::config::RDMA_TCP_FALLBACK_PARALLELISM, 2);
1994 let _chunk_guard = config.override_key(crate::config::RDMA_MAX_CHUNK_SIZE_MB, 1);
1995
1996 let buf_size = 3 * 1024 * 1024;
1997 let envs = setup_tcp_env_pairs(buf_size).await?;
1998
1999 let mut src0 = vec![0u8; buf_size];
2001 for (i, byte) in src0.iter_mut().enumerate() {
2002 *byte = (i % 256) as u8;
2003 }
2004 test_write(&envs[0].local_memory, 0, &src0)?;
2005 let mut src3 = vec![0u8; buf_size];
2006 for (i, byte) in src3.iter_mut().enumerate() {
2007 *byte = ((i * 7 + 13) % 256) as u8;
2008 }
2009 test_write(&envs[3].local_memory, 0, &src3)?;
2010
2011 let remote_1 = envs[1].rdma_remote_buf.clone();
2013 let remote_3 = envs[3].rdma_remote_buf.clone();
2014 let h0 = envs[0].tcp_backend.clone();
2015 let h2 = envs[2].tcp_backend.clone();
2016 let inst_0 = &envs[0].instance;
2017 let inst_2 = &envs[2].instance;
2018 let mem_0 = envs[0].local_memory.clone();
2019 let mem_2 = envs[2].local_memory.clone();
2020 let (write_result, read_result) = tokio::join!(
2021 h0.submit(
2022 inst_0,
2023 vec![RdmaOp {
2024 op_type: RdmaOpType::WriteFromLocal,
2025 local: mem_0,
2026 remote: remote_1,
2027 }],
2028 Duration::from_secs(30),
2029 ),
2030 h2.submit(
2031 inst_2,
2032 vec![RdmaOp {
2033 op_type: RdmaOpType::ReadIntoLocal,
2034 local: mem_2,
2035 remote: remote_3,
2036 }],
2037 Duration::from_secs(30),
2038 ),
2039 );
2040 write_result?;
2041 read_result?;
2042
2043 let mut dst1 = vec![0u8; buf_size];
2044 test_read(&envs[1].local_memory, 0, &mut dst1)?;
2045 for (i, byte) in dst1.iter().enumerate() {
2046 assert_eq!(*byte, (i % 256) as u8, "write mismatch at offset {i}");
2047 }
2048 let mut dst2 = vec![0u8; buf_size];
2049 test_read(&envs[2].local_memory, 0, &mut dst2)?;
2050 for (i, byte) in dst2.iter().enumerate() {
2051 assert_eq!(
2052 *byte,
2053 ((i * 7 + 13) % 256) as u8,
2054 "read mismatch at offset {i}"
2055 );
2056 }
2057
2058 Ok(())
2059 }
2060}