Skip to content
Draft
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ This produces the two binaries under `target/release/`:
Run without installing:

```bash
cargo run --release --bin rocm -- examine
cargo run --release --bin rocm -- doctor
```

Or copy the release binaries onto your `PATH`:
Expand Down Expand Up @@ -184,7 +184,7 @@ rocm install sdk
```

This downloads TheRock ROCm wheels and a matching PyTorch stack into a managed
environment. On machines with an existing ROCm install, `rocm examine` will
environment. On machines with an existing ROCm install, `rocm doctor` will
show it as `legacy_rocm_status: detected_unmanaged` — running `rocm install sdk`
creates a separate managed runtime alongside it.

Expand All @@ -207,7 +207,7 @@ requirements.
| Command | Description |
|---|---|
| `rocm` | Open the launcher menu (setup, serve, diagnose, chat, dashboard) |
| `rocm examine` | Check GPU, ROCm install, engines, and managed folders |
| `rocm doctor` | Check GPU, ROCm install, engines, and managed folders, and what looks wrong |
| `rocm install sdk` | Install TheRock ROCm wheels into a managed Python environment |
| `rocm install driver` | Install the AMD kernel driver on Linux |
| `rocm serve <model>` | Start a local OpenAI-compatible model server |
Expand Down
358 changes: 293 additions & 65 deletions apps/rocm/src/main.rs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions apps/rocm/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ fn openai_chat_request_body_with_stream(
fn rocm_openai_tool_definitions() -> Vec<serde_json::Value> {
vec![
rocm_openai_tool(
"examine",
"doctor",
"Read the current ROCm host, GPU, runtime, driver, and engine status.",
serde_json::json!({
"type": "object",
Expand Down Expand Up @@ -2354,7 +2354,7 @@ mod tests {

assert!(request.contains("\"tools\""));
assert!(request.contains("\"tool_choice\":\"auto\""));
assert!(request.contains("\"name\":\"examine\""));
assert!(request.contains("\"name\":\"doctor\""));
assert_eq!(response.content, "I will check first.");
assert_eq!(response.tool_calls.len(), 1);
assert_eq!(response.tool_calls[0].name, "examine");
Expand Down
3 changes: 3 additions & 0 deletions crates/e2e-report/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,9 @@ struct CommandKey {
/// coverage % reflects the real surface (a deliberate, reviewable denominator
/// beats silently drifting).
const KNOWN_COMMAND_SURFACE: &[&str] = &[
"rocm doctor",
// Superseded by `rocm doctor` and hidden from the advertised surface, but
// still dispatched and still covered, so they stay in the denominator.
"rocm examine",
"rocm diagnose",
"rocm fix",
Expand Down
40 changes: 18 additions & 22 deletions crates/rocm-core/src/diagnose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1299,9 +1299,9 @@ fn route_when_no_match(e: &Examination) -> Route {
#[must_use]
pub fn diagnose(e: &Examination, symptom: &str) -> DiagnoseReport {
// WSL2 is a distinct platform (it uses /dev/dxg + the Windows host driver,
// not the in-tree amdgpu module or /dev/kfd), and `examine` already treats
// it as out of scope (exit_code() == 2). Mirror that here: skip the
// bare-metal Linux catalog entirely so we don't emit false positives like
// not the in-tree amdgpu module or /dev/kfd), and the host report already
// routes it out via the "wsl" status. Mirror that here: skip the bare-metal
// Linux catalog entirely so we don't emit false positives like
// fix-4-render-group / fix-5-amdgpu-load on a healthy WSL2 box.
let out_of_scope = wsl_out_of_scope_message(e);
let matched = if out_of_scope.is_some() {
Expand All @@ -1318,38 +1318,34 @@ pub fn diagnose(e: &Examination, symptom: &str) -> DiagnoseReport {
}
}

/// ROCm-on-WSL2 setup guidance (distinct from the bare-metal catalog).
const WSL_DOCS_URL: &str = "https://rocm.docs.amd.com/projects/radeon-ryzen/en/latest/docs/install/installryz/wsl/howto_wsl.html";

fn wsl_out_of_scope_message(e: &Examination) -> Option<String> {
e.is_wsl.then(|| {
format!(
"ROCm on WSL2 is a distinct platform: it uses /dev/dxg and the Windows host \
driver (dxgkrnl), not the in-tree amdgpu kernel module or /dev/kfd. This catalog \
targets bare-metal Linux, so its checks (render group, /dev/kfd, modprobe amdgpu) \
do not apply here. For ROCm-on-WSL2 setup, see {WSL_DOCS_URL}"
)
})
// Shared with the host report rather than restated: this verdict was
// previously explained twice, in two wordings, and the two had drifted.
e.is_wsl
.then(|| crate::examine::WSL_ROUTE_OUT_NOTE.to_owned())
}

/// Render the human-facing diagnosis view (mirrors `diagnose.py`'s text output).
#[must_use]
pub fn render_report_text(report: &DiagnoseReport, top: usize) -> String {
pub fn render_report_text(report: &DiagnoseReport, top: usize, command: &str) -> String {
use std::fmt::Write as _;
let mut out = String::new();
if let Some(reason) = &report.out_of_scope {
out.push_str("rocm diagnose: out of scope for this platform.\n\n");
let _ = writeln!(out, "{command}: out of scope for this platform.\n");
out.push_str(reason);
out.push('\n');
return out;
}
if report.matched.is_empty() {
let route = &report.route_when_no_match;
out.push_str("rocm diagnose: no known misconfiguration matched.\n\n");
let _ = writeln!(out, "{command}: no known misconfiguration matched.\n");
out.push_str("This is the explicit 'I don't recognise this failure mode' case. Do not speculate; file the symptom + this examination output upstream:\n");
let _ = writeln!(out, " {:>12}: {}", route.target, route.url);
out.push('\n');
out.push_str("Include the JSON from `rocm examine --json` in your report.\n");
let _ = writeln!(
out,
"Include the JSON from `{command} --json` in your report."
);
return out;
}
for (i, d) in report.matched.iter().take(top).enumerate() {
Expand Down Expand Up @@ -1482,7 +1478,7 @@ mod tests {
"fixture must stay below the threshold for this test to mean anything"
);

let out = render_report_text(&report, 5);
let out = render_report_text(&report, 5, "rocm doctor");
assert!(
out.contains("below the HIGH_CONFIDENCE threshold"),
"the confidence caution must survive:\n{out}"
Expand All @@ -1499,7 +1495,7 @@ mod tests {
e.in_render_group = Some(false);
e.in_video_group = Some(false);
let report = diagnose(&e, "");
let out = render_report_text(&report, 5);
let out = render_report_text(&report, 5, "rocm doctor");

let applies = out.matches("apply with: rocm fix ").count();
let shown = report.matched.len().min(5);
Expand All @@ -1523,7 +1519,7 @@ mod tests {
let mut e = linux_base();
e.in_render_group = Some(false);
let report = diagnose(&e, "");
let out = render_report_text(&report, 5);
let out = render_report_text(&report, 5, "rocm doctor");
let top = &report.matched[0];
assert!(
out.contains(&format!("#1 ({})", top.id)),
Expand All @@ -1540,7 +1536,7 @@ mod tests {
if report.matched.len() < 2 {
return; // nothing to truncate on this fixture
}
let out = render_report_text(&report, 1);
let out = render_report_text(&report, 1, "rocm doctor");
assert!(
out.contains("Showing 1 of"),
"a truncated list must not read as the complete set:\n{out}"
Expand Down
10 changes: 8 additions & 2 deletions crates/rocm-core/src/examine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,13 @@ impl Default for Examination {

/// Route-out guidance shown when WSL2 is detected (out of scope for this
/// catalog, which targets bare-metal Linux). Mirrors `examine.py`.
pub const WSL_ROUTE_OUT_NOTE: &str = "Detected WSL2. rocm examine does not cover the ROCm-on-WSL flow (it requires Adrenalin Pro + the WSL kernel update on the Windows host). Either run `rocm examine` on the native Linux host, or follow AMD's WSL guide directly: https://rocm.docs.amd.com/projects/radeon-ryzen/en/latest/docs/install/installryz/wsl/howto_wsl.html";
/// Why WSL2 is routed out, and where to go instead.
///
/// One text, shared by the host report and the diagnosis catalog. These used to
/// be two independently-written explanations of the same verdict — one blamed
/// the Adrenalin driver, the other explained `/dev/dxg` — with the same doc URL
/// duplicated in both files. They had already drifted apart.
pub const WSL_ROUTE_OUT_NOTE: &str = "ROCm on WSL2 is a distinct platform: it uses /dev/dxg and the Windows host driver (dxgkrnl), not the in-tree amdgpu kernel module or /dev/kfd. The bare-metal Linux checks (render group, /dev/kfd, modprobe amdgpu) cannot apply here, so they are not run. Either run `rocm doctor` on a native Linux host, or follow AMD's ROCm-on-WSL2 guide: https://rocm.docs.amd.com/projects/radeon-ryzen/en/latest/docs/install/installryz/wsl/howto_wsl.html";

/// Which framework probe to run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -294,7 +300,7 @@ impl Examination {
probe_framework(&mut e, framework);
} else {
e.notes.push(format!(
"rocm examine supports Linux and Windows; got {}. This skill cannot help on this platform.",
"rocm doctor supports Linux and Windows; got {}. This platform is not covered.",
e.os_family
));
}
Expand Down
22 changes: 8 additions & 14 deletions crates/rocm-core/src/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ const fn current_os() -> &'static str {
}
}

/// Whether `value` is a `rocm diagnose` ranking position (`#2`, or a bare `2`)
/// Whether `value` is a `rocm doctor` ranking position (`#2`, or a bare `2`)
/// rather than a fix-id.
///
/// Used only to turn an unknown-id refusal into a corrective one; it never
Expand Down Expand Up @@ -457,18 +457,16 @@ pub fn apply(fix_id: &str, opts: &FixOptions) -> i32 {
let Some(recipe) = find_recipe(fix_id) else {
eprintln!("Unknown fix-id: {fix_id}");
if looks_like_a_diagnosis_position(fix_id) {
// `rocm diagnose` ranks findings `#1`, `#2`, and users reach for that
// `rocm doctor` ranks findings `#1`, `#2`, and users reach for that
// number here. It is a position in one report, not a name -- and it
// does not line up with the catalog's `fix-1 … fix-15` either, so a
// bare "unknown id" left them with nothing to correct.
eprintln!("`{fix_id}` looks like a position in a `rocm doctor` report, not a fix-id.");
eprintln!(
"`{fix_id}` looks like a position in a `rocm diagnose` report, not a fix-id."
);
eprintln!(
"Use the `id:` shown against that cause — `rocm diagnose` prints an `apply with:` line you can copy."
"Use the `id:` shown against that cause — `rocm doctor` prints an `apply with:` line you can copy."
);
} else {
eprintln!("Run `rocm diagnose` to see which fix-id applies.");
eprintln!("Run `rocm doctor` to see which fix-id applies.");
}
return 2;
};
Expand Down Expand Up @@ -781,11 +779,7 @@ fn run_path_export_linux(opts: &FixOptions) -> i32 {
if !confirm(&format!("Append to {}?", rc_file.display()), opts.yes) {
return 5;
}
if let Err(exc) = append_line(
&rc_file,
"# Added by rocm examine (fix-6-path)",
&export_line,
) {
if let Err(exc) = append_line(&rc_file, "# Added by rocm fix (fix-6-path)", &export_line) {
println!("Failed to write {}: {exc}", rc_file.display());
return 4;
}
Expand Down Expand Up @@ -892,7 +886,7 @@ fn run_hip_visible_devices_linux(opts: &FixOptions) -> i32 {
}
if let Err(exc) = append_line(
&rc_file,
"# Added by rocm examine (fix-9-igpu-dgpu)",
"# Added by rocm fix (fix-9-igpu-dgpu)",
&export_line,
) {
println!("Failed to write {}: {exc}", rc_file.display());
Expand Down Expand Up @@ -1165,7 +1159,7 @@ mod tests {

#[test]
fn diagnosis_positions_are_recognised_as_positions() {
// What `rocm diagnose` shows as `#1`/`#2`, plus the bare number a user
// What `rocm doctor` shows as `#1`/`#2`, plus the bare number a user
// might type instead.
for value in ["#1", "#2", "1", "12", " #3 "] {
assert!(
Expand Down
19 changes: 13 additions & 6 deletions crates/rocm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2012,7 +2012,13 @@ impl ExamineSummary {
})
}

pub fn render_text(&self) -> String {
/// Render the host summary, labelled with the command that produced it.
///
/// `examine` and `diagnose` are superseded but still dispatched, and their
/// stdout is a contract that shipped scripts assert on by exact substring.
/// So each command names itself rather than sharing one hardcoded label.
#[must_use]
pub fn render_text(&self, command: &str) -> String {
let legacy_paths = if self.legacy_rocm.paths.is_empty() {
"<none>".to_owned()
} else {
Expand All @@ -2034,7 +2040,8 @@ impl ExamineSummary {
"false (this run's output is captured, so the CLI will not prompt)"
};
format!(
"rocm examine\n os: {}\n arch: {}\n kernel: {}\n distro: {}\n cpu: {}\n system_ram: {}\n interactive_terminal: {}\n default_engine: {}\n detected_gfx_target: {}\n compatible_therock_family: {}\n detected_therock_family: {}\n driver_policy: {}\n driver_status: {}\n driver_detail: {}\n legacy_rocm_status: {}\n legacy_rocm_paths: {}\n legacy_rocm_version: {}\n legacy_rocm_detail: {}\n legacy_rocm_guidance: {}\n wsl: {}\n wsl_dxg_device: {}\n wsl_dxcore: {}\n wsl_librocdxg: {}\n wsl_rocdxg_dids: {}\n wsl_ldconfig_librocdxg: {}\n wsl_global_rocminfo: {}\n wsl_cargo: {}\n wsl_detail: {}\n managed_runtimes: {}\n managed_services: {}\n model_cache_entries: {}\n config_dir: {}\n data_dir: {}\n cache_dir: {}\n",
"{}\n os: {}\n arch: {}\n kernel: {}\n distro: {}\n cpu: {}\n system_ram: {}\n interactive_terminal: {}\n default_engine: {}\n detected_gfx_target: {}\n compatible_therock_family: {}\n detected_therock_family: {}\n driver_policy: {}\n driver_status: {}\n driver_detail: {}\n legacy_rocm_status: {}\n legacy_rocm_paths: {}\n legacy_rocm_version: {}\n legacy_rocm_detail: {}\n legacy_rocm_guidance: {}\n wsl: {}\n wsl_dxg_device: {}\n wsl_dxcore: {}\n wsl_librocdxg: {}\n wsl_rocdxg_dids: {}\n wsl_ldconfig_librocdxg: {}\n wsl_global_rocminfo: {}\n wsl_cargo: {}\n wsl_detail: {}\n managed_runtimes: {}\n managed_services: {}\n model_cache_entries: {}\n config_dir: {}\n data_dir: {}\n cache_dir: {}\n",
command,
self.os,
self.arch,
self.kernel.as_deref().unwrap_or("<unknown>"),
Expand Down Expand Up @@ -9450,7 +9457,7 @@ Class Name: Display
cache_dir: PathBuf::from("cache"),
};

let rendered = summary.render_text();
let rendered = summary.render_text("rocm examine");
assert!(rendered.contains("distro: Windows"));
assert!(rendered.contains("cpu: AMD Ryzen"));
assert!(rendered.contains("system_ram: 64 GiB"));
Expand Down Expand Up @@ -9507,14 +9514,14 @@ Class Name: Display
cache_dir: PathBuf::from("cache"),
};

let interactive = summary.render_text();
let interactive = summary.render_text("rocm examine");
assert!(
interactive.contains("interactive_terminal: true (this run has a terminal"),
"the true case must say it is about this run:\n{interactive}"
);

summary.interactive_terminal = false;
let captured = summary.render_text();
let captured = summary.render_text("rocm examine");
assert!(
captured.contains("interactive_terminal: false (this run's output is captured"),
"the false case must explain why, not just report it:\n{captured}"
Expand Down Expand Up @@ -9560,7 +9567,7 @@ Class Name: Display
cache_dir: PathBuf::from("cache"),
};

let rendered = summary.render_text();
let rendered = summary.render_text("rocm examine");

assert!(rendered.contains(
"legacy_rocm_guidance: legacy ROCm detected; install a managed TheRock runtime"
Expand Down
16 changes: 8 additions & 8 deletions crates/rocm-dash-tui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub enum Focus {
Setup,
/// The serve-a-model wizard — the launcher's `Serve a model` row.
Serve,
/// Read-only `rocm examine` environment check — the launcher's
/// Read-only `rocm doctor` environment check — the launcher's
/// `Diagnose & fix` row. Auto-runs on open.
Examine,
}
Expand Down Expand Up @@ -1561,7 +1561,7 @@ fn focused_close_key_blocked(state: &AppState, focus: Option<Focus>, code: KeyCo
}

/// Open the single overlay a focused host should host, returning any initial
/// job-bridge side effects to pump (Examine auto-runs `rocm examine` on open;
/// job-bridge side effects to pump (Doctor auto-runs `rocm doctor` on open;
/// Setup/Serve open their form and wait for input). Clears any other overlay
/// first (mutually-exclusive invariant). Pure w.r.t. process I/O — the caller
/// runs the returned effects through [`crate::jobs::run_effects`].
Expand Down Expand Up @@ -1990,7 +1990,7 @@ async fn event_loop(terminal: &mut Tui, args: &ResolvedArgs) -> color_eyre::Resu
crate::jobs::run_effects(fx, &job_tx);
}
// The examine overlay, when open, owns all keys (read-only
// `rocm examine` job through the job-bridge).
// `rocm doctor` job through the job-bridge).
Some(Ok(CtEvent::Key(k))) if state.examine_manager.is_some() => {
let fx = crate::ui::examine_manager::on_key(
&mut state.examine_manager,
Expand Down Expand Up @@ -5137,15 +5137,15 @@ mod tests {
rocm_dash_core::state::SideEffect::SpawnJob { cmd, args, .. } => {
assert!(cmd.contains("rocm"), "cmd resolves to the rocm exe: {cmd}");
assert!(
args.iter().any(|a| a == "examine"),
args.iter().any(|a| a == "doctor"),
"examine in args: {args:?}"
);
}
other => panic!("expected SpawnJob, got {other:?}"),
}
assert_eq!(
s.examine_manager.as_ref().unwrap().active_job.as_deref(),
Some("examine"),
Some("doctor"),
"the auto-run wires the active job"
);
}
Expand All @@ -5167,7 +5167,7 @@ mod tests {
.iter()
.map(ratatui::buffer::Cell::symbol)
.collect();
assert!(out.contains("Examine"), "overlay content present: {out:?}");
assert!(out.contains("Doctor"), "overlay content present: {out:?}");
assert!(
!out.contains("1–5"),
"no dash tab-shell hint in focused mode"
Expand All @@ -5192,7 +5192,7 @@ mod tests {
// Job terminal → first Esc dismisses the console back to the intro card;
// the overlay is still open, so the gate stays shut.
s.jobs.apply(rocm_dash_core::state::StateEvent::JobDone {
id: "examine".into(),
id: "doctor".into(),
code: 0,
});
let _ = crate::ui::examine_manager::on_key(
Expand Down Expand Up @@ -5257,7 +5257,7 @@ mod tests {

// Once the job is terminal, close keys are allowed again → normal exit.
s.jobs.apply(rocm_dash_core::state::StateEvent::JobDone {
id: "examine".into(),
id: "doctor".into(),
code: 0,
});
assert!(
Expand Down
Loading
Loading