Repository navigation
feat(vllm): enhance OOM diagnostics and guidance in serve summary (eai-8059) - #284
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 |
|
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>
Review round on #284. The previous merge's renumber was too broad and the section splice was wrong; both are my errors, not the reviewer's. - `docs/testing.md` renamed the *reuse* scenario's references to serve-33 as well, so serve-33 named two different scenarios and `-n "serve-33 - Reuse..."` selected nothing. The reuse references are serve-28 again; only the OOM scenario is serve-33. - The OOM section had been spliced into the middle of a sentence ("E2E scenarios, in" ... resuming 40 lines later), putting the reuse scenario list under the OOM heading. Both sections are restored from their own sources and are siblings again. - `docs/manual-testing.md`'s `### 5a`/`### 5b` follow their parent to `### 6a`/`### 6b`, and `terminal.rs`'s §5a citation with them. - The claim that "the CI dispatch filter takes the same route" was backwards: that route runs a prebuilt binary with the hook compiled out, which is the hazard the following paragraph describes. - `architecture.md` still said the terminal walk had "two callers" (it has four) and that `startup_diagnostics.rs` strips the tail (it re-renders it; stripping is what welded lines together). `service_launch.rs` linked `crate::append_oom_serve_note`, which now lives in `serve_summary`. Also merges main: `lib.rs` gained `ManagedServiceNotFound` upstream alongside this branch's `terminal` re-export. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
main added its own serve-33/34/35 (native-Windows vLLM refusal) and now runs to serve-39, so this branch's OOM scenario renumbers serve-33 -> **serve-40**, with its own references moving too. main's serve-33/34/35 references in README.md, docs/testing.md and workflow_contract.rs are left alone. main also pluralised `engine_env_note` to `engine_env_notes`; the OOM note block adapts to `notes.extend(engine_env_notes.iter().cloned())`. The release-feature guard doc and main's new comfyui/Windows paragraph are both additive and both kept. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Merged The scenario renumbers again: This is the second renumber this branch has taken, both forced by scenarios landing on Two other conflicts:
Verified on a Linux MI300X box with the merged tree applied onto
The |
jussielo-amd
left a comment
There was a problem hiding this comment.
Thanks Roman, the blockers from my last round are fixed in cd4f51f2: the head merges cleanly, the release-guard test calls the real function, serve-40 asserts no daemon is left behind, and the docs are consistent again (reuse scenario back to serve-28, OOM section no longer splices into the reuse list, 6a/6b and §6a). Hosted CI is green. The one red hosted check, remote control channel (containerised), failed on a rustup DNS lookup, so it needs a re-run and is not caused by this change. Approving.
Non-blocking, roughly in order of user impact:
- UX: for a vLLM launch that OOMs, the summary prints "the server did not report ready ... it may still be loading" (
serve_summary.rs:166-182) and then "ran out of GPU memory". Suppressing the first line when the OOM note applies would avoid the mixed message. No test asserts they don't appear together. - UX: the note disappears if the log holds one invalid-UTF-8 byte (strict
lines()vs the engine's lossy reader). You mentioned taking this next, so good. Also worth a line in the README: the check is a single read after the ~45 s startup wait, so a later OOM only shows inrocm services logs. --help(main.rs:554-557) says a colourised line falls back to the canonical symptom, but the code, README anddocs/vllm.mdquote the rendered row. Narrowing it to the apostrophe case would match.- The startup-tail echo expands every
\rrepaint into its own row (startup_diagnostics.rs:58-62), and the OOM scorer runs the diagnose registry per row. A progress-bar-heavy failure could print or spin a lot. Capping the echoed rows and pre-filtering on "memory" would bound both. - The hint placeholder
<fraction greater than 0 and at most 1>can't be pasted as-is and differs from--help'sFRACTION. Putting the bound in prose would be friendlier. - Small: the merge garbled two doc comments in
workflow_contract.rs(~4216-4219 and ~4383-4385), and the serve-40 skip reason says the hook is "compiled out" when the env var simply isn't set.
🤖 by agenthub on AMD AgentHub
|
@copilot resolve the merge conflicts in this pull request |
Keep main's serve-40 helper scenario and move the OOM launch scenario to serve-41, updating its documentation and test references. Signed-off-by: GitHub <noreply@github.com> Co-authored-by: r0x0r <1549657+r0x0r@users.noreply.github.com>
…nobs-note Co-authored-by: r0x0r <1549657+r0x0r@users.noreply.github.com> Signed-off-by: GitHub <noreply@github.com>
Head branch was pushed to by a user without write access
#284 landed, so `serve_summary.rs` and `docs/testing.md` now carry its OOM tests and prose alongside this branch's device-row ones; both kept. main also added the stop-confirmation contract to the README and the serve `--help` text, which sits beside this branch's device-line paragraph rather than replacing it. `engine_manages_own_runtime` moved to `engine_install_env`, and `rocm-core` gained `windows_amd_gpu_listing` next to this branch's `pinned_gpu_name` re-export. 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.