Skip to content

feat(vllm): Tackle out of memory errors (EAI-8058) - #251

Open
r0x0r wants to merge 6 commits into
mainfrom
gpu-out-of-memory
Open

feat(vllm): Tackle out of memory errors (EAI-8058)#251
r0x0r wants to merge 6 commits into
mainfrom
gpu-out-of-memory

Conversation

@r0x0r

@r0x0r r0x0r commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

This pull request improves GPU selection and user guidance for ROCm and vLLM, especially in shared or containerized environments where the standard amd-smi tool may not be available. It adds a fallback for GPU VRAM telemetry, enhances user warnings and hints for out-of-memory (OOM) conditions, and ensures consistent messaging across CLI and engine surfaces. The changes also include comprehensive tests for the new logic.

GPU selection and VRAM telemetry improvements:

  • Added a fallback to read per-GPU VRAM usage from the amdgpu DRM sysfs counters (/sys/class/drm/card*/device/mem_info_vram_{total,used}) when amd-smi is not available, so --gpu auto ranking and low-VRAM warnings still have telemetry on a single-AMD-GPU host without amd-smi. The fallback deliberately withholds telemetry on a host with more than one AMD DRM card, because card<N> numbering is not guaranteed to match HIP's device ordinal there — so it is a single-GPU convenience, not a multi-GPU replacement.
  • Explicit --gpu <index> validation is amd-smi-independent and checks membership, not a count: when amd-smi is unavailable it validates the index against the KFD/DRM usable set (rocm_core::usable_amd_gpu_indices — absolute ordinals after the HIP_VISIBLE_DEVICES/ROCR_VISIBLE_DEVICES mask), the same authority the engine device gate trusts. Validating against a length would hard-reject the sole usable device on a masked multi-GPU host (e.g. HIP_VISIBLE_DEVICES=2 on a 4-GPU host → usable [2], where --gpu 2 must pass and --gpu 0 must be rejected). The DRM sysfs VRAM rows are never used as a --gpu <index> bound; they stay scoped to --gpu auto ranking.
  • Added tests to verify that auto-selection uses the sysfs fallback, that VRAM usage is parsed and assigned ordinals correctly, and that pinned-index validation prefers the amd-smi count and otherwise falls back to usable-set membership (validate_pinned_gpu_index_against, exercised via an injected detected/usable seam so both branches are covered without touching hardware).

User guidance and warnings for vLLM:

  • Introduced a shared constant VLLM_GPU_MEMORY_UTILIZATION_HINT for the recommended workaround when running out of memory on a shared/busy GPU, ensuring CLI and engine logs use consistent wording.
  • The serve summary now prints a note about the --gpu-memory-utilization workaround when vLLM is selected and the GPU is busy, both interactively and in the deployment summary.
  • vLLM engine startup logs now append the same utilization hint if an OOM is detected, so users receive actionable advice post-failure.
  • Added tests to ensure the low-VRAM warning and vLLM utilization hint are paired correctly and only shown for vLLM engines.

Documentation:

  • Updated docs/vllm.md to explain the behavior on shared/busy GPUs, the single-GPU-only telemetry fallback, and the recommended OOM workaround.

Behavior coverage (e2e):

  • The user-observable behavior "an explicit --gpu <index> that is not usable is rejected outright, never silently remapped to another device" is covered by the Gherkin scenario @id:serve-absent-gpu-index-rejected (Scenario 13 in tests/e2e-cucumber/features/model_serving.feature). It is tagged @requires-gpu @requires-os:linux, so it runs only on the GPU hardware lanes (Strix Halo / Instinct), not the GitHub-hosted mock lane: on a no-GPU host the GPU-required pre-flight refuses with "no usable AMD GPU" before the index is ever validated, so the index-specific rejection can only be observed where a real device is present. The membership-vs-count refinement in this PR is additionally unit-tested on every lane by validate_pinned_gpu_index_falls_back_to_usable_set_membership.

