Skip to main content

monarch_rdma/
action.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//! Batched RDMA action layer.
10//!
11//! [`RdmaAction`] accumulates [`crate::RdmaOp`]s through `add_read_into_local`
12//! / `add_write_from_local`, validates the per-op sizes as ops are added,
13//! and then dispatches the queued ops across the available backends in
14//! parallel on [`RdmaAction::submit`].
15
16use std::time::Duration;
17
18use hyperactor::context;
19
20use crate::RdmaManagerActor;
21use crate::RdmaOp;
22use crate::RdmaOpType;
23use crate::backend::RdmaBackendHandle;
24use crate::local_memory::KeepaliveLocalMemory;
25use crate::rdma_components::RdmaRemoteBuffer;
26
27/// A batch of RDMA operations submitted as a single unit.
28///
29/// `RdmaAction` is a builder that accumulates read-into-local and
30/// write-from-local ops, checking each op's sizes as it is added, and then
31/// runs them concurrently across the available backends on
32/// [`Self::submit`].
33///
34/// Overlap between the queued ops' memory ranges is not tracked, on either
35/// the local or the remote side. Two ops in one action that write the same
36/// range race, and it is the caller's job not to queue them.
37pub struct RdmaAction {
38    entries: Vec<RdmaOp>,
39}
40
41impl Default for RdmaAction {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl RdmaAction {
48    pub fn new() -> Self {
49        Self {
50            entries: Vec::new(),
51        }
52    }
53
54    /// Queue a read from `remote` into `local`.
55    pub fn add_read_into_local(
56        &mut self,
57        remote: RdmaRemoteBuffer,
58        local: KeepaliveLocalMemory,
59    ) -> Result<&mut Self, anyhow::Error> {
60        if local.size() < remote.size {
61            anyhow::bail!(
62                "destination local memory size ({}) must be >= remote buffer size ({})",
63                local.size(),
64                remote.size,
65            );
66        }
67        self.entries.push(RdmaOp {
68            op_type: RdmaOpType::ReadIntoLocal,
69            local,
70            remote,
71        });
72        Ok(self)
73    }
74
75    /// Queue a write from `local` into `remote`.
76    pub fn add_write_from_local(
77        &mut self,
78        remote: RdmaRemoteBuffer,
79        local: KeepaliveLocalMemory,
80    ) -> Result<&mut Self, anyhow::Error> {
81        if local.size() > remote.size {
82            anyhow::bail!(
83                "source local memory size ({}) must be <= remote buffer size ({})",
84                local.size(),
85                remote.size,
86            );
87        }
88        self.entries.push(RdmaOp {
89            op_type: RdmaOpType::WriteFromLocal,
90            local,
91            remote,
92        });
93        Ok(self)
94    }
95
96    /// Submit all queued ops. Ops are grouped by backend and each group
97    /// is submitted in parallel. Safe to call more than once on the same
98    /// action — the queued ops are left intact.
99    ///
100    /// Takes `&mut self` so the borrow checker prevents two submit futures
101    /// from being alive on the same action at once, which would put two
102    /// in-flight dispatches over the same local memory.
103    pub async fn submit(
104        &mut self,
105        client: &(impl context::Actor + Send + Sync),
106        timeout: Duration,
107    ) -> Result<(), anyhow::Error> {
108        if self.entries.is_empty() {
109            return Ok(());
110        }
111
112        // This proc's spawned backends, in priority order.
113        let handles = RdmaManagerActor::local_backend_handles(client).await?;
114        let mut buckets: Vec<(RdmaBackendHandle, Vec<RdmaOp>)> =
115            handles.into_iter().map(|h| (h, Vec::new())).collect();
116
117        // Route each op to the first backend the local proc runs that the
118        // remote buffer also advertises.
119        for entry in &self.entries {
120            let op = RdmaOp {
121                op_type: entry.op_type,
122                local: entry.local.clone(),
123                remote: entry.remote.clone(),
124            };
125            match buckets
126                .iter_mut()
127                .find(|(handle, _)| entry.remote.is_compatible_with(handle))
128            {
129                Some((_, ops)) => ops.push(op),
130                None => anyhow::bail!("no compatible RDMA backend for buffer: {:?}", entry.remote),
131            }
132        }
133
134        // Submit each non-empty backend group in parallel, waiting for all
135        // groups to finish and accumulating every failure.
136        let pending = buckets.into_iter().filter(|(_, ops)| !ops.is_empty()).map(
137            |(handle, ops)| async move {
138                let name = handle.backend_name();
139                handle
140                    .submit(client, ops, timeout)
141                    .await
142                    .map_err(|e| format!("({name}) {e}"))
143            },
144        );
145        let errors: Vec<String> = futures::future::join_all(pending)
146            .await
147            .into_iter()
148            .filter_map(Result::err)
149            .collect();
150        if errors.is_empty() {
151            Ok(())
152        } else {
153            anyhow::bail!(
154                "RDMA submit failed on {} backend(s):\n{}",
155                errors.len(),
156                errors.join("\n")
157            )
158        }
159    }
160}