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}