1use scirs2_core::ndarray::{Array, Array1, Dimension, RemoveAxis};
2use sklears_core::error::SklearsError;
3use sklears_core::types::FloatBounds;
4use std::collections::HashMap;
5use std::time::Instant;
6
7pub type DistributedResult<T> = Result<T, SklearsError>;
9
10#[derive(Debug, Clone)]
12pub struct DistributedConfig<T: FloatBounds> {
13 pub num_workers: usize,
15 pub backend: DistributedBackend,
17 pub sync_strategy: GradientSyncStrategy,
19 pub batch_size_per_worker: usize,
21 pub sync_frequency: usize,
23 pub lr_scaling: LearningRateScaling<T>,
25 pub gradient_compression: bool,
27 pub compression_threshold: T,
29 pub max_grad_norm: Option<T>,
31 pub warmup_steps: usize,
33}
34
35impl<T: FloatBounds> Default for DistributedConfig<T> {
36 fn default() -> Self {
37 Self {
38 num_workers: 1,
39 backend: DistributedBackend::CPU,
40 sync_strategy: GradientSyncStrategy::AllReduce,
41 batch_size_per_worker: 32,
42 sync_frequency: 1,
43 lr_scaling: LearningRateScaling::Linear,
44 gradient_compression: false,
45 compression_threshold: T::from(0.01).unwrap_or_else(|| T::zero()),
46 max_grad_norm: Some(T::from(1.0).unwrap_or_else(|| T::zero())),
47 warmup_steps: 0,
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub enum DistributedBackend {
55 CPU,
57 GPU,
59 Mixed,
61}
62
63#[derive(Debug, Clone)]
65pub enum GradientSyncStrategy {
66 AllReduce,
68 ParameterServer,
70 Hierarchical,
72 Asynchronous,
74}
75
76#[derive(Debug, Clone)]
78pub enum LearningRateScaling<T: FloatBounds> {
79 Linear,
81 SquareRoot,
83 Custom(T),
85 None,
87}
88
89#[derive(Debug, Clone, Default)]
91pub struct DistributedStats<T: FloatBounds> {
92 pub communication_time_ms: Vec<f64>,
94 pub computation_time_ms: Vec<f64>,
96 pub gradient_norms_before: Vec<T>,
98 pub gradient_norms_after: Vec<T>,
100 pub memory_usage_mb: Vec<f64>,
102 pub load_balance_efficiency: Vec<f64>,
104}
105
106#[allow(dead_code)] pub struct DistributedTrainer<T: FloatBounds> {
109 config: DistributedConfig<T>,
111 workers: Vec<WorkerCoordinator<T>>,
113 parameter_server: Option<ParameterServer<T>>,
115 stats: DistributedStats<T>,
117 current_step: usize,
119 gradient_buffers: HashMap<String, Array1<T>>,
121}
122
123impl<T: FloatBounds + Default + std::iter::Sum<T> + scirs2_core::ndarray::ScalarOperand + Copy>
124 DistributedTrainer<T>
125{
126 pub fn new(config: DistributedConfig<T>) -> DistributedResult<Self> {
128 if config.num_workers == 0 {
129 return Err(SklearsError::InvalidParameter {
130 name: "num_workers".to_string(),
131 reason: "Number of workers must be greater than 0".to_string(),
132 });
133 }
134
135 let workers = (0..config.num_workers)
136 .map(|rank| WorkerCoordinator::new(rank, &config))
137 .collect::<Result<Vec<_>, _>>()?;
138
139 let parameter_server = match config.sync_strategy {
140 GradientSyncStrategy::ParameterServer => Some(ParameterServer::new(&config)?),
141 _ => None,
142 };
143
144 Ok(Self {
145 config,
146 workers,
147 parameter_server,
148 stats: DistributedStats::default(),
149 current_step: 0,
150 gradient_buffers: HashMap::new(),
151 })
152 }
153
154 pub fn distribute_data<D>(&self, data: &Array<T, D>) -> DistributedResult<Vec<Array<T, D>>>
156 where
157 D: Dimension + RemoveAxis,
158 {
159 let num_samples = data.shape()[0];
160 let samples_per_worker = num_samples / self.config.num_workers;
161
162 if samples_per_worker == 0 {
163 return Err(SklearsError::InvalidParameter {
164 name: "data_size".to_string(),
165 reason: "Data size too small for the number of workers".to_string(),
166 });
167 }
168
169 let mut distributed_data = Vec::new();
170
171 for i in 0..self.config.num_workers {
172 let start_idx = i * samples_per_worker;
173 let end_idx = if i == self.config.num_workers - 1 {
174 num_samples } else {
176 (i + 1) * samples_per_worker
177 };
178
179 let worker_data =
180 data.slice_axis(scirs2_core::ndarray::Axis(0), (start_idx..end_idx).into());
181 distributed_data.push(worker_data.to_owned());
182 }
183
184 Ok(distributed_data)
185 }
186
187 pub fn synchronize_gradients(
189 &mut self,
190 gradients: &mut HashMap<String, Array1<T>>,
191 ) -> DistributedResult<()> {
192 let start_time = Instant::now();
193
194 match self.config.sync_strategy {
195 GradientSyncStrategy::AllReduce => {
196 self.all_reduce_gradients(gradients)?;
197 }
198 GradientSyncStrategy::ParameterServer => {
199 self.parameter_server_sync(gradients)?;
200 }
201 GradientSyncStrategy::Hierarchical => {
202 self.hierarchical_sync(gradients)?;
203 }
204 GradientSyncStrategy::Asynchronous => {
205 }
207 }
208
209 if self.config.gradient_compression {
211 self.compress_gradients(gradients)?;
212 }
213
214 if let Some(max_norm) = self.config.max_grad_norm {
216 self.clip_gradients(gradients, max_norm)?;
217 }
218
219 let communication_time = start_time.elapsed().as_millis() as f64;
220 self.stats.communication_time_ms.push(communication_time);
221
222 Ok(())
223 }
224
225 fn all_reduce_gradients(
227 &mut self,
228 gradients: &mut HashMap<String, Array1<T>>,
229 ) -> DistributedResult<()> {
230 for (_name, grad) in gradients.iter_mut() {
231 let sum: T = grad.iter().copied().sum();
233 let mean = sum / T::from(self.config.num_workers).unwrap_or_else(|| T::zero());
234 grad.fill(mean);
235
236 let norm_before = self.compute_gradient_norm(grad);
238 self.stats.gradient_norms_before.push(norm_before);
239 }
240 Ok(())
241 }
242
243 fn parameter_server_sync(
245 &mut self,
246 gradients: &mut HashMap<String, Array1<T>>,
247 ) -> DistributedResult<()> {
248 if let Some(ref mut ps) = self.parameter_server {
249 ps.aggregate_gradients(gradients)?;
250 ps.broadcast_parameters(gradients)?;
251 }
252 Ok(())
253 }
254
255 fn hierarchical_sync(
257 &mut self,
258 gradients: &mut HashMap<String, Array1<T>>,
259 ) -> DistributedResult<()> {
260 let num_levels = (self.config.num_workers as f64).log2().ceil() as usize;
262
263 for level in 0..num_levels {
264 let group_size = 2_usize.pow(level as u32);
265 for (_name, grad) in gradients.iter_mut() {
266 let reduction_factor = T::from(group_size).unwrap_or_else(|| T::zero());
268 grad.mapv_inplace(|x| x / reduction_factor);
269 }
270 }
271
272 Ok(())
273 }
274
275 fn compress_gradients(
277 &self,
278 gradients: &mut HashMap<String, Array1<T>>,
279 ) -> DistributedResult<()> {
280 for grad in gradients.values_mut() {
281 grad.mapv_inplace(|x| {
282 if x.abs() < self.config.compression_threshold {
283 T::zero()
284 } else {
285 x
286 }
287 });
288 }
289 Ok(())
290 }
291
292 fn clip_gradients(
294 &self,
295 gradients: &mut HashMap<String, Array1<T>>,
296 max_norm: T,
297 ) -> DistributedResult<()> {
298 for grad in gradients.values_mut() {
299 let norm = self.compute_gradient_norm(grad);
300 if norm > max_norm {
301 let scale_factor = max_norm / norm;
302 grad.mapv_inplace(|x| x * scale_factor);
303 }
304 }
305 Ok(())
306 }
307
308 fn compute_gradient_norm(&self, grad: &Array1<T>) -> T {
310 grad.mapv(|x| x * x).sum().sqrt()
311 }
312
313 pub fn scale_learning_rate(&self, base_lr: T) -> T {
315 match self.config.lr_scaling {
316 LearningRateScaling::Linear => {
317 base_lr * T::from(self.config.num_workers).unwrap_or_else(|| T::zero())
318 }
319 LearningRateScaling::SquareRoot => {
320 base_lr
321 * T::from(self.config.num_workers as f64)
322 .unwrap_or_else(|| T::zero())
323 .sqrt()
324 }
325 LearningRateScaling::Custom(factor) => base_lr * factor,
326 LearningRateScaling::None => base_lr,
327 }
328 }
329
330 pub fn update_stats(&mut self, computation_time_ms: f64, memory_usage_mb: f64) {
332 self.stats.computation_time_ms.push(computation_time_ms);
333 self.stats.memory_usage_mb.push(memory_usage_mb);
334
335 let efficiency = self.calculate_load_balance_efficiency();
337 self.stats.load_balance_efficiency.push(efficiency);
338 }
339
340 fn calculate_load_balance_efficiency(&self) -> f64 {
342 if self.stats.computation_time_ms.is_empty() {
343 return 1.0;
344 }
345
346 let max_time = *self
347 .stats
348 .computation_time_ms
349 .iter()
350 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
351 .expect("value should be present");
352 let avg_time = self.stats.computation_time_ms.iter().sum::<f64>()
353 / self.stats.computation_time_ms.len() as f64;
354
355 if max_time > 0.0 {
356 avg_time / max_time
357 } else {
358 1.0
359 }
360 }
361
362 pub fn get_stats(&self) -> &DistributedStats<T> {
364 &self.stats
365 }
366
367 pub fn training_step<F>(
369 &mut self,
370 compute_fn: F,
371 gradients: &mut HashMap<String, Array1<T>>,
372 ) -> DistributedResult<()>
373 where
374 F: Fn() -> DistributedResult<f64> + Send + Sync,
375 {
376 let start_time = Instant::now();
377
378 let computation_time = compute_fn()?;
380
381 if self.current_step.is_multiple_of(self.config.sync_frequency) {
383 self.synchronize_gradients(gradients)?;
384 }
385
386 let _total_time = start_time.elapsed().as_millis() as f64;
388 self.update_stats(computation_time, 0.0); self.current_step += 1;
391
392 Ok(())
393 }
394}
395
396#[allow(dead_code)] pub struct WorkerCoordinator<T: FloatBounds> {
399 rank: usize,
401 local_gradients: HashMap<String, Array1<T>>,
403 config: DistributedConfig<T>,
405}
406
407impl<T: FloatBounds> WorkerCoordinator<T> {
408 pub fn new(rank: usize, config: &DistributedConfig<T>) -> DistributedResult<Self> {
410 Ok(Self {
411 rank,
412 local_gradients: HashMap::new(),
413 config: config.clone(),
414 })
415 }
416
417 pub fn get_rank(&self) -> usize {
419 self.rank
420 }
421
422 pub fn update_gradients(&mut self, gradients: HashMap<String, Array1<T>>) {
424 self.local_gradients = gradients;
425 }
426
427 pub fn get_gradients(&self) -> &HashMap<String, Array1<T>> {
429 &self.local_gradients
430 }
431}
432
433#[allow(dead_code)] pub struct ParameterServer<T: FloatBounds> {
436 global_parameters: HashMap<String, Array1<T>>,
438 accumulated_gradients: HashMap<String, Array1<T>>,
440 num_workers: usize,
442}
443
444impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand + Copy> ParameterServer<T> {
445 pub fn new(config: &DistributedConfig<T>) -> DistributedResult<Self> {
447 Ok(Self {
448 global_parameters: HashMap::new(),
449 accumulated_gradients: HashMap::new(),
450 num_workers: config.num_workers,
451 })
452 }
453
454 pub fn aggregate_gradients(
456 &mut self,
457 gradients: &HashMap<String, Array1<T>>,
458 ) -> DistributedResult<()> {
459 for (name, grad) in gradients {
460 let accumulated = self
461 .accumulated_gradients
462 .entry(name.clone())
463 .or_insert_with(|| Array1::zeros(grad.len()));
464
465 *accumulated = &*accumulated + grad;
466 }
467 Ok(())
468 }
469
470 pub fn broadcast_parameters(
472 &mut self,
473 gradients: &mut HashMap<String, Array1<T>>,
474 ) -> DistributedResult<()> {
475 for (name, grad) in gradients.iter_mut() {
476 if let Some(accumulated) = self.accumulated_gradients.get(name) {
477 let avg_grad = accumulated / T::from(self.num_workers).unwrap_or_else(|| T::zero());
479 *grad = avg_grad;
480 }
481 }
482
483 self.accumulated_gradients.clear();
485 Ok(())
486 }
487}
488
489#[allow(non_snake_case)]
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use approx::assert_relative_eq;
496 use scirs2_core::ndarray::Array2;
497
498 #[test]
499 fn test_distributed_config_default() {
500 let config = DistributedConfig::<f64>::default();
501 assert_eq!(config.num_workers, 1);
502 assert!(matches!(config.backend, DistributedBackend::CPU));
503 assert!(matches!(
504 config.sync_strategy,
505 GradientSyncStrategy::AllReduce
506 ));
507 }
508
509 #[test]
510 fn test_distributed_trainer_creation() {
511 let config = DistributedConfig::<f64> {
512 num_workers: 4,
513 ..Default::default()
514 };
515
516 let trainer = DistributedTrainer::new(config);
517 assert!(trainer.is_ok());
518 }
519
520 #[test]
521 fn test_data_distribution() {
522 let config = DistributedConfig::<f64> {
523 num_workers: 2,
524 ..Default::default()
525 };
526
527 let trainer = DistributedTrainer::new(config).expect("construction should succeed");
528 let data = Array2::<f64>::ones((100, 10));
529
530 let distributed_data = trainer
531 .distribute_data(&data)
532 .expect("operation should succeed");
533 assert_eq!(distributed_data.len(), 2);
534 assert_eq!(distributed_data[0].nrows(), 50);
535 assert_eq!(distributed_data[1].nrows(), 50);
536 }
537
538 #[test]
539 fn test_learning_rate_scaling() {
540 let config = DistributedConfig::<f64> {
541 num_workers: 4,
542 lr_scaling: LearningRateScaling::Linear,
543 ..Default::default()
544 };
545
546 let trainer = DistributedTrainer::new(config).expect("construction should succeed");
547 let base_lr = 0.01;
548 let scaled_lr = trainer.scale_learning_rate(base_lr);
549
550 assert_relative_eq!(scaled_lr, 0.04, epsilon = 1e-10);
551 }
552
553 #[test]
554 fn test_gradient_compression() {
555 let config = DistributedConfig::<f64> {
556 gradient_compression: true,
557 compression_threshold: 0.1,
558 ..Default::default()
559 };
560
561 let _trainer = DistributedTrainer::new(config).expect("construction should succeed");
562 let mut gradients = HashMap::new();
563 gradients.insert(
564 "test".to_string(),
565 Array1::from_vec(vec![0.05, 0.15, 0.02, 0.2]),
566 );
567
568 let grad = gradients.get("test").expect("operation should succeed");
569 assert_eq!(grad[0], 0.05); assert_eq!(grad[1], 0.15); }
572
573 #[test]
574 fn test_worker_coordinator() {
575 let config = DistributedConfig::<f64>::default();
576 let worker = WorkerCoordinator::new(0, &config);
577
578 assert!(worker.is_ok());
579 assert_eq!(worker.expect("operation should succeed").get_rank(), 0);
580 }
581
582 #[test]
583 fn test_parameter_server() {
584 let config = DistributedConfig::<f64> {
585 num_workers: 2,
586 ..Default::default()
587 };
588
589 let ps = ParameterServer::new(&config);
590 assert!(ps.is_ok());
591 }
592}