Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
628 changes: 539 additions & 89 deletions crates/rocm-core/src/examine.rs

Large diffs are not rendered by default.

1,310 changes: 1,310 additions & 0 deletions crates/rocm-core/src/examine_gpu_proptests.rs

Large diffs are not rendered by default.

225 changes: 209 additions & 16 deletions crates/rocm-core/src/host_gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ const fn detect_linux_primary_gpu_name() -> Option<String> {
None
}

fn parse_windows_display_gfx_target(text: &str) -> Option<String> {
pub(crate) fn parse_windows_display_gfx_target(text: &str) -> Option<String> {
let mut name_fallback = None;
let mut unsupported_match = None;
for line in text.lines().map(str::trim).filter(|line| !line.is_empty()) {
Expand All @@ -633,8 +633,17 @@ fn parse_windows_display_gfx_target(text: &str) -> Option<String> {
}
return Some(target.to_owned());
}
if name_fallback.is_none() {
name_fallback = gfx_target_from_amd_marketing_name(name).map(str::to_owned);
if name_fallback.is_none()
&& let Some(target) = gfx_target_from_amd_marketing_name(name)
{
// The same rule for a name: the marketing table names gfx90c by its
// codenames, and an iGPU recognised that way must not hide a
// discrete card further down the list either.
if gfx_target_has_no_rocm_build(target) {
unsupported_match = unsupported_match.or(Some(target));
} else {
name_fallback = Some(target.to_owned());
}
}
}
name_fallback.or_else(|| unsupported_match.map(str::to_owned))
Expand Down Expand Up @@ -691,7 +700,7 @@ fn amd_pci_device_id_from_pnp_id(pnp_id: &str) -> Option<String> {
}
}

fn gfx_target_from_amd_pci_device_id(device_id: &str) -> Option<&'static str> {
pub(crate) fn gfx_target_from_amd_pci_device_id(device_id: &str) -> Option<&'static str> {
match device_id.to_ascii_lowercase().as_str() {
// Navi 21 / 22 / 23 / 24: Radeon RX 6000 desktop and mobile ASICs.
"73a0" | "73a1" | "73a2" | "73a3" | "73a5" | "73a8" | "73a9" | "73ab" | "73ac" | "73ad"
Expand Down Expand Up @@ -728,7 +737,13 @@ fn gfx_target_from_amd_pci_device_id(device_id: &str) -> Option<&'static str> {
}
}

fn gfx_target_from_amd_marketing_name(name: &str) -> Option<&'static str> {
/// The gfx target an AMD marketing name or `lspci` codename names, or `None`
/// when no entry in [`AMD_MARKETING_GFX_TARGETS`] matches.
///
/// This is the crate's one name-to-target table: the install-side display
/// probes and `examine`'s GPU probes both answer from it, so a name resolves to
/// the same target whichever command reads it.
pub(crate) fn gfx_target_from_amd_marketing_name(name: &str) -> Option<&'static str> {
let lower = name.to_ascii_lowercase();
let normalized = normalize_marketing_name_for_match(&lower);
for entry in AMD_MARKETING_GFX_TARGETS {
Expand All @@ -740,12 +755,12 @@ fn gfx_target_from_amd_marketing_name(name: &str) -> Option<&'static str> {
}

