use std::cmp::Ordering;
use itertools::Itertools;
use mpi::{
collective::SystemOperation,
traits::{CommunicatorCollectives, Equivalence},
};
use paste::paste;
use rand::{Rng, RngExt, seq::index};
use crate::{
RlstResult, TotalCmp,
distributed_tools::{
all_to_allv,
array_tools::{gather_to_all, global_size},
sort_to_bins,
},
};
pub const OVERSAMPLING: usize = 16;
const EXACT_BRACKET: usize = 4 * OVERSAMPLING;
pub trait AsUnique: TotalCmp + Clone + Copy {
type Unique: Default + Copy + Clone + TotalCmp + Equivalence;
fn into_unique(value: Self, rank: usize, index: usize) -> Self::Unique;
fn from_unique(elem: Self::Unique) -> Self;
}
pub fn parsort<Item: AsUnique, C: CommunicatorCollectives, R: Rng + ?Sized>(
arr: &[Item],
comm: &C,
rng: &mut R,
) -> RlstResult<Vec<Item>> {
let rank = comm.rank() as usize;
let mut local = arr.to_vec();
if comm.size() == 1 {
local.sort_by(|left, right| left.total_cmp(*right));
return Ok(local);
}
let total = global_size(arr, comm);
if total == 0 {
return Ok(Vec::new());
}
let mut unique = local
.iter()
.enumerate()
.map(|(index, item)| Item::into_unique(*item, rank, index))
.collect_vec();
unique.sort_by(|left, right| left.total_cmp(*right));
let bins = histogram_splitters(&unique, total, comm, rng);
let counts = sort_to_bins(&unique, &bins);
let mut received = all_to_allv(comm, &counts, &unique).1;
received.sort_by(|left, right| left.total_cmp(*right));
Ok(received.into_iter().map(Item::from_unique).collect())
}
fn histogram_splitters<U, C, R>(arr: &[U], total: usize, comm: &C, rng: &mut R) -> Vec<U>
where
U: Default + Copy + TotalCmp + Equivalence,
C: CommunicatorCollectives,
R: Rng + ?Sized,
{
let mut brackets = initial_brackets(total, comm.size() as usize);
while brackets.iter().any(|bracket| bracket.splitter.is_none()) {
let local_candidates = choose_candidates(arr, &brackets, total, rng);
let mut candidates = gather_to_all(&local_candidates, comm);
candidates.sort_by(|left, right| left.total_cmp(*right));
let local = local_positions(arr, &candidates);
let mut positions = vec![0usize; candidates.len()];
comm.all_reduce_into(&local[..], &mut positions[..], SystemOperation::sum());
refine(&mut brackets, &candidates, &positions);
}
brackets
.into_iter()
.map(|bracket| bracket.splitter.expect("every bracket is resolved"))
.collect()
}
#[derive(Clone, Copy, Debug)]
struct Bracket<U> {
target: usize,
lower: Option<(U, usize)>,
upper: Option<(U, usize)>,
splitter: Option<U>,
boost: usize,
}
impl<U: Copy + TotalCmp> Bracket<U> {
fn positions(&self, total: usize) -> (usize, usize) {
let start = self.lower.map_or(0, |(_, position)| position + 1);
let end = self.upper.map_or(total, |(_, position)| position);
(start, end)
}
fn local_range(&self, arr: &[U]) -> (usize, usize) {
let start = self.lower.map_or(0, |(value, _)| {
arr.partition_point(|item| item.total_cmp(value) != Ordering::Greater)
});
let end = self.upper.map_or(arr.len(), |(value, _)| {
arr.partition_point(|item| item.total_cmp(value) == Ordering::Less)
});
(start, end.max(start))
}
}
fn initial_brackets<U>(total: usize, size: usize) -> Vec<Bracket<U>> {
(0..size)
.map(|r| Bracket {
target: (r as u128 * total as u128 / size as u128) as usize,
lower: None,
upper: None,
splitter: None,
boost: 1,
})
.collect()
}
fn choose_candidates<U, R>(arr: &[U], brackets: &[Bracket<U>], total: usize, rng: &mut R) -> Vec<U>
where
U: Copy + TotalCmp,
R: Rng + ?Sized,
{
let mut candidates = Vec::new();
let open = brackets
.iter()
.filter(|bracket| bracket.splitter.is_none())
.collect_vec();
for group in open.chunk_by(|left, right| left.positions(total) == right.positions(total)) {
let (start, end) = group[0].positions(total);
let inside = end - start;
let (first, last) = group[0].local_range(arr);
let local = last - first;
if local == 0 {
continue;
}
if inside <= EXACT_BRACKET {
candidates.extend_from_slice(&arr[first..last]);
continue;
}
let boost = group.iter().map(|bracket| bracket.boost).max().unwrap_or(1);
let wanted = OVERSAMPLING
.saturating_mul(group.len())
.saturating_mul(boost);
let fraction = (wanted as f64 / inside as f64).min(1.0);
let draws = ((local as f64 * fraction + rng.random::<f64>()).floor() as usize).min(local);
let mut picked = index::sample(rng, local, draws).into_vec();
picked.sort_unstable();
candidates.extend(picked.into_iter().map(|offset| arr[first + offset]));
}
candidates
}
fn local_positions<U: Copy + TotalCmp>(arr: &[U], candidates: &[U]) -> Vec<usize> {
candidates
.iter()
.map(|&candidate| arr.partition_point(|item| item.total_cmp(candidate) == Ordering::Less))
.collect()
}
fn refine<U: Copy + TotalCmp>(brackets: &mut [Bracket<U>], candidates: &[U], positions: &[usize]) {
for bracket in brackets
.iter_mut()
.filter(|bracket| bracket.splitter.is_none())
{
let p = positions.partition_point(|&position| position < bracket.target);
if p < positions.len() && positions[p] == bracket.target {
bracket.splitter = Some(candidates[p]);
continue;
}
let mut progress = false;
if p > 0
&& bracket
.lower
.is_none_or(|(_, position)| positions[p - 1] > position)
{
bracket.lower = Some((candidates[p - 1], positions[p - 1]));
progress = true;
}
if p < positions.len()
&& bracket
.upper
.is_none_or(|(_, position)| positions[p] < position)
{
bracket.upper = Some((candidates[p], positions[p]));
progress = true;
}
if !progress {
bracket.boost = bracket.boost.saturating_mul(2);
}
}
}
macro_rules! impl_as_unique_for {
($dtype:ty) => { paste! {
#[derive(Copy, Clone, PartialOrd, PartialEq, Equivalence, Default)]
#[allow(non_camel_case_types, missing_docs)]
pub struct [<UniqueItem_ $dtype>] { pub value: $dtype, pub rank: usize, pub index: usize }
impl TotalCmp for [<UniqueItem_$dtype>] {
fn total_cmp(self, other: Self) -> Ordering {
match self.value.total_cmp(other.value) {
Ordering::Equal => match self.rank.cmp(&other.rank) {
Ordering::Equal => self.index.cmp(&other.index), order => order,
}, order => order,
}
}
}
impl AsUnique for $dtype {
type Unique = [<UniqueItem_$dtype>];
fn into_unique(value: $dtype, rank: usize, index: usize) -> Self::Unique { Self::Unique { value, rank, index } }
fn from_unique(elem: Self::Unique) -> Self { elem.value }
}
}};
}
impl_as_unique_for!(f64);
impl_as_unique_for!(f32);
impl_as_unique_for!(i8);
impl_as_unique_for!(i16);
impl_as_unique_for!(i32);
impl_as_unique_for!(i64);
impl_as_unique_for!(isize);
impl_as_unique_for!(u8);
impl_as_unique_for!(u16);
impl_as_unique_for!(u32);
impl_as_unique_for!(u64);
impl_as_unique_for!(usize);
#[cfg(test)]
mod test {
use super::*;
use rand::{SeedableRng, rngs::StdRng};
type Unique = <u64 as AsUnique>::Unique;
fn simulate(ranks: &[Vec<u64>], seed: u64) -> (Vec<Unique>, Vec<Vec<Unique>>, usize) {
let mut rng = StdRng::seed_from_u64(seed);
let local: Vec<Vec<Unique>> = ranks
.iter()
.enumerate()
.map(|(rank, values)| {
let mut unique = values
.iter()
.enumerate()
.map(|(index, &value)| u64::into_unique(value, rank, index))
.collect_vec();
unique.sort_by(|left, right| left.total_cmp(*right));
unique
})
.collect();
let total = local.iter().map(Vec::len).sum();
let mut brackets = initial_brackets::<Unique>(total, ranks.len());
let mut rounds = 0;
while brackets.iter().any(|bracket| bracket.splitter.is_none()) {
rounds += 1;
assert!(rounds < 100, "the refinement must terminate");
let mut candidates = local
.iter()
.flat_map(|arr| choose_candidates(arr, &brackets, total, &mut rng))
.collect_vec();
candidates.sort_by(|left, right| left.total_cmp(*right));
let mut positions = vec![0usize; candidates.len()];
for arr in &local {
for (sum, position) in positions.iter_mut().zip(local_positions(arr, &candidates)) {
*sum += position;
}
}
refine(&mut brackets, &candidates, &positions);
}
let splitters = brackets.iter().map(|b| b.splitter.unwrap()).collect();
(splitters, local, rounds)
}
fn check_exact(ranks: &[Vec<u64>], seed: u64) -> usize {
let (splitters, local, rounds) = simulate(ranks, seed);
let mut all = local.concat();
all.sort_by(|left, right| left.total_cmp(*right));
let size = ranks.len();
for (r, splitter) in splitters.iter().enumerate() {
let target = r * all.len() / size;
assert!(
all[target].total_cmp(*splitter) == Ordering::Equal,
"splitter {r} is not the record at position {target}"
);
}
rounds
}
fn uniform(rng: &mut StdRng, n: usize) -> Vec<u64> {
(0..n).map(|_| rng.random::<u64>() >> 16).collect()
}
#[test]
fn test_alike_local_distributions_are_balanced_exactly() {
let mut rng = StdRng::seed_from_u64(1);
let ranks: Vec<Vec<u64>> = (0..72).map(|_| uniform(&mut rng, 2000)).collect();
let rounds = check_exact(&ranks, 2);
assert!(rounds <= 8, "{rounds} rounds");
}
#[test]
fn test_sorted_and_skewed_inputs() {
let mut rng = StdRng::seed_from_u64(3);
let sorted: Vec<Vec<u64>> = (0..16)
.map(|r| (0..500).map(|i| (r * 500 + i) as u64).collect())
.collect();
check_exact(&sorted, 4);
let mut on_zero = vec![Vec::new(); 8];
on_zero[0] = uniform(&mut rng, 10_000);
check_exact(&on_zero, 5);
let mut skewed: Vec<Vec<u64>> = (0..32).map(|_| uniform(&mut rng, 3)).collect();
skewed[7] = uniform(&mut rng, 50_000);
check_exact(&skewed, 6);
}
#[test]
fn test_duplicates_few_records_and_empty_ranks() {
let equal: Vec<Vec<u64>> = (0..12).map(|_| vec![7; 300]).collect();
check_exact(&equal, 7);
let mut few = vec![Vec::new(); 8];
few[3] = vec![5, 1, 4];
few[6] = vec![2, 2];
check_exact(&few, 8);
let mut one = vec![Vec::new(); 5];
one[4] = vec![42];
check_exact(&one, 9);
}
#[test]
fn test_initial_brackets_targets() {
let brackets = initial_brackets::<Unique>(10, 4);
let targets = brackets.iter().map(|b| b.target).collect_vec();
assert_eq!(targets, vec![0, 2, 5, 7]);
let brackets = initial_brackets::<Unique>(3, 5);
let targets = brackets.iter().map(|b| b.target).collect_vec();
assert_eq!(targets, vec![0, 0, 1, 1, 2]);
}
}