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