#[derive(Debug, Clone, Copy)]
struct AmdMarketingGfxTarget {
pattern: &'static str,
gfx_target: &'static str,
pub(crate) struct AmdMarketingGfxTarget {
pub(crate) pattern: &'static str,
pub(crate) gfx_target: &'static str,
}

const AMD_MARKETING_GFX_TARGETS: &[AmdMarketingGfxTarget] = &[
pub(crate) const AMD_MARKETING_GFX_TARGETS: &[AmdMarketingGfxTarget] = &[
// RDNA4 discrete.
AmdMarketingGfxTarget {
pattern: "ai pro r9700",
Expand Down Expand Up @@ -894,6 +909,17 @@ const AMD_MARKETING_GFX_TARGETS: &[AmdMarketingGfxTarget] = &[
pattern: "8040s",
gfx_target: "gfx1151",
},
// The codename and the product line, as `lspci` and `rocminfo` print them.
// There is no "8045S": the three Strix Halo SKU names are 8060S, 8050S and
// 8040S (the Ryzen AI Max PRO 380's), so this table never carried one.
AmdMarketingGfxTarget {
pattern: "strix halo",
gfx_target: "gfx1151",
},
AmdMarketingGfxTarget {
pattern: "ryzen ai max",
gfx_target: "gfx1151",
},
// RDNA3.5 APUs.
AmdMarketingGfxTarget {
pattern: "890m",
Expand All @@ -903,6 +929,10 @@ const AMD_MARKETING_GFX_TARGETS: &[AmdMarketingGfxTarget] = &[
pattern: "880m",
gfx_target: "gfx1150",
},
AmdMarketingGfxTarget {
pattern: "strix point",
gfx_target: "gfx1150",
},
AmdMarketingGfxTarget {
pattern: "860m",
gfx_target: "gfx1152",
Expand All @@ -911,6 +941,13 @@ const AMD_MARKETING_GFX_TARGETS: &[AmdMarketingGfxTarget] = &[
pattern: "840m",
gfx_target: "gfx1152",
},
// Krackan Point is gfx1152, as its device id (`1114`) and the 860M/840M it
// sells as both say. gfx1150 and gfx1152 are separate TheRock families, so
// calling it gfx1150 selects the wrong runtime build.
AmdMarketingGfxTarget {
pattern: "krackan",
gfx_target: "gfx1152",
},
AmdMarketingGfxTarget {
pattern: "820m",
gfx_target: "gfx1153",
Expand All @@ -928,6 +965,32 @@ const AMD_MARKETING_GFX_TARGETS: &[AmdMarketingGfxTarget] = &[
pattern: "740m",
gfx_target: "gfx1103",
},
// `lspci` prints the Phoenix codename with a die number (`Phoenix1`,
// `Phoenix3`), which is a different token from `phoenix`.
AmdMarketingGfxTarget {
pattern: "phoenix",
gfx_target: "gfx1103",
},
AmdMarketingGfxTarget {
pattern: "phoenix1",
gfx_target: "gfx1103",
},
AmdMarketingGfxTarget {
pattern: "phoenix2",
gfx_target: "gfx1103",
},
AmdMarketingGfxTarget {
pattern: "phoenix3",
gfx_target: "gfx1103",
},
AmdMarketingGfxTarget {
pattern: "phoenix4",
gfx_target: "gfx1103",
},
AmdMarketingGfxTarget {
pattern: "hawk point",
gfx_target: "gfx1103",
},
// RDNA2 APUs.
AmdMarketingGfxTarget {
pattern: "680m",
Expand All @@ -937,10 +1000,26 @@ const AMD_MARKETING_GFX_TARGETS: &[AmdMarketingGfxTarget] = &[
pattern: "660m",
gfx_target: "gfx1035",
},
AmdMarketingGfxTarget {
pattern: "rembrandt",
gfx_target: "gfx1035",
},
// Mendocino (Ryzen 7020 series) sells as a Radeon 610M too, which the row
// below answers as gfx1036; only the codename tells the two apart, so it is
// listed ahead of that row for `lspci`'s sake. A Windows Mendocino, named
// only "Radeon(TM) 610M", still reads as gfx1036 by name.
AmdMarketingGfxTarget {
pattern: "mendocino",
gfx_target: "gfx1037",
},
AmdMarketingGfxTarget {
pattern: "610m",
gfx_target: "gfx1036",
},
AmdMarketingGfxTarget {
pattern: "raphael",
gfx_target: "gfx1036",
},
AmdMarketingGfxTarget {
pattern: "steam deck",
gfx_target: "gfx1033",
Expand All @@ -949,6 +1028,37 @@ const AMD_MARKETING_GFX_TARGETS: &[AmdMarketingGfxTarget] = &[
pattern: "van gogh",
gfx_target: "gfx1033",
},
// `lspci` spells it as one word, and the Steam Deck's own display name is
// the part number rather than any Radeon model.
AmdMarketingGfxTarget {
pattern: "vangogh",
gfx_target: "gfx1033",
},
AmdMarketingGfxTarget {
pattern: "custom gpu 0405",
gfx_target: "gfx1033",
},
// Vega iGPUs in Ryzen 4000/5000/7030 parts. They sell as a bare "AMD
// Radeon Graphics", so the codename `lspci` prints is the only name that
// identifies them. gfx90c has no ROCm build (`gfx_target_has_no_rocm_build`),
// which is why the install-side probes rank a name answering it below every
// other recognised adapter.
AmdMarketingGfxTarget {
pattern: "renoir",
gfx_target: "gfx90c",
},
AmdMarketingGfxTarget {
pattern: "cezanne",
gfx_target: "gfx90c",
},
AmdMarketingGfxTarget {
pattern: "lucienne",
gfx_target: "gfx90c",
},
AmdMarketingGfxTarget {
pattern: "barcelo",
gfx_target: "gfx90c",
},
];

fn normalize_marketing_name_for_match(value: &str) -> String {
Expand Down Expand Up @@ -1780,16 +1890,22 @@ impl WindowsExamineInventory {
.iter()
.find(|display| pci_target(display).is_some_and(|t| !gfx_target_has_no_rocm_build(t)))
.or_else(|| {
self.displays
.iter()
.find(|display| gfx_target_from_amd_marketing_name(&display.name).is_some())
self.displays.iter().find(|display| {
gfx_target_from_amd_marketing_name(&display.name)
.is_some_and(|t| !gfx_target_has_no_rocm_build(t))
})
})
// A recognized-but-unsupported iGPU (gfx90c) ranks below every
// other recognized adapter, so it cannot hide a discrete card.
// other recognized adapter, so it cannot hide a discrete card --
// but above an adapter nothing recognizes, whether the iGPU was
// recognized by its device id or by its name. That is the order
// `parse_windows_display_gfx_target` takes, so the adapter named
// here is the one whose target is reported.
.or_else(|| {
self.displays
.iter()
.find(|display| pci_target(display).is_some())
self.displays.iter().find(|display| {
pci_target(display).is_some()
|| gfx_target_from_amd_marketing_name(&display.name).is_some()
})
})
.or_else(|| {
self.displays
Expand Down Expand Up @@ -3863,6 +3979,83 @@ mod tests {
);
}

/// A gfx90c iGPU recognised by its *name* must not hide a discrete card
/// either.
///
/// The name table now answers gfx90c for the Vega iGPUs' codenames, which
/// is what `examine` needs; on the install side that answer has to rank
/// where the PCI-id answer for the same iGPU already does, below every
/// other recognised adapter, or the first adapter's name would decide the
/// install family for the whole host.
#[test]
fn windows_display_parser_does_not_let_a_named_vega_igpu_hide_a_discrete_card() {
let text = "AMD Radeon(TM) Graphics (Renoir)\t\n\
AMD Radeon RX 6800M\tPCI\\VEN_1002&DEV_9999";
assert_eq!(
parse_windows_display_gfx_target(text),
Some("gfx1031".to_owned())
);
let text = "AMD Radeon(TM) Graphics (Renoir)\t";
assert_eq!(
parse_windows_display_gfx_target(text),
Some("gfx90c".to_owned()),
"alone, the iGPU is still reported as gfx90c"
);

let inventory = WindowsExamineInventory {
displays: vec![
WindowsDisplayAdapter {
name: "AMD Radeon(TM) Graphics (Renoir)".to_owned(),
driver_version: None,
driver_date: None,
pnp_device_id: None,
},
WindowsDisplayAdapter {
name: "AMD Radeon RX 6800M".to_owned(),
driver_version: None,
driver_date: None,
pnp_device_id: None,
},
],
..WindowsExamineInventory::default()
};
assert_eq!(
inventory.amd_display_name().as_deref(),
Some("AMD Radeon RX 6800M")
);
}

/// The other half of the same ranking: a gfx90c iGPU recognised by its
/// *name* still outranks an adapter nothing recognises, on both sides.
///
/// `parse_windows_display_gfx_target` reports gfx90c for this host, so the
/// adapter `preferred_amd_display` names must be the iGPU the target came
/// from, not whichever unrecognised adapter happens to be listed first.
#[test]
fn a_named_vega_igpu_outranks_an_unrecognised_adapter_in_both_install_probes() {
let unrecognised = "AMD Radeon Pro Unlisted";
let igpu = "AMD Radeon(TM) Graphics (Renoir)";
assert_eq!(gfx_target_from_amd_marketing_name(unrecognised), None);
assert_eq!(
parse_windows_display_gfx_target(&format!("{unrecognised}\t\n{igpu}\t")),
Some("gfx90c".to_owned())
);

let inventory = WindowsExamineInventory {
displays: [unrecognised, igpu]
.into_iter()
.map(|name| WindowsDisplayAdapter {
name: name.to_owned(),
driver_version: None,
driver_date: None,
pnp_device_id: None,
})
.collect(),
..WindowsExamineInventory::default()
};
assert_eq!(inventory.amd_display_name().as_deref(), Some(igpu));
}

#[test]
fn known_therock_families_all_round_trip() {
for family in known_therock_families() {
Expand Down
26 changes: 26 additions & 0 deletions docs/manual-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,32 @@ On Ubuntu 22.04, 24.04 and 26.04 the apt source line names the matching suite
first prepare command prints a "from a previous installation" warning; confirm
nothing is removed (`dpkg -s amdgpu-dkms` still reports installed afterwards).

## 2c. Integrated or discrete

On a machine with an AMD integrated GPU, check what `rocm examine --json`
reports for it under `gpus`:

```bash
rocm examine --framework skip --json
```

Expected result:

- Each AMD GPU carries a `gfx_target`, and an integrated one carries
`is_apu: true`. On a Ryzen 7000 or later desktop the iGPU is `gfx1036`; on a
Ryzen 6000 laptop `gfx1035`; on a Krackan Point laptop (Radeon 860M or 840M)
`gfx1152`, never `gfx1150`; on a Ryzen 4000 or 5000 machine `gfx90c`; on a
Ryzen 2000 or 3000 laptop (Raven Ridge or Picasso) `gfx902` or `gfx909`,
which today only the kernel reports, so an `is_apu: false` there means the
topology's target did not reach the verdict.
- A discrete card beside it carries `is_apu: false`, and `has_apu` and
`has_discrete_amd` are both `true`. An iGPU reported with `is_apu: false`, or
an iGPU-only machine with `has_discrete_amd: true`, is a regression unless
the GPU's `gfx_target` is empty (an unidentified row is reported as discrete
on purpose).
- On a machine whose only GPU is the iGPU, `rocm serve` does not warn about low
VRAM.

## 3. Lemonade GPU Verification

Lemonade is the default local assistant/server engine. Serve a small assistant
Expand Down
Loading