use num_complex::Complex64;
#[cfg(feature = "distributed-mpi")]
use crate::error::{PrismError, Result};
#[cfg(feature = "distributed-mpi")]
use mpi::environment::Threading;
#[cfg(any(test, feature = "distributed-mpi"))]
use std::thread::ThreadId;
pub trait RankComm: std::fmt::Debug + Send + Sync {
fn rank(&self) -> usize;
fn size(&self) -> usize;
fn allgather_c64(&self, local: &[Complex64]) -> Vec<Complex64>;
fn allgather_f64(&self, local: &[f64]) -> Vec<f64>;
fn allgatherv_u64(&self, local: &[u64], counts: &[usize]) -> Vec<u64>;
fn allgather_u64(&self, local: &[u64]) -> Vec<u64> {
self.allgatherv_u64(local, &vec![local.len(); self.size()])
}
fn allreduce_sum_f64(&self, value: f64) -> f64;
fn sendrecv_c64(&self, partner: usize, send: &[Complex64], recv: &mut [Complex64]);
fn barrier(&self);
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SerialComm;
impl RankComm for SerialComm {
#[inline]
fn rank(&self) -> usize {
0
}
#[inline]
fn size(&self) -> usize {
1
}
#[inline]
fn allgather_c64(&self, local: &[Complex64]) -> Vec<Complex64> {
local.to_vec()
}
#[inline]
fn allgather_f64(&self, local: &[f64]) -> Vec<f64> {
local.to_vec()
}
#[inline]
fn allgatherv_u64(&self, local: &[u64], _counts: &[usize]) -> Vec<u64> {
local.to_vec()
}
#[inline]
fn allreduce_sum_f64(&self, value: f64) -> f64 {
value
}
#[inline]
fn sendrecv_c64(&self, _partner: usize, send: &[Complex64], recv: &mut [Complex64]) {
debug_assert_eq!(send.len(), recv.len());
recv.copy_from_slice(send);
}
#[inline]
fn barrier(&self) {}
}
#[cfg(feature = "distributed-mpi")]
const _: () = assert!(std::mem::size_of::<Complex64>() == 2 * std::mem::size_of::<f64>());
#[cfg(feature = "distributed-mpi")]
#[inline]
fn as_f64(slice: &[Complex64]) -> &[f64] {
unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const f64, slice.len() * 2) }
}
#[cfg(feature = "distributed-mpi")]
#[inline]
fn as_f64_mut(slice: &mut [Complex64]) -> &mut [f64] {
unsafe { std::slice::from_raw_parts_mut(slice.as_mut_ptr() as *mut f64, slice.len() * 2) }
}
#[cfg(feature = "distributed-mpi")]
const REQUIRED_THREADING: Threading = Threading::Funneled;
#[cfg(feature = "distributed-mpi")]
fn check_threading(provided: Threading) -> Result<()> {
if provided >= REQUIRED_THREADING {
return Ok(());
}
Err(PrismError::IncompatibleBackend {
backend: "distributed".into(),
reason: format!(
"MPI thread level {provided:?} is below {REQUIRED_THREADING:?}, the minimum for \
running Rayon workers beside MPI"
),
})
}
#[cfg(feature = "distributed-mpi")]
fn owner_for(provided: Threading) -> Option<ThreadId> {
(provided < Threading::Multiple).then(|| std::thread::current().id())
}
#[cfg(any(test, feature = "distributed-mpi"))]
#[inline]
fn assert_owner_thread(owner: Option<ThreadId>) {
if let Some(owner) = owner {
let here = std::thread::current().id();
assert!(
here == owner,
"MPI call from thread {here:?}, but the MPI thread level confines calls to \
thread {owner:?}, which constructed the comm"
);
}
}
#[cfg(feature = "distributed-mpi")]
pub struct MpiComm {
_universe: Option<mpi::environment::Universe>,
owner: Option<ThreadId>,
rank: usize,
size: usize,
}
#[cfg(feature = "distributed-mpi")]
impl std::fmt::Debug for MpiComm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MpiComm")
.field("rank", &self.rank)
.field("size", &self.size)
.finish()
}
}
#[cfg(feature = "distributed-mpi")]
impl MpiComm {
pub fn world() -> Result<Self> {
let (universe, provided) = mpi::environment::initialize_with_threading(REQUIRED_THREADING)
.ok_or_else(|| PrismError::IncompatibleBackend {
backend: "distributed".into(),
reason: "MPI is already initialized; attach to it with `MpiComm::attach_world`"
.into(),
})?;
check_threading(provided)?;
Ok(Self::from_parts(Some(universe), provided))
}
pub fn attach_world() -> Result<Option<Self>> {
if !mpi::environment::is_initialized() {
return Ok(None);
}
let provided = mpi::environment::threading_support();
check_threading(provided)?;
if provided < Threading::Serialized && !is_thread_main() {
return Err(PrismError::IncompatibleBackend {
backend: "distributed".into(),
reason: "MPI runs at MPI_THREAD_FUNNELED, which confines MPI calls to the \
thread that initialized it; attach from that thread"
.into(),
});
}
Ok(Some(Self::from_parts(None, provided)))
}
fn from_parts(universe: Option<mpi::environment::Universe>, provided: Threading) -> Self {
use mpi::traits::Communicator;
let world = mpi::topology::SimpleCommunicator::world();
Self {
_universe: universe,
owner: owner_for(provided),
rank: world.rank() as usize,
size: world.size() as usize,
}
}
fn world_comm(&self) -> mpi::topology::SimpleCommunicator {
assert_owner_thread(self.owner);
mpi::topology::SimpleCommunicator::world()
}
}
#[cfg(feature = "distributed-mpi")]
fn is_thread_main() -> bool {
let mut flag: std::os::raw::c_int = 0;
unsafe { mpi::ffi::MPI_Is_thread_main(&mut flag) };
flag != 0
}
#[cfg(feature = "distributed-mpi")]
impl RankComm for MpiComm {
fn rank(&self) -> usize {
self.rank
}
fn size(&self) -> usize {
self.size
}
fn allgather_c64(&self, local: &[Complex64]) -> Vec<Complex64> {
use mpi::traits::CommunicatorCollectives;
let mut out = vec![Complex64::new(0.0, 0.0); local.len() * self.size];
self.world_comm()
.all_gather_into(as_f64(local), as_f64_mut(&mut out));
out
}
fn allgather_f64(&self, local: &[f64]) -> Vec<f64> {
use mpi::traits::CommunicatorCollectives;
let mut out = vec![0.0_f64; local.len() * self.size];
self.world_comm().all_gather_into(local, &mut out);
out
}
fn allgatherv_u64(&self, local: &[u64], counts: &[usize]) -> Vec<u64> {
use mpi::datatype::PartitionMut;
use mpi::traits::CommunicatorCollectives;
debug_assert_eq!(counts[self.rank], local.len());
let counts: Vec<mpi::Count> = counts.iter().map(|&c| c as mpi::Count).collect();
let mut displs = Vec::with_capacity(counts.len());
let mut total: mpi::Count = 0;
for &c in &counts {
displs.push(total);
total += c;
}
let mut out = vec![0_u64; total as usize];
self.world_comm()
.all_gather_varcount_into(local, &mut PartitionMut::new(&mut out[..], counts, displs));
out
}
fn allreduce_sum_f64(&self, value: f64) -> f64 {
use mpi::traits::CommunicatorCollectives;
let mut out = 0.0_f64;
self.world_comm().all_reduce_into(
&value,
&mut out,
mpi::collective::SystemOperation::sum(),
);
out
}
fn sendrecv_c64(&self, partner: usize, send: &[Complex64], recv: &mut [Complex64]) {
use mpi::point_to_point as p2p;
use mpi::traits::Communicator;
debug_assert_eq!(send.len(), recv.len());
let world = self.world_comm();
let peer = world.process_at_rank(partner as i32);
p2p::send_receive_into(as_f64(send), &peer, as_f64_mut(recv), &peer);
}
fn barrier(&self) {
use mpi::traits::CommunicatorCollectives;
self.world_comm().barrier();
}
}
#[cfg(test)]
mod owner_thread_tests {
use super::assert_owner_thread;
#[test]
fn owner_thread_check_admits_the_owner_and_rejects_other_threads() {
let owner = Some(std::thread::current().id());
assert_owner_thread(owner);
assert_owner_thread(None);
std::thread::spawn(|| assert_owner_thread(None))
.join()
.expect("an unconfined comm admits every thread");
let panic = std::thread::spawn(move || assert_owner_thread(owner))
.join()
.expect_err("a confined comm rejects other threads");
let message = panic
.downcast_ref::<String>()
.expect("formatted panic message");
assert!(message.contains("constructed the comm"), "{message}");
}
}
#[cfg(all(test, feature = "distributed-mpi"))]
mod threading_tests {
use super::*;
#[test]
fn thread_level_check_rejects_only_single() {
assert!(Threading::Single < Threading::Funneled);
assert!(Threading::Funneled < Threading::Serialized);
assert!(Threading::Serialized < Threading::Multiple);
assert!(check_threading(Threading::Single).is_err());
for level in [
Threading::Funneled,
Threading::Serialized,
Threading::Multiple,
] {
assert!(check_threading(level).is_ok(), "{level:?}");
}
}
#[test]
fn only_multiple_leaves_calls_unconfined() {
let here = Some(std::thread::current().id());
assert_eq!(owner_for(Threading::Funneled), here);
assert_eq!(owner_for(Threading::Serialized), here);
assert_eq!(owner_for(Threading::Multiple), None);
}
}