Skip to main content

torsh_nn/core/
module_ext.rs

1//! Module trait ergonomic extensions
2//!
3//! This module provides additional ergonomic helpers and utilities for the Module trait,
4//! following Rust best practices for trait extension patterns.
5
6use crate::Module;
7use torsh_core::device::DeviceType;
8use torsh_core::error::Result;
9use torsh_tensor::Tensor;
10
11#[cfg(feature = "std")]
12use std::collections::HashMap;
13
14#[cfg(not(feature = "std"))]
15use hashbrown::HashMap;
16
17/// Extension trait providing additional ergonomic methods for Module
18///
19/// This trait is automatically implemented for all types that implement Module,
20/// providing additional convenience methods without requiring changes to existing code.
21///
22/// # Design Philosophy
23///
24/// This extension follows Rust's extension trait pattern to:
25/// - Add functionality without breaking backward compatibility
26/// - Keep the core Module trait focused on essential methods
27/// - Provide advanced features for users who need them
28/// - Enable fluent/builder-style APIs
29pub trait ModuleExt: Module {
30    // === Fluent API / Builder Pattern Methods ===
31
32    /// Chain forward pass with a transformation function
33    ///
34    /// This enables functional-style chaining of operations.
35    ///
36    /// # Arguments
37    /// * `input` - Input tensor
38    /// * `f` - Transformation function to apply to output
39    ///
40    /// # Returns
41    /// * `Result<Tensor>` - Transformed output
42    ///
43    /// # Example
44    /// ```ignore
45    /// let output = layer.and_then(&input, |x| x.relu())?;
46    /// ```
47    fn and_then<F>(&self, input: &Tensor, f: F) -> Result<Tensor>
48    where
49        F: FnOnce(Tensor) -> Result<Tensor>,
50    {
51        let output = self.forward(input)?;
52        f(output)
53    }
54
55    /// Apply module and map the output with a function
56    ///
57    /// Similar to `and_then` but for non-failable transformations.
58    ///
59    /// # Arguments
60    /// * `input` - Input tensor
61    /// * `f` - Mapping function
62    ///
63    /// # Returns
64    /// * `Result<Tensor>` - Mapped output
65    fn map<F>(&self, input: &Tensor, f: F) -> Result<Tensor>
66    where
67        F: FnOnce(Tensor) -> Tensor,
68    {
69        let output = self.forward(input)?;
70        Ok(f(output))
71    }
72
73    /// Forward pass with input transformation
74    ///
75    /// Apply a transformation to the input before forwarding.
76    ///
77    /// # Arguments
78    /// * `input` - Input tensor
79    /// * `f` - Input transformation function
80    ///
81    /// # Returns
82    /// * `Result<Tensor>` - Module output
83    fn with_input<F>(&self, input: &Tensor, f: F) -> Result<Tensor>
84    where
85        F: FnOnce(&Tensor) -> Result<Tensor>,
86    {
87        let transformed = f(input)?;
88        self.forward(&transformed)
89    }
90
91    // === Inspection and Debugging Methods ===
92
93    /// Get human-readable summary of the module
94    ///
95    /// # Returns
96    /// * `String` - Formatted module summary
97    fn summary(&self) -> String {
98        let info = self.module_info();
99        format!(
100            "Module: {}\n\
101             Training: {}\n\
102             Parameters: {} ({} trainable)\n\
103             Memory: {:.2} MB\n\
104             Children: {}",
105            info.name,
106            info.training,
107            info.parameter_count,
108            info.trainable_parameter_count,
109            info.memory_usage_bytes as f64 / (1024.0 * 1024.0),
110            info.children_count
111        )
112    }
113
114    /// Print module summary to stdout
115    fn print_summary(&self) {
116        println!("{}", self.summary());
117    }
118
119    /// Get parameter statistics
120    ///
121    /// # Returns
122    /// * `ParameterStats` - Statistical information about parameters
123    fn parameter_stats(&self) -> ParameterStats {
124        let params = self.all_parameters();
125        let mut total_params = 0;
126        let mut trainable_params = 0;
127        let mut frozen_params = 0;
128        let mut total_memory = 0;
129
130        for param in params.values() {
131            let numel = param.numel().unwrap_or(0);
132            total_params += numel;
133            total_memory += numel * 4; // Assume f32
134
135            if param.requires_grad() {
136                trainable_params += numel;
137            } else {
138                frozen_params += numel;
139            }
140        }
141
142        ParameterStats {
143            total_parameters: total_params,
144            trainable_parameters: trainable_params,
145            frozen_parameters: frozen_params,
146            total_memory_bytes: total_memory,
147            parameter_count: params.len(),
148        }
149    }
150
151    /// Check if module has NaN or Inf in parameters
152    ///
153    /// # Returns
154    /// * `bool` - true if all parameters are finite
155    fn has_finite_parameters(&self) -> bool {
156        self.all_parameters()
157            .values()
158            .all(|p| p.is_finite().unwrap_or(false))
159    }
160
161    /// Get list of parameter names
162    ///
163    /// # Returns
164    /// * `Vec<String>` - Sorted list of parameter names
165    fn parameter_names(&self) -> Vec<String> {
166        let mut names: Vec<String> = self.all_named_parameters().keys().cloned().collect();
167        names.sort();
168        names
169    }
170
171    /// Get parameter by name
172    ///
173    /// # Arguments
174    /// * `name` - Parameter name
175    ///
176    /// # Returns
177    /// * `Option<Parameter>` - Parameter if found
178    fn get_parameter(&self, name: &str) -> Option<crate::Parameter> {
179        self.all_named_parameters().get(name).cloned()
180    }
181
182    // === Training Utilities ===
183
184    /// Freeze parameters whose name contains `pattern` (sets `requires_grad = false`)
185    ///
186    /// Pass an empty pattern (`""`) to freeze every parameter, since every name
187    /// contains the empty string.
188    ///
189    /// # Arguments
190    /// * `pattern` - Substring to match against parameter names
191    ///
192    /// # Returns
193    /// * `usize` - Number of parameters frozen
194    ///
195    /// # Note
196    /// A parameter's `requires_grad` flag is shared across clones (see
197    /// [`crate::Parameter`]), so updating the by-value clones handed out by
198    /// [`Module::all_named_parameters`] is observable on the module's own
199    /// parameters and on any subsequently fetched copies.
200    fn freeze_matching(&mut self, pattern: &str) -> usize {
201        let mut count = 0;
202        for (name, param) in self.all_named_parameters() {
203            if name.contains(pattern) {
204                param.set_requires_grad(false);
205                count += 1;
206            }
207        }
208        count
209    }
210
211    /// Unfreeze parameters whose name contains `pattern` (sets `requires_grad = true`)
212    ///
213    /// Pass an empty pattern (`""`) to unfreeze every parameter, since every name
214    /// contains the empty string.
215    ///
216    /// # Arguments
217    /// * `pattern` - Substring to match against parameter names
218    ///
219    /// # Returns
220    /// * `usize` - Number of parameters unfrozen
221    ///
222    /// # Note
223    /// See [`ModuleExt::freeze_matching`] for why mutating the by-value parameter
224    /// clones is observable on the owning module.
225    fn unfreeze_matching(&mut self, pattern: &str) -> usize {
226        let mut count = 0;
227        for (name, param) in self.all_named_parameters() {
228            if name.contains(pattern) {
229                param.set_requires_grad(true);
230                count += 1;
231            }
232        }
233        count
234    }
235
236    /// Get list of frozen parameters
237    ///
238    /// # Returns
239    /// * `Vec<String>` - Names of frozen parameters
240    fn frozen_parameters(&self) -> Vec<String> {
241        self.all_named_parameters()
242            .into_iter()
243            .filter(|(_, p)| !p.requires_grad())
244            .map(|(name, _)| name)
245            .collect()
246    }
247
248    /// Get list of trainable parameters
249    ///
250    /// # Returns
251    /// * `Vec<String>` - Names of trainable parameters
252    fn trainable_parameters(&self) -> Vec<String> {
253        self.all_named_parameters()
254            .into_iter()
255            .filter(|(_, p)| p.requires_grad())
256            .map(|(name, _)| name)
257            .collect()
258    }
259
260    // === Advanced Operations ===
261
262    /// Clone module parameters into a new state dict
263    ///
264    /// # Returns
265    /// * `HashMap<String, Tensor>` - Cloned state dictionary
266    fn clone_state_dict(&self) -> HashMap<String, Tensor> {
267        self.state_dict()
268    }
269
270    /// Apply a function to all parameters
271    ///
272    /// # Arguments
273    /// * `f` - Function to apply to each parameter
274    fn apply_to_parameters<F>(&self, mut f: F)
275    where
276        F: FnMut(&str, &crate::Parameter),
277    {
278        for (name, param) in self.all_named_parameters() {
279            f(&name, &param);
280        }
281    }
282
283    /// Count parameters by layer type
284    ///
285    /// # Returns
286    /// * `HashMap<String, usize>` - Parameter count per layer type
287    fn parameters_by_type(&self) -> HashMap<String, usize> {
288        let mut counts = HashMap::new();
289
290        for (name, param) in self.all_named_parameters() {
291            // Extract layer type from name (first component)
292            let layer_type = name.split('.').next().unwrap_or("unknown").to_string();
293
294            let numel = param.numel().unwrap_or(0);
295            *counts.entry(layer_type).or_insert(0) += numel;
296        }
297
298        counts
299    }
300
301    /// Validate module configuration
302    ///
303    /// Performs comprehensive validation of module state.
304    ///
305    /// # Returns
306    /// * `Result<ValidationReport>` - Validation results
307    fn validate(&self) -> Result<ValidationReport> {
308        let mut report = ValidationReport::default();
309
310        // Check for parameters
311        if !self.has_parameters() {
312            report.warnings.push("Module has no parameters".to_string());
313        }
314
315        // Check for finite parameters
316        if !self.has_finite_parameters() {
317            report
318                .errors
319                .push("Module has non-finite parameters (NaN or Inf)".to_string());
320        }
321
322        // Check memory usage
323        let memory_mb = self.memory_usage_mb();
324        if memory_mb > 1024.0 {
325            report
326                .warnings
327                .push(format!("Large memory usage: {:.2} GB", memory_mb / 1024.0));
328        }
329
330        // Check parameter count
331        let param_count = self.num_parameters();
332        if param_count > 100_000_000 {
333            report
334                .warnings
335                .push(format!("Very large model: {} parameters", param_count));
336        }
337
338        report.is_valid = report.errors.is_empty();
339        Ok(report)
340    }
341
342    /// Get the device the module's parameters reside on
343    ///
344    /// # Returns
345    /// * `Option<DeviceType>` - The device of the module's parameters, or `None`
346    ///   only when the module genuinely has no parameters (recursively).
347    ///
348    /// # Note
349    /// The device is read from the actual underlying parameter tensors. If a
350    /// module's parameters span multiple devices, the device of the first
351    /// parameter encountered is reported.
352    fn device(&self) -> Option<DeviceType> {
353        self.all_parameters().values().next().map(|p| p.device())
354    }
355
356    /// Check if all parameters are on CPU
357    ///
358    /// # Returns
359    /// * `bool` - true if all parameters on CPU
360    fn is_cpu(&self) -> bool {
361        self.device() == Some(DeviceType::Cpu)
362    }
363
364    /// Check if all parameters are on CUDA device
365    ///
366    /// # Returns
367    /// * `bool` - true if all parameters on CUDA
368    fn is_cuda(&self) -> bool {
369        matches!(self.device(), Some(DeviceType::Cuda(_)))
370    }
371}
372
373// Automatically implement ModuleExt for all types that implement Module
374impl<T: Module + ?Sized> ModuleExt for T {}
375
376// === Supporting Types ===
377
378/// Parameter statistics for a module
379#[derive(Debug, Clone)]
380pub struct ParameterStats {
381    /// Total number of parameter elements
382    pub total_parameters: usize,
383    /// Number of trainable parameter elements
384    pub trainable_parameters: usize,
385    /// Number of frozen parameter elements
386    pub frozen_parameters: usize,
387    /// Total memory usage in bytes
388    pub total_memory_bytes: usize,
389    /// Number of distinct parameters
390    pub parameter_count: usize,
391}
392
393impl ParameterStats {
394    /// Get memory usage in megabytes
395    pub fn memory_mb(&self) -> f64 {
396        self.total_memory_bytes as f64 / (1024.0 * 1024.0)
397    }
398
399    /// Get memory usage in gigabytes
400    pub fn memory_gb(&self) -> f64 {
401        self.memory_mb() / 1024.0
402    }
403
404    /// Get percentage of parameters that are trainable
405    pub fn trainable_percentage(&self) -> f64 {
406        if self.total_parameters == 0 {
407            0.0
408        } else {
409            (self.trainable_parameters as f64 / self.total_parameters as f64) * 100.0
410        }
411    }
412}
413
414/// Validation report for a module
415#[derive(Debug, Clone, Default)]
416pub struct ValidationReport {
417    /// Whether the module is valid
418    pub is_valid: bool,
419    /// List of errors found
420    pub errors: Vec<String>,
421    /// List of warnings
422    pub warnings: Vec<String>,
423}
424
425impl ValidationReport {
426    /// Check if validation passed without errors
427    pub fn passed(&self) -> bool {
428        self.is_valid && self.errors.is_empty()
429    }
430
431    /// Get total number of issues (errors + warnings)
432    pub fn issue_count(&self) -> usize {
433        self.errors.len() + self.warnings.len()
434    }
435
436    /// Format as human-readable string
437    pub fn format(&self) -> String {
438        let mut result = String::new();
439
440        result.push_str(&format!(
441            "Validation: {}\n",
442            if self.is_valid { "PASSED" } else { "FAILED" }
443        ));
444
445        if !self.errors.is_empty() {
446            result.push_str("\nErrors:\n");
447            for error in &self.errors {
448                result.push_str(&format!("  - {}\n", error));
449            }
450        }
451
452        if !self.warnings.is_empty() {
453            result.push_str("\nWarnings:\n");
454            for warning in &self.warnings {
455                result.push_str(&format!("  - {}\n", warning));
456            }
457        }
458
459        result
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::ModuleExt;
466    use crate::layers::Linear;
467    use crate::Module;
468    use torsh_core::device::DeviceType;
469    use torsh_core::error::Result;
470    use torsh_tensor::Tensor;
471
472    /// A module that genuinely owns no parameters, used to verify that `device()`
473    /// honestly reports `None` instead of a fabricated default.
474    struct EmptyModule;
475
476    impl Module for EmptyModule {
477        fn forward(&self, input: &Tensor) -> Result<Tensor> {
478            Ok(input.clone())
479        }
480    }
481
482    #[test]
483    fn test_freeze_unfreeze_sets_requires_grad() {
484        let mut linear = Linear::new(4, 3, true);
485
486        // A Linear(.., bias = true) layer exposes exactly two parameters.
487        let param_count = linear.all_parameters().len();
488        assert_eq!(
489            param_count, 2,
490            "Linear with bias should expose weight + bias"
491        );
492
493        // Freshly created parameters must require gradients.
494        assert!(
495            linear.all_parameters().values().all(|p| p.requires_grad()),
496            "new Linear parameters should require grad"
497        );
498
499        // Freeze every parameter (empty pattern matches all names).
500        let frozen = linear.freeze_matching("");
501        assert_eq!(
502            frozen, param_count,
503            "freeze_matching(\"\") must touch every parameter"
504        );
505
506        // The freeze must be observable on a *fresh* read of the parameters,
507        // proving the flag really mutated shared state (not a throwaway clone).
508        assert!(
509            linear.all_parameters().values().all(|p| !p.requires_grad()),
510            "after freeze no parameter may require grad"
511        );
512        assert_eq!(
513            linear.frozen_parameters().len(),
514            param_count,
515            "all parameters should be reported as frozen"
516        );
517        assert!(
518            linear.trainable_parameters().is_empty(),
519            "no parameter should be trainable after freeze"
520        );
521
522        // Unfreeze every parameter and verify it sticks.
523        let unfrozen = linear.unfreeze_matching("");
524        assert_eq!(unfrozen, param_count);
525        assert!(
526            linear.all_parameters().values().all(|p| p.requires_grad()),
527            "after unfreeze every parameter must require grad"
528        );
529        assert!(linear.frozen_parameters().is_empty());
530    }
531
532    #[test]
533    fn test_freeze_matching_only_affects_matching_names() {
534        let mut linear = Linear::new(4, 3, true);
535
536        // Freeze only the bias; the weight must stay trainable.
537        let frozen = linear.freeze_matching("bias");
538        assert_eq!(frozen, 1, "only the bias parameter should match");
539
540        let params = linear.all_named_parameters();
541        assert!(
542            !params["bias"].requires_grad(),
543            "bias must be frozen after freeze_matching(\"bias\")"
544        );
545        assert!(
546            params["weight"].requires_grad(),
547            "weight must remain trainable"
548        );
549    }
550
551    #[test]
552    fn test_device_reports_real_parameter_device() {
553        let linear = Linear::new(8, 5, true);
554
555        // Parameters are created on CPU, so the module device must be CPU.
556        assert_eq!(linear.device(), Some(DeviceType::Cpu));
557        assert!(linear.is_cpu());
558        assert!(!linear.is_cuda());
559    }
560
561    #[test]
562    fn test_device_is_none_without_parameters() {
563        let empty = EmptyModule;
564        // A parameter-less module must honestly report None, never a fake device.
565        assert_eq!(empty.device(), None);
566        assert!(!empty.is_cpu());
567        assert!(!empty.is_cuda());
568    }
569}