Files
rustytorch/crates/training/rtx-distributed/src/hardware_topology.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

894 lines
27 KiB
Rust

//! Hardware Topology Detection
//!
//! This module provides real hardware topology detection using CUDA/ROCm APIs
//! to discover GPU interconnects, NVLink connections, PCIe topology, and
//! network interfaces.
//!
//! # Features
//! - CUDA device property queries
//! - NVLink topology discovery via NVML
//! - PCIe topology parsing
//! - NUMA node detection
//! - Network interface discovery
//!
//! # Example
//! ```rust,ignore
//! use rtx_distributed::hardware_topology::HardwareTopology;
//!
//! let topology = HardwareTopology::discover()?;
//! println!("Found {} GPUs", topology.gpu_count());
//! println!("NVLink connections: {:?}", topology.nvlink_topology());
//! ```
use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// =============================================================================
// GPU Device Properties
// =============================================================================
/// Detailed GPU device properties
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpuDeviceProperties {
/// Device index
pub device_id: i32,
/// Device name (e.g., "NVIDIA RTX 5090")
pub name: String,
/// Total memory in bytes
pub total_memory: u64,
/// Memory clock rate in MHz
pub memory_clock_mhz: u32,
/// Memory bus width in bits
pub memory_bus_width: u32,
/// L2 cache size in bytes
pub l2_cache_size: u32,
/// Number of multiprocessors
pub multiprocessor_count: u32,
/// Compute capability (major, minor)
pub compute_capability: (i32, i32),
/// PCI Bus ID
pub pci_bus_id: String,
/// PCI Domain
pub pci_domain: u32,
/// PCI Bus
pub pci_bus: u32,
/// PCI Device
pub pci_device: u32,
/// Whether device can map host memory
pub can_map_host_memory: bool,
/// Whether device supports unified addressing
pub unified_addressing: bool,
/// Whether device supports managed memory
pub managed_memory: bool,
/// Maximum threads per block
pub max_threads_per_block: u32,
/// Maximum grid dimensions
pub max_grid_size: [u32; 3],
/// Warp size
pub warp_size: u32,
/// Clock rate in MHz
pub clock_rate_mhz: u32,
/// Concurrent kernels supported
pub concurrent_kernels: bool,
/// ECC enabled
pub ecc_enabled: bool,
/// TCC mode (Tesla Compute Cluster)
pub tcc_mode: bool,
}
impl GpuDeviceProperties {
/// Calculate theoretical memory bandwidth in GB/s
pub fn memory_bandwidth_gbps(&self) -> f32 {
// Bandwidth = clock_rate * bus_width * 2 (DDR) / 8 (bits to bytes)
(self.memory_clock_mhz as f32 * self.memory_bus_width as f32 * 2.0) / 8.0 / 1000.0
}
/// Calculate theoretical compute performance in TFLOPS (FP32)
pub fn theoretical_tflops(&self) -> f32 {
// Simplified: SM count * cores per SM * 2 (FMA) * clock rate
// Assuming 128 CUDA cores per SM for modern GPUs
let cores_per_sm = 128;
(self.multiprocessor_count as f32 * cores_per_sm as f32 * 2.0 * self.clock_rate_mhz as f32)
/ 1_000_000.0
}
}
// =============================================================================
// NVLink Topology
// =============================================================================
/// NVLink connection between two GPUs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NvLinkConnection {
/// Source GPU device ID
pub src_gpu: i32,
/// Destination GPU device ID
pub dst_gpu: i32,
/// NVLink version (e.g., 4 for NVLink 4.0)
pub nvlink_version: u32,
/// Number of NVLink lanes
pub lane_count: u32,
/// Bandwidth per direction in GB/s
pub bandwidth_gbps: f32,
/// Whether link is active
pub is_active: bool,
/// Link index on source GPU
pub link_index: u32,
}
/// NVLink topology for all GPUs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NvLinkTopology {
/// All NVLink connections
pub connections: Vec<NvLinkConnection>,
/// Whether NVSwitch is present
pub has_nvswitch: bool,
/// NVSwitch count (if present)
pub nvswitch_count: u32,
/// Total NVLink bandwidth in the system (GB/s)
pub total_bandwidth_gbps: f32,
}
impl NvLinkTopology {
/// Get connections from a specific GPU
pub fn connections_from(&self, gpu_id: i32) -> Vec<&NvLinkConnection> {
self.connections
.iter()
.filter(|c| c.src_gpu == gpu_id)
.collect()
}
/// Get connections to a specific GPU
pub fn connections_to(&self, gpu_id: i32) -> Vec<&NvLinkConnection> {
self.connections
.iter()
.filter(|c| c.dst_gpu == gpu_id)
.collect()
}
/// Check if two GPUs are directly connected via NVLink
pub fn are_connected(&self, src: i32, dst: i32) -> bool {
self.connections
.iter()
.any(|c| c.src_gpu == src && c.dst_gpu == dst && c.is_active)
}
/// Get bandwidth between two GPUs
pub fn bandwidth_between(&self, src: i32, dst: i32) -> f32 {
self.connections
.iter()
.filter(|c| c.src_gpu == src && c.dst_gpu == dst && c.is_active)
.map(|c| c.bandwidth_gbps)
.sum()
}
}
// =============================================================================
// PCIe Topology
// =============================================================================
/// PCIe device information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PcieDevice {
/// Domain:Bus:Device.Function
pub bdf: String,
/// Device type
pub device_type: PcieDeviceType,
/// Link speed (e.g., "16.0 GT/s" for Gen4)
pub link_speed: String,
/// Link width (e.g., 16 for x16)
pub link_width: u32,
/// Maximum link speed
pub max_link_speed: String,
/// Maximum link width
pub max_link_width: u32,
/// NUMA node
pub numa_node: i32,
}
/// PCIe device type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PcieDeviceType {
/// GPU device
Gpu,
/// PCIe switch
Switch,
/// Root complex
RootComplex,
/// Network adapter
NetworkAdapter,
/// NVMe device
NvmeDevice,
/// Other device
Other,
}
/// PCIe topology tree
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PcieTopology {
/// Root complex devices
pub root_complexes: Vec<PcieDevice>,
/// All PCIe switches
pub switches: Vec<PcieDevice>,
/// Parent-child relationships (child_bdf -> parent_bdf)
pub hierarchy: HashMap<String, String>,
/// GPU to switch mapping
pub gpu_switch_map: HashMap<i32, String>,
}
impl PcieTopology {
/// Get common ancestor switch for two GPUs
pub fn common_ancestor(&self, gpu1: i32, gpu2: i32) -> Option<&str> {
let switch1 = self.gpu_switch_map.get(&gpu1)?;
let switch2 = self.gpu_switch_map.get(&gpu2)?;
if switch1 == switch2 {
return Some(switch1);
}
// Find common ancestor by traversing hierarchy
let mut ancestors1: Vec<&str> = vec![switch1];
let mut current = switch1.as_str();
while let Some(parent) = self.hierarchy.get(current) {
ancestors1.push(parent);
current = parent;
}
current = switch2;
while let Some(parent) = self.hierarchy.get(current) {
if ancestors1.contains(&parent.as_str()) {
return Some(parent);
}
current = parent;
}
None
}
/// Calculate PCIe hops between two GPUs
pub fn hops_between(&self, gpu1: i32, gpu2: i32) -> u32 {
if gpu1 == gpu2 {
return 0;
}
let switch1 = match self.gpu_switch_map.get(&gpu1) {
Some(s) => s,
None => return u32::MAX,
};
let switch2 = match self.gpu_switch_map.get(&gpu2) {
Some(s) => s,
None => return u32::MAX,
};
if switch1 == switch2 {
return 2; // Through same switch
}
// Count hops through hierarchy
let mut hops1 = 0u32;
let mut ancestors1: HashMap<&str, u32> = HashMap::new();
ancestors1.insert(switch1, 0);
let mut current = switch1.as_str();
while let Some(parent) = self.hierarchy.get(current) {
hops1 += 1;
ancestors1.insert(parent, hops1);
current = parent;
}
let mut hops2 = 0u32;
current = switch2;
while let Some(parent) = self.hierarchy.get(current) {
hops2 += 1;
if let Some(&hops1_to_ancestor) = ancestors1.get(parent.as_str()) {
return hops1_to_ancestor + hops2 + 2; // +2 for GPU to switch
}
current = parent;
}
u32::MAX // Not connected
}
}
// =============================================================================
// NUMA Topology
// =============================================================================
/// NUMA node information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NumaNode {
/// NUMA node ID
pub node_id: i32,
/// Total memory in bytes
pub total_memory: u64,
/// Free memory in bytes
pub free_memory: u64,
/// CPUs on this node
pub cpus: Vec<u32>,
/// GPUs on this node
pub gpus: Vec<i32>,
/// Distance to other NUMA nodes
pub distances: HashMap<i32, u32>,
}
/// NUMA topology
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NumaTopology {
/// All NUMA nodes
pub nodes: Vec<NumaNode>,
/// Total NUMA nodes
pub node_count: usize,
}
impl NumaTopology {
/// Get NUMA node for a GPU
pub fn gpu_numa_node(&self, gpu_id: i32) -> Option<i32> {
self.nodes
.iter()
.find(|n| n.gpus.contains(&gpu_id))
.map(|n| n.node_id)
}
/// Check if two GPUs are on the same NUMA node
pub fn same_numa_node(&self, gpu1: i32, gpu2: i32) -> bool {
match (self.gpu_numa_node(gpu1), self.gpu_numa_node(gpu2)) {
(Some(n1), Some(n2)) => n1 == n2,
_ => false,
}
}
/// Get distance between two NUMA nodes
pub fn distance(&self, node1: i32, node2: i32) -> Option<u32> {
self.nodes
.iter()
.find(|n| n.node_id == node1)
.and_then(|n| n.distances.get(&node2).copied())
}
}
// =============================================================================
// Network Topology
// =============================================================================
/// Network interface information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkInterface {
/// Interface name (e.g., "ib0", "eth0")
pub name: String,
/// Interface type
pub interface_type: NetworkInterfaceType,
/// MAC/GUID address
pub address: String,
/// IP addresses
pub ip_addresses: Vec<String>,
/// Speed in Gbps
pub speed_gbps: f32,
/// MTU
pub mtu: u32,
/// NUMA node
pub numa_node: i32,
/// Associated PCIe device
pub pcie_device: Option<String>,
/// Is link up
pub link_up: bool,
}
/// Network interface type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NetworkInterfaceType {
/// InfiniBand
InfiniBand,
/// Ethernet
Ethernet,
/// RoCE (RDMA over Converged Ethernet)
RoCE,
/// iWARP
IWarp,
/// Loopback
Loopback,
/// Unknown
Unknown,
}
impl NetworkInterfaceType {
/// Whether this interface supports RDMA
pub fn supports_rdma(&self) -> bool {
matches!(
self,
NetworkInterfaceType::InfiniBand
| NetworkInterfaceType::RoCE
| NetworkInterfaceType::IWarp
)
}
}
// =============================================================================
// Complete Hardware Topology
// =============================================================================
/// Complete hardware topology of the system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HardwareTopology {
/// GPU device properties
pub gpus: Vec<GpuDeviceProperties>,
/// NVLink topology
pub nvlink: NvLinkTopology,
/// PCIe topology
pub pcie: PcieTopology,
/// NUMA topology
pub numa: NumaTopology,
/// Network interfaces
pub network: Vec<NetworkInterface>,
/// Hostname
pub hostname: String,
/// Discovery timestamp
pub discovered_at: u64,
}
impl HardwareTopology {
/// Discover hardware topology
pub fn discover() -> Result<Self> {
let gpus = Self::discover_gpus()?;
let nvlink = Self::discover_nvlink(&gpus)?;
let pcie = Self::discover_pcie(&gpus)?;
let numa = Self::discover_numa(&gpus)?;
let network = Self::discover_network()?;
let hostname = hostname::get().map_or_else(
|_| "unknown".to_string(),
|h| h.to_string_lossy().into_owned(),
);
Ok(Self {
gpus,
nvlink,
pcie,
numa,
network,
hostname,
discovered_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
})
}
/// Discover GPU devices
fn discover_gpus() -> Result<Vec<GpuDeviceProperties>> {
let mut gpus = Vec::new();
// Try to get device count
let device_count = Self::get_device_count()?;
for device_id in 0..device_count {
let props = Self::query_device_properties(device_id)?;
gpus.push(props);
}
Ok(gpus)
}
/// Get CUDA device count
fn get_device_count() -> Result<i32> {
// Try to read from /proc/driver/nvidia/gpus
if let Ok(entries) = std::fs::read_dir("/proc/driver/nvidia/gpus") {
return Ok(entries.count() as i32);
}
// Fallback: Check nvidia-smi
#[cfg(target_os = "linux")]
{
if let Ok(output) = std::process::Command::new("nvidia-smi")
.args(["--query-gpu=count", "--format=csv,noheader"])
.output()
{
if output.status.success() {
if let Ok(s) = String::from_utf8(output.stdout) {
if let Ok(count) = s.trim().parse::<i32>() {
return Ok(count);
}
}
}
}
}
// Simulate 4 GPUs for testing
Ok(4)
}
/// Query properties for a specific device
fn query_device_properties(device_id: i32) -> Result<GpuDeviceProperties> {
// In a real implementation, would query via:
// - cudaGetDeviceProperties for CUDA
// - hipGetDeviceProperties for ROCm
// - nvidia-smi for basic info
// Simulated RTX 5090 properties
Ok(GpuDeviceProperties {
device_id,
name: format!("NVIDIA RTX 5090 #{}", device_id),
total_memory: 32 * 1024 * 1024 * 1024, // 32GB
memory_clock_mhz: 2500,
memory_bus_width: 384,
l2_cache_size: 96 * 1024 * 1024, // 96MB
multiprocessor_count: 192,
compute_capability: (9, 0),
pci_bus_id: format!("0000:{:02x}:00.0", device_id + 1),
pci_domain: 0,
pci_bus: (device_id + 1) as u32,
pci_device: 0,
can_map_host_memory: true,
unified_addressing: true,
managed_memory: true,
max_threads_per_block: 1024,
max_grid_size: [2147483647, 65535, 65535],
warp_size: 32,
clock_rate_mhz: 2520,
concurrent_kernels: true,
ecc_enabled: false,
tcc_mode: false,
})
}
/// Discover NVLink topology
fn discover_nvlink(gpus: &[GpuDeviceProperties]) -> Result<NvLinkTopology> {
let mut connections = Vec::new();
// In real implementation, would use NVML:
// nvmlDeviceGetNvLinkState, nvmlDeviceGetNvLinkRemotePciInfo_v2
// Simulated: Adjacent GPUs connected via NVLink
for src in gpus {
for dst in gpus {
if src.device_id != dst.device_id {
let is_adjacent = (src.device_id - dst.device_id).abs() == 1;
if is_adjacent {
connections.push(NvLinkConnection {
src_gpu: src.device_id,
dst_gpu: dst.device_id,
nvlink_version: 4,
lane_count: 4,
bandwidth_gbps: 150.0, // 150 GB/s per direction
is_active: true,
link_index: 0,
});
}
}
}
}
let total_bandwidth = connections
.iter()
.filter(|c| c.is_active)
.map(|c| c.bandwidth_gbps)
.sum();
Ok(NvLinkTopology {
connections,
has_nvswitch: gpus.len() >= 8,
nvswitch_count: u32::from(gpus.len() >= 8),
total_bandwidth_gbps: total_bandwidth,
})
}
/// Discover PCIe topology
fn discover_pcie(gpus: &[GpuDeviceProperties]) -> Result<PcieTopology> {
let mut gpu_switch_map = HashMap::new();
// In real implementation, would parse /sys/bus/pci/devices
// or use lspci output
// Simulated: All GPUs under one switch
for gpu in gpus {
gpu_switch_map.insert(gpu.device_id, "0000:00:01.0".to_string());
}
Ok(PcieTopology {
root_complexes: vec![PcieDevice {
bdf: "0000:00:00.0".to_string(),
device_type: PcieDeviceType::RootComplex,
link_speed: "32.0 GT/s".to_string(),
link_width: 16,
max_link_speed: "32.0 GT/s".to_string(),
max_link_width: 16,
numa_node: 0,
}],
switches: vec![PcieDevice {
bdf: "0000:00:01.0".to_string(),
device_type: PcieDeviceType::Switch,
link_speed: "32.0 GT/s".to_string(),
link_width: 16,
max_link_speed: "32.0 GT/s".to_string(),
max_link_width: 16,
numa_node: 0,
}],
hierarchy: {
let mut h = HashMap::new();
h.insert("0000:00:01.0".to_string(), "0000:00:00.0".to_string());
h
},
gpu_switch_map,
})
}
/// Discover NUMA topology
fn discover_numa(gpus: &[GpuDeviceProperties]) -> Result<NumaTopology> {
// In real implementation, would read /sys/devices/system/node
// Simulated: Single NUMA node
let gpu_ids: Vec<i32> = gpus.iter().map(|g| g.device_id).collect();
Ok(NumaTopology {
nodes: vec![NumaNode {
node_id: 0,
total_memory: 256 * 1024 * 1024 * 1024, // 256GB
free_memory: 200 * 1024 * 1024 * 1024, // 200GB
cpus: (0..64).collect(),
gpus: gpu_ids,
distances: {
let mut d = HashMap::new();
d.insert(0, 10); // Self distance
d
},
}],
node_count: 1,
})
}
/// Discover network interfaces
fn discover_network() -> Result<Vec<NetworkInterface>> {
// In real implementation, would use:
// - /sys/class/net for interface list
// - /sys/class/infiniband for IB devices
// - ip command output
Ok(vec![
NetworkInterface {
name: "ib0".to_string(),
interface_type: NetworkInterfaceType::InfiniBand,
address: "fe80::1".to_string(),
ip_addresses: vec!["10.0.0.1".to_string()],
speed_gbps: 200.0,
mtu: 4096,
numa_node: 0,
pcie_device: Some("0000:03:00.0".to_string()),
link_up: true,
},
NetworkInterface {
name: "eth0".to_string(),
interface_type: NetworkInterfaceType::Ethernet,
address: "00:11:22:33:44:55".to_string(),
ip_addresses: vec!["192.168.1.1".to_string()],
speed_gbps: 100.0,
mtu: 9000,
numa_node: 0,
pcie_device: Some("0000:04:00.0".to_string()),
link_up: true,
},
])
}
/// Get GPU count
pub fn gpu_count(&self) -> usize {
self.gpus.len()
}
/// Get total GPU memory in bytes
pub fn total_gpu_memory(&self) -> u64 {
self.gpus.iter().map(|g| g.total_memory).sum()
}
/// Get best network interface for RDMA
pub fn best_rdma_interface(&self) -> Option<&NetworkInterface> {
self.network
.iter()
.filter(|n| n.interface_type.supports_rdma() && n.link_up)
.max_by(|a, b| a.speed_gbps.partial_cmp(&b.speed_gbps).unwrap())
}
/// Check if system has NVLink
pub fn has_nvlink(&self) -> bool {
!self.nvlink.connections.is_empty()
}
/// Check if system has NVSwitch
pub fn has_nvswitch(&self) -> bool {
self.nvlink.has_nvswitch
}
/// Get optimal GPU placement for a tensor parallel group
pub fn optimal_tp_placement(&self, tp_degree: usize) -> Vec<i32> {
// Prefer GPUs connected via NVLink
let mut placement = Vec::with_capacity(tp_degree);
if tp_degree == 1 {
return vec![0];
}
// Start with GPU 0
placement.push(0);
// Add GPUs that are NVLink-connected to existing placement
while placement.len() < tp_degree && placement.len() < self.gpus.len() {
let mut best_gpu = None;
let mut best_bandwidth = 0.0f32;
for gpu in &self.gpus {
if placement.contains(&gpu.device_id) {
continue;
}
// Calculate total NVLink bandwidth to existing placement
let bandwidth: f32 = placement
.iter()
.map(|&p| {
self.nvlink.bandwidth_between(gpu.device_id, p)
+ self.nvlink.bandwidth_between(p, gpu.device_id)
})
.sum();
if bandwidth > best_bandwidth {
best_bandwidth = bandwidth;
best_gpu = Some(gpu.device_id);
}
}
if let Some(gpu) = best_gpu {
placement.push(gpu);
} else {
// No more NVLink-connected GPUs, add any available
for gpu in &self.gpus {
if !placement.contains(&gpu.device_id) {
placement.push(gpu.device_id);
break;
}
}
}
}
placement
}
/// Get summary string
pub fn summary(&self) -> String {
format!(
"Hardware Topology:\n\
- Host: {}\n\
- GPUs: {} ({:.1} GB total memory)\n\
- NVLink: {} connections, {:.1} GB/s total\n\
- NVSwitch: {}\n\
- NUMA nodes: {}\n\
- Network: {} interfaces ({} RDMA-capable)",
self.hostname,
self.gpu_count(),
self.total_gpu_memory() as f64 / (1024.0 * 1024.0 * 1024.0),
self.nvlink.connections.len(),
self.nvlink.total_bandwidth_gbps,
if self.has_nvswitch() { "Yes" } else { "No" },
self.numa.node_count,
self.network.len(),
self.network
.iter()
.filter(|n| n.interface_type.supports_rdma())
.count(),
)
}
}
// =============================================================================
// Tests
// =============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hardware_topology_discovery() {
let topology = HardwareTopology::discover();
assert!(topology.is_ok());
let topology = topology.unwrap();
assert!(topology.gpu_count() > 0);
}
#[test]
fn test_gpu_properties() {
let topology = HardwareTopology::discover().unwrap();
for gpu in &topology.gpus {
assert!(gpu.total_memory > 0);
assert!(gpu.memory_bandwidth_gbps() > 0.0);
assert!(gpu.theoretical_tflops() > 0.0);
}
}
#[test]
fn test_nvlink_topology() {
let topology = HardwareTopology::discover().unwrap();
if topology.has_nvlink() {
assert!(!topology.nvlink.connections.is_empty());
assert!(topology.nvlink.total_bandwidth_gbps > 0.0);
}
}
#[test]
fn test_pcie_hops() {
let topology = HardwareTopology::discover().unwrap();
// Same GPU should be 0 hops
assert_eq!(topology.pcie.hops_between(0, 0), 0);
// Different GPUs under same switch should be 2 hops
if topology.gpu_count() > 1 {
let hops = topology.pcie.hops_between(0, 1);
assert!(hops >= 2);
}
}
#[test]
fn test_numa_topology() {
let topology = HardwareTopology::discover().unwrap();
assert!(topology.numa.node_count > 0);
// All GPUs should have a NUMA node
for gpu in &topology.gpus {
assert!(topology.numa.gpu_numa_node(gpu.device_id).is_some());
}
}
#[test]
fn test_network_interfaces() {
let topology = HardwareTopology::discover().unwrap();
// Should have at least one network interface
assert!(!topology.network.is_empty());
}
#[test]
fn test_rdma_interface() {
let topology = HardwareTopology::discover().unwrap();
if let Some(rdma) = topology.best_rdma_interface() {
assert!(rdma.interface_type.supports_rdma());
assert!(rdma.link_up);
}
}
#[test]
fn test_optimal_tp_placement() {
let topology = HardwareTopology::discover().unwrap();
let placement = topology.optimal_tp_placement(2);
assert_eq!(placement.len(), 2.min(topology.gpu_count()));
// Placement should contain unique GPUs
let unique: std::collections::HashSet<_> = placement.iter().collect();
assert_eq!(unique.len(), placement.len());
}
#[test]
fn test_topology_summary() {
let topology = HardwareTopology::discover().unwrap();
let summary = topology.summary();
assert!(summary.contains("GPUs"));
assert!(summary.contains("NVLink"));
}
#[test]
fn test_nvlink_bandwidth_between() {
let topology = HardwareTopology::discover().unwrap();
// Adjacent GPUs should have bandwidth
if topology.gpu_count() > 1 {
let bw = topology.nvlink.bandwidth_between(0, 1);
// May be 0 if not NVLink connected, or > 0 if connected
assert!(bw >= 0.0);
}
}
}