1use serde::{Deserialize, Serialize};
8
9use super::tensor::GradientAggregator;
10use super::GradientError;
11
12pub fn federated_average(gradients: &[Vec<f32>]) -> Result<Vec<f32>, GradientError> {
20 if gradients.is_empty() {
21 return Err(GradientError::EmptyGradients);
22 }
23 let dim = gradients[0].len();
24 if gradients.iter().any(|g| g.len() != dim) {
25 return Err(GradientError::DimensionMismatch);
26 }
27 let n = gradients.len() as f32;
28 let mut avg = vec![0.0f32; dim];
29 for grad in gradients {
30 for (a, &g) in avg.iter_mut().zip(grad.iter()) {
31 *a += g / n;
32 }
33 }
34 Ok(avg)
35}
36
37pub fn clip_gradient_norm(gradient: &mut [f32], max_norm: f32) {
41 let norm: f32 = gradient.iter().map(|&x| x * x).sum::<f32>().sqrt();
42 if norm > max_norm {
43 let scale = max_norm / norm;
44 for x in gradient.iter_mut() {
45 *x *= scale;
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub enum BackwardStepStatus {
55 Pending,
57 GradientRequested { peer_id: String },
59 GradientReceived { cid: String },
61 Aggregated,
63 Failed { reason: String },
65}
66
67#[derive(Debug)]
71pub struct BackwardPassStep {
72 pub node_id: String,
74 pub op: String,
76 pub peer_contributions: std::collections::HashMap<String, BackwardStepStatus>,
78 pub aggregated_gradient_cid: Option<String>,
80 pub started_at: std::time::Instant,
82}
83
84impl BackwardPassStep {
85 pub fn new(node_id: String, op: String) -> Self {
87 Self {
88 node_id,
89 op,
90 peer_contributions: std::collections::HashMap::new(),
91 aggregated_gradient_cid: None,
92 started_at: std::time::Instant::now(),
93 }
94 }
95
96 pub fn add_peer(&mut self, peer_id: &str) {
98 self.peer_contributions
99 .entry(peer_id.to_string())
100 .or_insert(BackwardStepStatus::Pending);
101 }
102
103 pub fn record_gradient_received(&mut self, peer_id: &str, cid: &str) {
105 self.peer_contributions.insert(
106 peer_id.to_string(),
107 BackwardStepStatus::GradientReceived {
108 cid: cid.to_string(),
109 },
110 );
111 }
112
113 pub fn record_gradient_failed(&mut self, peer_id: &str, reason: &str) {
115 self.peer_contributions.insert(
116 peer_id.to_string(),
117 BackwardStepStatus::Failed {
118 reason: reason.to_string(),
119 },
120 );
121 }
122
123 pub fn is_complete(&self) -> bool {
125 self.peer_contributions.values().all(|s| {
126 matches!(
127 s,
128 BackwardStepStatus::GradientReceived { .. }
129 | BackwardStepStatus::Aggregated
130 | BackwardStepStatus::Failed { .. }
131 )
132 })
133 }
134
135 pub fn ready_to_aggregate(&self) -> bool {
137 !self.peer_contributions.is_empty()
138 && self.peer_contributions.values().all(|s| {
139 matches!(
140 s,
141 BackwardStepStatus::GradientReceived { .. } | BackwardStepStatus::Aggregated
142 )
143 })
144 }
145
146 pub fn received_count(&self) -> usize {
148 self.peer_contributions
149 .values()
150 .filter(|s| {
151 matches!(
152 s,
153 BackwardStepStatus::GradientReceived { .. } | BackwardStepStatus::Aggregated
154 )
155 })
156 .count()
157 }
158
159 pub fn failed_count(&self) -> usize {
161 self.peer_contributions
162 .values()
163 .filter(|s| matches!(s, BackwardStepStatus::Failed { .. }))
164 .count()
165 }
166}
167
168#[derive(Debug, Clone, PartialEq)]
172pub enum AggregationMethod {
173 Sum,
175 Mean,
177 WeightedMean { weights: Vec<f32> },
179 FedAvg,
181}
182
183#[derive(Debug, Clone)]
187pub struct BackwardPassConfig {
188 pub max_peers: usize,
190 pub aggregation: AggregationMethod,
192 pub timeout: std::time::Duration,
194 pub gradient_clipping: Option<f32>,
196}
197
198impl Default for BackwardPassConfig {
199 fn default() -> Self {
200 Self {
201 max_peers: 8,
202 aggregation: AggregationMethod::FedAvg,
203 timeout: std::time::Duration::from_secs(60),
204 gradient_clipping: None,
205 }
206 }
207}
208
209#[derive(Debug, Default)]
213pub struct BackwardPassStats {
214 pub total_steps: usize,
216 pub completed_steps: usize,
218 pub pending_steps: usize,
220 pub failed_steps: usize,
222 pub total_gradient_bytes: usize,
224 pub participating_peers: usize,
226}
227
228pub struct BackwardPassCoordinator {
236 steps: Vec<BackwardPassStep>,
238 participating_peers: std::collections::HashSet<String>,
240 learning_rate: f32,
242 accumulation_buffer: std::collections::HashMap<String, Vec<f32>>,
244 config: BackwardPassConfig,
246}
247
248impl BackwardPassCoordinator {
249 pub fn new(config: BackwardPassConfig) -> Self {
251 Self {
252 steps: Vec::new(),
253 participating_peers: std::collections::HashSet::new(),
254 learning_rate: 0.01,
255 accumulation_buffer: std::collections::HashMap::new(),
256 config,
257 }
258 }
259
260 pub fn with_learning_rate(mut self, lr: f32) -> Self {
262 self.learning_rate = lr;
263 self
264 }
265
266 pub fn schedule_step(&mut self, node_id: &str, op: &str, peers: &[&str]) {
268 let mut step = BackwardPassStep::new(node_id.to_string(), op.to_string());
269 for &peer in peers {
270 step.add_peer(peer);
271 self.participating_peers.insert(peer.to_string());
272 }
273 self.steps.push(step);
274 }
275
276 pub fn receive_gradient(
279 &mut self,
280 node_id: &str,
281 peer_id: &str,
282 gradient_cid: &str,
283 ) -> Result<(), GradientError> {
284 let step = self
285 .steps
286 .iter_mut()
287 .find(|s| s.node_id == node_id)
288 .ok_or_else(|| GradientError::NodeNotFound(node_id.to_string()))?;
289
290 if !step.peer_contributions.contains_key(peer_id) {
291 return Err(GradientError::PeerNotFound(peer_id.to_string()));
292 }
293
294 step.record_gradient_received(peer_id, gradient_cid);
295 Ok(())
296 }
297
298 pub fn aggregate_gradients(
303 &mut self,
304 node_id: &str,
305 gradient_data: Vec<(String, Vec<f32>)>,
306 ) -> Result<Vec<f32>, GradientError> {
307 if gradient_data.is_empty() {
308 return Err(GradientError::EmptyGradients);
309 }
310
311 let dim = gradient_data[0].1.len();
312 if gradient_data.iter().any(|(_, g)| g.len() != dim) {
313 return Err(GradientError::DimensionMismatch);
314 }
315
316 let gradients: Vec<Vec<f32>> = gradient_data.into_iter().map(|(_, g)| g).collect();
317
318 let mut aggregated = match &self.config.aggregation {
319 AggregationMethod::Sum => {
320 let mut sum = vec![0.0f32; dim];
321 for grad in &gradients {
322 for (a, &g) in sum.iter_mut().zip(grad.iter()) {
323 *a += g;
324 }
325 }
326 sum
327 }
328 AggregationMethod::Mean | AggregationMethod::FedAvg => federated_average(&gradients)?,
329 AggregationMethod::WeightedMean { weights } => {
330 let w: Vec<f32> = weights.clone();
331 GradientAggregator::weighted_average(&gradients, &w)?
332 }
333 };
334
335 self.clip_gradients(&mut aggregated);
337
338 if let Some(step) = self.steps.iter_mut().find(|s| s.node_id == node_id) {
340 for status in step.peer_contributions.values_mut() {
341 if matches!(status, BackwardStepStatus::GradientReceived { .. }) {
342 *status = BackwardStepStatus::Aggregated;
343 }
344 }
345 }
346
347 self.accumulation_buffer
348 .insert(node_id.to_string(), aggregated.clone());
349
350 Ok(aggregated)
351 }
352
353 pub fn clip_gradients(&self, gradients: &mut [f32]) {
355 if let Some(max_norm) = self.config.gradient_clipping {
356 clip_gradient_norm(gradients, max_norm);
357 }
358 }
359
360 pub fn apply_gradient(
362 &self,
363 params: &mut [f32],
364 gradient: &[f32],
365 ) -> Result<(), GradientError> {
366 if params.len() != gradient.len() {
367 return Err(GradientError::DimensionMismatch);
368 }
369 for (p, &g) in params.iter_mut().zip(gradient.iter()) {
370 *p -= self.learning_rate * g;
371 }
372 Ok(())
373 }
374
375 pub fn next_ready_step(&self) -> Option<&BackwardPassStep> {
377 self.steps.iter().find(|s| s.ready_to_aggregate())
378 }
379
380 pub fn stats(&self) -> BackwardPassStats {
382 let total_steps = self.steps.len();
383 let completed_steps = self
384 .steps
385 .iter()
386 .filter(|s| {
387 s.peer_contributions
388 .values()
389 .all(|st| matches!(st, BackwardStepStatus::Aggregated))
390 && !s.peer_contributions.is_empty()
391 })
392 .count();
393 let failed_steps = self
394 .steps
395 .iter()
396 .filter(|s| {
397 s.peer_contributions
398 .values()
399 .any(|st| matches!(st, BackwardStepStatus::Failed { .. }))
400 })
401 .count();
402 let pending_steps = total_steps - completed_steps - failed_steps;
403
404 let total_gradient_bytes = self
405 .accumulation_buffer
406 .values()
407 .map(|v| v.len() * std::mem::size_of::<f32>())
408 .sum();
409
410 BackwardPassStats {
411 total_steps,
412 completed_steps,
413 pending_steps,
414 failed_steps,
415 total_gradient_bytes,
416 participating_peers: self.participating_peers.len(),
417 }
418 }
419}
420
421#[cfg(test)]
424mod backward_pass_tests {
425 use super::*;
426 use std::time::Duration;
427
428 fn default_config(method: AggregationMethod) -> BackwardPassConfig {
429 BackwardPassConfig {
430 max_peers: 4,
431 aggregation: method,
432 timeout: Duration::from_secs(30),
433 gradient_clipping: None,
434 }
435 }
436
437 #[test]
440 fn test_schedule_and_receive_gradient() {
441 let config = BackwardPassConfig {
442 max_peers: 3,
443 aggregation: AggregationMethod::Mean,
444 timeout: Duration::from_secs(30),
445 gradient_clipping: None,
446 };
447 let mut coord = BackwardPassCoordinator::new(config);
448 coord.schedule_step("layer1", "matmul", &["peer1", "peer2"]);
449
450 coord
451 .receive_gradient("layer1", "peer1", "cid_abc")
452 .expect("peer1 receive");
453 coord
454 .receive_gradient("layer1", "peer2", "cid_def")
455 .expect("peer2 receive");
456
457 assert!(
458 coord.next_ready_step().is_some(),
459 "step should be ready after both peers reported"
460 );
461 }
462
463 #[test]
464 fn test_receive_gradient_unknown_node() {
465 let mut coord = BackwardPassCoordinator::new(BackwardPassConfig::default());
466 let result = coord.receive_gradient("ghost_layer", "peer1", "cid_x");
467 assert!(matches!(result, Err(GradientError::NodeNotFound(_))));
468 }
469
470 #[test]
471 fn test_receive_gradient_unknown_peer() {
472 let mut coord = BackwardPassCoordinator::new(BackwardPassConfig::default());
473 coord.schedule_step("layer1", "relu", &["peer1"]);
474 let result = coord.receive_gradient("layer1", "peer_unknown", "cid_x");
475 assert!(matches!(result, Err(GradientError::PeerNotFound(_))));
476 }
477
478 #[test]
481 fn test_federated_average() {
482 let g1 = vec![1.0f32, 2.0, 3.0];
483 let g2 = vec![3.0f32, 4.0, 5.0];
484 let avg = federated_average(&[g1, g2]).expect("federated_average");
485 assert!((avg[0] - 2.0).abs() < 1e-6, "avg[0] = {}", avg[0]);
486 assert!((avg[1] - 3.0).abs() < 1e-6, "avg[1] = {}", avg[1]);
487 assert!((avg[2] - 4.0).abs() < 1e-6, "avg[2] = {}", avg[2]);
488 }
489
490 #[test]
491 fn test_federated_average_single() {
492 let g = vec![1.0f32, 2.0, 3.0];
493 let avg = federated_average(std::slice::from_ref(&g)).expect("single gradient average");
494 assert_eq!(avg, g);
495 }
496
497 #[test]
498 fn test_federated_average_empty() {
499 let result = federated_average(&[]);
500 assert!(matches!(result, Err(GradientError::EmptyGradients)));
501 }
502
503 #[test]
504 fn test_federated_average_dimension_mismatch() {
505 let g1 = vec![1.0f32, 2.0];
506 let g2 = vec![1.0f32, 2.0, 3.0];
507 let result = federated_average(&[g1, g2]);
508 assert!(matches!(result, Err(GradientError::DimensionMismatch)));
509 }
510
511 #[test]
514 fn test_gradient_clipping() {
515 let mut g = vec![3.0f32, 4.0]; clip_gradient_norm(&mut g, 1.0);
517 let norm: f32 = g.iter().map(|&x| x * x).sum::<f32>().sqrt();
518 assert!(
519 (norm - 1.0).abs() < 1e-5,
520 "clipped norm should be 1.0, got {norm}"
521 );
522 }
523
524 #[test]
525 fn test_gradient_clipping_no_op_when_within_bound() {
526 let mut g = vec![0.3f32, 0.4]; let original = g.clone();
528 clip_gradient_norm(&mut g, 1.0);
529 assert_eq!(
530 g, original,
531 "gradient must be unchanged when norm < max_norm"
532 );
533 }
534
535 #[test]
538 fn test_apply_gradient_with_lr() {
539 let config = BackwardPassConfig {
540 max_peers: 2,
541 aggregation: AggregationMethod::Mean,
542 timeout: Duration::from_secs(30),
543 gradient_clipping: None,
544 };
545 let coord = BackwardPassCoordinator::new(config).with_learning_rate(0.1);
546 let mut params = vec![1.0f32, 2.0, 3.0];
547 let gradient = vec![0.5f32, 1.0, 1.5];
548 coord
549 .apply_gradient(&mut params, &gradient)
550 .expect("apply_gradient");
551
552 assert!((params[0] - 0.95).abs() < 1e-6, "params[0] = {}", params[0]);
554 assert!((params[1] - 1.90).abs() < 1e-6, "params[1] = {}", params[1]);
555 assert!((params[2] - 2.85).abs() < 1e-6, "params[2] = {}", params[2]);
556 }
557
558 #[test]
559 fn test_apply_gradient_dimension_mismatch() {
560 let coord = BackwardPassCoordinator::new(BackwardPassConfig::default());
561 let mut params = vec![1.0f32, 2.0];
562 let gradient = vec![0.5f32, 1.0, 1.5];
563 let result = coord.apply_gradient(&mut params, &gradient);
564 assert!(matches!(result, Err(GradientError::DimensionMismatch)));
565 }
566
567 #[test]
570 fn test_backward_pass_stats() {
571 let mut coord = BackwardPassCoordinator::new(default_config(AggregationMethod::FedAvg));
572 coord.schedule_step("layer1", "matmul", &["peer1", "peer2"]);
573 coord.schedule_step("layer2", "relu", &["peer1", "peer2"]);
574
575 let stats = coord.stats();
576 assert_eq!(stats.total_steps, 2);
577 assert_eq!(stats.participating_peers, 2);
578 assert_eq!(stats.completed_steps, 0);
579 }
580
581 #[test]
584 fn test_aggregation_methods() {
585 let mut coord = BackwardPassCoordinator::new(default_config(AggregationMethod::Sum));
587 coord.schedule_step("l1", "op", &["p1", "p2"]);
588 coord
589 .receive_gradient("l1", "p1", "cid1")
590 .expect("receive p1");
591 coord
592 .receive_gradient("l1", "p2", "cid2")
593 .expect("receive p2");
594
595 let data = vec![
596 ("p1".to_string(), vec![1.0f32, 2.0]),
597 ("p2".to_string(), vec![3.0f32, 4.0]),
598 ];
599 let agg = coord
600 .aggregate_gradients("l1", data)
601 .expect("aggregate sum");
602 assert!((agg[0] - 4.0).abs() < 1e-6, "sum[0] = {}", agg[0]);
603 assert!((agg[1] - 6.0).abs() < 1e-6, "sum[1] = {}", agg[1]);
604 }
605
606 #[test]
607 fn test_aggregation_weighted_mean() {
608 let config = BackwardPassConfig {
609 max_peers: 2,
610 aggregation: AggregationMethod::WeightedMean {
611 weights: vec![1.0, 3.0],
612 },
613 timeout: Duration::from_secs(30),
614 gradient_clipping: None,
615 };
616 let mut coord = BackwardPassCoordinator::new(config);
617 coord.schedule_step("l1", "op", &["p1", "p2"]);
618 coord.receive_gradient("l1", "p1", "c1").expect("p1");
619 coord.receive_gradient("l1", "p2", "c2").expect("p2");
620
621 let data = vec![
622 ("p1".to_string(), vec![0.0f32]),
623 ("p2".to_string(), vec![4.0f32]),
624 ];
625 let agg = coord
627 .aggregate_gradients("l1", data)
628 .expect("weighted mean");
629 assert!(
630 (agg[0] - 3.0).abs() < 1e-5,
631 "weighted mean = {}, expected 3.0",
632 agg[0]
633 );
634 }
635
636 #[test]
637 fn test_aggregation_with_clipping() {
638 let config = BackwardPassConfig {
639 max_peers: 1,
640 aggregation: AggregationMethod::Mean,
641 timeout: Duration::from_secs(30),
642 gradient_clipping: Some(1.0),
643 };
644 let mut coord = BackwardPassCoordinator::new(config);
645 coord.schedule_step("l1", "op", &["p1"]);
646 coord.receive_gradient("l1", "p1", "c1").expect("p1");
647
648 let data = vec![("p1".to_string(), vec![3.0f32, 4.0])]; let agg = coord.aggregate_gradients("l1", data).expect("aggregate");
650 let norm: f32 = agg.iter().map(|&x| x * x).sum::<f32>().sqrt();
651 assert!((norm - 1.0).abs() < 1e-5, "clipped norm = {norm}");
652 }
653
654 #[test]
657 fn test_step_completion_tracking() {
658 let mut step = BackwardPassStep::new("layer1".to_string(), "matmul".to_string());
659 step.add_peer("p1");
660 step.add_peer("p2");
661 step.add_peer("p3");
662
663 assert!(!step.is_complete(), "not complete yet");
664 assert_eq!(step.received_count(), 0);
665 assert_eq!(step.failed_count(), 0);
666
667 step.record_gradient_received("p1", "cid1");
668 step.record_gradient_received("p2", "cid2");
669 assert!(!step.is_complete(), "still waiting for p3");
670 assert_eq!(step.received_count(), 2);
671
672 step.record_gradient_failed("p3", "timeout");
673 assert!(step.is_complete(), "complete after failure");
674 assert_eq!(step.failed_count(), 1);
675 assert!(
676 !step.ready_to_aggregate(),
677 "not ready_to_aggregate with failure"
678 );
679 }
680
681 #[test]
682 fn test_step_ready_to_aggregate_all_received() {
683 let mut step = BackwardPassStep::new("l".to_string(), "op".to_string());
684 step.add_peer("p1");
685 step.add_peer("p2");
686
687 assert!(!step.ready_to_aggregate());
688
689 step.record_gradient_received("p1", "c1");
690 step.record_gradient_received("p2", "c2");
691 assert!(step.ready_to_aggregate());
692 }
693}