Skip to main content

ruccl/rank/
topology.rs

1//! Topology-aware collective planning shared by the TCP prototype and future
2//! peer-memory/GX-Link transports.
3
4use super::{CollectiveAlgorithm, CollectiveTransport};
5use serde::{Deserialize, Serialize};
6use std::env;
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::fs;
10use std::path::Path;
11
12const DEFAULT_RING_THRESHOLD_BYTES: usize = 256 * 1024;
13const DEFAULT_BANDWIDTH_MBPS: u64 = 32_000;
14const DEFAULT_LATENCY_NS: u64 = 1_000;
15const DEFAULT_HIERARCHY_INFERENCE_BYTES: usize = 16 * 1024 * 1024;
16const EXACT_RING_SEARCH_MAX_RANKS: u32 = 9;
17const MAX_RING_CHANNELS: usize = 64;
18const MAX_P2P_RAILS: usize = 64;
19pub const COLLECTIVE_AUTOTUNE_PROFILE_VERSION: u32 = 2;
20const COLLECTIVE_EXECUTION_REVISION: u64 = 6;
21
22const fn default_ring_channels() -> usize {
23    1
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum AlgorithmPolicy {
28    Auto,
29    Direct,
30    Ring,
31    Hierarchical,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum CollectiveKind {
36    #[serde(rename = "allreduce")]
37    AllReduce,
38    #[serde(rename = "allgather")]
39    AllGather,
40    #[serde(rename = "reduce_scatter")]
41    ReduceScatter,
42    #[serde(rename = "alltoall")]
43    AllToAll,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum CollectivePlanSource {
48    Policy,
49    Autotune,
50    Model,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct CollectivePlan {
55    pub algorithm: CollectiveAlgorithm,
56    pub source: CollectivePlanSource,
57    pub estimated_time_ns: u64,
58    pub direct_estimated_time_ns: u64,
59    pub peer_estimated_time_ns: Option<u64>,
60    pub hierarchical_estimated_time_ns: Option<u64>,
61    pub ring_channels: usize,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct CollectiveAutotuneProfile {
66    pub version: u32,
67    pub world_size: u32,
68    pub topology_fingerprint: String,
69    #[serde(default)]
70    pub execution_fingerprint: String,
71    pub entries: Vec<CollectiveAutotuneEntry>,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75pub struct CollectiveAutotuneEntry {
76    pub operation: CollectiveKind,
77    pub min_payload_bytes: u64,
78    pub max_payload_bytes: u64,
79    pub algorithm: CollectiveAlgorithm,
80    #[serde(default = "default_ring_channels")]
81    pub ring_channels: usize,
82    pub measured_time_ns: u64,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86pub struct TopologyLink {
87    pub first_rank: u32,
88    pub second_rank: u32,
89    pub bandwidth_mbps: u64,
90    pub latency_ns: u64,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94pub struct TopologyRailLink {
95    pub rail: usize,
96    pub first_rank: u32,
97    pub second_rank: u32,
98    pub bandwidth_mbps: u64,
99    pub latency_ns: u64,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
103pub struct TopologyAggregateLink {
104    pub first_rank: u32,
105    pub second_rank: u32,
106    pub bandwidth_mbps: u64,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct CollectiveTopology {
111    world_size: u32,
112    links: Vec<Vec<Option<LinkCost>>>,
113    rail_links: Vec<Vec<Vec<Option<LinkCost>>>>,
114    aggregate_links: Vec<Vec<Option<u64>>>,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118struct LinkCost {
119    bandwidth_mbps: u64,
120    latency_ns: u64,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct CollectiveTuning {
125    pub algorithm_policy: AlgorithmPolicy,
126    pub ring_threshold_bytes: usize,
127    pub ring_order: Vec<u32>,
128    ring_orders: Vec<Vec<u32>>,
129    ring_channels: usize,
130    p2p_rails: usize,
131    rail_order_override: Option<Vec<usize>>,
132    hierarchy_groups: Option<Vec<Vec<u32>>>,
133    topology: CollectiveTopology,
134    direct_bandwidth_mbps: u64,
135    direct_latency_ns: u64,
136    transport: CollectiveTransport,
137    autotune_profile: Option<CollectiveAutotuneProfile>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum TopologyError {
142    EmptyWorld,
143    RankOutOfRange {
144        rank: u32,
145        world_size: u32,
146    },
147    SelfLink(u32),
148    ZeroBandwidth {
149        first_rank: u32,
150        second_rank: u32,
151    },
152    ZeroDirectBandwidth,
153    ProfileRead {
154        path: String,
155        message: String,
156    },
157    InvalidProfile(String),
158    InvalidRing(String),
159    InvalidRingChannels(usize),
160    InvalidP2pRails(usize),
161    InvalidRailOrder(String),
162    P2pRailMismatch {
163        tuning: usize,
164        session: usize,
165    },
166    TransportMismatch {
167        tuning: CollectiveTransport,
168        communicator: CollectiveTransport,
169    },
170    InvalidHierarchy(String),
171    InvalidEnvironment {
172        name: &'static str,
173        value: String,
174    },
175}
176
177impl Display for TopologyError {
178    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
179        match self {
180            Self::EmptyWorld => formatter.write_str("collective topology cannot be empty"),
181            Self::RankOutOfRange { rank, world_size } => {
182                write!(
183                    formatter,
184                    "topology rank {rank} is outside world size {world_size}"
185                )
186            }
187            Self::SelfLink(rank) => write!(formatter, "topology rank {rank} links to itself"),
188            Self::ZeroBandwidth {
189                first_rank,
190                second_rank,
191            } => write!(
192                formatter,
193                "topology link {first_rank}-{second_rank} has zero bandwidth"
194            ),
195            Self::ZeroDirectBandwidth => {
196                formatter.write_str("direct collective transport has zero bandwidth")
197            }
198            Self::ProfileRead { path, message } => {
199                write!(
200                    formatter,
201                    "cannot read collective autotune profile {path:?}: {message}"
202                )
203            }
204            Self::InvalidProfile(message) => {
205                write!(formatter, "invalid collective autotune profile: {message}")
206            }
207            Self::InvalidRing(message) => write!(formatter, "invalid collective ring: {message}"),
208            Self::InvalidRingChannels(channels) => write!(
209                formatter,
210                "invalid collective ring channel count {channels}; expected 1..={MAX_RING_CHANNELS}"
211            ),
212            Self::InvalidP2pRails(rails) => write!(
213                formatter,
214                "invalid point-to-point rail count {rails}; expected 1..={MAX_P2P_RAILS}"
215            ),
216            Self::InvalidRailOrder(message) => {
217                write!(formatter, "invalid collective rail order: {message}")
218            }
219            Self::P2pRailMismatch { tuning, session } => write!(
220                formatter,
221                "collective tuning declares {tuning} point-to-point rails, session has {session}"
222            ),
223            Self::TransportMismatch {
224                tuning,
225                communicator,
226            } => write!(
227                formatter,
228                "collective tuning targets {tuning:?}, communicator uses {communicator:?}"
229            ),
230            Self::InvalidHierarchy(message) => {
231                write!(formatter, "invalid collective hierarchy: {message}")
232            }
233            Self::InvalidEnvironment { name, value } => {
234                write!(formatter, "invalid {name} value {value:?}")
235            }
236        }
237    }
238}
239
240impl Error for TopologyError {}
241
242impl CollectiveTopology {
243    pub fn uniform(world_size: u32) -> Result<Self, TopologyError> {
244        if world_size == 0 {
245            return Err(TopologyError::EmptyWorld);
246        }
247        let mut topology = Self::empty(world_size)?;
248        for first in 0..world_size {
249            for second in first + 1..world_size {
250                topology.add_link(TopologyLink {
251                    first_rank: first,
252                    second_rank: second,
253                    bandwidth_mbps: DEFAULT_BANDWIDTH_MBPS,
254                    latency_ns: DEFAULT_LATENCY_NS,
255                })?;
256            }
257        }
258        Ok(topology)
259    }
260
261    pub fn empty(world_size: u32) -> Result<Self, TopologyError> {
262        if world_size == 0 {
263            return Err(TopologyError::EmptyWorld);
264        }
265        Ok(Self {
266            world_size,
267            links: vec![vec![None; world_size as usize]; world_size as usize],
268            rail_links: Vec::new(),
269            aggregate_links: vec![vec![None; world_size as usize]; world_size as usize],
270        })
271    }
272
273    pub const fn world_size(&self) -> u32 {
274        self.world_size
275    }
276
277    pub fn add_link(&mut self, link: TopologyLink) -> Result<(), TopologyError> {
278        self.validate_rank(link.first_rank)?;
279        self.validate_rank(link.second_rank)?;
280        if link.first_rank == link.second_rank {
281            return Err(TopologyError::SelfLink(link.first_rank));
282        }
283        if link.bandwidth_mbps == 0 {
284            return Err(TopologyError::ZeroBandwidth {
285                first_rank: link.first_rank,
286                second_rank: link.second_rank,
287            });
288        }
289        let cost = Some(LinkCost {
290            bandwidth_mbps: link.bandwidth_mbps,
291            latency_ns: link.latency_ns,
292        });
293        self.links[link.first_rank as usize][link.second_rank as usize] = cost;
294        self.links[link.second_rank as usize][link.first_rank as usize] = cost;
295        Ok(())
296    }
297
298    pub fn links(&self) -> Vec<TopologyLink> {
299        let mut links = Vec::new();
300        for first_rank in 0..self.world_size {
301            for second_rank in first_rank + 1..self.world_size {
302                if let Some(link) = self.link(first_rank, second_rank) {
303                    links.push(TopologyLink {
304                        first_rank,
305                        second_rank,
306                        bandwidth_mbps: link.bandwidth_mbps,
307                        latency_ns: link.latency_ns,
308                    });
309                }
310            }
311        }
312        links
313    }
314
315    pub fn add_rail_link(&mut self, link: TopologyRailLink) -> Result<(), TopologyError> {
316        validate_p2p_rails(link.rail.saturating_add(1))?;
317        self.validate_rank(link.first_rank)?;
318        self.validate_rank(link.second_rank)?;
319        if link.first_rank == link.second_rank {
320            return Err(TopologyError::SelfLink(link.first_rank));
321        }
322        if link.bandwidth_mbps == 0 {
323            return Err(TopologyError::ZeroBandwidth {
324                first_rank: link.first_rank,
325                second_rank: link.second_rank,
326            });
327        }
328        while self.rail_links.len() <= link.rail {
329            self.rail_links.push(vec![
330                vec![None; self.world_size as usize];
331                self.world_size as usize
332            ]);
333        }
334        let cost = Some(LinkCost {
335            bandwidth_mbps: link.bandwidth_mbps,
336            latency_ns: link.latency_ns,
337        });
338        self.rail_links[link.rail][link.first_rank as usize][link.second_rank as usize] = cost;
339        self.rail_links[link.rail][link.second_rank as usize][link.first_rank as usize] = cost;
340        Ok(())
341    }
342
343    pub fn rail_links(&self) -> Vec<TopologyRailLink> {
344        let mut links = Vec::new();
345        for (rail, rail_links) in self.rail_links.iter().enumerate() {
346            for first_rank in 0..self.world_size {
347                for second_rank in first_rank + 1..self.world_size {
348                    if let Some(link) = rail_links[first_rank as usize][second_rank as usize] {
349                        links.push(TopologyRailLink {
350                            rail,
351                            first_rank,
352                            second_rank,
353                            bandwidth_mbps: link.bandwidth_mbps,
354                            latency_ns: link.latency_ns,
355                        });
356                    }
357                }
358            }
359        }
360        links
361    }
362
363    pub fn add_aggregate_link(&mut self, link: TopologyAggregateLink) -> Result<(), TopologyError> {
364        self.validate_rank(link.first_rank)?;
365        self.validate_rank(link.second_rank)?;
366        if link.first_rank == link.second_rank {
367            return Err(TopologyError::SelfLink(link.first_rank));
368        }
369        if link.bandwidth_mbps == 0 {
370            return Err(TopologyError::ZeroBandwidth {
371                first_rank: link.first_rank,
372                second_rank: link.second_rank,
373            });
374        }
375        self.aggregate_links[link.first_rank as usize][link.second_rank as usize] =
376            Some(link.bandwidth_mbps);
377        self.aggregate_links[link.second_rank as usize][link.first_rank as usize] =
378            Some(link.bandwidth_mbps);
379        Ok(())
380    }
381
382    pub fn aggregate_links(&self) -> Vec<TopologyAggregateLink> {
383        let mut links = Vec::new();
384        for first_rank in 0..self.world_size {
385            for second_rank in first_rank + 1..self.world_size {
386                if let Some(bandwidth_mbps) =
387                    self.aggregate_links[first_rank as usize][second_rank as usize]
388                {
389                    links.push(TopologyAggregateLink {
390                        first_rank,
391                        second_rank,
392                        bandwidth_mbps,
393                    });
394                }
395            }
396        }
397        links
398    }
399
400    pub fn best_ring_order(&self) -> Result<Vec<u32>, TopologyError> {
401        Ok(self.best_ring_orders(1)?.remove(0))
402    }
403
404    pub fn best_ring_orders(&self, maximum: usize) -> Result<Vec<Vec<u32>>, TopologyError> {
405        if maximum == 0 {
406            return Ok(Vec::new());
407        }
408        if self.world_size == 1 {
409            return Ok(vec![vec![0]]);
410        }
411        if self.world_size <= EXACT_RING_SEARCH_MAX_RANKS {
412            let mut order = Vec::with_capacity(self.world_size as usize);
413            let mut used = vec![false; self.world_size as usize];
414            let mut candidates = Vec::new();
415            order.push(0);
416            used[0] = true;
417            collect_rings(self, &mut order, &mut used, &mut candidates)?;
418            return select_diverse_rings(self, candidates, maximum);
419        }
420        let mut candidates = Vec::<Vec<u32>>::new();
421        for start in 0..self.world_size {
422            let mut order = Vec::with_capacity(self.world_size as usize);
423            let mut used = vec![false; self.world_size as usize];
424            order.push(start);
425            used[start as usize] = true;
426            while order.len() < self.world_size as usize {
427                let current = *order.last().expect("ring always has a current rank");
428                let next = (0..self.world_size)
429                    .filter(|rank| !used[*rank as usize])
430                    .filter_map(|rank| {
431                        self.link(current, rank)
432                            .map(|cost| (rank, cost.bandwidth_mbps, cost.latency_ns))
433                    })
434                    .max_by_key(|(rank, bandwidth, latency)| {
435                        (*bandwidth, u64::MAX - *latency, u32::MAX - *rank)
436                    })
437                    .map(|(rank, _, _)| rank);
438                let Some(next) = next else {
439                    order.clear();
440                    break;
441                };
442                used[next as usize] = true;
443                order.push(next);
444            }
445            if order.len() != self.world_size as usize {
446                continue;
447            }
448            if self.link(*order.last().unwrap(), order[0]).is_none() {
449                continue;
450            }
451            rotate_ring_to_zero(&mut order);
452            if !candidates.contains(&order) {
453                candidates.push(order);
454            }
455        }
456        select_diverse_rings(self, candidates, maximum)
457    }
458
459    fn ring_score(&self, order: &[u32]) -> Result<RingScore, TopologyError> {
460        validate_ring_order(self.world_size, order)?;
461        let mut bottleneck = u64::MAX;
462        let mut total_bandwidth = 0_u128;
463        let mut total_latency = 0_u128;
464        for index in 0..order.len() {
465            let first = order[index];
466            let second = order[(index + 1) % order.len()];
467            let link = self.link(first, second).ok_or_else(|| {
468                TopologyError::InvalidRing(format!("ring edge {first}-{second} is not connected"))
469            })?;
470            bottleneck = bottleneck.min(link.bandwidth_mbps);
471            total_bandwidth += u128::from(link.bandwidth_mbps);
472            total_latency += u128::from(link.latency_ns);
473        }
474        Ok(RingScore {
475            bottleneck,
476            total_bandwidth,
477            inverse_latency: u128::MAX - total_latency,
478        })
479    }
480
481    fn link(&self, first: u32, second: u32) -> Option<LinkCost> {
482        self.links[first as usize][second as usize]
483    }
484
485    fn link_on_rail(&self, first: u32, second: u32, rail: usize) -> Option<LinkCost> {
486        self.rail_links
487            .get(rail)
488            .and_then(|links| links[first as usize][second as usize])
489            .or_else(|| self.link(first, second))
490    }
491
492    fn aggregate_bandwidth_mbps(&self, first: u32, second: u32) -> Option<u64> {
493        self.aggregate_links[first as usize][second as usize]
494    }
495
496    fn aggregate_transfer_cap_ns(&self, bytes_by_pair: &[Vec<usize>]) -> u64 {
497        let mut slowest = 0_u64;
498        for (first, forward) in bytes_by_pair
499            .iter()
500            .enumerate()
501            .take(self.world_size as usize)
502        {
503            for (second, reverse) in bytes_by_pair
504                .iter()
505                .enumerate()
506                .take(self.world_size as usize)
507                .skip(first + 1)
508            {
509                let bytes = forward[second].max(reverse[first]);
510                if bytes == 0 {
511                    continue;
512                }
513                if let Some(bandwidth_mbps) =
514                    self.aggregate_bandwidth_mbps(first as u32, second as u32)
515                {
516                    slowest = slowest.max(transfer_time_ns(bytes, bandwidth_mbps));
517                }
518            }
519        }
520        slowest
521    }
522
523    fn preferred_rail_order(&self, rail_count: usize) -> Vec<usize> {
524        let mut scores = (0..rail_count)
525            .map(|rail| (rail, self.rail_score(rail)))
526            .collect::<Vec<_>>();
527        scores.sort_by(|(first_rail, first_score), (second_rail, second_score)| {
528            second_score
529                .cmp(first_score)
530                .then_with(|| first_rail.cmp(second_rail))
531        });
532        scores.into_iter().map(|(rail, _)| rail).collect()
533    }
534
535    fn rail_score(&self, rail: usize) -> RingScore {
536        let mut bottleneck = u64::MAX;
537        let mut total_bandwidth = 0_u128;
538        let mut total_latency = 0_u128;
539        let mut link_count = 0_usize;
540        for first in 0..self.world_size {
541            for second in first + 1..self.world_size {
542                let Some(link) = self.link_on_rail(first, second, rail) else {
543                    continue;
544                };
545                bottleneck = bottleneck.min(link.bandwidth_mbps);
546                total_bandwidth = total_bandwidth.saturating_add(u128::from(link.bandwidth_mbps));
547                total_latency = total_latency.saturating_add(u128::from(link.latency_ns));
548                link_count += 1;
549            }
550        }
551        if link_count == 0 {
552            bottleneck = 0;
553        }
554        RingScore {
555            bottleneck,
556            total_bandwidth,
557            inverse_latency: u128::MAX - total_latency,
558        }
559    }
560
561    fn ring_step_time_ns_on_rail(&self, order: &[u32], bytes: usize, rail: usize) -> Option<u64> {
562        (0..order.len())
563            .map(|index| {
564                let first = order[index];
565                let second = order[(index + 1) % order.len()];
566                self.link_on_rail(first, second, rail)
567                    .map(|link| link_transfer_time_ns(link, bytes))
568            })
569            .collect::<Option<Vec<_>>>()?
570            .into_iter()
571            .max()
572    }
573
574    fn pairwise_time_ns(
575        &self,
576        order: &[u32],
577        bytes_per_peer: usize,
578        channel_count: usize,
579        rail_order: &[usize],
580    ) -> Option<u64> {
581        if order.len() <= 1 {
582            return Some(0);
583        }
584        if rail_order.is_empty() {
585            return None;
586        }
587        let channels = channel_count.min(bytes_per_peer.max(1)).max(1);
588        let rails = rail_order.len().min(channels).max(1);
589        let channel_bytes = balanced_sizes(bytes_per_peer, channels);
590        let mut total = 0_u64;
591        for step in 1..order.len() {
592            let mut rail_times = vec![0_u64; rails];
593            let mut aggregate_bytes =
594                vec![vec![0_usize; self.world_size as usize]; self.world_size as usize];
595            for (channel, bytes) in channel_bytes.iter().copied().enumerate() {
596                let rail_slot = channel % rails;
597                let rail = rail_order[rail_slot];
598                let mut slowest = 0_u64;
599                for position in 0..order.len() {
600                    let source = order[position];
601                    let destination = order[(position + step) % order.len()];
602                    aggregate_bytes[source as usize][destination as usize] = aggregate_bytes
603                        [source as usize][destination as usize]
604                        .saturating_add(bytes);
605                    slowest = slowest.max(self.shortest_transfer_time_ns_on_rail(
606                        source,
607                        destination,
608                        bytes,
609                        rail,
610                    )?);
611                }
612                rail_times[rail_slot] = rail_times[rail_slot].saturating_add(slowest);
613            }
614            let rail_time = rail_times.into_iter().max().unwrap_or(0);
615            let aggregate_time = if rails > 1 {
616                self.aggregate_transfer_cap_ns(&aggregate_bytes)
617            } else {
618                0
619            };
620            total = total.saturating_add(rail_time.max(aggregate_time));
621        }
622        Some(total)
623    }
624
625    fn shortest_transfer_time_ns(
626        &self,
627        source: u32,
628        destination: u32,
629        bytes: usize,
630    ) -> Option<u64> {
631        self.shortest_transfer_time_ns_for_rail(source, destination, bytes, None)
632    }
633
634    fn shortest_transfer_time_ns_on_rail(
635        &self,
636        source: u32,
637        destination: u32,
638        bytes: usize,
639        rail: usize,
640    ) -> Option<u64> {
641        self.shortest_transfer_time_ns_for_rail(source, destination, bytes, Some(rail))
642    }
643
644    fn shortest_transfer_time_ns_for_rail(
645        &self,
646        source: u32,
647        destination: u32,
648        bytes: usize,
649        rail: Option<usize>,
650    ) -> Option<u64> {
651        if source == destination {
652            return Some(0);
653        }
654        let rank_count = self.world_size as usize;
655        let mut distances = vec![u64::MAX; rank_count];
656        let mut visited = vec![false; rank_count];
657        distances[source as usize] = 0;
658        for _ in 0..rank_count {
659            let current = (0..rank_count)
660                .filter(|rank| !visited[*rank])
661                .min_by_key(|rank| distances[*rank])?;
662            if distances[current] == u64::MAX {
663                break;
664            }
665            if current == destination as usize {
666                return Some(distances[current]);
667            }
668            visited[current] = true;
669            for next in 0..rank_count {
670                if visited[next] {
671                    continue;
672                }
673                let link = match rail {
674                    Some(rail) => self.link_on_rail(current as u32, next as u32, rail),
675                    None => self.links[current][next],
676                };
677                let Some(link) = link else {
678                    continue;
679                };
680                let candidate =
681                    distances[current].saturating_add(link_transfer_time_ns(link, bytes));
682                distances[next] = distances[next].min(candidate);
683            }
684        }
685        (distances[destination as usize] != u64::MAX).then_some(distances[destination as usize])
686    }
687
688    fn validate_rank(&self, rank: u32) -> Result<(), TopologyError> {
689        if rank >= self.world_size {
690            return Err(TopologyError::RankOutOfRange {
691                rank,
692                world_size: self.world_size,
693            });
694        }
695        Ok(())
696    }
697
698    fn hierarchy_candidates(&self, payload_bytes: usize) -> Vec<Vec<Vec<u32>>> {
699        let mut thresholds = Vec::new();
700        for first in 0..self.world_size {
701            for second in first + 1..self.world_size {
702                if let Some(link) = self.link(first, second) {
703                    thresholds.push(link_transfer_time_ns(link, payload_bytes));
704                }
705            }
706        }
707        thresholds.sort_unstable();
708        thresholds.dedup();
709
710        let mut candidates = Vec::new();
711        for threshold in thresholds {
712            let mut visited = vec![false; self.world_size as usize];
713            let mut groups = Vec::new();
714            for first_rank in 0..self.world_size {
715                if visited[first_rank as usize] {
716                    continue;
717                }
718                visited[first_rank as usize] = true;
719                let mut pending = vec![first_rank];
720                let mut group = Vec::new();
721                while let Some(rank) = pending.pop() {
722                    group.push(rank);
723                    for peer in 0..self.world_size {
724                        if visited[peer as usize] {
725                            continue;
726                        }
727                        let fast = self.link(rank, peer).is_some_and(|link| {
728                            link_transfer_time_ns(link, payload_bytes) <= threshold
729                        });
730                        if fast {
731                            visited[peer as usize] = true;
732                            pending.push(peer);
733                        }
734                    }
735                }
736                group.sort_unstable();
737                groups.push(group);
738            }
739            groups.sort_unstable_by_key(|group| group[0]);
740            if groups.len() < 2
741                || groups.len() == self.world_size as usize
742                || candidates.contains(&groups)
743            {
744                continue;
745            }
746            if let Some(groups) = self.optimize_hierarchy(groups, payload_bytes)
747                && !candidates.contains(&groups)
748            {
749                candidates.push(groups);
750            }
751        }
752        candidates
753    }
754
755    fn optimize_hierarchy(
756        &self,
757        mut groups: Vec<Vec<u32>>,
758        payload_bytes: usize,
759    ) -> Option<Vec<Vec<u32>>> {
760        for group in &mut groups {
761            let leader = group
762                .iter()
763                .copied()
764                .filter_map(|candidate| {
765                    let total = group.iter().copied().try_fold(0_u64, |total, rank| {
766                        self.shortest_transfer_time_ns_within(candidate, rank, payload_bytes, group)
767                            .map(|cost| total.saturating_add(cost))
768                    })?;
769                    Some((total, candidate))
770                })
771                .min_by_key(|(total, candidate)| (*total, *candidate))?
772                .1;
773            group.sort_unstable();
774            let leader_position = group.iter().position(|rank| *rank == leader)?;
775            group.swap(0, leader_position);
776        }
777        let leaders = groups.iter().map(|group| group[0]).collect::<Vec<_>>();
778        let order = self.best_rank_cycle_order(&leaders, payload_bytes)?;
779        Some(
780            order
781                .into_iter()
782                .map(|index| groups[index].clone())
783                .collect(),
784        )
785    }
786
787    fn shortest_transfer_time_ns_within(
788        &self,
789        source: u32,
790        destination: u32,
791        bytes: usize,
792        allowed: &[u32],
793    ) -> Option<u64> {
794        if source == destination {
795            return Some(0);
796        }
797        let mut permitted = vec![false; self.world_size as usize];
798        for rank in allowed {
799            permitted[*rank as usize] = true;
800        }
801        let rank_count = self.world_size as usize;
802        let mut distances = vec![u64::MAX; rank_count];
803        let mut visited = vec![false; rank_count];
804        distances[source as usize] = 0;
805        for _ in 0..allowed.len() {
806            let current = (0..rank_count)
807                .filter(|rank| permitted[*rank] && !visited[*rank])
808                .min_by_key(|rank| distances[*rank])?;
809            if distances[current] == u64::MAX {
810                break;
811            }
812            if current == destination as usize {
813                return Some(distances[current]);
814            }
815            visited[current] = true;
816            for next in 0..rank_count {
817                if !permitted[next] || visited[next] {
818                    continue;
819                }
820                let Some(link) = self.links[current][next] else {
821                    continue;
822                };
823                let candidate =
824                    distances[current].saturating_add(link_transfer_time_ns(link, bytes));
825                distances[next] = distances[next].min(candidate);
826            }
827        }
828        (distances[destination as usize] != u64::MAX).then_some(distances[destination as usize])
829    }
830
831    fn best_rank_cycle_order(&self, ranks: &[u32], payload_bytes: usize) -> Option<Vec<usize>> {
832        if ranks.is_empty() {
833            return None;
834        }
835        if ranks.len() == 1 {
836            return Some(vec![0]);
837        }
838        if ranks.len() <= EXACT_RING_SEARCH_MAX_RANKS as usize {
839            let mut order = vec![0_usize];
840            let mut used = vec![false; ranks.len()];
841            used[0] = true;
842            let mut best = None;
843            collect_rank_cycles(self, ranks, payload_bytes, &mut order, &mut used, &mut best);
844            return best.map(|(_, order)| order);
845        }
846
847        let mut order = vec![0_usize];
848        let mut used = vec![false; ranks.len()];
849        used[0] = true;
850        while order.len() < ranks.len() {
851            let current = *order.last()?;
852            let next = (0..ranks.len())
853                .filter(|candidate| !used[*candidate])
854                .filter_map(|candidate| {
855                    self.shortest_transfer_time_ns(ranks[current], ranks[candidate], payload_bytes)
856                        .map(|cost| (cost, ranks[candidate], candidate))
857                })
858                .min_by_key(|(cost, rank, _)| (*cost, *rank))?
859                .2;
860            used[next] = true;
861            order.push(next);
862        }
863        self.shortest_transfer_time_ns(ranks[*order.last()?], ranks[order[0]], payload_bytes)?;
864        Some(order)
865    }
866}
867
868fn collect_rank_cycles(
869    topology: &CollectiveTopology,
870    ranks: &[u32],
871    payload_bytes: usize,
872    order: &mut Vec<usize>,
873    used: &mut [bool],
874    best: &mut Option<((u64, u128), Vec<usize>)>,
875) {
876    if order.len() == ranks.len() {
877        let mut maximum = 0_u64;
878        let mut total = 0_u128;
879        for position in 0..order.len() {
880            let source = ranks[order[position]];
881            let destination = ranks[order[(position + 1) % order.len()]];
882            let Some(cost) = topology.shortest_transfer_time_ns(source, destination, payload_bytes)
883            else {
884                return;
885            };
886            maximum = maximum.max(cost);
887            total = total.saturating_add(u128::from(cost));
888        }
889        let score = (maximum, total);
890        if best.as_ref().is_none_or(|(best_score, best_order)| {
891            score < *best_score || score == *best_score && order.as_slice() < best_order.as_slice()
892        }) {
893            *best = Some((score, order.clone()));
894        }
895        return;
896    }
897    for candidate in 1..ranks.len() {
898        if used[candidate] {
899            continue;
900        }
901        used[candidate] = true;
902        order.push(candidate);
903        collect_rank_cycles(topology, ranks, payload_bytes, order, used, best);
904        order.pop();
905        used[candidate] = false;
906    }
907}
908
909#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
910struct RingScore {
911    bottleneck: u64,
912    total_bandwidth: u128,
913    inverse_latency: u128,
914}
915
916fn collect_rings(
917    topology: &CollectiveTopology,
918    order: &mut Vec<u32>,
919    used: &mut [bool],
920    candidates: &mut Vec<Vec<u32>>,
921) -> Result<(), TopologyError> {
922    if order.len() == topology.world_size as usize {
923        if topology.link(*order.last().unwrap(), order[0]).is_none() {
924            return Ok(());
925        }
926        let reversed = std::iter::once(0)
927            .chain(order[1..].iter().rev().copied())
928            .collect::<Vec<_>>();
929        if order.as_slice() <= reversed.as_slice() {
930            candidates.push(order.clone());
931        }
932        return Ok(());
933    }
934    let current = *order.last().expect("ring search has a current rank");
935    for candidate in 1..topology.world_size {
936        if used[candidate as usize] || topology.link(current, candidate).is_none() {
937            continue;
938        }
939        used[candidate as usize] = true;
940        order.push(candidate);
941        collect_rings(topology, order, used, candidates)?;
942        order.pop();
943        used[candidate as usize] = false;
944    }
945    Ok(())
946}
947
948fn rotate_ring_to_zero(order: &mut [u32]) {
949    if let Some(position) = order.iter().position(|rank| *rank == 0) {
950        order.rotate_left(position);
951    }
952    let reversed = std::iter::once(0)
953        .chain(order[1..].iter().rev().copied())
954        .collect::<Vec<_>>();
955    if reversed.as_slice() < order {
956        order.copy_from_slice(&reversed);
957    }
958}
959
960fn select_diverse_rings(
961    topology: &CollectiveTopology,
962    mut candidates: Vec<Vec<u32>>,
963    maximum: usize,
964) -> Result<Vec<Vec<u32>>, TopologyError> {
965    if candidates.is_empty() {
966        return Err(TopologyError::InvalidRing(
967            "topology does not contain a closed rank ring".into(),
968        ));
969    }
970    candidates.sort_unstable();
971    candidates.dedup();
972    let rank_count = topology.world_size as usize;
973    let mut edge_usage = vec![vec![0_u16; rank_count]; rank_count];
974    let mut selected = Vec::with_capacity(maximum.min(candidates.len()));
975    while selected.len() < maximum && !candidates.is_empty() {
976        let mut best_index = 0;
977        let mut best_key = None::<(u16, u64, RingScore)>;
978        for (index, order) in candidates.iter().enumerate() {
979            let mut maximum_usage = 0_u16;
980            let mut total_usage = 0_u64;
981            for position in 0..order.len() {
982                let first = order[position] as usize;
983                let second = order[(position + 1) % order.len()] as usize;
984                let usage = edge_usage[first][second];
985                maximum_usage = maximum_usage.max(usage);
986                total_usage = total_usage.saturating_add(u64::from(usage));
987            }
988            let key = (
989                u16::MAX - maximum_usage,
990                u64::MAX - total_usage,
991                topology.ring_score(order)?,
992            );
993            if best_key.is_none_or(|current| key > current) {
994                best_key = Some(key);
995                best_index = index;
996            }
997        }
998        let order = candidates.remove(best_index);
999        for position in 0..order.len() {
1000            let first = order[position] as usize;
1001            let second = order[(position + 1) % order.len()] as usize;
1002            edge_usage[first][second] = edge_usage[first][second].saturating_add(1);
1003            edge_usage[second][first] = edge_usage[second][first].saturating_add(1);
1004        }
1005        selected.push(order);
1006    }
1007    Ok(selected)
1008}
1009
1010fn balanced_sizes(total: usize, parts: usize) -> Vec<usize> {
1011    let base = total / parts;
1012    let remainder = total % parts;
1013    (0..parts)
1014        .map(|part| base + usize::from(part < remainder))
1015        .collect()
1016}
1017
1018fn link_transfer_time_ns(link: LinkCost, bytes: usize) -> u64 {
1019    link.latency_ns
1020        .saturating_add(transfer_time_ns(bytes, link.bandwidth_mbps))
1021}
1022
1023fn transfer_time_ns(bytes: usize, bandwidth_mbps: u64) -> u64 {
1024    saturating_u64(transfer_time_ns_u128(bytes as u128, bandwidth_mbps))
1025}
1026
1027fn transfer_time_ns_u128(bytes: u128, bandwidth_mbps: u64) -> u128 {
1028    let numerator = bytes.saturating_mul(1_000);
1029    let denominator = u128::from(bandwidth_mbps);
1030    numerator.saturating_add(denominator - 1) / denominator
1031}
1032
1033fn saturating_u64(value: u128) -> u64 {
1034    u64::try_from(value).unwrap_or(u64::MAX)
1035}
1036
1037fn fingerprint_u64(hash: &mut u64, value: u64) {
1038    for byte in value.to_le_bytes() {
1039        *hash ^= u64::from(byte);
1040        *hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
1041    }
1042}
1043
1044fn ring_edges(order: &[u32]) -> Vec<(u32, u32)> {
1045    let mut edges = (0..order.len())
1046        .map(|index| {
1047            let first = order[index];
1048            let second = order[(index + 1) % order.len()];
1049            (first.min(second), first.max(second))
1050        })
1051        .collect::<Vec<_>>();
1052    edges.sort_unstable();
1053    edges
1054}
1055
1056fn ring_orders_with_primary(topology: &CollectiveTopology, primary: &[u32]) -> Vec<Vec<u32>> {
1057    let mut orders = vec![primary.to_vec()];
1058    let primary_edges = ring_edges(primary);
1059    if let Ok(candidates) = topology.best_ring_orders(MAX_RING_CHANNELS) {
1060        for candidate in candidates {
1061            let edges = ring_edges(&candidate);
1062            if edges != primary_edges && !orders.iter().any(|order| ring_edges(order) == edges) {
1063                orders.push(candidate);
1064            }
1065        }
1066    }
1067    orders
1068}
1069
1070impl CollectiveTuning {
1071    pub fn from_environment(world_size: u32) -> Result<Self, TopologyError> {
1072        Self::from_environment_impl(world_size, None)
1073    }
1074
1075    pub fn from_environment_with_topology(
1076        topology: CollectiveTopology,
1077    ) -> Result<Self, TopologyError> {
1078        let world_size = topology.world_size();
1079        Self::from_environment_impl(world_size, Some(topology))
1080    }
1081
1082    pub fn topology_probe_requested() -> Result<bool, TopologyError> {
1083        match env::var("GX1_TOPOLOGY_LINKS") {
1084            Ok(value) => Ok(value.trim().eq_ignore_ascii_case("probe")),
1085            Err(env::VarError::NotPresent) => Ok(false),
1086            Err(env::VarError::NotUnicode(value)) => Err(TopologyError::InvalidEnvironment {
1087                name: "GX1_TOPOLOGY_LINKS",
1088                value: value.to_string_lossy().into_owned(),
1089            }),
1090        }
1091    }
1092
1093    fn from_environment_impl(
1094        world_size: u32,
1095        probed_topology: Option<CollectiveTopology>,
1096    ) -> Result<Self, TopologyError> {
1097        let algorithm_policy = match env::var("GX1_COLLECTIVE_ALGORITHM") {
1098            Ok(value) => match value.trim().to_ascii_lowercase().as_str() {
1099                "auto" => AlgorithmPolicy::Auto,
1100                "direct" => AlgorithmPolicy::Direct,
1101                "ring" => AlgorithmPolicy::Ring,
1102                "hierarchical" => AlgorithmPolicy::Hierarchical,
1103                _ => {
1104                    return Err(TopologyError::InvalidEnvironment {
1105                        name: "GX1_COLLECTIVE_ALGORITHM",
1106                        value,
1107                    });
1108                }
1109            },
1110            Err(env::VarError::NotPresent) => AlgorithmPolicy::Auto,
1111            Err(env::VarError::NotUnicode(value)) => {
1112                return Err(TopologyError::InvalidEnvironment {
1113                    name: "GX1_COLLECTIVE_ALGORITHM",
1114                    value: value.to_string_lossy().into_owned(),
1115                });
1116            }
1117        };
1118        let ring_threshold_bytes =
1119            parse_usize_environment("GX1_RING_THRESHOLD_BYTES", DEFAULT_RING_THRESHOLD_BYTES)?;
1120        let ring_channels = parse_usize_environment("GX1_RING_CHANNELS", 1)?;
1121        validate_ring_channels(ring_channels)?;
1122        let p2p_rails = parse_usize_environment("GX1_P2P_RAILS", 1)?;
1123        validate_p2p_rails(p2p_rails)?;
1124        let direct_bandwidth_mbps =
1125            parse_u64_environment("GX1_DIRECT_BANDWIDTH_MBPS", DEFAULT_BANDWIDTH_MBPS)?;
1126        if direct_bandwidth_mbps == 0 {
1127            return Err(TopologyError::InvalidEnvironment {
1128                name: "GX1_DIRECT_BANDWIDTH_MBPS",
1129                value: "0".into(),
1130            });
1131        }
1132        let direct_latency_ns = parse_u64_environment("GX1_DIRECT_LATENCY_NS", DEFAULT_LATENCY_NS)?;
1133        let transport = parse_collective_transport_environment()?;
1134        let mut topology = match env::var("GX1_TOPOLOGY_LINKS") {
1135            Ok(value) if value.trim().eq_ignore_ascii_case("probe") => {
1136                probed_topology.ok_or_else(|| TopologyError::InvalidEnvironment {
1137                    name: "GX1_TOPOLOGY_LINKS",
1138                    value: value.clone(),
1139                })?
1140            }
1141            Ok(value) => parse_topology_links(world_size, &value)?,
1142            Err(env::VarError::NotPresent) => match probed_topology {
1143                Some(topology) => topology,
1144                None => CollectiveTopology::uniform(world_size)?,
1145            },
1146            Err(env::VarError::NotUnicode(value)) => {
1147                return Err(TopologyError::InvalidEnvironment {
1148                    name: "GX1_TOPOLOGY_LINKS",
1149                    value: value.to_string_lossy().into_owned(),
1150                });
1151            }
1152        };
1153        match env::var("GX1_TOPOLOGY_RAIL_LINKS") {
1154            Ok(value) => parse_topology_rail_links(&mut topology, &value, p2p_rails)?,
1155            Err(env::VarError::NotPresent) => {}
1156            Err(env::VarError::NotUnicode(value)) => {
1157                return Err(TopologyError::InvalidEnvironment {
1158                    name: "GX1_TOPOLOGY_RAIL_LINKS",
1159                    value: value.to_string_lossy().into_owned(),
1160                });
1161            }
1162        }
1163        match env::var("GX1_TOPOLOGY_AGGREGATE_LINKS") {
1164            Ok(value) => parse_topology_aggregate_links(&mut topology, &value)?,
1165            Err(env::VarError::NotPresent) => {}
1166            Err(env::VarError::NotUnicode(value)) => {
1167                return Err(TopologyError::InvalidEnvironment {
1168                    name: "GX1_TOPOLOGY_AGGREGATE_LINKS",
1169                    value: value.to_string_lossy().into_owned(),
1170                });
1171            }
1172        }
1173        let rail_order_override = match env::var("GX1_RAIL_ORDER") {
1174            Ok(value) if value.trim().eq_ignore_ascii_case("auto") => None,
1175            Ok(value) => Some(parse_rail_order(p2p_rails, &value)?),
1176            Err(env::VarError::NotPresent) => None,
1177            Err(env::VarError::NotUnicode(value)) => {
1178                return Err(TopologyError::InvalidEnvironment {
1179                    name: "GX1_RAIL_ORDER",
1180                    value: value.to_string_lossy().into_owned(),
1181                });
1182            }
1183        };
1184        let ring_order = match env::var("GX1_RING_ORDER") {
1185            Ok(value) => parse_ring_order(world_size, &value)?,
1186            Err(env::VarError::NotPresent) => topology.best_ring_order()?,
1187            Err(env::VarError::NotUnicode(value)) => {
1188                return Err(TopologyError::InvalidEnvironment {
1189                    name: "GX1_RING_ORDER",
1190                    value: value.to_string_lossy().into_owned(),
1191                });
1192            }
1193        };
1194        let (hierarchy_groups, inferred_hierarchy_bytes) = match env::var("GX1_TOPOLOGY_GROUPS") {
1195            Ok(value) if value.trim().eq_ignore_ascii_case("auto") => {
1196                let payload_bytes = parse_usize_environment(
1197                    "GX1_TOPOLOGY_GROUP_PAYLOAD_BYTES",
1198                    DEFAULT_HIERARCHY_INFERENCE_BYTES,
1199                )?;
1200                if payload_bytes == 0 {
1201                    return Err(TopologyError::InvalidEnvironment {
1202                        name: "GX1_TOPOLOGY_GROUP_PAYLOAD_BYTES",
1203                        value: "0".into(),
1204                    });
1205                }
1206                (None, Some(payload_bytes))
1207            }
1208            Ok(value) => (Some(parse_hierarchy_groups(world_size, &value)?), None),
1209            Err(env::VarError::NotPresent) => (None, None),
1210            Err(env::VarError::NotUnicode(value)) => {
1211                return Err(TopologyError::InvalidEnvironment {
1212                    name: "GX1_TOPOLOGY_GROUPS",
1213                    value: value.to_string_lossy().into_owned(),
1214                });
1215            }
1216        };
1217        let ring_orders = ring_orders_with_primary(&topology, &ring_order);
1218        let mut tuning = Self {
1219            algorithm_policy,
1220            ring_threshold_bytes,
1221            ring_order,
1222            ring_orders,
1223            ring_channels,
1224            p2p_rails,
1225            rail_order_override,
1226            hierarchy_groups,
1227            topology,
1228            direct_bandwidth_mbps,
1229            direct_latency_ns,
1230            transport,
1231            autotune_profile: None,
1232        };
1233        if let Some(payload_bytes) = inferred_hierarchy_bytes {
1234            tuning = tuning.with_inferred_hierarchy(payload_bytes)?;
1235        }
1236        if algorithm_policy == AlgorithmPolicy::Hierarchical && tuning.hierarchy_groups.is_none() {
1237            return Err(TopologyError::InvalidHierarchy(
1238                "GX1_COLLECTIVE_ALGORITHM=hierarchical requires explicit or inferred topology groups"
1239                    .into(),
1240            ));
1241        }
1242        match env::var("GX1_COLLECTIVE_AUTOTUNE_PROFILE") {
1243            Ok(path) => tuning = tuning.with_autotune_profile_path(path)?,
1244            Err(env::VarError::NotPresent) => {}
1245            Err(env::VarError::NotUnicode(value)) => {
1246                return Err(TopologyError::InvalidEnvironment {
1247                    name: "GX1_COLLECTIVE_AUTOTUNE_PROFILE",
1248                    value: value.to_string_lossy().into_owned(),
1249                });
1250            }
1251        }
1252        Ok(tuning)
1253    }
1254
1255    pub fn new(
1256        algorithm_policy: AlgorithmPolicy,
1257        ring_threshold_bytes: usize,
1258        ring_order: Vec<u32>,
1259    ) -> Result<Self, TopologyError> {
1260        let world_size = u32::try_from(ring_order.len())
1261            .map_err(|_| TopologyError::InvalidRing("ring is too large".into()))?;
1262        let topology = CollectiveTopology::uniform(world_size)?;
1263        Self::from_topology(
1264            algorithm_policy,
1265            ring_threshold_bytes,
1266            topology,
1267            Some(ring_order),
1268        )
1269    }
1270
1271    pub fn from_topology(
1272        algorithm_policy: AlgorithmPolicy,
1273        ring_threshold_bytes: usize,
1274        topology: CollectiveTopology,
1275        ring_order: Option<Vec<u32>>,
1276    ) -> Result<Self, TopologyError> {
1277        let world_size = topology.world_size();
1278        let ring_order = match ring_order {
1279            Some(order) => order,
1280            None => topology.best_ring_order()?,
1281        };
1282        validate_ring_order(world_size, &ring_order)?;
1283        let ring_orders = ring_orders_with_primary(&topology, &ring_order);
1284        Ok(Self {
1285            algorithm_policy,
1286            ring_threshold_bytes,
1287            ring_order,
1288            ring_orders,
1289            ring_channels: 1,
1290            p2p_rails: 1,
1291            rail_order_override: None,
1292            hierarchy_groups: None,
1293            topology,
1294            direct_bandwidth_mbps: DEFAULT_BANDWIDTH_MBPS,
1295            direct_latency_ns: DEFAULT_LATENCY_NS,
1296            transport: CollectiveTransport::TcpHostStaged,
1297            autotune_profile: None,
1298        })
1299    }
1300
1301    pub fn with_direct_transport(
1302        mut self,
1303        bandwidth_mbps: u64,
1304        latency_ns: u64,
1305    ) -> Result<Self, TopologyError> {
1306        if bandwidth_mbps == 0 {
1307            return Err(TopologyError::ZeroDirectBandwidth);
1308        }
1309        self.direct_bandwidth_mbps = bandwidth_mbps;
1310        self.direct_latency_ns = latency_ns;
1311        if let Some(profile) = &self.autotune_profile {
1312            self.validate_autotune_profile(profile)?;
1313        }
1314        Ok(self)
1315    }
1316
1317    pub fn with_transport(mut self, transport: CollectiveTransport) -> Result<Self, TopologyError> {
1318        self.transport = transport;
1319        if let Some(profile) = &self.autotune_profile {
1320            self.validate_autotune_profile(profile)?;
1321        }
1322        Ok(self)
1323    }
1324
1325    pub const fn transport(&self) -> CollectiveTransport {
1326        self.transport
1327    }
1328
1329    pub fn with_hierarchy(mut self, groups: Vec<Vec<u32>>) -> Result<Self, TopologyError> {
1330        validate_hierarchy_groups(self.topology.world_size(), &groups)?;
1331        self.hierarchy_groups = Some(groups);
1332        if let Some(profile) = &self.autotune_profile {
1333            self.validate_autotune_profile(profile)?;
1334        }
1335        Ok(self)
1336    }
1337
1338    pub fn with_inferred_hierarchy(mut self, payload_bytes: usize) -> Result<Self, TopologyError> {
1339        if payload_bytes == 0 {
1340            return Err(TopologyError::InvalidHierarchy(
1341                "automatic grouping payload must be greater than zero".into(),
1342            ));
1343        }
1344        let mut best = None::<(u128, Vec<Vec<u32>>)>;
1345        for groups in self.topology.hierarchy_candidates(payload_bytes) {
1346            self.hierarchy_groups = Some(groups.clone());
1347            let score = [
1348                CollectiveKind::AllReduce,
1349                CollectiveKind::AllGather,
1350                CollectiveKind::ReduceScatter,
1351            ]
1352            .into_iter()
1353            .try_fold(0_u128, |total, kind| {
1354                self.hierarchical_estimated_time_ns(kind, payload_bytes)
1355                    .map(|estimate| total.saturating_add(u128::from(estimate)))
1356            });
1357            let Some(score) = score else {
1358                continue;
1359            };
1360            if best.as_ref().is_none_or(|(best_score, best_groups)| {
1361                score < *best_score || score == *best_score && groups < *best_groups
1362            }) {
1363                best = Some((score, groups));
1364            }
1365        }
1366        let Some((_, groups)) = best else {
1367            self.hierarchy_groups = None;
1368            return Err(TopologyError::InvalidHierarchy(
1369                "automatic grouping found no distinct fast-link domains".into(),
1370            ));
1371        };
1372        self.hierarchy_groups = Some(groups);
1373        if let Some(profile) = &self.autotune_profile {
1374            self.validate_autotune_profile(profile)?;
1375        }
1376        Ok(self)
1377    }
1378
1379    pub fn hierarchy_groups(&self) -> Option<&[Vec<u32>]> {
1380        self.hierarchy_groups.as_deref()
1381    }
1382
1383    pub fn with_ring_channels(mut self, channels: usize) -> Result<Self, TopologyError> {
1384        validate_ring_channels(channels)?;
1385        self.ring_channels = channels;
1386        if let Some(profile) = &self.autotune_profile {
1387            self.validate_autotune_profile(profile)?;
1388        }
1389        Ok(self)
1390    }
1391
1392    pub const fn ring_channels(&self) -> usize {
1393        self.ring_channels
1394    }
1395
1396    pub fn ring_orders(&self) -> &[Vec<u32>] {
1397        &self.ring_orders
1398    }
1399
1400    pub fn topology_links(&self) -> Vec<TopologyLink> {
1401        self.topology.links()
1402    }
1403
1404    pub fn topology_rail_links(&self) -> Vec<TopologyRailLink> {
1405        self.topology.rail_links()
1406    }
1407
1408    pub fn topology_aggregate_links(&self) -> Vec<TopologyAggregateLink> {
1409        self.topology.aggregate_links()
1410    }
1411
1412    pub fn ring_order_for_channel(&self, channel: usize) -> &[u32] {
1413        &self.ring_orders[channel % self.ring_orders.len()]
1414    }
1415
1416    pub fn rail_order(&self) -> Vec<usize> {
1417        self.rail_order_override
1418            .clone()
1419            .unwrap_or_else(|| self.topology.preferred_rail_order(self.p2p_rails))
1420    }
1421
1422    pub fn rail_for_channel(&self, channel: usize) -> usize {
1423        let rail_order = self.rail_order();
1424        rail_order[channel % rail_order.len()]
1425    }
1426
1427    pub fn with_p2p_rails(mut self, rails: usize) -> Result<Self, TopologyError> {
1428        validate_p2p_rails(rails)?;
1429        if let Some(order) = &self.rail_order_override {
1430            validate_rail_order(rails, order)?;
1431        }
1432        self.p2p_rails = rails;
1433        if let Some(profile) = &self.autotune_profile {
1434            self.validate_autotune_profile(profile)?;
1435        }
1436        Ok(self)
1437    }
1438
1439    pub fn with_rail_order(mut self, order: Vec<usize>) -> Result<Self, TopologyError> {
1440        validate_rail_order(self.p2p_rails, &order)?;
1441        self.rail_order_override = Some(order);
1442        if let Some(profile) = &self.autotune_profile {
1443            self.validate_autotune_profile(profile)?;
1444        }
1445        Ok(self)
1446    }
1447
1448    pub const fn p2p_rails(&self) -> usize {
1449        self.p2p_rails
1450    }
1451
1452    pub fn with_autotune_profile_path(
1453        mut self,
1454        path: impl AsRef<Path>,
1455    ) -> Result<Self, TopologyError> {
1456        let path = path.as_ref();
1457        let encoded = fs::read_to_string(path).map_err(|error| TopologyError::ProfileRead {
1458            path: path.display().to_string(),
1459            message: error.to_string(),
1460        })?;
1461        let document = serde_json::from_str::<serde_json::Value>(&encoded).map_err(|error| {
1462            TopologyError::InvalidProfile(format!("{} is not valid JSON: {error}", path.display()))
1463        })?;
1464        let detected_topology_links = match document.get("detected_topology_links") {
1465            Some(serde_json::Value::String(value)) => Some(value.as_str()),
1466            Some(serde_json::Value::Null) | None => None,
1467            Some(_) => {
1468                return Err(TopologyError::InvalidProfile(
1469                    "detected_topology_links must be a string or null".into(),
1470                ));
1471            }
1472        };
1473        let detected_topology_rail_links = match document.get("detected_topology_rail_links") {
1474            Some(serde_json::Value::String(value)) => Some(value.as_str()),
1475            Some(serde_json::Value::Null) | None => None,
1476            Some(_) => {
1477                return Err(TopologyError::InvalidProfile(
1478                    "detected_topology_rail_links must be a string or null".into(),
1479                ));
1480            }
1481        };
1482        let detected_topology_aggregate_links =
1483            match document.get("detected_topology_aggregate_links") {
1484                Some(serde_json::Value::String(value)) => Some(value.as_str()),
1485                Some(serde_json::Value::Null) | None => None,
1486                Some(_) => {
1487                    return Err(TopologyError::InvalidProfile(
1488                        "detected_topology_aggregate_links must be a string or null".into(),
1489                    ));
1490                }
1491            };
1492        let profile = serde_json::from_str(&encoded).map_err(|error| {
1493            TopologyError::InvalidProfile(format!("{} is not valid JSON: {error}", path.display()))
1494        })?;
1495        self.validate_autotune_profile_with_detected(
1496            &profile,
1497            detected_topology_links,
1498            detected_topology_rail_links,
1499            detected_topology_aggregate_links,
1500            Self::topology_probe_requested()?,
1501        )?;
1502        self.autotune_profile = Some(profile);
1503        Ok(self)
1504    }
1505
1506    pub fn with_autotune_profile(
1507        mut self,
1508        profile: CollectiveAutotuneProfile,
1509    ) -> Result<Self, TopologyError> {
1510        self.validate_autotune_profile(&profile)?;
1511        self.autotune_profile = Some(profile);
1512        Ok(self)
1513    }
1514
1515    pub fn topology_fingerprint(&self) -> u64 {
1516        let mut hash = 0xcbf2_9ce4_8422_2325_u64;
1517        fingerprint_u64(&mut hash, u64::from(self.topology.world_size));
1518        for first in 0..self.topology.world_size {
1519            for second in first + 1..self.topology.world_size {
1520                fingerprint_u64(&mut hash, u64::from(first));
1521                fingerprint_u64(&mut hash, u64::from(second));
1522                match self.topology.link(first, second) {
1523                    Some(link) => {
1524                        fingerprint_u64(&mut hash, 1);
1525                        fingerprint_u64(&mut hash, link.bandwidth_mbps);
1526                        fingerprint_u64(&mut hash, link.latency_ns);
1527                    }
1528                    None => fingerprint_u64(&mut hash, 0),
1529                }
1530            }
1531        }
1532        if !self.topology.rail_links.is_empty() {
1533            fingerprint_u64(&mut hash, 0x4758_5241_494c_4c4b);
1534            fingerprint_u64(&mut hash, self.topology.rail_links.len() as u64);
1535            for (rail, links) in self.topology.rail_links.iter().enumerate() {
1536                fingerprint_u64(&mut hash, rail as u64);
1537                for first in 0..self.topology.world_size {
1538                    for second in first + 1..self.topology.world_size {
1539                        fingerprint_u64(&mut hash, u64::from(first));
1540                        fingerprint_u64(&mut hash, u64::from(second));
1541                        match links[first as usize][second as usize] {
1542                            Some(link) => {
1543                                fingerprint_u64(&mut hash, 1);
1544                                fingerprint_u64(&mut hash, link.bandwidth_mbps);
1545                                fingerprint_u64(&mut hash, link.latency_ns);
1546                            }
1547                            None => fingerprint_u64(&mut hash, 0),
1548                        }
1549                    }
1550                }
1551            }
1552        }
1553        if self
1554            .topology
1555            .aggregate_links
1556            .iter()
1557            .flatten()
1558            .any(Option::is_some)
1559        {
1560            fingerprint_u64(&mut hash, 0x4758_4147_4752_4547);
1561            for first in 0..self.topology.world_size {
1562                for second in first + 1..self.topology.world_size {
1563                    fingerprint_u64(&mut hash, u64::from(first));
1564                    fingerprint_u64(&mut hash, u64::from(second));
1565                    match self.topology.aggregate_bandwidth_mbps(first, second) {
1566                        Some(bandwidth_mbps) => {
1567                            fingerprint_u64(&mut hash, 1);
1568                            fingerprint_u64(&mut hash, bandwidth_mbps);
1569                        }
1570                        None => fingerprint_u64(&mut hash, 0),
1571                    }
1572                }
1573            }
1574        }
1575        fingerprint_u64(&mut hash, self.ring_orders.len() as u64);
1576        for order in &self.ring_orders {
1577            fingerprint_u64(&mut hash, order.len() as u64);
1578            for rank in order {
1579                fingerprint_u64(&mut hash, u64::from(*rank));
1580            }
1581        }
1582        match &self.hierarchy_groups {
1583            Some(groups) => {
1584                fingerprint_u64(&mut hash, groups.len() as u64);
1585                for group in groups {
1586                    fingerprint_u64(&mut hash, group.len() as u64);
1587                    for rank in group {
1588                        fingerprint_u64(&mut hash, u64::from(*rank));
1589                    }
1590                }
1591            }
1592            None => fingerprint_u64(&mut hash, 0),
1593        }
1594        fingerprint_u64(&mut hash, self.direct_bandwidth_mbps);
1595        fingerprint_u64(&mut hash, self.direct_latency_ns);
1596        if self.p2p_rails != 1 {
1597            fingerprint_u64(&mut hash, 0x4758_5241_494c_0001);
1598            fingerprint_u64(&mut hash, self.p2p_rails as u64);
1599        }
1600        let rail_order = self.rail_order();
1601        if !rail_order.iter().copied().eq(0..self.p2p_rails) {
1602            fingerprint_u64(&mut hash, 0x4758_5241_494c_4f52);
1603            for rail in rail_order {
1604                fingerprint_u64(&mut hash, rail as u64);
1605            }
1606        }
1607        hash
1608    }
1609
1610    pub fn topology_fingerprint_hex(&self) -> String {
1611        format!("{:016x}", self.topology_fingerprint())
1612    }
1613
1614    pub fn execution_fingerprint(&self) -> u64 {
1615        let mut hash = 0xcbf2_9ce4_8422_2325_u64;
1616        fingerprint_u64(&mut hash, COLLECTIVE_EXECUTION_REVISION);
1617        fingerprint_u64(
1618            &mut hash,
1619            match self.transport {
1620                CollectiveTransport::HostStaged => 1,
1621                CollectiveTransport::TcpHostStaged => 2,
1622                CollectiveTransport::TcpPeer => 3,
1623                CollectiveTransport::PciePeer => 4,
1624                CollectiveTransport::Rdma => 5,
1625                CollectiveTransport::GxLink => 6,
1626            },
1627        );
1628        hash
1629    }
1630
1631    pub fn execution_fingerprint_hex(&self) -> String {
1632        format!("{:016x}", self.execution_fingerprint())
1633    }
1634
1635    fn validate_autotune_profile(
1636        &self,
1637        profile: &CollectiveAutotuneProfile,
1638    ) -> Result<(), TopologyError> {
1639        self.validate_autotune_profile_with_detected(profile, None, None, None, false)
1640    }
1641
1642    fn validate_autotune_profile_with_detected(
1643        &self,
1644        profile: &CollectiveAutotuneProfile,
1645        detected_topology_links: Option<&str>,
1646        detected_topology_rail_links: Option<&str>,
1647        detected_topology_aggregate_links: Option<&str>,
1648        allow_detected_topology: bool,
1649    ) -> Result<(), TopologyError> {
1650        if profile.version != COLLECTIVE_AUTOTUNE_PROFILE_VERSION {
1651            return Err(TopologyError::InvalidProfile(format!(
1652                "unsupported version {}, expected {COLLECTIVE_AUTOTUNE_PROFILE_VERSION}",
1653                profile.version,
1654            )));
1655        }
1656        let world_size = self.ring_order.len() as u32;
1657        if profile.world_size != world_size {
1658            return Err(TopologyError::InvalidProfile(format!(
1659                "world size {} does not match configured world size {world_size}",
1660                profile.world_size
1661            )));
1662        }
1663        let fingerprint = profile
1664            .topology_fingerprint
1665            .strip_prefix("0x")
1666            .unwrap_or(&profile.topology_fingerprint)
1667            .to_ascii_lowercase();
1668        let expected = self.topology_fingerprint_hex();
1669        if fingerprint != expected {
1670            let detected_matches = match detected_topology_links {
1671                Some(encoded) if allow_detected_topology => {
1672                    let mut detected = parse_topology_links(world_size, encoded)?;
1673                    if let Some(encoded) = detected_topology_rail_links {
1674                        parse_topology_rail_links(&mut detected, encoded, self.p2p_rails)?;
1675                    }
1676                    if let Some(encoded) = detected_topology_aggregate_links {
1677                        parse_topology_aggregate_links(&mut detected, encoded)?;
1678                    }
1679                    let mut pinned = self.clone();
1680                    pinned.topology = detected.clone();
1681                    pinned.autotune_profile = None;
1682                    probed_topology_matches(&self.topology, &detected)
1683                        && self.rail_order() == pinned.rail_order()
1684                        && pinned.topology_fingerprint_hex() == fingerprint
1685                }
1686                _ => false,
1687            };
1688            if !detected_matches {
1689                return Err(TopologyError::InvalidProfile(format!(
1690                    "topology fingerprint {fingerprint:?} does not match {expected:?}"
1691                )));
1692            }
1693        }
1694        let execution_fingerprint = profile
1695            .execution_fingerprint
1696            .strip_prefix("0x")
1697            .unwrap_or(&profile.execution_fingerprint)
1698            .to_ascii_lowercase();
1699        let expected_execution = self.execution_fingerprint_hex();
1700        if execution_fingerprint != expected_execution {
1701            return Err(TopologyError::InvalidProfile(format!(
1702                "execution fingerprint {execution_fingerprint:?} does not match {expected_execution:?} for transport {:?}",
1703                self.transport
1704            )));
1705        }
1706        if profile.entries.is_empty() {
1707            return Err(TopologyError::InvalidProfile(
1708                "profile contains no measurements".into(),
1709            ));
1710        }
1711        for entry in &profile.entries {
1712            if entry.min_payload_bytes > entry.max_payload_bytes {
1713                return Err(TopologyError::InvalidProfile(format!(
1714                    "{:?} range {}..={} is reversed",
1715                    entry.operation, entry.min_payload_bytes, entry.max_payload_bytes
1716                )));
1717            }
1718            if entry.measured_time_ns == 0 {
1719                return Err(TopologyError::InvalidProfile(format!(
1720                    "{:?} range {}..={} has zero measured time",
1721                    entry.operation, entry.min_payload_bytes, entry.max_payload_bytes
1722                )));
1723            }
1724            validate_ring_channels(entry.ring_channels).map_err(|_| {
1725                TopologyError::InvalidProfile(format!(
1726                    "{:?} range {}..={} has invalid ring channel count {}",
1727                    entry.operation,
1728                    entry.min_payload_bytes,
1729                    entry.max_payload_bytes,
1730                    entry.ring_channels
1731                ))
1732            })?;
1733            let valid_algorithm = match entry.operation {
1734                CollectiveKind::AllToAll => matches!(
1735                    entry.algorithm,
1736                    CollectiveAlgorithm::Direct | CollectiveAlgorithm::Pairwise
1737                ),
1738                CollectiveKind::AllReduce
1739                | CollectiveKind::AllGather
1740                | CollectiveKind::ReduceScatter => matches!(
1741                    entry.algorithm,
1742                    CollectiveAlgorithm::Direct
1743                        | CollectiveAlgorithm::Ring
1744                        | CollectiveAlgorithm::Hierarchical
1745                ),
1746            };
1747            if !valid_algorithm {
1748                return Err(TopologyError::InvalidProfile(format!(
1749                    "algorithm {:?} cannot execute {:?}",
1750                    entry.algorithm, entry.operation
1751                )));
1752            }
1753            if entry.algorithm == CollectiveAlgorithm::Hierarchical
1754                && self.hierarchy_groups.is_none()
1755            {
1756                return Err(TopologyError::InvalidProfile(
1757                    "hierarchical measurement requires configured topology groups".into(),
1758                ));
1759            }
1760        }
1761        for kind in [
1762            CollectiveKind::AllReduce,
1763            CollectiveKind::AllGather,
1764            CollectiveKind::ReduceScatter,
1765            CollectiveKind::AllToAll,
1766        ] {
1767            let mut ranges = profile
1768                .entries
1769                .iter()
1770                .filter(|entry| entry.operation == kind)
1771                .map(|entry| (entry.min_payload_bytes, entry.max_payload_bytes))
1772                .collect::<Vec<_>>();
1773            ranges.sort_unstable();
1774            for pair in ranges.windows(2) {
1775                if pair[1].0 <= pair[0].1 {
1776                    return Err(TopologyError::InvalidProfile(format!(
1777                        "{kind:?} ranges {}..={} and {}..={} overlap",
1778                        pair[0].0, pair[0].1, pair[1].0, pair[1].1
1779                    )));
1780                }
1781            }
1782        }
1783        Ok(())
1784    }
1785
1786    fn autotune_entry(
1787        &self,
1788        kind: CollectiveKind,
1789        payload_bytes: usize,
1790    ) -> Option<&CollectiveAutotuneEntry> {
1791        let payload_bytes = u64::try_from(payload_bytes).unwrap_or(u64::MAX);
1792        self.autotune_profile
1793            .as_ref()?
1794            .entries
1795            .iter()
1796            .find(|entry| {
1797                entry.operation == kind
1798                    && payload_bytes >= entry.min_payload_bytes
1799                    && payload_bytes <= entry.max_payload_bytes
1800            })
1801    }
1802
1803    pub fn all_reduce_algorithm(&self, payload_bytes: usize) -> CollectiveAlgorithm {
1804        self.plan(CollectiveKind::AllReduce, payload_bytes)
1805            .algorithm
1806    }
1807
1808    pub fn all_gather_algorithm(&self, payload_bytes: usize) -> CollectiveAlgorithm {
1809        self.plan(CollectiveKind::AllGather, payload_bytes)
1810            .algorithm
1811    }
1812
1813    pub fn reduce_scatter_algorithm(&self, payload_bytes: usize) -> CollectiveAlgorithm {
1814        self.plan(CollectiveKind::ReduceScatter, payload_bytes)
1815            .algorithm
1816    }
1817
1818    pub fn all_to_all_algorithm(&self, payload_bytes: usize) -> CollectiveAlgorithm {
1819        self.plan(CollectiveKind::AllToAll, payload_bytes).algorithm
1820    }
1821
1822    pub fn plan(&self, kind: CollectiveKind, payload_bytes: usize) -> CollectivePlan {
1823        let direct_estimated_time_ns = self.direct_estimated_time_ns(kind, payload_bytes);
1824        let peer_algorithm = match kind {
1825            CollectiveKind::AllToAll => CollectiveAlgorithm::Pairwise,
1826            CollectiveKind::AllReduce
1827            | CollectiveKind::AllGather
1828            | CollectiveKind::ReduceScatter => CollectiveAlgorithm::Ring,
1829        };
1830        let peer_estimated_time_ns = self.peer_estimated_time_ns(kind, payload_bytes);
1831        let hierarchical_estimated_time_ns =
1832            self.hierarchical_estimated_time_ns(kind, payload_bytes);
1833        let mut ring_channels = self.ring_channels;
1834        let (algorithm, source, measured_time_ns) = if self.ring_order.len() <= 1 {
1835            (
1836                CollectiveAlgorithm::Direct,
1837                match self.algorithm_policy {
1838                    AlgorithmPolicy::Auto => CollectivePlanSource::Model,
1839                    AlgorithmPolicy::Direct
1840                    | AlgorithmPolicy::Ring
1841                    | AlgorithmPolicy::Hierarchical => CollectivePlanSource::Policy,
1842                },
1843                None,
1844            )
1845        } else {
1846            match self.algorithm_policy {
1847                AlgorithmPolicy::Direct => (
1848                    CollectiveAlgorithm::Direct,
1849                    CollectivePlanSource::Policy,
1850                    None,
1851                ),
1852                AlgorithmPolicy::Ring => (peer_algorithm, CollectivePlanSource::Policy, None),
1853                AlgorithmPolicy::Hierarchical => (
1854                    if kind == CollectiveKind::AllToAll {
1855                        peer_algorithm
1856                    } else if hierarchical_estimated_time_ns.is_some() {
1857                        CollectiveAlgorithm::Hierarchical
1858                    } else {
1859                        peer_algorithm
1860                    },
1861                    CollectivePlanSource::Policy,
1862                    None,
1863                ),
1864                AlgorithmPolicy::Auto => {
1865                    if let Some(entry) = self.autotune_entry(kind, payload_bytes) {
1866                        ring_channels = entry.ring_channels;
1867                        (
1868                            entry.algorithm,
1869                            CollectivePlanSource::Autotune,
1870                            Some(entry.measured_time_ns),
1871                        )
1872                    } else if payload_bytes < self.ring_threshold_bytes {
1873                        (
1874                            CollectiveAlgorithm::Direct,
1875                            CollectivePlanSource::Model,
1876                            None,
1877                        )
1878                    } else {
1879                        let mut best = (CollectiveAlgorithm::Direct, direct_estimated_time_ns);
1880                        if let Some(estimate) = peer_estimated_time_ns
1881                            && estimate < best.1
1882                        {
1883                            best = (peer_algorithm, estimate);
1884                        }
1885                        if kind != CollectiveKind::AllToAll
1886                            && let Some(estimate) = hierarchical_estimated_time_ns
1887                            && estimate < best.1
1888                        {
1889                            best = (CollectiveAlgorithm::Hierarchical, estimate);
1890                        }
1891                        (best.0, CollectivePlanSource::Model, None)
1892                    }
1893                }
1894            }
1895        };
1896        let estimated_time_ns = measured_time_ns.unwrap_or_else(|| match algorithm {
1897            CollectiveAlgorithm::Direct => direct_estimated_time_ns,
1898            CollectiveAlgorithm::Ring | CollectiveAlgorithm::Pairwise => {
1899                peer_estimated_time_ns.unwrap_or(u64::MAX)
1900            }
1901            CollectiveAlgorithm::Hierarchical => hierarchical_estimated_time_ns.unwrap_or(u64::MAX),
1902        });
1903        CollectivePlan {
1904            algorithm,
1905            source,
1906            estimated_time_ns,
1907            direct_estimated_time_ns,
1908            peer_estimated_time_ns,
1909            hierarchical_estimated_time_ns,
1910            ring_channels,
1911        }
1912    }
1913
1914    fn direct_estimated_time_ns(&self, kind: CollectiveKind, payload_bytes: usize) -> u64 {
1915        let ranks = self.ring_order.len() as u128;
1916        if ranks <= 1 {
1917            return 0;
1918        }
1919        let payload = payload_bytes as u128;
1920        let response = match kind {
1921            CollectiveKind::AllReduce | CollectiveKind::AllToAll => payload,
1922            CollectiveKind::AllGather => payload.saturating_mul(ranks),
1923            CollectiveKind::ReduceScatter => payload.div_ceil(ranks),
1924        };
1925        let aggregate_bytes = ranks.saturating_mul(payload.saturating_add(response));
1926        let transfer_ns = transfer_time_ns_u128(aggregate_bytes, self.direct_bandwidth_mbps);
1927        let latency_ns = (u128::from(self.direct_latency_ns))
1928            .saturating_mul(2)
1929            .saturating_mul(ranks);
1930        saturating_u64(transfer_ns.saturating_add(latency_ns))
1931    }
1932
1933    fn peer_estimated_time_ns(&self, kind: CollectiveKind, payload_bytes: usize) -> Option<u64> {
1934        let ranks = self.ring_order.len();
1935        if ranks <= 1 {
1936            return Some(0);
1937        }
1938        let rail_order = self.rail_order();
1939        let chunk_bytes = payload_bytes.div_ceil(ranks);
1940        match kind {
1941            CollectiveKind::AllReduce => self
1942                .multi_ring_step_time_ns(chunk_bytes)
1943                .map(|step| step.saturating_mul((2 * (ranks - 1)) as u64)),
1944            CollectiveKind::AllGather => self
1945                .multi_ring_step_time_ns(payload_bytes)
1946                .map(|step| step.saturating_mul((ranks - 1) as u64)),
1947            CollectiveKind::ReduceScatter => self
1948                .multi_ring_step_time_ns(chunk_bytes)
1949                .map(|step| step.saturating_mul((ranks - 1) as u64)),
1950            CollectiveKind::AllToAll => self.topology.pairwise_time_ns(
1951                &self.ring_order,
1952                chunk_bytes,
1953                self.ring_channels,
1954                &rail_order,
1955            ),
1956        }
1957    }
1958
1959    fn hierarchical_estimated_time_ns(
1960        &self,
1961        kind: CollectiveKind,
1962        payload_bytes: usize,
1963    ) -> Option<u64> {
1964        let groups = self.hierarchy_groups.as_ref()?;
1965        if kind == CollectiveKind::AllToAll {
1966            return None;
1967        }
1968        let leaders = groups.iter().map(|group| group[0]).collect::<Vec<_>>();
1969        let local_gather = groups
1970            .iter()
1971            .map(|group| {
1972                let leader = group[0];
1973                group[1..].iter().try_fold(0_u64, |total, rank| {
1974                    self.topology
1975                        .shortest_transfer_time_ns(*rank, leader, payload_bytes)
1976                        .map(|cost| total.saturating_add(cost))
1977                })
1978            })
1979            .collect::<Option<Vec<_>>>()?
1980            .into_iter()
1981            .max()
1982            .unwrap_or(0);
1983        let leader_count = leaders.len();
1984        let inter_collective = match kind {
1985            CollectiveKind::AllReduce => {
1986                let chunk_bytes = payload_bytes.div_ceil(leader_count);
1987                let step =
1988                    self.striped_ring_step_time_ns(&leaders, &vec![chunk_bytes; leader_count])?;
1989                step.saturating_mul((2 * (leader_count - 1)) as u64)
1990            }
1991            CollectiveKind::ReduceScatter => {
1992                let shard_bytes = payload_bytes.div_ceil(self.ring_order.len());
1993                let mut total = 0_u64;
1994                for step in 0..leader_count - 1 {
1995                    let bytes = (0..leader_count)
1996                        .map(|position| {
1997                            let group_index = (position + leader_count - 1 - step) % leader_count;
1998                            groups[group_index].len().saturating_mul(shard_bytes)
1999                        })
2000                        .collect::<Vec<_>>();
2001                    total = total.saturating_add(self.striped_ring_step_time_ns(&leaders, &bytes)?);
2002                }
2003                total
2004            }
2005            CollectiveKind::AllGather => {
2006                let mut total = 0_u64;
2007                for step in 0..leader_count - 1 {
2008                    let bytes = (0..leader_count)
2009                        .map(|position| {
2010                            let group_index = (position + leader_count - step) % leader_count;
2011                            groups[group_index].len().saturating_mul(payload_bytes)
2012                        })
2013                        .collect::<Vec<_>>();
2014                    total = total.saturating_add(self.striped_ring_step_time_ns(&leaders, &bytes)?);
2015                }
2016                total
2017            }
2018            CollectiveKind::AllToAll => unreachable!(),
2019        };
2020        let world_size = self.ring_order.len();
2021        let shard_bytes = payload_bytes.div_ceil(world_size);
2022        let local_distribute = groups
2023            .iter()
2024            .map(|group| {
2025                let leader = group[0];
2026                let bytes = match kind {
2027                    CollectiveKind::AllReduce => payload_bytes,
2028                    CollectiveKind::AllGather => payload_bytes.saturating_mul(world_size),
2029                    CollectiveKind::ReduceScatter => shard_bytes,
2030                    CollectiveKind::AllToAll => unreachable!(),
2031                };
2032                group[1..].iter().try_fold(0_u64, |total, rank| {
2033                    self.topology
2034                        .shortest_transfer_time_ns(leader, *rank, bytes)
2035                        .map(|cost| total.saturating_add(cost))
2036                })
2037            })
2038            .collect::<Option<Vec<_>>>()?
2039            .into_iter()
2040            .max()
2041            .unwrap_or(0);
2042        Some(
2043            local_gather
2044                .saturating_add(inter_collective)
2045                .saturating_add(local_distribute),
2046        )
2047    }
2048
2049    fn striped_ring_step_time_ns(&self, order: &[u32], bytes_by_position: &[usize]) -> Option<u64> {
2050        if order.len() != bytes_by_position.len() {
2051            return None;
2052        }
2053        let maximum_bytes = bytes_by_position.iter().copied().max().unwrap_or(0);
2054        let channels = self.ring_channels.min(maximum_bytes.max(1)).max(1);
2055        let rail_order = self.rail_order();
2056        let rails = rail_order.len().min(channels).max(1);
2057        let split = bytes_by_position
2058            .iter()
2059            .map(|bytes| balanced_sizes(*bytes, channels))
2060            .collect::<Vec<_>>();
2061        let split_by_channel = (0..channels)
2062            .map(|channel| {
2063                split
2064                    .iter()
2065                    .map(|position| position[channel])
2066                    .collect::<Vec<_>>()
2067            })
2068            .collect::<Vec<_>>();
2069        let mut rail_times = vec![0_u64; rails];
2070        let mut aggregate_bytes = vec![vec![0_usize; self.ring_order.len()]; self.ring_order.len()];
2071        for (channel, channel_bytes) in split_by_channel.into_iter().enumerate() {
2072            let rail_slot = channel % rails;
2073            let rail = rail_order[rail_slot];
2074            let mut slowest = 0_u64;
2075            for ((source, destination), bytes) in order
2076                .iter()
2077                .copied()
2078                .zip(order.iter().copied().cycle().skip(1))
2079                .take(order.len())
2080                .zip(channel_bytes)
2081            {
2082                aggregate_bytes[source as usize][destination as usize] =
2083                    aggregate_bytes[source as usize][destination as usize].saturating_add(bytes);
2084                slowest = slowest.max(self.topology.shortest_transfer_time_ns_on_rail(
2085                    source,
2086                    destination,
2087                    bytes,
2088                    rail,
2089                )?);
2090            }
2091            rail_times[rail_slot] = rail_times[rail_slot].saturating_add(slowest);
2092        }
2093        let rail_time = rail_times.into_iter().max().unwrap_or(0);
2094        let aggregate_time = if rails > 1 {
2095            self.topology.aggregate_transfer_cap_ns(&aggregate_bytes)
2096        } else {
2097            0
2098        };
2099        Some(rail_time.max(aggregate_time))
2100    }
2101
2102    fn multi_ring_step_time_ns(&self, bytes: usize) -> Option<u64> {
2103        let channels = self.ring_channels.min(bytes.max(1)).max(1);
2104        let rail_order = self.rail_order();
2105        let rails = rail_order.len().min(channels).max(1);
2106        let mut rail_times = vec![0_u64; rails];
2107        let mut aggregate_bytes = vec![vec![0_usize; self.ring_order.len()]; self.ring_order.len()];
2108        let base = bytes / channels;
2109        let remainder = bytes % channels;
2110        for channel in 0..channels {
2111            let channel_bytes = base + usize::from(channel < remainder);
2112            let rail_slot = channel % rails;
2113            let rail = rail_order[rail_slot];
2114            let order = self.ring_order_for_channel(channel);
2115            for (source, destination) in order
2116                .iter()
2117                .copied()
2118                .zip(order.iter().copied().cycle().skip(1))
2119                .take(order.len())
2120            {
2121                aggregate_bytes[source as usize][destination as usize] = aggregate_bytes
2122                    [source as usize][destination as usize]
2123                    .saturating_add(channel_bytes);
2124            }
2125            let step = self
2126                .topology
2127                .ring_step_time_ns_on_rail(order, channel_bytes, rail)?;
2128            rail_times[rail_slot] = rail_times[rail_slot].saturating_add(step);
2129        }
2130        let rail_time = rail_times.into_iter().max().unwrap_or(0);
2131        let aggregate_time = if rails > 1 {
2132            self.topology.aggregate_transfer_cap_ns(&aggregate_bytes)
2133        } else {
2134            0
2135        };
2136        Some(rail_time.max(aggregate_time))
2137    }
2138}
2139
2140fn parse_usize_environment(name: &'static str, default: usize) -> Result<usize, TopologyError> {
2141    match env::var(name) {
2142        Ok(value) => value
2143            .parse()
2144            .map_err(|_| TopologyError::InvalidEnvironment { name, value }),
2145        Err(env::VarError::NotPresent) => Ok(default),
2146        Err(env::VarError::NotUnicode(value)) => Err(TopologyError::InvalidEnvironment {
2147            name,
2148            value: value.to_string_lossy().into_owned(),
2149        }),
2150    }
2151}
2152
2153fn parse_u64_environment(name: &'static str, default: u64) -> Result<u64, TopologyError> {
2154    match env::var(name) {
2155        Ok(value) => value
2156            .parse()
2157            .map_err(|_| TopologyError::InvalidEnvironment { name, value }),
2158        Err(env::VarError::NotPresent) => Ok(default),
2159        Err(env::VarError::NotUnicode(value)) => Err(TopologyError::InvalidEnvironment {
2160            name,
2161            value: value.to_string_lossy().into_owned(),
2162        }),
2163    }
2164}
2165
2166fn parse_collective_transport_environment() -> Result<CollectiveTransport, TopologyError> {
2167    match env::var("GX1_COLLECTIVE_TRANSPORT") {
2168        Ok(value) => match value.trim().to_ascii_lowercase().as_str() {
2169            "coordinator" | "tcp_coordinator" | "tcp_host_staged" => {
2170                Ok(CollectiveTransport::TcpHostStaged)
2171            }
2172            "peer" | "tcp_peer" => Ok(CollectiveTransport::TcpPeer),
2173            _ => Err(TopologyError::InvalidEnvironment {
2174                name: "GX1_COLLECTIVE_TRANSPORT",
2175                value,
2176            }),
2177        },
2178        Err(env::VarError::NotPresent) => Ok(CollectiveTransport::TcpHostStaged),
2179        Err(env::VarError::NotUnicode(value)) => Err(TopologyError::InvalidEnvironment {
2180            name: "GX1_COLLECTIVE_TRANSPORT",
2181            value: value.to_string_lossy().into_owned(),
2182        }),
2183    }
2184}
2185
2186fn parse_ring_order(world_size: u32, value: &str) -> Result<Vec<u32>, TopologyError> {
2187    let order = value
2188        .split(',')
2189        .map(|rank| {
2190            rank.trim()
2191                .parse::<u32>()
2192                .map_err(|_| TopologyError::InvalidEnvironment {
2193                    name: "GX1_RING_ORDER",
2194                    value: value.to_owned(),
2195                })
2196        })
2197        .collect::<Result<Vec<_>, _>>()?;
2198    validate_ring_order(world_size, &order)?;
2199    Ok(order)
2200}
2201
2202fn parse_rail_order(rail_count: usize, value: &str) -> Result<Vec<usize>, TopologyError> {
2203    let order = value
2204        .split(',')
2205        .map(|rail| {
2206            rail.trim()
2207                .parse::<usize>()
2208                .map_err(|_| TopologyError::InvalidEnvironment {
2209                    name: "GX1_RAIL_ORDER",
2210                    value: value.to_owned(),
2211                })
2212        })
2213        .collect::<Result<Vec<_>, _>>()?;
2214    validate_rail_order(rail_count, &order)?;
2215    Ok(order)
2216}
2217
2218fn parse_hierarchy_groups(world_size: u32, value: &str) -> Result<Vec<Vec<u32>>, TopologyError> {
2219    let groups = value
2220        .split(';')
2221        .map(|group| {
2222            group
2223                .split(',')
2224                .filter(|rank| !rank.trim().is_empty())
2225                .map(|rank| {
2226                    rank.trim()
2227                        .parse::<u32>()
2228                        .map_err(|_| TopologyError::InvalidEnvironment {
2229                            name: "GX1_TOPOLOGY_GROUPS",
2230                            value: value.to_owned(),
2231                        })
2232                })
2233                .collect::<Result<Vec<_>, _>>()
2234        })
2235        .collect::<Result<Vec<_>, _>>()?;
2236    validate_hierarchy_groups(world_size, &groups)?;
2237    Ok(groups)
2238}
2239
2240fn validate_hierarchy_groups(world_size: u32, groups: &[Vec<u32>]) -> Result<(), TopologyError> {
2241    if groups.len() < 2 {
2242        return Err(TopologyError::InvalidHierarchy(
2243            "requires at least two non-empty groups".into(),
2244        ));
2245    }
2246    let mut seen = vec![false; world_size as usize];
2247    for (group_index, group) in groups.iter().enumerate() {
2248        if group.is_empty() {
2249            return Err(TopologyError::InvalidHierarchy(format!(
2250                "group {group_index} is empty"
2251            )));
2252        }
2253        for rank in group {
2254            if *rank >= world_size {
2255                return Err(TopologyError::RankOutOfRange {
2256                    rank: *rank,
2257                    world_size,
2258                });
2259            }
2260            if std::mem::replace(&mut seen[*rank as usize], true) {
2261                return Err(TopologyError::InvalidHierarchy(format!(
2262                    "rank {rank} appears more than once"
2263                )));
2264            }
2265        }
2266    }
2267    let missing = seen
2268        .iter()
2269        .enumerate()
2270        .filter_map(|(rank, included)| (!included).then_some(rank.to_string()))
2271        .collect::<Vec<_>>();
2272    if !missing.is_empty() {
2273        return Err(TopologyError::InvalidHierarchy(format!(
2274            "does not cover ranks [{}]",
2275            missing.join(",")
2276        )));
2277    }
2278    Ok(())
2279}
2280
2281fn parse_topology_links(world_size: u32, value: &str) -> Result<CollectiveTopology, TopologyError> {
2282    let mut topology = CollectiveTopology::empty(world_size)?;
2283    for encoded in value.split(',').filter(|link| !link.trim().is_empty()) {
2284        let (ranks, metrics) =
2285            encoded
2286                .trim()
2287                .split_once(':')
2288                .ok_or_else(|| TopologyError::InvalidEnvironment {
2289                    name: "GX1_TOPOLOGY_LINKS",
2290                    value: value.to_owned(),
2291                })?;
2292        let (first, second) =
2293            ranks
2294                .split_once('-')
2295                .ok_or_else(|| TopologyError::InvalidEnvironment {
2296                    name: "GX1_TOPOLOGY_LINKS",
2297                    value: value.to_owned(),
2298                })?;
2299        let (bandwidth, latency) =
2300            metrics
2301                .split_once(':')
2302                .ok_or_else(|| TopologyError::InvalidEnvironment {
2303                    name: "GX1_TOPOLOGY_LINKS",
2304                    value: value.to_owned(),
2305                })?;
2306        let parse = |part: &str| {
2307            part.trim()
2308                .parse::<u64>()
2309                .map_err(|_| TopologyError::InvalidEnvironment {
2310                    name: "GX1_TOPOLOGY_LINKS",
2311                    value: value.to_owned(),
2312                })
2313        };
2314        topology.add_link(TopologyLink {
2315            first_rank: first
2316                .trim()
2317                .parse()
2318                .map_err(|_| TopologyError::InvalidEnvironment {
2319                    name: "GX1_TOPOLOGY_LINKS",
2320                    value: value.to_owned(),
2321                })?,
2322            second_rank: second
2323                .trim()
2324                .parse()
2325                .map_err(|_| TopologyError::InvalidEnvironment {
2326                    name: "GX1_TOPOLOGY_LINKS",
2327                    value: value.to_owned(),
2328                })?,
2329            bandwidth_mbps: parse(bandwidth)?,
2330            latency_ns: parse(latency)?,
2331        })?;
2332    }
2333    Ok(topology)
2334}
2335
2336fn parse_topology_rail_links(
2337    topology: &mut CollectiveTopology,
2338    value: &str,
2339    p2p_rails: usize,
2340) -> Result<(), TopologyError> {
2341    for encoded in value.split(',').filter(|link| !link.trim().is_empty()) {
2342        let invalid = || TopologyError::InvalidEnvironment {
2343            name: "GX1_TOPOLOGY_RAIL_LINKS",
2344            value: value.to_owned(),
2345        };
2346        let (rail, link) = encoded.trim().split_once('@').ok_or_else(invalid)?;
2347        let rail = rail.trim().parse::<usize>().map_err(|_| invalid())?;
2348        if rail >= p2p_rails {
2349            return Err(invalid());
2350        }
2351        let (ranks, metrics) = link.split_once(':').ok_or_else(invalid)?;
2352        let (first, second) = ranks.split_once('-').ok_or_else(invalid)?;
2353        let (bandwidth, latency) = metrics.split_once(':').ok_or_else(invalid)?;
2354        topology.add_rail_link(TopologyRailLink {
2355            rail,
2356            first_rank: first.trim().parse().map_err(|_| invalid())?,
2357            second_rank: second.trim().parse().map_err(|_| invalid())?,
2358            bandwidth_mbps: bandwidth.trim().parse().map_err(|_| invalid())?,
2359            latency_ns: latency.trim().parse().map_err(|_| invalid())?,
2360        })?;
2361    }
2362    Ok(())
2363}
2364
2365fn parse_topology_aggregate_links(
2366    topology: &mut CollectiveTopology,
2367    value: &str,
2368) -> Result<(), TopologyError> {
2369    for encoded in value.split(',').filter(|link| !link.trim().is_empty()) {
2370        let invalid = || TopologyError::InvalidEnvironment {
2371            name: "GX1_TOPOLOGY_AGGREGATE_LINKS",
2372            value: value.to_owned(),
2373        };
2374        let (ranks, bandwidth) = encoded.trim().split_once(':').ok_or_else(invalid)?;
2375        let (first, second) = ranks.split_once('-').ok_or_else(invalid)?;
2376        topology.add_aggregate_link(TopologyAggregateLink {
2377            first_rank: first.trim().parse().map_err(|_| invalid())?,
2378            second_rank: second.trim().parse().map_err(|_| invalid())?,
2379            bandwidth_mbps: bandwidth.trim().parse().map_err(|_| invalid())?,
2380        })?;
2381    }
2382    Ok(())
2383}
2384
2385fn probed_topology_matches(current: &CollectiveTopology, detected: &CollectiveTopology) -> bool {
2386    if current.world_size != detected.world_size {
2387        return false;
2388    }
2389    for first in 0..current.world_size {
2390        for second in first + 1..current.world_size {
2391            match (current.link(first, second), detected.link(first, second)) {
2392                (None, None) => {}
2393                (Some(current), Some(detected))
2394                    if metric_within_factor(current.bandwidth_mbps, detected.bandwidth_mbps, 4)
2395                        && metric_within_factor(current.latency_ns, detected.latency_ns, 4) => {}
2396                _ => return false,
2397            }
2398        }
2399    }
2400    if current.rail_links.len() != detected.rail_links.len() {
2401        return false;
2402    }
2403    for (current_links, detected_links) in current.rail_links.iter().zip(&detected.rail_links) {
2404        for first in 0..current.world_size {
2405            for second in first + 1..current.world_size {
2406                match (
2407                    current_links[first as usize][second as usize],
2408                    detected_links[first as usize][second as usize],
2409                ) {
2410                    (None, None) => {}
2411                    (Some(current), Some(detected))
2412                        if metric_within_factor(
2413                            current.bandwidth_mbps,
2414                            detected.bandwidth_mbps,
2415                            4,
2416                        ) && metric_within_factor(
2417                            current.latency_ns,
2418                            detected.latency_ns,
2419                            4,
2420                        ) => {}
2421                    _ => return false,
2422                }
2423            }
2424        }
2425    }
2426    for first in 0..current.world_size {
2427        for second in first + 1..current.world_size {
2428            match (
2429                current.aggregate_bandwidth_mbps(first, second),
2430                detected.aggregate_bandwidth_mbps(first, second),
2431            ) {
2432                (None, None) => {}
2433                (Some(current), Some(detected)) if metric_within_factor(current, detected, 4) => {}
2434                _ => return false,
2435            }
2436        }
2437    }
2438    true
2439}
2440
2441fn metric_within_factor(first: u64, second: u64, factor: u64) -> bool {
2442    if first == 0 || second == 0 {
2443        return first == second;
2444    }
2445    let (smaller, larger) = if first < second {
2446        (first, second)
2447    } else {
2448        (second, first)
2449    };
2450    larger <= smaller.saturating_mul(factor)
2451}
2452
2453fn validate_ring_order(world_size: u32, order: &[u32]) -> Result<(), TopologyError> {
2454    if world_size == 0 {
2455        return Err(TopologyError::EmptyWorld);
2456    }
2457    if order.len() != world_size as usize {
2458        return Err(TopologyError::InvalidRing(format!(
2459            "contains {} ranks, expected {world_size}",
2460            order.len()
2461        )));
2462    }
2463    let mut seen = vec![false; world_size as usize];
2464    for rank in order {
2465        if *rank >= world_size {
2466            return Err(TopologyError::RankOutOfRange {
2467                rank: *rank,
2468                world_size,
2469            });
2470        }
2471        if std::mem::replace(&mut seen[*rank as usize], true) {
2472            return Err(TopologyError::InvalidRing(format!(
2473                "rank {rank} appears more than once"
2474            )));
2475        }
2476    }
2477    Ok(())
2478}
2479
2480fn validate_ring_channels(channels: usize) -> Result<(), TopologyError> {
2481    if channels == 0 || channels > MAX_RING_CHANNELS {
2482        return Err(TopologyError::InvalidRingChannels(channels));
2483    }
2484    Ok(())
2485}
2486
2487fn validate_p2p_rails(rails: usize) -> Result<(), TopologyError> {
2488    if rails == 0 || rails > MAX_P2P_RAILS {
2489        return Err(TopologyError::InvalidP2pRails(rails));
2490    }
2491    Ok(())
2492}
2493
2494fn validate_rail_order(rail_count: usize, order: &[usize]) -> Result<(), TopologyError> {
2495    if order.len() != rail_count {
2496        return Err(TopologyError::InvalidRailOrder(format!(
2497            "contains {} rails, expected {rail_count}",
2498            order.len()
2499        )));
2500    }
2501    let mut seen = vec![false; rail_count];
2502    for rail in order {
2503        if *rail >= rail_count {
2504            return Err(TopologyError::InvalidRailOrder(format!(
2505                "rail {rail} is outside configured rail count {rail_count}"
2506            )));
2507        }
2508        if std::mem::replace(&mut seen[*rail], true) {
2509            return Err(TopologyError::InvalidRailOrder(format!(
2510                "rail {rail} appears more than once"
2511            )));
2512        }
2513    }
2514    Ok(())
2515}
2516
2517#[cfg(test)]
2518mod tests {
2519    use super::*;
2520
2521    #[test]
2522    fn complete_four_rank_topology_builds_three_edge_balanced_rings() {
2523        let topology = CollectiveTopology::uniform(4).unwrap();
2524        let orders = topology.best_ring_orders(MAX_RING_CHANNELS).unwrap();
2525        assert_eq!(orders.len(), 3);
2526        let mut edge_counts = [[0_u32; 4]; 4];
2527        for order in &orders {
2528            validate_ring_order(4, order).unwrap();
2529            for index in 0..order.len() {
2530                let first = order[index] as usize;
2531                let second = order[(index + 1) % order.len()] as usize;
2532                edge_counts[first][second] += 1;
2533                edge_counts[second][first] += 1;
2534            }
2535        }
2536        for (first, counts) in edge_counts.iter().enumerate() {
2537            for (second, count) in counts.iter().enumerate().skip(first + 1) {
2538                assert_eq!(*count, 2, "edge {first}-{second}");
2539            }
2540        }
2541
2542        let tuning = CollectiveTuning::from_topology(
2543            AlgorithmPolicy::Ring,
2544            0,
2545            topology,
2546            Some(vec![0, 2, 1, 3]),
2547        )
2548        .unwrap();
2549        assert_eq!(tuning.ring_orders().len(), 3);
2550        assert_eq!(tuning.ring_order_for_channel(0), [0, 2, 1, 3]);
2551        assert_ne!(
2552            ring_edges(tuning.ring_order_for_channel(0)),
2553            ring_edges(tuning.ring_order_for_channel(1))
2554        );
2555    }
2556
2557    #[test]
2558    fn multi_ring_cost_serializes_channels_that_share_a_rail() {
2559        let base = CollectiveTuning::new(AlgorithmPolicy::Ring, 0, vec![0, 1]).unwrap();
2560        let single = base
2561            .clone()
2562            .with_ring_channels(1)
2563            .unwrap()
2564            .plan(CollectiveKind::AllReduce, 16 * 1024 * 1024)
2565            .peer_estimated_time_ns
2566            .unwrap();
2567        let shared_rail = base
2568            .clone()
2569            .with_ring_channels(4)
2570            .unwrap()
2571            .plan(CollectiveKind::AllReduce, 16 * 1024 * 1024)
2572            .peer_estimated_time_ns
2573            .unwrap();
2574        let independent_rails = base
2575            .with_ring_channels(4)
2576            .unwrap()
2577            .with_p2p_rails(4)
2578            .unwrap()
2579            .plan(CollectiveKind::AllReduce, 16 * 1024 * 1024)
2580            .peer_estimated_time_ns
2581            .unwrap();
2582        assert!(shared_rail > single);
2583        assert!(independent_rails < single);
2584    }
2585
2586    #[test]
2587    fn multi_ring_cost_uses_the_link_metrics_for_each_assigned_rail() {
2588        let build = |second_rail_bandwidth: u64| {
2589            let mut topology = CollectiveTopology::empty(2).unwrap();
2590            topology
2591                .add_link(TopologyLink {
2592                    first_rank: 0,
2593                    second_rank: 1,
2594                    bandwidth_mbps: second_rail_bandwidth.min(100_000),
2595                    latency_ns: 100,
2596                })
2597                .unwrap();
2598            for (rail, bandwidth_mbps) in [(0, 100_000), (1, second_rail_bandwidth)] {
2599                topology
2600                    .add_rail_link(TopologyRailLink {
2601                        rail,
2602                        first_rank: 0,
2603                        second_rank: 1,
2604                        bandwidth_mbps,
2605                        latency_ns: 100,
2606                    })
2607                    .unwrap();
2608            }
2609            CollectiveTuning::from_topology(AlgorithmPolicy::Ring, 0, topology, None)
2610                .unwrap()
2611                .with_ring_channels(2)
2612                .unwrap()
2613                .with_p2p_rails(2)
2614                .unwrap()
2615        };
2616        let balanced = build(100_000);
2617        let slow_second_rail = build(1_000);
2618        let balanced_time = balanced.multi_ring_step_time_ns(16 * 1024 * 1024).unwrap();
2619        let heterogeneous_time = slow_second_rail
2620            .multi_ring_step_time_ns(16 * 1024 * 1024)
2621            .unwrap();
2622        assert!(heterogeneous_time > balanced_time.saturating_mul(50));
2623        assert_ne!(
2624            balanced.topology_fingerprint(),
2625            slow_second_rail.topology_fingerprint()
2626        );
2627    }
2628
2629    #[test]
2630    fn concurrent_rail_aggregate_capacity_caps_ring_pairwise_and_hierarchy_costs() {
2631        let build = |aggregate_bandwidth_mbps: Option<u64>, channels: usize| {
2632            let mut topology = CollectiveTopology::empty(4).unwrap();
2633            for first_rank in 0..4_u32 {
2634                for second_rank in first_rank + 1..4_u32 {
2635                    topology
2636                        .add_link(TopologyLink {
2637                            first_rank,
2638                            second_rank,
2639                            bandwidth_mbps: 100_000,
2640                            latency_ns: 100,
2641                        })
2642                        .unwrap();
2643                    for rail in 0..2 {
2644                        topology
2645                            .add_rail_link(TopologyRailLink {
2646                                rail,
2647                                first_rank,
2648                                second_rank,
2649                                bandwidth_mbps: 100_000,
2650                                latency_ns: 100,
2651                            })
2652                            .unwrap();
2653                    }
2654                    if let Some(bandwidth_mbps) = aggregate_bandwidth_mbps {
2655                        topology
2656                            .add_aggregate_link(TopologyAggregateLink {
2657                                first_rank,
2658                                second_rank,
2659                                bandwidth_mbps,
2660                            })
2661                            .unwrap();
2662                    }
2663                }
2664            }
2665            CollectiveTuning::from_topology(
2666                AlgorithmPolicy::Ring,
2667                0,
2668                topology,
2669                Some(vec![0, 1, 2, 3]),
2670            )
2671            .unwrap()
2672            .with_ring_channels(channels)
2673            .unwrap()
2674            .with_p2p_rails(2)
2675            .unwrap()
2676        };
2677        let payload = 64 * 1024 * 1024;
2678        let uncapped = build(None, 2);
2679        let capped = build(Some(1_000), 2);
2680        assert!(
2681            capped.multi_ring_step_time_ns(payload).unwrap()
2682                > uncapped
2683                    .multi_ring_step_time_ns(payload)
2684                    .unwrap()
2685                    .saturating_mul(20)
2686        );
2687        assert!(
2688            capped
2689                .topology
2690                .pairwise_time_ns(&capped.ring_order, payload / 4, 2, &capped.rail_order())
2691                .unwrap()
2692                > uncapped
2693                    .topology
2694                    .pairwise_time_ns(&uncapped.ring_order, payload / 4, 2, &uncapped.rail_order(),)
2695                    .unwrap()
2696                    .saturating_mul(20)
2697        );
2698
2699        let uncapped_hierarchy = uncapped
2700            .with_hierarchy(vec![vec![0], vec![1], vec![2], vec![3]])
2701            .unwrap();
2702        let capped_hierarchy = capped
2703            .with_hierarchy(vec![vec![0], vec![1], vec![2], vec![3]])
2704            .unwrap();
2705        assert!(
2706            capped_hierarchy
2707                .hierarchical_estimated_time_ns(CollectiveKind::AllReduce, payload)
2708                .unwrap()
2709                > uncapped_hierarchy
2710                    .hierarchical_estimated_time_ns(CollectiveKind::AllReduce, payload)
2711                    .unwrap()
2712                    .saturating_mul(20)
2713        );
2714
2715        let uncapped_single = build(None, 1);
2716        let capped_single = build(Some(1_000), 1);
2717        assert_eq!(
2718            capped_single.multi_ring_step_time_ns(payload),
2719            uncapped_single.multi_ring_step_time_ns(payload)
2720        );
2721        assert_ne!(
2722            capped_single.topology_fingerprint(),
2723            uncapped_single.topology_fingerprint()
2724        );
2725    }
2726
2727    #[test]
2728    fn topology_aware_rail_order_skips_slow_noncontiguous_rails_first() {
2729        let mut topology = CollectiveTopology::empty(2).unwrap();
2730        topology
2731            .add_link(TopologyLink {
2732                first_rank: 0,
2733                second_rank: 1,
2734                bandwidth_mbps: 1_000,
2735                latency_ns: 1_000,
2736            })
2737            .unwrap();
2738        for (rail, bandwidth_mbps, latency_ns) in [
2739            (0, 100_000, 100),
2740            (1, 1_000, 1_000),
2741            (2, 80_000, 150),
2742            (3, 2_000, 900),
2743        ] {
2744            topology
2745                .add_rail_link(TopologyRailLink {
2746                    rail,
2747                    first_rank: 0,
2748                    second_rank: 1,
2749                    bandwidth_mbps,
2750                    latency_ns,
2751                })
2752                .unwrap();
2753        }
2754        let automatic = CollectiveTuning::from_topology(AlgorithmPolicy::Ring, 0, topology, None)
2755            .unwrap()
2756            .with_p2p_rails(4)
2757            .unwrap()
2758            .with_ring_channels(2)
2759            .unwrap();
2760        assert_eq!(automatic.rail_order(), vec![0, 2, 3, 1]);
2761        assert_eq!(
2762            (0..6)
2763                .map(|channel| automatic.rail_for_channel(channel))
2764                .collect::<Vec<_>>(),
2765            vec![0, 2, 3, 1, 0, 2]
2766        );
2767
2768        let same_explicit = automatic.clone().with_rail_order(vec![0, 2, 3, 1]).unwrap();
2769        assert_eq!(
2770            automatic.topology_fingerprint(),
2771            same_explicit.topology_fingerprint()
2772        );
2773        let natural = automatic.clone().with_rail_order(vec![0, 1, 2, 3]).unwrap();
2774        let automatic_time = automatic
2775            .plan(CollectiveKind::AllReduce, 16 * 1024 * 1024)
2776            .peer_estimated_time_ns
2777            .unwrap();
2778        let natural_time = natural
2779            .plan(CollectiveKind::AllReduce, 16 * 1024 * 1024)
2780            .peer_estimated_time_ns
2781            .unwrap();
2782        assert!(natural_time > automatic_time.saturating_mul(50));
2783        assert_ne!(
2784            automatic.topology_fingerprint(),
2785            natural.topology_fingerprint()
2786        );
2787
2788        assert!(matches!(
2789            automatic.clone().with_rail_order(vec![0, 1, 1, 3]),
2790            Err(TopologyError::InvalidRailOrder(_))
2791        ));
2792        assert!(matches!(
2793            automatic.clone().with_rail_order(vec![0, 1, 2]),
2794            Err(TopologyError::InvalidRailOrder(_))
2795        ));
2796        assert!(matches!(
2797            automatic.with_rail_order(vec![0, 1, 2, 4]),
2798            Err(TopologyError::InvalidRailOrder(_))
2799        ));
2800    }
2801
2802    #[test]
2803    fn pairwise_cost_uses_the_link_metrics_for_each_assigned_rail() {
2804        let build = |second_rail_bandwidth: u64, channels: usize| {
2805            let mut topology = CollectiveTopology::empty(2).unwrap();
2806            topology
2807                .add_link(TopologyLink {
2808                    first_rank: 0,
2809                    second_rank: 1,
2810                    bandwidth_mbps: 100_000,
2811                    latency_ns: 100,
2812                })
2813                .unwrap();
2814            for (rail, bandwidth_mbps) in [(0, 100_000), (1, second_rail_bandwidth)] {
2815                topology
2816                    .add_rail_link(TopologyRailLink {
2817                        rail,
2818                        first_rank: 0,
2819                        second_rank: 1,
2820                        bandwidth_mbps,
2821                        latency_ns: 100,
2822                    })
2823                    .unwrap();
2824            }
2825            CollectiveTuning::from_topology(AlgorithmPolicy::Ring, 0, topology, None)
2826                .unwrap()
2827                .with_ring_channels(channels)
2828                .unwrap()
2829                .with_p2p_rails(2)
2830                .unwrap()
2831                .plan(CollectiveKind::AllToAll, 16 * 1024 * 1024)
2832                .peer_estimated_time_ns
2833                .unwrap()
2834        };
2835
2836        let balanced = build(100_000, 2);
2837        let slow_second_rail = build(1_000, 2);
2838        let rail_zero_only = build(1_000, 1);
2839        assert!(slow_second_rail > balanced.saturating_mul(50));
2840        assert!(slow_second_rail > rail_zero_only.saturating_mul(40));
2841    }
2842
2843    #[test]
2844    fn hierarchical_inter_leader_cost_uses_assigned_rail_metrics() {
2845        let build = |second_rail_bandwidth: u64, channels: usize| {
2846            let mut topology = CollectiveTopology::empty(4).unwrap();
2847            for first_rank in 0..4_u32 {
2848                for second_rank in first_rank + 1..4_u32 {
2849                    topology
2850                        .add_link(TopologyLink {
2851                            first_rank,
2852                            second_rank,
2853                            bandwidth_mbps: 100_000,
2854                            latency_ns: 100,
2855                        })
2856                        .unwrap();
2857                    for (rail, bandwidth_mbps) in [(0, 100_000), (1, second_rail_bandwidth)] {
2858                        topology
2859                            .add_rail_link(TopologyRailLink {
2860                                rail,
2861                                first_rank,
2862                                second_rank,
2863                                bandwidth_mbps,
2864                                latency_ns: 100,
2865                            })
2866                            .unwrap();
2867                    }
2868                }
2869            }
2870            CollectiveTuning::from_topology(
2871                AlgorithmPolicy::Hierarchical,
2872                0,
2873                topology,
2874                Some(vec![0, 1, 2, 3]),
2875            )
2876            .unwrap()
2877            .with_hierarchy(vec![vec![0, 1], vec![2, 3]])
2878            .unwrap()
2879            .with_ring_channels(channels)
2880            .unwrap()
2881            .with_p2p_rails(2)
2882            .unwrap()
2883        };
2884
2885        let balanced = build(100_000, 2);
2886        let slow_second_rail = build(1_000, 2);
2887        let rail_zero_only = build(1_000, 1);
2888        for kind in [
2889            CollectiveKind::AllReduce,
2890            CollectiveKind::AllGather,
2891            CollectiveKind::ReduceScatter,
2892        ] {
2893            let balanced_time = balanced
2894                .plan(kind, 64 * 1024 * 1024)
2895                .hierarchical_estimated_time_ns
2896                .unwrap();
2897            let heterogeneous_time = slow_second_rail
2898                .plan(kind, 64 * 1024 * 1024)
2899                .hierarchical_estimated_time_ns
2900                .unwrap();
2901            let rail_zero_time = rail_zero_only
2902                .plan(kind, 64 * 1024 * 1024)
2903                .hierarchical_estimated_time_ns
2904                .unwrap();
2905            assert!(heterogeneous_time > balanced_time.saturating_mul(4));
2906            assert!(heterogeneous_time > rail_zero_time.saturating_mul(4));
2907        }
2908    }
2909
2910    #[test]
2911    fn explicit_rail_topology_parser_validates_the_configured_rail_count() {
2912        let mut topology = CollectiveTopology::uniform(2).unwrap();
2913        parse_topology_rail_links(&mut topology, "0@0-1:100000:100,1@0-1:25000:900", 2).unwrap();
2914        assert_eq!(
2915            topology.rail_links(),
2916            vec![
2917                TopologyRailLink {
2918                    rail: 0,
2919                    first_rank: 0,
2920                    second_rank: 1,
2921                    bandwidth_mbps: 100_000,
2922                    latency_ns: 100,
2923                },
2924                TopologyRailLink {
2925                    rail: 1,
2926                    first_rank: 0,
2927                    second_rank: 1,
2928                    bandwidth_mbps: 25_000,
2929                    latency_ns: 900,
2930                },
2931            ]
2932        );
2933        assert!(parse_topology_rail_links(&mut topology, "2@0-1:1:1", 2).is_err());
2934    }
2935
2936    #[test]
2937    fn explicit_aggregate_topology_parser_validates_and_preserves_bandwidth() {
2938        let mut topology = CollectiveTopology::uniform(3).unwrap();
2939        parse_topology_aggregate_links(&mut topology, "0-1:150000,1-2:75000").unwrap();
2940        assert_eq!(
2941            topology.aggregate_links(),
2942            vec![
2943                TopologyAggregateLink {
2944                    first_rank: 0,
2945                    second_rank: 1,
2946                    bandwidth_mbps: 150_000,
2947                },
2948                TopologyAggregateLink {
2949                    first_rank: 1,
2950                    second_rank: 2,
2951                    bandwidth_mbps: 75_000,
2952                },
2953            ]
2954        );
2955        assert!(parse_topology_aggregate_links(&mut topology, "0-1:0").is_err());
2956        assert!(parse_topology_aggregate_links(&mut topology, "0@1:100").is_err());
2957    }
2958
2959    #[test]
2960    fn topology_prefers_the_high_bandwidth_closed_ring() {
2961        let mut topology = CollectiveTopology::empty(4).unwrap();
2962        for (first, second, bandwidth) in [
2963            (0, 1, 100_000),
2964            (1, 2, 100_000),
2965            (2, 3, 100_000),
2966            (3, 0, 100_000),
2967            (0, 2, 10_000),
2968            (1, 3, 10_000),
2969        ] {
2970            topology
2971                .add_link(TopologyLink {
2972                    first_rank: first,
2973                    second_rank: second,
2974                    bandwidth_mbps: bandwidth,
2975                    latency_ns: 500,
2976                })
2977                .unwrap();
2978        }
2979        let ring = topology.best_ring_order().unwrap();
2980        let edges = (0..ring.len())
2981            .map(|index| {
2982                let first = ring[index];
2983                let second = ring[(index + 1) % ring.len()];
2984                (first.min(second), first.max(second))
2985            })
2986            .collect::<Vec<_>>();
2987        assert!(edges.iter().all(|edge| !matches!(edge, (0, 2) | (1, 3))));
2988    }
2989
2990    #[test]
2991    fn tuning_selects_direct_for_small_and_ring_for_large_payloads() {
2992        let tuning = CollectiveTuning::new(AlgorithmPolicy::Auto, 1024, vec![0, 1]).unwrap();
2993        assert_eq!(
2994            tuning.all_reduce_algorithm(1023),
2995            CollectiveAlgorithm::Direct
2996        );
2997        assert_eq!(tuning.all_reduce_algorithm(1024), CollectiveAlgorithm::Ring);
2998    }
2999
3000    #[test]
3001    fn auto_planner_uses_collective_specific_peer_algorithms_and_costs() {
3002        let tuning = CollectiveTuning::new(AlgorithmPolicy::Auto, 0, vec![0, 1, 2, 3]).unwrap();
3003        for kind in [
3004            CollectiveKind::AllReduce,
3005            CollectiveKind::AllGather,
3006            CollectiveKind::ReduceScatter,
3007        ] {
3008            let plan = tuning.plan(kind, 4 * 1024 * 1024);
3009            assert_eq!(plan.algorithm, CollectiveAlgorithm::Ring);
3010            assert!(plan.peer_estimated_time_ns.unwrap() < plan.direct_estimated_time_ns);
3011            assert_eq!(plan.estimated_time_ns, plan.peer_estimated_time_ns.unwrap());
3012        }
3013        let all_to_all = tuning.plan(CollectiveKind::AllToAll, 4 * 1024 * 1024);
3014        assert_eq!(all_to_all.algorithm, CollectiveAlgorithm::Pairwise);
3015        assert!(all_to_all.peer_estimated_time_ns.unwrap() < all_to_all.direct_estimated_time_ns);
3016    }
3017
3018    #[test]
3019    fn auto_planner_keeps_direct_when_peer_links_are_slower() {
3020        let mut topology = CollectiveTopology::empty(2).unwrap();
3021        topology
3022            .add_link(TopologyLink {
3023                first_rank: 0,
3024                second_rank: 1,
3025                bandwidth_mbps: 1,
3026                latency_ns: 1_000_000,
3027            })
3028            .unwrap();
3029        let tuning = CollectiveTuning::from_topology(AlgorithmPolicy::Auto, 0, topology, None)
3030            .unwrap()
3031            .with_direct_transport(100_000, 100)
3032            .unwrap();
3033        let plan = tuning.plan(CollectiveKind::AllReduce, 1024 * 1024);
3034        assert_eq!(plan.algorithm, CollectiveAlgorithm::Direct);
3035        assert!(plan.direct_estimated_time_ns < plan.peer_estimated_time_ns.unwrap());
3036    }
3037
3038    #[test]
3039    fn hierarchy_validation_requires_unique_full_rank_coverage() {
3040        let tuning = CollectiveTuning::new(AlgorithmPolicy::Auto, 0, vec![0, 1, 2, 3]).unwrap();
3041        assert!(matches!(
3042            tuning
3043                .clone()
3044                .with_hierarchy(vec![vec![0, 1], vec![1, 2, 3]]),
3045            Err(TopologyError::InvalidHierarchy(_))
3046        ));
3047        assert!(matches!(
3048            tuning.clone().with_hierarchy(vec![vec![0, 1], vec![2]]),
3049            Err(TopologyError::InvalidHierarchy(_))
3050        ));
3051        assert!(matches!(
3052            tuning.with_hierarchy(vec![vec![0, 1, 2, 3]]),
3053            Err(TopologyError::InvalidHierarchy(_))
3054        ));
3055    }
3056
3057    #[test]
3058    fn auto_planner_selects_hierarchy_for_fast_local_and_slow_cross_group_links() {
3059        let mut topology = CollectiveTopology::empty(4).unwrap();
3060        for first_rank in 0..4_u32 {
3061            for second_rank in first_rank + 1..4_u32 {
3062                let local = (first_rank < 2) == (second_rank < 2);
3063                topology
3064                    .add_link(TopologyLink {
3065                        first_rank,
3066                        second_rank,
3067                        bandwidth_mbps: if local { 200_000 } else { 10_000 },
3068                        latency_ns: if local { 200 } else { 50_000 },
3069                    })
3070                    .unwrap();
3071            }
3072        }
3073        let tuning = CollectiveTuning::from_topology(
3074            AlgorithmPolicy::Auto,
3075            0,
3076            topology,
3077            Some(vec![0, 1, 2, 3]),
3078        )
3079        .unwrap()
3080        .with_direct_transport(1_000, 100_000)
3081        .unwrap()
3082        .with_hierarchy(vec![vec![0, 1], vec![2, 3]])
3083        .unwrap();
3084
3085        for kind in [
3086            CollectiveKind::AllReduce,
3087            CollectiveKind::AllGather,
3088            CollectiveKind::ReduceScatter,
3089        ] {
3090            let plan = tuning.plan(kind, 16 * 1024 * 1024);
3091            assert_eq!(plan.algorithm, CollectiveAlgorithm::Hierarchical);
3092            assert!(
3093                plan.hierarchical_estimated_time_ns.unwrap() < plan.peer_estimated_time_ns.unwrap()
3094            );
3095            assert_eq!(
3096                plan.estimated_time_ns,
3097                plan.hierarchical_estimated_time_ns.unwrap()
3098            );
3099        }
3100        let all_to_all = tuning.plan(CollectiveKind::AllToAll, 16 * 1024 * 1024);
3101        assert_eq!(all_to_all.algorithm, CollectiveAlgorithm::Pairwise);
3102        assert_eq!(all_to_all.hierarchical_estimated_time_ns, None);
3103    }
3104
3105    #[test]
3106    fn inferred_hierarchy_finds_fast_domains_and_selects_low_cost_leaders() {
3107        let mut topology = CollectiveTopology::empty(6).unwrap();
3108        for first_rank in 0..6_u32 {
3109            for second_rank in first_rank + 1..6_u32 {
3110                let first_domain = first_rank / 3;
3111                let second_domain = second_rank / 3;
3112                let (bandwidth_mbps, latency_ns) = if first_domain != second_domain {
3113                    (10_000, 50_000)
3114                } else if first_rank.abs_diff(second_rank) == 1 {
3115                    (200_000, 200)
3116                } else {
3117                    (120_000, 300)
3118                };
3119                topology
3120                    .add_link(TopologyLink {
3121                        first_rank,
3122                        second_rank,
3123                        bandwidth_mbps,
3124                        latency_ns,
3125                    })
3126                    .unwrap();
3127            }
3128        }
3129        let base = CollectiveTuning::from_topology(AlgorithmPolicy::Auto, 0, topology, None)
3130            .unwrap()
3131            .with_ring_channels(2)
3132            .unwrap()
3133            .with_p2p_rails(2)
3134            .unwrap();
3135        let inferred = base
3136            .clone()
3137            .with_inferred_hierarchy(16 * 1024 * 1024)
3138            .unwrap();
3139        assert_eq!(
3140            inferred.hierarchy_groups(),
3141            Some([vec![1, 0, 2], vec![4, 3, 5]].as_slice())
3142        );
3143        for kind in [
3144            CollectiveKind::AllReduce,
3145            CollectiveKind::AllGather,
3146            CollectiveKind::ReduceScatter,
3147        ] {
3148            let plan = inferred.plan(kind, 16 * 1024 * 1024);
3149            assert_eq!(plan.algorithm, CollectiveAlgorithm::Hierarchical);
3150            assert!(
3151                plan.hierarchical_estimated_time_ns.unwrap() < plan.peer_estimated_time_ns.unwrap()
3152            );
3153        }
3154        let explicit = base
3155            .with_hierarchy(vec![vec![1, 0, 2], vec![4, 3, 5]])
3156            .unwrap();
3157        assert_eq!(
3158            inferred.topology_fingerprint(),
3159            explicit.topology_fingerprint()
3160        );
3161    }
3162
3163    #[test]
3164    fn inferred_hierarchy_rejects_uniform_topology_without_distinct_domains() {
3165        let tuning = CollectiveTuning::new(AlgorithmPolicy::Auto, 0, vec![0, 1, 2, 3]).unwrap();
3166        assert!(matches!(
3167            tuning.with_inferred_hierarchy(16 * 1024 * 1024),
3168            Err(TopologyError::InvalidHierarchy(message))
3169                if message.contains("no distinct fast-link domains")
3170        ));
3171    }
3172
3173    #[test]
3174    fn hierarchy_changes_the_autotune_fingerprint() {
3175        let tuning = CollectiveTuning::new(AlgorithmPolicy::Auto, 0, vec![0, 1, 2, 3]).unwrap();
3176        let flat = tuning.topology_fingerprint();
3177        let hierarchical = tuning
3178            .with_hierarchy(vec![vec![0, 1], vec![2, 3]])
3179            .unwrap()
3180            .topology_fingerprint();
3181        assert_ne!(flat, hierarchical);
3182    }
3183
3184    #[test]
3185    fn ring_channel_count_is_validated_without_changing_hardware_fingerprint() {
3186        let tuning = CollectiveTuning::new(AlgorithmPolicy::Ring, 0, vec![0, 1]).unwrap();
3187        assert!(matches!(
3188            tuning.clone().with_ring_channels(0),
3189            Err(TopologyError::InvalidRingChannels(0))
3190        ));
3191        assert!(matches!(
3192            tuning.clone().with_ring_channels(MAX_RING_CHANNELS + 1),
3193            Err(TopologyError::InvalidRingChannels(_))
3194        ));
3195        let original_fingerprint = tuning.topology_fingerprint();
3196        let tuning = tuning.with_ring_channels(4).unwrap();
3197        assert_eq!(tuning.ring_channels(), 4);
3198        assert_eq!(
3199            tuning.plan(CollectiveKind::AllReduce, 1024).ring_channels,
3200            4
3201        );
3202        assert_eq!(original_fingerprint, tuning.topology_fingerprint());
3203    }
3204
3205    #[test]
3206    fn point_to_point_rails_are_validated_and_change_hardware_fingerprint() {
3207        let tuning = CollectiveTuning::new(AlgorithmPolicy::Ring, 0, vec![0, 1]).unwrap();
3208        assert!(matches!(
3209            tuning.clone().with_p2p_rails(0),
3210            Err(TopologyError::InvalidP2pRails(0))
3211        ));
3212        assert!(matches!(
3213            tuning.clone().with_p2p_rails(MAX_P2P_RAILS + 1),
3214            Err(TopologyError::InvalidP2pRails(_))
3215        ));
3216        let original_fingerprint = tuning.topology_fingerprint();
3217        let single_rail_time = tuning
3218            .clone()
3219            .with_ring_channels(4)
3220            .unwrap()
3221            .plan(CollectiveKind::AllReduce, 16 * 1024 * 1024)
3222            .peer_estimated_time_ns
3223            .unwrap();
3224        let tuning = tuning.with_p2p_rails(4).unwrap();
3225        assert_eq!(tuning.p2p_rails(), 4);
3226        assert_ne!(original_fingerprint, tuning.topology_fingerprint());
3227        let four_rail_time = tuning
3228            .with_ring_channels(4)
3229            .unwrap()
3230            .plan(CollectiveKind::AllReduce, 16 * 1024 * 1024)
3231            .peer_estimated_time_ns
3232            .unwrap();
3233        assert!(four_rail_time < single_rail_time);
3234    }
3235
3236    #[test]
3237    fn tcp_peer_transport_changes_execution_but_not_hardware_fingerprint() {
3238        let coordinator = CollectiveTuning::new(AlgorithmPolicy::Ring, 0, vec![0, 1, 2]).unwrap();
3239        let peer = coordinator
3240            .clone()
3241            .with_transport(CollectiveTransport::TcpPeer)
3242            .unwrap();
3243        assert_eq!(peer.transport(), CollectiveTransport::TcpPeer);
3244        assert_eq!(
3245            coordinator.topology_fingerprint(),
3246            peer.topology_fingerprint()
3247        );
3248        assert_ne!(
3249            coordinator.execution_fingerprint(),
3250            peer.execution_fingerprint()
3251        );
3252    }
3253
3254    #[test]
3255    fn pairwise_estimate_routes_across_sparse_connected_topology() {
3256        let mut topology = CollectiveTopology::empty(4).unwrap();
3257        for (first_rank, second_rank) in [(0, 1), (1, 2), (2, 3), (3, 0)] {
3258            topology
3259                .add_link(TopologyLink {
3260                    first_rank,
3261                    second_rank,
3262                    bandwidth_mbps: 50_000,
3263                    latency_ns: 400,
3264                })
3265                .unwrap();
3266        }
3267        let tuning = CollectiveTuning::from_topology(
3268            AlgorithmPolicy::Ring,
3269            0,
3270            topology,
3271            Some(vec![0, 1, 2, 3]),
3272        )
3273        .unwrap();
3274        let plan = tuning.plan(CollectiveKind::AllToAll, 4096);
3275        assert_eq!(plan.algorithm, CollectiveAlgorithm::Pairwise);
3276        assert!(plan.peer_estimated_time_ns.is_some());
3277        assert_ne!(plan.estimated_time_ns, u64::MAX);
3278    }
3279
3280    #[test]
3281    fn autotune_profile_overrides_model_only_for_measured_ranges() {
3282        let tuning =
3283            CollectiveTuning::new(AlgorithmPolicy::Auto, 16 * 1024 * 1024, vec![0, 1]).unwrap();
3284        assert_eq!(
3285            tuning
3286                .plan(CollectiveKind::AllReduce, 1024 * 1024)
3287                .algorithm,
3288            CollectiveAlgorithm::Direct
3289        );
3290        let profile = CollectiveAutotuneProfile {
3291            version: COLLECTIVE_AUTOTUNE_PROFILE_VERSION,
3292            world_size: 2,
3293            topology_fingerprint: tuning.topology_fingerprint_hex(),
3294            execution_fingerprint: tuning.execution_fingerprint_hex(),
3295            entries: vec![CollectiveAutotuneEntry {
3296                operation: CollectiveKind::AllReduce,
3297                min_payload_bytes: 512 * 1024,
3298                max_payload_bytes: 2 * 1024 * 1024,
3299                algorithm: CollectiveAlgorithm::Ring,
3300                ring_channels: 3,
3301                measured_time_ns: 123_456,
3302            }],
3303        };
3304        let tuning = tuning.with_autotune_profile(profile).unwrap();
3305        let measured = tuning.plan(CollectiveKind::AllReduce, 1024 * 1024);
3306        assert_eq!(measured.algorithm, CollectiveAlgorithm::Ring);
3307        assert_eq!(measured.source, CollectivePlanSource::Autotune);
3308        assert_eq!(measured.ring_channels, 3);
3309        assert_eq!(measured.estimated_time_ns, 123_456);
3310        let unmeasured = tuning.plan(CollectiveKind::AllReduce, 4 * 1024 * 1024);
3311        assert_eq!(unmeasured.algorithm, CollectiveAlgorithm::Direct);
3312        assert_eq!(unmeasured.source, CollectivePlanSource::Model);
3313    }
3314
3315    #[test]
3316    fn probed_profile_accepts_one_adjacent_metric_bucket_but_not_a_topology_change() {
3317        let topology = |bandwidth_mbps, latency_ns| {
3318            let mut topology = CollectiveTopology::empty(2).unwrap();
3319            topology
3320                .add_link(TopologyLink {
3321                    first_rank: 0,
3322                    second_rank: 1,
3323                    bandwidth_mbps,
3324                    latency_ns,
3325                })
3326                .unwrap();
3327            for rail in 0..2 {
3328                topology
3329                    .add_rail_link(TopologyRailLink {
3330                        rail,
3331                        first_rank: 0,
3332                        second_rank: 1,
3333                        bandwidth_mbps,
3334                        latency_ns,
3335                    })
3336                    .unwrap();
3337            }
3338            topology
3339        };
3340        let pinned =
3341            CollectiveTuning::from_topology(AlgorithmPolicy::Auto, 0, topology(64, 65_536), None)
3342                .unwrap()
3343                .with_transport(CollectiveTransport::TcpPeer)
3344                .unwrap()
3345                .with_p2p_rails(2)
3346                .unwrap();
3347        let profile = CollectiveAutotuneProfile {
3348            version: COLLECTIVE_AUTOTUNE_PROFILE_VERSION,
3349            world_size: 2,
3350            topology_fingerprint: pinned.topology_fingerprint_hex(),
3351            execution_fingerprint: pinned.execution_fingerprint_hex(),
3352            entries: vec![CollectiveAutotuneEntry {
3353                operation: CollectiveKind::AllReduce,
3354                min_payload_bytes: 0,
3355                max_payload_bytes: u64::MAX,
3356                algorithm: CollectiveAlgorithm::Ring,
3357                ring_channels: 1,
3358                measured_time_ns: 1,
3359            }],
3360        };
3361        let adjacent =
3362            CollectiveTuning::from_topology(AlgorithmPolicy::Auto, 0, topology(256, 262_144), None)
3363                .unwrap()
3364                .with_transport(CollectiveTransport::TcpPeer)
3365                .unwrap()
3366                .with_p2p_rails(2)
3367                .unwrap();
3368        assert!(
3369            adjacent
3370                .clone()
3371                .with_autotune_profile(profile.clone())
3372                .is_err()
3373        );
3374        adjacent
3375            .validate_autotune_profile_with_detected(
3376                &profile,
3377                Some("0-1:64:65536"),
3378                Some("0@0-1:64:65536,1@0-1:64:65536"),
3379                None,
3380                true,
3381            )
3382            .unwrap();
3383
3384        let changed = CollectiveTuning::from_topology(
3385            AlgorithmPolicy::Auto,
3386            0,
3387            topology(1024, 1_048_576),
3388            None,
3389        )
3390        .unwrap()
3391        .with_transport(CollectiveTransport::TcpPeer)
3392        .unwrap()
3393        .with_p2p_rails(2)
3394        .unwrap();
3395        assert!(
3396            changed
3397                .validate_autotune_profile_with_detected(
3398                    &profile,
3399                    Some("0-1:64:65536"),
3400                    Some("0@0-1:64:65536,1@0-1:64:65536"),
3401                    None,
3402                    true,
3403                )
3404                .is_err()
3405        );
3406    }
3407
3408    #[test]
3409    fn probed_profile_tolerates_adjacent_aggregate_capacity_but_requires_the_cap() {
3410        let topology = |aggregate_bandwidth_mbps| {
3411            let mut topology = CollectiveTopology::empty(2).unwrap();
3412            topology
3413                .add_link(TopologyLink {
3414                    first_rank: 0,
3415                    second_rank: 1,
3416                    bandwidth_mbps: 64,
3417                    latency_ns: 65_536,
3418                })
3419                .unwrap();
3420            for rail in 0..2 {
3421                topology
3422                    .add_rail_link(TopologyRailLink {
3423                        rail,
3424                        first_rank: 0,
3425                        second_rank: 1,
3426                        bandwidth_mbps: 64,
3427                        latency_ns: 65_536,
3428                    })
3429                    .unwrap();
3430            }
3431            topology
3432                .add_aggregate_link(TopologyAggregateLink {
3433                    first_rank: 0,
3434                    second_rank: 1,
3435                    bandwidth_mbps: aggregate_bandwidth_mbps,
3436                })
3437                .unwrap();
3438            topology
3439        };
3440        let tuning = |aggregate_bandwidth_mbps| {
3441            CollectiveTuning::from_topology(
3442                AlgorithmPolicy::Auto,
3443                0,
3444                topology(aggregate_bandwidth_mbps),
3445                None,
3446            )
3447            .unwrap()
3448            .with_transport(CollectiveTransport::TcpPeer)
3449            .unwrap()
3450            .with_p2p_rails(2)
3451            .unwrap()
3452        };
3453        let pinned = tuning(64);
3454        let profile = CollectiveAutotuneProfile {
3455            version: COLLECTIVE_AUTOTUNE_PROFILE_VERSION,
3456            world_size: 2,
3457            topology_fingerprint: pinned.topology_fingerprint_hex(),
3458            execution_fingerprint: pinned.execution_fingerprint_hex(),
3459            entries: vec![CollectiveAutotuneEntry {
3460                operation: CollectiveKind::AllReduce,
3461                min_payload_bytes: 0,
3462                max_payload_bytes: u64::MAX,
3463                algorithm: CollectiveAlgorithm::Ring,
3464                ring_channels: 2,
3465                measured_time_ns: 1,
3466            }],
3467        };
3468
3469        let adjacent = tuning(256);
3470        adjacent
3471            .validate_autotune_profile_with_detected(
3472                &profile,
3473                Some("0-1:64:65536"),
3474                Some("0@0-1:64:65536,1@0-1:64:65536"),
3475                Some("0-1:64"),
3476                true,
3477            )
3478            .unwrap();
3479        assert!(
3480            adjacent
3481                .validate_autotune_profile_with_detected(
3482                    &profile,
3483                    Some("0-1:64:65536"),
3484                    Some("0@0-1:64:65536,1@0-1:64:65536"),
3485                    None,
3486                    true,
3487                )
3488                .is_err()
3489        );
3490        assert!(
3491            tuning(1024)
3492                .validate_autotune_profile_with_detected(
3493                    &profile,
3494                    Some("0-1:64:65536"),
3495                    Some("0@0-1:64:65536,1@0-1:64:65536"),
3496                    Some("0-1:64"),
3497                    true,
3498                )
3499                .is_err()
3500        );
3501    }
3502
3503    #[test]
3504    fn probed_profile_rejects_adjacent_metrics_when_the_preferred_rail_order_flips() {
3505        let topology = |first_rail_bandwidth, second_rail_bandwidth| {
3506            let mut topology = CollectiveTopology::empty(2).unwrap();
3507            topology
3508                .add_link(TopologyLink {
3509                    first_rank: 0,
3510                    second_rank: 1,
3511                    bandwidth_mbps: 64,
3512                    latency_ns: 65_536,
3513                })
3514                .unwrap();
3515            for (rail, bandwidth_mbps) in [(0, first_rail_bandwidth), (1, second_rail_bandwidth)] {
3516                topology
3517                    .add_rail_link(TopologyRailLink {
3518                        rail,
3519                        first_rank: 0,
3520                        second_rank: 1,
3521                        bandwidth_mbps,
3522                        latency_ns: 65_536,
3523                    })
3524                    .unwrap();
3525            }
3526            topology
3527        };
3528        let pinned =
3529            CollectiveTuning::from_topology(AlgorithmPolicy::Auto, 0, topology(64, 256), None)
3530                .unwrap()
3531                .with_transport(CollectiveTransport::TcpPeer)
3532                .unwrap()
3533                .with_p2p_rails(2)
3534                .unwrap();
3535        assert_eq!(pinned.rail_order(), vec![1, 0]);
3536        let profile = CollectiveAutotuneProfile {
3537            version: COLLECTIVE_AUTOTUNE_PROFILE_VERSION,
3538            world_size: 2,
3539            topology_fingerprint: pinned.topology_fingerprint_hex(),
3540            execution_fingerprint: pinned.execution_fingerprint_hex(),
3541            entries: vec![CollectiveAutotuneEntry {
3542                operation: CollectiveKind::AllReduce,
3543                min_payload_bytes: 0,
3544                max_payload_bytes: u64::MAX,
3545                algorithm: CollectiveAlgorithm::Ring,
3546                ring_channels: 2,
3547                measured_time_ns: 1,
3548            }],
3549        };
3550
3551        let current =
3552            CollectiveTuning::from_topology(AlgorithmPolicy::Auto, 0, topology(256, 64), None)
3553                .unwrap()
3554                .with_transport(CollectiveTransport::TcpPeer)
3555                .unwrap()
3556                .with_p2p_rails(2)
3557                .unwrap();
3558        assert_eq!(current.rail_order(), vec![0, 1]);
3559        assert!(
3560            current
3561                .validate_autotune_profile_with_detected(
3562                    &profile,
3563                    Some("0-1:64:65536"),
3564                    Some("0@0-1:64:65536,1@0-1:256:65536"),
3565                    None,
3566                    true,
3567                )
3568                .is_err()
3569        );
3570    }
3571
3572    #[test]
3573    fn autotune_profile_rejects_stale_overlapping_and_wrong_algorithm_entries() {
3574        let tuning = CollectiveTuning::new(AlgorithmPolicy::Auto, 0, vec![0, 1]).unwrap();
3575        let entry = CollectiveAutotuneEntry {
3576            operation: CollectiveKind::AllReduce,
3577            min_payload_bytes: 0,
3578            max_payload_bytes: 1024,
3579            algorithm: CollectiveAlgorithm::Ring,
3580            ring_channels: 1,
3581            measured_time_ns: 100,
3582        };
3583        let profile = |fingerprint: String, entries: Vec<CollectiveAutotuneEntry>| {
3584            CollectiveAutotuneProfile {
3585                version: COLLECTIVE_AUTOTUNE_PROFILE_VERSION,
3586                world_size: 2,
3587                topology_fingerprint: fingerprint,
3588                execution_fingerprint: tuning.execution_fingerprint_hex(),
3589                entries,
3590            }
3591        };
3592        assert!(matches!(
3593            tuning
3594                .clone()
3595                .with_autotune_profile(profile("0000000000000000".into(), vec![entry])),
3596            Err(TopologyError::InvalidProfile(_))
3597        ));
3598        let mut wrong_execution = profile(tuning.topology_fingerprint_hex(), vec![entry]);
3599        wrong_execution.execution_fingerprint = "0000000000000000".into();
3600        assert!(matches!(
3601            tuning.clone().with_autotune_profile(wrong_execution),
3602            Err(TopologyError::InvalidProfile(_))
3603        ));
3604        assert!(matches!(
3605            tuning.clone().with_autotune_profile(profile(
3606                tuning.topology_fingerprint_hex(),
3607                vec![
3608                    entry,
3609                    CollectiveAutotuneEntry {
3610                        min_payload_bytes: 1024,
3611                        max_payload_bytes: 2048,
3612                        ..entry
3613                    },
3614                ]
3615            )),
3616            Err(TopologyError::InvalidProfile(_))
3617        ));
3618        let profiled = tuning
3619            .clone()
3620            .with_autotune_profile(profile(tuning.topology_fingerprint_hex(), vec![entry]))
3621            .unwrap();
3622        assert!(matches!(
3623            profiled.with_transport(CollectiveTransport::HostStaged),
3624            Err(TopologyError::InvalidProfile(_))
3625        ));
3626        assert!(matches!(
3627            tuning.clone().with_autotune_profile(profile(
3628                tuning.topology_fingerprint_hex(),
3629                vec![CollectiveAutotuneEntry {
3630                    operation: CollectiveKind::AllToAll,
3631                    algorithm: CollectiveAlgorithm::Ring,
3632                    ..entry
3633                }]
3634            )),
3635            Err(TopologyError::InvalidProfile(_))
3636        ));
3637        assert!(matches!(
3638            tuning.clone().with_autotune_profile(profile(
3639                tuning.topology_fingerprint_hex(),
3640                vec![CollectiveAutotuneEntry {
3641                    algorithm: CollectiveAlgorithm::Hierarchical,
3642                    ..entry
3643                }]
3644            )),
3645            Err(TopologyError::InvalidProfile(_))
3646        ));
3647    }
3648
3649    #[test]
3650    fn autotune_profile_json_uses_stable_wire_names() {
3651        let profile = CollectiveAutotuneProfile {
3652            version: COLLECTIVE_AUTOTUNE_PROFILE_VERSION,
3653            world_size: 2,
3654            topology_fingerprint: "0123456789abcdef".into(),
3655            execution_fingerprint: "fedcba9876543210".into(),
3656            entries: vec![CollectiveAutotuneEntry {
3657                operation: CollectiveKind::AllToAll,
3658                min_payload_bytes: 0,
3659                max_payload_bytes: 4095,
3660                algorithm: CollectiveAlgorithm::Pairwise,
3661                ring_channels: 1,
3662                measured_time_ns: 42,
3663            }],
3664        };
3665        let encoded = serde_json::to_string(&profile).unwrap();
3666        assert!(encoded.contains("\"operation\":\"alltoall\""));
3667        assert!(encoded.contains("\"algorithm\":\"pairwise\""));
3668        assert!(encoded.contains("\"ring_channels\":1"));
3669        assert!(encoded.contains("\"execution_fingerprint\":\"fedcba9876543210\""));
3670        assert_eq!(
3671            serde_json::from_str::<CollectiveAutotuneProfile>(&encoded).unwrap(),
3672            profile
3673        );
3674    }
3675
3676    #[test]
3677    fn legacy_autotune_profile_defaults_to_one_ring_channel() {
3678        let encoded = r#"{
3679            "version": 1,
3680            "world_size": 2,
3681            "topology_fingerprint": "0123456789abcdef",
3682            "entries": [{
3683                "operation": "allreduce",
3684                "min_payload_bytes": 0,
3685                "max_payload_bytes": 4095,
3686                "algorithm": "ring",
3687                "measured_time_ns": 42
3688            }]
3689        }"#;
3690        let profile = serde_json::from_str::<CollectiveAutotuneProfile>(encoded).unwrap();
3691        assert_eq!(profile.entries[0].ring_channels, 1);
3692        assert!(profile.execution_fingerprint.is_empty());
3693        let tuning = CollectiveTuning::new(AlgorithmPolicy::Auto, 0, vec![0, 1]).unwrap();
3694        assert!(matches!(
3695            tuning.with_autotune_profile(profile),
3696            Err(TopologyError::InvalidProfile(_))
3697        ));
3698    }
3699
3700    #[test]
3701    fn duplicate_rank_is_rejected() {
3702        assert!(matches!(
3703            CollectiveTuning::new(AlgorithmPolicy::Ring, 0, vec![0, 0]),
3704            Err(TopologyError::InvalidRing(_))
3705        ));
3706    }
3707}