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}
215
216impl ParallelAdam {
217 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 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 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 return self.update_sequential(updates);
247 }
248
249 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 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 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 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 let should_parallelize = size >= chunk_size * 2 && self.state.config.num_threads > 1;
298 if should_parallelize {
299 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 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 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 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 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 #[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 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 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 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 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 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 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 }
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#[derive(Debug, Clone)]
499pub struct ParallelStats {
500 pub num_threads: usize,
502 pub memory_stats: StateMemoryStats,
504 pub config: ParallelConfig,
506 pub current_step: usize,
508}
509
510impl ParallelStats {
511 pub fn theoretical_speedup(&self, _sequential_time_ms: f64) -> f64 {
513 let parallel_fraction = 0.95; let num_threads = self.num_threads as f64;
516
517 1.0 / ((1.0 - parallel_fraction) + (parallel_fraction / num_threads))
518 }
519
520 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
555pub trait BatchUpdate {
557 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); 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 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 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 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}