1#![allow(dead_code)]
4
5pub mod collective;
6pub mod transport;
7
8use anyhow::Result;
9use collective::{Collective, ReduceOp};
10use std::collections::HashMap;
11use std::sync::Arc;
12use transport::Transport;
13use trustformers_core::parallel::CommunicationBackend;
14use trustformers_core::tensor::Tensor;
15
16#[derive(Debug, thiserror::Error)]
33pub enum AggregationError {
34 #[error(
36 "hierarchical aggregation over world size {world_size} requires a transport; \
37 build the aggregator with HierarchicalAggregator::with_transport"
38 )]
39 NoCommunicator {
40 world_size: usize,
42 },
43
44 #[error(
46 "transport world size {transport_world_size} does not match the configured world size \
47 {config_world_size}"
48 )]
49 WorldSizeMismatch {
50 transport_world_size: usize,
52 config_world_size: usize,
54 transport_rank: usize,
56 },
57
58 #[error(
60 "butterfly aggregation requires a power-of-two world size, got {world_size}; \
61 use AggregationStrategy::Ring or AggregationStrategy::BinaryTree"
62 )]
63 ButterflyRequiresPowerOfTwo {
64 world_size: usize,
66 },
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum AggregationStrategy {
71 BinaryTree,
73 Ring,
75 Butterfly,
77 Adaptive,
79}
80
81#[derive(Debug, Clone)]
82pub struct HierarchicalConfig {
83 pub num_nodes: usize,
85 pub devices_per_node: usize,
87 pub node_rank: usize,
89 pub local_rank: usize,
91 pub global_rank: usize,
93 pub strategy: AggregationStrategy,
95 pub comm_backend: CommunicationBackend,
97 pub enable_compression: bool,
99 pub compression_threshold: f32,
101 pub enable_fault_tolerance: bool,
103 pub comm_timeout_ms: u64,
105}
106
107impl Default for HierarchicalConfig {
108 fn default() -> Self {
109 Self {
110 num_nodes: 1,
111 devices_per_node: 1,
112 node_rank: 0,
113 local_rank: 0,
114 global_rank: 0,
115 strategy: AggregationStrategy::Adaptive,
116 comm_backend: CommunicationBackend::Mpi,
117 enable_compression: true,
118 compression_threshold: 0.1,
119 enable_fault_tolerance: true,
120 comm_timeout_ms: 30000,
121 }
122 }
123}
124
125impl HierarchicalConfig {
126 pub fn new(
127 num_nodes: usize,
128 devices_per_node: usize,
129 node_rank: usize,
130 local_rank: usize,
131 ) -> Self {
132 let global_rank = node_rank * devices_per_node + local_rank;
133 Self {
134 num_nodes,
135 devices_per_node,
136 node_rank,
137 local_rank,
138 global_rank,
139 ..Default::default()
140 }
141 }
142
143 pub fn world_size(&self) -> usize {
144 self.num_nodes * self.devices_per_node
145 }
146
147 pub fn is_master(&self) -> bool {
148 self.global_rank == 0
149 }
150
151 pub fn is_node_master(&self) -> bool {
152 self.local_rank == 0
153 }
154}
155
156pub struct HierarchicalAggregator {
158 config: HierarchicalConfig,
159 node_topology: NodeTopology,
160 communication_groups: CommunicationGroups,
161 aggregation_stats: AggregationStats,
162 fault_detector: Option<FaultDetector>,
163 communicator: Option<Collective<Arc<dyn Transport>>>,
164}
165
166#[derive(Debug, Clone)]
168pub struct NodeTopology {
169 pub node_adjacency: Vec<Vec<bool>>,
171 pub node_bandwidth: Vec<Vec<f32>>,
173 pub node_latency: Vec<Vec<f32>>,
175 pub intra_node_bandwidth: f32,
177 pub intra_node_latency: f32,
179}
180
181#[derive(Debug, Clone)]
183pub struct CommunicationGroups {
184 pub node_local_group: Vec<usize>,
186 pub cross_node_group: Vec<usize>,
188 pub tree_structure: TreeStructure,
190 pub ring_structure: RingStructure,
192 pub butterfly_structure: ButterflyStructure,
194}
195
196#[derive(Debug, Clone)]
197pub struct TreeStructure {
198 pub parent: Option<usize>,
200 pub children: Vec<usize>,
202 pub depth: usize,
204 pub height: usize,
206}
207
208#[derive(Debug, Clone)]
209pub struct RingStructure {
210 pub next_rank: usize,
212 pub prev_rank: usize,
214 pub ring_size: usize,
216}
217
218#[derive(Debug, Clone)]
219pub struct ButterflyStructure {
220 pub connections: Vec<Vec<usize>>,
222 pub num_stages: usize,
224}
225
226#[derive(Debug, Clone)]
228pub struct AggregationStats {
229 pub total_operations: usize,
231 pub avg_aggregation_time: f32,
233 pub total_bytes_transferred: usize,
235 pub compression_ratio: f32,
237 pub failed_operations: usize,
239 pub strategy_history: HashMap<AggregationStrategy, usize>,
241}
242
243#[derive(Debug)]
245pub struct FaultDetector {
246 pub failed_nodes: Vec<usize>,
248 pub timeout_threshold: u64,
250 pub recovery_strategy: RecoveryStrategy,
252}
253
254#[derive(Debug, Clone)]
255pub enum RecoveryStrategy {
256 Skip,
258 Retry,
260 Abort,
262}
263
264impl Default for AggregationStats {
265 fn default() -> Self {
266 Self {
267 total_operations: 0,
268 avg_aggregation_time: 0.0,
269 total_bytes_transferred: 0,
270 compression_ratio: 1.0,
271 failed_operations: 0,
272 strategy_history: HashMap::new(),
273 }
274 }
275}
276
277impl HierarchicalAggregator {
278 pub fn new(config: HierarchicalConfig) -> Result<Self> {
279 let node_topology = Self::detect_network_topology(&config)?;
280 let communication_groups = Self::build_communication_groups(&config, &node_topology)?;
281 let aggregation_stats = AggregationStats::default();
282
283 let fault_detector = if config.enable_fault_tolerance {
284 Some(FaultDetector {
285 failed_nodes: Vec::new(),
286 timeout_threshold: config.comm_timeout_ms,
287 recovery_strategy: RecoveryStrategy::Skip,
288 })
289 } else {
290 None
291 };
292
293 Ok(Self {
294 config,
295 node_topology,
296 communication_groups,
297 aggregation_stats,
298 fault_detector,
299 communicator: None,
300 })
301 }
302
303 pub fn with_transport(
308 config: HierarchicalConfig,
309 transport: Arc<dyn Transport>,
310 ) -> Result<Self> {
311 let mut aggregator = Self::new(config)?;
312 aggregator.attach_transport(transport)?;
313 Ok(aggregator)
314 }
315
316 pub fn attach_transport(&mut self, transport: Arc<dyn Transport>) -> Result<()> {
318 let config_world_size = self.config.world_size();
319 if transport.world_size() != config_world_size {
320 return Err(AggregationError::WorldSizeMismatch {
321 transport_world_size: transport.world_size(),
322 config_world_size,
323 transport_rank: transport.rank(),
324 }
325 .into());
326 }
327 self.communicator = Some(Collective::new(transport)?);
328 Ok(())
329 }
330
331 pub fn has_communicator(&self) -> bool {
333 self.communicator.is_some()
334 }
335
336 fn require_communicator(&self) -> Result<&Collective<Arc<dyn Transport>>> {
338 self.communicator.as_ref().ok_or_else(|| {
339 AggregationError::NoCommunicator {
340 world_size: self.config.world_size(),
341 }
342 .into()
343 })
344 }
345
346 fn ordered_names(gradients: &HashMap<String, Tensor>) -> Vec<String> {
348 let mut names: Vec<String> = gradients.keys().cloned().collect();
349 names.sort();
350 names
351 }
352
353 fn detect_network_topology(config: &HierarchicalConfig) -> Result<NodeTopology> {
355 let num_nodes = config.num_nodes;
356
357 let mut node_adjacency = vec![vec![false; num_nodes]; num_nodes];
359 let mut node_bandwidth = vec![vec![0.0; num_nodes]; num_nodes];
360 let mut node_latency = vec![vec![0.0; num_nodes]; num_nodes];
361
362 for i in 0..num_nodes {
365 for j in 0..num_nodes {
366 if i != j {
367 node_adjacency[i][j] = true;
368 node_bandwidth[i][j] = if (i as i32 - j as i32).abs() == 1 {
370 10000.0 } else {
372 1000.0 };
374 node_latency[i][j] = if (i as i32 - j as i32).abs() == 1 {
376 0.1 } else {
378 1.0 };
380 } else {
381 node_adjacency[i][j] = false;
382 node_bandwidth[i][j] = f32::INFINITY;
383 node_latency[i][j] = 0.0;
384 }
385 }
386 }
387
388 Ok(NodeTopology {
389 node_adjacency,
390 node_bandwidth,
391 node_latency,
392 intra_node_bandwidth: 80000.0, intra_node_latency: 0.01, })
395 }
396
397 fn build_communication_groups(
399 config: &HierarchicalConfig,
400 topology: &NodeTopology,
401 ) -> Result<CommunicationGroups> {
402 let node_local_group: Vec<usize> = (0..config.devices_per_node)
404 .map(|i| config.node_rank * config.devices_per_node + i)
405 .collect();
406
407 let cross_node_group: Vec<usize> =
409 (0..config.num_nodes).map(|i| i * config.devices_per_node).collect();
410
411 let tree_structure = Self::build_tree_structure(config, topology)?;
413
414 let ring_structure = Self::build_ring_structure(config)?;
416
417 let butterfly_structure = Self::build_butterfly_structure(config)?;
419
420 Ok(CommunicationGroups {
421 node_local_group,
422 cross_node_group,
423 tree_structure,
424 ring_structure,
425 butterfly_structure,
426 })
427 }
428
429 fn build_tree_structure(
431 config: &HierarchicalConfig,
432 _topology: &NodeTopology,
433 ) -> Result<TreeStructure> {
434 let world_size = config.world_size();
435 let rank = config.global_rank;
436
437 let parent = if rank == 0 { None } else { Some((rank - 1) / 2) };
439
440 let mut children = Vec::new();
441 let left_child = 2 * rank + 1;
442 let right_child = 2 * rank + 2;
443
444 if left_child < world_size {
445 children.push(left_child);
446 }
447 if right_child < world_size {
448 children.push(right_child);
449 }
450
451 let depth = (rank as f32).log2().floor() as usize;
453 let height = (world_size as f32).log2().ceil() as usize;
454
455 Ok(TreeStructure {
456 parent,
457 children,
458 depth,
459 height,
460 })
461 }
462
463 fn build_ring_structure(config: &HierarchicalConfig) -> Result<RingStructure> {
465 let world_size = config.world_size();
466 let rank = config.global_rank;
467
468 let next_rank = (rank + 1) % world_size;
469 let prev_rank = (rank + world_size - 1) % world_size;
470
471 Ok(RingStructure {
472 next_rank,
473 prev_rank,
474 ring_size: world_size,
475 })
476 }
477
478 fn build_butterfly_structure(config: &HierarchicalConfig) -> Result<ButterflyStructure> {
480 let world_size = config.world_size();
481 let rank = config.global_rank;
482 let num_stages = (world_size as f32).log2().ceil() as usize;
483
484 let mut connections = Vec::new();
485
486 for stage in 0..num_stages {
487 let mut stage_connections = Vec::new();
488 let distance = 1 << stage;
489
490 let partner = rank ^ distance;
492 if partner < world_size {
493 stage_connections.push(partner);
494 }
495
496 connections.push(stage_connections);
497 }
498
499 Ok(ButterflyStructure {
500 connections,
501 num_stages,
502 })
503 }
504
505 pub fn hierarchical_all_reduce(
507 &mut self,
508 gradients: &mut HashMap<String, Tensor>,
509 ) -> Result<()> {
510 let start_time = std::time::Instant::now();
511
512 let strategy = self.select_optimal_strategy(gradients)?;
514
515 match strategy {
517 AggregationStrategy::BinaryTree => {
518 self.tree_based_all_reduce(gradients)?;
519 },
520 AggregationStrategy::Ring => {
521 self.ring_based_all_reduce(gradients)?;
522 },
523 AggregationStrategy::Butterfly => {
524 self.butterfly_based_all_reduce(gradients)?;
525 },
526 AggregationStrategy::Adaptive => {
527 let optimal_strategy = self.adaptive_strategy_selection(gradients)?;
529 match optimal_strategy {
530 AggregationStrategy::BinaryTree => self.tree_based_all_reduce(gradients)?,
531 AggregationStrategy::Ring => self.ring_based_all_reduce(gradients)?,
532 AggregationStrategy::Butterfly => self.butterfly_based_all_reduce(gradients)?,
533 AggregationStrategy::Adaptive => {
534 return Err(anyhow::anyhow!(
535 "Invalid adaptive strategy selection: recursive Adaptive strategy returned"
536 ));
537 },
538 }
539 },
540 }
541
542 let elapsed = start_time.elapsed().as_millis() as f32;
544 self.update_aggregation_stats(strategy, elapsed, gradients)?;
545
546 Ok(())
547 }
548
549 fn select_optimal_strategy(
551 &self,
552 gradients: &HashMap<String, Tensor>,
553 ) -> Result<AggregationStrategy> {
554 match self.config.strategy {
555 AggregationStrategy::Adaptive => self.adaptive_strategy_selection(gradients),
556 strategy => Ok(strategy),
557 }
558 }
559
560 fn adaptive_strategy_selection(
562 &self,
563 gradients: &HashMap<String, Tensor>,
564 ) -> Result<AggregationStrategy> {
565 let world_size = self.config.world_size();
566 let num_nodes = self.config.num_nodes;
567
568 let total_data_size: usize = gradients.values().map(|tensor| tensor.memory_usage()).sum();
570
571 if world_size <= 8 {
573 Ok(AggregationStrategy::BinaryTree)
575 } else if total_data_size > 100 * 1024 * 1024 {
576 Ok(AggregationStrategy::Ring)
578 } else if num_nodes > 16 && world_size.is_power_of_two() {
579 Ok(AggregationStrategy::Butterfly)
582 } else {
583 Ok(AggregationStrategy::BinaryTree)
585 }
586 }
587
588 fn tree_based_all_reduce(&mut self, gradients: &mut HashMap<String, Tensor>) -> Result<()> {
594 let communicator = self.require_communicator()?;
595 const TREE_ROOT: usize = 0;
598 let root = TREE_ROOT;
599
600 for name in Self::ordered_names(gradients) {
601 let Some(gradient) = gradients.get(&name) else {
602 continue;
603 };
604 let shape = gradient.shape();
605 let mut values = gradient.to_vec_f32()?;
606
607 communicator.reduce(&mut values, root, ReduceOp::Sum)?;
608 communicator.broadcast(&mut values, root)?;
609
610 if let Some(slot) = gradients.get_mut(&name) {
611 *slot = Tensor::from_slice(&values, &shape)?;
612 }
613 }
614
615 Ok(())
616 }
617
618 fn ring_based_all_reduce(&mut self, gradients: &mut HashMap<String, Tensor>) -> Result<()> {
623 let communicator = self.require_communicator()?;
624
625 for name in Self::ordered_names(gradients) {
626 let Some(gradient) = gradients.get(&name) else {
627 continue;
628 };
629 let shape = gradient.shape();
630 let mut values = gradient.to_vec_f32()?;
631
632 communicator.all_reduce(&mut values, ReduceOp::Sum)?;
633
634 if let Some(slot) = gradients.get_mut(&name) {
635 *slot = Tensor::from_slice(&values, &shape)?;
636 }
637 }
638
639 Ok(())
640 }
641
642 fn butterfly_based_all_reduce(
650 &mut self,
651 gradients: &mut HashMap<String, Tensor>,
652 ) -> Result<()> {
653 let communicator = self.require_communicator()?;
654 let world_size = communicator.world_size();
655
656 if !world_size.is_power_of_two() {
657 return Err(AggregationError::ButterflyRequiresPowerOfTwo { world_size }.into());
658 }
659 if world_size == 1 {
660 return Ok(());
661 }
662
663 let rank = communicator.rank();
664 let butterfly = self.communication_groups.butterfly_structure.clone();
665
666 for name in Self::ordered_names(gradients) {
667 let Some(gradient) = gradients.get(&name) else {
668 continue;
669 };
670 let shape = gradient.shape();
671 let mut values = gradient.to_vec_f32()?;
672
673 for stage in 0..butterfly.num_stages {
674 let partner = rank ^ (1usize << stage);
675 if partner >= world_size {
676 continue;
677 }
678 let incoming = communicator.exchange(partner, &values)?;
679 for (slot, value) in values.iter_mut().zip(incoming) {
680 *slot += value;
681 }
682 }
683
684 if let Some(slot) = gradients.get_mut(&name) {
685 *slot = Tensor::from_slice(&values, &shape)?;
686 }
687 }
688
689 Ok(())
690 }
691
692 fn update_aggregation_stats(
694 &mut self,
695 strategy: AggregationStrategy,
696 elapsed_ms: f32,
697 gradients: &HashMap<String, Tensor>,
698 ) -> Result<()> {
699 let stats = &mut self.aggregation_stats;
700
701 stats.total_operations += 1;
702 stats.avg_aggregation_time =
703 (stats.avg_aggregation_time * (stats.total_operations - 1) as f32 + elapsed_ms)
704 / stats.total_operations as f32;
705
706 let bytes_transferred: usize = gradients.values().map(|tensor| tensor.memory_usage()).sum();
707 stats.total_bytes_transferred += bytes_transferred;
708
709 *stats.strategy_history.entry(strategy).or_insert(0) += 1;
710
711 Ok(())
712 }
713
714 pub fn get_stats(&self) -> &AggregationStats {
716 &self.aggregation_stats
717 }
718
719 pub fn reset_stats(&mut self) {
721 self.aggregation_stats = AggregationStats::default();
722 }
723
724 pub fn get_recommended_strategy(&self) -> AggregationStrategy {
726 let world_size = self.config.world_size();
727 let num_nodes = self.config.num_nodes;
728
729 if world_size <= 8 {
730 AggregationStrategy::BinaryTree
731 } else if num_nodes > 16 {
732 AggregationStrategy::Butterfly
733 } else {
734 AggregationStrategy::Ring
735 }
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::transport::InProcessSession;
742 use super::*;
743
744 fn spmd_aggregators<R, F>(
747 num_nodes: usize,
748 devices_per_node: usize,
749 strategy: AggregationStrategy,
750 body: F,
751 ) -> Vec<R>
752 where
753 R: Send + 'static,
754 F: Fn(usize, &mut HierarchicalAggregator) -> R + Send + Sync + 'static,
755 {
756 let world_size = num_nodes * devices_per_node;
757 let session = InProcessSession::new(world_size).expect("session must be created in test");
758 let body = std::sync::Arc::new(body);
759
760 let handles: Vec<_> = (0..world_size)
761 .map(|global_rank| {
762 let transport: Arc<dyn Transport> = Arc::new(
763 session.transport(global_rank).expect("rank must be claimable in test"),
764 );
765 let mut config = HierarchicalConfig::new(
766 num_nodes,
767 devices_per_node,
768 global_rank / devices_per_node,
769 global_rank % devices_per_node,
770 );
771 config.strategy = strategy;
772 let body = std::sync::Arc::clone(&body);
773 std::thread::spawn(move || {
774 let mut aggregator = HierarchicalAggregator::with_transport(config, transport)
775 .expect("aggregator must build in test");
776 body(global_rank, &mut aggregator)
777 })
778 })
779 .collect();
780
781 handles
782 .into_iter()
783 .map(|handle| handle.join().expect("rank thread must not panic in test"))
784 .collect()
785 }
786
787 fn gradient_map(rank: usize) -> HashMap<String, Tensor> {
788 let mut gradients = HashMap::new();
789 gradients.insert(
790 "layer.0.weight".to_string(),
791 Tensor::from_slice(&[rank as f32, rank as f32 + 1.0, rank as f32 + 2.0], &[3])
792 .expect("tensor must build in test"),
793 );
794 gradients.insert(
795 "layer.0.bias".to_string(),
796 Tensor::from_slice(&[rank as f32 * 0.5], &[1]).expect("tensor must build in test"),
797 );
798 gradients
799 }
800
801 fn expected_sums(world_size: usize) -> HashMap<String, Vec<f32>> {
803 let mut expected = HashMap::new();
804 expected.insert(
805 "layer.0.weight".to_string(),
806 (0..3)
807 .map(|i| (0..world_size).map(|r| r as f32 + i as f32).sum::<f32>())
808 .collect::<Vec<f32>>(),
809 );
810 expected.insert(
811 "layer.0.bias".to_string(),
812 vec![(0..world_size).map(|r| r as f32 * 0.5).sum::<f32>()],
813 );
814 expected
815 }
816
817 fn assert_matches_reference(
818 results: &[HashMap<String, Tensor>],
819 world_size: usize,
820 label: &str,
821 ) {
822 let expected = expected_sums(world_size);
823 for (rank, gradients) in results.iter().enumerate() {
824 for (name, want) in &expected {
825 let got = gradients
826 .get(name)
827 .unwrap_or_else(|| panic!("{label}: rank {rank} lost `{name}`"))
828 .to_vec_f32()
829 .expect("tensor read must succeed in test");
830 assert_eq!(
831 got.len(),
832 want.len(),
833 "{label}: rank {rank} `{name}` length"
834 );
835 for (got_value, want_value) in got.iter().zip(want) {
836 approx::assert_relative_eq!(got_value, want_value, epsilon = 1e-4);
837 }
838 }
839 }
840 }
841
842 #[test]
843 fn ring_all_reduce_matches_elementwise_sum() {
844 let world_size = 4;
845 let results = spmd_aggregators(2, 2, AggregationStrategy::Ring, |rank, aggregator| {
846 let mut gradients = gradient_map(rank);
847 aggregator
848 .hierarchical_all_reduce(&mut gradients)
849 .expect("all-reduce must succeed in test");
850 gradients
851 });
852 assert_matches_reference(&results, world_size, "ring");
853 }
854
855 #[test]
856 fn tree_all_reduce_matches_elementwise_sum() {
857 let world_size = 4;
858 let results =
859 spmd_aggregators(1, 4, AggregationStrategy::BinaryTree, |rank, aggregator| {
860 let mut gradients = gradient_map(rank);
861 aggregator
862 .hierarchical_all_reduce(&mut gradients)
863 .expect("all-reduce must succeed in test");
864 gradients
865 });
866 assert_matches_reference(&results, world_size, "tree");
867 }
868
869 #[test]
870 fn butterfly_all_reduce_matches_elementwise_sum() {
871 let world_size = 4;
872 let results = spmd_aggregators(2, 2, AggregationStrategy::Butterfly, |rank, aggregator| {
873 let mut gradients = gradient_map(rank);
874 aggregator
875 .hierarchical_all_reduce(&mut gradients)
876 .expect("all-reduce must succeed in test");
877 gradients
878 });
879 assert_matches_reference(&results, world_size, "butterfly");
880 }
881
882 #[test]
883 fn all_reduce_result_depends_on_peer_data() {
884 let results = spmd_aggregators(1, 2, AggregationStrategy::Ring, |rank, aggregator| {
888 let mut gradients = HashMap::new();
889 gradients.insert(
890 "w".to_string(),
891 Tensor::from_slice(&[if rank == 0 { 1.0 } else { 10.0 }; 4], &[4])
892 .expect("tensor must build in test"),
893 );
894 aggregator
895 .hierarchical_all_reduce(&mut gradients)
896 .expect("all-reduce must succeed in test");
897 gradients
898 .get("w")
899 .expect("gradient must survive")
900 .to_vec_f32()
901 .expect("tensor read must succeed in test")
902 });
903
904 for values in &results {
905 assert_eq!(values.len(), 4, "shape must be preserved");
906 assert_eq!(values, &vec![11.0f32; 4]);
907 }
908 }
909
910 #[test]
911 fn aggregation_without_transport_errors_instead_of_faking() {
912 let config = HierarchicalConfig::new(2, 2, 0, 0);
913 let mut aggregator =
914 HierarchicalAggregator::new(config).expect("aggregator must build in test");
915 assert!(!aggregator.has_communicator());
916
917 let mut gradients = gradient_map(0);
918 let err = aggregator
919 .hierarchical_all_reduce(&mut gradients)
920 .expect_err("aggregation without a transport must fail");
921 assert!(matches!(
922 err.downcast_ref::<AggregationError>(),
923 Some(AggregationError::NoCommunicator { .. })
924 ));
925 }
926
927 #[test]
928 fn transport_world_size_must_match_configuration() {
929 let session = InProcessSession::new(3).expect("session must be created in test");
930 let transport: Arc<dyn Transport> =
931 Arc::new(session.transport(0).expect("rank must be claimable in test"));
932 let config = HierarchicalConfig::new(2, 2, 0, 0); let err = match HierarchicalAggregator::with_transport(config, transport) {
934 Ok(_) => panic!("mismatched world size must be rejected"),
935 Err(err) => err,
936 };
937 assert!(matches!(
938 err.downcast_ref::<AggregationError>(),
939 Some(AggregationError::WorldSizeMismatch { .. })
940 ));
941 }
942
943 #[test]
944 fn butterfly_rejects_non_power_of_two_world_size() {
945 let results = spmd_aggregators(1, 3, AggregationStrategy::Butterfly, |rank, aggregator| {
946 let mut gradients = gradient_map(rank);
947 aggregator.hierarchical_all_reduce(&mut gradients).is_err()
948 });
949 assert!(results.iter().all(|failed| *failed));
950 }
951
952 #[test]
953 fn test_hierarchical_config() {
954 let config = HierarchicalConfig::new(4, 8, 2, 3);
955 assert_eq!(config.num_nodes, 4);
956 assert_eq!(config.devices_per_node, 8);
957 assert_eq!(config.node_rank, 2);
958 assert_eq!(config.local_rank, 3);
959 assert_eq!(config.global_rank, 19);
960 assert_eq!(config.world_size(), 32);
961 assert!(!config.is_master());
962 assert!(!config.is_node_master());
963 }
964
965 #[test]
966 fn test_tree_structure_building() {
967 let config = HierarchicalConfig::new(2, 4, 0, 0);
968 let topology = HierarchicalAggregator::detect_network_topology(&config)
969 .expect("Operation failed in test");
970 let tree = HierarchicalAggregator::build_tree_structure(&config, &topology)
971 .expect("Operation failed in test");
972
973 assert_eq!(tree.parent, None); assert_eq!(tree.children, vec![1, 2]);
975 assert_eq!(tree.depth, 0);
976 }
977
978 #[test]
979 fn test_ring_structure_building() {
980 let config = HierarchicalConfig::new(2, 4, 0, 1);
981 let ring = HierarchicalAggregator::build_ring_structure(&config)
982 .expect("Operation failed in test");
983
984 assert_eq!(ring.next_rank, 2);
985 assert_eq!(ring.prev_rank, 0);
986 assert_eq!(ring.ring_size, 8);
987 }
988
989 #[test]
990 fn test_adaptive_strategy_selection() {
991 let config = HierarchicalConfig::new(4, 4, 0, 0);
992 let aggregator = HierarchicalAggregator::new(config).expect("Construction failed");
993
994 let mut gradients = HashMap::new();
995 gradients.insert(
997 "param1".to_string(),
998 Tensor::zeros(&[8000, 8000]).expect("Failed to create tensor"),
999 );
1000
1001 let strategy = aggregator
1002 .adaptive_strategy_selection(&gradients)
1003 .expect("Operation failed in test");
1004 assert!(matches!(strategy, AggregationStrategy::Ring));
1006 }
1007
1008 #[test]
1009 fn test_aggregation_stats_update() {
1010 let config = HierarchicalConfig::new(2, 2, 0, 0);
1011 let mut aggregator = HierarchicalAggregator::new(config).expect("Construction failed");
1012
1013 let mut gradients = HashMap::new();
1014 gradients.insert(
1015 "param1".to_string(),
1016 Tensor::zeros(&[10, 10]).expect("Failed to create tensor"),
1017 );
1018
1019 aggregator
1020 .update_aggregation_stats(AggregationStrategy::BinaryTree, 100.0, &gradients)
1021 .expect("Operation failed in test");
1022
1023 let stats = aggregator.get_stats();
1024 assert_eq!(stats.total_operations, 1);
1025 assert_eq!(stats.avg_aggregation_time, 100.0);
1026 assert_eq!(
1027 stats.strategy_history.get(&AggregationStrategy::BinaryTree),
1028 Some(&1)
1029 );
1030 }
1031
1032 #[test]
1033 fn test_recommended_strategy() {
1034 let small_config = HierarchicalConfig::new(2, 2, 0, 0);
1035 let small_aggregator =
1036 HierarchicalAggregator::new(small_config).expect("Construction failed");
1037 assert!(matches!(
1038 small_aggregator.get_recommended_strategy(),
1039 AggregationStrategy::BinaryTree
1040 ));
1041
1042 let large_config = HierarchicalConfig::new(20, 1, 0, 0);
1043 let large_aggregator =
1044 HierarchicalAggregator::new(large_config).expect("Construction failed");
1045 assert!(matches!(
1046 large_aggregator.get_recommended_strategy(),
1047 AggregationStrategy::Butterfly
1048 ));
1049 }
1050
1051 #[test]
1052 fn test_butterfly_structure() {
1053 let config = HierarchicalConfig::new(1, 8, 0, 0);
1054 let butterfly = HierarchicalAggregator::build_butterfly_structure(&config)
1055 .expect("Operation failed in test");
1056
1057 assert_eq!(butterfly.num_stages, 3); assert_eq!(butterfly.connections.len(), 3);
1059 }
1060
1061 #[test]
1062 fn test_network_topology_detection() {
1063 let config = HierarchicalConfig::new(3, 2, 0, 0);
1064 let topology = HierarchicalAggregator::detect_network_topology(&config)
1065 .expect("Operation failed in test");
1066
1067 assert_eq!(topology.node_adjacency.len(), 3);
1068 assert_eq!(topology.node_bandwidth.len(), 3);
1069 assert_eq!(topology.node_latency.len(), 3);
1070 assert!(topology.intra_node_bandwidth > 0.0);
1071 assert!(topology.intra_node_latency > 0.0);
1072 }
1073}