1use crate::common::{BiasCorrection, ParameterUpdate, StateMemoryStats};
15use scirs2_core::parallel_ops::*; use 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#[derive(Debug, Clone)]
24pub struct ParallelConfig {
25 pub num_threads: usize,
27 pub min_params_per_thread: usize,
29 pub enable_work_stealing: bool,
31 pub numa_aware: bool,
33 pub chunk_size: usize,
35 pub lock_free: bool,
37}
38
39impl Default for ParallelConfig {
40 fn default() -> Self {
41 Self {
42 num_threads: 0, 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 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 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 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 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#[derive(Debug)]
96pub struct ParallelOptimizerState {
97 parameter_states: RwLock<HashMap<String, Arc<Mutex<ParameterState>>>>,
99 global_step: Arc<std::sync::atomic::AtomicUsize>,
101 config: ParallelConfig,
103}
104
105#[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 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 pub fn get_or_create_state(&self, param_id: String, size: usize) -> Arc<Mutex<ParameterState>> {
137 {
139 let states =
140 self.parameter_states.read().unwrap_or_else(|poisoned| poisoned.into_inner());
141 if let Some(state) = states.get(¶m_id) {
142 return state.clone();
143 }
144 }
145
146 let mut states =
148 self.parameter_states.write().unwrap_or_else(|poisoned| poisoned.into_inner());
149 if let Some(state) = states.get(¶m_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 pub fn step(&self) {
161 self.global_step.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
162 }
163
164 pub fn get_step(&self) -> usize {
166 self.global_step.load(std::sync::atomic::Ordering::Relaxed)
167 }
168
169 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 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#[derive(Debug)]
203pub struct ParallelAdam {
204 lr: f32,
206 betas: (f32, f32),
208 eps: f32,
210 weight_decay: f32,
212 state: ParallelOptimizerState,
214 params: crate::param_id::ParamRegistry,
219}
220
221impl ParallelAdam {
222 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 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 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 return self.update_sequential(updates);
253 }
254
255 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 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 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 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 let should_parallelize = size >= chunk_size * 2 && self.state.config.num_threads > 1;
304 if should_parallelize {
305 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 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 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 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 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 #[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 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 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 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 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 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 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 }
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#[derive(Debug, Clone)]
505pub struct ParallelStats {
506 pub num_threads: usize,
508 pub memory_stats: StateMemoryStats,
510 pub config: ParallelConfig,
512 pub current_step: usize,
514}
515
516impl ParallelStats {
517 pub fn theoretical_speedup(&self, _sequential_time_ms: f64) -> f64 {
519 let parallel_fraction = 0.95; let num_threads = self.num_threads as f64;
522
523 1.0 / ((1.0 - parallel_fraction) + (parallel_fraction / num_threads))
524 }
525
526 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
561pub trait BatchUpdate {
563 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); 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 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 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 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}