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]>
894 lines
27 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|