1use crate::kernels::{create_kernel, Kernel, KernelType};
8use crate::smo::{SmoConfig, SmoSolver};
9use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
10use sklears_core::{
11 error::{Result, SklearsError},
12 traits::{Estimator, Fit, Predict, Trained, Untrained},
13 types::Float,
14};
15use std::marker::PhantomData;
16use std::sync::{Arc, Mutex};
17use std::thread;
18
19#[derive(Debug, Clone)]
21pub struct DistributedConfig {
22 pub n_workers: usize,
24 pub sync_interval: usize,
26 pub max_global_iter: usize,
28 pub tolerance: Float,
30 pub async_updates: bool,
32 pub chunk_size: usize,
34 pub cache_size_mb: usize,
36}
37
38impl Default for DistributedConfig {
39 fn default() -> Self {
40 Self {
41 n_workers: 4,
42 sync_interval: 10,
43 max_global_iter: 100,
44 tolerance: 1e-3,
45 async_updates: false,
46 chunk_size: 1000,
47 cache_size_mb: 50,
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Default)]
54pub enum DistributedStrategy {
55 #[default]
57 DataParallel,
58 ModelParallel,
60 Hybrid,
62}
63
64#[derive(Debug, Clone, PartialEq, Default)]
66pub enum CommunicationProtocol {
67 #[default]
69 Synchronous,
70 Asynchronous,
72 ParameterServer,
74}
75
76#[allow(dead_code)] #[derive(Debug, Clone)]
79struct WorkerState {
80 worker_id: usize,
82 local_alpha: Array1<Float>,
84 data_indices: Vec<usize>,
86 converged: bool,
88 local_iterations: usize,
90}
91
92#[derive(Debug)]
94struct SharedState {
95 global_alpha: Arc<Mutex<Array1<Float>>>,
97 global_intercept: Arc<Mutex<Float>>,
99 worker_convergence: Arc<Mutex<Vec<bool>>>,
101 global_iteration: Arc<Mutex<usize>>,
103 #[allow(dead_code)] n_samples: usize,
106}
107
108#[derive(Debug, Clone)]
110pub struct DistributedSVM<S> {
111 pub c: Float,
113 pub kernel: KernelType,
115 pub config: DistributedConfig,
117 pub strategy: DistributedStrategy,
119 pub protocol: CommunicationProtocol,
121 pub n_classes: Option<usize>,
123 pub support_vectors: Option<Array2<Float>>,
125 pub dual_coef: Option<Array1<Float>>,
127 pub intercept: Float,
129 pub classes: Option<Array1<i32>>,
131 pub n_support: Option<Array1<usize>>,
133 _state: PhantomData<S>,
135}
136
137impl DistributedSVM<Untrained> {
138 pub fn new(
140 c: Float,
141 kernel: KernelType,
142 config: DistributedConfig,
143 strategy: DistributedStrategy,
144 protocol: CommunicationProtocol,
145 ) -> Self {
146 Self {
147 c,
148 kernel,
149 config,
150 strategy,
151 protocol,
152 n_classes: None,
153 support_vectors: None,
154 dual_coef: None,
155 intercept: 0.0,
156 classes: None,
157 n_support: None,
158 _state: PhantomData,
159 }
160 }
161
162 pub fn builder() -> DistributedSVMBuilder {
164 DistributedSVMBuilder::new()
165 }
166}
167
168pub struct DistributedSVMBuilder {
170 c: Float,
171 kernel: KernelType,
172 config: DistributedConfig,
173 strategy: DistributedStrategy,
174 protocol: CommunicationProtocol,
175}
176
177impl DistributedSVMBuilder {
178 pub fn new() -> Self {
179 Self {
180 c: 1.0,
181 kernel: KernelType::Rbf { gamma: 1.0 },
182 config: DistributedConfig::default(),
183 strategy: DistributedStrategy::default(),
184 protocol: CommunicationProtocol::default(),
185 }
186 }
187
188 pub fn c(mut self, c: Float) -> Self {
189 self.c = c;
190 self
191 }
192
193 pub fn kernel(mut self, kernel: KernelType) -> Self {
194 self.kernel = kernel;
195 self
196 }
197
198 pub fn n_workers(mut self, n_workers: usize) -> Self {
199 self.config.n_workers = n_workers;
200 self
201 }
202
203 pub fn sync_interval(mut self, sync_interval: usize) -> Self {
204 self.config.sync_interval = sync_interval;
205 self
206 }
207
208 pub fn max_global_iter(mut self, max_global_iter: usize) -> Self {
209 self.config.max_global_iter = max_global_iter;
210 self
211 }
212
213 pub fn tolerance(mut self, tolerance: Float) -> Self {
214 self.config.tolerance = tolerance;
215 self
216 }
217
218 pub fn strategy(mut self, strategy: DistributedStrategy) -> Self {
219 self.strategy = strategy;
220 self
221 }
222
223 pub fn protocol(mut self, protocol: CommunicationProtocol) -> Self {
224 self.protocol = protocol;
225 self
226 }
227
228 pub fn chunk_size(mut self, chunk_size: usize) -> Self {
229 self.config.chunk_size = chunk_size;
230 self
231 }
232
233 pub fn build(self) -> DistributedSVM<Untrained> {
234 DistributedSVM::new(
235 self.c,
236 self.kernel,
237 self.config,
238 self.strategy,
239 self.protocol,
240 )
241 }
242}
243
244impl Default for DistributedSVMBuilder {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, Float>> for DistributedSVM<Untrained> {
251 type Fitted = DistributedSVM<Trained>;
252
253 fn fit(self, x: &ArrayView2<Float>, y: &ArrayView1<Float>) -> Result<Self::Fitted> {
254 self.fit_distributed(*x, *y)
255 }
256}
257
258impl DistributedSVM<Untrained> {
259 pub fn fit_distributed(
261 &self,
262 x: ArrayView2<Float>,
263 y: ArrayView1<Float>,
264 ) -> Result<DistributedSVM<Trained>> {
265 let n_samples = x.nrows();
266 let n_features = x.ncols();
267
268 let mut classes_vec = Vec::new();
270 for &label in y.iter() {
271 let label_i32 = label as i32;
272 if !classes_vec.contains(&label_i32) {
273 classes_vec.push(label_i32);
274 }
275 }
276 classes_vec.sort_unstable();
277 let n_classes = classes_vec.len();
278
279 if n_classes != 2 {
280 return Err(SklearsError::InvalidInput(
281 "Multi-class distributed SVM not yet implemented".to_string(),
282 ));
283 }
284
285 let mut binary_y = Array1::zeros(n_samples);
287 for (i, &label) in y.iter().enumerate() {
288 binary_y[i] = if label as i32 == classes_vec[0] {
289 -1.0
290 } else {
291 1.0
292 };
293 }
294
295 let shared_state = SharedState {
297 global_alpha: Arc::new(Mutex::new(Array1::zeros(n_samples))),
298 global_intercept: Arc::new(Mutex::new(0.0)),
299 worker_convergence: Arc::new(Mutex::new(vec![false; self.config.n_workers])),
300 global_iteration: Arc::new(Mutex::new(0)),
301 n_samples,
302 };
303
304 let chunk_size = n_samples / self.config.n_workers;
306 let mut worker_handles = Vec::new();
307
308 for worker_id in 0..self.config.n_workers {
309 let start_idx = worker_id * chunk_size;
310 let end_idx = if worker_id == self.config.n_workers - 1 {
311 n_samples
312 } else {
313 (worker_id + 1) * chunk_size
314 };
315
316 let worker_x = x
318 .slice(scirs2_core::ndarray::s![start_idx..end_idx, ..])
319 .to_owned();
320 let worker_y = binary_y
321 .slice(scirs2_core::ndarray::s![start_idx..end_idx])
322 .to_owned();
323 let data_indices: Vec<usize> = (start_idx..end_idx).collect();
324
325 let kernel = self.kernel.clone();
327 let c = self.c;
328 let tolerance = self.config.tolerance;
329 let sync_interval = self.config.sync_interval;
330 let max_global_iter = self.config.max_global_iter;
331 let cache_size_mb = self.config.cache_size_mb;
332
333 let global_alpha = shared_state.global_alpha.clone();
335 let global_intercept = shared_state.global_intercept.clone();
336 let worker_convergence = shared_state.worker_convergence.clone();
337 let global_iteration = shared_state.global_iteration.clone();
338
339 let handle = thread::spawn(move || {
341 Self::worker_thread(
342 worker_id,
343 worker_x,
344 worker_y,
345 data_indices,
346 kernel,
347 c,
348 tolerance,
349 sync_interval,
350 max_global_iter,
351 cache_size_mb,
352 global_alpha,
353 global_intercept,
354 worker_convergence,
355 global_iteration,
356 )
357 });
358
359 worker_handles.push(handle);
360 }
361
362 for handle in worker_handles {
364 handle.join().map_err(|_| {
365 SklearsError::NumericalError("Worker thread panicked".to_string())
366 })??;
367 }
368
369 let final_alpha = shared_state
371 .global_alpha
372 .lock()
373 .expect("lock not poisoned")
374 .clone();
375 let final_intercept = *shared_state
376 .global_intercept
377 .lock()
378 .expect("lock not poisoned");
379
380 let support_indices: Vec<usize> = final_alpha
382 .iter()
383 .enumerate()
384 .filter(|(_, &coef)| coef.abs() > 1e-10)
385 .map(|(i, _)| i)
386 .collect();
387
388 let n_support_vectors = support_indices.len();
389 let mut support_vectors = Array2::zeros((n_support_vectors, n_features));
390 let mut support_dual_coef = Array1::zeros(n_support_vectors);
391
392 for (i, &idx) in support_indices.iter().enumerate() {
393 support_vectors.row_mut(i).assign(&x.row(idx));
394 support_dual_coef[i] = final_alpha[idx];
395 }
396
397 let mut n_support = Array1::zeros(n_classes);
399 for &coef in support_dual_coef.iter() {
400 if coef > 0.0 {
401 n_support[1] += 1;
402 } else {
403 n_support[0] += 1;
404 }
405 }
406
407 Ok(DistributedSVM {
408 c: self.c,
409 kernel: self.kernel.clone(),
410 config: self.config.clone(),
411 strategy: self.strategy.clone(),
412 protocol: self.protocol.clone(),
413 n_classes: Some(n_classes),
414 support_vectors: Some(support_vectors),
415 dual_coef: Some(support_dual_coef),
416 intercept: final_intercept,
417 classes: Some(Array1::from_vec(classes_vec)),
418 n_support: Some(n_support),
419 _state: PhantomData,
420 })
421 }
422
423 #[allow(clippy::too_many_arguments)]
425 fn worker_thread(
426 worker_id: usize,
427 x: Array2<Float>,
428 y: Array1<Float>,
429 data_indices: Vec<usize>,
430 kernel: KernelType,
431 c: Float,
432 tolerance: Float,
433 sync_interval: usize,
434 max_global_iter: usize,
435 cache_size_mb: usize,
436 global_alpha: Arc<Mutex<Array1<Float>>>,
437 global_intercept: Arc<Mutex<Float>>,
438 worker_convergence: Arc<Mutex<Vec<bool>>>,
439 global_iteration: Arc<Mutex<usize>>,
440 ) -> Result<()> {
441 let mut local_alpha = Array1::zeros(x.nrows());
443 let mut local_iterations = 0;
444
445 let smo_config = SmoConfig {
447 c,
448 tol: tolerance,
449 max_iter: sync_interval,
450 cache_size: cache_size_mb,
451 shrinking: true,
452 ..Default::default()
453 };
454
455 let concrete_kernel = create_kernel(kernel)?;
456 let mut smo_solver = SmoSolver::new(smo_config, concrete_kernel);
457
458 loop {
459 if local_iterations >= max_global_iter * 2 {
461 break;
462 }
463 local_iterations += 1;
464
465 {
467 let global_iter = *global_iteration.lock().expect("lock not poisoned");
468 let convergence_flags = worker_convergence.lock().expect("lock not poisoned");
469
470 if global_iter >= max_global_iter || convergence_flags.iter().all(|&x| x) {
471 break;
472 }
473 }
474
475 let smo_result = smo_solver.solve(&x, &y)?;
477
478 for (i, &coef) in smo_result.alpha.iter().enumerate() {
480 local_alpha[i] = coef;
481 }
482
483 {
485 let mut global_alpha_lock = global_alpha.lock().expect("lock not poisoned");
486 let mut global_intercept_lock = global_intercept.lock().expect("lock not poisoned");
487 let mut convergence_lock = worker_convergence.lock().expect("lock not poisoned");
488 let mut global_iter_lock = global_iteration.lock().expect("lock not poisoned");
489
490 for (local_idx, &global_idx) in data_indices.iter().enumerate() {
492 global_alpha_lock[global_idx] = local_alpha[local_idx];
493 }
494
495 *global_intercept_lock = (*global_intercept_lock * worker_id as Float
497 + smo_result.b)
498 / (worker_id + 1) as Float;
499
500 convergence_lock[worker_id] = smo_result.converged;
502
503 if worker_id == 0 {
505 *global_iter_lock += 1;
506 }
507 }
508
509 thread::yield_now();
511 }
512
513 Ok(())
514 }
515}
516
517impl Predict<ArrayView2<'_, Float>, Array1<Float>> for DistributedSVM<Trained> {
518 fn predict(&self, x: &ArrayView2<Float>) -> Result<Array1<Float>> {
519 let support_vectors =
520 self.support_vectors
521 .as_ref()
522 .ok_or_else(|| SklearsError::NotFitted {
523 operation: "predict".to_string(),
524 })?;
525
526 let dual_coef = self
527 .dual_coef
528 .as_ref()
529 .ok_or_else(|| SklearsError::NotFitted {
530 operation: "predict".to_string(),
531 })?;
532
533 let classes = self
534 .classes
535 .as_ref()
536 .ok_or_else(|| SklearsError::NotFitted {
537 operation: "predict".to_string(),
538 })?;
539
540 let concrete_kernel = create_kernel(self.kernel.clone())?;
541 let mut predictions = Array1::zeros(x.nrows());
542
543 for i in 0..x.nrows() {
545 let mut decision_value = 0.0;
546
547 for j in 0..support_vectors.nrows() {
549 let kernel_value = concrete_kernel.compute(
550 x.row(i).to_owned().view(),
551 support_vectors.row(j).to_owned().view(),
552 );
553 decision_value += dual_coef[j] * kernel_value;
554 }
555
556 decision_value += self.intercept;
557
558 predictions[i] = if decision_value >= 0.0 {
560 classes[1] as Float
561 } else {
562 classes[0] as Float
563 };
564 }
565
566 Ok(predictions)
567 }
568}
569
570impl Estimator for DistributedSVM<Untrained> {
571 type Config = DistributedConfig;
572 type Error = SklearsError;
573 type Float = Float;
574
575 fn config(&self) -> &Self::Config {
576 &self.config
577 }
578}
579
580impl Estimator for DistributedSVM<Trained> {
581 type Config = DistributedConfig;
582 type Error = SklearsError;
583 type Float = Float;
584
585 fn config(&self) -> &Self::Config {
586 &self.config
587 }
588}
589
590#[allow(non_snake_case)]
591#[cfg(test)]
592mod tests {
593 use super::*;
594 use scirs2_core::ndarray::{Array1, Array2};
595
596 #[test]
597 fn test_distributed_svm_creation() {
598 let config = DistributedConfig::default();
599 let svm = DistributedSVM::new(
600 1.0,
601 KernelType::Linear,
602 config,
603 DistributedStrategy::DataParallel,
604 CommunicationProtocol::Synchronous,
605 );
606 assert_eq!(svm.c, 1.0);
607 assert_eq!(svm.config().n_workers, 4);
608 }
609
610 #[test]
611 fn test_distributed_svm_builder() {
612 let svm = DistributedSVM::builder()
613 .c(2.0)
614 .kernel(KernelType::Rbf { gamma: 0.5 })
615 .n_workers(8)
616 .tolerance(1e-4)
617 .strategy(DistributedStrategy::ModelParallel)
618 .protocol(CommunicationProtocol::Asynchronous)
619 .build();
620
621 assert_eq!(svm.c, 2.0);
622 assert_eq!(svm.config().n_workers, 8);
623 assert_eq!(svm.config().tolerance, 1e-4);
624 assert_eq!(svm.strategy, DistributedStrategy::ModelParallel);
625 assert_eq!(svm.protocol, CommunicationProtocol::Asynchronous);
626 }
627
628 #[test]
629 #[ignore = "Disabled due to potential deadlock in worker threads - needs architecture redesign"]
630 fn test_distributed_svm_training() -> Result<()> {
631 let x = Array2::from_shape_vec(
633 (8, 2),
634 vec![
635 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 6.0, 7.0, 7.0, 8.0, 8.0, 9.0,
636 ],
637 )?;
638 let y = Array1::from_vec(vec![1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0]);
639
640 let svm = DistributedSVM::builder()
641 .c(1.0)
642 .kernel(KernelType::Linear)
643 .n_workers(2)
644 .tolerance(1e-2)
645 .max_global_iter(10)
646 .build();
647
648 let trained_svm = svm.fit(&x.view(), &y.view())?;
649
650 let predictions = trained_svm.predict(&x.view())?;
652 assert_eq!(predictions.len(), 8);
653
654 for &pred in predictions.iter() {
656 assert!(pred == 1.0 || pred == -1.0);
657 }
658
659 Ok(())
660 }
661}