use crate::channel::Prepare;
use crate::engine::timing::timed;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Instant;
use super::error::GpuError;
use super::layer::GpuLayerOptions;
use super::partition::DevicePartition;
use super::sum::GpuSum;
use super::truncation::KeepProgram;
use super::wire::PeerWire;
use crate::circuit::Circuit;
use crate::collectives::Collectives;
use crate::engine::backend::PartitionStorage;
use crate::engine::driver::DECLINED_PREPARATION;
use crate::engine::partitioned::driver::scatter_local;
use crate::engine::partitioned::{
PartitionConfig, PartitionRowPolicy, PartitionRuntime, PartitionedSum, Placement, ScatterRows,
};
#[cfg(feature = "phase-timing")]
use crate::engine::stats::PartitionPhaseStats;
use crate::engine::{Direction, PropagateOptions};
use crate::pauli_sum::hash::{Gf2Hash, PartitionRows};
use crate::pauli_sum::PauliSum;
use crate::truncation::BuiltinTruncation;
use crate::engine::LOG_TARGET;
pub type GpuPartitionedSum<const W: usize> = PartitionedSum<W, DevicePartition<W>>;
pub(super) fn lower_for_run<const W: usize>(
circuit: &Circuit<W>,
policy: &BuiltinTruncation,
direction: Direction,
hash: &Gf2Hash<W>,
single_partition: bool,
) -> Result<KeepProgram, GpuError> {
if !policy.supports_partitioned() && !single_partition {
return Err(GpuError::Unsupported("exact TopN on device"));
}
if policy.contains_collapse_sample() {
return Err(GpuError::Unsupported("CollapseSample on device"));
}
let keep = KeepProgram::lower(policy)?;
let adjoint = matches!(direction, Direction::Heisenberg);
if circuit.channels.iter().any(|ch| ch.prepare(hash, adjoint).is_none()) {
return Err(GpuError::Unsupported(DECLINED_PREPARATION));
}
Ok(keep)
}
pub(crate) fn first_failure(collectives: &dyn Collectives, code: u64) -> Option<(usize, u64)> {
let mut codes = vec![0u64; collectives.size() as usize];
codes[collectives.rank() as usize] = code;
collectives.allreduce_sum_u64(&mut codes);
first_nonzero(&codes)
}
fn first_nonzero(codes: &[u64]) -> Option<(usize, u64)> {
codes.iter().position(|&code| code != 0).map(|rank| (rank, codes[rank]))
}
pub struct GpuScatterOptions<const W: usize> {
pub devices: Arc<PartitionRuntime>,
pub rows: ScatterRows<W>,
pub layer: GpuLayerOptions,
}
impl<const W: usize> GpuScatterOptions<W> {
pub fn new(devices: &[u32]) -> Result<Self, GpuError> {
let devices = match devices {
&[device] => device_runtime(device)?,
_ => {
let config = PartitionConfig {
placement: Placement::Devices { devices: devices.to_vec(), per_device: 1 },
bind_memory: false,
threads_per_partition: None,
};
PartitionRuntime::new(&config).map_err(GpuError::Topology)?
}
};
Ok(Self {
devices,
rows: ScatterRows::Policy(PartitionRowPolicy::Seeded(None)),
layer: GpuLayerOptions::default(),
})
}
}
pub(super) fn device_runtime(device: u32) -> Result<Arc<PartitionRuntime>, GpuError> {
static RUNTIMES: Mutex<Vec<(u32, Arc<PartitionRuntime>)>> = Mutex::new(Vec::new());
let mut cache = RUNTIMES.lock().unwrap_or_else(PoisonError::into_inner);
if let Some((_, runtime)) = cache.iter().find(|(cached, _)| *cached == device) {
return Ok(runtime.clone());
}
let config = PartitionConfig {
placement: Placement::Devices { devices: vec![device], per_device: 1 },
bind_memory: false,
threads_per_partition: None,
};
let runtime = PartitionRuntime::new(&config).map_err(GpuError::Topology)?;
cache.push((device, runtime.clone()));
Ok(runtime)
}
pub(super) fn upload_share<const W: usize>(
sum: &PauliSum<W>,
rows: &PartitionRows<W>,
(rank, size): (u32, u32),
device: u32,
collectives: &dyn Collectives,
(layer, extra_options): (GpuLayerOptions, &[String]),
) -> Result<DevicePartition<W>, GpuError> {
let device_sum = if size == 1 {
GpuSum::from_host_with_options(sum, device, extra_options)?
} else {
let local = scatter_local(sum, rows, rank, collectives);
GpuSum::from_host_with_options(&local, device, extra_options)?
};
let mut partition = DevicePartition::new(device_sum, layer)?;
partition.group_size = size;
Ok(partition)
}
impl<const W: usize> PartitionedSum<W, DevicePartition<W>> {
pub fn from_host(sum: &PauliSum<W>, ordinal: u32) -> Result<Self, GpuError> {
Self::one_device(sum, ordinal, &[])
}
pub(crate) fn one_device(
sum: &PauliSum<W>,
ordinal: u32,
extra_options: &[String],
) -> Result<Self, GpuError> {
let runtime = device_runtime(ordinal)?;
let rows = PartitionRows::none(sum.num_qubits());
Self::upload(sum, rows, runtime, (GpuLayerOptions::default(), extra_options))
}
pub fn scatter_to_devices(sum: &PauliSum<W>, devices: &[u32]) -> Result<Self, GpuError> {
Self::scatter_to_devices_with(sum, GpuScatterOptions::new(devices)?)
}
pub fn scatter_to_devices_with(
sum: &PauliSum<W>,
options: GpuScatterOptions<W>,
) -> Result<Self, GpuError> {
let GpuScatterOptions { devices, rows, layer } = options;
layer.assert_valid();
let rows = rows.resolve(sum, devices.partition_bits());
Self::upload(sum, rows, devices, (layer, &[]))
}
fn upload(
sum: &PauliSum<W>,
rows: PartitionRows<W>,
runtime: Arc<PartitionRuntime>,
options: (GpuLayerOptions, &[String]),
) -> Result<Self, GpuError> {
let size = runtime.num_partitions();
rows.assert_splits(sum.hash(), sum.num_qubits(), size);
let devices: Vec<u32> = runtime
.slots()
.iter()
.map(|slot| {
slot.device.ok_or(GpuError::Unsupported(
"GpuPartitionedSum needs a runtime resolved from Placement::Devices",
))
})
.collect::<Result<_, _>>()?;
let started = Instant::now();
let partitions: Vec<Result<DevicePartition<W>, GpuError>> = {
let (rows, devices) = (&rows, &devices);
let wires = PeerWire::group(size as u32);
runtime.map_partitions(wires, |rank, wire, transport| {
let ids = (rank as u32, size as u32);
let mut partition =
upload_share(sum, rows, ids, devices[rank], transport, options)?;
partition.scratch_mut().export.wire = Some(Arc::new(wire));
Ok(partition)
})
};
let partitions = partitions.into_iter().collect::<Result<Vec<_>, _>>()?;
log::info!(
target: LOG_TARGET,
"scatter_gpu: {} terms over {size} device partitions [{}], {} bucket bits, {:.3} s",
sum.len(),
runtime.placement_summary(),
partitions[0].hash().bits(),
started.elapsed().as_secs_f64(),
);
let scatter_ns = started.elapsed().as_nanos() as u64;
Ok(Self::from_backend(partitions, rows, runtime, 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 partitions = self.backends_mut();
partitions[0].check_poison()?;
let single = partitions.len() == 1;
let keep = lower_for_run(circuit, &policy, direction, partitions[0].hash(), single)?;
for partition in partitions.iter_mut() {
partition.take_error()?;
partition.keep = keep;
}
self.propagate_on_backend(circuit, &policy, direction, options);
let partitions = self.backends_mut();
let (mut outcomes, codes): (Vec<_>, Vec<u64>) =
partitions.iter_mut().map(DevicePartition::take_failure).unzip();
let Some((rank, code)) = first_nonzero(&codes) else {
return Ok(());
};
if !single {
for partition in partitions.iter_mut() {
partition.poison(rank, code as usize - 1);
}
}
std::mem::replace(&mut outcomes[rank], Ok(()))
}
pub fn gather(&self) -> Result<PauliSum<W>, GpuError> {
let partitions = self.backends();
partitions[0].check_poison()?;
timed!(let started = Instant::now());
let shares = std::thread::scope(|scope| {
let downloads: Vec<_> = partitions
.iter()
.map(|partition| scope.spawn(|| partition.sum().to_host()))
.collect();
downloads
.into_iter()
.map(|download| download.join().expect("a download thread panicked"))
.collect::<Result<Vec<_>, _>>()
})?;
let out = PauliSum::merge_partitions(shares);
timed!(self.lap_gather(started.elapsed().as_nanos() as u64));
Ok(out)
}
pub fn devices(&self) -> Vec<u32> {
self.backends().iter().map(|partition| partition.sum().device()).collect()
}
pub fn set_layer_options(&mut self, options: GpuLayerOptions) {
options.assert_valid();
for partition in self.backends_mut() {
partition.scratch_mut().options = options;
}
}
#[cfg(feature = "phase-timing")]
pub fn take_stats(&mut self) -> PartitionPhaseStats {
let per_partition = self
.backends_mut()
.iter_mut()
.map(|partition| std::mem::take(partition.stats()))
.collect();
let (scatter_ns, gather_ns, layers) = self.take_driver_laps();
PartitionPhaseStats { per_partition, scatter_ns, gather_ns, layers }
}
}
pub fn propagate_gpu<const W: usize>(
circuit: &Circuit<W>,
sum: &PauliSum<W>,
policy: impl Into<BuiltinTruncation>,
direction: Direction,
device: u32,
) -> Result<PauliSum<W>, GpuError> {
let mut resident = GpuPartitionedSum::from_host(sum, device)?;
resident.propagate(circuit, policy, direction)?;
resident.gather()
}
pub fn propagate_gpu_partitioned<const W: usize>(
circuit: &Circuit<W>,
sum: &PauliSum<W>,
policy: impl Into<BuiltinTruncation>,
direction: Direction,
config: &PartitionConfig,
) -> Result<PauliSum<W>, GpuError> {
let options = GpuScatterOptions {
devices: PartitionRuntime::new(config).map_err(GpuError::Topology)?,
rows: ScatterRows::Policy(PartitionRowPolicy::Seeded(None)),
layer: GpuLayerOptions::default(),
};
let mut split = GpuPartitionedSum::scatter_to_devices_with(sum, options)?;
split.propagate(circuit, policy, direction)?;
split.gather()
}