use alloc::vec::Vec;
use burn_tensor::Device;
#[derive(Debug, Clone, PartialEq)]
pub struct LayerPlacement {
pub input: Device,
pub hidden: Vec<Device>,
pub output: Device,
}
impl LayerPlacement {
pub fn new(stages: &[LayerStage]) -> Self {
let first = stages
.first()
.expect("a placement needs at least one stage");
let last = stages.last().expect("a placement needs at least one stage");
Self {
input: first.device.clone(),
hidden: stages
.iter()
.flat_map(|stage| {
core::iter::repeat_n(stage.device.clone(), stage.num_hidden_layers)
})
.collect(),
output: last.device.clone(),
}
}
pub fn devices(&self) -> Vec<Device> {
let mut devices: Vec<Device> = Vec::new();
for device in core::iter::once(&self.input)
.chain(&self.hidden)
.chain(core::iter::once(&self.output))
{
if !devices.contains(device) {
devices.push(device.clone());
}
}
devices
}
pub fn stages(&self) -> Vec<LayerStage> {
self.hidden
.chunk_by(|a, b| a == b)
.map(|run| LayerStage {
device: run[0].clone(),
num_hidden_layers: run.len(),
})
.collect()
}
pub fn even(devices: &[Device], num_hidden_layers: usize) -> Self {
assert!(!devices.is_empty(), "a placement needs at least one device");
let devices = &devices[..num_hidden_layers.clamp(1, devices.len())];
let per_device = num_hidden_layers / devices.len();
let with_one_more = num_hidden_layers % devices.len();
let stages: Vec<LayerStage> = devices
.iter()
.enumerate()
.map(|(index, device)| LayerStage {
device: device.clone(),
num_hidden_layers: per_device + usize::from(index < with_one_more),
})
.collect();
Self::new(&stages)
}
pub fn by_memory(devices: &[DeviceMemory], memory: &LayerMemory) -> Option<Self> {
let layers = memory.in_fill_order();
let capacities: Vec<u64> = devices.iter().map(|device| device.capacity).collect();
let on = OrderedFill {
capacities: &capacities,
layers: &layers,
}
.runs()?;
let device = |layer: usize| devices[on[layer]].device.clone();
let output = match memory.output {
OutputMemory::AfterHidden(_) => device(layers.len() - 1),
OutputMemory::WithInput(_) => device(0),
};
Some(Self {
input: device(0),
hidden: (1..=memory.hidden.len()).map(device).collect(),
output,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LayerStage {
pub device: Device,
pub num_hidden_layers: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LayerMemory {
pub input: u64,
pub hidden: Vec<u64>,
pub output: OutputMemory,
}
impl LayerMemory {
fn in_fill_order(&self) -> Vec<u64> {
let mut layers = Vec::with_capacity(self.hidden.len() + 2);
layers.push(self.input);
layers.extend(&self.hidden);
match self.output {
OutputMemory::AfterHidden(bytes) => layers.push(bytes),
OutputMemory::WithInput(bytes) => layers[0] += bytes,
}
layers
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OutputMemory {
AfterHidden(u64),
WithInput(u64),
}
#[derive(Debug, Clone, PartialEq)]
pub struct DeviceMemory {
pub device: Device,
pub capacity: u64,
}
struct OrderedFill<'a> {
capacities: &'a [u64],
layers: &'a [u64],
}
impl OrderedFill<'_> {
const SEARCH_STEPS: u32 = 48;
fn runs(&self) -> Option<Vec<usize>> {
let share = self.smallest_share()?;
self.balanced(share)
}
fn smallest_share(&self) -> Option<f64> {
if !self.fits(1.0) {
return None;
}
let (mut fits, mut short) = (1.0, 0.0);
for _ in 0..Self::SEARCH_STEPS {
let share = (fits + short) / 2.0;
if self.fits(share) {
fits = share;
} else {
short = share;
}
}
Some(fits)
}
fn fits(&self, share: f64) -> bool {
let mut limits = self
.capacities
.iter()
.map(|&capacity| (capacity as f64 * share) as u64);
let mut room = 0;
for &bytes in self.layers {
while bytes > room {
match limits.next() {
Some(limit) => room = limit,
None => return false,
}
}
room -= bytes;
}
true
}
fn balanced(&self, share: f64) -> Option<Vec<usize>> {
let mut on = Vec::with_capacity(self.layers.len());
let mut next = 0;
for (device, &capacity) in self.capacities.iter().enumerate() {
let target = self.target(device, next);
let limit = (capacity as f64 * share) as u64;
let mut load = 0;
while let Some(&bytes) = self.layers.get(next) {
let wanted = load as f64 + bytes as f64 / 2.0 <= target;
let needed = || !self.after(device, next).fits(share);
if load + bytes > limit || !(wanted || needed()) {
break;
}
load += bytes;
on.push(device);
next += 1;
}
}
(next == self.layers.len()).then_some(on)
}
fn target(&self, device: usize, next: usize) -> f64 {
let left: u64 = self.layers[next..].iter().sum();
let rest: u64 = self.capacities[device..].iter().sum();
match rest {
0 => 0.0,
rest => left as f64 * self.capacities[device] as f64 / rest as f64,
}
}
fn after(&self, device: usize, next: usize) -> Self {
Self {
capacities: &self.capacities[device + 1..],
layers: &self.layers[next..],
}
}
}
#[cfg(test)]
mod ordered_fill_tests {
use super::*;
use alloc::{vec, vec::Vec};
fn worst_share(capacities: &[u64], layers: &[u64], on: &[usize]) -> Option<f64> {
let mut loads = vec![0u64; capacities.len()];
for (&bytes, &device) in layers.iter().zip(on) {
loads[device] += bytes;
}
loads
.iter()
.zip(capacities)
.try_fold(0.0f64, |worst, (&load, &capacity)| match load {
0 => Some(worst),
_ if load > capacity => None,
_ => Some(worst.max(load as f64 / capacity as f64)),
})
}
fn every_split(devices: usize, layers: usize) -> Vec<Vec<usize>> {
(0..layers).fold(vec![Vec::new()], |splits, _| {
splits
.into_iter()
.flat_map(|split: Vec<usize>| {
let from = split.last().copied().unwrap_or(0);
(from..devices).map(move |device| {
let mut longer = split.clone();
longer.push(device);
longer
})
})
.collect()
})
}
fn cases() -> impl Iterator<Item = (Vec<u64>, Vec<u64>)> {
let mut state = 0x2545_f491_4f6c_dd1du64;
let mut next = move |below: u64| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 33) % below
};
(0..600).map(move |_| {
let devices = 1 + next(4) as usize;
let layers = 1 + next(7) as usize;
let capacities = (0..devices).map(|_| next(61)).collect();
let sizes = (0..layers).map(|_| next(21)).collect();
(capacities, sizes)
})
}
#[test]
fn the_split_is_the_least_loaded_any_split_in_order_can_be() {
for (capacities, layers) in cases() {
let best = every_split(capacities.len(), layers.len())
.iter()
.filter_map(|split| worst_share(&capacities, &layers, split))
.reduce(f64::min);
let found = OrderedFill {
capacities: &capacities,
layers: &layers,
}
.runs();
match (best, found) {
(None, None) => {}
(Some(best), Some(on)) => {
let share = worst_share(&capacities, &layers, &on)
.expect("every device holds what it can");
assert!(
share <= best + 1e-9,
"{layers:?} on {capacities:?}: {on:?} holds {share}, a split holds {best}"
);
assert!(on.windows(2).all(|pair| pair[0] <= pair[1]), "{on:?}");
}
(best, found) => panic!(
"{layers:?} on {capacities:?}: the best split is {best:?}, found {found:?}"
),
}
}
}
#[test]
fn equal_layers_on_equal_devices_split_as_evenly_as_the_count_allows() {
for devices in 1..=4 {
for count in devices..=12 {
let on = OrderedFill {
capacities: &vec![1000; devices],
layers: &vec![10; count],
}
.runs()
.expect("they fit");
let mut held = vec![0; devices];
for device in on {
held[device] += 1;
}
let (least, most) = (held.iter().min().unwrap(), held.iter().max().unwrap());
assert!(most - least <= 1, "{count} layers on {devices}: {held:?}");
}
}
}
}
#[cfg(all(test, feature = "autodiff"))]
mod tests {
use super::*;
use crate::test_device;
use alloc::vec;
#[test]
fn an_even_placement_leaves_out_a_device_it_has_no_layer_for() {
let device = test_device();
let spare = device.clone().autodiff();
let placement = LayerPlacement::even(&[device, spare.clone(), spare], 1);
assert_eq!(placement.hidden.len(), 1);
assert!(!placement.input.is_autodiff());
assert!(!placement.output.is_autodiff());
}
#[test]
fn a_device_with_twice_the_capacity_takes_twice_the_layers() {
let device = test_device();
let (small, large) = (device.clone(), device.autodiff());
let placement = LayerPlacement::by_memory(
&[
DeviceMemory {
device: small,
capacity: 100,
},
DeviceMemory {
device: large,
capacity: 200,
},
],
&LayerMemory {
input: 0,
hidden: vec![10; 6],
output: OutputMemory::AfterHidden(0),
},
)
.expect("60 bytes fit in 300");
let on_large: Vec<bool> = placement.hidden.iter().map(Device::is_autodiff).collect();
assert_eq!(on_large, [false, false, true, true, true, true]);
}
#[test]
fn the_input_layer_counts_against_its_device() {
let device = test_device();
let placement = LayerPlacement::by_memory(
&[
DeviceMemory {
device: device.clone(),
capacity: 100,
},
DeviceMemory {
device: device.autodiff(),
capacity: 100,
},
],
&LayerMemory {
input: 60,
hidden: vec![10; 4],
output: OutputMemory::AfterHidden(0),
},
)
.expect("100 bytes fit in 200");
assert!(!placement.input.is_autodiff());
assert!(placement.hidden.iter().all(Device::is_autodiff));
}
#[test]
fn an_output_layer_with_the_input_runs_and_counts_on_its_device() {
let device = test_device();
let devices = [device.clone(), device.autodiff()].map(|device| DeviceMemory {
device,
capacity: 40,
});
let placement = LayerPlacement::by_memory(
&devices,
&LayerMemory {
input: 10,
hidden: vec![10; 4],
output: OutputMemory::WithInput(10),
},
)
.expect("60 bytes fit in 80");
let on_second: Vec<bool> = placement.hidden.iter().map(Device::is_autodiff).collect();
assert_eq!(on_second, [false, true, true, true]);
assert!(!placement.input.is_autodiff());
assert!(!placement.output.is_autodiff());
}
#[test]
fn layers_that_fit_on_no_device_are_refused() {
let device = test_device();
let devices = [device.clone(), device.autodiff()].map(|device| DeviceMemory {
device,
capacity: 10,
});
let memory = LayerMemory {
input: 0,
hidden: vec![8; 3],
output: OutputMemory::AfterHidden(0),
};
assert_eq!(LayerPlacement::by_memory(&devices, &memory), None);
}
#[test]
fn an_even_placement_gives_the_extra_layers_to_the_first_devices() {
let device = test_device();
let placement = LayerPlacement::even(&[device.clone().autodiff(), device], 3);
let on_autodiff: Vec<bool> = placement.hidden.iter().map(Device::is_autodiff).collect();
assert_eq!(on_autodiff, [true, true, false]);
assert!(placement.input.is_autodiff());
assert!(!placement.output.is_autodiff());
}
}