Skip to main content

cubecl_runtime/client/
group.rs

1//! Collectives over the clients of several devices: queued by one caller for every member at
2//! once, or by each member for itself.
3
4use 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/// The clients of distinct devices of one runtime that run collectives together: for a caller
14/// holding every member's buffers, or through [`members`](Self::members) for each member queueing
15/// its own part.
16///
17/// Member `i` is rank `i`, in ascending device order. Work queued after a collective reads its
18/// result, but per-device `all_reduce` calls are not ordered against it, and on a device both use
19/// the two can hang. A member whose part fails, such as one whose source an earlier failure left
20/// stale, leaves the others waiting in it and runs no later collective over the group. Buffers
21/// move as bytes, so a pitched handle's row padding counts toward its chunks.
22#[derive(Clone)]
23pub struct ClientGroup {
24    clients: Vec<Client>,
25    device_ids: Vec<DeviceId>,
26    reduces: BTreeSet<ElemType>,
27}
28
29/// One member's buffers in a collective.
30#[derive(Debug)]
31pub struct MemberBuffers {
32    /// What the member contributes.
33    pub src: Handle,
34    /// Where the member's result lands.
35    pub dst: Handle,
36}
37
38/// Why clients cannot form a [`ClientGroup`].
39#[derive(Debug, Error, Clone, PartialEq, Eq)]
40pub enum ClientGroupError {
41    /// There are no clients.
42    #[error("a client group needs at least one client")]
43    Empty,
44    /// The device handle blocks the caller, which collectives cannot run on.
45    #[error("collectives cannot run on a blocking device handle")]
46    BlockingDeviceHandle,
47    /// The clients run different runtimes, which share no transport.
48    #[error("the clients run different runtimes")]
49    MixedRuntimes,
50    /// The runtime has no transport between its devices.
51    #[error("{runtime} has no transport between its devices")]
52    NoDeviceTransport {
53        /// The runtime's name.
54        runtime: &'static str,
55    },
56    /// The devices are not in strictly ascending order, so a member would not be its rank, or a
57    /// device would hold two ranks.
58    #[error("the devices are not in strictly ascending order: {devices:?}")]
59    NotAscending {
60        /// The devices, in the order given.
61        devices: Vec<DeviceId>,
62    },
63}
64
65/// Whether a collective may run with a member's source and destination on the same bytes.
66#[derive(Debug, Clone, Copy)]
67enum InPlace {
68    Allowed,
69    Refused,
70}
71
72impl ClientGroup {
73    /// A group of `clients`, one per member.
74    ///
75    /// # Errors
76    ///
77    /// When there are none, when the device handle blocks, when they mix runtimes, when their
78    /// devices are not in strictly ascending order, and when their runtime has no device
79    /// transport.
80    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        // NCCL refuses a reduction unless every member's device supports it.
111        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    /// The members' clients, member `i` first being rank `i`.
123    pub fn clients(&self) -> &[Client] {
124        &self.clients
125    }
126
127    /// Whether the members reduce elements of `dtype` together. Every other collective moves bytes,
128    /// whatever their type.
129    pub fn reduces(&self, dtype: ElemType) -> bool {
130        self.reduces.contains(&dtype)
131    }
132
133    /// Every member's `dst` receives every member's `src` reduced by `op`. A member's `src` may be
134    /// its `dst`.
135    #[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    /// Every member's `dst` receives every member's `src`, concatenated in member order.
144    #[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    /// Member `i`'s `dst` receives chunk `i` of every member's `src` reduced by `op`.
153    #[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    /// Every member's `dst` receives `src`, sent by the member whose device holds it. That
167    /// member's `dst` may be `src`.
168    #[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    /// Member `i`'s `dst` receives chunk `i` of every member's `src`, concatenated in member
204    /// order.
205    #[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    /// Every member, rank `i` at position `i`, each queueing its own part of the collectives over
214    /// the group: for a caller that reaches each member's buffers from somewhere else, such as
215    /// that device's own thread.
216    ///
217    /// Sets the group up on every device first, so that no part queued afterwards reaches its
218    /// device before the group it runs over.
219    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    /// Checks every member's part of `exchange`, with sources of one size, then queues them all.
230    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    /// Checks `size` bytes split evenly over the members, and returns the size of a chunk.
265    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    /// Checks a member's source and destination are on its device, and apart unless the
275    /// collective runs in place on exactly the same bytes.
276    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    /// Prepares every member, then queues each member's part before anything else can queue.
331    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        // Outside the lock, which setting the group up takes itself.
338        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        // A part waits in its client's queue until flushed, and a member blocked reading its
344        // result would wait on a peer whose part never reached the device.
345        for client in &self.clients {
346            client.device.flush_queue();
347        }
348    }
349}
350
351/// One member of a [`ClientGroup`], queueing its own part of each collective over the group.
352///
353/// Every member queues its part of every collective over the group, and in one order on every
354/// member: the members wait for each other on their devices, so a part queued out of order, or
355/// never, hangs the others. Two callers queueing over one group agree on that order themselves.
356/// Each part is checked against its own member's buffers only: sources of different sizes on
357/// different members fail on the devices instead.
358#[derive(Clone)]
359pub struct GroupMember {
360    group: ClientGroup,
361    rank: usize,
362}
363
364impl GroupMember {
365    /// This member's rank in its group.
366    pub fn rank(&self) -> usize {
367        self.rank
368    }
369
370    /// This member's part of [`ClientGroup::all_reduce`].
371    pub fn all_reduce(&self, buffers: MemberBuffers, dtype: ElemType, op: ReduceOperation) {
372        self.exchange(Exchange::AllReduce(Reduction { dtype, op }), buffers);
373    }
374
375    /// This member's part of [`ClientGroup::all_gather`].
376    pub fn all_gather(&self, buffers: MemberBuffers) {
377        self.exchange(Exchange::AllGather, buffers);
378    }
379
380    /// This member's part of [`ClientGroup::reduce_scatter`].
381    pub fn reduce_scatter(&self, buffers: MemberBuffers, dtype: ElemType, op: ReduceOperation) {
382        self.exchange(Exchange::ReduceScatter(Reduction { dtype, op }), buffers);
383    }
384
385    /// This member's part of [`ClientGroup::all_to_all`].
386    pub fn all_to_all(&self, buffers: MemberBuffers) {
387        self.exchange(Exchange::AllToAll, buffers);
388    }
389
390    /// Checks and queues this member's part, then hands it to its device at once: the peers
391    /// queueing theirs wait in it on their devices until it arrives.
392    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/// A collective every member contributes a source to and receives a destination from.
401#[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    /// Checks member `rank`'s buffers: on its device, apart unless the collective runs in place,
420    /// and sized for its source.
421    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    /// Queues one member's part on `client`, its compute stream waiting for it before anything
458    /// queued after.
459    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}