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    /// Stable parameter identity registry (see [`crate::param_id`]).
215    ///
216    /// Replaces heap-address keys, which change in every process and so made
217    /// checkpoint resume silently restore nothing.
218    params: crate::param_id::ParamRegistry,
219}
220
221impl ParallelAdam {
222    /// Creates a new parallel Adam optimizer.
223    pub fn new(lr: f32, betas: (f32, f32), eps: f32, weight_decay: f32) -> Self {
224        Self::with_config(lr, betas, eps, weight_decay, ParallelConfig::default())
225    }
226
227    /// Creates a parallel Adam optimizer with custom configuration.
228    pub fn with_config(
229        lr: f32,
230        betas: (f32, f32),
231        eps: f32,
232        weight_decay: f32,
233        config: ParallelConfig,
234    ) -> Self {
235        Self {
236            lr,
237            betas,
238            eps,
239            weight_decay,
240            state: ParallelOptimizerState::new(config),
241            params: crate::param_id::ParamRegistry::new(),
242        }
243    }
244
245    /// Updates multiple parameters in parallel.
246    pub fn update_parallel(&self, updates: Vec<(String, &mut [f32], &[f32])>) -> Result<()> {
247        let _chunk_size = self.state.config.chunk_size;
248        let min_params = self.state.config.min_params_per_thread;
249
250        if updates.len() < min_params || !self.should_parallelize(&updates) {
251            // Use sequential processing for small workloads
252            return self.update_sequential(updates);
253        }
254
255        // Parallel processing using rayon
256        let results: Result<Vec<()>> = updates
257            .into_par_iter()
258            .with_min_len(1)
259            .map(|(param_id, param, grad)| self.update_single_parameter(param_id, param, grad))
260            .collect();
261
262        results.map(|_| ())
263    }
264
265    /// Updates parameters sequentially.
266    fn update_sequential(&self, updates: Vec<(String, &mut [f32], &[f32])>) -> Result<()> {
267        for (param_id, param, grad) in updates {
268            self.update_single_parameter(param_id, param, grad)?;
269        }
270        Ok(())
271    }
272
273    /// Updates a single parameter with parallel chunk processing.
274    fn update_single_parameter(
275        &self,
276        param_id: String,
277        param: &mut [f32],
278        grad: &[f32],
279    ) -> Result<()> {
280        if param.len() != grad.len() {
281            return Err(TrustformersError::tensor_op_error(
282                "Parameter and gradient size mismatch",
283                "update_single_parameter",
284            ));
285        }
286
287        let size = param.len();
288        let state_arc = self.state.get_or_create_state(param_id, size);
289        let chunk_size = self.state.config.chunk_size;
290
291        // Lock the parameter state
292        let mut param_state = state_arc.lock().map_err(|_| {
293            TrustformersError::lock_error("parallel optimizer state mutex poisoned".to_string())
294        })?;
295        param_state.step += 1;
296        param_state.last_update = std::time::Instant::now();
297
298        let step = param_state.step;
299        let (bias_correction1, bias_correction2) =
300            BiasCorrection::compute_adam_corrections(self.betas.0, self.betas.1, step);
301
302        // Determine if we should parallelize this parameter
303        let should_parallelize = size >= chunk_size * 2 && self.state.config.num_threads > 1;
304        if should_parallelize {
305            // Parallel chunk processing
306            let ParameterState {
307                ref mut momentum,
308                ref mut variance,
309                ..
310            } = *param_state;
311            self.update_parameter_parallel(
312                param,
313                grad,
314                momentum,
315                variance,
316                bias_correction1,
317                bias_correction2,
318                chunk_size,
319            );
320        } else {
321            // Sequential processing for small parameters
322            let ParameterState {
323                ref mut momentum,
324                ref mut variance,
325                ..
326            } = *param_state;
327            self.update_parameter_sequential(
328                param,
329                grad,
330                momentum,
331                variance,
332                bias_correction1,
333                bias_correction2,
334            );
335        }
336
337        Ok(())
338    }
339
340    /// Updates parameter using parallel chunk processing.
341    fn update_parameter_parallel(
342        &self,
343        param: &mut [f32],
344        grad: &[f32],
345        momentum: &mut [f32],
346        variance: &mut [f32],
347        bias_correction1: f32,
348        bias_correction2: f32,
349        chunk_size: usize,
350    ) {
351        // Use parallel iterators for chunk-based processing
352        param
353            .par_chunks_mut(chunk_size)
354            .zip(grad.par_chunks(chunk_size))
355            .zip(momentum.par_chunks_mut(chunk_size))
356            .zip(variance.par_chunks_mut(chunk_size))
357            .for_each(|(((p_chunk, g_chunk), m_chunk), v_chunk)| {
358                self.process_chunk(
359                    p_chunk,
360                    g_chunk,
361                    m_chunk,
362                    v_chunk,
363                    bias_correction1,
364                    bias_correction2,
365                );
366            });
367    }
368
369    /// Updates parameter sequentially.
370    fn update_parameter_sequential(
371        &self,
372        param: &mut [f32],
373        grad: &[f32],
374        momentum: &mut [f32],
375        variance: &mut [f32],
376        bias_correction1: f32,
377        bias_correction2: f32,
378    ) {
379        self.process_chunk(
380            param,
381            grad,
382            momentum,
383            variance,
384            bias_correction1,
385            bias_correction2,
386        );
387    }
388
389    /// Processes a chunk of parameters.
390    #[inline]
391    fn process_chunk(
392        &self,
393        param_chunk: &mut [f32],
394        grad_chunk: &[f32],
395        momentum_chunk: &mut [f32],
396        variance_chunk: &mut [f32],
397        bias_correction1: f32,
398        bias_correction2: f32,
399    ) {
400        // Use the minimum length to avoid index out of bounds
401        let len = param_chunk
402            .len()
403            .min(grad_chunk.len())
404            .min(momentum_chunk.len())
405            .min(variance_chunk.len());
406
407        for i in 0..len {
408            let grad_val = grad_chunk[i] + self.weight_decay * param_chunk[i];
409
410            // Update momentum and variance
411            ParameterUpdate::update_ema(&mut momentum_chunk[i], grad_val, self.betas.0);
412            ParameterUpdate::update_ema(&mut variance_chunk[i], grad_val * grad_val, self.betas.1);
413
414            // Apply bias-corrected update
415            let m_hat = momentum_chunk[i] / bias_correction1;
416            let v_hat = variance_chunk[i] / bias_correction2;
417
418            ParameterUpdate::adam_update(&mut param_chunk[i], self.lr, m_hat, v_hat, self.eps);
419        }
420    }
421
422    /// Determines if parallelization should be used based on workload.
423    fn should_parallelize(&self, updates: &[(String, &mut [f32], &[f32])]) -> bool {
424        let total_elements: usize = updates.iter().map(|(_, param, _)| param.len()).sum();
425        let num_threads = self.state.config.effective_num_threads();
426
427        total_elements >= self.state.config.min_params_per_thread * num_threads
428    }
429
430    /// Gets parallel performance statistics.
431    pub fn parallel_stats(&self) -> ParallelStats {
432        let memory_stats = self.state.memory_usage();
433        let num_threads = self.state.config.effective_num_threads();
434
435        ParallelStats {
436            num_threads,
437            memory_stats,
438            config: self.state.config.clone(),
439            current_step: self.state.get_step(),
440        }
441    }
442
443    /// Configures thread pool for optimal performance.
444    pub fn configure_thread_pool(&self) -> Result<()> {
445        let num_threads = self.state.config.effective_num_threads();
446
447        ThreadPoolBuilder::new().num_threads(num_threads).build_global().map_err(|e| {
448            TrustformersError::tensor_op_error(
449                &format!("Failed to configure thread pool: {}", e),
450                "configure_thread_pool",
451            )
452        })?;
453
454        Ok(())
455    }
456}
457
458impl Optimizer for ParallelAdam {
459    fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
460        match (parameter, grad) {
461            (Tensor::F32(param), Tensor::F32(grad_arr)) => {
462                let param_id = self.params.key_for_addr(param.as_ptr() as usize, param.len())?;
463                self.update_single_parameter(
464                    param_id,
465                    param.as_slice_mut().ok_or_else(|| {
466                        TrustformersError::tensor_op_error(
467                            "Parameter array must have contiguous layout",
468                            "update",
469                        )
470                    })?,
471                    grad_arr.as_slice().ok_or_else(|| {
472                        TrustformersError::tensor_op_error(
473                            "Gradient array must have contiguous layout",
474                            "update",
475                        )
476                    })?,
477                )
478            },
479            _ => Err(TrustformersError::tensor_op_error(
480                "Unsupported tensor types for ParallelAdam",
481                "update",
482            )),
483        }
484    }
485
486    fn zero_grad(&mut self) {
487        // No explicit gradient storage
488    }
489
490    fn step(&mut self) {
491        self.state.step();
492    }
493
494    fn get_lr(&self) -> f32 {
495        self.lr
496    }
497
498    fn set_lr(&mut self, lr: f32) {
499        self.lr = lr;
500    }
501}
502
503/// Performance statistics for parallel optimization.
504#[derive(Debug, Clone)]
505pub struct ParallelStats {
506    /// Number of worker threads
507    pub num_threads: usize,
508    /// Memory usage statistics
509    pub memory_stats: StateMemoryStats,
510    /// Parallel configuration
511    pub config: ParallelConfig,
512    /// Current optimization step
513    pub current_step: usize,
514}
515
516impl ParallelStats {
517    /// Calculates theoretical speedup based on workload.
518    pub fn theoretical_speedup(&self, _sequential_time_ms: f64) -> f64 {
519        // Simple Amdahl's law approximation
520        let parallel_fraction = 0.95; // Assume 95% of work can be parallelized
521        let num_threads = self.num_threads as f64;
522
523        1.0 / ((1.0 - parallel_fraction) + (parallel_fraction / num_threads))
524    }
525
526    /// Suggests optimization improvements.
527    pub fn optimization_suggestions(&self) -> Vec<String> {
528        let mut suggestions = Vec::new();
529
530        if self.num_threads == 1 {
531            suggestions.push(
532                "Consider increasing number of threads for better parallelization".to_string(),
533            );
534        }
535
536        if self.num_threads > num_cpus::get() {
537            suggestions.push("Number of threads exceeds CPU cores; consider reducing".to_string());
538        }
539
540        if self.config.chunk_size < 256 {
541            suggestions
542                .push("Small chunk size may cause overhead; consider increasing".to_string());
543        }
544
545        if self.config.chunk_size > 8192 {
546            suggestions.push("Large chunk size may reduce parallelization efficiency".to_string());
547        }
548
549        if !self.config.enable_work_stealing {
550            suggestions.push("Enable work stealing for better load balancing".to_string());
551        }
552
553        if suggestions.is_empty() {
554            suggestions.push("Parallel configuration appears optimal".to_string());
555        }
556
557        suggestions
558    }
559}
560
561/// Batch parameter update interface for better parallelization.
562pub trait BatchUpdate {
563    /// Updates multiple parameters in a single batch operation.
564    fn update_batch(&mut self, batch: Vec<(&mut Tensor, &Tensor)>) -> Result<()>;
565}
566
567impl BatchUpdate for ParallelAdam {
568    fn update_batch(&mut self, batch: Vec<(&mut Tensor, &Tensor)>) -> Result<()> {
569        let mut updates = Vec::new();
570
571        for (param, grad) in batch {
572            match (param, grad) {
573                (Tensor::F32(p), Tensor::F32(g)) => {
574                    let param_id = self.params.key_for_addr(p.as_ptr() as usize, p.len())?;
575                    updates.push((
576                        param_id,
577                        p.as_slice_mut().ok_or_else(|| {
578                            TrustformersError::tensor_op_error(
579                                "Parameter array must have contiguous layout",
580                                "update_batch",
581                            )
582                        })?,
583                        g.as_slice().ok_or_else(|| {
584                            TrustformersError::tensor_op_error(
585                                "Gradient array must have contiguous layout",
586                                "update_batch",
587                            )
588                        })?,
589                    ));
590                },
591                _ => {
592                    return Err(TrustformersError::tensor_op_error(
593                        "Unsupported tensor types",
594                        "update_batch",
595                    ))
596                },
597            }
598        }
599
600        self.update_parallel(updates)
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn test_parallel_config() {
610        let config = ParallelConfig::default();
611        assert_eq!(config.num_threads, 0); // Auto-detect
612        assert!(config.enable_work_stealing);
613
614        let cpu_config = ParallelConfig::cpu_optimized();
615        assert_eq!(cpu_config.num_threads, num_cpus::get());
616
617        let effective_threads = config.effective_num_threads();
618        assert!(effective_threads > 0);
619        assert_eq!(effective_threads, num_cpus::get());
620    }
621
622    #[test]
623    fn test_parallel_optimizer_state() {
624        let config = ParallelConfig::default();
625        let state = ParallelOptimizerState::new(config);
626
627        assert_eq!(state.get_step(), 0);
628        state.step();
629        assert_eq!(state.get_step(), 1);
630
631        let param_state = state.get_or_create_state("test_param".to_string(), 100);
632        let locked_state = param_state.lock().expect("Parallel optimizer state lock poisoned");
633        assert_eq!(locked_state.momentum.len(), 100);
634        assert_eq!(locked_state.variance.len(), 100);
635    }
636
637    #[test]
638    fn test_parallel_adam() {
639        let optimizer = ParallelAdam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
640        assert_eq!(optimizer.get_lr(), 1e-3);
641        assert_eq!(optimizer.betas, (0.9, 0.999));
642
643        let stats = optimizer.parallel_stats();
644        assert!(stats.num_threads > 0);
645        assert_eq!(stats.current_step, 0);
646    }
647
648    #[test]
649    fn test_should_parallelize() {
650        let config = ParallelConfig {
651            min_params_per_thread: 1000,
652            num_threads: 4,
653            ..Default::default()
654        };
655        let optimizer = ParallelAdam::with_config(1e-3, (0.9, 0.999), 1e-8, 0.01, config);
656
657        // Small workload - should not parallelize
658        let mut small_params = [0.0; 100];
659        let small_grads = [0.0; 100];
660        let small_updates = vec![(
661            "param1".to_string(),
662            &mut small_params[..],
663            &small_grads[..],
664        )];
665        assert!(!optimizer.should_parallelize(&small_updates));
666
667        // Large workload - should parallelize
668        let mut large_params = [0.0; 5000];
669        let large_grads = [0.0; 5000];
670        let large_updates = vec![(
671            "param1".to_string(),
672            &mut large_params[..],
673            &large_grads[..],
674        )];
675        assert!(optimizer.should_parallelize(&large_updates));
676    }
677
678    #[test]
679    fn test_parallel_stats() {
680        let optimizer = ParallelAdam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
681        let stats = optimizer.parallel_stats();
682
683        let speedup = stats.theoretical_speedup(1000.0);
684        assert!(speedup > 1.0);
685        assert!(speedup <= stats.num_threads as f64);
686
687        let suggestions = stats.optimization_suggestions();
688        assert!(!suggestions.is_empty());
689    }
690
691    #[test]
692    fn test_memory_usage() {
693        let config = ParallelConfig::default();
694        let state = ParallelOptimizerState::new(config);
695
696        // Create some parameter states
697        state.get_or_create_state("param1".to_string(), 1000);
698        state.get_or_create_state("param2".to_string(), 2000);
699
700        let memory_stats = state.memory_usage();
701        assert_eq!(memory_stats.num_parameters, 2);
702        assert_eq!(memory_stats.momentum_elements, 3000);
703        assert_eq!(memory_stats.variance_elements, 3000);
704    }
705}