use std::sync::Arc;
use std::time::Instant;
use super::driver::{
device_runtime, first_failure, lower_for_run, upload_share, GpuScatterOptions,
};
use super::error::GpuError;
use super::layer::GpuLayerOptions;
use super::partition::DevicePartition;
use crate::circuit::Circuit;
use crate::collectives::Collectives;
use crate::engine::backend::PartitionStorage;
use crate::engine::partitioned::distributed::{gather_share, group_bits};
use crate::engine::partitioned::{
DistributedSum, PartitionRowPolicy, PartitionRuntime, ScatterRows,
};
#[cfg(feature = "phase-timing")]
use crate::engine::stats::PartitionPhaseStats;
use crate::engine::timing::timed;
use crate::engine::transport::Transport;
use crate::engine::{Direction, PropagateOptions};
use crate::pauli_sum::hash::PartitionRows;
use crate::pauli_sum::PauliSum;
use crate::truncation::BuiltinTruncation;
#[cfg(feature = "mpi")]
use crate::engine::partitioned::mpi::{rsmpi::topology::Communicator, MpiTransport};
use crate::engine::LOG_TARGET;
#[cfg(feature = "mpi")]
mod affinity;
#[cfg(feature = "mpi")]
pub use affinity::local_device_for_comm;
fn agree_with<T>(
collectives: &dyn Collectives,
result: Result<T, GpuError>,
discard: impl FnOnce(T),
) -> Result<T, GpuError> {
match (first_failure(collectives, u64::from(result.is_err())), result) {
(_, Err(e)) => Err(e),
(None, Ok(value)) => Ok(value),
(Some((rank, _)), Ok(value)) => {
discard(value);
Err(GpuError::Poisoned { rank, layer: 0 })
}
}
}
fn agree<T>(collectives: &dyn Collectives, result: Result<T, GpuError>) -> Result<T, GpuError> {
agree_with(collectives, result, drop)
}
pub type GpuDistributedSum<const W: usize, X> = DistributedSum<W, X, DevicePartition<W>>;
#[cfg(feature = "mpi")]
pub type MpiGpuSum<const W: usize> = GpuDistributedSum<W, MpiTransport>;
impl<const W: usize, X: Transport> DistributedSum<W, X, DevicePartition<W>> {
pub fn scatter_to_device(
sum: &PauliSum<W>,
transport: X,
device: u32,
) -> Result<Self, GpuError> {
let rows = ScatterRows::Policy(PartitionRowPolicy::Seeded(None));
let rows = rows.resolve(sum, group_bits(transport.size()));
let runtime = device_runtime(device);
let layer = GpuLayerOptions::default();
Self::scatter_then(sum, transport, (runtime, layer), rows, start_exchange)
}
pub fn scatter_to_device_with(
sum: &PauliSum<W>,
transport: X,
options: GpuScatterOptions<W>,
) -> Result<Self, GpuError> {
let rows = options.rows.resolve(sum, group_bits(transport.size()));
let devices = (Ok(options.devices), options.layer);
Self::scatter_then(sum, transport, devices, rows, start_exchange)
}
fn scatter_then(
sum: &PauliSum<W>,
transport: X,
(runtime, layer): (Result<Arc<PartitionRuntime>, GpuError>, GpuLayerOptions),
rows: PartitionRows<W>,
start: impl FnOnce(&X, &mut DevicePartition<W>) -> Result<(), GpuError>,
) -> Result<Self, GpuError> {
let (rank, size) = (transport.rank(), transport.size());
layer.assert_valid();
rows.assert_splits(sum.hash(), sum.num_qubits(), size as usize);
let started = Instant::now();
let placed = runtime.and_then(|runtime| Ok((lone_device(&runtime)?, runtime)));
let (device, runtime) = agree(&transport, placed)?;
let partition = {
let (rows, transport) = (&rows, &transport);
let options = (layer, &[][..]);
runtime
.install(move || upload_share(sum, rows, (rank, size), device, transport, options))
};
let mut partition = agree(&transport, partition)?;
start(&transport, &mut partition)?;
log::info!(
target: LOG_TARGET,
"scatter_gpu: rank {rank}/{size} on device {device}, {} terms in, {} kept locally, {} bucket bits, {:.3} s",
sum.len(),
partition.len(),
partition.hash().bits(),
started.elapsed().as_secs_f64(),
);
let scatter_ns = started.elapsed().as_nanos() as u64;
Ok(Self::from_backend(partition, rows, runtime, transport, scatter_ns))
}
pub fn propagate(
&mut self,
circuit: &Circuit<W>,
policy: impl Into<BuiltinTruncation>,
direction: Direction,
) -> Result<(), GpuError> {
self.propagate_with(circuit, policy, direction, PropagateOptions::default())
}
pub fn propagate_with(
&mut self,
circuit: &Circuit<W>,
policy: impl Into<BuiltinTruncation>,
direction: Direction,
options: PropagateOptions,
) -> Result<(), GpuError> {
options.assert_valid();
let policy = policy.into();
let single = self.size() == 1;
let partition = self.backend_mut();
partition.check_poison()?;
let keep = lower_for_run(circuit, &policy, direction, partition.hash(), single)?;
partition.resync_hash();
partition.keep = keep;
self.propagate_on_backend(circuit, &policy, direction, options);
let (own, code) = self.backend_mut().take_failure();
match first_failure(self.transport(), code) {
None => Ok(()),
Some((rank, code)) => {
let layer = code as usize - 1;
if !single {
self.backend_mut().poison(rank, layer);
}
own.and(Err(GpuError::Poisoned { rank, layer }))
}
}
}
pub fn gather(&self) -> Result<Option<PauliSum<W>>, GpuError> {
self.backend().check_poison()?;
timed!(let started = Instant::now());
let share = agree(self.transport(), self.local_to_host())?;
let out = gather_share(&share, self.transport());
timed!(self.lap_gather(started.elapsed().as_nanos() as u64));
Ok(out)
}
pub fn local_to_host(&self) -> Result<PauliSum<W>, GpuError> {
self.backend().sum().to_host()
}
pub fn device(&self) -> u32 {
self.backend().sum().device()
}
pub fn set_layer_options(&mut self, options: GpuLayerOptions) {
options.assert_valid();
self.backend_mut().scratch_mut().options = options;
}
#[cfg(feature = "phase-timing")]
pub fn take_stats(&mut self) -> PartitionPhaseStats {
let per = std::mem::take(self.backend_mut().stats());
let (scatter_ns, gather_ns, layers) = self.take_driver_laps();
PartitionPhaseStats { per_partition: vec![per], scatter_ns, gather_ns, layers }
}
}
fn lone_device(runtime: &PartitionRuntime) -> Result<u32, GpuError> {
match runtime.slots() {
[slot] => slot.device.ok_or(GpuError::Unsupported(
"a distributed device scatter needs a runtime resolved from Placement::Devices",
)),
_ => Err(GpuError::Unsupported(
"a distributed device scatter needs a runtime of one partition",
)),
}
}
fn start_exchange<const W: usize, X: Transport>(
transport: &X,
partition: &mut DevicePartition<W>,
) -> Result<(), GpuError> {
if transport.size() == 1 {
return Ok(());
}
#[cfg(feature = "mpi")]
return start_nccl(transport, partition);
#[cfg(not(feature = "mpi"))]
{
let _ = partition;
Err(GpuError::Unsupported(
"a device group of more than one rank exchanges over NCCL, which needs the mpi feature",
))
}
}
#[cfg(feature = "mpi")]
fn start_nccl<const W: usize>(
collectives: &dyn Collectives,
partition: &mut DevicePartition<W>,
) -> Result<(), GpuError> {
use super::nccl::{self, NcclComm};
use super::wire::DeviceWire;
let context = partition.sum().context.clone();
let device =
if super::device::nccl_available() { nccl::device_uuid(&context).ok() } else { None };
nccl::agree_start(collectives, device.is_some(), device.unwrap_or_default())?;
let stream = partition.sum().stream.clone();
let comm = bootstrap(
collectives,
|| NcclComm::init(collectives, &context),
|comm| comm.warm_up(&stream),
NcclComm::abort,
)?;
partition.scratch_mut().export.wire = Some(std::sync::Arc::new(comm));
Ok(())
}
#[cfg(feature = "mpi")]
fn bootstrap<C>(
collectives: &dyn Collectives,
init: impl FnOnce() -> Result<C, GpuError>,
warm: impl FnOnce(&C) -> Result<(), GpuError>,
abort: impl Fn(&C),
) -> Result<C, GpuError> {
agree_with(collectives, init(), |comm| abort(&comm)).and_then(|comm| {
let warmed = match warm(&comm) {
Ok(()) => Ok(comm),
Err(e) => {
abort(&comm);
Err(e)
}
};
agree_with(collectives, warmed, |comm| abort(&comm))
})
}
#[cfg(feature = "mpi")]
pub fn propagate_mpi_gpu<const W: usize>(
circuit: &Circuit<W>,
sum: &PauliSum<W>,
policy: impl Into<BuiltinTruncation>,
direction: Direction,
options: PropagateOptions,
comm: &impl Communicator,
device: Option<u32>,
) -> Result<Option<PauliSum<W>>, GpuError> {
let transport = MpiTransport::from_communicator(comm);
let picked = match device {
Some(ordinal) => Ok(ordinal),
None => local_device_for_comm(comm),
};
let device = agree(&transport, picked)?;
let mut split = MpiGpuSum::<W>::scatter_to_device(sum, transport, device)?;
split.propagate_with(circuit, policy, direction, options)?;
split.gather()
}
#[cfg(test)]
mod tests;