burn_core/module/parallel/
placement.rs1use alloc::vec::Vec;
2
3use burn_tensor::Device;
4
5#[derive(Debug, Clone, PartialEq)]
8pub struct LayerPlacement {
9 pub input: Device,
11 pub hidden: Vec<Device>,
13 pub output: Device,
15}
16
17impl LayerPlacement {
18 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 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 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 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 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#[derive(Debug, Clone, PartialEq)]
121pub struct LayerStage {
122 pub device: Device,
124 pub num_hidden_layers: usize,
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub struct LayerMemory {
131 pub input: u64,
133 pub hidden: Vec<u64>,
135 pub output: OutputMemory,
137}
138
139impl LayerMemory {
140 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#[derive(Debug, Clone, PartialEq)]
156pub enum OutputMemory {
157 AfterHidden(u64),
159 WithInput(u64),
161}
162
163#[derive(Debug, Clone, PartialEq)]
165pub struct DeviceMemory {
166 pub device: Device,
168 pub capacity: u64,
170}
171
172struct OrderedFill<'a> {
175 capacities: &'a [u64],
176 layers: &'a [u64],
177}
178
179impl OrderedFill<'_> {
180 const SEARCH_STEPS: u32 = 48;
182
183 fn runs(&self) -> Option<Vec<usize>> {
188 let share = self.smallest_share()?;
189 self.balanced(share)
190 }
191
192 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 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 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 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 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 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 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 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 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}