Skip to main content

monarch_rdma/
rdma_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//! # RDMA Manager Actor
10//!
11//! Per-proc singleton service actor that owns RDMA buffer registrations and
12//! delegates transport-specific work to a NIC backend and [`TcpManagerActor`].
13//!
14//! ## Responsibilities
15//!
16//! - Assigns a unique `remote_buf_id` to each registered local memory handle
17//!   and stores the [`KeepaliveLocalMemory`] for later retrieval.
18//! - Produces [`RdmaRemoteBuffer`] tokens that can be sent to remote peers so
19//!   they can address this buffer over RDMA.
20//! - Delegates MR registration, QP management, and data movement to a NIC
21//!   backend when available, or falls back to the TCP backend
22//!   ([`TcpManagerActor`]).
23//! - Handles remote [`ReleaseBuffer`] requests to clean up registrations.
24//!
25//! ## Service topology and readiness
26//!
27//! `spawn_service("rdma_manager", ...)` creates or reuses one
28//! `RdmaManagerActor` on each target proc and materializes an `ActorMesh` view
29//! over them. Overlapping proc-mesh views reuse the same actor on shared procs;
30//! each view still has its own `ActorMeshController`.
31//!
32//! The owner casts [`RdmaManagerReady`] to every actor in a view. Actor
33//! messages are dispatched only after `init()` succeeds, so the resulting
34//! [`ReadyAck`](crate::ReadyAck) certifies that this proc's manager backends
35//! are initialized. If initialization fails, no acknowledgement is sent and
36//! every controller monitoring that actor reports the failure to the owner.
37
38use std::collections::HashMap;
39use std::sync::Arc;
40use std::sync::LazyLock;
41use std::sync::OnceLock;
42use std::sync::RwLock;
43
44use async_trait::async_trait;
45use hyperactor::Actor;
46use hyperactor::ActorEnvironment;
47use hyperactor::ActorHandle;
48use hyperactor::ActorRef;
49use hyperactor::Context;
50use hyperactor::HandleClient;
51use hyperactor::Handler;
52use hyperactor::Instance;
53use hyperactor::OncePortHandle;
54use hyperactor::OncePortRef;
55use hyperactor::ProcAddr;
56use hyperactor::RefClient;
57use hyperactor::RemoteSpawn;
58use hyperactor::context;
59use serde::Deserialize;
60use serde::Serialize;
61use tokio::sync::OnceCell;
62use typeuri::Named;
63
64use crate::backend::RdmaBackendHandle;
65use crate::backend::RdmaBackends;
66use crate::backend::RdmaConfig;
67use crate::backend::ibverbs::primitives::IbvConfig;
68use crate::backend::tcp::manager_actor::TcpManagerActor;
69use crate::local_memory::KeepaliveLocalMemory;
70use crate::rdma_components::RdmaRemoteBuffer;
71use crate::rdma_manager_owner::EntryId;
72use crate::rdma_manager_owner::RdmaManagerOwnerActor;
73use crate::rdma_manager_owner::RdmaManagerReady;
74use crate::rdma_manager_owner::RdmaManagerReadyHandler;
75use crate::rdma_manager_owner::ReadyAckClient;
76
77// Each proc has one RDMA manager for its lifetime, so its backend handles never
78// need replacement or invalidation while they can still be used.
79static BACKEND_HANDLE_CACHE: LazyLock<
80    RwLock<HashMap<ProcAddr, Arc<OnceCell<Vec<RdmaBackendHandle>>>>>,
81> = LazyLock::new(|| RwLock::new(HashMap::new()));
82
83/// Helper function to get detailed error messages from RDMAXCEL error codes
84pub fn get_rdmaxcel_error_message(error_code: i32) -> String {
85    unsafe {
86        let c_str = rdmaxcel_sys::rdmaxcel_error_string(error_code);
87        std::ffi::CStr::from_ptr(c_str)
88            .to_string_lossy()
89            .into_owned()
90    }
91}
92
93/// Local-only messages for the [`RdmaManagerActor`].
94///
95/// These messages carry [`KeepaliveLocalMemory`] and are therefore not
96/// serializable -- they can only be sent within the same process.
97#[derive(Handler, HandleClient, Debug)]
98pub enum RdmaManagerMessage {
99    /// Register a local memory handle and return a [`RdmaRemoteBuffer`] that
100    /// remote peers can use to address this buffer over RDMA.
101    RequestBuffer {
102        local: KeepaliveLocalMemory,
103        #[reply]
104        reply: OncePortHandle<RdmaRemoteBuffer>,
105    },
106    /// Look up the local memory handle for a given `remote_buf_id`. Returns
107    /// `None` if the id does not correspond to a registered buffer.
108    RequestLocalMemory {
109        remote_buf_id: usize,
110        #[reply]
111        reply: OncePortHandle<Option<KeepaliveLocalMemory>>,
112    },
113    /// Return in-process handles to all spawned backends, in priority order.
114    GetBackendHandles {
115        #[reply]
116        reply: OncePortHandle<Vec<RdmaBackendHandle>>,
117    },
118}
119
120/// Serializable release message for wire transport.
121///
122/// Used by [`RdmaRemoteBuffer::drop_buffer`] to release a buffer
123/// from a remote process.
124#[derive(Handler, HandleClient, RefClient, Debug, Serialize, Deserialize, Named)]
125pub struct ReleaseBuffer {
126    pub id: usize,
127}
128wirevalue::register_type!(ReleaseBuffer);
129
130/// Serializable query for resolving the [`TcpManagerActor`] ref
131/// from a remote [`RdmaManagerActor`].
132#[derive(Handler, HandleClient, RefClient, Debug, Serialize, Deserialize, Named)]
133pub struct GetTcpActorRef {
134    #[reply]
135    pub reply: OncePortRef<ActorRef<TcpManagerActor>>,
136}
137wirevalue::register_type!(GetTcpActorRef);
138
139#[derive(Debug)]
140#[hyperactor::export(
141    handlers = [
142        GetTcpActorRef,
143        ReleaseBuffer,
144        RdmaManagerReady,
145    ],
146)]
147#[hyperactor::spawnable]
148pub struct RdmaManagerActor {
149    next_remote_buf_id: usize,
150    buffers: HashMap<usize, KeepaliveLocalMemory>,
151    params: Option<IbvConfig>,
152    backends: OnceLock<RdmaBackends>,
153}
154
155impl RdmaManagerActor {
156    /// Construct an [`ActorHandle`] for the [`RdmaManagerActor`] co-located
157    /// with the caller.
158    pub fn local_handle(client: &impl context::Actor) -> ActorHandle<Self> {
159        let actor_ref = ActorRef::attest(
160            client
161                .mailbox()
162                .actor_addr()
163                .proc_addr()
164                .actor_addr("rdma_manager"),
165        );
166        actor_ref
167            .downcast_handle(client)
168            .expect("RdmaManagerActor is not in the local process")
169    }
170
171    pub async fn local_backend_handles(
172        client: &(impl context::Actor + Send + Sync),
173    ) -> Result<Vec<RdmaBackendHandle>, anyhow::Error> {
174        let key = client.mailbox().actor_addr().proc_addr();
175        let cell = BACKEND_HANDLE_CACHE
176            .read()
177            .expect("backend handle cache lock should not be poisoned")
178            .get(&key)
179            .cloned();
180        let cell = cell.unwrap_or_else(|| {
181            Arc::clone(
182                BACKEND_HANDLE_CACHE
183                    .write()
184                    .expect("backend handle cache lock should not be poisoned")
185                    .entry(key)
186                    .or_insert_with(|| Arc::new(OnceCell::new())),
187            )
188        });
189        cell.get_or_try_init(async || Self::local_handle(client).get_backend_handles(client).await)
190            .await
191            .cloned()
192    }
193}
194
195#[async_trait]
196impl RemoteSpawn for RdmaManagerActor {
197    type Params = Option<IbvConfig>;
198
199    async fn new(
200        params: Self::Params,
201        _environment: &ActorEnvironment,
202    ) -> Result<Self, anyhow::Error> {
203        Ok(Self {
204            next_remote_buf_id: 0,
205            buffers: HashMap::new(),
206            params,
207            backends: OnceLock::new(),
208        })
209    }
210}
211
212#[async_trait]
213impl Actor for RdmaManagerActor {
214    async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
215        this.set_system();
216        // An explicit per-manager target takes precedence over
217        // `RDMA_IBVERBS_TARGET`. Otherwise validate the process setting here:
218        // `spawn_available` may ignore an ibverbs initialization failure when
219        // TCP starts, silently turning a malformed pin into TCP fallback.
220        if self
221            .params
222            .as_ref()
223            .and_then(|config| config.target.as_ref())
224            .is_none()
225        {
226            let _ = crate::backend::ibverbs::device_selection::configured_ibverbs_target()?;
227        }
228        // Likewise: nothing reads the affinity policy until a transfer runs, so
229        // a malformed value would otherwise surface far from its cause.
230        let _ = crate::backend::ibverbs::device_selection::configured_peer_device_affinity()?;
231
232        // Spawn every available backend. `spawn_available` bails when none
233        // is available (e.g. no NIC and TCP fallback disabled).
234        let backends = RdmaBackends::spawn_available(
235            this,
236            &RdmaConfig {
237                ibv: self.params.clone(),
238            },
239        )
240        .await?;
241        self.backends.set(backends).expect("backends set once");
242        Ok(())
243    }
244
245    // This actor is implemented in Rust, but the RDMA registration path may enter
246    // Python and take the GIL. Run its loop on the dedicated rdma runtime rather
247    // than the shared control-plane runtime; see `crate::rdma_runtime`.
248    fn spawn_server_task<F>(future: F) -> tokio::task::JoinHandle<F::Output>
249    where
250        F: std::future::Future + Send + 'static,
251        F::Output: Send + 'static,
252    {
253        crate::rdma_runtime::spawn_on_rdma_runtime(future)
254    }
255}
256
257#[async_trait]
258#[hyperactor::handle(GetTcpActorRef)]
259impl GetTcpActorRefHandler for RdmaManagerActor {
260    async fn get_tcp_actor_ref(
261        &mut self,
262        _cx: &Context<Self>,
263    ) -> Result<ActorRef<TcpManagerActor>, anyhow::Error> {
264        self.backends
265            .get()
266            .expect("backends set in init")
267            .handles()
268            .into_iter()
269            .find_map(|h| match h {
270                RdmaBackendHandle::Tcp(backend) => Some(backend.bind()),
271                _ => None,
272            })
273            .ok_or_else(|| anyhow::anyhow!("TCP backend not available"))
274    }
275}
276
277#[async_trait]
278#[hyperactor::handle(ReleaseBuffer)]
279impl ReleaseBufferHandler for RdmaManagerActor {
280    async fn release_buffer(&mut self, cx: &Context<Self>, id: usize) -> Result<(), anyhow::Error> {
281        self.buffers.remove(&id);
282        self.backends
283            .get()
284            .expect("backends set in init")
285            .release_all(cx, id)
286            .await?;
287        Ok(())
288    }
289}
290
291#[async_trait]
292#[hyperactor::handle(RdmaManagerMessage)]
293impl RdmaManagerMessageHandler for RdmaManagerActor {
294    async fn request_buffer(
295        &mut self,
296        cx: &Context<Self>,
297        local: KeepaliveLocalMemory,
298    ) -> Result<RdmaRemoteBuffer, anyhow::Error> {
299        let remote_buf_id = self.next_remote_buf_id;
300        self.next_remote_buf_id += 1;
301        let size = local.size();
302
303        let backends = self
304            .backends
305            .get()
306            .expect("backends set in init")
307            .register_all(cx, remote_buf_id, local.clone())
308            .await?;
309        self.buffers.insert(remote_buf_id, local);
310
311        Ok(RdmaRemoteBuffer {
312            id: remote_buf_id,
313            size,
314            owner: cx.bind().clone(),
315            backends,
316        })
317    }
318
319    async fn request_local_memory(
320        &mut self,
321        _cx: &Context<Self>,
322        remote_buf_id: usize,
323    ) -> Result<Option<KeepaliveLocalMemory>, anyhow::Error> {
324        Ok(self.buffers.get(&remote_buf_id).cloned())
325    }
326
327    async fn get_backend_handles(
328        &mut self,
329        _cx: &Context<Self>,
330    ) -> Result<Vec<RdmaBackendHandle>, anyhow::Error> {
331        Ok(self.backends.get().expect("backends set in init").handles())
332    }
333}
334
335#[async_trait]
336#[hyperactor::handle(RdmaManagerReady)]
337impl RdmaManagerReadyHandler for RdmaManagerActor {
338    async fn rdma_manager_ready(
339        &mut self,
340        cx: &Context<Self>,
341        owner: ActorRef<RdmaManagerOwnerActor>,
342        entry: EntryId,
343    ) -> Result<(), anyhow::Error> {
344        // Runs strictly after `init()` (RMR-1): ack the owner so it can count
345        // this rank toward readiness.
346        owner.ready_ack(cx, entry).await?;
347        Ok(())
348    }
349}