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) {
self.exchange(Exchange::AllReduce(Reduction { dtype, op }), buffers);
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self, buffers))
)]
pub fn all_gather(&self, buffers: Vec<MemberBuffers>) {
self.exchange(Exchange::AllGather, buffers);
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self, buffers))
)]
pub fn reduce_scatter(
&self,
buffers: Vec<MemberBuffers>,
dtype: ElemType,
op: ReduceOperation,
) {
self.exchange(Exchange::ReduceScatter(Reduction { dtype, op }), buffers);
}
#[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>) {
self.exchange(Exchange::AllToAll, buffers);
}
pub fn members(&self) -> Vec<GroupMember> {
self.clients[0].prepare_collective(Collective::CommInit, self.device_ids.clone());
(0..self.clients.len())
.map(|rank| GroupMember {
group: self.clone(),
rank,
})
.collect()
}
fn exchange(&self, exchange: Exchange, buffers: Vec<MemberBuffers>) {
let collective = exchange.collective();
self.expect_parts(collective, buffers.len());
let src = buffers[0].src.size_in_used();
Self::expect_sizes(
collective,
CollectiveBuffer::Source,
buffers.iter().map(|it| &it.src),
src,
);
for (rank, buffers) in buffers.iter().enumerate() {
exchange.expect_part(self, rank, buffers);
}
self.queue(collective, buffers, |_, client, buffers, device_ids| {
exchange.queue(client, buffers, device_ids);
});
}
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() {
Self::expect_size(collective, buffer, member, handle, expected);
}
}
fn expect_size(
collective: Collective,
buffer: CollectiveBuffer,
member: usize,
handle: &Handle,
expected: u64,
) {
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();
}
}
}
#[derive(Clone)]
pub struct GroupMember {
group: ClientGroup,
rank: usize,
}
impl GroupMember {
pub fn rank(&self) -> usize {
self.rank
}
pub fn all_reduce(&self, buffers: MemberBuffers, dtype: ElemType, op: ReduceOperation) {
self.exchange(Exchange::AllReduce(Reduction { dtype, op }), buffers);
}
pub fn all_gather(&self, buffers: MemberBuffers) {
self.exchange(Exchange::AllGather, buffers);
}
pub fn reduce_scatter(&self, buffers: MemberBuffers, dtype: ElemType, op: ReduceOperation) {
self.exchange(Exchange::ReduceScatter(Reduction { dtype, op }), buffers);
}
pub fn all_to_all(&self, buffers: MemberBuffers) {
self.exchange(Exchange::AllToAll, buffers);
}
fn exchange(&self, exchange: Exchange, buffers: MemberBuffers) {
exchange.expect_part(&self.group, self.rank, &buffers);
let client = &self.group.clients[self.rank];
exchange.queue(client, buffers, self.group.device_ids.clone());
client.device.flush_queue();
}
}
#[derive(Debug, Clone, Copy)]
enum Exchange {
AllReduce(Reduction),
AllGather,
ReduceScatter(Reduction),
AllToAll,
}
impl Exchange {
fn collective(self) -> Collective {
match self {
Self::AllReduce(_) => Collective::AllReduce,
Self::AllGather => Collective::AllGather,
Self::ReduceScatter(_) => Collective::ReduceScatter,
Self::AllToAll => Collective::AllToAll,
}
}
fn expect_part(self, group: &ClientGroup, rank: usize, buffers: &MemberBuffers) {
let collective = self.collective();
if let Self::AllReduce(reduction) | Self::ReduceScatter(reduction) = self {
group.expect_reduces(collective, reduction.dtype);
}
let in_place = match self {
Self::AllReduce(_) => InPlace::Allowed,
_ => InPlace::Refused,
};
group.expect_member_buffers(collective, rank, &buffers.src, &buffers.dst, in_place);
let src = buffers.src.size_in_used();
let dst = match self {
Self::AllReduce(reduction) => {
Client::expect_whole_elements(collective, reduction.dtype, src);
src
}
Self::AllGather => src * group.clients.len() as u64,
Self::ReduceScatter(reduction) => {
let chunk = group.expect_chunks(collective, src);
Client::expect_whole_elements(collective, reduction.dtype, chunk);
chunk
}
Self::AllToAll => {
group.expect_chunks(collective, src);
src
}
};
ClientGroup::expect_size(
collective,
CollectiveBuffer::Destination,
rank,
&buffers.dst,
dst,
);
}
fn queue(self, client: &Client, buffers: MemberBuffers, device_ids: Vec<DeviceId>) {
let MemberBuffers { src, dst } = buffers;
match self {
Self::AllReduce(reduction) => {
client.queue_all_reduce(src, dst, reduction, device_ids, ComputeWait::Now)
}
Self::AllGather => client.queue_all_gather(src, dst, device_ids),
Self::ReduceScatter(reduction) => {
client.queue_reduce_scatter(src, dst, reduction, device_ids)
}
Self::AllToAll => client.queue_all_to_all(src, dst, device_ids),
}
}
}