use ruda_core::device::DeviceId;
use ruda_tensor::distributed::ReduceOperation;
use std::{
collections::HashMap,
error::Error,
fmt::{Display, Formatter},
ops::Range,
panic::{AssertUnwindSafe, catch_unwind},
sync::{Arc, Condvar, Mutex},
};
pub trait NativeCollectiveDevice: Clone + Send + Sync + 'static {
type Tensor: Send + 'static;
type Prepared: Send + 'static;
type Work;
type Error: Error + Send + Sync + 'static;
fn prepare(
&self,
tensor: Self::Tensor,
device: DeviceId,
) -> Result<Self::Prepared, Self::Error>;
fn begin(
&self,
devices: &[DeviceId],
inputs: &[Self::Prepared],
) -> Result<Self::Work, Self::Error>;
fn elements(&self, work: &Self::Work) -> usize;
fn exchange(
&self,
work: &mut Self::Work,
ranges: &[Range<usize>],
reduce: bool,
) -> Result<(), Self::Error>;
fn finish(
&self,
work: Self::Work,
operation: ReduceOperation,
) -> Result<Vec<Self::Tensor>, Self::Error>;
}
fn ring_all_reduce<D: NativeCollectiveDevice>(
device: &D,
devices: &[DeviceId],
inputs: &[D::Prepared],
operation: ReduceOperation,
) -> Result<Vec<D::Tensor>, D::Error> {
let mut work = device.begin(devices, inputs)?;
let world = devices.len();
let chunks = crate::rank::layout::balanced_ranges(device.elements(&work), world);
for step in 0..world - 1 {
let ranges = (0..world)
.map(|rank| chunks[(rank + world - step) % world].clone())
.collect::<Vec<_>>();
device.exchange(&mut work, &ranges, true)?;
}
for step in 0..world - 1 {
let ranges = (0..world)
.map(|rank| chunks[(rank + 1 + world - step) % world].clone())
.collect::<Vec<_>>();
device.exchange(&mut work, &ranges, false)?;
}
device.finish(work, operation)
}
#[derive(Clone, Debug)]
pub struct NativeCollectiveError(pub String);
impl Display for NativeCollectiveError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for NativeCollectiveError {}
struct Submission<P> {
operation: ReduceOperation,
prepared: Result<P, NativeCollectiveError>,
}
struct State<D: NativeCollectiveDevice> {
inputs: Vec<Option<Submission<D::Prepared>>>,
outputs: Vec<Option<Result<D::Tensor, NativeCollectiveError>>>,
arrived: usize,
departed: usize,
ready: bool,
}
pub struct NativeCommunicator<D: NativeCollectiveDevice> {
devices: Vec<DeviceId>,
device: D,
state: Mutex<State<D>>,
changed: Condvar,
}
impl<D: NativeCollectiveDevice> NativeCommunicator<D> {
pub fn new(devices: Vec<DeviceId>, device: D) -> Result<Self, NativeCollectiveError> {
if devices.is_empty()
|| devices
.iter()
.enumerate()
.any(|(i, id)| devices[..i].contains(id))
{
return Err(NativeCollectiveError(
"native collective requires distinct devices and a nonempty world".into(),
));
}
let world = devices.len();
Ok(Self {
devices,
device,
state: Mutex::new(State {
inputs: (0..world).map(|_| None).collect(),
outputs: (0..world).map(|_| None).collect(),
arrived: 0,
departed: 0,
ready: false,
}),
changed: Condvar::new(),
})
}
pub fn world_size(&self) -> usize {
self.devices.len()
}
pub fn devices(&self) -> &[DeviceId] {
&self.devices
}
pub fn all_reduce(
&self,
rank: usize,
tensor: D::Tensor,
operation: ReduceOperation,
) -> Result<D::Tensor, NativeCollectiveError> {
let device = *self.devices.get(rank).ok_or_else(|| {
NativeCollectiveError("native collective rank is outside the world".into())
})?;
let prepared = catch_unwind(AssertUnwindSafe(|| self.device.prepare(tensor, device)))
.map_err(|_| NativeCollectiveError("native device preparation panicked".into()))
.and_then(|result| result.map_err(|error| NativeCollectiveError(error.to_string())));
let mut state = self
.state
.lock()
.map_err(|_| NativeCollectiveError("native collective state is poisoned".into()))?;
while state.ready || state.inputs[rank].is_some() {
state = self
.changed
.wait(state)
.map_err(|_| NativeCollectiveError("native collective state is poisoned".into()))?;
}
state.inputs[rank] = Some(Submission {
operation,
prepared,
});
state.arrived += 1;
if state.arrived == self.world_size() {
let submissions = state
.inputs
.iter_mut()
.map(|entry| entry.take().expect("all native ranks arrived"))
.collect::<Vec<_>>();
let agreed = submissions[0].operation;
let result = if submissions.iter().any(|entry| entry.operation != agreed) {
Err(NativeCollectiveError(
"native collective reduction modes differ between ranks".into(),
))
} else {
submissions
.into_iter()
.map(|entry| entry.prepared)
.collect::<Result<Vec<_>, _>>()
.and_then(|inputs| {
catch_unwind(AssertUnwindSafe(|| {
ring_all_reduce(&self.device, &self.devices, &inputs, agreed)
}))
.map_err(|_| {
NativeCollectiveError("native collective executor panicked".into())
})
.and_then(|result| {
result.map_err(|error| NativeCollectiveError(error.to_string()))
})
})
};
match result {
Ok(outputs) if outputs.len() == self.world_size() => {
state.outputs = outputs.into_iter().map(|output| Some(Ok(output))).collect();
}
result => {
let error = match result {
Err(error) => error,
Ok(outputs) => NativeCollectiveError(format!(
"native collective returned {} outputs for {} ranks",
outputs.len(),
self.world_size()
)),
};
state.outputs = (0..self.world_size())
.map(|_| Some(Err(error.clone())))
.collect();
}
}
state.ready = true;
self.changed.notify_all();
} else {
while !state.ready {
state = self.changed.wait(state).map_err(|_| {
NativeCollectiveError("native collective state is poisoned".into())
})?;
}
}
let output = state.outputs[rank]
.take()
.expect("one native output per rank");
state.departed += 1;
if state.departed == self.world_size() {
state.arrived = 0;
state.departed = 0;
state.ready = false;
self.changed.notify_all();
}
output
}
}
pub struct NativeCommunicators<D: NativeCollectiveDevice> {
device: D,
groups: Mutex<HashMap<Vec<DeviceId>, Arc<NativeCommunicator<D>>>>,
}
impl<D: NativeCollectiveDevice> NativeCommunicators<D> {
pub fn new(device: D) -> Self {
Self {
device,
groups: Mutex::new(HashMap::new()),
}
}
pub fn communicator(
&self,
devices: Vec<DeviceId>,
) -> Result<Arc<NativeCommunicator<D>>, NativeCollectiveError> {
let mut groups = self.groups.lock().map_err(|_| {
NativeCollectiveError("native communicator registry is poisoned".into())
})?;
if let Some(group) = groups.get(&devices) {
return Ok(group.clone());
}
let group = Arc::new(NativeCommunicator::new(
devices.clone(),
self.device.clone(),
)?);
groups.insert(devices, group.clone());
Ok(group)
}
}