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}