These improvements make GPU selection more robust in diverse environments and provide clear, actionable guidance to users encountering memory issues with vLLM.

@r0x0r
r0x0r requested a review from a team as a code owner August 13, 2026 13:06
@r0x0r
r0x0r force-pushed the gpu-out-of-memory branch 5 times, most recently from 6190726 to ee202f0 Compare August 19, 2026 08:35
@volen-silo

Copy link
Copy Markdown
Collaborator

A few observations from a read of this change:

  • No Gherkin scenario covers the new user-visible behavior (the extra serve note, --gpu auto picking from the sysfs fallback); AGENTS.md §3 asks for one or a stated reason. The unit tests cover helpers rather than behavior.
  • validate_pinned_gpu_index still sees only the amd-smi count, so on the exact environment this targets (no amd-smi, sysfs works) an out-of-range --gpu <n> is accepted and fails later in the engine.
  • read_sysfs_u64(...mem_info_vram_used).unwrap_or(0) makes a card with an unreadable used counter look 100% free — which is what --gpu auto prefers first.
  • read_drm_vram_usage orders by ascending card<N>; an AMD APU passes the same vendor + mem_info_vram_total filter, so on APU + dGPU the ordinal can diverge from HIP's, and it feeds HIP_VISIBLE_DEVICES directly.
  • docs/vllm.md: the fallback probes amd-smi only, never rocm-smi.

On the stack: #284's diff against this branch removes the sysfs fallback, its tests and the docs bullet — looks unintended.

r0x0r added 2 commits August 21, 2026 09:57
…e documentation

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…back

Address review feedback on PR #251:

- resolve_gpu_indices now validates an explicit --gpu <index> against
  the DRM sysfs fallback's device count when amd-smi is unavailable,
  instead of only the amd-smi count. Previously an out-of-range index
  was silently accepted on a host with no amd-smi (sysfs works) and
  only failed later inside the engine.
- read_drm_vram_usage no longer treats an unreadable
  mem_info_vram_used counter as 0 bytes used (which made the card look
  100% free -- exactly what --gpu auto prefers first); it now skips
  that card instead.
- read_drm_vram_usage withholds telemetry entirely when more than one
  AMD DRM card is present, since ascending card<N> order is only
  guaranteed to match HIP's compute-topology ordinal on a single-GPU
  host (an APU passes the same vendor + mem_info_vram_total filter as
  a discrete GPU, so an APU+dGPU host could previously feed a
  diverged ordinal into HIP_VISIBLE_DEVICES).
- docs/vllm.md: corrected the fallback description, which only ever
  probes amd-smi (never rocm-smi) before falling back to DRM sysfs.

Extracted the count-fallback logic into a new effective_gpu_count
helper shared by --gpu auto ranking and --gpu <index> validation, and
added unit tests for all of the above.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
@r0x0r
r0x0r force-pushed the gpu-out-of-memory branch from ee202f0 to a97a39f Compare August 21, 2026 10:14
@r0x0r

r0x0r commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed review — pushed a fix commit (a97a39f) addressing the concrete bugs:

  • validate_pinned_gpu_index only saw the amd-smi count: resolve_gpu_indices now derives an effective_gpu_count that falls back to the DRM sysfs row count when amd-smi is unavailable, so an out-of-range --gpu <n> is rejected up front on exactly the environment this PR targets (no amd-smi, sysfs works), instead of failing later inside the engine.
  • read_sysfs_u64(...mem_info_vram_used).unwrap_or(0): a card whose used counter can't be read is now skipped entirely rather than treated as 0 bytes used (100% free).
  • read_drm_vram_usage ordinal divergence on APU + dGPU: ascending card<N> order only mirrors HIP's KFD-topology ordinal when there's exactly one AMD DRM card. The function now returns no rows at all when more than one AMD card is found, instead of guessing an ordinal that could feed the wrong device into HIP_VISIBLE_DEVICES.
  • docs/vllm.md: fixed — the fallback only ever probes amd-smi, never rocm-smi; wording corrected.

