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 |
…tall `engines/vllm/src/process.rs`'s test module referenced `MAX_TAIL_READ` by bare name; main dropped the `use crate::state::MAX_TAIL_READ` import from that file in favour of the fully-qualified path, and git merged both sides without a marker. Qualify the two call sites the same way. Also fix the duplicated `## 8` heading in docs/manual-testing.md left by this branch's section insertion. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
#683 landed a whole-tree ASCII contract after this branch forked. The merge took both sides with no marker, so the OOM note's em dash and the one in `rocm serve --help` only showed up as a red `every_literal_that_can_reach_a_redirectable_sink_is_ascii` and `every_help_text_in_the_command_tree_is_ascii` on a real build. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Split done — the reuse exemption now ships as #703Taking the scoping finding: ROCMAI-419 says this changes diagnostics only, not Removed from this branch (
Nothing was lost by giving up Numbering. #683 merged after this branch forked and took Two silent semantic conflicts from the
Gate at The |
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # crates/rocm-core/src/lib.rs # docs/manual-testing.md
AGENTS.md section 6 says a new subsystem defaults to its own file from day one rather than growing inside lib.rs awaiting a future extraction pass. This PR's OOM diagnostics added 536 lines to crates/rocm-core/src/lib.rs, the file #666 had just shrunk by 2726 for exactly that reason. Pure code motion, verified byte-identical: the two shared text markers, the three private helpers, the two public entry points, and the five tests plus their log_shows_oom helper move together; no logic, naming, or assertion changes. Call sites keep their rocm_core::<name> paths via a lib.rs pub use, matching the pub mod + pub use convention every sibling module in this crate already follows. docs/architecture.md's rocm-core inventory gains the new file. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…move
Six links in the relocated doc comments resolved through lib.rs's
`pub use terminal::{...}` / `pub use diagnose::{...}` re-exports, which are
not in scope inside vllm_oom.rs; `cargo doc` with
-D rustdoc::broken_intra_doc_links reports each as unresolved. Giving them
explicit paths restores resolution with the rendered link text unchanged.
Text-only; no code, assertions, or behavior touched.
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · ebc2671
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.
This implements the ROCMAI-419 / EAI-8059 OOM guidance: the serve-summary note, the engine hint and the rocm diagnose routing. The reuse no-GPU exemption is no longer in this branch; the description says it moved to #703. The ticket itself was not retrieved (see What this covered).
Blocking
- [standing, widened] The squash body will describe work this change does not contain (commit history from
prw-base..HEAD)- The PR description says this repo builds the squash commit body from the commit messages. That is also the documented merge mechanic for this repo.
- About 16 of the 77 non-merge commits describe behaviour that is absent from the final diff:
- Reuse pre-gate,
cpu_onlyrefusal and serve-26..30 reuse scenarios:6b6cd18,76b0580,1de6135,a3fb51c,d0fee30,a1e15a8,f49f670(part).5567142removed all of this. --gpuvalidation against sysfs/DRM/usable devices, and auto-select:38257fe,af059ae,235adb9,7c09129,73b8bfb,ba2b741,7e4f664,90ce2a6. The finalmain.rsdiff adds onlyappend_oom_serve_noteand its tests; novalidate_pinned_gpu_indexor usable-set change remains.956891f, the already-merged #290: its message says the fix-16 checker is "gated to Linux".13bd32clater reversed that.
- Reuse pre-gate,
- The description's banner asks the merger to fix one false sentence in
1de6135. The problem is much wider: the commit landing onmainwould claim a GPU-validation and reuse-exemption change that it does not make. - Confidence 85 · architectural · Fix: at merge time, supply a squash body written from the final diff (OOM note,
vllm_oom.rsreader, quoting guard move, fault-injection feature and release guard, serve-26, docs). Do not inherit the per-commit trail. - Driver note — why this now gates when the
a3fb51cdround did not:- That round gated nothing on one false sentence in
1de61359, expecting the merger to reword it. - Two things are new. Since the split, about 16 commits narrate work that is not in this diff. Some of it is in #703;
validate_pinned_gpu_indexis already onprw-base, so those commits re-narrate #251. - The repository's squash setting is
COMMIT_MESSAGES, read from the API. The driver knows of no merge-queue route that edits the body, so the banner's "reword at merge" cannot be relied on, and these sentences reachmain. - What lifts this block: rewrite the branch into a few commits whose messages describe the final diff, then force-push. A merger can instead commit to writing the body by hand on a path that allows it; if so, say so here.
- The code itself is not in question.
- That round gated nothing on one false sentence in
Driver: the a3fb51cd round's other non-gating objections are settled.
- The reuse exemption is out of scope, and three of its refusals were never exercised through
serve().- Resolved. This round did not re-find either one blind. A separate check against the code then found none of the reuse code at this head.
- Both describe code that now lives in #703.
- The description claimed
terminal.rshelpers moved.- Resolved. The description now says the file already existed.
Non-blocking
- [new] A doc comment's example contradicts the new rule —
crates/rocm-core/src/vllm_oom.rs:190-199- It says a bare
RuntimeError: ... killed: out of memorythat follows a detailedtorch.OutOfMemoryError"wins" and that "both are diagnosable". - A worker's run showed the opposite:
vllm_oom_symptom_is_diagnosable("vllm: RuntimeError: worker killed: out of memory")returnsfalse, and the selector returns the torch line.sub_threshold_lines_carry_no_hint_at_allasserts the same; the code governs. - "Callers print the whole log tail beside the hint" is false for the
rocm servenote.terminal.rs'squotable_in_single_quotesdoc says so itself. - Confidence 100 · mechanical · Fix: use two lines that both score as the example, and scope the "whole tail" sentence to the engine surface.
- It says a bare
- [new] A comment cites a path that does not exist —
engines/vllm/src/process.rs:684- It says "See
rocm_core::vllm_log_segments". That function is private, atrocm_core::vllm_oom::vllm_log_segments(crates/rocm-core/src/vllm_oom.rs:140). - Confidence 85 · mechanical · Fix: correct the path.
- It says "See
- [new] The same two-reads / own-line-skip / quotable-implies-diagnosable rationale is restated in about six places
- Locations:
vllm_oom.rs(module doc,is_rocm_authored_log_line,segment_shows_oom,vllm_log_segments,vllm_oom_diagnose_symptom),terminal.rs,process.rs(oom_utilization_hint,startup_log_context), twodiagnose.rsdoc comments,serve_summary.rsanddocs/vllm.md. - The first finding above shows one copy has already drifted.
- Confidence 70 · mechanical · Fix: state it once in the
vllm_oommodule doc and link to it from the others.
- Locations:
- [new] serve-26 leaves a
rocm daemonrunning after the scenario ends —apps/rocm/src/service_launch.rs(thesimulate_oom_managed_launchreturn),apps/rocm/src/serve_cmd.rs(ensure_background_helper_running_quietafterstart_managed_service),tests/e2e-cucumber/tests/e2e/serving_steps.rs:1317-1340- The fault hook returns a launch report and
servethen starts the background helper. No teardown stops it, so it keeps watching astartingrecord whose pid has exited. - Confirmed on the review host:
rocm daemonprocesses from earlier serve-26 runs of this PR are still alive after more than 36 hours. Their binaries come from deleted checkouts, and their environment hasHOME=/tmp/rocm-e2e-*andROCM_E2E_SIMULATE_OOM_LAUNCH=1. - A companion daemon without that variable suggests the leak predates this PR for other managed-serve scenarios. This change adds one more.
- Confidence 85 · logic · Fix: kill the scenario's daemon in teardown, or skip starting the helper when the simulated launch is armed.
- The fault hook returns a launch report and
- [new]
docs/testing.md:1592-1601says no focused command can run serve-26 correctly- It claims a run "would run on a binary built without the hook". A worker ran
cargo xtask e2e -- --name "serve-26": xtask builds with the hook and sets the capability, and the scenario passed 5/5 steps with exit 0. The CI dispatch filter uses the same route. - The risk is real only with a prebuilt
ROCM_CLI_BINARY. - Confidence 100 · mechanical · Fix: document the xtask
--nameroute and state the prebuilt-binary caveat.
- It claims a run "would run on a binary built without the hook". A worker ran
- [new] Workflow comments are now stale —
.github/workflows/nightly.yml:434,524,600,682,769,1026,.github/workflows/e2e-selfhosted.yml:333,511,846,1399,1581- They still say the
e2e-test-hooksset "must match whatcargo xtask e2ewould build". xtask now also enablese2e-oom-fault-injection, andxtask/src/workflow_contract.rswas relaxed to check only the shared floor. - This is the AGENTS.md §5 "same claim repeated across surfaces" rule. The description lists it as a deferral.
- Confidence 85 · mechanical.
- They still say the
- [new] The release-feature guard covers
release.ymlonly —xtask/src/workflow_contract.rs(the_release_workflow_builds_no_feature_gated_binaries)nightly.yml:129,232publishes prereleases and is not checked. No path enables the feature there today.- The workspace clippy job never compiles the
#[cfg(feature = "e2e-oom-fault-injection")]code atservice_launch.rs:567-639. A local run with that feature was clean (exit 0). - Confidence 85 · architectural · Fix: follow-up guard over the nightly publishing jobs, and a clippy invocation with the feature on.
- [new]
rocm serve --helppromises the user's own failing line without the fallback caveat —apps/rocm/src/main.rs:473-483- The command falls back to the canonical symptom when the line carries a quote or control bytes. The README states this;
--helpdoes not. - Confidence 70 · mechanical · Fix: add the clause to
--help.
- The command falls back to the canonical symptom when the line carries a quote or control bytes. The README states this;
- [standing] New note-gating logic grows
main.rs—apps/rocm/src/main.rs:3733-3831(append_oom_serve_note, plus about 230 lines of tests)- AGENTS.md §6 asks new domain logic not to grow
main.rs. Its natural home,serve_summary.rs, already holdsoom_memory_noteandserve_failed_to_become_ready. - Confidence 70 · architectural · Fix: move the function and its tests there.
- AGENTS.md §6 asks new domain logic not to grow
- [new] The description is stale against the head
- Its Verification section reports the full gate at
7bc22302.d4a1cc4andebc2671came after it and extract a new module,crates/rocm-core/src/vllm_oom.rs, which the description never mentions. The description also still says, in its own words, that no new file was involved. - The "Changes in the latest round" sections describe serve-29 and reuse-gate work that is no longer in the branch. A banner discloses this, but those sections still read as current.
- Confidence 85 · mechanical · Fix: add the
vllm_oom.rsextraction and re-state verification at the current head.
- Its Verification section reports the full gate at
Decisions for the author
- The OOM note only reaches interactive users — tradeoff
- The note renders only in
summary_mode(background serve on a terminal). Scripted, CI and assistant runs takeprint_managed_launch_plainand get no post-launch OOM confirmation or diagnose command. The comment atserve_cmd.rs(theappend_oom_serve_notecall) documents this. - Machine-readable output stays prose-free. The same failed launch is explained in one invocation mode and not the other.
- The note renders only in
- The fault injection is a second Cargo feature rather than part of
e2e-test-hooks— tradeoff- Keeping it separate isolates the hook. It also creates the two guard gaps above, plus stale workflow comments, which a single feature would not have.
- The serve note narrows to the diagnose threshold — tradeoff
RuntimeError: HIP error: out of memory(raised whenhipMallocfails outside torch's caching allocator) now gets no hint at all, where it used to get the canonical one.- This keeps the note consistent with
rocm diagnoseand is documented indocs/vllm.md. Some genuine OOMs now go unexplained.
Positive signals
- The own-line filter is keyed off the shared
VLLM_OOM_DETECTED_PREFIX/VLLM_OOM_DIAGNOSE_COMMANDconstants, so the hint and the filter that skips it cannot drift apart. - The tests hold up under mutation. Over about 30 single-branch mutations across
rocm-core,rocm-engine-vllm,rocmandxtask, every new test failed when the branch it covers was broken. The only survivors were the.trim()invllm_log_segmentsand the redundant outer status check inappend_oom_serve_note. - The
fix.rswording pin names the test that proves the behaviour, as AGENTS.md §3 asks.
Deployment notes
- The vLLM engine's startup-failure text changes. The header becomes "Startup log, last N lines as the terminal drew them", the tail is echoed as rendered rows, and the hint no longer fires for a bare
HIP error: out of memory, a kernel OOM-killer line or vLLM'sEngineCorewrapper. No scenario covers this text; the description says so. - New Cargo feature
e2e-oom-fault-injectioninapps/rocm. It must never be enabled in a published build, and onlyrelease.ymlis guarded.
What this covered
- Read: every file in
prw-base...HEAD, completely, split across two independent workers.- The vLLM OOM core:
vllm_oom.rs,terminal.rs,lib.rs,diagnose.rs,fix.rs,engines/vllm/src/process.rs,docs/vllm.md, andskills/rocm-doctor/reference.mdfor consistency. - Serve, xtask and e2e:
main.rs,serve_cmd.rs,serve_summary.rs,service_launch.rs,Cargo.toml,xtask,e2e-report,e2e-cucumber, README anddocs/*. - Commits:
6186f159…ebc26718.
- The vLLM OOM core:
- What was run:
- Unit suites for
rocm-core(135),rocm-engine-vllmprocess::(43), and therocm,xtaskande2e-cucumbergroups, plus single-branch mutation runs. cargo xtask e2e -- --name serve-26.- Feature-on clippy for
rocm.
- Unit suites for
- CI: all checks are green except "E2E tests (Strix Halo, Windows, current)".
- The failed scenarios are setx PATH, full model name ×2 and the nested runtime folder. None of them touches the OOM path.
- The reviewer found the same four failing on
main's 2026-10-16 runs. The driver confirmedmain's own run of this lane atc7a0d65falso failed. This does not gate. - CI ran on 2026-10-17. The head is now 9 commits behind
prw-base; it merges cleanly withgit merge-tree, but the merged result was not built or tested here.
- Not run:
- The full e2e suite, and any Windows-target build or check.
- The prior-changes pass: earlier PR reviews are deliberately out of bounds for this run.
- The ROCMAI-419 / EAI-8059 ticket was not retrieved: no tracker tool was available.
- Fan-out: two workers, per the operator budget. They shared one checkout and briefly raced on a mutation; the affected run was repeated on a clean tree.
- Prompt injection: none found in the checkout.
- Reconciliation: the findings were not reconciled against the PR's discussion. The driver settled the earlier objections afterwards, as noted under Blocking.
nowycondro
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · ebc2671
Review — no blocking findings
Read the full diff between main (merge-base d4e7c751) and head ebc26718 (25 files, +3078/-284): the new crates/rocm-core/src/vllm_oom.rs OOM-selection module, the terminal.rs quoting guard it shares with rocm-engine-vllm, the engines/vllm/src/process.rs rework, the new apps/rocm wiring (serve_summary.rs, serve_cmd.rs, service_launch.rs, main.rs), the e2e fault-injection plumbing (xtask, e2e-report, tests/e2e-cucumber), and the README/docs updates. This implements the OOM-guidance half of ROCMAI-419 (ex EAI-8059); the serve-behaviour (reuse-exemption) half is stated to have moved to #703, and I confirmed the current diff carries none of that logic (no reuse_pregate_opens/managed_service_reuse_refusal/gpu_preferred hunks remain). Ticket ROCMAI-419 is named but not retrievable from this environment (no tracker access here), so I could not check it directly — the assessment below is against the code and the PR's own description only.
Overall this is unusually well-substantiated: I spot-checked several of the description's more specific claims against the actual code/history (the (0, 1] domain of parse_gpu_memory_utilization, the ready/running/starting mapping in status_for_readiness, the already_running ⇒ log_path: None invariant, the "duplicate tail reader is pre-existing and already used for service logs" claim, the release.yml/nightly.yml feature-flag claims, and the presence of commits 1de61359/a3fb51cd in history) and every one held up. I did not find a defect that survives verification.
Blocking
None
Non-blocking
None
Decisions for the author
append_oom_serve_note's log read uses the UTF-8-strictread_tail_lines(viaread_optional_tail_lines), not the lossyrocm_core::tail_linesthe engine's own OOM detection uses — non-blocking-improvement. If a service log ever contains invalid UTF-8 near the failure (a genuine possibility for a crashing subprocess's output), this call site returns a single<failed to read ...>placeholder and the OOM note silently disappears, while the engine's own hint (which reads viarocm_core::tail_lines) would still have fired. I verified this is not a new risk introduced here, though:read_optional_tail_lineswas already used onmainfor this exact kind of data (main.rs's existingrocm services logs-style service-log read at the pre-PR equivalent of line 6749), and the PR's own "Known deferrals" section already names the duplicate-tail-reader gap and explains why unifying it is out of scope for this change. Flagging only so the asymmetry with the engine-side reader is visible in one place.- The CLI's OOM-note read window can miss an OOM the engine itself diagnosed, when the engine's own appended bail is long enough to push the raw line out of the shared tail budget — answered-by-intent. This is a real, measured gap (engine and CLI share one log but read it at different points), and the PR's own text states it plainly: "the obvious fix trades one defect for another... I would rather raise it than guess at it". It's pinned by a dedicated regression test (
the_cli_window_can_miss_an_oom_the_engine_saw) so a future fix can't silently regress the case it's trading away, and it's called out in the README's "absence is not evidence" list for users. Would otherwise be a blocking gap in a less transparent PR; here it's fully disclosed with a structural fix proposed (recording the engine's selected symptom as data) and explicitly deferred. Confirm that's still the intended scope for this PR. - The release-feature-leak CI guard (
the_release_workflow_builds_no_feature_gated_binaries) only scopesrelease.yml, notnightly.yml's own publishing build — tradeoff. I checkednightly.yml's twocargo build --release -p rocm -p rocmd [-p xtask]publish-path invocations directly and confirmed neither currently passes any--features/-Fflag, so there's no live leak today. The PR text already states why a blanket scan ofnightly.ymlisn't the right fix (that file legitimately builds--features rocm/e2e-test-hooksfor its own E2E lanes). Worth a follow-up guard aimed specifically at the publish steps if that file is expected to stay feature-free going forward, but not something this PR needs to carry.
Positive signals
- The shared-rule design (
rocm_core::vllm_oom_diagnose_symptom/vllm_oom_display_lineused identically by the engine's startup-failure hint and the CLI's serve summary) is exactly the right way to prevent the two surfaces from silently drifting apart on what counts as a vLLM OOM, and it's backed by tests that exercise the real two-read sequence (engine writes, then CLI re-reads the same growing log file), not just each function in isolation. is_rocm_authored_log_lineand its regression test (the_engine_hint_is_never_quoted_back_as_the_users_own_line) catch a genuinely subtle self-referential bug: without it, the newest-first scan would always select rocm-cli's own previously-written hint sentence (which contains') instead of the user's real error, silently defeating the entire "quote the user's own failing line" feature in every real run.xtask/src/workflow_contract.rs's newthe_release_feature_guard_catches_every_spellingtest is a nice piece of defensive test design — it pins the guard against compact (-Fname) and line-continuation-wrapped--featuresspellings specifically because those are the forms a real workflow edit is likely to produce, not just the obvious case.
Deployment notes
gh pr checksshows one failing check:E2E tests (Strix Halo, Windows, current)(self-hosted hardware lane). I could not fetch its job logs from this environment (blocked), so I can't confirm whether it's related to this change or a pre-existing flake on that runner. The new scenario here (serve-26) is explicitly@requires-os:linux, and vLLM-OOM detection code is cross-platform (compiled for Windows too), so I'd want this confirmed green or explicitly identified as an unrelated flake before merge. Every other check (Build Linux PR binaries, clippy, Analyze rust/python/actions, CodeQL, Commit signatures + sign-off, Test (affected crates), E2E consolidated report, the three other E2E self-hosted lanes including MI300X, Markdown links, license headers, docs build) is green.- The new
e2e-oom-fault-injectionCargo feature is test-only and is verified compiled out ofrelease.ymlby a new CI test in this PR; it's deliberately absent from the self-hosted/nightly prebuilt-binary lanes, where the new scenario reports a skip rather than a false pass. - No schema/migration concerns; this only changes diagnostic text and adds a dev/test-only feature flag.
What this covered
Read the full diff (merge-base d4e7c751194d1643eacc3b1bda33afe10766def73 … head ebc267182a7d77042f1f27a72e1bc41680d7b7e6) across all 25 changed files, completely, plus the surrounding non-diff code needed to verify specific claims (parse_gpu_memory_utilization, status_for_readiness, ManagedLaunchReport's reuse short-circuit, summarize_startup_log_tail, collect_serve_notes' hint insertion, and nightly.yml's publish-build invocations). Checked gh pr checks for CI status. This was a single-context review without subagent fan-out, per this task's instructions — coverage is accordingly narrower than a split multi-pass review would achieve, though the change's small number of tightly-coupled modules and its unusually extensive self-documentation made single-pass verification tractable here.
Did not run: cargo test/cargo clippy/cargo fmt (no Rust toolchain or package-registry network available in this environment) — relied on the CI results reported above instead of re-deriving them. Did not retrieve the named ticket (ROCMAI-419 / ex EAI-8059) — no tracker tool available here. Not reconciled against the existing human review thread on this PR (from r0x0r) or its many prior automated rounds; this is a fresh, independent read of the current head only.
jussielo-amd
left a comment
There was a problem hiding this comment.
🔴 AI-assisted review (Claude Code) · head ebc26718
This is an assisted human review, not an autonomous merge gate: no GitHub approval or change-request is being filed — the merge decision stays with maintainers.
Review — needs work
Full review of the PR's own diff (base origin/main 3dec0eb2, merge-base d4e7c751, head ebc26718; 25 files, +3078/−284). I did not narrow it to what changed since my earlier review at a3fb51cd, because four of that review's five findings could not be tied to a single file.
The ticket named in the description could not be retrieved from here, so the ticket's stated scope was not checked. The description's own scope claim is "diagnostics only".
Blocking
- The head cannot merge as it stands, and one check on it is red — PR state
CONFLICTING;tests/e2e-cucumber/features/model_serving.feature:358,docs/testing.md(sixserve-26references),tests/e2e-cucumber/src/capability.rs,tests/e2e-cucumber/tests/e2e/serving_steps.rs- Conflicts.
git merge-treeagainst currentmainshows 12 conflict hunks across 10 files:README.md,docs/vllm.md,engines/vllm/src/process.rs,xtask/src/workflow_contract.rsand six e2e-harness files. - Scenario id.
mainhas since landedserve-26(AIM inference) andserve-27inmodel_serving.feature, and this PR's OOM scenario is alsoserve-26. After a rebase,tests/e2e-cucumber/tests/feature_naming.rs(scenario_names_are_indexed_sequentially_per_feature,scenario_indexes_are_unique_across_the_suite) goes red until it is renumbered. - Red check.
E2E tests (Strix Halo, Windows, current)failed 4 of 145 scenarios (serve-01,serve-02,diagnose-38,runtime-02); the other 29 checks passed and 2 were skipped. That run is from 2026-10-16 against the pre-rebase head,main's Windows job was green on 2026-10-29, and the job log shows pre-warm download failures (llama.cpp HTTP 404, TheRock HTTP 403). The PR's net change toserve_cmd.rsis only the test-hook bypass plus theappend_oom_serve_notecall, so I could not tieserve-01/serve-02to this diff by reading. I did not reproduce the failures. A red check still blocks until it is re-run on a rebased head. - Confidence 100 · mechanical · Fix: rebase onto
main, renumber the scenario to the next free index (serve-28) and update every reference, then re-run CI.
- Conflicts.
Non-blocking
-
The serve note vanishes silently on a log with one invalid UTF-8 byte —
apps/rocm/src/main.rs:3823,:7569,:7641
append_oom_serve_notereads the log withread_optional_tail_lines/read_tail_lines, which useBufRead::lines(). That returnsErron invalid UTF-8, the error becomes one<failed to read service log…>line, andvllm_oom_diagnose_symptomreturnsNone. The engine reads the same file withrocm_core::tail_lines, which decodes lossily on purpose (crates/rocm-core/src/log_tail.rsexplains why), and the CLI path also reads the whole file unbounded. The description defers this as "wider … six call sites", but that reason does not apply to a change local toappend_oom_serve_note, which touches none of the other five. No test plants an invalid byte.
Confidence 85 · logic · Fix: callrocm_core::tail_lineswith a byte cap there. The engine's reader governs, because it is the shared one and says why it is lossy. -
The summary can print "it may still be loading" and "ran out of GPU memory" together, and the help text promises that cannot happen —
apps/rocm/src/serve_summary.rs:170-181,main.rs:472-483, README "failed to become ready" bullet
status_for_readinessmaps an unreachable endpoint tostarting, sostartingmeans only "not answering at the 45 s timeout".render_summaryalways adds the generic "may still be loading" note for it, and the OOM note then states the attempt ran out of memory. The--helptext says the note is "never shown for a healthy or still-loading server", while the description's own "Known deferrals" concedes it "can in principle fire on a still-loading server". The README's list of conditions also omits timing: the log is read once, after the fixed 45 s wait, so an OOM that happens later yieldsstartingwith no note.
Confidence 70 · logic · Fix: the code governs (the gate isstartingplus log text). Narrow the help and README to say therunningstatus is excluded, add the timing condition, and drop or qualify the generic "may still be loading" line when the OOM note applies. -
A doc paragraph on
vllm_oom_diagnose_symptomis false —crates/rocm-core/src/vllm_oom.rs:193-199
It says that after a real allocator line, a bareRuntimeError: … killed: out of memoryline "wins" and that "both are diagnosable". A bareout of memoryscores 25, belowMIN_SCORE_FOR_MATCH(50) inKEYWORDS_VLLM_OOM, and the module's own test asserts!log_shows_oom("HIP error: out of memory"), so such a line is never selected. The comment was carried over from the old loose detector.
Confidence 85 · mechanical · Fix: the code and its tests govern, so rewrite the paragraph. -
terminal.rsdocs are stale, and the stripper now has no reachable production caller —crates/rocm-core/src/terminal.rs:13-15,:409-416,vllm_oom.rs:251-257,lib.rs:176
The module doc still says "The vLLM engine strips the sequences out". The stripper's doc says "every caller here has already split its input into lines", which is false for the one production use left and is exactly the welding hazard this PR fixes. After this PR the stripper's only non-test reference is theunwrap_or_elsefallback invllm_oom_display_line, and both production callers pass a segment the selector already chose, so that branch is unreachable from them. The root re-export atlib.rs:176is consumed only by aprocess.rstest.
Confidence 75 · mechanical · Fix: update both docs, and drop the re-export or the fallback. -
Comments narrate review or edit history, or cite a path that does not exist —
engines/vllm/src/process.rs:684,crates/rocm-core/src/fix.rs(hint test, ~2723-2736),crates/rocm-core/src/lib.rs:336-340,tests/e2e-cucumber/tests/e2e/serving_steps.rs:1303-1304,crates/e2e-report/src/lib.rs:33-36,model_serving.feature:344
process.rs:684citesrocm_core::vllm_log_segments, which is private tovllm_oomand not a crate-root path. Thefix.rstest andlib.rscomments narrate branch and merge history, and the others say "previously …" or "(a review asked for it …)".
Confidence 75 · mechanical · Fix: state the invariant and drop the history. -
The engine and the CLI each hand-render the hint and command, so "the same guard" holds by convention only —
engines/vllm/src/process.rs:713-726,apps/rocm/src/serve_summary.rs:245-268,apps/rocm/src/service_launch.rs(~611-627),vllm_oom.rs:99,216,255
Both consumers repeatif quotable { s } else { VLLM_OOM_CANONICAL_SYMPTOM }and build`{VLLM_OOM_DIAGNOSE_COMMAND} '{symptom}'`by hand. The"vllm: "prefix is a bare literal in four places, andprocess.rs:714doesstrip_prefix("vllm: ").unwrap_or(&symptom). The e2e fixture inservice_launch.rshand-copies the engine's bail text, and only the two marker constants are shared, so the engine header can change whileserve-26stays green.docs/vllm.mdandterminal.rsclaim neither surface can hand out a command the other would reject. Separately,origin/mainnow hascrates/rocm-core/src/quoting.rs(#743); this branch predates it, and the new reject-not-escape predicate does not reference it.
Confidence 70 · architectural · Fix: one function invllm_oom.rsthat returns the rendered hint or{display_line, command_symptom}, used by the engine, the serve note and the fixture. -
New serve logic and about 220 test lines went into the 21k-line
main.rs—apps/rocm/src/main.rs:3771-3830, tests ~:17835-18050
serve_summary.rsalready holds the siblingoom_memory_note, and AGENTS.md §6 says new domain code defaults to its own file rather than growingmain.rs. I rate this lower than the earlierlib.rsfinding because it extends an existing serve-domain file's subject rather than adding a new subsystem.
Confidence 70 · architectural · Fix: moveappend_oom_serve_noteand its tests besideoom_memory_note. -
serve-26pins the note's wording without proving the command it prints —serving_steps.rs:1345-1418,model_serving.feature:358-363
AGENTS.md §3 requires remediation advice naming a command to be asserted together with the behaviour, or the wording pin to carry a comment naming the proving test. None of the threeThensteps does either. Runningrocm diagnose --symptom '<printed line>'by hand routes tofix-16-vllm-oom, and thequotable ⇒ diagnosablecontract is covered byevery_rendered_line_break_keeps_the_gate_in_step_with_the_scorer, so the claim is true today but unpinned in the scenario.
Confidence 70 · logic · Fix: add a step that runs the printed command and assertsfix-16-vllm-oom, or add the pointer comments. -
The last block of
the_colourised_round_trip_quotes_the_row_the_terminal_drewtests nothing in production —engines/vllm/src/process.rs:1499-1521
Theinside_echofilter is built inside the test and then asserted to find nothing. It documents a rejected design as executable text, under a comment saying it "forecloses the tightening". Test verdicts: that block passes with the production change reverted, because it exercises none of the production code; the first half of the test does go red.
Confidence 85 · mechanical · Fix: delete the block and keep the comment as prose. -
The "never reaches a shipped binary" guard covers only
release.yml, and the docs list the wrong test —docs/testing.md~1578-1583 and ~1633-1636,xtask/src/workflow_contract.rs:3934-3957,xtask/src/e2e.rs:108-119,.github/workflows/ci.yml:709,925
docs/testing.mdlistscargo test -p xtask prebuilt_e2e_lanes_enable_test_hooksunder "The fault-injection feature must never reach a shipped binary", but that test asserts the prebuilt lanes do enablee2e-test-hooks, the opposite purpose. Onlythe_release_workflow_builds_no_feature_gated_binariesguards release, and it readsrelease.ymlonly; the PR's downloadable bundle builds inci.ymlare unguarded, and the description concedesnightly.ymlis. Comments inxtask/src/e2e.rs,serving_steps.rs:1181and the workflows still say the lanes must "match whatcargo xtask e2ebuilds", but xtask now builds a superset. The feature is off by default andrelease.ymlpasses no flags, so I see no leak today; nothing proves a default build ignores the arming env var.
Confidence 75 · mechanical · Fix: correct the docs, extend the guard to every workflow build that is packaged, and add a no-feature test that the env var is inert. -
The description is not the final diff — PR body
The verification section is pinned to head7bc22302, and the head has since moved (thevllm_oom.rsextraction, a doc-link fix and anothermainmerge). Large sections describe code that is no longer in this branch (the reuse pre-gate,reuse_pregate_opens,serve-25/27/28/29); the body says so, but AGENTS.md §5 asks for the body to be written from the final diff. The body also names internal review-automation tools, which AGENTS.md §2 asks to keep out of upstream text, and it does not name the newvllm_oom.rsmodule.
Confidence 75 · mechanical · Fix: rewrite the body from the final diff and re-run the verification at the new head.
Decisions for the author
-
The CLI's 80-line window can miss an OOM the engine itself diagnosed — answered-by-intent
append_oom_serve_notereads after the engine has appended its own longer bail. In the PR's fixture an OOM followed by 77 lines gives an engine hint and no serve note, andthe_cli_window_can_miss_an_oom_the_engine_sawpinsis_none(). The description answers it: "This is not fixed here … the obvious fix trades one defect for another … say the word if it should land here." The README's conditions list names it too. This would otherwise be blocking, because a note that exists to help on OOM is silent in the case the PR itself measured. Please confirm the structured-record fix (the engine writing its selected symptom as data next to the log) is the intended follow-up. -
No Gherkin scenario covers the vLLM engine's changed startup-failure text — answered-by-intent
The header wording, rendered-row echo, narrowed hint trigger and the\r/ESC[2Krepaint case are covered only by unit tests. The description's "Stated gap" names the lane that would cover it (E2E tests (MI300X)) and why it is not added: "provoking one reliably on a 192 GB MI300X … is GPU-expensive and racy." This would otherwise be blocking under AGENTS.md §3. Please confirm the tradeoff is still intended. -
The engine hint no longer fires for a bare
HIP error: out of memory— tradeoff
Onmainthe engine hinted on anyout of memorysubstring; now only an allocator-shaped signature scoring ≥ 50 does, to stay in step withrocm diagnose. One side: the hint and the diagnosis can never disagree, and kernel OOM-killer lines and theEngineCorewrapper stop producing false hints. The other side: a user whose log says onlyHIP error: out of memorynow gets no memory guidance and no pointer torocm diagnose.docs/vllm.mdjustifies this with "each of those also appears for failures that have nothing to do with memory", and nothing in the repo supports that for this particular string, which I believe is PyTorch-ROCm's rendering ofhipErrorOutOfMemory(not verified here). Please confirm this was deliberate. -
A second Cargo feature plus an env var plus a capability tag for the fault-injection hook — non-blocking-improvement
The existinge2e-test-hooksfeature already reaches the prebuilt lanes through theROCM_E2E_LEMONADE_BACKEND_INSTALL_FAILUREseam, with no capability plumbing. Separately, if the arming env var were lost on the mock lane,serve-26would resolve to a reported skip rather than a failure; the skip is recorded inplatform.jsonbut nothing fails.
Positive signals
- The vLLM OOM reader now lives in its own module,
crates/rocm-core/src/vllm_oom.rs(new file;lib.rsonly declares and re-exports it). - Quoting that rejects rather than escapes unquotable log text keeps untrusted bytes out of the printed command, and the shared
Cf/Zl/Zppredicate has a per-predicate test. the_release_workflow_builds_no_feature_gated_binariesjoins\continuations first and asserts a build was found, so a renamed build step fails loudly instead of passing vacuously.- The fault-injection hook compiles to
const falsein a default build, andxtask e2eclears the arming variable for prebuilt binaries. Building the hooked release binary and runningserve-26's exact arguments under a PTY printed the note, and the printedrocm diagnose --symptom '…'routed tofix-16-vllm-oom; the same binary without the variable refused at "no usable AMD GPU", so a missing arm fails loudly.
Deployment notes
- Rebase onto current
mainbefore anything else. It has extracted large parts ofmain.rssince, and has its ownserve-26/serve-27andquoting.rs. - The squash-merge note in the PR banner still applies: commit
1de61359is still in the PR history and its message still contains the falsegpu_preferredsentence the banner flags. The repo builds the squash body from commit messages. - The engine's startup-failure message header changed from "Last 80 lines of startup log:" to "Startup log, last N lines as the terminal drew them:".
git grepfinds no in-repo parser of the old header, but anything outside the repo scraping it would break. - A new Cargo feature,
e2e-oom-fault-injection, is built only bycargo xtask e2e.
Since my earlier review at a3fb51cd
- Resolved: the OOM subsystem inline in
lib.rs(blocking then).d4a1cc41extracts it tocrates/rocm-core/src/vllm_oom.rs. - Resolved: shipping the GPU-reuse exemption separately.
55671427splits it out, and I found no pre-gate or reuse-exemption code at the head; the netserve_cmd.rschange is only the test-hook bypass and the note call. I did not check the state of #703. - Unchanged: the missing scenario for the engine's startup-failure text is still answered only by the description's "Stated gap" (see Decisions for the author).
- No longer applies here: the deployment note about GPU-less hosts, because it moved with the exemption.
- Still applies: the squash-merge hygiene note.
The earlier needs work rested on the lib.rs finding, which is resolved; the current needs work rests on the head being unmergeable and a check being red.
What this covered
I read the PR's own diff at ebc26718 in three coupled groups, each by an independent review pass: OOM core plus the vLLM engine, the serve CLI plus docs, and the e2e scenarios plus xtask. Each pass also checked AGENTS.md adherence, history and code comments within its group. I re-verified every finding above against the PR head myself.
- Read completely:
vllm_oom.rs,terminal.rs,serve_summary.rs,model_serving.feature,serving_steps.rs,capability.rs,xtask/src/e2e.rs,crates/e2e-report/src/lib.rs,docs/vllm.md,docs/architecture.md, andengines/vllm/src/process.rslines 1–1330 (all production code and all OOM/sanitize tests). - Read in part:
serve_cmd.rs: lines 1–140 and 380–1130 in detail, the rest skimmed.service_launch.rs: lines 1–1300, which cover every PR change and its callers; the remaining ~570 lines are pre-existing tests.main.rs,diagnose.rs,fix.rs,workflow_contract.rs,tui_driver.rs: the PR hunks plus every touched function, test and in-file caller.lib.rs: lines 1–420 plus a keyword grep of the rest.expectation.rs: header, tag parsing,resolveand the new test.- README,
docs/manual-testing.md,docs/testing.md: hunks and surrounding sections.
- Ran:
cargo test -p rocm-engine-vllm --lib process::(43 passed), therocm-coreOOM/terminal tests (19 passed), andcargo test -p xtaskfore2e::(3),release_(12) andtest_hooks(2), all passing. I also built the hooked release binary and exercisedserve-26's arguments by hand (see Positive signals), ran the diff leak scan (clean), and read the CI rollup and the Windows job log. - Did not run:
- mutation checks, so every test verdict above is reasoned from code, except that the round-trip block's verdict is established by reading its construction;
- the cucumber suite, the full gate, and
python scripts/smoke_local.py; - a prior-changes pass;
- separate design-review passes (the group passes' design questions fed Decisions for the author).
- Several lower-confidence candidates were dropped under the 70 cutoff: the unlinted
e2e-oom-fault-injectioncode path, a redundant outerstatuscheck, and\thandling in the echo. - The fixes on this PR were written by its author, not by me, and the findings were reached from the code alone, not reconciled against the review threads.
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · ebc2671
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 — no blocking findings
Full review of the whole change: git diff d4e7c75194d1643eacc3b1bda33afe10766def73...ebc267182a7d77042f1f27a72e1bc41680d7b7e6 (25 files, +3078/-284), every changed file read completely plus the surrounding production code it calls into (apps/rocm/src/main.rs's read_optional_tail_lines, serve_cmd.rs's full call site, rocm-engine-protocol's DEFAULT_LOG_TAIL_LINES, engines/vllm/src/process.rs::resolve_model_response, AGENTS.md §3). This change implements the diagnostics scope of ROCMAI-419/ex-EAI-8059 ("tell the user which memory knobs to reach for when a serve runs out of VRAM") — detecting a vLLM OOM signature in a failed managed serve's log and surfacing --gpu-memory-utilization/--gpu guidance plus a rocm diagnose --symptom command. The ticket's explicit scope ("diagnostics only, not serve behaviour") matches what's in front of this diff; the serve-behaviour reuse-exemption piece is confirmed absent here (split to #703, per the PR body). One deliberate deviation from the ticket's text: the ticket lists Engine core initialization failed as an OOM signature to detect, but vllm_oom.rs/serve_summary.rs explicitly and correctly exclude it (it's vLLM's generic crash wrapper, not OOM-specific) — informational, not a defect.
Blocking
None.
Non-blocking
None.
Decisions for the author
- No Gherkin scenario covers the vLLM engine's own startup-failure text (the header rewording, the per-row echo, and the narrowed OOM-hint trigger in
engines/vllm/src/process.rs'sstartup_log_context/oom_utilization_hint) — answered-by-intent.AGENTS.md:88("User-observable behavior needs a scenario, not only a unit test... a unit test asserting the internal helper does NOT discharge this") would otherwise make this blocking, since this text is observable viarocm services logs/--verbose/--foregroundand only unit-tested (e.g.the_colourised_round_trip_quotes_the_row_the_terminal_drew,the_echoed_tail_never_welds_two_rendered_rows_into_one_line). The PR body's own "Stated gap" section names the exact rule, explains why the one lane that could exercise it (E2E tests (MI300X)) can't do so without a GPU-expensive, racy provoked OOM, and explicitly asks "say the word if it should land here." Confirm this is still the accepted tradeoff rather than a TODO that slipped through. - The documented tail-window gap (
append_oom_serve_noteinapps/rocm/src/main.rs:3771, pinned bythe_cli_window_can_miss_an_oom_the_engine_sawinengines/vllm/src/process.rs) is a real, measured limitation — an OOM followed by ~77 more log lines silently drops the CLI note even though the engine's own message still shows it — tradeoff, not a defect: the alternative (re-windowing to agree with the engine) was shown to reintroduce the colourised-line quoting holethe_colourised_round_trip_quotes_the_row_the_terminal_drewexists to prevent, and this one is pinned by a dedicated regression test plus a README "absence is not evidence" callout. No action needed beyond confirming the tradeoff still reads as acceptable.
Positive signals
vllm_oom_diagnose_symptom(crates/rocm-core/src/vllm_oom.rs:209) andquotable_in_single_quotes(crates/rocm-core/src/terminal.rs:221) are shared verbatim between the vLLM engine's own startup-failure hint and therocm serveCLI summary note, closing a real class of bug (the two surfaces silently disagreeing on what counts as an OOM or what's safe to shell-quote) rather than just documenting the risk.is_rocm_authored_log_line(crates/rocm-core/src/vllm_oom.rs:54) correctly keeps a newest-first log scan from quoting rocm-cli's own previously-written hint back at the user as though it were the engine's error — a subtle self-referential bug that unit tests (the_engine_hint_is_never_quoted_back_as_the_users_own_line) prove was real before the fix.- The
e2e-oom-fault-injectionfeature is defensively gated three different ways (per-crate feature flag defaulting off,release.ymlshape-based guard inxtask/src/workflow_contract.rscatching-Fname/continuation-line spellings, and a runtime env-var check requiring the exact string"1"), so a test-only fault-injection hook has no plausible path into a shipped binary.
Deployment notes
gh pr viewreportsmergeStateStatus: DIRTY/mergeable: CONFLICTINGagainst the currentmaintip —mainhas moved since this PR's recorded base and the branch needs a rebase before it can land; this doesn't reflect a defect in the reviewed diff itself.- CI at this exact head (
ebc26718) is green on every required lane. The one failing check,E2E tests (Strix Halo, Windows, current)in theE2E self-hostedworkflow, is confirmed non-required:.github/workflows/e2e-selfhosted.yml's own header comment states this workflow was removed from branch protection's required-status-check list (confirmed 2026-09-11), independent ofcontinue-on-error(which isfalsethroughout).
What this covered
Every file in the base...head diff, read completely (25 files, 4163-line diff), plus directly-verified surrounding context: read_optional_tail_lines, resolve_engine_selection/resolve_model_response, AGENTS.md §3's Gherkin-scenario rule, DEFAULT_LOG_TAIL_LINES's value, and the serve_cmd.rs call site confirming append_oom_serve_note only runs on the interactive-summary path. CI checked via gh pr view --json statusCheckRollup against the exact head SHA; the one failure was traced to a workflow confirmed non-required by its own in-repo documentation.
This was a single-context pass by deliberate instruction for this run (fan-out to independent sub-reviewers was available but explicitly disallowed here), not a fan-out failure — coverage is accordingly narrower than the skill's multi-pass method would achieve, and a follow-up multi-pass run is recommended if higher assurance is wanted. Did not run: History/prior-changes passes (no in-scope git blame anomalies surfaced during reading; not separately fanned out). Not reconciled against the PR's extensive prior review-comment history, per the blind-review method — this report was reached from the code and the PR's own stated intent only.
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · ebc2671
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 — no blocking findings
Full review of the whole change: git diff d4e7c75194d1643eacc3b1bda33afe10766def73...ebc267182a7d77042f1f27a72e1bc41680d7b7e6 (25 files, +3078/-284), every changed file read completely plus the surrounding production code it calls into (apps/rocm/src/main.rs's read_optional_tail_lines, serve_cmd.rs's full call site, rocm-engine-protocol's DEFAULT_LOG_TAIL_LINES, engines/vllm/src/process.rs::resolve_model_response, AGENTS.md §3). This change implements the diagnostics scope of ROCMAI-419/ex-EAI-8059 ("tell the user which memory knobs to reach for when a serve runs out of VRAM") — detecting a vLLM OOM signature in a failed managed serve's log and surfacing --gpu-memory-utilization/--gpu guidance plus a rocm diagnose --symptom command. The ticket's explicit scope ("diagnostics only, not serve behaviour") matches what's in front of this diff; the serve-behaviour reuse-exemption piece is confirmed absent here (split to #703, per the PR body). One deliberate deviation from the ticket's text: the ticket lists Engine core initialization failed as an OOM signature to detect, but vllm_oom.rs/serve_summary.rs explicitly and correctly exclude it (it's vLLM's generic crash wrapper, not OOM-specific) — informational, not a defect.
Blocking
None.
Non-blocking
None.
Decisions for the author
- No Gherkin scenario covers the vLLM engine's own startup-failure text (the header rewording, the per-row echo, and the narrowed OOM-hint trigger in
engines/vllm/src/process.rs'sstartup_log_context/oom_utilization_hint) — answered-by-intent.AGENTS.md:88("User-observable behavior needs a scenario, not only a unit test... a unit test asserting the internal helper does NOT discharge this") would otherwise make this blocking, since this text is observable viarocm services logs/--verbose/--foregroundand only unit-tested (e.g.the_colourised_round_trip_quotes_the_row_the_terminal_drew,the_echoed_tail_never_welds_two_rendered_rows_into_one_line). The PR body's own "Stated gap" section names the exact rule, explains why the one lane that could exercise it (E2E tests (MI300X)) can't do so without a GPU-expensive, racy provoked OOM, and explicitly asks "say the word if it should land here." Confirm this is still the accepted tradeoff rather than a TODO that slipped through. - The documented tail-window gap (
append_oom_serve_noteinapps/rocm/src/main.rs:3771, pinned bythe_cli_window_can_miss_an_oom_the_engine_sawinengines/vllm/src/process.rs) is a real, measured limitation — an OOM followed by ~77 more log lines silently drops the CLI note even though the engine's own message still shows it — tradeoff, not a defect: the alternative (re-windowing to agree with the engine) was shown to reintroduce the colourised-line quoting holethe_colourised_round_trip_quotes_the_row_the_terminal_drewexists to prevent, and this one is pinned by a dedicated regression test plus a README "absence is not evidence" callout. No action needed beyond confirming the tradeoff still reads as acceptable.
Positive signals
vllm_oom_diagnose_symptom(crates/rocm-core/src/vllm_oom.rs:209) andquotable_in_single_quotes(crates/rocm-core/src/terminal.rs:221) are shared verbatim between the vLLM engine's own startup-failure hint and therocm serveCLI summary note, closing a real class of bug (the two surfaces silently disagreeing on what counts as an OOM or what's safe to shell-quote) rather than just documenting the risk.is_rocm_authored_log_line(crates/rocm-core/src/vllm_oom.rs:54) correctly keeps a newest-first log scan from quoting rocm-cli's own previously-written hint back at the user as though it were the engine's error — a subtle self-referential bug that unit tests (the_engine_hint_is_never_quoted_back_as_the_users_own_line) prove was real before the fix.- The
e2e-oom-fault-injectionfeature is defensively gated three different ways (per-crate feature flag defaulting off,release.ymlshape-based guard inxtask/src/workflow_contract.rscatching-Fname/continuation-line spellings, and a runtime env-var check requiring the exact string"1"), so a test-only fault-injection hook has no plausible path into a shipped binary.
Deployment notes
gh pr viewreportsmergeStateStatus: DIRTY/mergeable: CONFLICTINGagainst the currentmaintip —mainhas moved since this PR's recorded base and the branch needs a rebase before it can land; this doesn't reflect a defect in the reviewed diff itself.- CI at this exact head (
ebc26718) is green on every required lane. The one failing check,E2E tests (Strix Halo, Windows, current)in theE2E self-hostedworkflow, is confirmed non-required:.github/workflows/e2e-selfhosted.yml's own header comment states this workflow was removed from branch protection's required-status-check list (confirmed 2026-09-11), independent ofcontinue-on-error(which isfalsethroughout).
What this covered
Every file in the base...head diff, read completely (25 files, 4163-line diff), plus directly-verified surrounding context: read_optional_tail_lines, resolve_engine_selection/resolve_model_response, AGENTS.md §3's Gherkin-scenario rule, DEFAULT_LOG_TAIL_LINES's value, and the serve_cmd.rs call site confirming append_oom_serve_note only runs on the interactive-summary path. CI checked via gh pr view --json statusCheckRollup against the exact head SHA; the one failure was traced to a workflow confirmed non-required by its own in-repo documentation.
This was a single-context pass by deliberate instruction for this run (fan-out to independent sub-reviewers was available but explicitly disallowed here), not a fan-out failure — coverage is accordingly narrower than the skill's multi-pass method would achieve, and a follow-up multi-pass run is recommended if higher assurance is wanted. Did not run: History/prior-changes passes (no in-scope git blame anomalies surfaced during reading; not separately fanned out). Not reconciled against the PR's extensive prior review-comment history, per the blind-review method — this report was reached from the code and the PR's own stated intent only.
(Retry: an earlier submission of this same review may not have been recorded — see note below if duplicates appear.)
Resolve the modularization conflicts: `main`'s Phase-6x/7x work moved most
of what this branch touched out of `apps/rocm/src/main.rs` and
`crates/rocm-core/src/lib.rs`, so several hunks had an empty `origin/main`
side. Each was re-applied into the module that now owns the code rather than
dropped.
- `apps/rocm/src/serve_cmd.rs`: `resolve_engine_selection`, `gpu_vram_usage`,
`parse_device_policy` and friends now come from `engine_runtime_select` /
`gpu_policy` / `engine_ipc`, so the branch's wide `use crate::{...}` is
replaced by `main`'s narrow one plus `append_oom_serve_note`. The OOM-note
call and `main`'s `engine_env_note` push are both kept — they are
independent additions to the same block.
- `crates/rocm-core/src/lib.rs`: keep both module declarations
(`vllm_oom`, `windows_driver`) and both re-export groups.
- `engines/vllm/src/process.rs`: `main` added `import_failure_hint` /
`import_failure_line` next to the local `quotable_in_single_quotes` and
`log_tail_shows_oom` this branch moved into `rocm-core`. Keep the import
helpers, drop the two relocated ones, and point the import helper at
`rocm_core::quotable_in_single_quotes` /
`rocm_core::strip_terminal_control_sequences`.
`startup_log_context`'s doc keeps this branch's `rendered_lines` rationale
and picks up `main`'s mention of `import_failure_hint`.
- `xtask/src/workflow_contract.rs`, `docs/architecture.md`: both sides'
additions kept.
- `docs/manual-testing.md`: keep `main`'s new 4a, renumber around it — the
vLLM memory section is 5, ComfyUI 6, cloud key 7, planning 8, uninstall 9,
AIM 10 (it collided with uninstall at 9).
- `docs/vllm.md`: keep this branch's fuller OOM paragraph, which subsumes
`main`'s shorter one.
- `read_optional_tail_lines` moved to `services_text`; import it there.
Scenario renumbering: `main` took `serve-26`/`serve-27` for the AIM lanes, so
this branch's OOM-launch scenario becomes `serve-28`
(`feature_naming::scenario_names_are_indexed_sequentially_per_feature`
requires the index to equal the declaration position, so appending is the only
legal choice). Every reference moved with it: the feature file,
`tests/e2e-cucumber/src/capability.rs`,
`tests/e2e-cucumber/tests/e2e/serving_steps.rs` and `docs/testing.md`.
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…ai-8059) Takes the mechanical and logic findings from the 2026-10-22 `pr-review-watcher` CHANGES_REQUESTED review. Each is checked against the code rather than taken on the report's word. - `apps/rocm/src/serve_cmd.rs`: do not start the background helper when the managed launch was fabricated by the test-only fault-injection hook. The hook writes a `starting` record for a process that never existed, so the helper was left supervising a pid that will never appear, with nothing in the scenario's teardown to stop it — observed as `rocm daemon` processes outliving their e2e checkout by days, still carrying `ROCM_E2E_SIMULATE_OOM_LAUNCH=1`. `e2e_simulate_oom_launch` is a `const fn` returning `false` without the feature, so the branch folds away in shipped builds. Falsified: `cargo xtask e2e -- -n "serve-28"` on a Linux box leaves no `rocm daemon` behind; it did before. - `apps/rocm/src/serve_summary.rs`: move `append_oom_serve_note` and its six unit tests out of `main.rs` (AGENTS.md section 6 — new domain logic should not grow `main.rs`). `oom_memory_note` and `serve_failed_to_become_ready`, the two functions it is built on, already live here. Pure relocation: no behaviour change, and the tests move with it. - `crates/rocm-core/src/vllm_oom.rs`: fix a doc example that contradicted the rule it was illustrating. It used a bare `RuntimeError: ... killed: out of memory` as the line that "wins" the newest-first scan and called both lines diagnosable; that line is sub-threshold and is not a candidate at all (`sub_threshold_lines_carry_no_hint_at_all` in `engines/vllm` pins exactly that). Both example lines now score. Also drop the claim that "callers print the whole log tail beside the hint" — true of the engine's bail, false of the `rocm serve` note, which carries the one selected line. - `apps/rocm/src/main.rs`: `rocm serve --help` promised the user's own failing line in the `rocm diagnose --symptom` command without the caveat the README carries — a line with an apostrophe or control bytes falls back to the canonical symptom. Say so. - `engines/vllm/src/process.rs`: a comment cited `rocm_core::vllm_log_segments`, a path that does not resolve (the function is private to `rocm_core::vllm_oom`). Cite the file instead, and say why. - `docs/testing.md`: the section claimed there is deliberately no focused command for the OOM-launch scenario. There is: `cargo xtask e2e -- -n "serve-28"` builds the binary with the fault-injection hook and sets the capability, which is also the route CI's dispatch filter takes. Verified by running it. The filter bypass is a hazard only against a prebuilt `ROCM_CLI_BINARY`; scope the warning to that. - `.github/workflows/nightly.yml`, `.github/workflows/e2e-selfhosted.yml`: the comments still said the prebuilt lanes' `e2e-test-hooks` set "must match what `cargo xtask e2e` would build". It has not matched since xtask also began enabling `e2e-oom-fault-injection`; the contract test checks a floor, not equality. Say floor, and record why the fault-injection hook is deliberately absent here (those scenarios resolve to a reported skip). - `apps/rocm/src/advised_commands.rs`, `engines/vllm/src/process.rs`: two tests `main` added after this branch last merged now cover text this branch changed. `VLLM_OOM_DIAGNOSE_COMMAND` is a marker prefix, not a runnable invocation, so it gets a `NOT_INVOCATIONS` entry with that reason; and `import_failure_hint_is_part_of_the_startup_failure_message` asserted the old `Last 80 lines of startup log:` header, which this branch replaced with `Startup log, last 80 lines as the terminal drew them:`. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Review round: triage of the 37 open threads, merge with
|
| Thread | State now |
|---|---|
main.rs — the already_hinted early return destroys the whole OOM note |
Fixed. De-duplication moved to composition level exactly as suggested: main.rs computes already_hinted and passes it into oom_memory_note, which omits the one shared hint fragment. The note itself — the OOM confirmation, the doesn't-fit branch, the diagnose command — is never suppressed. |
main.rs — the test cements the suppression (notes == pre_launch_notes) |
Fixed. The equality assertion is gone. append_oom_serve_note_prints_the_shared_hint_exactly_once now pins "the hint appears exactly once across the whole summary", with a sibling in serve_summary.rs counting the same thing. |
rocm-core/lib.rs — <0-1> is still here |
Fixed. crates/rocm-core/src/lib.rs:345 reads --gpu-memory-utilization <fraction greater than 0 and at most 1>. The pin was extended as asked: crates/rocm-core/src/fix.rs:2739-2744 asserts both the new wording and !hint.contains("<0-1>"), so the merge-reversion failure mode you traced cannot recur silently. |
tui_driver.rs — duplicate extra_env loop inverts the credential strip |
Fixed. There is one loop, at :288, above the env_remove block — your "simplest fix is to delete this loop". The comment now says the position is deliberate and why. |
serving_steps.rs — hardcoded 30s instead of default_timeout() |
Fixed. interactive_summary_timeout() is default_timeout(), with a doc comment giving your reason (E2E_TUI_TIMEOUT_SECS on a contended lane). |
The other 32 threads have @r0x0r last. Re-reading them for "answered with a
promise rather than a change", the ones still genuinely open are design
decisions, not unaddressed findings:
- The interactive-only asymmetry (
print_managed_launch_plainprints no
post-launch note) — documented, not closed, and explicitly handed over for a
decision on whether that path grows anote:key. - Whether a compile-gated seam waiving a production safety bail is acceptable
(test(e2e): drive the Lemonade recovery scenario with a planted runtime (ROCMAI-669) #351 vs this PR) — above this PR. diagnose.rs'sgpu[-_]memory[-_]utilizationkeyword not tolerating spaces —
accepted by the reviewer as a fast-follow.
Everything else in that set was fixed with a commit cited in the reply; I
spot-checked the mechanical ones (screen_without_whitespace on both
assertions, OOM_FAULT_INJECTION_ENV single-sourced in e2e-report,
STARTUP_FAILURE_LOG_TAIL_LINES derived from the protocol constant,
the_release_workflow_builds_no_feature_gated_binaries handling \
continuations and compact -F) and they hold at this head.
Merge with main
Eight conflicted files. main's Phase-6x/7x modularization moved most of what
this branch touches, so several hunks had an empty origin/main side — the
code was relocated, not deleted. Each was re-applied into its new home; the
merge commit message has the file-by-file account. The ones worth calling out:
apps/rocm/src/serve_cmd.rs— the branch's wideuse crate::{...}is dead:
resolve_engine_selection,gpu_vram_usage,parse_device_policyand the
rest now come fromengine_runtime_select/gpu_policy/engine_ipc.
Kept both sides of the notes block (main'sengine_env_notepush and this
branch's OOM note).engines/vllm/src/process.rs—mainaddedimport_failure_hint/
import_failure_lineright next to the localquotable_in_single_quotesand
log_tail_shows_oomthat this branch moved intorocm-core. Kept the import
helpers, dropped the two relocated ones, and repointed the import helper at
rocm_core::quotable_in_single_quotes/
rocm_core::strip_terminal_control_sequences.docs/manual-testing.md— keptmain's new 4a; sections renumbered around
it (AIM went 9 → 10, where it had collided with Uninstall).
No PR test was lost. I enumerated every function name this branch adds
(59, tests included) and grepped the merged tree for each: all present.
the_stripper_follows_the_escape_grammar_not_just_the_colour_case looks dropped
from engines/vllm/src/process.rs but was deliberately relocated by this branch
into crates/rocm-core/src/terminal.rs.
Scenario renumbering: serve-26 → serve-28. main took
serve-26/serve-27 for the AIM lanes.
feature_naming::scenario_names_are_indexed_sequentially_per_feature requires
the index to equal the declaration position, so appending is the only legal
choice. Every reference moved: the feature file, capability.rs,
serving_steps.rs, docs/testing.md. The @id: is unchanged
(serve-oom-launch-memory-guidance).
Two tests main added after this branch last merged turned red against it, and
both were right to:
advised_commands::every_advised_command_parses—VLLM_OOM_DIAGNOSE_COMMAND
("rocm diagnose --symptom") is a marker prefix, not a runnable invocation.
Added aNOT_INVOCATIONSentry saying so.import_failure_hint_is_part_of_the_startup_failure_message— asserted
Last 80 lines of startup log:, which this branch replaced with
Startup log, last 80 lines as the terminal drew them:.
The 2026-10-22 automated round
Taken, with the file each landed in:
rocm daemonleaked by serve-28 (apps/rocm/src/serve_cmd.rs:954) —
the fault hook writes astartingrecord for a process that never existed,
andservethen started the background helper to supervise it. Nothing in
teardown stopped it. Now skipped when the launch is fabricated.
e2e_simulate_oom_launchis aconst fnreturningfalsewithout the
feature, so the branch folds away in shipped builds. Falsified rather than
assumed:pgrep -af "rocm daemon"before and after
cargo xtask e2e -- -n "serve-28"on the Linux box — nothing left behind.append_oom_serve_notegrowsmain.rs(AGENTS.md §6) — moved, with its
six unit tests, intoapps/rocm/src/serve_summary.rs:378, next to
oom_memory_noteandserve_failed_to_become_ready. Pure relocation.crates/rocm-core/src/vllm_oom.rs:190— the doc example contradicted the
rule. The "losing" line it used (RuntimeError: ... killed: out of memory) is
sub-threshold and is not a candidate at all, which
sub_threshold_lines_carry_no_hint_at_allalready pins. Both example lines
now score, and the false "callers print the whole log tail beside the hint"
is scoped to the engine surface — the serve note does not.apps/rocm/src/main.rs:514—rocm serve --helppromised the user's own
failing line with no fallback caveat. Added.engines/vllm/src/process.rs:690— citedrocm_core::vllm_log_segments,
a path that does not resolve (private torocm_core::vllm_oom). Cites the
file now, and says why it is spelled that way.docs/testing.md:1963— the claim that there is deliberately no focused
command was wrong.cargo xtask e2e -- -n "serve-28"works, because xtask
builds the binary with the hook and sets the capability. Verified by
running it: 1 scenario, 5/5 steps, exit 0. The filter-bypass hazard is real
only against a prebuiltROCM_CLI_BINARY; the warning now says that.- Stale workflow comments (
nightly.yml,e2e-selfhosted.yml) — they still
said the prebuilte2e-test-hooksset "must match whatcargo xtask e2e
would build". It is a floor, not equality, since xtask also enables
e2e-oom-fault-injection. Reworded, with a note on why the fault-injection
hook is deliberately absent there (reported skip, not silent pass). - The blocking squash-body finding — taken the route the review offers:
the PR description's banner now instructs whoever merges to write the
squash body by hand from the final diff, and lists what the stale commits
narrate (the reuse pre-gate, now in feat(serve): exempt reuse of a running service from the no-GPU refusal #703; the--gpuvalidation work, already
onmainvia feat(vllm): Tackle out of memory errors (EAI-8058) #251;956891f's "gated to Linux" claim, reversed by
13bd32c). History is not being rewritten — that needs a force-push to a
shared branch. - The stale description — banner rewritten,
serve-26→serve-28
throughout, and thevllm_oom.rsextraction named.
Not taken, deliberately: the ~6-way duplicated two-reads rationale (a
docs-consolidation pass across six files, better on its own), and the
nightly.yml publishing-job feature guard (the same follow-up already agreed
upstream in the workflow_contract.rs thread).
Verification
Darwin cannot build this workspace (engines/lemonade — st_mode is u16
there, u32 on Linux), so the Darwin pre-push hooks cargo-clippy and
cargo-test were skipped (SKIP=cargo-clippy,cargo-test). Everything else
ran — verify-commits, license-headers, cargo fmt, the e2e-harness clippy,
shellcheck — and passed. The two skipped hooks were run for real on a Linux
MI300X box, against the exact merged tree (git diff origin/main..HEAD applied
onto origin/main):
cargo build --workspace --all-targets -> exit 0
cargo clippy --workspace --all-targets -- -D warnings -> exit 0
cargo test --workspace --all-targets --no-fail-fast -> exit 0
76 test binaries, 4840 passed, 0 failed
cargo fmt --all -- --check -> exit 0
cargo xtask e2e -- -n "serve-28" -> exit 0
1 scenario (1 passed), 5 steps (5 passed), 0 unexpected failures
What I could not verify. That box has KFD nodes but no amd-smi/rocm-smi
and no usable GPU telemetry, so a real GPU OOM was not reproducible: the
end-to-end evidence is the fault-injected launch (serve-28) plus the unit
coverage, not a card that actually ran out of memory. I also could not run the
self-hosted E2E tests (GPU) or E2E tests (Strix Halo, Ubuntu) lanes — CI
remains the only authority on those.
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · 4187652
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 (28 files, +3174/-310). Implements OOM guidance on a failed rocm serve (ROCMAI-419, ex EAI-8059): the serve-summary OOM note, the shared rocm_core::vllm_oom reader, the shell-quoting guard's move into rocm-core, the e2e-oom-fault-injection feature, serve-28, and docs. The serve-behavior/reuse-exemption half was correctly split out to #703 per the PR's own note; this review covers only what remains in this branch.
Blocking
-
simulate_oom_managed_launch's fabricated service record never pairs its PID with a start-time token, so a laterrocm services stopcan SIGKILL an unrelated process tree once that PID is recycled —apps/rocm/src/service_launch.rs:587-603(builds the record viaManagedServiceRecord::new(..., std::process::id(), ...)and never callsrecord_supervisor_identity/record_engine_identityafterward, unlike the real spawn path at:532-534),crates/rocm-core/src/proc_lifecycle.rs:103(identity_state's(None, _) => IdentityState::Matches— a PID recorded without a token is trusted on sight),apps/rocm/src/services_text.rs:712-734(terminate_recorded_service_pidsbuildsProcessIdentity::new(record.supervisor_pid, record.supervisor_start_ticks)for exactly this record and callsterminate_verified(..., KillScope::Tree, force: true)),tests/e2e-cucumber/tests/e2e.rs:708-751(stop_managed_servicesteardown runsrocm services stop <id> --yesunconditionally against every on-disk record it finds, with no status-based guard beforeterminate).
record_supervisor_identity's own doc comment names this exact hazard ("a PID written on its own is signalled on trust alone once it has been recycled"). The short-livedrocm serveinvocation that writesstd::process::id()exits almost immediately, freeing the PID for reuse; on a Linux CI host (serve-28is@requires-os:linux) that reassigns the stale PID to any live process before scenario teardown, that unrelated process and its whole descendant tree would receive SIGTERM/SIGKILL.
Confidence 85 · architectural · traced end-to-end against the source (reachability and match-arm behavior confirmed directly; PID-reuse itself not reproduced live — no toolchain in this environment). Fix: callrecord.record_supervisor_identity(std::process::id())immediately after constructing the record insimulate_oom_managed_launch, while the process is genuinely still alive, soidentity_statecan later reportRecycled/Goneinstead of alwaysMatches. -
import_failure_hintwas not given the same "rendered-row" fix as its stated siblingoom_utilization_hint, so it can still weld two terminal rows into one fabricated displayed line —engines/vllm/src/process.rs:751-769(import_failure_hint, computessymptom_linevia the old flatrocm_core::strip_terminal_control_sequences(raw_line)) vs.engines/vllm/src/process.rs:710-722(oom_utilization_hint, rewritten in this PR to render viarocm_core::vllm_oom_display_line, specifically to avoid welding a bare-\rprogress-bar segment into the diagnosed line — the exact bug three new tests in this PR guard against for the OOM path).
import_failure_hint's own doc comment calls itself "the sibling ofoom_utilization_hint," but a one-\n-terminated physical line containing an embedded bare\r(e.g. a progress-bar repaint immediately followed by an import traceback) would still render as one welded, never-actually-printed string for the import path, while the OOM path now correctly splits and picks the scoring row. The printed--symptomcommand itself stays shell-safe either way (\rischar::is_control(), soquotable_in_single_quotesstill rejects it and falls back to the canonical symptom) — this is a display-only inconsistency, not a shell-safety issue, and the real-world trigger window is narrow (vLLM's import failures occur before any weight-loading progress bar exists). No test exercises this path's control-byte handling either way.
Confidence 75 · logic/consistency · Fix: renderimport_failure_hint'ssymptom_linethrough the same rendered-row-aware path asoom_utilization_hint(reuseterminal::rendered_linesdirectly, or add a sibling tovllm_oom_display_line), and add a test analogous tothe_echoed_tail_never_welds_two_rendered_rows_into_one_linefor the import-failure case.
Non-blocking
None.
Decisions for the author
- The CLI's note can miss an OOM the engine saw, when the engine's own bail pushes the original line outside the CLI's 80-line re-read window — answered-by-intent. The PR's own "Known gap" section states this exactly (measured: 77 lines after the OOM line already suppresses the note entirely), explains why the obvious fix (re-windowing before the bail) trades it for the colourised-quoting regression this PR's own tests prevent, names the correct structural fix as out of scope, and pins the gap with a dedicated test (
the_cli_window_can_miss_an_oom_the_engine_saw). Nothing above adds to this; it would otherwise have been a blocking finding. - README.md:934's new OOM-note link to
docs/vllm.mduses an absolute GitHub blob URL rather than a relative link, while the identical target is linked relatively three lines away (README.md:43) and every other new cross-doc link this PR adds is relative — non-blocking-improvement. AGENTS.md §5's stated rationale for preferring relative links is specifically about a bare backtick path defeating thedocs-links/lycheechecks; an absolute URL is still a real, checked link (lychee would catch it going stale), so this reads as a style inconsistency rather than a rule violation. Worth tidying for consistency, not blocking.
Positive signals
- The OOM-detection rewrite is mutation-tested with its results recorded directly in a doc comment (which specific mutations were confirmed to survive the pre-existing suite before the new tests were added) — a notably rigorous level of self-verification.
- The self-quoting bug (the engine's own previously-written hint being re-selected as "the user's error" on a second read of the shared log) is fixed via
is_rocm_authored_log_line, keyed off shared constants so the hint and the filter cannot drift, and backed by a dedicated regression test that reproduces the exact two-read scenario rather than unit-testing the exclusion in isolation. quotable_in_single_quotesis invoked before interpolation at every call site that builds arocm diagnose --symptom '...'recommendation (both engine hints and the CLI'sserve_summary.rs::oom_memory_note), so a control-byte or apostrophe-bearing line can never reach a shell-quoted command raw on any surface.simulate_oom_managed_launch's fixture deliberately reproduces the full shared-log shape (real line, echo, header, engine's own hint sentence) rather than just a raw OOM line, specifically so the self-quoting bug class above would have been caught by this fixture had it still existed — good adversarial test design, independent of the identity-pairing gap noted above.- The PR description itself is unusually rigorous: it tracks its own review history verbatim, withdraws claims later found false rather than quietly dropping them, and states every known gap and deferral explicitly with the reasoning for not fixing it now.
Deployment notes
e2e-oom-fault-injectionis confirmed feature-gated out of release builds (release.ymlnever passes it) — the PID-identity gap above is confined to the mock E2E lane and its CI runners, not shipped behavior.- No other migration, new dependency, or user-facing default change beyond what the PR description's own "Behavior coverage"/"Stated gap" sections already disclose.
What this covered
Every one of the 28 changed files, read completely, across 6 independent blind fan-out workers grouped by coupling: (1) crates/rocm-core/src/vllm_oom.rs+terminal.rs+lib.rs; (2) crates/rocm-core/src/diagnose.rs+fix.rs; (3) apps/rocm/src/serve_summary.rs+serve_cmd.rs+advised_commands.rs+Cargo.toml+main.rs diff; (4) engines/vllm/src/process.rs+apps/rocm/src/service_launch.rs (this group initially failed to return and was re-run after the first attempt was lost to a session-level concurrency limit — two independent passes over this group ultimately landed, one finding each, both included above); (5) the e2e test infrastructure (model_serving.feature, capability.rs, expectation.rs, serving_steps.rs, tui_driver.rs, crates/e2e-report/src/lib.rs, xtask/src/e2e.rs); (6) docs (README.md, docs/architecture.md, docs/manual-testing.md, docs/testing.md, docs/vllm.md) and the two CI workflow comment-only diffs. Each worker's candidates were re-verified against source before inclusion.
CI at the reviewed commit: all checks that can report on a GitHub-hosted lane are green (build-and-test, windows-build-and-test, Test (affected crates), E2E tests, E2E consolidated report, CodeQL, clippy, coverage, Sphinx docs build, lychee, license/notices, commit-signatures). Two self-hosted Strix Halo Ubuntu lanes (nightly, release) show cancelled rather than a pass; MI300X and W7900 lanes were still queued at check time — none had failed.
Did not run as separate passes: "history" (git log/git blame) — the PR's own extensive, already-reconciled review history covers this better than a fresh blame sweep would for a 123-commit branch; "prior-changes" — this method does not read prior review threads, so whether earlier review comments apply here could not be and was not checked. No Rust toolchain was available in this environment, so no finding above rests on an executed run; each is argued from direct source tracing instead. Ticket ROCMAI-419 was not independently retrieved (no Jira tool available in this environment); the PR's own description was treated as the account of its scope, per the method's rule that a stated intent is read but not substituted for retrieval where retrieval is possible — it was not possible here, so this is recorded as a gap rather than assumed. Not reconciled against any discussion on this PR beyond the mandatory claim-check, which surfaced no reviews at this exact head.
🤖 Generated with Claude Code
Both sides added tests to `capability.rs`'s test module: this branch's `only_the_exact_value_the_producer_writes_arms_the_hook` (the value half of the `@requires-oom-fault-injection` contract) and main's three `permission_probe_*` tests. Kept both. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Merged One conflict, in Verified on the Linux MI300X box with the merged tree applied onto main ( Heads-up on a scenario-number collisionThis branch and #703 both currently declare #703 is approved and adds |
jussielo-amd
left a comment
There was a problem hiding this comment.
🔴 AI-assisted review (Claude Code) · head 28843727
This is an assisted human review, not an autonomous merge gate: no GitHub approval is being filed, and the merge decision stays with maintainers. The change request below is for the blocking items only.
Review — needs work
Full review of the PR's own diff at 28843727 (base 6eeec620, merge-base 81f4a46f; 28 files, +3174/−310). The ticket was not retrievable from here, so its scope was not checked. This round re-checks the earlier review at ebc26718 against the author's commit 4187652a and the main merge.
Blocking
- The head cannot merge and no CI has run on it.
engines/vllm/src/process.rshas 4 conflict hunks againstmain, which has since moved the startup diagnostics intoengines/vllm/src/startup_diagnostics.rs. The PR's hunks and about 900 lines of tests have to be ported there. No workflow run exists for this head (GitHub skips PR workflows on a conflicting PR), and the previous commit'sE2E self-hostedrun wascancelled, so the Windows E2E that was red onebc26718has not been re-run to a result. Fix: rebase, port, get a full CI run. - The release-guard test passes when its guard is broken.
xtask/src/workflow_contract.rs:4246-4275vs:4317-4350:the_release_feature_guard_catches_every_spellingre-spells the predicate in its own closure instead of calling the real loop. Ran in a scratch copy: withoffenders.pushmade a no-op and the feature added torelease.yml, both tests stayed green. Fix: extract onerelease_feature_offenders()and call it from both. - The new helper-spawn guard is untested and its comment is false.
apps/rocm/src/serve_cmd.rs:954-966: deletingif !e2e_simulate_oom_launch()leavesserve-28green and arocm daemonprocess running (ran). The comment says the note "is computed before it would have run", but the helper call (line 965) comes before the note computation (line 986 onward). Fix: assert no daemon state or autostart claim remains after the scenario, and correct the comment.
Non-blocking
- Note vanishes on one invalid UTF-8 byte (open from last round):
serve_summary.rs:429-435,services_text.rs:1289-1303. The CLI uses strictlines()over the whole file; the engine uses lossyrocm_core::tail_lines. Ran: a stray byte on line 1 gave no note. Use the engine's reader. - "may still be loading" prints beside "ran out of GPU memory" (open):
serve_summary.rs:166-185,main.rs:510-523. Ran.--helpsays the note is never shown for a still-loading server, but for vLLM that state is normallystarting, which is eligible. The README also omits that the log is read once after a fixed 45 s wait. - OOM selector is slow on the failure path (new):
vllm_oom.rs:96-100, 221-229,diagnose.rs:2268-2285. It runs the whole diagnose registry on two hosts per row. Measured by a review pass: 234 ms for 80 lines, 14.3 s for 6,000\rrows. Pre-filter, or score onlyfix-16. - Stale docs:
terminal.rs:13-15, 415anddocs/architecture.md:160("two callers", now three). The stripper's only production use is an unreachable fallback. - Fault-injection guard covers
release.ymlonly, anddocs/testing.md:1957-1961lists the wrong test. Ran: adding the feature to the nightly publishing builds and theci.ymlbundle build kept all 76 contract tests green. docs/testing.md:1979-1981is wrong: the only CI dispatch filter runs a prebuilt binary, soname_filter=serve-28runs without the hook.- Inaccurate or history-narrating comments:
capability.rs,expectation.rs,xtask/src/e2e.rs, the feature file and the README call the lane binary "the shipping release build" and name the wrong skip gate (requires-no-gpuis checked first).lib.rs:272,fix.rs:3142,serving_steps.rs:1378ande2e-reportnarrate history.service_launch.rs:568links tocrate::append_oom_serve_note, which has moved toserve_summary.
- Hint/command rendering is duplicated in
process.rs:721-727andserve_summary.rs:246-251, and the fixture hand-copies the engine's bail text. One shared function would make "same guard" true by construction. - Two tests claim more than they check: the last block of
the_colourised_round_trip…tests a filter built inside the test, andthe_cli_window_can_miss_an_oom…pins a defect with a hard-coded 77. serve-28never runs the printedrocm diagnosecommand (it does route tofix-16, checked by hand), and the "no note when piped" promise has no test.- PR body is partly stale: verification is pinned to
7bc22302, it says "re-verified" with no CI behind it, and it names an internal review tool.
Decisions for the author
- CLI window can miss an OOM the engine diagnosed (answered-by-intent): the description's "Known gap" says this is deliberate. Confirm the structured-record follow-up.
- No scenario for the engine's startup-failure text (answered-by-intent): the description's "Stated gap" names the lane and why it is not added. Confirm.
- A bare
HIP error: out of memoryno longer gets a hint (tradeoff): the engine hint androcm diagnosestay in step, but thedocs/vllm.mdreason for dropping this string is unsupported. Confirm it was deliberate.
Since the last review
- Fixed:
serve-26renumbered toserve-28;append_oom_serve_notemoved toserve_summary.rs; the falsevllm_oom.rsdoc paragraph; theprocess.rscomment path; the--helpapostrophe caveat; theadvised_commands.rsexclusion; the workflow comments now say "floor". - Open: non-blocking items 1, 2, 4, 5, 8, 9, 10, 11, and the conflict/CI gap (now on a different file).
Deployment notes
- The rebase is real work:
mainalso hasquoting.rsand the AIMserve-26/serve-27. - The squash-body note in the PR banner still applies.
- The engine's startup-failure header text changed.
- The
e2e-oom-fault-injectionfeature is built only bycargo xtask e2e.
What this covered
Three independent passes read the PR diff, and I re-verified the key findings against the PR head. Ran in scratch copies: engine and serve-summary tests, xtask and contract tests, a hooked serve-28 run, clippy, and mutation edits.
Not run: the full workspace gate and smoke_local.py, Windows paths, and a history pass on pre-existing regions. The review threads were not read or reconciled. The fixes on this PR were written by its author.
main moved vLLM's startup diagnostics out of engines/vllm/src/process.rs
into engines/vllm/src/startup_diagnostics.rs, so all four conflict hunks
had an empty main side: the code moved, it was not deleted.
Resolved by taking main's process.rs verbatim and porting this branch's
contribution onto main's copies in startup_diagnostics.rs:
- startup_log_context, oom_utilization_hint and import_failure_hint
replaced with this branch's versions (startup_log_context keeps the
pub(crate) main gave it when it moved);
- STARTUP_FAILURE_LOG_TAIL_LINES now derives from DEFAULT_LOG_TAIL_LINES
rather than spelling 80;
- quotable_in_single_quotes and log_tail_shows_oom dropped here, as this
branch moves the first into rocm_core::terminal and replaces the second
with rocm_core::vllm_oom_diagnose_symptom returning Option.
All 23 diagnostics tests moved with them, byte-identical to this branch's
versions, including the IMPORT_TRACEBACK fixture. Verified that every one
of the 54 #[test] fns this branch's process.rs declared is present in the
merged tree: 23 in startup_diagnostics.rs, 31 left in process.rs.
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…able Review round on #284. - `the_release_feature_guard_catches_every_spelling` re-spelled the predicate in its own closure instead of calling the real loop, so breaking the production guard left both tests green while a feature-carrying release build sailed through. Both now call one extracted `release_feature_offenders`. - Deleting the `if !e2e_simulate_oom_launch()` guard in `serve()` left serve-28 green and a `rocm daemon` process running, because every other assertion in that scenario is about summary text. serve-28 now asserts no daemon runtime-state and no autostart claim survive the fabricated launch. - The comment on that guard claimed the OOM note "is computed before it would have run". It is not: the suppression runs before the note is computed in the `summary_mode` block below. Corrected to state the real justification — there is no spawned process to supervise. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
@jussielo-amd — all three blocking items addressed. Pushed as 1. The head cannot merge / no CI has runMerged. Resolved by comparing bodies rather than names, because name-presence is not body-equivalence here: of the ~32 same-named symbols in the conflicted regions, 3 genuinely differ and carry this PR's whole contribution — All 7 PR-authored tests in those hunks were relocated into 2. The release-guard test passed when its guard was brokenYou were right, and the mutation reproduces exactly as you described. Extracted one Proven falsifiable rather than asserted: with 3. The helper-spawn guard was untested and its comment was falseBoth fixed. serve-28 now ends with On the comment — you were right that it was backwards. The suppression runs before the note is computed in the VerificationLinux MI300X box, merged tree applied onto
Scenario numberingThis branch still declares Your three decision questions — all confirmed deliberate
Still open from your non-blocking listNot fixed in this round, and I would rather say so than imply otherwise: #1 (strict Not verifiable here either way: the box has no |
All three blocking items addressed in e229c69: the branch merges again (main moved vLLM startup diagnostics to startup_diagnostics.rs; 3 production fns ported, 7 tests relocated and verified present); the release-feature guard is now one extracted release_feature_offenders called by both tests, proven by mutation to fail where it previously stayed green; and serve-28 now asserts no daemon runtime-state or autostart claim survives, with the false ordering comment corrected. Non-blocking items 1, 2, 5, 8, 9, 10 and parts of 11 remain open and are listed as such in the comment. Re-requesting your review.
#703 landed, so this branch's scenario finally renumbers: `serve-28` is now its, and this one becomes **serve-33**. Every reference that belongs to this branch moved with it (docs/testing.md, capability.rs, serving_steps.rs, serve_cmd.rs); #703's own cross-references to serve-28/30/31/32 are left alone. The substantive overlap is the no-usable-GPU bail in `serve()`. #703 refactored it onto `no_usable_gpu_refusal(&device_policy)` and `reuse_existing && host_has_no_usable_gpu`; this branch adds `!e2e_simulate_oom_launch()` so the fault-injected launch is not refused before it can fabricate its OOM. Merged condition carries both terms. Everything else is both-sides-additive documentation: README, docs/testing.md and the serve `--help` text each gain the reuse-exemption section next to the OOM-guidance one. In docs/manual-testing.md both sides claimed `## 5`, so the reuse section keeps it and the memory-guidance section becomes `## 6`, with the sections after it shifted to 7..11. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Merged The substantive overlap was the no-usable-GPU bail in One thing worth recording, because it nearly shipped: my first resolution of Verified on a Linux MI300X box with the merged tree applied onto |
jussielo-amd
left a comment
There was a problem hiding this comment.
Thanks for the work on this, and for the last round. I confirmed the release-guard test now calls the real release_feature_offenders, and the helper-spawn guard has its serve-33 assertion. I read the whole diff at fe213ce7 from the code. Two things block, and both are cheap:
Blocking
- Today's renumber merge left the docs wrong.
- The existing reuse scenario is still
serve-28 - Reuse succeeds on a GPU-less hostinmodel_serving.feature(L395), butdocs/testing.mdnow calls it serve-33 in several places (~L2063, 2068, 2072, 2093-94, 2114). So serve-33 names two different scenarios, and-n "serve-33 - Reuse..."selects nothing. The new OOM scenario is the real serve-33; the reuse text should be serve-28 again. - The new "Serve OOM Guidance" section is spliced into the middle of a sentence: L1993 ends "E2E scenarios, in" and the sentence only resumes at ~L2047, so the reuse scenario list now sits under the OOM heading. Moving the OOM block after the serve-31 bullet fixes it.
docs/manual-testing.md:## 6has subsections### 5a/### 5b, andterminal.rs:203cites "§5a". They should be 6a/6b and §6a.- Still open from last round: "The CI dispatch filter takes the same route" (~L2142) is wrong. The dispatch filter runs a prebuilt binary, so
name_filter=serve-33runs without the hook.
- The existing reuse scenario is still
- The branch conflicts with
mainagain (crates/rocm-core/src/lib.rs; GitHub shows it as dirty). It needs another merge and a full CI run.
Non-blocking
- The two notes can still print together for the exact launch this targets: "may still be loading" (
serve_summary.rs~L166-182) beside "ran out of GPU memory". No test asserts the first is absent. serve_summary.rs's module doc still says it is engine-agnostic, but this adds ~600 lines of vLLM-specific OOM logic. AGENTS.md §6 points to its own file.- Still open from last round: the OOM selector's cost on the failure path, the note vanishing on one invalid UTF-8 byte, and no test for the "no note when piped" promise.
service_launch.rs:701linkscrate::append_oom_serve_note; it now lives inserve_summary.- The release guard misses PowerShell backtick continuations (
release.ymlhas ashell: pwshjob), counts eachrun: |build twice and doesn't strip comments. docs/architecture.mdstill says the terminal stripper has "two callers" and describesstartup_diagnostics.rsas stripping the tail.- The fixtures hand-copy the engine's bail header, so serve-33 would stay green if the real text drifts.
CI: hosted checks are green; the Strix Halo Windows lane is red (it fails on unrelated PRs too) and MI300X is still queued. Not run: tests (read-only review).
🤖 by agenthub on AMD AgentHub
ROCm#703) * feat(serve): exempt reuse of a running service from the no-GPU refusal Under the default `gpu_required` policy `rocm serve` refuses outright on a host with no usable AMD GPU, before any engine is prepared. A `serve` that matches an already-running managed service for the same engine and model launches nothing and pins no GPU, so that refusal buys nothing: this adds a pre-gate that lets exactly that case through to the reuse short-circuit. This is a user-visible behaviour change. `rocm serve <model>` on a GPU-less host can now succeed where every prior release refused. The exemption is narrow. The gate opens only when the host predicate the bail itself uses is true, the policy is not `cpu_only`, and a live managed record matches this engine and model; the record must then also survive `managed_service_reuse_refusal`, which now additionally refuses a `gpu_required` serve against a service recorded `cpu_only`. `--gpu <index>` validation keeps the position ROCm#267 gave it, ahead of the pre-gate's work. Split out of ROCm#284 at reviewer request: ROCMAI-419 scopes that PR to OOM diagnostics and states it changes diagnostics only, not serve behaviour, so under AGENTS.md section 11 this belongs in its own PR. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> * test(serve): cover the reuse pre-gate's own arguments, not just its predicate The unit tests pinned `managed_service_reuse_refusal` and the record filter, but nothing pinned which values `serve()` hands them. Three mutations of the call site survived; two of them silently restore the dead end this change exists to remove. Measured, not assumed: - passing `None` for the requested recipe — SURVIVED the whole suite. Every scenario plants `engine_recipe_json: null` against a serve that requests no recipe, so the argument matched either way. - passing `None` for the device policy — SURVIVED the whole suite, including the unit tests that cover the term directly. - dropping `!reuse_existing` from the bail — caught by serve-26 on a GPU-less host (`serve-reuse-exempt-from-no-gpu-refusal` fails). serve-30 closes the first: a live service for the same model against a serve asking for a `--tool-call-parser` it was not started with is a recipe mismatch, so it must still get the no-GPU refusal. With the argument dropped the bypass is granted and the spawn's "stop it and run `rocm serve` again" reply replaces it — the exact dead end, reproduced. The second is now a compile error: `managed_service_reuse_refusal` takes the device policy by value rather than as an `Option`. The spawn has one in hand, so it passes its own instead of opting out, which also makes its `DevicePolicyTooWeak` bail reachable rather than dead. Nothing observable widens: `parse_device_policy` refuses `cpu_only`, so no record this CLI writes can carry the policy that term refuses. Also from review: `host_has_no_usable_gpu` is a named function called by both the bail and its unit test, which used to restate the expression; the bail's comment no longer claims the pre-gate opens "only when this invocation is about to reuse" (lenient model matching means it can open for a service that is then refused, and an error from the probe can replace the GPU refusal); the always-false `cpu_only` term says why it is kept; README states that an explicit `--gpu <index>` defeats the exemption for every index, not just an absent GPU 0; the manual-QA section is marked Linux/WSL-only with a PowerShell substitution; docs/testing.md reconciles its "no focused command" note with the `-n` guidance elsewhere in the file; and the stale serve-27/28/30 cross- references in comments are corrected. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> --------- Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Important
Whoever squash-merges this: write the squash body by hand, from the final
diff — do not inherit the per-commit trail. This repo's squash setting is
COMMIT_MESSAGES, and about 16 of this branch's commits narrate work that isno longer in the diff: the reuse pre-gate and its
serve-26..30scenarios(moved to #703), the
--gpuvalidation / auto-select work (already onmainvia #251), and
956891f's claim that the fix-16 checker is "gated to Linux"(reversed by
13bd32c).1de61359's message also contains one sentence thatwas false when written. The final diff is: the serve-summary OOM note, the
crates/rocm-core/src/vllm_oom.rsreader, the shell-quoting guard's move intorocm-core, thee2e-oom-fault-injectionfeature and the release-featureguard,
serve-28, and the docs. History is not being rewritten for this(it would need a force-push to a shared branch).
Note
Merged with
mainon 2026-11-02 and re-verified.main's Phase-6x/7xmodularization moved most of what this branch touches, so several conflict
hunks had an empty
origin/mainside; each was re-applied into the modulethat now owns the code (see the merge commit message for the file-by-file
account).
maintookserve-26/serve-27for the AIM lanes, so thisbranch's OOM-launch scenario is now
serve-28— the feature file, thestep file,
capability.rsanddocs/testing.mdall moved with it.The sections below that still describe
serve-29and the reuse pre-gatedescribe work that now lives in #703, not in this branch.
Summary
OOM guidance on a failed
rocm serve(ROCMAI-419, ex EAI-8059). When amanaged vLLM launch fails to become ready and its own log carries a real
allocator OOM signature, the interactive deployment summary names the memory
knobs (
--gpu-memory-utilization,--gpu <index>), says plainly that loweringthe reservation does not help a model that simply does not fit, and routes the
user's actual failing line into
rocm diagnose --symptom '<line>'.Note
The serve-behaviour half of this PR has been split out to #703.
Reusing an already-running managed service is now exempt from the no-GPU
refusal — a user-visible change outside ROCMAI-419's "diagnostics only" scope.
Both a human reviewer and
pr-review-watcherasked for it to ship on its ownunder AGENTS.md section 11, and
7bc22302removes it from this branch. Themechanics are unchanged; only the PR they live in moved. What this branch
loses with it is
serve-25(Reuse succeeds on a GPU-less host and the summarycarries no memory guidance), and it loses no coverage doing so: that
scenario's memory assertion was already vacuous, because the real reuse path
reports
log_path: Noneandappend_oom_serve_noteis withheld on that groundbefore the
already_runningclause is reached. The clause itself stays pinnedhere by the unit test
append_oom_serve_note_ignores_an_already_running_services_log, which feeds itan OOM-bearing log path directly.
Detection is deliberately narrow: each tail line is classified with the same
rule as the
rocm diagnosevLLM-OOM checker (scored asvllm: <line>againstthe shared match threshold), so a bare
out of memoryfrom a kernel OOM-killerline, a dependency's log, or vLLM's generic
EngineCorewrapper never getsreported as memory exhaustion. One shared helper,
rocm_core::vllm_oom_diagnose_symptom, is both the OOM gate and the lineselector for the engine hint and the CLI note, so neither surface can apply a
rule the other does not. That is a shared rule, not a shared answer: the two
read the shared log at different points, and two measured consequences of that
are stated under Correction and Known gap below. An earlier version of this
paragraph claimed the two "cannot drift"; that claim was false and is withdrawn.
quotable_in_single_quotesmoves out ofengines/vllm/src/process.rsinto theexisting
crates/rocm-core/src/terminal.rs, so both surfaces share one copy ofthe rule that decides whether the user's line can be put in a shell-quoted
argument. (
strip_terminal_control_sequencesandrendered_linesalready livedthere on
main; an earlier version of this paragraph described all three asmoving into a new file, which was wrong on both counts.)
Base is
main. #251 merged on 2026-10-06 and is no longer a dependency.Behavior coverage
@id:)serve-oom-launch-memory-guidance(serve-28)@requires-oom-fault-injectionThe four reuse-exemption scenarios this table used to list moved to
#703 with the exemption.
serve-28is this PR's only new scenario; itsits behind
main's ownserve-25(The recommended model list printsASCII-only text, from #683, merged after this branch forked).
@id:diagnose-vllm-oom-is-conditionaland@id:serve-vllm-low-vram-oom-guidance(
serve-23) came from #251, now merged; this PR does not 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 byROCM_E2E_SIMULATE_OOM_LAUNCH) that fabricates exactly the failed-launch state.The hook is absent from shipped binaries (
release.ymlnever passes thefeature), so the scenario reports a skip on the self-hosted
(prebuilt-release) lanes rather than a silent green.
That hook proves rendering, not the launch path: it returns before the spawn,
the record claim and the readiness wait, so
serve-28shows that a failedlaunch report renders the note, not that a real failed launch produces one.
Stated gap: the vLLM engine's own startup-failure text has no scenario
AGENTS.md section 3 asks that a change to command output be covered by a Gherkin
scenario, and that a gap be stated in the PR text with the lane named. Stating it:
no scenario covers the vLLM engine's startup-failure message, and none of
today's lanes would run one.
The text in question is what a user sees through
rocm services logs <service-id>,--verboseor--foregroundwhen a managed vLLM launch dies.This PR changes it four ways (
startup_log_context/oom_utilization_hintinengines/vllm/src/process.rs):Startup log, last 80 lines as the terminal drew them;HIP error: out of memory, a kernelOOM-killer line, or vLLM's generic
EngineCorewrapper (the deliberatenarrowing described above, which keeps the hint in step with
rocm diagnose);\r/ESC[2Krepaint lines it previously missed.Nothing under
tests/e2e-cucumber/features/reaches that code: grepping forexited before becoming readyandStartup logfinds no match, and serve-28'slog fixture is written by the CLI's own fault-injection hook, so it never calls
the engine.
The lane that would cover it is
E2E tests (MI300X)ine2e-selfhosted.yml— the only lanewhose GPU family makes vLLM the effective serve engine, and so the only one where
a real vLLM process can be started and made to fail. It does not cover it today,
and this PR does not add a scenario there, for two reasons worth stating rather
than burying: that lane runs the prebuilt release binary, which has the
fault-injection hook compiled out by design, so the failure would have to be a
genuine VRAM exhaustion; and provoking one reliably on a 192 GB MI300X means
either a deliberately oversized model download or a
--gpu-memory-utilizationvalue tuned against whatever else is resident, which is GPU-expensive and racy
against the other scenarios sharing that card.
What does hold the changed text today:
rocm-engine-vllm's unit tests(
the_echoed_tail_never_welds_two_rendered_rows_into_one_line,the_hint_reads_the_raw_tail_and_displays_the_row_that_scored,the_colourised_round_trip_quotes_the_row_the_terminal_drew,oom_utilization_hint_stays_quiet_for_unrelated_failures) exercisestartup_log_contextend to end over a real file. Per AGENTS.md section 3 thatproves the function, not the behaviour, so it is a gap and not a discharge.
Known gap: the note can go missing when the OOM line is early in the window
Reported by the automated review and reproduced here (
PROBE engine_hinted=true cli=None). Both surfaces use the same tail budget(
rocm_engine_protocol::DEFAULT_LOG_TAIL_LINES= 80), but not the same window.The engine scans the last 80 raw lines and bails; the supervisor appends that
whole bail — a header, the echoed tail, the hint — to the same shared log; the
CLI then reads 80 lines of the now-longer file. Its window therefore always
reaches less far back into the original log than the engine's did, and when the
bail is itself longer than 80 rows it holds nothing the engine wrote directly.
Measured: a log with the OOM line followed by 77 further lines is diagnosed by
the engine and produces no serve note at all.
This is not fixed here, and the reason is that the obvious fix trades one defect
for another. Re-windowing the CLI to the 80 lines before the bail restores
agreement, but it brings the raw colourised line back into view, and the note
stops quoting the user's own rendered row — the regression
the_colourised_round_trip_quotes_the_row_the_terminal_drewexists to prevent.The clean fix is structural: have the engine record its selected symptom as data
(in the service record or a sidecar) instead of having the CLI re-parse text the
engine already diagnosed and echoed. That is a larger change than this PR, and I
would rather raise it than guess at it — say the word if it should land here.
What changed instead: the gap is now pinned by
the_cli_window_can_miss_an_oom_the_engine_saw, the comment inappend_oom_serve_notethat claimed the two surfaces "agree on what counts as'the tail'" states the gap, and the README's "absence is not evidence" list names
this condition alongside the others.
Verification
Full gate green on Linux at the pushed head
7bc22302, all four lanes:cargo test --workspace --all-targets --no-fail-fast(3805 run across 33 testbinaries; 0 real failures — the only red was
cli_progress::tests::animated_spinner_keeps_ticking_without_progress_calls, aknown timing flake under parallel load, which passes in isolation: 1 passed, exit
0),
cargo clippy --workspace --all-targets -- -D warnings(exit 0), and theseparate
cargo clippy --locked -p e2e-cucumber --test e2e -- -D warnings(exit 0), which
--all-targetsskips because that harness isharness = false.cargo fmt --allclean.cargo test -p e2e-cucumber --test feature_naming(exit 0, 4 passed) pins the renumbering.
Merged
main(f0d6ce3b) in43032a37, and two silent semantic conflictsonly a real build caught — git took
main's copy of each with no marker:engines/vllm/src/process.rsreferencedMAX_TAIL_READby bare name in itstest module;
mainhad dropped that import in favour of the fully-qualifiedpath. Fixed in
860f906e.the OOM note's em dash plus the one in
rocm serve --helptrippedevery_literal_that_can_reach_a_redirectable_sink_is_asciiandevery_help_text_in_the_command_tree_is_ascii. Fixed in7bc22302.Changes in the latest round (2026-10-15 review)
The round was COMMENTED — nothing gated. Every claim was checked against the
code before acting; all of them held.
The
gpu_preferredguard from1de61359is reverted (blocking, and theautomation retracted its own earlier advice that produced it).
parse_device_policy(
main.rs) mapsauto,gpu,gpu_requiredandgpu_preferredontoDevicePolicy::GpuRequiredand rejectscpu_onlywith an error, soGpuRequiredis the only policyserve()can handmanaged_service_reuse_refusal— its only caller that supplies one. The!matches!(.., None | Some(CpuOnly))predicate was therefore identical atruntime to the
GpuRequiredcheck it replaced, and three artefactsdescribed a bypass that cannot happen: the production comment, the test
comment, and
1de61359's own commit message (see the banner above).Reverted rather than kept as defence in depth. The broader match guards
nothing a caller can produce, it contradicts README.md's "that is the only
policy pair refused" — and the README governs, since
GpuRequiredis the onlyvalue the CLI can pass — and this repo does not carry branches for states that
cannot occur. The term is back on
Some(GpuRequired)with the exhaustivenessargument written out and pointed at
parse_device_policy_defaults_to_gpu_required_without_cpu_fallback. The twounreachable-input assertions go with it, one of which was a verbatim duplicate
of the
CpuOnlycase already below it (a separate non-blocking finding,resolved by the same revert). Falsified: dropping the device-policy branch
makes
reuse_is_refused_on_every_term_the_spawn_and_the_gpu_bail_depend_onfail (1 failed), restored green (1 passed).
serve-29 closes the untested seam at
serve()'s pre-gate call site(blocking). The claim was reproduced exactly: restoring the old
!cpu_only && any_live_managed_service_for_model(..)call site leaves thewhole
rocmbin suite green — 1170 passed, 0 failed. The unit test pinsreuse_pregate_opensbut re-derives the host predicate in a local closure, andserve-25/27/28 all run on GPU-less hosts where the gate opens either way.
A unit test cannot close this: the host GPU probe is real hardware with no
injection point, so the hand-off is only observable end-to-end. serve-29
(
@id:serve-live-reuse-candidate-does-not-preempt-gpu-validation,@requires-gpu @requires-os:linux) plants a live vLLM record matching onliveness, engine and canonical model id — so every pre-gate term except
host_has_no_usable_gpuis satisfied and that predicate alone holds the gateshut — then serves the same model with
--gpu 99. It borrows serve-27'ssetup-venv fixture, and deliberately passes no
--env-id, sorecover_setup_runtime_registrationis reachable: with the gate wrongly openthe manifest is re-filed before the refusal.
Falsified on a real GPU host (
platform=mi300x os=linux gpu=true): green aswritten (1 scenario, 6 steps passed); with the call site mutated back, red at
exactly the discriminating step —
no runtime registry manifest was written(1 scenario failed, 5 of 6 steps passed,
recover_setup_runtime_registrationre-filed
therock-release-gfx942-1.0.0.json). Restored, green again.docs/testing.md was stale (blocking). The quoted serve-25 name predated its
retitle in
1de61359— corrected, in the passage that exists to warn that-nmatches the scenario name. "The pre-gate has three conditions" omitted
host_has_no_usable_gpuandcpu_only; it now documents all five, splits theminto the match terms and the admission terms, and records serve-29 with its
lane.
Non-blocking items taken:
reuse_pregate_opens's doc said the gate opens "exactly when the bail is inplay". The bail also requires
!scripted_backend_failureand!e2e_simulate_oom_launch(), so in test-hook builds the gate can open wherethe bail cannot fire. Qualified, with why that is deliberate and why it costs
nothing in a shipped binary.
and the serve note print different commands. Confirmed overstated against
every_rendered_line_break_keeps_the_gate_in_step_with_the_scorer: the log issplit on
\r, so a bare-CR repaint yields a clean segment both surfaces quoteidentically. Narrowed to repaints carrying an escape (
ESC[1A,ESC[2K).crates/rocm-core/src/terminal.rsclaimed the rejected line "stays visible inthe human-readable sentence beside the command (and in the log tail printed
with it)". True of the vLLM engine hint, which renders the row and prints the
tail above it;
oom_memory_noteis one sentence plus the command and showsneither. Split per surface, pointing at
rocm services logs.docs/manual-testing.md's colour-codes bullet cited the README as plain text;now a relative link, as AGENTS.md §5 asks.
tui_driver.rsnamedROCM_E2E_SIMULATE_OOM_LAUNCHas a user ofspawn_with_env. Confirmed stale: the switch goes ontoworld.command_env,which the shared spawn path already applies, and the only
spawn_with_envcaller is the fix-prompt step in
diagnose_steps.rs.Non-blocking items declined:
!matches!(gpu_selection, GpuSelection::Index(_))inreuse_pregate_opens. The reasoning is sound — on a GPU-less host every--gpu <index>is out of range, so the gate can still install Lemonade andwrite the registry before refusing. But it is a behaviour change on a path no
finding in this round calls wrong, it needs its own scenario to be worth
having, and this PR is already carrying one out-of-ticket behaviour change
(see Open question). Follow-up, not this round.
e2e-selfhosted.yml,nightly.yml).Still true, still unrelated to this PR's hunks; left in Known deferrals.
Changes in the latest round (2026-10-14 review)
Three gating findings, all confirmed by reading before any change.
The reuse pre-gate no longer does engine and runtime work before
--gpuisvalidated. This was a real ordering regression this PR introduced: the gate
was
!cpu_only && any_live_managed_service_for_model(..), but its onlyproduct,
reuse_existing, is a term of a bail that can fire only whenvisible_gpu_indices == Some([]). So on any host with an overlapping liveservice name — GPU or no GPU —
validate_engine_selection_runtime(which canwrite the runtime registry),
ensure_self_managed_engine_ready(which caninstall Lemonade) and
ResolveModelall ran ahead ofvalidate_pinned_gpu_index, and a?from any of them pre-empted theGPU-specific refusal. The comments at the
--gpuvalidation and at theruntime resolution that claimed otherwise were false, and are corrected.
The bail's host predicate is now computed once and handed to both through
reuse_pregate_opens, which takes the record probe as a closure — on ahost with a GPU the managed-service read, and the
load_managed_servicesliveness refresh it entails, is not paid at all.
the_reuse_pregate_stays_shut_on_a_host_that_has_a_usable_gpuwas proved tofail first: with the predicate dropped from the gate the test panics (exit
101), and it asserts both halves — the gate is shut, and the probe is never
consulted.
--helpno longer promises that nothing is prepared for a live server thisrun cannot reuse. A same-model service that
managed_service_reuse_refusalrejects (recipe mismatch, or
--require-api-keyagainst a keyless server)still passes
any_live_managed_service_for_model, so preparation has alreadyrun by the time the refusal is known. The README was already correct; the help
was the copy that drifted, which made the previous round's claim that all three
surfaces had been corrected untrue. It is true now.
docs/manual-testing.md§5a no longer tells testers a colourised line fallsback to the canonical symptom. That is the engine's behaviour; the note
quotes the row the terminal drew. §5a now splits the apostrophe case from the
colour case and names
the_colourised_round_trip_quotes_the_row_the_terminal_drew. This was thefourth surface of a claim a previous round fixed on three; the tree was grepped
for the claim's meaning (a multiline search for colour/control-byte wording
near "canonical"/"falls back"), and README,
docs/vllm.mdandrocm-core/src/lib.rsalready state it correctly — there is no fifth.Two of the four non-gating Blocking items were taken as well:
the GPU bail, not just
gpu_required.serve()keyscpu_onlyoffmatches!(policy, CpuOnly), so a--device gpu_preferredserve goes throughthe bail and could have bought its bypass from a
cpu_onlyservice. The unittest now discriminates
gpu_preferredandcpu_onlyseparately. (The e2erecord writer still plants
device_policy: null, so the term remains withoute2e coverage — that half is not taken; the state it guards needs a
cpu_onlyrecord the CLI never writes.)
OOM-absence assertion cannot fail on that path — the reuse path reports
log_path: None, so the note is withheld on that ground alone — and thecomment now says so. What only serve-25 proves is the exemption itself.
the_utilization_hint_states_the_bound_the_parser_acceptsnow namesgpu_memory_utilization_rejects_out_of_range_and_unparsable_valuesrather thanthe function, as AGENTS.md §3 asks.
Left, with reasons: the duplicate tail reader (
main.rs'sread_tail_linesuses
BufReader::lines()and errors on invalid UTF-8, so the note silentlydisappears) is real, but it is shared by six call sites including
rocm services logs, so swapping it forrocm_core::tail_linesis a wider change than agating-fix round should carry — raised rather than guessed at. The serve-25
already_runningcall-site argument is still held by nothing; covering itmeans a new seam, which is the thing the previous round deliberately removed.
Changes in the previous round (automated review follow-up)
The classifier now keeps step with the diagnose scorer across the whole
line-break set. The log was split on
\n/\r, but the scorer re-splits withrendered_lines, which also breaks on every non-SGR CSI (ESC[2K,ESC[1A),ESC E/ESC D, VT, FF,U+0085andU+2028/U+2029. A real progress-barrepaint,
...90%\r\x1b[2Ktorch.OutOfMemoryError: ..., therefore left thevllm:anchor on an empty row, scored 0, and silently dropped both theengine hint and the serve note. Scoring is now done per rendered row while the
raw segment is still what gets returned, so the quoting guard keeps
rejecting colourised lines and the canonical fallback keeps firing. A segment
that renders as more than one row must contain a control byte, and
quotable_in_single_quotesrejects every one of those — so within one readit cannot reach the printed command as a stripped lookalike of the user's
error. See the correction below for the bound on that, which is narrower than
this paragraph originally claimed.
The note now quotes the user's own failing line in real use. The supervisor
and the engine share one log. When vLLM OOMs the engine bails with its
startup-log context — which embeds rocm-cli's own OOM hint — and the CLI
writes that error back into the same file, after the real line. That hint
scores as an OOM and contains
', so the newest-first scan always selected itand the note fell back to the canonical symptom in every real run: the
headline "we quote your failing line" promise never actually held in the field.
Lines rocm-cli authored are now skipped, keyed off shared constants so the hint
and the filter cannot drift. The e2e fixture now writes the engine's full error
block, so
serve-28can see this shape.The exact contract is now stated, because the stronger one is false.
vllm_oom_diagnose_symptomdoes not guarantee every returned line isdiagnosable, and cannot without reintroducing the control-byte hole. What holds
is: a line that passes
quotable_in_single_quotesis diagnosable, and one thatdoes not is replaced by the canonical symptom, which is. That is exactly what
the callers gate on. Found by the new break-set test falsifying the old claim.
Dead code removed.
vllm_log_shows_oomwas apub fnwith no productioncaller; its threshold assertions moved onto the gate that actually ships. The
two
serve_summary.rstests that only re-tested it now go throughoom_memory_note, so they can fail when that module changes.Pre-gate test gap closed. Two of the pre-gate's three conditions were
deletable with the suite green. Added a case where only
record.model_refmatches and one with a non-live record; both mutants are now killed.
Doc/comment corrections.
--help, README anddocs/manual-testing.mdnolonger promise that nothing is prepared before the no-GPU refusal.
docs/vllm.mdstates the narrowed hint trigger. Theserve_cmd.rscommentclaiming the pre-gate "reads the managed-service records and nothing else" is
qualified against the liveness refresh it actually does.
Correction: the raw-split guarantee is per-read, not end-to-end
The automated round at
40e7b5c6was right that for a colourised OOM linethe engine and the CLI print different
rocm diagnose --symptomarguments, andthat the code and this description both claimed otherwise. Measured:
The cause is the engine's own bail. It echoes the tail as rendered rows — it has
to, that text goes to a terminal — and the supervisor appends the whole bail to
the same shared log. By the time the CLI reads, the echo is the only copy of
the failing line left in the 80-line window: on a 200-line log the raw line sits
84 lines from the end, and the echo header has scrolled out as well.
Suppressing the echo to make the two agree is not available — measured, it
leaves no OOM line in the window at all and the serve note disappears for every
real managed launch. So the guarantee is stated for what it is, a property of a
single read, and the real two-read sequence is pinned by
the_colourised_round_trip_quotes_the_row_the_terminal_drew.The difference is benign: the CLI's argument is a row the terminal really drew,
carries no quotes or control bytes, and quotable still implies diagnosable.
Scenario numbering. An earlier round of this PR had taken
serve-24— thelanded
--require-api-keyreuse refusal — for a new scenario and renumbered thelanded one, leaving two references on
mainpointing at the wrong scenario. Thelanded scenario now keeps
serve-24and the new ones take 25-28 below it, soboth the landed id and the file's positional numbering hold.
Split out of this PR
The reuse exemption now ships as #703, off current
main. Nothing aboutits mechanics changed in the move; the scenarios are renumbered against
main(serve-28 through serve-29 there) and the OOM-flavoured dressing on the reuse
scenario — the planted OOM log, the vacuous "does not blame this invocation for
GPU memory" assertion — stayed here as OOM work rather than travelling with it.
Review history below that discusses the pre-gate,
reuse_pregate_opens,managed_service_reuse_refusaland the former serve-25/27/28/29 is kept for therecord, but that code is no longer in this branch. The
[!IMPORTANT]banner at the top still applies:1de61359is still in thisbranch's history and this repo builds the squash body from commit messages.
Known deferrals
Non-blocking review items not addressed here, listed so they are not lost:
reuse is still decided without the launch lock; the note can in principle fire on
a still-loading server;
nightly.yml's publishing builds are not covered by therelease-feature guard; and some feature-file comments still name drifted scenario
numbers. See the review reply comment for the full list and rationale.