use super::{Client, PEER_ORDER};
use crate::server::{
Collective, CollectiveBuffer, ComputeWait, Handle, Overlap, ReduceOperation, Reduction, Server,
};
use alloc::{collections::BTreeSet, vec::Vec};
use cubecl_common::{device::DeviceId, device_handle::DeviceHandle};
use cubecl_ir::ElemType;
use thiserror::Error;
#[derive(Clone)]
pub struct ClientGroup {
clients: Vec<Client>,
device_ids: Vec<DeviceId>,
reduces: BTreeSet<ElemType>,
}
#[derive(Debug)]
pub struct MemberBuffers {
pub src: Handle,
pub dst: Handle,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ClientGroupError {
#[error("a client group needs at least one client")]
Empty,
#[error("collectives cannot run on a blocking device handle")]
BlockingDeviceHandle,
#[error("the clients run different runtimes")]
MixedRuntimes,
#[error("{runtime} has no transport between its devices")]
NoDeviceTransport {
runtime: &'static str,
},
#[error("the devices are not in strictly ascending order: {devices:?}")]
NotAscending {
devices: Vec<DeviceId>,
},
}
#[derive(Debug, Clone, Copy)]
enum InPlace {
Allowed,
Refused,
}
impl ClientGroup {
pub fn new(clients: Vec<Client>) -> Result<Self, ClientGroupError> {
let first = clients.first().ok_or(ClientGroupError::Empty)?;
if DeviceHandle::<dyn Server>::is_blocking() {
return Err(ClientGroupError::BlockingDeviceHandle);
}
let runtime = first.service_id().service;
if clients
.iter()
.any(|client| client.service_id().service != runtime)
{
return Err(ClientGroupError::MixedRuntimes);
}
let device_ids: Vec<DeviceId> = clients
.iter()
.map(|client| client.device.device_id())
.collect();
if device_ids.windows(2).any(|pair| pair[0] >= pair[1]) {
return Err(ClientGroupError::NotAscending {
devices: device_ids,
});
}
let Some(transports) = clients
.iter()
.map(|client| client.utilities.device_transport.as_ref())
.collect::<Option<Vec<_>>>()
else {
return Err(ClientGroupError::NoDeviceTransport {
runtime: first.name(),
});
};
let mut reduces = transports[0].reduces.clone();
for transport in &transports[1..] {
reduces.retain(|dtype| transport.reduces.contains(dtype));
}
Ok(Self {
clients,
device_ids,
reduces,
})
}
pub fn clients(&self) -> &[Client] {
&self.clients
}
pub fn reduces(&self, dtype: ElemType) -> bool {
self.reduces.contains(&dtype)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self, buffers))
)]
pub fn all_reduce(&self, buffers: Vec<MemberBuffers>, dtype: ElemType, op: ReduceOperation) {
let collective = Collective::AllReduce;
self.expect_reduces(collective, dtype);
let src = self.expect_buffers(collective, &buffers, InPlace::Allowed);
Client::expect_whole_elements(collective, dtype, src);
Self::expect_sizes(
collective,
CollectiveBuffer::Destination,
buffers.iter().map(|it| &it.dst),
src,
);
self.queue(collective, buffers, |_, client, buffers, device_ids| {
client.queue_all_reduce(
buffers.src,
buffers.dst,
Reduction { dtype, op },
device_ids,
ComputeWait::Now,
)
});
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self, buffers))
)]
pub fn all_gather(&self, buffers: Vec<MemberBuffers>) {
let collective = Collective::AllGather;
let src = self.expect_buffers(collective, &buffers, InPlace::Refused);
let members = self.clients.len() as u64;
Self::expect_sizes(
collective,
CollectiveBuffer::Destination,
buffers.iter().map(|it| &it.dst),
src * members,
);
self.queue(collective, buffers, |_, client, buffers, device_ids| {
client.queue_all_gather(buffers.src, buffers.dst, device_ids)
});
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self, buffers))
)]
pub fn reduce_scatter(
&self,
buffers: Vec<MemberBuffers>,
dtype: ElemType,
op: ReduceOperation,
) {
let collective = Collective::ReduceScatter;
self.expect_reduces(collective, dtype);
let src = self.expect_buffers(collective, &buffers, InPlace::Refused);
let chunk = self.expect_chunks(collective, src);
Client::expect_whole_elements(collective, dtype, chunk);
Self::expect_sizes(
collective,
CollectiveBuffer::Destination,
buffers.iter().map(|it| &it.dst),
chunk,
);
self.queue(collective, buffers, |_, client, buffers, device_ids| {
let reduction = Reduction { dtype, op };
client.queue_reduce_scatter(buffers.src, buffers.dst, reduction, device_ids)
});
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self, src, dst))
)]
pub fn broadcast(&self, src: Handle, dst: Vec<Handle>) {
let collective = Collective::Broadcast;
self.expect_parts(collective, dst.len());
let root = self
.clients
.iter()
.position(|client| client.service_id() == src.service)
.unwrap_or_else(|| {
panic!(
"a broadcast's source is on {}, which is no member's device",
src.service
)
});
self.expect_member_buffers(collective, root, &src, &dst[root], InPlace::Allowed);
for (member, dst) in dst.iter().enumerate() {
self.expect_on_member(member, dst);
}
Self::expect_sizes(
collective,
CollectiveBuffer::Destination,
dst.iter(),
src.size_in_used(),
);
let root_id = self.device_ids[root];
self.queue(collective, dst, |member, client, dst, device_ids| {
let src = (member == root).then(|| src.clone());
client.queue_broadcast(src, dst, root_id, device_ids)
});
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self, buffers))
)]
pub fn all_to_all(&self, buffers: Vec<MemberBuffers>) {
let collective = Collective::AllToAll;
let src = self.expect_buffers(collective, &buffers, InPlace::Refused);
self.expect_chunks(collective, src);
Self::expect_sizes(
collective,
CollectiveBuffer::Destination,
buffers.iter().map(|it| &it.dst),
src,
);
self.queue(collective, buffers, |_, client, buffers, device_ids| {
client.queue_all_to_all(buffers.src, buffers.dst, device_ids)
});
}
fn expect_buffers(
&self,
collective: Collective,
buffers: &[MemberBuffers],
in_place: InPlace,
) -> u64 {
self.expect_parts(collective, buffers.len());
for (member, buffers) in buffers.iter().enumerate() {
self.expect_member_buffers(collective, member, &buffers.src, &buffers.dst, in_place);
}
let src = buffers[0].src.size_in_used();
Self::expect_sizes(
collective,
CollectiveBuffer::Source,
buffers.iter().map(|it| &it.src),
src,
);
src
}
fn expect_parts(&self, collective: Collective, parts: usize) {
let members = self.clients.len();
assert!(
parts == members,
"{collective} over {members} members takes one part each, not {parts}"
);
}
fn expect_reduces(&self, collective: Collective, dtype: ElemType) {
assert!(
self.reduces(dtype),
"{collective} on {} cannot reduce {dtype:?}",
self.clients[0].name()
);
}
fn expect_chunks(&self, collective: Collective, size: u64) -> u64 {
let members = self.clients.len() as u64;
assert!(
size.is_multiple_of(members),
"{collective} over {members} members cannot split {size} bytes evenly"
);
size / members
}
fn expect_member_buffers(
&self,
collective: Collective,
member: usize,
src: &Handle,
dst: &Handle,
in_place: InPlace,
) {
self.expect_on_member(member, src);
self.expect_on_member(member, dst);
let overlap = src.overlap(dst);
assert!(
matches!(
(overlap, in_place),
(Overlap::Disjoint, _) | (Overlap::Same, InPlace::Allowed)
),
"member {member}'s {collective} source and destination overlap ({overlap:?})"
);
}
fn expect_on_member(&self, member: usize, handle: &Handle) {
let device = self.clients[member].service_id();
assert!(
handle.service == device,
"member {member}'s buffer is on {}, not on its device {device}",
handle.service
);
}
fn expect_sizes<'a>(
collective: Collective,
buffer: CollectiveBuffer,
handles: impl Iterator<Item = &'a Handle>,
expected: u64,
) {
for (member, handle) in handles.enumerate() {
let size = handle.size_in_used();
assert!(
size == expected,
"member {member}'s {collective} {buffer} holds {size} bytes where it needs {expected}"
);
}
}
fn queue<Part>(
&self,
collective: Collective,
parts: Vec<Part>,
mut queue_part: impl FnMut(usize, &Client, Part, Vec<DeviceId>),
) {
self.clients[0].prepare_collective(collective, self.device_ids.clone());
let _order = PEER_ORDER.lock();
for (member, (client, part)) in self.clients.iter().zip(parts).enumerate() {
queue_part(member, client, part, self.device_ids.clone());
}
for client in &self.clients {
client.device.flush_queue();
}
}
}