Skip to main content

burn_train/learner/supervised/step/
train.rs

1use crate::LearnerModel;
2use crate::{TrainOutput, TrainStep, TrainingModelInput, TrainingModelOutput};
3use burn_core::data::dataloader::DataLoaderIterator;
4use burn_core::data::dataloader::Progress;
5use burn_core::data::dataset::DatasetError;
6use burn_core::tensor::Device;
7use std::sync::mpsc::{Receiver, Sender};
8use std::thread::spawn;
9
10/// Outputs and progress collected from workers for one step.
11type StepOutput<TO> = (Vec<MultiTrainOutput<TO>>, Progress);
12
13/// Error that happened on one device during a step of multi-device training.
14#[derive(Debug)]
15pub enum MultiDeviceStepError {
16    /// Error while loading data.
17    Dataset(DatasetError),
18    /// The training step panicked on one or more workers.
19    Workers(Vec<WorkerFailure>),
20}
21
22/// A training step that panicked on a worker.
23#[derive(Debug)]
24pub struct WorkerFailure {
25    /// The worker's device index.
26    pub device_id: usize,
27    /// The panic message.
28    pub message: String,
29}
30
31impl core::fmt::Display for WorkerFailure {
32    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33        write!(
34            f,
35            "training worker on device {} failed: {}",
36            self.device_id, self.message
37        )
38    }
39}
40
41/// Every failure, on one line if there is only one.
42pub(crate) fn fmt_worker_failures(
43    failures: &[WorkerFailure],
44    f: &mut core::fmt::Formatter<'_>,
45) -> core::fmt::Result {
46    match failures {
47        [failure] => write!(f, "{failure}"),
48        failures => {
49            write!(f, "{} training workers failed:", failures.len())?;
50            for failure in failures {
51                write!(f, "\n  {failure}")?;
52            }
53            Ok(())
54        }
55    }
56}
57
58impl core::fmt::Display for MultiDeviceStepError {
59    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60        match self {
61            Self::Dataset(err) => write!(f, "dataset error during training step: {err}"),
62            Self::Workers(failures) => fmt_worker_failures(failures, f),
63        }
64    }
65}
66
67impl core::error::Error for MultiDeviceStepError {}
68
69/// Multi devices train step.
70pub struct MultiDevicesTrainStep<M: LearnerModel> {
71    workers: Vec<Worker<M>>,
72    receiver: Receiver<WorkerMessage<TrainingModelOutput<M>>>,
73}
74
75struct Message<M, TI> {
76    item: TI,
77    model: M,
78}
79
80enum WorkerMessage<TO> {
81    Output(MultiTrainOutput<TO>),
82    Error(usize, String),
83}
84
85fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
86    if let Some(s) = payload.downcast_ref::<&str>() {
87        s.to_string()
88    } else if let Some(s) = payload.downcast_ref::<String>() {
89        s.clone()
90    } else {
91        "unknown panic in training worker".to_string()
92    }
93}
94
95struct Worker<M: LearnerModel> {
96    // Not that complex. Extracting into another type would only make it more confusing.
97    // #[allow(clippy::type_complexity)]
98    sender_input: Sender<Message<M, TrainingModelInput<M>>>,
99    device: Device,
100    device_id: usize,
101}
102
103impl<M: LearnerModel> Worker<M> {
104    fn register(&self, item: TrainingModelInput<M>, model: &M) {
105        let message = Message {
106            item,
107            model: model.clone(),
108        };
109        self.sender_input.send(message).unwrap();
110    }
111
112    // Not that complex. Extracting into another type would only make it more confusing.
113    // #[allow(clippy::type_complexity)]
114    fn start(
115        &self,
116        sender_output: Sender<WorkerMessage<TrainingModelOutput<M>>>,
117        receiver_input: Receiver<Message<M, TrainingModelInput<M>>>,
118    ) {
119        let device = self.device.clone();
120        let device_id = self.device_id;
121
122        spawn(move || {
123            loop {
124                match receiver_input.recv() {
125                    Ok(item) => {
126                        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
127                            let model = item.model.fork(&device);
128                            TrainStep::step(&model, item.item)
129                        }));
130
131                        let message = match result {
132                            Ok(output) => {
133                                WorkerMessage::Output(MultiTrainOutput { output, device_id })
134                            }
135                            Err(payload) => {
136                                WorkerMessage::Error(device_id, panic_message(payload.as_ref()))
137                            }
138                        };
139
140                        if sender_output.send(message).is_err() {
141                            break;
142                        }
143                    }
144                    Err(_err) => {
145                        log::info!("Closing thread on device {device:?}");
146                        break;
147                    }
148                }
149            }
150        });
151    }
152}
153
154/// Multiple output items.
155pub struct MultiTrainOutput<TO> {
156    /// The training output.
157    pub output: TrainOutput<TO>,
158    /// The worker/device on which the computing happened.
159    pub(crate) device_id: usize,
160}
161
162impl<M: LearnerModel> MultiDevicesTrainStep<M> {
163    /// Create a new multi devices train step.
164    ///
165    /// # Arguments
166    ///
167    /// * `devices` - Devices.
168    ///
169    /// # Returns
170    ///
171    /// MultiDevicesTrainStep instance.
172    pub fn new(devices: &[Device]) -> Self {
173        let (sender_output, receiver_output) = std::sync::mpsc::channel();
174        let workers = devices
175            .iter()
176            .enumerate()
177            .map(|(device_id, device)| {
178                let (sender_input, receiver_input) = std::sync::mpsc::channel();
179                let worker = Worker {
180                    sender_input,
181                    device: device.clone(),
182                    device_id,
183                };
184
185                worker.start(sender_output.clone(), receiver_input);
186                worker
187            })
188            .collect();
189
190        Self {
191            workers,
192            receiver: receiver_output,
193        }
194    }
195
196    /// Collect outputs from workers for one step.
197    ///
198    /// # Arguments
199    ///
200    /// * `model` - Model.
201    /// * `dataloaders` - The data loader for each worker.
202    ///
203    /// # Returns
204    ///
205    /// Outputs.
206    pub fn step<'a>(
207        &self,
208        dataloaders: &mut [Box<dyn DataLoaderIterator<TrainingModelInput<M>> + 'a>],
209        model: &M,
210    ) -> Result<StepOutput<TrainingModelOutput<M>>, MultiDeviceStepError> {
211        let mut num_send = 0;
212
213        let mut items_total = 0;
214        let mut items_processed = 0;
215        let unit: Option<String> = Some("items".to_string());
216
217        for (i, worker) in self.workers.iter().enumerate() {
218            let dataloader = &mut dataloaders[i];
219            match dataloader.next() {
220                Some(Ok(item)) => {
221                    worker.register(item, model);
222                    num_send += 1;
223                    let progress = dataloader.progress();
224                    items_total += progress.items_total;
225                    items_processed += progress.items_processed;
226                }
227                Some(Err(err)) => return Err(MultiDeviceStepError::Dataset(err)),
228                None => {}
229            }
230        }
231
232        let mut outputs = Vec::with_capacity(num_send);
233        let mut failures = Vec::new();
234
235        // Every worker answers before this returns, even after one failed: an answer left in
236        // the channel would be taken for the next step's.
237        for _ in 0..num_send {
238            match self.receiver.recv().unwrap() {
239                WorkerMessage::Output(output) => outputs.push(output),
240                WorkerMessage::Error(device_id, message) => {
241                    failures.push(WorkerFailure { device_id, message })
242                }
243            }
244        }
245
246        if !failures.is_empty() {
247            return Err(MultiDeviceStepError::Workers(failures));
248        }
249        Ok((outputs, Progress::new(items_processed, items_total, unit)))
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    fn failure(device_id: usize, message: &str) -> WorkerFailure {
258        WorkerFailure {
259            device_id,
260            message: message.to_string(),
261        }
262    }
263
264    #[test]
265    fn every_failed_worker_is_reported() {
266        let error = MultiDeviceStepError::Workers(vec![
267            failure(0, "out of memory"),
268            failure(2, "illegal address"),
269        ]);
270
271        assert_eq!(
272            error.to_string(),
273            "2 training workers failed:\n  \
274             training worker on device 0 failed: out of memory\n  \
275             training worker on device 2 failed: illegal address"
276        );
277    }
278}