Coverage Report

Created: 2026-08-20 05:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
src/kfd.rs
Line
Count
Source
1
// SPDX-License-Identifier: Apache-2.0
2
//! AMD compute-topology detection via KFD sysfs
3
//! (`/sys/class/kfd/kfd/topology/nodes`). No `ROCm` install required — the
4
//! `amdgpu` kernel driver publishes this tree, and it is world-readable.
5
//!
6
//! Each node is either the host CPU (`gfx_target_version 0`, skipped here) or a
7
//! GPU exposing the `gfx` target its `ROCm`/HIP code objects must be built for.
8
//! `drm_render_minor` ties a node back to the DRM card the `drm` module found,
9
//! which is how a [`crate::GfxTarget`] reaches the right [`crate::GpuInfo`].
10
11
use crate::GfxTarget;
12
13
/// Root of the KFD topology tree.
14
#[allow(dead_code)] // used on Linux; unused on other targets
15
const TOPOLOGY: &str = "/sys/class/kfd/kfd/topology/nodes";
16
17
/// One GPU node from the KFD topology.
18
#[allow(dead_code)] // constructed on Linux; unused on other targets
19
#[derive(Debug, Clone, PartialEq, Eq)]
20
pub(crate) struct ComputeNode {
21
    /// Minor number of the node's render device (`128` for `renderD128`).
22
    pub render_minor: u32,
23
    /// Kernel codename for the ASIC, e.g. `cyan_skillfish`, when non-empty.
24
    pub name: Option<String>,
25
    /// Architecture the node's code objects target.
26
    pub gfx_target: GfxTarget,
27
}
28
29
/// Look up one `key value` pair in a KFD `properties` file.
30
#[allow(dead_code)] // used on Linux + in tests; unused on other targets
31
9
fn property(content: &str, key: &str) -> Option<u64> {
32
23
    for line in 
content9
.
lines9
() {
33
23
        let mut parts = line.split_whitespace();
34
23
        if parts.next() == Some(key) {
35
6
            return parts.next()
?1
.
parse5
().
ok5
();
36
17
        }
37
    }
38
3
    None
39
9
}
40
41
/// Decode `gfx_target_version` — `major * 10000 + minor * 100 + step`, so
42
/// `100103` is `gfx1013`. Returns `None` for `0`, which marks a CPU node.
43
#[allow(dead_code)] // used on Linux + in tests; unused on other targets
44
5
fn parse_gfx_target(version: u64) -> Option<GfxTarget> {
45
5
    if version == 0 {
46
1
        return None;
47
4
    }
48
12
    let 
field4
= |v: u64| u32::try_from(v).ok();
49
4
    Some(GfxTarget::new(
50
4
        field(version / 10_000)
?0
,
51
4
        field(version / 100 % 100)
?0
,
52
4
        field(version % 100)
?0
,
53
    ))
54
5
}
55
56
/// Every GPU node the KFD driver reports, in directory order. Empty when the
57
/// tree is absent (no `amdgpu`, or a kernel built without KFD).
58
#[cfg(target_os = "linux")]
59
1
pub(crate) fn nodes() -> Vec<ComputeNode> {
60
1
    let mut nodes = Vec::new();
61
1
    let Ok(
entries0
) = std::fs::read_dir(TOPOLOGY) else {
62
1
        return nodes;
63
    };
64
0
    for entry in entries.flatten() {
65
0
        let dir = entry.path();
66
0
        let Ok(properties) = std::fs::read_to_string(dir.join("properties")) else {
67
0
            continue;
68
        };
69
        // A CPU node has no gfx target; skipping it also skips its render minor
70
        // of 0, which would otherwise collide with a real card.
71
0
        let Some(gfx_target) =
72
0
            property(&properties, "gfx_target_version").and_then(parse_gfx_target)
73
        else {
74
0
            continue;
75
        };
76
0
        let Some(render_minor) = property(&properties, "drm_render_minor")
77
0
            .and_then(|m| u32::try_from(m).ok())
78
0
            .filter(|&m| m > 0)
79
        else {
80
0
            continue;
81
        };
82
0
        nodes.push(ComputeNode {
83
0
            render_minor,
84
0
            name: std::fs::read_to_string(dir.join("name"))
85
0
                .ok()
86
0
                .map(|n| n.trim().to_string())
87
0
                .filter(|n| !n.is_empty()),
88
0
            gfx_target,
89
        });
90
    }
91
0
    nodes
92
1
}
93
94
// Only the tests call this stub, and the lib target is built without
95
// `cfg(test)` — so there it really is dead, and `-D warnings` fails on it.
96
#[allow(dead_code)]
97
#[cfg(not(target_os = "linux"))]
98
pub(crate) fn nodes() -> Vec<ComputeNode> {
99
    Vec::new()
100
}
101
102
#[cfg(test)]
103
mod tests {
104
    use super::*;
105
106
    /// Trimmed from an AMD GPU node.
107
    const NODE: &str = "cpu_cores_count 0\nsimd_count 48\ngfx_target_version 100103\n\
108
                        vendor_id 4098\ndevice_id 5118\ndrm_render_minor 128\nnum_xcc 1\n";
109
110
    #[test]
111
1
    fn reads_properties_by_key() {
112
1
        assert_eq!(property(NODE, "gfx_target_version"), Some(100_103));
113
1
        assert_eq!(property(NODE, "drm_render_minor"), Some(128));
114
1
        assert_eq!(property(NODE, "simd_count"), Some(48));
115
1
        assert_eq!(property(NODE, "absent"), None);
116
1
    }
117
118
    #[test]
119
1
    fn property_matches_whole_key_only() {
120
        // A prefix must not match: `simd_count` is not `simd_count_base`.
121
1
        assert_eq!(property("simd_count_base 7\n", "simd_count"), None);
122
1
        assert_eq!(property("simd 1\nsimd_count 48\n", "simd_count"), Some(48));
123
1
    }
124
125
    #[test]
126
1
    fn property_tolerates_malformed_lines() {
127
1
        assert_eq!(property("gfx_target_version\n", "gfx_target_version"), None);
128
1
        assert_eq!(
129
1
            property("gfx_target_version x\n", "gfx_target_version"),
130
            None
131
        );
132
1
        assert_eq!(property("", "gfx_target_version"), None);
133
1
    }
134
135
    #[test]
136
1
    fn decodes_gfx_target_versions() {
137
        // A gfx1013 part.
138
1
        assert_eq!(parse_gfx_target(100_103), Some(GfxTarget::new(10, 1, 3)));
139
1
        assert_eq!(parse_gfx_target(100_300), Some(GfxTarget::new(10, 3, 0)));
140
        // MI200: step 10, which renders as the `a` in `gfx90a`.
141
1
        assert_eq!(parse_gfx_target(90_010), Some(GfxTarget::new(9, 0, 10)));
142
1
        assert_eq!(parse_gfx_target(110_000), Some(GfxTarget::new(11, 0, 0)));
143
1
    }
144
145
    #[test]
146
1
    fn cpu_nodes_have_no_gfx_target() {
147
1
        assert_eq!(parse_gfx_target(0), None);
148
1
    }
149
150
    #[test]
151
1
    fn node_listing_never_panics() {
152
        // Environment-dependent: asserts invariants, not the presence of a GPU.
153
1
        for 
node0
in nodes() {
154
0
            assert!(node.render_minor > 0);
155
0
            assert!(node.gfx_target.major > 0);
156
0
            assert!(node.name.as_ref().is_none_or(|n| !n.is_empty()));
157
        }
158
1
    }
159
}