Skip to main content

burn_core/module/
base.rs

1use crate::module::{LoraConfig, ParamGroup};
2
3use super::{LoraMapper, Param, ParamId, QLoraMapper, Quantizer};
4use alloc::{
5    string::{String, ToString},
6    vec::Vec,
7};
8pub use burn_derive::Module;
9use burn_tensor::{Bool, Device, Int, Tensor};
10
11/// Type alias to `Vec<Device>` which supports `no_std` environments, but automatically using
12/// the `alloc` crate.
13pub type Devices = Vec<Device>;
14
15// At the moment, our plan is to continue experimenting with the macro internally and monitor its development.
16// We may consider making it public in the future.
17macro_rules! module {
18    (map=$module:ident, ops=$item:expr) => {{
19        struct Mapper;
20        impl ModuleMapper for Mapper {
21            fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
22                let (id, tensor, mapper) = param.consume();
23                let func = $item;
24                let tensor = func(tensor);
25                Param::from_mapped_value(id, tensor, mapper)
26            }
27        }
28        let mut mapper = Mapper;
29        $module.map(&mut mapper)
30    }};
31    (map=$module:ident, ops=$item:expr, group=$group:ident) => {{
32        struct Mapper {
33            pub path: Vec<String>,
34            pub group: ParamGroup,
35        }
36        impl ModuleMapper for Mapper {
37            fn enter_module(&mut self, name: &str, _container_type: &str) {
38                self.path.push(name.to_string());
39            }
40
41            fn exit_module(&mut self, _name: &str, _container_type: &str) {
42                self.path.pop();
43            }
44
45            fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
46                let (id, tensor, mapper) = param.consume();
47                let path = self.path.join(".");
48                if self.group.matches(&id, Some(&path)) {
49                    let func = $item;
50                    let tensor = func(tensor);
51                    return Param::from_mapped_value(id, tensor, mapper);
52                }
53                Param::from_mapped_value(id, tensor, mapper)
54            }
55        }
56        let mut mapper = Mapper {
57            path: alloc::vec![],
58            group: $group,
59        };
60        $module.map(&mut mapper)
61    }};
62    (visit_float=$module:ident, ops=$item:expr, state=$state_ty:ty, init=$init:expr) => {{
63        struct Visitor<'a> {
64            state: &'a mut $state_ty,
65        }
66        impl<'a> ModuleVisitor for Visitor<'a> {
67            fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
68                let func = $item;
69                func(&param.val(), &mut self.state)
70            }
71        }
72        #[allow(clippy::redundant_closure_call)]
73        let mut state = $init();
74        let mut visitor = Visitor { state: &mut state };
75        $module.visit(&mut visitor);
76        state
77    }};
78}
79
80/// Trait for all neural network modules.
81///
82/// Modules should be created using the [derive](burn_derive::Module) attribute.
83/// This will make your module trainable, savable and loadable via
84/// `state` and `load`.
85///
86/// # Example
87///
88/// ```rust, ignore
89/// // Not necessary when using the burn crate directly.
90/// use burn_core as burn;
91///
92/// use burn::{
93///     module::Module,
94///     nn::Linear,
95///     tensor::Tensor,
96/// };
97///
98/// #[derive(Module, Debug)]
99/// struct MyModule {
100///   my_param: Linear,
101///   my_other_field: usize,
102/// }
103/// ```
104pub trait Module: Clone + Send + core::fmt::Debug {
105    /// Return all the devices found in the underneath module tree added to the given vector
106    /// without duplicates.
107    fn collect_devices(&self, devices: Devices) -> Devices;
108
109    /// Return all the devices found in the underneath module tree without duplicates.
110    fn devices(&self) -> Devices {
111        self.collect_devices(Devices::new())
112    }
113
114    /// Fork the module and all of its sub-modules to the given device.
115    ///
116    /// # Notes
117    ///
118    /// This is similar to [to_device](Module::to_device), but it ensures the output module on the
119    /// new device will have its own autodiff graph.
120    fn fork(self, device: &Device) -> Self;
121
122    /// Move the module and all of its sub-modules to the given device.
123    ///
124    /// # Warnings
125    ///
126    /// The operation supports autodiff and it will be registered when activated. However, this may
127    /// not be what you want. The output model will be an intermediary model, meaning that you
128    /// can't optimize it with gradient descent. If you want to optimize the output network on the
129    /// target device, use [fork](Module::fork) instead.
130    fn to_device(self, device: &Device) -> Self;
131
132    /// Each tensor in the module tree will not require grad.
133    ///
134    /// # Warnings
135    ///
136    /// This should not be used for inference, use [valid](AutodiffModule::valid) when using
137    /// AD modules. This is mostly useful when performing partial finetuning, which is updating only
138    /// a small fraction of the parameters instead of finetuning all of them.
139    fn no_grad(self) -> Self {
140        module!(
141            map = self,
142            ops = |tensor: Tensor<D>| tensor.set_require_grad(false)
143        )
144    }
145
146    /// Set `require_grad` to `false` for every parameter in the given group, leaving the rest
147    /// of the module untouched.
148    ///
149    /// This is the group-scoped counterpart to [no_grad](Module::no_grad): where `no_grad` freezes
150    /// the whole module tree, `freeze_group` freezes only the parameters matched by `group`.  
151    ///
152    /// # Warnings
153    ///
154    /// Like [no_grad](Module::no_grad), this should not be used for inference; use
155    /// [valid](AutodiffModule::valid) with AD modules instead.
156    fn freeze_group(self, group: ParamGroup) -> Self {
157        module!(
158            map = self,
159            ops = |tensor: Tensor<D>| tensor.set_require_grad(false),
160            group = group
161        )
162    }
163
164    /// Set `require_grad` to `true` for every parameter in the given group, leaving the rest
165    /// of the module untouched.
166    ///
167    /// The inverse of [freeze_group](Module::freeze_group): it re-enables gradient tracking for the
168    /// parameters matched by `group`, e.g. to unfreeze a previously frozen module.
169    fn unfreeze_group(self, group: ParamGroup) -> Self {
170        module!(
171            map = self,
172            ops = |tensor: Tensor<D>| tensor.set_require_grad(true),
173            group = group
174        )
175    }
176
177    /// Move the module and all of its sub-modules to the autodiff backend.
178    ///
179    /// # Notes
180    ///
181    /// * Only plain modules (not already on an autodiff backend) can be moved.
182    /// * Calling `train()` on a module that is already on an autodiff backend
183    ///   will result in a type error, because the module's inner backend does not match.
184    fn train(self) -> Self
185    where
186        Self: AutodiffModule,
187    {
188        AutodiffModule::from_inner(self)
189    }
190
191    /// Get the number of parameters the module has, including all of its sub-modules.
192    fn num_params(&self) -> usize {
193        module!(
194            visit_float = self,
195            ops = |tensor: &Tensor<D>, state: &mut usize| {
196                *state += tensor.shape().num_elements();
197            },
198            state = usize,
199            init = || 0
200        )
201    }
202    /// Visit each tensor parameter in the module with a [visitor](ModuleVisitor).
203    fn visit<Visitor: ModuleVisitor>(&self, visitor: &mut Visitor);
204
205    /// Map each tensor parameter in the module with a [mapper](ModuleMapper).
206    fn map<Mapper: ModuleMapper>(self, mapper: &mut Mapper) -> Self;
207
208    /// Quantize the weights of the module.
209    fn quantize_weights(self, quantizer: &mut Quantizer) -> Self {
210        self.map(quantizer)
211    }
212
213    /// Quantize the weights of the given parameter group.
214    fn quantize_weights_group(self, quantizer: &mut Quantizer, group: ParamGroup) -> Self {
215        quantizer.set_param_group(group);
216        self.map(quantizer)
217    }
218
219    /// Attach LoRA adapters to the module's 2-D weights, freezing the base weights.
220    ///
221    /// The same module keeps working without any code changes; adapted weights now produce
222    /// `base + scale * (a @ b)`, and only the adapter factors are trainable.
223    fn apply_lora(self, config: LoraConfig) -> Self
224    where
225        Self: Sized,
226    {
227        let mut mapper = LoraMapper::new(config);
228        self.map(&mut mapper)
229    }
230
231    /// Apply QLoRA to the module: quantize the (frozen) base weights and attach trainable LoRA
232    /// adapters to 2-D weights.
233    fn apply_qlora(self, config: LoraConfig, quantizer: Quantizer) -> Self
234    where
235        Self: Sized,
236    {
237        let mut mapper = QLoraMapper::new(config, quantizer);
238        self.map(&mut mapper)
239    }
240
241    /// Collect this module's parameters into a [`ModuleRecord`](crate::store::ModuleRecord).
242    ///
243    /// The record can be saved to a burnpack file or byte buffer and applied back with
244    /// [`load_record`](Module::load_record).
245    fn into_record(self) -> crate::store::ModuleRecord
246    where
247        Self: Sized,
248    {
249        crate::store::ModuleRecord::from_module(self)
250    }
251
252    /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, returning the loaded
253    /// module.
254    ///
255    /// Honors the record's [`DTypePolicy`](crate::store::DTypePolicy), `validate`, and
256    /// `allow_partial` settings.
257    fn try_load_record(
258        self,
259        record: crate::store::ModuleRecord,
260    ) -> Result<Self, crate::store::RecordError>
261    where
262        Self: Sized,
263    {
264        record.apply(self)
265    }
266
267    /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, consuming and returning
268    /// it.
269    ///
270    /// Panics if validation fails; use [`try_load_record`](Module::try_load_record) for the
271    /// fallible variant.
272    fn load_record(self, record: crate::store::ModuleRecord) -> Self
273    where
274        Self: Sized,
275    {
276        self.try_load_record(record).expect("Failed to load record")
277    }
278
279    /// Save this module's parameters to a burnpack file on disk.
280    ///
281    /// Convenience for [`into_record`](Module::into_record) followed by
282    /// [`ModuleRecord::save`](crate::store::ModuleRecord::save). For non-default load behavior
283    /// (dtype policy, partial loading, validation), go through the record directly.
284    #[cfg(feature = "std")]
285    fn save_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<(), crate::store::RecordError>
286    where
287        Self: Sized,
288    {
289        self.into_record().save(path)
290    }
291
292    /// Load this module's parameters from a burnpack file on disk, returning the loaded module.
293    ///
294    /// Uses the default load behavior. Panics on I/O or validation errors; use
295    /// [`try_load_file`](Module::try_load_file) for the fallible variant, or go through
296    /// [`ModuleRecord`](crate::store::ModuleRecord) to configure dtype policy, partial loading or
297    /// validation.
298    #[cfg(feature = "std")]
299    fn load_file<P: AsRef<std::path::Path>>(self, path: P) -> Self
300    where
301        Self: Sized,
302    {
303        self.try_load_file(path)
304            .expect("Failed to load module from file")
305    }
306
307    /// Fallible variant of [`load_file`](Module::load_file).
308    ///
309    /// Reads the record from `path` with [`ModuleRecord::load`](crate::store::ModuleRecord::load)
310    /// and applies it through [`try_load_record`](Module::try_load_record).
311    #[cfg(feature = "std")]
312    fn try_load_file<P: AsRef<std::path::Path>>(
313        self,
314        path: P,
315    ) -> Result<Self, crate::store::RecordError>
316    where
317        Self: Sized,
318    {
319        let record = crate::store::ModuleRecord::load(path)?;
320        self.try_load_record(record)
321    }
322}
323
324/// Module visitor trait for traversing and inspecting module parameters.
325pub trait ModuleVisitor {
326    /// Visit a float parameter in the module.
327    ///
328    /// # Parameters
329    /// - `param`: The float parameter to visit
330    #[allow(unused_variables)]
331    fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {}
332
333    /// Visit an int parameter in the module.
334    ///
335    /// # Parameters
336    /// - `param`: The integer parameter to visit
337    #[allow(unused_variables)]
338    fn visit_int<const D: usize>(&mut self, param: &Param<Tensor<D, Int>>) {}
339
340    /// Visit a bool parameter in the module.
341    ///
342    /// # Parameters
343    /// - `param`: The boolean parameter to visit
344    #[allow(unused_variables)]
345    fn visit_bool<const D: usize>(&mut self, param: &Param<Tensor<D, Bool>>) {}
346
347    /// Called when entering a submodule.
348    ///
349    /// # Parameters
350    /// - `name`: The name of the submodule being entered
351    /// - `container_type`: The type of the container with format:
352    ///   - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
353    ///   - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
354    ///   - For Vec containers: "Vec" (name is the index)
355    ///   - For Tuple containers: "Tuple" (name is the index)
356    ///   - For Array containers: "Array" (name is the index)
357    ///
358    /// Note: Option containers do not call enter_module/exit_module to preserve
359    /// the field name in the path (e.g., "bias" instead of "bias.Some")
360    #[allow(unused_variables)]
361    fn enter_module(&mut self, name: &str, container_type: &str) {}
362
363    /// Called when exiting a submodule.
364    ///
365    /// # Parameters
366    /// - `name`: The name of the submodule being exited
367    /// - `container_type`: The type of the container with format:
368    ///   - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
369    ///   - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
370    ///   - For Vec containers: "Vec" (name is the index)
371    ///   - For Tuple containers: "Tuple" (name is the index)
372    ///   - For Array containers: "Array" (name is the index)
373    ///
374    /// Note: Option containers do not call enter_module/exit_module to preserve
375    /// the field name in the path (e.g., "bias" instead of "bias.Some")
376    #[allow(unused_variables)]
377    fn exit_module(&mut self, name: &str, container_type: &str) {}
378
379    /// Visit a float tensor with its full module path.
380    ///
381    /// # Parameters
382    /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
383    ///   Each element represents a module name in the hierarchy, with the final element
384    ///   being the parameter name. This allows efficient reuse of the path stack.
385    /// - `id`: The unique identifier of the parameter
386    /// - `tensor`: The float tensor to visit
387    #[allow(unused_variables)]
388    fn visit_float_with_path<const D: usize>(
389        &mut self,
390        path: &[String],
391        id: ParamId,
392        tensor: &Tensor<D>,
393    ) {
394    }
395
396    /// Visit an int tensor with its full module path.
397    ///
398    /// # Parameters
399    /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
400    ///   Each element represents a module name in the hierarchy, with the final element
401    ///   being the parameter name. This allows efficient reuse of the path stack.
402    /// - `id`: The unique identifier of the parameter
403    /// - `tensor`: The integer tensor to visit
404    #[allow(unused_variables)]
405    fn visit_int_with_path<const D: usize>(
406        &mut self,
407        path: &[String],
408        id: ParamId,
409        tensor: &Tensor<D, Int>,
410    ) {
411    }
412
413    /// Visit a bool tensor with its full module path.
414    ///
415    /// # Parameters
416    /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
417    ///   Each element represents a module name in the hierarchy, with the final element
418    ///   being the parameter name. This allows efficient reuse of the path stack.
419    /// - `id`: The unique identifier of the parameter
420    /// - `tensor`: The boolean tensor to visit
421    #[allow(unused_variables)]
422    fn visit_bool_with_path<const D: usize>(
423        &mut self,
424        path: &[String],
425        id: ParamId,
426        tensor: &Tensor<D, Bool>,
427    ) {
428    }
429}
430
431/// Module mapper trait for transforming module parameters.
432pub trait ModuleMapper {
433    /// Called when entering a submodule.
434    ///
435    /// # Parameters
436    /// - `name`: The name of the submodule being entered
437    /// - `container_type`: The type of the container with format:
438    ///   - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
439    ///   - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
440    ///   - For Vec containers: "Vec" (name is the index)
441    ///   - For Tuple containers: "Tuple" (name is the index)
442    ///   - For Array containers: "Array" (name is the index)
443    ///
444    /// Note: Option containers do not call enter_module/exit_module to preserve
445    /// the field name in the path (e.g., "bias" instead of "bias.Some")
446    #[allow(unused_variables)]
447    fn enter_module(&mut self, name: &str, container_type: &str) {}
448
449    /// Called when exiting a submodule.
450    ///
451    /// # Parameters
452    /// - `name`: The name of the submodule being exited
453    /// - `container_type`: The type of the container with format:
454    ///   - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
455    ///   - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
456    ///   - For Vec containers: "Vec" (name is the index)
457    ///   - For Tuple containers: "Tuple" (name is the index)
458    ///   - For Array containers: "Array" (name is the index)
459    ///
460    /// Note: Option containers do not call enter_module/exit_module to preserve
461    /// the field name in the path (e.g., "bias" instead of "bias.Some")
462    #[allow(unused_variables)]
463    fn exit_module(&mut self, name: &str, container_type: &str) {}
464
465    /// Map a float parameter in the module.
466    ///
467    /// # Parameters
468    /// - `param`: The float parameter to transform
469    ///
470    /// # Returns
471    /// The transformed parameter
472    #[allow(unused_variables)]
473    fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
474        let (id, tensor, mapper) = param.consume();
475        Param::from_mapped_value(id, tensor, mapper)
476    }
477
478    /// Map an int parameter in the module.
479    ///
480    /// # Parameters
481    /// - `param`: The integer parameter to transform
482    ///
483    /// # Returns
484    /// The transformed parameter
485    #[allow(unused_variables)]
486    fn map_int<const D: usize>(&mut self, param: Param<Tensor<D, Int>>) -> Param<Tensor<D, Int>> {
487        let (id, tensor, mapper) = param.consume();
488        Param::from_mapped_value(id, tensor, mapper)
489    }
490
491    /// Map a bool parameter in the module.
492    ///
493    /// # Parameters
494    /// - `param`: The boolean parameter to transform
495    ///
496    /// # Returns
497    /// The transformed parameter
498    #[allow(unused_variables)]
499    fn map_bool<const D: usize>(
500        &mut self,
501        param: Param<Tensor<D, Bool>>,
502    ) -> Param<Tensor<D, Bool>> {
503        let (id, tensor, mapper) = param.consume();
504        Param::from_mapped_value(id, tensor, mapper)
505    }
506}
507
508/// Module with auto-differentiation backend.
509pub trait AutodiffModule: Module + Send + core::fmt::Debug {
510    /// Returns the same module, but on the inner backend without auto-differentiation.
511    fn valid(&self) -> Self;
512
513    /// Wraps an inner module back into an auto-diff module.
514    fn from_inner(module: Self) -> Self;
515}
516
517#[cfg(all(test, feature = "autodiff"))]
518mod tests {
519    use super::*;
520
521    use crate::module::ParamGroup;
522    use crate::{test_device, test_utils::SimpleLinear};
523
524    #[test]
525    fn test_module_val_train_stateful() {
526        let device = test_device().autodiff();
527        let module = SimpleLinear::new(4, 4, &device);
528
529        assert!(module.weight.is_require_grad());
530        assert!(module.weight.require_grad);
531
532        let module = module.valid();
533        assert!(!module.weight.is_require_grad());
534        assert!(module.weight.require_grad); // stateful
535
536        // Without `HasAutodiffModule`, we would need to specify the module type as well, which would be annoying
537        // let module: SimpleLinear<TestAutodiffBackend> = module.train();
538        let module = module.train();
539        assert!(module.weight.is_require_grad());
540        assert!(module.weight.require_grad); // stateful
541
542        let module = module.no_grad();
543        assert!(!module.weight.is_require_grad());
544        assert!(!module.weight.require_grad); // stateful
545
546        let module = module.valid();
547        assert!(!module.weight.is_require_grad()); // always
548        assert!(!module.weight.require_grad); // stateful
549
550        let module = module.train();
551        assert!(!module.weight.is_require_grad());
552        assert!(!module.weight.require_grad); // stateful
553    }
554
555    #[test]
556    fn freeze_group_freezes_only_selected_params() {
557        let device = test_device().autodiff();
558        let module = SimpleLinear::new(4, 4, &device);
559
560        assert!(module.weight.is_require_grad());
561        assert!(module.bias.as_ref().unwrap().is_require_grad());
562
563        let module = module.freeze_group(ParamGroup::from_path("weight"));
564
565        assert!(!module.weight.is_require_grad());
566        assert!(!module.weight.require_grad);
567
568        let bias = module.bias.as_ref().unwrap();
569        assert!(bias.is_require_grad());
570        assert!(bias.require_grad);
571    }
572
573    #[test]
574    fn unfreeze_group_only_thaws_selected_params() {
575        let device = test_device().autodiff();
576        let module = SimpleLinear::new(4, 4, &device);
577
578        let module = module.no_grad();
579        assert!(!module.weight.is_require_grad());
580        assert!(!module.bias.as_ref().unwrap().is_require_grad());
581
582        let module = module.unfreeze_group(ParamGroup::from_path("weight"));
583
584        assert!(module.weight.is_require_grad());
585        assert!(module.weight.require_grad);
586        assert!(!module.bias.as_ref().unwrap().is_require_grad());
587        assert!(!module.bias.as_ref().unwrap().require_grad);
588    }
589}