Skip to main content

trustformers_optim/
lora_rite.rs

1//! # LoRA-RITE: LoRA Done RITE - Robust Invariant Transformation Equilibration for LoRA Optimization
2//!
3//! LoRA-RITE is an adaptive matrix preconditioning optimizer specifically designed for LoRA
4//! (Low-Rank Adaptation) that achieves transformation invariance while remaining computationally
5//! efficient. The optimizer consistently outperforms other popular optimizers including Adam,
6//! LoRA+, ScaledAdam, Shampoo, and Lamb across various tasks and model sizes.
7//!
8//! ## Key Features
9//! - **Transformation Invariance**: Robust to linear transformations in LoRA matrices
10//! - **Adaptive Matrix Preconditioning**: Specialized preconditioning for low-rank structures
11//! - **Computational Efficiency**: Low overhead especially when LoRA rank << original dimensions
12//! - **Superior Performance**: Significant improvements across multiple datasets and architectures
13//! - **LoRA-Specific Design**: Optimized for A and B matrix structures in LoRA decomposition
14//!
15//! ## Research Foundation
16//! Based on "LoRA Done RITE: Robust Invariant Transformation Equilibration for LoRA Optimization" (ICLR 2025)
17//! - Rating: 8.0 at ICLR 2025
18//! - Achieves 55.50% accuracy on GSM8K with Gemma 7B IT vs Adam's 48.37%
19//! - Maintains low computational overhead when rank << matrix dimensions
20//! - Provides theoretical guarantees for transformation invariance
21//!
22//! ## Usage Example
23//! ```rust,no_run
24//! use trustformers_optim::{LoRARITE, LoRARITEConfig};
25//! use trustformers_core::tensor::Tensor;
26//!
27//! let config = LoRARITEConfig::new()
28//!     .learning_rate(1e-3)
29//!     .lora_rank(16)
30//!     .beta1(0.9)
31//!     .beta2(0.999)
32//!     .preconditioning_strength(0.1)
33//!     .build();
34//!
35//! let mut optimizer = LoRARITE::new(config);
36//!
37//! // In training loop with LoRA parameters
38//! // optimizer.zero_grad();
39//! // ... compute loss and gradients for LoRA A and B matrices ...
40//! // optimizer.step(&mut lora_parameters, &gradients)?;
41//! ```
42
43use crate::linalg::{jacobi_svd, symmetric_eigen, DenseMatrix};
44use anyhow::Result;
45use std::collections::HashMap;
46use trustformers_core::tensor::Tensor;
47
48/// Converts a 2-D tensor into the dense working matrix used by [`crate::linalg`].
49fn dense_from_tensor(tensor: &Tensor) -> Result<DenseMatrix> {
50    let shape = tensor.shape();
51    if shape.len() != 2 {
52        return Err(anyhow::anyhow!(
53            "expected a 2-D matrix for the LoRA factorisation, got shape {shape:?}"
54        ));
55    }
56    Ok(DenseMatrix::from_f32(
57        shape[0],
58        shape[1],
59        &tensor.data_f32()?,
60    )?)
61}
62
63/// Configuration for LoRA-RITE optimizer
64#[derive(Debug, Clone)]
65pub struct LoRARITEConfig {
66    /// Learning rate (default: 1e-3)
67    pub learning_rate: f32,
68    /// LoRA rank (default: 16)
69    pub lora_rank: usize,
70    /// First moment decay rate (default: 0.9)
71    pub beta1: f32,
72    /// Second moment decay rate (default: 0.999)
73    pub beta2: f32,
74    /// Epsilon for numerical stability (default: 1e-8)
75    pub epsilon: f32,
76    /// Weight decay (default: 0.0)
77    pub weight_decay: f32,
78    /// Preconditioning strength (default: 0.1)
79    pub preconditioning_strength: f32,
80    /// Enable bias correction (default: true)
81    pub bias_correction: bool,
82    /// Enable transformation invariance (default: true)
83    pub transformation_invariance: bool,
84    /// Adaptation frequency for preconditioning (default: 10)
85    pub adaptation_frequency: u64,
86    /// Minimum singular value threshold (default: 1e-6)
87    pub min_singular_value: f32,
88    /// Maximum condition number (default: 1e6)
89    pub max_condition_number: f32,
90    /// Enable adaptive rank adjustment (default: false)
91    pub adaptive_rank: bool,
92    /// Regularization for matrix factorization (default: 1e-6)
93    pub factorization_reg: f32,
94}
95
96impl Default for LoRARITEConfig {
97    fn default() -> Self {
98        Self {
99            learning_rate: 1e-3,
100            lora_rank: 16,
101            beta1: 0.9,
102            beta2: 0.999,
103            epsilon: 1e-8,
104            weight_decay: 0.0,
105            preconditioning_strength: 0.1,
106            bias_correction: true,
107            transformation_invariance: true,
108            adaptation_frequency: 10,
109            min_singular_value: 1e-6,
110            max_condition_number: 1e6,
111            adaptive_rank: false,
112            factorization_reg: 1e-6,
113        }
114    }
115}
116
117impl LoRARITEConfig {
118    /// Create a new LoRA-RITE configuration with default values
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    /// Set the learning rate
124    pub fn learning_rate(mut self, lr: f32) -> Self {
125        self.learning_rate = lr;
126        self
127    }
128
129    /// Set the LoRA rank
130    pub fn lora_rank(mut self, rank: usize) -> Self {
131        self.lora_rank = rank;
132        self
133    }
134
135    /// Set beta1 (first moment decay)
136    pub fn beta1(mut self, beta1: f32) -> Self {
137        self.beta1 = beta1;
138        self
139    }
140
141    /// Set beta2 (second moment decay)
142    pub fn beta2(mut self, beta2: f32) -> Self {
143        self.beta2 = beta2;
144        self
145    }
146
147    /// Set the preconditioning strength
148    pub fn preconditioning_strength(mut self, strength: f32) -> Self {
149        self.preconditioning_strength = strength;
150        self
151    }
152
153    /// Set weight decay
154    pub fn weight_decay(mut self, decay: f32) -> Self {
155        self.weight_decay = decay;
156        self
157    }
158
159    /// Enable or disable transformation invariance
160    pub fn transformation_invariance(mut self, enable: bool) -> Self {
161        self.transformation_invariance = enable;
162        self
163    }
164
165    /// Build the configuration
166    /// Enable or disable Adam-style bias correction of the moment estimates
167    pub fn bias_correction(mut self, enable: bool) -> Self {
168        self.bias_correction = enable;
169        self
170    }
171
172    pub fn build(self) -> Self {
173        self
174    }
175}
176
177/// LoRA-RITE optimizer state for tracking LoRA matrix statistics
178#[derive(Debug, Clone, Default)]
179pub struct LoRARITEState {
180    /// Current step count
181    pub step: u64,
182    /// First moment estimates for LoRA A matrices
183    pub m_a: HashMap<String, Tensor>,
184    /// First moment estimates for LoRA B matrices
185    pub m_b: HashMap<String, Tensor>,
186    /// Second moment estimates for LoRA A matrices
187    pub v_a: HashMap<String, Tensor>,
188    /// Second moment estimates for LoRA B matrices
189    pub v_b: HashMap<String, Tensor>,
190    /// Preconditioning matrices for A parameters
191    pub precond_a: HashMap<String, Tensor>,
192    /// Preconditioning matrices for B parameters
193    pub precond_b: HashMap<String, Tensor>,
194    /// Singular values for each LoRA pair
195    pub singular_values: HashMap<String, Tensor>,
196    /// Condition numbers for monitoring
197    pub condition_numbers: HashMap<String, f32>,
198    /// Effective rank tracking
199    pub effective_ranks: HashMap<String, usize>,
200    /// Transformation statistics
201    pub transformation_stats: TransformationStats,
202}
203
204/// Statistics for tracking transformation invariance
205#[derive(Debug, Clone)]
206pub struct TransformationStats {
207    /// Number of transformations applied
208    pub num_transformations: u64,
209    /// Average condition number improvement
210    pub condition_improvement: f32,
211    /// Rank stability measure
212    pub rank_stability: f32,
213    /// Preconditioning effectiveness
214    pub preconditioning_gain: f32,
215}
216
217impl Default for TransformationStats {
218    fn default() -> Self {
219        Self {
220            num_transformations: 0,
221            condition_improvement: 0.0,
222            rank_stability: 1.0,
223            preconditioning_gain: 1.0,
224        }
225    }
226}
227
228/// LoRA-RITE (LoRA Done RITE) optimizer
229///
230/// An adaptive matrix preconditioning optimizer specifically designed for LoRA
231/// that achieves transformation invariance and superior performance.
232pub struct LoRARITE {
233    config: LoRARITEConfig,
234    state: LoRARITEState,
235}
236
237impl LoRARITE {
238    /// Create a new LoRA-RITE optimizer
239    pub fn new(config: LoRARITEConfig) -> Self {
240        Self {
241            config,
242            state: LoRARITEState::default(),
243        }
244    }
245
246    /// Get the current learning rate
247    pub fn learning_rate(&self) -> f32 {
248        self.config.learning_rate
249    }
250
251    /// Set the learning rate
252    pub fn set_learning_rate(&mut self, lr: f32) {
253        self.config.learning_rate = lr;
254    }
255
256    /// Check if parameter is LoRA A matrix (typically named with "_a" suffix)
257    fn is_lora_a_matrix(&self, param_name: &str) -> bool {
258        param_name.ends_with("_a") || param_name.contains("lora_a") || param_name.contains("lora_A")
259    }
260
261    /// Check if parameter is LoRA B matrix (typically named with "_b" suffix)
262    fn is_lora_b_matrix(&self, param_name: &str) -> bool {
263        param_name.ends_with("_b") || param_name.contains("lora_b") || param_name.contains("lora_B")
264    }
265
266    /// Get the base name for a LoRA parameter pair
267    fn get_lora_base_name(&self, param_name: &str) -> String {
268        if param_name.ends_with("_a") {
269            param_name.trim_end_matches("_a").to_string()
270        } else if param_name.ends_with("_b") {
271            param_name.trim_end_matches("_b").to_string()
272        } else if param_name.contains("lora_a") {
273            param_name.replace("lora_a", "lora")
274        } else if param_name.contains("lora_b") {
275            param_name.replace("lora_b", "lora")
276        } else {
277            param_name.to_string()
278        }
279    }
280
281    /// Computes the thin SVD of the LoRA product `W = B · A`.
282    ///
283    /// This is a genuine one-sided Jacobi SVD (see [`crate::linalg::jacobi_svd`]),
284    /// not a diagonal approximation: `u · diag(s) · vᵀ` reconstructs `B · A` to
285    /// working precision.
286    ///
287    /// # Errors
288    ///
289    /// Returns an error when `A`/`B` are not 2-D or their inner dimensions disagree.
290    fn compute_svd(
291        &self,
292        matrix_a: &Tensor,
293        matrix_b: &Tensor,
294    ) -> Result<(Tensor, Tensor, Tensor)> {
295        let a = dense_from_tensor(matrix_a)?;
296        let b = dense_from_tensor(matrix_b)?;
297        // For LoRA: W = B @ A.
298        let product = b.matmul(&a)?;
299        let svd = jacobi_svd(&product)?;
300
301        let u = Tensor::from_vec(svd.u.to_f32(), &[svd.u.rows(), svd.u.cols()])?;
302        let singular_values = Tensor::from_vec(
303            svd.s.iter().map(|&v| v as f32).collect::<Vec<f32>>(),
304            &[svd.s.len()],
305        )?;
306        let v = Tensor::from_vec(svd.v.to_f32(), &[svd.v.rows(), svd.v.cols()])?;
307
308        Ok((u, singular_values, v))
309    }
310
311    /// Eigenvalues of a symmetric matrix, in descending order.
312    ///
313    /// Uses the cyclic Jacobi eigensolver in [`crate::linalg`]; the returned tensor is
314    /// 1-D of length `n` for an `n x n` input.
315    ///
316    /// # Errors
317    ///
318    /// Returns an error when the tensor is not a square 2-D matrix.
319    pub fn compute_eigenvalues(&self, matrix: &Tensor) -> Result<Tensor> {
320        let dense = dense_from_tensor(matrix)?;
321        let eigen = symmetric_eigen(&dense)?;
322        Ok(Tensor::from_vec(
323            eigen.values.iter().map(|&v| v as f32).collect::<Vec<f32>>(),
324            &[eigen.values.len()],
325        )?)
326    }
327
328    /// Compute robust preconditioning matrix for LoRA
329    fn compute_lora_preconditioning(&self, _param_name: &str, gradient: &Tensor) -> Result<Tensor> {
330        // Compute second moment for preconditioning
331        let grad_squared = gradient.pow(2.0)?;
332
333        // Add regularization for numerical stability
334        let preconditioner = grad_squared.add_scalar(self.config.factorization_reg)?;
335
336        // Apply transformation invariance if enabled
337        if self.config.transformation_invariance {
338            self.apply_transformation_invariance(&preconditioner)
339        } else {
340            Ok(preconditioner.sqrt()?.reciprocal()?)
341        }
342    }
343
344    /// Apply transformation invariance to preconditioning.
345    ///
346    /// Bounds the condition number of the elementwise second-moment preconditioner by
347    /// clamping it into `[min_singular_value, min_singular_value * max_condition_number]`
348    /// before inverting, which keeps the update invariant to the scale ambiguity
349    /// `(A, B) -> (sA, B/s)` inherent to a LoRA factorisation.
350    fn apply_transformation_invariance(&self, preconditioner: &Tensor) -> Result<Tensor> {
351        let min_val = self.config.min_singular_value;
352        let max_val = self.config.min_singular_value * self.config.max_condition_number;
353        let clamped = preconditioner.clamp(min_val, max_val)?;
354
355        // Reconstruct preconditioner with controlled condition number
356        let sqrt_values = clamped.sqrt()?;
357        Ok(sqrt_values.reciprocal()?)
358    }
359
360    /// Update moment estimates for Adam-like behavior
361    fn update_moments(&mut self, param_name: &str, gradient: &Tensor) -> Result<(Tensor, Tensor)> {
362        let beta1 = self.config.beta1;
363        let beta2 = self.config.beta2;
364
365        // Determine which state maps to use based on parameter type
366        let (m_map, v_map) = if self.is_lora_a_matrix(param_name) {
367            (&mut self.state.m_a, &mut self.state.v_a)
368        } else {
369            (&mut self.state.m_b, &mut self.state.v_b)
370        };
371
372        // Update first moment
373        let m = if let Some(prev_m) = m_map.get(param_name) {
374            let beta1_tensor = Tensor::scalar(beta1)?;
375            let one_minus_beta1 = Tensor::scalar(1.0 - beta1)?;
376
377            let weighted_prev = prev_m.mul(&beta1_tensor)?;
378            let weighted_grad = gradient.mul(&one_minus_beta1)?;
379            weighted_prev.add(&weighted_grad)?
380        } else {
381            gradient.mul(&Tensor::scalar(1.0 - beta1)?)?
382        };
383
384        // Update second moment
385        let grad_squared = gradient.pow(2.0)?;
386        let v = if let Some(prev_v) = v_map.get(param_name) {
387            let beta2_tensor = Tensor::scalar(beta2)?;
388            let one_minus_beta2 = Tensor::scalar(1.0 - beta2)?;
389
390            let weighted_prev = prev_v.mul(&beta2_tensor)?;
391            let weighted_grad_sq = grad_squared.mul(&one_minus_beta2)?;
392            weighted_prev.add(&weighted_grad_sq)?
393        } else {
394            grad_squared.mul(&Tensor::scalar(1.0 - beta2)?)?
395        };
396
397        // Store updated moments
398        m_map.insert(param_name.to_string(), m.clone());
399        v_map.insert(param_name.to_string(), v.clone());
400
401        Ok((m, v))
402    }
403
404    /// Apply bias correction to moments
405    fn apply_bias_correction(&self, moment: &Tensor, beta: f32) -> Result<Tensor> {
406        if !self.config.bias_correction {
407            return Ok(moment.clone());
408        }
409
410        let step = self.state.step as f32;
411        let correction_factor = 1.0 - beta.powf(step);
412        Ok(moment.div_scalar(correction_factor)?)
413    }
414
415    /// Compute effective rank of LoRA decomposition
416    fn compute_effective_rank(&self, singular_values: &Tensor) -> Result<usize> {
417        let sv_data = singular_values.data()?;
418        let total_variance: f32 = sv_data.iter().sum();
419        let threshold = 0.95 * total_variance; // 95% of total variance
420
421        let mut cumulative_variance = 0.0;
422        let mut effective_rank = 0;
423
424        for &sv in sv_data.iter() {
425            cumulative_variance += sv;
426            effective_rank += 1;
427            if cumulative_variance >= threshold {
428                break;
429            }
430        }
431
432        Ok(effective_rank.min(self.config.lora_rank))
433    }
434
435    /// Update LoRA-specific statistics
436    fn update_lora_stats(
437        &mut self,
438        base_name: &str,
439        matrix_a: &Tensor,
440        matrix_b: &Tensor,
441    ) -> Result<()> {
442        // Compute SVD for the LoRA pair
443        let (_, singular_values, _) = self.compute_svd(matrix_a, matrix_b)?;
444
445        // Compute condition number
446        let sv_data = singular_values.data()?;
447        let max_sv = sv_data.iter().fold(0.0f32, |a, &b| a.max(b));
448        let min_sv = sv_data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
449        let condition_number = max_sv / (min_sv + self.config.epsilon);
450
451        // Compute effective rank
452        let effective_rank = self.compute_effective_rank(&singular_values)?;
453
454        // Store statistics
455        self.state.singular_values.insert(base_name.to_string(), singular_values);
456        self.state.condition_numbers.insert(base_name.to_string(), condition_number);
457        self.state.effective_ranks.insert(base_name.to_string(), effective_rank);
458
459        Ok(())
460    }
461
462    /// Perform optimization step
463    pub fn step(
464        &mut self,
465        parameters: &mut HashMap<String, Tensor>,
466        gradients: &HashMap<String, Tensor>,
467    ) -> Result<()> {
468        self.state.step += 1;
469
470        // Process LoRA A and B matrices together for better preconditioning
471        let mut processed_pairs: std::collections::HashSet<String> =
472            std::collections::HashSet::new();
473
474        for (param_name, gradient) in gradients.iter() {
475            if let Some(parameter) = parameters.get_mut(param_name) {
476                let base_name = self.get_lora_base_name(param_name);
477
478                // Apply weight decay if configured
479                let mut effective_gradient = gradient.clone();
480                if self.config.weight_decay > 0.0 {
481                    let weight_decay_term =
482                        parameter.mul(&Tensor::scalar(self.config.weight_decay)?)?;
483                    effective_gradient = effective_gradient.add(&weight_decay_term)?;
484                }
485
486                // Update moments
487                let (m, v) = self.update_moments(param_name, &effective_gradient)?;
488
489                // Apply bias correction
490                let corrected_m = self.apply_bias_correction(&m, self.config.beta1)?;
491                let corrected_v = self.apply_bias_correction(&v, self.config.beta2)?;
492
493                // Compute LoRA-specific preconditioning
494                let preconditioner =
495                    self.compute_lora_preconditioning(param_name, &effective_gradient)?;
496
497                // Combine Adam-like update with LoRA preconditioning
498                let v_sqrt = corrected_v.sqrt()?;
499                let v_sqrt_eps = v_sqrt.add(&Tensor::scalar(self.config.epsilon)?)?;
500                let adam_update = corrected_m.div(&v_sqrt_eps)?;
501
502                // Apply LoRA preconditioning
503                let strength = Tensor::scalar(self.config.preconditioning_strength)?;
504                let one_minus_strength =
505                    Tensor::scalar(1.0 - self.config.preconditioning_strength)?;
506
507                let preconditioned_update = adam_update
508                    .mul(&strength)?
509                    .mul(&preconditioner)?
510                    .add(&adam_update.mul(&one_minus_strength)?)?;
511
512                // Apply learning rate and update parameter
513                let lr_tensor = Tensor::scalar(self.config.learning_rate)?;
514                let param_update = preconditioned_update.mul(&lr_tensor)?;
515
516                *parameter = parameter.sub(&param_update)?;
517
518                // Update the per-pair LoRA statistics once per pair. Only the
519                // *statistics* are deduplicated: both the A and the B matrix of a pair
520                // must receive their own parameter update above, which an earlier
521                // `continue` here silently skipped for whichever matrix was visited
522                // second.
523                if !processed_pairs.contains(&base_name)
524                    && (self.is_lora_a_matrix(param_name) || self.is_lora_b_matrix(param_name))
525                {
526                    let a_name = format!("{}_a", base_name);
527                    let b_name = format!("{}_b", base_name);
528
529                    if let (Some(matrix_a), Some(matrix_b)) =
530                        (parameters.get(&a_name), parameters.get(&b_name))
531                    {
532                        self.update_lora_stats(&base_name, matrix_a, matrix_b)?;
533                        processed_pairs.insert(base_name);
534                    }
535                }
536            }
537        }
538
539        // Update transformation statistics
540        if self.state.step.is_multiple_of(self.config.adaptation_frequency) {
541            self.update_transformation_stats()?;
542        }
543
544        Ok(())
545    }
546
547    /// Update transformation invariance statistics
548    fn update_transformation_stats(&mut self) -> Result<()> {
549        let mut total_condition_improvement = 0.0;
550        let mut count = 0;
551
552        for &condition_number in self.state.condition_numbers.values() {
553            if condition_number < self.config.max_condition_number {
554                total_condition_improvement += 1.0 / condition_number;
555                count += 1;
556            }
557        }
558
559        if count > 0 {
560            self.state.transformation_stats.condition_improvement =
561                total_condition_improvement / count as f32;
562            self.state.transformation_stats.num_transformations += 1;
563        }
564
565        // Update rank stability
566        let rank_variance;
567        let ranks: Vec<f32> = self.state.effective_ranks.values().map(|&r| r as f32).collect();
568        if !ranks.is_empty() {
569            let mean_rank: f32 = ranks.iter().sum::<f32>() / ranks.len() as f32;
570            rank_variance =
571                ranks.iter().map(|&r| (r - mean_rank).powi(2)).sum::<f32>() / ranks.len() as f32;
572            self.state.transformation_stats.rank_stability = 1.0 / (1.0 + rank_variance.sqrt());
573        }
574
575        Ok(())
576    }
577
578    /// Get LoRA-specific optimization statistics
579    pub fn get_lora_stats(&self) -> LoRARITEStats {
580        let avg_condition_number = if self.state.condition_numbers.is_empty() {
581            1.0
582        } else {
583            self.state.condition_numbers.values().sum::<f32>()
584                / self.state.condition_numbers.len() as f32
585        };
586
587        let avg_effective_rank = if self.state.effective_ranks.is_empty() {
588            self.config.lora_rank
589        } else {
590            self.state.effective_ranks.values().sum::<usize>() / self.state.effective_ranks.len()
591        };
592
593        LoRARITEStats {
594            step: self.state.step,
595            avg_condition_number,
596            avg_effective_rank,
597            num_lora_pairs: self.state.singular_values.len(),
598            transformation_invariance_score: self.state.transformation_stats.condition_improvement,
599            rank_stability: self.state.transformation_stats.rank_stability,
600            preconditioning_effectiveness: self.state.transformation_stats.preconditioning_gain,
601        }
602    }
603
604    /// Reset optimizer state (useful for transfer learning)
605    pub fn reset_state(&mut self) {
606        self.state = LoRARITEState::default();
607    }
608
609    /// Get condition numbers for all LoRA pairs
610    pub fn get_condition_numbers(&self) -> &HashMap<String, f32> {
611        &self.state.condition_numbers
612    }
613
614    /// Get effective ranks for all LoRA pairs
615    pub fn get_effective_ranks(&self) -> &HashMap<String, usize> {
616        &self.state.effective_ranks
617    }
618}
619
620/// LoRA-RITE optimizer statistics for monitoring and analysis
621#[derive(Debug, Clone)]
622pub struct LoRARITEStats {
623    /// Current optimization step
624    pub step: u64,
625    /// Average condition number across LoRA pairs
626    pub avg_condition_number: f32,
627    /// Average effective rank across LoRA pairs
628    pub avg_effective_rank: usize,
629    /// Number of LoRA parameter pairs
630    pub num_lora_pairs: usize,
631    /// Transformation invariance effectiveness score
632    pub transformation_invariance_score: f32,
633    /// Rank stability measure
634    pub rank_stability: f32,
635    /// Preconditioning effectiveness
636    pub preconditioning_effectiveness: f32,
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use trustformers_core::tensor::Tensor;
643
644    #[test]
645    fn test_lora_rite_creation() {
646        let config = LoRARITEConfig::new().learning_rate(1e-3).lora_rank(16).beta1(0.9).build();
647
648        let optimizer = LoRARITE::new(config);
649        assert_eq!(optimizer.learning_rate(), 1e-3);
650    }
651
652    #[test]
653    fn test_lora_rite_config_builder() {
654        let config = LoRARITEConfig::new()
655            .learning_rate(2e-3)
656            .lora_rank(32)
657            .beta1(0.95)
658            .beta2(0.999)
659            .preconditioning_strength(0.2)
660            .weight_decay(1e-4)
661            .build();
662
663        assert_eq!(config.learning_rate, 2e-3);
664        assert_eq!(config.lora_rank, 32);
665        assert_eq!(config.beta1, 0.95);
666        assert_eq!(config.beta2, 0.999);
667        assert_eq!(config.preconditioning_strength, 0.2);
668        assert_eq!(config.weight_decay, 1e-4);
669    }
670
671    #[test]
672    fn test_lora_matrix_detection() {
673        let config = LoRARITEConfig::new().build();
674        let optimizer = LoRARITE::new(config);
675
676        assert!(optimizer.is_lora_a_matrix("layer1_a"));
677        assert!(optimizer.is_lora_b_matrix("layer1_b"));
678        assert!(optimizer.is_lora_a_matrix("attention.lora_a"));
679        assert!(optimizer.is_lora_b_matrix("attention.lora_b"));
680        assert!(!optimizer.is_lora_a_matrix("layer1.weight"));
681    }
682
683    #[test]
684    fn test_lora_base_name_extraction() {
685        let config = LoRARITEConfig::new().build();
686        let optimizer = LoRARITE::new(config);
687
688        assert_eq!(optimizer.get_lora_base_name("layer1_a"), "layer1");
689        assert_eq!(optimizer.get_lora_base_name("layer1_b"), "layer1");
690        assert_eq!(
691            optimizer.get_lora_base_name("attention.lora_a"),
692            "attention.lora"
693        );
694        assert_eq!(
695            optimizer.get_lora_base_name("attention.lora_b"),
696            "attention.lora"
697        );
698    }
699
700    #[test]
701    fn test_lora_rite_step() -> Result<()> {
702        let config = LoRARITEConfig::new().learning_rate(1e-2).lora_rank(4).build();
703        let mut optimizer = LoRARITE::new(config);
704
705        // Create LoRA A and B matrices
706        let mut parameters = HashMap::new();
707        parameters.insert("layer1_a".to_string(), Tensor::ones(&[4, 8])?); // rank=4, input_dim=8
708        parameters.insert("layer1_b".to_string(), Tensor::ones(&[2, 4])?); // output_dim=2, rank=4
709
710        let mut gradients = HashMap::new();
711        gradients.insert(
712            "layer1_a".to_string(),
713            Tensor::ones(&[4, 8])?.mul_scalar(0.1)?,
714        );
715        gradients.insert(
716            "layer1_b".to_string(),
717            Tensor::ones(&[2, 4])?.mul_scalar(0.1)?,
718        );
719
720        // Store original values
721        let orig_a = parameters.get("layer1_a").expect("Key not found").clone();
722        let orig_b = parameters.get("layer1_b").expect("Key not found").clone();
723
724        // Perform optimization step
725        optimizer.step(&mut parameters, &gradients)?;
726
727        // Check that parameters were updated
728        let updated_a = parameters.get("layer1_a").expect("Key not found");
729        let updated_b = parameters.get("layer1_b").expect("Key not found");
730
731        assert_ne!(updated_a.mean()?.to_scalar()?, orig_a.mean()?.to_scalar()?);
732        assert_ne!(updated_b.mean()?.to_scalar()?, orig_b.mean()?.to_scalar()?);
733
734        Ok(())
735    }
736
737    #[test]
738    fn test_moment_updates() -> Result<()> {
739        let config = LoRARITEConfig::new().build();
740        let mut optimizer = LoRARITE::new(config);
741
742        let gradient = Tensor::ones(&[2, 2])?.mul_scalar(0.5)?;
743
744        // First update
745        let (m1, v1) = optimizer.update_moments("test_a", &gradient)?;
746
747        // Second update
748        let (m2, v2) = optimizer.update_moments("test_a", &gradient)?;
749
750        // Moments should change between updates
751        assert_ne!(m1.mean()?.to_scalar()?, m2.mean()?.to_scalar()?);
752        assert_ne!(v1.mean()?.to_scalar()?, v2.mean()?.to_scalar()?);
753
754        Ok(())
755    }
756
757    #[test]
758    fn test_bias_correction() -> Result<()> {
759        let config = LoRARITEConfig::new().bias_correction(true).build();
760        let optimizer = LoRARITE::new(config);
761
762        let moment = Tensor::ones(&[2, 2])?.mul_scalar(0.5)?;
763        let beta = 0.9;
764
765        let corrected = optimizer.apply_bias_correction(&moment, beta)?;
766
767        // Corrected moment should be larger due to bias correction
768        assert!(corrected.mean()?.to_scalar()? > moment.mean()?.to_scalar()?);
769
770        Ok(())
771    }
772
773    #[test]
774    fn test_lora_stats() -> Result<()> {
775        let config = LoRARITEConfig::new().lora_rank(4).build();
776        let mut optimizer = LoRARITE::new(config);
777
778        // Add some dummy statistics
779        optimizer.state.condition_numbers.insert("layer1".to_string(), 2.5);
780        optimizer.state.condition_numbers.insert("layer2".to_string(), 3.0);
781        optimizer.state.effective_ranks.insert("layer1".to_string(), 3);
782        optimizer.state.effective_ranks.insert("layer2".to_string(), 4);
783
784        let stats = optimizer.get_lora_stats();
785        assert_eq!(stats.num_lora_pairs, 0); // singular_values is empty
786        assert_eq!(stats.avg_condition_number, 2.75); // (2.5 + 3.0) / 2
787        assert_eq!(stats.avg_effective_rank, 3); // (3 + 4) / 2
788
789        Ok(())
790    }
791
792    #[test]
793    fn test_learning_rate_methods() {
794        let config = LoRARITEConfig::new().learning_rate(1e-3).build();
795        let mut optimizer = LoRARITE::new(config);
796
797        assert_eq!(optimizer.learning_rate(), 1e-3);
798
799        optimizer.set_learning_rate(2e-3);
800        assert_eq!(optimizer.learning_rate(), 2e-3);
801    }
802
803    #[test]
804    fn test_weight_decay() -> Result<()> {
805        let config = LoRARITEConfig::new().learning_rate(1e-2).weight_decay(1e-2).build();
806        let mut optimizer = LoRARITE::new(config);
807
808        let mut parameters = HashMap::new();
809        parameters.insert("layer1_a".to_string(), Tensor::ones(&[2, 2])?);
810
811        let mut gradients = HashMap::new();
812        gradients.insert("layer1_a".to_string(), Tensor::zeros(&[2, 2])?);
813
814        let initial_param_value =
815            parameters.get("layer1_a").expect("Key not found").mean()?.to_scalar()?;
816
817        optimizer.step(&mut parameters, &gradients)?;
818
819        let final_param_value =
820            parameters.get("layer1_a").expect("Key not found").mean()?.to_scalar()?;
821
822        // With weight decay, parameter should decrease even with zero gradient
823        assert!(final_param_value < initial_param_value);
824
825        Ok(())
826    }
827
828    #[test]
829    fn test_transformation_invariance() -> Result<()> {
830        let config = LoRARITEConfig::new().transformation_invariance(true).build();
831        let optimizer = LoRARITE::new(config);
832
833        let preconditioner = Tensor::ones(&[2, 2])?.mul_scalar(2.0)?;
834        let transformed = optimizer.apply_transformation_invariance(&preconditioner)?;
835
836        // Result should be positive and finite
837        let result_value = transformed.mean()?.to_scalar()?;
838        assert!(result_value > 0.0);
839        assert!(result_value.is_finite());
840
841        Ok(())
842    }
843}