all-smi 0.26.2

Command-line utility for monitoring GPU hardware. It provides a real-time view of GPU utilization, memory usage, temperature, power consumption, and other metrics.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
// Copyright 2025 Lablup Inc. and Jeongkyu Shin
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Edge classification for the topology tab (issue #190).
//!
//! `nvidia-smi topo -m` renders the relationship between every pair of GPUs
//! as a short label (`NV4`, `NV8`, `SYS`, `PXB`, `NODE`, `X`). The topology
//! view reproduces that vocabulary for the matrix mode and re-uses the same
//! classifier for graph-mode edge labels.
//!
//! The `NVn` family where `n` is the bandwidth hint is the trickiest part.
//! NVML can report per-link bandwidth on a narrow subset of boards; when
//! the hint is missing we fall back to a generic `"NV"` so we never
//! misreport a generation.

use crate::device::{GpuInfo, NvLinkRemoteDevice, NvLinkRemoteType};

/// Short topology label for a pair of endpoints, mirroring the
/// `nvidia-smi topo -m` legend plus one ad-hoc variant (`NSW`) for
/// NvSwitch mesh members.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeClass {
    /// Self-cell along the matrix diagonal.
    SelfCell,
    /// Direct NvLink connection. `count` is the number of active parallel
    /// links between the endpoints; `generation` is the `NVn` bandwidth
    /// hint (None when unknown).
    NvLink { count: u32, generation: Option<u8> },
    /// Remote is an NvSwitch (GPU → Switch on this row, or Switch → GPU).
    ///
    /// Reserved for a future classifier extension that materialises the
    /// switch node as a first-class edge target; the current matrix
    /// renderer still treats switch-mediated pairs as `NvSwitchMesh`
    /// because NVML does not expose the remote GPU UUID.
    #[allow(dead_code)]
    NvSwitch { count: u32 },
    /// Two GPUs that share an NvSwitch mesh but do not have a direct link.
    /// nvidia-smi labels this `NV#` too; we keep a distinct variant so the
    /// graph renderer can draw a dashed switch-mediated edge.
    NvSwitchMesh,
    /// Same NUMA node, same PCIe root complex (nvidia-smi `PXB`).
    ///
    /// Reserved for a future classifier extension that reads PCIe root
    /// complex info from `detail` and distinguishes PXB from NODE.
    #[allow(dead_code)]
    PcieSameRoot,
    /// Same NUMA node but different PCIe root complexes (nvidia-smi `NODE`).
    PcieSameNuma,
    /// Different NUMA nodes — traversal goes through system fabric
    /// (nvidia-smi `SYS`).
    SysInterconnect,
    /// No information available for this pair (non-NVIDIA path or
    /// partial data). Rendered as a dim `--`.
    Unknown,
}

impl EdgeClass {
    /// Canonical matrix-cell label. `NVn` is collapsed to `NV` when the
    /// generation hint is missing so the operator is never misled by a
    /// hallucinated generation.
    pub fn label(&self) -> String {
        match self {
            Self::SelfCell => "X".to_string(),
            Self::NvLink {
                count: _,
                generation: Some(g),
            } => format!("NV{g}"),
            Self::NvLink {
                count: _,
                generation: None,
            } => "NV".to_string(),
            Self::NvSwitch { .. } => "NSW".to_string(),
            Self::NvSwitchMesh => "NV".to_string(),
            Self::PcieSameRoot => "PXB".to_string(),
            Self::PcieSameNuma => "NODE".to_string(),
            Self::SysInterconnect => "SYS".to_string(),
            Self::Unknown => "--".to_string(),
        }
    }
}

