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}