Skip to content
Merged
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
171 changes: 149 additions & 22 deletions crates/rocm-core/src/diagnose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1744,11 +1744,7 @@ fn vllm_anchored_lines(symptom: &str) -> String {
/// a cause rather than reading as "the tool checked and found nothing".
#[must_use]
pub fn vllm_oom_symptom_is_diagnosable(symptom: &str) -> bool {
let e = Examination {
os_family: "linux".to_owned(),
..Examination::default()
};
check_16_vllm_oom(&e, symptom).score >= MIN_SCORE_FOR_MATCH
scores_on_every_engine_host(symptom, "fix-16-vllm-oom", run_all_checks)
}

/// vLLM ran the GPU out of memory. Keyword-only: an [`Examination`] carries no
Expand Down Expand Up @@ -1942,6 +1938,60 @@ fn check_18_comgr_conflict(e: &Examination, symptom: &str) -> Diagnosis {
finalize(ID, TITLE, score, evidence, fix)
}

/// A canonical engine-startup import-failure `--symptom` string guaranteed to
/// clear [`MIN_SCORE_FOR_MATCH`].
///
/// `libtorch_cuda.so` is the one token in that traceback that cannot mean
/// anything else (a ROCm build of torch ships `libtorch_hip.so` and never that
/// file), and it is worth exactly the threshold on its own on purpose; see
/// [`KEYWORDS_TORCH_DLPACK_CUDA_VARIANT`]. The vLLM engine falls back to this when
/// the user's actual failing line would not itself be diagnosable, so the
/// `rocm diagnose` command it prints always reports a cause.
pub const VLLM_IMPORT_CANONICAL_SYMPTOM: &str =
"OSError: libtorch_cuda.so: cannot open shared object file";

/// Whether `symptom` clears [`MIN_SCORE_FOR_MATCH`] for the engine-startup
/// import failure.
///
/// The vLLM engine calls this to decide whether the user's actual failing line
/// is worth routing into a `--symptom` example, or whether to fall back to
/// [`VLLM_IMPORT_CANONICAL_SYMPTOM`] so the printed command is guaranteed to
/// report a cause rather than reading as "the tool checked and found nothing".
///
/// The hint fires on every host the vLLM engine runs on, which is Linux and
/// WSL2. The symptom therefore has to clear the threshold on each of those
/// families, run through the same registry `rocm diagnose` uses, so a check
/// that is registered for one family and not the other cannot make the engine
/// name a command that finds nothing.
#[must_use]
pub fn vllm_import_symptom_is_diagnosable(symptom: &str) -> bool {
scores_on_every_engine_host(symptom, "fix-17-torch-dlpack", run_all_checks)
}

/// Whether the entry `id` clears [`MIN_SCORE_FOR_MATCH`] for `symptom` on every
/// host family the vLLM engine runs on (Linux and WSL2). `run` is the registry
/// run for one host. It is a parameter so a test can hand in a registry where
/// the entry is missing on one family.
fn scores_on_every_engine_host(
symptom: &str,
id: &str,
run: impl Fn(&Examination, &str) -> Vec<Diagnosis>,
) -> bool {
let linux = Examination {
os_family: "linux".to_owned(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard hardcodes os_family: "linux", so it always reports "diagnosable" regardless of the real host. But the checker it calls, check_17_torch_dlpack_cuda_variant, is registered in CHECKERS for &["linux"] only -- unlike its siblings check_16_vllm_oom and check_18_comgr_conflict, both &["linux", "wsl"]. run_all_checks filters by platform_family(e) before invoking any checker, so on a real WSL Examination this checker never actually runs, even though docs/vllm.md documents vLLM as supported on both Linux and WSL.

Concretely: a WSL user hits this import failure, follows the new hint, runs rocm diagnose --symptom '<line>', and gets no diagnosis -- because the real check never fires on that platform, even though this guard (and the PR's test plan) claims every emitted symptom is diagnosable.

Two ways to close this: register check_17_torch_dlpack_cuda_variant for &["linux", "wsl"] to match its siblings and have this guard use the real host instead of a hardcoded one, or scope the engine-side hint in process.rs to skip on WSL to match what's actually diagnosable today.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Check 17 was registered for Linux only, and the guard hard-coded the host. Check 17 is now registered for Linux and WSL, and fix-17 applies on both, like fix-16. The guard no longer names a host. It runs the registry for a Linux and a WSL host and needs the entry to score on both. I added a WSL test like the OOM one, and the catalog and the doctor skill reference are updated. Fixed in 8c976a2.

..Examination::default()
};
let wsl = Examination {
is_wsl: true,
..linux.clone()
};
[linux, wsl].iter().all(|host| {
run(host, symptom)
.iter()
.any(|d| d.id == id && d.score >= MIN_SCORE_FOR_MATCH)
})
}

/// The vLLM engine-startup import failure (EAI-8012).
///
/// Keyword-only, and not by preference. The fact that decides this failure is
Expand All @@ -1953,15 +2003,15 @@ fn check_18_comgr_conflict(e: &Examination, symptom: &str) -> Diagnosis {
/// why the parameter is unused: the user has to supply the error text through
/// `rocm diagnose --symptom "<pasted error>"`.
///
/// That is the only route to this entry from the symptom, and it is a narrow
/// one: nothing on the serve or `rocm services` path points at `rocm diagnose`
/// at all — a service that died at startup renders a `logs:` and a `restart:`
/// hint and no more — so reaching this requires already knowing to paste the
/// error into `diagnose`. The recipe is also reachable by name (`rocm fix` lists
/// it, `rocm fix fix-17-torch-dlpack` prints it), but that needs the id rather
/// than the symptom. Closing that gap means adding a diagnose hint to the
/// service-failure output, which changes a shared surface for every failed
/// service regardless of cause and belongs in its own change.
/// That is the only route to this entry from the symptom. The vLLM engine's own
/// startup-failure message now points at it (`import_failure_hint` in
/// `engines/vllm`, which prints `rocm diagnose --symptom '<the error line>'`),
/// but only for that engine. A service that died at startup for any other reason
/// still renders a `logs:` and a `restart:` hint and no more, because adding a
/// diagnose hint there changes a shared surface for every failed service
/// regardless of cause and belongs in its own change. The recipe is also
/// reachable by name (`rocm fix` lists it, `rocm fix fix-17-torch-dlpack` prints
/// it), but that needs the id rather than the symptom.
fn check_17_torch_dlpack_cuda_variant(_e: &Examination, symptom: &str) -> Diagnosis {
let (score, evidence) = keyword_score(symptom, KEYWORDS_TORCH_DLPACK_CUDA_VARIANT);
if score <= 0 {
Expand Down Expand Up @@ -2707,11 +2757,13 @@ const CHECKERS: &[Checker] = &[
(check_14_adrenalin_too_old, &["windows"]),
(check_15_msvc_redist, &["windows"]),
(check_16_vllm_oom, &["linux", "wsl"]),
// Linux-only: `libtorch_cuda.so` is an ELF name, and the vLLM engine is
// gated off native Windows. 17 rather than 16 because the vLLM
// out-of-memory entry above already holds 16; the number is a stable
// handle, so the two do not get to share one.
(check_17_torch_dlpack_cuda_variant, &["linux"]),
// Linux and WSL2: `libtorch_cuda.so` is an ELF name, and the vLLM engine is
// gated off native Windows but runs on WSL2. The engine's startup-failure
// hint fires on both, so the entry it points at must be reachable on both.
// 17 rather than 16 because the vLLM out-of-memory entry above already
// holds 16; the number is a stable handle, so the two do not get to share
// one.
(check_17_torch_dlpack_cuda_variant, &["linux", "wsl"]),
(check_wsl_1_gpu_not_exposed, WSL_ONLY),
(check_wsl_2_dxcore_missing, WSL_ONLY),
(check_wsl_3_rocdxg_missing, WSL_ONLY),
Expand Down Expand Up @@ -2828,9 +2880,9 @@ pub fn diagnose(e: &Examination, symptom: &str) -> DiagnoseReport {
// in the catalog now, with its own entries; what keeps the bare-metal checks
// off it is `platform_family`, not a special case at this level.
//
// `fix-16-vllm-oom` opts into `wsl` in CHECKERS for that reason: it is a
// keyword-only check whose failure mode is just as real under WSL2, so it
// still answers there alongside the WSL entries.
// `fix-16-vllm-oom` and `fix-17-torch-dlpack` opt into `wsl` in CHECKERS for
// that reason: they are keyword-only checks whose failure modes are just as
// real under WSL2, so they still answer there alongside the WSL entries.
//
// `out_of_scope` still exists, for the platforms that genuinely have no
// entries. That case used to fall through to an empty catalog and report "no
Expand Down Expand Up @@ -6427,6 +6479,81 @@ mod tests {
);
}

#[test]
fn canonical_vllm_import_symptom_is_diagnosable() {
// The engine's fallback symptom must always clear the threshold, so the
// `rocm diagnose` command it prints never reports nothing.
assert!(vllm_import_symptom_is_diagnosable(
VLLM_IMPORT_CANONICAL_SYMPTOM
));
// A frame that names only the shim's module, and not the missing library,
// is not enough on its own, and the engine has to know that.
assert!(!vllm_import_symptom_is_diagnosable(
"File \"tvm_ffi/_optional_torch_c_dlpack.py\", line 42, in <module>"
));
assert!(!vllm_import_symptom_is_diagnosable("an ordinary failure"));
}

#[test]
fn vllm_import_keyword_diagnosis_is_available_on_wsl() {
// The engine's import hint fires on WSL2 too, so `rocm diagnose
// --symptom` has to answer there. Check 17 used to be registered for
// Linux only, and this report had no match.
let report = diagnose(&wsl_base(), VLLM_IMPORT_CANONICAL_SYMPTOM);
assert!(report.out_of_scope.is_none());
assert!(report.has_match());
assert_eq!(report.matched[0].id, "fix-17-torch-dlpack");
}

#[test]
fn the_engine_host_guard_requires_the_entry_to_score_on_every_host_family() {
// A registry stand-in: `listed_on` says which hosts list the entry.
fn registry(
id: &'static str,
listed_on_wsl: bool,
listed_on_linux: bool,
) -> impl Fn(&Examination, &str) -> Vec<Diagnosis> {
move |host, _symptom| {
let listed = if host.is_wsl {
listed_on_wsl
} else {
listed_on_linux
};
if !listed {
return Vec::new();
}
vec![Diagnosis {
id: id.to_owned(),
score: 100,
..Diagnosis::default()
}]
}
}
let guard = |run| scores_on_every_engine_host("x", "fix-17-torch-dlpack", run);
assert!(guard(registry("fix-17-torch-dlpack", true, true)));
assert!(
!guard(registry("fix-17-torch-dlpack", false, true)),
"an entry missing on WSL must not count"
);
assert!(
!guard(registry("fix-17-torch-dlpack", true, false)),
"an entry missing on Linux must not count"
);
assert!(
!guard(registry("fix-16-vllm-oom", true, true)),
"a different entry scoring is not this entry scoring"
);
// And the real registry lists both entries on both families.
assert!(vllm_import_symptom_is_diagnosable(
VLLM_IMPORT_CANONICAL_SYMPTOM
));
assert!(vllm_oom_symptom_is_diagnosable(VLLM_OOM_CANONICAL_SYMPTOM));
// An ISA error scores on another check, not on this one.
assert!(!vllm_import_symptom_is_diagnosable(
"hipErrorNoBinaryForGpu: Unable to find code object for all current devices"
));
}

#[test]
fn canonical_vllm_oom_symptom_is_diagnosable() {
// The engine's fallback symptom must always clear the threshold, so the
Expand Down
7 changes: 6 additions & 1 deletion crates/rocm-core/src/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,12 @@ const RECIPES: &[FixRecipe] = &[
"A service that failed at startup is hidden from a plain `rocm services list`; pass --all to recover its id.",
],
doc_url: None,
applies_on: PRINT_ON_LINUX,
// Matches `check_17_torch_dlpack_cuda_variant`'s `["linux", "wsl"]`
// registration. vLLM is supported on WSL2 and its engine runs the same
// ELF import there, so the diagnosis the engine hint points at has to
// be reachable there too. Pinned by
// `every_checker_platform_is_covered_by_its_recipe`.
applies_on: PRINT_ON_LINUX_AND_WSL,
runner: None,
},
FixRecipe {
Expand Down
6 changes: 4 additions & 2 deletions crates/rocm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ pub use crypto::{
verify_rsa_pkcs1_sha256_signature,
};
pub use diagnose::{
DiagnoseReport, Diagnosis, Fix, Route, VLLM_OOM_CANONICAL_SYMPTOM, diagnose as run_diagnose,
render_report_text as render_diagnose_text, vllm_oom_symptom_is_diagnosable,
DiagnoseReport, Diagnosis, Fix, Route, VLLM_IMPORT_CANONICAL_SYMPTOM,
VLLM_OOM_CANONICAL_SYMPTOM, diagnose as run_diagnose,
render_report_text as render_diagnose_text, vllm_import_symptom_is_diagnosable,
vllm_oom_symptom_is_diagnosable,
};
pub use disk_space::{
SpaceCheck, available_space_for_path, check_space_for_path, ensure_space_for,
Expand Down
39 changes: 39 additions & 0 deletions docs/vllm.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,45 @@ rocm serve <model> --engine vllm --gpu 1 --gpu-memory-utilization 0.5
rocm diagnose --symptom 'vllm: torch.OutOfMemoryError: HIP out of memory'
```

## Engine import failures

A startup failure can also come from the engine failing to import, with no
memory problem at all. The failure message then names the failing line and
points at `rocm diagnose --symptom '<the error>'`.

The hint fires only when the log tail has one of two kinds of line. The first
is a line that contains `libtorch_cuda.so` and also contains `OSError:` or
`ImportError:`, compared without regard to case. It does not have to start with
that text, because the real line begins with a process prefix such as
`(EngineCore pid=412)`. A warning that quotes the exception in its repr form,
such as `ImportError('libtorch_cuda.so ...')`, does not count. A warning written
with a colon, such as `... with ImportError: libtorch_cuda.so ...`, does count,
and it can raise the hint when the real failure is something else. The
second is a traceback frame, meaning a line that contains `file "` and
`_optional_torch_c_dlpack`, compared without regard to case. The library line
wins when both are present, and when several lines name the library the last
one is quoted. A log line that only mentions the `_optional_torch_c_dlpack`
module, such as a `UserWarning`, does not fire the hint. Any other startup
failure prints only the log tail, except an out-of-memory failure, which prints
the memory hint above. If a failure matches both, both paragraphs are printed,
the memory hint first.

The printed command quotes the failing line when it can be rendered as one
intact single-quoted argument and `rocm diagnose` recognises it. Otherwise it
quotes the canonical import symptom,
`OSError: libtorch_cuda.so: cannot open shared object file`. A line with an
apostrophe falls back this way. So does a frame that does not score on its own,
such as `... in <module>`, which names only the shim module and scores below the
match threshold. A loader frame that ends in `in load_torch_c_dlpack_extension`
also names the extension, so it scores on its own and is quoted as it is. When a
frame alone does not score, the command quotes a canonical symptom that the
user's log did not contain. The command then always reports a cause, and the
user's own line stays visible in the message above it.

The command only reports. On Linux and WSL it matches the conditional
engine-reinstall remediation (`fix-17-torch-dlpack`), which `rocm fix` prints
and does not run.

## Tool calling

The TUI chat tab attaches tool definitions to every chat request. vLLM rejects
Expand Down
9 changes: 5 additions & 4 deletions docs/wsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,12 @@ The WSL entries, in the order a broken stack usually reveals them:
| `fix-wsl-5-distro-too-old` | The distro release is below the floor in the [prerequisites](#prerequisites) above |
| `fix-wsl-6-host-driver-too-old` | The distro-side plumbing is complete but the Windows host driver is missing or too old |

Seven entries that are not WSL-specific also run on WSL: `fix-1-arch`,
Eight entries that are not WSL-specific also run on WSL: `fix-1-arch`,
`fix-2-unset-override`, `fix-6-path`, `fix-8-wheel-rocm`, `fix-16-vllm-oom`,
`fix-18-comgr-conflict`, and `fix-19-shm-too-small`. `fix-16-vllm-oom` is driven
by the error text you pass with `--symptom` and reads nothing from the
environment or the ROCm install. `fix-18-comgr-conflict` and
`fix-17-torch-dlpack`, `fix-18-comgr-conflict`, and `fix-19-shm-too-small`.
`fix-16-vllm-oom` and `fix-17-torch-dlpack` are driven by the error text you
pass with `--symptom` and read nothing from the environment or the ROCm
install. `fix-18-comgr-conflict` and
`fix-19-shm-too-small` apply when you run `rocm diagnose` inside the distro. A
`--distro` run skips both (see
[Diagnose a distro from Windows](#diagnose-a-distro-from-windows) later in this
Expand Down
4 changes: 4 additions & 0 deletions doctor-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@
{
"os": "linux",
"class": "print-only"
},
{
"os": "wsl",
"class": "print-only"
}
],
"needs_sudo": false,
Expand Down
Loading
Loading