Added unit tests for all four (effective_gpu_count_*, resolve_gpu_indices_rejects_out_of_range_index_from_sysfs_fallback_count, read_drm_vram_usage_skips_a_card_with_an_unreadable_used_counter, read_drm_vram_usage_withholds_telemetry_when_multiple_amd_cards_are_present).

On the Gherkin scenario: I didn't add one for the "no amd-smi, sysfs fallback" path — there's no runner in the current fleet shaped like that (real GPU hardware with amd-smi absent), so I couldn't author or verify a @requires-gpu scenario for it. The corrected behavior is covered at the unit level instead (pure functions, planted sysfs fixtures). Happy to add e2e coverage if/when a suitable lane exists.

On #284: worth double-checking before merging that stack — its diff against this branch appears to drop the sysfs fallback, its tests, and the docs bullet, which does look unintended given this PR is what introduces them.

r0x0r added a commit that referenced this pull request Aug 25, 2026
…back

Address review feedback on PR #251:

- resolve_gpu_indices now validates an explicit --gpu <index> against
  the DRM sysfs fallback's device count when amd-smi is unavailable,
  instead of only the amd-smi count. Previously an out-of-range index
  was silently accepted on a host with no amd-smi (sysfs works) and
  only failed later inside the engine.
- read_drm_vram_usage no longer treats an unreadable
  mem_info_vram_used counter as 0 bytes used (which made the card look
  100% free -- exactly what --gpu auto prefers first); it now skips
  that card instead.
- read_drm_vram_usage withholds telemetry entirely when more than one
  AMD DRM card is present, since ascending card<N> order is only
  guaranteed to match HIP's compute-topology ordinal on a single-GPU
  host (an APU passes the same vendor + mem_info_vram_total filter as
  a discrete GPU, so an APU+dGPU host could previously feed a
  diverged ordinal into HIP_VISIBLE_DEVICES).
- docs/vllm.md: corrected the fallback description, which only ever
  probes amd-smi (never rocm-smi) before falling back to DRM sysfs.

Extracted the count-fallback logic into a new effective_gpu_count
helper shared by --gpu auto ranking and --gpu <index> validation, and
added unit tests for all of the above.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
r0x0r added a commit that referenced this pull request Aug 25, 2026
…back

Address review feedback on PR #251:

- resolve_gpu_indices now validates an explicit --gpu <index> against
  the DRM sysfs fallback's device count when amd-smi is unavailable,
  instead of only the amd-smi count. Previously an out-of-range index
  was silently accepted on a host with no amd-smi (sysfs works) and
  only failed later inside the engine.
- read_drm_vram_usage no longer treats an unreadable
  mem_info_vram_used counter as 0 bytes used (which made the card look
  100% free -- exactly what --gpu auto prefers first); it now skips
  that card instead.
- read_drm_vram_usage withholds telemetry entirely when more than one
  AMD DRM card is present, since ascending card<N> order is only
  guaranteed to match HIP's compute-topology ordinal on a single-GPU
  host (an APU passes the same vendor + mem_info_vram_total filter as
  a discrete GPU, so an APU+dGPU host could previously feed a
  diverged ordinal into HIP_VISIBLE_DEVICES).
- docs/vllm.md: corrected the fallback description, which only ever
  probes amd-smi (never rocm-smi) before falling back to DRM sysfs.

Extracted the count-fallback logic into a new effective_gpu_count
helper shared by --gpu auto ranking and --gpu <index> validation, and
added unit tests for all of the above.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
@volen-silo

Copy link
Copy Markdown
Collaborator

