torsh_nn/core/mod.rs
1//! Core module trait system for neural network modules
2//!
3//! This module provides the foundational Module trait and essential interfaces
4//! for all neural network components in ToRSh.
5
6pub mod module_ext;
7
8pub use module_ext::{ModuleExt, ParameterStats, ValidationReport};
9
10use torsh_core::device::DeviceType;
11use torsh_core::error::Result;
12use torsh_tensor::Tensor;
13
14// Conditional imports for std/no_std compatibility
15#[cfg(feature = "std")]
16use std::collections::HashMap;
17
18#[cfg(not(feature = "std"))]
19use hashbrown::HashMap;
20
21/// Base trait for all neural network modules
22///
23/// This trait provides the core interface for all neural network components,
24/// following PyTorch-compatible patterns for maximum interoperability.
25///
26/// # Design Philosophy
27///
28/// This trait is designed for maximum ergonomics while maintaining flexibility:
29/// - Most methods have sensible defaults to reduce boilerplate
30/// - Core functionality (forward, training mode) is required
31/// - Parameter management is streamlined with helper methods
32/// - Hook system is optional but well-integrated
33///
34/// # Required Methods
35///
36/// Only [`forward()`](Module::forward) must be implemented. All other methods have sensible defaults.
37///
38/// # Implementing a Custom Module
39///
40/// ## Basic Module (No Parameters)
41///
42/// ```rust
43/// use torsh_nn::{Module, ModuleBase};
44/// use torsh_tensor::Tensor;
45/// use torsh_core::error::Result;
46///
47/// /// Simple ReLU activation function
48/// struct MyReLU {
49/// base: ModuleBase,
50/// }
51///
52/// impl MyReLU {
53/// fn new() -> Self {
54/// Self {
55/// base: ModuleBase::new(),
56/// }
57/// }
58/// }
59///
60/// impl Module for MyReLU {
61/// fn forward(&self, input: &Tensor) -> Result<Tensor> {
62/// // Apply ReLU: max(0, x)
63/// input.relu()
64/// }
65///
66/// fn training(&self) -> bool {
67/// self.base.training()
68/// }
69///
70/// fn set_training(&mut self, training: bool) {
71/// self.base.set_training(training);
72/// }
73/// }
74/// ```
75///
76/// ## Module with Parameters
77///
78/// ```rust
79/// use torsh_nn::{Module, ModuleBase, Parameter};
80/// use torsh_tensor::{Tensor, creation};
81/// use torsh_core::error::Result;
82/// use std::collections::HashMap;
83///
84/// /// Custom linear layer with learnable weight and bias
85/// struct MyLinear {
86/// base: ModuleBase,
87/// in_features: usize,
88/// out_features: usize,
89/// }
90///
91/// impl MyLinear {
92/// fn new(in_features: usize, out_features: usize) -> Result<Self> {
93/// let mut base = ModuleBase::new();
94///
95/// // Initialize weight: [in_features, out_features]
96/// let weight = creation::randn(&[in_features, out_features])?;
97/// base.register_parameter("weight".to_string(), Parameter::new(weight));
98///
99/// // Initialize bias: [out_features]
100/// let bias = creation::zeros(&[out_features])?;
101/// base.register_parameter("bias".to_string(), Parameter::new(bias));
102///
103/// Ok(Self {
104/// base,
105/// in_features,
106/// out_features,
107/// })
108/// }
109/// }
110///
111/// impl Module for MyLinear {
112/// fn forward(&self, input: &Tensor) -> Result<Tensor> {
113/// // Get parameters
114/// let weight = self.base.parameters["weight"].tensor().read().clone();
115/// let bias = self.base.parameters["bias"].tensor().read().clone();
116///
117/// // Compute: input @ weight + bias
118/// let output = input.matmul(&weight)?;
119/// output.add(&bias)
120/// }
121///
122/// fn parameters(&self) -> HashMap<String, Parameter> {
123/// self.base.parameters.clone()
124/// }
125///
126/// fn named_parameters(&self) -> HashMap<String, Parameter> {
127/// self.base.named_parameters()
128/// }
129///
130/// fn training(&self) -> bool {
131/// self.base.training()
132/// }
133///
134/// fn set_training(&mut self, training: bool) {
135/// self.base.set_training(training);
136/// }
137/// }
138/// ```
139///
140/// ## Module with Training/Evaluation Modes
141///
142/// ```rust
143/// use torsh_nn::{Module, ModuleBase};
144/// use torsh_tensor::{Tensor, creation};
145/// use torsh_core::error::Result;
146///
147/// /// Dropout layer with different behavior in train/eval modes
148/// struct MyDropout {
149/// base: ModuleBase,
150/// p: f32, // Dropout probability
151/// }
152///
153/// impl MyDropout {
154/// fn new(p: f32) -> Self {
155/// Self {
156/// base: ModuleBase::new(),
157/// p,
158/// }
159/// }
160/// }
161///
162/// impl Module for MyDropout {
163/// fn forward(&self, input: &Tensor) -> Result<Tensor> {
164/// if self.training() {
165/// // During training: randomly zero out units
166/// let rand_vals = creation::rand_like(input)?;
167/// let threshold = creation::full_like(input, self.p)?;
168/// // Keep elements where random value >= p
169/// let keep_mask = rand_vals.ge(&threshold)?;
170/// let zeros = creation::zeros_like(input)?;
171/// let masked = input.where_tensor(&keep_mask, &zeros)?;
172/// // Scale by 1/(1-p) to maintain expected value (inverted dropout)
173/// masked.div_scalar(1.0 - self.p)
174/// } else {
175/// // During evaluation: pass through unchanged
176/// Ok(input.clone())
177/// }
178/// }
179///
180/// fn training(&self) -> bool {
181/// self.base.training()
182/// }
183///
184/// fn set_training(&mut self, training: bool) {
185/// self.base.set_training(training);
186/// }
187/// }
188/// ```
189///
190/// # Complete Training Loop Example
191///
192/// ```rust,no_run
193/// use torsh_nn::prelude::{Module, Linear, Sequential};
194/// use torsh_tensor::{Tensor, creation};
195/// use torsh_core::error::Result;
196///
197/// fn train_eval_example() -> Result<()> {
198/// // 1. Create model
199/// let mut model = Sequential::new()
200/// .add(Linear::new(784, 128, true))
201/// .add(Linear::new(128, 10, true));
202///
203/// // 2. Set model to training mode
204/// model.train();
205///
206/// // 3. Forward pass
207/// let inputs = creation::randn(&[32, 784])?;
208/// let outputs = model.forward(&inputs)?;
209///
210/// // 4. Compute loss (simplified MSE)
211/// let targets = creation::randn(&[32, 10])?;
212/// let diff = outputs.sub(&targets)?;
213/// let loss = diff.pow_scalar(2.0)?.mean(None, false)?;
214///
215/// // 5. Backward pass (computes gradients)
216/// loss.backward()?;
217///
218/// // 6. Switch to evaluation mode
219/// model.eval();
220/// let test_input = creation::randn(&[10, 784])?;
221/// let test_output = model.forward(&test_input)?;
222///
223/// Ok(())
224/// }
225/// ```
226///
227/// # Method Categories
228///
229/// ## Core Methods (Required)
230/// - [`forward()`](Module::forward) - Forward pass computation
231///
232/// ## Parameter Management
233/// - [`parameters()`](Module::parameters) - Get all trainable parameters
234/// - [`named_parameters()`](Module::named_parameters) - Get parameters with names
235/// - [`all_parameters()`](Module::all_parameters) - Get parameters recursively
236/// - [`zero_grad()`](Module::zero_grad) - Clear all gradients
237///
238/// ## Buffer Management
239/// Buffers are persistent, *untrained* tensors (a `BatchNorm`'s `running_mean`,
240/// `running_var`, `num_batches_tracked`). They are not returned by
241/// `parameters()` and no optimizer touches them, but they are model state and
242/// travel in the checkpoint.
243/// - [`buffers()`](Module::buffers) - Get all buffers
244/// - [`named_buffers()`](Module::named_buffers) - Get buffers with names
245/// - [`all_named_buffers()`](Module::all_named_buffers) - Get buffers recursively
246///
247/// ## Training Mode Control
248/// - [`training()`](Module::training) - Check if in training mode
249/// - [`train()`](Module::train) - Set to training mode
250/// - [`eval()`](Module::eval) - Set to evaluation mode
251/// - [`set_training()`](Module::set_training) - Set training mode explicitly
252///
253/// ## Module Hierarchy
254/// - [`children()`](Module::children) - Get direct child modules
255/// - [`named_children()`](Module::named_children) - Get children with names
256/// - [`modules()`](Module::modules) - Get all modules recursively
257///
258/// ## State Management
259/// - [`state_dict()`](Module::state_dict) - Save module state (parameters *and* buffers)
260/// - [`load_state_dict()`](Module::load_state_dict) - Load module state (parameters *and* buffers)
261/// - [`to_device()`](Module::to_device) - Move module to device
262///
263/// ## Utilities
264/// - [`freeze()`](Module::freeze) - Freeze all parameters
265/// - [`unfreeze()`](Module::unfreeze) - Unfreeze all parameters
266/// - [`num_parameters()`](Module::num_parameters) - Count total parameters
267/// - [`diagnose()`](Module::diagnose) - Check module health
268///
269/// # PyTorch Compatibility
270///
271/// ToRSh's Module trait closely follows PyTorch's `nn.Module` interface:
272///
273/// | PyTorch | ToRSh | Notes |
274/// |---------|-------|-------|
275/// | `forward(x)` | `forward(&x)` | Returns `Result<Tensor>` |
276/// | `parameters()` | `parameters()` | Returns `HashMap<String, Parameter>` |
277/// | `named_buffers()` | `named_buffers()` | Returns `HashMap<String, Arc<RwLock<Tensor>>>` — live handles, not copies |
278/// | `train()` | `train()` | Sets training mode |
279/// | `eval()` | `eval()` | Sets evaluation mode |
280/// | `state_dict()` | `state_dict()` | Returns parameter **and buffer** tensors |
281/// | `load_state_dict()` | `load_state_dict()` | Loads parameters **and buffers** from a HashMap |
282/// | `to(device)` | `to_device(device)` | Moves to device |
283/// | `zero_grad()` | `zero_grad()` | Clears gradients |
284///
285/// One deliberate divergence: PyTorch exempts `num_batches_tracked` from
286/// strict-mode key checking for backwards compatibility with pre-0.4.1
287/// checkpoints. ToRSh grants no per-key exemptions — every buffer is required
288/// under `strict = true`.
289///
290/// # Best Practices
291///
292/// 1. **Always use `ModuleBase`**: Store a `ModuleBase` instance in your module to handle
293/// common functionality like parameters, buffers, and training state.
294///
295/// 2. **Register parameters in constructor**: Use `base.register_parameter()` to register
296/// all trainable parameters during module creation.
297///
298/// 3. **Implement training/eval behavior**: If your module behaves differently during
299/// training vs evaluation (like Dropout, BatchNorm), check `self.training()`.
300///
301/// 4. **Use `Result<Tensor>` for error handling**: Always return `Result<Tensor>` from
302/// `forward()` to properly propagate errors.
303///
304/// 5. **Delegate to `ModuleBase`**: Implement parameter and training mode methods by
305/// delegating to your `ModuleBase` instance.
306///
307/// 6. **Initialize parameters properly**: Use initialization methods from `torsh_nn::init`
308/// for better training convergence.
309pub trait Module: Send + Sync {
310 /// Forward pass through the module
311 ///
312 /// This is the only required method that must be implemented by all modules.
313 ///
314 /// # Arguments
315 /// * `input` - Input tensor
316 ///
317 /// # Returns
318 /// * `Result<Tensor>` - Output tensor or error
319 fn forward(&self, input: &Tensor) -> Result<Tensor>;
320
321 /// Get all parameters in the module (non-recursive)
322 ///
323 /// Override this method if your module has trainable parameters.
324 /// The default implementation returns an empty map.
325 ///
326 /// # Returns
327 /// * `HashMap<String, Parameter>` - Map of parameter names to parameters
328 fn parameters(&self) -> HashMap<String, crate::Parameter> {
329 HashMap::new()
330 }
331
332 /// Get named parameters (non-recursive)
333 ///
334 /// Default implementation delegates to `parameters()`. Override if you need
335 /// different behavior for named vs unnamed parameter access.
336 ///
337 /// # Returns
338 /// * `HashMap<String, Parameter>` - Map of parameter names to parameters
339 fn named_parameters(&self) -> HashMap<String, crate::Parameter> {
340 self.parameters()
341 }
342
343 /// Get all parameters recursively including submodules
344 ///
345 /// # Returns
346 /// * `HashMap<String, Parameter>` - Flattened map of all parameters
347 fn all_parameters(&self) -> HashMap<String, crate::Parameter> {
348 let mut all_params = self.parameters();
349
350 for child in self.children() {
351 let child_params = child.all_parameters();
352 for (name, param) in child_params {
353 all_params.insert(name, param);
354 }
355 }
356
357 all_params
358 }
359
360 /// Get all named parameters recursively with module prefixes
361 ///
362 /// # Returns
363 /// * `HashMap<String, Parameter>` - Hierarchical parameter names
364 fn all_named_parameters(&self) -> HashMap<String, crate::Parameter> {
365 let mut all_params = HashMap::new();
366
367 // Add own parameters
368 for (name, param) in self.named_parameters() {
369 all_params.insert(name, param);
370 }
371
372 // Add child parameters with prefixes
373 let children_named = self.named_children();
374 for (child_name, child) in children_named {
375 for (param_name, param) in child.all_named_parameters() {
376 let full_name = format!("{}.{}", child_name, param_name);
377 all_params.insert(full_name, param);
378 }
379 }
380
381 all_params
382 }
383
384 /// Get all named buffers recursively with module prefixes
385 ///
386 /// The buffer-side twin of [`all_named_parameters()`](Module::all_named_parameters),
387 /// and the recursion [`state_dict()`](Module::state_dict) uses to reach a
388 /// child's non-trainable state. Buffers are a module's *persistent, untrained*
389 /// tensors — a `BatchNorm`'s `running_mean` / `running_var` /
390 /// `num_batches_tracked` — which evaluation mode consumes in place of the
391 /// batch statistics, so losing them turns a trained normalization layer back
392 /// into a freshly constructed one.
393 ///
394 /// Child names come from [`named_children()`](Module::named_children); a
395 /// module that overrides only `children()` contributes no buffer names here,
396 /// exactly as it contributes no parameter names to `all_named_parameters()`.
397 ///
398 /// # Returns
399 /// * `HashMap<String, Arc<RwLock<Tensor>>>` - Hierarchical buffer names,
400 /// holding the module's *live* handles rather than copies
401 fn all_named_buffers(&self) -> HashMap<String, std::sync::Arc<parking_lot::RwLock<Tensor>>> {
402 let mut all_buffers = self.named_buffers();
403
404 for (child_name, child) in self.named_children() {
405 for (buffer_name, buffer) in child.all_named_buffers() {
406 all_buffers.insert(format!("{}.{}", child_name, buffer_name), buffer);
407 }
408 }
409
410 all_buffers
411 }
412
413 /// Check if in training mode
414 ///
415 /// Default implementation returns true. Override if your module tracks training state.
416 fn training(&self) -> bool {
417 true
418 }
419
420 /// Set training mode
421 ///
422 /// Convenience method that calls `set_training(true)`.
423 fn train(&mut self) {
424 self.set_training(true);
425 }
426
427 /// Set evaluation mode
428 ///
429 /// Convenience method that calls `set_training(false)`.
430 fn eval(&mut self) {
431 self.set_training(false);
432 }
433
434 /// Set training mode (internal implementation)
435 ///
436 /// Default implementation does nothing. Override if your module needs to track
437 /// training state or propagate it to child modules.
438 fn set_training(&mut self, _training: bool) {
439 // Default: do nothing
440 }
441
442 /// Move module to device
443 ///
444 /// Default implementation does nothing. Override if your module has parameters
445 /// or buffers that need to be moved between devices.
446 fn to_device(&mut self, _device: DeviceType) -> Result<()> {
447 Ok(())
448 }
449
450 /// Load state dictionary into the module
451 ///
452 /// Restores **parameters and buffers**, matching PyTorch's
453 /// `nn.Module.load_state_dict`. Buffers are written through the live
454 /// handles published by [`all_named_buffers()`](Module::all_named_buffers),
455 /// so a `BatchNorm`'s running statistics come back into the layer itself
456 /// rather than into a detached copy.
457 ///
458 /// # Arguments
459 /// * `state_dict` - Map of parameter *and buffer* names to tensors
460 /// * `strict` - Whether to require exact name matches. Strict mode counts
461 /// buffers: a checkpoint missing `running_mean` is rejected, with no
462 /// per-key exemptions (PyTorch's legacy leniency for
463 /// `num_batches_tracked` is deliberately not reproduced — silently
464 /// accepting a partial checkpoint is the defect this method exists to
465 /// prevent).
466 ///
467 /// # Returns
468 /// * `Result<()>` - Success or error with details about missing/unexpected keys
469 ///
470 /// # Errors
471 ///
472 /// Shapes are validated for every matching entry *before* anything is
473 /// written, so a corrupt checkpoint leaves the module exactly as it was
474 /// instead of half-loaded. A shape mismatch is an error in both strict and
475 /// non-strict mode; `strict` governs which *names* must be present, not
476 /// whether the values that are present have to fit.
477 fn load_state_dict(
478 &mut self,
479 state_dict: &HashMap<String, Tensor>,
480 strict: bool,
481 ) -> Result<()> {
482 let current_params = self.all_named_parameters();
483 let current_buffers = self.all_named_buffers();
484 let mut missing_keys = Vec::new();
485 let mut unexpected_keys = Vec::new();
486
487 // Check for missing parameters and buffers
488 for name in current_params.keys().chain(current_buffers.keys()) {
489 if !state_dict.contains_key(name) {
490 missing_keys.push(name.clone());
491 }
492 }
493
494 // Check for entries the module cannot consume
495 for name in state_dict.keys() {
496 if !current_params.contains_key(name) && !current_buffers.contains_key(name) {
497 unexpected_keys.push(name.clone());
498 }
499 }
500
501 if strict && (!missing_keys.is_empty() || !unexpected_keys.is_empty()) {
502 // Sorted so the report is reproducible; both maps iterate in
503 // unspecified order.
504 missing_keys.sort();
505 unexpected_keys.sort();
506 return Err(torsh_core::error::TorshError::Other(format!(
507 "State dict loading failed. Missing keys: {:?}, Unexpected keys: {:?}",
508 missing_keys, unexpected_keys
509 )));
510 }
511
512 // Validate every shape before mutating anything.
513 for (name, param) in ¤t_params {
514 if let Some(new_tensor) = state_dict.get(name) {
515 let current_shape = param.shape()?;
516 let new_shape = new_tensor.shape().dims().to_vec();
517 if current_shape != new_shape {
518 return Err(torsh_core::error::TorshError::Other(format!(
519 "Shape mismatch for parameter '{}': expected {:?}, got {:?}",
520 name, current_shape, new_shape
521 )));
522 }
523 }
524 }
525 for (name, buffer) in ¤t_buffers {
526 if let Some(new_tensor) = state_dict.get(name) {
527 // The read guard is a statement-scoped temporary: it must be
528 // released before the write pass below takes the same lock,
529 // because `parking_lot::RwLock` is not reentrant.
530 let current_shape = buffer.read().shape().dims().to_vec();
531 let new_shape = new_tensor.shape().dims().to_vec();
532 if current_shape != new_shape {
533 return Err(torsh_core::error::TorshError::Other(format!(
534 "Shape mismatch for buffer '{}': expected {:?}, got {:?}",
535 name, current_shape, new_shape
536 )));
537 }
538 }
539 }
540
541 // Load matching parameters
542 for (name, param) in current_params {
543 if let Some(new_tensor) = state_dict.get(&name) {
544 *param.tensor().write() = new_tensor.clone();
545 }
546 }
547
548 // Load matching buffers
549 for (name, buffer) in current_buffers {
550 if let Some(new_tensor) = state_dict.get(&name) {
551 *buffer.write() = new_tensor.clone();
552 }
553 }
554
555 Ok(())
556 }
557
558 /// Load state dictionary with default strict=true
559 fn load_state_dict_strict(&mut self, state_dict: &HashMap<String, Tensor>) -> Result<()> {
560 self.load_state_dict(state_dict, true)
561 }
562
563 /// Save state dictionary from the module
564 ///
565 /// Carries **parameters and buffers**, matching PyTorch's
566 /// `nn.Module.state_dict`. Buffers are a module's persistent untrained
567 /// state — `running_mean`, `running_var`, `num_batches_tracked` — and
568 /// evaluation mode consumes them in place of the batch statistics, so a
569 /// checkpoint that omitted them reloaded as a *different* model with no
570 /// error reported anywhere.
571 ///
572 /// Values are snapshots taken at call time, keyed by
573 /// [`all_named_parameters()`](Module::all_named_parameters) and
574 /// [`all_named_buffers()`](Module::all_named_buffers), which is exactly the
575 /// key set [`load_state_dict()`](Module::load_state_dict) expects back.
576 fn state_dict(&self) -> HashMap<String, Tensor> {
577 let mut state = HashMap::new();
578 for (name, param) in self.all_named_parameters() {
579 state.insert(name, param.clone_data());
580 }
581 for (name, buffer) in self.all_named_buffers() {
582 // PyTorch rejects registering a buffer under a name already taken
583 // by a parameter; nothing enforces that here, so the impossible
584 // case is resolved deterministically in the parameter's favour
585 // rather than by silently overwriting trainable state.
586 state.entry(name).or_insert_with(|| buffer.read().clone());
587 }
588 state
589 }
590
591 /// Get the module name (optional, for debugging and serialization)
592 fn name(&self) -> Option<&str> {
593 None
594 }
595
596 /// Get all buffers (non-trainable parameters)
597 fn buffers(&self) -> Vec<std::sync::Arc<parking_lot::RwLock<Tensor>>> {
598 Vec::new()
599 }
600
601 /// Get named buffers
602 fn named_buffers(&self) -> HashMap<String, std::sync::Arc<parking_lot::RwLock<Tensor>>> {
603 HashMap::new()
604 }
605
606 /// Get all direct child modules
607 ///
608 /// Default implementation returns an empty vector. Override if your module
609 /// contains child modules.
610 fn children(&self) -> Vec<&dyn Module> {
611 Vec::new()
612 }
613
614 /// Get all direct child modules with names
615 ///
616 /// Default implementation returns an empty vector. Override if your module
617 /// contains named child modules.
618 fn named_children(&self) -> Vec<(String, &dyn Module)> {
619 Vec::new()
620 }
621
622 /// Get all modules recursively (depth-first traversal)
623 fn modules(&self) -> Vec<&dyn Module>
624 where
625 Self: Sized,
626 {
627 let mut modules: Vec<&dyn Module> = vec![self];
628 for child in self.children() {
629 // Since child is &dyn Module, we need to use a different approach
630 // We'll just collect immediate children for now
631 modules.push(child);
632 }
633 modules
634 }
635
636 /// Get all modules recursively with hierarchical names
637 fn named_modules(&self) -> Vec<(String, &dyn Module)>
638 where
639 Self: Sized,
640 {
641 let mut modules: Vec<(String, &dyn Module)> = vec![(String::new(), self)];
642
643 for (child_name, child) in self.named_children() {
644 // Since child is &dyn Module, we need to use a different approach
645 // We'll just collect immediate named children for now
646 modules.push((child_name, child));
647 }
648
649 modules
650 }
651
652 /// Zero all gradients recursively
653 ///
654 /// Default implementation does nothing. Override if your module has parameters
655 /// with gradients that need to be zeroed.
656 fn zero_grad(&mut self) {
657 // Default: do nothing
658 }
659
660 /// Count total number of parameters
661 fn num_parameters(&self) -> usize {
662 self.all_parameters()
663 .values()
664 .map(|p| p.numel().unwrap_or(0))
665 .sum()
666 }
667
668 /// Count trainable parameters
669 fn num_trainable_parameters(&self) -> usize {
670 self.all_parameters()
671 .values()
672 .filter(|p| p.requires_grad())
673 .map(|p| p.numel().unwrap_or(0))
674 .sum()
675 }
676
677 /// Get memory usage estimate in bytes
678 fn memory_usage(&self) -> usize {
679 self.all_parameters()
680 .values()
681 .map(|p| p.numel().unwrap_or(0) * 4) // Assume f32 = 4 bytes
682 .sum()
683 }
684
685 /// Freeze all parameters (set requires_grad = false)
686 ///
687 /// Default implementation does nothing. Override if your module has parameters
688 /// that can be frozen/unfrozen.
689 fn freeze(&mut self) {
690 // Default: do nothing
691 }
692
693 /// Unfreeze all parameters (set requires_grad = true)
694 ///
695 /// Default implementation does nothing. Override if your module has parameters
696 /// that can be frozen/unfrozen.
697 fn unfreeze(&mut self) {
698 // Default: do nothing
699 }
700
701 /// Get string representation
702 fn extra_repr(&self) -> String {
703 String::new()
704 }
705
706 /// Register a hook for this module (default implementation does nothing)
707 fn register_hook(
708 &mut self,
709 _hook_type: crate::HookType,
710 _callback: crate::HookCallback,
711 ) -> Option<crate::HookHandle> {
712 None
713 }
714
715 /// Remove a hook by handle (default implementation does nothing)
716 fn remove_hook(&mut self, _hook_type: crate::HookType, _handle: crate::HookHandle) -> bool {
717 false
718 }
719
720 /// Execute hooks of a specific type (default implementation does nothing)
721 fn execute_hooks(
722 &self,
723 _hook_type: crate::HookType,
724 _input: &Tensor,
725 _output: Option<&Tensor>,
726 ) -> Result<()> {
727 Ok(())
728 }
729
730 /// Forward pass with hooks support
731 fn forward_with_hooks(&self, input: &Tensor) -> Result<Tensor> {
732 // Execute pre-forward hooks
733 self.execute_hooks(crate::HookType::PreForward, input, None)?;
734
735 // Perform forward pass
736 let output = self.forward(input)?;
737
738 // Execute post-forward hooks
739 self.execute_hooks(crate::HookType::PostForward, input, Some(&output))?;
740
741 Ok(output)
742 }
743
744 /// Check if module has hooks registered
745 fn has_hooks(&self, _hook_type: crate::HookType) -> bool {
746 false
747 }
748
749 // === Ergonomic Helper Methods ===
750
751 /// Convenient method to call forward and handle common patterns
752 ///
753 /// This is equivalent to `forward()` but provides a more ergonomic interface
754 /// for chaining operations.
755 fn call(&self, input: &Tensor) -> Result<Tensor> {
756 self.forward(input)
757 }
758
759 /// Apply the module to input (alias for forward)
760 ///
761 /// PyTorch-style method name for compatibility.
762 fn apply(&self, input: &Tensor) -> Result<Tensor> {
763 self.forward(input)
764 }
765
766 /// Check if the module has any parameters
767 fn has_parameters(&self) -> bool {
768 !self.parameters().is_empty()
769 }
770
771 /// Check if the module has any child modules
772 fn has_children(&self) -> bool {
773 !self.children().is_empty()
774 }
775
776 /// Get parameter count (convenience method)
777 fn parameter_count(&self) -> usize {
778 self.num_parameters()
779 }
780
781 /// Get trainable parameter count (convenience method)
782 fn trainable_parameter_count(&self) -> usize {
783 self.num_trainable_parameters()
784 }
785
786 /// Get memory usage in MB (convenience method)
787 fn memory_usage_mb(&self) -> f64 {
788 self.memory_usage() as f64 / (1024.0 * 1024.0)
789 }
790
791 /// Toggle training mode (convenience method)
792 fn toggle_training(&mut self) {
793 self.set_training(!self.training());
794 }
795
796 /// Check if module is in evaluation mode
797 fn eval_mode(&self) -> bool {
798 !self.training()
799 }
800
801 // === Enhanced Ergonomic Methods ===
802
803 /// Sequential forward pass through multiple modules
804 ///
805 /// This provides a convenient way to chain multiple forward passes.
806 ///
807 /// # Arguments
808 /// * `modules` - Slice of modules to apply sequentially
809 /// * `input` - Input tensor
810 ///
811 /// # Returns
812 /// * `Result<Tensor>` - Final output after all modules
813 ///
814 /// # Example
815 /// ```ignore
816 /// let result = Module::sequential_forward(&[&layer1, &layer2, &layer3], &input)?;
817 /// ```
818 fn sequential_forward(modules: &[&dyn Module], mut input: Tensor) -> Result<Tensor>
819 where
820 Self: Sized,
821 {
822 for module in modules {
823 input = module.forward(&input)?;
824 }
825 Ok(input)
826 }
827
828 /// Apply module multiple times with different inputs (batch processing)
829 ///
830 /// This is useful for processing multiple independent inputs through the same module.
831 ///
832 /// # Arguments
833 /// * `inputs` - Vector of input tensors
834 ///
835 /// # Returns
836 /// * `Result<Vec<Tensor>>` - Vector of output tensors
837 fn batch_forward(&self, inputs: &[Tensor]) -> Result<Vec<Tensor>> {
838 inputs.iter().map(|input| self.forward(input)).collect()
839 }
840
841 /// Forward with condition - only apply if condition is true
842 ///
843 /// This provides conditional execution of modules.
844 ///
845 /// # Arguments
846 /// * `input` - Input tensor
847 /// * `condition` - Whether to apply this module
848 ///
849 /// # Returns
850 /// * `Result<Tensor>` - Output tensor (input if condition is false)
851 fn conditional_forward(&self, input: &Tensor, condition: bool) -> Result<Tensor> {
852 if condition {
853 self.forward(input)
854 } else {
855 Ok(input.clone())
856 }
857 }
858
859 /// Forward with residual connection
860 ///
861 /// Applies the module and adds the result to the input (residual/skip connection).
862 ///
863 /// # Arguments
864 /// * `input` - Input tensor
865 ///
866 /// # Returns
867 /// * `Result<Tensor>` - Output tensor (input + forward(input))
868 fn residual_forward(&self, input: &Tensor) -> Result<Tensor> {
869 let output = self.forward(input)?;
870 // This would use tensor addition when available
871 // For now, just return the output
872 Ok(output)
873 }
874
875 /// Get detailed module information for debugging
876 ///
877 /// This provides comprehensive information about the module state.
878 ///
879 /// # Returns
880 /// * `ModuleInfo` - Detailed module information
881 fn module_info(&self) -> crate::ModuleInfo {
882 crate::ModuleInfo {
883 name: self.name().unwrap_or("Unknown").to_string(),
884 training: self.training(),
885 parameter_count: self.num_parameters(),
886 trainable_parameter_count: self.num_trainable_parameters(),
887 memory_usage_bytes: self.memory_usage(),
888 has_children: self.has_children(),
889 children_count: self.children().len(),
890 }
891 }
892
893 /// Check if module is ready for training
894 ///
895 /// Performs various checks to ensure the module is properly configured for training.
896 ///
897 /// # Returns
898 /// * `Result<()>` - Ok if ready, Error with details if not
899 fn check_training_readiness(&self) -> Result<()> {
900 // Check if module has parameters
901 if !self.has_parameters() {
902 return Err(torsh_core::error::TorshError::Other(
903 "Module has no parameters - may not be trainable".to_string(),
904 ));
905 }
906
907 // Check if in training mode
908 if !self.training() {
909 return Err(torsh_core::error::TorshError::Other(
910 "Module is in evaluation mode - switch to training mode first".to_string(),
911 ));
912 }
913
914 // Check for finite parameters
915 for param in self.parameters().values() {
916 if !param.is_finite().unwrap_or(false) {
917 return Err(torsh_core::error::TorshError::Other(
918 "Module contains non-finite parameters (NaN or infinity)".to_string(),
919 ));
920 }
921 }
922
923 Ok(())
924 }
925
926 /// Get parameter names matching a pattern
927 ///
928 /// This helps with selective parameter access and manipulation.
929 ///
930 /// # Arguments
931 /// * `pattern` - String pattern to match against parameter names
932 ///
933 /// # Returns
934 /// * `Vec<String>` - Vector of parameter names matching the pattern
935 fn parameter_names_matching(&self, pattern: &str) -> Vec<String> {
936 self.all_named_parameters()
937 .keys()
938 .filter(|name| name.contains(pattern))
939 .cloned()
940 .collect()
941 }
942
943 /// Get parameters by layer type (e.g., "weight", "bias")
944 ///
945 /// # Arguments
946 /// * `param_type` - Type of parameters to retrieve
947 ///
948 /// # Returns
949 /// * `HashMap<String, Parameter>` - Filtered parameters
950 fn parameters_by_type(&self, param_type: &str) -> HashMap<String, crate::Parameter> {
951 self.all_named_parameters()
952 .into_iter()
953 .filter(|(name, _)| name.contains(param_type))
954 .collect()
955 }
956
957 /// Clone module parameters (for creating copies or checkpoints)
958 ///
959 /// # Returns
960 /// * `HashMap<String, Tensor>` - Cloned parameter tensors
961 fn clone_parameters(&self) -> HashMap<String, Tensor> {
962 self.all_named_parameters()
963 .into_iter()
964 .map(|(name, param)| (name, param.clone_data()))
965 .collect()
966 }
967
968 /// Quick diagnostic check of module health
969 ///
970 /// # Returns
971 /// * `ModuleDiagnostics` - Diagnostic information
972 fn diagnose(&self) -> crate::ModuleDiagnostics {
973 let mut issues = Vec::new();
974 let mut warnings = Vec::new();
975
976 // Check parameter health
977 for (name, param) in self.all_named_parameters() {
978 if let Ok(diag) = param.diagnose() {
979 if !diag.issues.is_empty() {
980 issues.extend(
981 diag.issues
982 .into_iter()
983 .map(|issue| format!("{}: {}", name, issue)),
984 );
985 }
986 if !diag.warnings.is_empty() {
987 warnings.extend(
988 diag.warnings
989 .into_iter()
990 .map(|warning| format!("{}: {}", name, warning)),
991 );
992 }
993 }
994 }
995
996 // Check training readiness
997 if let Err(e) = self.check_training_readiness() {
998 warnings.push(format!("Training readiness: {}", e));
999 }
1000
1001 crate::ModuleDiagnostics {
1002 module_info: self.module_info(),
1003 issues,
1004 warnings,
1005 parameter_diagnostics: self
1006 .all_named_parameters()
1007 .into_iter()
1008 .filter_map(|(name, param)| param.diagnose().ok().map(|d| (name, d)))
1009 .collect(),
1010 }
1011 }
1012}
1013
1014/// Forwards **every** `Module` method to `(**self)`.
1015///
1016/// `Module` has one required method and ~50 defaulted ones, and those defaults
1017/// split into two families. *Composed* defaults (`all_named_parameters`,
1018/// `state_dict`, `num_parameters`, `residual_forward`, …) are written in terms
1019/// of other trait methods, so an impl that forwards the primitives inherits
1020/// them correctly. *Leaf* defaults (`buffers`, `named_buffers`, `name`,
1021/// `zero_grad`, `freeze`, `unfreeze`, `extra_repr`, and the whole hook
1022/// protocol) return emptiness — `Vec::new()`, `HashMap::new()`, `None`,
1023/// `false`, `Ok(())`, an empty body. A smart-pointer impl that *omits* one of
1024/// those does not fall through to the pointee: it answers "nothing" on the
1025/// pointee's behalf, with no error anywhere.
1026///
1027/// That distinction is why this macro exists rather than a hand-written list.
1028/// The predecessor forwarded nine methods and omitted every leaf default, so a
1029/// boxed `BatchNorm1d` reported zero buffers while the bare layer reported
1030/// three — and `Sequential`/`ModuleList` store their children as
1031/// `Vec<Box<dyn Module>>`, where *every* trait call on a child resolves through
1032/// this impl. A checkpoint or device migration taken through the box therefore
1033/// dropped `running_mean` / `running_var` / `num_batches_tracked` silently,
1034/// reverting a trained normalization layer to its initialization on reload.
1035/// `tests/hardening_nn_module_box.rs` pins the whole surface.
1036///
1037/// The body is spelled `(**self)` for both implementors on purpose:
1038/// - for `Box<dyn Module>` that is the `dyn Module` pointee, i.e. a vtable call;
1039/// - for `&mut Box<dyn Module>` that is the `Box<dyn Module>` itself, i.e. a
1040/// call into the impl directly above, which then performs the vtable call.
1041///
1042/// `modules`/`named_modules` are excluded: they carry a `where Self: Sized`
1043/// bound, so they cannot be invoked on an unsized `dyn Module` and are supplied
1044/// per-implementor below. `sequential_forward` is excluded too — it is an
1045/// associated function with no receiver, so there is nothing to forward to.
1046macro_rules! forward_all_module_methods {
1047 () => {
1048 fn forward(&self, input: &Tensor) -> Result<Tensor> {
1049 (**self).forward(input)
1050 }
1051
1052 fn parameters(&self) -> HashMap<String, crate::Parameter> {
1053 (**self).parameters()
1054 }
1055
1056 fn named_parameters(&self) -> HashMap<String, crate::Parameter> {
1057 (**self).named_parameters()
1058 }
1059
1060 fn all_parameters(&self) -> HashMap<String, crate::Parameter> {
1061 (**self).all_parameters()
1062 }
1063
1064 fn all_named_parameters(&self) -> HashMap<String, crate::Parameter> {
1065 (**self).all_named_parameters()
1066 }
1067
1068 fn all_named_buffers(
1069 &self,
1070 ) -> HashMap<String, std::sync::Arc<parking_lot::RwLock<Tensor>>> {
1071 (**self).all_named_buffers()
1072 }
1073
1074 fn training(&self) -> bool {
1075 (**self).training()
1076 }
1077
1078 fn train(&mut self) {
1079 (**self).train()
1080 }
1081
1082 fn eval(&mut self) {
1083 (**self).eval()
1084 }
1085
1086 fn set_training(&mut self, training: bool) {
1087 (**self).set_training(training)
1088 }
1089
1090 fn to_device(&mut self, device: DeviceType) -> Result<()> {
1091 (**self).to_device(device)
1092 }
1093
1094 fn load_state_dict(
1095 &mut self,
1096 state_dict: &HashMap<String, Tensor>,
1097 strict: bool,
1098 ) -> Result<()> {
1099 (**self).load_state_dict(state_dict, strict)
1100 }
1101
1102 fn load_state_dict_strict(&mut self, state_dict: &HashMap<String, Tensor>) -> Result<()> {
1103 (**self).load_state_dict_strict(state_dict)
1104 }
1105
1106 fn state_dict(&self) -> HashMap<String, Tensor> {
1107 (**self).state_dict()
1108 }
1109
1110 fn name(&self) -> Option<&str> {
1111 (**self).name()
1112 }
1113
1114 fn buffers(&self) -> Vec<std::sync::Arc<parking_lot::RwLock<Tensor>>> {
1115 (**self).buffers()
1116 }
1117
1118 fn named_buffers(&self) -> HashMap<String, std::sync::Arc<parking_lot::RwLock<Tensor>>> {
1119 (**self).named_buffers()
1120 }
1121
1122 fn children(&self) -> Vec<&dyn Module> {
1123 (**self).children()
1124 }
1125
1126 fn named_children(&self) -> Vec<(String, &dyn Module)> {
1127 (**self).named_children()
1128 }
1129
1130 fn zero_grad(&mut self) {
1131 (**self).zero_grad()
1132 }
1133
1134 fn num_parameters(&self) -> usize {
1135 (**self).num_parameters()
1136 }
1137
1138 fn num_trainable_parameters(&self) -> usize {
1139 (**self).num_trainable_parameters()
1140 }
1141
1142 fn memory_usage(&self) -> usize {
1143 (**self).memory_usage()
1144 }
1145
1146 fn freeze(&mut self) {
1147 (**self).freeze()
1148 }
1149
1150 fn unfreeze(&mut self) {
1151 (**self).unfreeze()
1152 }
1153
1154 fn extra_repr(&self) -> String {
1155 (**self).extra_repr()
1156 }
1157
1158 fn register_hook(
1159 &mut self,
1160 hook_type: crate::HookType,
1161 callback: crate::HookCallback,
1162 ) -> Option<crate::HookHandle> {
1163 (**self).register_hook(hook_type, callback)
1164 }
1165
1166 fn remove_hook(&mut self, hook_type: crate::HookType, handle: crate::HookHandle) -> bool {
1167 (**self).remove_hook(hook_type, handle)
1168 }
1169
1170 fn execute_hooks(
1171 &self,
1172 hook_type: crate::HookType,
1173 input: &Tensor,
1174 output: Option<&Tensor>,
1175 ) -> Result<()> {
1176 (**self).execute_hooks(hook_type, input, output)
1177 }
1178
1179 fn forward_with_hooks(&self, input: &Tensor) -> Result<Tensor> {
1180 (**self).forward_with_hooks(input)
1181 }
1182
1183 fn has_hooks(&self, hook_type: crate::HookType) -> bool {
1184 (**self).has_hooks(hook_type)
1185 }
1186
1187 fn call(&self, input: &Tensor) -> Result<Tensor> {
1188 (**self).call(input)
1189 }
1190
1191 fn apply(&self, input: &Tensor) -> Result<Tensor> {
1192 (**self).apply(input)
1193 }
1194
1195 fn has_parameters(&self) -> bool {
1196 (**self).has_parameters()
1197 }
1198
1199 fn has_children(&self) -> bool {
1200 (**self).has_children()
1201 }
1202
1203 fn parameter_count(&self) -> usize {
1204 (**self).parameter_count()
1205 }
1206
1207 fn trainable_parameter_count(&self) -> usize {
1208 (**self).trainable_parameter_count()
1209 }
1210
1211 fn memory_usage_mb(&self) -> f64 {
1212 (**self).memory_usage_mb()
1213 }
1214
1215 fn toggle_training(&mut self) {
1216 (**self).toggle_training()
1217 }
1218
1219 fn eval_mode(&self) -> bool {
1220 (**self).eval_mode()
1221 }
1222
1223 fn batch_forward(&self, inputs: &[Tensor]) -> Result<Vec<Tensor>> {
1224 (**self).batch_forward(inputs)
1225 }
1226
1227 fn conditional_forward(&self, input: &Tensor, condition: bool) -> Result<Tensor> {
1228 (**self).conditional_forward(input, condition)
1229 }
1230
1231 fn residual_forward(&self, input: &Tensor) -> Result<Tensor> {
1232 (**self).residual_forward(input)
1233 }
1234
1235 fn module_info(&self) -> crate::ModuleInfo {
1236 (**self).module_info()
1237 }
1238
1239 fn check_training_readiness(&self) -> Result<()> {
1240 (**self).check_training_readiness()
1241 }
1242
1243 fn parameter_names_matching(&self, pattern: &str) -> Vec<String> {
1244 (**self).parameter_names_matching(pattern)
1245 }
1246
1247 // Spelled as a fully-qualified call because `ModuleExt` (blanket-impl'd
1248 // for every `Module`) also has a `parameters_by_type`, with a different
1249 // signature; plain method syntax is ambiguous between the two.
1250 fn parameters_by_type(&self, param_type: &str) -> HashMap<String, crate::Parameter> {
1251 Module::parameters_by_type(&**self, param_type)
1252 }
1253
1254 fn clone_parameters(&self) -> HashMap<String, Tensor> {
1255 (**self).clone_parameters()
1256 }
1257
1258 fn diagnose(&self) -> crate::ModuleDiagnostics {
1259 (**self).diagnose()
1260 }
1261 };
1262}
1263
1264/// Implementation for boxed trait objects
1265impl Module for Box<dyn Module> {
1266 forward_all_module_methods!();
1267
1268 /// Rooted at the *inner* module rather than at the box.
1269 ///
1270 /// The trait default pushes `self`, which here is the `Box` — a legal
1271 /// `&dyn Module`, but one extra indirection deep, so a caller walking the
1272 /// tree sees a wrapper that has no counterpart in the module hierarchy it
1273 /// is describing. `(**self).modules()` cannot be used to delegate because
1274 /// the method carries `where Self: Sized` and `dyn Module` is unsized, so
1275 /// the walk is rebuilt here from the same two pieces the default uses.
1276 fn modules(&self) -> Vec<&dyn Module>
1277 where
1278 Self: Sized,
1279 {
1280 let inner: &dyn Module = &**self;
1281 let mut modules: Vec<&dyn Module> = vec![inner];
1282 modules.extend(inner.children());
1283 modules
1284 }
1285
1286 /// Rooted at the inner module, for the same reason as [`Module::modules`].
1287 fn named_modules(&self) -> Vec<(String, &dyn Module)>
1288 where
1289 Self: Sized,
1290 {
1291 let inner: &dyn Module = &**self;
1292 let mut modules: Vec<(String, &dyn Module)> = vec![(String::new(), inner)];
1293 modules.extend(inner.named_children());
1294 modules
1295 }
1296}
1297
1298/// Implementation for mutable references to boxed trait objects
1299impl Module for &mut Box<dyn Module> {
1300 forward_all_module_methods!();
1301
1302 /// Delegates to the `Box<dyn Module>` impl: `**self` *is* a `Box`, which is
1303 /// `Sized`, so unlike the boxed case this one can simply forward.
1304 fn modules(&self) -> Vec<&dyn Module>
1305 where
1306 Self: Sized,
1307 {
1308 (**self).modules()
1309 }
1310
1311 /// Delegates to the `Box<dyn Module>` impl, as [`Module::modules`] does.
1312 fn named_modules(&self) -> Vec<(String, &dyn Module)>
1313 where
1314 Self: Sized,
1315 {
1316 (**self).named_modules()
1317 }
1318}