1#![allow(dead_code)]
45
46use crate::traits::StatefulOptimizer;
47use serde::{Deserialize, Serialize};
48use std::collections::HashMap;
49use trustformers_core::errors::{Result, TrustformersError};
50use trustformers_core::tensor::Tensor;
51use trustformers_core::traits::Optimizer;
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ProdigyConfig {
56 pub d0: f64,
58 pub beta1: f64,
60 pub beta2: f64,
62 pub eps: f64,
64 pub weight_decay: f64,
66 pub growth_rate: f64,
68 pub warmup_steps: usize,
70 pub bias_correction: bool,
72 pub safeguard_bound: f64,
74}
75
76impl Default for ProdigyConfig {
77 fn default() -> Self {
78 Self {
79 d0: 1e-6,
80 beta1: 0.9,
81 beta2: 0.999,
82 eps: 1e-8,
83 weight_decay: 0.0,
84 growth_rate: 1.02,
85 warmup_steps: 0,
86 bias_correction: true,
87 safeguard_bound: 2.0,
88 }
89 }
90}
91
92impl ProdigyConfig {
93 pub fn for_language_models() -> Self {
95 Self {
96 d0: 1e-6,
97 beta1: 0.9,
98 beta2: 0.999,
99 eps: 1e-8,
100 weight_decay: 0.1,
101 growth_rate: 1.02,
102 warmup_steps: 1000,
103 bias_correction: true,
104 safeguard_bound: 2.0,
105 }
106 }
107
108 pub fn for_vision() -> Self {
110 Self {
111 d0: 1e-6,
112 beta1: 0.9,
113 beta2: 0.999,
114 eps: 1e-8,
115 weight_decay: 0.05,
116 growth_rate: 1.01,
117 warmup_steps: 100,
118 bias_correction: true,
119 safeguard_bound: 1.5,
120 }
121 }
122
123 pub fn for_fast_training() -> Self {
125 Self {
126 d0: 1e-5,
127 beta1: 0.9,
128 beta2: 0.99,
129 eps: 1e-8,
130 weight_decay: 0.01,
131 growth_rate: 1.05,
132 warmup_steps: 10,
133 bias_correction: false,
134 safeguard_bound: 3.0,
135 }
136 }
137
138 pub fn for_stable_training() -> Self {
140 Self {
141 d0: 1e-7,
142 beta1: 0.95,
143 beta2: 0.9999,
144 eps: 1e-8,
145 weight_decay: 0.001,
146 growth_rate: 1.005,
147 warmup_steps: 2000,
148 bias_correction: true,
149 safeguard_bound: 1.2,
150 }
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ProdigyParameterState {
157 pub momentum: Vec<f32>,
159 pub variance: Vec<f32>,
161 pub distance: f64,
163 pub step: usize,
165}
166
167impl ProdigyParameterState {
168 pub fn new(param_size: usize, initial_distance: f64) -> Self {
169 Self {
170 momentum: vec![0.0; param_size],
171 variance: vec![0.0; param_size],
172 distance: initial_distance,
173 step: 0,
174 }
175 }
176
177 pub fn memory_usage(&self) -> ProdigyMemoryStats {
179 let momentum_bytes = self.momentum.len() * std::mem::size_of::<f32>();
180 let variance_bytes = self.variance.len() * std::mem::size_of::<f32>();
181 let metadata_bytes = std::mem::size_of::<f64>() + std::mem::size_of::<usize>();
182
183 ProdigyMemoryStats {
184 momentum_bytes,
185 variance_bytes,
186 metadata_bytes,
187 total_bytes: momentum_bytes + variance_bytes + metadata_bytes,
188 }
189 }
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ProdigyMemoryStats {
195 pub momentum_bytes: usize,
196 pub variance_bytes: usize,
197 pub metadata_bytes: usize,
198 pub total_bytes: usize,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ProdigyOptimizerState {
204 pub parameters: HashMap<String, ProdigyParameterState>,
206 pub global_step: usize,
208 pub global_distance: f64,
210 pub distance_history: Vec<f64>,
212}
213
214impl Default for ProdigyOptimizerState {
215 fn default() -> Self {
216 Self {
217 parameters: HashMap::new(),
218 global_step: 0,
219 global_distance: 1e-6,
220 distance_history: Vec::new(),
221 }
222 }
223}
224
225impl ProdigyOptimizerState {
226 pub fn clear(&mut self) {
228 self.parameters.clear();
229 self.global_step = 0;
230 self.global_distance = 1e-6;
231 self.distance_history.clear();
232 }
233
234 pub fn total_memory_usage(&self) -> ProdigyMemoryStats {
236 let mut total_momentum = 0;
237 let mut total_variance = 0;
238 let mut total_metadata = 0;
239
240 for param_state in self.parameters.values() {
241 let stats = param_state.memory_usage();
242 total_momentum += stats.momentum_bytes;
243 total_variance += stats.variance_bytes;
244 total_metadata += stats.metadata_bytes;
245 }
246
247 total_metadata += std::mem::size_of::<usize>()
249 + std::mem::size_of::<f64>()
250 + self.distance_history.len() * std::mem::size_of::<f64>();
251
252 ProdigyMemoryStats {
253 momentum_bytes: total_momentum,
254 variance_bytes: total_variance,
255 metadata_bytes: total_metadata,
256 total_bytes: total_momentum + total_variance + total_metadata,
257 }
258 }
259}
260
261pub struct Prodigy {
263 config: ProdigyConfig,
264 state: ProdigyOptimizerState,
265}
266
267impl Prodigy {
268 pub fn new() -> Self {
270 Self {
271 config: ProdigyConfig::default(),
272 state: ProdigyOptimizerState::default(),
273 }
274 }
275
276 pub fn with_config(config: ProdigyConfig) -> Self {
278 let state = ProdigyOptimizerState {
279 global_distance: config.d0,
280 ..Default::default()
281 };
282
283 Self { config, state }
284 }
285
286 pub fn for_language_models() -> Self {
288 Self::with_config(ProdigyConfig::for_language_models())
289 }
290
291 pub fn for_vision() -> Self {
293 Self::with_config(ProdigyConfig::for_vision())
294 }
295
296 pub fn for_fast_training() -> Self {
298 Self::with_config(ProdigyConfig::for_fast_training())
299 }
300
301 pub fn for_stable_training() -> Self {
303 Self::with_config(ProdigyConfig::for_stable_training())
304 }
305
306 pub fn get_lr(&self) -> f64 {
308 self.state.global_distance
309 }
310
311 pub fn set_lr(&mut self, distance: f64) {
313 self.state.global_distance = distance.max(1e-10);
314 }
315
316 pub fn reset(&mut self) {
318 self.state.clear();
319 self.state.global_distance = self.config.d0;
320 }
321
322 pub fn memory_usage(&self) -> ProdigyMemoryStats {
324 self.state.total_memory_usage()
325 }
326
327 fn update_distance_estimate(&mut self, grad_norm: f64, param_norm: f64) {
329 if grad_norm > 0.0 && param_norm > 0.0 {
330 let distance_estimate = (param_norm / grad_norm).min(self.config.safeguard_bound);
332
333 let alpha = 0.01; self.state.global_distance = (1.0 - alpha) * self.state.global_distance
336 + alpha * distance_estimate * self.config.growth_rate;
337
338 self.state.distance_history.push(self.state.global_distance);
340 if self.state.distance_history.len() > 100 {
341 self.state.distance_history.remove(0);
342 }
343 }
344 }
345
346 fn bias_correction(&self, step: usize) -> (f64, f64) {
348 if self.config.bias_correction && step > 0 {
349 let beta1_correction = 1.0 - self.config.beta1.powi(step as i32);
350 let beta2_correction = 1.0 - self.config.beta2.powi(step as i32);
351 (beta1_correction, beta2_correction)
352 } else {
353 (1.0, 1.0)
354 }
355 }
356
357 fn warmup_scaling(&self, step: usize) -> f64 {
359 if self.config.warmup_steps > 0 && step < self.config.warmup_steps {
360 (step as f64 + 1.0) / (self.config.warmup_steps as f64)
361 } else {
362 1.0
363 }
364 }
365
366 pub fn update_parameter(
368 &mut self,
369 param_name: &str,
370 param: &mut Tensor,
371 grad: &Tensor,
372 ) -> Result<()> {
373 let mut param_data = param.data().map_err(|e| {
374 TrustformersError::tensor_op_error(
375 &format!("Failed to get parameter data: {}", e),
376 "prodigy_update",
377 )
378 })?;
379 let grad_data = grad.data().map_err(|e| {
380 TrustformersError::tensor_op_error(
381 &format!("Failed to get gradient data: {}", e),
382 "prodigy_update",
383 )
384 })?;
385
386 if param_data.len() != grad_data.len() {
387 return Err(TrustformersError::tensor_op_error(
388 "Parameter and gradient size mismatch",
389 "prodigy_update",
390 ));
391 }
392
393 let param_size = param_data.len();
395
396 let grad_norm: f64 = grad_data.iter().map(|&g| (g as f64).powi(2)).sum::<f64>().sqrt();
398 let param_norm: f64 = param_data.iter().map(|&p| (p as f64).powi(2)).sum::<f64>().sqrt();
399
400 self.update_distance_estimate(grad_norm, param_norm);
402
403 let param_state = self
405 .state
406 .parameters
407 .entry(param_name.to_string())
408 .or_insert_with(|| ProdigyParameterState::new(param_size, self.config.d0));
409
410 if param_state.momentum.len() != param_size {
412 param_state.momentum.resize(param_size, 0.0);
413 param_state.variance.resize(param_size, 0.0);
414 }
415
416 param_state.step += 1;
417 let current_step = param_state.step;
418
419 let warmup_scale =
421 if self.config.warmup_steps > 0 && current_step < self.config.warmup_steps {
422 (current_step as f64 + 1.0) / (self.config.warmup_steps as f64)
423 } else {
424 1.0
425 };
426 let effective_distance = self.state.global_distance * warmup_scale;
427
428 let (beta1_correction, beta2_correction) =
430 if self.config.bias_correction && current_step > 0 {
431 let beta1_correction = 1.0 - self.config.beta1.powi(current_step as i32);
432 let beta2_correction = 1.0 - self.config.beta2.powi(current_step as i32);
433 (beta1_correction, beta2_correction)
434 } else {
435 (1.0, 1.0)
436 };
437
438 for i in 0..param_size {
440 let grad_val = grad_data[i] as f64;
441
442 let grad_with_decay = if self.config.weight_decay > 0.0 {
444 grad_val + self.config.weight_decay * (param_data[i] as f64)
445 } else {
446 grad_val
447 };
448
449 param_state.momentum[i] = (self.config.beta1 * param_state.momentum[i] as f64
451 + (1.0 - self.config.beta1) * grad_with_decay)
452 as f32;
453
454 param_state.variance[i] = (self.config.beta2 * param_state.variance[i] as f64
456 + (1.0 - self.config.beta2) * grad_with_decay.powi(2))
457 as f32;
458
459 let m_hat = param_state.momentum[i] as f64 / beta1_correction;
461 let v_hat = param_state.variance[i] as f64 / beta2_correction;
462
463 let denominator = v_hat.sqrt() + self.config.eps;
465 let update = effective_distance * m_hat / denominator;
466
467 param_data[i] = (param_data[i] as f64 - update) as f32;
469 }
470
471 *param = Tensor::new(param_data)?;
473
474 Ok(())
475 }
476}
477
478impl Default for Prodigy {
479 fn default() -> Self {
480 Self::new()
481 }
482}
483
484impl Optimizer for Prodigy {
485 fn step(&mut self) {
486 self.state.global_step += 1;
487 }
488
489 fn zero_grad(&mut self) {
490 }
493
494 fn update(&mut self, param: &mut Tensor, grad: &Tensor) -> Result<()> {
495 self.update_parameter("default", param, grad)
497 }
498
499 fn get_lr(&self) -> f32 {
500 self.state.global_distance as f32
501 }
502
503 fn set_lr(&mut self, lr: f32) {
504 self.state.global_distance = (lr as f64).max(1e-10);
505 }
506}
507
508impl StatefulOptimizer for Prodigy {
509 type Config = ProdigyConfig;
510 type State = ProdigyOptimizerState;
511
512 fn config(&self) -> &Self::Config {
513 &self.config
514 }
515
516 fn state(&self) -> &Self::State {
517 &self.state
518 }
519
520 fn state_mut(&mut self) -> &mut Self::State {
521 &mut self.state
522 }
523
524 fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
525 let mut state_dict = HashMap::new();
526
527 state_dict.insert("lr".to_string(), Tensor::new(vec![self.config.d0 as f32])?);
529 state_dict.insert(
530 "beta1".to_string(),
531 Tensor::new(vec![self.config.beta1 as f32])?,
532 );
533 state_dict.insert(
534 "beta2".to_string(),
535 Tensor::new(vec![self.config.beta2 as f32])?,
536 );
537 state_dict.insert(
538 "eps".to_string(),
539 Tensor::new(vec![self.config.eps as f32])?,
540 );
541 state_dict.insert(
542 "weight_decay".to_string(),
543 Tensor::new(vec![self.config.weight_decay as f32])?,
544 );
545 state_dict.insert(
546 "growth_rate".to_string(),
547 Tensor::new(vec![self.config.growth_rate as f32])?,
548 );
549 state_dict.insert(
550 "warmup_steps".to_string(),
551 Tensor::new(vec![self.config.warmup_steps as f32])?,
552 );
553 state_dict.insert(
554 "global_step".to_string(),
555 Tensor::new(vec![self.state.global_step as f32])?,
556 );
557 state_dict.insert(
558 "global_distance".to_string(),
559 Tensor::new(vec![self.state.global_distance as f32])?,
560 );
561
562 for (param_name, param_state) in &self.state.parameters {
564 state_dict.insert(
565 format!("momentum_{}", param_name),
566 Tensor::new(param_state.momentum.clone())?,
567 );
568 state_dict.insert(
569 format!("variance_{}", param_name),
570 Tensor::new(param_state.variance.clone())?,
571 );
572 state_dict.insert(
573 format!("distance_{}", param_name),
574 Tensor::new(vec![param_state.distance as f32])?,
575 );
576 state_dict.insert(
577 format!("step_{}", param_name),
578 Tensor::new(vec![param_state.step as f32])?,
579 );
580 }
581
582 Ok(state_dict)
583 }
584
585 fn load_state_dict(&mut self, state_dict: HashMap<String, Tensor>) -> Result<()> {
586 if let Some(lr_tensor) = state_dict.get("lr") {
588 if let Ok(lr_vec) = lr_tensor.data() {
589 if !lr_vec.is_empty() {
590 self.config.d0 = lr_vec[0] as f64;
591 }
592 }
593 }
594 if let Some(beta1_tensor) = state_dict.get("beta1") {
595 if let Ok(beta1_vec) = beta1_tensor.data() {
596 if !beta1_vec.is_empty() {
597 self.config.beta1 = beta1_vec[0] as f64;
598 }
599 }
600 }
601 if let Some(beta2_tensor) = state_dict.get("beta2") {
602 if let Ok(beta2_vec) = beta2_tensor.data() {
603 if !beta2_vec.is_empty() {
604 self.config.beta2 = beta2_vec[0] as f64;
605 }
606 }
607 }
608
609 if let Some(global_step_tensor) = state_dict.get("global_step") {
611 if let Ok(global_step_vec) = global_step_tensor.data() {
612 if !global_step_vec.is_empty() {
613 self.state.global_step = global_step_vec[0] as usize;
614 }
615 }
616 }
617 if let Some(global_distance_tensor) = state_dict.get("global_distance") {
618 if let Ok(global_distance_vec) = global_distance_tensor.data() {
619 if !global_distance_vec.is_empty() {
620 self.state.global_distance = global_distance_vec[0] as f64;
621 }
622 }
623 }
624
625 Ok(())
630 }
631
632 fn memory_usage(&self) -> crate::common::StateMemoryStats {
633 let total_momentum_elements: usize =
634 self.state.parameters.values().map(|p| p.momentum.len()).sum();
635 let total_variance_elements: usize =
636 self.state.parameters.values().map(|p| p.variance.len()).sum();
637
638 let momentum_bytes = total_momentum_elements * std::mem::size_of::<f32>();
639 let variance_bytes = total_variance_elements * std::mem::size_of::<f32>();
640 let metadata_bytes = self.state.parameters.len()
641 * (std::mem::size_of::<f64>() + std::mem::size_of::<usize>());
642
643 crate::common::StateMemoryStats {
644 momentum_elements: total_momentum_elements,
645 variance_elements: total_variance_elements,
646 third_moment_elements: 0,
647 total_bytes: momentum_bytes + variance_bytes + metadata_bytes,
648 num_parameters: self.state.parameters.len(),
649 }
650 }
651
652 fn reset_state(&mut self) {
653 self.reset();
654 }
655
656 fn num_parameters(&self) -> usize {
657 self.state.parameters.len()
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 #[test]
666 fn test_prodigy_creation() {
667 let optimizer = Prodigy::new();
668 assert_eq!(optimizer.config.d0, 1e-6);
669 assert_eq!(optimizer.config.beta1, 0.9);
670 assert_eq!(optimizer.config.beta2, 0.999);
671 }
672
673 #[test]
674 fn test_prodigy_with_config() {
675 let config = ProdigyConfig {
676 d0: 1e-5,
677 beta1: 0.95,
678 beta2: 0.99,
679 weight_decay: 0.1,
680 ..Default::default()
681 };
682 let optimizer = Prodigy::with_config(config.clone());
683 assert_eq!(optimizer.config.d0, config.d0);
684 assert_eq!(optimizer.config.beta1, config.beta1);
685 assert_eq!(optimizer.config.weight_decay, config.weight_decay);
686 }
687
688 #[test]
689 fn test_prodigy_presets() {
690 let lm_optimizer = Prodigy::for_language_models();
691 assert_eq!(lm_optimizer.config.warmup_steps, 1000);
692 assert_eq!(lm_optimizer.config.weight_decay, 0.1);
693
694 let vision_optimizer = Prodigy::for_vision();
695 assert_eq!(vision_optimizer.config.warmup_steps, 100);
696 assert_eq!(vision_optimizer.config.weight_decay, 0.05);
697
698 let fast_optimizer = Prodigy::for_fast_training();
699 assert_eq!(fast_optimizer.config.growth_rate, 1.05);
700 assert!(!fast_optimizer.config.bias_correction);
701
702 let stable_optimizer = Prodigy::for_stable_training();
703 assert_eq!(stable_optimizer.config.warmup_steps, 2000);
704 assert_eq!(stable_optimizer.config.safeguard_bound, 1.2);
705 }
706
707 #[test]
708 fn test_lr_getter_setter() {
709 let mut optimizer = Prodigy::new();
710 let initial_lr = optimizer.get_lr();
711 assert_eq!(initial_lr, 1e-6);
712
713 optimizer.set_lr(0.001);
714 assert_eq!(optimizer.get_lr(), 0.001);
715
716 optimizer.set_lr(-1.0);
718 assert!(optimizer.get_lr() >= 1e-10);
719 }
720
721 #[test]
722 fn test_parameter_state_creation() {
723 let param_state = ProdigyParameterState::new(100, 1e-6);
724 assert_eq!(param_state.momentum.len(), 100);
725 assert_eq!(param_state.variance.len(), 100);
726 assert_eq!(param_state.distance, 1e-6);
727 assert_eq!(param_state.step, 0);
728 assert!(param_state.momentum.iter().all(|&x| x == 0.0));
729 assert!(param_state.variance.iter().all(|&x| x == 0.0));
730 }
731
732 #[test]
733 fn test_memory_usage_tracking() {
734 let param_state = ProdigyParameterState::new(1000, 1e-6);
735 let memory_stats = param_state.memory_usage();
736
737 assert_eq!(memory_stats.momentum_bytes, 1000 * 4); assert_eq!(memory_stats.variance_bytes, 1000 * 4);
739 assert!(memory_stats.metadata_bytes > 0);
740 assert_eq!(
741 memory_stats.total_bytes,
742 memory_stats.momentum_bytes + memory_stats.variance_bytes + memory_stats.metadata_bytes
743 );
744 }
745
746 #[test]
747 fn test_optimizer_state_operations() {
748 let mut state = ProdigyOptimizerState::default();
749 state
750 .parameters
751 .insert("param1".to_string(), ProdigyParameterState::new(100, 1e-6));
752 state
753 .parameters
754 .insert("param2".to_string(), ProdigyParameterState::new(200, 1e-6));
755 state.global_step = 10;
756
757 let memory_stats = state.total_memory_usage();
758 assert!(memory_stats.total_bytes > 0);
759 assert_eq!(memory_stats.momentum_bytes, (100 + 200) * 4);
760
761 state.clear();
762 assert_eq!(state.parameters.len(), 0);
763 assert_eq!(state.global_step, 0);
764 assert_eq!(state.global_distance, 1e-6);
765 }
766
767 #[test]
768 fn test_reset() {
769 let mut optimizer = Prodigy::new();
770 optimizer.state.global_step = 100;
771 optimizer
772 .state
773 .parameters
774 .insert("test".to_string(), ProdigyParameterState::new(10, 1e-6));
775
776 optimizer.reset();
777 assert_eq!(optimizer.state.global_step, 0);
778 assert_eq!(optimizer.state.parameters.len(), 0);
779 assert_eq!(optimizer.state.global_distance, optimizer.config.d0);
780 }
781
782 #[test]
783 fn test_config_serialization() {
784 let config = ProdigyConfig::for_language_models();
785 let serialized = serde_json::to_string(&config).expect("Serialization failed");
786 let deserialized: ProdigyConfig =
787 serde_json::from_str(&serialized).expect("Deserialization failed");
788
789 assert_eq!(config.d0, deserialized.d0);
790 assert_eq!(config.beta1, deserialized.beta1);
791 assert_eq!(config.warmup_steps, deserialized.warmup_steps);
792 }
793
794 #[test]
795 fn test_state_dict_operations() {
796 let mut optimizer = Prodigy::for_vision();
797 optimizer.state.global_step = 50;
798 optimizer.state.parameters.insert(
799 "test_param".to_string(),
800 ProdigyParameterState::new(5, 1e-5),
801 );
802
803 let state_dict = optimizer.state_dict().expect("Failed to get state dict");
805 assert!(state_dict.contains_key("lr"));
806 assert!(state_dict.contains_key("global_step"));
807
808 let mut new_optimizer = Prodigy::new();
810 new_optimizer.load_state_dict(state_dict).expect("Failed to load state dict");
811
812 assert_eq!(new_optimizer.state.global_step, 50);
813 }
816
817 #[test]
818 fn test_step_and_zero_grad() {
819 let mut optimizer = Prodigy::new();
820 assert_eq!(optimizer.state.global_step, 0);
821
822 optimizer.step();
823 assert_eq!(optimizer.state.global_step, 1);
824
825 optimizer.zero_grad(); }
827
828 #[test]
829 fn test_stateful_optimizer_trait() {
830 let optimizer = Prodigy::for_fast_training();
831
832 let config = optimizer.config();
834 assert_eq!(config.growth_rate, 1.05);
835
836 let state = optimizer.state();
838 assert_eq!(state.global_step, 0);
839 }
840
841 #[test]
842 fn test_distance_estimation_bounds() {
843 let mut optimizer = Prodigy::with_config(ProdigyConfig {
844 safeguard_bound: 2.0,
845 ..Default::default()
846 });
847
848 optimizer.update_distance_estimate(1.0, 10.0); assert!(optimizer.get_lr() <= 2.0);
851 }
852
853 #[test]
854 fn test_bias_correction() {
855 let optimizer = Prodigy::new();
856
857 let (bc1, bc2) = optimizer.bias_correction(1);
859 assert!(bc1 > 0.0 && bc1 < 1.0);
860 assert!(bc2 > 0.0 && bc2 < 1.0);
861
862 let (bc1_late, bc2_late) = optimizer.bias_correction(1000);
864 assert!(bc1_late > 0.9);
865 assert!(bc2_late > 0.6); }
867
868 #[test]
869 fn test_warmup_scaling() {
870 let optimizer = Prodigy::with_config(ProdigyConfig {
871 warmup_steps: 100,
872 ..Default::default()
873 });
874
875 let scale_early = optimizer.warmup_scaling(10);
877 assert!(scale_early < 1.0);
878 assert_eq!(scale_early, 11.0 / 100.0);
879
880 let scale_late = optimizer.warmup_scaling(200);
882 assert_eq!(scale_late, 1.0);
883 }
884}