Skip to main content

torsh_nn/layers/normalization/
common.rs

1//! Common utilities and types for normalization layers
2//!
3//! This module provides shared functionality used across different normalization
4//! implementations including configuration types, utility functions, and common patterns.
5
6use parking_lot::RwLock;
7use std::sync::Arc;
8use torsh_core::error::Result;
9use torsh_tensor::{creation::*, Tensor};
10
11// Conditional imports for std/no_std compatibility
12
13#[cfg(not(feature = "std"))]
14use hashbrown::HashMap;
15
16/// Configuration for normalization layers
17#[derive(Debug, Clone)]
18pub struct NormalizationConfig {
19    /// Small constant added to variance for numerical stability
20    pub eps: f32,
21    /// Momentum for running statistics update
22    pub momentum: f32,
23    /// Whether to use learnable affine parameters (weight and bias)
24    pub affine: bool,
25    /// Whether to track running statistics for batch norm
26    pub track_running_stats: bool,
27}
28
29impl Default for NormalizationConfig {
30    fn default() -> Self {
31        Self {
32            eps: 1e-5,
33            momentum: 0.1,
34            affine: true,
35            track_running_stats: true,
36        }
37    }
38}
39
40impl NormalizationConfig {
41    /// Create configuration for training mode with tracking
42    pub fn training() -> Self {
43        Self::default()
44    }
45
46    /// Create configuration for inference mode without tracking
47    pub fn inference() -> Self {
48        Self {
49            track_running_stats: false,
50            ..Self::default()
51        }
52    }
53
54    /// Create configuration without learnable parameters
55    pub fn non_affine() -> Self {
56        Self {
57            affine: false,
58            ..Self::default()
59        }
60    }
61
62    /// Create configuration with custom epsilon for numerical stability
63    pub fn with_eps(eps: f32) -> Self {
64        Self {
65            eps,
66            ..Self::default()
67        }
68    }
69
70    /// Create configuration with custom momentum for running stats
71    pub fn with_momentum(momentum: f32) -> Self {
72        Self {
73            momentum,
74            ..Self::default()
75        }
76    }
77}
78
79/// Normalization statistics for tracking and analysis
80#[derive(Debug, Clone)]
81pub struct NormalizationStats {
82    pub mean: Tensor,
83    pub var: Tensor,
84    pub running_mean: Option<Tensor>,
85    pub running_var: Option<Tensor>,
86    pub num_batches_tracked: Option<Tensor>,
87}
88
89impl NormalizationStats {
90    /// Create new normalization statistics
91    pub fn new(num_features: usize, track_running: bool) -> Result<Self> {
92        let mean = zeros(&[num_features])?;
93        let var = ones(&[num_features])?;
94
95        let (running_mean, running_var, num_batches_tracked) = if track_running {
96            (
97                Some(zeros(&[num_features])?),
98                Some(ones(&[num_features])?),
99                Some(zeros(&[1])?),
100            )
101        } else {
102            (None, None, None)
103        };
104
105        Ok(Self {
106            mean,
107            var,
108            running_mean,
109            running_var,
110            num_batches_tracked,
111        })
112    }
113
114    /// Update running statistics
115    pub fn update_running_stats(
116        &mut self,
117        batch_mean: &Tensor,
118        batch_var: &Tensor,
119        momentum: f32,
120    ) -> Result<()> {
121        if let (Some(ref mut running_mean), Some(ref mut running_var)) =
122            (&mut self.running_mean, &mut self.running_var)
123        {
124            // running_mean = (1 - momentum) * running_mean + momentum * batch_mean
125            let one_minus_momentum = 1.0 - momentum;
126            *running_mean = running_mean
127                .mul_scalar(one_minus_momentum)?
128                .add(&batch_mean.mul_scalar(momentum)?)?;
129
130            // running_var = (1 - momentum) * running_var + momentum * batch_var
131            *running_var = running_var
132                .mul_scalar(one_minus_momentum)?
133                .add(&batch_var.mul_scalar(momentum)?)?;
134
135            // Increment batch counter
136            if let Some(ref mut num_batches) = self.num_batches_tracked {
137                *num_batches = num_batches.add_scalar(1.0)?;
138            }
139        }
140        Ok(())
141    }
142}
143
144/// Running statistics buffers for batch-normalization layers.
145///
146/// The buffers live behind `Arc<RwLock<Tensor>>` so a `forward(&self, ...)` can
147/// update them, and so the very same handles can be published through
148/// [`crate::Module::named_buffers`] — there is exactly one copy of the state,
149/// not a registered buffer plus a disconnected shadow.
150#[derive(Debug, Clone)]
151pub struct RunningStats {
152    running_mean: Arc<RwLock<Tensor>>,
153    running_var: Arc<RwLock<Tensor>>,
154    num_batches_tracked: Arc<RwLock<Tensor>>,
155}
156
157impl RunningStats {
158    /// Create zero-mean / unit-variance running statistics for `num_features`.
159    pub fn new(num_features: usize) -> Result<Self> {
160        Ok(Self {
161            running_mean: Arc::new(RwLock::new(zeros(&[num_features])?)),
162            running_var: Arc::new(RwLock::new(ones(&[num_features])?)),
163            num_batches_tracked: Arc::new(RwLock::new(zeros(&[1])?)),
164        })
165    }
166
167    /// Shared handle to the running mean buffer.
168    pub fn running_mean_handle(&self) -> Arc<RwLock<Tensor>> {
169        Arc::clone(&self.running_mean)
170    }
171
172    /// Shared handle to the running variance buffer.
173    pub fn running_var_handle(&self) -> Arc<RwLock<Tensor>> {
174        Arc::clone(&self.running_var)
175    }
176
177    /// Shared handle to the batch counter buffer.
178    pub fn num_batches_tracked_handle(&self) -> Arc<RwLock<Tensor>> {
179        Arc::clone(&self.num_batches_tracked)
180    }
181
182    /// Snapshot of the current running mean.
183    pub fn running_mean(&self) -> Tensor {
184        self.running_mean.read().clone()
185    }
186
187    /// Snapshot of the current running variance.
188    pub fn running_var(&self) -> Tensor {
189        self.running_var.read().clone()
190    }
191
192    /// Number of batches folded into the running statistics so far.
193    pub fn num_batches_tracked(&self) -> Result<f32> {
194        let counter = self.num_batches_tracked.read();
195        Ok(counter.to_vec()?.first().copied().unwrap_or(0.0))
196    }
197
198    /// Fold a batch into the running statistics.
199    ///
200    /// `batch_var` must be the **unbiased** batch variance, matching PyTorch,
201    /// which normalizes with the biased variance but tracks the unbiased one.
202    pub fn update(&self, batch_mean: &Tensor, batch_var: &Tensor, momentum: f32) -> Result<()> {
203        let one_minus_momentum = 1.0 - momentum;
204
205        {
206            let mut running_mean = self.running_mean.write();
207            *running_mean = running_mean
208                .mul_scalar(one_minus_momentum)?
209                .add(&batch_mean.mul_scalar(momentum)?)?;
210        }
211        {
212            let mut running_var = self.running_var.write();
213            *running_var = running_var
214                .mul_scalar(one_minus_momentum)?
215                .add(&batch_var.mul_scalar(momentum)?)?;
216        }
217        {
218            let mut counter = self.num_batches_tracked.write();
219            *counter = counter.add_scalar(1.0)?;
220        }
221
222        Ok(())
223    }
224}
225
226/// Convert a biased (population) variance into the unbiased (sample) variance.
227///
228/// `count` is the number of elements that contributed to each channel
229/// statistic. With a single sample the correction is undefined, so the biased
230/// value is returned unchanged.
231pub fn unbiased_variance(biased: &Tensor, count: usize) -> Result<Tensor> {
232    if count > 1 {
233        biased.mul_scalar(count as f32 / (count - 1) as f32)
234    } else {
235        Ok(biased.clone())
236    }
237}
238
239/// Common utility functions for normalization implementations
240pub mod utils {
241    use super::*;
242
243    /// Axes a per-channel statistic reduces over: the batch axis and every
244    /// spatial axis, never the channel axis.
245    fn channel_reduce_dims(rank: usize) -> Vec<usize> {
246        core::iter::once(0usize).chain(2..rank).collect()
247    }
248
249    /// Ranks for which a channel statistic is defined: `(N, C)`, `(N, C, H, W)`
250    /// and `(N, C, D, H, W)`.
251    fn validate_channel_rank(dims: &[usize]) -> Result<()> {
252        match dims.len() {
253            2 | 4 | 5 => Ok(()),
254            other => Err(torsh_core::error::TorshError::InvalidShape(format!(
255                "Unsupported input dimensions: {other}"
256            ))),
257        }
258    }
259
260    /// Channel-wise mean of an `NC...` tensor, **kept on the autograd graph**.
261    ///
262    /// This used to be a `to_vec()` loop feeding `Tensor::from_data`, which
263    /// returns a detached leaf: `backward()` then treated the batch mean as a
264    /// constant and every batch-normalization layer produced the gradient of a
265    /// plain affine rescaling instead of the real normalization Jacobian.
266    /// Reducing with `mean` records `Operation::SumDim`/`DivScalar`, so the
267    /// statistics are differentiated through exactly as PyTorch does.
268    pub fn compute_channel_mean(input: &Tensor) -> Result<Tensor> {
269        let input_shape = input.shape();
270        let dims = input_shape.dims();
271        validate_channel_rank(dims)?;
272
273        input.mean(Some(&channel_reduce_dims(dims.len())), false)
274    }
275
276    /// Channel-wise *biased* variance of an `NC...` tensor, kept on the autograd
277    /// graph.
278    ///
279    /// Computed as `E[(x - mean)²]` rather than the algebraically equivalent
280    /// `E[x²] - E[x]²`: the centered form both records a usable graph and avoids
281    /// the cancellation that the two-moment form suffers for large means.
282    pub fn compute_channel_variance(input: &Tensor, mean: &Tensor) -> Result<Tensor> {
283        let input_shape = input.shape();
284        let dims = input_shape.dims();
285        validate_channel_rank(dims)?;
286
287        let broadcast = channel_broadcast_shape(dims.len(), dims[1]);
288        let centered = input.sub(&mean.reshape(&broadcast)?)?;
289        centered
290            .pow_scalar(2.0)?
291            .mean(Some(&channel_reduce_dims(dims.len())), false)
292    }
293
294    /// Broadcast shape that lines a per-channel vector up with an `NC...` tensor.
295    ///
296    /// Public because every layer that applies per-channel affine parameters has
297    /// to reshape them *explicitly*: a bare trailing-axis broadcast silently
298    /// aligns `C` with the last axis whenever the two extents coincide.
299    pub fn channel_broadcast_shape(rank: usize, channels: usize) -> Vec<i32> {
300        let mut shape = vec![1i32; rank];
301        if rank >= 2 {
302            shape[1] = channels as i32;
303        }
304        shape
305    }
306
307    /// Copy a statistics tensor off the autograd graph.
308    ///
309    /// [`Tensor::detach`] clears `requires_grad` but keeps the recorded
310    /// operation, so a running-statistics buffer built from it would still hold
311    /// an `Arc` chain into every training batch it ever saw. Rebuilding from the
312    /// raw values drops that chain outright.
313    pub fn detached_statistic(stat: &Tensor) -> Result<Tensor> {
314        Tensor::from_data(stat.to_vec()?, stat.shape().dims().to_vec(), stat.device())
315    }
316
317    /// Per-instance normalization of an `NC...` tensor: every `(sample, channel)`
318    /// pair is normalized over its own spatial extent.
319    ///
320    /// Statistics stay on the autograd graph, and `weight`/`bias` are per-channel
321    /// vectors that are reshaped to `[1, C, 1, ...]` before broadcasting — never
322    /// left to trailing-axis alignment, which would scale the width axis instead
323    /// of the channel axis on an input whose width happens to equal `C`.
324    pub fn instance_normalize(
325        input: &Tensor,
326        weight: Option<&Tensor>,
327        bias: Option<&Tensor>,
328        eps: f32,
329    ) -> Result<Tensor> {
330        let input_shape = input.shape();
331        let dims = input_shape.dims();
332
333        if dims.len() < 2 {
334            return Err(torsh_core::error::TorshError::InvalidShape(format!(
335                "instance normalization expects an (N, C, ...) tensor, got {dims:?}"
336            )));
337        }
338
339        let instances = dims[0] * dims[1];
340        let spatial: usize = dims[2..].iter().product::<usize>().max(1);
341
342        let flat = input.reshape(&[instances as i32, spatial as i32])?;
343        let mean = flat.mean(Some(&[1]), true)?;
344        let centered = flat.sub(&mean)?;
345        let variance = centered.pow_scalar(2.0)?.mean(Some(&[1]), true)?;
346        let std = variance.add_scalar(eps)?.sqrt()?;
347
348        let original: Vec<i32> = dims.iter().map(|&d| d as i32).collect();
349        let mut normalized = centered.div(&std)?.reshape(&original)?;
350
351        let broadcast = channel_broadcast_shape(dims.len(), dims[1]);
352        if let Some(w) = weight {
353            normalized = normalized.mul(&w.reshape(&broadcast)?)?;
354        }
355        if let Some(b) = bias {
356            normalized = normalized.add(&b.reshape(&broadcast)?)?;
357        }
358
359        Ok(normalized)
360    }
361
362    /// Apply a *per-channel* normalization: `(x - mean) / sqrt(var + eps) * weight + bias`.
363    ///
364    /// `mean`, `var`, `weight` and `bias` are 1-D tensors of length `C` and are
365    /// explicitly reshaped to `[1, C, 1, ...]` before broadcasting. This is what
366    /// distinguishes it from [`apply_normalization`], whose generic trailing-axis
367    /// broadcast silently aligns `C` with the *last* axis whenever the two happen
368    /// to have the same length (e.g. an `[N, 2, 2, 2]` input).
369    pub fn apply_channel_normalization(
370        input: &Tensor,
371        mean: &Tensor,
372        var: &Tensor,
373        weight: Option<&Tensor>,
374        bias: Option<&Tensor>,
375        eps: f32,
376    ) -> Result<Tensor> {
377        let input_shape = input.shape();
378        let dims = input_shape.dims();
379
380        if dims.len() < 2 {
381            return Err(torsh_core::error::TorshError::InvalidShape(format!(
382                "channel normalization expects an (N, C, ...) tensor, got {dims:?}"
383            )));
384        }
385
386        let channels = dims[1];
387        let broadcast = channel_broadcast_shape(dims.len(), channels);
388
389        let inv_std = var.add_scalar(eps)?.sqrt()?;
390        let centered = input.sub(&mean.reshape(&broadcast)?)?;
391        let mut normalized = centered.div(&inv_std.reshape(&broadcast)?)?;
392
393        if let Some(w) = weight {
394            normalized = normalized.mul(&w.reshape(&broadcast)?)?;
395        }
396        if let Some(b) = bias {
397            normalized = normalized.add(&b.reshape(&broadcast)?)?;
398        }
399
400        Ok(normalized)
401    }
402
403    /// Apply a per-channel affine map `x * scale + shift`.
404    ///
405    /// `scale` and `shift` are *constants*: this folds a whole normalization
406    /// into two per-channel numbers, which necessarily severs the statistics
407    /// from the autograd graph. Batch renormalization used to be written this
408    /// way and produced a gradient that ignored `mu_B`/`sigma_B` entirely; it
409    /// now composes the expression out of recording ops instead. Reach for this
410    /// helper only where the scale really is a constant.
411    pub fn apply_channel_affine(input: &Tensor, scale: &[f32], shift: &[f32]) -> Result<Tensor> {
412        let input_shape = input.shape();
413        let dims = input_shape.dims();
414
415        if dims.len() < 2 {
416            return Err(torsh_core::error::TorshError::InvalidShape(format!(
417                "channel affine expects an (N, C, ...) tensor, got {dims:?}"
418            )));
419        }
420
421        let channels = dims[1];
422        if scale.len() != channels || shift.len() != channels {
423            return Err(torsh_core::error::TorshError::InvalidShape(format!(
424                "expected {channels} scale/shift values, got {} and {}",
425                scale.len(),
426                shift.len()
427            )));
428        }
429
430        let broadcast: Vec<usize> = channel_broadcast_shape(dims.len(), channels)
431            .into_iter()
432            .map(|d| d as usize)
433            .collect();
434        let scale_tensor = Tensor::from_data(scale.to_vec(), broadcast.clone(), input.device())?;
435        let shift_tensor = Tensor::from_data(shift.to_vec(), broadcast, input.device())?;
436
437        input.mul(&scale_tensor)?.add(&shift_tensor)
438    }
439
440    /// Apply normalization transformation: (x - mean) / sqrt(var + eps) * weight + bias
441    ///
442    /// # Hazard
443    ///
444    /// `weight`/`bias` are broadcast by generic *trailing-axis* alignment, which
445    /// silently lines a per-channel vector up with the **last** axis whenever the
446    /// two extents coincide — an `[N, 3, H, 3]` input scales the width, not the
447    /// channels. Layers whose affine parameters are per-channel must reshape them
448    /// with [`channel_broadcast_shape`] instead (or use
449    /// [`apply_channel_normalization`] / [`instance_normalize`], which do it for
450    /// you). This entry point is only correct where the parameters really are
451    /// trailing-axis shaped, as `LayerNorm`'s are.
452    pub fn apply_normalization(
453        input: &Tensor,
454        mean: &Tensor,
455        var: &Tensor,
456        weight: Option<&Tensor>,
457        bias: Option<&Tensor>,
458        eps: f32,
459    ) -> Result<Tensor> {
460        // Try to use the tensor's built-in broadcasting first
461        // If that fails, we can implement manual broadcasting
462        match try_apply_normalization_simple(input, mean, var, weight, bias, eps) {
463            Ok(result) => Ok(result),
464            Err(_) => {
465                // Fall back to manual broadcasting if simple approach fails
466                apply_normalization_with_broadcasting(input, mean, var, weight, bias, eps)
467            }
468        }
469    }
470
471    /// Simple approach that relies on built-in broadcasting
472    fn try_apply_normalization_simple(
473        input: &Tensor,
474        mean: &Tensor,
475        var: &Tensor,
476        weight: Option<&Tensor>,
477        bias: Option<&Tensor>,
478        eps: f32,
479    ) -> Result<Tensor> {
480        // Subtract mean
481        let centered = input.sub(mean)?;
482
483        // Compute standard deviation
484        let std = var.add_scalar(eps)?.sqrt()?;
485
486        // Normalize
487        let mut normalized = centered.div(&std)?;
488
489        // Apply learnable parameters if provided
490        if let Some(w) = weight {
491            normalized = normalized.mul(w)?;
492        }
493
494        if let Some(b) = bias {
495            normalized = normalized.add(b)?;
496        }
497
498        Ok(normalized)
499    }
500
501    /// Manual broadcasting approach for when simple broadcasting doesn't work
502    fn apply_normalization_with_broadcasting(
503        input: &Tensor,
504        mean: &Tensor,
505        var: &Tensor,
506        weight: Option<&Tensor>,
507        bias: Option<&Tensor>,
508        eps: f32,
509    ) -> Result<Tensor> {
510        let input_shape = input.shape();
511        let input_dims = input_shape.dims();
512        let mean_shape = mean.shape();
513        let mean_dims = mean_shape.dims();
514
515        // For broadcasting: if mean/var are 1D [C] and input is 4D [N,C,H,W],
516        // we need to make mean/var into [1,C,1,1] for proper broadcasting
517        let (broadcast_mean, broadcast_var) = if input_dims.len() == 4 && mean_dims.len() == 1 {
518            let channels = mean_dims[0];
519            let mean_broadcast = mean.reshape(&[1i32, channels as i32, 1i32, 1i32])?;
520            let var_broadcast = var.reshape(&[1i32, channels as i32, 1i32, 1i32])?;
521            (mean_broadcast, var_broadcast)
522        } else if input_dims.len() == 2 && mean_dims.len() == 1 {
523            let channels = mean_dims[0];
524            let mean_broadcast = mean.reshape(&[1i32, channels as i32])?;
525            let var_broadcast = var.reshape(&[1i32, channels as i32])?;
526            (mean_broadcast, var_broadcast)
527        } else {
528            // Already compatible shapes
529            (mean.clone(), var.clone())
530        };
531
532        // Subtract mean
533        let centered = input.sub(&broadcast_mean)?;
534
535        // Compute standard deviation
536        let std = broadcast_var.add_scalar(eps)?.sqrt()?;
537
538        // Normalize
539        let mut normalized = centered.div(&std)?;
540
541        // Apply learnable parameters if provided with proper broadcasting
542        if let Some(w) = weight {
543            let weight_shape = w.shape();
544            let weight_dims = weight_shape.dims();
545            let broadcast_weight = if input_dims.len() == 4 && weight_dims.len() == 1 {
546                let channels = weight_dims[0];
547                w.reshape(&[1i32, channels as i32, 1i32, 1i32])?
548            } else if input_dims.len() == 2 && weight_dims.len() == 1 {
549                let channels = weight_dims[0];
550                w.reshape(&[1i32, channels as i32])?
551            } else {
552                w.clone()
553            };
554            normalized = normalized.mul(&broadcast_weight)?;
555        }
556
557        if let Some(b) = bias {
558            let bias_shape = b.shape();
559            let bias_dims = bias_shape.dims();
560            let broadcast_bias = if input_dims.len() == 4 && bias_dims.len() == 1 {
561                let channels = bias_dims[0];
562                b.reshape(&[1i32, channels as i32, 1i32, 1i32])?
563            } else if input_dims.len() == 2 && bias_dims.len() == 1 {
564                let channels = bias_dims[0];
565                b.reshape(&[1i32, channels as i32])?
566            } else {
567                b.clone()
568            };
569            normalized = normalized.add(&broadcast_bias)?;
570        }
571
572        Ok(normalized)
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn test_normalization_config() {
582        let config = NormalizationConfig::default();
583        assert_eq!(config.eps, 1e-5);
584        assert_eq!(config.momentum, 0.1);
585        assert!(config.affine);
586        assert!(config.track_running_stats);
587
588        let inference_config = NormalizationConfig::inference();
589        assert!(!inference_config.track_running_stats);
590
591        let non_affine_config = NormalizationConfig::non_affine();
592        assert!(!non_affine_config.affine);
593    }
594
595    #[test]
596    fn test_normalization_stats_creation() {
597        let stats = NormalizationStats::new(10, true).expect("Normalization Stats should succeed");
598        assert!(stats.running_mean.is_some());
599        assert!(stats.running_var.is_some());
600        assert!(stats.num_batches_tracked.is_some());
601
602        let stats_no_tracking =
603            NormalizationStats::new(10, false).expect("Normalization Stats should succeed");
604        assert!(stats_no_tracking.running_mean.is_none());
605        assert!(stats_no_tracking.running_var.is_none());
606        assert!(stats_no_tracking.num_batches_tracked.is_none());
607    }
608
609    #[test]
610    fn test_channel_mean_computation() {
611        // Test 2D case (batch_size=2, channels=3)
612        let input = Tensor::from_data(
613            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
614            vec![2, 3],
615            torsh_core::device::DeviceType::Cpu,
616        )
617        .expect("operation should succeed");
618        let mean = utils::compute_channel_mean(&input).expect("utils should succeed");
619        let expected_mean = vec![2.5, 3.5, 4.5]; // Channel-wise means
620        let mean_data = mean
621            .to_vec()
622            .expect("tensor to vec conversion should succeed");
623
624        for (i, &expected) in expected_mean.iter().enumerate() {
625            assert!((mean_data[i] - expected).abs() < 1e-6);
626        }
627    }
628}