Skip to main content

burn_core/module/parallel/
placement.rs

1use alloc::vec::Vec;
2
3use burn_tensor::Device;
4
5/// Where each layer of a [`LayerParallelism`](super::LayerParallelism) model runs: the device of
6/// its input layer, of each hidden layer, and of its output layer.
7#[derive(Debug, Clone, PartialEq)]
8pub struct LayerPlacement {
9    /// Where the input layer runs.
10    pub input: Device,
11    /// Where each hidden layer runs, in forward order.
12    pub hidden: Vec<Device>,
13    /// Where the output layer runs.
14    pub output: Device,
15}
16
17impl LayerPlacement {
18    /// One device per hidden layer, read off the stages in order. The input layer runs on the
19    /// first stage's device and the output layer on the last stage's, so a stage of no hidden
20    /// layers gives one of them a device to itself.
21    ///
22    /// # Panics
23    ///
24    /// Panics when `stages` is empty.
25    pub fn new(stages: &[LayerStage]) -> Self {
26        let first = stages
27            .first()
28            .expect("a placement needs at least one stage");
29        let last = stages.last().expect("a placement needs at least one stage");
30
31        Self {
32            input: first.device.clone(),
33            hidden: stages
34                .iter()
35                .flat_map(|stage| {
36                    core::iter::repeat_n(stage.device.clone(), stage.num_hidden_layers)
37                })
38                .collect(),
39            output: last.device.clone(),
40        }
41    }
42
43    /// Every device a layer runs on, each once, in forward order: the input layer's first.
44    pub fn devices(&self) -> Vec<Device> {
45        let mut devices: Vec<Device> = Vec::new();
46        for device in core::iter::once(&self.input)
47            .chain(&self.hidden)
48            .chain(core::iter::once(&self.output))
49        {
50            if !devices.contains(device) {
51                devices.push(device.clone());
52            }
53        }
54        devices
55    }
56
57    /// Each device's run of consecutive hidden layers, in forward order.
58    pub fn stages(&self) -> Vec<LayerStage> {
59        self.hidden
60            .chunk_by(|a, b| a == b)
61            .map(|run| LayerStage {
62                device: run[0].clone(),
63                num_hidden_layers: run.len(),
64            })
65            .collect()
66    }
67
68    /// The hidden layers shared out in order across `devices`, as evenly as the count allows:
69    /// when it does not divide, the first devices take one layer more. Devices past the layer
70    /// count are left out rather than given a stage with nothing to run; [`new`](Self::new) places
71    /// those on purpose.
72    ///
73    /// # Panics
74    ///
75    /// Panics when `devices` is empty.
76    pub fn even(devices: &[Device], num_hidden_layers: usize) -> Self {
77        assert!(!devices.is_empty(), "a placement needs at least one device");
78        let devices = &devices[..num_hidden_layers.clamp(1, devices.len())];
79        let per_device = num_hidden_layers / devices.len();
80        let with_one_more = num_hidden_layers % devices.len();
81        let stages: Vec<LayerStage> = devices
82            .iter()
83            .enumerate()
84            .map(|(index, device)| LayerStage {
85                device: device.clone(),
86                num_hidden_layers: per_device + usize::from(index < with_one_more),
87            })
88            .collect();
89        Self::new(&stages)
90    }
91
92    /// The layers in order across `devices`, in runs sized to each device's capacity: the device
93    /// using the largest share of its capacity uses as small a share as a split in order allows,
94    /// so every device keeps about the same fraction free. A layer too large for what a device has
95    /// left moves on to the next, and devices past the last run are left out.
96    ///
97    /// `None` when the layers do not fit on `devices` at all.
98    pub fn by_memory(devices: &[DeviceMemory], memory: &LayerMemory) -> Option<Self> {
99        let layers = memory.in_fill_order();
100        let capacities: Vec<u64> = devices.iter().map(|device| device.capacity).collect();
101        let on = OrderedFill {
102            capacities: &capacities,
103            layers: &layers,
104        }
105        .runs()?;
106        let device = |layer: usize| devices[on[layer]].device.clone();
107        let output = match memory.output {
108            OutputMemory::AfterHidden(_) => device(layers.len() - 1),
109            OutputMemory::WithInput(_) => device(0),
110        };
111        Some(Self {
112            input: device(0),
113            hidden: (1..=memory.hidden.len()).map(device).collect(),
114            output,
115        })
116    }
117}
118
119/// A run of consecutive hidden layers on one device.
120#[derive(Debug, Clone, PartialEq)]
121pub struct LayerStage {
122    /// The device of every hidden layer in the run.
123    pub device: Device,
124    /// How many hidden layers the run holds.
125    pub num_hidden_layers: usize,
126}
127
128/// How many bytes each layer of a split model holds on its device.
129#[derive(Debug, Clone, PartialEq)]
130pub struct LayerMemory {
131    /// The input layer's.
132    pub input: u64,
133    /// Each hidden layer's, in forward order.
134    pub hidden: Vec<u64>,
135    /// The output layer's, and where it runs.
136    pub output: OutputMemory,
137}
138
139impl LayerMemory {
140    /// Each layer's bytes in the order they fill devices: the input layer's first, counting an
141    /// output layer that runs with it.
142    fn in_fill_order(&self) -> Vec<u64> {
143        let mut layers = Vec::with_capacity(self.hidden.len() + 2);
144        layers.push(self.input);
145        layers.extend(&self.hidden);
146        match self.output {
147            OutputMemory::AfterHidden(bytes) => layers.push(bytes),
148            OutputMemory::WithInput(bytes) => layers[0] += bytes,
149        }
150        layers
151    }
152}
153
154/// How many bytes the output layer of a split model holds, and which device it runs on.
155#[derive(Debug, Clone, PartialEq)]
156pub enum OutputMemory {
157    /// On the device after the last hidden layer's run.
158    AfterHidden(u64),
159    /// On the input layer's device, as an output layer sharing the input layer's parameters must.
160    WithInput(u64),
161}
162
163/// A device and how many bytes it can hold.
164#[derive(Debug, Clone, PartialEq)]
165pub struct DeviceMemory {
166    /// The device.
167    pub device: Device,
168    /// What it can hold, in bytes.
169    pub capacity: u64,
170}
171
172/// Layers laid in order across devices, each device holding a run of consecutive layers, for
173/// [`LayerPlacement::by_memory`].
174struct OrderedFill<'a> {
175    capacities: &'a [u64],
176    layers: &'a [u64],
177}
178
179impl OrderedFill<'_> {
180    /// Halvings of the share's range; an f64 stops changing at about 50.
181    const SEARCH_STEPS: u32 = 48;
182
183    /// The index of the device each layer runs on. The smallest share of every capacity that a
184    /// fill in order fits under bounds each device; within it, each device takes about its
185    /// capacity's share of the layers still to place, and more only when the rest would not fit
186    /// without them.
187    fn runs(&self) -> Option<Vec<usize>> {
188        let share = self.smallest_share()?;
189        self.balanced(share)
190    }
191
192    /// The smallest share of every device's capacity that a fill in order fits under.
193    fn smallest_share(&self) -> Option<f64> {
194        if !self.fits(1.0) {
195            return None;
196        }
197        let (mut fits, mut short) = (1.0, 0.0);
198        for _ in 0..Self::SEARCH_STEPS {
199            let share = (fits + short) / 2.0;
200            if self.fits(share) {
201                fits = share;
202            } else {
203                short = share;
204            }
205        }
206        Some(fits)
207    }
208
209    /// Whether every layer is placed when each device in turn takes as many as fit under `share`
210    /// of its capacity.
211    fn fits(&self, share: f64) -> bool {
212        let mut limits = self
213            .capacities
214            .iter()
215            .map(|&capacity| (capacity as f64 * share) as u64);
216        let mut room = 0;
217        for &bytes in self.layers {
218            while bytes > room {
219                match limits.next() {
220                    Some(limit) => room = limit,
221                    None => return false,
222                }
223            }
224            room -= bytes;
225        }
226        true
227    }
228
229    /// Each layer's device, every device taking about its capacity's share of the layers left
230    /// under `share` of its capacity, and more only when the devices after it would not hold the
231    /// rest.
232    fn balanced(&self, share: f64) -> Option<Vec<usize>> {
233        let mut on = Vec::with_capacity(self.layers.len());
234        let mut next = 0;
235        for (device, &capacity) in self.capacities.iter().enumerate() {
236            let target = self.target(device, next);
237            let limit = (capacity as f64 * share) as u64;
238            let mut load = 0;
239            while let Some(&bytes) = self.layers.get(next) {
240                // A layer whose middle lands within the target, so a run rounds to the nearest layer.
241                let wanted = load as f64 + bytes as f64 / 2.0 <= target;
242                let needed = || !self.after(device, next).fits(share);
243                if load + bytes > limit || !(wanted || needed()) {
244                    break;
245                }
246                load += bytes;
247                on.push(device);
248                next += 1;
249            }
250        }
251        (next == self.layers.len()).then_some(on)
252    }
253
254    /// `device`'s share of the layers from `next` on, by its capacity against the devices from it
255    /// on.
256    fn target(&self, device: usize, next: usize) -> f64 {
257        let left: u64 = self.layers[next..].iter().sum();
258        let rest: u64 = self.capacities[device..].iter().sum();
259        match rest {
260            0 => 0.0,
261            rest => left as f64 * self.capacities[device] as f64 / rest as f64,
262        }
263    }
264
265    /// The layers from `next` on, over the devices after `device`.
266    fn after(&self, device: usize, next: usize) -> Self {
267        Self {
268            capacities: &self.capacities[device + 1..],
269            layers: &self.layers[next..],
270        }
271    }
272}
273
274#[cfg(test)]
275mod ordered_fill_tests {
276    use super::*;
277    use alloc::{vec, vec::Vec};
278
279    /// The largest share of its capacity any device holds, `None` past a capacity.
280    fn worst_share(capacities: &[u64], layers: &[u64], on: &[usize]) -> Option<f64> {
281        let mut loads = vec![0u64; capacities.len()];
282        for (&bytes, &device) in layers.iter().zip(on) {
283            loads[device] += bytes;
284        }
285        loads
286            .iter()
287            .zip(capacities)
288            .try_fold(0.0f64, |worst, (&load, &capacity)| match load {
289                0 => Some(worst),
290                _ if load > capacity => None,
291                _ => Some(worst.max(load as f64 / capacity as f64)),
292            })
293    }
294
295    /// Every way to run `layers` across `devices` devices in order, a layer never on an earlier
296    /// device than the one before it.
297    fn every_split(devices: usize, layers: usize) -> Vec<Vec<usize>> {
298        (0..layers).fold(vec![Vec::new()], |splits, _| {
299            splits
300                .into_iter()
301                .flat_map(|split: Vec<usize>| {
302                    let from = split.last().copied().unwrap_or(0);
303                    (from..devices).map(move |device| {
304                        let mut longer = split.clone();
305                        longer.push(device);
306                        longer
307                    })
308                })
309                .collect()
310        })
311    }
312
313    /// Small cases drawn from a fixed linear congruential sequence.
314    fn cases() -> impl Iterator<Item = (Vec<u64>, Vec<u64>)> {
315        let mut state = 0x2545_f491_4f6c_dd1du64;
316        let mut next = move |below: u64| {
317            state = state
318                .wrapping_mul(6364136223846793005)
319                .wrapping_add(1442695040888963407);
320            (state >> 33) % below
321        };
322        (0..600).map(move |_| {
323            let devices = 1 + next(4) as usize;
324            let layers = 1 + next(7) as usize;
325            let capacities = (0..devices).map(|_| next(61)).collect();
326            let sizes = (0..layers).map(|_| next(21)).collect();
327            (capacities, sizes)
328        })
329    }
330
331    #[test]
332    fn the_split_is_the_least_loaded_any_split_in_order_can_be() {
333        for (capacities, layers) in cases() {
334            let best = every_split(capacities.len(), layers.len())
335                .iter()
336                .filter_map(|split| worst_share(&capacities, &layers, split))
337                .reduce(f64::min);
338            let found = OrderedFill {
339                capacities: &capacities,
340                layers: &layers,
341            }
342            .runs();
343
344            match (best, found) {
345                (None, None) => {}
346                (Some(best), Some(on)) => {
347                    let share = worst_share(&capacities, &layers, &on)
348                        .expect("every device holds what it can");
349                    assert!(
350                        share <= best + 1e-9,
351                        "{layers:?} on {capacities:?}: {on:?} holds {share}, a split holds {best}"
352                    );
353                    assert!(on.windows(2).all(|pair| pair[0] <= pair[1]), "{on:?}");
354                }
355                (best, found) => panic!(
356                    "{layers:?} on {capacities:?}: the best split is {best:?}, found {found:?}"
357                ),
358            }
359        }
360    }
361
362    #[test]
363    fn equal_layers_on_equal_devices_split_as_evenly_as_the_count_allows() {
364        for devices in 1..=4 {
365            for count in devices..=12 {
366                let on = OrderedFill {
367                    capacities: &vec![1000; devices],
368                    layers: &vec![10; count],
369                }
370                .runs()
371                .expect("they fit");
372                let mut held = vec![0; devices];
373                for device in on {
374                    held[device] += 1;
375                }
376                let (least, most) = (held.iter().min().unwrap(), held.iter().max().unwrap());
377                assert!(most - least <= 1, "{count} layers on {devices}: {held:?}");
378            }
379        }
380    }
381}
382
383#[cfg(all(test, feature = "autodiff"))]
384mod tests {
385    use super::*;
386    use crate::test_device;
387    use alloc::vec;
388
389    #[test]
390    fn an_even_placement_leaves_out_a_device_it_has_no_layer_for() {
391        let device = test_device();
392        let spare = device.clone().autodiff();
393        let placement = LayerPlacement::even(&[device, spare.clone(), spare], 1);
394
395        assert_eq!(placement.hidden.len(), 1);
396        assert!(!placement.input.is_autodiff());
397        assert!(!placement.output.is_autodiff());
398    }
399
400    #[test]
401    fn a_device_with_twice_the_capacity_takes_twice_the_layers() {
402        let device = test_device();
403        let (small, large) = (device.clone(), device.autodiff());
404        let placement = LayerPlacement::by_memory(
405            &[
406                DeviceMemory {
407                    device: small,
408                    capacity: 100,
409                },
410                DeviceMemory {
411                    device: large,
412                    capacity: 200,
413                },
414            ],
415            &LayerMemory {
416                input: 0,
417                hidden: vec![10; 6],
418                output: OutputMemory::AfterHidden(0),
419            },
420        )
421        .expect("60 bytes fit in 300");
422
423        let on_large: Vec<bool> = placement.hidden.iter().map(Device::is_autodiff).collect();
424        assert_eq!(on_large, [false, false, true, true, true, true]);
425    }
426
427    #[test]
428    fn the_input_layer_counts_against_its_device() {
429        let device = test_device();
430        let placement = LayerPlacement::by_memory(
431            &[
432                DeviceMemory {
433                    device: device.clone(),
434                    capacity: 100,
435                },
436                DeviceMemory {
437                    device: device.autodiff(),
438                    capacity: 100,
439                },
440            ],
441            &LayerMemory {
442                input: 60,
443                hidden: vec![10; 4],
444                output: OutputMemory::AfterHidden(0),
445            },
446        )
447        .expect("100 bytes fit in 200");
448
449        assert!(!placement.input.is_autodiff());
450        assert!(placement.hidden.iter().all(Device::is_autodiff));
451    }
452
453    #[test]
454    fn an_output_layer_with_the_input_runs_and_counts_on_its_device() {
455        let device = test_device();
456        let devices = [device.clone(), device.autodiff()].map(|device| DeviceMemory {
457            device,
458            capacity: 40,
459        });
460        let placement = LayerPlacement::by_memory(
461            &devices,
462            &LayerMemory {
463                input: 10,
464                hidden: vec![10; 4],
465                output: OutputMemory::WithInput(10),
466            },
467        )
468        .expect("60 bytes fit in 80");
469
470        let on_second: Vec<bool> = placement.hidden.iter().map(Device::is_autodiff).collect();
471        assert_eq!(on_second, [false, true, true, true]);
472        assert!(!placement.input.is_autodiff());
473        assert!(!placement.output.is_autodiff());
474    }
475
476    #[test]
477    fn layers_that_fit_on_no_device_are_refused() {
478        let device = test_device();
479        let devices = [device.clone(), device.autodiff()].map(|device| DeviceMemory {
480            device,
481            capacity: 10,
482        });
483        let memory = LayerMemory {
484            input: 0,
485            hidden: vec![8; 3],
486            output: OutputMemory::AfterHidden(0),
487        };
488
489        assert_eq!(LayerPlacement::by_memory(&devices, &memory), None);
490    }
491
492    #[test]
493    fn an_even_placement_gives_the_extra_layers_to_the_first_devices() {
494        let device = test_device();
495        let placement = LayerPlacement::even(&[device.clone().autodiff(), device], 3);
496
497        let on_autodiff: Vec<bool> = placement.hidden.iter().map(Device::is_autodiff).collect();
498        assert_eq!(on_autodiff, [true, true, false]);
499        assert!(placement.input.is_autodiff());
500        assert!(!placement.output.is_autodiff());
501    }
502}