Skip to main content

cubecl_server/command/
collective.rs

1//! Collectives across devices: the groups this device has joined, and what a
2//! driver has to supply to run one.
3//!
4//! The bookkeeping is the same whichever library is underneath. A group is
5//! named by the devices in it, every rank joins under one identifier the group
6//! agrees on, and a device's rank is its position in the sorted list — get
7//! that wrong on one rank and the whole group hangs. That is what lives here.
8//!
9//! What does not is [`CollectiveDriver`]: agreeing the identifier, joining,
10//! naming an element type, and the three operations themselves.
11
12use super::{DeviceResource, Driver};
13use crate::server::{CommunicationId, ReduceOperation, ServerError};
14use alloc::format;
15use alloc::vec::Vec;
16use cubecl_common::device::DeviceId;
17use cubecl_environment::backtrace::BackTrace;
18use cubecl_environment::collections::HashMap;
19use cubecl_ir::ElemType;
20
21/// A driver that can run collectives across devices.
22pub trait CollectiveDriver: Driver {
23    /// This device's membership of one group, joined once and kept.
24    type Communicator;
25    /// The identifier every rank of a group joins under.
26    type UniqueId: Copy;
27    /// How the driver names an element type.
28    type DataType: Copy;
29    /// The stream collectives are issued on, kept apart from the compute
30    /// streams so a collective never blocks one.
31    type CommStream: Copy;
32
33    /// The identifier the group `id` names joins under.
34    ///
35    /// Minted by whichever rank asks first and remembered for the rest, so
36    /// this is process-wide state the driver keeps: the servers of two devices
37    /// in one group are two objects that have to agree on one answer.
38    ///
39    /// # Errors
40    ///
41    /// The driver's refusal to mint one, which stops the group forming at all.
42    fn group_id(id: &CommunicationId) -> Result<Self::UniqueId, ServerError>;
43
44    /// Join the group `id` names as rank `rank` of `ranks`.
45    ///
46    /// # Errors
47    ///
48    /// The driver's refusal to join, which every other rank sees as this one
49    /// never arriving.
50    fn join(
51        id: Self::UniqueId,
52        ranks: usize,
53        rank: usize,
54    ) -> Result<Self::Communicator, ServerError>;
55
56    /// How the driver names `dtype`, and how many elements `size` bytes hold.
57    ///
58    /// # Errors
59    ///
60    /// An element type the driver has no name for. Reported rather than fatal:
61    /// a collective is one operation among many, and refusing it is not a
62    /// reason to take the process down — the caller can pick another type, or
63    /// another way to move the tensor.
64    fn data_type(dtype: ElemType, size: u64) -> Result<(Self::DataType, usize), ServerError>;
65
66    /// Reduce `src` across the group into `dst` on every rank.
67    ///
68    /// # Errors
69    ///
70    /// The driver's refusal to enqueue the reduction.
71    fn all_reduce(
72        comm: &Self::Communicator,
73        src: &DeviceResource<Self>,
74        dst: &DeviceResource<Self>,
75        dtype: Self::DataType,
76        count: usize,
77        op: ReduceOperation,
78        stream: Self::CommStream,
79    ) -> Result<(), ServerError>;
80
81    /// Send `src` to `peer`.
82    ///
83    /// # Errors
84    ///
85    /// The driver's refusal to enqueue the send.
86    fn send(
87        comm: &Self::Communicator,
88        src: &DeviceResource<Self>,
89        dtype: Self::DataType,
90        count: usize,
91        peer: usize,
92        stream: Self::CommStream,
93    ) -> Result<(), ServerError>;
94
95    /// Receive into `dst` from `peer`.
96    ///
97    /// # Errors
98    ///
99    /// The driver's refusal to enqueue the receive.
100    fn recv(
101        comm: &Self::Communicator,
102        dst: &DeviceResource<Self>,
103        dtype: Self::DataType,
104        count: usize,
105        peer: usize,
106        stream: Self::CommStream,
107    ) -> Result<(), ServerError>;
108}
109
110/// The groups this device has joined.
111///
112/// One per server, unlike the identifiers behind
113/// [`group_id`](CollectiveDriver::group_id): an identifier is what every rank
114/// of a group agrees on, a communicator is one rank's membership of it.
115pub struct Collectives<D: CollectiveDriver> {
116    /// This device, whose position in a sorted group is its rank.
117    device: DeviceId,
118    joined: HashMap<CommunicationId, D::Communicator>,
119}
120
121impl<D: CollectiveDriver> core::fmt::Debug for Collectives<D> {
122    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
123        // The communicators are opaque driver handles, so which groups this
124        // device is in is the whole of what there is to say.
125        f.debug_struct("Collectives")
126            .field("device", &self.device)
127            .field("joined", &self.joined.len())
128            .finish()
129    }
130}
131
132impl<D: CollectiveDriver> Collectives<D> {
133    /// A device that has joined nothing yet.
134    pub fn new(device: DeviceId) -> Self {
135        Self {
136            device,
137            joined: HashMap::default(),
138        }
139    }
140
141    /// Join the group over `devices`, and answer with the id it is known by.
142    ///
143    /// A group already joined is joined once: `Ok(None)` says there was
144    /// nothing to do, so the caller does not announce a membership twice.
145    ///
146    /// # Errors
147    ///
148    /// [`ServerError::Generic`] when this device is not among `devices` — it
149    /// would have no rank in a group it is not in — and whatever the driver
150    /// says about agreeing an identifier or joining.
151    pub fn join(&mut self, devices: Vec<DeviceId>) -> Result<Option<CommunicationId>, ServerError> {
152        let id = CommunicationId::from(devices.clone());
153        if self.joined.contains_key(&id) {
154            return Ok(None);
155        }
156        // Sorted, because a device's rank is its position and every rank has
157        // to compute the same one. Two ranks disagreeing does not fail: it
158        // hangs, with each waiting for a peer that is answering to a different
159        // number.
160        let mut devices = devices;
161        devices.sort();
162        let rank = rank_in(self.device, &devices).ok_or_else(|| ServerError::Generic {
163            reason: format!(
164                "this device ({:?}) is not among the {} the group was formed over, \
165                     so it has no rank in it",
166                self.device,
167                devices.len()
168            ),
169            backtrace: BackTrace::capture(),
170        })?;
171
172        let comm = D::join(D::group_id(&id)?, devices.len(), rank)?;
173        self.joined.insert(id.clone(), comm);
174        Ok(Some(id))
175    }
176
177    /// This device's membership of the group over `devices`.
178    ///
179    /// # Errors
180    ///
181    /// [`ServerError::Generic`] when this device never joined that group, which
182    /// is a missing [`join`](Self::join) rather than anything the device did.
183    pub fn get(&self, devices: &CommunicationId) -> Result<&D::Communicator, ServerError> {
184        self.joined
185            .get(devices)
186            .ok_or_else(|| ServerError::Generic {
187                reason: "no communicator for this group; it has to be joined first".into(),
188                backtrace: BackTrace::capture(),
189            })
190    }
191
192    /// The rank of the one device in `devices` that is not this one.
193    ///
194    /// For the two-device operations — a send has exactly one peer, and its
195    /// rank is whichever position this device does not occupy.
196    ///
197    /// # Errors
198    ///
199    /// [`ServerError::Generic`] when every device in the pair is this one, so
200    /// there is no peer to name.
201    pub fn peer_rank(&self, devices: &[DeviceId]) -> Result<usize, ServerError> {
202        peer_of(self.device, devices).ok_or_else(|| ServerError::Generic {
203            reason: format!(
204                "every device in the pair is this one ({:?}), so there is no peer",
205                self.device
206            ),
207            backtrace: BackTrace::capture(),
208        })
209    }
210}
211
212/// `device`'s position in `devices`, which is its rank.
213///
214/// Free of the driver because a rank is arithmetic over device ids and
215/// nothing else — which is also what lets it be checked without one.
216fn rank_in(device: DeviceId, devices: &[DeviceId]) -> Option<usize> {
217    devices.iter().position(|id| id.index_id == device.index_id)
218}
219
220/// The position of the one device in `devices` that is not `device`.
221fn peer_of(device: DeviceId, devices: &[DeviceId]) -> Option<usize> {
222    devices.iter().position(|id| id.index_id != device.index_id)
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use alloc::vec;
229
230    /// A device is its index, since that is all a rank is computed from.
231    fn device(index: u16) -> DeviceId {
232        DeviceId {
233            type_id: 0,
234            index_id: index,
235        }
236    }
237
238    /// A rank is a position in the sorted group, so every member computes the
239    /// same one whatever order it was handed the devices in.
240    ///
241    /// The property worth defending above every other here, because breaking
242    /// it does not fail: two ranks that disagree hang, each waiting for a peer
243    /// that is answering to a different number.
244    #[test]
245    fn a_rank_does_not_depend_on_the_order_the_group_was_given_in() {
246        let group = [device(7), device(2), device(5)];
247        for given in [
248            vec![group[0], group[1], group[2]],
249            vec![group[2], group[1], group[0]],
250            vec![group[1], group[2], group[0]],
251        ] {
252            let mut sorted = given.clone();
253            sorted.sort();
254            for (expected, member) in sorted.iter().enumerate() {
255                assert_eq!(
256                    rank_in(*member, &sorted),
257                    Some(expected),
258                    "{member:?} in {given:?}"
259                );
260            }
261        }
262    }
263
264    /// A device outside the group has no rank in it, rather than silently
265    /// taking someone else's.
266    #[test]
267    fn a_device_outside_the_group_has_no_rank() {
268        assert_eq!(rank_in(device(9), &[device(1), device(2)]), None);
269    }
270
271    /// The peer of a pair is whichever member is not this device.
272    #[test]
273    fn the_peer_of_a_pair_is_the_other_member() {
274        let pair = [device(3), device(8)];
275        assert_eq!(peer_of(device(3), &pair), Some(1));
276        assert_eq!(peer_of(device(8), &pair), Some(0));
277    }
278
279    /// A pair of one device has no peer, rather than a rank that would send a
280    /// transfer to the device it came from.
281    #[test]
282    fn a_pair_of_one_device_has_no_peer() {
283        assert_eq!(peer_of(device(4), &[device(4), device(4)]), None);
284    }
285
286    /// Ranks are compared by device index alone, so two devices of different
287    /// kinds at the same index are the same member of a group.
288    ///
289    /// Not obviously right — it is what the collective has always done — but
290    /// worth pinning, because a group spanning two device types would silently
291    /// give both the same rank.
292    #[test]
293    fn a_rank_is_the_device_index_and_nothing_else() {
294        let other_kind = DeviceId {
295            type_id: 1,
296            index_id: 2,
297        };
298        assert_eq!(rank_in(other_kind, &[device(1), device(2)]), Some(1));
299    }
300}