Repository navigation
Conversation
|
A few observations, mostly around the signature list and the stacking:
|
ee202f0 to
a97a39f
Compare
738cdf4 to
b9316f3
Compare
|
Thanks for the detailed review — addressed all of these in b9316f3 (rebased onto the current
All changes covered by unit tests; |
a910068 to
83dc552
Compare
|
Round 2. Items 1, 4 and 5 are genuinely fixed — I checked the mechanisms, not just the claims. The dedup works ( Blocking:
On #290: the claim that nothing defines Smaller things:
Checked and clean: log lifecycle ( Centralising the signature into |
|
Thanks for the Round-2 review — pushed Note could fire for a healthy Dead signature / narrowing regression. Inert
The Positive e2e scenario — not added, and here's why. The negative case (reuse must not blame this invocation) is the one the harness can drive without a GPU, because it reuses a pre-planted live service rather than launching. A positive end-to-end (this invocation launches, the launch OOMs, the note renders) needs the mock engine to emit a real allocator OOM signature in a log it owns ( Smaller items: added the missing rocm-core test for Coordination with #290: once both land there are two OOM detectors ( |
|
@volen-silo — correction on the scenario-15 lane, because my Round-2 reply was wrong and CI called it out.
Proper fix ( Scenario 15 now runs on the blocking lane with the default device — Follow-up Note: the current red E2E tests (GPU) lane is unrelated — 9 scenarios fail identically with vLLM Your other two Round-2 items (restore the deleted |
|
Round 3. I re-verified every Round-2 item against the code at I also checked the new gate the other way, for over-narrowing: BlockingThe positive e2e scenario is still missing, and the infeasibility argument only covers half of it. You are right about the post-failure path. The pre-launch half is coverable today, with no new harness capability:
That is a fixture, not a new primitive. Nits (non-blocking)
Everything else I checked came back clean: the shared-constant centralisation means the engine hint and the CLI note cannot drift, |
|
Thanks for the Round-3 review — and for re-verifying the Round-2 items against the code rather than the claims. Blocking item — the positive scenario. You're right on both halves, so I've split them explicitly in the PR body (new "e2e coverage and the scenario gap" section), per §3's escape hatch and §2's body-vs-comment distinction:
Nits. Noted and deferred as non-blocking: the Two red checks (self-hosted GPU, readthedocs) fail identically on unrelated recently-merged PRs; the blocking GitHub-hosted mock lane runs and passes |
|
I read through this PR's diff (base The change is well-scoped and I didn't find anything blocking:
This looks safe to approve after the usual final checks — passing CI and a maintainer's own pass — since this is a diff-only read and doesn't replace running the test suite or verifying against real hardware. |
395ed9e to
e8e19e8
Compare
|
Rebased onto the updated One semantic merge decision worth flagging: the base branch had independently evolved The rest of the series replayed unchanged; |
|
Re-reviewed the update pushed since the last review (commit What's new since the last pass: it adds the positive e2e scenario the earlier review rounds flagged as missing (Scenario 16, I checked:
No findings from this pass. This looks safe to approve after the usual final checks — passing CI and a maintainer's own pass — since this is a diff-only read and doesn't replace running the test suite or verifying on real hardware. |
|
Pushed `5bd1bfc`: gated the `serve-oom-launch-memory-guidance` scenario on a new `@requires-oom-fault-injection` capability so the WSL2 self-hosted E2E lane skips it instead of failing. Why the lane failed: that scenario drives the test-only `e2e-oom-fault-injection` hook to fabricate a GPU-less OOM launch. The hook is compiled out of the shipping release binary. The self-hosted lanes test a prebuilt `ROCM_CLI_BINARY`, so The gate: `xtask e2e` now sets `ROCM_E2E_OOM_FAULT_INJECTION=1` only when it built the binary with the feature; the harness probe reads it into `HostCapability.oom_fault_injection`, and `resolve()` skips the tagged scenario when the hook is absent. This preserves the mock-lane coverage that gates every PR and cleanly skips the feature-less self-hosted lanes. Added unit tests for the resolver gate (both directions) and the xtask env signal. |
|
Re-reviewed the update pushed since the last review (commit What's new: the author gated the positive The fix:
I also re-read the full diff against base again (not just this delta) given how long this PR has been through review rounds: the OOM signature matcher, the This looks safe to approve after the usual final checks — passing CI (the self-hosted GPU/WSL lanes plus the mock lane) and a maintainer's own pass — since this remains a diff-only read. |
r0x0r
left a comment
There was a problem hiding this comment.
The attribution reasoning here is the strongest part: gating the post-failure note on already_running == false and status == "starting" and the serve's own log_path is exactly the right defense against blaming this invocation for another process's OOM, and serve_failed_to_become_ready correctly keeps a healthy still-loading running service out of the failure path. The const false fault-injection seam compiling out of release builds is also clean.
One substantive question on precision: vllm_log_shows_oom is a case-insensitive substring scan for out of memory / outofmemory over the last 80 log lines, and the note fires whenever that substring appears in the tail of a failed-to-become-ready serve. It isn't anchored to the terminal/fatal error line. So a serve that died for an unrelated reason but whose tail happens to mention "out of memory" somewhere (a benign warning, a retried allocation that later succeeded, a model/path name containing the token) would be misreported as an OOM failure. The already_running/starting/own-log triad prevents cross-process misattribution, but not "wrong cause, same process." Is anchoring to the last matching line (the way oom_utilization_hint already picks the failing line) worth it here, or is substring-in-tail deemed precise enough in practice?
Smaller related point: the tail is read at 80 lines. If an OOM traceback is followed by more than 80 lines of shutdown/teardown noise, the signature scrolls out of the window and the note is silently withheld. Worth confirming 80 matches the engine's own DEFAULT_LOG_TAIL_LINES budget so the two surfaces don't disagree on what counts as "the tail."
|
Pushed Tail budget: the serve summary read a bare Anchoring vs substring-in-tail: deliberately keeping the whole-tail scan.
Meanwhile |
rominf
left a comment
There was a problem hiding this comment.
Reviewed as the delta against gpu-out-of-memory (#251), so #251's own work isn't re-litigated here — I left that separately on #251.
The note logic is good and the fault-injection seam is a nice piece of design: feature-gated with a #[cfg(not(feature))] const fn -> false fallback, armed per-invocation by an env var, reported to the harness as a capability so the scenario skips rather than silently passes on a prebuilt binary. oom_memory_note is properly unit-tested and the reuse-vs-launch distinction it draws is exactly right.
Three things I'd want fixed before merge.
The reorder is the significant one. To decide reuse_existing, the new block runs load_managed_services (side-effecting — liveness probes, record writes), a full ResolveModel round-trip, and ensure_self_managed_engine_ready before the no-usable-GPU bail. The comment right below it still says "BEFORE preparing or launching any engine (no wasted engine download …)", and that's no longer true. It also drags resolve_gpu_indices from before the no-runtime bail to after it — and the GPU lane on this head is failing serve-absent-gpu-index-rejected with exactly the message that predicts:
Step panicked. Captured output: expected the absent GPU index to be reported unavailable, got:
Error: device_policy: gpu_required; no active ROCm runtime is configured; ...
I checked #251's GPU lane as the control — same runner, same base — and it doesn't fail that scenario (its failures are all serve-timeout/resolved model shaped). So this looks caused by the reorder rather than inherited.
Second: xtask/src/e2e.rs replaces the single --features value with rocm/e2e-oom-fault-injection, but main now passes rocm/e2e-test-hooks there — added by #249, which isn't an ancestor of this branch. On rebase those compete for one slot, and workflow_contract.rs asserts the literal --features rocm/e2e-test-hooks string, so a naive resolution either breaks the contract test or silently compiles out lemonade's failure seams.
Third, smaller: the negative OOM assertion uses a raw contains while both positive ones normalize whitespace — by the PR's own stated reason (80-col soft wrap), so Scenario 15 can pass without testing anything.
On process — this is stacked on #251's branch but isn't draft and has no Depends on #251 in the body. AGENTS.md §11 asks for both, and it matters here: about half of what a reviewer sees belongs to the other PR, and #284's delta relocates code #251 introduces. Worth considering whether the rocm-core placement should just land in #251 directly.
Leak scan is clean and the EAI-8059 reference is fine per AGENTS.md §2.
| .is_some(); | ||
| resolved_model = Some(probe); | ||
| } | ||
| // Fail fast under a GPU-required policy when the host has no usable AMD GPU, |
There was a problem hiding this comment.
This comment is now false, and the work it promises won't happen is exactly what runs above it.
"BEFORE preparing or launching any engine (no wasted engine download, and an actionable message instead of a late engine crash)" — but the reuse block at :4877-4897 already ran:
any_live_managed_service_for_engine→load_managed_services, which is side-effecting:refresh_from_engine_state,refresh_managed_service_runtime_liveness(HTTP readiness/inference probes with a 750ms timeout each), andrecord.write();- a full
ResolveModelengine round-trip; ensure_self_managed_engine_readyfor self-managed engines.
That last one is the expensive one. On a GPU-less host with lemonade selected, the user now sees "Preparing lemonade for GPU serving…" and potentially a multi-GiB install, and then gets told there's no usable AMD GPU. AGENTS.md §6's "preserve strict GPU-required behavior" is about the outcome, but the fail-fast part is what this comment is claiming and it's what's lost.
The !reuse_existing exemption itself is correct — nothing is launched when reusing, so the gate shouldn't fire. It's the cost of computing reuse_existing that's the problem.
A cheap ordering fix: gate the whole reuse block on usable_amd_gpu_indices() being non-empty (or None), so on a GPU-less host you fall straight through to the bail. Reuse can't be the right answer on a host with no GPU under gpu_required anyway. Alternatively determine reuse from a non-mutating read of the records and defer the ResolveModel/prepare work until after the gate.
Whichever way it goes, the comment needs to match — if the contract genuinely changed, say what the new one is.
There was a problem hiding this comment.
Fixed in 4a108b9 — you were right that the comment promised something the block above it had already spent.
The reuse pre-gate keyed on the engine alone, so a live service for a model this invocation can never reuse was enough to pull the ResolveModel round-trip and — the expensive one you named — ensure_self_managed_engine_ready, complete with Preparing lemonade for GPU serving... and a possible multi-GiB install, in front of the no-usable-GPU bail. It now keys on the model too (any_live_managed_service_for_model), so on a GPU-less host with a live lemonade service for some other model the block is skipped entirely and the bail is once again the first thing that runs.
On the two options you offered: gating the whole reuse block on usable_amd_gpu_indices() being non-empty would have been cheaper, but it breaks @id:serve-oom-memory-guidance (serve-20), which reaches the reuse short-circuit precisely on a GPU-less host. So I took the second one — decide reuse from the records and let only a plausible match pay for the engine round-trip. Matching uses service_model_names_match, the same lenient relation the service-listing surfaces already use, so a short-vs-canonical spelling still reaches the probe; the probe then still decides reuse authoritatively on the canonical id via existing_live_managed_service. Reuse detection is not narrowed in practice.
The residual, stated plainly: when a live service does match this engine and model, ensure_self_managed_engine_ready still runs before the bail. That is the case where this invocation is about to reuse and legitimately skip the bail, so the work is not wasted — and the comment now says exactly that instead of claiming an absolute.
Regression tests (apps/rocm/src/main.rs): reuse_pregate_skips_engine_work_for_an_unrelated_live_model — this is the one that fails before the fix, since the old function body had no model term at all and returned true for any live service of the engine; plus reuse_pregate_admits_the_same_model_including_a_short_spelling, reuse_pregate_still_keys_on_the_engine, and reuse_pregate_admits_the_model_id_the_oom_reuse_scenario_plants, which pins the literal id serve-20 plants so a matcher change cannot silently turn that scenario into a launch attempt on the one lane it runs on.
Verified on a Linux MI300X box: full workspace cargo test + clippy -D warnings + fmt --check, and cargo xtask e2e with serve-20 and serve-21 both passing. I could not run the self-hosted E2E tests (GPU) or E2E tests (Strix Halo, Ubuntu) lanes — that hardware is not available to me, so CI is the only authority for those two.
| env_id.as_deref(), | ||
| ); | ||
| let resolved_selection = validate_engine_selection_runtime(&paths, resolved_selection)?; | ||
| if !matches!(device_policy, DevicePolicy::CpuOnly) |
There was a problem hiding this comment.
resolve_gpu_indices moved from before the no-runtime bail to after it, and the GPU lane on this head is failing on exactly that.
On the base (:4873-4878 there), resolve_gpu_indices — the source of --gpu index N is out of range — ran ahead of the no active ROCm runtime is configured bail. Here it's at :4934, after. So a bad --gpu value now surfaces as an environment complaint instead of an argument complaint.
From run 33489993381 on c337c7c4:
Step panicked. Captured output: expected the absent GPU index to be reported unavailable, got:
Error: device_policy: gpu_required; no active ROCm runtime is configured; run `rocm runtimes list` ...
I pulled #251's GPU lane (run 33166192967, job 98832217235) as the control, since that lane has other unrelated problems and I didn't want to over-attribute. It does not fail serve-absent-gpu-index-rejected — its failures are all serve-timeout / missing-resolved model shaped. So the regression appears with this delta.
User-facing, argument validation before environment validation is the right order regardless: --gpu 99 is wrong no matter what runtime is active, and telling the user to go activate a runtime sends them down the wrong path.
The reorder that reuse needs shouldn't have to drag resolve_gpu_indices with it — gpu_vram_usage()/resolve_gpu_indices don't depend on resolved_model, so they can go back above the runtime bail.
There was a problem hiding this comment.
Fixed in 7c09129 (already on the branch when you filed this); re-verified against the current head rather than the claim.
resolve_gpu_indices now runs at apps/rocm/src/main.rs:5209, immediately after the no-usable-GPU pre-flight and before the no-runtime bail at :5211. So --gpu 99 is an out-of-range argument error again regardless of runtime state, and a GPU-less host still refuses with "no usable AMD GPU" first. Your point that argument validation belongs ahead of environment validation is the ordering that shipped.
Lane evidence, since this was a GPU-lane regression: serve-16 / @id:serve-absent-gpu-index-rejected ran and passed on E2E tests (GPU) at 83ecfaad, and it also passes in my own cargo xtask e2e run on a Linux MI300X box at the new head 4a108b9. It is not among that lane's current failures.
Thanks for pulling #251's run as a control before attributing it — that was the right call and it is what made the delta unambiguous.
| if binaries.build_release { | ||
| let status = Command::new(&cargo) | ||
| .args(["build", "--release", "-p", "rocm", "-p", "rocmd"]) | ||
| // `--features rocm/e2e-oom-fault-injection` compiles in the test-only |
There was a problem hiding this comment.
This replaces a --features value that main added, and there's a contract test asserting the old string.
On origin/main this call site is --features rocm/e2e-test-hooks (from #249, not an ancestor of this branch). Here it becomes --features rocm/e2e-oom-fault-injection, and apps/rocm/Cargo.toml in this diff declares only e2e-oom-fault-injection. git merge-tree origin/main pr/284 conflicts in apps/rocm/Cargo.toml, apps/rocm/src/main.rs, model_serving.feature, serving_steps.rs, and this file — and this one is literally the two feature strings competing for one slot.
The part that makes it more than a routine conflict: xtask/src/workflow_contract.rs:255-274 asserts every prebuilt E2E lane block contains the exact string
cargo build --release -p rocm -p rocmd --features rocm/e2e-test-hooks
So a resolution that keeps only the new feature fails that test, and one that keeps only the old feature silently disables the new scenario (it degrades to a skip via the capability gate — a green run that tested nothing). Both failure modes are quiet in different ways.
On rebase this wants: a combined list (rocm/e2e-test-hooks,rocm/e2e-oom-fault-injection), e2e-test-hooks restored in apps/rocm/Cargo.toml, and the workflow_contract.rs expectation updated to match. Worth flagging in the PR text too, since whoever resolves the conflict may not know the contract test exists.
The configure_harness_env half is right — clearing the env var for a prebuilt binary rather than letting an inherited value through is the correct call.
There was a problem hiding this comment.
Addressed across 7c09129 and 4a108b9.
The overwrite is gone: xtask/src/e2e.rs:125 now passes a single space-separated value, "rocm/e2e-test-hooks rocm/e2e-oom-fault-injection", so e2e-test-hooks (#249) survives alongside the new hook rather than losing the one --features slot to it. apps/rocm/Cargo.toml declares both.
On workflow_contract.rs:255-274: the assertion is a contains on cargo build --release -p rocm -p rocmd --features rocm/e2e-test-hooks, and every prebuilt lane still spells exactly that, so it passes unchanged. But your framing exposed that its doc and failure message had become false — they claimed the asserted string matched what cargo xtask e2e builds for itself, which is now a superset. 4a108b9 rewrites both to describe it as the shared hook floor, and records why the fault-injection hook is deliberately absent from the prebuilt lanes: those scenarios carry @requires-oom-fault-injection, so a missing hook is a reported skip, not the silent green you were worried about. That closes the third failure mode — a stale comment nobody re-reads on rebase.
Your two other resolution notes are now moot for this branch: this PR targets gpu-out-of-memory, not main, and the base already carries the serve-NN scenario naming, so merge-tree no longer conflicts in model_serving.feature. I have put the combined-feature-list requirement in the PR body anyway, since whoever eventually rebases this onto main will hit exactly the collision you describe and should not have to rediscover the contract test.
Agreed on configure_harness_env — it env_removes on the prebuilt path rather than letting an ambient 1 through, and that is unchanged here.
| .expect("no interactive serve summary") | ||
| .screen_text(); | ||
| assert!( | ||
| !screen.contains("ran out of GPU memory"), |
There was a problem hiding this comment.
This negative assertion is whitespace-sensitive while both positive ones aren't, so Scenario 15 can pass vacuously.
assert_oom_launch_memory_guidance (:954) uses screen_without_whitespace(&screen).contains("ranoutofGPUmemory"), and the comment above it explains why: "The note is a long line the 80-column PTY wraps across grid rows … otherwise a soft wrap between 'GPU' and 'memory' would break a literal contains and mask a rendered note."
That reasoning applies identically here, in the direction that matters more. If the note were wrongly emitted on the reuse path and happened to wrap — which the positive step says is the normal case for this string — the raw contains wouldn't see it and the scenario would pass. So the regression guard is disarmed precisely when the note renders the way the code actually renders it.
screen_without_whitespace(&screen).contains("ranoutofGPUmemory") with the ! is the one-line fix, and it makes the pair symmetric.
The scenario itself is a good one — "don't blame this invocation for another process's OOM" is a real distinction and worth pinning.
There was a problem hiding this comment.
Fixed in 7c09129; verified against the current code, not the claim.
tests/e2e-cucumber/tests/e2e/serving_steps.rs:1218 is now:
!screen_without_whitespace(&screen).contains("ranoutofGPUmemory"),exactly the one-line fix you proposed, so the negative and the positive (:1272) are symmetric and the guard survives the soft wrap the positive step documents as the normal rendering. The comment above it now spells out why the whitespace collapse is load-bearing in the negative direction specifically — that a literal contains would have passed even if the note were wrongly emitted.
The scenario is now serve-20 (renumbered when the base landed serve-19); the @id: is unchanged. It ran and passed in cargo xtask e2e on a Linux MI300X box at 4a108b9, and it also runs on the blocking GitHub-hosted mock lane.
Thanks — that one really would have been a silently disarmed regression guard.
| /// `torch.cuda.OutOfMemoryError`, `hipErrorOutOfMemory` — while `out of memory` | ||
| /// catches the spaced runtime phrasing (`HIP out of memory`, `CUDA out of | ||
| /// memory`). An earlier `hip out of memory` entry was dead code, since any log | ||
| /// containing it already contains `out of memory`. |
There was a problem hiding this comment.
Two coupled issues with this detector and the note it drives.
The threshold contradicts #251's. This matches a bare out of memory substring anywhere in the tail. In the same stack, check_16_vllm_oom deliberately scores a bare "out of memory" at 25 — below MIN_SCORE_FOR_MATCH — with a comment explaining that it "is deliberately sub-threshold so this only claims the failure mode when the vLLM/HIP shape of the error is present," and gates the whole table behind VLLM_ANCHOR_PATTERN. Same failure class, same repo, two thresholds, and the looser one is the one driving user-facing serve output.
A managed-service log tail containing out of memory from any source — a kernel OOM-killer line, a dependency's log, an unrelated subprocess — produces the vLLM OOM note. The doc comment here is careful about excluding vLLM's generic wrapper line, which suggests the false-positive risk was on your mind; the bare-phrase case is the bigger one.
The guidance is wrong for the model-too-large case. The note only ever suggests --gpu-memory-utilization and --gpu <index>. Both help when the GPU is shared or busy. Neither helps when the model simply doesn't fit — and lowering the reservation there actively makes it worse, trading an earlier OOM for a later one.
#251 already gets this right: check_16_vllm_oom's summary says "Only lower --gpu-memory-utilization when the GPU is shared or already busy; on a GPU dedicated to this server it does not create room a too-large model needs." That caveat doesn't survive into the note this PR shows users, and --max-model-len isn't mentioned anywhere in the new guidance.
I'd reuse #251's anchored matcher rather than maintain a second looser one, and either carry the shared-vs-dedicated caveat into the note or have it point at rocm diagnose --symptom 'vllm: …', which already does the nuanced version. Pointing at diagnose is the lighter option, and it exercises the handoff the two PRs are building.
Small related note on the wording: the hint says --gpu-memory-utilization <0-1>, but parse_gpu_memory_utilization enforces (0, 1] — 0 is rejected.
There was a problem hiding this comment.
All three points fixed in 83ecfaa. Taking them in your order:
Threshold. You were right that maintaining a second, looser matcher for the same failure class was the bug, and that the looser one driving user-facing output made it worse. rocm_core::vllm_log_shows_oom no longer does a bare substring scan — it classifies each tail line with the same rule as check_16_vllm_oom, scoring the line as vllm: <line> against the shared MIN_SCORE_FOR_MATCH threshold (crates/rocm-core/src/lib.rs:7424). The tail is vLLM's own process output, so the checker's required VLLM_ANCHOR_PATTERN is supplied by context rather than dropped. Sub-threshold lines that used to fire now correctly do not: a kernel OOM-killer line, a bare out of memory, a bare HIP error: out of memory, a bare torch.OutOfMemoryError with no allocator corroboration. Genuine allocator failures still clear it. One helper feeds both the engine hint and the CLI note, so there is no second matcher left to drift.
Model-too-large guidance. Also fixed, and I took your lighter option and carried the caveat, since the note is what the user actually reads. oom_memory_note (apps/rocm/src/serve_summary.rs:219) now says that if the model simply does not fit, lowering the reservation will not help, and to serve a smaller or quantized model instead — then routes to rocm diagnose --symptom '<their actual failing line>' for the case-appropriate fix, which exercises the handoff you point out the two changes are building. --max-model-len is deliberately left to diagnose, which does the nuanced version, rather than duplicated into a fixed string here.
Wording. <0-1> is gone. The shared hint now reads "a fraction greater than 0 and at most 1", matching what parse_gpu_memory_utilization actually enforces on (0, 1].
Verified at 4a108b9 on a Linux MI300X box: full-workspace cargo test + clippy -D warnings, and cargo xtask e2e with diagnose-13 (@id:diagnose-vllm-oom-is-conditional), serve-20 and serve-21 all passing. I could not run the self-hosted E2E tests (GPU) or E2E tests (Strix Halo, Ubuntu) lanes — no such hardware available to me — so CI remains the only authority on those two.
| let Some(log_path) = log_path else { | ||
| return notes; | ||
| }; | ||
| // Read the same tail budget the engine's own OOM surfaces use |
There was a problem hiding this comment.
This comment claims an invariant that isn't enforced anywhere.
"Read the same tail budget the engine's own OOM surfaces use (rocm_engine_protocol::DEFAULT_LOG_TAIL_LINES) … rather than drifting apart on independent magic literals" — but the vLLM engine's OOM surface reads STARTUP_FAILURE_LOG_TAIL_LINES, a local const … = 80 at engines/vllm/src/lib.rs:34, used at :1817 and :1823. That's precisely the independent magic literal the comment says has been eliminated.
They're both 80 today, so nothing is broken. But the comment (and the commit message) describes an enforced coupling, and a future change to DEFAULT_LOG_TAIL_LINES would silently desynchronize the two surfaces — with the comment still asserting they can't.
Pointing STARTUP_FAILURE_LOG_TAIL_LINES at the protocol constant is a one-liner and makes the claim true. Otherwise the comment should describe the intent rather than a guarantee.
There was a problem hiding this comment.
You were right, and this one was genuinely still open — the earlier response commits did not touch it. Fixed in 4a108b9.
engines/vllm/src/lib.rs:35 was still const STARTUP_FAILURE_LOG_TAIL_LINES: usize = 80;, the exact independent literal the serve-summary comment claimed had been eliminated. It now reads:
const STARTUP_FAILURE_LOG_TAIL_LINES: usize = DEFAULT_LOG_TAIL_LINES;DEFAULT_LOG_TAIL_LINES was already imported in that file, so it is the one-liner you described. I took your first option rather than weakening the comment to intent, because the coupling is the thing worth having: a change to the protocol budget now moves both surfaces, and append_oom_serve_note and oom_utilization_hint cannot silently disagree on what "the tail" of a failed launch means. The const carries a doc comment saying so, so the next person is not tempted to re-inline the literal.
Deliberately scoped to vLLM: engines/lemonade has its own STARTUP_FAILURE_LOG_TAIL_LINES alongside a local DEFAULT_LOG_TAIL_LINES = 200, and it is not part of the OOM surface this PR couples, so I left it alone rather than widen the change.
Verified at 4a108b9: full-workspace cargo test + clippy -D warnings + fmt --check on a Linux MI300X box, plus the vLLM engine's own tail-budget tests. Not verified on E2E tests (GPU) or E2E tests (Strix Halo, Ubuntu) — that hardware is unavailable to me.
| /// Environment variable `xtask e2e` sets to `1` when it compiled the binary | ||
| /// under test with the `rocm/e2e-oom-fault-injection` feature. Kept in sync with | ||
| /// the same name in `xtask::e2e`. | ||
| pub const OOM_FAULT_INJECTION_ENV: &str = "ROCM_E2E_OOM_FAULT_INJECTION"; |
There was a problem hiding this comment.
This constant is declared twice — here and in xtask/src/e2e.rs:24 — held together only by a "kept in sync" comment on each side.
A typo in either doesn't fail to compile and doesn't fail a test. xtask sets one name, the harness reads another, oom_fault_injection is false, @requires-oom-fault-injection maps to Expectation::Skip, and the scenario silently stops running. Green suite, zero coverage — the same failure mode the comment on probe_host_capability is careful about elsewhere.
Since the harness crate is already a build dependency relationship away, hoisting the constant into a shared crate would be cleanest. Failing that, a test in either crate asserting the two literals are equal costs three lines and closes it.
Minor, but this scenario is the only thing exercising the fault-injection path, so a silent skip is expensive.
There was a problem hiding this comment.
Fixed in 83ecfaa, and I took the cleaner of your two options.
ROCM_E2E_OOM_FAULT_INJECTION is now declared exactly once, in the e2e-report crate that both xtask and e2e-cucumber already depend on:
crates/e2e-report/src/lib.rs:30—pub const OOM_FAULT_INJECTION_ENV: &str = "ROCM_E2E_OOM_FAULT_INJECTION";xtask/src/e2e.rs:30—use e2e_report::OOM_FAULT_INJECTION_ENV;tests/e2e-cucumber/src/capability.rs:134—pub use e2e_report::OOM_FAULT_INJECTION_ENV;
so the two former copies are re-exports of one source and a typo is now a compile error rather than a silent oom_fault_injection = false. Since it is a single constant, the equality test you offered as the fallback is no longer needed — there is nothing left to hold in sync.
Your framing of the cost is what decided it: this is the only thing exercising the fault-injection path, so a @requires-oom-fault-injection scenario degrading to Expectation::Skip would have been a green suite with zero coverage of the PR's headline behavior. The resolver still has tests for both directions (hook present → run, hook absent → skip), and serve-21 ran and passed under cargo xtask e2e on a Linux box at 4a108b9, so the signal is reaching the harness for real rather than only in principle.
| # index-specific rejection can only be observed where a real device is present. | ||
| @id:serve-absent-gpu-index-rejected @requires-gpu @requires-os:linux | ||
| Scenario: 13 - Serving pinned to a GPU that does not exist is refused | ||
| When the user serves a model pinned to a GPU index that does not exist |
There was a problem hiding this comment.
These reuse scenario numbers that already exist on main.
origin/main's model_serving.feature has a Scenario: 15 at line 143 ("Serving with a temperature below zero is refused…"), another Scenario: 15 at 173 ("Selecting both a runtime and an environment at once…" — main already has a duplicate), and a Scenario: 16 at 184. This PR adds a third 15 and a second 16, in different hunks, so git merges them cleanly and nothing complains.
The cost is triage: a lane reports a scenario by name and number, and "Scenario 15 failed" stops identifying anything. Worth renumbering on rebase — or dropping the manual numbers in favor of the @id: tags the repo already uses everywhere, which is what the tooling actually keys on.
Both new scenarios read well otherwise; the positive/negative pair covers the distinction properly.
There was a problem hiding this comment.
Fixed — the collision is gone as of 83ecfaa, though partly by the base moving under us.
origin/main no longer carries bare Scenario: 15 / 16 headings; every scenario in model_serving.feature is now Scenario: serve-NN - ..., running to serve-18 on main. The base branch (#251) adds serve-19, and this PR's two are serve-20 and serve-21, so the numbering is unique and sequential across the file and feature_naming is satisfied. Verified against origin/main and origin/gpu-out-of-memory at the current head rather than from memory.
On dropping the manual numbers in favour of @id:: agreed that @id: is what the tooling keys on, and the two scenarios here carry stable ones (serve-oom-memory-guidance, serve-oom-launch-memory-guidance) that survived both renumberings untouched — I have quoted the @id: rather than the number everywhere in the PR body for exactly the triage reason you give. Removing the numeric prefixes wholesale is a rename across every feature file plus the feature_naming test, which is a bigger and unrelated change than this PR should carry; happy to raise it separately.
Both scenarios ran and passed under cargo xtask e2e on a Linux MI300X box at 4a108b9. Thanks for the note on the pair reading well — the positive/negative split was the thing earlier rounds spent the most time getting right.
e94ca5f to
e1adff1
Compare
c337c7c to
886a1ce
Compare
|
Thanks for the review. Rebased onto the updated #251 head and addressed the points; the branch is now two commits (the squashed feature + a review-response commit) and back in draft while it depends on #251.
Also renumbered the two OOM scenarios to 17/18 to sit alongside the runtime/env and Lemonade-recovery scenarios that landed on the base, and fixed a stale |
fbf5ff6 to
fe41981
Compare
|
🔴 Automated review · pr-review-watcher · c951af5 This automation never files a GitHub approval, so no approving review will SummaryThis PR adds vLLM out-of-memory detection. One shared rule in Round scope. Every commit since the last round ( Reviewed: the PR's own diff
Verified:
Blocking: 0 · Non-blocking: 11. 🚫 Blocking (must fix before merge)None. Non-blocking
|
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # docs/vllm.md
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # crates/rocm-core/src/lib.rs # tests/e2e-cucumber/features/diagnose.feature
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # crates/rocm-core/src/diagnose.rs # crates/rocm-core/src/fix.rs # skills/rocm-doctor/reference.md # tests/e2e-cucumber/features/diagnose.feature
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # crates/rocm-core/src/fix.rs
`check_16_vllm_oom` built its `Fix` with an inline `auto_applicable: false`. `no_checker_hand_sets_auto_applicable` forbids that: only `take_applicability_from_the_catalog` may set the field, because the catalog pass overwrites whatever a checker puts there. The output was right either way -- which is the point. A checker that disagreed with the catalog would be silently masked rather than caught, the way `fix-9-igpu-dgpu` once drifted. The field is now left at `Fix::default()` and filled from the recipe, whose `applies_on: PRINT_ON_LINUX_AND_WSL` already carries `FixClass::PrintOnly`. This was latent rather than new: the guard never ran on the previous head because the suite aborted at an unrelated flaky test first, so the offending line reached `gpu-out-of-memory` unnoticed. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Automated review updated for c951af5 (the head only merged base-branch updates since the last round; no blocking findings, 11 non-blocking). The full report is in the review comment above: #284 (comment) |
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # apps/rocm/src/main.rs # crates/rocm-core/src/fix.rs # crates/rocm-core/src/lib.rs # crates/rocm-core/src/terminal.rs # docs/vllm.md # engines/vllm/src/process.rs # tests/e2e-cucumber/features/diagnose.feature # tests/e2e-cucumber/features/model_serving.feature
… feature-free Two non-blocking review points from EAI-8059, neither changing behaviour. `service_model_names_match` is a bidirectional `contains` shared by four call sites, so the serve reuse pre-gate admits any overlapping name: a live service for `qwen3-8b-instruct` satisfies a query for `qwen`, and on that path the `ResolveModel` round-trip and any self-managed engine install run ahead of the no-usable-GPU bail for a service this invocation cannot reuse. Left lenient deliberately -- tightening it trades a bounded, visible cost for false negatives that silently break legitimate reuse -- but the bound is now stated rather than implied. `the_release_workflow_builds_no_feature_gated_binaries` asserts release.yml's build steps pass no `--features` at all, rather than deny-listing names. The pre-existing guard spells `e2e-test-hooks` literally, which does not match the substring `e2e-oom-fault-injection` this branch adds, so a release build carrying only the new feature would have passed. Falsified: adding `--features rocm/e2e-oom-fault-injection` to a release build fails the test; the unmutated workflow passes. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · a1e15a8
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.
Round history: every finding below is new this round. Our previous round (at e47d0d6d) raised no blocking findings; this one is a full re-review of the whole change against main, which the PR now targets.
Review — needs work
Full review of the whole change.
The description says the PR adds vLLM out-of-memory guidance to the rocm serve summary. The ticket named in the title could not be retrieved from here. The description no longer matches the branch (see Blocking).
Blocking
-
The summary's
rocm diagnosecommand quotes the engine's own hint, not the user's failing line, on a real managed launch —apps/rocm/src/main.rs:7129-7163,crates/rocm-core/src/lib.rs:8244-8251,engines/vllm/src/process.rs:732-783,apps/rocm/src/main.rs:7352-7361- The managed engine is
rocmitself, started withlog_path = record.log_path, and its stdout and stderr are appended to that same file (attach_background_stdio). - When vLLM exits early,
wait_for_vllm_readybails withstartup_log_context.exit_code_forprints that error to stderr, so the service log ends with the engine's own block:…Detected an out-of-memory failure (…)followed byFor conditional remediation, run `rocm diagnose --symptom 'vllm: …'`. append_oom_serve_notethen reads that log after the 45 s wait.vllm_oom_diagnose_symptomscans with.rev()and picks the last line that clears the classifier, which is the engine's hint line.- That line contains
', so it failsquotable_in_single_quotesand the canonical symptom is printed instead. - Reproduced by building the real
startup_log_contextoutput into a log and calling the shared selector:- Selected:
Some("vllm: For conditional remediation, run `rocm diagnose --symptom 'vllm: torch.OutOfMemoryError: HIP out of memory. Tried to allocate 7.21 GiB.'`."), quotable:false. - With only the vLLM output in the log, the real line is selected.
- Selected:
- So "routes the user's actual failing line" fails on the path it was built for. Every test feeds a clean synthetic tail, so none catch this.
- Confidence 85 · logic · Fix: stop the CLI-side selection at the engine's
Last N lines of startup log:marker, or skip lines the engine emits. Add a test whose tail ends with the engine's hint block.
- The managed engine is
-
The reuse pre-gate's liveness clause is not covered by any test —
apps/rocm/src/main.rs:20027-20036, tests atmain.rs:30752-30880- Deleting
&& managed_service_is_live(record)leaves all fourreuse_pregate_*tests and everyappend_oom_serve_note_*test green. That was run: 10 passed. reuse_pregate_foronly plants live records (status = "starting",engine_pid= the test process).- Without the clause, a stopped or dead record for the same model unlocks the
ResolveModelround-trip and the self-managed install ahead of the no-usable-GPU bail. - Confidence 100 · logic · Fix: add a case with a stopped record or a dead pid that must return
false.
- Deleting
-
the_release_workflow_builds_no_feature_gated_binariespasses when a release build enables the test-only feature —xtask/src/workflow_contract.rs:2034-2046- Changing
release.yml:127tocargo build --release --all-features -p rocm -p rocmd -p xtaskleaves the test green (run). That build would compile ine2e-oom-fault-injection. - The filter is
line.contains("cargo build") && line.contains("--features"). It also misses-F …and a--featuresplaced on a\continuation line. - It has no positive control: a
release.ymlwith nocargo buildline passes too. - Its doc says it "covers every future test-only feature", which it does not.
- Confidence 100 · logic · Fix: match
--features,--all-featuresand-Fover the joined run blocks (multiline_run_blocksis already in this file), and assert at least onecargo buildwas seen.
- Changing
-
With a matching live service, the reuse pre-gate does runtime and engine work before the explicit
--gpucheck —apps/rocm/src/main.rs:6564-6625againstmain.rs:6663-6679- The existing contract at 6663 reads: "Validate an explicit
--gpu <index>up front — before engine/runtime resolution — so an out-of-range or masked-out ordinal produces a GPU-specific refusal even when no ROCm runtime is configured." - When
any_live_managed_service_for_modelmatches (by the lenient both-wayscontainsrelation, soqwenmatches a liveqwen3-8b-instruct), the new block first runs three steps:validate_engine_selection_runtime(...)?, which can write a runtime manifest or fail on it throughrecover_setup_runtime_registration;ensure_self_managed_engine_ready, which can install;ResolveModel.
- So
rocm serve qwen --gpu 99next to a liveqwen3-8b-instructservice can report a runtime-manifest error, or install, before the GPU-specific refusal. Two independent passes reached this by reading; it was not run. - Confidence 70 · logic · Fix: run the pre-gate only on the path that would otherwise refuse (
visible_gpu_indices == Some([])). This also removes the pre-gate's cost from every host with a usable GPU. Alternatively, validate--gpufirst.
- The existing contract at 6663 reads: "Validate an explicit
-
A
Givenstep is a no-op, and its precondition is set up in theWhen—tests/e2e-cucumber/tests/e2e/serving_steps.rs:1451-1459,:1461-1494plant_oom_launchhas an empty body; its comment says it "exists to state the scenario's premise".- The fault injection is armed inside
open_oom_launch_summary(spawn_with_env(..., &[("ROCM_E2E_SIMULATE_OOM_LAUNCH", "1")])). So theGivenestablishes nothing and theWhenboth sets up the state and acts. - This file already has the right pattern: serve-18's
lemonade_preparation_cannot_complete(:855-860) pushes its arming variable intoworld.command_env. - Confidence 85 · mechanical · Fix: have the
Givenpush the variable intoworld.command_env, and have theWhenspawn plainly.
-
Several comments added or invalidated by this change are false, and would mislead the next reader — locations below
xtask/src/workflow_contract.rs:2005-2048: the new test and its doc were inserted betweenci_windows_lifecycle_lane_reuses_the_binaries_it_built's doc block and its#[test]. The Windows-lane doc ("…must build WITHOUT them…") now documents the release-workflow test, and the lifecycle test at:2049has none.tests/e2e-cucumber/features/model_serving.feature:322("Positive counterpart of Scenario 23") and:353("scenario 23 plants the SAME model"): serve-23 is the low-VRAM plan preview and plants nothing. The scenario that plants a same-model service is serve-24.crates/rocm-core/src/lib.rs:8225-8236: says that on a torch OOM followed by a bareRuntimeError: ... killed: out of memory, "the vaguer line wins… Both are diagnosable".- The selector only accepts lines that clear the checker.
- The PR's own
sub_threshold_lines_carry_no_hint_at_all(process.rs) pins"the process was killed: out of memory"as not diagnosable, so the torch line wins. - The paragraph was correct for the old substring detector it was moved from.
crates/rocm-core/src/diagnose.rs:1556-1558and:1640-1643: these still say the engine falls back to the canonical symptom "when a user's actual failing line would not itself be diagnosable" and that "the vLLM engine calls this".- After this change the only production caller is
vllm_log_shows_oom. - The fallback now fires when a line is not quotable.
- After this change the only production caller is
xtask/src/e2e.rs:113-116: "cargo takes one--featuresargument, so both must be listed together rather than in two overriding flags". Cargo accepts repeated--featuresand adds them together. The single value works; the stated reason is wrong.- Confidence 85 · mechanical · Fix: move the new test and its doc out of the lifecycle doc, point the two feature comments at serve-24 or its
@id, and rewrite the three false paragraphs to match the code.
-
The description does not match the branch — PR description
- It says "Depends on #251… This PR is stacked on it and targets that branch; it must not merge before #251 does, after which it will be rebased onto
main." The PR targetsmain, and #251 is already inmain. - The Behavior coverage table lists two scenarios, both with stale numbers:
serve-oom-launch-memory-guidanceis listed as serve-24 and is serve-25.serve-oom-memory-guidanceis listed as serve-23 and is serve-24.
- It omits the third scenario this PR adds,
serve-unrelated-live-service-still-fails-fast(serve-26). - It does not mention a user-observable behaviour this branch introduces against
main: re-runningservefor a model whose managed service is already live now skips the no-usable-GPU refusal.reuse_existingdoes not exist onmain; serve-24 depends on it. - The verification section has a failing-scenario table headed "Status at
83ecfaad", an older head, with no lead-in. It sits directly under "Every lane is green at this head", so a reader cannot tell what it describes. - Confidence 85 · mechanical · Fix: narrow and refresh the description to this head. Drop the stacking note, list all three scenarios by
@id, state the reuse-skips-the-GPU-refusal behaviour, and remove or explain the stale table.
- It says "Depends on #251… This PR is stacked on it and targets that branch; it must not merge before #251 does, after which it will be rebased onto
Non-blocking
-
An OOM line with a carriage-return or erase-line prefix is no longer detected —
crates/rocm-core/src/lib.rs:8202-8206vllm_log_shows_oomsplits on\nand scoresvllm: {line}. The checker then splits on rendered lines, so only the first segment keeps thevllm:anchor.- Run:
vllm_log_shows_oom("\u{1b}[2K\rtorch.OutOfMemoryError: HIP out of memory. Tried to allocate 7.21 GiB.")andvllm_log_shows_oom("Loading 40%\rtorch.OutOfMemoryError: …")both returnfalse. The old engine substring scan matched both. - This is consistent with
rocm diagnose, and in a vLLM traceback the OOM line is normally its own line, so the practical impact is likely small. - Confidence 100 · logic · Fix: classify per rendered segment, prefixing each segment with
vllm:.
-
If the capability signal breaks, serve-25 silently becomes a skip that reports green —
tests/e2e-cucumber/src/capability.rs:395-396- Nothing tests the env parse (
is_some_and(|value| value == "1")). The unit tests buildHostCapabilityby hand, and nothing asserts the mock lane actually runsserve-oom-launch-memory-guidance. - The shared constant prevents a typo in the variable's name. It does not prevent a broken parse or a lane that stops setting the variable.
- Confidence 70 · architectural · Fix: either extract the parse into a pure function and test it, or fail the report when that
@idisskipon the mock platform.
- Nothing tests the env parse (
-
The scenario id
serve-oom-memory-guidancenames the opposite of what it asserts —model_serving.feature:315- It asserts the summary carries no memory guidance. Its positive counterpart is
serve-oom-launch-memory-guidance. - The PR description already pairs the ids with the wrong scenario numbers, so this confusion is likely to happen again.
- Confidence 70 · mechanical · Fix: rename it (for example
serve-reuse-not-blamed-for-oom), if the id is not yet relied on elsewhere.
- It asserts the summary carries no memory guidance. Its positive counterpart is
-
Two
serve_summarytests only exerciserocm_core—apps/rocm/src/serve_summary.rs:504-528oom_signatures_are_detected_case_insensitivelyandunrelated_failures_are_not_flagged_as_oomcall onlyrocm_core::vllm_log_shows_oom, whichrocm-corealready tests.- Confidence 85 · mechanical · Fix: delete them, or move any case
rocm-corelacks intorocm-core.
-
The status gate is applied twice —
apps/rocm/src/main.rs:7136,apps/rocm/src/serve_summary.rs:227- Deleting
|| !serve_summary::serve_failed_to_become_ready(status)fromappend_oom_serve_noteleaves every test green (run), becauseoom_memory_notere-checks the status. - The behaviour holds either way. The early return only saves the log read, and nothing pins which copy is meant to own the rule.
- Confidence 100 · mechanical · Fix: keep one gate, and say in the remaining doc that it owns the rule.
- Deleting
Decisions for the author
-
Where the reuse pre-gate runs — tradeoff
- As written, reuse works for any spelling and reuses the probe's
ResolveModelanswer. - The cost is that engine work can run on GPU-equipped hosts whenever a lenient name match exists, including ahead of
--gpuvalidation (see Blocking). - Scoping it to the no-usable-GPU case removes that cost everywhere else. An exact
model_ref/canonical_model_idmatch there would also avoid the engine round-trip, but a user who types a different alias would get the refusal instead of reuse. - Note the decision is made before the launch lock. If the matching service exits before
spawn_managed_engine_childre-checks, a fresh launch proceeds with the bail already skipped, and relies on the engine's own GPU check. - The existing reuse short-circuit compares only engine, canonical id, recipe and auth, not
--port,--gpuor--runtime-id. This change extends that short-circuit past the no-GPU refusal.
- As written, reuse works for any spelling and reuses the probe's
-
Non-interactive serves get no post-failure OOM guidance — tradeoff
print_managed_launch_plainprints onlyreadiness: starting; the in-code comment acknowledges this.- For: the plain output is machine-readable by design.
- Against: assistant-driven and scripted
rocm serveis a first-class surface, and the same failed launch is explained in one invocation and not the other. - A single
note:line, emitted only when the launch failed and the log shows OOM, would close the gap.
-
The narrower detector also silences the engine hint — tradeoff
HIP error: out of memory, a baretorch.OutOfMemoryError, and kernel OOM-killer lines now get no memory hint on any surface; the engine surface used to fall back to the canonical symptom for them.- For: consistent with
rocm diagnose. - Against:
HIP error: out of memoryis a plausible real HIP-runtime failure line. - Confirm this is intended; the other option is widening the diagnose table.
-
What serve-25 proves — tradeoff
- The hook replaces the whole spawn → readiness-wait → status path, and hard-codes
status: "starting"and the log path. - serve-25 therefore verifies how the note is composed and rendered for a given failed-launch state. It does not verify that a real failed launch produces that state; only GPU lanes or unit tests can.
- The hook replaces the whole spawn → readiness-wait → status path, and hard-codes
-
Scope and history — non-blocking-improvement
- The branch combines separable threads:
- the OOM detector, note and diagnose routing;
- the reuse pre-gate that bypasses the GPU refusal;
- moving the quoting guard into
rocm-core's terminal module; - the vLLM engine refactor;
- the e2e fault-injection scaffolding;
- a new release-workflow contract test.
- The first-parent history has about 20 merge commits from a feature branch. Splitting out the reuse/GPU-refusal change and the test scaffolding, or at least squashing into meaningful commits, would make each part reviewable.
- The branch combines separable threads:
Positive signals
vllm_oom_diagnose_symptomreturnsOption, so selecting the line and gating on OOM are one predicate shared by the engine and the CLI, not two copies that can drift.e2e_simulate_oom_launchis aconst fnreturningfalsewithout the feature, and nothing in the workspace enables that feature exceptxtask e2e.- serve-26 pins the pre-gate's model keying end to end. That is behaviour no unit test of the predicate could show.
append_oom_serve_note_reads_the_shared_engine_tail_budgetplaces the OOM line at the oldest in-window line and then one line past it, which pins the shared constant exactly.- The quoting tests assert the exact canonical fallback rather than just "no
'survived", which rules out a stripped lookalike passing.
Deployment notes
None
What this covered
What was read and checked:
- Scope: every file in
prw-base...HEAD(merge base 1845df3…, head a1e15a8), 18 files. - Read in full:
serve_summary.rs,terminal.rs,engines/vllm/src/process.rs, the feature file,serving_steps.rs,capability.rs,expectation.rs,tui_driver.rsandxtask/src/e2e.rs. - Read in part:
apps/rocm/src/main.rs(~40k lines) andcrates/rocm-core/src/lib.rs(~13.7k lines) were read as every changed item plus its callers, callees and the surrounding serve, launch and reuse code, not the whole files. Same forworkflow_contract.rs,diagnose.rsand the CI workflows. - Passes run: coupled-group code passes, plus instruction adherence, history, prior reviews on earlier PRs that touched these files, and code comments. Three design questions were investigated.
- Run:
- targeted mutation checks on the pre-gate liveness clause, the duplicated status gate and the release-workflow guard;
- a probe of the shared selector against the engine's real failure output;
- the CR/erase-line classifier probe.
Not run or not checked:
- No full test or e2e run: the e2e suite was not run locally, so the scenario verdicts are by reading.
- Ticket: the ticket named in the title could not be retrieved from here.
- CI: CI was read from the status rollup, where every reported check passed.
Skill checks (skillscope)andRegenerate manifestsreported skipped. No GPU or self-hosted e2e lane appears in the rollup at this head, so serve-24 to serve-26 are exercised only on the mock lane here.
Not reconciled against any discussion on this PR.
…tealing a doc
Two defects in the guard I added last round, both reported and both real.
It missed the evasions that matter. The filter was `contains("--features")`,
and `--all-features` does not contain that substring -- so a release build
with `--all-features`, which compiles in every test-only feature at once,
passed. `-F` was missed for the same reason, and a flag on a `\` continuation
line was never seen because the scan was line-at-a-time rather than over the
joined `run: |` blocks. It also had no positive control: a release.yml with no
`cargo build` line at all passed vacuously, so a renamed build step would have
silently disarmed it. The doc claiming it "covers every future test-only
feature" was therefore false.
Now matches `--features`, `--all-features` and a standalone `-F` across
joined run blocks, and asserts at least one `cargo build` was seen. Falsified
against all four cases -- each is caught, and the unmutated workflow passes.
It was also inserted between `ci_windows_lifecycle_lane_reuses_the_binaries_it_built`'s
doc comment and its `#[test]`, so that doc described my test and the lifecycle
test had none. Moved below it; the doc is reattached to the test it describes.
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
@rominf — description finding addressed (description only; no code change, so the head is still You were right that the numbering had drifted again. Resolving every
Two things I deliberately did not do:
Same reason for the rest of your non-blocking list, which is all in Rust or workflow YAML: the comment at |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 88e3d18
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.
Review — needs work
Full review of the whole change.
Implements part of EAI-8059 (now keyed ROCMAI-419): the post-failure OOM note naming --gpu-memory-utilization and --gpu. It departs from the ticket in two places. It deliberately does not treat Engine core initialization failed as an OOM signature. And it also changes serve behaviour (a reused service skips the no-GPU bail), which the ticket says is out of scope ("This changes diagnostics only, not serve behaviour").
Round note: each finding is marked new (first raised this round) or standing (raised in the previous automated round at c951af53 and still present). The previous round's report had no blocking findings, so there were no standing blockers to settle.
Blocking
-
[new] The release-feature guard does not catch what its doc and commit say it catches —
xtask/src/workflow_contract.rs:188-207(multiline_run_blocks), andthe_release_workflow_builds_no_feature_gated_binarieswith its doc comment (added at about:2086)- Continuation lines are missed. The doc says it reads "joined
run: |blocks so a flag on a\continuation line is still seen", and commit 88e3d18 says all four evasions were "falsified … each is caught".multiline_run_blocksonly trims lines and joins them with newlines. It never merges\continuations, and the guard testscargo buildand--featureson the same line.- I changed
release.yml:127tocargo build --release -p rocm -p rocmd -p xtask \with--features rocm/e2e-oom-fault-injectionon the next line. The test passed. - With the flag on the same line, the test fails, so it is live and only the continuation shape escapes. That multi-line
bash -lc '…'block is exactly where someone would add a continuation.
nightly.ymlis not covered. The doc says "Nothing shipped may be built with a test-only feature", but the guard reads onlyrelease.yml.nightly.yml:123and:226build the same shape (cargo build --release -p rocm -p rocmd -p xtask).- That workflow publishes a public prerelease (
gh release create … --prerelease,publish-nightly).
- A glued
-Frocm/…also slips past thew == "-F"check. - Confidence 100 · logic · Fix: join
\-continued lines before scanning, and run the same check overnightly.yml. Otherwise narrow the doc and the "falsified against all four cases" claim to what is actually checked.
- Continuation lines are missed. The doc says it reads "joined
-
[new] Two branches of the new reuse pre-gate are pinned by no test —
apps/rocm/src/main.rs:20028-20036(any_live_managed_service_for_model), tests atmain.rs:30755-30880- The liveness filter. I replaced
&& managed_service_is_live(record)with&& trueand rancargo test -p rocm --bin rocm -- reuse_pregate: all 4 tests passed.- That filter is what stops a stale or stopped record from pulling
ensure_self_managed_engine_ready(an install) andResolveModelin front of the no-usable-GPU bail. The fail-fast contract this change says it "made true" rests on it.
- That filter is what stops a stale or stopped record from pulling
- The
record.model_refarm. Dropping it also leaves all 4 green.- The "exact ref" case's canonical id
qwen-canonicalalready matchesqwenthrough the lenient relation, so that arm is never needed. A worker found this by mutation; I re-derived it from the fixture.
- The "exact ref" case's canonical id
- Confidence 100 · logic · Fix: add a stopped-record case that must return
false, and a case where onlymodel_refmatches.
- The liveness filter. I replaced
-
[new] The PR description misstates the change — PR body
- Stale stacking. It says "Depends on #251 … targets that branch; it must not merge before #251 does".
- #251 merged on 2026-10-06, and this PR targets
main.
- #251 merged on 2026-10-06, and this PR targets
- Wrong ordering claim. It says index validation "now runs immediately after the no-usable-GPU pre-flight and before any runtime or engine work; a GPU-less host still refuses with 'no usable AMD GPU' first".
- When a live service with a matching name exists,
serve()runsvalidate_engine_selection_runtime,ensure_self_managed_engine_readyandResolveModelbefore both the bail and--gpuvalidation (main.rs:6566-6600). - On reuse the bail is skipped entirely (
main.rs:6637).
- When a live service with a matching name exists,
- Wrong CI claim. It says "Every lane is green at this head".
- At 88e3d18, both "E2E tests (Strix Halo, Ubuntu)" lanes are CANCELLED and "E2E tests (MI300X)" is QUEUED.
- The text describing the WSL2 lane as a "24h runner timeout" is also stale: that lane is now SUCCESS.
- Unmentioned work. The new
release.ymlfeature guard (a1e15a8, 88e3d18) is not described. - Behaviour change described as a fix. "Reuse skips the GPU bail" is a new serve behaviour that the ticket rules out of scope. The description presents it as a fix to existing reuse detection, but at
prw-basethe bail runs first and there is no pre-gate. - Confidence 85 · architectural · Fix: update the description (the change itself does not need to grow).
- Stale stacking. It says "Depends on #251 … targets that branch; it must not merge before #251 does".
Non-blocking
-
[standing] Skipping the bail is not tied to an actual reuse (TOCTOU) —
main.rs:6598-6600,6637,7440-7470reuse_existingis decided unlocked.spawn_managed_engine_childre-checks under the launch lock and falls through to a real launch if the service died in between.- That launches an engine on a GPU-less host with the bail skipped. Only the engine's own backstop catches it.
- Confidence 75 · architectural · Fix: when the bail was skipped for reuse, refuse in the spawn path unless it returns
AlreadyRunning(or re-check the no-GPU condition under the lock).
-
[standing] The
serve()comment overstates when engine work precedes the bail —main.rs:6608-6622againstmain.rs:23272-23285- The comment says the work runs first "only when this invocation is about to reuse it".
- The doc on
service_model_names_matchin this same diff saysqwenmatches a liveqwen3-8b-instruct. In that case the install andResolveModelrun, the canonical ids then differ, and the bail fires after the work. - The matcher doc governs, because the matcher is what the code calls.
- Confidence 85 · mechanical · Fix: narrow the comment to "when a similarly-named live service exists".
-
[standing] Wrong scenario cross-references —
tests/e2e-cucumber/features/model_serving.feature, the comments aboveserve-25andserve-26- "Positive counterpart of Scenario 23" and "scenario 23 plants the SAME model" both mean
serve-24.serve-23is the low-VRAM plan scenario and plants nothing. - Confidence 90 · mechanical · Fix: refer to
@id:serve-oom-memory-guidancerather than a positional number, since the numbers have already drifted twice.
- "Positive counterpart of Scenario 23" and "scenario 23 plants the SAME model" both mean
-
[new] Docs made stale by moving the classifier
crates/rocm-core/src/diagnose.rs:1553-1561and:1637-1644still say the engine callsvllm_oom_symptom_is_diagnosableto decide between routing and the canonical fallback. The fallback is now reached only throughquotable_in_single_quotes.crates/rocm-core/src/lib.rs:8184-8190says the pre-launch low-VRAM note shares this classification. That note is driven by VRAM telemetry and never calls it.- Confidence 85 · mechanical · Fix: update the three doc comments.
-
[new]
docs/vllm.mdno longer matches the hint's reach —docs/vllm.md:236-248- "When a startup failure log shows an out-of-memory error, the failure message suggests…" now overstates. A bare
HIP error: out of memorygets no hint (sub_threshold_lines_carry_no_hint_at_all). - The serve-summary note reaches only an interactive TTY. The plain path prints no notes (the comment at
main.rs:6908). - AGENTS.md §5 asks that behaviour claims be kept in sync across docs.
- Confidence 75 · mechanical · Fix: say "allocator-shaped OOM errors" and state the interactive-only scope.
- "When a startup failure log shows an out-of-memory error, the failure message suggests…" now overstates. A bare
-
[standing]
serve-24cannot fail on what its title andThenclaim —model_serving.feature:316,serving_steps.rsassert_no_oom_memory_guidance- A worker made
append_oom_serve_notealways return early:serve-24stayed green whileserve-25went red. - What
serve-24actually discriminates is the reuse bypass of the GPU bail. The comment discloses this, and the unit testappend_oom_serve_note_ignores_an_already_running_services_logpins the clause. - Confidence 90 · logic · Fix: retitle the scenario to the behaviour it proves, or give it a premise that reaches the clause.
- A worker made
Decisions for the author
-
Matching
rocm diagnose's threshold drops a real OOM form — tradeoff- The gain: no false memory advice for kernel OOM-killer lines or bare substrings.
- The cost:
RuntimeError: HIP error: out of memory, a genuine PyTorch-on-ROCm allocation failure, scores 25 and now gets no hint at all, where it used to get the canonical one. - The ticket also listed
Engine core initialization failedas a signature to detect; the PR excludes it on purpose. - The alternative is to raise the scorer's weight for that HIP form, so both surfaces keep sharing one rule.
-
Reuse skipping the no-GPU bail exists mainly so
serve-24can run on the GPU-less lane — non-blocking-improvement- The real user case (a live service, then a later invocation with a different
HIP_VISIBLE_DEVICES) is plausible but nothing cites it. - The cost is the pre-gate itself:
- engine work before the bail on lenient name matches;
- the TOCTOU above;
--gpuvalidation now running after the install;- about 100 lines of comments explaining the ordering.
- An alternative is to keep the bail first and reach the reuse summary through the fault-injection seam, which already exists for
serve-25.
- The real user case (a live service, then a later invocation with a different
-
The OOM note only fires when vLLM dies within the 45 s readiness wait, and only on a TTY — tradeoff
wait_for_service_http_ready_with_progressnever checks the process. An OOM after 45 s (for example, during weight loading for a large model) renders a summary with no note.- Nothing else surfaces the guidance afterwards; rocmd recovery does not read the log for OOM.
- Within this ticket's scope that is acceptable. It is worth stating so it is not mistaken for full coverage.
Positive signals
quotable_in_single_quotesandvllm_oom_diagnose_symptomnow live once inrocm-core. Both surfaces that printrocm diagnose --symptom '…'share one selector and one guard, which closes the twin-implementation drift.- The fault-injection seam compiles out to a
const fnreturningfalse. Clippy is clean with and without the feature. - The capability gate makes a missing hook a reported skip rather than a silent pass.
- The
OOM_FAULT_INJECTION_ENVproducer/consumer literal is hoisted intoe2e-report, soxtaskand the harness cannot drift.
Deployment notes
- Behaviour change: on a host with no usable AMD GPU,
rocm serve <model>now reuses a live managed service for the same engine and model instead of refusing. - Behaviour change: the vLLM startup-failure message no longer carries the memory hint for sub-threshold OOM lines, such as
HIP error: out of memoryor a baretorch.cuda.OutOfMemoryError. - A new test-only cargo feature,
rocm/e2e-oom-fault-injection, must never be enabled in a release or nightly build. Today's guard does not fully enforce that (see Blocking).
What this covered
- Diff reviewed:
git diff prw-base...HEAD, 18 files, every changed hunk read completely. The range is merge-base 1845df3 … head 88e3d18 (prw-basetip f9a8011). Workers also read the surrounding code:serve()in full, plusspawn_managed_engine_child,start_managed_service,run_attached_service,load_managed_servicesand the readiness wait;- the diagnose vLLM-OOM checker;
- the
release.yml,nightly.yml,ci.ymlande2e-selfhosted.ymlworkflows.
- Fan-out: three code workers grouped by coupling (OOM core; serve CLI; e2e/xtask), one worker for the four source-reading passes (agent instructions, history, prior changes via the #251 review comments, code comments), and one design worker.
- Shared checkout: the workers mutated the same checkout concurrently, so I re-ran the liveness-filter mutation alone. A merge of
prw-baseinto the head is clean. - Ticket: EAI-8059 was retrieved (title, body and state; it is now keyed ROCMAI-419).
- Runs:
cargo test -p xtaskon the guard, with mutations ofrelease.yml;cargo test -p rocm --bin rocm -- reuse_pregate, with mutations;- targeted
rocm-core,rocmandxtasktests; - clippy on
rocmwith and without the feature; serve-24,-25and-26against a locally built feature binary, which all pass (on a WSL host, not the actual mock lane).
- Did not run: the full workspace test suite and the full e2e suite. I relied on CI for those (build-and-test, windows-build-and-test, clippy, E2E mock lane, Windows and WSL2 lanes are all green).
- CI gaps at this head: both Strix Halo Ubuntu lanes were cancelled and MI300X is queued, so no GPU-Linux lane has run.
- Discussion: not reconciled against the PR's discussion here; its comments and earlier reviews were not read.
- Prompt injection: no prompt-injection content was found.
Resolve against main's three large extractions. * rocm-core/src/lib.rs: #528 moved the host/GPU detection tests out to host_gpu.rs, deleting the block this branch appended its two `vllm_log_shows_oom` / `vllm_oom_diagnose_symptom` tests to. Keep only those two; the rest live in host_gpu.rs now. The OOM rule itself (`vllm_log_shows_oom`, `vllm_oom_diagnose_symptom`, the `quotable_in_single_quotes` re-export) stayed in lib.rs and merged clean. * apps/rocm/src/main.rs: #540 moved `fn serve` into serve_cmd.rs after this branch had changed it, so an unresolved merge would have taken serve_cmd.rs straight from main and silently dropped the reuse pre-gate, the OOM fault-injection bypass and the serve-summary OOM note. Port all three hunks onto serve_cmd.rs and import the helpers that stayed in main.rs (`any_live_managed_service_for_model`, `append_oom_serve_note`, `e2e_simulate_oom_launch`, `existing_live_managed_service`, `EngineSelection`). The remaining main.rs hunks and the six `append_oom_serve_note` tests stay where they were. * tests/e2e-cucumber capability/expectation: main's `@requires-case-sensitive-fs` and this branch's `@requires-oom-fault-injection` are independent additions to the same lists; keep both, with main's `requires_os` check still ahead of the filesystem premise. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
#475 reworked the workflow-contract test module, colliding with this branch's `the_release_workflow_builds_no_feature_gated_binaries`. Both are additions to the same test module, so keep both — and drop this branch's copy of the "Extractor guards" comment, which #475 deliberately moved and reworded above the pin-extractor guards. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
volen-silo
left a comment
There was a problem hiding this comment.
I reviewed this PR at a513aa6 and found three issues to address before merge: allocator OOM guidance can disappear after a progress-bar carriage return, the release-feature guard misses ordinary command formatting, and the required behavior documentation is missing. Details are inline.
This was a read-only diff and source review. I reproduced the feature guard false passes using its loop logic, but did not run PR-head Rust tests or a GPU launch. Three CI jobs remain queued, so final CI validation is still pending.
| pub fn vllm_log_shows_oom(log: &str) -> bool { | ||
| log.lines().any(|line| { | ||
| let line = line.trim(); | ||
| !line.is_empty() && diagnose::vllm_oom_symptom_is_diagnosable(&format!("vllm: {line}")) |
There was a problem hiding this comment.
The synthetic vLLM anchor is attached to a physical line, but the diagnostic splits that line again with terminal::rendered_lines. For example, Loading weights: 90% torch.OutOfMemoryError: HIP out of memory. Tried to allocate 7.21 GiB. becomes an anchored progress segment and an unanchored allocator exception. The exception is discarded, so vllm_oom_diagnose_symptom returns None and both the engine hint and serve-summary note disappear. The previous engine detector retained the OOM hint and used the canonical symptom for this control-bearing input. Please apply the vLLM context to each rendered segment while preserving safe command rendering, and add a regression case for a progress-bar CR followed by the allocator exception.
| .into_iter() | ||
| .chain(std::iter::once(release.clone())) | ||
| { | ||
| for line in block.lines().map(str::trim) { |
There was a problem hiding this comment.
This loop checks physical lines, so a normal shell continuation defeats the new guard: cargo build --release -p rocm -p rocmd \ followed on the next line by --features rocm/e2e-oom-fault-injection increments builds but leaves offenders empty. The adjacent comment explicitly promises to catch this shape. Compact -Frocm/e2e-oom-fault-injection also passes the exact w == "-F" check. I reproduced both false passes using this loop logic; current release build commands do not enable the hook. Please inspect logical commands with continuations joined, handle compact -F arguments, and add fixtures covering these forms.
| // no notes at all — that path is machine-readable by design and is not | ||
| // the place to grow prose, but it does mean the same failed launch is | ||
| // explained in one invocation and not the other. | ||
| let notes = append_oom_serve_note( |
There was a problem hiding this comment.
This adds observable serve behavior, and the reuse pre-gate also introduces an exception to the no-usable-GPU refusal. AGENTS.md section 5 requires README.md, the command help/doc comment, docs/testing.md, and docs/manual-testing.md to be updated in the same change. None of those four surfaces changes here; the docs/vllm.md addition only explains the shared quoting guard. Please document the OOM note conditions and interactive-only scope, the no-GPU reuse exception, and the focused automated/manual checks, including the fault-injection capability and prebuilt-lane skips.
There was a problem hiding this comment.
@volen-silo — you were right, and the four surfaces are now updated in f49f670c.
README.md — two new subsections under Model serving:
- When a serve runs out of GPU memory states the note's conditions as a list, each one falsifiable: vLLM only; the interactive deployment summary only (
summary_modeisbackground && stdout().is_terminal(), so a piped/redirected run takesprint_managed_launch_plain, which printsreadiness: <status>and carries no notes at all, and--verbose/--foregroundstreams the traceback instead); only when the service failed to become ready (statusstarting— areadyor still-loadingrunningservice is healthy); only on a real allocator signature (HIP out of memory,CUDA out of memory,hipErrorOutOfMemory, ortorch.OutOfMemoryErrorcorroborated by an allocator message — a kernel OOM-killer line does not qualify); and only when this invocation launched the process. On the reuse path I wrote the mechanism rather than the conclusion: the reuse summary carries no log path at all, so the running service's log is never read and the note cannot fire there whatever that log contains — withrocm services logs <service-id>as the thing to do instead. I also recorded that the shared--gpu-memory-utilizationhint is de-duplicated as a fragment, not as a suppression of the note. - Reuse is exempt from the no-GPU refusal says it plainly, as you asked: this is a change to a refusal users rely on, and on a GPU-less host
rocm serve <model>can now succeed where it previously always failed. It then bounds the exemption — same engine and model, launches nothing, pins no GPU; a live service for a different model or engine still fails fast and prepares nothing.
rocm serve --help — the doc comment on Command::Serve (apps/rocm/src/main.rs:421) gains two paragraphs covering the same two things in short form. Rendered output verified on Linux; -h is unchanged (clap still shows only the summary line).
docs/testing.md — new section Serve OOM Guidance And The Reuse Exemption: the focused cargo test invocations for the classifier/selector (rocm-core), the note and its quoting guard, the append_oom_serve_note_ gate and the reuse_pregate_ pre-gate, and the two release-guard tests. It records that append_oom_serve_note_ignores_an_already_running_services_log is the only check that discriminates the already_running clause, since the E2E reuse scenario goes through a path with no log path and the note is withheld on that ground alone. Then serve-24/25/26 with their @requires-* tags, and a subsection on the fault-injection capability: the e2e-oom-fault-injection feature armed by ROCM_E2E_SIMULATE_OOM_LAUNCH=1, that only cargo xtask e2e builds with it and only it sets ROCM_E2E_OOM_FAULT_INJECTION=1, that this is the one capability the harness cannot probe for, and that every prebuilt-ROCM_CLI_BINARY lane (self-hosted, nightly, and the Windows lifecycle lane in ci.yml) therefore resolves serve-25 to a reported skip rather than a silent pass — it gates PRs on the mock lane. Every test filter in that section was run on Linux (see the caveat below).
docs/manual-testing.md — new section 5, vLLM Serve Memory Guidance And The Reuse Exemption (5–7 renumbered to 6–8): 5a drives a real OOM with --gpu-memory-utilization 1 and checks the note's content, including pasting the printed rocm diagnose --symptom back and confirming it reports a cause and stays one balanced quoted argument; 5b walks the cases where the note is correctly absent so its scope is not mistaken for flakiness; 5c checks the reuse exemption and the refusal it does not change, using HIP_VISIBLE_DEVICES= to stand in for a GPU-less host — reuse goes through, a different model/engine is refused with no Preparing lemonade for GPU serving… and nothing downloaded, and after stopping the service the masked run refuses again.
docs/vllm.md is left as-is; it already carries the shared quoting guard and the flag itself, and the new README section links to it rather than restating it.
Your other two findings were fixed in fd34ad77, both with tests that fail without the fix: the carriage-return one by splitting the tail with terminal::rendered_lines and anchoring each segment, pinned by the Loading weights: 90%\rtorch.OutOfMemoryError: … case in vllm_oom_diagnose_symptom_selects_the_failing_line_or_reports_no_oom (which asserts the exception segment is quoted, not the progress-bar segment that carried the anchor); and the release-feature guard by joining \ continuations before matching and by catching -F in both its detached and compact spellings, plus --all-features, pinned by the_release_workflow_builds_no_feature_gated_binaries.
One thing I did not fix, and it needs your eye. That first fix has a side effect that is currently red on this branch. rendered_lines strips terminal control sequences, so vllm_log_segments now hands quotable_in_single_quotes an already-clean string and the canonical-symptom fallback never fires for a colourised line. serve_summary::tests::control_bytes_from_the_log_never_reach_the_printed_command fails at f28718c7 — it gets "vllm: RuntimeError: HIP out of memory" where it asserts VLLM_OOM_CANONICAL_SYMPTOM — which is exactly the "stripped into a lookalike" outcome that test was written to reject, and it contradicts what docs/vllm.md in this PR says a control-byte-bearing line does. I reproduced it on Linux at f28718c7 with my commit absent, so it is not from the docs change, and CI's Test (affected crates) is already failing on it. I left it alone because choosing between "quote the stripped line" and "fall back to canonical" is a behaviour call that belongs to this PR's author, not to a docs pass. Everything else I cite in docs/testing.md passes on Linux, and cargo clippy --locked --workspace --all-targets -- -D warnings is clean at f49f670c.
vLLM draws its loading bar with bare carriage returns, so a real failure
arrives as `Loading weights: 90%\rtorch.OutOfMemoryError: HIP out of
memory. Tried to allocate 7.21 GiB.` on ONE `\n` line.
The detector anchored whole physical lines as `vllm: <line>`, but
`vllm_oom_symptom_is_diagnosable` re-splits with `terminal::rendered_lines`,
which also ends a line on `\r`, and then keeps only segments carrying the
anchor. So the anchor landed on the progress segment, the allocator
exception became an unanchored segment and was dropped, and the tail
scored as "no OOM" -- withdrawing both the engine hint and the
serve-summary note for the commonest shape of the failure they exist to
explain.
Split here the way the diagnostic splits, and anchor each segment, so the
two sides agree on where a line ends. Reported by volen-silo.
Falsified: reverting to per-physical-line anchoring fails the new case.
Also makes the release-feature guard catch the two spellings it missed.
volen-silo reproduced both; I reproduced them again before fixing:
cargo build ... \ <- flag on the continuation line
--features rocm/e2e-oom-fault-injection
cargo build ... -Frocm/e2e-oom-fault-injection <- compact short flag
The first counted the build and found no offender, because the loop read
physical lines while the real release build is wrapped -- the doc comment
already claimed continuations were joined, and they were not. The second
slipped a whole-token `== "-F"` test. Now joins continuations into logical
commands and accepts both `-F name` and `-Fname`, with fixtures for each
form plus negative cases (`--frozen`, a clean wrapped build).
Falsified: reverting the compact-flag arm fails the new fixture.
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
`-D warnings` rejects `flat_map(|line| f(line))` where `flat_map(f)` will do. Mine, from the carriage-return fix in fd34ad7: I ran the targeted tests but not clippy before pushing. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…xception AGENTS.md section 5 requires README.md, the command's --help/doc comment, docs/testing.md and docs/manual-testing.md to move with observable behavior; this change only reached docs/vllm.md, and that addition covers the shared quoting guard rather than the new serve behavior. README.md and `rocm serve --help` now state when the out-of-memory note appears and when it does not -- vLLM only, the interactive deployment summary only (a piped run takes the plain `readiness:` form with no notes), only on a launch this invocation started that failed to become ready, and only on a real allocator signature. They also say plainly that reusing an already-running managed service for the same engine and model is now exempt from the no-usable-GPU refusal, and that a live service for another model or engine is not. docs/testing.md records the focused unit tests, the three E2E scenarios, the test-only `e2e-oom-fault-injection` feature that stands in for a GPU the mock lane does not have, and why every prebuilt-binary lane reports a skip for the scenario that needs it. docs/manual-testing.md adds the hands-on checks for both behaviors, including the masked-device steps that separate the reuse exemption from the refusal it does not change. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…l bytes My carriage-return fix in fd34ad7 routed each log line through terminal::rendered_lines. That fixed the anchoring, but rendered_lines also discards escape sequences and control bytes -- and the segment it returns becomes the `rocm diagnose --symptom` argument. So a colourised OOM line arrived at quotable_in_single_quotes already sanitised, was never rejected, and the VLLM_OOM_CANONICAL_SYMPTOM fallback stopped firing: the printed command carried a stripped lookalike instead of refusing the line. That is exactly the failure `control_bytes_from_the_log_never_reach_the_printed_command` exists to catch, and its own comment warns against a stripper that "leaves `31m`/`0m` behind". I broke that test and did not notice, because I ran the two tests I had just written rather than the suite. Split on a raw `\r` instead. Each segment still gets its own `vllm:` anchor, so the progress-bar case stays fixed, but the bytes survive into the symptom so the quotability guard can still reject them. Scoring is unaffected: the diagnostic strips its own copy regardless. Verified on Linux at this commit: control_bytes_from_the_log_never_reach_the_printed_command 1 passed rocm-core vllm tests (incl. the CR regression case) 20 passed cargo clippy --workspace --all-targets -- -D warnings exit 0 cargo test --workspace --all-targets only the known ETXTBSY flake in engines/vllm (Text file busy), which passes in isolation Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
@volen-silo — correcting the record on something a follow-up commit claimed, because it was wrong and it was my regression. The agent that added the docs reported
So I broke it, in the very commit that fixed your first finding. Mechanism. My fix routed each log line through That is precisely what the test exists to catch, and its own comment warns against a stripper that "leaves Fix ( Verified on Linux at
How I caused it is worth stating plainly: after fixing your first two findings I ran the two tests I had just written instead of the suite, so neither this nor a clippy |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · a5e259a
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.
Review — needs work
Full review of the whole change.
Ticket EAI-8059 (now ROCMAI-419): this PR implements its post-failure OOM note. It also changes serve behaviour (reuse now skips the no-GPU refusal), which the ticket rules out: "This changes diagnostics only, not serve behaviour." The description has several mismatches; they are listed under Blocking.
Round note: each finding is marked new (first raised this round) or standing (raised at the previous automated round at 88e3d18e and still present). That round's three blockers were settled as follows. The pre-gate test gap and the description mismatch were re-found independently by this round's blind review. The release-feature guard was not re-found; it was put to the reviewer after its review ended (an anchored check, not a blind re-finding). That check found parts (1) continuation lines and (3) the compact -F spelling fixed, and part (2) still holding. It is appended last under Blocking as standing, narrowed to that part.
Blocking
-
[new] Required CI check
Sphinx docs build (-W)fails because of a link this PR adds —README.md:624
The failing job log (run 37924095626) has exactly one warning:README.md:9799: WARNING: 'myst' cross-reference target not found: 'docs/vllm.md#shared-or-busy-gpus'. The README is pulled into the Sphinx tree with{include}, and the docs config cannot resolve that fragment.README.md:41links the same file without a fragment and builds fine.
Confidence 100 · mechanical · Fix: drop the#shared-or-busy-gpusfragment, or use the absolute GitHub URL form the README already uses at line 209. -
[standing] The PR description does not match the change — PR body
- It says "Every lane is green at this head; there are no failing checks." The Sphinx check above is failing.
- It says "Depends on #251 … targets that branch". The base is
main, and #251 is already inprw-base(5ed9365). - It says "One shared helper (
rocm_core::vllm_log_shows_oom) feeds both the engine hint and the CLI note". No production code calls that function (see the dead-code finding below). Both surfaces usevllm_oom_diagnose_symptom. - It presents the reuse detection ahead of the GPU bail as already existing. It is new here:
prw-base'sserve_cmd.rshas no reuse check before the bail. The README this PR adds calls it "a real change to a refusal you may be relying on", but the description never says so, and the ticket scopes the work to diagnostics only.
Confidence 85 · architectural · Fix: correct the description. State the reuse/no-GPU exemption as a behaviour change, or move it to its own PR (see Decisions).
-
[new] The
--helptext and docs promise that nothing is prepared before the no-GPU refusal; the code breaks that for overlapping model names —apps/rocm/src/main.rs:441-445,README.md:659-673,docs/manual-testing.md:393-399,apps/rocm/src/serve_cmd.rs:678- The pre-gate
any_live_managed_service_for_modelusesservice_model_names_match, which is a two-waycontains. - So a live
qwen3-8b-instructservice letsrocm serve qwenpass the pre-gate. On a GPU-less host it then runsvalidate_engine_selection_runtime(which can write a runtime manifest),ensure_self_managed_engine_ready(which prints "Preparing…" and can install) and aResolveModelcall, all before the refusal. - The code admits this in two places: the doc on
service_model_names_match, andserve_cmd.rs:752-758("it is not 'nothing runs before the pre-flight'"). serve_cmd.rs:678("reads the managed-service records and nothing else") contradicts that later comment.- The code governs here: its leniency is a recorded, deliberate choice. The user-facing text is what is wrong.
- AGENTS.md §3 requires promises that something will not happen to be backed by the behaviour. Scenario serve-26 only uses a model name that does not overlap, so this case is never exercised.
Confidence 85 · logic · Fix: qualify the help text, README and manual-testing step 5c.3 for overlapping names, and fix the line-678 comment. Alternatively, tighten the pre-gate so the promise becomes true.
- The pre-gate
-
[new] In real use, the serve-summary note never quotes the user's own failing line —
apps/rocm/src/main.rs(append_oom_serve_note),crates/rocm-core/src/lib.rs:4507(vllm_oom_diagnose_symptom),engines/vllm/src/process.rs:776-783,README.md:618-621,docs/vllm.md:258-261- The managed supervisor's stderr is attached to
record.log_path(attach_background_stdio,main.rs:4019). - When vLLM OOMs, the engine bails with
startup_log_context, which includes its own hint.exit_code_for(main.rs:1425) then writesError: {e:?}into that same log. - The engine reports the failure within milliseconds, while the CLI waits out its full 45s readiness window. So whenever the OOM line is in the log, the engine's hint is there too, after it.
- That hint's last line,
For conditional remediation, run `rocm diagnose --symptom 'vllm: torch.OutOfMemoryError: HIP out of memory…'`., carries the same allocator keywords and clears the scorer. .rev()therefore selects that line. It contains'and backticks, soquotable_in_single_quotesrejects it and the note always falls back toVLLM_OOM_CANONICAL_SYMPTOM.
The README ("
rocm diagnose --symptom '<your failing line>'"),docs/vllm.md("builds the same command from the same engine log"), the description and theoom_memory_notedoc all claim the user's real line. The guidance itself stays correct, because the canonical symptom is diagnosable. The fault-injection fixture (simulate_oom_managed_launch) writes only the raw OOM line and none of the engine's error block, so serve-25 cannot see this.
Traced statically on the Linux spawn path; not run. Confidence 85 · logic · Fix: ignore lines from rocm's own hint (or read only up to the engine'sError:block) when choosing the symptom. Make the fixture include the engine's error block, or narrow the claims. - The managed supervisor's stderr is attached to
-
[new] Splitting the log only on
\rdoes not keep the classifier in step with the diagnose scorer, despite what its doc says —crates/rocm-core/src/lib.rs:4440-4467, consumed viadiagnose.rs:1628-1652,terminal.rs:280-322vllm_log_segmentsadds thevllm:anchor per\rsegment.vllm_anchored_linesthen re-splits withrendered_lines, which also breaks on every non-SGR CSI (ESC[2K,ESC[1A),ESC E/ESC D, VT/FF, U+0085 and U+2028/2029.- Take a rich-style repaint
…90%\r\x1b[2Ktorch.OutOfMemoryError: HIP out of memory. The segment becomesvllm: \x1b[2Ktorch…, which renders as["vllm: ", "torch.OutOfMemoryError…"]. Only the empty anchored piece is scored, so the tail reads as "no OOM", and both the engine hint and the summary note disappear. - The doc's promise ("keeps the two sides agreeing about where this line ends") holds only for a bare
\r. Tests cover only\r.
Confidence 70 (argued from the code, not run) · logic · Fix: for scoring, anchor each
rendered_linespiece; keep the raw segment only for the quoting check. Add table rows for\r\x1b[2K,\x0cand U+2028. -
[new]
vllm_log_shows_oomis a newpub fnthat no production code calls —crates/rocm-core/src/lib.rs:4436; callers are only tests (lib.rs:7692-7761,engines/vllm/src/process.rs:1069-1366,apps/rocm/src/serve_summary.rs:507-525)
The production paths gate onvllm_oom_diagnose_symptom(..)directly, and their own comments say callingvllm_log_shows_oomfirst would be "a branch that can never be taken". Its doc also wrongly says the pre-launch low-VRAM note uses this rule; that note is driven by VRAM telemetry. Twoserve_summary.rstests (oom_signatures_are_detected_case_insensitively,unrelated_failures_are_not_flagged_as_oom) only re-test thisrocm_corefunction fromapps/rocm.
Confidence 85 · mechanical · Fix: delete it, along with its mention indocs/testing.md, and point the tests atvllm_oom_diagnose_symptom(..).is_some(). Remove the duplicateserve_summary.rstests. -
[standing] The reuse pre-gate tests leave two of its three conditions untested —
apps/rocm/src/main.rs(reuse_pregate_admits_the_same_model_including_a_short_spelling,reuse_pregate_admits_the_model_id_the_oom_reuse_scenario_plants)- In every positive case
canonical_model_idalso matches the query ("qwen-canonical"contains"qwen"; the other cases use identical fields). Deleting therecord.model_refcheck stays green. - No test plants a dead or stopped record, so deleting
managed_service_is_live(record)also stays green. - Verdict 1: all four tests fail with the function removed.
- Verdict 2: two of its three conditions survive being broken.
Confidence 85 (read, not mutated) · logic · Fix: add a case where only
record.model_refmatches and one with a non-live record. - In every positive case
-
[new] The Given step for serve-25 does nothing; the setup happens in the When step —
tests/e2e-cucumber/tests/e2e/serving_steps.rs:1451-1459
plant_oom_launchhas an empty body. The fault is actually armed byROCM_E2E_SIMULATE_OOM_LAUNCHin the When step (:1476), so the Given text "a managed vLLM launch will run out of GPU memory" sets up nothing, which breaks the Given/When/Then pattern the suite uses.
Confidence 70 · mechanical · Fix: have the Given record the arming in the world (for exampleworld.command_env), and have the When step consume it. -
[standing] The release-feature guard's doc claims more than it checks:
nightly.yml's published builds are not covered —xtask/src/workflow_contract.rs,the_release_workflow_builds_no_feature_gated_binariesand its doc comment- Settled by putting the earlier objection to the reviewer after its review ended (anchored, not re-found blind). Parts (1) and (3) of that objection are fixed:
logical_commandsjoins\continuations, and the compact-Fspelling is caught. - Still holding: the doc opens "Nothing shipped may be built with a test-only feature", but the test reads only
read_workflow("release.yml").nightly.yml:123and:226buildcargo build --release -p rocm -p rocmd -p xtaskand publish a public prerelease (gh release create … --prerelease). Nothing guards those builds. They carry no features today. - A wholesale scan of
nightly.ymlwould fire on its legitimate--features rocm/e2e-test-hookse2e builds (:464,:580,:684,:899,:1131,:1550), so a fix has to target the publishing jobs only.
Static, not run · logic · Fix: extend the check to
nightly.yml's publishing builds, or narrow the doc torelease.yml. - Settled by putting the earlier objection to the reviewer after its review ended (anchored, not re-found blind). Parts (1) and (3) of that objection are fixed:
Non-blocking
-
[standing] Reuse is decided without the lock, so a service that dies before launch defeats the fail-fast refusal —
apps/rocm/src/serve_cmd.rs:~700-735,apps/rocm/src/main.rs:4136
If the service dies betweenreuse_existingbeing set andspawn_managed_engine_child's own re-check, the code writes a record and spawns a child on a GPU-less host. The engine's own refusal (engines/vllm/src/process.rs:354-365,engines/lemonade/src/state.rs:368-383) stops it late, instead of the clear early message.
Confidence 85 · logic · Fix: once the launch lock is held, rerun the no-GPU bail if reuse no longer applies. -
[new] When a live service matches the model name, pre-gate errors now surface before
--gpuvalidation and the refusal —serve_cmd.rs:~700-735vs the kept comments atserve_cmd.rs:~795-800andmain.rsvalidate_pinned_gpu_index("serve()'s fail-fast normally reports this first")
A runtime-manifest orResolveModelfailure now reports first.
Confidence 70 · logic · Fix: validate--gpubefore the pre-gate, or reword those comments. -
[standing] The engine's OOM hint now fires on fewer lines, and
docs/vllm.mdstill describes the broad trigger —engines/vllm/src/process.rs(oom_utilization_hint),docs/vllm.md:~253
HIP error: out of memory, a baretorch.cuda.OutOfMemoryErrorand the kernel OOM-killer line now get no hint at all. The doc still says "When a startup failure log shows an out-of-memory error, the failure message suggests…".
Confidence 70 · mechanical · Fix: state the narrower trigger in the doc. -
[new]
quotable_in_single_quotesdoc says the user's line stays visible beside the command, which is false for the new serve-summary note —crates/rocm-core/src/terminal.rs:194-196
oom_memory_noteprints neither the line nor a log tail.
Confidence 85 · mechanical · Fix: limit that sentence to the engine hint. -
[standing] Feature-file comments point at the wrong scenario number —
tests/e2e-cucumber/features/model_serving.feature:~322("Positive counterpart of Scenario 23"),:~353("scenario 23 plants the SAME model")
serve-23 is the low-VRAM plan scenario. The reuse scenario is serve-24, which is whatdocs/testing.mdcorrectly says.
Confidence 100 · mechanical · Fix: refer to@id:serve-oom-memory-guidancerather than a number, since the numbers keep drifting. -
[standing] serve-24's title says it guards attribution, but its check cannot fail on the OOM-note logic —
model_serving.feature:~316,serving_steps.rs(assert_no_oom_memory_guidance,assert_reused_running_service)
The real reuse path reportslog_path: None, so deletingappend_oom_serve_notekeeps it green. The comment admits this, but the scenario title still claims the guard.assert_reused_running_servicealso matches the bare textalreadyrunning.
Confidence 70 · mechanical · Fix: retitle the Then step to what it proves, and match the summary heading. -
[new] The release-feature guard's spelling test copies the guard loop instead of calling it —
xtask/src/workflow_contract.rs:~2609(caughtclosure)
The closure duplicates the loop body ofthe_release_workflow_builds_no_feature_gated_binaries, so a change to the real loop would not turn the spelling test red. Separately, the guard scans both the run blocks and the whole file, so every build is counted twice.
Confidence 70 · mechanical · Fix: extract oneoffending_builds(text)and call it from both tests. -
[new] Prebuilt e2e lanes are called "shipping"/"release" binaries, but they build with
rocm/e2e-test-hooks—tests/e2e-cucumber/README.md:146,tests/e2e-cucumber/src/capability.rs:~133,src/expectation.rs:~137,xtask/src/e2e.rs:~19-25,crates/e2e-report/src/lib.rs
workflow_contractrequires those hooks on the self-hosted and nightly prebuilt lanes.
Confidence 85 · mechanical · Fix: say "a prebuilt binary, built withoute2e-oom-fault-injection". -
[new] A step comment names the wrong file —
serving_steps.rs:~1357
The "Preparing for GPU serving..." line comes fromapps/rocm/src/engines_cmd.rs:355, notmain.rs.
Confidence 85 · mechanical. -
[new] One test assertion can never fail —
apps/rocm/src/serve_summary.rs(the_shared_hint_is_de_duplicated_without_losing_the_rest_of_the_oom_note)
The finalprinted == 1count seeds the hint itself and follows a!note.contains(hint)check, so it always holds. Another test,oom_note_renders_in_the_summary_notes, pushes the note into the summary by hand and passes with theserve_cmd.rswiring reverted; only serve-25 covers that wiring.
Confidence 85 · mechanical.
Decisions for the author
-
Should the reuse exemption from the no-GPU refusal ship in a separate PR? — non-blocking-improvement
It is its own user-visible change, with its own scenario (serve-26) and README section. It sits outside the ticket's "diagnostics only" scope. serve-24 needs it in order to run on the GPU-less mock lane, but that dependency argues for landing it first as a separate PR, not for bundling it here (AGENTS.md §11). -
Lenient vs exact model match in the reuse pre-gate — tradeoff
Lenient matching keeps short-name reuse working, but lets an overlapping name run engine work before the refusal. Exact matching makes the docs' promise true, but can miss a legitimate reuse. The code records lenient as deliberate. The docs then have to say so (Blocking above). -
Piped, scripted and assistant runs get no OOM guidance — tradeoff
print_managed_launch_plainprints onlyreadiness: starting. A failed OOM launch then looks the same as a model still loading, and the chat assistant (an AGENTS.md §7 consumer) gets no pointer torocm diagnoseorrocm services logs. The comment atserve_cmd.rs:1047-1052records this asymmetry. Please confirm it is still intended. -
The note only appears if the OOM is already logged when the 45s readiness wait ends — tradeoff
A slow weight load that OOMs later gets no note, and those are the likeliest OOM cases. Statusstartingalso stands in for "failed" while rocmd may still retry the service. -
The memory advice has two renderers — non-blocking-improvement
Classification is now shared, butoom_utilization_hint(process.rs) andoom_memory_note(serve_summary.rs) each phrase the advice and repeat the quote-or-fallback step. The engine hint lacks the "model does not fit" branch that the note and fix-16 carry. A singlerocm_corerenderer would stop the two drifting apart. -
The engine hint no longer fires on sub-threshold OOM lines — answered-by-intent
The description says: "a bareout of memoryfrom a kernel OOM-killer line, a dependency's log, or vLLM's genericEngineCorewrapper never gets reported as memory exhaustion. One shared helper … feeds both the engine hint and the CLI note". Without that, dropping the hint forHIP error: out of memoryand the baretorch.cuda.OutOfMemoryErrorwould have been a blocking regression finding. Confirm those lines should get no guidance at all. The staledocs/vllm.mdsentence is still listed under Non-blocking.
Positive signals
e2e_report::OOM_FAULT_INJECTION_ENVis shared by producer (xtask) and consumer (harness), and xtask clears it for a prebuilt binary. A typo can no longer turn a scenario into a silent skip, and theenv_removebranch is unit-tested.- The release-feature guard asserts
builds > 0, so if the build step moves, the guard fails instead of passing on nothing. quotable_in_single_quotesmoved intorocm-coreintact, with its rationale and tests, so the engine hint and the summary note share one quoting guard.
Deployment notes
- On a GPU-less host,
rocm serve <model>now succeeds instead of refusing when a live managed service for the same engine and model exists. Scripts that relied on the refusal will see a different result. - New cargo feature
rocm/e2e-oom-fault-injection(test-only). Onlycargo xtask e2ebuilds it, andrelease.ymlis guarded against it.
What this covered
- Read every file in
git diff prw-base...HEAD: merge-base03afcf43, heada5e259a0, 22 files. Also read the surrounding serve, launch, reuse, diagnose and terminal code paths, and the engine refusal backstops. - The fan-out ran as independent workers:
- four coupled-group code passes: vLLM/rocm-core, the serve app, e2e/xtask, and docs
- agent-instruction adherence, code-comments, history and prior-changes passes
- two design workers
- History:
git merge-treeagainstprw-baseis clean, and no merge reverted main's work. - All verification was static. Free disk read 39G, under the 40G floor, so nothing was built, tested or mutated. The only run evidence is CI's
statusCheckRollupand the failing Sphinx job log. All other CI checks were success or skipped, and no GPU lanes appear in the rollup for this head. - What was not done:
- The vLLM/rocm-core worker did not read the method's reference files before its pass.
- The prior-changes pass sampled only #251's review comments.
- Mutation checks were argued from the code, not executed.
- The PR's review discussion was not read, so this report is not reconciled against it.
The `Sphinx docs build (-W)` gate failed on: README.md: WARNING: 'myst' cross-reference target not found: 'docs/vllm.md#shared-or-busy-gpus' [myst.xref_missing] The heading does exist in docs/vllm.md, but this was the only anchored cross-file link in README.md -- every other reference into docs/ is a plain file link, and MyST does not resolve an anchor into that file from here. Dropped to match the existing convention rather than inventing a mechanism the rest of the file does not use. The sentence already names the section, so the reader loses nothing. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
One additional issue, still present at They pass Please document unfiltered |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 73dabf5
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.
Review — needs work
Full review of the whole change (prw-base...HEAD, 22 files).
Implements the out-of-memory memory-knob note from EAI-8059 (now ROCMAI-419, Sub-task, status QA). It also changes when rocm serve refuses on a host with no GPU, which that ticket rules out: "This changes diagnostics only, not serve behaviour." The ticket also lists Engine core initialization failed as a signature to detect; the change deliberately excludes that generic wrapper.
Round note: the only commit since the previous automated round (a5e259a0) is 73dabf50, a one-line README link change. This round is still a full blind review, because several open findings (description, missing tests) are not tied to one file. Each finding is marked new (first raised this round) or standing (raised at an earlier automated round and still present). Of the previous round's nine blockers, five were re-found independently by this blind review. The other four were not re-found. Each was put to the reviewer after its review had ended. That is an anchored check, not a blind re-finding. All four still hold, and they are listed last under Blocking. This change request replaces the earlier ones as the automation's complete position.
Blocking
-
[standing] The README link breaks the required Sphinx
-Wbuild —README.md:624
docs/rocm-docs/commands.mdpulls in README from## Commandsto## Contributing, so[docs/vllm.md](docs/vllm.md)is resolved inside the Sphinx tree, where no such page exists. CI at this head fails withREADME.md:9799: WARNING: 'myst' cross-reference target not found: 'docs/vllm.md'(check Sphinx docs build (-W), run 37930656620). The last commit, "link docs/vllm.md without an anchor Sphinx cannot resolve", did not fix it.
Confidence 100 · mechanical · Fix: follow the README's existing convention for files outside the Sphinx tree — an absolute GitHub URL (as atREADME.md:35) or a plain code span (as at:921-925). -
[standing] A non-colour escape at the start of a log line hides a real OOM, and the comment saying it cannot is false —
crates/rocm-core/src/lib.rs:4465-4470(vllm_log_segments),:4436-4438,:4521-4528;crates/rocm-core/src/terminal.rs:287-293,:357-367
Segments are split only on\n/\rand then scored asvllm: {segment}. The checker re-splits withrendered_lines, where every CSI escape except…mbecomes a line break (skip_csi_bodyreturns true only for final bytem).
Traced case: the segment\x1b[2KRuntimeError: HIP out of memoryrenders as two rows,["vllm: ", "RuntimeError: HIP out of memory"]. Only the empty first row carries the anchor, so the score is 0. Both the engine hint and the serve-summary note are then silently dropped.
This is a regression: on base,oom_utilization_hintgated on a substring scan and fired for this line. The same applies toESC[A,ESC E,\x0b,\x0cand\u{85}. The doc comment's "Scoring is unaffected, because the diagnostic strips its own copy regardless" holds only for colour (SGR) and string sequences. No test uses a non-SGR escape.
Confidence 85 · logic · Fix: anchor and score eachrendered_linesrow of the segment, and keep the raw segment only for the quotability gate. Add a fixture with a leading\x1b[2K. -
[standing]
vllm_log_shows_oomis dead in production, and the tests built on it test the wrong module —crates/rocm-core/src/lib.rs:4436;apps/rocm/src/serve_summary.rs:505-527;engines/vllm/src/process.rs:1069-1366(test uses)
A repo-wide grep finds only test callers, plus comments atserve_summary.rs:239,process.rs:765andmain.rs:3741that explain why production code avoids it. Both surfaces actually gate onvllm_oom_diagnose_symptom.
The function is new in this change andpub, so the compiler does not warn. Its doc also says it keeps "the pre-launch low-VRAM note … and diagnose" in agreement, but the pre-launch note (main.rs:~3672) is driven by telemetry, not by this function.
The twoserve_summary.rstests (oom_signatures_are_detected_case_insensitively,unrelated_failures_are_not_flagged_as_oom) call onlyrocm_core::vllm_log_shows_oom, so they would pass with everyserve_summary.rschange reverted.
Confidence 85 · mechanical · Fix: delete the function and move its threshold assertions ontovllm_oom_diagnose_symptom(..).is_some(). Or, if it is meant to be the shared predicate the PR description says it is, make both surfaces call it. -
[new] Reuse skips the no-GPU refusal without checking the reused service's device policy —
apps/rocm/src/serve_cmd.rs:~700-775;apps/rocm/src/main.rs:16699-16712,16738-16747
existing_live_managed_servicematches on engine, canonical model id and liveness only. The record'sdevice_policy, whichmain.rs:15804shows is stored, is never compared.
Sorocm serve <model>under the defaultgpu_required, on a host with no GPU or with every GPU masked byHIP_VISIBLE_DEVICES, now succeeds by reusing any live service for that model, including one launched--device cpu_only. Nothing tells the user it is not on a GPU.
AGENTS.md §6 requires "preserve strict GPU-required behavior; do not introduce silent CPU fallback". README's "The reused service was vetted against the policy at its own launch" is true of that service's own policy, not the caller's.
Confidence 70 (argued from the match code; I did not trace whether a CPU-only launch yields the same canonical id) · architectural · Fix: require the matched record's policy to satisfy the caller's, and its pinned GPU to still be visible. Or drop the exemption (see Decisions). -
[standing, extended] The documented reuse exemption does not match the code in two cases —
README.md:657-672;--helpdoc comment atapps/rocm/src/main.rs:441-445;docs/manual-testing.md:390-397;docs/testing.md:1294-1297- "before preparing any engine" and "those still fail fast … and prepare nothing" are false when the model names overlap.
any_live_managed_service_for_modelusesservice_model_names_match, a two-waycontains. A live Lemonade service forqwen3-8b-instructtherefore opens the pre-gate forrocm serve qwen, which runsensure_self_managed_engine_ready("Preparing…" plus install) andResolveModelbefore the refusal. The code itself admits this atmain.rs:19983-19996. serve-26 and the unit tests use only names that do not overlap. - (new this round) "Reusing … is the one exception: it launches nothing and pins no GPU, so it is allowed through" is false with an explicit
--gpu N.validate_pinned_gpu_indexstill runs after the bail and refuses when no GPU is visible (serve_cmd.rs:~802).
The code governs, because its matching relation is deliberately lenient and documented as such. AGENTS.md §3 requires a promise that something will not happen to be asserted together with the behaviour, and §5 requires each copy of a claim to be checked against the code path.
Confidence 85 · logic · Fix: narrow the README,--help, manual-testing and testing.md wording to "a model whose name does not overlap" and "default--gpu auto". Alternatively, tighten the pre-gate match. - "before preparing any engine" and "those still fail fast … and prepare nothing" are false when the model names overlap.
-
[standing] The PR description misstates the change — the PR body
- It says "One shared helper (
rocm_core::vllm_log_shows_oom) feeds both the engine hint and the CLI note". That helper has no production caller (see above). - It says "CI. Every lane is green at this head; there are no failing checks." Sphinx
-Wis failing at 73dabf5. - It says "Depends on #251 … targets that branch; it must not merge before #251 does". The base is
main, and #251 merged on 2026-10-06. - The Summary does not mention the user-visible change to the no-GPU refusal. README itself calls it "a real change to a refusal you may be relying on", and the ticket rules it out. It appears only inside "Changes since the last review". The move of the quoting and stripping helpers into
rocm-core/src/terminal.rsis also unmentioned.
Confidence 85 · mechanical · Fix: rewrite the description to match the change — the helper actually shared, the current CI state, base
main, and the refusal change stated up front. - It says "One shared helper (
-
[standing] In real use, the serve-summary note never quotes the user's own failing line —
apps/rocm/src/main.rs:4019-4028,:1425,:4389-4397;engines/vllm/src/process.rs:274-280,:787-791,:808-812;crates/rocm-core/src/lib.rs:4521-4528;README.md:618-621;docs/vllm.md:258-261
Not re-found blind this round. It was put to the reviewer after its review ended (an anchored check), and it still holds whenever vLLM runs out of memory within the 45s readiness wait:- The supervisor's stderr and vLLM's output share one log.
- On an early exit, the engine's error, including its own OOM hint, is the last text written (via
exit_code_for). - The CLI waits out the full 45s without checking whether the process is alive, and only then reads the tail.
- The newest-first scan selects the hint's own
For conditional remediation, run `rocm diagnose --symptom '…'`line. That line contains', so the note always falls back to the canonical symptom.
The README,
docs/vllm.mdand theoom_memory_notedoc all claim the user's real line. serve-25's fixture writes only the raw OOM line, so it cannot see this.
Static, not run · logic · Fix: ignore rocm's own hint lines (or read only up to the engine'sError:block) when choosing the symptom. Make the fixture include the engine's error block, or narrow the claims. -
[standing] The reuse pre-gate tests leave two of its three conditions untested —
apps/rocm/src/main.rs:16742,:16744; test helperreuse_pregate_forat:27623-27653
Not re-found blind this round. It was put to the reviewer after its review ended (an anchored check), and it still holds for both conditions:- The helper always plants a live record (
status starting, the test's own pid,:27646-27647). Deletingmanaged_service_is_live(record)keeps every test green. - In every positive case the
canonical_model_idarm matches on its own. Deleting therecord.model_refarm also stays green. - serve-26 is closed by the model mismatch alone, so it discriminates neither condition.
Static, not run · logic · Fix: add a case where only
record.model_refmatches (e.g. a canonical id that shares nothing with the query), and one with a stopped or dead record. - The helper always plants a live record (
-
[standing] The Given step for serve-25 does nothing; the setup happens in the When step —
tests/e2e-cucumber/tests/e2e/serving_steps.rs:1451-1459,:1477-1488
Not re-found blind this round. It was put to the reviewer after its review ended (an anchored check), and it still holds:plant_oom_launchhas an empty body. The fault is armed only byROCM_E2E_SIMULATE_OOM_LAUNCHin the When step, so deleting the Given changes nothing.
Static, not run · mechanical · Fix: have the Given record the arming in the world, and have the When consume it. -
[standing] The release-feature guard's doc claims more than it checks:
nightly.yml's published builds are not covered —xtask/src/workflow_contract.rs(the_release_workflow_builds_no_feature_gated_binariesand its doc comment);.github/workflows/nightly.yml:123,:226
Not re-found blind this round. It was put to the reviewer after its review ended (an anchored check), and it still holds:- The doc opens "Nothing shipped may be built with a test-only feature", but the test reads only
release.yml. nightly.yml:123and:226runcargo build --release -p rocm -p rocmd -p xtaskin the jobs that stage and upload the publicnightlyprerelease (:175/:198,:308/:311). Nothing guards those builds. They carry no features today.- The other nightly builds legitimately use
--features rocm/e2e-test-hooks, so a fix has to target only the publishing jobs, for example jobs that rungh release createorgh release upload.
Static, not run · logic · Fix: extend the check to
nightly.yml's publishing builds, or narrow the doc torelease.yml. - The doc opens "Nothing shipped may be built with a test-only feature", but the test reads only
Non-blocking
-
[standing] The reuse pre-gate also runs on hosts with GPUs, ahead of
--gpuvalidation —apps/rocm/src/serve_cmd.rs:701vs:796-801
The block's only purpose is to skip the no-GPU bail, but it runs whenever a matching live service exists. Runtime selection, Lemonade install andResolveModelcan then run, or fail, before the--gpucheck. The comment at:796says that check comes "before engine/runtime resolution".
Confidence 70 · logic · Fix: also requirevisible_gpu_indices.as_deref().is_some_and(<[u32]>::is_empty). -
[new] The note can tell a still-loading server it ran out of memory —
apps/rocm/src/serve_summary.rs:191-196;apps/rocm/src/main.rs:~3799-3830,~4390,19822;--helpatmain.rs:436-437
startingmeans "nothing answered on HTTP within 45 s", which a large vLLM model that is still loading also hits.append_oom_serve_notedoes not check whether the process is still alive.
An OOM-signature line in a live server's tail would therefore produce "the serve attempt ran out of GPU memory", against the help's "never shown for a … still-loading server". The log is truncated per launch (main.rs:4228), so an earlier launch cannot be blamed. I did not establish that vLLM emits recoverable allocator-OOM lines.
Confidence 70 · logic · Fix: passchild_pidand require the process to have exited before asserting, or soften the wording when it is alive. -
[new] The documented scenario commands select nothing —
docs/testing.md:1283-1285
cargo xtask e2e -- -n serve-oom-memory-guidancepasses an@idto cucumber's-n, which filters on the scenario name ("serve-24 - Reusing …"). The name does not contain the id. The working precedent at:1420uses-n diagnose-2, which is in the name.
Confidence 70 (based on cucumber-rs--namesemantics; I could not read the crate source locally) · mechanical · Fix: use-n serve-24etc., or a--tags '@id:…'filter. -
[standing, extended] Comments made stale by this change —
tests/e2e-cucumber/features/model_serving.feature:322,:353;.github/workflows/e2e-selfhosted.yml:329-330
The feature comments say "Positive counterpart of Scenario 23" and "scenario 23 plants the SAME model". serve-23 is the low-VRAM plan scenario; both mean serve-24.
(new this round) The workflow comment still says--features rocm/e2e-test-hooks"must match whatcargo xtask e2ebuilds". xtask now builds a superset and deliberately leaves the OOM hook out of prebuilt lanes;workflow_contract.rswas updated to say this and the workflow comment was not.
Confidence 85 · mechanical. -
[standing]
docs/vllm.mdstill says any OOM error triggers the hint —docs/vllm.md:~251
"When a startup failure log shows an out-of-memory error, the failure message suggests…". The change narrows this to lines that clear the checker's threshold: a bareHIP error: out of memoryor a kernel OOM-killer line no longer qualifies. README documents the narrowing; this file was edited in the same change but not for it.
Confidence 70 · mechanical.
Decisions for the author
- Whether to change the no-GPU refusal in this PR at all — tradeoff
- The feature file's own comment shows the exemption exists so serve-24 can reach the reuse summary on the no-GPU mock lane (
model_serving.feature:~311-314). That comment also says serve-24 cannot tell apart thealready_runningclause it is about; the unit testappend_oom_serve_note_ignores_an_already_running_services_logis what pins it. - For: a reuse launches nothing, and serve-24 then runs on every PR.
- Against: a shipped refusal is loosened (a GPU-less
rocm servecan now succeed), for test coverage, in a PR whose ticket says "diagnostics only". - The alternatives keep the refusal strict: unit coverage plus serve-24 on a GPU lane, or an e2e-only bail waiver like the existing
scripted_backend_failure.
- The feature file's own comment shows the exemption exists so serve-24 can reach the reuse summary on the no-GPU mock lane (
- The fault-injection seam proves rendering, not the launch path — non-blocking-improvement
- The
e2e-oom-fault-injectionhook returns atmain.rs:4347, before the spawn, the record claim, the readiness wait andstatus_for_readiness. serve-25 therefore proves that a hand-builtManagedLaunchReportrenders the note, not that a real failed launch produces one. - The vLLM launcher is the runtime manifest's
command(engines/vllm/src/process.rs:255). A fake launcher that writes the traceback and exits would drive the real path using only the existinge2e-test-hookswaiver, without a second feature and a second capability tag. - At least, say in the scenario and docs that it covers rendering only.
- The
- Scope — non-blocking-improvement
The PR does several things: the OOM note, the refusal change, extraction of the terminal module, ordering of--gpuvalidation, and the xtask release-feature guard. The refusal change is the piece that most needs its own review and discussion; consider splitting it out.
Positive signals
quotable_in_single_quotesnow lives once inrocm-coreand is used by both the engine hint and the serve summary. That removes the risk of two copies of the quoting rule drifting apart. The adversarial tests (apostrophe, control bytes, Cf, Zl/Zp) pin the fallback to the canonical symptom rather than to a stripped lookalike.- The managed log is truncated per launch (
main.rs:4228), and the reuse path never reads a log, so another launch's OOM is never blamed on this one. - The release-feature guard in
xtask/src/workflow_contract.rscan actually fail: it joins\continuations and catches--features,--all-featuresand both-Fspellings, with a test for each. OOM_FAULT_INJECTION_ENVis defined once ine2e-reportfor both xtask (producer) and the harness (consumer). The CI mock lane log shows serve-24/25/26 executing their steps rather than being skipped.
Deployment notes
- Behaviour change: on a host with no usable GPU, under the default
gpu_required,rocm serve <model>now succeeds when a live managed service for the same engine and model exists. Scripts relying on the refusal will see success. - New cargo feature
rocm/e2e-oom-fault-injection. It must never be enabled in a release build;workflow_contract.rsguardsrelease.ymlagainst it.
What this covered
What was read: every file in prw-base...HEAD (c1c2aeb…73dabf50, 22 files), read by six read-only workers in coupled groups:
- vLLM detection and the core terminal module;
- the serve path, summary and README;
- the e2e harness and xtask;
- docs checked against the code;
- history, prior PRs, AGENTS.md and code comments;
- three design questions.
Two of those reads were partial:
apps/rocm/src/main.rswas read by changed hunk plus the enclosing functions and the callers and callees of changed symbols.- For
apps/rocm/src/serve_cmd.rs, theserve()body and every hunk were read in full; the unchanged helpers and its test module were not.
Verification was static only, because the shared disk is critically low. No cargo build, test, clippy, e2e or Sphinx run happened. CI results were read with gh instead:
- Sphinx
-Wfails. It passes onmain(c1c2aeb), so this PR causes the failure. - Every other reported check passes.
- No self-hosted GPU-lane check appears in the rollup for this commit. The new scenarios are all
@requires-no-gpu, so no lane exercised them on GPU hardware.
Test verdicts (reasoned, not run):
- serve-24 would still pass with
append_oom_serve_notereverted (its own comment says so), but fails with the reuse exemption reverted. - serve-25 fails if the note is removed, but not if the
statusorenginegates are broken alone, because its record is fabricated. - serve-26 fails with the pre-gate reverted.
- The two
serve_summary.rstests above pass with that module reverted. - The rest of the new unit tests fail when their production change is reverted.
Also checked:
- Ticket EAI-8059 was retrieved: title, body and state only.
- The history and prior-change passes ran, with no reversal of work on main.
- Whether
rocm-engine-vllmstays above its coverage floor after moving tests out could not be established statically. - I found no prompt-injection content.
Not reconciled: the blind review was not compared against the PR's existing discussion. The four standing objections above were settled after it, as described in the round note.
Summary
Adds GPU out-of-memory (OOM) guidance to
rocm servefor vLLM: when a managedlaunch fails to become ready with a real allocator OOM signature in its own log,
the interactive deployment summary names the memory knobs (
--gpu-memory-utilization,--gpu <index>), says plainly that lowering the reservation does not help amodel that simply does not fit, and routes the user's actual failing line into
rocm diagnose --symptom '<line>'.Detection is deliberately narrow: the serve summary classifies each tail line with
the same rule as the
rocm diagnosevLLM-OOM checker (each line scored asvllm: <line>against the shared match threshold), so a bareout of memoryfroma kernel OOM-killer line, a dependency's log, or vLLM's generic
EngineCorewrapper never gets reported as memory exhaustion. One shared helper
(
rocm_core::vllm_log_shows_oom) feeds both the engine hint and the CLI note, sothe two surfaces cannot drift.
Behavior coverage
Scenarios this PR adds:
@id:)serve-oom-launch-memory-guidance(serve-25)@requires-oom-fault-injectionserve-oom-memory-guidance(serve-24)@requires-no-gpuserve-unrelated-live-service-still-fails-fast(serve-26)@requires-no-gpu@id:diagnose-vllm-oom-is-conditionaland@id:serve-vllm-low-vram-oom-guidance(
serve-23) come from the base branch (#251), not from this PR; this PR doesnot modify them.
The positive scenario cannot run a real launch on a GPU-less host, so the mock-lane
binary compiles in a test-only fault-injection hook (feature
e2e-oom-fault-injection,armed per child process by
ROCM_E2E_SIMULATE_OOM_LAUNCH) that fabricates exactlythe failed-launch state. The hook is absent from shipped binaries (
release.ymlnever passes the feature), so the scenario reports a skip on the self-hosted
(prebuilt-release) lanes rather than a silent green, and runs where
xtaskbuildsthe binary itself.
Verification, and what could not be run
Green at the pushed head on a Linux MI300X box:
cargo test --workspace --all-targetscargo clippy --workspace --all-targets -- -D warningscargo fmt --all --checkcargo xtask e2e— including this PR's own scenarios:serve-24(
serve-oom-memory-guidance),serve-25(serve-oom-launch-memory-guidance)and
serve-26(serve-unrelated-live-service-still-fails-fast) all pass, asdoes
serve-16(serve-absent-gpu-index-rejected), the laneevidence for the
--gpu-ordering fix below.CI. Every lane is green at this head; there are no failing checks.
83ecfaadchat-06"auto"tool choice needs--enable-auto-tool-choiceserve-01,serve-02resolved modellineserve-07,serve-08serve-13@requires-no-gpuscenario, forced onto a GPU host by the name filterserve-19serve-10,bench-04runtime-02Folder:line inexamineoutputScenario numbers in that table are positional as of
83ecfaadand havesince drifted;
serve-19there is@id:serve-vllm-low-vram-oom-guidance,which is
serve-23at this head.E2E tests (Strix Halo, WSL2)is a 24h runner timeout — infrastructure, not anassertion failure.
Changes since the last review
--gpuvalidation ordering (blocking).resolve_gpu_indiceshad movedafter the no-runtime bail, so
--gpu 99surfaced as "no active ROCm runtimeis configured" instead of an out-of-range argument error, and
serve-absent-gpu-index-rejectedfailed on the GPU lane. Index validation nowruns immediately after the no-usable-GPU pre-flight and before any runtime or
engine work; a GPU-less host still refuses with "no usable AMD GPU" first.
serve-16passes on the GPU lane at the reviewed head.OOM detector threshold reconciled with
rocm diagnose. The serve-summarydetector used a looser bare-substring scan than feat(vllm): Tackle out of memory errors (EAI-8058) #251's
check_16_vllm_oom,which deliberately scores a bare
out of memorybelowMIN_SCORE_FOR_MATCH.Both now classify with the same rule through a single shared helper. The note
also carries the shared-vs-dedicated caveat and points at
rocm diagnoseforthe case-appropriate fix, and the
--gpu-memory-utilizationwording iscorrected to "a fraction greater than 0 and at most 1" (the parser enforces
(0, 1], so<0-1>wrongly implied0was accepted).GPU fail-fast contract made true. The comment above the no-usable-GPU bail
promised no engine work could precede it, while the reuse detection above it
ran a
ResolveModelround-trip and — for a self-managing engine — anensure_self_managed_engine_readythat can print "Preparing lemonade for GPUserving..." and install. The pre-gate keyed on the engine alone, so a live
service for an unrelated model, which this invocation can never reuse, pulled
that work in front of the bail. It now also keys on the model
(
any_live_managed_service_for_model), using the same lenient name relation theservice-listing surfaces already use so a short-vs-canonical spelling still
reaches the probe (which then decides reuse authoritatively on the canonical
id). The comment now states the real contract instead of an absolute one.
Regression tests:
reuse_pregate_skips_engine_work_for_an_unrelated_live_model,reuse_pregate_admits_the_same_model_including_a_short_spelling,reuse_pregate_still_keys_on_the_engine.Shared log-tail budget actually shared. The serve summary's comment claimed
it read the same budget as the engine's OOM surface, but
engines/vllm'sSTARTUP_FAILURE_LOG_TAIL_LINESwas an independent80literal. It is now derived from
rocm_engine_protocol::DEFAULT_LOG_TAIL_LINES,so a change to the protocol budget moves both surfaces and the claim holds.
xtask feature collision.
cargo xtask e2ebuilds the mock-lane binary witha single
--features "rocm/e2e-test-hooks rocm/e2e-oom-fault-injection"valueinstead of overwriting
e2e-test-hooks(fix(lemonade): retry interrupted backend setup #249).workflow_contract.rs'sprebuilt-lane assertion is a
containson--features rocm/e2e-test-hooksandstill holds; its doc and failure message now describe that string as the shared
hook floor (xtask builds a superset) rather than an exact match, and record
why the fault-injection hook is deliberately absent from prebuilt lanes.
Duplicated env constant.
ROCM_E2E_OOM_FAULT_INJECTIONwas declared twice,in
xtask/src/e2e.rsandtests/e2e-cucumber/src/capability.rs, held togetheronly by a "kept in sync" comment — a typo in either would have silently turned
the scenario into a skip. Hoisted into the shared
e2e-reportcrate bothalready depend on; the two former copies re-export the single source.
Negative assertion no longer vacuous. "does not blame this invocation for
GPU memory" collapses whitespace exactly like the positive checks, so it catches
a note that is wrongly rendered and soft-wrapped across the 80-column PTY grid
instead of passing against a literal substring that could never match.
Scenario numbering. This PR's scenarios are
serve-24/serve-25/serve-26at this head. They wereserve-20/serve-21when written; thebase has since inserted scenarios ahead of them, so the numbers drifted — and
the base's own low-VRAM scenario is now
serve-23. The stable@id:tags areunchanged and are what to match on — expect the numbers to drift again before
this lands.
Coordination
#290's diagnose catalog has merged, so
rocm diagnose's vLLM-OOM checker and thisPR's serve-summary detector now share one classification rule rather than being
two independent detectors.