test(e2e): activate a runtime explicitly in the shared pre-warm tree - #322
Conversation
tomastola
left a comment
There was a problem hiding this comment.
Reviewed at ac1016e. The diagnosis is convincing and refusing to guess between several runtimes is the right default. One thing I'd fix before merge, two smaller ones.
1. activatable_runtime_keys isn't using the same rule as the pre-warm's repair
shared_runtime.rs:86 claims parity:
Judged by the same rule the pre-warm's own repair uses
but assess (xtask/src/e2e_prewarm.rs:184-189) matches against two roots:
let mut roots = vec![runtimes_dir.to_path_buf()];
if let Ok(resolved) = runtimes_dir.canonicalize()
&& resolved != *runtimes_dir
{
roots.push(resolved);
}with the reason spelled out directly above it — "so a caller that passes a path through a symlinked parent still matches the roots the CLI wrote." shared_runtime.rs:107 keeps only the first:
Path::new(root).starts_with(runtimes_dir)That second arm isn't decorative here, because the two sides are resolved-vs-as-given by construction:
install_rootin the manifest is canonicalized before it is written — that is exactly what #317, this branch's merge base, changed.runtimes_dirisE2E_SHARED_RUNTIMES_DIRused verbatim:validated_shared_dir(tests/e2e-cucumber/tests/e2e.rs:106) checks absolute-and-no-..and never resolves.
So if the tree is ever reached through a symlinked component of $RUNNER_WORKSPACE — or, on Windows, one differing by 8.3 shortening or the verbatim prefix, both of which #317's own commit message calls out — starts_with fails for every healthy entry. activatable_runtime_keys comes back empty, runtime_key_to_activate returns None, and activate_shared_runtime takes the eprintln branch and returns. That is the bug this PR fixes, restored through a different door, and silently: it's best-effort by design, so the only trace is one stderr line and the serve fails with no active ROCm runtime is configured again.
To be clear about what I did and didn't establish: I have not shown this fires on the current runners — $RUNNER_WORKSPACE/e2e-prewarm/data/runtimes is very likely already canonical on all five lanes. The narrower claim is that the doc comment states an equivalence that isn't there, and the arm it's missing is the one the pre-warm added on purpose.
Worth noting runtime_steps.rs:37-52 — in a file this PR already edits — carries linked_runtimes_target, whose whole docstring is about resolving a harness path "so it can be compared against a path the CLI resolved", \\?\ strip included. Same comparison, third implementation, two of the three handling it.
2. active_runtime_install_root's rationale is now unreachable (capability.rs:210-217)
The retained comment justifies deriving <runtimes_dir>/wheel/<key> over the manifest field like this:
That field records the absolute path where the runtime was first installed — on Strix a per-scenario temp dir that no longer exists by report time
After this change such an entry can't reach that code: runtime_key_to_activate filters an out-of-tree install_root out first, so the fallback on line 231 can only ever yield an in-tree path (or None). The fallback still earns its keep for a pre-wheel/ in-tree layout — but the example the comment turns on is now impossible, and it's the load-bearing half of the explanation.
A behaviour change hides in the same place that the description doesn't mention: on a tree where every entry is out-of-tree — precisely the shape that comment describes — the report now emits no version where it previously emitted one. "Absent beats wrong" is the right principle and I'd keep it; it just deserves a line in the PR body, since the consolidated report is what loses a field.
3. The freshness fallback fires in a case its comment excludes (runtime_steps.rs:316, :325-328)
let line = active
.as_deref()
.and_then(|key| runtime_lines().find(|line| line.split_whitespace().nth(1) == Some(key)))
.or_else(|| runtime_lines().next());The comment says it "falls back to the first line when nothing is active". It also falls back when something is active but has no matching runtime <key> line — and then asserts on a runtime the run didn't use, which is the misattribution this change exists to remove. By the PR's own standard that a clear failure beats a silently mismatched pass, I'd panic on known-active-but-unmatched and keep the fallback strictly for active == None.
Minor
active_runtime_key (:290) takes &mut E2eWorld but only calls run_rocm, which takes &E2eWorld (e2e.rs:605). Dropping the mut lets assert_update_reports_freshness keep its original world.cli_output.as_deref().unwrap_or("") and drops the clone plus re-borrow at :307-308.
Things I liked
- Scenario 5 has always been titled "the active runtime's freshness"; until now the assertion read whichever line came first. Good to see the test catch up with its own name.
skips_a_runtime_whose_install_root_left_the_treedocumenting the regression from the first attempt at the fix.- Filtering on "root is inside the tree" rather than "root exists" is the right predicate, and the reason given — a foreign path that happens to exist is worse than none — is the correct one.
|
Verified on hardware (GPU lane,
The counts alone were misleading — before and after the first fix both read "4 unexpected failures" for entirely different reasons. The decisive evidence is this line, which was The suite now selects and activates a runtime, and picks the sound one: the registry still lists a runtime whose install root left the tree, and selection correctly steps over it. No The GPU lane is still red and this PR does not fix that. The three remaining failures ( Other lanes on this head: Strix Halo Windows, WSL2 and rad3 R9700 all pass. Strix Halo Ubuntu was cancelled at its 35-minute All 16 required checks are green. |
volen-silo
left a comment
There was a problem hiding this comment.
Reviewed at ac1016e, independently and before reading @tomastola's review — we converged on three of the same items (the starts_with canonicalization gap, the .or_else fallback at runtime_steps.rs:325, and the needless &mut on active_runtime_key). No point restating them. Their framing of the first one is sharper than mine: anchoring it to the parity claim in the shared_runtime.rs:86 doc comment, and to the fact that assess added the second root deliberately, makes the case better than "these two paths are resolved differently" does.
Approving on the read that the residual risk is latent rather than live — but the test gap below bears directly on it, so it's worth folding in before merge.
What I can add on the questions the diff raises
Coverage is complete. All three use_shared_runtimes() call sites enumerated: runtime_steps.rs:127, :151, :282. The first two are patched; the third (user_checks_for_updates) is reached only from runtime_setup.feature:47, preceded in the same scenario by Given a managed runtime is active at line 46, so it inherits activation. No Background: sections in any feature file, so there is no fourth path. Of the 14 shared-tree scenarios, every one that performs a real serve reaches a patched step — the GPU serve steps in serving_steps.rs never opt into the shared tree themselves and always sit behind an activation Given.
No concurrency hazard. e2e.rs:1116 pins max_concurrent_scenarios to 1 whenever a GPU is present, and the shared tree only exists on GPU hosts — so there is no concurrent writer to active.json. Scenarios that activate different keys all work in their own isolated registries and never call use_shared_runtimes().
The pre-warm can't be confused by this. Worth stating explicitly since it's the obvious worry when a scenario starts writing to a shared tree: nothing in the pre-warm reads active.json. decide() parses rocm update stdout, and assess() / repair_poisoned_runtimes() parse runtimes list keyed on install_root: — both iterate every registered runtime regardless of which is active. A scenario overwriting the marker cannot affect the #275 refresh logic.
The expectations claim in the checklist holds. No row covers this failure mode, so these reconcile as unexpected_fail — as stated. The XPASS hazard is also clear: the only rows touching real GPU serve paths are the lemonade ones, already flaky = true, and the affected scenarios still fail post-fix (now on #314), so no stale_xpass is created either way.
One finding I didn't see raised
The three new tests never exercise the filter they look like they protect
manifest() (shared_runtime.rs:146-148) writes a bare "{}" — no install_root. So names_the_active_runtime_when_several_are_installed, falls_back_to_the_sole_manifest_without_a_marker and refuses_to_guess_between_several_without_a_marker all take the "no recorded root, keep it" branch at lines 102-106. All three would pass identically if line 107 were deleted.
That shape is also unproducible by the real CLI: install_root: PathBuf is non-optional in InstalledRuntimeManifest (therock.rs:255), and load_runtime_manifests silently skips any registry file that fails to deserialize.
This matters for the canonicalization item specifically — whatever fix that takes, no existing test would catch a regression in it. Switching those three to the realistic installed() helper covers the intended path; one case whose in-tree root is reached via a symlinked alias would pin the fix itself.
A second reason the capability.rs fallback is load-bearing
Separate from the point about the Strix temp-dir example being unreachable: the manifest-install_root fallback is also the only correct path for tarball-format runtimes. MANAGED_RUNTIME_FORMATS is ["wheel", "tarball"] (runtime.rs:445), a tarball root is <tree>/tarball/<key>, and the derived <tree>/wheel/<key> never matches it — the pre-warm's own fixture models exactly this shape. So the comment describing it as being for trees "that predate the wheel layout" undersells it twice over, and reads like an invitation to delete it as dead code.
Tradeoff worth recording rather than changing
activate_shared_runtime (e2e.rs:404) swallows a nonzero exit. The obvious objection is that a Given silently failing its precondition is the anti-pattern this PR diagnoses. But it holds up better than I expected: a failed activate bails before writing the config, so the scenario proceeds with no active key and validate_engine_selection_runtime falls through to single_ready_runtime_key — and because the harness's filter is weaker than "ready", the common divergence case (several registered, one genuinely ready) self-heals. Defensible as-is; asserting the precondition in setup_active_runtime would keep the benefit while removing the blind spot.
Liked
- Downstream safety of the new
Nonewas actually verified rather than assumed —PlatformVersionscarriesskip_serializing_if, both render sites guard on emptiness, andgrid_heading_omits_absent_versionspins it. No consumer unwraps. - Three commits, each self-contained, and the third exists because the first two were run on hardware and surfaced a case that hadn't been modelled — with the PR body saying so.
- Stating plainly that the lane will not go green from this PR. Easier to write a green-sounding summary; this is more useful.
|
Thanks both — all four findings verified against source before acting, and all four held. Pushed as 1. Your note that you hadn't shown it fires on the current runners is the right characterisation and I'm not claiming otherwise — 2. The three tests never exercised the filter (@volen-silo). This one was the most useful finding in either review, and I checked it empirically rather than by reading: stubbing the root check to I also took the suggestion of pinning the canonicalization fix itself: 3. Freshness fallback (@tomastola, @volen-silo). Agreed, and it's the PR's own standard turned on itself. Now asserts on the active runtime's line or fails naming the key; the fallback is strictly 4. Minor. On Also flagging something neither review could have seen: Strix Halo Ubuntu was cancelled at its 35-minute cap on the last run. That's pre-existing — the same lane hits the same cap on CI is running; I'll report what the GPU lane actually shows rather than assume it's unaffected. |
The shared pre-warm tree used to hold exactly one runtime, so serve scenarios could rely on the CLI auto-selecting it. That auto-selection is deliberately all-or-nothing: with more than one ready runtime installed the CLI refuses to guess. Once the pre-warm started adopting a newer runtime side by side with the old one, every GPU serve scenario began failing with "no active ROCm runtime is configured", and the precondition step still passed because a runtime *was* present. Name the runtime instead of relying on the count. The pre-warm's own activation is recorded in <runtimes>/active.json, which lives inside the shared tree and so is visible through the symlink even though every scenario keeps its own config dir. Read it back, fall back to the sole registry manifest, and refuse to guess otherwise -- activating an arbitrary one of several would serve against an unintended ROCm version and pass. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Follow-up to the previous commit, from review. The shared tree holding more than one runtime breaks three more places, all the same way: they pick whichever runtime came first and present it as the one in use. - The report attributed a run to an arbitrary manifest's ROCm version when `active.json` named none, so it could name a version the run did not serve on. Report nothing instead -- absent reads as unknown, wrong reads as fact -- and resolve the runtime the way the scenarios do. - The update-freshness scenario asserted on the first `runtime` line, which is the newest rather than the active one. Select by key. - A failed or skipped activation was silent, and neither precondition assertion can detect it: `installed: none` reports an empty registry, not an unset active key, and `engines list` scans every manifest regardless of which is active. Say what happened on stderr so the serve failure that follows is diagnosable. Also corrects two comments that claimed checks the code does not perform. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Activating a runtime by name replaced one silent failure with a loud one on the GPU lane: every activate died with "install root is missing" and pointed at a per-scenario temp dir that no longer existed. A scenario that installs through its own `data/runtimes` symlink records that temp dir as the runtime's install root, so the registry entry outlives the folder. The pre-warm evicts such entries, but its repair is deliberately non-fatal and skips the tree when `runtimes list` itself fails — which a poisoned entry makes it do. The tree therefore still holds corpses when a scenario reads it, and naming one guarantees a failed activate. Choose only among entries the CLI would accept, judged by the same rule the pre-warm's repair uses: an install root inside the shared tree is sound, one outside it is a corpse. Existence alone would be the wrong test — a foreign path that happens to exist would have the scenario serve against a runtime outside the tree. The diagnostic listing stays unfiltered, so a skipped entry is still named when nothing is left to activate. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Selection compared a recorded install root against the shared tree using
only the spelling it was handed, but the two sides are resolved-vs-as-given
by construction: the CLI canonicalizes an install root before writing it,
while E2E_SHARED_RUNTIMES_DIR arrives verbatim — validated_shared_dir
checks it is absolute and free of `..` and deliberately does not resolve.
Reach the tree through a symlinked component of the workspace and every
healthy entry then looks out-of-tree. Selection returns nothing, the
scenario declines to activate, and the serve fails with the same "no
active ROCm runtime is configured" this module exists to prevent — with a
single stderr line as the only trace, since declining is best-effort.
Compare against the resolved spelling too, matching the rule the pre-warm's
own repair uses and stating in the doc why one comparison cannot be enough.
Windows needs a third: canonicalize yields a `\\?\` path there while the
CLI records a plain one, so the prefix has to come off or the comparison
fails on the prefix rather than the folder. That trap is already documented
on linked_runtimes_target, which does the same thing for the same reason.
The three older tests wrote a bare `{}` manifest, taking the "no recorded
root" branch — all three passed with the root check deleted entirely, and
that shape cannot occur in CI because install_root is non-optional and
unparseable manifests are dropped. Use the realistic helper, so they
exercise the filter they appear to protect, and add the symlinked-parent
case that pins this fix.
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The freshness assertion picked the active runtime's line and otherwise fell back to the first one. Its comment said that fallback was for "when nothing is active", but it also caught the case where something IS active and no line matches it — and then asserted against a runtime the run did not use, passing while doing so. That is the misattribution this selection was added to remove. Fall back only when nothing is active; when something is, assert on its line or fail naming it. Also correct active_runtime_install_root's rationale. It turned on a per-scenario temp dir that no longer exists by report time, which can no longer reach that code — such an entry is filtered out before it gets there. The manifest fallback still earns its keep and the comment undersold why: a tarball runtime lives at <tree>/tarball/<key>, which the derived wheel path never matches, so for those it is the only correct answer. Calling it a legacy-layout leftover reads as an invitation to delete live code. Record too that a tree of entirely out-of-tree entries now reports no version at all, which is a deliberate absent-beats-wrong trade rather than an oversight. active_runtime_key took &mut E2eWorld while only calling run_rocm, which borrows shared; dropping it removes a clone and a re-borrow at the call site. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
`resolves_a_tree_reached_through_a_symlinked_parent` failed on Windows. The fixture wrote the install root exactly as `TempDir` handed it over, but Windows hands out an 8.3 short path (`RUNNER~1\...`) while the CLI records a canonicalized one. The two spellings never `starts_with`-match, so the entry was disqualified and selection came back empty. The bug was in the fixture, not the filter: it built a manifest the CLI cannot produce. Canonicalize the root before recording it, matching what `rocm install sdk` writes. The test now covers the symlink tolerance it was written for on every platform instead of passing on Unix by accident of the two spellings already agreeing there. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
9255675 to
c802140
Compare
|
Rebased onto The rebase is a no-op for review purposes. Six commits replayed with zero conflicts and no file overlap between the two sides — Why rebase at all: the Read the Docs check was red, and it was never about this branch's content. The docs site landed in One real bug, mine, fixed in GPU lane result on the pre-rebase head (
I enumerated all 8 remaining failures rather than assume: every one is downstream of the vLLM Separately — I chased the Strix Halo Ubuntu cancellation I flagged earlier and it is not a slow lane, so raising the cap would be the wrong fix. Each serve scenario re-downloads the 3.3 GB llama.cpp backend, because CI is re-running on the rebased head; the self-hosted run is currently held behind the superseded one on the concurrency group. I'll confirm when it reports. |
tomastola
left a comment
There was a problem hiding this comment.
Re-reviewed at c802140 after the latest changes. The canonical/as-given path comparison now matches the pre-warm rule, including symlinked parents and Windows path spellings; the fixtures exercise realistic canonical install roots; the freshness assertion fails rather than falling back to an unintended runtime; and the reporting comments/body accurately capture the absent-version tradeoff.
No remaining code findings from my review. The red Windows unit test is outside this diff and passed on the immediately preceding main run; it should be rerun. The MI300X GPU lane reaching the separately tracked device-inference blocker is consistent with the PR's stated boundary rather than a regression in this fix.
|
CI settled on the rebased head Two reds appeared after the rebase and I chased both rather than waving them through.
It surfaces only on Windows because Verified rather than assumed: the same test passed on Incidentally this is the second
Ready for re-review. I'm not merging — that's your call, and the two open items from my last comment (the |
tests/e2e-cucumber/expectations.tomlfor the fixed ticket ID and removed/narrowed any now-stale xfail rows. (No rows covered this failure mode — the scenarios were failing unexpectedly, not xfailing.)Summary
Every GPU E2E serve scenario has been failing with
no active ROCm runtime is configured, while the precondition step that is supposed to guarantee a runtimestill passed.
Root cause. Scenarios that only need a runtime present share one
pre-warmed managed-runtime tree, and used to rely on the CLI auto-selecting the
single runtime in it. That auto-selection is deliberately all-or-nothing: with
more than one ready runtime installed the CLI refuses to guess. Once the
pre-warm learned to install a newer runtime side by side with the old one
(#275), the tree grew past one and auto-selection started returning nothing, so
serve bailed before it ever launched an engine. The precondition only checks
that a runtime is installed, not that one is active, so it stayed green and
the failure surfaced one step later with no explanation.
Fix. Name the runtime instead of relying on the count. The pre-warm's own
activation is recorded in
<runtimes>/active.json, which lives inside theshared tree and so is visible through the symlink even though every scenario
keeps its own config dir. A new resolver reads it back, validates it against the
registry, falls back to the sole manifest, and refuses to guess otherwise —
activating an arbitrary one of several would serve against an unintended ROCm
version and pass, which is worse than failing loudly.
The second commit fixes three more places the multi-runtime tree broke the same
way: the consolidated report could attribute a run to a ROCm version it did not
serve on, the update-freshness scenario asserted on the newest runtime rather
than the active one, and a failed or skipped activation was silent even though
neither precondition assertion can detect it.
Naming a runtime is not the same as naming a usable one (third commit, found
on the GPU lane by the first two). A registry entry can outlive the folder it
points at: a scenario that installs through its own
data/runtimessymlinkrecords its per-scenario temp dir as the install root, and that folder dies with
the scenario. The pre-warm evicts such entries (#316), but its repair is
deliberately non-fatal and skips the tree entirely when
runtimes listitselffails — which a poisoned entry makes it do. So the suite has to expect to meet
one. Selection is now filtered to entries
activatewould accept, judged by thesame rule the pre-warm's repair uses: an install root inside the shared tree is
sound, one outside it is a corpse. Testing mere existence would be wrong — a
foreign path that happens to exist would have the scenario serve against a
runtime outside the shared tree.
Behaviour change worth naming: on a tree where every registry entry
records an install root outside the tree, the consolidated report now emits no
ROCm version where it previously emitted one. That is the intended trade — an
absent field reads as unknown, a wrong one reads as fact — but the report is
what loses a field, so it should not be discovered from the diff.
Risk: low. Test-harness and docs only; no product code changes.
Test plan
marker, corrupt marker, empty tree, refuse-to-guess, install roots that left
the tree, and a tree reached through a symlinked parent). These run in the
required
e2ejob. Each was checked to fail against the commit before itsfix — including confirming that the earlier tests passed with the root check
deleted, which is why they now build manifests the way the CLI really does.
rocmbinary on a two-runtime tree: serve failswith the exact error above,
rocm runtimes activate <key>(the call theharness now makes) clears it, and it still fails from a fresh per-scenario
config dir until the activation is re-applied — which is precisely the CI
shape.
no active ROCm runtime is configuredon the GPU lane. After the first two commits: zero — the servesnow reach vLLM launch and fail on a separate, already-tracked blocker
(
Failed to infer device type, fix(install): always keep the SDK's build of the torch an engine pins #314), which this PR does not claim to fix.That same run is what surfaced the dead-install-root case the third commit
handles.
@requires-gpuscenarios on the self-hostede2e-gpulane, which is advisory rather than required, so that lane is whatconfirms the fix on hardware. The GPU lane will not go green from this PR
— the remaining failures there belong to fix(install): always keep the SDK's build of the torch an engine pins #314.