Skip to main content

rusty_stack/
gpu.rs

1//! GPU classification — the SINGLE source of truth for distinguishing AMD
2//! discrete GPUs (dGPUs) from integrated GPUs (iGPUs).
3//!
4//! # Tenet (v0.3.0 remediation)
5//!
6//! "The iGPU is CONSISTENTLY filtered from env vars and is NEVER included —
7//! EVER. iGPUs are NOT supported and will ONLY cause issues. Intelligent
8//! filtering must avoid including the iGPU from env vars or ANY component,
9//! while ensuring dGPUs are never missed."
10//!
11//! This module exists because the crate previously had **three divergent**
12//! iGPU classifiers (`installer.rs::is_igpu_name`, `bootstrap/env_setup.rs::
13//! is_integrated_gpu_name`, and inline checks in `hardware.rs` /
14//! `platform/linux.rs` / `benchmarks`). They disagreed, and three consumers
15//! bypassed filtering entirely (`textgen`/`comfyui` hardcoded `"0,1"`;
16//! `migraphx_python` trusted inherited `HIP_VISIBLE_DEVICES`). The leaking
17//! iGPU broke the MIGraphX/ONNX worker. Everything now routes through here.
18//!
19//! # Classification rule
20//!
21//! A device is **integrated** if ANY of:
22//! 1. Its PCI device id is in [`INTEGRATED_PCI_DEVICE_IDS`] (structural,
23//!    definitive — e.g. Raphael `0x164e`).
24//! 2. Its marketing/lspci name matches [`is_integrated_gpu_name`] (rich
25//!    curated heuristic: APU codenames, "Ryzen && !RX", APU model suffixes).
26//!
27//! [`device_is_integrated`] additionally treats a low-VRAM device
28//! (`< 4 GiB`) as integrated when the name is ambiguous — but only as a
29//! **confirmation**, never as the sole reason to drop a device whose VRAM is
30//! unreadable (that would risk missing a real dGPU on kernels without
31//! `mem_info_vram_total`). dGPUs (Navi 31/32, 24/16 GB) are never caught.
32
33/// PCI device IDs of known AMD integrated GPUs (APU/iGPU).
34///
35/// Conservative — only IDs that are *definitively* integrated, so the denylist
36/// can never accidentally exclude a discrete card. Extend as new APU families
37/// ship.
38pub const INTEGRATED_PCI_DEVICE_IDS: &[&str] = &[
39    "0x164e", // Raphael — Ryzen 7000 desktop iGPU (e.g. 7800X3D)
40    "0x15c8", // Phoenix — Ryzen 7040 mobile APU
41    "0x15e0", // Phoenix-Hawk
42    "0x1681", // Strix Point (Ryzen AI 300)
43    "0x1506", // Renoir — Ryzen 4000/5000 APU
44    "0x1638", // Rembrandt — Ryzen 6000/7040 APU
45    "0x15d8", // Raven Ridge — Ryzen 2000/3000 APU
46    "0x1640", // Cezanne/Barcelo — Ryzen 5000 APU
47];
48
49/// VRAM threshold below which a device is treated as integrated (4 GiB).
50pub const DISCRETE_MIN_VRAM_BYTES: u64 = 4 * 1024 * 1024 * 1024;
51
52/// AMD gfx architectures that are **unambiguously integrated** (iGPU-only).
53///
54/// These are the structural fallback when a device's marketing name is empty
55/// (rocminfo/lspci sometimes report only `Name: gfxNNNN` for the iGPU agent).
56/// Every entry is an iGPU-only architecture — no discrete card ships with one —
57/// so this list can never cause a dGPU to be missed. Extend cautiously: only
58/// add a gfx id once confirmed iGPU-exclusive.
59pub const INTEGRATED_GFX_ARCHS: &[&str] = &[
60    "gfx1036", // Raphael — Ryzen 7000 desktop iGPU (e.g. 7800X3D)
61    "gfx1103", // Phoenix — Ryzen 7040 mobile APU iGPU
62];
63
64/// Is this gfx architecture string an iGPU-only architecture?
65pub fn is_integrated_by_gfx_arch(gfx: &str) -> bool {
66    let gfx = gfx.trim().to_ascii_lowercase();
67    INTEGRATED_GFX_ARCHS
68        .iter()
69        .any(|a| a.eq_ignore_ascii_case(&gfx))
70}
71
72/// Curated marketing/lspci name fragments indicating an AMD integrated GPU.
73pub const INTEGRATED_NAME_PATTERNS: &[&str] = &[
74    // APU codenames (internal names from rocminfo / lspci)
75    "Cezanne",
76    "Rembrandt",
77    "Phoenix",
78    "Raphael",
79    "Barcelo",
80    "Pink Sardine",
81    "Yellow Carp",
82    "Green Sardine",
83    "Hawk Point",
84    "Dragon Range",
85    "Fire Range",
86    "Stella",
87    "Krackan",
88    "Strix Point",
89    "Mendocino",
90    "Raven",
91    "Picasso",
92    "Renoir",
93    // Marketing patterns
94    "Integrated",
95    "iGPU",
96    "APU",
97    "AMD Radeon Graphics",
98    "Radeon Graphics",
99    // APU model suffixes
100    " 5600G",
101    " 5700G",
102    " 5600GE",
103    " 5700GE",
104    " 5700G",
105    " 5600GT",
106    " 5700GT",
107    " 8600G",
108    " 8700G",
109    " 8500G",
110    " 8300G",
111    // X3D APUs with integrated graphics (Raphael/Phoenix-based)
112    "7800X3D",
113    "7950X3D",
114    "7900X3D",
115    "7700X3D",
116    "7600X3D",
117    // Mobile APU series
118    "Ryzen 7 7735",
119    "Ryzen 7 7840",
120    "Ryzen 7 8840",
121    "Ryzen 5 7535",
122    "Ryzen 5 7640",
123    "Ryzen 5 8640",
124    "Ryzen 9 7945",
125    "Ryzen 9 8945",
126];
127
128/// Pre-lowercased snapshot of [`INTEGRATED_NAME_PATTERNS`], computed once.
129///
130/// Avoids re-allocating `pattern.to_lowercase()` on every call to
131/// [`is_integrated_gpu_name`] (the patterns are a `const`, so they cannot be
132/// stored pre-lowercased at const-eval time).
133fn integrated_name_patterns_lower() -> &'static [String] {
134    use std::sync::OnceLock;
135    static LOWER: OnceLock<Vec<String>> = OnceLock::new();
136    LOWER.get_or_init(|| {
137        INTEGRATED_NAME_PATTERNS
138            .iter()
139            .map(|p| p.to_lowercase())
140            .collect()
141    })
142}
143
144/// Does a marketing/lspci/rocminfo name indicate an AMD **integrated** GPU?
145///
146/// This is the canonical name classifier — the single replacement for the
147/// three former divergent copies. Returns `false` for discrete cards
148/// ("Radeon RX 7900 XTX", "RX 7800 XT", …) and `true` for APUs/iGPUs.
149pub fn is_integrated_gpu_name(marketing_name: &str) -> bool {
150    let name_lower = marketing_name.to_lowercase();
151    let name_upper = marketing_name.to_uppercase();
152
153    // Case-insensitive match — rocminfo/lspci casing varies ("raphael",
154    // "Raphael", "RAPHAEL"). Patterns are compared against the lowercased name
155    // via a pre-lowercased snapshot (computed once, not per iteration).
156    for pattern in integrated_name_patterns_lower() {
157        if name_lower.contains(pattern) {
158            return true;
159        }
160    }
161
162    // "Ryzen" without "RX" ⇒ APU/iGPU. Discrete cards never contain "Ryzen".
163    if name_upper.contains("RYZEN") && !name_upper.contains("RX") {
164        return true;
165    }
166
167    // APU model suffixes: "Ryzen 5 5600G", "Ryzen 7 8700G", "Ryzen 5 5600GE",
168    // "Ryzen 5 5600GT" (but not RX/Radeon/XT/XTX). Iterate ALL words after
169    // "RYZEN" — standard AMD branding inserts a generation/tier number ("5"/"7")
170    // between "Ryzen" and the model, so the suffix is NOT the first word.
171    if let Some(pos) = name_upper.find("RYZEN") {
172        let after_ryzen = &name_upper[pos + 5..];
173        for word in after_ryzen.split_whitespace() {
174            if (word.ends_with('G') || word.ends_with("GE") || word.ends_with("GT"))
175                && word.len() >= 4
176                && !word.starts_with("RX")
177                && !word.starts_with("RADEON")
178                && !word.contains("XT")
179                && !word.contains("XTX")
180            {
181                return true;
182            }
183        }
184    }
185
186    // Generic "Radeon Graphics" without a model ⇒ iGPU.
187    if name_upper.contains("RADEON GRAPHICS")
188        && !name_upper.contains("RX")
189        && (!marketing_name.chars().any(|c| c.is_ascii_digit())
190            || marketing_name.contains("AMD Radeon Graphics"))
191    {
192        return true;
193    }
194
195    false
196}
197
198/// Is this PCI device id a known integrated GPU?
199///
200/// `pci_device_id` may be lowercase/uppercase and with or without a `0x`
201/// prefix (e.g. `"0x164e"`, `"164E"`).
202pub fn is_integrated_by_pci_id(pci_device_id: &str) -> bool {
203    let normalized = pci_device_id.trim().to_ascii_lowercase();
204    let normalized = normalized.trim_start_matches("0x");
205    INTEGRATED_PCI_DEVICE_IDS
206        .iter()
207        .any(|id| id.trim_start_matches("0x").eq_ignore_ascii_case(normalized))
208}
209
210/// Combined, fail-safe classification of a single device.
211///
212/// Inputs are all optional — provide whatever the caller has. Returns `true`
213/// (integrated) when ANY of:
214/// - the PCI device id is a known iGPU (`pci_device_id`), OR
215/// - the gfx architecture is iGPU-only (`gfx_arch`, e.g. `gfx1036` Raphael) —
216///   the structural fallback when the marketing name is empty, OR
217/// - the name classifies as integrated (`name`), OR
218/// - the name is ambiguous AND the device's VRAM is readable and below
219///   [`DISCRETE_MIN_VRAM_BYTES`] (confirmation only).
220///
221/// Unreadable VRAM alone NEVER classifies a device as integrated (that would
222/// risk dropping a real dGPU on kernels lacking `mem_info_vram_total`); the
223/// PCI id, gfx arch, and name are the authoritative signals. A dGPU is never
224/// missed: every exclusion requires a positive iGPU signal.
225pub fn device_is_integrated(
226    name: Option<&str>,
227    pci_device_id: Option<&str>,
228    gfx_arch: Option<&str>,
229    vram_bytes: Option<u64>,
230) -> bool {
231    if let Some(id) = pci_device_id {
232        if is_integrated_by_pci_id(id) {
233            return true;
234        }
235    }
236    if let Some(g) = gfx_arch {
237        if !g.trim().is_empty() && is_integrated_by_gfx_arch(g) {
238            return true;
239        }
240    }
241    if let Some(name) = name {
242        if !name.trim().is_empty() && is_integrated_gpu_name(name) {
243            return true;
244        }
245        // VRAM confirmation: only when the name is ambiguous (carries NO
246        // positive dGPU signal) AND VRAM is concretely readable + low. Never
247        // the sole reason, and never VRAM-rejects an explicitly-named dGPU
248        // (e.g. a <4 GiB Radeon RX / Radeon Pro / Instinct / FirePro card) —
249        // "dGPUs are never missed."
250        //
251        // The marker is an EXPLICIT dGPU qualifier only ("Radeon RX", " RX ",
252        // "Radeon Pro", "Instinct", "FirePro"). A bare brand name ("Radeon",
253        // "AMD Radeon") with no model qualifier is ambiguous — that is the APU
254        // reporting style — and must fall through to the VRAM gate rather than
255        // being treated as authoritative discrete. (Matching on "RADEON" alone
256        // would shield bare iGPU marketing names from the gate, leaking the
257        // iGPU.) RDNA iGPUs with model numbers ("Radeon 780M", "Radeon 680M")
258        // carry no qualifier either, so they too fall through and are gated by
259        // VRAM — correct, since they are APUs. The only cards lost are
260        // pre-RDNA "Radeon HD" dGPUs, which no ROCm release supports.
261        if let Some(vram) = vram_bytes {
262            let upper = name.to_ascii_uppercase();
263            let has_discrete_marker = upper.contains("RADEON RX")
264                || upper.contains(" RX ")
265                || upper.contains("RADEON PRO")
266                || upper.contains("INSTINCT")
267                || upper.contains("FIREPRO");
268            if !name.trim().is_empty() && !has_discrete_marker && vram < DISCRETE_MIN_VRAM_BYTES {
269                return true;
270            }
271        }
272    }
273    false
274}
275
276/// Read a PCI device's `mem_info_vram_total` (bytes) from sysfs, if available.
277///
278/// `pci_slot` is the `BB:DD.F` form (with or without a `0000:` domain prefix).
279pub fn read_vram_bytes(pci_slot: &str) -> Option<u64> {
280    let slot = pci_slot.trim().trim_start_matches("0000:");
281    let path = format!("/sys/bus/pci/devices/0000:{slot}/mem_info_vram_total");
282    let value = std::fs::read_to_string(&path).ok()?;
283    value.trim().parse::<u64>().ok()
284}
285
286// ===========================================================================
287// Tests
288// ===========================================================================
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn discrete_cards_are_not_integrated() {
296        assert!(!is_integrated_gpu_name("AMD Radeon RX 7900 XTX"));
297        assert!(!is_integrated_gpu_name("Radeon RX 7800 XT"));
298        assert!(!is_integrated_gpu_name("AMD Radeon RX 6900 XT"));
299        assert!(!is_integrated_gpu_name("AMD Radeon RX 9070 XT"));
300    }
301
302    #[test]
303    fn apu_codenames_are_integrated() {
304        assert!(is_integrated_gpu_name("Raphael"));
305        assert!(is_integrated_gpu_name("raphael")); // case-insensitive (rocminfo/lspci varies)
306        assert!(is_integrated_gpu_name("RAPHAEL"));
307        assert!(!is_integrated_gpu_name("gfx1036")); // raw gfx arch is NOT a name signal
308        assert!(is_integrated_gpu_name("AMD Cezanne (Radeon Graphics)"));
309        assert!(is_integrated_gpu_name("Phoenix"));
310        assert!(is_integrated_gpu_name("Rembrandt"));
311        assert!(is_integrated_gpu_name("Strix Point")); // Ryzen AI 300 APU
312    }
313
314    #[test]
315    fn asus_strix_dgpu_is_not_integrated() {
316        // "Strix Point" is an APU codename, but "ROG Strix" is an Asus dGPU
317        // board line — must NOT be filtered. (Regression guard.)
318        assert!(!is_integrated_gpu_name("ASUS ROG Strix Radeon RX 7900 XTX"));
319        assert!(!is_integrated_gpu_name("ROG Strix LC Radeon RX 7800 XT"));
320    }
321
322    #[test]
323    fn ryzen_cpus_with_igpu_are_integrated() {
324        assert!(is_integrated_gpu_name(
325            "AMD Ryzen 7 7800X3D 8-Core Processor"
326        ));
327        assert!(is_integrated_gpu_name(
328            "AMD Ryzen 9 7950X3D 16-Core Processor"
329        ));
330        assert!(is_integrated_gpu_name("AMD Ryzen 5 8600G 6-Core Processor"));
331        assert!(is_integrated_gpu_name("AMD Ryzen 7 8700G"));
332    }
333
334    #[test]
335    fn ryzen_apu_suffix_without_amd_prefix_is_integrated() {
336        // Regression: the old " RYZEN " search + first-word check never matched
337        // "Ryzen 5 5600G" (no leading space, model number is the 2nd word).
338        assert!(is_integrated_gpu_name("Ryzen 5 5600G"));
339        assert!(is_integrated_gpu_name("Ryzen 7 5700G"));
340        assert!(is_integrated_gpu_name("Ryzen 5 5600GE"));
341        assert!(is_integrated_gpu_name("Ryzen 5 5600GT"));
342        assert!(is_integrated_gpu_name("Ryzen 7 8700G"));
343    }
344
345    #[test]
346    fn low_vram_discrete_cards_are_not_vram_rejected() {
347        // Regression: <4 GiB must NOT drop an explicitly-named dGPU
348        // ("dGPUs are never missed"). Older Radeon RX/Pro cards <4 GB.
349        let low = 2 * 1024 * 1024 * 1024u64;
350        assert!(!device_is_integrated(
351            Some("AMD Radeon RX 550"),
352            None,
353            None,
354            Some(low)
355        ));
356        assert!(!device_is_integrated(
357            Some("AMD Radeon Pro WX 2100"),
358            None,
359            None,
360            Some(low)
361        ));
362        assert!(!device_is_integrated(
363            Some("AMD Instinct MI50"),
364            None,
365            None,
366            Some(low)
367        ));
368        // Ambiguous name + low VRAM is still treated as integrated.
369        assert!(device_is_integrated(
370            Some("AMD Device"),
371            Some("0x9999"),
372            None,
373            Some(512 * 1024 * 1024)
374        ));
375    }
376
377    #[test]
378    fn bare_radeon_marketing_name_is_vram_gated() {
379        // Regression (PR #21 re-review): a bare brand string with no model
380        // qualifier, gfx arch, or PCI id at <4 GiB is the iGPU reporting style
381        // and must be VRAM-gated to integrated — NOT shielded by a "RADEON"
382        // substring. The marker list matches only explicit qualifiers.
383        let low = 2 * 1024 * 1024 * 1024u64;
384        assert!(device_is_integrated(Some("Radeon"), None, None, Some(low)));
385        assert!(device_is_integrated(
386            Some("AMD Radeon"),
387            None,
388            None,
389            Some(low)
390        ));
391
392        // RDNA iGPU models with no qualifier ("Radeon 780M" = Phoenix/Strix APU)
393        // also fall through to the VRAM gate and are correctly integrated — a
394        // naive "has-digit ⇒ discrete" rule would misclassify these as dGPUs.
395        assert!(device_is_integrated(
396            Some("AMD Radeon 780M"),
397            None,
398            None,
399            Some(low)
400        ));
401        assert!(device_is_integrated(
402            Some("AMD Radeon 680M"),
403            None,
404            None,
405            Some(low)
406        ));
407
408        // Same bare names with ample VRAM are NOT integrated (fail-safe: a
409        // bare name is never an authoritative dGPU signal, but ample VRAM
410        // declines to reclassify).
411        let high = 16 * 1024 * 1024 * 1024u64;
412        assert!(!device_is_integrated(
413            Some("Radeon"),
414            None,
415            None,
416            Some(high)
417        ));
418
419        // Explicitly-named dGPUs survive the gate regardless of VRAM.
420        assert!(!device_is_integrated(
421            Some("AMD Radeon RX 7900 XTX"),
422            None,
423            None,
424            Some(low)
425        ));
426    }
427
428    #[test]
429    fn generic_radeon_graphics_is_integrated() {
430        assert!(is_integrated_gpu_name("AMD Radeon Graphics"));
431        assert!(!is_integrated_gpu_name("AMD Radeon RX Graphics 7900"));
432    }
433
434    #[test]
435    fn pci_id_denylist_catches_raphael() {
436        assert!(is_integrated_by_pci_id("0x164e"));
437        assert!(is_integrated_by_pci_id("164E"));
438        assert!(is_integrated_by_pci_id("0x164E"));
439        assert!(!is_integrated_by_pci_id("0x744c")); // Navi 31 dGPU (7900 XTX)
440        assert!(!is_integrated_by_pci_id("0x747e")); // Navi 32 dGPU (7800 XT)
441    }
442
443    #[test]
444    fn device_is_integrated_combines_signals() {
445        // Name-only iGPU.
446        assert!(device_is_integrated(Some("Raphael"), None, None, None));
447        // PCI id-only iGPU.
448        assert!(device_is_integrated(None, Some("0x164e"), None, None));
449        // gfx-arch-only iGPU (nameless iGPU agent fallback).
450        assert!(device_is_integrated(None, None, Some("gfx1036"), None));
451        assert!(device_is_integrated(None, None, Some("gfx1103"), None));
452        // dGPU by name with large VRAM ⇒ not integrated.
453        assert!(!device_is_integrated(
454            Some("AMD Radeon RX 7900 XTX"),
455            Some("0x744c"),
456            Some("gfx1100"),
457            Some(24 * 1024 * 1024 * 1024)
458        ));
459        // Ambiguous name + low readable VRAM ⇒ integrated (confirmation).
460        assert!(device_is_integrated(
461            Some("AMD Device"),
462            Some("0x9999"),
463            None,
464            Some(512 * 1024 * 1024)
465        ));
466        // Ambiguous name + UNREADABLE VRAM ⇒ NOT integrated (never miss a dGPU).
467        assert!(!device_is_integrated(
468            Some("AMD Device"),
469            Some("0x9999"),
470            None,
471            None
472        ));
473    }
474
475    #[test]
476    fn gfx_arch_signal_is_safe_for_dgpus() {
477        // iGPU-only archs are caught.
478        assert!(is_integrated_by_gfx_arch("gfx1036"));
479        assert!(is_integrated_by_gfx_arch("GFX1103"));
480        // dGPU archs are NEVER caught (no false exclusions).
481        assert!(!is_integrated_by_gfx_arch("gfx1100"));
482        assert!(!is_integrated_by_gfx_arch("gfx1101"));
483        assert!(!is_integrated_by_gfx_arch("gfx1030"));
484    }
485}