Skip to main content

burn_core/module/
base.rs

1use crate::module::{Lora, ParamGroup, QLora};
2
3use super::{ApplyReparameterization, Flag, Param, ParamId, Quantizer, Reparameterizer};
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, state=$state:ident:$state_ty:ty) => {{
19        struct Mapper {
20            $state: $state_ty,
21        }
22        impl ModuleMapper for Mapper {
23            fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
24                let func = $item;
25                func(param, &self.$state)
26            }
27        }
28        let mut mapper = Mapper { $state };
29        $module.map(&mut mapper)
30    }};
31    (map=$module:ident, ops=$item:expr, training=$training:expr) => {{
32        struct Mapper;
33        impl ModuleMapper for Mapper {
34            fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
35                let func = $item;
36                func(param)
37            }
38
39            fn map_flag(&mut self, flag: Param<Flag>) -> Param<Flag> {
40                flag.with_value($training)
41            }
42        }
43        let mut mapper = Mapper;
44        $module.map(&mut mapper)
45    }};
46    (map=$module:ident, ops=$item:expr, group=$group:ident, state=$state:ident:$state_ty:ty) => {{
47        struct Mapper {
48            pub path: Vec<String>,
49            pub group: ParamGroup,
50            $state: $state_ty,
51        }
52        impl ModuleMapper for Mapper {
53            fn enter_module(&mut self, name: &str, _container_type: &str) {
54                self.path.push(name.to_string());
55            }
56
57            fn exit_module(&mut self, _name: &str, _container_type: &str) {
58                self.path.pop();
59            }
60
61            fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
62                let path = self.path.join(".");
63                if self.group.matches(&param.id, Some(&path)) {
64                    let func = $item;
65                    return func(param, &self.$state);
66                }
67                param
68            }
69        }
70        let mut mapper = Mapper {
71            path: alloc::vec![],
72            group: $group,
73            $state,
74        };
75        $module.map(&mut mapper)
76    }};
77    (map=$module:ident, ops=$item:expr, group=$group:ident, training=$training:expr) => {{
78        struct Mapper {
79            pub path: Vec<String>,
80            pub group: ParamGroup,
81        }
82        impl ModuleMapper for Mapper {
83            fn enter_module(&mut self, name: &str, _container_type: &str) {
84                self.path.push(name.to_string());
85            }
86
87            fn exit_module(&mut self, _name: &str, _container_type: &str) {
88                self.path.pop();
89            }
90
91            fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
92                let path = self.path.join(".");
93                if self.group.matches(&param.id, Some(&path)) {
94                    let func = $item;
95                    return func(param);
96                }
97                param
98            }
99
100            fn map_flag(&mut self, flag: Param<Flag>) -> Param<Flag> {
101                let path = self.path.join(".");
102                match self.group.matches(&flag.id, Some(&path)) {
103                    true => flag.with_value($training),
104                    false => flag,
105                }
106            }
107        }
108        let mut mapper = Mapper {
109            path: alloc::vec![],
110            group: $group,
111        };
112        $module.map(&mut mapper)
113    }};
114    (visit_float=$module:ident, ops=$item:expr, state=$state_ty:ty, init=$init:expr) => {{
115        struct Visitor<'a> {
116            state: &'a mut $state_ty,
117        }
118        impl<'a> ModuleVisitor for Visitor<'a> {
119            fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
120                let func = $item;
121                func(param, &mut self.state)
122            }
123        }
124        #[allow(clippy::redundant_closure_call)]
125        let mut state = $init();
126        let mut visitor = Visitor { state: &mut state };
127        $module.visit(&mut visitor);
128        state
129    }};
130}
131
132/// Trait for all neural network modules.
133///
134/// Modules should be created using the [derive](burn_derive::Module) attribute.
135/// This will make your module trainable, savable and loadable via
136/// [`into_record`](Module::into_record) and [`load_record`](Module::load_record).
137///
138/// # Example
139///
140/// ```rust, ignore
141/// // Not necessary when using the burn crate directly.
142/// use burn_core as burn;
143///
144/// use burn::{
145///     module::Module,
146///     nn::Linear,
147///     tensor::Tensor,
148/// };
149///
150/// #[derive(Module, Debug)]
151/// struct MyModule {
152///   my_param: Linear,
153///   my_other_field: usize,
154/// }
155/// ```
156/// Training and validation use the same module type. This trait provides state transitions,
157/// not a guarantee that a value currently has autodiff enabled. Inspect individual parameter
158/// tensors with [`Tensor::is_autodiff`] and [`Tensor::is_require_grad`]; contexts can be mixed.
159#[diagnostic::on_unimplemented(
160    note = "If `{Self}` is used as configuration or runtime state in a `#[derive(Module)]` type, mark that field with `#[module(skip)]`."
161)]
162pub trait Module: Clone + Send + core::fmt::Debug {
163    /// Return all the devices found in the underneath module tree added to the given vector
164    /// without duplicates.
165    ///
166    /// Device collection should not initialize lazy parameters. Use [`Param::lazy_device`]
167    /// to inspect their devices without accessing tensor values.
168    fn collect_devices(&self, devices: Devices) -> Devices;
169
170    /// Return all the devices found in the underneath module tree without duplicates.
171    ///
172    /// Lazy tensor parameters report their initialization device without being initialized.
173    /// Device equality ignores autodiff and checkpointing settings, so this list is not a summary
174    /// of the module's training state.
175    fn devices(&self) -> Devices {
176        self.collect_devices(Devices::new())
177    }
178
179    /// Fork the module and all of its sub-modules to the given device.
180    ///
181    /// # Notes
182    ///
183    /// This is similar to [to_device](Module::to_device), but it ensures the output module on the
184    /// new device will have its own autodiff graph.
185    /// Both transfers preserve the source tensors' autodiff association and checkpointing
186    /// strategy. The destination's autodiff defaults do not enable training; use
187    /// [`train`](Module::train) first when starting from a validation module.
188    ///
189    /// A parameter not initialized yet initializes on the destination, unless a clone shares
190    /// it, in which case it initializes where it is and is then copied.
191    fn fork(self, device: &Device) -> Self;
192
193    /// Move the module and all of its sub-modules to the given device.
194    ///
195    /// # Warnings
196    ///
197    /// For initialized parameters, the operation supports autodiff and is registered when activated.
198    /// This may not be what you want. The output model will be an intermediary model, meaning that you
199    /// can't optimize it with gradient descent. If you want to optimize the output network on the
200    /// target device, use [fork](Module::fork) instead.
201    ///
202    /// A parameter not initialized yet initializes on the destination, unless a clone shares
203    /// it, in which case it initializes where it is and is then moved.
204    /// To train the module on the destination device, use [`fork`](Module::fork). This preserves
205    /// gradient retention whether the parameters are uninitialized, initialized, or shared with clones.
206    fn to_device(self, device: &Device) -> Self;
207
208    /// Set whether every floating-point tensor parameter in the module tree requires gradients.
209    ///
210    /// Module-owned control flags are left unchanged. Use [`freeze`](Module::freeze) to disable
211    /// both gradients and training behavior.
212    /// On a backend without autodiff, the enabled setting is preserved and takes effect when
213    /// [`train`](Module::train) transitions the module back to training.
214    /// Setting this to `true` overrides selective gradient configurations such as the frozen dense
215    /// base weights established by [`apply_lora`](Module::apply_lora).
216    fn set_require_grad(self, require_grad: bool) -> Self {
217        module!(
218            map = self,
219            ops = |param: Param<Tensor<D>>, require_grad: &bool| param
220                .set_require_grad(*require_grad),
221            state = require_grad: bool
222        )
223    }
224
225    /// Set whether matched floating-point tensor parameters require gradients.
226    ///
227    /// Only floating-point tensor parameters within `group` are affected. Other parameter values
228    /// matched by the group, including module-owned control flags, are ignored; unmatched tensor
229    /// parameters are also left unchanged. This means a group created with
230    /// [`ParamGroup::ids_from_module`] can select an entire subtree while this method changes only
231    /// its tensor gradient state.
232    /// On a backend without autodiff, the enabled setting is preserved and takes effect through
233    /// [`train`](Module::train).
234    ///
235    /// Use [`freeze_group`](Module::freeze_group) or
236    /// [`unfreeze_group`](Module::unfreeze_group) when matched training flags should be changed
237    /// together with tensor gradients.
238    fn set_require_grad_group(self, group: ParamGroup, require_grad: bool) -> Self {
239        module!(
240            map = self,
241            ops = |param: Param<Tensor<D>>, require_grad: &bool| param
242                .set_require_grad(*require_grad),
243            group = group,
244            state = require_grad: bool
245        )
246    }
247
248    /// Disable gradients for every floating-point tensor parameter in the module tree.
249    ///
250    /// This is equivalent to `set_require_grad(false)`. Module-owned control flags are left
251    /// unchanged, so dropout, noise, randomized activations and batch-normalization running
252    /// statistics retain their current behavior. Use [`freeze`](Module::freeze) to disable both
253    /// gradients and training behavior.
254    ///
255    /// # Gradient and training state
256    ///
257    /// This persistently disables gradient tracking, including on a backend without autodiff. A
258    /// later [`train`](Module::train) keeps gradients disabled; it does not undo `no_grad`.
259    ///
260    /// # Warnings
261    ///
262    /// Use [valid](Module::valid) for inference. `no_grad` is useful for partial fine-tuning
263    /// when tensor gradients should be disabled
264    /// while layer training behavior remains active. For example, matched batch-normalization
265    /// layers continue updating their running statistics, and dropout remains enabled. Use
266    /// [`freeze`](Module::freeze) when that module-owned training behavior should also stop.
267    fn no_grad(self) -> Self {
268        self.set_require_grad(false)
269    }
270
271    /// Freeze the whole module tree.
272    ///
273    /// Every floating-point tensor parameter stops requiring gradients, and every module-owned
274    /// training flag is disabled. This applies both to parameterless stochastic layers such as
275    /// dropout, noise and randomized activations, and to stateful layers such as batch
276    /// normalization.
277    ///
278    /// # Gradient and training state
279    ///
280    /// Gradient tracking and training flags are persistently disabled. On a backend without
281    /// autodiff, flags are disabled immediately, and [`train`](Module::train) keeps tensors
282    /// untracked rather than undoing the freeze.
283    ///
284    /// # Warnings
285    ///
286    /// Use [`valid`](Module::valid) for inference. `freeze` is intended for partial finetuning
287    /// where a module remains on the training
288    /// device but should not participate in training.
289    fn freeze(self) -> Self {
290        module!(
291            map = self,
292            ops = |param: Param<Tensor<D>>| param.set_require_grad(false),
293            training = false
294        )
295    }
296
297    /// Unfreeze the whole module tree.
298    ///
299    /// Every floating-point tensor parameter is configured to require gradients, and every
300    /// module-owned training flag is set to enabled rather than restored to a prior value. This
301    /// overwrites selective gradient configurations, including frozen dense base weights
302    /// established by [`apply_lora`](Module::apply_lora).
303    ///
304    /// # Gradient and training state
305    ///
306    /// On a backend without autodiff, training flags are enabled immediately while gradient
307    /// tracking takes effect when [`train`](Module::train) transitions the module back to the
308    /// autodiff backend.
309    fn unfreeze(self) -> Self {
310        module!(
311            map = self,
312            ops = |param: Param<Tensor<D>>| param.set_require_grad(true),
313            training = true
314        )
315    }
316
317    /// Freeze every module-owned value in the given group, leaving the rest of the module
318    /// untouched.
319    ///
320    /// This is the group-scoped counterpart to [`freeze`](Module::freeze): where `freeze` freezes
321    /// the whole module tree, `freeze_group` clears gradient requirements on matched tensor
322    /// parameters and disables matched training flags.
323    ///
324    /// # Gradient and training state
325    ///
326    /// Gradient tracking and training flags are persistently disabled for matched values. On a
327    /// backend without autodiff, matched flags are disabled immediately, and
328    /// [`train`](Module::train) keeps matched tensors untracked rather than undoing the group
329    /// freeze.
330    ///
331    /// # Warnings
332    ///
333    /// Like [`freeze`](Module::freeze), this should not be used for inference; use
334    /// [valid](Module::valid) instead.
335    fn freeze_group(self, group: ParamGroup) -> Self {
336        module!(
337            map = self,
338            ops = |param: Param<Tensor<D>>| param.set_require_grad(false),
339            group = group,
340            training = false
341        )
342    }
343
344    /// Unfreeze every module-owned value in the given group, leaving the rest of the module
345    /// untouched.
346    ///
347    /// The inverse of [freeze_group](Module::freeze_group): matched tensor parameters are configured
348    /// to require gradients, and matched training flags are set to enabled rather than restored to
349    /// prior values.
350    ///
351    /// # Gradient and training state
352    ///
353    /// On a backend without autodiff, matched training flags are enabled immediately while gradient
354    /// tracking for matched tensors takes effect when [`train`](Module::train) transitions the
355    /// module back to the autodiff backend.
356    fn unfreeze_group(self, group: ParamGroup) -> Self {
357        module!(
358            map = self,
359            ops = |param: Param<Tensor<D>>| param.set_require_grad(true),
360            group = group,
361            training = true
362        )
363    }
364
365    /// Enables autodiff and restores configured training state throughout the module.
366    ///
367    /// # Gradient and training state
368    ///
369    /// This is the supported transition back to training after [`valid`](Module::valid).
370    /// It applies the latest gradient-tracking settings and training-flag values. Mappings, device
371    /// moves, forks and explicit state changes on a validation module preserve or update what this
372    /// method applies.
373    ///
374    /// Tensor-bearing modules require the `autodiff` feature to enable autodiff. This does not
375    /// undo explicit freezing or restore tensor checkpointing strategies discarded by
376    /// [`valid`](Module::valid).
377    fn train(self) -> Self;
378
379    /// Returns a validation snapshot without autodiff or active training flags.
380    ///
381    /// # Gradient and training state
382    ///
383    /// Tensor gradient requirements and module-owned training flags are disabled in the returned
384    /// value while their configured state is retained internally. Use [`Module::train`] to apply
385    /// that state. Mappings, device moves and forks preserve both the disabled effective validation
386    /// state and what `train` will restore. Calling `valid` when the module's tensors are already on
387    /// a plain device still disables its training flags, so the operation is idempotent but not
388    /// necessarily a structural no-op.
389    ///
390    /// Parameter reparameterizations and their stored bases are preserved, including quantized
391    /// bases. Their nested modules are also put into validation mode. Use
392    /// [`materialize`](Module::materialize) explicitly to fold reparameterizations into weights.
393    /// Tensor checkpointing strategies are removed with autodiff and are not restored by
394    /// [`Module::train`]. Keep the original module when continuing training after validation.
395    fn valid(&self) -> Self;
396
397    /// Fold parameter reparameterizations into their effective values and remove them.
398    ///
399    /// Each reparameterized parameter is replaced by the value computed from its stored base and
400    /// reparameterization state. Other parameters and module training flags are unchanged. The
401    /// resulting parameters retain their base parameter IDs, layout mappings and configured
402    /// trainability, but are detached from the computation that produced them.
403    /// Parameters without reparameterizations remain lazy; this does not initialize them.
404    ///
405    /// This consumes the module and cannot be undone on the returned value. Clone the module first
406    /// to retain its reparameterizations. To prepare merged inference weights, use
407    /// `model.valid().materialize()`; materialization alone does not switch to validation mode.
408    ///
409    /// # Memory and precision
410    ///
411    /// LoRA adapters are merged into dense weights. For QLoRA this also replaces the packed base
412    /// with a dense effective weight, which can substantially increase memory use. Requantization
413    /// must be requested separately and may change the model's outputs.
414    fn materialize(self) -> Self;
415
416    /// Get the number of parameters the module has, including all of its sub-modules, without
417    /// initializing the ones not initialized yet.
418    fn num_params(&self) -> usize {
419        module!(
420            visit_float = self,
421            ops = |param: &Param<Tensor<D>>, state: &mut usize| {
422                *state += param.lazy_shape().num_elements();
423            },
424            state = usize,
425            init = || 0
426        )
427    }
428    /// Visit each parameter and module-owned control value with a [visitor](ModuleVisitor).
429    fn visit<Visitor: ModuleVisitor>(&self, visitor: &mut Visitor);
430
431    /// Map each parameter and module-owned control value with a [mapper](ModuleMapper).
432    fn map<Mapper: ModuleMapper>(self, mapper: &mut Mapper) -> Self;
433
434    /// Quantize the weights of the module.
435    fn quantize_weights(self, quantizer: &mut Quantizer) -> Self {
436        self.map(quantizer)
437    }
438
439    /// Quantize the weights of the given parameter group.
440    fn quantize_weights_group(self, quantizer: &mut Quantizer, group: ParamGroup) -> Self {
441        quantizer.set_param_group(group);
442        self.map(quantizer)
443    }
444
445    /// Attach reparameterizations using the given [`Reparameterizer`].
446    ///
447    /// Every floating-point parameter is passed to the reparameterizer along with its module path.
448    /// The reparameterizer prepares its structural base and optionally creates the state used by
449    /// [`Param::val`]. [`Lora`] is a built-in example.
450    ///
451    /// # Limitations
452    ///
453    /// Nested reparameterizations are not supported. This method should only be called on modules
454    /// that don't already contain reparameterized parameters.
455    fn apply_reparameterization<R>(self, reparameterizer: R) -> Self
456    where
457        Self: Sized,
458        R: Reparameterizer,
459    {
460        self.map(&mut ApplyReparameterization::new(reparameterizer))
461    }
462
463    /// Attach LoRA adapters to the module's 2-D weights, freezing all base tensor parameters.
464    ///
465    /// The same module keeps working without any code changes; adapted weights now produce
466    /// `base + scale * (a @ b)`, and only the adapter factors are trainable.
467    /// Module-owned control flags are preserved, so dropout and batch normalization keep their
468    /// current training behavior. Call [`freeze`](Module::freeze) before this method if the base
469    /// module's control behavior and running statistics should also be frozen.
470    fn apply_lora(self, lora: Lora) -> Self
471    where
472        Self: Sized,
473    {
474        self.apply_reparameterization(lora)
475    }
476
477    /// Apply QLoRA to the module: quantize the (frozen) base tensor parameters and attach trainable
478    /// LoRA adapters to 2-D weights.
479    ///
480    /// Dense effective weights can still materialize during training, even with fusion enabled.
481    /// See [`QLora`'s memory limitations](QLora#memory-limitations) before sizing a training run.
482    ///
483    /// Module-owned control flags are preserved. Call [`freeze`](Module::freeze) before this
484    /// method if the base module's control behavior and running statistics should also be frozen.
485    fn apply_qlora(self, qlora: QLora) -> Self
486    where
487        Self: Sized,
488    {
489        self.apply_reparameterization(qlora)
490    }
491
492    /// Collect this module's tensor parameters into a [`ModuleRecord`](crate::store::ModuleRecord).
493    ///
494    /// The record can be saved to a burnpack file or byte buffer and applied back with
495    /// [`load_record`](Module::load_record).
496    /// Module-owned control values such as [`Flag`] are runtime configuration and are not
497    /// recorded; loading preserves their state and identity from the destination module.
498    fn into_record(self) -> crate::store::ModuleRecord
499    where
500        Self: Sized,
501    {
502        crate::store::ModuleRecord::from_module(self, None)
503    }
504
505    /// Collect the tensor parameters `group` names into a [`ModuleRecord`](crate::store::ModuleRecord).
506    ///
507    /// The record of a part of the module rather than all of it — what a run that trained a
508    /// group writes when the rest of the module is the checkpoint it started from, and what
509    /// [`load_record`](Module::load_record) applies back over that checkpoint (with
510    /// [`allow_partial`](crate::store::ModuleRecord::allow_partial), since the record holds
511    /// nothing for the parameters outside the group).
512    ///
513    /// A parameter the group does not match is skipped before its data is read, so this never
514    /// materializes the rest of the module.
515    fn into_record_group(self, group: ParamGroup) -> crate::store::ModuleRecord
516    where
517        Self: Sized,
518    {
519        crate::store::ModuleRecord::from_module(self, Some(group))
520    }
521
522    /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, returning the loaded
523    /// module.
524    ///
525    /// Honors the record's [`DTypePolicy`](crate::store::DTypePolicy), `validate`, and
526    /// `allow_partial` settings.
527    fn try_load_record(
528        self,
529        record: crate::store::ModuleRecord,
530    ) -> Result<Self, crate::store::RecordError>
531    where
532        Self: Sized,
533    {
534        record.apply(self)
535    }
536
537    /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, consuming and returning
538    /// it.
539    ///
540    /// Panics if validation fails; use [`try_load_record`](Module::try_load_record) for the
541    /// fallible variant.
542    fn load_record(self, record: crate::store::ModuleRecord) -> Self
543    where
544        Self: Sized,
545    {
546        self.try_load_record(record).expect("Failed to load record")
547    }
548
549    /// Save this module's parameters to a burnpack file on disk.
550    ///
551    /// Convenience for [`into_record`](Module::into_record) followed by
552    /// [`ModuleRecord::save`](crate::store::ModuleRecord::save). For non-default load behavior
553    /// (dtype policy, partial loading, validation), go through the record directly.
554    #[cfg(feature = "std")]
555    fn save_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<(), crate::store::RecordError>
556    where
557        Self: Sized,
558    {
559        self.into_record().save(path)
560    }
561
562    /// Load this module's parameters from a burnpack file on disk, returning the loaded module.
563    ///
564    /// Uses the default load behavior. Panics on I/O or validation errors; use
565    /// [`try_load_file`](Module::try_load_file) for the fallible variant, or go through
566    /// [`ModuleRecord`](crate::store::ModuleRecord) to configure dtype policy, partial loading or
567    /// validation.
568    #[cfg(feature = "std")]
569    fn load_file<P: AsRef<std::path::Path>>(self, path: P) -> Self
570    where
571        Self: Sized,
572    {
573        self.try_load_file(path)
574            .expect("Failed to load module from file")
575    }
576
577    /// Fallible variant of [`load_file`](Module::load_file).
578    ///
579    /// Reads the record from `path` with [`ModuleRecord::load`](crate::store::ModuleRecord::load)
580    /// and applies it through [`try_load_record`](Module::try_load_record).
581    #[cfg(feature = "std")]
582    fn try_load_file<P: AsRef<std::path::Path>>(
583        self,
584        path: P,
585    ) -> Result<Self, crate::store::RecordError>
586    where
587        Self: Sized,
588    {
589        let record = crate::store::ModuleRecord::load(path)?;
590        self.try_load_record(record)
591    }
592}
593
594/// Module visitor trait for traversing and inspecting module parameters.
595pub trait ModuleVisitor {
596    /// Visit a float parameter in the module.
597    ///
598    /// # Parameters
599    /// - `param`: The float parameter to visit
600    #[allow(unused_variables)]
601    fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {}
602
603    /// Visit an int parameter in the module.
604    ///
605    /// # Parameters
606    /// - `param`: The integer parameter to visit
607    #[allow(unused_variables)]
608    fn visit_int<const D: usize>(&mut self, param: &Param<Tensor<D, Int>>) {}
609
610    /// Visit a bool parameter in the module.
611    ///
612    /// # Parameters
613    /// - `param`: The boolean parameter to visit
614    #[allow(unused_variables)]
615    fn visit_bool<const D: usize>(&mut self, param: &Param<Tensor<D, Bool>>) {}
616
617    /// Visit a [`Param<Flag>`] in the module.
618    ///
619    /// An identified boolean value owned by a module, so traversals can reason about control state
620    /// as well as tensor parameters.
621    ///
622    /// # Parameters
623    /// - `flag`: The flag to visit
624    #[allow(unused_variables)]
625    fn visit_flag(&mut self, flag: &Param<Flag>) {}
626
627    /// Called when entering a submodule.
628    ///
629    /// # Parameters
630    /// - `name`: The name of the submodule being entered
631    /// - `container_type`: The type of the container with format:
632    ///   - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
633    ///   - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
634    ///   - For Vec containers: "Vec" (name is the index)
635    ///   - For Tuple containers: "Tuple" (name is the index)
636    ///   - For Array containers: "Array" (name is the index)
637    ///
638    /// Note: Option containers do not call enter_module/exit_module to preserve
639    /// the field name in the path (e.g., "bias" instead of "bias.Some")
640    #[allow(unused_variables)]
641    fn enter_module(&mut self, name: &str, container_type: &str) {}
642
643    /// Called when exiting a submodule.
644    ///
645    /// # Parameters
646    /// - `name`: The name of the submodule being exited
647    /// - `container_type`: The type of the container with format:
648    ///   - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
649    ///   - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
650    ///   - For Vec containers: "Vec" (name is the index)
651    ///   - For Tuple containers: "Tuple" (name is the index)
652    ///   - For Array containers: "Array" (name is the index)
653    ///
654    /// Note: Option containers do not call enter_module/exit_module to preserve
655    /// the field name in the path (e.g., "bias" instead of "bias.Some")
656    #[allow(unused_variables)]
657    fn exit_module(&mut self, name: &str, container_type: &str) {}
658
659    /// Visit a float tensor with its full module path.
660    ///
661    /// # Parameters
662    /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
663    ///   Each element represents a module name in the hierarchy, with the final element
664    ///   being the parameter name. This allows efficient reuse of the path stack.
665    /// - `id`: The unique identifier of the parameter
666    /// - `tensor`: The float tensor to visit
667    #[allow(unused_variables)]
668    fn visit_float_with_path<const D: usize>(
669        &mut self,
670        path: &[String],
671        id: ParamId,
672        tensor: &Tensor<D>,
673    ) {
674    }
675
676    /// Visit an int tensor with its full module path.
677    ///
678    /// # Parameters
679    /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
680    ///   Each element represents a module name in the hierarchy, with the final element
681    ///   being the parameter name. This allows efficient reuse of the path stack.
682    /// - `id`: The unique identifier of the parameter
683    /// - `tensor`: The integer tensor to visit
684    #[allow(unused_variables)]
685    fn visit_int_with_path<const D: usize>(
686        &mut self,
687        path: &[String],
688        id: ParamId,
689        tensor: &Tensor<D, Int>,
690    ) {
691    }
692
693    /// Visit a bool tensor with its full module path.
694    ///
695    /// # Parameters
696    /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
697    ///   Each element represents a module name in the hierarchy, with the final element
698    ///   being the parameter name. This allows efficient reuse of the path stack.
699    /// - `id`: The unique identifier of the parameter
700    /// - `tensor`: The boolean tensor to visit
701    #[allow(unused_variables)]
702    fn visit_bool_with_path<const D: usize>(
703        &mut self,
704        path: &[String],
705        id: ParamId,
706        tensor: &Tensor<D, Bool>,
707    ) {
708    }
709}
710
711/// Module mapper trait for transforming module parameters.
712pub trait ModuleMapper {
713    /// Called when entering a submodule.
714    ///
715    /// # Parameters
716    /// - `name`: The name of the submodule being entered
717    /// - `container_type`: The type of the container with format:
718    ///   - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
719    ///   - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
720    ///   - For Vec containers: "Vec" (name is the index)
721    ///   - For Tuple containers: "Tuple" (name is the index)
722    ///   - For Array containers: "Array" (name is the index)
723    ///
724    /// Note: Option containers do not call enter_module/exit_module to preserve
725    /// the field name in the path (e.g., "bias" instead of "bias.Some")
726    #[allow(unused_variables)]
727    fn enter_module(&mut self, name: &str, container_type: &str) {}
728
729    /// Called when exiting a submodule.
730    ///
731    /// # Parameters
732    /// - `name`: The name of the submodule being exited
733    /// - `container_type`: The type of the container with format:
734    ///   - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
735    ///   - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
736    ///   - For Vec containers: "Vec" (name is the index)
737    ///   - For Tuple containers: "Tuple" (name is the index)
738    ///   - For Array containers: "Array" (name is the index)
739    ///
740    /// Note: Option containers do not call enter_module/exit_module to preserve
741    /// the field name in the path (e.g., "bias" instead of "bias.Some")
742    #[allow(unused_variables)]
743    fn exit_module(&mut self, name: &str, container_type: &str) {}
744
745    /// Map a float parameter in the module.
746    ///
747    /// # Parameters
748    /// - `param`: The float parameter to transform
749    ///
750    /// # Returns
751    /// The transformed parameter
752    #[allow(unused_variables)]
753    fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
754        param
755    }
756
757    /// Map an int parameter in the module.
758    ///
759    /// # Parameters
760    /// - `param`: The integer parameter to transform
761    ///
762    /// # Returns
763    /// The transformed parameter
764    #[allow(unused_variables)]
765    fn map_int<const D: usize>(&mut self, param: Param<Tensor<D, Int>>) -> Param<Tensor<D, Int>> {
766        param
767    }
768
769    /// Map a bool parameter in the module.
770    ///
771    /// # Parameters
772    /// - `param`: The boolean parameter to transform
773    ///
774    /// # Returns
775    /// The transformed parameter
776    #[allow(unused_variables)]
777    fn map_bool<const D: usize>(
778        &mut self,
779        param: Param<Tensor<D, Bool>>,
780    ) -> Param<Tensor<D, Bool>> {
781        param
782    }
783
784    /// Map a [`Param<Flag>`] in the module.
785    ///
786    /// The default is the identity, so a mapper with no opinion about control state (for example,
787    /// a record, a quantizer or a module sharder) leaves it untouched. Device moves are handled
788    /// directly by [`Module::to_device`] and do not go through a [`ModuleMapper`].
789    ///
790    /// # Parameters
791    /// - `flag`: The flag to map
792    ///
793    /// # Returns
794    /// The mapped flag
795    fn map_flag(&mut self, flag: Param<Flag>) -> Param<Flag> {
796        flag
797    }
798}
799
800#[cfg(all(test, feature = "autodiff"))]
801mod tests {
802    use super::*;
803
804    use crate::module::ParamGroup;
805    use crate::{test_device, test_utils::SimpleLinear};
806
807    fn stateful_module(device: &Device) -> (SimpleLinear, Param<Flag>) {
808        (
809            SimpleLinear::new(4, 4, device),
810            Param::<Flag>::from_bool(true),
811        )
812    }
813
814    #[test]
815    fn test_module_val_train_stateful() {
816        let device = test_device().autodiff();
817        let module = SimpleLinear::new(4, 4, &device);
818
819        assert!(module.weight.is_require_grad());
820        assert!(module.weight.is_active);
821
822        let module = module.valid();
823        assert!(!module.weight.is_require_grad());
824        assert!(module.weight.is_active); // stateful
825
826        let module = module.train();
827        assert!(module.weight.is_require_grad());
828        assert!(module.weight.is_active); // stateful
829
830        let module = module.no_grad();
831        assert!(!module.weight.is_require_grad());
832        assert!(!module.weight.is_active); // stateful
833
834        let module = module.valid();
835        assert!(!module.weight.is_require_grad()); // always
836        assert!(!module.weight.is_active); // stateful
837
838        let module = module.train();
839        assert!(!module.weight.is_require_grad());
840        assert!(!module.weight.is_active); // stateful
841    }
842
843    /// `valid` on a module already on the inner backend returns it unchanged rather than
844    /// panicking.
845    #[test]
846    fn valid_is_idempotent() {
847        let device = test_device().autodiff();
848        let module = SimpleLinear::new(4, 4, &device).valid();
849
850        let module = module.valid();
851
852        assert!(!module.weight.is_require_grad());
853        assert!(!module.weight.val().device().is_autodiff());
854    }
855
856    #[test]
857    fn valid_on_a_plain_device_keeps_tensors_plain_and_disables_flags() {
858        let module = stateful_module(&test_device());
859
860        let module = module.valid();
861
862        assert!(!module.0.weight.val().device().is_autodiff());
863        assert!(!module.1.is_enabled());
864    }
865
866    #[test]
867    fn unfreeze_during_validation_is_fully_applied_by_train() {
868        let device = test_device().autodiff();
869        let module = stateful_module(&device).valid().unfreeze();
870
871        // The plain validation tensor can't require gradients yet, but the setting is preserved.
872        assert!(!module.0.weight.is_require_grad());
873        assert!(module.0.weight.is_active);
874        assert!(module.1.is_enabled());
875
876        let module = module.train();
877
878        assert!(module.0.weight.is_require_grad());
879        assert!(module.0.bias.as_ref().unwrap().is_require_grad());
880        assert!(module.1.is_enabled());
881    }
882
883    #[test]
884    fn set_require_grad_during_validation_is_fully_applied_by_train() {
885        let device = test_device().autodiff();
886        let module = stateful_module(&device)
887            .freeze()
888            .valid()
889            .set_require_grad(true);
890
891        assert!(!module.0.weight.is_require_grad());
892        assert!(module.0.weight.is_active);
893        assert!(!module.1.is_enabled());
894
895        let module = module.train();
896
897        assert!(module.0.weight.is_require_grad());
898        assert!(module.0.bias.as_ref().unwrap().is_require_grad());
899        assert!(!module.1.is_enabled());
900    }
901
902    #[test]
903    fn freeze_during_validation_survives_train() {
904        let device = test_device().autodiff();
905        let module = stateful_module(&device).valid().freeze().train();
906
907        assert!(!module.0.weight.is_require_grad());
908        assert!(!module.0.bias.as_ref().unwrap().is_require_grad());
909        assert!(!module.1.is_enabled());
910    }
911
912    #[test]
913    fn group_activation_settings_during_validation_are_applied_by_train() {
914        let device = test_device().autodiff();
915        let module = stateful_module(&device).freeze().valid();
916        let group = ParamGroup::from_ids(vec![module.0.weight.id, module.1.id]);
917
918        let module = module.unfreeze_group(group).train();
919
920        assert!(module.0.weight.is_require_grad());
921        assert!(!module.0.bias.as_ref().unwrap().is_require_grad());
922        assert!(module.1.is_enabled());
923    }
924
925    #[test]
926    fn group_gradient_settings_during_validation_ignore_flags_and_restore_tensors() {
927        let device = test_device().autodiff();
928        let module = stateful_module(&device).freeze().valid();
929        let group = ParamGroup::from_ids(vec![module.0.weight.id, module.1.id]);
930
931        let module = module.set_require_grad_group(group, true).train();
932
933        assert!(module.0.weight.is_require_grad());
934        assert!(!module.0.bias.as_ref().unwrap().is_require_grad());
935        assert!(!module.1.is_enabled());
936    }
937
938    #[test]
939    fn validation_state_survives_fork_until_train() {
940        let device = test_device().autodiff();
941        let module = stateful_module(&device).valid().fork(&device);
942
943        assert!(!module.0.weight.is_require_grad());
944        assert!(module.0.weight.is_active);
945        assert!(!module.1.is_enabled());
946
947        let module = module.train();
948
949        assert!(module.0.weight.is_require_grad());
950        assert!(module.0.bias.as_ref().unwrap().is_require_grad());
951        assert!(module.1.is_enabled());
952    }
953
954    #[test]
955    fn trained_validation_module_remains_trainable_after_fork() {
956        let device = test_device().autodiff();
957        let module = stateful_module(&device).valid().train().fork(&device);
958
959        assert!(module.0.weight.is_require_grad());
960        assert!(module.0.bias.as_ref().unwrap().is_require_grad());
961        assert!(module.1.is_enabled());
962    }
963
964    #[test]
965    fn validation_state_survives_device_move_until_train() {
966        let device = test_device().autodiff();
967        let target = test_device();
968        let module = stateful_module(&device).valid().to_device(&target);
969
970        assert!(!module.0.weight.is_require_grad());
971        assert!(module.0.weight.is_active);
972        assert!(!module.1.is_enabled());
973
974        let module = module.train();
975
976        assert!(module.0.weight.is_require_grad());
977        assert!(module.0.bias.as_ref().unwrap().is_require_grad());
978        assert!(module.1.is_enabled());
979    }
980
981    #[test]
982    fn freeze_group_freezes_only_selected_params() {
983        let device = test_device().autodiff();
984        let module = SimpleLinear::new(4, 4, &device);
985
986        assert!(module.weight.is_require_grad());
987        assert!(module.bias.as_ref().unwrap().is_require_grad());
988
989        let module = module.freeze_group(ParamGroup::from_path("weight"));
990
991        assert!(!module.weight.is_require_grad());
992        assert!(!module.weight.is_active);
993
994        let bias = module.bias.as_ref().unwrap();
995        assert!(bias.is_require_grad());
996        assert!(bias.is_active);
997    }
998
999    #[test]
1000    fn unfreeze_group_only_thaws_selected_params() {
1001        let device = test_device().autodiff();
1002        let module = SimpleLinear::new(4, 4, &device);
1003
1004        let module = module.no_grad();
1005        assert!(!module.weight.is_require_grad());
1006        assert!(!module.bias.as_ref().unwrap().is_require_grad());
1007
1008        let module = module.unfreeze_group(ParamGroup::from_path("weight"));
1009
1010        assert!(module.weight.is_require_grad());
1011        assert!(module.weight.is_active);
1012        assert!(!module.bias.as_ref().unwrap().is_require_grad());
1013        assert!(!module.bias.as_ref().unwrap().is_active);
1014    }
1015}