1#![allow(dead_code)]
10
11use crate::OptimizerState;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::{Arc, Mutex};
15use trustformers_core::errors::{Result, TrustformersError};
16use trustformers_core::Tensor;
17
18#[derive(Debug, Clone)]
20pub enum FusedOperation {
21 FusedAdam {
23 lr: f64,
24 beta1: f64,
25 beta2: f64,
26 eps: f64,
27 weight_decay: f64,
28 },
29 FusedAdamW {
31 lr: f64,
32 beta1: f64,
33 beta2: f64,
34 eps: f64,
35 weight_decay: f64,
36 },
37 FusedSGDMomentum {
39 lr: f64,
40 momentum: f64,
41 dampening: f64,
42 weight_decay: f64,
43 nesterov: bool,
44 },
45 FusedGradientClipping { max_norm: f64, scale_factor: f64 },
47 FusedBatchNorm { eps: f64, momentum: f64 },
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct FusionConfig {
54 pub enable_memory_coalescing: bool,
56 pub enable_vectorization: bool,
58 pub batch_size: usize,
60 pub enable_kernel_fusion: bool,
62 pub buffer_size: usize,
64 pub enable_async_updates: bool,
66}
67
68impl Default for FusionConfig {
69 fn default() -> Self {
70 Self {
71 enable_memory_coalescing: true,
72 enable_vectorization: true,
73 batch_size: 64,
74 enable_kernel_fusion: true,
75 buffer_size: 1024,
76 enable_async_updates: false,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct FusedOptimizerState {
84 pub parameter_states: HashMap<String, OptimizerState>,
86 pub operation_buffers: HashMap<String, Vec<f64>>,
88 pub fusion_stats: FusionStats,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct FusionStats {
95 pub fused_operations: u64,
97 pub memory_bandwidth_saved: u64,
99 pub flops_saved: u64,
101 pub avg_batch_size: f64,
103 pub fusion_efficiency: f64,
105}
106
107impl Default for FusionStats {
108 fn default() -> Self {
109 Self {
110 fused_operations: 0,
111 memory_bandwidth_saved: 0,
112 flops_saved: 0,
113 avg_batch_size: 0.0,
114 fusion_efficiency: 0.0,
115 }
116 }
117}
118
119#[derive(Debug)]
135pub struct FusedOptimizer {
136 config: FusionConfig,
137 state: Arc<Mutex<FusedOptimizerState>>,
138 pending_operations: Arc<Mutex<Vec<(String, FusedOperation, Tensor, Tensor)>>>,
139 operation_queue: Arc<Mutex<HashMap<String, Vec<FusedOperation>>>>,
140 updated_parameters: Arc<Mutex<HashMap<String, Tensor>>>,
142 clipped_gradients: Arc<Mutex<HashMap<String, Tensor>>>,
144}
145
146impl FusedOptimizer {
147 pub fn new(config: FusionConfig) -> Result<Self> {
149 let state = FusedOptimizerState {
150 parameter_states: HashMap::new(),
151 operation_buffers: HashMap::new(),
152 fusion_stats: FusionStats::default(),
153 };
154
155 Ok(Self {
156 config,
157 state: Arc::new(Mutex::new(state)),
158 pending_operations: Arc::new(Mutex::new(Vec::new())),
159 operation_queue: Arc::new(Mutex::new(HashMap::new())),
160 updated_parameters: Arc::new(Mutex::new(HashMap::new())),
161 clipped_gradients: Arc::new(Mutex::new(HashMap::new())),
162 })
163 }
164
165 fn record_updated_parameter(&self, param_name: String, param: Tensor) -> Result<()> {
167 let mut updated = self.updated_parameters.lock().map_err(|_| {
168 TrustformersError::lock_error("fusion updated-parameter mutex poisoned".to_string())
169 })?;
170 updated.insert(param_name, param);
171 Ok(())
172 }
173
174 pub fn take_updated_parameters(&mut self) -> Result<HashMap<String, Tensor>> {
176 let mut updated = self.updated_parameters.lock().map_err(|_| {
177 TrustformersError::lock_error("fusion updated-parameter mutex poisoned".to_string())
178 })?;
179 Ok(std::mem::take(&mut *updated))
180 }
181
182 pub fn updated_parameter(&self, param_name: &str) -> Result<Option<Tensor>> {
184 let updated = self.updated_parameters.lock().map_err(|_| {
185 TrustformersError::lock_error("fusion updated-parameter mutex poisoned".to_string())
186 })?;
187 Ok(updated.get(param_name).cloned())
188 }
189
190 pub fn take_clipped_gradients(&mut self) -> Result<HashMap<String, Tensor>> {
192 let mut clipped = self.clipped_gradients.lock().map_err(|_| {
193 TrustformersError::lock_error("fusion clipped-gradient mutex poisoned".to_string())
194 })?;
195 Ok(std::mem::take(&mut *clipped))
196 }
197
198 pub fn apply_in_place(
204 &mut self,
205 param_name: &str,
206 operation: FusedOperation,
207 parameter: &mut Tensor,
208 gradient: &mut Tensor,
209 ) -> Result<()> {
210 let mut state = self
211 .state
212 .lock()
213 .map_err(|_| TrustformersError::lock_error("fusion mutex poisoned".to_string()))?;
214 let opt_state = state.parameter_states.entry(param_name.to_string()).or_insert_with(|| {
215 OptimizerState {
216 step: 0,
217 momentum: HashMap::new(),
218 variance: HashMap::new(),
219 ..Default::default()
220 }
221 });
222
223 match operation {
224 FusedOperation::FusedAdam {
225 lr,
226 beta1,
227 beta2,
228 eps,
229 weight_decay,
230 } => Self::fused_adam_update(
231 param_name,
232 parameter,
233 gradient,
234 opt_state,
235 lr,
236 beta1,
237 beta2,
238 eps,
239 weight_decay,
240 ),
241 FusedOperation::FusedAdamW {
242 lr,
243 beta1,
244 beta2,
245 eps,
246 weight_decay,
247 } => Self::fused_adamw_update(
248 param_name,
249 parameter,
250 gradient,
251 opt_state,
252 lr,
253 beta1,
254 beta2,
255 eps,
256 weight_decay,
257 ),
258 FusedOperation::FusedSGDMomentum {
259 lr,
260 momentum,
261 dampening,
262 weight_decay,
263 nesterov,
264 } => Self::fused_sgd_update(
265 param_name,
266 parameter,
267 gradient,
268 opt_state,
269 lr,
270 momentum,
271 dampening,
272 weight_decay,
273 nesterov,
274 ),
275 FusedOperation::FusedGradientClipping {
276 max_norm,
277 scale_factor,
278 } => {
279 let norm = gradient.norm()? as f64;
280 let scale = if norm > max_norm && norm > 0.0 {
281 (max_norm / norm) * scale_factor
282 } else {
283 scale_factor
284 };
285 Self::scale_tensor_in_place(gradient, scale as f32)
286 },
287 FusedOperation::FusedBatchNorm { .. } => Err(TrustformersError::not_implemented(
288 "FusedOperation::FusedBatchNorm has no fused implementation".to_string(),
289 )),
290 }
291 }
292
293 fn scale_tensor_in_place(tensor: &mut Tensor, scale: f32) -> Result<()> {
295 let mut data = tensor.data()?;
296 for value in data.iter_mut() {
297 *value *= scale;
298 }
299 tensor.set_data_f32(&data)
300 }
301
302 pub fn queue_operation(
304 &mut self,
305 param_name: String,
306 operation: FusedOperation,
307 parameter: Tensor,
308 gradient: Tensor,
309 ) -> Result<()> {
310 let should_execute = {
311 let mut pending = self
312 .pending_operations
313 .lock()
314 .map_err(|_| TrustformersError::lock_error("fusion mutex poisoned".to_string()))?;
315 pending.push((param_name, operation, parameter, gradient));
316 pending.len() >= self.config.batch_size
317 };
318
319 if should_execute {
321 self.execute_fused_batch()?;
322 }
323
324 Ok(())
325 }
326
327 pub fn execute_fused_batch(&mut self) -> Result<()> {
329 let mut pending = self
330 .pending_operations
331 .lock()
332 .map_err(|_| TrustformersError::lock_error("fusion mutex poisoned".to_string()))?;
333 if pending.is_empty() {
334 return Ok(());
335 }
336
337 let operations = std::mem::take(&mut *pending);
338 drop(pending);
339
340 let mut adam_ops = Vec::new();
342 let mut adamw_ops = Vec::new();
343 let mut sgd_ops = Vec::new();
344 let mut clip_ops = Vec::new();
345
346 for (param_name, op, param, grad) in operations {
347 match op {
348 FusedOperation::FusedAdam { .. } => adam_ops.push((param_name, op, param, grad)),
349 FusedOperation::FusedAdamW { .. } => adamw_ops.push((param_name, op, param, grad)),
350 FusedOperation::FusedSGDMomentum { .. } => {
351 sgd_ops.push((param_name, op, param, grad))
352 },
353 FusedOperation::FusedGradientClipping { .. } => {
354 clip_ops.push((param_name, op, param, grad))
355 },
356 _ => {
357 self.execute_single_operation(param_name, op, param, grad)?;
359 },
360 }
361 }
362
363 if !adam_ops.is_empty() {
365 self.execute_fused_adam_batch(adam_ops)?;
366 }
367 if !adamw_ops.is_empty() {
368 self.execute_fused_adamw_batch(adamw_ops)?;
369 }
370 if !sgd_ops.is_empty() {
371 self.execute_fused_sgd_batch(sgd_ops)?;
372 }
373 if !clip_ops.is_empty() {
374 self.execute_fused_clipping_batch(clip_ops)?;
375 }
376
377 Ok(())
378 }
379
380 fn execute_fused_adam_batch(
382 &mut self,
383 operations: Vec<(String, FusedOperation, Tensor, Tensor)>,
384 ) -> Result<()> {
385 let mut state = self
386 .state
387 .lock()
388 .map_err(|_| TrustformersError::lock_error("fusion mutex poisoned".to_string()))?;
389 let batch_size = operations.len();
390
391 for (param_name, op, mut param, grad) in operations {
392 if let FusedOperation::FusedAdam {
393 lr,
394 beta1,
395 beta2,
396 eps,
397 weight_decay,
398 } = op
399 {
400 let opt_state =
402 state.parameter_states.entry(param_name.clone()).or_insert_with(|| {
403 OptimizerState {
404 step: 0,
405 momentum: HashMap::new(),
406 variance: HashMap::new(),
407 ..Default::default()
408 }
409 });
410
411 Self::fused_adam_update(
413 ¶m_name,
414 &mut param,
415 &grad,
416 opt_state,
417 lr,
418 beta1,
419 beta2,
420 eps,
421 weight_decay,
422 )?;
423 self.record_updated_parameter(param_name, param)?;
424 }
425 }
426
427 state.fusion_stats.fused_operations += 1;
429 state.fusion_stats.avg_batch_size = (state.fusion_stats.avg_batch_size
430 * (state.fusion_stats.fused_operations - 1) as f64
431 + batch_size as f64)
432 / state.fusion_stats.fused_operations as f64;
433
434 let bandwidth_saved = batch_size * 4 * 8; state.fusion_stats.memory_bandwidth_saved += bandwidth_saved as u64;
437
438 Ok(())
439 }
440
441 fn execute_fused_adamw_batch(
443 &mut self,
444 operations: Vec<(String, FusedOperation, Tensor, Tensor)>,
445 ) -> Result<()> {
446 let mut state = self
447 .state
448 .lock()
449 .map_err(|_| TrustformersError::lock_error("fusion mutex poisoned".to_string()))?;
450 let batch_size = operations.len();
451
452 for (param_name, op, mut param, grad) in operations {
453 if let FusedOperation::FusedAdamW {
454 lr,
455 beta1,
456 beta2,
457 eps,
458 weight_decay,
459 } = op
460 {
461 let opt_state =
462 state.parameter_states.entry(param_name.clone()).or_insert_with(|| {
463 OptimizerState {
464 step: 0,
465 momentum: HashMap::new(),
466 variance: HashMap::new(),
467 ..Default::default()
468 }
469 });
470
471 Self::fused_adamw_update(
473 ¶m_name,
474 &mut param,
475 &grad,
476 opt_state,
477 lr,
478 beta1,
479 beta2,
480 eps,
481 weight_decay,
482 )?;
483 self.record_updated_parameter(param_name, param)?;
484 }
485 }
486
487 state.fusion_stats.fused_operations += 1;
489 let bandwidth_saved = batch_size * 4 * 8;
490 state.fusion_stats.memory_bandwidth_saved += bandwidth_saved as u64;
491
492 Ok(())
493 }
494
495 fn execute_fused_sgd_batch(
497 &mut self,
498 operations: Vec<(String, FusedOperation, Tensor, Tensor)>,
499 ) -> Result<()> {
500 let mut state = self
501 .state
502 .lock()
503 .map_err(|_| TrustformersError::lock_error("fusion mutex poisoned".to_string()))?;
504 let batch_size = operations.len();
505
506 for (param_name, op, mut param, grad) in operations {
507 if let FusedOperation::FusedSGDMomentum {
508 lr,
509 momentum,
510 dampening,
511 weight_decay,
512 nesterov,
513 } = op
514 {
515 let opt_state =
516 state.parameter_states.entry(param_name.clone()).or_insert_with(|| {
517 OptimizerState {
518 step: 0,
519 momentum: HashMap::new(),
520 ..Default::default()
521 }
522 });
523
524 Self::fused_sgd_update(
526 ¶m_name,
527 &mut param,
528 &grad,
529 opt_state,
530 lr,
531 momentum,
532 dampening,
533 weight_decay,
534 nesterov,
535 )?;
536 self.record_updated_parameter(param_name, param)?;
537 }
538 }
539
540 state.fusion_stats.fused_operations += 1;
542 let bandwidth_saved = batch_size * 2 * 8; state.fusion_stats.memory_bandwidth_saved += bandwidth_saved as u64;
544
545 Ok(())
546 }
547
548 fn execute_fused_clipping_batch(
550 &mut self,
551 operations: Vec<(String, FusedOperation, Tensor, Tensor)>,
552 ) -> Result<()> {
553 let mut state = self
554 .state
555 .lock()
556 .map_err(|_| TrustformersError::lock_error("fusion mutex poisoned".to_string()))?;
557 let batch_size = operations.len();
558
559 let mut gradients = Vec::new();
561 for (_, _, _, grad) in &operations {
562 gradients.push(grad.clone());
563 }
564
565 let global_norm = self.compute_global_norm(&gradients)?;
567
568 for (param_name, op, _, mut grad) in operations {
571 if let FusedOperation::FusedGradientClipping {
572 max_norm,
573 scale_factor,
574 } = op
575 {
576 let scale = if global_norm > max_norm && global_norm > 0.0 {
577 (max_norm / global_norm) * scale_factor
578 } else {
579 scale_factor
580 };
581 Self::scale_tensor_in_place(&mut grad, scale as f32)?;
582
583 let mut clipped = self.clipped_gradients.lock().map_err(|_| {
584 TrustformersError::lock_error(
585 "fusion clipped-gradient mutex poisoned".to_string(),
586 )
587 })?;
588 clipped.insert(param_name, grad);
589 }
590 }
591
592 state.fusion_stats.fused_operations += 1;
594 let bandwidth_saved = batch_size * 8; state.fusion_stats.memory_bandwidth_saved += bandwidth_saved as u64;
596
597 Ok(())
598 }
599
600 fn execute_single_operation(
602 &mut self,
603 _param_name: String,
604 _operation: FusedOperation,
605 _parameter: Tensor,
606 _gradient: Tensor,
607 ) -> Result<()> {
608 Ok(())
610 }
611
612 fn fused_adam_update(
617 param_name: &str,
618 param: &mut Tensor,
619 grad: &Tensor,
620 state: &mut OptimizerState,
621 lr: f64,
622 beta1: f64,
623 beta2: f64,
624 eps: f64,
625 weight_decay: f64,
626 ) -> Result<()> {
627 state.step += 1;
628 let param_id = param_name.to_string();
629 let param_len = param.data()?.len();
630
631 let momentum =
633 state.momentum.entry(param_id.clone()).or_insert_with(|| vec![0.0; param_len]);
634 let variance = state.variance.entry(param_id).or_insert_with(|| vec![0.0; param_len]);
635
636 let grad_data = grad.data()?;
637 let mut param_data = param.data()?;
638
639 let bias_correction1 = 1.0 - beta1.powi(state.step as i32);
641 let bias_correction2 = 1.0 - beta2.powi(state.step as i32);
642
643 for i in 0..param_data.len() {
645 let mut grad_val = grad_data[i];
646
647 if weight_decay > 0.0 {
649 grad_val += weight_decay as f32 * param_data[i];
650 }
651
652 momentum[i] = beta1 as f32 * momentum[i] + (1.0 - beta1 as f32) * grad_val;
654
655 variance[i] = beta2 as f32 * variance[i] + (1.0 - beta2 as f32) * grad_val * grad_val;
657
658 let m_hat = momentum[i] / bias_correction1 as f32;
660 let v_hat = variance[i] / bias_correction2 as f32;
661
662 param_data[i] -= lr as f32 * m_hat / (v_hat.sqrt() + eps as f32);
664 }
665
666 param.set_data_f32(¶m_data)
667 }
668
669 fn fused_adamw_update(
673 param_name: &str,
674 param: &mut Tensor,
675 grad: &Tensor,
676 state: &mut OptimizerState,
677 lr: f64,
678 beta1: f64,
679 beta2: f64,
680 eps: f64,
681 weight_decay: f64,
682 ) -> Result<()> {
683 state.step += 1;
684 let param_id = param_name.to_string();
685 let param_len = param.data()?.len();
686
687 let momentum =
689 state.momentum.entry(param_id.clone()).or_insert_with(|| vec![0.0; param_len]);
690 let variance = state.variance.entry(param_id).or_insert_with(|| vec![0.0; param_len]);
691
692 let grad_data = grad.data()?;
693 let mut param_data = param.data()?;
694
695 let bias_correction1 = 1.0 - beta1.powi(state.step as i32);
697 let bias_correction2 = 1.0 - beta2.powi(state.step as i32);
698
699 for i in 0..param_data.len() {
701 let grad_val = grad_data[i];
702
703 momentum[i] = beta1 as f32 * momentum[i] + (1.0 - beta1 as f32) * grad_val;
705
706 variance[i] = beta2 as f32 * variance[i] + (1.0 - beta2 as f32) * grad_val * grad_val;
708
709 let m_hat = momentum[i] / bias_correction1 as f32;
711 let v_hat = variance[i] / bias_correction2 as f32;
712
713 let adaptive_step = lr as f32 * m_hat / (v_hat.sqrt() + eps as f32);
715 let weight_decay_step = lr as f32 * weight_decay as f32 * param_data[i];
716
717 param_data[i] -= adaptive_step + weight_decay_step;
719 }
720
721 param.set_data_f32(¶m_data)
722 }
723
724 fn fused_sgd_update(
728 param_name: &str,
729 param: &mut Tensor,
730 grad: &Tensor,
731 state: &mut OptimizerState,
732 lr: f64,
733 momentum_coef: f64,
734 dampening: f64,
735 weight_decay: f64,
736 nesterov: bool,
737 ) -> Result<()> {
738 state.step += 1;
739 let param_id = param_name.to_string();
740 let param_len = param.data()?.len();
741
742 let momentum = state.momentum.entry(param_id).or_insert_with(|| vec![0.0; param_len]);
744
745 let grad_data = grad.data()?;
746 let mut param_data = param.data()?;
747
748 for i in 0..param_data.len() {
750 let mut grad_val = grad_data[i];
751
752 if weight_decay > 0.0 {
754 grad_val += weight_decay as f32 * param_data[i];
755 }
756
757 if momentum_coef > 0.0 {
759 if state.step == 1 {
760 momentum[i] = grad_val;
762 } else {
763 momentum[i] =
765 momentum_coef as f32 * momentum[i] + (1.0 - dampening as f32) * grad_val;
766 }
767
768 let update_direction = if nesterov {
770 grad_val + momentum_coef as f32 * momentum[i]
771 } else {
772 momentum[i]
773 };
774
775 param_data[i] -= lr as f32 * update_direction;
777 } else {
778 param_data[i] -= lr as f32 * grad_val;
780 }
781 }
782
783 param.set_data_f32(¶m_data)
784 }
785
786 fn compute_global_norm(&self, gradients: &[Tensor]) -> Result<f64> {
788 let mut total_norm_sq = 0.0;
789
790 for grad in gradients {
791 let norm = grad.norm()?;
792 total_norm_sq += norm * norm;
793 }
794
795 Ok(total_norm_sq.sqrt() as f64)
796 }
797
798 pub fn flush(&mut self) -> Result<()> {
800 self.execute_fused_batch()
801 }
802
803 pub fn get_fusion_stats(&self) -> FusionStats {
805 let state = self.state.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
806 state.fusion_stats.clone()
807 }
808
809 pub fn reset_stats(&mut self) {
811 let mut state = self.state.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
812 state.fusion_stats = FusionStats::default();
813 }
814
815 pub fn update_config(&mut self, config: FusionConfig) {
817 self.config = config;
818 }
819}
820
821#[cfg(target_arch = "x86_64")]
823pub mod simd {
824
825 pub fn simd_adam_update(
827 param: &mut [f32],
828 grad: &[f32],
829 momentum: &mut [f32],
830 velocity: &mut [f32],
831 lr: f32,
832 beta1: f32,
833 beta2: f32,
834 eps: f32,
835 step: i32,
836 ) {
837 use std::arch::x86_64::*;
838
839 let bias_correction1 = 1.0 - beta1.powi(step);
840 let bias_correction2 = 1.0 - beta2.powi(step);
841 let corrected_lr = lr * (bias_correction2.sqrt() / bias_correction1);
842
843 unsafe {
844 let beta1_vec = _mm256_set1_ps(beta1);
845 let beta2_vec = _mm256_set1_ps(beta2);
846 let one_minus_beta1 = _mm256_set1_ps(1.0 - beta1);
847 let one_minus_beta2 = _mm256_set1_ps(1.0 - beta2);
848 let eps_vec = _mm256_set1_ps(eps);
849 let lr_vec = _mm256_set1_ps(corrected_lr);
850
851 let chunks = param.len() / 8;
852 for i in 0..chunks {
853 let idx = i * 8;
854
855 let p = _mm256_loadu_ps(param.as_ptr().add(idx));
857 let g = _mm256_loadu_ps(grad.as_ptr().add(idx));
858 let m = _mm256_loadu_ps(momentum.as_ptr().add(idx));
859 let v = _mm256_loadu_ps(velocity.as_ptr().add(idx));
860
861 let m_new = _mm256_fmadd_ps(beta1_vec, m, _mm256_mul_ps(one_minus_beta1, g));
863
864 let g_sq = _mm256_mul_ps(g, g);
866 let v_new = _mm256_fmadd_ps(beta2_vec, v, _mm256_mul_ps(one_minus_beta2, g_sq));
867
868 let v_sqrt = _mm256_sqrt_ps(v_new);
870 let v_sqrt_eps = _mm256_add_ps(v_sqrt, eps_vec);
871 let update = _mm256_div_ps(m_new, v_sqrt_eps);
872 let p_new = _mm256_fnmadd_ps(lr_vec, update, p);
873
874 _mm256_storeu_ps(param.as_mut_ptr().add(idx), p_new);
876 _mm256_storeu_ps(momentum.as_mut_ptr().add(idx), m_new);
877 _mm256_storeu_ps(velocity.as_mut_ptr().add(idx), v_new);
878 }
879
880 for i in (chunks * 8)..param.len() {
882 let g = grad[i];
883 momentum[i] = beta1 * momentum[i] + (1.0 - beta1) * g;
884 velocity[i] = beta2 * velocity[i] + (1.0 - beta2) * g * g;
885 param[i] -= corrected_lr * momentum[i] / (velocity[i].sqrt() + eps);
886 }
887 }
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894 use trustformers_core::Tensor;
895
896 #[test]
897 fn test_fused_optimizer_creation() {
898 let config = FusionConfig::default();
899 let optimizer = FusedOptimizer::new(config).expect("Failed to create fused optimizer");
900
901 let stats = optimizer.get_fusion_stats();
902 assert_eq!(stats.fused_operations, 0);
903 }
904
905 #[test]
906 fn test_fused_adam_operation() {
907 let config = FusionConfig::default();
908 let mut optimizer = FusedOptimizer::new(config).expect("Failed to create fused optimizer");
909
910 let param = Tensor::ones(&[10, 10]).expect("Failed to create tensor");
911 let grad = Tensor::ones(&[10, 10]).expect("Failed to create tensor");
912
913 let operation = FusedOperation::FusedAdam {
914 lr: 0.001,
915 beta1: 0.9,
916 beta2: 0.999,
917 eps: 1e-8,
918 weight_decay: 0.0,
919 };
920
921 optimizer
922 .queue_operation("param1".to_string(), operation, param, grad)
923 .expect("Failed to queue operation");
924
925 optimizer.flush().expect("Flush failed");
926
927 let stats = optimizer.get_fusion_stats();
928 assert_eq!(stats.fused_operations, 1);
929 }
930
931 #[test]
932 fn test_fused_adamw_operation() {
933 let config = FusionConfig::default();
934 let mut optimizer = FusedOptimizer::new(config).expect("Failed to create fused optimizer");
935
936 let param = Tensor::ones(&[5, 5]).expect("Failed to create tensor");
937 let grad = Tensor::ones(&[5, 5]).expect("Failed to create tensor");
938
939 let operation = FusedOperation::FusedAdamW {
940 lr: 0.001,
941 beta1: 0.9,
942 beta2: 0.999,
943 eps: 1e-8,
944 weight_decay: 0.01,
945 };
946
947 optimizer
948 .queue_operation("param2".to_string(), operation, param, grad)
949 .expect("Failed to queue operation");
950
951 optimizer.flush().expect("Flush failed");
952
953 let stats = optimizer.get_fusion_stats();
954 assert_eq!(stats.fused_operations, 1);
955 }
956
957 #[test]
958 fn test_fused_sgd_operation() {
959 let config = FusionConfig::default();
960 let mut optimizer = FusedOptimizer::new(config).expect("Failed to create fused optimizer");
961
962 let param = Tensor::ones(&[3, 3]).expect("Failed to create tensor");
963 let grad = Tensor::ones(&[3, 3]).expect("Failed to create tensor");
964
965 let operation = FusedOperation::FusedSGDMomentum {
966 lr: 0.01,
967 momentum: 0.9,
968 dampening: 0.0,
969 weight_decay: 0.0,
970 nesterov: false,
971 };
972
973 optimizer
974 .queue_operation("param3".to_string(), operation, param, grad)
975 .expect("Failed to queue operation");
976
977 optimizer.flush().expect("Flush failed");
978
979 let stats = optimizer.get_fusion_stats();
980 assert_eq!(stats.fused_operations, 1);
981 }
982
983 #[test]
984 fn test_batch_fusion() {
985 let config = FusionConfig {
986 batch_size: 2,
987 ..FusionConfig::default()
988 };
989 let mut optimizer = FusedOptimizer::new(config).expect("Failed to create fused optimizer");
990
991 for i in 0..3 {
993 let param = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
994 let grad = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
995
996 let operation = FusedOperation::FusedAdam {
997 lr: 0.001,
998 beta1: 0.9,
999 beta2: 0.999,
1000 eps: 1e-8,
1001 weight_decay: 0.0,
1002 };
1003
1004 optimizer
1005 .queue_operation(format!("param_{}", i), operation, param, grad)
1006 .expect("Operation failed in test");
1007 }
1008
1009 let stats = optimizer.get_fusion_stats();
1011 assert!(stats.fused_operations > 0);
1012 }
1013
1014 #[test]
1015 fn test_fusion_stats() {
1016 let config = FusionConfig::default();
1017 let mut optimizer = FusedOptimizer::new(config).expect("Failed to create fused optimizer");
1018
1019 let param = Tensor::ones(&[10, 10]).expect("Failed to create tensor");
1020 let grad = Tensor::ones(&[10, 10]).expect("Failed to create tensor");
1021
1022 let operation = FusedOperation::FusedAdam {
1023 lr: 0.001,
1024 beta1: 0.9,
1025 beta2: 0.999,
1026 eps: 1e-8,
1027 weight_decay: 0.0,
1028 };
1029
1030 optimizer
1031 .queue_operation("param1".to_string(), operation, param, grad)
1032 .expect("Failed to queue operation");
1033
1034 optimizer.flush().expect("Flush failed");
1035
1036 let stats = optimizer.get_fusion_stats();
1037 assert_eq!(stats.fused_operations, 1);
1038 assert!(stats.memory_bandwidth_saved > 0);
1039
1040 optimizer.reset_stats();
1041 let reset_stats = optimizer.get_fusion_stats();
1042 assert_eq!(reset_stats.fused_operations, 0);
1043 assert_eq!(reset_stats.memory_bandwidth_saved, 0);
1044 }
1045
1046 #[test]
1047 fn test_global_norm_computation() {
1048 let config = FusionConfig::default();
1049 let optimizer = FusedOptimizer::new(config).expect("Failed to create fused optimizer");
1050
1051 let grad1 = Tensor::ones(&[3, 3]).expect("Failed to create tensor");
1052 let grad2 = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
1053
1054 let gradients = vec![grad1, grad2];
1055 let global_norm = optimizer
1056 .compute_global_norm(&gradients)
1057 .expect("Failed to compute global norm");
1058
1059 assert!((global_norm - 3.606).abs() < 0.01);
1061 }
1062
1063 #[test]
1066 fn test_fused_clipping_actually_clips() {
1067 let config = FusionConfig::default();
1068 let mut optimizer = FusedOptimizer::new(config).expect("create fused optimizer");
1069
1070 let grad = Tensor::from_vec(vec![5.0_f32; 4], &[4]).expect("grad tensor");
1072 let param = Tensor::from_vec(vec![0.0_f32; 4], &[4]).expect("param tensor");
1073 let before = grad.norm().expect("norm before");
1074 assert!((before - 10.0).abs() < 1e-4, "precondition norm: {before}");
1075
1076 optimizer
1077 .queue_operation(
1078 "w".to_string(),
1079 FusedOperation::FusedGradientClipping {
1080 max_norm: 1.0,
1081 scale_factor: 1.0,
1082 },
1083 param,
1084 grad,
1085 )
1086 .expect("queue clipping");
1087 optimizer.flush().expect("flush");
1088
1089 let clipped = optimizer.take_clipped_gradients().expect("take clipped");
1090 let out = clipped.get("w").expect("clipped gradient recorded");
1091 let after = out.norm().expect("norm after");
1092 assert!(
1093 (after - 1.0).abs() < 1e-4,
1094 "gradient norm must be clipped to max_norm, got {after}"
1095 );
1096 }
1097
1098 #[test]
1100 fn test_fused_clipping_applies_scale_factor_below_threshold() {
1101 let config = FusionConfig::default();
1102 let mut optimizer = FusedOptimizer::new(config).expect("create fused optimizer");
1103
1104 let grad = Tensor::from_vec(vec![1.0_f32; 4], &[4]).expect("grad tensor");
1105 let param = Tensor::from_vec(vec![0.0_f32; 4], &[4]).expect("param tensor");
1106
1107 optimizer
1108 .queue_operation(
1109 "w".to_string(),
1110 FusedOperation::FusedGradientClipping {
1111 max_norm: 100.0,
1112 scale_factor: 0.5,
1113 },
1114 param,
1115 grad,
1116 )
1117 .expect("queue clipping");
1118 optimizer.flush().expect("flush");
1119
1120 let clipped = optimizer.take_clipped_gradients().expect("take clipped");
1121 let data = clipped.get("w").expect("clipped gradient").data().expect("data");
1122 for v in data {
1123 assert!((v - 0.5).abs() < 1e-6, "scale_factor must be applied: {v}");
1124 }
1125 }
1126
1127 #[test]
1130 fn test_fused_adam_moves_parameter() {
1131 let config = FusionConfig::default();
1132 let mut optimizer = FusedOptimizer::new(config).expect("create fused optimizer");
1133
1134 let param = Tensor::from_vec(vec![1.0_f32; 4], &[4]).expect("param tensor");
1135 let grad = Tensor::from_vec(vec![1.0_f32; 4], &[4]).expect("grad tensor");
1136
1137 optimizer
1138 .queue_operation(
1139 "w".to_string(),
1140 FusedOperation::FusedAdam {
1141 lr: 0.1,
1142 beta1: 0.9,
1143 beta2: 0.999,
1144 eps: 1e-8,
1145 weight_decay: 0.0,
1146 },
1147 param,
1148 grad,
1149 )
1150 .expect("queue adam");
1151 optimizer.flush().expect("flush");
1152
1153 let updated = optimizer.take_updated_parameters().expect("take updated");
1154 let data = updated.get("w").expect("updated parameter").data().expect("data");
1155 for v in data {
1157 assert!(v < 1.0, "parameter must decrease, got {v}");
1158 assert!(
1159 (v - 0.9).abs() < 1e-3,
1160 "first Adam step should be ≈ lr = 0.1, got {v}"
1161 );
1162 }
1163 }
1164
1165 #[test]
1168 fn test_fused_adam_in_place_carries_state() {
1169 let config = FusionConfig::default();
1170 let mut optimizer = FusedOptimizer::new(config).expect("create fused optimizer");
1171
1172 let mut param = Tensor::from_vec(vec![0.0_f32; 2], &[2]).expect("param");
1173 let mut grad = Tensor::from_vec(vec![1.0_f32; 2], &[2]).expect("grad");
1174 let op = FusedOperation::FusedAdam {
1175 lr: 0.1,
1176 beta1: 0.9,
1177 beta2: 0.999,
1178 eps: 1e-8,
1179 weight_decay: 0.0,
1180 };
1181
1182 optimizer
1183 .apply_in_place("w", op.clone(), &mut param, &mut grad)
1184 .expect("step 1");
1185 let after_first = param.data().expect("data")[0];
1186 assert!(after_first < 0.0, "first step must move the parameter");
1187
1188 let mut zero_grad = Tensor::from_vec(vec![0.0_f32; 2], &[2]).expect("zero grad");
1191 optimizer.apply_in_place("w", op, &mut param, &mut zero_grad).expect("step 2");
1192 let after_second = param.data().expect("data")[0];
1193
1194 assert!(
1195 (after_second - after_first).abs() > 1e-6,
1196 "carried momentum must still move the parameter on a zero gradient: \
1197 {after_first} -> {after_second}"
1198 );
1199 }
1200
1201 #[test]
1203 fn test_fused_sgd_in_place_moves_parameter() {
1204 let config = FusionConfig::default();
1205 let mut optimizer = FusedOptimizer::new(config).expect("create fused optimizer");
1206
1207 let mut param = Tensor::from_vec(vec![1.0_f32, 2.0], &[2]).expect("param");
1208 let mut grad = Tensor::from_vec(vec![1.0_f32, 1.0], &[2]).expect("grad");
1209
1210 optimizer
1211 .apply_in_place(
1212 "w",
1213 FusedOperation::FusedSGDMomentum {
1214 lr: 0.5,
1215 momentum: 0.0,
1216 dampening: 0.0,
1217 weight_decay: 0.0,
1218 nesterov: false,
1219 },
1220 &mut param,
1221 &mut grad,
1222 )
1223 .expect("sgd step");
1224
1225 let data = param.data().expect("data");
1226 assert!((data[0] - 0.5).abs() < 1e-6, "got {}", data[0]);
1228 assert!((data[1] - 1.5).abs() < 1e-6, "got {}", data[1]);
1229 }
1230
1231 #[test]
1233 fn test_fused_adam_converges_on_quadratic() {
1234 let config = FusionConfig::default();
1235 let mut optimizer = FusedOptimizer::new(config).expect("create fused optimizer");
1236
1237 let mut param = Tensor::from_vec(vec![1.0_f32; 4], &[4]).expect("param");
1238 let initial_loss: f32 = param.data().expect("data").iter().map(|v| v * v).sum();
1239
1240 for _ in 0..400 {
1241 let grad_data: Vec<f32> = param.data().expect("data").iter().map(|v| 2.0 * v).collect();
1242 let mut grad = Tensor::from_vec(grad_data, &[4]).expect("grad");
1243 optimizer
1244 .apply_in_place(
1245 "w",
1246 FusedOperation::FusedAdam {
1247 lr: 0.05,
1248 beta1: 0.9,
1249 beta2: 0.999,
1250 eps: 1e-8,
1251 weight_decay: 0.0,
1252 },
1253 &mut param,
1254 &mut grad,
1255 )
1256 .expect("adam step");
1257 }
1258
1259 let final_loss: f32 = param.data().expect("data").iter().map(|v| v * v).sum();
1260 assert!(
1261 final_loss < initial_loss * 1e-2,
1262 "loss must decrease: {initial_loss} -> {final_loss}"
1263 );
1264 }
1265}