Skip to main content

burn_train/learner/
base.rs

1use crate::checkpoint::{
2    AsyncCheckpointer, Checkpointer, CheckpointingAction, CheckpointingStrategy,
3};
4use crate::metric::store::EventStoreClient;
5use crate::{
6    CloneEarlyStoppingStrategy, LearnerModel, TrainOutput, TrainStep, TrainingError,
7    TrainingModelInput, TrainingModelOutput,
8};
9use burn_core::store::ModuleRecord;
10use burn_core::tensor::Device;
11use burn_optim::lr_scheduler::LrSchedulerRecord;
12use burn_optim::lr_scheduler::module_lr_scheduler::{ModuleLearningRate, ModuleLrScheduler};
13use burn_optim::{GradientsParams, ModuleOptimizer, MultiGradientsParams, OptimizerRecord};
14use std::marker::PhantomData;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, Mutex};
17
18/// Learner struct encapsulating all components necessary to train a Neural Network model.
19pub struct Learner<M: LearnerModel> {
20    pub(crate) model: M,
21    optim: ModuleOptimizer,
22    lr_scheduler: ModuleLrScheduler,
23    lr_module: ModuleLearningRate,
24}
25
26impl<M: LearnerModel> Clone for Learner<M> {
27    fn clone(&self) -> Self {
28        Self {
29            model: self.model.clone(),
30            optim: self.optim.clone(),
31            lr_scheduler: self.lr_scheduler.clone(),
32            lr_module: self.lr_module.clone(),
33        }
34    }
35}
36
37impl<M: LearnerModel> Learner<M> {
38    /// Create a learner.
39    pub fn new(
40        model: M,
41        optim: ModuleOptimizer,
42        lr_scheduler: impl Into<ModuleLrScheduler>,
43    ) -> Self {
44        Self {
45            model,
46            optim,
47            lr_scheduler: lr_scheduler.into(),
48            lr_module: 0.0.into(),
49        }
50    }
51}
52
53impl<M: LearnerModel> Learner<M> {
54    /// Fork the learner's model to the given device.
55    pub fn fork(&mut self, device: &Device) {
56        self.model = self.model().fork(device);
57    }
58
59    /// Returns the current model.
60    pub fn model(&self) -> M {
61        self.model.clone()
62    }
63
64    /// Returns the current learning rate.
65    pub fn lr_current(&self) -> ModuleLearningRate {
66        self.lr_module.clone()
67    }
68
69    /// Executes a step of the learning rate scheduler.
70    pub fn lr_step(&mut self) {
71        self.lr_module = self.lr_scheduler.step();
72    }
73
74    /// Runs a step of the model for training, which executes the forward and backward passes.
75    ///
76    /// # Arguments
77    ///
78    /// * `item` - The input for the model.
79    ///
80    /// # Returns
81    ///
82    /// The output containing the model output and the gradients.
83    pub fn train_step(&self, item: TrainingModelInput<M>) -> TrainOutput<TrainingModelOutput<M>> {
84        TrainStep::step(&self.model, item)
85    }
86
87    /// Optimize the current module with the provided gradients and learning rate.
88    ///
89    /// # Arguments
90    ///
91    /// * `optim`: Optimizer used for learning.
92    /// * `lr`: The learning rate used for this step.
93    /// * `grads`: The gradients of each parameter in the current model.
94    pub fn optimizer_step(&mut self, grads: GradientsParams) {
95        self.model = self
96            .model()
97            .optimize(&mut self.optim, self.lr_module.clone(), grads);
98    }
99
100    /// Optimize the current module with the provided gradients and learning rate.
101    ///
102    /// # Arguments
103    ///
104    /// * `optim`: Optimizer used for learning.
105    /// * `lr`: The learning rate used for this step.
106    /// * `grads`: Multiple gradients associated to each parameter in the current model.
107    pub fn optimizer_step_multi(&mut self, grads: MultiGradientsParams) {
108        self.model = self
109            .model()
110            .optimize_multi(&mut self.optim, self.lr_module.clone(), grads);
111    }
112
113    /// Load the module state from a [record](ModuleRecord).
114    pub fn load_model(&mut self, record: ModuleRecord) {
115        self.model = self.model.clone().load_record(record);
116    }
117
118    /// Load the state of the learner's optimizer from a [record](OptimizerRecord).
119    ///
120    /// No device is needed: the optimizer state is migrated to each parameter's device on the next
121    /// step (see [`ModuleOptimizer::load_record`](burn_optim::ModuleOptimizer::load_record)).
122    pub fn load_optim(&mut self, record: OptimizerRecord) {
123        self.optim = self.optim.clone().load_record(record);
124    }
125
126    /// Load the state of the learner's scheduler from a [record](LrSchedulerRecord).
127    pub fn load_scheduler(&mut self, record: LrSchedulerRecord) {
128        self.lr_scheduler = self.lr_scheduler.clone().load_record(record);
129    }
130}
131
132/// Used to create, delete, or load checkpoints of the training process.
133pub struct LearningCheckpointer<M: LearnerModel> {
134    model: AsyncCheckpointer<ModuleRecord>,
135    optim: AsyncCheckpointer<OptimizerRecord>,
136    lr_scheduler: AsyncCheckpointer<LrSchedulerRecord>,
137    strategy: Box<dyn CheckpointingStrategy>,
138    _phantom: PhantomData<M>,
139}
140
141impl<M: LearnerModel> LearningCheckpointer<M> {
142    /// Create a new learning checkpointer.
143    pub fn new(
144        model: AsyncCheckpointer<ModuleRecord>,
145        optim: AsyncCheckpointer<OptimizerRecord>,
146        lr_scheduler: AsyncCheckpointer<LrSchedulerRecord>,
147        strategy: Box<dyn CheckpointingStrategy>,
148    ) -> Self {
149        Self {
150            model,
151            optim,
152            lr_scheduler,
153            strategy,
154            _phantom: PhantomData,
155        }
156    }
157
158    /// Create checkpoint for the training process.
159    pub fn checkpoint(&mut self, learner: &Learner<M>, epoch: usize, store: &EventStoreClient) {
160        let actions = self.strategy.checkpointing(epoch, store);
161
162        for action in actions {
163            match action {
164                CheckpointingAction::Delete(epoch) => {
165                    self.model
166                        .delete(epoch)
167                        .expect("Can delete model checkpoint.");
168                    self.optim
169                        .delete(epoch)
170                        .expect("Can delete optimizer checkpoint.");
171                    self.lr_scheduler
172                        .delete(epoch)
173                        .expect("Can delete learning rate scheduler checkpoint.");
174                }
175                CheckpointingAction::Save => {
176                    self.model
177                        .save(epoch, learner.model.clone().into_record())
178                        .expect("Can save model checkpoint.");
179                    self.optim
180                        .save(epoch, learner.optim.to_record())
181                        .expect("Can save optimizer checkpoint.");
182                    self.lr_scheduler
183                        .save(epoch, learner.lr_scheduler.to_record())
184                        .expect("Can save learning rate scheduler checkpoint.");
185                }
186            }
187        }
188    }
189
190    /// Load a training checkpoint.
191    ///
192    /// No device is taken: checkpoints are device-free burnpack records (file-backed bytes). On
193    /// load, the model keeps the device of the learner's existing parameters, and the optimizer
194    /// state is migrated to each parameter's device on the next step. The training device is fixed
195    /// earlier, when the learner's model is created/forked.
196    pub fn load_checkpoint(&self, mut learner: Learner<M>, epoch: usize) -> Learner<M> {
197        let record = self
198            .model
199            .restore(epoch)
200            .expect("Can load model checkpoint.");
201        learner.load_model(record);
202
203        let record = self
204            .optim
205            .restore(epoch)
206            .expect("Can load optimizer checkpoint.");
207        learner.load_optim(record);
208
209        let record = self
210            .lr_scheduler
211            .restore(epoch)
212            .expect("Can load learning rate scheduler checkpoint.");
213        learner.load_scheduler(record);
214
215        learner
216    }
217}
218
219/// Cloneable reference to an early stopping strategy
220pub(crate) type EarlyStoppingStrategyRef = Box<dyn CloneEarlyStoppingStrategy>;
221
222/// A stop made through [`Interrupter::stop`], without an error.
223#[derive(Clone, Debug, PartialEq, Eq)]
224pub struct Interruption {
225    /// The reason given to [`Interrupter::stop`], if any.
226    pub reason: Option<String>,
227}
228
229#[derive(Clone, Default)]
230/// A handle that allows aborting the training/evaluation process early.
231///
232/// The process stops either on request, through [`stop`](Self::stop), or because of an error,
233/// recorded with [`fail`](Self::fail). An error takes precedence.
234pub struct Interrupter {
235    state: Arc<AtomicBool>,
236    message: Arc<Mutex<Option<String>>>,
237    error: Arc<Mutex<Option<Arc<TrainingError>>>>,
238}
239
240impl Interrupter {
241    /// Create a new instance.
242    pub fn new() -> Self {
243        Self::default()
244    }
245
246    /// Notify the learner that it should stop.
247    /// # Arguments
248    /// * `reason` - A string describing the reason the training was stopped.
249    pub fn stop(&self, reason: Option<&str>) {
250        self.state.store(true, Ordering::Relaxed);
251        reason.inspect(|r| {
252            let mut message = self.message.lock().unwrap();
253            *message = Some(String::from(*r));
254        });
255    }
256
257    /// Stop the process and keep `error` as the cause.
258    ///
259    /// The first call to this function logs the error and keeps it as the cause of the
260    /// interruption. Subsequent calls only log the errors as they are most likely consequences
261    /// of the first one.
262    pub fn fail(&self, error: impl Into<TrainingError>) {
263        let error = error.into();
264        log::error!("Stopped by an error: {error}");
265        let mut slot = self.error.lock().unwrap();
266        if slot.is_none() {
267            *slot = Some(Arc::new(error));
268        }
269        self.state.store(true, Ordering::Relaxed);
270    }
271
272    /// [`fail`](Self::fail) with `result`'s error if there is one.
273    pub fn fail_on_error<E: Into<TrainingError>>(&self, result: Result<(), E>) {
274        if let Err(error) = result {
275            self.fail(error);
276        }
277    }
278
279    /// Reset the interrupter, forgetting any stop and error.
280    pub fn reset(&self) {
281        self.state.store(false, Ordering::Relaxed);
282        self.error.lock().unwrap().take();
283    }
284
285    /// True if the process should stop: [`stop`](Self::stop) or [`fail`](Self::fail) was called.
286    pub fn should_stop(&self) -> bool {
287        self.state.load(Ordering::Relaxed)
288    }
289
290    /// The stop, if the process was stopped without an error.
291    pub fn interruption(&self) -> Option<Interruption> {
292        (self.should_stop() && self.error().is_none()).then(|| Interruption {
293            reason: self.get_message(),
294        })
295    }
296
297    /// The error the process stopped on, if [`fail`](Self::fail) was called.
298    pub fn error(&self) -> Option<Arc<TrainingError>> {
299        self.error.lock().unwrap().clone()
300    }
301
302    /// Get the message associated with the interrupt.
303    pub fn get_message(&self) -> Option<String> {
304        let message = self.message.lock().unwrap();
305        message.clone()
306    }
307}