1use 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}