1#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50pub enum ScaleUpdatePolicy {
51 Static,
53 Dynamic,
56 Gradual,
60}
61
62impl std::fmt::Display for ScaleUpdatePolicy {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::Static => write!(f, "Static"),
66 Self::Dynamic => write!(f, "Dynamic"),
67 Self::Gradual => write!(f, "Gradual"),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct LossScalerConfig {
75 pub policy: ScaleUpdatePolicy,
77 pub initial_scale: f64,
79 pub min_scale: f64,
81 pub max_scale: f64,
83 pub scale_up_factor: f64,
87 pub scale_down_factor: f64,
89 pub scale_up_interval: u64,
92}
93
94impl Default for LossScalerConfig {
95 fn default() -> Self {
96 Self {
97 policy: ScaleUpdatePolicy::Dynamic,
98 initial_scale: 65_536.0,
99 min_scale: 1.0,
100 max_scale: 16_777_216.0, scale_up_factor: 2.0,
102 scale_down_factor: 0.5,
103 scale_up_interval: 2000,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct ScalerStats {
111 pub total_steps: u64,
113 pub overflow_events: u64,
115 pub scale_ups: u64,
117 pub scale_downs: u64,
119 pub current_scale: f64,
121}
122
123#[derive(Debug, Clone)]
127pub struct LossScaler {
128 config: LossScalerConfig,
129 current_scale: f64,
130 steps_since_overflow: u64,
132 overflow_count: u64,
135 stats: ScalerStats,
136}
137
138impl LossScaler {
139 pub fn new(config: LossScalerConfig) -> Self {
146 let scale = config
147 .initial_scale
148 .clamp(config.min_scale, config.max_scale);
149 let mut scaler = Self {
150 config,
151 current_scale: scale,
152 steps_since_overflow: 0,
153 overflow_count: 0,
154 stats: ScalerStats::default(),
155 };
156 scaler.stats.current_scale = scaler.current_scale;
157 scaler
158 }
159
160 #[inline]
167 pub fn scale_loss(&self, loss: f64) -> f64 {
168 loss * self.current_scale
169 }
170
171 pub fn unscale_gradients(&self, grads: &mut [f64]) {
178 if self.current_scale == 0.0 {
179 return;
180 }
181 let inv = 1.0 / self.current_scale;
182 for g in grads.iter_mut() {
183 *g *= inv;
184 }
185 }
186
187 pub fn has_overflow(grads: &[f64]) -> bool {
189 grads.iter().any(|&g| !Self::is_finite(g))
190 }
191
192 pub fn update(&mut self, overflow: bool) {
197 self.stats.total_steps += 1;
198
199 if overflow {
200 self.overflow_count += 1;
201 self.stats.overflow_events += 1;
202 self.steps_since_overflow = 0;
203
204 match self.config.policy {
205 ScaleUpdatePolicy::Static => {}
206 ScaleUpdatePolicy::Dynamic => {
207 self.current_scale *= 0.5;
208 self.clamp_scale();
209 self.stats.scale_downs += 1;
210 }
211 ScaleUpdatePolicy::Gradual => {
212 self.current_scale *= self.config.scale_down_factor;
213 self.clamp_scale();
214 self.stats.scale_downs += 1;
215 }
216 }
217 } else {
218 self.steps_since_overflow += 1;
219
220 match self.config.policy {
221 ScaleUpdatePolicy::Static => {}
222 ScaleUpdatePolicy::Dynamic => {
223 if self.steps_since_overflow >= self.config.scale_up_interval {
224 self.current_scale *= self.config.scale_up_factor;
225 self.clamp_scale();
226 self.stats.scale_ups += 1;
227 self.steps_since_overflow = 0;
228 }
229 }
230 ScaleUpdatePolicy::Gradual => {
231 if self.steps_since_overflow >= self.config.scale_up_interval {
232 self.current_scale *= self.config.scale_up_factor;
233 self.clamp_scale();
234 self.stats.scale_ups += 1;
235 self.steps_since_overflow = 0;
236 }
237 }
238 }
239 }
240
241 self.stats.current_scale = self.current_scale;
242 }
243
244 #[inline]
250 pub fn current_scale(&self) -> f64 {
251 self.current_scale
252 }
253
254 #[inline]
256 pub fn stats(&self) -> &ScalerStats {
257 &self.stats
258 }
259
260 #[inline]
262 pub fn config(&self) -> &LossScalerConfig {
263 &self.config
264 }
265
266 pub fn reset(&mut self) {
272 self.current_scale = self
273 .config
274 .initial_scale
275 .clamp(self.config.min_scale, self.config.max_scale);
276 self.steps_since_overflow = 0;
277 self.overflow_count = 0;
278 self.stats = ScalerStats {
279 current_scale: self.current_scale,
280 ..ScalerStats::default()
281 };
282 }
283
284 #[inline]
290 pub fn is_finite(x: f64) -> bool {
291 x.is_finite()
292 }
293
294 pub fn clamp_scale(&mut self) {
296 self.current_scale = self
297 .current_scale
298 .clamp(self.config.min_scale, self.config.max_scale);
299 }
300}
301
302#[cfg(test)]
307mod tests {
308 use super::*;
309
310 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
313 (a - b).abs() < tol
314 }
315
316 fn dynamic_scaler() -> LossScaler {
317 LossScaler::new(LossScalerConfig {
318 policy: ScaleUpdatePolicy::Dynamic,
319 initial_scale: 1024.0,
320 min_scale: 1.0,
321 max_scale: 1_048_576.0,
322 scale_up_factor: 2.0,
323 scale_down_factor: 0.5,
324 scale_up_interval: 5,
325 })
326 }
327
328 fn static_scaler() -> LossScaler {
329 LossScaler::new(LossScalerConfig {
330 policy: ScaleUpdatePolicy::Static,
331 initial_scale: 512.0,
332 ..LossScalerConfig::default()
333 })
334 }
335
336 fn gradual_scaler() -> LossScaler {
337 LossScaler::new(LossScalerConfig {
338 policy: ScaleUpdatePolicy::Gradual,
339 initial_scale: 256.0,
340 min_scale: 1.0,
341 max_scale: 1_048_576.0,
342 scale_up_factor: 1.1,
343 scale_down_factor: 0.8,
344 scale_up_interval: 3,
345 })
346 }
347
348 #[test]
351 fn scale_loss_multiplies_by_scale() {
352 let scaler = dynamic_scaler();
353 let scaled = scaler.scale_loss(2.5);
354 assert!(approx_eq(scaled, 2.5 * 1024.0, 1e-10));
355 }
356
357 #[test]
358 fn scale_loss_zero_loss_remains_zero() {
359 let scaler = dynamic_scaler();
360 assert_eq!(scaler.scale_loss(0.0), 0.0);
361 }
362
363 #[test]
364 fn scale_loss_negative_loss() {
365 let scaler = dynamic_scaler();
366 let scaled = scaler.scale_loss(-1.0);
367 assert!(approx_eq(scaled, -1024.0, 1e-10));
368 }
369
370 #[test]
373 fn unscale_gradients_divides_by_scale() {
374 let scaler = dynamic_scaler();
375 let mut grads = vec![1024.0, 2048.0, 512.0];
376 scaler.unscale_gradients(&mut grads);
377 assert!(approx_eq(grads[0], 1.0, 1e-10));
378 assert!(approx_eq(grads[1], 2.0, 1e-10));
379 assert!(approx_eq(grads[2], 0.5, 1e-10));
380 }
381
382 #[test]
383 fn unscale_gradients_empty_slice_ok() {
384 let scaler = dynamic_scaler();
385 let mut grads: Vec<f64> = vec![];
386 scaler.unscale_gradients(&mut grads); }
388
389 #[test]
390 fn unscale_gradients_zero_scale_noop() {
391 let mut scaler = LossScaler::new(LossScalerConfig {
393 policy: ScaleUpdatePolicy::Static,
394 initial_scale: 0.0,
395 min_scale: 0.0,
396 max_scale: 1.0,
397 ..LossScalerConfig::default()
398 });
399 scaler.current_scale = 0.0; let mut grads = vec![3.0, 4.0];
401 scaler.unscale_gradients(&mut grads);
402 assert!(approx_eq(grads[0], 3.0, 1e-15));
404 assert!(approx_eq(grads[1], 4.0, 1e-15));
405 }
406
407 #[test]
408 fn unscale_zero_gradient_stays_zero() {
409 let scaler = dynamic_scaler();
410 let mut grads = vec![0.0_f64];
411 scaler.unscale_gradients(&mut grads);
412 assert_eq!(grads[0], 0.0);
413 }
414
415 #[test]
418 fn has_overflow_clean_gradients_false() {
419 let grads = vec![0.1, 0.2, -0.3, 0.0];
420 assert!(!LossScaler::has_overflow(&grads));
421 }
422
423 #[test]
424 fn has_overflow_nan_detected() {
425 let grads = vec![1.0, f64::NAN, 3.0];
426 assert!(LossScaler::has_overflow(&grads));
427 }
428
429 #[test]
430 fn has_overflow_positive_inf_detected() {
431 let grads = vec![1.0, f64::INFINITY];
432 assert!(LossScaler::has_overflow(&grads));
433 }
434
435 #[test]
436 fn has_overflow_negative_inf_detected() {
437 let grads = vec![f64::NEG_INFINITY, 0.0];
438 assert!(LossScaler::has_overflow(&grads));
439 }
440
441 #[test]
442 fn has_overflow_empty_slice_false() {
443 assert!(!LossScaler::has_overflow(&[]));
444 }
445
446 #[test]
449 fn dynamic_scale_up_after_interval() {
450 let mut scaler = dynamic_scaler(); let initial = scaler.current_scale();
452 for _ in 0..5 {
453 scaler.update(false);
454 }
455 assert!(approx_eq(scaler.current_scale(), initial * 2.0, 1e-10));
457 assert_eq!(scaler.stats().scale_ups, 1);
458 }
459
460 #[test]
461 fn dynamic_scale_up_resets_streak_counter() {
462 let mut scaler = dynamic_scaler(); for _ in 0..5 {
465 scaler.update(false);
466 }
467 let after_first = scaler.current_scale();
468 for _ in 0..5 {
470 scaler.update(false);
471 }
472 assert!(approx_eq(scaler.current_scale(), after_first * 2.0, 1e-10));
473 assert_eq!(scaler.stats().scale_ups, 2);
474 }
475
476 #[test]
477 fn dynamic_no_scale_up_before_interval() {
478 let mut scaler = dynamic_scaler(); let initial = scaler.current_scale();
480 for _ in 0..4 {
481 scaler.update(false);
482 }
483 assert!(approx_eq(scaler.current_scale(), initial, 1e-10));
485 assert_eq!(scaler.stats().scale_ups, 0);
486 }
487
488 #[test]
491 fn dynamic_scale_down_on_overflow() {
492 let mut scaler = dynamic_scaler();
493 let initial = scaler.current_scale();
494 scaler.update(true);
495 assert!(approx_eq(scaler.current_scale(), initial * 0.5, 1e-10));
496 assert_eq!(scaler.stats().scale_downs, 1);
497 }
498
499 #[test]
500 fn dynamic_consecutive_overflows_halve_repeatedly() {
501 let mut scaler = dynamic_scaler();
502 let initial = scaler.current_scale();
503 for i in 1..=4 {
504 scaler.update(true);
505 let expected = initial * 0.5_f64.powi(i);
506 assert!(
507 approx_eq(scaler.current_scale(), expected, 1e-8),
508 "after {i} overflows: expected {expected}, got {}",
509 scaler.current_scale()
510 );
511 }
512 assert_eq!(scaler.stats().scale_downs, 4);
513 assert_eq!(scaler.stats().overflow_events, 4);
514 }
515
516 #[test]
517 fn dynamic_overflow_resets_streak() {
518 let mut scaler = dynamic_scaler(); for _ in 0..4 {
521 scaler.update(false);
522 }
523 scaler.update(true);
525 for _ in 0..4 {
527 scaler.update(false);
528 }
529 assert_eq!(scaler.stats().scale_ups, 0);
530 }
531
532 #[test]
535 fn scale_does_not_exceed_max() {
536 let mut scaler = LossScaler::new(LossScalerConfig {
537 policy: ScaleUpdatePolicy::Dynamic,
538 initial_scale: 512.0,
539 max_scale: 1024.0,
540 scale_up_factor: 4.0,
541 scale_up_interval: 1,
542 ..LossScalerConfig::default()
543 });
544 scaler.update(false); assert!(scaler.current_scale() <= 1024.0);
546 }
547
548 #[test]
549 fn scale_does_not_fall_below_min() {
550 let mut scaler = LossScaler::new(LossScalerConfig {
551 policy: ScaleUpdatePolicy::Dynamic,
552 initial_scale: 4.0,
553 min_scale: 2.0,
554 scale_down_factor: 0.1,
555 scale_up_interval: 100,
556 ..LossScalerConfig::default()
557 });
558 scaler.update(true); assert!(scaler.current_scale() >= 2.0);
560 scaler.update(true); assert!(scaler.current_scale() >= 2.0);
562 }
563
564 #[test]
565 fn clamp_scale_direct_call() {
566 let mut scaler = dynamic_scaler();
567 scaler.current_scale = 1e18; scaler.clamp_scale();
569 assert!(scaler.current_scale() <= scaler.config().max_scale);
570
571 scaler.current_scale = -5.0; scaler.clamp_scale();
573 assert!(scaler.current_scale() >= scaler.config().min_scale);
574 }
575
576 #[test]
579 fn static_policy_never_changes_on_success() {
580 let mut scaler = static_scaler();
581 let initial = scaler.current_scale();
582 for _ in 0..1000 {
583 scaler.update(false);
584 }
585 assert!(approx_eq(scaler.current_scale(), initial, 1e-10));
586 assert_eq!(scaler.stats().scale_ups, 0);
587 }
588
589 #[test]
590 fn static_policy_never_changes_on_overflow() {
591 let mut scaler = static_scaler();
592 let initial = scaler.current_scale();
593 for _ in 0..50 {
594 scaler.update(true);
595 }
596 assert!(approx_eq(scaler.current_scale(), initial, 1e-10));
597 assert_eq!(scaler.stats().scale_downs, 0);
598 assert_eq!(scaler.stats().overflow_events, 50);
600 }
601
602 #[test]
605 fn gradual_scale_up_after_interval() {
606 let mut scaler = gradual_scaler(); let initial = scaler.current_scale();
608 for _ in 0..3 {
609 scaler.update(false);
610 }
611 let expected = initial * 1.1;
612 assert!(
613 approx_eq(scaler.current_scale(), expected, 1e-8),
614 "gradual scale_up: expected {expected}, got {}",
615 scaler.current_scale()
616 );
617 assert_eq!(scaler.stats().scale_ups, 1);
618 }
619
620 #[test]
621 fn gradual_scale_down_on_overflow() {
622 let mut scaler = gradual_scaler(); let initial = scaler.current_scale();
624 scaler.update(true);
625 let expected = initial * 0.8;
626 assert!(
627 approx_eq(scaler.current_scale(), expected, 1e-8),
628 "gradual scale_down: expected {expected}, got {}",
629 scaler.current_scale()
630 );
631 assert_eq!(scaler.stats().scale_downs, 1);
632 }
633
634 #[test]
635 fn gradual_multiple_up_cycles() {
636 let mut scaler = gradual_scaler(); let mut expected = scaler.current_scale();
638 for _ in 0..3 {
639 for _ in 0..3 {
640 scaler.update(false);
641 }
642 expected *= 1.1;
643 }
644 assert!(
645 approx_eq(scaler.current_scale(), expected, 1e-6),
646 "after 3 cycles: expected {expected}, got {}",
647 scaler.current_scale()
648 );
649 assert_eq!(scaler.stats().scale_ups, 3);
650 }
651
652 #[test]
655 fn reset_restores_initial_scale() {
656 let mut scaler = dynamic_scaler();
657 for _ in 0..10 {
658 scaler.update(true);
659 }
660 scaler.reset();
661 assert!(approx_eq(scaler.current_scale(), 1024.0, 1e-10));
662 }
663
664 #[test]
665 fn reset_clears_stats() {
666 let mut scaler = dynamic_scaler();
667 for _ in 0..20 {
668 scaler.update(false);
669 }
670 scaler.update(true);
671 scaler.reset();
672 let s = scaler.stats();
673 assert_eq!(s.total_steps, 0);
674 assert_eq!(s.overflow_events, 0);
675 assert_eq!(s.scale_ups, 0);
676 assert_eq!(s.scale_downs, 0);
677 assert!(approx_eq(s.current_scale, 1024.0, 1e-10));
678 }
679
680 #[test]
683 fn stats_total_steps_increments() {
684 let mut scaler = dynamic_scaler();
685 for i in 1..=10_u64 {
686 scaler.update(false);
687 assert_eq!(scaler.stats().total_steps, i);
688 }
689 }
690
691 #[test]
692 fn stats_current_scale_reflects_latest() {
693 let mut scaler = dynamic_scaler(); for _ in 0..5 {
695 scaler.update(false);
696 }
697 assert!(approx_eq(
698 scaler.stats().current_scale,
699 scaler.current_scale(),
700 1e-15
701 ));
702 }
703
704 #[test]
707 fn is_finite_normal_values() {
708 assert!(LossScaler::is_finite(0.0));
709 assert!(LossScaler::is_finite(1.0));
710 assert!(LossScaler::is_finite(-1e300));
711 }
712
713 #[test]
714 fn is_finite_nan_false() {
715 assert!(!LossScaler::is_finite(f64::NAN));
716 }
717
718 #[test]
719 fn is_finite_inf_false() {
720 assert!(!LossScaler::is_finite(f64::INFINITY));
721 assert!(!LossScaler::is_finite(f64::NEG_INFINITY));
722 }
723
724 #[test]
727 fn scale_up_factor_math_exact() {
728 let mut scaler = dynamic_scaler();
730 for _ in 0..5 {
731 scaler.update(false);
732 }
733 assert!(approx_eq(scaler.current_scale(), 2048.0, 1e-10));
734 }
735
736 #[test]
737 fn scale_down_factor_math_exact() {
738 let mut scaler = dynamic_scaler();
740 scaler.update(true);
741 assert!(approx_eq(scaler.current_scale(), 512.0, 1e-10));
742 }
743
744 #[test]
747 fn mixed_pattern_tracks_correctly() {
748 let mut scaler = dynamic_scaler(); for _ in 0..3 {
751 scaler.update(false);
752 }
753 scaler.update(true);
754 for _ in 0..2 {
755 scaler.update(false);
756 }
757 let s = scaler.stats();
758 assert_eq!(s.total_steps, 6);
759 assert_eq!(s.overflow_events, 1);
760 assert_eq!(s.scale_ups, 0);
761 assert_eq!(s.scale_downs, 1);
762 }
763}