fix(install): keep a torch that runs GPU kernels, realign one that does not - #314
Conversation
volen-silo
left a comment
There was a problem hiding this comment.
Reviewed at bc0477d. The diagnosis here is the best writing I've seen on this problem in the repo — separating "which torch release" from "which build of that release", instead of picking a winner between the two installers, is the insight that was missing from every previous attempt. "Read the manifest, not the environment" is the right principle and the reasoning for it is correct.
Requesting changes on one thing: the change applies that principle on the CLI side but not on the engine side, and the two mechanisms now work against each other. Two smaller defects below that.
1. This leaves two merged mechanisms fighting, and the loop does not converge
3426a858 (#264) changed vLLM's install short-circuit from "vLLM resolves" to "vLLM's own Requires-Dist are met". This PR deliberately and permanently leaves one of those requirements unmet. Nothing reconciles the two.
The chain:
crates/rocm-core/src/uv.rs:288-296—violations_requiringfilters byviolation.requiring, i.e. the requirer (vllm), not the required package. The intended torch divergence therefore lands squarely inside vLLM's own violation set.engines/vllm/src/lib.rs:541-566—repair_from_violationsreturnsneeded: truefor it.engines/vllm/src/lib.rs:422-461—needed: trueskips the short-circuit and runs a fullinstall_vllm_with_uv, which rewrites torch back to the engine's build. That rewrite isn't incidental; it's the mechanism #264 relies on, per the comment atlib.rs:417-421.apps/rocm/src/main.rs:7971-7998—settle_engine_installthen realigns torch back to the SDK's build.- The steady state is once again "vLLM's torch pin violated", so the next invocation repeats step 1.
The clearest evidence is in the vLLM crate's own suite. engines/vllm/src/lib.rs:2585-2604, the_whole_replaced_torch_stack_is_reported_one_finding_per_line, encodes this PR's exact steady state — requires torch==2.11.0+gitd0c8b1f, installed 2.11.0+rocm7.13.0 — and asserts assessment.needed. a_replaced_pinned_torch_forces_a_reinstall (lib.rs:2565) does the same. Both are merged and currently passing, and both pin the behaviour this PR needs not to happen. The diff doesn't touch engines/vllm/src/lib.rs.
Reachability, which is narrower than it first looks:
| Path | Churns? |
|---|---|
rocm engines install vllm (no --reinstall), main.rs:3684 |
every time |
rocm install sdk auto-installing vLLM, main.rs:7297 |
every time |
rocm engines shell first-time setup, main.rs:4223 |
first time only |
rocm serve |
no |
I specifically tried to make this worse and couldn't: serve doesn't call EngineMethod::Install per invocation — it goes resolve_engine_selection → validate_engine_selection_runtime → EngineMethod::ResolveModel, and resolve_engine_env has exactly one caller (engine_shell, main.rs:4079) gated behind interactive_terminal(). So there's no per-serve churn.
Cost is two full torch-stack flips per affected invocation instead of one. uv's wheel cache blunts that to extract-and-link churn after the first round trip, but on cache-cold hosts — CI, ephemeral containers, pruned disks — it's a repeated multi-gigabyte download. And by both this PR's framing and #264's, rocm install sdk re-runs are the common case rather than the edge case.
It's also user-visible in a way that undercuts the change: every affected run prints warning: the runtime environment did not satisfy vLLM's pinned dependencies; vLLM was reinstalled to restore them (engines/vllm/src/lib.rs:546-547). Under this PR that sentence is false — the divergence is intended, which is the entire reason ExpectedDivergence exists. The CLI learns to say "this divergence is expected" while the engine underneath keeps saying "this divergence is a defect I just repaired".
Suggested direction, staying off the rocm-engine-protocol contract surface: teach assess_runtime_repair / repair_from_violations the same distinction classify_dependency_details already makes on the CLI side — a torch-only violation whose installed version matches the runtime manifest's recorded sdk_torch build is not a defect and must not set needed = true; anything else still is. The engine already reads TheRockRuntimeManifest in collect_managed_runtime_candidates (lib.rs:1136), so it can read sdk_torch directly without a protocol change. That applies this PR's own "manifest, not environment" principle to the one place it didn't reach. The two tests above would need to move with it.
Verification boundary, stated plainly: I did not run uv. Steps 1, 2, 4 and 5 are confirmed by reading source and by the two tests named above. Step 3 — that uv pip install vllm==X without --reinstall actually rewrites an out-of-spec transitive torch — rests on #264's stated design intent and its code comments, which would make that commit a no-op if it weren't true. If that inference is wrong, this finding collapses, and you're better placed than I am to say so.
2. Lemonade is run through the torch and device probes with a non-Python executable
The description says "Environments rocm-cli does not own are left untouched." The guard implementing that doesn't cover Lemonade.
apps/rocm/src/main.rs:7977— the only guard isif response.managed_env == Some(false) { return Ok(()); }engines/lemonade/src/lib.rs:416,:456— Lemonade reportsmanaged_env: Some(true), so it isn't skippedengines/lemonade/src/lib.rs:453— Lemonade'spython_executableismanifest.lemonade, the Lemonade CLI binary. There's no Python interpreter and no torch anywhere in a Lemonade runtimemain.rs:7980takes that aspythonand hands it toprobe_torch_alignmentandprobe_runtime_devices, both of which doCommand::new(python).arg(<probe.py>)
Reached from rocm engines install lemonade, and — because ensure_self_managed_engine_ready is called from serve() at main.rs:4884 — from rocm serve on any fresh Lemonade install or version bump (main.rs:3905). The result is two pointless spawns of the Lemonade binary with a .py argument, and user-facing nonsense on the serve path: torch_alignment: not_applicable (failed to launch torch alignment probe via …) and device_check: not_verified (…), plus audit entries claiming a torch check was attempted for an engine that has no torch.
Not a false failure — NotApplicable and NotVerified sit outside the fatal conjunction — but it's wrong output on a hot command and it contradicts the invariant the PR states. Gating on !engine_manages_own_runtime(engine) (main.rs:3757) would cover it.
3. The doc comment carrying the change's central invariant is on the wrong function
apps/rocm/src/main.rs:7858-7871. Two doc blocks are merged with no separator, so the text describing settle_engine_install's contract —
Every path that installs an engine into a managed runtime must call this. Skipping it anywhere lets the engine's own torch win silently… External environments are left alone: rocm-cli does not own them.
— is followed immediately by /// Whether an install finished having produced a runtime that cannot serve. and lands on const fn install_left_runtime_unusable (main.rs:7871). settle_engine_install (main.rs:7971) has no doc comment at all.
Ordinarily a nit, but this is the most important invariant the change introduces, and finding 2 is precisely the failure it warns about — which suggests the misplacement isn't harmless.
Worth answering, not necessarily fixing here
No escape hatch. If the SDK ever publishes a build of the engine's pinned release that's ABI-incompatible with the engine, alignment reports Realigned, the device check passes (torch opens devices fine), and the install exits 0 with an engine that may not work — every check is about torch, not about the engine. I grepped for an opt-out and there's no flag, env var, or config key. The description says anyone wanting a different torch can install it directly, but alignment runs on every engine-install path, so a hand-installed torch is overwritten on the next engines install, install sdk, or engines shell. Given the PR's own known limitation about the mixed stack not being validated against the supported matrix, an env-var opt-out seems cheap insurance.
A third torch policy now exists. vLLM pins to its own index; this PR takes release-from-engine and build-from-manifest, explicitly because reading the environment is a trap; ComfyUI's probe_torch_stack_versions reads the live environment and constrains to whatever it finds. Install vLLM then ComfyUI and ComfyUI pins to the engine's build — the one this PR documents as enumerating zero devices. Not this PR's job, but its own reasoning implies the bug, and comfyui.rs here only gains sdk_torch: None in three fixtures.
rocm update --apply bypasses all of this — it calls therock::install_sdk directly (main.rs:14978-15069) and never reaches finish_sdk_install or settle_engine_install. Pre-existing and not introduced here, but it's a hole in "every path that installs an engine" and looks like a follow-up.
Smaller items
sdk_torch_build_for_key(main.rs:7842) has no test coverage — grep returns the definition and its single call site at:7981, nothing else. The description says "Both paths are covered by tests" of the manifest-fallback story; the hardware row is real but that half isn't. This is the function the "repair already-broken machines" case rests on, and its fallback is a string reconstruction. (I did chase the fallback's correctness and it holds:package_rocm_suffix,therock.rs:1472-1478, rejects any torch version without a literal+rocmsegment, so a recordedsdk_torchalways carries a usable local segment. Sound, just untested.)main.rs:26351andmain.rs:26621are byte-identical tests — same arguments toplan_torch_alignment, same assertion, only the doc comments differ. The second's stated point (build comes from the manifest, not the environment) issdk_torch_build_for_key's job, andplan_torch_alignmenttakessdk_buildas an opaque parameter, so nothing in it demonstrates that. It's counted as a distinct decision-surface row but isn't one — and the facet it claims to cover is the untested one above.report_torch_alignment's doc (main.rs:7856-7860) describes a different function — it's a near-verbatim copy ofdeliberately_diverged_package's doc (main.rs:7676), where it's correct.- The device probe discards its real error text.
main.rs:7742-7744: whentorch.cuda.device_count()raises — a genuine ROCm failure mode, HIP init errors — the probe returns the real exception inerror, and theNonearm replaces it with the generic"torch imported but did not report a device count". That's exactly the diagnostic the probe exists to capture;probe.error.unwrap_or_else(…)keeps it. NotVerifiedis never fatal despite its doc saying "never assume healthy" (main.rs:7706vs:7871-7882, which fires only onNoDevices). A runtime where torch can't import at all exits 0. The conjunction is defensible — you can't fail a multi-gigabyte install because a probe couldn't run — but the enum doc promises more than the predicate delivers.capture_python_stdout_with_env(therock.rs:3062+) usesStdio::piped()on every platform, while its siblingcapture_python_stdout(:3101+) branches onruntime_is_windows()and uses arunpyfile-redirect workaround elaborate enough to have been written against a real problem. The new helper knows about the split (it uses it to pick the temp dir,:3072-3076) and then ignores it for capture. If that workaround is load-bearing, both new probes degrade toNotApplicable/NotVerifiedon Windows and the fix silently no-ops there — and per the previous point, nothing fails. I have no Windows host, so this is a question rather than a confirmed break.install_pinned_package(therock.rs:2825-2850) uses--extra-index-url, leaving PyPI in the candidate set, and has no--no-depsor constraint file, so transitive deps can drift during what's meant to be a surgical "put this exact build back".3e59435asolved the same class of problem incomfyui.rswith a--constraintfile — worth matching or noting why not.RealignedandAlreadyAligned— the two states a healthy run actually produces — are unasserted anywhere.render_torch_alignmentis tested onUnavailable,InstallFailedandNotApplicableonly, and greppingtests/e2e-cucumber/fortorch_alignmentorexpected_divergencereturns nothing.torchvision/torchaudiodivergences are now invisible rather than merely unfixed: becauseviolations_requiringfilters by requirer, atorchvision → torchcomplaint carriesrequiring: "torchvision"and is dropped entirely, so a mixed stack yields neither a violation nor a divergence. Pre-existing filter behaviour, but the known limitation makes the mixed stack the expected steady state, which changes what that silence costs.
Verification
Ran on this branch: cargo fmt --check clean; cargo clippy --locked --workspace --all-targets -- -D warnings clean, zero warnings; cargo test -p rocm --all-targets → 500 passed, 0 failed, 1 ignored, matching the description exactly. All 18 new tests run individually and pass; the count is exact and none are vacuous. One workspace failure in rocm-dash-daemon::gen_tps_expiry_boundary_held_then_unavailable traced to a timing flake in a crate this PR doesn't touch — passes in isolation. The anyhow claim about is::<T>() surviving .context() was confirmed in a standalone repro; it holds.
The expectations.toml checklist claim holds — grepped the full file for the scenario id, torch, device_check and dependency_check; zero hits, nothing stale. The e2e change itself is correct: device_check: usable matches the renderer byte-for-byte including the two leading spaces, and the old step and scenario title are fully removed repo-wide.
Liked
The Unavailable vs InstallFailed split — refusing to claim "the index has no such build" unless the resolver actually said so, and degrading to the honest answer otherwise — is the kind of restraint that usually gets skipped. render_engine_dependency_check's ExpectedDivergence arm deliberately withholding the reinstall remedy (main.rs:8074-8080), because that remedy is what produces the broken state, is a genuinely thoughtful piece of UX. Using device_count() rather than is_available(), and probing with the runtime's recorded library paths rather than initialize_process(), are both non-obvious and both backed by measurement. And moving the e2e assertion from a string match on a diagnostic to "can the runtime open a GPU" is strictly stronger than what it replaces.
rominf
left a comment
There was a problem hiding this comment.
I read through this one carefully. The core idea is right and the reasoning in the doc comments is genuinely good — the alignment/device-check split, the unsettled && NoDevices conjunction, and the decision to keep a successful exit for an ordinary engine-install failure but not for a runtime left unable to open a device are all well argued, and the 18 new unit tests cover the interesting branches. My concerns are about how the new code is wired in, not about the design.
Two things I'd want fixed before merge:
- The new torch-realignment install runs
uvwithout the environment every otheruvcall in the tree applies. That loses both the long HTTP timeout and the managed cache directory, and the cache one has a real cost thatcrates/rocm-core/src/uv.rsexplicitly warns about. settle_engine_installis now on the lemonade path, where the "python executable" is actually a native binary. The repo already hasengine_manages_own_runtime()for exactly this distinction and it isn't used here.
The rest are smaller — a doc comment that looks like it got merged with its neighbour during an edit, an index flag that disagrees with how the SDK's own installs are done, and some output ordering. Details inline.
A few things I chased and decided were fine, for what it's worth: the install_left_runtime_unusable conjunction can't be tripped by NotApplicable, so a probe that simply can't answer won't fail an install; engine_auto_install_failure_is_fatal correctly survives .context() wrapping because anyhow's is::<T>() walks the chain; and classify_dependency_details does keep saying "violated" when there's an unrelated violation alongside the deliberate torch divergence.
Unrelated to the code: the Read the Docs check is red on this head (build 4363268 — the link is on the checks tab). This PR touches no docs, so it's probably not yours, but a red X will stall the merge — worth a re-run or a look.
I reviewed by reading the diff and the surrounding code at the PR head; I didn't run the GPU e2e scenario, so the behavioural claims below are from reading the call paths, not from execution.
rominf
left a comment
There was a problem hiding this comment.
This is a solid, well-explained fix for the real bug (torch build vs. release getting overwritten on a shared runtime) — the writeup, the manifest field, and the alignment/device-check logic all hold up under a close read, and I traced it against the recently-merged install-root fix (#317) with no conflict there.
One thing worth fixing before merge: settle_engine_install now runs unconditionally for every engine on every install path, including lemonade, which manages its own runtime and was never part of the shared-Python-venv problem this PR fixes. Every sibling helper that branches on engine type (resolve_engine_install_runtime_id, env_root_for_runtime, env_root_for_engine_install, ensure_self_managed_engine_ready itself) checks engine_manages_own_runtime(engine) first and skips lemonade — settle_engine_install doesn't, and its only early-return guard is response.managed_env == Some(false), which lemonade's adapter never sets (it always reports managed_env: Some(true)).
Concretely: for a lemonade install, response.python_executable is the lemonade CLI binary, not a Python interpreter. settle_engine_install still passes it to probe_torch_alignment/probe_runtime_devices, which launch it as <binary> <probe-script>.py. That will not behave like a real Python probe and should surface as a probe failure. In practice it degrades gracefully to torch_alignment: not_applicable, dependency_check: not_verified, and device_check: not_verified rather than a hard error — I confirmed install_left_runtime_unusable can't fire off a NotApplicable alignment, so this doesn't break the exit code — but every rocm engines install lemonade (both the direct CLI command and the auto-triggered install before serving) now prints three lines of noise that don't apply to it and don't mean anything for that engine. Since lemonade never registers a runtime manifest under runtime_manifest_for_selector, sdk_torch_build_for_key also always misses for it, which is a second symptom of the same root cause.
I don't see a test (unit or e2e) that currently exercises this path for lemonade, so nothing in CI should catch it. The fix looks like adding if engine_manages_own_runtime(engine) { return Ok(()); } at the top of settle_engine_install, mirroring the guard already used everywhere else this distinction matters.
Everything else — the manifest field, the alignment/device probes, the divergence-vs-violation classification, the fatal-vs-warning split for install sdk, and the updated e2e scenario — looks correct and well-tested for the vLLM path this was written for.
|
Re-reviewed at 700f185. Prior blocking finding (lemonade probed as if it were a Python interpreter): fixed.
fn settles_runtime_torch(engine: &str, managed_env: Option<bool>) -> bool {
managed_env != Some(false) && !engine_manages_own_runtime(engine)
}This adds the missing I also checked the other individual review comments left on 700f185 (the New pass over the rest of the diff (bug-scan): nothing new at >=70 confidence. The alignment/plan/device-check state machine ( Looks safe to approve after final CI/hardware validation. |
|
Thanks both — this was a genuinely useful pair of reviews. Everything raised is addressed in The two mechanisms fighting (@volen-silo finding 1)Fixed, and the chain was real. I verified each step rather than taking it on trust:
On your step 3, the one you flagged as resting on inference: it is stated as design intent in-tree, at One correction to the framing, because it changes the severity rather than the conclusion: each individual invocation does end in the right state, since settling runs last. What oscillates is the steady state across invocations. So the cost is the churn, the cache-cold re-download, and a warning that says the opposite of what the CLI says — not a runtime left broken. The fix is the direction you suggested, with one addition. The two tests you named moved with it. I checked both regression tests are load-bearing by reverting the predicate to The parsing needed by both sides now lives once in Smaller itemsAll fixed: the device probe keeps its real exception; On On the Windows capture question — I could not settle it either, no Windows host. Left as-is and worth a separate look; On Deferred, trackedThe four "worth answering, not necessarily fixing" items are deliberately not in this change: the missing escape hatch, ComfyUI's third torch policy reading the live environment, HousekeepingThe branch had gone The Verification boundary, stated plainly
None of this is verified on GPU hardware. The hardware evidence in the description predates these changes, and two of them alter what a real install does: the |
|
Re-reviewed at 6ca96d0 (moved from 700f185). What changed since the last review: nothing authored. This push is a merge commit ( Re-verified: Merge conflict resolution: checked line-by-line — both feature sets from the conflicting sides landed correctly with nothing dropped or duplicated. CI: the self-hosted GPU E2E lane fails on this head, but the same 4 scenarios ( No new findings. Still looks safe to approve pending final CI/hardware validation. |
CI status on
|
Two installers write torch into the same managed runtime. `install sdk` writes TheRock's build; an engine install then writes the build from its own index, pinned to an exact version. Letting either side win outright is wrong, and both failures have been seen on real hardware: - With the engine's build, the runtime can hold a torch that loads against the installed SDK and then enumerates no devices. vLLM resolves no platform and serving dies with "Failed to infer device type", long after the install reported success. - With the SDK's build, the runtime can hold a torch *release* the engine does not accept, which breaks the engine a different way. The release and the build answer different questions, so take them from different places: the release from the engine, which was built against it, and the build from the SDK, which owns the libraries the runtime loads. The SDK's torch is read before the engine install, since that install is what overwrites it, and its build identifier is carried onto the release the engine pins. Where the SDK publishes no such build, the engine's own is kept rather than failing a multi-gigabyte install; where the install fails for any other reason the error is reported as it happened, since calling a network or permission failure a missing wheel sends the reader hunting for something that exists. This applies to every path that installs an engine into a managed runtime, not just the auto-install after `install sdk`. A standalone `rocm engines install <engine> --reinstall` is what someone reaches for when a runtime already looks wrong, so it above all must not be the thing that breaks it; it now re-settles torch and leaves the runtime usable. Environments rocm-cli does not own are left alone, and anyone wanting a different torch can still install it into the environment directly. Reporting is adjusted so the result is legible. A new device check asks the runtime whether torch can actually open a GPU, because a satisfied dependency check does not mean a usable environment -- the broken state reports `is_available() == True` with a device count of zero, which is why it stayed invisible until first serve. The check composes the runtime's recorded library paths, not `rocm_sdk.initialize_process()`: measured on a runtime that serves correctly, the former reports 8 devices and the latter 0, so a probe built on it would condemn healthy runtimes. The dependency check learns to tell a deliberate divergence from a real violation, since after alignment the engine's exact pin is unsatisfied by design and the reinstall remedy no longer applies. An install that leaves a runtime unable to open a device now fails instead of exiting 0, but only on that conjunction: an alignment that could not run may still leave a working environment, and a device count of zero is the right answer wherever no GPU is present. `install sdk` needs the distinction, because it deliberately downgrades engine auto-install failures to a warning so a failed engine install does not discard a good multi-gigabyte SDK -- which is still right, and still what happens for every other engine failure. It was catching this one too, so the single case where the install really did produce something broken kept reporting success, which is the reported symptom exactly. The error says the SDK itself installed fine and what to do next, so a transient index failure does not read as a ruined install. The nightly scenario covering a second SDK install moves with it. It asserted the wording of the dependency check, which a settled runtime no longer produces; it now asserts that the runtime can still open a GPU, which is the outcome it was always trying to protect, while still failing on any genuine unmet requirement. Verified end to end on MI300X: an unmodified `install sdk` realigns torch, reports the runtime usable with 8 devices, and serves a real completion; a subsequent `engines install --reinstall` re-settles it rather than breaking it. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
The alignment left one of vLLM's own pins permanently unsatisfied, and the engine reads exactly that signal to decide a runtime needs repairing. So the two mechanisms fought: `uv pip check` reported the intended divergence as vLLM's violation, the engine reinstalled vLLM and rewrote torch back to its own build, the CLI realigned it, and the next invocation started over. Two full torch-stack flips per `engines install` or `install sdk`, a repeated multi-gigabyte download on cache-cold hosts, and a warning claiming a repair that had just undone the intended state. The engine now applies the same rule the CLI does — release from the pin, build from the SDK — reading the build from the runtime manifest rather than the environment, for the reason the CLI already does: after an install the environment holds the engine's build, so trusting it would call the wrong torch correct. A torch of the wrong release, of a build belonging to neither side, or in a runtime whose manifest identifies no SDK build all still force the reinstall. Also from review: - Skip settling for engines that manage their own runtime. Lemonade reports `managed_env: Some(true)` but its `python_executable` is a native binary, so it was being spawned twice with a generated `.py` path as `argv[1]`, printing torch and device checks for a runtime that has neither — on the serve path. - Run the realignment install through the uv environment every other uv call uses, restoring `UV_HTTP_TIMEOUT` and the managed `UV_CACHE_DIR` (without which uv silently copies the whole stack per environment) and the e2e shared cache. It also captures stderr on Windows, where the caller classifies this install's outcome by matching the resolver's message. - Make the SDK index authoritative for that install and add `--no-deps`, so a build the index does not publish cannot resolve against PyPI behind the `Unavailable` check, and a surgical swap cannot move the rest of the stack. - Keep the device probe's real exception instead of replacing it with the generic no-count line; a raising `device_count()` is the diagnostic it exists to capture. - Settle after the install header so the check blocks print under the lines they describe, and the config bookkeeping still lands when settling fails. - Name the engine in the realigned line, which sanitized a literal before. - Split a merged doc block that left `settle_engine_install` undocumented, and correct `report_torch_alignment`'s, which described another function. Test coverage follows the fixes: the manifest lookup the repair path rests on was untested and now covers both the recorded value and the reconstruction older manifests need; a byte-identical duplicate test is replaced by one that exercises the facet it claimed; and the two alignment states a healthy run produces were unasserted. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
759ac43 to
0669185
Compare
|
The alignment rewrites a package the user may have installed deliberately, and it runs on every path that installs an engine, so a hand-installed torch is replaced again by the next `engines install`, `install sdk`, or `engines shell`. There was no way to say no. That matters more than it would for a cosmetic correction, because the stack we resolve to — the SDK's build of the release the engine pins — is not validated against the supported matrix. "The SDK's build does not work on this machine" is a case that can happen rather than a hypothetical one, and until now its only exit was to stop using the CLI. Setting ROCM_CLI_DISABLE_TORCH_ALIGNMENT skips the rewrite. The check runs before the probe, so opting out means nothing ran, rather than running and being described differently; someone reaches for this precisely when their machine does not probe cleanly, so it cannot depend on a clean probe first. The dependency and device checks are untouched: this suppresses the correction, not the diagnosis, and a runtime that cannot open a device is still reported as one. The variable follows the existing convention for CLI switches — presence, not value, as with ROCM_CLI_DISABLE_STARTUP_UPDATE_CHECK. The two tests are a pair on purpose. Set, the call returns the opt-out reason even though it points at a Python that does not exist, which is what shows the gate precedes the probe. Unset, the same call has to fail at the probe instead, so the opt-out cannot appear to work for an unrelated reason. Raised in review by Eugene Volen. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
Review read `RuntimeDeviceCheck::NotVerified` as a case where an unimportable
torch should fail the install, and the one-line doc ("never assume healthy")
did not explain why it does not.
The variant fires for benign reasons as well as real ones — a runtime whose
Python could not be located, a probe that could not launch — so making it
fatal would fail multi-gigabyte installs because a probe did not run. Only
`NoDevices` is acted on, in `install_left_runtime_unusable`, and the cost of
that choice is real and worth stating: a runtime whose torch is present but
unimportable is reported rather than failed.
Separating the two cases means splitting the variant, which is a change to
behaviour rather than to a comment. Documenting the reasoning first so the
next reader does not have to re-derive it.
No functional change.
Raised in review by Eugene Volen.
Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
|
| package | version | origin |
|---|---|---|
torch |
2.11.0+rocm7.13.0 |
SDK — realigned by this PR |
triton |
3.6.0+rocm7.13.0 |
SDK |
torchvision |
0.24.1+d801a34 |
engine's PyPI build |
torchaudio |
2.9.0+eaa9e4e |
engine's PyPI build |
So the stack is genuinely mixed, and it is four packages, not three. It works for vLLM — device_check: usable, 79 scenarios pass — which is exactly what makes it easy to miss. torchvision-0.24.1+d801a34 is character-for-character the string in EAI-8051's reproduction, so the provenance is not in doubt.
Two things fall out of that:
- ComfyUI's
TORCH_STACK_PACKAGESnames three packages.tritonappears nowhere in the Rust sources (grep -rn triton --include='*.rs' apps/rocm/src/is empty), yet it ships at+rocm7.13.0and EAI-8051/EAI-8363 both show it being swapped for a generic build. Three of the four managed packages are protected by name; the fourth is not. - Extending the realignment to torchvision/torchaudio is cheap, because the resolver already computes them and even carries a
compatibility_key— we simply drop them, persisting onlysdk_torch. Triton is not carried at all and needs a resolver change.
I have not widened this PR to cover it: it changes what the alignment is allowed to touch, and it deserves its own review rather than being appended to one that already has approval blocked. Filed as EAI-8427 and linked to EAI-8294.
Scenario 4 asserted the outcome that matters — the runtime can still open a GPU after a second `install sdk` — but nothing in the suite asserted that the alignment itself fired. Those are not the same check. A runtime whose torch was never touched can still open a device, so the device check passes whether `settle_engine_install` reached the engine or not. That leaves the gate in front of the settle step, the part most likely to be widened or narrowed by a later change, with no e2e coverage at all: silently skipping the alignment would look identical to doing it correctly. The new step reads the `torch_alignment:` block. It asserts the block is present, that it reached one of the two healthy outcomes, and — separately, negatively — that it reached none of the three unhealthy ones. The negative half is not redundant: the positive check would pass on a run that also printed a failed second block. `not_applicable` is the one that would otherwise go unnoticed, because it is what a manifest yielding no SDK build produces, which is exactly the repair path for every runtime installed before `sdk_torch` existed. Both healthy outcomes are accepted rather than pinning one. Whether the reinstall rewrites torch or finds it already correct depends on what the shared pre-warm tree held when the scenario started, and the rule held either way. The divergence assertion is conditional on purpose. A divergence is today's steady state — the engine pins an exact build, the SDK supplies a different one of the same release — but a future pair could agree, and then there is nothing to classify. Requiring it unconditionally would encode today's versions into the scenario. What must never happen is the CLI reporting a divergence and calling it a defect, and that is what is asserted. No expectations.toml entry: the scenario is expected to pass. Raised in review by Eugene Volen. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
volen-silo
left a comment
There was a problem hiding this comment.
Re-reviewed at 01fff76a.
The previous blockers are genuinely closed. I checked each against source rather than against the summary, and the substantive ones hold up: is_intended_torch_divergence (engines/vllm/src/lib.rs:585-604) implements the three-condition rule rather than hardcoding a winner, and both named tests are load-bearing — they fail if the predicate is pinned either way. The Lemonade guard (settles_runtime_torch, main.rs:8146) covers all three real call sites and is still load-bearing, since the adapter reports managed_env: Some(true) with a native binary. The misplaced docs, the sdk_torch_build_from_manifest tests, the duplicate test, the preserved device-probe error text, and the --index-url / --no-deps / uv_command_env flags are all closed. Both deferrals check out too: apply_runtime_update (main.rs:15166-15268) genuinely never installs an engine, and repair_poisoned_runtimes lives in xtask/, which this PR does not touch.
The problem is that the opt-out in 2492bfc2 introduces two new defects, and the first re-opens the exact failure this PR exists to close.
Blocking
1. Under the opt-out, the CLI recommends the command that produces the broken runtime
main.rs:7655 → :7832 → :8216-8232, rendered at :8250-8272
With the var set, align_runtime_torch returns TorchAlignment::NotApplicable. deliberately_diverged_package(&NotApplicable) then returns None (:7832), so classify_dependency_details(details, None) short-circuits to Violated (:8224) — and the Violated arm prints action: rocm engines install <engine> --reinstall. That is precisely the remedy the ExpectedDivergence arm exists to withhold, with a comment saying so.
The engine side does not read the var, so a single invocation contradicts itself:
warning: the runtime holds the SDK's build of the torch vLLM pins;
that divergence is intended and a reinstall would undo it
torch_alignment: not_applicable (torch alignment is disabled by ROCM_CLI_DISABLE_TORCH_ALIGNMENT)
dependency_check: violated
violation: The package `vllm` requires `torch==2.11.0+gitd0c8b1f`, but `2.11.0+rocm7.13.0` is installed
action: rocm engines install vllm --reinstall
Reproduction, on an already correctly-aligned runtime:
export ROCM_CLI_DISABLE_TORCH_ALIGNMENT=1
rocm engines install vllm # prints the above
rocm engines install vllm --reinstall # following the adviceThe second command restores the engine's build, skips realignment, and yields device_check: no_devices. Because NotApplicable is not part of the fatal conjunction (install_left_runtime_unusable, main.rs:8038, requires Unavailable|InstallFailed and NoDevices), it exits 0 — the silent-success behaviour the PR was written to eliminate, now reachable through the PR's own escape hatch. No test covers this path.
Either deliberately_diverged_package should still return Some("torch") when the opt-out produced the NotApplicable, or the opt-out should carry its own TorchAlignment variant instead of reusing one that means four other things.
2. The opt-out does not do what its doc promises
main.rs:7631-7646 (doc) vs engines/vllm/src/lib.rs:585-604 and :1195-1216
The doc justifies the var on the grounds that a hand-installed torch is otherwise "replaced again by the next engines install, install sdk, or engines shell". But is_intended_torch_divergence whitelists only the SDK's build. A user's own build is neither the engine's pin nor the SDK's, so repair_from_violations returns needed: true, and install_vllm_with_uv runs uv pip install vllm==<pin>, which pulls torch==2.11.0+gitd0c8b1f over the top of it.
So a user in the documented situation — "the SDK's build does not work on my machine" — sets the var, installs a working torch by hand, runs engines install for any routine reason, and loses it. Alignment is skipped by the opt-out, dependency_check now reports satisfied because torch matches the pin exactly, device_check: no_devices, exit 0. The hatch fails on its first intended use.
The engine needs to honour the same opt-out, or the doc should stop promising this.
3. New user-observable behaviour with no scenario and no stated reason — AGENTS.md §3
main.rs:7644-7646
ROCM_CLI_DISABLE_TORCH_ALIGNMENT changes command output and which torch ends up installed, so §3 applies. grep -r DISABLE_TORCH_ALIGNMENT tests/ returns nothing, and the PR description contains no mention of the var, the opt-out, or an escape hatch — the description was not updated after 2492bfc2. §3 wants either a scenario or an explicit note saying why one is not needed; neither is present.
Worth noting the interaction: the var forces torch_alignment: not_applicable, which is one of the three strings the new step in 01fff76a asserts must never appear.
For contrast, the new non-zero exit path has no scenario either, but the description's "What is not covered" section explains why end-to-end reproduction is impractical. That one reads as disclosed.
Still open from the previous round
Windows stdio capture
therock.rs:3083-3125
capture_python_stdout_with_env, new in 93085058 and the sole transport for both new probes, uses Stdio::piped() unconditionally. Its two siblings in the same file — capture_python_stdout (:3127-3191) and capture_command_output_with_temp_files (~:3048) — both branch on runtime_is_windows() and use a runpy.run_path plus file-redirect workaround. The new function is aware of the split; it calls runtime_is_windows() at :3072-3076 to pick the temp dir, then ignores it for capture.
If the workaround exists for the reason its shape suggests, then on Windows both probes degrade to NotApplicable and NotVerified. Neither is in the fatal conjunction, so rocm engines install vllm exits 0 having realigned nothing, and the fix is inert on a platform AGENTS.md §6 treats as first-class.
I could not confirm this — no Windows host, and git log -S "runpy" -- apps/rocm/src/therock.rs traces only to the squashed initial import, so the original motivation is not recoverable. Flagging it as plausible rather than proven, but it is the one item that could make the whole fix a no-op on a supported platform, and it has been open across five commits.
Worth fixing, not blocking
The sdk_torch fallback is untested against the version form it will actually meet. main.rs:7998-8030, engines/vllm/src/lib.rs:1330-1345, test at :3081-3095. The fallback is format!("rocm{version}") where version comes from rocm_sdk.__version__, but the value it reconstructs is derived from the index version. Those agree on the release channel. This repo's own fixtures carry rocm_sdk_version values like "7.13.0a20260423" (therock.rs:5428, :5492), and the description's evidence table shows a real SDK torch of 2.9.1+rocm7.14.0a…. The only test uses "7.13.0". If the forms disagree on a dated-alpha channel the result is Unavailable + NoDevices — the fatal conjunction — so both engines install and install sdk exit non-zero on a machine that was previously only broken at serve. One test with the dated-alpha form would settle it; preferring the index-derived provenance would remove the question.
The fallback's scope is wider than "pre-existing manifests". sdk_torch is written in exactly one place (therock.rs:988, wheel install) and set to None at :1117 (tarball install) and in adopt_runtime_from_probe. Newly created tarball and adopted runtimes take the fallback permanently too. For adopted runtimes it is a guess — an externally-built environment's torch has no necessary relation to the probed SDK version — though that degrades to Unavailable rather than a wrong install.
NotApplicable now carries four meanings. main.rs:7660-7663, :7770-7776. It covers the genuine no-ops, a probe launch failure, and now the opt-out. That conflation is the mechanism behind blocker 1, it excludes probe failures from the fatal conjunction, and it logs a failed probe at audit level info.
The fatal predicate is inverted for the more-broken case. main.rs:8038 fires on Unavailable|InstallFailed + NoDevices. A runtime whose torch cannot import at all yields NotVerified and exits 0, while the strictly less broken "imports but sees zero devices" fails the install. 22fb6d82 is honest about NotVerified being non-fatal, but describes the cost as "reported rather than failed" — not as the worse state exiting more successfully than the better one. Worth a sentence.
Presence-not-value only half matches the repo. ROCM_CLI_DISABLE_STARTUP_UPDATE_CHECK is presence-based, but ROCM_CLI_DISABLE_MANAGED_PYTHON_BOOTSTRAP (therock.rs:3338) is value-based (1/true/yes/on). This picks the looser one, so ROCM_CLI_DISABLE_TORCH_ALIGNMENT=0 disables alignment. Nothing in .github/, xtask/, tests/ or docs/ sets a ROCM_CLI_DISABLE_* var to 0, so it is not live — but it is a real inconsistency.
The conditional divergence assertion is vacuous today. tests/e2e-cucumber/tests/e2e/runtime_steps.rs:237-242. The only emitter of a divergence: line is the ExpectedDivergence arm, which unconditionally prints dependency_check: expected_divergence in the same block, so the if body cannot be false. Reasonable as forward-looking insurance; the comment claims more than it currently delivers.
A diagnostic hint is hidden in a case it describes. engines/vllm/src/lib.rs:660-663. The "if this recurs after rocm install sdk" hint is gated on sdk_torch_build.is_none(), but in the wrong-release defect the build is known and does match the SDK's. Cosmetic — needed: true still repairs correctly.
crates/rocm-core/src/uv.rs:301-303 says "Both the CLI and the vLLM engine need the same two fields", but main.rs never calls violation_subject / ViolationSubject; the CLI decides from a direct Python probe. Only the engine consumes it.
Two claims in the description are stale. The sample output at line 94 still reads (the SDK's build of the release the engine pins), while render_torch_alignment (main.rs:7778-7793) emits the release vllm pins. The "500 passing in the CLI crate" figure is now 527.
Checked and clean
The e2e negative assertion in 01fff76a holds: I constructed the two-block output (one realigned, one not_applicable) and walked runtime_steps.rs:203-243 — str::contains scans the whole haystack, so the not_applicable assertion fails on that text as intended, and there is no substring overlap between the healthy and unhealthy literals. settle_engine_install is also called at most once per invocation from all three sites, so two blocks are not reachable today regardless. The step fails loudly when the block is absent rather than skipping.
main.rs's +1347 hides nothing unrelated — roughly 740 production and 475 test lines; every production hunk outside the main new-code block (:2444, :3812, :4001, :4336, :7389, :8199, :8559) is on-topic, with the only incidental edits being borrow changes forced by the new call sites. Scenario tags and IDs are unique and consistent with siblings, orphan no step definition, and need no expectations.toml row. sdk_torch is Option<String> with #[serde(default)] and no deny_unknown_fields, so it is compatible in both directions and no schema constant needed bumping.
cargo fmt --all -- --check and cargo clippy --workspace --all-targets -- -D warnings are both clean.
One thing I could not pin down: my first cargo test --workspace --all-targets reported 526 passed; 1 failed in -p rocm --bin rocm, and 24 subsequent runs were green. I did not capture the name. ScopedTestEnv serialises only tests that take its mutex, while std::env::set_var is process-global, so any concurrent test that merely reads an env var can race — a pre-existing hazard this PR widens slightly by adding two more env-mutating tests. Recording it rather than dropping it, since I could not reproduce it to confirm.
Signed-off-by: Michael Roy <michael.roy@amd.com>
|
Integrated the kernel-execution safety and addressed the opt-out blockers in Key changes since
Local verification on the actual updated PR tree:
@volen-silo @rominf please re-review the current head. I have not resolved any of your threads. |
Signed-off-by: Michael Roy <michael.roy@amd.com>
|
Follow-up |
|
Fresh hardware evidence at
The MI300X-labelled lane remains red because its actual reported target is gfx943 and no compatible published device package is available. This is now an explicit, early unsupported-artifact failure—not the original silent success. Run: https://github.com/ROCm/rocm-cli/actions/runs/33427086701/job/99603389430 The separate CodeQL app check lists 17 pre-existing cleartext-key alerts in unrelated provider-key code because this PR changes the large |
The divergence check in this step could not fail. `divergence:` is emitted only by the `expected_divergence` render arm, which prints that verdict one line above it, so requiring the verdict whenever the lines appear asserted that a single arm prints both of its own halves. The property the step was reaching for is the neighbouring one: a torch this tool put here on purpose must never be reported as a defect. `violated` is the only rendering that would say so, and it is now rejected outright. Unconditional, where the old form was guarded on today's versions being different. That guard is no longer needed: should a future engine pin and SDK build agree, there is no divergence to classify — and still no violation to report, so the assertion holds unchanged rather than silently going quiet. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
Every runtime installed before `sdk_torch` was recorded derives its SDK torch build from the recorded SDK version, and nightly SDKs version as `7.14.0a20260812`. The date belongs to the build — TheRock's torch for that SDK is `+rocm7.14.0a20260812` — but the existing cases covered only a plain release, an absent version, and a blank one, so nothing held the whole string in place. That leaves the obvious tidy-up unguarded: normalising the version to its `7.14.0` release before interpolating looks like a cleanup and instead names a build belonging to a different SDK, so the alignment would install a torch built against libraries the runtime does not have. On nightly runtimes only, which is where it would be found last. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
`settle_engine_install` derived the SDK torch build and the device-probe library paths from a caller-supplied selector, but installed into `response.python_executable`. Two of its three callers pass a `runtime_id`, which is shared by every side-by-side install of one channel and family, so the selector can name two runtimes at once. `runtime_manifest_for_selector` correctly declines to guess, both lookups come back empty, and the settle runs blind. Where the selector does resolve it can still name the *active* runtime while the engine was installed into an older one, and then the wrong tree's torch is settled. Resolve the runtime from the interpreter instead: an install root contains one runtime by construction, so the environment names itself. The caller's selector remains the fallback for interpreters outside every install root (external and self-managed environments). Paths are compared verbatim and canonicalized, because the CLI writes `install_root` canonicalized while an engine adapter reports back whatever path it was handed; comparing one form only would make ownership fail silently on a symlinked runtimes directory. Roots can nest, so the longest containing root wins. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
Why the GPU lane was red — two stacked problems, both now clearedThe failure had two independent causes. One is a real defect in this PR's area and is fixed here; the other was state on the self-hosted runners and has been cleaned up. 1.
|
.hip_fatbin |
libtorch_hip.so |
offload targets | device check | |
|---|---|---|---|---|
2.11.0+rocm7.14.0 (multi-arch) |
NOBITS, 0 bytes |
136 MB | none | hipErrorInvalidImage |
2.11.0+rocm7.13.0 (family index) |
PROGBITS |
188 MB | gfx942 | usable, 1 device |
This is not a 7.14 regression and the wheel is not corrupt — on-disk size matches the published wheel exactly, and +rocm7.13.0 under whl-multi-arch is stripped the same way. It is simply that resolving a version published only in the multi-arch channel into a family-specific tree always yields a kernel-less torch. That is the bug class #308's WheelRuntimeComposition / required_aggregate_device_target machinery exists to prevent — so #308 is the structural fix and this PR is the targeted one.
Worth calling out: unusable_runtime_error fired on both variants — no_devices on one tree, kernel_failed on the other. The device check worked exactly as designed; it caught a real cross-wiring rather than a false positive. It should not be weakened to get the lane green.
What was done to the runners
The 7.14.0 multi-arch runtime and its registry entry were removed from the pre-warm cache on both runners, and the active-runtime pointer on the one that referenced it was repointed at 7.13.0. The 7.13.0 tree is left warm and intact, so Decision::Reuse still fires and the lane still exercises the ensure_default_engine path on a warm tree — the case that surfaced this bug in the first place. main cannot re-create the 7.14.0 runtime, so this is not a treadmill; it will recur only when #308's hardware matrix next runs.
Follow-ups, deliberately not in this PR
- The engine env id is built from
runtime_id, so it drops the version: the active runtime is not necessarily the one an engine ends up using. This PR makes the used runtime work; it does not make the right runtime get used. Wants its own change. - Pre-warm runs on different branches share one retained cache, so a branch that can compose a runtime
maincannot leaves that runtime behind for everyone else. Worth an isolation or version-fencing change independent of fix(install): resolve canonical ROCm streams (EAI-8268) #308.
juhovainio
left a comment
There was a problem hiding this comment.
Reviewed at da2ea4c. The design here is sound and the problem is real: a shared Python env that two installers both write torch into, with every health surface reporting ready afterwards, is exactly the kind of silent failure worth this much machinery. Separating kernel launch from device enumeration is the right signal, and the pure decision functions with their unit tests make the whole thing reviewable.
First, one thing I withdraw. I initially read the hipErrorInvalidImage on the GPU lane as evidence that "the SDK's build of the release the engine pins" is a bad premise. It is not, and your diagnosis checks out: the existing test in therock.rs confirms multi-arch is always built as {base}/{family}, so the flat whl-multi-arch index on that cached runtime cannot come from main. The device check caught a real cross-wiring rather than a false positive, and I agree it should not be weakened to get the lane green.
That said, the lane is red again at 9 seconds, and it is worth looking at before merge:
pre-warm: reusing the shared release runtime (runtime is up to date with the channel index)
pre-warm: ensuring the vllm engine is installed
Error: runtime selector `release-wheel-gfx94x-dcgpu-7-14-0` ... is not an exact usable runtime:
installed runtime not found: release-wheel-gfx94x-dcgpu-7-14-0
The runtime is gone but an engine record still names it. Partly incomplete cleanup, but it also shows the new prewarm step has no tolerance for that state, and this PR is what put a fatal step on every warm reuse. Details inline.
Six comments below. The one on the prewarm is the one I would want fixed before merge; the rest are smaller, and two of them are one-liners.
A note on where the comments landed. GitHub does not serve a patch for apps/rocm/src/main.rs on this PR (the diff is too large for it to render), so I could not anchor anything there. Four of the six comments are about main.rs code and are attached to the nearest related hunk in another file, with the real location named in the comment text. Related: the default git diff reports 16.8k/13.2k across the ten files, while git diff --histogram gives the real 3,771/145. Worth putting in the PR description, since it affects how anyone reads this.
On CodeQL: the 17 high-severity alerts are false positives. All seventeen point at resolve_provider_api_key / set_provider_api_key / clear_provider_api_key, and this PR touches none of that code (grepping the diff for provider_api_key returns nothing). CodeQL says as much in its own summary: adding 2.2k lines to a 29k-line file shifted the line numbers past its change-detection threshold. Those want dismissing rather than fixing.
Two smaller things I checked that did not turn into comments: install_pinned_package using --index-url rather than --extra-index-url is the safer choice and is explained, and the Windows PATH versus LD_LIBRARY_PATH split in the probe is correct. One genuine nit not worth its own thread: detect_host_gpu() runs ExamineSummary::gather() at the top of settle_engine_install, but the result is only read in the last four lines, so it could move into the branch that needs it.
Also worth saying: ensure_default_engine is a good fix, and running it before the Reuse early return is what surfaced all of this in the first place.
| .context("`rocm engines list` did not identify a default engine")?; | ||
| println!("pre-warm: ensuring the {engine} engine is installed"); | ||
| rocm_command(rocm, prewarm_dir) | ||
| .args(["engines", "install", engine, "--yes"]) |
There was a problem hiding this comment.
This is where the lane is failing now, and I think it has a claim on this PR rather than only on the follow-up list.
The cleanup removed the 7.14.0 runtime, but an engine record still names it, so resolve_runtime_selector_to_exact_key bails and takes the whole job down 9 seconds in. repair_poisoned_runtimes already heals one flavour of stale state (a runtime recording an install root outside the tree), but nothing heals the inverse: an engine pointing at a runtime that no longer exists.
This matters more after this PR than before it, because this PR is what makes the engine check run on every warm reuse and be fatal on failure. The doc comment says fatal is deliberate, and for "the engine is missing" it clearly is. For "the engine names a runtime that was deleted out from under it" the prewarm can just fix it, and the shared retained cache means it will keep happening: any branch that can compose a runtime main cannot leaves it behind for everyone, which is your own second follow-up bullet.
Either extending repair_poisoned_runtimes to drop engine records whose runtime is gone, or catching this specific error here and reinstalling, would stop the next occurrence needing a human on the runners.
There was a problem hiding this comment.
Done in 83586fb — the pre-warm now repairs this instead of dying on it.
One correction on the mechanism, which does not change your conclusion: the dead name is not held by an engine record. resolve_engine_install_runtime_id takes its selector from config.active_runtime_key, falling back to config.default_runtime_id, and hands that to resolve_runtime_selector_to_exact_key. So the state that killed the lane was a config pointer at a runtime that had been removed, and an engine record never enters it. rocm runtimes list surfaces it as active_status: missing manifest for <field>=<selector>, which is what the repair keys off.
dangling_active_runtime reads that line, activation_candidates lists the managed runtimes the tree could be pointed at instead, and repair_dangling_active_runtime repoints. Both pointers are rewritten — the active.json marker and config.json's active_runtime_key — because they are read by different callers, and repairing only one leaves the engine install still following the dead key. That asymmetry cost me an hour on the runners.
Two deliberate non-repairs. Read-only entries are excluded as candidates: runtimes adopt records a folder the pre-warm does not own, and silently making somebody's external ROCm install the default for every GPU scenario is a bigger decision than fixing a pointer. And a tree with no runtimes at all still fails as before — there is nothing to repoint it at, and inventing one would hide an install that produced nothing.
You are right that this has a claim on this PR rather than the follow-up list, and for the reason you give: this PR is what makes the engine check run on every warm reuse and be fatal. Six tests cover it.
|
|
||
| # `ROCM_CLI_DISABLE_TORCH_ALIGNMENT` is the exit for the machine where the stack | ||
| # the alignment settles on — the SDK's build of the release the engine pins — | ||
| # does not work. That stack is not validated against the supported matrix, and |
There was a problem hiding this comment.
This comment is about classify_retained_torch in main.rs, anchored here because GitHub will not serve a patch for that file. This scenario's prose is the closest thing to an acknowledgement of it.
Retention requires the installed torch to equal the engine pin or the SDK build. A torch that is Usable, meaning it has been proven to run a GPU kernel, but is a third build (a user's own working install) falls through to Realign. If the replacement then fails the kernel probe, the install exits 1 and the working torch is gone, with no rollback.
The comment right here says the target stack "is not validated against the supported matrix", which is exactly the condition under which replacing a kernel-proven torch is the wrong trade. ROCM_CLI_DISABLE_TORCH_ALIGNMENT only protects someone who already knew to set it before the first run.
Two options, either seems fine: retain any Usable torch and report the divergence rather than correcting it, or keep the current behaviour but restore the previous torch when the post-realign probe fails. The second keeps the intent and removes the one-way door.
There was a problem hiding this comment.
Real, and I am not changing the behaviour in this PR. Reasoning, then what I am doing instead.
You have the mechanism right: retention requires equality with the engine pin or the SDK build, so a third build that is Usable falls through to Realign, and if the replacement fails the kernel probe the install exits 1 with the working torch gone. One-way door, accurately named.
Of your two options I would take the first — retain any Usable torch and report the divergence — not the rollback. A kernel-proven torch is the strongest evidence the tool has about that runtime, stronger than either name it matches against, and classify_retained_torch already treats "it runs a kernel" as decisive for the two builds it recognises. Extending that to any build makes the rule simpler: proven beats named, full stop. The rollback keeps the current intent but adds a second failure mode to reason about, since restoring a torch is another multi-gigabyte install that can itself fail, and it leaves the tool still preferring a name over a proof.
But that is a behaviour change to the retention rule, on a PR whose whole subject is the retention rule, landing after two rounds of review on the current one. It also widens TorchRetention into a state neither reviewer has seen — a divergence that is neither the engine's build nor the SDK's, reported and kept — and the reporting side of that is what makes it safe rather than silent. That deserves its own review, not a late commit here.
What this PR now does carry, and what changes the stakes:
- The pre-realignment verdict is printed under
torch_alignment: realigned(#3903676438, 19f2da0). So when this fires, the output says outright that ausableruntime was replaced by akernel_failedone, rather than showing only the after state. The one-way door is at least no longer an unlit one. ROCM_CLI_DISABLE_TORCH_ALIGNMENTis documented (#3903676458, 05fb20b), including the "locally built wheel / stack pinned for a reproduction" case, which is exactly the user you describe. Your objection that it "only protects someone who already knew to set it" still stands for the first run — documentation narrows that, it does not close it.
Filing the retention change as a follow-up with your first option as the proposal, and linking it here.
There was a problem hiding this comment.
Filed as EAI-8479, with your first option as the proposal: retain any torch whose kernel probe returns Usable — whatever installed it — and report the divergence, instead of installing a third build over a working one with no rollback.
The ticket records the argument you made here (the probe is stronger evidence than the provenance test the retention rule currently applies, so overriding it discards the better evidence), and notes the four things whoever picks it up has to decide: that broadening retention also makes an accidentally-odd torch a permanent fixed point, that a kernel-proven third build still is not a validated combination and has to say so, that NotVerified must stay firmly distinct from Usable if retention rests entirely on the probe, and that only torch is aligned today so this widens the set of reachable mixed stacks.
Leaving it out of this PR because it is a behaviour change rather than a fix for the bug, and it deserves to be decided on its own merits rather than widened in under a bug fix.
| /// `<version>` is installed``. Returns `None` for anything that does not carry both | ||
| /// halves of that frame, so a differently-shaped diagnostic yields no subject rather | ||
| /// than a confidently wrong one. | ||
| pub fn violation_subject(detail: &str) -> Option<ViolationSubject> { |
There was a problem hiding this comment.
This function is the right abstraction, and engines/vllm uses it properly. classify_dependency_details in main.rs does not, which is the one place in the PR that hand-rolls what this parses:
let marker = format!("`{package}==");
if details.iter().all(|detail| detail.contains(&marker)) { ExpectedDivergence(details) }
else { Violated(details) }Two consequences:
containsdoes not check that torch is the subject of the violation rather than the requirer. That distinction is precisely whatviolation_subjectexists to make..all(...)is all or nothing. One unrelated violation, say numpy, demotes the whole set back toViolated, which rendersaction: rocm engines install <engine> --reinstallover the torch that was deliberately installed. That is the exact instructionassert_no_reinstall_remedyasserts must never appear, and it would only be caught there in the opt-out scenario.
The vLLM side already partitions intended divergences from real defects rather than deciding for the whole set at once. Doing the same here, through this function, would make both sides agree by construction instead of by review.
There was a problem hiding this comment.
Done in 7b6f99f, including the numpy case, which was the real defect — the hand-rolled matcher was the mechanism.
classify_dependency_details now partitions per line rather than judging the set:
rocm_core::violation_subject(detail)
.is_some_and(|subject| subject.package.eq_ignore_ascii_case(package))so an unrelated numpy violation stays a violation and the deliberate torch divergence is still reported as one, instead of either demoting the other. EngineDependencyCheck::Violated now carries both lists (violations and expected), and the renderer prints the expected ones as divergence: lines.
That also fixed a worse consequence I had not seen: when the set was demoted, the report advised rocm engines install <engine> --reinstall, which would have undone the divergence the alignment had just put there on purpose. Where any expected divergence is present, the advice is now to repair the real violations without reinstalling.
Two tests cover the boundary — an unparsable line stays a violation rather than being silently swallowed, and a divergence on any other package is still a violation.
volen-silo
left a comment
There was a problem hiding this comment.
Re-reviewed at da2ea4cd. Gates in a clean worktree: cargo fmt --all -- --check clean, cargo clippy --workspace --all-targets -- -D warnings clean, cargo test --workspace --all-targets 2384 passed / 0 failed / 8 ignored. The one-off test failure I could not reproduce last round did not recur across this round either.
Real progress since 01fff76a. The Disabled variant is the right shape and its doc comment does the valuable thing of explaining why it is not NotApplicable; install_left_runtime_unusable becoming device-only closes the exit-0 half of my first blocker outright; e57e5e4b turns a tautological assertion into a falsifiable one and writes down why, which is exactly the right instinct; and classify_retained_torch comparing the running interpreter's reported version rather than distribution metadata avoids a real trap deliberately.
All three of my blockers are nonetheless partially closed — each has a residual that is reachable, not theoretical — and the five new commits introduce four more. Detail below, most severe first.
New blocking issues
N1. 74a157a6 makes the prewarm fatal on all three decisions, against two precedents in this repo
xtask/src/e2e_prewarm.rs:376
ensure_default_engine(&rocm, prewarm_dir)? sits after the match &decision block, so it runs for Install and Update as well as Reuse. Every failure inside it (:435-450) takes the whole job down. The commit narrative and the doc comment at :431-434 frame this entirely around Reuse; the new fatal dependency on the other two paths is not discussed anywhere.
Two precedents in this repo say that is too broad:
decide()'s own stated bias (:68-72): "the bias is deliberately conservative… A GPU lane must not be turned red… because the package index was briefly unreachable." It returnsReusespecifically whenrocm updatereportsstatus=error(:97-105).ensure_default_enginethen runs with no equivalent tolerance, so the exact transient that justifiedReusekills the job one call later.finish_sdk_install(apps/rocm/src/main.rs:8432-8469) does the functionally identical thing in the product and treats engine-install failure as non-fatal, withinstall_sdk_survives_an_engine_install_failure(:27554) commenting: "Failing here would throw away a multi-gigabyte install over an unreachable engine index."
Because ensure_default_engine passes no --runtime-id (:447-449), the CLI falls back to runtime_id.or(active_runtime_key).or(default_runtime_id) (main.rs:3857-3859). Six pre-existing states now become hard failures, each traced through resolve_engine_install_runtime_id → select_runtime_manifest (:9124-9158):
- A stale global pointer naming a deleted runtime —
bail!("installed runtime not found: …")at:9146, which is the currently observed 9-second failure verbatim. - No runtime configured even with exactly one ready runtime on disk —
bail!at:3861-3863, thoughsingle_ready_runtime_key(:18548) exists and is used elsewhere. - An ambiguous selector matching multiple retained installs —
bail!at:9153. Retention keeps up to--keep Nper channel/format/family (storage.rs:36,:286-328), so this is reachable by design. - Cross-channel pointer clobbering.
active_runtime_key/previous_runtime_key/default_runtime_idare a single global triple (crates/rocm-core/src/lib.rs:5209-5217) while the module doc says the shared tree holds multiple channels (e2e_prewarm.rs:39). A second channel's lane overwrites the pointer and demotes the first channel's in-use runtime out of two of its three retention protections (storage.rs:160-268). This is the most plausible mechanism for the observed "selector names an already-superseded version". - A transient index or network failure during the engine install —
bail!at:441/:449, with no analogue ofprobe()'sErr→Reusefallback (:325-340). - Disk-full. Not new in kind, but newly reachable on
Reuse, which previously touched nothing beyondmkdirs.
Suggested split: "the selector genuinely cannot identify a usable runtime" stays fatal, since that is real misconfiguration; "the install itself failed" warns, as finish_sdk_install already does. The smallest concrete fix is at the call site — pass an explicit --runtime-id derived from the runtime this same run() just confirmed, rather than letting the CLI fall back to global config that may be stale.
N2. da2ea4cd hand-rolls path containment while the correct helper is already imported into the same file
apps/rocm/src/main.rs:3969-3986
fn both_forms(path: &Path) -> Vec<PathBuf> {
let verbatim = path.to_path_buf();
match path.canonicalize() {
Ok(resolved) if resolved != verbatim => vec![verbatim, resolved],
_ => vec![verbatim],
}
}
… .any(|root| pythons.iter().any(|python| python.starts_with(root)))
.max_by_key(|manifest| manifest.install_root.as_os_str().len())runtime_path_is_same_or_inside (crates/rocm-core/src/runtime.rs:434-443, via runtime_paths_equivalent at :418-431) already does separator-normalized, case-insensitive-on-Windows, existence-independent containment — and is already imported at main.rs:44. disk_space.rs:144-157 is a second precedent for this same "longest containing prefix wins" pattern and documents why: "Windows filesystems are case-insensitive, so a volume mounted at C:\Data must still match a path spelled C:\data\...", alongside strip_verbatim_prefix (:122-141) for the \\?\ prefix canonicalize adds on Windows.
Three silent fallbacks all land on the ambiguous selector (:8519), reopening precisely the bug this commit fixes: a Windows case difference between the CLI's stored install_root and the adapter's echoed python_executable (Path::starts_with is case-sensitive, so it only matches if canonicalize rescues both sides); canonicalize() failing; and runtime_key_for_python (:3945-3948) swallowing a registry read failure via load_runtime_manifests(paths).ok()?.
This now feeds a fatal path. A wrong runtime_key gives a wrong sdk_torch_build_for_key, so classify_retained_torch compares the installed torch against another runtime's SDK build, returns Realign, installs the wrong tree's torch build, fails the post-realign probe with KernelFailed, and exits 1 via unusable_runtime_error (:8571).
Concretely: side-by-side release-wheel-gfx94x-dcgpu-7-13-0 and -7-14-0 sharing runtime_id therock-release:gfx94X-dcgpu, engine env in 7-13-0, registry read transiently fails → owned = None → runtime_key = selector → where the selector resolves to the active 7-14-0, that runtime's SDK torch build is installed into 7-13-0's python and the install exits 1.
One thing I checked and can rule out: the classic /a/runtimes/foo versus /a/runtimes/foobar prefix bug is not present. Path::starts_with is component-wise, and trailing / and . components normalize away. That part is correct.
N3. da2ea4cd's three unit tests never exercise the mechanism the commit is built around
apps/rocm/src/main.rs:27835-27849
side_by_side_runtimes() uses /runtimes/release-wheel-gfx94x-dcgpu-7-13-0 and similar, and the outside-root test uses /opt/somewhere-else/bin/python3. None of these exist on the test machine, so path.canonicalize() fails for every path in every test and both_forms only ever takes the _ => vec![verbatim] branch. The "compared both verbatim and canonicalized" logic — the whole justification in the doc comment at :3960-3964, and the stated reason a single-form compare fails on a symlinked runtimes directory — has zero coverage.
Worth adding: a real-tempdir test where canonicalize succeeds, a symlinked-install-root test, a dangling-interpreter test, and a foo/foobar sibling test to pin behaviour that is currently correct only by accident of Path::starts_with's semantics.
Related: max_by_key (:3985) ranks by the verbatim configured install_root length regardless of which form-combination produced the match. Where two manifests both match — one root a symlink alias into another's tree — the winner is whichever string is longer, not the one that owns the resolved directory.
N4. The new non-zero exit fires on a bare except Exception, on a sensor that cannot see the failure modes it gates, with no override
apps/rocm/src/main.rs:8381
install_left_runtime_unusable is HostGpu::Detected && (NoDevices | KernelFailed). detect_host_gpu (:8338) returns Detected iff ExamineSummary::gather() yields detected_gfx_target.is_some(). Gather only errors when AppPaths::discover() fails (crates/rocm-core/src/lib.rs:1975, :1564), every detector under it swallows I/O errors with .ok()?, and it reads only /sys/class/kfd/kfd/topology/nodes (:4637) — world-readable, and entirely independent of whether the runtime's Python can open /dev/kfd. So NotVerified is effectively unreachable and Detected is true in cases where torch legitimately sees nothing:
| host state | detect_host_gpu |
device check | previously 0, now non-zero |
|---|---|---|---|
container with host /sys but no --device=/dev/kfd --device=/dev/dri |
Detected |
NoDevices |
yes |
user not in render / video group |
Detected |
NoDevices |
yes |
ambient HIP_VISIBLE_DEVICES / ROCR_VISIBLE_DEVICES inherited by the probe (therock.rs:3117) |
Detected |
NoDevices |
yes |
| AMD iGPU registered with kfd, discrete GPU not enumerated | Detected |
NoDevices |
yes |
8-GPU host, GPU 0 faulty — the probe only touches the default device, never loops over device_count (therock.rs:2727-2763) |
Detected |
KernelFailed |
yes |
| GPU fully claimed by another tenant, HIP OOM on the trivial alloc | Detected |
KernelFailed |
yes |
The probe does call torch.cuda.synchronize(), so the async-launch false negative is correctly avoided — that part is right. But it wraps the launch in except Exception, and classify_runtime_device_probe (:8017) turns any non-empty kernel_error unconditionally into KernelFailed, which is unconditionally fatal. An unrelated AttributeError, an OOM, or an ECC/driver-reset message all become a hard install failure.
There is no override: no --force, no --skip-device-check, and torch_alignment_disabled() is explicitly documented as not suppressing it (:7695-7700). unusable_runtime_error (:8404-8409) advises installing "a torch build that suits both this SDK and this GPU", which is right for the real defect and misleading for every row above.
detect_host_gpu — the sensor the whole predicate rests on — has no test against any of these conditions, and the two e2e scenarios assert only that no_devices / kernel_failed are absent.
N5. The opt-out does not escape the new fatal exit, and the two compose badly
Under the opt-out, retention is Realign by definition, align_runtime_torch returns Disabled and installs nothing, and devices = probed — the pre-existing verdict — via the _ arm at :8547-8552. On a GPU-detected host whose torch enumerates zero devices, the install exits 1 having deliberately declined to change anything. Someone who sets the variable precisely because the correction is wrong for their machine gets a hard failure and no way out. It is documented as intentional at :7695-7700, but it still means the PR's only escape hatch does not escape the PR's only new failure mode. Combined with N4's false positives, this is the pairing I would most want an override for.
My earlier blockers — what remains
B1 — partially closed
Fixed and verified: TorchAlignment::Disabled { wanted, kept } (:7619-7627) is produced only once a replacement is actually due (:7745-7747), deliberately_diverged_package maps it to Some("torch") (:7937-7945), and the ExpectedDivergence arm prints action: none; this torch is kept on purpose (:8648-8662). The exit-0 half is closed too, since install_left_runtime_unusable is now device-only. Both new tests are load-bearing — they construct Disabled directly, so reverting the variant breaks compilation.
Residual: classify_dependency_details still gates on .all():
// main.rs:8617-8618
let marker = format!("`{package}==");
if details.iter().all(|detail| detail.contains(&marker)) {One unrelated violation demotes the whole set to Violated, whose arm prints action: rocm engines install <engine> --reinstall (:8638-8646). Reachable state: opt-out set, a hand-built torch==2.9.0+cpu, plus a torchvision mismatch — the normal companion state for a hand-built torch, not a contrived one. Output then reads torch_alignment: disabled … keeping 2.9.0+cpu immediately followed by dependency_check: violated and the reinstall remedy. The contradiction is now conditional on a co-occurring violation rather than unconditional, which is an improvement, but it is the same contradiction.
B2 — partially closed
Fixed: is_intended_torch_divergence short-circuits to true for any torch pin violation when the opt-out is set (engines/vllm/src/lib.rs:618-620), and the doc no longer overclaims.
Residual: the classification is honoured, the mechanism is not. When any non-torch pin is also violated, repair_from_violations returns needed: true — its own test an_opted_out_custom_torch_still_repairs_an_unrelated_defect (:3208) uses a torchvision companion — which calls install_vllm_with_uv (:1247-1281):
uv pip install vllm==<pin> --extra-index-url <url>
No --no-deps, no constraint. vllm==<pin> declares an exact torch==<engine-pin> dependency the environment by construction does not satisfy, so uv resolves and overwrites the user's torch. Nothing asserts the invocation preserves torch, and scenario 9 covers only the single-divergence case.
B3 — partially closed
Closed: the scenario is real and correctly wired — tests/e2e-cucumber/features/runtime_setup.feature:73-80, @id:runtime-torch-alignment-opt-out @requires-gpu @requires-engine:vllm @nightly, unique id, tags matching sibling scenario 4, no orphan steps, no expectations.toml row needed, and the variable genuinely reaches the subprocess (tests/e2e.rs:719-740).
Open: the opt-out-specific assertions are conditional on channel state the scenario does not control, and on the shared warm tree they will not fire. torch_alignment_disabled() is read only inside align_runtime_torch, which runs only on TorchRetention::Realign (:8534). classify_retained_torch (:8218-8248) short-circuits to EngineBuild / SdkBuild whenever the installed torch already ran a kernel and matches either build — and scenario 4's own comment says that is what happens on this fixture. Scenario 9 is scenario 4 plus the variable, so Disabled is never produced, and the only assertion naming the variable is guarded by if verdict == "disabled". The step's doc comment is honest that disabled "cannot be demanded unconditionally", but that describes the gap rather than closing it. Net over scenario 4, this adds two negative string assertions. Deterministic coverage needs a Given that plants a third build first.
Also open: ROCM_CLI_DISABLE_TORCH_ALIGNMENT is in neither docs/ nor README.md, which between them document around 21 other ROCM_CLI_* variables; docs/vllm.md:18-24 is the natural home. The PR body discusses the opt-out but never cites the @id:, which §3 requires. And the new non-zero exit path has no scenario at all and no §3 justification — the body explains why the exit code is unit-tested, but §3 says a unit test does not discharge this and asks for either a named gated lane or a stated reason.
Windows stdio capture — unchanged, still unproven
capture_python_stdout_with_env (therock.rs:3117-3154) still uses unconditional Stdio::piped() at :3138-3140 while selecting a Windows-versus-Linux temp dir at :3123-3127. Both siblings still use the file-redirect workaround. None of the five new commits touched it; d616d14b is the only one editing therock.rs and only adds kernel_error. git log -S "runpy" still resolves to the squashed initial import, so the original motivation remains unrecoverable.
One mitigating finding: the consequences are bounded. probe_torch_alignment Err gives NotApplicable (:7724-7725), probe_runtime_devices Err gives NotVerified (:8000), and neither is in the fatal set. So the worst case is the fix silently no-opping on Windows rather than a broken runtime reported healthy. runtime_is_windows() is cfg!(windows), which is the correct condition.
Cheapest way to settle it without a Windows host: one round-trip test calling capture_python_stdout_with_env on a trivial print(json.dumps(...)), run by the existing windows-latest job.
On the concurrent review
I looked at all six of the inline comments in the other outstanding review and agree with all six, with two corrections worth recording.
On the prewarm: the suggested remedy of extending repair_poisoned_runtimes will not work. It runs first (:321) and is correctly ordered, but it only talks to rocm runtimes list and reasons about install_root: text — it has no engine visibility, and there is no subcommand to surgically clear a stale per-engine pointer. The only available lever is engines install --reinstall, which duplicates ensure_default_engine. Passing an explicit --runtime-id at the call site is the smaller change.
On classify_dependency_details: the all-or-nothing .all() consequence is real and is B1's residual above. The subject-versus-requirer consequence, though, cannot arise on this path — violations_requiring(&violations, engine) (uv.rs:288-296, called at main.rs:8590) has already filtered details to requiring == engine, and uv renders the requirer as `name` followed by ` requires, never `name==. Building the suggested counter-example confirms `torch== is not a substring of it. Separately, assert_no_reinstall_remedy would not catch the .all() case either, since it runs only in scenario 9, which has no engineered second violation.
On the third-party-torch realignment, of the two proposed options the second — restore the previous torch when the post-realign probe fails — is the right one. Retaining any Usable torch would reopen the earlier defect, because an SDK torch release the engine will not accept can still run a kernel perfectly well.
Smaller items
- Dated-alpha fallback — closed on the construction side.
main.rs:27801-27818uses a genuine"7.14.0a20260812"and asserts the fullSome("rocm7.14.0a20260812"), load-bearing against a date trim since production is a naiveformat!("rocm{version}")(:8185-8190). Whether that build string resolves on a real SDK index remains untested, which is inherent rather than a regression. - Fallback scope — unchanged. Tarball installs (
therock.rs:1117) andadopt_runtime_from_probe(main.rs:8955) still writesdk_torch: None, so newly created runtimes take the fallback permanently, not only pre-existing manifests. NotApplicableconflation — down from four meanings to two. The opt-out has its own variant now, but a genuine probe launch failure still returnsNotApplicable(:7724-7725) and is still logged at audit levelinfo(:7910-7915).- Inverted fatal predicate — unchanged but now disclosed. On a GPU host, a torch that cannot import at all exits 0 while the strictly less broken imports-but-sees-nothing exits 1. The
22fb6d82doc (:7979-7989) now names the cost explicitly, which satisfies what I asked for last round; the asymmetry itself remains. - Presence-not-value parsing — unchanged, not live. Both copies agree with each other. The repo majority is value-based:
truthy_env(therock.rs:1831) andenv_flag(rocm-core/src/lib.rs:1764) back five variables against two presence-based ones. Nothing in.github/,xtask/,tests/,docs/orscripts/sets such a variable to0. - Vacuous divergence assertion — closed.
e57e5e4breplaced it with an unconditional, falsifiable!output.contains("dependency_check: violated"), and the comment explains why the old form could never fail. No new vacuous guards ine57e5e4bor74a157a6's four tests. crates/rocm-core/src/uv.rs:301-303still says both the CLI and the engine needviolation_subject, butmain.rsstill never calls it.- PR body has no mention of
da2ea4cd. The HEAD commit changes which runtime gets settled and adds a whole new decision with tests at:27827-27920, but the "Unit tests" list — which presents itself as covering the decision surface rather than one happy path — omits it, and the "On hardware" table predates it. Both staleness items from my last review are resolved: the test-count figure is gone and the sample-output wording now matchesrender_torch_alignment(:7799). - CodeQL. Agreed these are false positives, with one refinement to the evidence: grepping the diff for
provider_api_keyunder git's default algorithm returns four lines, not none. They are an identical-/+pair, i.e. alignment noise. Under--histogramit returns zero, and the diff sizes as 3,771/145 rather than 16,811/13,185. That--histogramfigure is worth putting in the description, since it is also why GitHub will not render a patch formain.rs.
`ROCM_CLI_DISABLE_TORCH_ALIGNMENT` was read in two places — the CLI that owns the alignment and the vLLM engine that repairs what it finds — with the variable name and the presence-is-the-signal rule spelled out separately in each. Nothing held the two together, and the failure mode of drift is not a warning: a runtime the CLI deliberately left alone would be rewritten by the engine on the very next `rocm engines install vllm`, which is the fight the opt-out exists to end. The engine cannot call into the CLI binary, so the read moves to `rocm-core`, which both already depend on, and both sides now make the same call rather than matching implementations. The reasoning that has to stay true for either reader moves with it; the callers keep only what is local to them. No behaviour change: same variable, same presence test. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
The trailing hint — that a recurring defect after `rocm install sdk` means the SDK torch stack is being written over vLLM's pins — was suppressed whenever the opt-out was set, on the reasoning that torch is never a defect there so the hint would name the one package that cannot be the cause. That conflated the package `torch` with the SDK torch stack. `is_intended_torch_divergence` spares only the package literally named `torch`, so a `torchvision` or `torchaudio` defect still reaches this point under the opt-out — and those are the same SDK stack written over the same pins. The opt-out has switched off the step that would have corrected them, which makes the hint more use there, not less. The remaining gate is the one that was load-bearing all along: once the manifest names the SDK's build, the alignment identifies that stack and settles it, so a defect surviving to here is something else and the hint would misdirect. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
`classify_dependency_details` demoted the whole run to `Violated` unless *every*
line named the realigned package. One unrelated violation — say numpy — took the
deliberate torch divergence down with it, and the block then advised
`rocm engines install <engine> --reinstall`: a remedy that repairs numpy and
reinstates the engine's own build of torch, leaving a runtime that cannot open a
device. Trading one defect for a worse one, on the strength of a defect that had
nothing to do with the alignment.
Each line is now judged alone, and `Violated` carries both sets. The subject
comes from `rocm_core::violation_subject` rather than a local ```{package}==```
substring test — one parser for the shape `uv pip check` emits, so the CLI and
the engine cannot disagree about what a line is about. A line whose subject does
not parse stays a violation: an unrecognised shape is not evidence that a
divergence was intended.
Where both sets are populated the reinstall is no longer named at all. The block
prints the violations, prints the divergences under their own `divergence:` key,
and says to repair the former without reinstalling. With no divergence in play
the old advice is unchanged.
Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
The `device_check:` block prints the runtime as it is *after* the install settles its torch. On a realignment that is the only state reported, so an install that ends in a runtime which cannot open a device does not say whether the alignment broke something that worked or found something already broken. Those need opposite responses, and telling them apart afterwards means going to the machine — by which time the torch that produced the earlier verdict is gone. The probe that answered the question is already in hand at the call site, so `torch_alignment: realigned` now carries it, in the printed block and in the audit line both. The log matters as much as the console here: it is read long after the install, when the runtime on disk can no longer be asked what it was like beforehand. Only the realigned arm prints it. Every other outcome left the runtime alone, so the device check below already concerns the same torch this block names, and a before/after pair there would invite reading a change into an outcome that made none. The quoted form is `device_check_verdict`, not the full block: it keeps the verdict name and the torch it judged, and drops the explanation and the suggested consequence. Those are right where the verdict is the answer and wrong where it is context for something else — two full blocks in a row read as two competing diagnoses rather than as one pair. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
`settle_engine_install` ran `detect_host_gpu` unconditionally while gathering facts, but both readers of the result additionally required `runtime_cannot_serve`. On a healthy install — the overwhelmingly common case — the whole host scan was paid for an answer nothing went on to read. The probe moves behind an early return on that same predicate, so it now runs only where its answer decides something: whether a runtime that cannot serve is this install's failure, or a host with no GPU to serve with. Behaviour is unchanged. The early return sits after both reports are printed, so a healthy install still emits the same dependency-check and device-check output; only the two host-conditional branches are skipped, and neither could have fired in that state anyway. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
…ease `the_sdks_release_is_corrected_to_the_one_the_engine_pins` passed the same release on both sides, so the only difference the plan could act on was the build — which is what the test directly above it already covers. It asserted the right outcome for the wrong reason and would have kept passing had the release half of the rule been dropped. The arguments now disagree on the release and agree on the build, the inverse of the neighbouring case: installed `2.11.0+rocm7.14.0a20260611` against a pin of `2.10.0+git8514f05`. The expectation follows the rule — the engine's release, the SDK's build — so a regression that took the release from the installed torch instead of the pin now fails here. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
A pre-warm whose cached tree names a runtime that no longer exists on disk died nine seconds in, before any test ran: the engine install reads the active runtime key, resolves it to a directory, and finds nothing there. The cache is shared across runs, so one interrupted or hand-edited tree kept failing every subsequent job on that runner until someone repaired it by hand. The pre-warm now checks the pointer against the runtimes actually present and, where it dangles, repoints it at an installed runtime rather than failing. Both pointers are rewritten — the `active.json` marker and `config.json`'s `active_runtime_key` — because they are read by different callers and repairing only one leaves the engine install still following the dead key. A pointer that already resolves is left alone, and a tree with no runtimes at all is left to fail as before: there is nothing to repoint it at, and inventing one would hide an install that produced nothing. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
Engine install can now replace the torch in a managed runtime, which is a surprising thing for an install to do if the only place it is explained is the line it prints while doing it. The adapter doc gains the rule — a kernel-proven torch is kept, otherwise the runtime moves to the SDK's build of the release the engine pins — and says where each half of that version comes from and why. It also documents `ROCM_CLI_DISABLE_TORCH_ALIGNMENT` as the escape hatch for a locally built wheel or a stack pinned for a reproduction, including that any value works because presence is the signal, that the device check still runs under it, and that the result is not validated against the supported matrix. Signed-off-by: Tomas Saaristola <tsaarist@amd.com>
|
Signed-off-by: Michael Roy <michael.roy@amd.com>
feedback addressed
The symptom
A fresh
rocm install sdkon a gfx94X host produces a managed runtime that cannot serve. The install exits 0 and every health surface reportsready, but the first serve dies with:Earlier in the same log, more plainly:
Triton is installed but 0 active driver(s) found (expected 1).What is actually wrong
Two installers write torch into the same environment.
install sdkwrites TheRock's build; the engine install that follows writes the build from its own index, pinned to an exact version, replacing it:That torch imports cleanly against the installed SDK and then enumerates no devices, so vLLM resolves
UnspecifiedPlatformand refuses to start.Measured by swapping only torch — same environment, SDK, driver and vLLM:
device_count()2.11.0+gitd0c8b1f2.11.0+rocm7.13.0vLLM itself is fine. The served response carried
system_fingerprint: vllm-0.26.0-d391b761, soVLLM_PINNED_SPECdoes not need to change.Why neither obvious fix is correct
Both directions have been observed failing:
2.9.1+rocm7.14.0a…2.10.0+git8514f052.11.0+rocm7.13.02.11.0+gitd0c8b1fSo "the engine's pin always wins" and "the SDK's build always wins" each re-create the other bug. Pinning a different vLLM release is not an option either:
wheels.vllm.ai/rocm/<version>/publishes only therocm723ABI for 0.23.0 through 0.27.0, so working and broken environments alike run arocm723build. The variable is the torch each release pins, not the ABI tag.The rule
Because both directions fail in the field, compatibility is tested rather than assumed, in two stages.
First, a torch that already runs a GPU kernel with this SDK is kept exactly as it is. Not merely one that imports, and not merely one that enumerates a device — one that has launched a kernel. Two builds are allowed to stand this way: the engine's own exact pin, and the SDK's build of the release the engine pins. Both are states this tool produces deliberately, and each is a fixed point, so a rerun over either keeps it and installs nothing. That is what stops
--reinstallfrom oscillating between the two package sources and re-downloading multiple gigabytes to arrive where it started.Everything else is repaired: take the torch release from the engine's pin, and the build from the SDK. The release is what the engine was built against; the build is what the installed libraries belong to. Applied to the table above, both rows resolve correctly, and #264's guarantee is preserved rather than reverted.
Where the SDK publishes no build of that release, the engine's own is kept rather than failing a multi-gigabyte install, and the device check reports the outcome.
The kernel probe is what makes the first stage safe. Enumeration alone is not enough to justify keeping a torch: a build can report a device and still hold no kernel image for this exact target, failing on the first tensor operation instead of at startup. Device enumeration and kernel launch are therefore reported as distinct outcomes, and only a launched kernel earns retention — every other verdict, including no answer at all, goes to the repair path.
Why the build comes from the manifest, not the environment
This is worth calling out because the obvious implementation is wrong.
Reading "the SDK's torch" from the environment just before the engine install seems natural, and it works — exactly once. On any later install the environment already holds the engine's build, so that reading mistakes it for the SDK's, concludes the runtime is already correct, and leaves it broken permanently. Repeat installs are the common case, not the edge case: a refresh, a re-run, or a user reaching for
engines install --reinstallbecause something already looks wrong.The SDK's torch is therefore recorded in the runtime manifest at install time and read back from there. Manifests written before this change fall back to deriving the build from the recorded SDK version, including dated nightlies, where the date is part of the build identifier and not decoration on it. Both paths are covered by tests, and the fallback was exercised end to end against a runtime that was already stuck on the engine's build — it recovered.
Opting out
ROCM_CLI_DISABLE_TORCH_ALIGNMENTkeeps whatever torch the user installed. It is for the case where someone is deliberately running a hand-built torch and needs this tool to leave it alone.This is documented in
docs/vllm.mdalongside the alignment rule itself, so the escape hatch is discoverable without reading the source or tripping over the log line.The variable is read only once a replacement is actually due, so it reports the install it stopped rather than standing in for a runtime that needed nothing. The opt-out is then honoured consistently by the dependency check as well: a torch the user chose is not a violation, and it is not offered a reinstall — which would install the engine's build over exactly the torch the opt-out exists to keep. Only torch is spared; every other unmet requirement still reports as a violation.
One unrelated violation must not undo the alignment
The dependency check reports
uv pip checkoutput, and a deliberate torch divergence has to be excused there or every install ends on a violation. The excusing was applied to the run rather than to each line, so a single unrelated violation — numpy, say — demoted the whole result and the block then advisedrocm engines install <engine> --reinstall. That remedy repairs numpy and reinstates the engine's own build of torch, leaving a runtime that cannot open a device: a worse defect, adopted on the strength of one that had nothing to do with torch.Each line is now judged on its own subject, and a mixed result carries both sets — violations under the existing key, divergences under
divergence:— and says to repair the violations without reinstalling. With no divergence in play the advice is unchanged. The subject is parsed by shared code rather than a substring test, so the CLI and the engine cannot disagree about what a line is about, and a line whose shape does not parse stays a violation rather than being assumed intentional.Why a device check, and why it looks the way it does
A satisfied dependency check does not mean a usable environment. In the broken state the engine's requirements are fully satisfied — its own pinned torch is installed — and the runtime still cannot serve. That is why this went unnoticed until first serve.
The check reports four outcomes rather than a boolean: usable, no devices, a kernel failure after successful enumeration, and not verified. The last is distinct on purpose — a host with no GPU at all must not be reported as a broken runtime, and a probe that could not run must not be read as a healthy one.
The block reports the runtime after the torch is settled. On a realignment that alone cannot say whether the alignment broke something that worked or repaired something already broken, and those want opposite responses. The realigned outcome therefore also carries the verdict the probe returned beforehand, in the printed block and in the audit line both — the log especially, because it is read long after the torch that produced the earlier verdict has been replaced. Only the realigned outcome carries it; every other one left the runtime alone, so a before/after pair there would invite reading a change into an outcome that made none.
Verification
On hardware
install sdkon a clean runtimedevice_check: usable (8 device(s), torch 2.11.0+rocm7.13.0), serves a real completionengines install --reinstallafterwardsrocm engines installThe third row matters because it is the state real machines are already in — the fix has to repair them, not just avoid creating new ones.
Unit tests
The suite covers the decision surface rather than one happy path:
That last one is worth explaining.
install sdktakes its engine auto-install as a parameter, so a test can drive both outcomes and assert what the command actually returns, without a multi-gigabyte install behind it. One test pins that an unusable runtime fails it (and that the SDK's own audit record is still written first), one that an ordinary engine-install failure does not, and one that the distinction survives being wrapped in context on the way up — flattening it into a plain message is how this would quietly regress.To confirm that test is load-bearing rather than merely green, the catch was reverted to warning-and-continue and the test was observed to fail. A regression test that cannot fail is decoration.
End-to-end
Two nightly GPU scenarios, both functional rather than string matches:
The pre-warm also repairs an active-runtime pointer that names a runtime no longer on disk. A tree in that state died nine seconds in, before any test ran, and because the cache is shared across runs one interrupted or hand-edited tree kept failing every subsequent job on that runner until somebody fixed it by hand. Both pointers are rewritten — the
active.jsonmarker andconfig.json'sactive_runtime_key— since different callers read different ones and repairing only the marker leaves the engine install still following the dead key. A pointer that resolves is left alone, and a tree with no runtimes at all still fails: there is nothing to repoint it at, and inventing one would hide an install that produced nothing.The pre-warm used by these lanes installs the engine on a reused runtime as well as a freshly installed one. Without that, a warm shared tree could serve every GPU scenario a runtime with no engine — and, because the engine install is where the alignment settles, a warm tree would never exercise this change at all.
Known limitation
Only
torchis realigned.torchvision,torchaudioandtritonare left as the engine's install leaves them. That combination served correctly in testing, but the SDK resolves those four as a coordinated set, so the mixed stack has not been validated against the full matrix of supported combinations. Worth a follow-up if the engine's pins and the SDK's ever diverge further than a build identifier.tests/e2e-cucumber/expectations.tomlfor the fixed ticket ID and removed/narrowed any now-stale xfail rows. (No xfail row covered this; the affected scenario is a passing GPU scenario that this PR strengthens.)