1use super::{Client, PEER_ORDER};
5use crate::server::{
6 Collective, CollectiveBuffer, ComputeWait, Handle, Overlap, ReduceOperation, Reduction, Server,
7};
8use alloc::{collections::BTreeSet, vec::Vec};
9use cubecl_common::{device::DeviceId, device_handle::DeviceHandle};
10use cubecl_ir::ElemType;
11use thiserror::Error;
12
13#[derive(Clone)]
23pub struct ClientGroup {
24 clients: Vec<Client>,
25 device_ids: Vec<DeviceId>,
26 reduces: BTreeSet<ElemType>,
27}
28
29#[derive(Debug)]
31pub struct MemberBuffers {
32 pub src: Handle,
34 pub dst: Handle,
36}
37
38#[derive(Debug, Error, Clone, PartialEq, Eq)]
40pub enum ClientGroupError {
41 #[error("a client group needs at least one client")]
43 Empty,
44 #[error("collectives cannot run on a blocking device handle")]
46 BlockingDeviceHandle,
47 #[error("the clients run different runtimes")]
49 MixedRuntimes,
50 #[error("{runtime} has no transport between its devices")]
52 NoDeviceTransport {
53 runtime: &'static str,
55 },
56 #[error("the devices are not in strictly ascending order: {devices:?}")]
59 NotAscending {
60 devices: Vec<DeviceId>,
62 },
63}
64
65#[derive(Debug, Clone, Copy)]
67enum InPlace {
68 Allowed,
69 Refused,
70}
71
72impl ClientGroup {
73 pub fn new(clients: Vec<Client>) -> Result<Self, ClientGroupError> {
81 let first = clients.first().ok_or(ClientGroupError::Empty)?;
82 if DeviceHandle::<dyn Server>::is_blocking() {
83 return Err(ClientGroupError::BlockingDeviceHandle);
84 }
85 let runtime = first.service_id().service;
86 if clients
87 .iter()
88 .any(|client| client.service_id().service != runtime)
89 {
90 return Err(ClientGroupError::MixedRuntimes);
91 }
92 let device_ids: Vec<DeviceId> = clients
93 .iter()
94 .map(|client| client.device.device_id())
95 .collect();
96 if device_ids.windows(2).any(|pair| pair[0] >= pair[1]) {
97 return Err(ClientGroupError::NotAscending {
98 devices: device_ids,
99 });
100 }
101 let Some(transports) = clients
102 .iter()
103 .map(|client| client.utilities.device_transport.as_ref())
104 .collect::<Option<Vec<_>>>()
105 else {
106 return Err(ClientGroupError::NoDeviceTransport {
107 runtime: first.name(),
108 });
109 };
110 let mut reduces = transports[0].reduces.clone();
112 for transport in &transports[1..] {
113 reduces.retain(|dtype| transport.reduces.contains(dtype));
114 }
115 Ok(Self {
116 clients,
117 device_ids,
118 reduces,
119 })
120 }
121
122 pub fn clients(&self) -> &[Client] {
124 &self.clients
125 }
126
127 pub fn reduces(&self, dtype: ElemType) -> bool {
130 self.reduces.contains(&dtype)
131 }
132
133 #[cfg_attr(
136 feature = "tracing",
137 tracing::instrument(level = "trace", skip(self, buffers))
138 )]
139 pub fn all_reduce(&self, buffers: Vec<MemberBuffers>, dtype: ElemType, op: ReduceOperation) {
140 self.exchange(Exchange::AllReduce(Reduction { dtype, op }), buffers);
141 }
142
143 #[cfg_attr(
145 feature = "tracing",
146 tracing::instrument(level = "trace", skip(self, buffers))
147 )]
148 pub fn all_gather(&self, buffers: Vec<MemberBuffers>) {
149 self.exchange(Exchange::AllGather, buffers);
150 }
151
152 #[cfg_attr(
154 feature = "tracing",
155 tracing::instrument(level = "trace", skip(self, buffers))
156 )]
157 pub fn reduce_scatter(
158 &self,
159 buffers: Vec<MemberBuffers>,
160 dtype: ElemType,
161 op: ReduceOperation,
162 ) {
163 self.exchange(Exchange::ReduceScatter(Reduction { dtype, op }), buffers);
164 }
165
166 #[cfg_attr(
169 feature = "tracing",
170 tracing::instrument(level = "trace", skip(self, src, dst))
171 )]
172 pub fn broadcast(&self, src: Handle, dst: Vec<Handle>) {
173 let collective = Collective::Broadcast;
174 self.expect_parts(collective, dst.len());
175 let root = self
176 .clients
177 .iter()
178 .position(|client| client.service_id() == src.service)
179 .unwrap_or_else(|| {
180 panic!(
181 "a broadcast's source is on {}, which is no member's device",
182 src.service
183 )
184 });
185 self.expect_member_buffers(collective, root, &src, &dst[root], InPlace::Allowed);
186 for (member, dst) in dst.iter().enumerate() {
187 self.expect_on_member(member, dst);
188 }
189 Self::expect_sizes(
190 collective,
191 CollectiveBuffer::Destination,
192 dst.iter(),
193 src.size_in_used(),
194 );
195
196 let root_id = self.device_ids[root];
197 self.queue(collective, dst, |member, client, dst, device_ids| {
198 let src = (member == root).then(|| src.clone());
199 client.queue_broadcast(src, dst, root_id, device_ids)
200 });
201 }
202
203 #[cfg_attr(
206 feature = "tracing",
207 tracing::instrument(level = "trace", skip(self, buffers))
208 )]
209 pub fn all_to_all(&self, buffers: Vec<MemberBuffers>) {
210 self.exchange(Exchange::AllToAll, buffers);
211 }
212
213 pub fn members(&self) -> Vec<GroupMember> {
220 self.clients[0].prepare_collective(Collective::CommInit, self.device_ids.clone());
221 (0..self.clients.len())
222 .map(|rank| GroupMember {
223 group: self.clone(),
224 rank,
225 })
226 .collect()
227 }
228
229 fn exchange(&self, exchange: Exchange, buffers: Vec<MemberBuffers>) {
231 let collective = exchange.collective();
232 self.expect_parts(collective, buffers.len());
233 let src = buffers[0].src.size_in_used();
234 Self::expect_sizes(
235 collective,
236 CollectiveBuffer::Source,
237 buffers.iter().map(|it| &it.src),
238 src,
239 );
240 for (rank, buffers) in buffers.iter().enumerate() {
241 exchange.expect_part(self, rank, buffers);
242 }
243 self.queue(collective, buffers, |_, client, buffers, device_ids| {
244 exchange.queue(client, buffers, device_ids);
245 });
246 }
247
248 fn expect_parts(&self, collective: Collective, parts: usize) {
249 let members = self.clients.len();
250 assert!(
251 parts == members,
252 "{collective} over {members} members takes one part each, not {parts}"
253 );
254 }
255
256 fn expect_reduces(&self, collective: Collective, dtype: ElemType) {
257 assert!(
258 self.reduces(dtype),
259 "{collective} on {} cannot reduce {dtype:?}",
260 self.clients[0].name()
261 );
262 }
263
264 fn expect_chunks(&self, collective: Collective, size: u64) -> u64 {
266 let members = self.clients.len() as u64;
267 assert!(
268 size.is_multiple_of(members),
269 "{collective} over {members} members cannot split {size} bytes evenly"
270 );
271 size / members
272 }
273
274 fn expect_member_buffers(
277 &self,
278 collective: Collective,
279 member: usize,
280 src: &Handle,
281 dst: &Handle,
282 in_place: InPlace,
283 ) {
284 self.expect_on_member(member, src);
285 self.expect_on_member(member, dst);
286 let overlap = src.overlap(dst);
287 assert!(
288 matches!(
289 (overlap, in_place),
290 (Overlap::Disjoint, _) | (Overlap::Same, InPlace::Allowed)
291 ),
292 "member {member}'s {collective} source and destination overlap ({overlap:?})"
293 );
294 }
295
296 fn expect_on_member(&self, member: usize, handle: &Handle) {
297 let device = self.clients[member].service_id();
298 assert!(
299 handle.service == device,
300 "member {member}'s buffer is on {}, not on its device {device}",
301 handle.service
302 );
303 }
304
305 fn expect_sizes<'a>(
306 collective: Collective,
307 buffer: CollectiveBuffer,
308 handles: impl Iterator<Item = &'a Handle>,
309 expected: u64,
310 ) {
311 for (member, handle) in handles.enumerate() {
312 Self::expect_size(collective, buffer, member, handle, expected);
313 }
314 }
315
316 fn expect_size(
317 collective: Collective,
318 buffer: CollectiveBuffer,
319 member: usize,
320 handle: &Handle,
321 expected: u64,
322 ) {
323 let size = handle.size_in_used();
324 assert!(
325 size == expected,
326 "member {member}'s {collective} {buffer} holds {size} bytes where it needs {expected}"
327 );
328 }
329
330 fn queue<Part>(
332 &self,
333 collective: Collective,
334 parts: Vec<Part>,
335 mut queue_part: impl FnMut(usize, &Client, Part, Vec<DeviceId>),
336 ) {
337 self.clients[0].prepare_collective(collective, self.device_ids.clone());
339 let _order = PEER_ORDER.lock();
340 for (member, (client, part)) in self.clients.iter().zip(parts).enumerate() {
341 queue_part(member, client, part, self.device_ids.clone());
342 }
343 for client in &self.clients {
346 client.device.flush_queue();
347 }
348 }
349}
350
351#[derive(Clone)]
359pub struct GroupMember {
360 group: ClientGroup,
361 rank: usize,
362}
363
364impl GroupMember {
365 pub fn rank(&self) -> usize {
367 self.rank
368 }
369
370 pub fn all_reduce(&self, buffers: MemberBuffers, dtype: ElemType, op: ReduceOperation) {
372 self.exchange(Exchange::AllReduce(Reduction { dtype, op }), buffers);
373 }
374
375 pub fn all_gather(&self, buffers: MemberBuffers) {
377 self.exchange(Exchange::AllGather, buffers);
378 }
379
380 pub fn reduce_scatter(&self, buffers: MemberBuffers, dtype: ElemType, op: ReduceOperation) {
382 self.exchange(Exchange::ReduceScatter(Reduction { dtype, op }), buffers);
383 }
384
385 pub fn all_to_all(&self, buffers: MemberBuffers) {
387 self.exchange(Exchange::AllToAll, buffers);
388 }
389
390 fn exchange(&self, exchange: Exchange, buffers: MemberBuffers) {
393 exchange.expect_part(&self.group, self.rank, &buffers);
394 let client = &self.group.clients[self.rank];
395 exchange.queue(client, buffers, self.group.device_ids.clone());
396 client.device.flush_queue();
397 }
398}
399
400#[derive(Debug, Clone, Copy)]
402enum Exchange {
403 AllReduce(Reduction),
404 AllGather,
405 ReduceScatter(Reduction),
406 AllToAll,
407}
408
409impl Exchange {
410 fn collective(self) -> Collective {
411 match self {
412 Self::AllReduce(_) => Collective::AllReduce,
413 Self::AllGather => Collective::AllGather,
414 Self::ReduceScatter(_) => Collective::ReduceScatter,
415 Self::AllToAll => Collective::AllToAll,
416 }
417 }
418
419 fn expect_part(self, group: &ClientGroup, rank: usize, buffers: &MemberBuffers) {
422 let collective = self.collective();
423 if let Self::AllReduce(reduction) | Self::ReduceScatter(reduction) = self {
424 group.expect_reduces(collective, reduction.dtype);
425 }
426 let in_place = match self {
427 Self::AllReduce(_) => InPlace::Allowed,
428 _ => InPlace::Refused,
429 };
430 group.expect_member_buffers(collective, rank, &buffers.src, &buffers.dst, in_place);
431 let src = buffers.src.size_in_used();
432 let dst = match self {
433 Self::AllReduce(reduction) => {
434 Client::expect_whole_elements(collective, reduction.dtype, src);
435 src
436 }
437 Self::AllGather => src * group.clients.len() as u64,
438 Self::ReduceScatter(reduction) => {
439 let chunk = group.expect_chunks(collective, src);
440 Client::expect_whole_elements(collective, reduction.dtype, chunk);
441 chunk
442 }
443 Self::AllToAll => {
444 group.expect_chunks(collective, src);
445 src
446 }
447 };
448 ClientGroup::expect_size(
449 collective,
450 CollectiveBuffer::Destination,
451 rank,
452 &buffers.dst,
453 dst,
454 );
455 }
456
457 fn queue(self, client: &Client, buffers: MemberBuffers, device_ids: Vec<DeviceId>) {
460 let MemberBuffers { src, dst } = buffers;
461 match self {
462 Self::AllReduce(reduction) => {
463 client.queue_all_reduce(src, dst, reduction, device_ids, ComputeWait::Now)
464 }
465 Self::AllGather => client.queue_all_gather(src, dst, device_ids),
466 Self::ReduceScatter(reduction) => {
467 client.queue_reduce_scatter(src, dst, reduction, device_ids)
468 }
469 Self::AllToAll => client.queue_all_to_all(src, dst, device_ids),
470 }
471 }
472}