1use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct DeviceId(pub usize);
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub enum DeviceKind {
37 Cpu,
39 Cuda,
41 Rocm,
43 Metal,
45 IntelGpu,
47 Other,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub enum Interconnect {
54 NvLink,
56 Pcie,
58 InfinityFabric,
60 SharedMemory,
62 Network,
64 Unknown,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct DeviceInfo {
71 pub ordinal: usize,
73 pub kind: DeviceKind,
75 pub name: String,
77 pub memory_bytes: u64,
79 pub gflops: f64,
81}
82
83impl DeviceInfo {
84 pub fn new(ordinal: usize, kind: DeviceKind, name: impl Into<String>) -> Self {
86 Self {
87 ordinal,
88 kind,
89 name: name.into(),
90 memory_bytes: 0,
91 gflops: 0.0,
92 }
93 }
94
95 pub fn with_memory_bytes(mut self, bytes: u64) -> Self {
97 self.memory_bytes = bytes;
98 self
99 }
100
101 pub fn with_gflops(mut self, gflops: f64) -> Self {
103 self.gflops = gflops;
104 self
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct Link {
111 pub interconnect: Interconnect,
113 pub bandwidth_mb_s: u64,
115 pub latency_us: u64,
117}
118
119impl Link {
120 pub fn new(interconnect: Interconnect, bandwidth_mb_s: u64) -> Self {
122 Self {
123 interconnect,
124 bandwidth_mb_s,
125 latency_us: 0,
126 }
127 }
128
129 pub fn with_latency_us(mut self, us: u64) -> Self {
131 self.latency_us = us;
132 self
133 }
134
135 pub fn transfer_time_us(&self, bytes: u64) -> Option<f64> {
139 if self.bandwidth_mb_s == 0 {
140 return None;
141 }
142 let mb = bytes as f64 / (1024.0 * 1024.0);
143 let seconds = mb / self.bandwidth_mb_s as f64;
144 Some(self.latency_us as f64 + seconds * 1_000_000.0)
145 }
146}
147
148#[derive(Debug, Clone, Default)]
150pub struct DeviceTopology {
151 devices: Vec<DeviceInfo>,
152 links: HashMap<(DeviceId, DeviceId), Link>,
154}
155
156impl DeviceTopology {
157 pub fn new() -> Self {
159 Self::default()
160 }
161
162 pub fn add_device(&mut self, info: DeviceInfo) -> DeviceId {
164 let id = DeviceId(self.devices.len());
165 self.devices.push(info);
166 id
167 }
168
169 pub fn add_link(&mut self, a: DeviceId, b: DeviceId, link: Link) {
171 self.links.insert((a, b), link.clone());
172 self.links.insert((b, a), link);
173 }
174
175 pub fn add_directed_link(&mut self, from: DeviceId, to: DeviceId, link: Link) {
177 self.links.insert((from, to), link);
178 }
179
180 pub fn num_devices(&self) -> usize {
182 self.devices.len()
183 }
184
185 pub fn device(&self, id: DeviceId) -> Option<&DeviceInfo> {
187 self.devices.get(id.0)
188 }
189
190 pub fn link(&self, from: DeviceId, to: DeviceId) -> Option<&Link> {
192 self.links.get(&(from, to))
193 }
194
195 pub fn bandwidth_mb_s(&self, from: DeviceId, to: DeviceId) -> Option<u64> {
197 self.links.get(&(from, to)).map(|l| l.bandwidth_mb_s)
198 }
199
200 pub fn transfer_time_us(&self, from: DeviceId, to: DeviceId, bytes: u64) -> Option<f64> {
202 self.links.get(&(from, to))?.transfer_time_us(bytes)
203 }
204
205 pub fn device_ids(&self) -> Vec<DeviceId> {
207 (0..self.devices.len()).map(DeviceId).collect()
208 }
209
210 pub fn devices_of_kind(&self, kind: DeviceKind) -> Vec<DeviceId> {
212 self.devices
213 .iter()
214 .enumerate()
215 .filter(|(_, d)| d.kind == kind)
216 .map(|(i, _)| DeviceId(i))
217 .collect()
218 }
219
220 pub fn total_memory(&self, kind: DeviceKind) -> u64 {
222 self.devices
223 .iter()
224 .filter(|d| d.kind == kind)
225 .map(|d| d.memory_bytes)
226 .sum()
227 }
228
229 pub fn fastest_peer(&self, from: DeviceId) -> Option<(DeviceId, &Link)> {
231 self.links
232 .iter()
233 .filter(|((src, dst), _)| *src == from && *dst != from)
234 .max_by_key(|(_, l)| l.bandwidth_mb_s)
235 .map(|((_, dst), l)| (*dst, l))
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn two_gpu_topology() -> DeviceTopology {
244 let mut topo = DeviceTopology::new();
245 topo.add_device(
246 DeviceInfo::new(0, DeviceKind::Cuda, "GPU 0").with_memory_bytes(24_000_000_000),
247 );
248 topo.add_device(
249 DeviceInfo::new(1, DeviceKind::Cuda, "GPU 1").with_memory_bytes(24_000_000_000),
250 );
251 topo.add_link(
252 DeviceId(0),
253 DeviceId(1),
254 Link::new(Interconnect::NvLink, 600_000),
255 );
256 topo
257 }
258
259 #[test]
260 fn add_devices_and_links() {
261 let topo = two_gpu_topology();
262 assert_eq!(topo.num_devices(), 2);
263 assert_eq!(topo.device(DeviceId(0)).unwrap().name, "GPU 0");
264 }
265
266 #[test]
267 fn bidirectional_link() {
268 let topo = two_gpu_topology();
269 assert_eq!(topo.bandwidth_mb_s(DeviceId(0), DeviceId(1)), Some(600_000));
270 assert_eq!(topo.bandwidth_mb_s(DeviceId(1), DeviceId(0)), Some(600_000));
271 }
272
273 #[test]
274 fn no_link() {
275 let topo = two_gpu_topology();
276 assert!(topo.bandwidth_mb_s(DeviceId(0), DeviceId(0)).is_none());
277 }
278
279 #[test]
280 fn transfer_time() {
281 let topo = two_gpu_topology();
282 let time = topo
284 .transfer_time_us(DeviceId(0), DeviceId(1), 1024 * 1024)
285 .unwrap();
286 assert!(time > 0.0 && time < 100.0);
287 }
288
289 #[test]
290 fn devices_of_kind() {
291 let mut topo = two_gpu_topology();
292 topo.add_device(DeviceInfo::new(0, DeviceKind::Cpu, "Host"));
293 assert_eq!(topo.devices_of_kind(DeviceKind::Cuda).len(), 2);
294 assert_eq!(topo.devices_of_kind(DeviceKind::Cpu).len(), 1);
295 }
296
297 #[test]
298 fn total_memory() {
299 let topo = two_gpu_topology();
300 assert_eq!(topo.total_memory(DeviceKind::Cuda), 48_000_000_000);
301 }
302
303 #[test]
304 fn fastest_peer() {
305 let mut topo = DeviceTopology::new();
306 topo.add_device(DeviceInfo::new(0, DeviceKind::Cuda, "GPU 0"));
307 topo.add_device(DeviceInfo::new(1, DeviceKind::Cuda, "GPU 1"));
308 topo.add_device(DeviceInfo::new(2, DeviceKind::Cuda, "GPU 2"));
309 topo.add_link(
310 DeviceId(0),
311 DeviceId(1),
312 Link::new(Interconnect::Pcie, 32_000),
313 );
314 topo.add_link(
315 DeviceId(0),
316 DeviceId(2),
317 Link::new(Interconnect::NvLink, 600_000),
318 );
319
320 let (peer, link) = topo.fastest_peer(DeviceId(0)).unwrap();
321 assert_eq!(peer, DeviceId(2));
322 assert_eq!(link.interconnect, Interconnect::NvLink);
323 }
324
325 #[test]
326 fn directed_link() {
327 let mut topo = DeviceTopology::new();
328 topo.add_device(DeviceInfo::new(0, DeviceKind::Cuda, "GPU 0"));
329 topo.add_device(DeviceInfo::new(1, DeviceKind::Cuda, "GPU 1"));
330 topo.add_directed_link(
331 DeviceId(0),
332 DeviceId(1),
333 Link::new(Interconnect::Pcie, 16_000),
334 );
335
336 assert!(topo.link(DeviceId(0), DeviceId(1)).is_some());
337 assert!(topo.link(DeviceId(1), DeviceId(0)).is_none());
338 }
339
340 #[test]
341 fn link_latency() {
342 let link = Link::new(Interconnect::Pcie, 32_000).with_latency_us(5);
343 let time = link.transfer_time_us(1024 * 1024).unwrap();
345 assert!(time > 30.0 && time < 40.0);
346 }
347
348 #[test]
349 fn device_builder_pattern() {
350 let info = DeviceInfo::new(0, DeviceKind::Cuda, "RTX 4090")
351 .with_memory_bytes(24_000_000_000)
352 .with_gflops(82_600.0);
353 assert_eq!(info.memory_bytes, 24_000_000_000);
354 assert!((info.gflops - 82_600.0).abs() < 1.0);
355 }
356
357 #[test]
358 fn empty_topology() {
359 let topo = DeviceTopology::new();
360 assert_eq!(topo.num_devices(), 0);
361 assert!(topo.device_ids().is_empty());
362 }
363}