/// Bandwidth-to-generation hint table. The boundaries follow NVIDIA's
/// public NvLink generation ceilings per link direction (rounded down to
/// 2 decimals):
///
/// | Gen | Per-link BW (GB/s) | Approx MB/s   |
/// |-----|--------------------|---------------|
/// | 1   | 20                 |  20_000       |
/// | 2   | 25                 |  25_000       |
/// | 3   | 25                 |  25_000       |
/// | 4   | 25                 |  25_000       |
/// | 5   | 50                 |  50_000       |
/// | 6   | 100                | 100_000       |
///
/// NvLink Gen 2, 3, and 4 all share the same ~25 GB/s per-link ceiling;
/// NVML does not expose a field that distinguishes them, so any sample
/// in the 22–40 GB/s band is collapsed into the `Some(4)` label —
/// chosen as the most common Gen for that bandwidth on current-gen
/// datacenter GPUs. `Some(2)` and `Some(3)` are therefore never returned
/// by design; the matrix labels `NV2` / `NV3` are never emitted.
///
/// We use widened floors so the classifier is forgiving of slight
/// under-reporting (e.g. a 24 GB/s sample still resolves into the Gen
/// 2/3/4 bucket rather than degrading to Gen-1).
pub fn bandwidth_to_generation(bandwidth_mb_s: u32) -> Option<u8> {
    match bandwidth_mb_s {
        0 => None,
        1..=22_000 => Some(1),
        22_001..=40_000 => Some(4),
        40_001..=80_000 => Some(5),
        _ => Some(6),
    }
}

/// Is this endpoint a GPU (not a switch / ibmnpu / unknown)?
fn remote_is_gpu(d: &NvLinkRemoteDevice) -> bool {
    matches!(d.remote_type, NvLinkRemoteType::Gpu)
}

/// Is this endpoint an NvSwitch?
fn remote_is_switch(d: &NvLinkRemoteDevice) -> bool {
    matches!(d.remote_type, NvLinkRemoteType::Switch)
}

/// Derive the edge classification between two GPU rows.
///
/// `a` and `b` are the row / column GPUs respectively; `a.nvlink_remote_devices`
/// is the authoritative source of active links. We cannot tell which
/// **remote GPU** a specific link connects to (NVML does not expose remote
/// UUIDs in this reader), so the best heuristic for direct links is:
///
/// * If `a` has `k` active NvLink links of type `Gpu` and `k >= GPU_COUNT-1`,
///   the mesh is complete and every other GPU row gets an NvLink class.
/// * Otherwise we fall back to PCIe / NUMA classification.
///
/// This heuristic is intentionally optimistic: on DGX-like topologies with
/// full mesh + switch overlay it matches reality; on partial topologies the
/// matrix may show an `NVn` label for a pair that is actually switch-only,
/// which is the same ambiguity `nvidia-smi topo -m` exhibits.
pub fn classify(
    a_index: u32,
    b_index: u32,
    a: &GpuInfo,
    b: &GpuInfo,
    total_gpu_count: u32,
) -> EdgeClass {
    if a_index == b_index {
        return EdgeClass::SelfCell;
    }

    // Counts of each remote kind on the row GPU. `a` is the authoritative
    // source because `nvlink_remote_devices` is populated per parent GPU.
    let gpu_remote_count = a
        .nvlink_remote_devices
        .iter()
        .filter(|d| remote_is_gpu(d))
        .count() as u32;
    let switch_remote_count = a
        .nvlink_remote_devices
        .iter()
        .filter(|d| remote_is_switch(d))
        .count() as u32;

    // If the row has no NvLink info, classify strictly by PCIe / NUMA so
    // non-NVIDIA hosts still get a readable matrix.
    if gpu_remote_count == 0 && switch_remote_count == 0 {
        return pcie_class(a, b);
    }

    // Direct-link heuristic: when the row GPU has enough GPU-type links
    // to reach every peer, the pair is likely a direct NvLink.
    let peer_count = total_gpu_count.saturating_sub(1);
    if peer_count > 0 && gpu_remote_count >= peer_count {
        let generation = dominant_generation(&a.nvlink_remote_devices);
        return EdgeClass::NvLink {
            count: (gpu_remote_count / peer_count.max(1)).max(1),
            generation,
        };
    }

    // Switch-mediated heuristic: either row has switch links but no direct
    // GPU link to this peer, so classify the pair as switch-mesh.
    if switch_remote_count > 0 {
        return EdgeClass::NvSwitchMesh;
    }

    // Partial NvLink mesh: classify as generic NvLink with the count
    // divided among peers. Slightly optimistic but closer to what the
    // operator expects.
    if gpu_remote_count > 0 {
        let generation = dominant_generation(&a.nvlink_remote_devices);
        let count = gpu_remote_count
            .checked_div(peer_count.max(1))
            .unwrap_or(0)
            .max(1);
        return EdgeClass::NvLink { count, generation };
    }

    pcie_class(a, b)
}

