Conversation
6190726 to
ee202f0
Compare
|
A few observations from a read of this change:
On the stack: #284's diff against this branch removes the sysfs fallback, its tests and the docs bullet — looks unintended. |
…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>
ee202f0 to
a97a39f
Compare
|
Thanks for the detailed review — pushed a fix commit (a97a39f) addressing the concrete bugs:
Added unit tests for all four ( On the Gherkin scenario: I didn't add one for the "no 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. |
…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>
…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>
|
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.
Smaller things:
Untested: the |
|
Thanks for the Round-2 review — pushed Multi-card guard counted surviving rows, not AMD cards found. Fixed.
The "rejects out-of-range index" test didn't call Body should name the declined scenario + its Gherkin reason. Done in the body: Smaller items: removed the dead sort/enumerate tail after the guard; |
|
Round 3. The multi-card guard (A), the dead sort/enumerate tail, Blockers
Nits (non-blocking)
|
|
Thanks for the Round-3 review — pushed Blocker 1 — the fallback reduced the usable set to a length. Fixed at the layer you named. The count bound is gone; Blocker 2 — the fallback branch wasn't injectable. Split the seam you asked for: Blocker 3 — scenario + gated lane in the body. Done in the PR body (not just this comment). The user-observable "explicit Nits. Fixed the two Verification note: the full |
rominf
left a comment
There was a problem hiding this comment.
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.
59c7f31 to
e94ca5f
Compare
|
Thanks — good catch, the merge-conflict resolution had left two consecutive |
|
@rominf the duplicated |
rominf
left a comment
There was a problem hiding this comment.
Re-reviewing at e94ca5f1 — the docs fence I flagged earlier is fixed, thanks. The four layers here are individually well built: the keyword table's own comment explains why a bare "out of memory" is sub-threshold, VLLM_ANCHOR_PATTERN exists precisely because the torch OOM shape isn't vLLM-specific, and the WSL rework is careful to keep sub-threshold hits in matched rather than dropping them. --gpu auto ranking on free VRAM with the multi-card ordinal guard in read_drm_vram_usage is a good piece of work.
My concerns are all at the seams between the layers, where each side is individually reasonable and the pair isn't.
The biggest: the engine detects an OOM with a bare substring scan, then tells the user to run rocm diagnose --symptom 'vllm: <the failing line>' — but the diagnose table scores several of the lines the engine accepts below MIN_SCORE_FOR_MATCH. I replicated keyword_score against the table in this diff rather than eyeballing it:
DET score= 0 NONE vllm: RuntimeError: hipErrorOutOfMemory
DET score= 0 NONE vllm: torch.cuda.OutOfMemoryError
DET score= 25 WEAK vllm: HIP error: out of memory
DET score= 25 WEAK vllm: the process was killed: out of memory
DET score= 90 LIKELY vllm: torch.OutOfMemoryError: HIP out of memory. Tried to allocate 7.21 GiB.
DET means log_tail_shows_oom accepted it, so the engine printed the command. The first two produce nothing at all. Details inline.
Second: hip out of memory (45) and out of memory (25) both match the same token span, and keyword_score sums the top two, so one phrase yields 70 and two "independent" evidence bullets. That's the exact overlap torch_oom_class_alone_is_not_a_vllm_match guards against for the PyTorch pattern — the guard just isn't applied here.
Third: making out_of_scope depend on the symptom turns it from a host property into a host+symptom property, and diagnose-states-whether-the-platform-is-covered asserts it agrees with examine's status — which is unconditionally "wsl" on WSL. There's no WSL lane in CI (the scenario's own comment says so), so this lands on a developer.
For what it's worth I checked and disproved several things, so they're not worth your time: applies_on: LINUX_ONLY does not block fix-16 on WSL; \boutofmemory\b and torch\.outofmemoryerror don't double-count; wsl_out_of_scope_message is byte-identical to main; fix-16 is the only checker registered for "wsl".
On CI — the red E2E tests (GPU) lane looks like a stale base rather than this PR. All four failures share device_policy: gpu_required; no active ROCm runtime is configured, which 283811f (#322) fixed on main; main's latest self-hosted run is green. A rebase should clear it, and is worth doing before merge so the lane is actually informative.
…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>
e94ca5f to
e1adff1
Compare
|
Thanks @rominf — pushed Engine hint vs. scorer coupling (the printed Double-counted keyword spans / "one bullet per phrase". Same-line anchor. WSL out-of-scope routing.
Duplicated fix-16 remediation text. Pre-launch low-VRAM note scenario. Full workspace |
…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>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…ndidate `select_auto_gpu_index` builds its candidate ordinals from the VRAM rows alone whenever telemetry is present. That was introduced so a visibility mask reporting non-contiguous ordinals is scanned by its real indices instead of a synthetic `0..count` range, which is right — but a short row set is not always a mask. `parse_gpu_vram_usage` drops any device entry missing `/mem_usage/used_vram/value` or `/mem_usage/total_vram/value`, so a device the lighter `list` enumeration counts can simply have no row. With device 1 absent from the rows and device 0 pinned by a managed service, `reported` was `[0]`, the busy filter emptied the candidate list, all three passes iterated nothing and the terminal fallback returned device 0 — `serve` pinned to an already-occupied GPU, then failing on VRAM, which is the fault this selection exists to reduce. Union the detected `0..count` range back in only when `visible` is known. The existing retain then validates every ordinal against the mask, so no unconfirmed index is ever synthesised: under a `[2, 3]` mask with `count == 2` the synthetic `[0, 1]` is dropped wholesale and the rows still stand alone. Without `visible` there is no second source to confirm an ordinal against, so the rows remain authoritative. Pins the case with a test: a two-GPU count, one row, an all-busy reported set, which the suite never exercised (every all-busy case passed `detected: None`). Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Superseded. This objection was filed at an earlier commit; it is retired here and replaced by a fresh review at the current head, so that only one of our change requests is live at a time. Retiring it is bookkeeping, not a withdrawal on the merits: the selection defect it named is confirmed fixed on the path where the host's usable device set is known, and confirmed still present on the path where it is not. The replacement states both precisely.
|
🔴 Automated review · pr-review-watcher · 73b8bfb This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryAdds vLLM out-of-memory diagnosis (new catalog entry, recipe, terminal-escape module), a DRM sysfs VRAM fallback for 🚫 Blocking (must fix before merge)
The head commit unions the detected
Running the three extracted revisions over count = 2, one VRAM row for GPU 0, GPU 0 service-pinned, both GPUs present:
That is exactly the failure the commit message describes: "serve pinned to an already-occupied GPU, then failing on VRAM, which is the fault this selection exists to reduce." The inline rationale at The PR's new test does not hold the gate either. Forcing Concrete fix: discriminate on the actual cause rather than on
The Pass 2 comment reads: "No "does this ordinal have a row?" guard is needed: inside this branch every candidate came from a reported row, so The union added at line 20933 puts synthetic I checked whether this is also a behaviour bug and it is not — I re-added the removed Non-blocking
Prior roundOur earlier review text was not available while this review ran, so nothing is quoted verbatim; instead both counts were re-verified from the source at this head, and all 44 commit objects in the range were read whole rather than as summaries. Count 1, selection behaviour — PARTIALLY DISCHARGED, and the remainder STANDS. Discharged on the Count 2, docs contradicting code — STANDS, in two confirmed places at this head: the Pass 2 rationale comment falsified by the same commit's union (and by the commit's own new test), and the union rationale at lines 20916-20930 claiming a guarantee the code delivers only under What misled a previous cycle, and the cheap guard against a repeat. This is codebase-invited confusion, not reviewer error: the repair and the mask logic are fused into one expression, so the diff reads as a general fix while the gate silently narrows it to one platform family, and the surrounding comment asserts the general guarantee. A future reviewer will make the same misread. The cheap prevention is the test named in the first blocker — one case with No prompt-injection content was found in the diff, the commit objects or the touched docs; the leak scan over the diff and over all 44 raw commit objects is clean, and the bare ticket IDs present are the form this project expressly permits. |
siloteemu
left a comment
There was a problem hiding this comment.
pr-review-watcher · 73b8bfb
Two blocking findings at this head; the full report is in the round comment.
-
The GPU-selection repair is gated on whether the host's usable device set is known. The fault it fixes is unrelated to that: it comes from a counted device having no telemetry row. On hosts where the usable set is never known -- which includes every Windows host -- the selection still returns the busy card where the base branch returns the idle one. Forcing that gate true leaves all three of the new test's assertions green, so nothing pins it, and no test anywhere combines a known device count with an unknown usable set and a short row set. Discriminating on the actual cause rather than on the usable set, plus one test covering that combination, would close it.
-
The rationale comment on the second ranking pass states an invariant that this same commit falsified, and the new test disproves it: a selected ordinal can now have no telemetry row. Re-adding the removed guard and running both variants shows no behavioural difference, so the guard removal itself is sound -- only the stated reason is wrong. It still needs correcting, because it advertises a property a maintainer would rely on while editing this ranking code.
Our earlier change request on this PR was filed at an older commit and has been retired, so this is the only one of ours that is live.
…ible` The previous commit unioned the detected `0..count` range back into the auto-select candidates only `if visible.is_some()`, so the fix reached only hosts whose visibility set could be probed. `probe_usable_amd_gpu_indices` returns `None` unconditionally for `#[cfg(not(target_os = "linux"))]`, and `serve` threads that straight into `visible` through `select_gpu_indices_under_launch_lock`. So `visible` is always `None` on Windows — a supported platform — and on any Linux host whose KFD topology and DRM cards are both unreadable. With `count == 2`, one VRAM row for GPU 0 and GPU 0 pinned by a managed service, those hosts still returned the busy GPU 0 where the base branch returned the idle GPU 1: the exact fault this selection exists to reduce. `visible` was never the right discriminator. The rows are a re-indexed or sparse set precisely when some row index falls at or above `count` (`[2, 3]` against `count == 2`), which is the shape the rows-only construction was introduced for; below the count they are a subset of a dense range the count already asserts exists, and unioning `0..count` there is no stronger an assumption than the telemetry-less arm already makes. Require `detected` to be `Some(n > 0)` as well, so the count comes from the `list` enumeration rather than from `rows.len()` restating the rows' own size — `parse_gpu_vram_usage` only falls back to the array position when an entry has no `gpu` field, so a payload naming `"gpu": 0` twice yields duplicate ordinals that would otherwise conjure a GPU 1. The visible retain still runs on top whenever a mask is known, so nothing synthesised survives where the mask contradicts it. Pins both halves of the condition: forcing either to `true` turns an assertion of the new test red, and the masked assertion now holds with `visible: None` so it pins the row-index rule itself rather than the retain. Also replaces the Pass 2 rationale, which the union falsified. `usage_for` is no longer total over `candidate_indices`, but no row guard is needed for a different reason: the comparator keys are `Option<u64>` and `None` sorts below every `Some`, so a rowless ordinal can win only when no candidate has a row, and then the index tie-break returns the lowest — exactly what Pass 3 would return. Re-adding the guard leaves all 771 unit tests green, so the guard is behaviourally inert and stays out. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Withdrawing this change request: both counts are discharged at 2aa6434.
Count 1 — the usable-set gate is gone. The detected range is now unioned back in under a condition that requires the count to come from the enumeration and the rows to be a dense subset of it, with the mask applied strictly on top. Verified by reproducing the original probe in a scratch copy: restoring the old gate now fails the new unprobeable-host test, and forcing either half of the new condition true fails a different assertion of that same test, so both halves are independently pinned rather than covered only by a wholesale revert.
Count 2 — the rationale comment no longer claims the falsified invariant. It states the opposite and gives the correct reason, which checks out against the comparator.
A separate change request has been filed at this head for a new defect found in this round; it is unrelated to either count above.
|
🔴 Automated review · pr-review-watcher · ce7a234 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryThe PR adds OOM diagnosis and a shared terminal-sanitising module; the new commit closes the quoting guard on the two mandatory line-break scalars. Our prior objection is DISCHARGED on code and on pins — the fix is the shared predicate we asked for, not a local patch. Outcome: no blocking findings; one residual from the recurrence half of our own objection. Verified by mutation in scratch copies (never the tree under review): (1) reverting the gate to the narrow 🚫 Blocking (must fix before merge)None. Non-blocking
|
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 2aa6434
Blocking: 1. Both counts of our previous change request are discharged and that review has been withdrawn; this one is new and unrelated to either.
engines/vllm/src/lib.rs:2780 — the quoting guard admits the two mandatory line-break scalars into the printed diagnose command, contradicting the contract this same pull request states for them.
The predicate rejects a single quote and anything the shared control-or-format test matches, but that test is character-class based and returns false for both LINE SEPARATOR and PARAGRAPH SEPARATOR. This pull request's own character classifier widens past that same test precisely for these two, and documents at its definition that the shared test is false for both and that a mandatory line break is exactly the kind of non-drawing character the stripper exists to remove. So the two call sites disagree about the same question, and the narrower one is the one guarding output.
The gate is load-bearing rather than cosmetic. The value it admits is built from the raw, unstripped line, not from the sanitised one, and line splitting does not break on LINE SEPARATOR — so such a scalar survives inside a selected line. A log line carrying one after an out-of-memory phrase clears the scan, clears this gate, scores above threshold, and is embedded verbatim in the command printed for the user to copy and paste. The bytes stay inside the quotes, so this is display control rather than a shell-quoting escape, but it is the same class the guard's own doc comment says it closes.
Nothing pins it. The neighbouring test deliberately exercises this guard for the bidi override, with a comment explaining that the two call sites are tested separately, yet the two line-break scalars are pinned against the stripper only.
Fix, and it is worth doing as the shared predicate rather than the local patch: export one line-boundary test and use it at both call sites, then add the missing assertion for both scalars alongside the bidi one. The local patch would work, but the confusion recurs otherwise — the shared test's own doc comment advertises itself as the answer to this exact concern while the classifier's documents that it is not sufficient, so the next call site reaches for the narrower one again.
Five non-blocking items, including a detector that disagrees with its twin about what counts as evidence, are in the review comment posted alongside this one.
`quotable_in_single_quotes` gates what is embedded in the `rocm diagnose --symptom '...'` command the OOM hint prints for the user to paste, and it tested each character with `is_control_or_format` alone. That predicate is `char::is_control` plus the enumerated `Cf` set, and `U+2028 LINE SEPARATOR` / `U+2029 PARAGRAPH SEPARATOR` are `Zl`/`Zp`, so both passed the gate. The value reaching it is raw: `candidate` is built from `raw_line`, not from the stripped `symptom_line` beside it, and `str::lines` does not split on `U+2028`, so the scalar survives inside a selected line. A tail line like `RuntimeError: HIP out of memory<U+2028>...` cleared the OOM scan, cleared this gate, scored above threshold and was printed verbatim inside the quotes — the bytes stay inside them, but the rest of the command renders on the next row. That is the same class of display control the guard's own documentation says it closes. `classify_char` already widened past `is_control_or_format` for exactly this pair, so the module stated one contract and the gate implemented a narrower one. Rather than repeat the `matches!` at the gate, export `is_control_or_line_separator` from `rocm_core::terminal` and use it at both sites: `is_control_or_format`'s own documentation advertises itself as the answer to this concern, so the next call site would reach for the narrower test again. One predicate removes the trap, and the rationale moves onto it instead of being restated at each user. The stripper table test pinned both scalars against `strip_terminal_control_sequences` only; it now pins them against the quoting guard as well, which is where they got through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Discharged at ce7a234. The shared line-boundary predicate was exported and both call sites now use it, and mutation confirms each of the two scalars is pinned on its own: dropping either one from the predicate turns tests red in both crates, and reverting the gate to the narrower test fails naming the right thing. Withdrawing this change request. One residual doc-comment point is non-blocking and left in the review comment.
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # engines/vllm/src/lib.rs
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
🔴 Automated review · pr-review-watcher · 5379d56 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryThis pull request adds a shared ECMA-48 escape-sequence walk, routes the vLLM out-of-memory hint and the diagnose scorer through it, shares one constant for the 🚫 Blocking (must fix before merge)None. Non-blocking
|
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # tests/e2e-cucumber/features/model_serving.feature
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # crates/rocm-core/src/fix.rs
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # crates/rocm-core/src/lib.rs
`fix-16-vllm-oom` is added to the CLI's closed catalog by this branch, but `skills/rocm-doctor/reference.md` still described `fix-16` as "a reserved handle, not a missing row". The skill doc is asserted against the CLI catalog by three e2e scenarios, and all three failed on the merge: `rocm fix` offers fix-16-vllm-oom, which reference.md does not document. Add the catalog row (linux/wsl, not auto-applicable, matching the recipe's LINUX_AND_WSL and auto_applicable: false), raise the catalog count from 24 to 25, and move the reserved-handle note to `fix-18`, which is still the only gap in the numeric series. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # docs/vllm.md
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # crates/rocm-core/src/lib.rs # tests/e2e-cucumber/features/diagnose.feature
This pull request improves GPU selection and user guidance for ROCm and vLLM, especially in shared or containerized environments where the standard
amd-smitool may not be available. It adds a fallback for GPU VRAM telemetry, enhances user warnings and hints for out-of-memory (OOM) conditions, adds the vLLM startup OOM to thediagnose/fixcatalog, and ensures consistent messaging across CLI and engine surfaces.GPU selection and VRAM telemetry improvements:
/sys/class/drm/card*/device/mem_info_vram_{total,used}) whenamd-smiis not available, so--gpu autoranking 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, becausecard<N>numbering is not guaranteed to match HIP's device ordinal there — so it is a single-GPU convenience, not a multi-GPU replacement.--gpu <index>validation was reworked on this branch to check membership in the KFD/DRM usable set rather than a device count. That is superseded by fix(serve): close serve races and translate GPU selection through visibility masks (EAI-7194) #267, which landed a validator consulting the visibility-resolved set directly (and with mask-aware wording), and the merge below drops this branch's version in favour of it — the two answered the same question, and fix(serve): close serve races and translate GPU selection through visibility masks (EAI-7194) #267's answer is the stronger one. The DRM sysfs VRAM rows are never used as a--gpu <index>bound; they stay scoped to--gpu autoranking.--gpu autoranks the actual reported VRAM row indices, which can be non-contiguous under a visibility mask, rather than a synthetic0..countrange that would look up absent rows and fall through to a bogus GPU 0.User guidance and warnings for vLLM:
VLLM_GPU_MEMORY_UTILIZATION_HINTfor the recommended workaround when running out of memory on a shared/busy GPU, ensuring CLI and engine logs use consistent wording.--gpu-memory-utilizationworkaround when vLLM is selected and the GPU is busy, both interactively and in the deployment summary.vLLM startup OOM in the diagnose/fix catalog (EAI-8060):
check_16_vllm_oom) plus a print-onlyfix-16-vllm-oomrecipe. The remediation stays conditional — it distinguishes a tenancy collision with vLLM's fixed ~90% VRAM reservation from a model that genuinely does not fit — rather than prescribing a lower--gpu-memory-utilizationunconditionally.vllm/gpu_memory_utilization) are scored — never the whole pasted log — so an incidentalvllmmention cannot misattribute another framework's OOM. The anchor is not a boolean gate: because regex matching ignores line boundaries, gating on the anchor and then scoring the whole symptom still counted keyword hits from unanchored lines at full weight (a benigngpu_memory_utilization=0.9config echo plus an unrelatedtorch.OutOfMemoryError: CUDA out of memoryon the next line reached 95, aboveHIGH_CONFIDENCE). The same-line requirement now falls out of the scoring itself: an anchored line with no OOM token contributes nothing. Pinned byonly_the_anchored_lines_are_scored_not_the_whole_paste.gpu_memory_utilizationremains an anchor even though it is also a 20-point scoring entry: with only anchored lines scored, a bare flag echo (a config dump, or a paste of rocm-cli's own low-VRAM hint, which prints--gpu-memory-utilization) is worth 20 — far belowMIN_SCORE_FOR_MATCH— so it can surface as a weak signal but can never carry a verdict, nor lend its anchor to an OOM elsewhere in the paste (rocm_cli_s_own_low_vram_hint_is_never_a_verdict_on_its_own).keyword_score_collapsing_overlaps, which de-duplicates overlapping match spans, so one phrase yields one evidence bullet and one weight (HIP out of memoryis not also counted as the nestedout of memory). The rest of the catalog keeps the default "every matching entry is an independent signal" scoring: several older tables pair a greedy.*pattern with a second, independent keyword that the greedy span swallows, and collapsing there would push real diagnoses belowMIN_SCORE_FOR_MATCH.rocm diagnose --symptom '...'is decoupled from its own coarse detector: it consultsvllm_oom_symptom_is_diagnosableand falls back toVLLM_OOM_CANONICAL_SYMPTOMwhen the user's real line would score sub-threshold, so the command it hands the user always reports a cause. The user's actual failing line stays visible in the human-readable message.Fixand therocm fixcatalog are cross-checked for shared fix ids, so the two surfaces cannot silently drift.has_match(a checker actually clearedMIN_SCORE_FOR_MATCH), not onmatchedbeing non-empty, so a weak sub-threshold signal cannot bury the WSL setup guidance.Documentation:
docs/vllm.md,README.md, andskills/rocm-cli-assistant/SKILL.mdto explain the behavior on shared/busy GPUs, the single-GPU-only telemetry fallback, therocm diagnose --symptompointer, and the recommended OOM workaround.Behavior coverage (e2e) — per AGENTS.md §3, with the lane named for each:
@id:diagnose-vllm-oom-is-conditional(diagnose-20)rocm fix <id>the report names actually runs on the host that produced it@requires-os:linux— runs on every Linux lane including the GitHub-hosted mock lane; WSL2 matches this tag, so the self-hostede2e-wsllane is the one that can observe the platform-gate halfserve-vllm-low-vram-oom-guidance(serve-22)@requires-gpu @requires-engine:vllm @requires-os:linux— vLLM GPU hardware lane onlyserve-absent-gpu-index-rejected(serve-16)--gpu <index>that is not usable is rejected outright, never silently remapped — under either validation authority@requires-gpu @requires-os:linux— GPU hardware lanes onlyWhy the two GPU-gated ones cannot run on the mock lane: on a no-GPU host the GPU-required pre-flight refuses with "no usable AMD GPU" before an index is ever validated or a plan is ever built, so neither the index-specific rejection nor the pre-launch VRAM note is observable there.
serve-22additionally injects its near-full reading through thee2e-test-hooks-gatedROCM_E2E_FORCE_LOW_VRAM=<ordinal>seam, because the GPU lane's real cards are comfortably free; the engine still launches against the real device. The low-VRAM seam is unit-tested on every lane byforced_low_vram_hook_trips_the_serve_plan_warning.A coverage gap this branch had stated is now closed by #267. The masked sub-case of
--gpuvalidation (a visibility mask leaving a non-contiguous usable set) had no scenario of its own here, because it was reachable only where amd-smi is absent. #267 revalidates against the visible set unconditionally and shipsserve-19(serve-masked-gpu-index-rejected) andserve-20(serve-rocr-reindexed-gpu-index-rejected) for it, so the gap and theserve-16comment that recorded it are both gone.serve-22's twoThensteps divide the work deliberately: the warning step is the scenario's premise (the low-VRAM warning predates this change) and pins only that the warning names the GPU the serve was pinned to; the memory-knob step is the verdict and requires the note's own reservation rationale, because matching the bare flag name would also be satisfied by a recipe echo of a user-supplied--gpu-memory-utilization. Both literals are additionally pinned in the always-run unit testserve_notes_pair_low_vram_with_the_vllm_utilization_hint, so a rewording fails on every lane rather than silently on the one lane that can observe the scenario.Review follow-ups (2026-09-11).
fix-16-vllm-oomnow applies on WSL2.check_16_vllm_oomis registered for["linux", "wsl"], but the recipe wasLINUX_ONLY, androcm fixre-gates a fix-id on the recipe's list against the running OS — where WSL2 is its own family. Sorocm diagnoseprintedapply with: rocm fix fix-16-vllm-oomand that command then printed the plan and exited 3 with "This fix only applies on: linux. Running OS is: wsl.", leaving the supported way to serve from a Windows host with a diagnosis and no fix path. Nothing about the remediation is bare-metal — it is vLLM CLI flags. The invariant is now asserted for the whole catalog byevery_checker_platform_is_covered_by_its_recipe, which requires every platform family a checker answers on to be one its recipe will act on (containment, not equality:rocm fix <id>is reachable without a diagnosis). It surfaced no other mismatch across the remaining 24 entries (25 in the catalog, recounted after the merge).--gpu-memory-utilizationhint said0.1while every command said0.5.VLLM_GPU_MEMORY_UTILIZATION_HINTis printed verbatim by the serve low-VRAM note and the vLLM post-failure hint, and interpolated into thefix-16-vllm-oomdiagnosis summary — so a singlerocm diagnoseprinted "e.g. 0.1 for a small model" directly aboverocm serve <model> --gpu-memory-utilization 0.5. The const is now0.5, andthe_utilization_hint_example_matches_the_recipe_commandpins the hint's worked value to the value the recipe commands. Neither existing guard covered it: the diagnose/fix cross-check exempts the summary as prose framing, and a line-based grep misses the const because the flag and the value sit on different continuation lines.Review follow-ups (2026-09-14).
rocm diagnose --symptom '...'command the OOM hint prints is no longer built from unescaped subprocess output. The vLLM engine routed the user's actual failing line — taken verbatim from the engine's own startup-log tail — into a single-quoted shell argument in a message that invites the user to paste it. A single apostrophe, routine in Python error text (can't allocate,model 'foo'), closed the quote, and a line only has to mention running out of memory to be selected; the neighbouring human-readable echo passed the same bytes through including ANSI. A line that cannot be rendered as one intact single-quoted word is now treated as unusable and takes theVLLM_OOM_CANONICAL_SYMPTOMfallback that already existed for exactly that case, rather than being escaped: escaping keeps the exact bytes but yields a command a reader cannot check by eye, and a wrong escape is runnable and misleading instead of obviously broken, while control bytes would still reach the terminal. The user's own line stays visible in the human-readable sentence (and in the log tail printed with it), now stripped of ANSI sequences and control bytes. Pinned byan_apostrophe_in_the_failing_line_cannot_break_out_of_the_printed_commandandcontrol_bytes_from_the_log_are_stripped_from_the_echoed_line, both of which fail on the previous code.DiagnoseReport::out_of_scope's doc comment matched the code again. It had been changed to assert thatmatchedmay hold sub-threshold entries alongside the field and that the field is set when nothing clearsMIN_SCORE_FOR_MATCH. Neither is true:diagnoseclearsmatchedwheneverout_of_scopeis set, and the field is decided purely bycatalog_covers— platform coverage, never a score. The(e.g. WSL2)example can no longer occur at all now that WSL2 is a covered platform. Since this is the serialized JSON contract, a consumer trusting it would wait for rows that can never arrive.an_out_of_scope_report_never_carries_matched_entriesnow pins the relationship across all four platform families.Coverage note for the first item. The engine's post-failure OOM hint is only emitted when a real vLLM server process fails to start on a GPU host with an out-of-memory error, which no CI lane can produce (the GPU lanes' cards are free, and
serve-22forces only a low-VRAM reading, not a failed launch). The behaviour is therefore covered by the two unit tests above, which run on every lane, and the gap is stated here per AGENTS.md §3 rather than papered over with a scenario that cannot run.diagnose-20continues to cover therocm diagnoseside, which is unchanged.These improvements make GPU selection more robust in diverse environments and provide clear, actionable guidance to users encountering memory issues with vLLM.
Merge with
main+ a blocking fix (2026-09-15).mainmoved under this branch (#331, #327, #267), leaving six conflict hunks — five inapps/rocm/src/main.rs, one inmodel_serving.feature. Merged (never rebased).--gpuvalidation. This branch'sresolve_pinned_gpu_index/validate_pinned_gpu_index_againstare dropped in favour of fix(serve): close serve races and translate GPU selection through visibility masks (EAI-7194) #267'svalidate_pinned_gpu_index, which consults the visibility-resolved set and words its refusal differently depending on whether a mask is actually set. Keeping both would have been two validators for one question.effective_gpu_countsurvives: it is what lets--gpu autorank devices when amd-smi is absent but the DRM sysfs fallback produced rows.select_auto_gpu_indexis a genuine union. Candidate ordinals come from the actual VRAM rows (this branch — a mask makes reported ordinals non-contiguous), and are then narrowed to the visible set (fix(serve): close serve races and translate GPU selection through visibility masks (EAI-7194) #267 — never target a masked-out device). Neither side alone is correct after the merge.mainpublished serve-19/20/21 and they are cross-referenced fromsrc/expectation.rsand from each other, so they keep their numbers; this branch's low-VRAM OOM scenario shifts to serve-22. Its@id:is unchanged, soexpectations.tomlis unaffected.validate_pinned_gpu_indextail that kept this branch'susablebail inside fix(serve): close serve races and translate GPU selection through visibility masks (EAI-7194) #267's function body; avec![all[0]]fallback whoseallbinding lived on the other side of the hunk; and threeselect_auto_gpu_indexcall sites still on the old arity. A fourth was marker-free and compiled: aserve-16comment naming a unit test the merge had deleted.Blocking fix: the all-busy auto-selection fallback returned a GPU that was never reported. Once
--gpu autocandidates come from the VRAM rows, the terminal "every candidate is busy, pick one anyway" fallback could no longer return a hardcoded0. On the base branch that was safe by construction (candidates were0..count, so ordinal 0 was always real). With rows reported at[2, 3]and both pinned by a managed service the candidate list empties, all three passes fall through, and the function returned[0]— an ordinal absent from the reported set, exported verbatim asHIP_VISIBLE_DEVICES. Under the strict GPU-required / no-silent-fallback policy that is a silently wrong pin, not a fail-fast. The fallback now returns the lowest ordinal that was actually reported, and the adjacent comment (which claimed "Every detected GPU is pinned... We still return GPU 0") is corrected. This is a fabrication removed, not a policy change: "all busy, pick one anyway" still holds, androcm serverefuses in exactly the same situations as before.auto_selection_all_reported_busy_falls_back_to_a_reported_ordinalcovers it. The pre-existingauto_selection_ranks_the_actual_reported_row_indicesclaimed pass 3 "never returns a fabricated index 0" but only exercised the partially busy case, which never reaches the fallback. Falsified: reverting the fallback tovec![0]fails the new test withleft: [0], right: [2]; restoring it passes. Equality assertion, not fragment absence.Why no new scenario for the fallback (AGENTS.md §3). Reaching it needs every GPU the host reports to be simultaneously pinned by a live rocm-cli managed service, on a host whose reported ordinals are sparse. No lane can arrange that without a seam that would make the scenario assert against the seam rather than the product, and a
serveunder those conditions is a deliberate over-commit that the low-VRAM warning already reports. The selection function is pure and injectable, so the unit test covers the decision exactly; what is not covered live is the sparse-ordinal host itself, which is the same hardware gap #267 records onserve-20.Review follow-ups (2026-09-17).
rendered_lines's never-merge guarantee is qualified where a consumer reads it. The doc stated it as an unqualified absolute, while the one counterexample was written down in a body comment inside the privatenext_token— so it reached no generated documentation — and the public doc pointed at it withSee [next_token], a link to a private item that rustdoc will not resolve. The exception now has a single authoritative statement in the module documentation, which is public and rendered: a line break inside a well-formed string-argument body (OSC,DCS,SOS,PM,APC) is consumed with that body, worked example included.rendered_linescarries the qualifier inline so the only absolute a consumer can read is true, and links to the module docs instead. The behaviour is unchanged; following the grammar rather than guessing where an unterminated body ends is still the right call.a_line_break_inside_a_well_formed_string_body_is_consumed_with_itpins the worked example, plus the two shapes that bound the hole (the same break outside a string body still splits; an unterminated body still stops at the followingESC).cargo doc -p rocm-core --no-depswarned on the dangling link and goes from 6 warnings to 3 with this change — the remaining three are pre-existing onmain.Docs scope for this PR.
docs/testing.mdanddocs/manual-testing.mdare deliberately untouched. Neither file itemises fix IDs or diagnose signatures today —docs/testing.mddocuments how to run the suites anddocs/manual-testing.mdwalks the install/serve/dash flows — so the three user-observable additions here (thefix-16-vllm-oomcatalog entry, the OOM diagnosis, and the pre-launch low-VRAM serve warning) have no row to update in either. The user-facing surfaces that do describe them —README.md,docs/vllm.md, andskills/rocm-cli-assistant/SKILL.md— are updated in this PR, and each behaviour is covered by the@id:-tagged scenarios tabulated above. (Worth noting precisely: this branch'sAGENTS.mdcarries no unconditional same-change docs rule. Its only such requirement is the scenario rule — "User-observable behavior needs a scenario, not only a unit test" — which this PR discharges in the table above.main's copy has since grown a stronger clause, "when a command's flags, defaults, arguments, or observable behavior change, update README.md, its --help/doc comment, docs/testing.md, and docs/manual-testing.md in the same change"; that text is not in this branch'sAGENTS.md, and it will apply on the next merge frommain, at which point the reasoning above is the answer: no flag, default or argument changed, and neither file states a behaviour claim this PR falsifies.)On the three
effective_gpu_countassertions (apps/rocm/src/main.rs): they are characterisation tests. They are revert-sensitive to thefilter(|&count| count > 0)guard, but that guard predates this branch and this PR's own change to the function is doc-only — so they close a pre-existing coverage gap on a function the merge kept, rather than pinning behaviour this PR introduces.