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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ form works depends on the engine your GPU selects.
| `rocm` | Open the launcher menu (setup, serve, diagnose, chat, dashboard) |
| `rocm examine` | Check GPU, ROCm install, engines, and managed folders |
| `rocm diagnose` | Match this machine against known ROCm/PyTorch/llama.cpp failure modes |
| `rocm diagnose --report` | Show what this machine would contribute to a problem report, and send nothing |
| `rocm fix [<fix-id>]` | Apply a fix reported by `rocm diagnose` |
| `rocm install sdk` | Install TheRock ROCm wheels into a managed Python environment |
| `rocm install driver` | Install the AMD kernel driver on Linux |
Expand Down Expand Up @@ -260,7 +261,7 @@ the JSON report, not the human-readable one.
### Diagnose and fix

```
rocm diagnose [--symptom TEXT] [--top N] [--json] [--distro [NAME]]
rocm diagnose [--symptom TEXT] [--top N] [--json] [--distro [NAME]] [--report]
rocm fix [<fix-id>] [--yes] [--dry-run] [--device-index N]
```

Expand All @@ -281,6 +282,25 @@ fix` takes the id, not the position.
skips checks that need to read the distribution's own environment
(`HSA_OVERRIDE_GFX_VERSION`, `PATH`, the framework/ROCm pairing) — run
`rocm diagnose` inside the distribution for those.
- `--report` shows exactly what this machine would contribute to a problem
report, and sends nothing — there is no transport yet, and there will be no
automatic one: a report leaves a machine only by its owner's own action. The
content is deliberately narrow (a schema version, the matched entry, whether
a fix was offered for it, the GPU architecture and which compatibility
matrix snapshot it was checked against, the OS family, distribution and
major version, the ROCm release, the inference engine and its release, the
CLI version), and it carries no host name, user name, file path, or error
text. The ROCm release and the inference engine's release are each cut
back to a release, so a build number that would narrow toward one machine
never appears there; the CLI's own version is the exception, since it names
the tool that wrote the report rather than something read off the machine.
The distribution is checked against a list of known names rather than
repeated from the machine. Hardware that is not on AMD's published
compatibility matrix produces no report at all, and the CLI says why. So
does a WSL machine, for a different reason: this CLI does not inspect the
GPU on WSL yet, so it cannot confirm the hardware is on the compatibility
matrix and says that rather than claiming the architecture could not be
read.

`fix` applies a known fix by the `id:` that `diagnose` reported — not the
ranking position noted above, which isn't a stable name. Run it with no id
Expand Down
166 changes: 164 additions & 2 deletions apps/rocm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ enum Command {
/// name only when more than one is installed.
#[arg(long, value_name = "NAME", num_args = 0..=1, default_missing_value = "")]
distro: Option<String>,
/// Show the report this machine would contribute, and send nothing.
///
/// Nothing leaves the machine: this prints the exact content so it can
/// be read before any of it is shared. Hardware that is not on AMD's
/// published compatibility matrix produces no report at all.
///
/// Not combinable with `--distro`: a report describes this machine, and
/// a WSL distribution reached remotely is not fully examined (see
/// `--distro`'s own help), so it cannot back the disclosure guard's
/// architecture check.
#[arg(long, conflicts_with = "distro")]
report: bool,
},
/// Apply a known fix by id (see `rocm diagnose`); run with no id to list fixes.
///
Expand Down Expand Up @@ -2066,7 +2078,8 @@ fn dispatch(cli: Cli) -> Result<()> {
top,
json,
distro,
}) => diagnose(symptom, top, json, distro),
report,
}) => diagnose(symptom, top, json, distro, report),
// Keep this error chained rather than discarding it into a fresh
// `anyhow!(...)` (e.g. via a `.map_err` that restringifies it) -- see
// `FixExitCode`'s doc comment for why that would silently break its
Expand Down Expand Up @@ -2737,7 +2750,13 @@ fn examine(json: bool, framework: rocm_core::FrameworkProbe) -> Result<()> {
Ok(())
}

fn diagnose(symptom: Option<String>, top: usize, json: bool, distro: Option<String>) -> Result<()> {
fn diagnose(
symptom: Option<String>,
top: usize,
json: bool,
distro: Option<String>,
report_requested: bool,
) -> Result<()> {
// `rocm diagnose` is a query: it exits 0 whether it matched, found nothing,
// or is out of scope. Callers read `has_match` / `out_of_scope` /
// `route_when_no_match` from `--json` rather than branching on the exit code.
Expand Down Expand Up @@ -2770,6 +2789,9 @@ fn diagnose(symptom: Option<String>, top: usize, json: bool, distro: Option<Stri
.as_ref()
.is_some_and(|wsl| !wsl.locally_probed);
let report = rocm_core::run_diagnose(&examination, &symptom.unwrap_or_default());
if report_requested {
return show_prepared_report(&examination, &report, json);
}
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
Expand All @@ -2790,6 +2812,87 @@ fn diagnose(symptom: Option<String>, top: usize, json: bool, distro: Option<Stri
Ok(())
}

/// The catalog entry a report should name, and whether a fix was offered for it.
///
/// Reads `has_match` rather than taking the head of `matched`. Several checkers
/// open with a nonzero score for a situation that is merely *potentially*
/// relevant, so `matched` is rarely empty even on a healthy machine — taking its
/// head regardless would publish a sub-threshold signal as though it were an
/// established cause, and the counts built on those reports would be wrong in a
/// way nothing downstream could detect.
fn established_entry(report: &rocm_core::DiagnoseReport) -> (Option<&str>, bool) {
if !report.has_match {
return (None, false);
}
report.matched.first().map_or((None, false), |top| {
(Some(top.id.as_str()), top.fix.is_some())
})
}

/// Print the report this machine would contribute, and send nothing.
fn show_prepared_report(
examination: &rocm_core::Examination,
report: &rocm_core::DiagnoseReport,
json: bool,
) -> Result<()> {
let (entry, fix_offered) = established_entry(report);
// Exit 0 either way. A refusal is this command working, not failing: it
// decided correctly and said why, and a nonzero code would send a caller
// looking for a fault. Anything scripting this reads the outcome from
// `--json` rather than from the exit code, exactly as `rocm diagnose` itself
// already asks callers to do.
match rocm_core::prepare_report(examination, entry, fix_offered) {
Ok(prepared) => {
if json {
println!("{}", serde_json::to_string_pretty(&prepared)?);
} else {
println!("This is the whole of what a report would carry:");
println!();
println!("{}", serde_json::to_string_pretty(&prepared)?);
println!();
println!("Nothing has been sent. Sending is not implemented yet.");
}
Ok(())
}
Err(refusal) => {
let explanation = match refusal {
rocm_core::ReportRefusal::UnreleasedHardware => {
// "Doctor" is what the epic calls this capability; the CLI
// has no such command, so a user reading this has nothing
// to run and nothing to look up.
"This machine holds hardware that is not on AMD's published ROCm \
compatibility matrix, so no report was prepared. A report describes only \
hardware the compatibility matrix lists as supported."
}
rocm_core::ReportRefusal::ArchitectureUnreadable => {
"No AMD GPU architecture could be read here, so nothing confirms this \
hardware is on the ROCm compatibility matrix. No report was prepared."
}
rocm_core::ReportRefusal::PlatformNotProbed => {
// Says what happened rather than dressing it as a finding
// about the machine. The earlier wording told a healthy WSL
// user their GPU could not be read, when nothing had looked.
"This CLI does not inspect the GPU on WSL yet, so it cannot confirm whether \
this hardware is on the ROCm compatibility matrix. No report was prepared. \
This is a gap in the tool, not a problem with the machine."
}
};
if json {
println!(
"{}",
Comment thread
volen-silo marked this conversation as resolved.
serde_json::to_string_pretty(&rocm_core::refusal_envelope(
refusal,
explanation
))?
);
} else {
println!("{explanation}");
}
Ok(())
}
}
}

fn fix(fix_id: Option<String>, yes: bool, dry_run: bool, device_index: Option<i64>) -> Result<()> {
let Some(fix_id) = fix_id else {
print!("{}", rocm_core::list_fix_recipes());
Expand Down Expand Up @@ -22063,6 +22166,65 @@ fn treat_as_natural_language(args: &[String]) -> bool {

#[cfg(test)]
mod tests {

/// A diagnosis report holding exactly one finding.
///
/// `has_match` is passed independently of the score on purpose: the point
/// under test is that the two are read together, so a fixture that derived
/// one from the other could not express the case being guarded against.
fn report_of(
has_match: bool,
id: &str,
score: i32,
fix: Option<rocm_core::Fix>,
) -> rocm_core::DiagnoseReport {
rocm_core::DiagnoseReport {
has_match,
matched: vec![rocm_core::Diagnosis {
id: id.to_owned(),
title: "under test".to_owned(),
score,
evidence: Vec::new(),
fix,
}],
min_score_for_match: 50,
high_confidence_threshold: 80,
route_when_no_match: rocm_core::diagnose::Route {
target: String::new(),
url: String::new(),
},
out_of_scope: None,
}
}

/// The entry a report names is one the diagnosis established, not merely
/// the strongest signal it saw.
///
/// This is a wiring test, not a logic one. `established_entry` is correct in
/// itself; what it could get wrong is being handed `matched.first()`
/// unconditionally. Several checkers open with a nonzero score for a
/// situation that is only potentially relevant, so a healthy machine
/// produces a `matched` list full of sub-threshold entries — and a report
/// naming one of those would look like an established cause to every
/// counter downstream, with nothing able to tell the difference afterwards.
#[test]
fn a_report_names_an_established_cause_and_not_the_loudest_weak_signal() {
let weak_only = report_of(false, "fix-10-container", 25, None);
assert_eq!(
established_entry(&weak_only),
(None, false),
"nothing cleared the bar, so the report has no entry to name"
);

// Non-vacuity: an established cause must come through, or the assertion
// above is satisfied by never naming anything.
let established = report_of(true, "fix-6-path", 90, Some(rocm_core::Fix::default()));
assert_eq!(
established_entry(&established),
(Some("fix-6-path"), true),
"an established cause with a fix is exactly what a report is for"
);
}
use std::process::ExitCode;

/// `Ok(())` must map to a clean exit so `rocm`'s successful commands don't
Expand Down
11 changes: 11 additions & 0 deletions crates/rocm-core/src/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,17 @@ pub(crate) fn torch_rocm_indexes_named_in<'a>(
.collect()
}

/// Whether `fix_id` names an entry in the catalog.
///
/// A predicate rather than the lookup itself, because the caller that needs it
/// is deciding whether a caller-supplied string may be published, not reading a
/// recipe. Handing back the recipe would also make a private type reachable
/// from outside this module.
#[must_use]
pub(crate) fn is_catalog_id(fix_id: &str) -> bool {
find_recipe(fix_id).is_some()
}

/// The platform family a recipe's `applies_on` is matched against.
///
/// WSL2 is its own family rather than `linux`, mirroring `diagnose`. That is what
Expand Down
6 changes: 6 additions & 0 deletions crates/rocm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod examine;
pub mod fix;
pub mod openmpi;
pub mod proc_lifecycle;
pub mod report;
pub mod runtime;
#[cfg(test)]
mod test_env;
Expand All @@ -55,6 +56,11 @@ pub use proc_lifecycle::{
IdentityState, KillScope, ProcessIdentity, TerminationOutcome, identity_state,
process_start_ticks, terminate_verified,
};
pub use report::{
APPROVED_ARCHITECTURES, APPROVED_ARCHITECTURES_SOURCE, REPORT_SCHEMA_VERSION, ReadOutcome,
Refusal as ReportRefusal, Report, is_rocm_supported, prepare_report, read_report,
refusal_envelope,
};
use runtime::env_path_override;
pub use runtime::{
RUNTIME_LIBRARY_PATH_ENV, RuntimeHost, RuntimePlatform, current_executable_path,
Expand Down
Loading
Loading