Skip to main content

cubecl_runtime/client/
group.rs

1//! Collectives queued by one caller over the clients of several devices.
2
3use 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/// The clients of distinct devices of one runtime that run collectives together, for a caller
13/// holding every member's buffers.
14///
15/// Member `i` is rank `i`, in ascending device order. Work queued after a collective reads its
16/// result, but per-device `all_reduce` calls are not ordered against it, and on a device both use
17/// the two can hang. A member whose part fails, such as one whose source an earlier failure left
18/// stale, leaves the others waiting in it and runs no later collective over the group. Buffers
19/// move as bytes, so a pitched handle's row padding counts toward its chunks.
20#[derive(Clone)]
21pub struct ClientGroup {
22    clients: Vec<Client>,
23    device_ids: Vec<DeviceId>,
24    reduces: BTreeSet<ElemType>,
25}
26
27/// One member's buffers in a collective.
28#[derive(Debug)]
29pub struct MemberBuffers {
30    /// What the member contributes.
31    pub src: Handle,
32    /// Where the member's result lands.
33    pub dst: Handle,
34}
35
36/// Why clients cannot form a [`ClientGroup`].
37#[derive(Debug, Error, Clone, PartialEq, Eq)]
38pub enum ClientGroupError {
39    /// There are no clients.
40    #[error("a client group needs at least one client")]
41    Empty,
42    /// The device handle blocks the caller, which collectives cannot run on.
43    #[error("collectives cannot run on a blocking device handle")]
44    BlockingDeviceHandle,
45    /// The clients run different runtimes, which share no transport.
46    #[error("the clients run different runtimes")]
47    MixedRuntimes,
48    /// The runtime has no transport between its devices.
49    #[error("{runtime} has no transport between its devices")]
50    NoDeviceTransport {
51        /// The runtime's name.
52        runtime: &'static str,
53    },
54    /// The devices are not in strictly ascending order, so a member would not be its rank, or a
55    /// device would hold two ranks.
56    #[error("the devices are not in strictly ascending order: {devices:?}")]
57    NotAscending {
58        /// The devices, in the order given.
59        devices: Vec<DeviceId>,
60    },
61}
62
63/// Whether a collective may run with a member's source and destination on the same bytes.
64#[derive(Debug, Clone, Copy)]
65enum InPlace {
66    Allowed,
67    Refused,
68}
69
70impl ClientGroup {
71    /// A group of `clients`, one per member.
72    ///
73    /// # Errors
74    ///
75    /// When there are none, when the device handle blocks, when they mix runtimes, when their
76    /// devices are not in strictly ascending order, and when their runtime has no device
77    /// transport.
78    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        // NCCL refuses a reduction unless every member's device supports it.
109        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    /// The members' clients, member `i` first being rank `i`.
121    pub fn clients(&self) -> &[Client] {
122        &self.clients
123    }
124
125    /// Whether the members reduce elements of `dtype` together. Every other collective moves bytes,
126    /// whatever their type.
127    pub fn reduces(&self, dtype: ElemType) -> bool {
128        self.reduces.contains(&dtype)
129    }
130
131    /// Every member's `dst` receives every member's `src` reduced by `op`. A member's `src` may be
132    /// its `dst`.
133    #[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    /// Every member's `dst` receives every member's `src`, concatenated in member order.
160    #[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    /// Member `i`'s `dst` receives chunk `i` of every member's `src` reduced by `op`.
180    #[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    /// Every member's `dst` receives `src`, sent by the member whose device holds it. That
208    /// member's `dst` may be `src`.
209    #[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    /// Member `i`'s `dst` receives chunk `i` of every member's `src`, concatenated in member
245    /// order.
246    #[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    /// Checks one part per member, each member's buffers, and sources of one size; returns the
266    /// size of each source.
267    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    /// Checks `size` bytes split evenly over the members, and returns the size of a chunk.
304    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    /// Checks a member's source and destination are on its device, and apart unless the
314    /// collective runs in place on exactly the same bytes.
315    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    /// Prepares every member, then queues each member's part before anything else can queue.
360    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        // Outside the lock, which setting the group up takes itself.
367        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        // A part waits in its client's queue until flushed, and a member blocked reading its
373        // result would wait on a peer whose part never reached the device.
374        for client in &self.clients {
375            client.device.flush_queue();
376        }
377    }
378}