Skip to main content

trustformers_optim/
fusion.rs

1//! Optimizer Fusion Techniques
2//!
3//! This module provides advanced optimizer fusion techniques for performance optimization.
4//! It combines multiple optimizer operations into fused kernels to reduce memory bandwidth
5//! and improve overall training performance.
6
7// reason: research-stage module — reserved API/scaffolding fields and methods
8// retained intentionally for in-progress features; not yet on active call paths.
9#![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/// Fused optimizer operations for performance optimization
19#[derive(Debug, Clone)]
20pub enum FusedOperation {
21    /// Fused Adam update (parameter, gradient, momentum, velocity)
22    FusedAdam {
23        lr: f64,
24        beta1: f64,
25        beta2: f64,
26        eps: f64,
27        weight_decay: f64,
28    },
29    /// Fused AdamW update with decoupled weight decay
30    FusedAdamW {
31        lr: f64,
32        beta1: f64,
33        beta2: f64,
34        eps: f64,
35        weight_decay: f64,
36    },
37    /// Fused SGD with momentum
38    FusedSGDMomentum {
39        lr: f64,
40        momentum: f64,
41        dampening: f64,
42        weight_decay: f64,
43        nesterov: bool,
44    },
45    /// Fused gradient clipping and scaling
46    FusedGradientClipping { max_norm: f64, scale_factor: f64 },
47    /// Fused batch normalization update
48    FusedBatchNorm { eps: f64, momentum: f64 },
49}
50
51/// Configuration for fused optimizer operations
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct FusionConfig {
54    /// Enable memory bandwidth optimization
55    pub enable_memory_coalescing: bool,
56    /// Use vectorized operations when possible
57    pub enable_vectorization: bool,
58    /// Batch size for parameter updates
59    pub batch_size: usize,
60    /// Enable kernel fusion for compatible operations
61    pub enable_kernel_fusion: bool,
62    /// Buffer size for batched operations
63    pub buffer_size: usize,
64    /// Enable asynchronous updates
65    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/// Fused optimizer state for multiple parameters
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct FusedOptimizerState {
84    /// Parameter states indexed by parameter name
85    pub parameter_states: HashMap<String, OptimizerState>,
86    /// Fused operation buffers
87    pub operation_buffers: HashMap<String, Vec<f64>>,
88    /// Fusion statistics
89    pub fusion_stats: FusionStats,
90}
91
92/// Statistics for fusion operations
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct FusionStats {
95    /// Number of fused operations executed
96    pub fused_operations: u64,
97    /// Memory bandwidth saved (bytes)
98    pub memory_bandwidth_saved: u64,
99    /// FLOPS saved through fusion
100    pub flops_saved: u64,
101    /// Average batch size
102    pub avg_batch_size: f64,
103    /// Fusion efficiency ratio
104    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/// Fused optimizer that combines multiple optimization operations.
120///
121/// # Result retrieval contract
122///
123/// [`queue_operation`](FusedOptimizer::queue_operation) takes **owned** parameter and
124/// gradient tensors, so the caller's own bindings are never mutated. The updated
125/// parameters (and, for [`FusedOperation::FusedGradientClipping`], the clipped
126/// gradients) are stored internally keyed by the `param_name` supplied at queue time and
127/// must be collected with [`take_updated_parameters`](FusedOptimizer::take_updated_parameters)
128/// / [`take_clipped_gradients`](FusedOptimizer::take_clipped_gradients) after
129/// [`flush`](FusedOptimizer::flush).
130///
131/// Callers that want in-place semantics should use
132/// [`apply_in_place`](FusedOptimizer::apply_in_place), which updates a `&mut Tensor`
133/// directly and bypasses the batching queue.
134#[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    /// Parameters updated by the most recent batch executions, keyed by `param_name`.
141    updated_parameters: Arc<Mutex<HashMap<String, Tensor>>>,
142    /// Gradients clipped by the most recent batch executions, keyed by `param_name`.
143    clipped_gradients: Arc<Mutex<HashMap<String, Tensor>>>,
144}
145
146impl FusedOptimizer {
147    /// Create new fused optimizer
148    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    /// Record an updated parameter tensor so the caller can retrieve it after `flush()`.
166    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    /// Takes (and clears) the parameters updated by previously executed batches.
175    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    /// Returns a clone of the most recent updated value for `param_name`, if any.
183    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    /// Takes (and clears) the gradients clipped by previously executed batches.
191    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    /// Applies a single fused operation directly to a caller-owned parameter tensor.
199    ///
200    /// Unlike [`queue_operation`](FusedOptimizer::queue_operation) this bypasses the
201    /// batching queue and mutates `parameter` (or, for gradient clipping, `gradient`)
202    /// in place, so no result retrieval step is needed.
203    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    /// Multiplies every element of `tensor` by `scale`, in place.
294    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    /// Add operation to fusion queue
303    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        // Execute batch if buffer is full
320        if should_execute {
321            self.execute_fused_batch()?;
322        }
323
324        Ok(())
325    }
326
327    /// Execute all pending operations in a fused manner
328    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        // Group operations by type for maximum fusion efficiency
341        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                    // Handle other operations individually
358                    self.execute_single_operation(param_name, op, param, grad)?;
359                },
360            }
361        }
362
363        // Execute fused batches
364        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    /// Execute fused Adam operations
381    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                // Get or create optimizer state
401                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                // Fused Adam update with optimized memory access
412                Self::fused_adam_update(
413                    &param_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        // Update fusion statistics
428        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        // Estimate memory bandwidth savings (simplified)
435        let bandwidth_saved = batch_size * 4 * 8; // 4 tensors * 8 bytes per element (approximate)
436        state.fusion_stats.memory_bandwidth_saved += bandwidth_saved as u64;
437
438        Ok(())
439    }
440
441    /// Execute fused AdamW operations
442    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                // Fused AdamW update with decoupled weight decay
472                Self::fused_adamw_update(
473                    &param_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        // Update statistics
488        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    /// Execute fused SGD operations
496    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                // Fused SGD with momentum update
525                Self::fused_sgd_update(
526                    &param_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        // Update statistics
541        state.fusion_stats.fused_operations += 1;
542        let bandwidth_saved = batch_size * 2 * 8; // SGD uses fewer tensors
543        state.fusion_stats.memory_bandwidth_saved += bandwidth_saved as u64;
544
545        Ok(())
546    }
547
548    /// Execute fused gradient clipping operations
549    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        // Collect all gradients for global norm computation
560        let mut gradients = Vec::new();
561        for (_, _, _, grad) in &operations {
562            gradients.push(grad.clone());
563        }
564
565        // Compute global gradient norm for batch
566        let global_norm = self.compute_global_norm(&gradients)?;
567
568        // Scale factor shared by the whole batch: clip only when the *global* norm
569        // exceeds `max_norm`, matching `torch.nn.utils.clip_grad_norm_` semantics.
570        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        // Update statistics
593        state.fusion_stats.fused_operations += 1;
594        let bandwidth_saved = batch_size * 8; // Single pass through gradients
595        state.fusion_stats.memory_bandwidth_saved += bandwidth_saved as u64;
596
597        Ok(())
598    }
599
600    /// Execute single operation (fallback for non-batchable operations)
601    fn execute_single_operation(
602        &mut self,
603        _param_name: String,
604        _operation: FusedOperation,
605        _parameter: Tensor,
606        _gradient: Tensor,
607    ) -> Result<()> {
608        // Implementation for individual operations
609        Ok(())
610    }
611
612    /// Optimized Adam update with fused operations.
613    ///
614    /// Writes the updated values back into `param` — the per-parameter momentum and
615    /// variance buffers live in `state`, keyed by the caller-supplied `param_name`.
616    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        // Get or initialize momentum and variance buffers
632        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        // Bias correction factors
640        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        // Fused update loop - combines all operations in single pass
644        for i in 0..param_data.len() {
645            let mut grad_val = grad_data[i];
646
647            // Apply weight decay if specified (L2 regularization)
648            if weight_decay > 0.0 {
649                grad_val += weight_decay as f32 * param_data[i];
650            }
651
652            // Update biased first moment estimate (momentum)
653            momentum[i] = beta1 as f32 * momentum[i] + (1.0 - beta1 as f32) * grad_val;
654
655            // Update biased second raw moment estimate (variance)
656            variance[i] = beta2 as f32 * variance[i] + (1.0 - beta2 as f32) * grad_val * grad_val;
657
658            // Compute bias-corrected first and second moment estimates
659            let m_hat = momentum[i] / bias_correction1 as f32;
660            let v_hat = variance[i] / bias_correction2 as f32;
661
662            // Update parameter with fused Adam step
663            param_data[i] -= lr as f32 * m_hat / (v_hat.sqrt() + eps as f32);
664        }
665
666        param.set_data_f32(&param_data)
667    }
668
669    /// Optimized AdamW update with fused operations and decoupled weight decay.
670    ///
671    /// Writes the updated values back into `param`.
672    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        // Get or initialize momentum and variance buffers
688        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        // Bias correction factors
696        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        // Fused AdamW update loop - decoupled weight decay
700        for i in 0..param_data.len() {
701            let grad_val = grad_data[i];
702
703            // Update biased first moment estimate (momentum)
704            momentum[i] = beta1 as f32 * momentum[i] + (1.0 - beta1 as f32) * grad_val;
705
706            // Update biased second raw moment estimate (variance)
707            variance[i] = beta2 as f32 * variance[i] + (1.0 - beta2 as f32) * grad_val * grad_val;
708
709            // Compute bias-corrected first and second moment estimates
710            let m_hat = momentum[i] / bias_correction1 as f32;
711            let v_hat = variance[i] / bias_correction2 as f32;
712
713            // AdamW update: apply weight decay directly to parameters (decoupled)
714            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            // Combined update with decoupled weight decay
718            param_data[i] -= adaptive_step + weight_decay_step;
719        }
720
721        param.set_data_f32(&param_data)
722    }
723
724    /// Optimized SGD update with fused momentum.
725    ///
726    /// Writes the updated values back into `param`.
727    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        // Get or initialize momentum buffer
743        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        // Fused SGD update loop with momentum
749        for i in 0..param_data.len() {
750            let mut grad_val = grad_data[i];
751
752            // Apply weight decay if specified
753            if weight_decay > 0.0 {
754                grad_val += weight_decay as f32 * param_data[i];
755            }
756
757            // Update momentum buffer
758            if momentum_coef > 0.0 {
759                if state.step == 1 {
760                    // First step: initialize momentum with gradient
761                    momentum[i] = grad_val;
762                } else {
763                    // Update momentum with dampening
764                    momentum[i] =
765                        momentum_coef as f32 * momentum[i] + (1.0 - dampening as f32) * grad_val;
766                }
767
768                // Apply Nesterov momentum if enabled
769                let update_direction = if nesterov {
770                    grad_val + momentum_coef as f32 * momentum[i]
771                } else {
772                    momentum[i]
773                };
774
775                // Update parameter
776                param_data[i] -= lr as f32 * update_direction;
777            } else {
778                // Simple SGD without momentum
779                param_data[i] -= lr as f32 * grad_val;
780            }
781        }
782
783        param.set_data_f32(&param_data)
784    }
785
786    /// Compute global gradient norm for clipping
787    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    /// Flush all pending operations
799    pub fn flush(&mut self) -> Result<()> {
800        self.execute_fused_batch()
801    }
802
803    /// Get fusion statistics
804    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    /// Reset fusion statistics
810    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    /// Update fusion configuration
816    pub fn update_config(&mut self, config: FusionConfig) {
817        self.config = config;
818    }
819}
820
821/// SIMD-optimized vectorized operations
822#[cfg(target_arch = "x86_64")]
823pub mod simd {
824
825    /// SIMD-optimized Adam update
826    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                // Load values
856                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                // Update momentum: momentum = beta1 * momentum + (1 - beta1) * grad
862                let m_new = _mm256_fmadd_ps(beta1_vec, m, _mm256_mul_ps(one_minus_beta1, g));
863
864                // Update velocity: velocity = beta2 * velocity + (1 - beta2) * grad^2
865                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                // Update parameter: param = param - lr * momentum / (sqrt(velocity) + eps)
869                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                // Store results
875                _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            // Handle remaining elements
881            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        // Queue multiple operations
992        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        // Should have executed batch automatically
1010        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        // Expected: sqrt(9 + 4) = sqrt(13) ≈ 3.606
1060        assert!((global_norm - 3.606).abs() < 0.01);
1061    }
1062
1063    /// Regression: the clipping arm used to compute `clip_coef` and drop the result,
1064    /// so a gradient with norm above `max_norm` came back unchanged.
1065    #[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        // 4 elements of 5.0 => norm = sqrt(4 * 25) = 10.0
1071        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    /// Regression: `scale_factor` was also dropped on the non-clipping path.
1099    #[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    /// Regression: `fused_adam_update` wrote into `param.data()?`, a throwaway copy,
1128    /// so the parameter never moved.
1129    #[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        // Step 1 Adam with g=1: m_hat = g, v_hat = g^2 => step ≈ lr.
1156        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    /// Adam state must persist across steps: identical gradients give shrinking steps
1166    /// once the bias correction saturates.
1167    #[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        // Second step with a *zero* gradient: only carried momentum can move the
1189        // parameter now. A stateless implementation would leave it exactly in place.
1190        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    /// Regression: SGD wrote into a throwaway copy too.
1202    #[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        // Plain SGD: p -= lr * g  =>  1.0 - 0.5 = 0.5, 2.0 - 0.5 = 1.5
1227        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    /// Convergence smoke test on a quadratic bowl f(x) = sum(x^2), grad = 2x.
1232    #[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}