1use crate::arrow::TensorDtype;
13use thiserror::Error;
14
15pub mod arrow_ipc;
16pub mod backward_pass;
17pub mod checkpoint;
18pub mod computation_graph;
19pub mod coordination;
20pub mod federated;
21pub mod tensor;
22
23#[derive(Debug, Error)]
27pub enum GradientError {
28 #[error("Shape mismatch: expected {expected:?}, got {actual:?}")]
29 ShapeMismatch {
30 expected: Vec<usize>,
31 actual: Vec<usize>,
32 },
33
34 #[error("Checksum verification failed")]
35 ChecksumFailed,
36
37 #[error("Invalid compression ratio: {0}")]
38 InvalidCompressionRatio(f32),
39
40 #[error("Empty gradient set")]
41 EmptyGradientSet,
42
43 #[error("Incompatible dtype: {0:?}")]
44 IncompatibleDtype(TensorDtype),
45
46 #[error("Outlier detected at index {index}: value {value}")]
47 OutlierDetected { index: usize, value: f32 },
48
49 #[error("Invalid gradient: {0}")]
50 InvalidGradient(String),
51
52 #[error("Empty gradients provided")]
53 EmptyGradients,
54
55 #[error("Gradient dimension mismatch between peers")]
56 DimensionMismatch,
57
58 #[error("Node not found in backward pass schedule: {0}")]
59 NodeNotFound(String),
60
61 #[error("Peer not found in step: {0}")]
62 PeerNotFound(String),
63}
64
65pub use arrow_ipc::{load_gradient_from_arrow, store_gradient_as_arrow};
68
69pub use backward_pass::{
70 clip_gradient_norm, federated_average, AggregationMethod, BackwardPassConfig,
71 BackwardPassCoordinator as LegacyBackwardPassCoordinator, BackwardPassStats, BackwardPassStep,
72 BackwardStepStatus,
73};
74
75pub use checkpoint::GradientCheckpoint;
76
77pub use computation_graph::{ComputationGraphError, ComputationGraphStore, ComputationNode};
78
79pub use federated::{
80 ClientInfo, ClientState, ConvergenceConfig, ConvergenceDetector, DPMechanism,
81 DifferentialPrivacy, DistributedGradientAccumulator, FederatedError, FederatedRound,
82 GossipModelSync, ModelSyncProtocol, ModelUpdate, PrivacyBudget, RoundStats, SecureAggregation,
83};
84
85pub use tensor::{
86 GradientAggregator, GradientCompressor, GradientDelta, GradientVerifier, LayerGradient,
87 QuantizedGradient, SparseGradient,
88};
89
90pub use coordination::{
91 ArrowBlockError, BackwardPassCoordinator, BackwardPassId, CoordinationError,
92 CoordinationStatus, GradientArrowBlock, GradientContribution,
93};
94
95#[cfg(test)]
98mod tests {
99 use super::*;
100 use ipfrs_core::Cid;
101
102 #[test]
103 fn test_sparse_gradient() {
104 let indices = vec![0, 5, 10];
105 let values = vec![1.0, 2.0, 3.0];
106 let shape = vec![20];
107
108 let sparse = SparseGradient::new(indices.clone(), values.clone(), shape);
109
110 assert_eq!(sparse.nnz(), 3);
111 assert_eq!(sparse.total_elements(), 20);
112 assert!((sparse.sparsity_ratio() - 0.85).abs() < 0.01);
113
114 let dense = sparse.to_dense();
115 assert_eq!(dense.len(), 20);
116 assert_eq!(dense[0], 1.0);
117 assert_eq!(dense[5], 2.0);
118 assert_eq!(dense[10], 3.0);
119 }
120
121 #[test]
122 fn test_quantized_gradient() {
123 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
124 let shape = vec![5];
125
126 let quantized = QuantizedGradient::from_dense(&values, shape);
127 let dequantized = quantized.to_dense();
128
129 for (i, (orig, deq)) in values.iter().zip(&dequantized).enumerate() {
133 let error = (orig - deq).abs();
134 assert!(
136 error < 0.02,
137 "Value {} mismatch: orig={}, deq={}, error={}",
138 i,
139 orig,
140 deq,
141 error
142 );
143 }
144 }
145
146 #[test]
147 fn test_gradient_delta() {
148 let base_cid = Cid::default();
149 let mut delta = GradientDelta::new(base_cid);
150
151 delta.add_dense_gradient("layer1".to_string(), vec![1.0, 2.0, 3.0], vec![3]);
152 delta.add_dense_gradient("layer2".to_string(), vec![4.0, 5.0], vec![2]);
153
154 assert_eq!(delta.layer_gradients.len(), 2);
155 assert!(delta.verify_checksum().is_ok());
156 }
157
158 #[test]
159 fn test_top_k_compression() {
160 let values = vec![1.0, 5.0, 2.0, 8.0, 3.0];
161 let shape = vec![5];
162
163 let sparse = GradientCompressor::top_k(&values, shape, 2).expect("test: should succeed");
164
165 assert_eq!(sparse.nnz(), 2);
166 assert!(sparse.values.contains(&8.0));
167 assert!(sparse.values.contains(&5.0));
168 }
169
170 #[test]
171 fn test_threshold_compression() {
172 let values = vec![0.1, 5.0, 0.2, 8.0, 0.3];
173 let shape = vec![5];
174
175 let sparse = GradientCompressor::threshold(&values, shape, 1.0);
176
177 assert_eq!(sparse.nnz(), 2);
178 assert!(sparse.values.contains(&5.0));
179 assert!(sparse.values.contains(&8.0));
180 }
181
182 #[test]
183 fn test_gradient_averaging() {
184 let g1 = vec![1.0, 2.0, 3.0];
185 let g2 = vec![3.0, 4.0, 5.0];
186 let gradients = vec![g1, g2];
187
188 let avg = GradientAggregator::average(&gradients).expect("test: should succeed");
189
190 assert_eq!(avg, vec![2.0, 3.0, 4.0]);
191 }
192
193 #[test]
194 fn test_weighted_averaging() {
195 let g1 = vec![1.0, 2.0, 3.0];
196 let g2 = vec![3.0, 4.0, 5.0];
197 let gradients = vec![g1, g2];
198 let weights = vec![0.25, 0.75];
199
200 let avg = GradientAggregator::weighted_average(&gradients, &weights)
201 .expect("test: should succeed");
202
203 assert!((avg[0] - 2.5).abs() < 0.01);
205 assert!((avg[1] - 3.5).abs() < 0.01);
206 assert!((avg[2] - 4.5).abs() < 0.01);
207 }
208
209 #[test]
210 fn test_momentum() {
211 let current = vec![1.0, 2.0, 3.0];
212 let previous = vec![0.5, 1.0, 1.5];
213
214 let result = GradientAggregator::apply_momentum(¤t, &previous, 0.9)
215 .expect("test: should succeed");
216
217 assert!((result[0] - 1.45).abs() < 0.01);
219 assert!((result[1] - 2.9).abs() < 0.01);
220 assert!((result[2] - 4.35).abs() < 0.01);
221 }
222
223 #[test]
224 fn test_gradient_verification() {
225 let gradient = vec![1.0, 2.0, 3.0, 4.0];
226
227 assert!(GradientVerifier::verify_shape(&gradient, &[4]).is_ok());
229 assert!(GradientVerifier::verify_shape(&gradient, &[2, 2]).is_ok());
230 assert!(GradientVerifier::verify_shape(&gradient, &[5]).is_err());
231
232 assert!(GradientVerifier::verify_finite(&gradient).is_ok());
234
235 let invalid = vec![1.0, f32::NAN, 3.0];
236 assert!(GradientVerifier::verify_finite(&invalid).is_err());
237 }
238
239 #[test]
240 fn test_gradient_clipping() {
241 let mut gradient = vec![3.0, 4.0]; GradientVerifier::clip_by_norm(&mut gradient, 2.5);
244
245 let norm = GradientVerifier::l2_norm(&gradient);
246 assert!((norm - 2.5).abs() < 0.01);
247 }
248
249 #[test]
250 fn test_privacy_budget() {
251 let mut budget = PrivacyBudget::new(1.0, 1e-5);
252
253 assert_eq!(budget.remaining_epsilon, 1.0);
254 assert!(!budget.is_exhausted());
255
256 budget.consume(0.5).expect("test: should succeed");
258 assert_eq!(budget.remaining_epsilon, 0.5);
259 assert!((budget.remaining_fraction() - 0.5).abs() < 1e-6);
260
261 budget.consume(0.5).expect("test: should succeed");
263 assert!(budget.is_exhausted());
264
265 assert!(budget.consume(0.1).is_err());
267 }
268
269 #[test]
270 fn test_differential_privacy_gaussian() {
271 let mut dp = DifferentialPrivacy::new(1.0, 1e-5, 1.0, DPMechanism::Gaussian);
272 let mut gradient = vec![1.0, 2.0, 3.0, 4.0];
273 let original = gradient.clone();
274
275 dp.add_gaussian_noise(&mut gradient)
276 .expect("test: should succeed");
277
278 assert_ne!(gradient, original);
280
281 assert!(GradientVerifier::verify_finite(&gradient).is_ok());
283
284 assert!(dp.remaining_budget() < 1.0);
286 }
287
288 #[test]
289 fn test_differential_privacy_laplacian() {
290 let mut dp = DifferentialPrivacy::new(1.0, 1e-5, 1.0, DPMechanism::Laplacian);
291 let mut gradient = vec![1.0, 2.0, 3.0, 4.0];
292 let original = gradient.clone();
293
294 dp.add_laplacian_noise(&mut gradient)
295 .expect("test: should succeed");
296
297 assert_ne!(gradient, original);
299
300 assert!(GradientVerifier::verify_finite(&gradient).is_ok());
302
303 assert!(dp.remaining_budget() < 1.0);
305 }
306
307 #[test]
308 fn test_dp_sgd() {
309 let mut dp = DifferentialPrivacy::new(1.0, 1e-5, 1.0, DPMechanism::Gaussian);
310 let mut gradient = vec![3.0, 4.0, 5.0, 6.0]; let original_norm = GradientVerifier::l2_norm(&gradient);
312
313 dp.apply_dp_sgd(&mut gradient, 5.0)
314 .expect("test: should succeed");
315
316 let new_norm = GradientVerifier::l2_norm(&gradient);
318
319 assert!(original_norm != new_norm);
322
323 assert!(GradientVerifier::verify_finite(&gradient).is_ok());
325 }
326
327 #[test]
328 fn test_privacy_budget_exhaustion() {
329 let mut dp = DifferentialPrivacy::new(1.0, 1e-5, 1.0, DPMechanism::Gaussian);
330 let mut gradient = vec![1.0, 2.0];
331
332 let mut successful_calls = 0;
335 for _ in 0..200 {
336 if dp.add_gaussian_noise(&mut gradient).is_ok() {
337 successful_calls += 1;
338 } else {
339 break;
341 }
342 }
343
344 assert!(
346 (90..=110).contains(&successful_calls),
347 "Expected ~100 calls, got {}",
348 successful_calls
349 );
350
351 let remaining = dp.remaining_budget();
353 assert!(
354 remaining < 0.02,
355 "Expected nearly exhausted budget, got {}",
356 remaining
357 );
358
359 let mut new_gradient = vec![1.0, 2.0];
361 let result = dp.add_gaussian_noise(&mut new_gradient);
362 let _ = result;
365 }
366
367 #[test]
368 fn test_noise_multiplier_calculation() {
369 let epsilon = 1.0;
370 let delta = 1e-5;
371 let sensitivity = 1.0;
372
373 let multiplier =
374 DifferentialPrivacy::calculate_noise_multiplier(epsilon, delta, sensitivity);
375
376 assert!(multiplier > 0.0);
378 assert!(multiplier < 10.0); let multiplier_high_eps =
382 DifferentialPrivacy::calculate_noise_multiplier(10.0, delta, sensitivity);
383 assert!(multiplier_high_eps < multiplier);
384 }
385
386 #[test]
387 fn test_secure_aggregation() {
388 let mut aggregator = SecureAggregation::new(3);
389
390 assert_eq!(aggregator.participant_count(), 0);
391 assert!(!aggregator.can_aggregate());
392
393 aggregator.add_participant();
395 aggregator.add_participant();
396 assert!(!aggregator.can_aggregate());
397
398 aggregator.add_participant();
399 assert!(aggregator.can_aggregate());
400
401 let g1 = vec![1.0, 2.0, 3.0];
403 let g2 = vec![2.0, 3.0, 4.0];
404 let g3 = vec![3.0, 4.0, 5.0];
405 let gradients = vec![g1, g2, g3];
406
407 let result = aggregator
408 .aggregate_secure(&gradients)
409 .expect("test: should succeed");
410
411 assert!((result[0] - 2.0).abs() < 0.01);
413 assert!((result[1] - 3.0).abs() < 0.01);
414 assert!((result[2] - 4.0).abs() < 0.01);
415
416 aggregator.reset();
418 assert_eq!(aggregator.participant_count(), 0);
419 }
420
421 #[test]
422 fn test_secure_aggregation_insufficient_participants() {
423 let aggregator = SecureAggregation::new(5);
424
425 let g1 = vec![1.0, 2.0];
426 let g2 = vec![3.0, 4.0];
427 let gradients = vec![g1, g2];
428
429 let result = aggregator.aggregate_secure(&gradients);
431 assert!(result.is_err());
432 }
433
434 #[test]
435 fn test_dp_mechanism_types() {
436 let gaussian = DPMechanism::Gaussian;
437 let laplacian = DPMechanism::Laplacian;
438
439 assert_eq!(gaussian, DPMechanism::Gaussian);
440 assert_eq!(laplacian, DPMechanism::Laplacian);
441 assert_ne!(gaussian, laplacian);
442 }
443
444 #[test]
445 fn test_client_info() {
446 let mut client = ClientInfo::new("client1".to_string(), 1000);
447
448 assert_eq!(client.client_id, "client1");
449 assert_eq!(client.state, ClientState::Idle);
450 assert_eq!(client.sample_count, 1000);
451
452 client.start_training();
453 assert_eq!(client.state, ClientState::Training);
454
455 client.complete_training();
456 assert_eq!(client.state, ClientState::Completed);
457
458 client.mark_failed();
459 assert_eq!(client.state, ClientState::Failed);
460 }
461
462 #[test]
463 fn test_federated_round() {
464 let model_cid = Cid::default();
465 let mut round = FederatedRound::new(0, model_cid, 5);
466
467 assert_eq!(round.round_num, 0);
468 assert_eq!(round.client_count, 5);
469 assert_eq!(round.completed_count, 0);
470 assert!(!round.is_complete());
471
472 for _ in 0..5 {
474 round.mark_client_completed();
475 }
476
477 assert_eq!(round.completed_count, 5);
478 assert!(round.is_complete());
479
480 let gradient = vec![1.0, 2.0, 3.0];
482 round.complete(gradient.clone());
483
484 assert_eq!(round.aggregated_gradient, Some(gradient));
485 assert!(round.end_time.is_some());
486 assert!(round.duration().is_some());
487 }
488
489 #[test]
490 fn test_convergence_detector() {
491 let mut detector = ConvergenceDetector::new(3, 0.01);
492
493 detector.add_loss(1.0);
495 detector.add_loss(0.99);
496 detector.add_loss(0.98);
497
498 assert!(detector.has_converged());
499 assert_eq!(detector.latest_loss(), Some(0.98));
500 assert_eq!(detector.history().len(), 3);
501
502 detector.reset();
504 assert_eq!(detector.history().len(), 0);
505 }
506
507 #[test]
508 fn test_convergence_detector_not_converged() {
509 let mut detector = ConvergenceDetector::new(3, 0.01);
510
511 detector.add_loss(1.0);
513 detector.add_loss(0.5);
514 detector.add_loss(1.5);
515
516 assert!(!detector.has_converged());
517 }
518
519 #[test]
520 fn test_model_sync_protocol() {
521 let mut protocol = ModelSyncProtocol::new(10, 3, 3, 0.01);
522
523 assert_eq!(protocol.current_round(), 0);
524 assert_eq!(protocol.max_rounds(), 10);
525 assert!(protocol.should_continue());
526
527 let model_cid = Cid::default();
529 let round_num = protocol
530 .start_round(model_cid, 5)
531 .expect("test: should succeed");
532
533 assert_eq!(round_num, 0);
534 assert_eq!(protocol.current_round(), 1);
535 assert_eq!(protocol.total_rounds(), 1);
536
537 let gradient = vec![1.0, 2.0, 3.0];
539 protocol
540 .complete_round(round_num, gradient.clone(), 1.0)
541 .expect("test: should succeed");
542
543 assert_eq!(protocol.latest_loss(), Some(1.0));
544
545 let round = protocol.get_round(0).expect("test: should succeed");
547 assert_eq!(round.round_num, 0);
548 assert_eq!(round.aggregated_gradient, Some(gradient));
549 }
550
551 #[test]
552 fn test_model_sync_protocol_convergence() {
553 let mut protocol = ModelSyncProtocol::new(10, 2, 3, 0.01);
554
555 let model_cid = Cid::default();
556
557 for i in 0..3 {
559 protocol
560 .start_round(model_cid, 3)
561 .expect("test: should succeed");
562 let gradient = vec![1.0, 2.0];
563 let loss = 1.0 - (i as f64 * 0.001);
564 protocol
565 .complete_round(i, gradient, loss)
566 .expect("test: should succeed");
567 }
568
569 assert!(protocol.has_converged());
571 assert!(!protocol.should_continue());
572 }
573
574 #[test]
575 fn test_model_sync_protocol_max_rounds() {
576 let mut protocol = ModelSyncProtocol::new(2, 1, 3, 0.01);
577
578 let model_cid = Cid::default();
579
580 protocol
582 .start_round(model_cid, 2)
583 .expect("test: should succeed");
584 protocol
585 .start_round(model_cid, 2)
586 .expect("test: should succeed");
587
588 let result = protocol.start_round(model_cid, 2);
590 assert!(result.is_err());
591 }
592
593 #[test]
594 fn test_model_sync_protocol_min_clients() {
595 let mut protocol = ModelSyncProtocol::new(10, 5, 3, 0.01);
596
597 let model_cid = Cid::default();
598
599 let result = protocol.start_round(model_cid, 3);
601 assert!(result.is_err());
602
603 let result = protocol.start_round(model_cid, 5);
605 assert!(result.is_ok());
606 }
607
608 #[test]
609 fn test_client_state_enum() {
610 let idle = ClientState::Idle;
611 let training = ClientState::Training;
612 let completed = ClientState::Completed;
613 let failed = ClientState::Failed;
614
615 assert_ne!(idle, training);
616 assert_ne!(training, completed);
617 assert_ne!(completed, failed);
618 assert_eq!(idle, ClientState::Idle);
619 }
620}
621
622#[cfg(test)]
625mod distributed_accumulator_tests {
626 use super::*;
627 use ipfrs_storage::traits::BlockStore as _;
628
629 fn make_store() -> std::sync::Arc<ipfrs_storage::MemoryBlockStore> {
634 std::sync::Arc::new(ipfrs_storage::MemoryBlockStore::new())
635 }
636
637 #[tokio::test]
638 async fn test_distributed_accumulator_fedavg() {
639 let store = make_store();
640
641 let config = BackwardPassConfig::default();
642 let mut acc = DistributedGradientAccumulator::new("session-fedavg", config);
643
644 let local_grad = vec![1.0f32, 2.0, 3.0];
646 let _cid = acc
647 .commit_local(local_grad, &*store)
648 .await
649 .expect("commit_local");
650
651 let peer_a_bytes =
653 store_gradient_as_arrow(&[3.0f32, 4.0, 5.0]).expect("peer_a arrow encode");
654 let block_a = ipfrs_core::Block::new(bytes::Bytes::from(peer_a_bytes)).expect("block_a");
655 let cid_a = block_a.cid();
656 store.put(&block_a).await.expect("put block_a");
657
658 acc.add_peer_gradient("peer_a", cid_a, &*store)
659 .await
660 .expect("add_peer_gradient peer_a");
661
662 let peer_b_bytes =
664 store_gradient_as_arrow(&[5.0f32, 6.0, 7.0]).expect("peer_b arrow encode");
665 let block_b = ipfrs_core::Block::new(bytes::Bytes::from(peer_b_bytes)).expect("block_b");
666 let cid_b = block_b.cid();
667 store.put(&block_b).await.expect("put block_b");
668
669 acc.add_peer_gradient("peer_b", cid_b, &*store)
670 .await
671 .expect("add_peer_gradient peer_b");
672
673 assert_eq!(acc.peer_count(), 2, "should have 2 peer gradients");
674
675 let agg = acc.aggregate().expect("aggregate");
677 assert_eq!(agg.len(), 3);
678 assert!((agg[0] - 3.0).abs() < 1e-5, "agg[0] = {}", agg[0]);
679 assert!((agg[1] - 4.0).abs() < 1e-5, "agg[1] = {}", agg[1]);
680 assert!((agg[2] - 5.0).abs() < 1e-5, "agg[2] = {}", agg[2]);
681 }
682
683 #[test]
684 fn test_accumulator_not_ready() {
685 let config = BackwardPassConfig::default();
686 let mut acc = DistributedGradientAccumulator::new("session-not-ready", config);
687
688 acc.peer_gradients
690 .insert("peer_x".to_string(), vec![1.0f32, 2.0]);
691
692 assert!(
694 !acc.is_ready(3),
695 "is_ready(3) must be false with only 1 peer"
696 );
697
698 assert!(acc.is_ready(1), "is_ready(1) must be true with 1 peer");
700 }
701}