1use super::{Client, PEER_ORDER};
4use crate::server::{
5 Collective, CollectiveBuffer, ComputeWait, Handle, Overlap, ReduceOperation, Reduction, Server,
6};
7use alloc::{collections::BTreeSet, vec::Vec};
8use cubecl_common::{device::DeviceId, device_handle::DeviceHandle};
9use cubecl_ir::ElemType;
10use thiserror::Error;
11
12#[derive(Clone)]
21pub struct ClientGroup {
22 clients: Vec<Client>,
23 device_ids: Vec<DeviceId>,
24 reduces: BTreeSet<ElemType>,
25}
26
27#[derive(Debug)]
29pub struct MemberBuffers {
30 pub src: Handle,
32 pub dst: Handle,
34}
35
36#[derive(Debug, Error, Clone, PartialEq, Eq)]
38pub enum ClientGroupError {
39 #[error("a client group needs at least one client")]
41 Empty,
42 #[error("collectives cannot run on a blocking device handle")]
44 BlockingDeviceHandle,
45 #[error("the clients run different runtimes")]
47 MixedRuntimes,
48 #[error("{runtime} has no transport between its devices")]
50 NoDeviceTransport {
51 runtime: &'static str,
53 },
54 #[error("the devices are not in strictly ascending order: {devices:?}")]
57 NotAscending {
58 devices: Vec<DeviceId>,
60 },
61}
62
63#[derive(Debug, Clone, Copy)]
65enum InPlace {
66 Allowed,
67 Refused,
68}
69
70impl ClientGroup {
71 pub fn new(clients: Vec<Client>) -> Result<Self, ClientGroupError> {
79 let first = clients.first().ok_or(ClientGroupError::Empty)?;
80 if DeviceHandle::<dyn Server>::is_blocking() {
81 return Err(ClientGroupError::BlockingDeviceHandle);
82 }
83 let runtime = first.service_id().service;
84 if clients
85 .iter()
86 .any(|client| client.service_id().service != runtime)
87 {
88 return Err(ClientGroupError::MixedRuntimes);
89 }
90 let device_ids: Vec<DeviceId> = clients
91 .iter()
92 .map(|client| client.device.device_id())
93 .collect();
94 if device_ids.windows(2).any(|pair| pair[0] >= pair[1]) {
95 return Err(ClientGroupError::NotAscending {
96 devices: device_ids,
97 });
98 }
99 let Some(transports) = clients
100 .iter()
101 .map(|client| client.utilities.device_transport.as_ref())
102 .collect::<Option<Vec<_>>>()
103 else {
104 return Err(ClientGroupError::NoDeviceTransport {
105 runtime: first.name(),
106 });
107 };
108 let mut reduces = transports[0].reduces.clone();
110 for transport in &transports[1..] {
111 reduces.retain(|dtype| transport.reduces.contains(dtype));
112 }
113 Ok(Self {
114 clients,
115 device_ids,
116 reduces,
117 })
118 }
119
120 pub fn clients(&self) -> &[Client] {
122 &self.clients
123 }
124
125 pub fn reduces(&self, dtype: ElemType) -> bool {
128 self.reduces.contains(&dtype)
129 }
130
131 #[cfg_attr(
134 feature = "tracing",
135 tracing::instrument(level = "trace", skip(self, buffers))
136 )]
137 pub fn all_reduce(&self, buffers: Vec<MemberBuffers>, dtype: ElemType, op: ReduceOperation) {
138 let collective = Collective::AllReduce;
139 self.expect_reduces(collective, dtype);
140 let src = self.expect_buffers(collective, &buffers, InPlace::Allowed);
141 Client::expect_whole_elements(collective, dtype, src);
142 Self::expect_sizes(
143 collective,
144 CollectiveBuffer::Destination,
145 buffers.iter().map(|it| &it.dst),
146 src,
147 );
148 self.queue(collective, buffers, |_, client, buffers, device_ids| {
149 client.queue_all_reduce(
150 buffers.src,
151 buffers.dst,
152 Reduction { dtype, op },
153 device_ids,
154 ComputeWait::Now,
155 )
156 });
157 }
158
159 #[cfg_attr(
161 feature = "tracing",
162 tracing::instrument(level = "trace", skip(self, buffers))
163 )]
164 pub fn all_gather(&self, buffers: Vec<MemberBuffers>) {
165 let collective = Collective::AllGather;
166 let src = self.expect_buffers(collective, &buffers, InPlace::Refused);
167 let members = self.clients.len() as u64;
168 Self::expect_sizes(
169 collective,
170 CollectiveBuffer::Destination,
171 buffers.iter().map(|it| &it.dst),
172 src * members,
173 );
174 self.queue(collective, buffers, |_, client, buffers, device_ids| {
175 client.queue_all_gather(buffers.src, buffers.dst, device_ids)
176 });
177 }
178
179 #[cfg_attr(
181 feature = "tracing",
182 tracing::instrument(level = "trace", skip(self, buffers))
183 )]
184 pub fn reduce_scatter(
185 &self,
186 buffers: Vec<MemberBuffers>,
187 dtype: ElemType,
188 op: ReduceOperation,
189 ) {
190 let collective = Collective::ReduceScatter;
191 self.expect_reduces(collective, dtype);
192 let src = self.expect_buffers(collective, &buffers, InPlace::Refused);
193 let chunk = self.expect_chunks(collective, src);
194 Client::expect_whole_elements(collective, dtype, chunk);
195 Self::expect_sizes(
196 collective,
197 CollectiveBuffer::Destination,
198 buffers.iter().map(|it| &it.dst),
199 chunk,
200 );
201 self.queue(collective, buffers, |_, client, buffers, device_ids| {
202 let reduction = Reduction { dtype, op };
203 client.queue_reduce_scatter(buffers.src, buffers.dst, reduction, device_ids)
204 });
205 }
206
207 #[cfg_attr(
210 feature = "tracing",
211 tracing::instrument(level = "trace", skip(self, src, dst))
212 )]
213 pub fn broadcast(&self, src: Handle, dst: Vec<Handle>) {
214 let collective = Collective::Broadcast;
215 self.expect_parts(collective, dst.len());
216 let root = self
217 .clients
218 .iter()
219 .position(|client| client.service_id() == src.service)
220 .unwrap_or_else(|| {
221 panic!(
222 "a broadcast's source is on {}, which is no member's device",
223 src.service
224 )
225 });
226 self.expect_member_buffers(collective, root, &src, &dst[root], InPlace::Allowed);
227 for (member, dst) in dst.iter().enumerate() {
228 self.expect_on_member(member, dst);
229 }
230 Self::expect_sizes(
231 collective,
232 CollectiveBuffer::Destination,
233 dst.iter(),
234 src.size_in_used(),
235 );
236
237 let root_id = self.device_ids[root];
238 self.queue(collective, dst, |member, client, dst, device_ids| {
239 let src = (member == root).then(|| src.clone());
240 client.queue_broadcast(src, dst, root_id, device_ids)
241 });
242 }
243
244 #[cfg_attr(
247 feature = "tracing",
248 tracing::instrument(level = "trace", skip(self, buffers))
249 )]
250 pub fn all_to_all(&self, buffers: Vec<MemberBuffers>) {
251 let collective = Collective::AllToAll;
252 let src = self.expect_buffers(collective, &buffers, InPlace::Refused);
253 self.expect_chunks(collective, src);
254 Self::expect_sizes(
255 collective,
256 CollectiveBuffer::Destination,
257 buffers.iter().map(|it| &it.dst),
258 src,
259 );
260 self.queue(collective, buffers, |_, client, buffers, device_ids| {
261 client.queue_all_to_all(buffers.src, buffers.dst, device_ids)
262 });
263 }
264
265 fn expect_buffers(
268 &self,
269 collective: Collective,
270 buffers: &[MemberBuffers],
271 in_place: InPlace,
272 ) -> u64 {
273 self.expect_parts(collective, buffers.len());
274 for (member, buffers) in buffers.iter().enumerate() {
275 self.expect_member_buffers(collective, member, &buffers.src, &buffers.dst, in_place);
276 }
277 let src = buffers[0].src.size_in_used();
278 Self::expect_sizes(
279 collective,
280 CollectiveBuffer::Source,
281 buffers.iter().map(|it| &it.src),
282 src,
283 );
284 src
285 }
286
287 fn expect_parts(&self, collective: Collective, parts: usize) {
288 let members = self.clients.len();
289 assert!(
290 parts == members,
291 "{collective} over {members} members takes one part each, not {parts}"
292 );
293 }
294
295 fn expect_reduces(&self, collective: Collective, dtype: ElemType) {
296 assert!(
297 self.reduces(dtype),
298 "{collective} on {} cannot reduce {dtype:?}",
299 self.clients[0].name()
300 );
301 }
302
303 fn expect_chunks(&self, collective: Collective, size: u64) -> u64 {
305 let members = self.clients.len() as u64;
306 assert!(
307 size.is_multiple_of(members),
308 "{collective} over {members} members cannot split {size} bytes evenly"
309 );
310 size / members
311 }
312
313 fn expect_member_buffers(
316 &self,
317 collective: Collective,
318 member: usize,
319 src: &Handle,
320 dst: &Handle,
321 in_place: InPlace,
322 ) {
323 self.expect_on_member(member, src);
324 self.expect_on_member(member, dst);
325 let overlap = src.overlap(dst);
326 assert!(
327 matches!(
328 (overlap, in_place),
329 (Overlap::Disjoint, _) | (Overlap::Same, InPlace::Allowed)
330 ),
331 "member {member}'s {collective} source and destination overlap ({overlap:?})"
332 );
333 }
334
335 fn expect_on_member(&self, member: usize, handle: &Handle) {
336 let device = self.clients[member].service_id();
337 assert!(
338 handle.service == device,
339 "member {member}'s buffer is on {}, not on its device {device}",
340 handle.service
341 );
342 }
343
344 fn expect_sizes<'a>(
345 collective: Collective,
346 buffer: CollectiveBuffer,
347 handles: impl Iterator<Item = &'a Handle>,
348 expected: u64,
349 ) {
350 for (member, handle) in handles.enumerate() {
351 let size = handle.size_in_used();
352 assert!(
353 size == expected,
354 "member {member}'s {collective} {buffer} holds {size} bytes where it needs {expected}"
355 );
356 }
357 }
358
359 fn queue<Part>(
361 &self,
362 collective: Collective,
363 parts: Vec<Part>,
364 mut queue_part: impl FnMut(usize, &Client, Part, Vec<DeviceId>),
365 ) {
366 self.clients[0].prepare_collective(collective, self.device_ids.clone());
368 let _order = PEER_ORDER.lock();
369 for (member, (client, part)) in self.clients.iter().zip(parts).enumerate() {
370 queue_part(member, client, part, self.device_ids.clone());
371 }
372 for client in &self.clients {
375 client.device.flush_queue();
376 }
377 }
378}