/// Classify edge based purely on NUMA / PCIe info. Used as a fallback when
/// NvLink data is absent and for non-NVIDIA paths.
fn pcie_class(a: &GpuInfo, b: &GpuInfo) -> EdgeClass {
    match (a.numa_node_id, b.numa_node_id) {
        (Some(na), Some(nb)) if na == nb => EdgeClass::PcieSameNuma,
        (Some(_), Some(_)) => EdgeClass::SysInterconnect,
        _ => EdgeClass::Unknown,
    }
}

/// Pick the most common bandwidth hint among active links. When a single
/// GPU has links of multiple generations we prefer the majority; ties fall
/// back to `None` (ambiguous, render as generic `NV`).
fn dominant_generation(links: &[NvLinkRemoteDevice]) -> Option<u8> {
    // Tally generations across all links that report a bandwidth.
    let mut counts = [0u32; 7]; // index = generation (1..=6)
    let mut any = false;
    for link in links {
        if let Some(bw) = link.bandwidth_mb_s
            && let Some(generation) = bandwidth_to_generation(bw)
            && (generation as usize) < counts.len()
        {
            counts[generation as usize] += 1;
            any = true;
        }
    }
    if !any {
        return None;
    }
    let (mut best_gen, mut best_count, mut tied) = (0u8, 0u32, false);
    for (generation, &cnt) in counts.iter().enumerate().skip(1) {
        if cnt > best_count {
            best_gen = generation as u8;
            best_count = cnt;
            tied = false;
        } else if cnt == best_count && cnt > 0 {
            tied = true;
        }
    }
    if tied { None } else { Some(best_gen) }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::device::NvLinkRemoteType;
    use std::collections::HashMap;

    fn gpu_with_numa(numa: Option<i32>) -> GpuInfo {
        GpuInfo {
            uuid: "GPU-X".to_string(),
            time: String::new(),
            name: "Test".to_string(),
            device_type: "GPU".to_string(),
            host_id: "h".to_string(),
            hostname: "h".to_string(),
            instance: "h".to_string(),
            utilization: 0.0,
            ane_utilization: 0.0,
            dla_utilization: None,
            tensorcore_utilization: None,
            temperature: 0,
            used_memory: 0,
            total_memory: 0,
            frequency: 0,
            power_consumption: 0.0,
            gpu_core_count: None,
            temperature_threshold_slowdown: None,
            temperature_threshold_shutdown: None,
            temperature_threshold_max_operating: None,
            temperature_threshold_acoustic: None,
            performance_state: None,
            fan_speed_rpm: None,
            numa_node_id: numa,
            gsp_firmware_mode: None,
            gsp_firmware_version: None,
            nvlink_remote_devices: Vec::new(),
            gpm_metrics: None,
            detail: HashMap::new(),
        }
    }

    fn link(remote: NvLinkRemoteType, bw: Option<u32>) -> NvLinkRemoteDevice {
        NvLinkRemoteDevice {
            link_index: 0,
            remote_type: remote,
            bandwidth_mb_s: bw,
        }
    }

    #[test]
    fn self_cell_is_identity() {
        let a = gpu_with_numa(Some(0));
        assert_eq!(classify(0, 0, &a, &a, 8), EdgeClass::SelfCell);
    }

    #[test]
    fn no_nvlink_same_numa_is_node() {
        let a = gpu_with_numa(Some(0));
        let b = gpu_with_numa(Some(0));
        assert_eq!(classify(0, 1, &a, &b, 8), EdgeClass::PcieSameNuma);
    }

    #[test]
    fn no_nvlink_cross_numa_is_sys() {
        let a = gpu_with_numa(Some(0));
        let b = gpu_with_numa(Some(1));
        assert_eq!(classify(0, 1, &a, &b, 8), EdgeClass::SysInterconnect);
    }

    #[test]
    fn missing_numa_is_unknown() {
        let a = gpu_with_numa(None);
        let b = gpu_with_numa(None);
        assert_eq!(classify(0, 1, &a, &b, 8), EdgeClass::Unknown);
    }

    #[test]
    fn full_mesh_gpu_links_classify_as_nvlink() {
        let mut a = gpu_with_numa(Some(0));
        // 7 GPU links: full mesh across 8 GPUs.
        a.nvlink_remote_devices = (0..7)
            .map(|i| NvLinkRemoteDevice {
                link_index: i,
                remote_type: NvLinkRemoteType::Gpu,
                bandwidth_mb_s: Some(50_000),
            })
            .collect();
        let b = gpu_with_numa(Some(0));
        let edge = classify(0, 1, &a, &b, 8);
        match edge {
            EdgeClass::NvLink { generation, .. } => assert_eq!(generation, Some(5)),
            other => panic!("expected NvLink, got {other:?}"),
        }
    }

    #[test]
    fn switch_remotes_without_full_gpu_mesh_classify_as_mesh() {
        let mut a = gpu_with_numa(Some(0));
        // 5 GPU links + 1 switch link — sub-mesh + switch overlay.
        a.nvlink_remote_devices = (0..5)
            .map(|_| link(NvLinkRemoteType::Gpu, Some(50_000)))
            .enumerate()
            .map(|(i, mut d)| {
                d.link_index = i as u32;
                d
            })
            .collect();
        a.nvlink_remote_devices.push(NvLinkRemoteDevice {
            link_index: 5,
            remote_type: NvLinkRemoteType::Switch,
            bandwidth_mb_s: None,
        });
        // With 8 total GPUs, peer_count=7 and GPU remotes=5 < 7, so we
        // should fall to the switch-mesh branch rather than mis-classify.
        let b = gpu_with_numa(Some(0));
        assert_eq!(classify(0, 1, &a, &b, 8), EdgeClass::NvSwitchMesh);
    }

    #[test]
    fn bandwidth_to_generation_thresholds() {
        assert_eq!(bandwidth_to_generation(0), None);
        assert_eq!(bandwidth_to_generation(20_000), Some(1));
        assert_eq!(bandwidth_to_generation(25_000), Some(4));
        assert_eq!(bandwidth_to_generation(50_000), Some(5));
        assert_eq!(bandwidth_to_generation(90_000), Some(6));
        assert_eq!(bandwidth_to_generation(150_000), Some(6));
    }

    #[test]
    fn edge_label_falls_back_to_nv_when_generation_unknown() {
        assert_eq!(
            EdgeClass::NvLink {
                count: 4,
                generation: None,
            }
            .label(),
            "NV"
        );
        assert_eq!(
            EdgeClass::NvLink {
                count: 4,
                generation: Some(5),
            }
            .label(),
            "NV5"
        );
    }

    #[test]
    fn dominant_generation_requires_populated_hints() {
        let links = vec![
            link(NvLinkRemoteType::Gpu, None),
            link(NvLinkRemoteType::Gpu, None),
        ];
        assert_eq!(dominant_generation(&links), None);
    }

    #[test]
    fn dominant_generation_picks_majority() {
        let links = vec![
            link(NvLinkRemoteType::Gpu, Some(50_000)),
            link(NvLinkRemoteType::Gpu, Some(50_000)),
            link(NvLinkRemoteType::Gpu, Some(25_000)),
        ];
        assert_eq!(dominant_generation(&links), Some(5));
    }

    #[test]
    fn dominant_generation_ties_resolve_to_none() {
        let links = vec![
            link(NvLinkRemoteType::Gpu, Some(50_000)),
            link(NvLinkRemoteType::Gpu, Some(25_000)),
        ];
        assert_eq!(dominant_generation(&links), None);
    }
}