Round 2. Items 3 and 5 are genuinely fixed. Items 2 and 4 are fixed in intent, but the implementations leave a hole and add a new regression.

  • The multi-card guard counts surviving rows, not AMD cards found. In read_drm_vram_usage, three continues drop a card before cards.push(...) — vendor mismatch, missing mem_info_vram_total, missing mem_info_vram_used (the new item-3 skip). if cards.len() > 1 { return Vec::new() } then runs on the already-shrunk vector. On a genuine 2-AMD-card host where one card is filtered out, the guard doesn't fire and the survivor is assigned ordinal 0 regardless of its real card<N> — which feeds HIP_VISIBLE_DEVICES. That is the APU+dGPU misattribution the guard exists to prevent, and it's worst in exactly the case the doc comment reasons about (an APU whose mem_info_vram_total is absent gets dropped, the dGPU becomes "ordinal 0"). read_drm_vram_usage_skips_a_card_with_an_unreadable_used_counter uses a single-card tree, so it can't catch it. Count AMD cards in a separate counter incremented before the telemetry filters.

  • effective_gpu_count turns best-effort telemetry into a hard input-validation bound. A sysfs row count of 1 now hard-rejects --gpu 1..n at the CLI. Two ways that refuses a legitimate index: via the bug above (transient used read failures shrink the count), and via DRM-vs-KFD divergence — combine_amd_gpu_counts exists for precisely this and its own test asserts combine_amd_gpu_counts(Some(3), Some(1)) == Some(3) ("KFD larger than DRM, e.g. multi-partition compute nodes"). usable_amd_gpu_indices() is the amd-smi-independent authority here and is already used for this class of check in main.rs:4858, engines/vllm/src/lib.rs:1122, engines/lemonade/src/lib.rs:3784,3826. Deriving the count from it would also keep read_drm_vram_usage's withholding scoped to VRAM ranking rather than disabling the feature outright on multi-GPU hosts.

  • resolve_gpu_indices_rejects_out_of_range_index_from_sysfs_fallback_count doesn't call resolve_gpu_indices. It hand-composes validate_pinned_gpu_index(1, effective_gpu_count(None, Some(&single_gpu))). Reverting the production fix leaves it green. resolve_gpu_indices has no test caller at all.

  • On the Gherkin decline: agreed for the low-VRAM note: line — the harness has only tag-based host selection plus HIP_VISIBLE_DEVICES masking, no amd-smi shim or fixture sysfs tree. But AGENTS.md §3 wants the reason in the PR text, and @id:serve-absent-gpu-index-rejected (model_serving.feature:152) already covers the out-of-range rejection this PR changes behavior for — §3 asks that be named rather than silently relied on. Body edit, no code change.

Smaller things:

  • After the guard, cards.sort_by_key(...) + .enumerate() can only ever see 0 or 1 elements — the sort, the tuple sort key and the "Placeholder ordinal" comment are dead, and the doc comment's "assigns sequential 0-based ordinals in ascending card order" is not what the code does.
  • drm_device_is_amd checks vendor == 0x1002 only; rocm_core::is_amdgpu_device checks vendor or uevent containing DRIVER=amdgpu. The weaker one feeds the guard. The card walk also duplicates linux_drm_amdgpu_card_count line for line — both rocm-core helpers are private, so a shared exported enumerator would be better than a third, weaker predicate.
  • README.md:353 and skills/rocm-cli-assistant/SKILL.md:30 both still describe amd-smi as the only VRAM source.
  • docs/vllm.md:132 says the note prints "before launch" — true for the plain path, but in summary mode collect_serve_notes runs after start_managed_service and the smoke test. And the --gpu 3 example at :145 sits under the paragraph explaining the fallback only works single-GPU, where --gpu 3 is now rejected.
  • PR body says the fallback works "in stripped-down containers and shared nodes", but it yields nothing on any host with >1 AMD DRM card; docs/vllm.md:129 states the real scope correctly.
  • #[allow(clippy::too_many_arguments)] on collect_serve_notes at 8 positional args — a params struct would read better at the 4 call sites.

Untested: the gpu_vram_usage() amd-smi→sysfs chaining itself, the plain-path note: print, non-numeric sysfs contents.

@r0x0r

r0x0r commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the Round-2 review — pushed 3dcaa7b addressing it.

