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 operations themselves.
11
12use super::{DeviceResource, Driver};
13use crate::server::{CommunicationId, ReduceOperation, ServerError};
14use alloc::{format, vec, vec::Vec};
15use cubecl_common::device::DeviceId;
16use cubecl_environment::backtrace::BackTrace;
17use cubecl_environment::collections::HashMap;
18use cubecl_ir::ElemType;
19
20/// A driver that can run collectives across devices.
21pub trait CollectiveDriver: Driver {
22    /// This device's membership of one group, joined once and kept.
23    type Communicator;
24    /// The identifier every rank of a group joins under.
25    type UniqueId: Copy;
26    /// How the driver names an element type.
27    type DataType: Copy;
28    /// The stream collectives are issued on, kept apart from the compute
29    /// streams so a collective never blocks one.
30    type CommStream: Copy;
31
32    /// The identifier the group `id` names joins under.
33    ///
34    /// Minted by whichever rank asks first and remembered for the rest, so
35    /// this is process-wide state the driver keeps: the servers of two devices
36    /// in one group are two objects that have to agree on one answer.
37    ///
38    /// # Errors
39    ///
40    /// The driver's refusal to mint one, which stops the group forming at all.
41    fn group_id(id: &CommunicationId) -> Result<Self::UniqueId, ServerError>;
42
43    /// Join the group `id` names as rank `rank` of `ranks`.
44    ///
45    /// # Errors
46    ///
47    /// The driver's refusal to join, which every other rank sees as this one
48    /// never arriving.
49    fn join(
50        id: Self::UniqueId,
51        ranks: usize,
52        rank: usize,
53    ) -> Result<Self::Communicator, ServerError>;
54
55    /// The elements of `dtype` that `size` bytes hold, as the driver names them.
56    ///
57    /// # Errors
58    ///
59    /// An element type the driver has no name for, and a size that is not whole elements.
60    /// Reported rather than fatal: a collective is one operation among many, and
61    /// refusing it is not a reason to take the process down. The caller can pick
62    /// another type, or another way to move the tensor.
63    fn elements(dtype: ElemType, size: u64) -> Result<Elements<Self::DataType>, ServerError>;
64
65    /// Reduce `src` across the group into `dst` on every rank.
66    ///
67    /// # Errors
68    ///
69    /// The driver's refusal to enqueue the reduction.
70    fn all_reduce(
71        comm: &Self::Communicator,
72        src: &DeviceResource<Self>,
73        dst: &DeviceResource<Self>,
74        elements: Elements<Self::DataType>,
75        op: ReduceOperation,
76        stream: Self::CommStream,
77    ) -> Result<(), ServerError>;
78
79    /// Write every rank's `src` of `elements`, concatenated in rank order, to `dst`.
80    ///
81    /// # Errors
82    ///
83    /// The driver's refusal to enqueue the gather.
84    fn all_gather(
85        comm: &Self::Communicator,
86        src: &DeviceResource<Self>,
87        dst: &DeviceResource<Self>,
88        elements: Elements<Self::DataType>,
89        stream: Self::CommStream,
90    ) -> Result<(), ServerError>;
91
92    /// Reduce every rank's `src` and write this rank's chunk of `elements` to `dst`.
93    ///
94    /// # Errors
95    ///
96    /// The driver's refusal to enqueue the reduction.
97    fn reduce_scatter(
98        comm: &Self::Communicator,
99        src: &DeviceResource<Self>,
100        dst: &DeviceResource<Self>,
101        elements: Elements<Self::DataType>,
102        op: ReduceOperation,
103        stream: Self::CommStream,
104    ) -> Result<(), ServerError>;
105
106    /// Write rank `root`'s `src` of `elements` to `dst` on every rank. Only `root` has a `src`.
107    ///
108    /// # Errors
109    ///
110    /// The driver's refusal to enqueue the broadcast.
111    fn broadcast(
112        comm: &Self::Communicator,
113        src: Option<&DeviceResource<Self>>,
114        dst: &DeviceResource<Self>,
115        elements: Elements<Self::DataType>,
116        root: usize,
117        stream: Self::CommStream,
118    ) -> Result<(), ServerError>;
119
120    /// Send chunk `r` of `src` to rank `r` and write the chunk rank `r` sends to chunk `r` of
121    /// `dst`, each of the `ranks` chunks holding `elements`.
122    ///
123    /// # Errors
124    ///
125    /// The driver's refusal to enqueue the exchange.
126    fn all_to_all(
127        comm: &Self::Communicator,
128        src: &DeviceResource<Self>,
129        dst: &DeviceResource<Self>,
130        elements: Elements<Self::DataType>,
131        ranks: usize,
132        stream: Self::CommStream,
133    ) -> Result<(), ServerError>;
134
135    /// Send `src` to `peer`.
136    ///
137    /// # Errors
138    ///
139    /// The driver's refusal to enqueue the send.
140    fn send(
141        comm: &Self::Communicator,
142        src: &DeviceResource<Self>,
143        elements: Elements<Self::DataType>,
144        peer: usize,
145        stream: Self::CommStream,
146    ) -> Result<(), ServerError>;
147
148    /// Receive into `dst` from `peer`.
149    ///
150    /// # Errors
151    ///
152    /// The driver's refusal to enqueue the receive.
153    fn recv(
154        comm: &Self::Communicator,
155        dst: &DeviceResource<Self>,
156        elements: Elements<Self::DataType>,
157        peer: usize,
158        stream: Self::CommStream,
159    ) -> Result<(), ServerError>;
160}
161
162/// A buffer's elements, with their type as the driver names it.
163#[derive(Debug, Clone, Copy)]
164pub struct Elements<T> {
165    /// The driver's name for the element type.
166    pub data_type: T,
167    /// How many elements.
168    pub count: usize,
169}
170
171/// The groups this device has joined.
172///
173/// One per server, unlike the identifiers behind
174/// [`group_id`](CollectiveDriver::group_id): an identifier is what every rank
175/// of a group agrees on, a communicator is one rank's membership of it.
176pub struct Collectives<D: CollectiveDriver> {
177    /// This device, whose position in a sorted group is its rank.
178    device: DeviceId,
179    joined: HashMap<CommunicationId, D::Communicator>,
180    /// The groups whose part here failed, each with the failure that broke it.
181    broken: HashMap<CommunicationId, ServerError>,
182}
183
184impl<D: CollectiveDriver> core::fmt::Debug for Collectives<D> {
185    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
186        // The communicators are opaque driver handles, so which groups this
187        // device is in is the whole of what there is to say.
188        f.debug_struct("Collectives")
189            .field("device", &self.device)
190            .field("joined", &self.joined.len())
191            .field("broken", &self.broken.len())
192            .finish()
193    }
194}
195
196impl<D: CollectiveDriver> Collectives<D> {
197    /// A device that has joined nothing yet.
198    pub fn new(device: DeviceId) -> Self {
199        Self {
200            device,
201            joined: HashMap::default(),
202            broken: HashMap::default(),
203        }
204    }
205
206    /// Records that this device's part of a collective over `group` failed with `cause`. Its
207    /// peers wait in that collective, and another one over the group would pair with theirs.
208    pub fn mark_broken(&mut self, group: &CommunicationId, cause: ServerError) {
209        self.broken.entry(group.clone()).or_insert(cause);
210    }
211
212    /// Refuses a collective over `group` once a part of one here has failed.
213    ///
214    /// # Errors
215    ///
216    /// [`ServerError::Generic`] naming the failure that broke the group.
217    pub fn ensure_intact(&self, group: &CommunicationId) -> Result<(), ServerError> {
218        match self.broken.get(group) {
219            None => Ok(()),
220            Some(cause) => Err(ServerError::Generic {
221                reason: format!(
222                    "an earlier collective over this group failed on this device, so its peers \
223                     wait in that one and this device runs no other over the group: {cause}"
224                ),
225                backtrace: BackTrace::capture(),
226            }),
227        }
228    }
229
230    /// Join the group over `devices`, and answer with the id it is known by.
231    ///
232    /// A group already joined is joined once: `Ok(None)` says there was
233    /// nothing to do, so the caller does not announce a membership twice.
234    ///
235    /// # Errors
236    ///
237    /// [`ServerError::Generic`] when this device is not among `devices` — it
238    /// would have no rank in a group it is not in — and whatever the driver
239    /// says about agreeing an identifier or joining.
240    pub fn join(&mut self, devices: Vec<DeviceId>) -> Result<Option<CommunicationId>, ServerError> {
241        self.join_as(CommunicationId::from(devices.clone()), devices)
242    }
243
244    /// The communicator for transfers between this device and `peer`, joined on first use.
245    ///
246    /// # Errors
247    ///
248    /// Whatever the driver says about agreeing an identifier or joining.
249    pub fn transfers_with(&mut self, peer: DeviceId) -> Result<&D::Communicator, ServerError> {
250        let id = CommunicationId::transfers_between(self.device, peer);
251        self.join_as(id.clone(), vec![self.device, peer])?;
252        self.get(&id)
253    }
254
255    fn join_as(
256        &mut self,
257        id: CommunicationId,
258        devices: Vec<DeviceId>,
259    ) -> Result<Option<CommunicationId>, ServerError> {
260        if self.joined.contains_key(&id) {
261            return Ok(None);
262        }
263        // Sorted, because a device's rank is its position and every rank has
264        // to compute the same one. Two ranks disagreeing does not fail: it
265        // hangs, with each waiting for a peer that is answering to a different
266        // number.
267        let mut devices = devices;
268        devices.sort();
269        // Two ranks on one device would each wait in its join for the other.
270        if let Some(pair) = devices.windows(2).find(|pair| pair[0] == pair[1]) {
271            return Err(ServerError::Generic {
272                reason: format!(
273                    "{:?} appears twice among the devices of a group, which need one rank each",
274                    pair[0]
275                ),
276                backtrace: BackTrace::capture(),
277            });
278        }
279        let rank = rank_in(self.device, &devices).ok_or_else(|| ServerError::Generic {
280            reason: format!(
281                "this device ({:?}) is not among the {} the group was formed over, \
282                     so it has no rank in it",
283                self.device,
284                devices.len()
285            ),
286            backtrace: BackTrace::capture(),
287        })?;
288
289        let comm = D::join(D::group_id(&id)?, devices.len(), rank)?;
290        self.joined.insert(id.clone(), comm);
291        Ok(Some(id))
292    }
293
294    /// This device's membership of the group over `devices`.
295    ///
296    /// # Errors
297    ///
298    /// [`ServerError::Generic`] when this device never joined that group, which
299    /// is a missing [`join`](Self::join) rather than anything the device did.
300    pub fn get(&self, devices: &CommunicationId) -> Result<&D::Communicator, ServerError> {
301        self.joined
302            .get(devices)
303            .ok_or_else(|| ServerError::Generic {
304                reason: "no communicator for this group; it has to be joined first".into(),
305                backtrace: BackTrace::capture(),
306            })
307    }
308
309    /// The rank of `device` in the group over `devices`.
310    ///
311    /// # Errors
312    ///
313    /// [`ServerError::Generic`] when `device` is not among `devices`.
314    pub fn rank(&self, device: DeviceId, devices: &[DeviceId]) -> Result<usize, ServerError> {
315        let mut sorted = devices.to_vec();
316        sorted.sort();
317        rank_in(device, &sorted).ok_or_else(|| ServerError::Generic {
318            reason: format!("{device:?} is not among the devices of the group"),
319            backtrace: BackTrace::capture(),
320        })
321    }
322
323    /// The rank of the one device in `devices` that is not this one.
324    ///
325    /// For the two-device operations — a send has exactly one peer, and its
326    /// rank is whichever position this device does not occupy.
327    ///
328    /// # Errors
329    ///
330    /// [`ServerError::Generic`] when every device in the pair is this one, so
331    /// there is no peer to name.
332    pub fn peer_rank(&self, devices: &[DeviceId]) -> Result<usize, ServerError> {
333        peer_of(self.device, devices).ok_or_else(|| ServerError::Generic {
334            reason: format!(
335                "every device in the pair is this one ({:?}), so there is no peer",
336                self.device
337            ),
338            backtrace: BackTrace::capture(),
339        })
340    }
341}
342
343/// `device`'s position in `devices`, which is its rank.
344///
345/// Free of the driver because a rank is arithmetic over device ids and
346/// nothing else — which is also what lets it be checked without one.
347fn rank_in(device: DeviceId, devices: &[DeviceId]) -> Option<usize> {
348    devices.iter().position(|id| id.index_id == device.index_id)
349}
350
351/// The position of the one device in `devices` that is not `device`.
352fn peer_of(device: DeviceId, devices: &[DeviceId]) -> Option<usize> {
353    devices.iter().position(|id| id.index_id != device.index_id)
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use alloc::vec;
360
361    /// A device is its index, since that is all a rank is computed from.
362    fn device(index: u16) -> DeviceId {
363        DeviceId {
364            type_id: 0,
365            index_id: index,
366        }
367    }
368
369    /// A rank is a position in the sorted group, so every member computes the
370    /// same one whatever order it was handed the devices in.
371    ///
372    /// The property worth defending above every other here, because breaking
373    /// it does not fail: two ranks that disagree hang, each waiting for a peer
374    /// that is answering to a different number.
375    #[test]
376    fn a_rank_does_not_depend_on_the_order_the_group_was_given_in() {
377        let group = [device(7), device(2), device(5)];
378        for given in [
379            vec![group[0], group[1], group[2]],
380            vec![group[2], group[1], group[0]],
381            vec![group[1], group[2], group[0]],
382        ] {
383            let mut sorted = given.clone();
384            sorted.sort();
385            for (expected, member) in sorted.iter().enumerate() {
386                assert_eq!(
387                    rank_in(*member, &sorted),
388                    Some(expected),
389                    "{member:?} in {given:?}"
390                );
391            }
392        }
393    }
394
395    /// A device outside the group has no rank in it, rather than silently
396    /// taking someone else's.
397    #[test]
398    fn a_device_outside_the_group_has_no_rank() {
399        assert_eq!(rank_in(device(9), &[device(1), device(2)]), None);
400    }
401
402    /// The peer of a pair is whichever member is not this device.
403    #[test]
404    fn the_peer_of_a_pair_is_the_other_member() {
405        let pair = [device(3), device(8)];
406        assert_eq!(peer_of(device(3), &pair), Some(1));
407        assert_eq!(peer_of(device(8), &pair), Some(0));
408    }
409
410    /// A pair of one device has no peer, rather than a rank that would send a
411    /// transfer to the device it came from.
412    #[test]
413    fn a_pair_of_one_device_has_no_peer() {
414        assert_eq!(peer_of(device(4), &[device(4), device(4)]), None);
415    }
416
417    /// Ranks are compared by device index alone, so two devices of different
418    /// kinds at the same index are the same member of a group.
419    ///
420    /// Not obviously right — it is what the collective has always done — but
421    /// worth pinning, because a group spanning two device types would silently
422    /// give both the same rank.
423    #[test]
424    fn a_rank_is_the_device_index_and_nothing_else() {
425        let other_kind = DeviceId {
426            type_id: 1,
427            index_id: 2,
428        };
429        assert_eq!(rank_in(other_kind, &[device(1), device(2)]), Some(1));
430    }
431}