use std::path::{Path, PathBuf};
use std::sync::OnceLock;
const ORDERED_SONAMES: &[&str] = &[
"libamdhip64.so", "librocblas.so", "libhipblas.so", "librocfft.so", "libMIOpen.so", "libmigraphx_c.so", ];
#[derive(Debug, Default, Clone)]
pub struct RocmPreload {
pub loaded: Vec<String>,
pub dirs: Vec<PathBuf>,
pub hip: bool,
pub miopen: bool,
}
static PRELOAD: OnceLock<RocmPreload> = OnceLock::new();
pub fn ensure() -> &'static RocmPreload {
PRELOAD.get_or_init(|| {
apply_gfx_override();
apply_migraphx_cache_dir();
run()
})
}
pub const ORT_MIGRAPHX_MODEL_CACHE_PATH_ENV: &str = "ORT_MIGRAPHX_MODEL_CACHE_PATH";
pub const ORT_MIGRAPHX_CACHE_PATH_ENV: &str = "ORT_MIGRAPHX_CACHE_PATH";
pub fn default_migraphx_cache_dir() -> PathBuf {
match std::env::var_os("HOME") {
Some(home) if !home.is_empty() => {
Path::new(&home).join(".cache").join("nornir").join("migraphx")
}
_ => PathBuf::from(".nornir-migraphx"),
}
}
pub fn apply_migraphx_cache_dir() {
if std::env::var_os(ORT_MIGRAPHX_MODEL_CACHE_PATH_ENV).is_some_and(|v| !v.is_empty()) {
return; }
let dir = default_migraphx_cache_dir();
if std::fs::create_dir_all(&dir).is_err() {
return; }
unsafe {
std::env::set_var(ORT_MIGRAPHX_MODEL_CACHE_PATH_ENV, &dir);
if std::env::var_os(ORT_MIGRAPHX_CACHE_PATH_ENV).is_none() {
std::env::set_var(ORT_MIGRAPHX_CACHE_PATH_ENV, &dir);
}
}
}
pub const GFX_HSA_OVERRIDE_TABLE: &[(&str, &str)] = &[
("gfx1150", "11.5.0"), ("gfx1151", "11.5.1"), ("gfx1103", "11.0.3"), ("gfx1102", "11.0.2"), ("gfx1101", "11.0.1"), ("gfx1100", "11.0.0"), ("gfx1036", "10.3.6"), ("gfx1035", "10.3.5"), ];
pub const GFX_NATIVE_ROCM: &[&str] = &[
"gfx1150", "gfx1151", "gfx1100", "gfx1101", "gfx1102", "gfx942", "gfx90a", "gfx908", ];
pub fn hsa_override_for(gfx: &str) -> Option<&'static str> {
GFX_HSA_OVERRIDE_TABLE.iter().find(|(g, _)| *g == gfx).map(|(_, v)| *v)
}
pub fn gfx_natively_supported(gfx: &str) -> bool {
GFX_NATIVE_ROCM.contains(&gfx)
}
pub const NORNIR_HSA_OVERRIDE_ENV: &str = "NORNIR_HSA_OVERRIDE";
pub fn available() -> bool {
let p = ensure();
driver_present() && p.hip && p.miopen
}
#[derive(Debug, Default, Clone)]
pub struct GfxInfo {
pub gfx: Option<String>,
pub applied_override: Option<String>,
pub user_preset: bool,
pub reason: String,
}
static GFX: OnceLock<GfxInfo> = OnceLock::new();
pub fn detect_gfx() -> Option<String> {
if let Some(v) = std::env::var_os("NORNIR_GFX_OVERRIDE") {
let s = v.to_string_lossy().trim().to_string();
if !s.is_empty() {
return Some(s);
}
}
let root = std::path::Path::new("/sys/class/kfd/kfd/topology/nodes");
let entries = std::fs::read_dir(root).ok()?;
for e in entries.flatten() {
let props = e.path().join("properties");
let Ok(text) = std::fs::read_to_string(&props) else {
continue;
};
if let Some(g) = parse_gfx_target(&text) {
return Some(g);
}
}
None
}
pub fn parse_gfx_target(properties: &str) -> Option<String> {
for line in properties.lines() {
let mut it = line.split_whitespace();
if it.next() == Some("gfx_target_version") {
let packed: u32 = it.next()?.parse().ok()?;
if packed == 0 {
return None; }
let major = packed / 10000;
let minor = (packed / 100) % 100;
let step = packed % 100;
return Some(format!("gfx{major}{minor}{step:x}"));
}
}
None
}
pub fn decide_hsa_override(
detected_gfx: Option<&str>,
user_preset: bool,
policy: Option<&str>,
) -> (Option<String>, String) {
if user_preset {
return (None, "HSA_OVERRIDE_GFX_VERSION already set by user — left untouched".into());
}
if let Some(pol) = policy {
let p = pol.trim();
if p.is_empty() || p.eq_ignore_ascii_case("off") || p.eq_ignore_ascii_case("none") {
return (None, format!("auto-override disabled via {NORNIR_HSA_OVERRIDE_ENV}={pol}"));
}
return (
Some(p.to_string()),
format!("explicit {NORNIR_HSA_OVERRIDE_ENV}={p} applied"),
);
}
match detected_gfx {
None => (None, "no GPU ISA readable from sysfs → no override".into()),
Some(g) if gfx_natively_supported(g) => (
None,
format!("{g} is supported natively by recent ROCm → no override (native detection works)"),
),
Some(g) => match hsa_override_for(g) {
Some(v) => (
Some(v.to_string()),
format!("{g} not natively listed → HSA_OVERRIDE_GFX_VERSION={v} \
(spoof to nearest supported ISA from the gfx table)"),
),
None => (
None,
format!("{g} detected, no table entry → no override (let ROCm decide)"),
),
},
}
}
pub fn gfx_info() -> &'static GfxInfo {
GFX.get_or_init(|| {
let gfx = detect_gfx();
let user_preset = std::env::var_os("HSA_OVERRIDE_GFX_VERSION")
.is_some_and(|v| !v.is_empty());
let policy = std::env::var("NORNIR_HSA_OVERRIDE").ok();
let (applied_override, reason) =
decide_hsa_override(gfx.as_deref(), user_preset, policy.as_deref());
GfxInfo { gfx, applied_override, user_preset, reason }
})
}
pub fn apply_gfx_override() {
let info = gfx_info();
if let Some(val) = &info.applied_override {
if std::env::var_os("HSA_OVERRIDE_GFX_VERSION").is_none() {
unsafe { std::env::set_var("HSA_OVERRIDE_GFX_VERSION", val) };
}
}
}
pub fn xdna_npu_present() -> bool {
std::path::Path::new("/dev/accel/accel0").exists()
}
pub fn kfd_present() -> bool {
std::path::Path::new("/dev/kfd").exists()
}
fn run() -> RocmPreload {
let dirs = candidate_dirs();
let mut out = RocmPreload {
dirs: dirs.clone(),
..Default::default()
};
for soname in ORDERED_SONAMES {
if let Some(path) = find_lib(&dirs, soname) {
if dlopen_global(&path) {
out.loaded.push(soname.to_string());
if soname.starts_with("libamdhip64") {
out.hip = true;
} else if soname.starts_with("libMIOpen") {
out.miopen = true;
}
}
}
}
out
}
pub fn driver_present() -> bool {
use libloading::os::unix::{Library, RTLD_NOW};
unsafe { Library::open(Some(std::ffi::OsStr::new("libamdhip64.so")), RTLD_NOW) }.is_ok()
}
fn yn(b: bool) -> &'static str {
if b {
"found"
} else {
"MISSING"
}
}
pub fn preflight() -> (bool, String) {
let p = ensure();
let driver = driver_present();
let gpu_ready = available();
let gfx = gfx_info();
let ort = onnxruntime_dylib();
let mut s = String::from("nornir ROCm preflight (embed-ort-rocm AMD GPU path)\n");
s.push_str(&format!(" /dev/kfd (amdgpu compute) : {}\n", yn(kfd_present())));
s.push_str(&format!(
" iGPU/dGPU ISA target : {}\n",
gfx.gfx.as_deref().unwrap_or("(not readable from sysfs)")
));
s.push_str(&format!(" AMD HIP runtime libamdhip64 : {}\n", yn(driver && p.hip)));
s.push_str(&format!(" MIOpen (ROCm DNN) : {}\n", yn(p.miopen)));
s.push_str(&format!(
" ROCm-onnxruntime : {}\n",
match &ort {
Some(path) => format!("found ({})", path.display()),
None => "MISSING".into(),
}
));
s.push_str(&format!(
" HSA_OVERRIDE_GFX_VERSION : {}\n",
match (&gfx.applied_override, gfx.user_preset) {
(_, true) => "(user-set; not touched by nornir)".into(),
(Some(v), false) => format!("{v} (applied by nornir) — {}", gfx.reason),
(None, false) => format!("not applied — {}", gfx.reason),
}
));
s.push_str(&format!(
" MIGraphX model cache : {}\n",
match std::env::var(ORT_MIGRAPHX_MODEL_CACHE_PATH_ENV) {
Ok(v) if !v.is_empty() => format!("{v} (compiled-model `.mxr` cache; warm embeds ~10× faster)"),
_ => format!(
"UNSET → MIGraphX errors on first embed; nornir defaults it to {}",
default_migraphx_cache_dir().display()
),
}
));
s.push_str(&format!(
" ROCm libs loaded : {}\n",
if p.loaded.is_empty() { "(none)".into() } else { p.loaded.join(", ") }
));
s.push_str(&format!(
" dirs searched : {}\n",
p.dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")
));
if xdna_npu_present() {
s.push_str(
" XDNA NPU (/dev/accel/accel0): detected — SEPARATE Ryzen AI accelerator, \
unused (no VitisAI/XDNA EP yet)\n",
);
}
let missing: Vec<&str> = ORDERED_SONAMES
.iter()
.copied()
.filter(|n| !p.loaded.iter().any(|l| l == n))
.collect();
if !missing.is_empty() {
s.push_str(&format!(" runtime libs not found : {}\n", missing.join(", ")));
}
s.push_str(&format!(
"\n verdict: AMD GPU embedding {}\n",
if gpu_ready { "READY ✓" } else { "unavailable → CPU fallback" }
));
if gpu_ready {
s.push_str(" → all set; embed-ort runs on the AMD GPU via MIGraphX/ROCm.\n");
} else {
let facts = HintFacts {
gpu_ready,
runtime_present: driver && p.hip && p.miopen,
onnxruntime_present: ort.is_some(),
gfx: gfx.gfx.clone(),
};
s.push_str(&rocm_install_hint(detect_distro(), &facts));
}
(gpu_ready, s)
}
fn candidate_dirs() -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = Vec::new();
let push = |p: PathBuf, dirs: &mut Vec<PathBuf>| {
if p.is_dir() && !dirs.contains(&p) {
dirs.push(p);
}
};
if let Some(v) = std::env::var_os("NORNIR_ROCM_LIBS") {
for p in std::env::split_paths(&v) {
push(p, &mut dirs);
}
}
for key in ["ROCM_PATH", "HIP_PATH"] {
if let Some(root) = std::env::var_os(key) {
push(Path::new(&root).join("lib"), &mut dirs);
push(Path::new(&root).join("lib64"), &mut dirs);
}
}
for sys in [
"/opt/nornir/rocm",
"/opt/rocm/lib",
"/opt/rocm-7.0.0/lib",
"/opt/rocm-6.4.0/lib",
"/opt/rocm-6.2.0/lib",
"/opt/rocm-6.1.0/lib",
"/opt/rocm-6.0.0/lib",
"/opt/rocm/lib64",
"/usr/lib/x86_64-linux-gnu",
] {
push(PathBuf::from(sys), &mut dirs);
}
dirs
}
fn find_lib(dirs: &[PathBuf], soname: &str) -> Option<PathBuf> {
for d in dirs {
let exact = d.join(soname);
if exact.exists() {
return Some(exact);
}
if let Ok(entries) = std::fs::read_dir(d) {
for e in entries.flatten() {
if let Some(name) = e.file_name().to_str() {
if name.starts_with(soname) {
return Some(e.path());
}
}
}
}
}
None
}
fn dlopen_global(path: &Path) -> bool {
use libloading::os::unix::{Library, RTLD_GLOBAL, RTLD_NOW};
match unsafe { Library::open(Some(path), RTLD_NOW | RTLD_GLOBAL) } {
Ok(lib) => {
std::mem::forget(lib);
true
}
Err(_) => false,
}
}
fn loadable(path: &Path) -> bool {
use libloading::os::unix::{Library, RTLD_NOW};
let arg: &std::ffi::OsStr = path.as_os_str();
let Ok(lib) = (unsafe { Library::open(Some(arg), RTLD_NOW) }) else {
return false;
};
unsafe { lib.get::<unsafe extern "C" fn() -> *const std::ffi::c_void>(b"OrtGetApiBase") }.is_ok()
}
pub fn onnxruntime_dylib() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("ORT_DYLIB_PATH") {
let path = PathBuf::from(p);
if !path.as_os_str().is_empty() && loadable(&path) {
return Some(path);
}
}
if let Some(path) = find_lib(&candidate_dirs(), "libonnxruntime.so") {
if loadable(&path) {
return Some(path);
}
}
let bare = PathBuf::from("libonnxruntime.so");
if loadable(&bare) {
return Some(bare);
}
None
}
pub fn arm_onnxruntime() -> bool {
match onnxruntime_dylib() {
Some(path) => {
if path.is_absolute() {
unsafe { std::env::set_var("ORT_DYLIB_PATH", &path) };
}
true
}
None => false,
}
}
pub fn setup(target: &Path) -> anyhow::Result<(Vec<String>, Vec<String>)> {
use anyhow::Context;
let dirs = candidate_dirs();
std::fs::create_dir_all(target)
.with_context(|| format!("create {} (need root? try sudo)", target.display()))?;
let mut copied = Vec::new();
let mut missing = Vec::new();
for soname in ORDERED_SONAMES {
match find_lib(&dirs, soname) {
Some(src) => {
let dst = target.join(src.file_name().unwrap_or_default());
std::fs::copy(&src, &dst)
.with_context(|| format!("copy {} -> {}", src.display(), dst.display()))?;
let alias = target.join(soname);
if !alias.exists() {
std::fs::copy(&src, &alias).ok();
}
copied.push(soname.to_string());
}
None => missing.push(soname.to_string()),
}
}
match onnxruntime_dylib() {
Some(src) if src.is_absolute() => {
let dst = target.join("libonnxruntime.so");
std::fs::copy(&src, &dst)
.with_context(|| format!("copy {} -> {}", src.display(), dst.display()))?;
copied.push("libonnxruntime.so".to_string());
}
_ => missing.push("libonnxruntime.so (ROCm-enabled)".to_string()),
}
Ok((copied, missing))
}
#[derive(Debug, Default, Clone)]
pub struct HintFacts {
pub gpu_ready: bool,
pub runtime_present: bool,
pub onnxruntime_present: bool,
pub gfx: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Distro {
Debian,
Fedora,
Unknown,
}
pub fn detect_distro() -> Distro {
if let Ok(v) = std::env::var("NORNIR_DISTRO") {
return match v.to_ascii_lowercase().as_str() {
"debian" | "ubuntu" | "apt" => Distro::Debian,
"fedora" | "rhel" | "dnf" => Distro::Fedora,
_ => Distro::Unknown,
};
}
let text = std::fs::read_to_string("/etc/os-release").unwrap_or_default();
classify_os_release(&text)
}
pub fn classify_os_release(os_release: &str) -> Distro {
let mut id = String::new();
let mut like = String::new();
for line in os_release.lines() {
let line = line.trim();
if let Some(v) = line.strip_prefix("ID=") {
id = v.trim_matches('"').to_ascii_lowercase();
} else if let Some(v) = line.strip_prefix("ID_LIKE=") {
like = v.trim_matches('"').to_ascii_lowercase();
}
}
let hay = format!("{id} {like}");
if hay.contains("debian") || hay.contains("ubuntu") {
Distro::Debian
} else if hay.contains("rhel") || hay.contains("fedora") || hay.contains("centos") {
Distro::Fedora
} else {
Distro::Unknown
}
}
pub fn rocm_install_hint(distro: Distro, facts: &HintFacts) -> String {
if facts.gpu_ready {
return String::new();
}
let mut s = String::from("\n FIX — run these, then re-run `nornir vector doctor`:\n");
if !facts.runtime_present {
match distro {
Distro::Debian => {
s.push_str(
" # 1. ROCm 7.x userspace via AMD's apt repo (userspace-only; /dev/kfd comes\n\
\x20 # from the in-tree amdgpu driver, so NO DKMS needed). On Ubuntu 26.04\n\
\x20 # LTS point at the noble (24.04) repo — AMD has no resolute/26.04 build\n\
\x20 # yet and ROCm's userspace libs are forward-compatible:\n\
\x20 sudo mkdir -p /etc/apt/keyrings\n\
\x20 curl -s https://repo.radeon.com/rocm/rocm.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/rocm.gpg\n\
\x20 echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/latest noble main' | sudo tee /etc/apt/sources.list.d/rocm.list\n\
\x20 printf 'Package: *\\nPin: release o=repo.radeon.com\\nPin-Priority: 600\\n' | sudo tee /etc/apt/preferences.d/rocm-pin-600\n\
\x20 sudo apt-get update\n\
\x20 sudo apt-get install -y rocminfo rocm-hip-runtime rocm-smi-lib migraphx rocblas miopen-hip rocrand\n",
);
}
Distro::Fedora => {
s.push_str(
" # 1. ROCm 6.4+/7.0 userspace (gfx1151 needs ≥6.4; 7.0 lists it natively):\n\
\x20 sudo dnf install -y rocm-hip rocm-hip-runtime miopen-hip migraphx rocblas\n\
\x20 # (or AMD's amdgpu-install: sudo amdgpu-install --usecase=rocm --no-dkms)\n",
);
}
Distro::Unknown => {
s.push_str(
" # 1. Install ROCm 6.4+/7.0 userspace for your distro (gfx1151 needs ≥6.4):\n\
\x20 AMD's amdgpu-install --usecase=rocm --no-dkms, or your distro's\n\
\x20 rocm-hip-runtime + miopen-hip + migraphx + rocblas packages.\n\
\x20 /dev/kfd is already present, so only the USERSPACE is needed.\n",
);
}
}
}
if !facts.onnxruntime_present {
s.push_str(
" # 2. A ROCm-enabled onnxruntime (the ort dynamic-load dylib):\n\
\x20 pip install onnxruntime-rocm # provides libonnxruntime.so with ROCm+MIGraphX EPs\n\
\x20 # nornir finds it via ORT_DYLIB_PATH=/path/to/libonnxruntime.so, or drop it in\n\
\x20 # /opt/nornir/rocm (a built-in search dir) — see step 3.\n",
);
}
if let Some(gfx) = facts.gfx.as_deref() {
if gfx_natively_supported(gfx) {
s.push_str(&format!(
" # {gfx}: supported natively by recent ROCm (e.g. ROCm 7.2.4 sees gfx1150 — \
Strix Point —\n\
\x20 and gfx1151 — Strix Halo — directly), so NO HSA override is needed.\n",
));
} else if let Some(v) = hsa_override_for(gfx) {
s.push_str(&format!(
" # {gfx}: older ROCm may need HSA_OVERRIDE_GFX_VERSION={v} — nornir now sets this\n\
\x20 AUTOMATICALLY for non-native parts (override via NORNIR_HSA_OVERRIDE, or\n\
\x20 NORNIR_HSA_OVERRIDE=off to disable). Upgrading ROCm usually removes the need.\n",
));
}
}
s.push_str(
" # 3. Pin the discovered libs so the GPU works with no env, every run:\n\
\x20 sudo nornir vector setup-rocm # copies the ROCm set (+ onnxruntime) into /opt/nornir/rocm\n",
);
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_degrades_to_cpu_without_rocm() {
let p = ensure();
assert!(!p.dirs.is_empty(), "candidate dirs must be populated");
let (ready, report) = preflight();
assert_eq!(ready, available(), "preflight verdict must match available()");
assert!(report.contains("ROCm preflight"), "report header missing: {report}");
if available() {
assert!(driver_present() && p.hip && p.miopen, "available() implies hip+miopen+driver");
assert!(report.contains("READY"), "ready box should say READY: {report}");
} else {
assert!(!ready, "no ROCm => not ready");
assert!(report.contains("CPU fallback"), "verdict should name CPU fallback: {report}");
assert!(report.contains("FIX — run these"), "no-ROCm report must carry fix commands: {report}");
assert!(report.contains("nornir vector setup-rocm"), "fix must mention setup-rocm: {report}");
}
}
#[test]
fn parse_gfx_target_decodes_strix_halo() {
assert_eq!(parse_gfx_target("gfx_target_version 110501").as_deref(), Some("gfx1151"));
assert_eq!(parse_gfx_target("gfx_target_version 110000").as_deref(), Some("gfx1100"));
assert_eq!(parse_gfx_target("gfx_target_version 90402").as_deref(), Some("gfx942"));
assert_eq!(parse_gfx_target("gfx_target_version 0"), None);
let props = "cpu_cores_count 16\ngfx_target_version 110501\nsimd_count 8\n";
assert_eq!(parse_gfx_target(props).as_deref(), Some("gfx1151"));
assert_eq!(parse_gfx_target("no version here"), None);
}
#[test]
fn hsa_override_decision_is_pure_and_safe() {
let (val, why) = decide_hsa_override(Some("gfx1150"), false, None);
assert_eq!(val, None, "gfx1150 is native on ROCm 7.2.4 → no override");
assert!(why.contains("nativ"), "reason should say native: {why}");
assert_eq!(decide_hsa_override(Some("gfx1151"), false, None).0, None);
let (val, _) = decide_hsa_override(Some("gfx1103"), false, None);
assert_eq!(val.as_deref(), Some("11.0.3"), "non-native table part gets its override");
assert_eq!(hsa_override_for("gfx1150"), Some("11.5.0"));
assert_eq!(hsa_override_for("gfx1151"), Some("11.5.1"));
let (val, why) = decide_hsa_override(Some("gfx1103"), true, None);
assert_eq!(val, None, "must not clobber user value");
assert!(why.contains("user"), "reason should say user-set: {why}");
for off in ["off", "none", "", " "] {
let (val, _) = decide_hsa_override(Some("gfx1103"), false, Some(off));
assert_eq!(val, None, "policy {off:?} should disable override");
}
let (val, _) = decide_hsa_override(Some("gfx1150"), false, Some("11.0.0"));
assert_eq!(val.as_deref(), Some("11.0.0"));
assert_eq!(decide_hsa_override(Some("gfx9999"), false, None).0, None);
assert_eq!(decide_hsa_override(None, false, None).0, None);
}
#[test]
fn classify_os_release_picks_family() {
assert_eq!(classify_os_release("ID=ubuntu\nID_LIKE=debian\n"), Distro::Debian);
assert_eq!(classify_os_release("ID=debian\n"), Distro::Debian);
assert_eq!(classify_os_release("ID=fedora\n"), Distro::Fedora);
assert_eq!(classify_os_release("ID=\"rhel\"\nID_LIKE=\"fedora\"\n"), Distro::Fedora);
assert_eq!(classify_os_release("ID=arch\n"), Distro::Unknown);
}
#[test]
fn default_migraphx_cache_dir_is_under_home_cache() {
let dir = default_migraphx_cache_dir();
assert!(dir.ends_with("nornir/migraphx") || dir == Path::new(".nornir-migraphx"));
assert!(!dir.as_os_str().is_empty(), "cache dir is never the empty path the EP chokes on");
}
#[test]
fn rocm_install_hint_is_distro_aware_and_gated() {
let ready = HintFacts { gpu_ready: true, ..Default::default() };
assert!(rocm_install_hint(Distro::Debian, &ready).is_empty(), "ready box gets no hint");
let strix = HintFacts {
gpu_ready: false,
runtime_present: false,
onnxruntime_present: false,
gfx: Some("gfx1151".into()),
};
let deb = rocm_install_hint(Distro::Debian, &strix);
assert!(deb.contains("apt-get install"), "Debian hint must use apt-get: {deb}");
assert!(deb.contains("repo.radeon.com/rocm/rocm.gpg.key"), "must add AMD's repo key: {deb}");
assert!(
deb.contains("https://repo.radeon.com/rocm/apt/latest noble main"),
"must point at the noble repo (26.04 has no resolute build yet): {deb}"
);
assert!(deb.contains("/etc/apt/preferences.d/rocm-pin-600"), "must pin the repo: {deb}");
assert!(
deb.contains("rocm-hip-runtime") && deb.contains("migraphx") && deb.contains("miopen-hip"),
"must install the runtime + migraphx + miopen: {deb}"
);
assert!(deb.contains("noble"), "must note the noble repo fallback: {deb}");
assert!(deb.contains("onnxruntime-rocm"), "must give the onnxruntime step: {deb}");
assert!(deb.contains("natively"), "native part must note no override needed: {deb}");
assert!(!deb.contains("HSA_OVERRIDE_GFX_VERSION=11.5.1"), "native gfx1151 must not force an override: {deb}");
assert!(deb.contains("nornir vector setup-rocm"), "must give the setup-rocm one-liner: {deb}");
assert!(deb.contains("re-run `nornir vector doctor`"), "verdict must be actionable: {deb}");
let old = HintFacts {
gpu_ready: false,
runtime_present: false,
onnxruntime_present: false,
gfx: Some("gfx1103".into()),
};
let old_deb = rocm_install_hint(Distro::Debian, &old);
assert!(old_deb.contains("HSA_OVERRIDE_GFX_VERSION=11.0.3"), "non-native part gets its override note: {old_deb}");
let fed = rocm_install_hint(Distro::Fedora, &strix);
assert!(fed.contains("dnf"), "Fedora hint must use dnf: {fed}");
assert!(!fed.contains("apt update"), "Fedora hint must not use apt: {fed}");
let just_ort = HintFacts {
gpu_ready: false,
runtime_present: true,
onnxruntime_present: false,
gfx: Some("gfx1100".into()),
};
let h = rocm_install_hint(Distro::Debian, &just_ort);
assert!(h.contains("onnxruntime-rocm"), "missing-ort hint must give the ort step: {h}");
assert!(!h.contains("amdgpu-install"), "runtime present ⇒ no driver install block: {h}");
assert!(!h.contains("HSA_OVERRIDE_GFX_VERSION"), "no gfx1151 ⇒ no HSA note: {h}");
}
}