Skip to main content

ferrotensor_trainer/
scaler.rs

1//! Dynamic Loss Scaling for Mixed Precision Training
2//!
3//! Prevents gradient underflow/overflow during FP16 training by dynamically
4//! adjusting the loss scale factor.
5
6use ferrotensor_backend::{ComputeEngine, UnifiedTensor, WgpuContext};
7
8/// Dynamic gradient scaler for FP16 mixed precision training.
9///
10/// Scales the loss to prevent gradient underflow in FP16, then unscales
11/// gradients before the optimizer step. Automatically adjusts the scale
12/// based on whether gradients contain inf/nan.
13///
14/// # Example
15/// ```no_run
16/// # use ferrotensor_backend::{ComputeEngine, UnifiedTensor, WgpuContext};
17/// # use ferrotensor_trainer::GradScaler;
18/// # let ctx = pollster::block_on(WgpuContext::new());
19/// # let engine = ComputeEngine::new(&ctx);
20/// # let loss = UnifiedTensor::zeros(&ctx, &[1]);
21/// # let gradients: Vec<UnifiedTensor> = Vec::new();
22/// let mut scaler = GradScaler::new();
23///
24/// // Scale the loss in place, then run the backward pass on it.
25/// scaler.scale_loss(&loss, &ctx, &engine);
26/// // ... backward pass fills `gradients` ...
27///
28/// // Unscale and skip the optimizer step if any gradient overflowed.
29/// if scaler.unscale_and_check(&gradients, &ctx, &engine) {
30///     // optimizer.step(...)
31/// }
32/// scaler.update();
33/// ```
34pub struct GradScaler {
35    /// Current loss scale factor (starts high, e.g., 65536)
36    scale: f32,
37    /// Factor to grow scale when no overflow occurs
38    growth_factor: f32,
39    /// Factor to shrink scale when overflow occurs  
40    backoff_factor: f32,
41    /// Number of consecutive successful steps before growing
42    growth_interval: u32,
43    /// Counter of consecutive successful steps
44    consecutive_good_steps: u32,
45    /// Whether overflow was detected in the current step
46    found_overflow: bool,
47    /// Whether the scaler is enabled
48    enabled: bool,
49}
50
51/// Serialized [`GradScaler`] state.
52#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
53pub struct GradScalerCheckpoint {
54    /// Current loss scale.
55    pub scale: f32,
56    /// Factor applied after `growth_interval` good steps.
57    pub growth_factor: f32,
58    /// Factor applied after an overflow.
59    pub backoff_factor: f32,
60    /// Good steps between scale increases.
61    pub growth_interval: u32,
62    /// Good steps since the last change.
63    pub consecutive_good_steps: u32,
64    /// Whether the last step overflowed.
65    pub found_overflow: bool,
66    /// Whether scaling is on.
67    pub enabled: bool,
68}
69
70impl GradScaler {
71    /// Create a new GradScaler with default settings.
72    ///
73    /// Default: scale=65536, growth_factor=2.0, backoff_factor=0.5, growth_interval=2000
74    pub fn new() -> Self {
75        Self {
76            scale: 65536.0, // 2^16, common starting point
77            growth_factor: 2.0,
78            backoff_factor: 0.5,
79            growth_interval: 2000,
80            consecutive_good_steps: 0,
81            found_overflow: false,
82            enabled: true,
83        }
84    }
85
86    /// Create a GradScaler with custom parameters.
87    pub fn with_params(
88        init_scale: f32,
89        growth_factor: f32,
90        backoff_factor: f32,
91        growth_interval: u32,
92    ) -> Self {
93        Self {
94            scale: init_scale,
95            growth_factor,
96            backoff_factor,
97            growth_interval,
98            consecutive_good_steps: 0,
99            found_overflow: false,
100            enabled: true,
101        }
102    }
103
104    /// Get current scale factor.
105    pub fn scale_factor(&self) -> f32 {
106        if self.enabled {
107            self.scale
108        } else {
109            1.0
110        }
111    }
112
113    /// Scale the loss value before backward pass.
114    ///
115    /// Scales `loss` in place (no new tensor is allocated).
116    pub fn scale_loss(&self, loss: &UnifiedTensor, ctx: &WgpuContext, engine: &ComputeEngine) {
117        if !self.enabled {
118            return;
119        }
120
121        // Scale loss in-place using gradient_scale kernel
122        engine.gradient_scale(ctx, loss, self.scale);
123    }
124
125    /// Unscale gradients and check for inf/nan.
126    ///
127    /// Returns true if gradients are valid (no overflow), false otherwise.
128    /// If false, you should skip the optimizer step.
129    pub fn unscale_and_check(
130        &mut self,
131        gradients: &[UnifiedTensor],
132        ctx: &WgpuContext,
133        engine: &ComputeEngine,
134    ) -> bool {
135        if !self.enabled {
136            return true;
137        }
138
139        let inv_scale = 1.0 / self.scale;
140        let mut has_overflow = false;
141
142        for grad in gradients {
143            // Unscale the gradient using gradient_scale kernel
144            engine.gradient_scale(ctx, grad, inv_scale);
145
146            // Check for inf/nan (CPU readback - sync point)
147            let data = pollster::block_on(grad.to_vec(ctx));
148            if data.iter().any(|&x| x.is_infinite() || x.is_nan()) {
149                has_overflow = true;
150                break;
151            }
152        }
153
154        self.found_overflow = has_overflow;
155        !has_overflow
156    }
157
158    /// Update the scale factor based on whether overflow occurred.
159    ///
160    /// Call this at the end of each training step.
161    pub fn update(&mut self) {
162        if !self.enabled {
163            return;
164        }
165
166        if self.found_overflow {
167            // Reduce scale on overflow
168            self.scale *= self.backoff_factor;
169            self.consecutive_good_steps = 0;
170
171            // Clamp to minimum scale
172            if self.scale < 1.0 {
173                self.scale = 1.0;
174            }
175        } else {
176            // Increment good step counter
177            self.consecutive_good_steps += 1;
178
179            // Grow scale after enough good steps
180            if self.consecutive_good_steps >= self.growth_interval {
181                self.scale *= self.growth_factor;
182                self.consecutive_good_steps = 0;
183
184                // Clamp to maximum scale
185                if self.scale > 65536.0 * 256.0 {
186                    self.scale = 65536.0 * 256.0;
187                }
188            }
189        }
190
191        // Reset overflow flag for next step
192        self.found_overflow = false;
193    }
194
195    /// Check if overflow was detected in the last step.
196    pub fn had_overflow(&self) -> bool {
197        self.found_overflow
198    }
199
200    /// Enable or disable the scaler.
201    pub fn set_enabled(&mut self, enabled: bool) {
202        self.enabled = enabled;
203    }
204
205    /// Check if scaler is enabled.
206    pub fn is_enabled(&self) -> bool {
207        self.enabled
208    }
209
210    /// Get statistics for logging.
211    pub fn stats(&self) -> GradScalerStats {
212        GradScalerStats {
213            scale: self.scale,
214            consecutive_good_steps: self.consecutive_good_steps,
215            enabled: self.enabled,
216        }
217    }
218
219    /// Snapshot the scaler for a checkpoint.
220    pub fn checkpoint(&self) -> GradScalerCheckpoint {
221        GradScalerCheckpoint {
222            scale: self.scale,
223            growth_factor: self.growth_factor,
224            backoff_factor: self.backoff_factor,
225            growth_interval: self.growth_interval,
226            consecutive_good_steps: self.consecutive_good_steps,
227            found_overflow: self.found_overflow,
228            enabled: self.enabled,
229        }
230    }
231
232    /// Restore a snapshot taken by [`checkpoint`](Self::checkpoint).
233    pub fn load_checkpoint(&mut self, checkpoint: &GradScalerCheckpoint) {
234        self.scale = checkpoint.scale;
235        self.growth_factor = checkpoint.growth_factor;
236        self.backoff_factor = checkpoint.backoff_factor;
237        self.growth_interval = checkpoint.growth_interval;
238        self.consecutive_good_steps = checkpoint.consecutive_good_steps;
239        self.found_overflow = checkpoint.found_overflow;
240        self.enabled = checkpoint.enabled;
241    }
242}
243
244impl Default for GradScaler {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250/// Statistics from the GradScaler for logging/monitoring.
251#[derive(Debug, Clone)]
252pub struct GradScalerStats {
253    /// Current loss scale.
254    pub scale: f32,
255    /// Good steps since the last change.
256    pub consecutive_good_steps: u32,
257    /// Whether scaling is on.
258    pub enabled: bool,
259}
260
261impl std::fmt::Display for GradScalerStats {
262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263        write!(
264            f,
265            "GradScaler(scale={:.0}, good_steps={}, enabled={})",
266            self.scale, self.consecutive_good_steps, self.enabled
267        )
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_scaler_creation() {
277        let scaler = GradScaler::new();
278        assert_eq!(scaler.scale_factor(), 65536.0);
279        assert!(scaler.is_enabled());
280    }
281
282    #[test]
283    fn test_scaler_update_no_overflow() {
284        let mut scaler = GradScaler::with_params(1024.0, 2.0, 0.5, 5);
285
286        // Simulate 5 good steps
287        for _ in 0..5 {
288            scaler.found_overflow = false;
289            scaler.update();
290        }
291
292        // Scale should have doubled
293        assert_eq!(scaler.scale, 2048.0);
294    }
295
296    #[test]
297    fn test_scaler_update_with_overflow() {
298        let mut scaler = GradScaler::with_params(1024.0, 2.0, 0.5, 5);
299
300        // Simulate overflow
301        scaler.found_overflow = true;
302        scaler.update();
303
304        // Scale should have halved
305        assert_eq!(scaler.scale, 512.0);
306        assert_eq!(scaler.consecutive_good_steps, 0);
307    }
308
309    #[test]
310    fn test_scaler_disabled() {
311        let mut scaler = GradScaler::new();
312        scaler.set_enabled(false);
313
314        assert_eq!(scaler.scale_factor(), 1.0);
315        assert!(!scaler.is_enabled());
316    }
317}