use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockRole {
Single,
First,
Middle,
Last,
}
pub fn block_role(rank: u32, world: u32) -> BlockRole {
if world <= 1 {
BlockRole::Single
} else if rank == world - 1 {
BlockRole::First
} else if rank == 0 {
BlockRole::Last
} else {
BlockRole::Middle
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct LayerSplit {
pub counts: Vec<u32>,
}
impl LayerSplit {
pub fn even(layers: usize, world: u32) -> Self {
let world = world.max(1) as usize;
let base = layers / world;
let extra = layers % world;
let counts = (0..world)
.map(|r| (base + usize::from(world - 1 - r < extra)) as u32)
.collect();
Self { counts }
}
pub fn weighted(layers: usize, weights: &[u64]) -> Self {
let world = weights.len();
if world == 0 || layers == 0 {
return Self { counts: Vec::new() };
}
if world > layers {
return Self::even(layers, world as u32);
}
let total: u128 = weights.iter().map(|&w| w as u128).sum();
if total == 0 {
return Self::even(layers, world as u32);
}
let spare = layers - world;
let mut counts: Vec<u32> = vec![1; world];
let mut shares: Vec<(usize, f64)> = Vec::with_capacity(world);
let mut given = 0usize;
for (r, &w) in weights.iter().enumerate() {
let exact = spare as f64 * (w as f64 / total as f64);
let whole = exact.floor() as usize;
counts[r] += whole as u32;
given += whole;
shares.push((r, exact - exact.floor()));
}
shares.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
for (r, _) in shares.into_iter().take(spare - given) {
counts[r] += 1;
}
Self { counts }
}
pub fn parse(s: &str) -> Result<Self, String> {
let mut counts = Vec::new();
for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
let n: u32 = part
.parse()
.map_err(|_| format!("{part:?} is not a layer count"))?;
counts.push(n);
}
if counts.is_empty() {
return Err("a layer split needs one count per rank, like 14,14".into());
}
Ok(Self { counts })
}
pub fn total(&self) -> usize {
self.counts.iter().map(|&c| c as usize).sum()
}
pub fn range(&self, rank: u32) -> Option<std::ops::Range<usize>> {
let r = rank as usize;
if r >= self.counts.len() {
return None;
}
let start: usize = self.counts[r + 1..].iter().map(|&c| c as usize).sum();
Some(start..start + self.counts[r] as usize)
}
pub fn fingerprint(&self) -> u32 {
let mut h: u32 = 0x811c9dc5;
for &c in &self.counts {
for b in c.to_le_bytes() {
h ^= b as u32;
h = h.wrapping_mul(0x0100_0193);
}
}
h & 0x00ff_ffff
}
pub fn validate(&self, layers: usize, world: u32) -> Result<(), String> {
if self.counts.len() as u32 != world {
return Err(format!(
"the layer split has {} entries for a world of {world}: it needs \
one count per rank",
self.counts.len()
));
}
if let Some(r) = self.counts.iter().position(|&c| c == 0) {
return Err(format!(
"rank {r} was given no layers, so it would take a turn in the \
chain to do nothing"
));
}
if self.total() != layers {
return Err(format!(
"the layer split covers {} layers but the model has {layers}",
self.total()
));
}
Ok(())
}
}
pub fn pipeline_layer_range(num_layers: usize, rank: u32, world: u32) -> Range<usize> {
let world = world.max(1) as usize;
let rank = rank as usize;
assert!(rank < world, "rank {rank} out of range for world {world}");
let per = num_layers / world;
let extra = num_layers % world;
let j = world - 1 - rank;
let size_of = |i: usize| per + if i < extra { 1 } else { 0 };
let start: usize = (0..j).map(size_of).sum();
let len = size_of(j);
start..(start + len)
}
#[cfg(test)]
mod tests {
use super::*;
fn covers_every_layer_once(split: &LayerSplit, layers: usize) {
let world = split.counts.len() as u32;
let mut seen = vec![0u32; layers];
for rank in 0..world {
for g in split.range(rank).expect("every rank has a range") {
seen[g] += 1;
}
}
assert!(
seen.iter().all(|&c| c == 1),
"split {:?} over {layers} layers: {seen:?}",
split.counts
);
for rank in 1..world {
let lower = split.range(rank).unwrap();
let upper = split.range(rank - 1).unwrap();
assert_eq!(
lower.end,
upper.start,
"rank {rank} must abut rank {}",
rank - 1
);
}
}
#[test]
fn an_even_split_matches_what_no_split_at_all_does() {
for layers in [4usize, 28, 32, 36, 64, 80] {
for world in 1..=8u32 {
if world as usize > layers {
continue;
}
let split = LayerSplit::even(layers, world);
assert_eq!(split.total(), layers);
covers_every_layer_once(&split, layers);
}
}
}
#[test]
fn a_weighted_split_follows_the_memory() {
let split = LayerSplit::weighted(28, &[64 << 30, 16 << 30]);
assert_eq!(split.total(), 28);
covers_every_layer_once(&split, 28);
assert!(
split.counts[0] > split.counts[1] * 3,
"expected roughly 4:1, got {:?}",
split.counts
);
}
#[test]
fn a_weighted_split_never_leaves_a_rank_with_nothing() {
let split = LayerSplit::weighted(28, &[1 << 40, 1, 1]);
assert_eq!(split.total(), 28);
assert!(split.counts.iter().all(|&c| c >= 1), "{:?}", split.counts);
covers_every_layer_once(&split, 28);
}
#[test]
fn a_weighted_split_handles_degenerate_inputs() {
assert_eq!(
LayerSplit::weighted(28, &[8, 8]).counts,
LayerSplit::even(28, 2).counts
);
assert_eq!(
LayerSplit::weighted(28, &[0, 0]).counts,
LayerSplit::even(28, 2).counts
);
let many = LayerSplit::weighted(3, &[9, 1, 1, 1, 1]);
assert_eq!(many.total(), 3);
assert!(LayerSplit::weighted(0, &[1]).counts.is_empty());
assert!(LayerSplit::weighted(28, &[]).counts.is_empty());
}
#[test]
fn the_even_split_and_the_range_function_describe_the_same_thing() {
for layers in [1usize, 2, 4, 7, 28, 32, 33, 64, 80] {
for world in 1..=8u32 {
if world as usize > layers {
continue;
}
let split = LayerSplit::even(layers, world);
for rank in 0..world {
assert_eq!(
split.range(rank).unwrap(),
pipeline_layer_range(layers, rank, world),
"layers={layers} world={world} rank={rank}"
);
}
}
}
}
#[test]
fn a_split_is_read_the_way_it_is_written() {
let split = LayerSplit::parse("10,18").unwrap();
assert_eq!(split.range(1).unwrap(), 0..18, "rank 1 takes the first 18");
assert_eq!(split.range(0).unwrap(), 18..28, "rank 0 takes the last 10");
assert_eq!(split.range(2), None);
covers_every_layer_once(&split, 28);
}
#[test]
fn a_split_that_is_not_a_split_is_refused() {
let s = LayerSplit::parse("14,14").unwrap();
assert_eq!(s.validate(28, 2), Ok(()));
assert!(
s.validate(28, 3)
.unwrap_err()
.contains("one count per rank")
);
assert!(s.validate(32, 2).unwrap_err().contains("covers 28"));
let empty = LayerSplit::parse("28,0").unwrap();
assert!(empty.validate(28, 2).unwrap_err().contains("no layers"));
assert!(LayerSplit::parse("14,x").is_err());
assert!(LayerSplit::parse("").is_err());
assert!(LayerSplit::parse(" , ").is_err());
}
#[test]
fn the_fingerprint_separates_splits_that_would_otherwise_pass_silently() {
let a = LayerSplit::parse("14,14").unwrap();
let b = LayerSplit::parse("13,15").unwrap();
let c = LayerSplit::parse("15,13").unwrap();
assert_ne!(a.fingerprint(), b.fingerprint());
assert_ne!(b.fingerprint(), c.fingerprint(), "order has to matter");
assert_eq!(
a.fingerprint(),
LayerSplit::parse("14, 14").unwrap().fingerprint()
);
}
#[test]
fn a_fingerprint_survives_the_float_control_channel() {
for counts in [vec![14u32, 14], vec![1, 27], vec![9, 9, 10], vec![80]] {
let f = LayerSplit { counts }.fingerprint();
assert!(f < (1 << 24), "{f} is too large for f32");
assert_eq!(f as f32 as u32, f);
}
}
#[test]
fn roles_map_correctly() {
assert_eq!(block_role(0, 1), BlockRole::Single);
assert_eq!(block_role(0, 4), BlockRole::Last);
assert_eq!(block_role(3, 4), BlockRole::First);
assert_eq!(block_role(1, 4), BlockRole::Middle);
assert_eq!(block_role(2, 4), BlockRole::Middle);
}
#[test]
fn even_split_is_reversed() {
assert_eq!(pipeline_layer_range(16, 0, 4), 12..16);
assert_eq!(pipeline_layer_range(16, 1, 4), 8..12);
assert_eq!(pipeline_layer_range(16, 2, 4), 4..8);
assert_eq!(pipeline_layer_range(16, 3, 4), 0..4);
}
#[test]
fn uneven_split_tiles_exactly() {
assert_eq!(pipeline_layer_range(10, 0, 4), 8..10);
assert_eq!(pipeline_layer_range(10, 1, 4), 6..8);
assert_eq!(pipeline_layer_range(10, 2, 4), 3..6);
assert_eq!(pipeline_layer_range(10, 3, 4), 0..3);
let mut covered = [0u8; 10];
for r in 0..4u32 {
for i in pipeline_layer_range(10, r, 4) {
covered[i] += 1;
}
}
assert!(covered.iter().all(|&c| c == 1), "every layer covered once");
}
#[test]
fn single_rank_owns_all() {
assert_eq!(pipeline_layer_range(28, 0, 1), 0..28);
}
#[test]
fn more_ranks_than_layers_is_safe() {
let total: usize = (0..4u32).map(|r| pipeline_layer_range(2, r, 4).len()).sum();
assert_eq!(total, 2);
}
}