Skip to main content

monarch_rdma/backend/tcp/
manager_actor.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! TCP manager actor for RDMA fallback transport.
10//!
11//! Transfers buffer data over the default hyperactor channel transport
12//! in chunks controlled by
13//! [`RDMA_MAX_CHUNK_SIZE_MB`](crate::config::RDMA_MAX_CHUNK_SIZE_MB).
14
15use 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/// [`Named`] wrapper around [`Part`] for use as a reply type.
66///
67/// [`Part`] itself does not implement [`Named`], which is required by
68/// [`OncePortRef`]. This newtype adds the missing trait.
69#[derive(Debug, Clone, Serialize, Deserialize, Named)]
70pub struct TcpChunk(Part);
71wirevalue::register_type!(TcpChunk);
72
73/// Data chunk sent over direct parallel channels.
74#[derive(Debug, Clone, Serialize, Deserialize, Named)]
75struct TcpDataChunk {
76    // Which specific transfer this chunk is associated with.
77    transfer_id: usize,
78    // Offset into the buffer for this chunk.
79    offset: usize,
80    data: Part,
81}
82wirevalue::register_type!(TcpDataChunk);
83
84/// Tracks the progress of a single parallel transfer (read or write).
85///
86/// Shared between channel receive loops and actor message handlers
87/// via a [`DashMap`].
88#[derive(Debug)]
89struct TransferState {
90    /// Buffer backing this transfer, provided at construction.
91    local_memory: KeepaliveLocalMemory,
92
93    /// Number of chunks received so far.
94    chunks_received: usize,
95
96    /// Total chunks expected for this transfer.
97    total_chunks: usize,
98
99    /// Completion reply port. Fired when all chunks arrive
100    /// or an error occurs.
101    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/// Sends the result of a completed transfer to the caller's reply port.
120///
121/// Sending an actor message from the spawned receiver task requires the
122/// loop to own a dummy [`context::Actor`] impl. If the task sent directly
123/// to a remote [`OncePortRef`], the message would appear to come from
124/// this dummy context, and undeliverable messages wouldn't be handled
125/// properly. This intermediate message lets us use a [`PortHandle`]
126/// whose message cannot be undeliverable; the handler then forwards the
127/// result using the real actor's context.
128#[derive(Debug, Serialize, Deserialize, Named)]
129struct SendTransferResult {
130    done: OncePortRef<Result<(), String>>,
131    result: Result<(), String>,
132}
133
134/// Fatal error from the receive loop.
135///
136/// The handler logs the error and returns `Err`, which triggers a
137/// supervision event and crashes the actor.
138#[derive(Debug, Serialize, Deserialize, Named)]
139struct TransferError {
140    message: String,
141}
142
143/// Set up the local TcpManagerActor to receive a parallel transfer from
144/// a remote TcpManagerActor.
145#[derive(Debug)]
146struct RegisterTransferLocal {
147    local_memory: KeepaliveLocalMemory,
148    total_chunks: usize,
149    done: OncePortRef<Result<(), String>>,
150    // The transfer ID
151    reply: OncePortHandle<usize>,
152}
153
154/// Tell the local TcpManagerActor to read local memory and push
155/// chunks to `dest_addr`.
156#[derive(Debug)]
157struct ExecuteTransferLocal {
158    transfer_id: usize,
159    local_memory: KeepaliveLocalMemory,
160    chunk_size: usize,
161    dest_addr: ChannelAddr,
162}
163
164/// Serializable messages for the [`TcpManagerActor`].
165///
166/// These travel over the wire between processes. The [`Part`] payload
167/// is transferred via the multipart codec without an extra copy.
168#[derive(Handler, HandleClient, RefClient, Debug, Serialize, Deserialize, Named)]
169enum TcpManagerMessage {
170    /// Write a chunk of data into a registered buffer at the given offset.
171    WriteChunk {
172        buf_id: usize,
173        offset: usize,
174        data: Part,
175        #[reply]
176        reply: OncePortRef<Result<(), String>>,
177    },
178    /// Read a chunk of data from a registered buffer at the given offset.
179    ReadChunk {
180        buf_id: usize,
181        offset: usize,
182        size: usize,
183        #[reply]
184        reply: OncePortRef<Result<TcpChunk, String>>,
185    },
186    /// Return the channel address served by this actor for parallel transfers.
187    /// `None` when parallelism is 1.
188    GetChannelAddress {
189        #[reply]
190        reply: OncePortRef<Option<ChannelAddr>>,
191    },
192    /// Set up a remote TcpManagerActor to receive a parallel transfer from
193    /// the sender.
194    RegisterTransferRemote {
195        buf_id: usize,
196        total_chunks: usize,
197        done: OncePortRef<Result<(), String>>,
198        #[reply]
199        reply: OncePortRef<Result<usize, String>>,
200    },
201    /// Tell the remote TcpManagerActor to read its local memory and push
202    /// chunks to the dest_addr provided by the sender.
203    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/// TCP fallback RDMA backend actor.
215///
216/// Spawned as a child of [`RdmaManagerActor`]. Transfers buffer data
217/// over the default hyperactor channel transport in chunks.
218#[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    /// Address of the direct channel served for parallel transfers.
227    /// `None` when parallelism is 1 (default).
228    channel_addr: Option<ChannelAddr>,
229    /// Cached outbound connections keyed by remote channel address.
230    outbound: HashMap<ChannelAddr, Vec<Arc<ChannelTx<TcpDataChunk>>>>,
231    /// Cancellation token for spawned tasks.
232    cancel: CancellationToken,
233    /// Signaled when the parallel receive loop exits.
234    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                    // SAFETY: the caller is responsible for ensuring that no other
324                    // component writes the target byte range concurrently.
325                    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    /// Construct an [`ActorHandle`] for the local [`TcpManagerActor`]
358    /// by querying the local [`RdmaManagerActor`].
359    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                        // SAFETY: the caller is responsible for ensuring that no other
442                        // component reads or writes the target byte range concurrently.
443                        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        // SAFETY: the remote peer that issued this `WriteChunk` had to
515        // register the buffer locally first; its caller is responsible
516        // for ensuring no other component reads or writes the target byte
517        // range concurrently.
518        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        // SAFETY: the remote peer that issued this `ReadChunk` had to
541        // register the buffer locally first; its caller is responsible
542        // for ensuring no other component writes the target byte range
543        // concurrently.
544        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/// Wrapper around [`ActorHandle<TcpManagerActor>`] that moves the TCP
649/// data-plane (chunked reads/writes) off the actor loop while keeping
650/// buffer resolution serialized through actor messages.
651///
652/// Because submit logic now runs outside the actor loop, same-process
653/// messages no longer deadlock — the actor loop is free to handle
654/// `WriteChunk`/`ReadChunk` messages.
655#[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    /// Execute a parallel write: register the transfer on the remote
667    /// side, then execute locally to push chunks over direct channels.
668    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        // A write transfers the whole local buffer into the remote prefix; the
676        // remote buffer may be larger, so its tail is left untouched.
677        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    /// Execute a parallel read: register the transfer locally, then
731    /// ask the remote side to push chunks to our channel.
732    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        // A read transfers the whole remote buffer into the local prefix; the
740        // local buffer may be larger, so its tail is left untouched.
741        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    /// Execute a write operation: read local memory in chunks and write
801    /// them into the remote buffer via actor messages.
802    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        // A write transfers the whole local buffer into the remote prefix; the
810        // remote buffer may be larger, so its tail is left untouched.
811        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            // SAFETY: `op.local_memory` is the caller's buffer; that
831            // caller is responsible for excluding external writers
832            // while the `write_from_local` operation is in flight.
833            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    /// Execute a read operation: request chunks from the remote buffer
852    /// and write them into local memory via actor messages.
853    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        // A read transfers the whole remote buffer into the local prefix; the
861        // local buffer may be larger, so its tail is left untouched.
862        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            // SAFETY: `op.local_memory` is the caller's buffer; that
897            // caller is responsible for excluding external readers
898            // and writers while the `read_into_local` operation is in
899            // flight.
900            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    /// TCP is available when fallback is enabled.
915    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    /// TCP needs no per-buffer registration.
932    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    /// Submit a batch of RDMA operations over TCP.
950    ///
951    /// Each op is executed directly — sending chunked write/read messages
952    /// to the remote [`TcpManagerActor`].
953    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            // Release the buffer so the actor drops its local_memory
1049            // clone while the CUDA runtime is still alive.
1050            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        /// Create a standalone test environment with its own proc and rdma manager.
1064        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        /// Create a buffer on an existing proc's rdma manager.
1096        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    /// Two separate procs, one buffer each.
1133    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    /// Single proc, two buffers.
1141    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    /// Two procs, two buffers each (4 total). For concurrent tests that
1154    /// need independent source/dest pairs.
1155    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    // --- Shared test helpers ---
1168
1169    /// Test-only wrapper around [`KeepaliveLocalMemory::write_at`].
1170    ///
1171    /// Every [`TcpTestProcEnv`] owns a distinct CPU buffer that no
1172    /// other thread accesses outside of explicit, serialized test
1173    /// operations, so the safety obligation of `write_at` is trivially
1174    /// satisfied across the whole module.
1175    fn test_write(mem: &KeepaliveLocalMemory, offset: usize, src: &[u8]) -> anyhow::Result<()> {
1176        // SAFETY: see the function-level comment.
1177        unsafe { mem.write_at(offset, src) }
1178    }
1179
1180    /// Test-only wrapper around [`KeepaliveLocalMemory::read_at`]. See
1181    /// [`test_write`] for the safety rationale.
1182    fn test_read(mem: &KeepaliveLocalMemory, offset: usize, dst: &mut [u8]) -> anyhow::Result<()> {
1183        // SAFETY: see the function-level comment.
1184        unsafe { mem.read_at(offset, dst) }
1185    }
1186
1187    /// Fill envs[0], write to envs[1], verify.
1188    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    /// Fill envs[1], read into envs[0], verify.
1222    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    /// Write, clear, read-back, verify round-trip.
1260    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    // --- Non-parallel two-proc tests ---
1313
1314    /// Write from local buffer 0 into remote buffer 1.
1315    #[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    /// Read from remote buffer 1 into local buffer 0.
1325    #[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    /// Write, clear, read-back, verify round-trip.
1335    #[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    /// Multi-chunk write (1 MiB chunks, 1.5 MiB buffer).
1345    #[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    /// Multi-chunk read.
1384    #[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    /// Multi-chunk write-then-read round-trip.
1423    #[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    /// resolve_tcp finds the Tcp backend context in a buffer.
1481    #[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    /// Write to a released buffer returns an error without crashing.
1501    #[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        // Normal write should succeed.
1516        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        // Release the remote buffer.
1531        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        // Writing to the released buffer should fail.
1538        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    /// Read from a released buffer returns an error without crashing.
1557    #[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        // Release the remote buffer.
1566        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        // Reading from the released buffer should fail.
1573        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    // --- Non-parallel same-proc tests ---
1596
1597    /// Same-process write.
1598    #[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    /// Same-process read.
1608    #[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    /// Same-process write-then-read round-trip.
1618    #[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    // --- Multi-GPU TCP fallback tests ---
1628
1629    use crate::backend::cuda_test_utils::CudaAllocator;
1630    use crate::backend::cuda_test_utils::cuda_device_count;
1631
1632    impl TcpTestProcEnv {
1633        /// Create a test environment backed by CUDA device memory.
1634        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    /// TCP write from GPU on cuda:0 to GPU on cuda:1.
1670    #[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    /// TCP read from GPU on cuda:1 into GPU on cuda:0.
1689    #[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    /// TCP write-then-read round-trip between cuda:0 and cuda:1.
1708    #[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    /// Stopping the RdmaManagerActor with parallelism enabled cleanly
1727    /// shuts down the TcpManagerActor's receive loop without hanging.
1728    #[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        // Do a transfer so the receive loop and outbound connections are live.
1739        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        // Stop the RdmaManagerActor, which cascades to TcpManagerActor.
1759        // The test timeout ensures we detect hangs in the cleanup path.
1760        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    // --- Parallel transfer tests ---
1769
1770    /// Parallel write via direct channels.
1771    #[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        // 3 MiB, 3 chunks spread across 2 workers.
1779        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    /// Parallel read via direct channels.
1785    #[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    /// Parallel write-then-read round-trip.
1798    #[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    /// Same-process parallel write.
1811    #[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    /// Same-process parallel read.
1824    #[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    // --- Concurrent parallel tests (4 envs, 2 independent pairs) ---
1837
1838    /// Two concurrent parallel writes to independent buffer pairs.
1839    #[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        // Fill source buffers with distinct patterns.
1850        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        // Pair 1: envs[0] -> envs[1], Pair 2: envs[2] -> envs[3].
1862        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    /// Two concurrent parallel reads from independent buffer pairs.
1912    #[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        // Fill remote buffers with distinct patterns.
1923        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        // Pair 1: envs[0] <- envs[1], Pair 2: envs[2] <- envs[3].
1935        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    /// Concurrent parallel write and read on independent buffer pairs.
1989    #[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        // Fill source buffers.
2000        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        // Write envs[0] -> envs[1], read envs[2] <- envs[3] concurrently.
2012        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}