Multi-card guard counted surviving rows, not AMD cards found. Fixed. read_drm_vram_usage now increments a dedicated amd_cards_found counter the moment drm_device_is_amd returns true — before any telemetry (readable-counter) filtering — and withholds all rows when amd_cards_found > 1, so a second AMD card that yields no counter still suppresses the potentially-misnumbered telemetry. New test: read_drm_vram_usage_withholds_telemetry_when_a_second_amd_card_has_no_counter.

effective_gpu_count hard-bounded --gpu from best-effort telemetry. Fixed. --gpu validation now goes through pinned_index_bound, which prefers the amd-smi count and falls back to rocm_core::usable_amd_gpu_indices().map(|i| i.len()), never the VRAM-row count. effective_gpu_count is now used only for auto-ranking (doc updated). New tests: resolve_pinned_gpu_index_validates_against_the_detected_count, pinned_index_bound_prefers_the_amd_smi_count.

The "rejects out-of-range index" test didn't call resolve_gpu_indices. Fixed by extracting resolve_pinned_gpu_index(index, detected) (the Index arm) and testing it directly, replacing the old test that never exercised the path.

Body should name the declined scenario + its Gherkin reason. Done in the body: @id:serve-absent-gpu-index-rejected's premise (an index-specific rejection) can only be observed on a real GPU host, since a no-GPU host refuses at the GPU-required pre-flight first.

Smaller items: removed the dead sort/enumerate tail after the guard; drm_device_is_amd now matches rocm_core::is_amdgpu_device (vendor 0x1002 or uevent DRIVER=amdgpu); README and the assistant SKILL now mention the DRM sysfs VRAM fallback.

@volen-silo

Copy link
Copy Markdown
Collaborator

Round 3. The multi-card guard (A), the dead sort/enumerate tail, drm_device_is_amd, and the README/SKILL updates are all genuinely fixed — verified against source, not just the reply. Two things still block.

