Skip to main content

trustformers_optim/
hierarchical_aggregation.rs

1// reason: research-stage module — reserved API/scaffolding fields and methods
2// retained intentionally for in-progress features; not yet on active call paths.
3#![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/// Hierarchical aggregation strategies for distributed training
17///
18/// This module provides advanced hierarchical aggregation algorithms that optimize
19/// communication patterns for different network topologies and cluster configurations.
20/// It supports tree-based, ring-based, and butterfly aggregation patterns.
21///
22/// # Communication
23///
24/// Aggregation is performed over a real [`transport::Transport`] (in-process
25/// shared memory or TCP) through the algorithms in [`collective`]. A
26/// [`HierarchicalAggregator`] built without a transport can only service a
27/// world size of one; any larger configuration returns
28/// [`AggregationError::NoCommunicator`] instead of aggregating against
29/// fabricated data.
30///
31/// Errors raised by hierarchical aggregation.
32#[derive(Debug, thiserror::Error)]
33pub enum AggregationError {
34    /// No transport was attached, so no peer data can be exchanged.
35    #[error(
36        "hierarchical aggregation over world size {world_size} requires a transport; \
37         build the aggregator with HierarchicalAggregator::with_transport"
38    )]
39    NoCommunicator {
40        /// Configured world size.
41        world_size: usize,
42    },
43
44    /// The attached transport disagrees with the configuration.
45    #[error(
46        "transport world size {transport_world_size} does not match the configured world size \
47         {config_world_size}"
48    )]
49    WorldSizeMismatch {
50        /// World size reported by the transport.
51        transport_world_size: usize,
52        /// World size derived from the configuration.
53        config_world_size: usize,
54        /// Rank reported by the transport.
55        transport_rank: usize,
56    },
57
58    /// Butterfly (recursive doubling) requires a power-of-two world size.
59    #[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        /// Configured world size.
65        world_size: usize,
66    },
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum AggregationStrategy {
71    /// Binary tree aggregation (optimal for small clusters)
72    BinaryTree,
73    /// Ring-based aggregation (bandwidth-optimal)
74    Ring,
75    /// Butterfly aggregation (latency-optimal)
76    Butterfly,
77    /// Adaptive strategy that selects best algorithm based on cluster topology
78    Adaptive,
79}
80
81#[derive(Debug, Clone)]
82pub struct HierarchicalConfig {
83    /// Number of nodes in the cluster
84    pub num_nodes: usize,
85    /// Number of devices per node
86    pub devices_per_node: usize,
87    /// Node rank (0-based)
88    pub node_rank: usize,
89    /// Local rank within node
90    pub local_rank: usize,
91    /// Global rank across all nodes
92    pub global_rank: usize,
93    /// Aggregation strategy
94    pub strategy: AggregationStrategy,
95    /// Communication backend
96    pub comm_backend: CommunicationBackend,
97    /// Enable compression during aggregation
98    pub enable_compression: bool,
99    /// Compression threshold (only compress if savings > threshold)
100    pub compression_threshold: f32,
101    /// Enable fault tolerance
102    pub enable_fault_tolerance: bool,
103    /// Timeout for communication operations (ms)
104    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
156/// Hierarchical aggregation coordinator
157pub 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/// Network topology representation
167#[derive(Debug, Clone)]
168pub struct NodeTopology {
169    /// Adjacency matrix for inter-node connectivity
170    pub node_adjacency: Vec<Vec<bool>>,
171    /// Bandwidth matrix between nodes (MB/s)
172    pub node_bandwidth: Vec<Vec<f32>>,
173    /// Latency matrix between nodes (ms)
174    pub node_latency: Vec<Vec<f32>>,
175    /// Intra-node connectivity (assumed full connectivity)
176    pub intra_node_bandwidth: f32,
177    /// Intra-node latency
178    pub intra_node_latency: f32,
179}
180
181/// Communication groups for hierarchical operations
182#[derive(Debug, Clone)]
183pub struct CommunicationGroups {
184    /// Ranks within the same node
185    pub node_local_group: Vec<usize>,
186    /// Node master ranks for cross-node communication
187    pub cross_node_group: Vec<usize>,
188    /// Binary tree structure for tree-based aggregation
189    pub tree_structure: TreeStructure,
190    /// Ring structure for ring-based aggregation
191    pub ring_structure: RingStructure,
192    /// Butterfly structure for butterfly aggregation
193    pub butterfly_structure: ButterflyStructure,
194}
195
196#[derive(Debug, Clone)]
197pub struct TreeStructure {
198    /// Parent rank in the tree (-1 if root)
199    pub parent: Option<usize>,
200    /// Children ranks in the tree
201    pub children: Vec<usize>,
202    /// Tree depth
203    pub depth: usize,
204    /// Tree height
205    pub height: usize,
206}
207
208#[derive(Debug, Clone)]
209pub struct RingStructure {
210    /// Next rank in the ring
211    pub next_rank: usize,
212    /// Previous rank in the ring
213    pub prev_rank: usize,
214    /// Ring size
215    pub ring_size: usize,
216}
217
218#[derive(Debug, Clone)]
219pub struct ButterflyStructure {
220    /// Butterfly connections for each stage
221    pub connections: Vec<Vec<usize>>,
222    /// Number of stages
223    pub num_stages: usize,
224}
225
226/// Aggregation operation statistics
227#[derive(Debug, Clone)]
228pub struct AggregationStats {
229    /// Total number of aggregation operations
230    pub total_operations: usize,
231    /// Average aggregation time (ms)
232    pub avg_aggregation_time: f32,
233    /// Total bytes transferred
234    pub total_bytes_transferred: usize,
235    /// Compression ratio achieved
236    pub compression_ratio: f32,
237    /// Number of failed operations
238    pub failed_operations: usize,
239    /// Strategy selection history
240    pub strategy_history: HashMap<AggregationStrategy, usize>,
241}
242
243/// Fault detection and recovery
244#[derive(Debug)]
245pub struct FaultDetector {
246    /// Failed nodes
247    pub failed_nodes: Vec<usize>,
248    /// Timeout threshold for detecting failures
249    pub timeout_threshold: u64,
250    /// Recovery strategy
251    pub recovery_strategy: RecoveryStrategy,
252}
253
254#[derive(Debug, Clone)]
255pub enum RecoveryStrategy {
256    /// Skip failed nodes and continue
257    Skip,
258    /// Retry with backup nodes
259    Retry,
260    /// Abort aggregation
261    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    /// Build an aggregator bound to a real transport.
304    ///
305    /// The transport's world size must match `num_nodes * devices_per_node`
306    /// and its rank must match `global_rank`.
307    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    /// Attach (or replace) the transport used for aggregation.
317    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    /// Whether a real communicator is attached.
332    pub fn has_communicator(&self) -> bool {
333        self.communicator.is_some()
334    }
335
336    /// Borrow the communicator, or explain why aggregation cannot proceed.
337    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    /// Deterministic, rank-independent parameter ordering.
347    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    /// Detect network topology and measure bandwidth/latency
354    fn detect_network_topology(config: &HierarchicalConfig) -> Result<NodeTopology> {
355        let num_nodes = config.num_nodes;
356
357        // Initialize topology matrices
358        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 this implementation, assume full connectivity with estimated values
363        // In practice, these would be measured through benchmarking
364        for i in 0..num_nodes {
365            for j in 0..num_nodes {
366                if i != j {
367                    node_adjacency[i][j] = true;
368                    // Estimate bandwidth based on network topology
369                    node_bandwidth[i][j] = if (i as i32 - j as i32).abs() == 1 {
370                        10000.0 // Adjacent nodes: 10 GB/s
371                    } else {
372                        1000.0 // Non-adjacent nodes: 1 GB/s
373                    };
374                    // Estimate latency
375                    node_latency[i][j] = if (i as i32 - j as i32).abs() == 1 {
376                        0.1 // Adjacent nodes: 0.1ms
377                    } else {
378                        1.0 // Non-adjacent nodes: 1ms
379                    };
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, // 80 GB/s intra-node
393            intra_node_latency: 0.01,      // 0.01ms intra-node
394        })
395    }
396
397    /// Build communication groups for different aggregation strategies
398    fn build_communication_groups(
399        config: &HierarchicalConfig,
400        topology: &NodeTopology,
401    ) -> Result<CommunicationGroups> {
402        // Node-local group
403        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        // Cross-node group (node masters)
408        let cross_node_group: Vec<usize> =
409            (0..config.num_nodes).map(|i| i * config.devices_per_node).collect();
410
411        // Build tree structure
412        let tree_structure = Self::build_tree_structure(config, topology)?;
413
414        // Build ring structure
415        let ring_structure = Self::build_ring_structure(config)?;
416
417        // Build butterfly structure
418        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    /// Build binary tree structure for tree-based aggregation
430    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        // Build binary tree
438        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        // Calculate depth and height
452        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    /// Build ring structure for ring-based aggregation
464    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    /// Build butterfly structure for butterfly aggregation
479    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            // XOR-based butterfly connections
491            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    /// Perform hierarchical all-reduce operation
506    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        // Select optimal strategy based on configuration and topology
513        let strategy = self.select_optimal_strategy(gradients)?;
514
515        // Perform aggregation based on selected strategy
516        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                // Adaptive strategy selects the best algorithm dynamically
528                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        // Update statistics
543        let elapsed = start_time.elapsed().as_millis() as f32;
544        self.update_aggregation_stats(strategy, elapsed, gradients)?;
545
546        Ok(())
547    }
548
549    /// Select optimal aggregation strategy
550    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    /// Adaptive strategy selection based on cluster topology and data characteristics
561    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        // Calculate total data size
569        let total_data_size: usize = gradients.values().map(|tensor| tensor.memory_usage()).sum();
570
571        // Strategy selection heuristics
572        if world_size <= 8 {
573            // Small clusters: tree is optimal
574            Ok(AggregationStrategy::BinaryTree)
575        } else if total_data_size > 100 * 1024 * 1024 {
576            // Large data: ring is bandwidth-optimal
577            Ok(AggregationStrategy::Ring)
578        } else if num_nodes > 16 && world_size.is_power_of_two() {
579            // Large clusters with small data: butterfly (recursive doubling) is
580            // latency-optimal, but it is only defined for power-of-two sizes.
581            Ok(AggregationStrategy::Butterfly)
582        } else {
583            // Default to tree for medium-sized clusters
584            Ok(AggregationStrategy::BinaryTree)
585        }
586    }
587
588    /// Tree-based all-reduce: a binomial-tree reduce to the root followed by a
589    /// binomial-tree broadcast back out.
590    ///
591    /// Latency scales as `2 * log2(world_size)` messages, which is why it is
592    /// preferred for small clusters and small payloads.
593    fn tree_based_all_reduce(&mut self, gradients: &mut HashMap<String, Tensor>) -> Result<()> {
594        let communicator = self.require_communicator()?;
595        // The binomial tree built by `build_communication_groups` is rooted at
596        // global rank 0 (rank 0 is the only node with `parent == None`).
597        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    /// Ring all-reduce: reduce-scatter around the ring followed by all-gather.
619    ///
620    /// Each rank transmits `2 * (world_size - 1) / world_size` of the payload,
621    /// which is bandwidth-optimal and independent of the world size.
622    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    /// Butterfly (recursive-doubling) all-reduce.
643    ///
644    /// At stage `s` every rank exchanges its full buffer with the partner whose
645    /// rank differs in bit `s` and combines the two, so after `log2(n)` stages
646    /// every rank holds the complete reduction. Requires a power-of-two world
647    /// size; other sizes return
648    /// [`AggregationError::ButterflyRequiresPowerOfTwo`].
649    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    /// Update aggregation statistics
693    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    /// Get current aggregation statistics
715    pub fn get_stats(&self) -> &AggregationStats {
716        &self.aggregation_stats
717    }
718
719    /// Reset aggregation statistics
720    pub fn reset_stats(&mut self) {
721        self.aggregation_stats = AggregationStats::default();
722    }
723
724    /// Get recommended strategy for current configuration
725    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    /// Run `body` on `world_size` ranks in parallel, each with its own
745    /// aggregator bound to a shared in-process transport.
746    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    /// Reference: elementwise sum over all ranks of `gradient_map`.
802    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        // The previous implementation replaced every non-root gradient with a
885        // shape-[1] zero tensor, so this asserts both the shape and the fact
886        // that peers actually contribute.
887        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); // world size 4, not 3
933        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); // Root node
974        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        // Create a large tensor that exceeds 100MB threshold: 8000x8000x4bytes = 256MB
996        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        // Should select ring for large data
1005        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); // log2(8) = 3
1058        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}