Skip to main content

trustformers_optim/
parallel.rs

1//! Parallel optimization algorithms for multi-threaded training.
2//!
3//! This module provides thread-safe optimizers that can leverage multiple CPU cores
4//! for parallel parameter updates, improving performance on multi-core systems.
5//!
6//! # Key Features
7//!
8//! - **Thread-Safe State Management**: Lock-free and fine-grained locking strategies
9//! - **Parallel Parameter Updates**: Distribute parameter updates across threads
10//! - **Work Stealing**: Dynamic load balancing for uneven parameter distributions
11//! - **NUMA Awareness**: Optimize for Non-Uniform Memory Access architectures
12//! - **Scalability**: Efficient scaling from 2 to 64+ cores
13
14use crate::common::{BiasCorrection, ParameterUpdate, StateMemoryStats};
15use scirs2_core::parallel_ops::*; // SciRS2 Integration Policy - replaces rayon
16use std::collections::HashMap;
17use std::sync::{Arc, Mutex, RwLock};
18use trustformers_core::errors::{Result, TrustformersError};
19use trustformers_core::tensor::Tensor;
20use trustformers_core::traits::Optimizer;
21
22/// Configuration for parallel optimization.
23#[derive(Debug, Clone)]
24pub struct ParallelConfig {
25    /// Number of worker threads (0 = auto-detect)
26    pub num_threads: usize,
27    /// Minimum parameters per thread to justify parallelization
28    pub min_params_per_thread: usize,
29    /// Enable work stealing for load balancing
30    pub enable_work_stealing: bool,
31    /// Enable NUMA-aware thread pinning
32    pub numa_aware: bool,
33    /// Chunk size for parameter processing
34    pub chunk_size: usize,
35    /// Enable lock-free optimizations where possible
36    pub lock_free: bool,
37}
38
39impl Default for ParallelConfig {
40    fn default() -> Self {
41        Self {
42            num_threads: 0, // Auto-detect
43            min_params_per_thread: 1000,
44            enable_work_stealing: true,
45            numa_aware: false,
46            chunk_size: 1024,
47            lock_free: true,
48        }
49    }
50}
51
52impl ParallelConfig {
53    /// Creates configuration optimized for CPU-bound workloads.
54    pub fn cpu_optimized() -> Self {
55        Self {
56            num_threads: num_cpus::get(),
57            chunk_size: 512,
58            enable_work_stealing: true,
59            ..Default::default()
60        }
61    }
62
63    /// Creates configuration for large model training.
64    pub fn large_model() -> Self {
65        Self {
66            num_threads: num_cpus::get(),
67            min_params_per_thread: 10000,
68            chunk_size: 4096,
69            numa_aware: true,
70            ..Default::default()
71        }
72    }
73
74    /// Creates configuration for memory-bound workloads.
75    pub fn memory_bound() -> Self {
76        Self {
77            num_threads: (num_cpus::get() / 2).max(1),
78            chunk_size: 2048,
79            numa_aware: true,
80            ..Default::default()
81        }
82    }
83
84    /// Gets the effective number of threads.
85    pub fn effective_num_threads(&self) -> usize {
86        if self.num_threads == 0 {
87            num_cpus::get()
88        } else {
89            self.num_threads
90        }
91    }
92}
93
94/// Thread-safe optimizer state with fine-grained locking.
95#[derive(Debug)]
96pub struct ParallelOptimizerState {
97    /// Per-parameter state with individual locks
98    parameter_states: RwLock<HashMap<String, Arc<Mutex<ParameterState>>>>,
99    /// Global step counter
100    global_step: Arc<std::sync::atomic::AtomicUsize>,
101    /// Parallel configuration
102    config: ParallelConfig,
103}
104
105/// Individual parameter state with momentum and variance.
106#[derive(Debug)]
107pub struct ParameterState {
108    pub momentum: Vec<f32>,
109    pub variance: Vec<f32>,
110    pub step: usize,
111    pub last_update: std::time::Instant,
112}
113
114impl ParameterState {
115    fn new(size: usize) -> Self {
116        Self {
117            momentum: vec![0.0; size],
118            variance: vec![0.0; size],
119            step: 0,
120            last_update: std::time::Instant::now(),
121        }
122    }
123}
124
125impl ParallelOptimizerState {
126    /// Creates a new parallel optimizer state.
127    pub fn new(config: ParallelConfig) -> Self {
128        Self {
129            parameter_states: RwLock::new(HashMap::new()),
130            global_step: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
131            config,
132        }
133    }
134
135    /// Gets or creates parameter state.
136    pub fn get_or_create_state(&self, param_id: String, size: usize) -> Arc<Mutex<ParameterState>> {
137        // Try read-only access first
138        {
139            let states =
140                self.parameter_states.read().unwrap_or_else(|poisoned| poisoned.into_inner());
141            if let Some(state) = states.get(&param_id) {
142                return state.clone();
143            }
144        }
145
146        // Need to create new state - upgrade to write lock
147        let mut states =
148            self.parameter_states.write().unwrap_or_else(|poisoned| poisoned.into_inner());
149        // Double-check pattern in case another thread created it
150        if let Some(state) = states.get(&param_id) {
151            return state.clone();
152        }
153
154        let new_state = Arc::new(Mutex::new(ParameterState::new(size)));
155        states.insert(param_id, new_state.clone());
156        new_state
157    }
158
159    /// Increments global step counter atomically.
160    pub fn step(&self) {
161        self.global_step.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
162    }
163
164    /// Gets current global step.
165    pub fn get_step(&self) -> usize {
166        self.global_step.load(std::sync::atomic::Ordering::Relaxed)
167    }
168
169    /// Gets memory usage statistics.
170    pub fn memory_usage(&self) -> StateMemoryStats {
171        let states = self.parameter_states.read().unwrap_or_else(|poisoned| poisoned.into_inner());
172        let mut total_momentum = 0;
173        let mut total_variance = 0;
174        let num_params = states.len();
175
176        for state_arc in states.values() {
177            if let Ok(state) = state_arc.try_lock() {
178                total_momentum += state.momentum.len();
179                total_variance += state.variance.len();
180            }
181        }
182
183        StateMemoryStats {
184            momentum_elements: total_momentum,
185            variance_elements: total_variance,
186            third_moment_elements: 0,
187            total_bytes: (total_momentum + total_variance) * std::mem::size_of::<f32>(),
188            num_parameters: num_params,
189        }
190    }
191
192    /// Clears all parameter states.
193    pub fn clear(&self) {
194        let mut states =
195            self.parameter_states.write().unwrap_or_else(|poisoned| poisoned.into_inner());
196        states.clear();
197        self.global_step.store(0, std::sync::atomic::Ordering::Relaxed);
198    }
199}
200
201/// Parallel Adam optimizer with multi-threaded parameter updates.
202#[derive(Debug)]
203pub struct ParallelAdam {
204    /// Learning rate
205    lr: f32,
206    /// Beta coefficients
207    betas: (f32, f32),
208    /// Epsilon for numerical stability
209    eps: f32,
210    /// Weight decay coefficient
211    weight_decay: f32,
212    /// Parallel optimizer state
213    state: ParallelOptimizerState,
214}
215
216impl ParallelAdam {
217    /// Creates a new parallel Adam optimizer.
218    pub fn new(lr: f32, betas: (f32, f32), eps: f32, weight_decay: f32) -> Self {
219        Self::with_config(lr, betas, eps, weight_decay, ParallelConfig::default())
220    }
221
222    /// Creates a parallel Adam optimizer with custom configuration.
223    pub fn with_config(
224        lr: f32,
225        betas: (f32, f32),
226        eps: f32,
227        weight_decay: f32,
228        config: ParallelConfig,
229    ) -> Self {
230        Self {
231            lr,
232            betas,
233            eps,
234            weight_decay,
235            state: ParallelOptimizerState::new(config),
236        }
237    }
238
239    /// Updates multiple parameters in parallel.
240    pub fn update_parallel(&self, updates: Vec<(String, &mut [f32], &[f32])>) -> Result<()> {
241        let _chunk_size = self.state.config.chunk_size;
242        let min_params = self.state.config.min_params_per_thread;
243
244        if updates.len() < min_params || !self.should_parallelize(&updates) {
245            // Use sequential processing for small workloads
246            return self.update_sequential(updates);
247        }
248
249        // Parallel processing using rayon
250        let results: Result<Vec<()>> = updates
251            .into_par_iter()
252            .with_min_len(1)
253            .map(|(param_id, param, grad)| self.update_single_parameter(param_id, param, grad))
254            .collect();
255
256        results.map(|_| ())
257    }
258
259    /// Updates parameters sequentially.
260    fn update_sequential(&self, updates: Vec<(String, &mut [f32], &[f32])>) -> Result<()> {
261        for (param_id, param, grad) in updates {
262            self.update_single_parameter(param_id, param, grad)?;
263        }
264        Ok(())
265    }
266
267    /// Updates a single parameter with parallel chunk processing.
268    fn update_single_parameter(
269        &self,
270        param_id: String,
271        param: &mut [f32],
272        grad: &[f32],
273    ) -> Result<()> {
274        if param.len() != grad.len() {
275            return Err(TrustformersError::tensor_op_error(
276                "Parameter and gradient size mismatch",
277                "update_single_parameter",
278            ));
279        }
280
281        let size = param.len();
282        let state_arc = self.state.get_or_create_state(param_id, size);
283        let chunk_size = self.state.config.chunk_size;
284
285        // Lock the parameter state
286        let mut param_state = state_arc.lock().map_err(|_| {
287            TrustformersError::lock_error("parallel optimizer state mutex poisoned".to_string())
288        })?;
289        param_state.step += 1;
290        param_state.last_update = std::time::Instant::now();
291
292        let step = param_state.step;
293        let (bias_correction1, bias_correction2) =
294            BiasCorrection::compute_adam_corrections(self.betas.0, self.betas.1, step);
295
296        // Determine if we should parallelize this parameter
297        let should_parallelize = size >= chunk_size * 2 && self.state.config.num_threads > 1;
298        if should_parallelize {
299            // Parallel chunk processing
300            let ParameterState {
301                ref mut momentum,
302                ref mut variance,
303                ..
304            } = *param_state;
305            self.update_parameter_parallel(
306                param,
307                grad,
308                momentum,
309                variance,
310                bias_correction1,
311                bias_correction2,
312                chunk_size,
313            );
314        } else {
315            // Sequential processing for small parameters
316            let ParameterState {
317                ref mut momentum,
318                ref mut variance,
319                ..
320            } = *param_state;
321            self.update_parameter_sequential(
322                param,
323                grad,
324                momentum,
325                variance,
326                bias_correction1,
327                bias_correction2,
328            );
329        }
330
331        Ok(())
332    }
333
334    /// Updates parameter using parallel chunk processing.
335    fn update_parameter_parallel(
336        &self,
337        param: &mut [f32],
338        grad: &[f32],
339        momentum: &mut [f32],
340        variance: &mut [f32],
341        bias_correction1: f32,
342        bias_correction2: f32,
343        chunk_size: usize,
344    ) {
345        // Use parallel iterators for chunk-based processing
346        param
347            .par_chunks_mut(chunk_size)
348            .zip(grad.par_chunks(chunk_size))
349            .zip(momentum.par_chunks_mut(chunk_size))
350            .zip(variance.par_chunks_mut(chunk_size))
351            .for_each(|(((p_chunk, g_chunk), m_chunk), v_chunk)| {
352                self.process_chunk(
353                    p_chunk,
354                    g_chunk,
355                    m_chunk,
356                    v_chunk,
357                    bias_correction1,
358                    bias_correction2,
359                );
360            });
361    }
362
363    /// Updates parameter sequentially.
364    fn update_parameter_sequential(
365        &self,
366        param: &mut [f32],
367        grad: &[f32],
368        momentum: &mut [f32],
369        variance: &mut [f32],
370        bias_correction1: f32,
371        bias_correction2: f32,
372    ) {
373        self.process_chunk(
374            param,
375            grad,
376            momentum,
377            variance,
378            bias_correction1,
379            bias_correction2,
380        );
381    }
382
383    /// Processes a chunk of parameters.
384    #[inline]
385    fn process_chunk(
386        &self,
387        param_chunk: &mut [f32],
388        grad_chunk: &[f32],
389        momentum_chunk: &mut [f32],
390        variance_chunk: &mut [f32],
391        bias_correction1: f32,
392        bias_correction2: f32,
393    ) {
394        // Use the minimum length to avoid index out of bounds
395        let len = param_chunk
396            .len()
397            .min(grad_chunk.len())
398            .min(momentum_chunk.len())
399            .min(variance_chunk.len());
400
401        for i in 0..len {
402            let grad_val = grad_chunk[i] + self.weight_decay * param_chunk[i];
403
404            // Update momentum and variance
405            ParameterUpdate::update_ema(&mut momentum_chunk[i], grad_val, self.betas.0);
406            ParameterUpdate::update_ema(&mut variance_chunk[i], grad_val * grad_val, self.betas.1);
407
408            // Apply bias-corrected update
409            let m_hat = momentum_chunk[i] / bias_correction1;
410            let v_hat = variance_chunk[i] / bias_correction2;
411
412            ParameterUpdate::adam_update(&mut param_chunk[i], self.lr, m_hat, v_hat, self.eps);
413        }
414    }
415
416    /// Determines if parallelization should be used based on workload.
417    fn should_parallelize(&self, updates: &[(String, &mut [f32], &[f32])]) -> bool {
418        let total_elements: usize = updates.iter().map(|(_, param, _)| param.len()).sum();
419        let num_threads = self.state.config.effective_num_threads();
420
421        total_elements >= self.state.config.min_params_per_thread * num_threads
422    }
423
424    /// Gets parallel performance statistics.
425    pub fn parallel_stats(&self) -> ParallelStats {
426        let memory_stats = self.state.memory_usage();
427        let num_threads = self.state.config.effective_num_threads();
428
429        ParallelStats {
430            num_threads,
431            memory_stats,
432            config: self.state.config.clone(),
433            current_step: self.state.get_step(),
434        }
435    }
436
437    /// Configures thread pool for optimal performance.
438    pub fn configure_thread_pool(&self) -> Result<()> {
439        let num_threads = self.state.config.effective_num_threads();
440
441        ThreadPoolBuilder::new().num_threads(num_threads).build_global().map_err(|e| {
442            TrustformersError::tensor_op_error(
443                &format!("Failed to configure thread pool: {}", e),
444                "configure_thread_pool",
445            )
446        })?;
447
448        Ok(())
449    }
450}
451
452impl Optimizer for ParallelAdam {
453    fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
454        match (parameter, grad) {
455            (Tensor::F32(param), Tensor::F32(grad_arr)) => {
456                let param_id = format!("{:p}", param.as_ptr());
457                self.update_single_parameter(
458                    param_id,
459                    param.as_slice_mut().ok_or_else(|| {
460                        TrustformersError::tensor_op_error(
461                            "Parameter array must have contiguous layout",
462                            "update",
463                        )
464                    })?,
465                    grad_arr.as_slice().ok_or_else(|| {
466                        TrustformersError::tensor_op_error(
467                            "Gradient array must have contiguous layout",
468                            "update",
469                        )
470                    })?,
471                )
472            },
473            _ => Err(TrustformersError::tensor_op_error(
474                "Unsupported tensor types for ParallelAdam",
475                "update",
476            )),
477        }
478    }
479
480    fn zero_grad(&mut self) {
481        // No explicit gradient storage
482    }
483
484    fn step(&mut self) {
485        self.state.step();
486    }
487
488    fn get_lr(&self) -> f32 {
489        self.lr
490    }
491
492    fn set_lr(&mut self, lr: f32) {
493        self.lr = lr;
494    }
495}
496
497/// Performance statistics for parallel optimization.
498#[derive(Debug, Clone)]
499pub struct ParallelStats {
500    /// Number of worker threads
501    pub num_threads: usize,
502    /// Memory usage statistics
503    pub memory_stats: StateMemoryStats,
504    /// Parallel configuration
505    pub config: ParallelConfig,
506    /// Current optimization step
507    pub current_step: usize,
508}
509
510impl ParallelStats {
511    /// Calculates theoretical speedup based on workload.
512    pub fn theoretical_speedup(&self, _sequential_time_ms: f64) -> f64 {
513        // Simple Amdahl's law approximation
514        let parallel_fraction = 0.95; // Assume 95% of work can be parallelized
515        let num_threads = self.num_threads as f64;
516
517        1.0 / ((1.0 - parallel_fraction) + (parallel_fraction / num_threads))
518    }
519
520    /// Suggests optimization improvements.
521    pub fn optimization_suggestions(&self) -> Vec<String> {
522        let mut suggestions = Vec::new();
523
524        if self.num_threads == 1 {
525            suggestions.push(
526                "Consider increasing number of threads for better parallelization".to_string(),
527            );
528        }
529
530        if self.num_threads > num_cpus::get() {
531            suggestions.push("Number of threads exceeds CPU cores; consider reducing".to_string());
532        }
533
534        if self.config.chunk_size < 256 {
535            suggestions
536                .push("Small chunk size may cause overhead; consider increasing".to_string());
537        }
538
539        if self.config.chunk_size > 8192 {
540            suggestions.push("Large chunk size may reduce parallelization efficiency".to_string());
541        }
542
543        if !self.config.enable_work_stealing {
544            suggestions.push("Enable work stealing for better load balancing".to_string());
545        }
546
547        if suggestions.is_empty() {
548            suggestions.push("Parallel configuration appears optimal".to_string());
549        }
550
551        suggestions
552    }
553}
554
555/// Batch parameter update interface for better parallelization.
556pub trait BatchUpdate {
557    /// Updates multiple parameters in a single batch operation.
558    fn update_batch(&mut self, batch: Vec<(&mut Tensor, &Tensor)>) -> Result<()>;
559}
560
561impl BatchUpdate for ParallelAdam {
562    fn update_batch(&mut self, batch: Vec<(&mut Tensor, &Tensor)>) -> Result<()> {
563        let mut updates = Vec::new();
564
565        for (param, grad) in batch {
566            match (param, grad) {
567                (Tensor::F32(p), Tensor::F32(g)) => {
568                    let param_id = format!("{:p}", p.as_ptr());
569                    updates.push((
570                        param_id,
571                        p.as_slice_mut().ok_or_else(|| {
572                            TrustformersError::tensor_op_error(
573                                "Parameter array must have contiguous layout",
574                                "update_batch",
575                            )
576                        })?,
577                        g.as_slice().ok_or_else(|| {
578                            TrustformersError::tensor_op_error(
579                                "Gradient array must have contiguous layout",
580                                "update_batch",
581                            )
582                        })?,
583                    ));
584                },
585                _ => {
586                    return Err(TrustformersError::tensor_op_error(
587                        "Unsupported tensor types",
588                        "update_batch",
589                    ))
590                },
591            }
592        }
593
594        self.update_parallel(updates)
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn test_parallel_config() {
604        let config = ParallelConfig::default();
605        assert_eq!(config.num_threads, 0); // Auto-detect
606        assert!(config.enable_work_stealing);
607
608        let cpu_config = ParallelConfig::cpu_optimized();
609        assert_eq!(cpu_config.num_threads, num_cpus::get());
610
611        let effective_threads = config.effective_num_threads();
612        assert!(effective_threads > 0);
613        assert_eq!(effective_threads, num_cpus::get());
614    }
615
616    #[test]
617    fn test_parallel_optimizer_state() {
618        let config = ParallelConfig::default();
619        let state = ParallelOptimizerState::new(config);
620
621        assert_eq!(state.get_step(), 0);
622        state.step();
623        assert_eq!(state.get_step(), 1);
624
625        let param_state = state.get_or_create_state("test_param".to_string(), 100);
626        let locked_state = param_state.lock().expect("Parallel optimizer state lock poisoned");
627        assert_eq!(locked_state.momentum.len(), 100);
628        assert_eq!(locked_state.variance.len(), 100);
629    }
630
631    #[test]
632    fn test_parallel_adam() {
633        let optimizer = ParallelAdam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
634        assert_eq!(optimizer.get_lr(), 1e-3);
635        assert_eq!(optimizer.betas, (0.9, 0.999));
636
637        let stats = optimizer.parallel_stats();
638        assert!(stats.num_threads > 0);
639        assert_eq!(stats.current_step, 0);
640    }
641
642    #[test]
643    fn test_should_parallelize() {
644        let config = ParallelConfig {
645            min_params_per_thread: 1000,
646            num_threads: 4,
647            ..Default::default()
648        };
649        let optimizer = ParallelAdam::with_config(1e-3, (0.9, 0.999), 1e-8, 0.01, config);
650
651        // Small workload - should not parallelize
652        let mut small_params = [0.0; 100];
653        let small_grads = [0.0; 100];
654        let small_updates = vec![(
655            "param1".to_string(),
656            &mut small_params[..],
657            &small_grads[..],
658        )];
659        assert!(!optimizer.should_parallelize(&small_updates));
660
661        // Large workload - should parallelize
662        let mut large_params = [0.0; 5000];
663        let large_grads = [0.0; 5000];
664        let large_updates = vec![(
665            "param1".to_string(),
666            &mut large_params[..],
667            &large_grads[..],
668        )];
669        assert!(optimizer.should_parallelize(&large_updates));
670    }
671
672    #[test]
673    fn test_parallel_stats() {
674        let optimizer = ParallelAdam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
675        let stats = optimizer.parallel_stats();
676
677        let speedup = stats.theoretical_speedup(1000.0);
678        assert!(speedup > 1.0);
679        assert!(speedup <= stats.num_threads as f64);
680
681        let suggestions = stats.optimization_suggestions();
682        assert!(!suggestions.is_empty());
683    }
684
685    #[test]
686    fn test_memory_usage() {
687        let config = ParallelConfig::default();
688        let state = ParallelOptimizerState::new(config);
689
690        // Create some parameter states
691        state.get_or_create_state("param1".to_string(), 1000);
692        state.get_or_create_state("param2".to_string(), 2000);
693
694        let memory_stats = state.memory_usage();
695        assert_eq!(memory_stats.num_parameters, 2);
696        assert_eq!(memory_stats.momentum_elements, 3000);
697        assert_eq!(memory_stats.variance_elements, 3000);
698    }
699}