Skip to main content

burn_train/metric/processor/
async_wrapper.rs

1use crate::metric::processor::{EvaluatorEvent, EventProcessorError, EventProcessorEvaluation};
2
3use super::EventProcessorTraining;
4use async_channel::{Receiver, Sender};
5use std::thread::JoinHandle;
6
7/// Event processor for the training process.
8///
9/// Events are processed on a separate thread. An error an event causes is only known after
10/// the call that submitted it has returned and is reported by a later call:
11/// [`process_train`](EventProcessorTraining::process_train),
12/// [`process_valid`](EventProcessorTraining::process_valid) or
13/// [`flush`](EventProcessorTraining::flush), whichever comes first. That call returns every
14/// failure since the last one it reported, as one [`EventProcessorError`].
15pub struct AsyncProcessorTraining<ET, EV> {
16    sender: Sender<Message<ET, EV>>,
17    worker: Worker,
18}
19
20/// Event processor for the model evaluation.
21///
22/// Like [`AsyncProcessorTraining`], an event's error is reported by a later call.
23pub struct AsyncProcessorEvaluation<P: EventProcessorEvaluation> {
24    sender: Sender<EvalMessage<P>>,
25    worker: Worker,
26}
27
28/// The thread a processor runs on, kept so its failure can be reported.
29///
30/// A processor reads tensors (metrics) where a failed computation surfaces. Those failures come
31/// back as errors through `error_rec`.
32struct Worker {
33    handle: Option<JoinHandle<()>>,
34    error_rec: Receiver<EventProcessorError>,
35}
36
37impl Worker {
38    fn new(handle: JoinHandle<()>, error_rec: Receiver<EventProcessorError>) -> Self {
39        Self {
40            handle: Some(handle),
41            error_rec,
42        }
43    }
44
45    /// Every error the worker reported since the last call, if any, merged in order.
46    fn check_errors(&self) -> Result<(), EventProcessorError> {
47        let Ok(mut reported) = self.error_rec.try_recv() else {
48            return Ok(());
49        };
50        while let Ok(later) = self.error_rec.try_recv() {
51            reported.merge(later);
52        }
53        Err(reported)
54    }
55
56    /// The channel to the worker is closed: re-raise whatever stopped it.
57    fn died(&mut self) -> ! {
58        match self.handle.take().map(JoinHandle::join) {
59            Some(Err(payload)) => std::panic::resume_unwind(payload),
60            _ => panic!("the event processor worker stopped without reporting an error"),
61        }
62    }
63}
64
65struct WorkerTraining<ET, EV, P: EventProcessorTraining<ET, EV>> {
66    processor: P,
67    rec: Receiver<Message<ET, EV>>,
68    error_sender: Sender<EventProcessorError>,
69}
70
71struct WorkerEvaluation<P: EventProcessorEvaluation> {
72    processor: P,
73    rec: Receiver<EvalMessage<P>>,
74    error_sender: Sender<EventProcessorError>,
75}
76
77fn report(error_sender: &Sender<EventProcessorError>, result: Result<(), EventProcessorError>) {
78    if let Err(err) = result {
79        let _ = error_sender.try_send(err);
80    }
81}
82
83impl<ET: Send + 'static, EV: Send + 'static, P: EventProcessorTraining<ET, EV> + 'static>
84    WorkerTraining<ET, EV, P>
85{
86    pub fn start(
87        processor: P,
88        rec: Receiver<Message<ET, EV>>,
89        error_sender: Sender<EventProcessorError>,
90    ) -> JoinHandle<()> {
91        let mut worker = Self {
92            processor,
93            rec,
94            error_sender,
95        };
96        std::thread::Builder::new()
97            .name("train-worker".into())
98            .spawn(move || {
99                while let Ok(msg) = worker.rec.recv_blocking() {
100                    match msg {
101                        Message::Train(event) => {
102                            report(&worker.error_sender, worker.processor.process_train(event))
103                        }
104                        Message::Valid(event) => {
105                            report(&worker.error_sender, worker.processor.process_valid(event))
106                        }
107                        Message::Flush(callback) => {
108                            report(&worker.error_sender, worker.processor.flush());
109                            callback.send_blocking(()).unwrap();
110                        }
111                        Message::Renderer(callback) => {
112                            callback.send_blocking(worker.processor.renderer()).unwrap();
113                            return;
114                        }
115                    }
116                }
117            })
118            .unwrap()
119    }
120}
121impl<P: EventProcessorEvaluation + 'static> WorkerEvaluation<P> {
122    pub fn start(
123        processor: P,
124        rec: Receiver<EvalMessage<P>>,
125        error_sender: Sender<EventProcessorError>,
126    ) -> JoinHandle<()> {
127        let mut worker = Self {
128            processor,
129            rec,
130            error_sender,
131        };
132
133        std::thread::Builder::new()
134            .name("evel-worker".into())
135            .spawn(move || {
136                while let Ok(event) = worker.rec.recv_blocking() {
137                    match event {
138                        EvalMessage::Test(event) => {
139                            report(&worker.error_sender, worker.processor.process_test(event))
140                        }
141                        EvalMessage::Flush(callback) => {
142                            report(&worker.error_sender, worker.processor.flush());
143                            callback.send_blocking(()).unwrap();
144                        }
145                        EvalMessage::Renderer(sender) => {
146                            sender.send_blocking(worker.processor.renderer()).unwrap();
147                            return;
148                        }
149                    }
150                }
151            })
152            .unwrap()
153    }
154}
155
156impl<ET: Send + 'static, EV: Send + 'static> AsyncProcessorTraining<ET, EV> {
157    /// Create an event processor for training.
158    pub fn new<P: EventProcessorTraining<ET, EV> + 'static>(processor: P) -> Self {
159        let (sender, rec) = async_channel::bounded(1);
160        let (error_sender, error_rec) = async_channel::unbounded();
161
162        let worker = Worker::new(
163            WorkerTraining::start(processor, rec, error_sender),
164            error_rec,
165        );
166
167        Self { sender, worker }
168    }
169}
170
171impl<P: EventProcessorEvaluation + 'static> AsyncProcessorEvaluation<P> {
172    /// Create an event processor for model evaluation.
173    pub fn new(processor: P) -> Self {
174        let (sender, rec) = async_channel::bounded(1);
175        let (error_sender, error_rec) = async_channel::unbounded();
176
177        let worker = Worker::new(
178            WorkerEvaluation::start(processor, rec, error_sender),
179            error_rec,
180        );
181
182        Self { sender, worker }
183    }
184}
185
186enum Message<EventTrain, EventValid> {
187    Train(EventTrain),
188    Valid(EventValid),
189    Flush(Sender<()>),
190    Renderer(Sender<Box<dyn crate::renderer::MetricsRenderer>>),
191}
192
193enum EvalMessage<P: EventProcessorEvaluation> {
194    Test(EvaluatorEvent<P::ItemTest>),
195    Flush(Sender<()>),
196    Renderer(Sender<Box<dyn crate::renderer::MetricsRenderer>>),
197}
198
199impl<ET: Send, EV: Send> EventProcessorTraining<ET, EV> for AsyncProcessorTraining<ET, EV> {
200    fn process_train(&mut self, event: ET) -> Result<(), EventProcessorError> {
201        if self.sender.send_blocking(Message::Train(event)).is_err() {
202            self.worker.died();
203        }
204        self.worker.check_errors()
205    }
206
207    fn process_valid(&mut self, event: EV) -> Result<(), EventProcessorError> {
208        if self.sender.send_blocking(Message::Valid(event)).is_err() {
209            self.worker.died();
210        }
211        self.worker.check_errors()
212    }
213
214    fn flush(&mut self) -> Result<(), EventProcessorError> {
215        let (sender, receiver) = async_channel::bounded(1);
216        if self.sender.send_blocking(Message::Flush(sender)).is_err()
217            || receiver.recv_blocking().is_err()
218        {
219            self.worker.died();
220        }
221        self.worker.check_errors()
222    }
223
224    fn renderer(mut self) -> Box<dyn crate::renderer::MetricsRenderer> {
225        let (sender, rec) = async_channel::bounded(1);
226        if self
227            .sender
228            .send_blocking(Message::Renderer(sender))
229            .is_err()
230        {
231            self.worker.died();
232        }
233
234        match rec.recv_blocking() {
235            Ok(value) => value,
236            Err(_) => self.worker.died(),
237        }
238    }
239}
240
241impl<P: EventProcessorEvaluation> EventProcessorEvaluation for AsyncProcessorEvaluation<P> {
242    type ItemTest = P::ItemTest;
243
244    fn process_test(
245        &mut self,
246        event: EvaluatorEvent<Self::ItemTest>,
247    ) -> Result<(), EventProcessorError> {
248        if self.sender.send_blocking(EvalMessage::Test(event)).is_err() {
249            self.worker.died();
250        }
251        self.worker.check_errors()
252    }
253
254    fn flush(&mut self) -> Result<(), EventProcessorError> {
255        let (sender, receiver) = async_channel::bounded(1);
256        if self
257            .sender
258            .send_blocking(EvalMessage::Flush(sender))
259            .is_err()
260            || receiver.recv_blocking().is_err()
261        {
262            self.worker.died();
263        }
264        self.worker.check_errors()
265    }
266
267    fn renderer(mut self) -> Box<dyn crate::renderer::MetricsRenderer> {
268        let (sender, rec) = async_channel::bounded(1);
269        if self
270            .sender
271            .send_blocking(EvalMessage::Renderer(sender))
272            .is_err()
273        {
274            self.worker.died();
275        }
276
277        match rec.recv_blocking() {
278            Ok(value) => value,
279            Err(_) => self.worker.died(),
280        }
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::metric::{
288        processor::{EventProcessorFailure, ItemLazy, MetricError},
289        store::Split,
290    };
291    use crate::renderer::{MetricsRenderer, cli::CliMetricsRenderer};
292    use burn_core::tensor::TensorReadError;
293    use burn_std::ExecutionError;
294    use std::sync::{
295        Arc,
296        atomic::{AtomicUsize, Ordering},
297    };
298
299    struct TestProcessor {
300        processed: Arc<AtomicUsize>,
301        processed_on_flush: Arc<AtomicUsize>,
302    }
303
304    impl EventProcessorTraining<usize, usize> for TestProcessor {
305        fn process_train(&mut self, event: usize) -> Result<(), EventProcessorError> {
306            self.processed.fetch_add(event, Ordering::SeqCst);
307            Ok(())
308        }
309
310        fn process_valid(&mut self, event: usize) -> Result<(), EventProcessorError> {
311            self.processed.fetch_add(event, Ordering::SeqCst);
312            Ok(())
313        }
314
315        fn flush(&mut self) -> Result<(), EventProcessorError> {
316            self.processed_on_flush
317                .store(self.processed.load(Ordering::SeqCst), Ordering::SeqCst);
318            Ok(())
319        }
320
321        fn renderer(self) -> Box<dyn MetricsRenderer> {
322            Box::new(CliMetricsRenderer::new())
323        }
324    }
325
326    #[test]
327    fn flush_waits_for_queued_training_events() {
328        let processed = Arc::new(AtomicUsize::new(0));
329        let processed_on_flush = Arc::new(AtomicUsize::new(0));
330        let mut processor = AsyncProcessorTraining::new(TestProcessor {
331            processed: processed.clone(),
332            processed_on_flush: processed_on_flush.clone(),
333        });
334
335        processor.process_train(2).unwrap();
336        processor.process_valid(3).unwrap();
337        processor.flush().unwrap();
338
339        assert_eq!(processed.load(Ordering::SeqCst), 5);
340        // The wrapped processor is flushed before the acknowledgement is sent back.
341        assert_eq!(processed_on_flush.load(Ordering::SeqCst), 5);
342    }
343
344    /// A processor whose metric cannot read events `0` and `1`, the way metrics fail when the
345    /// computation they read failed. Every other event is counted.
346    struct FailingProcessor {
347        processed: Arc<AtomicUsize>,
348    }
349
350    impl EventProcessorTraining<usize, usize> for FailingProcessor {
351        fn process_train(&mut self, event: usize) -> Result<(), EventProcessorError> {
352            if event < 2 {
353                return EventProcessorError::from_errors(vec![MetricError {
354                    metric: Arc::new(format!("event-{event}")),
355                    split: Split::Train,
356                    source: ExecutionError::device_poisoned("the loss could not be read").into(),
357                }]);
358            }
359            self.processed.fetch_add(event, Ordering::SeqCst);
360            Ok(())
361        }
362
363        fn process_valid(&mut self, _event: usize) -> Result<(), EventProcessorError> {
364            Ok(())
365        }
366
367        fn renderer(self) -> Box<dyn MetricsRenderer> {
368            Box::new(CliMetricsRenderer::new())
369        }
370    }
371
372    #[test]
373    fn every_event_error_reaches_the_caller_and_the_worker_keeps_going() {
374        let processed = Arc::new(AtomicUsize::new(0));
375        let mut processor = AsyncProcessorTraining::new(FailingProcessor {
376            processed: processed.clone(),
377        });
378
379        // Each call returns whatever failed since the last report, so the failures are split
380        // across these calls depending on how far the worker got; the flush gets the rest.
381        let reports = [
382            processor.process_train(0),
383            processor.process_train(1),
384            processor.flush(),
385        ];
386        let failures: Vec<MetricError> = reports
387            .into_iter()
388            .filter_map(Result::err)
389            .flat_map(EventProcessorError::into_failures)
390            .map(|failure| match failure {
391                EventProcessorFailure::Metric(error) => error,
392                other => panic!("expected a metric failure, got {other}"),
393            })
394            .collect();
395
396        let failed: Vec<&str> = failures.iter().map(|err| err.metric.as_str()).collect();
397        assert_eq!(failed, ["event-0", "event-1"], "every failure, in order");
398        assert!(failures.iter().all(|err| matches!(
399            &err.source,
400            TensorReadError::Execution(err) if err.is_device_poisoned()
401        )));
402
403        // Each failure was reported once, and the worker still processes what comes next.
404        processor.process_train(2).unwrap();
405        processor.flush().unwrap();
406        assert_eq!(processed.load(Ordering::SeqCst), 2);
407    }
408
409    /// An evaluation processor whose metric fails at the end of every test split.
410    struct FailingEvalProcessor;
411
412    struct NoItem;
413
414    impl ItemLazy for NoItem {
415        fn sync(self) -> Result<Self, ExecutionError> {
416            Ok(self)
417        }
418    }
419
420    impl EventProcessorEvaluation for FailingEvalProcessor {
421        type ItemTest = NoItem;
422
423        fn process_test(
424            &mut self,
425            event: EvaluatorEvent<NoItem>,
426        ) -> Result<(), EventProcessorError> {
427            match event {
428                EvaluatorEvent::EndTest => EventProcessorError::from_errors(vec![MetricError {
429                    metric: Arc::new("Accuracy".to_string()),
430                    split: Split::Test(None),
431                    source: ExecutionError::with_context("refused").into(),
432                }]),
433                _ => Ok(()),
434            }
435        }
436
437        fn renderer(self) -> Box<dyn MetricsRenderer> {
438            Box::new(CliMetricsRenderer::new())
439        }
440    }
441
442    #[test]
443    fn flush_reports_last_events_errors() {
444        let mut processor = AsyncProcessorEvaluation::new(FailingEvalProcessor);
445
446        // The last event's own call may return before the worker processed it: the flush
447        // is what guarantees its error is seen before the processor is consumed.
448        let reported = processor
449            .process_test(EvaluatorEvent::EndTest)
450            .err()
451            .or_else(|| processor.flush().err())
452            .expect("the last event's error must reach the caller");
453        match &reported.failures()[0] {
454            EventProcessorFailure::Metric(error) => assert_eq!(error.metric.as_str(), "Accuracy"),
455            other => panic!("expected a metric failure, got {other}"),
456        }
457
458        processor.flush().unwrap();
459    }
460
461    /// A processor that panics, the one failure that still stops the worker.
462    struct PanickingProcessor;
463
464    impl EventProcessorTraining<usize, usize> for PanickingProcessor {
465        fn process_train(&mut self, _event: usize) -> Result<(), EventProcessorError> {
466            panic!("the processor panicked");
467        }
468
469        fn process_valid(&mut self, _event: usize) -> Result<(), EventProcessorError> {
470            Ok(())
471        }
472
473        fn renderer(self) -> Box<dyn MetricsRenderer> {
474            Box::new(CliMetricsRenderer::new())
475        }
476    }
477
478    #[test]
479    fn a_worker_panic_reaches_the_caller_with_its_cause() {
480        let mut processor = AsyncProcessorTraining::new(PanickingProcessor);
481
482        let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
483            // The worker dies on the first event; the channel has room for
484            // one more, so the failure is noticed by the second send or the
485            // flush at the latest.
486            let _ = processor.process_train(1);
487            let _ = processor.process_train(2);
488            let _ = processor.flush();
489        }))
490        .expect_err("the worker's panic must reach the caller");
491
492        let message = payload
493            .downcast_ref::<&str>()
494            .map(|message| message.to_string())
495            .or_else(|| payload.downcast_ref::<String>().cloned())
496            .unwrap_or_default();
497        assert!(
498            message.contains("the processor panicked"),
499            "the caller must see the worker's own panic, got: {message:?}"
500        );
501    }
502}