Blockers

  1. apps/rocm/src/main.rs:16751-16756pinned_index_bound collapses usable_amd_gpu_indices() to .len() and uses it as an upper bound. That function returns absolute, unrenumbered ordinals after applying the visibility mask — crates/rocm-core/src/lib.rs:11548 asserts usable_amd_gpu_indices_from(4, Some("2,0")) == Some(vec![2, 0]). So on a 4-GPU host with HIP_VISIBLE_DEVICES=2 and no amd-smi (detected == None — exactly this PR's target environment), pinned_index_bound yields Some(1) and --gpu 2 is hard-rejected as "out of range", even though 2 is the only usable device. The mirror case also holds: with usable == [2, 3], --gpu 0 passes CLI validation for a device that isn't visible at all.

    This is a regression: pre-PR, detected == None meant no CLI-side validation, and the request reached the engine gate, which does it correctly — engines/vllm/src/lib.rs:1139-1150 checks usable.contains(index), and engines/lemonade/src/lib.rs:3784 does the same. main.rs:16754 is the only call site in the repo that reduces that API to a length. The check wants membership, not a count.

  2. apps/rocm/src/main.rs:23962-23967pinned_index_bound_prefers_the_amd_smi_count only exercises Some(4) / Some(0); nothing calls pinned_index_bound(None). The .or_else fallback is the entire behavior this round added, and it's the branch carrying the bug above. As written it isn't injectable (it calls the real probe), so testing it means splitting a pinned_index_bound_from(detected, usable) seam — the same shape resolve_gpu_indices_against already uses in the engines. Same gap as round 2's item 3, one layer down.

  3. AGENTS.md §3 — the reply says the Gherkin decline reason and @id:serve-absent-gpu-index-rejected went into the PR body, but the body is unchanged from the original. §3 wants the named scenario and the gated-lane reason in the PR text. Body edit, no code change.

Nits (non-blocking)

  • docs/vllm.md:133 says the note prints "before launch". True on the plain path; in the default interactive summary mode collect_serve_notes runs at main.rs:5058, after start_managed_service and the smoke test.
  • docs/vllm.md:145 — the --gpu 3 example still sits under the paragraph explaining the fallback withholds telemetry on multi-GPU hosts, so the reader gets a 4-GPU example in the section about the case where none of this telemetry exists. Both new examples also drop --engine vllm, unlike every other bash block in the file.
  • PR body still claims the fallback makes things work "in stripped-down containers and shared nodes" and that "the device count for selection now also derives from these sysfs rows" — the first yields nothing on any host with >1 AMD DRM card, and the second is now auto-ranking only.
  • read_drm_vram_usage's card walk is still a line-for-line third copy of the enumeration in rocm_core::linux_drm_amdgpu_card_count + is_amdgpu_device. A single exported enumerator would keep the two from drifting again.
  • log_tail_shows_oom matches "out of memory" anywhere in the 80-line tail, so an unrelated failure whose tail happens to mention memory gets the hint appended. Low impact — the hint is additive — but it isn't guarded.

cargo fmt and cargo clippy --workspace --all-targets are clean; -p rocm --bins and -p rocm-engine-vllm --lib pass (one unrelated flake in providers::tests under parallel run, passes in isolation, untouched by this PR).

@r0x0r

r0x0r commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the Round-3 review — pushed 59c7f31 (rebased onto the current branch tip, which now carries #290's squash; one docs/vllm.md example-block conflict resolved to keep both your diagnose line and the --engine vllm fix).

Blocker 1 — the fallback reduced the usable set to a length. Fixed at the layer you named. The count bound is gone; --gpu <index> now validates membership. When amd-smi is present its renumbered 0..count is still the authority (index >= count → out of range); when it's absent, the index must be a member of usable_amd_gpu_indices() — the absolute, unrenumbered ordinals the engine gate already trusts. On your 4-GPU / HIP_VISIBLE_DEVICES=2 case, --gpu 2 (usable [2]) now passes and the mirror case — a hidden --gpu 0 with usable == [2, 3] — is rejected, with a message that names the usable set and the visibility vars. An empty usable set is deliberately not authoritative here (the no-usable-GPU fail-fast owns that message), so it doesn't reject.

Blocker 2 — the fallback branch wasn't injectable. Split the seam you asked for: validate_pinned_gpu_index_against(index, detected, usable). resolve_pinned_gpu_index supplies the real usable_amd_gpu_indices().as_deref(), and the new validate_pinned_gpu_index_falls_back_to_usable_set_membership test drives the detected == None branch directly (sole-usable passes, hidden rejected, empty set allowed through, amd-smi count still wins). The pre-existing tests now go through the same seam.

Blocker 3 — scenario + gated lane in the body. Done in the PR body (not just this comment). The user-observable "explicit --gpu <index> is rejected outright, never remapped" behavior is @id:serve-absent-gpu-index-rejected (Scenario 13, tests/e2e-cucumber/features/model_serving.feature), tagged @requires-gpu @requires-os:linux: on a no-GPU host the GPU-required pre-flight refuses with "no usable AMD GPU" before the index is ever validated, so the index-specific rejection is only observable on the GPU hardware lanes. The membership refinement is unit-covered on every lane.

Nits. Fixed the two docs/vllm.md inaccuracies (the "before launch" timing now also names the interactive post-readiness path; both workaround examples gained --engine vllm, and the stray 4-GPU --gpu 3 example is now --gpu 1). Corrected the PR-body overclaims you flagged — "stripped-down containers and shared nodes" is now scoped to single-GPU, and the "device count for selection derives from sysfs rows" line is gone (that path is auto-ranking only). Left read_drm_vram_usage's card-walk dedup and the broad log_tail_shows_oom match as noted-but-deferred; happy to fold the DRM enumerator into rocm_core in a follow-up if you'd rather have it here.

Verification note: the full cargo test/clippy workspace build can't run on my macOS box — an unrelated st_mode u16/u32 libc mismatch in engines/lemonade, and macOS is unsupported per AGENTS §6 — so the Linux CI lanes are the authoritative gate for the compiled and e2e checks. cargo fmt and rust-analyzer are clean locally.

@rominf rominf left a comment

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 is a well-tested change and the core GPU-selection/validation logic (the amd-smi-independent membership check, the DRM sysfs fallback and its multi-card guard, the conditional OOM diagnosis) looks sound after the several rounds of review already visible on the thread. I only found one new issue, introduced in the latest commit while resolving a rebase conflict in the docs.

In docs/vllm.md, the "Explicitly, the workaround for an OOM on a shared card is:" example has a duplicated ```bash fence opener right before the example commands. As written, the block opens, immediately closes/reopens on the literal text, and the commands after it are no longer inside a fenced code block for the rest of that section (until the next ```` ) closes it) - so the example won't render as a code block. Please drop the duplicate line.

Comment thread docs/vllm.md
r0x0r added 4 commits August 28, 2026 11:09
…RM guard

Round-2 review follow-ups on the DRM sysfs GPU-selection fallback.

- Validate an explicit --gpu <index> against the amd-smi list count or, when
  amd-smi is absent, the KFD/DRM authority behind usable_amd_gpu_indices()
  (pinned_index_bound) instead of the DRM VRAM-fallback row count. The row
  count withholds telemetry on multi-GPU hosts and shrinks on a transient
  counter-read failure, so borrowing it as a validation bound hard-rejected
  legitimate indices. VRAM rows stay scoped to --gpu auto ranking.
- The multi-card ordinal-ambiguity guard now counts AMD cards *found* (before
  the telemetry filters) rather than surviving rows, so a second AMD card with
  an unreadable counter still trips the guard instead of mislabelling the
  survivor ordinal 0 (the APU+dGPU misattribution the guard exists to prevent).
- drm_device_is_amd now matches on vendor id OR an amdgpu uevent DRIVER line,
  the same two-signal test as rocm_core::is_amdgpu_device, so the fallback probe
  and the count authority agree on what an AMD card is.
- Drop the now-dead sort/enumerate tail in read_drm_vram_usage (at most one card
  survives the guard) and fix the stale doc comment.
- Tests: exercise the real Index-arm delegate (resolve_pinned_gpu_index) and add
  a two-AMD-card-with-unreadable-counter regression; add pinned_index_bound
  coverage.
- Docs: README and the assistant SKILL note the DRM sysfs VRAM fallback rather
  than presenting amd-smi as the only source.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…g (EAI-8060) (#290)

* feat(diagnose): add vLLM out-of-memory failure mode to the catalog (EAI-8060)

Add a keyword-scored OOM signature to the rocm-core diagnosis catalog and a
matching print-only remediation recipe. The error covers two distinct faults
(a tenancy collision with vLLM's fixed ~90% VRAM reservation, versus a model
that genuinely does not fit), so the wording stays conditional: it never
prescribes lowering --gpu-memory-utilization as the unconditional answer, and
the verify step avoids the tenancy knob. Because an Examination carries no
per-GPU VRAM or tenancy fields, the match is keyword-only and the checker is
gated to Linux (vLLM is Linux/WSL-only).

- diagnose.rs: KEYWORDS_VLLM_OOM table + check_16_vllm_oom (linux-only)
- fix.rs: print-only fix-16-vllm-oom recipe; catalog count 15 -> 16 and
  AUTO-set assertion strengthened
- engines/vllm: serve OOM hint points users at 'rocm diagnose --symptom'
- e2e-cucumber: conditional-remediation scenario + step defs and catalog
  contract updated

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* fix(diagnose): require a vLLM anchor before matching the OOM checker

Review feedback on #290 flagged that KEYWORDS_VLLM_OOM never required
anything vLLM-specific: 'torch.OutOfMemoryError: CUDA out of memory'
scores 45+45=90 (high confidence) under the keyword table alone, so
any ROCm PyTorch job's OOM would be misreported as a vLLM startup OOM
with rocm-serve-only remediation. gpu_memory_utilization -- the one
vLLM-specific token -- was weighted lowest and never required.

check_16_vllm_oom now requires an explicit vLLM anchor (the word
'vllm', or one of its distinctive flags: gpu[-_]memory[-_]utilization,
tensor[-_]parallel) before the keyword table is scored at all. Update
the serve OOM hint's suggested --symptom text and the matching
e2e-cucumber step to carry that anchor so the self-referential
'rocm serve' -> 'rocm diagnose' flow keeps working, and add a
regression test for the reported false positive.

Also fix a second issue from the same review: the diagnose() WSL
branch dropped sub-threshold hits from matched entirely when nothing
cleared MIN_SCORE_FOR_MATCH, which the DiagnoseReport::matched doc
says should never happen. It now keeps whatever run_all_checks
returns (empty only when no wsl-applicable checker fired at all) and
adds a regression test.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* diagnose: drop the non-existent multi-GPU sharding remedy and tighten the vLLM OOM anchor

Round-2 review (EAI-8060):
- Remove the `--tensor-parallel-size` / multi-GPU sharding remediation from
  both the fix catalog (fix-16-vllm-oom) and the diagnose summary: the flag does
  not exist and rocm-cli serves one model on a single GPU (docs/vllm.md). The
  'model does not fit' branch now points only at a smaller/quantized model.
- Drop `tensor[-_]parallel` from VLLM_ANCHOR_PATTERN: it is a Megatron/DeepSpeed
  term, so anchoring on it would misattribute those frameworks' OOMs to vLLM.
  The anchor is now just `vllm|gpu[-_]memory[-_]utilization`.
- Route the user's *actual* failing log line into the `rocm diagnose --symptom`
  hint (vllm-anchored) instead of a canned literal.
- Escape the dot in the `torch\.outofmemoryerror` keyword regex and let the
  `gpu[-_]memory[-_]utilization` keyword accept a hyphen like the anchor.
- Tests: e2e reads high_confidence_threshold from the report instead of a
  hardcoded 75, locks out `--tensor-parallel-size`, and shares a find_vllm_oom
  helper; the sub-threshold unit test uses `.expect()` instead of a vacuous
  `if let`.
- docs/vllm.md and the @requires-bare-metal doc note the `rocm diagnose
  --symptom` pointer and the WSL keyword-only exception.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* style: rustfmt the new OOM --symptom routing test assertion

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…gth (EAI-8058)

When amd-smi is unavailable, `--gpu <index>` validation fell back to the *length* of usable_amd_gpu_indices(). That set holds absolute, unrenumbered ordinals after the visibility mask, so on a masked multi-GPU host (e.g. HIP_VISIBLE_DEVICES=2 on a 4-GPU box -> usable [2]) the length-1 bound rejected --gpu 2 (the sole usable device) and accepted a hidden --gpu 0.

Replace the count bound with an injectable validate_pinned_gpu_index_against that prefers the amd-smi count and otherwise checks index membership in the usable set -- the same authority the engine device gate uses. The detected/usable seam makes both branches unit-testable without hardware.

Also correct two docs/vllm.md inaccuracies (note timing; missing --engine vllm in the workaround examples).

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
A merge-conflict resolution left two consecutive ```bash fence openers before
the shared-card OOM workaround example, so the block opened and immediately
reopened on the literal text and the commands rendered outside a code block.
Drop the duplicate opener so the example renders as a single fenced block.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
@r0x0r
r0x0r force-pushed the gpu-out-of-memory branch from 59c7f31 to e94ca5f Compare August 28, 2026 11:11
@r0x0r

r0x0r commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — good catch, the merge-conflict resolution had left two consecutive bash fence openers before the shared-card workaround example, so the block reopened on the literal text and the commands rendered outside the fence. Fixed in e94ca5f (now the PR head): dropped the duplicate opener so it renders as a single fenced block. No logic touched — docs-only.

@r0x0r
r0x0r requested a review from rominf August 31, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants