Skip to main content

burn_core/module/parallel/
model.rs

1use alloc::vec::Vec;
2
3use burn_tensor::Device;
4
5use super::{DistributedLayer, LayerParallelism, LayerPlacement, LayerSequence, LayerStage};
6use crate::module::{Devices, Module, ModuleMapper, ModuleVisitor};
7
8/// A [`LayerParallelism`] model split across devices, holding where each of its layers runs so
9/// the forward reads it rather than working it out again on every call. It is itself a
10/// [`Module`], so records, devices and training flags work as they do on the model alone.
11#[derive(Clone, Debug)]
12pub struct DistributedLayeredModel<M: LayerParallelism> {
13    model: M,
14    placement: LayerPlacement,
15}
16
17impl<M: LayerParallelism> DistributedLayeredModel<M> {
18    /// Split `model` across the devices `placement` gives its layers. Nothing moves: each layer is
19    /// built where it runs, and this checks it is.
20    ///
21    /// # Panics
22    ///
23    /// Panics when `placement` does not give a device to every hidden layer, or when a layer holds
24    /// a tensor off the device `placement` gives it.
25    pub fn new(model: M, placement: &LayerPlacement) -> Self {
26        assert_built_where_placed(&model, placement);
27
28        Self {
29            model,
30            placement: placement.clone(),
31        }
32    }
33
34    /// Where each layer runs, which is where its own parameters are.
35    pub fn placement(&self) -> &LayerPlacement {
36        &self.placement
37    }
38
39    /// Run each layer on its device, moving what passes between them.
40    pub fn forward(
41        &self,
42        input: <M::InputLayer as DistributedLayer>::Input,
43    ) -> <M::OutputLayer as DistributedLayer>::Output {
44        let placement = &self.placement;
45        let mut signal = self
46            .model
47            .layer_input()
48            .forward(input.to_device(&placement.input));
49        for (layer, device) in self.model.layers_hidden().zip(&placement.hidden) {
50            signal = layer.forward(signal.to_device(device));
51        }
52        self.model
53            .layer_output()
54            .forward(signal.to_device(&placement.output))
55    }
56
57    /// The model, its layers where the placement puts them.
58    pub fn model(&self) -> &M {
59        &self.model
60    }
61
62    /// The model on its own, no longer carrying where its layers run.
63    pub fn into_inner(self) -> M {
64        self.model
65    }
66
67    /// The model taken apart, its hidden layers cut into the placement's stages.
68    ///
69    /// # Panics
70    ///
71    /// Panics when a stage would hold a layer off its device, which is what an
72    /// [`into_layers`](LayerParallelism::into_layers) giving the hidden layers in another order than
73    /// [`layers_hidden`](LayerParallelism::layers_hidden) does.
74    pub fn into_stages(self) -> StagedLayers<M> {
75        let LayerSequence {
76            input,
77            hidden,
78            output,
79        } = self.model.into_layers();
80        let mut hidden = hidden.into_iter();
81        let stages = self
82            .placement
83            .stages()
84            .into_iter()
85            .enumerate()
86            .map(|(index, stage)| {
87                let layers = hidden.by_ref().take(stage.num_hidden_layers).collect();
88                hold_stage::<M>(index, stage, layers)
89            })
90            .collect();
91        StagedLayers {
92            input,
93            stages,
94            output,
95        }
96    }
97
98    /// A move of the whole model puts every layer on one device, the hidden layer count unchanged.
99    fn on_one_device(&self, device: &Device) -> LayerPlacement {
100        LayerPlacement::new(&[LayerStage {
101            device: device.clone(),
102            num_hidden_layers: self.placement.hidden.len(),
103        }])
104    }
105}
106
107/// A mapper is trusted to leave each parameter where it found it, so only a move of the whole
108/// model rewrites the placement.
109impl<M: LayerParallelism> Module for DistributedLayeredModel<M> {
110    fn collect_devices(&self, devices: Devices) -> Devices {
111        self.model.collect_devices(devices)
112    }
113
114    fn fork(self, device: &Device) -> Self {
115        let placement = self.on_one_device(device);
116        Self {
117            model: self.model.fork(device),
118            placement,
119        }
120    }
121
122    fn to_device(self, device: &Device) -> Self {
123        let placement = self.on_one_device(device);
124        Self {
125            model: self.model.to_device(device),
126            placement,
127        }
128    }
129
130    fn train(self) -> Self {
131        Self {
132            model: self.model.train(),
133            placement: self.placement,
134        }
135    }
136
137    fn valid(&self) -> Self {
138        Self {
139            model: self.model.valid(),
140            placement: self.placement.clone(),
141        }
142    }
143
144    fn visit<Visitor: ModuleVisitor>(&self, visitor: &mut Visitor) {
145        self.model.visit(visitor);
146    }
147
148    fn map<Mapper: ModuleMapper>(self, mapper: &mut Mapper) -> Self {
149        Self {
150            model: self.model.map(mapper),
151            placement: self.placement,
152        }
153    }
154
155    fn materialize(self) -> Self {
156        Self {
157            model: self.model.materialize(),
158            placement: self.placement,
159        }
160    }
161}
162
163/// A [`DistributedLayeredModel`]'s layers, owned, its hidden layers in the placement's stages.
164#[derive(Debug)]
165pub struct StagedLayers<M: LayerParallelism> {
166    /// The input layer.
167    pub input: M::InputLayer,
168    /// The stages, in forward order.
169    pub stages: Vec<HeldStage<M>>,
170    /// The output layer.
171    pub output: M::OutputLayer,
172}
173
174/// One stage's hidden layers and the device they run on.
175#[derive(Debug)]
176pub struct HeldStage<M: LayerParallelism> {
177    /// The device every layer of the stage runs on.
178    pub device: Device,
179    /// The stage's hidden layers, in forward order.
180    pub layers: Vec<M::HiddenLayer>,
181}
182
183/// `layers` as stage `index`, checked to be on the stage's device.
184fn hold_stage<M: LayerParallelism>(
185    index: usize,
186    stage: LayerStage,
187    layers: Vec<M::HiddenLayer>,
188) -> HeldStage<M> {
189    assert!(
190        layers.iter().all(|layer| layer_is_on(layer, &stage.device)),
191        "stage {index} holds a layer off {:?}: `into_layers` must give the hidden layers in \
192         the order `layers_hidden` does",
193        stage.device
194    );
195    HeldStage {
196        layers,
197        device: stage.device,
198    }
199}
200
201fn assert_built_where_placed<M: LayerParallelism>(model: &M, placement: &LayerPlacement) {
202    let count = model.layers_hidden().count();
203    assert!(
204        count == placement.hidden.len(),
205        "the placement must give a device to every hidden layer: the model has {count}, the \
206         placement gives {}",
207        placement.hidden.len()
208    );
209
210    assert!(
211        layer_is_on(model.layer_input(), &placement.input),
212        "the input layer must be built on {:?}",
213        placement.input
214    );
215    for (index, (layer, device)) in model.layers_hidden().zip(&placement.hidden).enumerate() {
216        assert!(
217            layer_is_on(layer, device),
218            "hidden layer {index} must be built on {device:?}"
219        );
220    }
221    assert!(
222        layer_is_on(model.layer_output(), &placement.output),
223        "the output layer must be built on {:?}",
224        placement.output
225    );
226}
227
228fn layer_is_on(layer: &impl Module, device: &Device) -> bool {
229    layer.devices().iter().all(|found| found == device)
230}
231
232#[cfg(test)]
233mod tests {
234    use alloc::vec::Vec;
235
236    use burn_tensor::{Distribution, Tensor, Tolerance};
237
238    use super::*;
239    use crate as burn;
240    use crate::module::parallel::LayerSequence;
241    use crate::{test_device, test_utils::SimpleLinear};
242
243    const WIDTH: usize = 8;
244
245    #[test]
246    fn the_forward_on_one_device_is_the_plain_forward() {
247        let device = test_device();
248        let stack = Stack::new(3, &device);
249        let input = Tensor::random([4, WIDTH], Distribution::Default, &device);
250        let expected = stack.plain_forward(input.clone());
251
252        let placement = LayerPlacement::even(core::slice::from_ref(&device), 3);
253        DistributedLayeredModel::new(stack, &placement)
254            .forward(input)
255            .into_data()
256            .assert_approx_eq::<f32>(&expected.into_data(), Tolerance::default());
257    }
258
259    #[test]
260    #[should_panic(expected = "every hidden layer")]
261    fn a_placement_short_of_a_hidden_layer_is_refused() {
262        let device = test_device();
263        let placement = LayerPlacement::even(core::slice::from_ref(&device), 2);
264        DistributedLayeredModel::new(Stack::new(3, &device), &placement);
265    }
266
267    #[test]
268    #[should_panic(expected = "every hidden layer")]
269    fn a_placement_past_the_last_hidden_layer_is_refused() {
270        let device = test_device();
271        let placement = LayerPlacement::even(core::slice::from_ref(&device), 4);
272        DistributedLayeredModel::new(Stack::new(3, &device), &placement);
273    }
274
275    #[cfg(feature = "autodiff")]
276    #[test]
277    fn every_parameter_receives_a_gradient() {
278        let device = test_device().autodiff();
279        let placement = LayerPlacement::even(core::slice::from_ref(&device), 3);
280        let stack = DistributedLayeredModel::new(Stack::new(3, &device), &placement);
281        let input = Tensor::random([4, WIDTH], Distribution::Default, &device);
282
283        let grads = stack.forward(input).sum().backward();
284
285        let model = stack.model();
286        let hidden = model.hidden.iter().map(|layer| &layer.linear);
287        for (index, linear) in [&model.input.linear, &model.output.linear]
288            .into_iter()
289            .chain(hidden)
290            .enumerate()
291        {
292            assert!(
293                linear.weight.grad(&grads).is_some(),
294                "layer {index} has no gradient"
295            );
296        }
297    }
298
299    #[derive(Module, Debug)]
300    struct Stack {
301        input: Tanh,
302        hidden: Vec<Tanh>,
303        output: Head,
304    }
305
306    impl Stack {
307        fn new(hidden: usize, device: &Device) -> Self {
308            Self {
309                input: Tanh::new(device),
310                hidden: (0..hidden).map(|_| Tanh::new(device)).collect(),
311                output: Head {
312                    linear: SimpleLinear::new(WIDTH, 2, device),
313                },
314            }
315        }
316
317        /// The reference the split forward has to reproduce.
318        fn plain_forward(&self, input: Tensor<2>) -> Tensor<2> {
319            let hidden = self
320                .hidden
321                .iter()
322                .fold(self.input.forward(input), |x, layer| layer.forward(x));
323            self.output.forward(hidden)
324        }
325    }
326
327    impl LayerParallelism for Stack {
328        type InputLayer = Tanh;
329        type HiddenLayer = Tanh;
330        type OutputLayer = Head;
331
332        fn layer_input(&self) -> &Tanh {
333            &self.input
334        }
335
336        fn layers_hidden(&self) -> impl Iterator<Item = &Tanh> {
337            self.hidden.iter()
338        }
339
340        fn layer_output(&self) -> &Head {
341            &self.output
342        }
343
344        fn into_layers(self) -> LayerSequence<Self> {
345            LayerSequence {
346                input: self.input,
347                hidden: self.hidden,
348                output: self.output,
349            }
350        }
351    }
352
353    #[derive(Module, Debug)]
354    struct Tanh {
355        linear: SimpleLinear,
356    }
357
358    impl Tanh {
359        fn new(device: &Device) -> Self {
360            Self {
361                linear: SimpleLinear::new(WIDTH, WIDTH, device),
362            }
363        }
364    }
365
366    impl DistributedLayer for Tanh {
367        type Input = Tensor<2>;
368        type Output = Tensor<2>;
369
370        fn forward(&self, input: Tensor<2>) -> Tensor<2> {
371            linear(&self.linear, input).tanh()
372        }
373    }
374
375    #[derive(Module, Debug)]
376    struct Head {
377        linear: SimpleLinear,
378    }
379
380    impl DistributedLayer for Head {
381        type Input = Tensor<2>;
382        type Output = Tensor<2>;
383
384        fn forward(&self, input: Tensor<2>) -> Tensor<2> {
385            linear(&self.linear, input)
386        }
387    }
388
389    /// `SimpleLinear` is a bare pair of parameters, with no forward of its own.
390    fn linear(layer: &SimpleLinear, x: Tensor<2>) -> Tensor<2> {
391        let out = x.matmul(layer.weight.val().transpose());
392        match &layer.bias {
393            Some(bias) => out + bias.val().unsqueeze(),
394            None => out,
395        }
396    }
397}