Repository navigation
Conversation
…8457) `amd-smi` launched against a kernel fusion driver in a bad state blocks in uninterruptible sleep, which no signal escapes — so `rocm serve` hangs and survives both Ctrl-C and SIGKILL. The repo documents that and calls the `/dev/kfd` pre-flight mandatory, but three of the four production launch sites skipped it. Two of those, `gpu_vram_usage` and `detect_gpu_count`, sit on the normal `rocm serve` path for any run that is not CPU-only, and had no timeout either. Give the pre-flight one definition in `rocm-core` and route the three unguarded sites through a single guarded launcher, so a future launch site inherits the guard rather than having to remember it. The gate is Linux-only on purpose. `/dev/kfd` exists nowhere else, and `amd-smi.exe` is a supported Windows binary with a live Strix Halo lane, so a check that merely failed to find the device would have disabled GPU detection on every Windows host. Two consequences worth naming: - On Linux the two `serve` probes now decline to launch when `/dev/kfd` cannot be opened, where before they launched and usually failed anyway. GPU auto-selection degrades to service state alone, which is what it already does whenever amd-smi is unavailable. - `run_command_with_timeout` now drains both pipes on their own threads. Polling `try_wait` without draining deadlocks once the child fills the 64 KiB pipe buffer — which `amd-smi metric --json` clears easily on a multi-GPU host — and that deadlock surfaced as a spurious timeout. Routing the serve path through the helper without this would have traded the hang for silent loss of VRAM telemetry on exactly the machines that matter. Its timeout message no longer quotes partial output, because collecting it means waiting on a child that may be the unkillable one. The dash collector keeps its own pre-flight: it already runs one, it needs an async launcher, and an edge from `rocm-dash-collectors` to `rocm-core` is what the `crate_edges` layering guard exists to stop. Its check is not cfg-gated, so the dash detects no GPU off Linux — a separate pre-existing bug, untouched here. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
The hand-wrapped import list and the amd_smi_json call did not match what rustfmt produces; the prek hygiene lane caught it. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
🔴 Automated review · pr-review-watcher · 5a29292 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryThe PR consolidates the 🚫 Blocking (must fix before merge)
Non-blocking
|
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 5a29292
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
The PR consolidates the /dev/kfd pre-flight and the timeout helper into the core crate and routes three of the four amd-smi launch sites through one guarded launcher; outcome: Needs work. Reviewed at check-run conclusions 21 success, 1 failure, 4 pending, 1 skipped. Verified by reading the full commit messages and a structural sweep of the source and workflow files (no build, no test run, no suite of any kind was executed here): the four-row launch-site table is accurate at the base tip and there is no fifth site; the three unguarded sites do now route through amd_smi_json; the off-Linux wave-through test is cfg(not(target_os = "linux")) and a Windows lane does run cargo test --workspace --all-targets whenever Rust files change, and the Linux-gated test runs on the Linux lane, so neither discriminating test is stranded on a platform nothing exercises; each of the three new tests dies under a separate single-branch mutation (removing the bail! gate, or either one of the two pipe drains, or the non-Linux kfd_readable stub) rather than only under a wholesale revert; no caller anywhere parses the helper's error text and none of the two remaining callers loses meaningful diagnostics from the dropped partial output; the fourth site's exclusion and its ungated check are as described. Blocking: 1 · Non-blocking: 5.
🚫 Blocking (must fix before merge)
crates/rocm-core/src/lib.rs:7954 and crates/rocm-core/src/lib.rs:7980 — the gate treats WSL as bare-metal Linux, which silently disables amd-smi there. The comment above the constant asserts the device node is "Linux-only; no other platform exposes one", and the commit message generalises that to "/dev/kfd exists nowhere else" — but this repo documents in docs/wsl.md:175 and in crates/rocm-core/src/examine.rs:375 that WSL reaches the GPU through a different device and has no /dev/kfd at all. WSL is target_os = "linux", so it compiles the real-open branch, the open always fails, and amd_smi_json unconditionally declines every launch. That silently turns off all three newly routed sites on that platform: gpu_vram_usage and detect_gpu_count on the ordinary serve path (apps/rocm/src/main.rs:6028 and apps/rocm/src/main.rs:6039, reached on any run that is not CPU-only), and the daemon bridge snapshot (apps/rocmd/src/lib.rs:1470). This is the same failure the PR reasons carefully about for Windows, missed on the adjacent supported platform — AGENTS.md lists WSL as supported, the repo ships a WSL-specific amd-smi, there is a live WSL GPU lane in .github/workflows/e2e-selfhosted.yml:696, and tests/e2e-cucumber/features/model_serving.feature:227 documents a scenario whose outcome turns directly on whether detect_gpu_count() answers with a count or with nothing. It blocks because the regression is silent (both serve probes swallow the error with .ok()?), affects a documented platform with hardware coverage, and neither a test nor a line of the PR text addresses it. Concrete fix: wave WSL through in the Linux branch — the crate already exports is_wsl_host() (crates/rocm-core/src/lib.rs:2510), so if is_wsl_host() { return true; } at the top of the cfg(target_os = "linux") kfd_readable restores the old behaviour there; correct the constant's comment and the commit message's "nowhere else" claim at the same time, and add a test pinning the WSL wave-through the way the off-Linux one is pinned.
Non-blocking
crates/rocm-core/src/lib.rs:7998— the doc onamd_smi_jsondescribes the helper as one "which kills the child and then waits for it", which is precisely the behaviour this same commit removed; it contradicts the helper's own doc sixty lines below.crates/rocm-core/src/lib.rs:7975—amd_smi_preflight_okis exported, marked must-use and documented as the gate, but no production code calls it:amd_smi_json_withcallskfd_readabledirectly, and the only reference outside doc comments is one test.crates/rocm-core/src/lib.rs:8093— the timeout path kills without reaping and never joins the drain threads, so each timeout leaves a zombie plus two live threads behind; a detached thread that just waits on the child would reap it without ever blocking the caller.crates/rocm-core/src/lib.rs:8064— nothing tests the timeout branch itself, which is the riskiest new code; a child sleeping thirty seconds against a one-second timeout, asserting the call returns an error promptly, would pin the "returns rather than joining" promise the doc makes.AGENTS.mdsection 3 requires either a Gherkin scenario for changed user-observable behaviour or an explicit statement of why none is needed; serve's GPU auto-selection behaviour changes here and neither appears.
WSL is `target_os = "linux"`, so it compiled the real-open branch of `kfd_readable` -- but it has no `/dev/kfd` at all. It reaches the GPU through `/dev/dxg` and the Windows host driver, which is exactly why the WSL diagnosis catalog skips every bare-metal driver check (docs/wsl.md, examine::WslFacts). So the open always failed there and `amd_smi_json` declined every launch: `gpu_vram_usage` and `detect_gpu_count` on the ordinary serve path, and the daemon bridge snapshot. Both serve probes discard the error with `.ok()?`, so this was a silent loss of GPU detection on a supported platform -- one with its own diagnosis catalog and its own GPU E2E lane. The D-state hazard the gate exists for is an amdgpu/KFD one, which is precisely the stack WSL never loads. Wave WSL through at the top of the Linux `kfd_readable`, with the determination injected via `kfd_readable_with` so the behaviour can be pinned from an ordinary Linux host. Correct the `KFD_DEVICE` comment, which asserted no other platform exposes one without excluding WSL. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…preflight Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
🔴 Automated review · pr-review-watcher · 877cf06 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryRoutes three previously unguarded Previous round
🚫 Blocking (must fix before merge)
assert_eq!(
state, None,
"pid {pid} was never reaped: the timeout path must kill the child and wait on it"
);With PR text / Non-blocking
|
Withdrawing this change request: the objection is discharged.
The gate no longer treats WSL as bare-metal Linux. kfd_readable now routes through the pre-existing is_wsl_host() detector -- the same one examine.rs uses to set the is_wsl flag that drives the WSL catalog -- so it fires on a real WSL host for the same reason the catalog does. The KFD_DEVICE comment that asserted the device node is Linux-only has been corrected, and the wave-through is asserted from both sides.
Verified by mutation rather than by reading: deleting or inverting the WSL branch fails the wave-through test, and deleting or inverting the pre-flight guard fails the gate test.
A separate blocking finding, unrelated to this objection, is filed at the current head.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · df1d926
Change request filed by automation. The findings below are the blocking half of the round published in the report comment on this pull request; the non-blocking notes stay there. This will be withdrawn once they are addressed — no human needs to clear it.
🚫 Blocking (must fix before merge)
crates/rocm-core/src/lib.rs:8140-8150 — the timeout path kills the child and bail!s without ever reaping it, so every timeout leaks a zombie for the lifetime of the parent. Rust's Child has no reaping Drop, and both copies this replaces called wait_with_output() after the kill, so this is a new defect rather than inherited behaviour. It matters because one caller is a long-running daemon: apps/rocmd/src/lib.rs:1471 uses a 2-second AMD_SMI_PROBE_TIMEOUT, reached from the watcher tick loop at apps/rocmd/src/lib.rs:3010 via gather_gpu_snapshot_for_config on the 60-second GPU_METRICS_INTERVAL_MS cadence, twice per collection. A host whose /dev/kfd is readable but whose amd-smi exceeds 2s — an ordinary outcome on a busy multi-GPU box — therefore accrues zombies indefinitely and will eventually exhaust the PID table. The pre-flight does not cover this: it prevents the spawn only when the device is unreadable.
The comment at crates/rocm-core/src/lib.rs:8144-8148 is right that this thread must not wait — but the wait can be moved off it. Reap on a detached thread instead:
if started.elapsed() >= timeout {
let _ = child.kill();
// Reap off this thread: an unkillable child would block the wait, which is
// the hang this timeout exists to bound.
thread::spawn(move || {
let _ = child.wait();
});
bail!("process exceeded {}s timeout", timeout.as_secs());
}This compiles despite the move inside the loop because bail! diverges, it keeps the "never block the caller" property the current comment is defending, and in the unkillable case it costs one parked thread instead of one zombie — no worse, and strictly better in the ordinary case that actually recurs.
…preflight Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
`run_command_with_timeout` killed a timed-out child and then dropped the handle. `std::process::Child` has no reaping `Drop`, so the corpse stayed in state `Z` for the lifetime of the parent. Both per-app copies this function replaced called `wait_with_output()` after the `kill`, so the reap used to happen; consolidating them dropped it. That is a real leak rather than a tidiness point because one caller is a daemon. `rocmd` probes `amd-smi` with a 2s timeout from its watcher tick, twice per GPU-telemetry collection, on a 60s cadence. A host whose `/dev/kfd` is readable but whose `amd-smi` simply overruns 2s — ordinary on a busy multi-GPU box — therefore accrues zombies indefinitely. The KFD pre-flight added here does not cover it: that only prevents the spawn when the device is unreadable. Restoring the `wait` inline would reintroduce the hang the timeout exists to bound, since an uninterruptible-sleep child blocks the `wait` exactly as it ignores the `kill`. Hand the child to a detached reaper thread instead, so the calling thread still returns promptly. The reap runs whether or not `kill` succeeded, because the likeliest failure is a child that exited between the poll and the kill — already a zombie, and already in need of reaping. The doc comment is explicit that this is best-effort: an unkillable child parks its reaper instead of leaving a zombie, trading a stranded PID for a stranded thread. That is better than blocking the caller and no worse than the leak it replaces, but it is still a leak, and the pre-flight remains the only actual mitigation. Reapers do not accumulate otherwise — one per timeout, each exiting as soon as its child does. Covered by a Linux-only test that reads the child's recorded pid straight out of `/proc`: without the reap it observes state `Z` for the full ten- second poll budget, with it the entry is gone in under a second. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
juhovainio
left a comment
There was a problem hiding this comment.
Reviewed this with a combined Standards/Spec/bug-scan pass. The engineering here is solid: the zombie-reap fix in the last commit is correct (verified the new a_timed_out_child_is_reaped_rather_than_left_a_zombie test passes, and traced that the reaper thread takes ownership of child right before the bail! with nothing left to race it), the WSL wave-through is properly pinned from both directions, and cargo clippy --workspace --all-targets -- -D warnings is clean. Both of pr-review-watcher's prior blocking rounds are genuinely discharged at this head.
Two things this repo's own AGENTS.md treats as hard requirements are still unmet, though:
-
This PR changes what
rocm servedoes that a user can observe — on a Linux host where/dev/kfdcan't be opened, GPU auto-selection now declines to launchamd-smiat all and silently falls back to service-state-only detection, where before it attempted the launch. AGENTS.md §3 is explicit that this needs a Gherkin scenario (or an named@id:, a named gated lane, or an explicit "purely internal" justification) and that a unit test on the internal helper doesn't discharge it. The PR's two new unit tests provekfd_readable/amd_smi_json, not the observableservebehavior, and the PR body's "Docs" section addresses a different question (whether a doc file needs editing) without touching this. Left a comment on one of the two call sites below. -
The PR body ends with an undisclosed "🤖 Generated with Claude Code" footer, which AGENTS.md §11 asks PRs to avoid.
One more thing worth a look before merge, not blocking: crates/rocm-dash-collectors/src/amd_smi.rs's kfd_accessible() still has no WSL wave-through — the same silent-detection-loss bug this PR fixes on the serve path, left standing on the dash path. The PR's stated reason for not routing it through the new shared launcher (a crate_edges layering guard plus a sync/async mismatch) checks out against xtask/src/crate_edges.rs, and it's disclosed rather than hidden, so I'm not blocking on it — just flagging in case a quick follow-up ticket is warranted.
| return None; | ||
| } | ||
| let value: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?; | ||
| let value = rocm_core::amd_smi_json(&["metric", "--json"], AMD_SMI_SERVE_PROBE_TIMEOUT).ok()?; |
There was a problem hiding this comment.
This (and the sibling call in detect_gpu_count a bit further down, around line 21354) is the user-observable change AGENTS.md §3 is about: on a Linux host with an unreadable /dev/kfd, serve's GPU auto-selection now declines the amd-smi launch entirely and falls back to service-state-only, where before it attempted the launch. That needs a Gherkin scenario in tests/e2e-cucumber/features/ (or a stated reason none applies) per §3 — the two new unit tests added in this PR cover the helper, not this observable behavior, and neither is cited as already covering it.
There was a problem hiding this comment.
Both points taken, and both are addressed in 9d2024ba, pushed after this review — so the head you reviewed (877cf068) genuinely did not have them.
1. §3. You are right that the two unit tests prove the helper, not the behaviour, and that the body did not address it. I have not added a scenario, because I do not think one can exist on any lane this suite runs; instead I have stated the gap explicitly, in the two places §3 asks for:
- The PR body now has a dedicated section, "AGENTS.md §3: why consequence 1 ships without a Gherkin scenario", replacing the silence you found. It names the premise the scenario would need — a host that is both GPU-present by the KFD/DRM sysfs rule and unable to
open("/dev/kfd")(a container started without--device /dev/kfd, or a user outside the render group) — and walks each lane: the no-GPU mock lane never reaches either probe, becauseusable_amd_gpu_indicesreturns an authoritativeSome([])andservefails fast on that first; that fail-fast is@id:serve-no-gpu-fails-fast, which passes on every PR, so the code in question is unreachable there rather than merely untested.@requires-gpulanes have a readable/dev/kfdby construction, and@requires-wslexercises the opposite branch (the wave-through). 9d2024baalso records the same reasoning as aKNOWN GAPnote at the end oftests/e2e-cucumber/features/model_serving.feature, in the style that file already uses for serve-20, so it is visible to whoever next adds GPU-selection coverage rather than only in a PR description.
On why not to fake it: a scenario planting a stub amd-smi under ROCM_CLI_DATA_DIR would take the allowed branch on a GPU lane and never run at all on the mock lane — green without exercising anything, which is the vacuity the rule exists to prevent. KFD_DEVICE is a compile-time constant with no path override, the node is root-owned, and supplementary group membership cannot be dropped without privilege, so the harness cannot manufacture the premise either. If you would rather have a @nightly-gated scenario with a container that has sysfs topology and no /dev/kfd passed through, I will take that as the answer instead — it is an infrastructure change, not a code one.
The unit coverage it falls back to is the_kfd_gate_stops_amd_smi_before_it_is_spawned (crates/rocm-core/src/lib.rs:9883), which asserts the spawn does not happen — a fake amd-smi leaves a marker file that must not appear — with the same fixture re-run against a readable device to prove the fake would otherwise have executed. Non-vacuous by construction rather than by assertion.
2. Footer. Removed; the body now ends at the Test plan checklist.
On kfd_accessible() — agreed, and thank you for checking the layering reasoning rather than taking it on trust. It is still a bare open(KFD_DEVICE) at crates/rocm-dash-collectors/src/amd_smi.rs:137, with neither the cfg(target_os) gate nor the WSL wave-through, so AmdSmi::detect() returns None on Windows, macOS and WSL. Untouched since #295 and outside this PR's blast radius, so I am filing it as a follow-up ticket rather than widening this change.
There was a problem hiding this comment.
Closing the loop on your non-blocking kfd_accessible() note: it has been fixed upstream, so the follow-up ticket I owed you is no longer needed.
#375 ("fix(dash): detect amd-smi through WSL DXG", EAI-8450) landed on main as d37cfbab. kfd_accessible() is gone entirely, replaced by:
fn preflight_passes(skip_device_preflight: bool, gpu_reachable: bool, kfd_device: &Path) -> bool {
skip_device_preflight || gpu_reachable || device_accessible(kfd_device)
}(crates/rocm-dash-collectors/src/amd_smi.rs:154) — the bare device open is now the last resort rather than the only test. apps/rocm/src/dash.rs:74 supplies gpu_reachable from rocm_core::has_usable_amd_gpu(), and the comment at :82 records that the verdict may only substitute for the device open on WSL, so bare-metal behaviour is unchanged. That is exactly the wave-through this PR adds on the serve path, now present on the dash path too.
Worth noting it was solved the way you and I both reasoned it should be: in the dash crates themselves rather than by routing them through rocm-core and tripping the crate_edges layering guard. The commit message says as much — it adds "a second, weaker WSL check" local to rocm-dash-collectors precisely so those crates "stay free of" a rocm-core dependency.
I have updated this PR's description accordingly; it previously described the gap as still open, which would now mislead a reader.
The §3 and footer items from your review remain addressed in 9d2024ba as described above, and this branch has since merged current main (a43223d8) to clear a conflict in model_serving.feature — #409 took serve-22, so my trailing KNOWN GAP note now follows it. Full workspace tests, both clippy passes and fmt are clean on the merge.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 877cf06
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Routes three previously unguarded amd-smi launches through one /dev/kfd-gated helper in rocm-core, adds pipe draining and a detached reaper to run_command_with_timeout, and waves the gate through on WSL. Outcome: Needs work — the code is sound and the round-3 remediation is genuinely load-bearing, but one new test passes for the wrong reason under a neighbouring mutation, and the contributor rule on user-observable behaviour is left unaddressed in the PR text. Verified: on a scratch copy I mutated each production line the new tests name and re-ran the rocm-core lib tests — deleting reap_in_background (or swapping it for drop(child)) fails the zombie test in ~10s; restoring the pre-change body of run_command_with_timeout genuinely deadlocks the 300 KB child test; neutering the bail, or making kfd_readable_with unconditionally true, fails the gate test; deleting the WSL wave-through fails the WSL test, and so does making the whole function true, so its bare-metal control really is load-bearing; the original checkout was confirmed unmodified afterwards, and I independently confirmed the stderr/stdout precedence refactor in amd_smi_json_with is logically identical to what it replaces, that the OsStr import is used on all targets, and that no test or doc consumes the timeout message's dropped partial-output suffix. The full test suite and the e2e lanes were not run in this round. Head checks: 26 success, 1 skipped, 1 pending, zero failures; trunk separately carries 3 red nightly GPU-hardware lanes that this diff cannot be attributed to. Blocking: 2 · Non-blocking: 5.
Previous round
- Zombie left by the timeout path (
crates/rocm-core/src/lib.rs) — resolved.run_command_with_timeoutnow hands the killed child toreap_in_background, whichwaits on a named detached thread, and reaps unconditionally rather than only on a successfulkill— exactly the shape requested. Confirmed non-vacuous by mutation, not by reading: removing the call, and separately replacing it withdrop(child), both faila_timed_out_child_is_reaped_rather_than_left_a_zombiewith "pid is still a zombie" after burning the full 10s poll budget; unmutated, the process-table entry is already gone. The doc comment onreap_in_backgroundstates the residual unkillable-child case honestly, and that disclosure checks out against the code.
🚫 Blocking (must fix before merge)
crates/rocm-core/src/lib.rs — a_timed_out_child_is_reaped_rather_than_left_a_zombie, the final assert_ne!(state, Some('Z')). The assertion is satisfied by two materially different outcomes, and one of them is a broken timeout path. Empirically: removing let _ = child.kill(); from run_command_with_timeout while keeping the reaper leaves this test passing in 0.31s — the sleep 30 child is never killed, so the poll loop only ever observes state S, never Z, and assert_ne! holds for the wrong reason. The test therefore proves "reaping happens given the child was killed", not that the timeout path kills and reaps; the result.is_err() assertion above it is a timeout error, which is not evidence of a kill. On a genuinely passing run the observed state is None (entry fully gone) — I instrumented the scratch copy to confirm that, then reverted it. Fix: assert the reaped outcome exactly rather than merely not-zombie —
assert_eq!(
state, None,
"pid {pid} was never reaped: the timeout path must kill the child and wait on it"
);With assert_eq!(state, None) the kill-removal mutation fails as it should (Some('S') != None), and the reaper mutations still fail as they already do. I re-ran the adversarial pass on this fix: it does not weaken the existing coverage, and it cannot flake on a slower box because the surrounding loop already polls to a 10s deadline before the assertion runs — the only way to reach the assertion with Some('S') is the defect itself.
PR text / tests/e2e-cucumber/features/ — the contributor rule on user-observable behaviour is neither satisfied nor discharged. The base tip's AGENTS.md section 3 requires that a change altering what a user of the CLI can observe — explicitly including "which runtime/engine it selects" — be covered by a scenario under tests/e2e-cucumber/features/, and states in terms that "a unit test asserting the internal helper does NOT discharge this". This change alters rocm serve GPU auto-selection on a Linux host whose /dev/kfd cannot be opened: gpu_vram_usage and detect_gpu_count now decline to launch instead of launching, and both discard the error with .ok()?. The rule offers two cheap escapes — name the existing @id: the change makes pass, or, if only a gated lane can exercise it, say so and name the lane; for a purely internal change, say why none is needed. The PR text takes none of them: its Tests section lists three unit tests and nothing else, and no scenario under tests/e2e-cucumber/features/ covers an unreadable-but-present KFD node (the nearest, around device masking and absence, cover different conditions). Fix: either add the scenario, or state plainly in the PR body which gated lane covers this and why a scenario is impractical for a wedged-driver condition. This is a one-sentence discharge if the second route is the honest one — but the rule asks for it explicitly and it is currently absent.
Non-blocking
crates/rocm-dash-collectors/src/amd_smi.rs:137— the fourth launch site's ownkfd_accessible()runs only atdetect(); the three methods that actually spawn (metrics,processes,system_info, viarun/run_jsonaround lines 114-133) never re-check, so that site is guarded once per collector lifetime rather than per launch. The PR's table records it as "pre-flight before: yes", which reads stronger than check-then-use across a daemon's lifetime; worth a caveat in the table given the title says every launch site.crates/rocm-core/src/lib.rs:8011—amd_smi_preflight_ok, thepubfunction whose doc comment carries the "mandatory before every launch" claim, has no production caller anywhere in the tree and, on Linux, no test caller either (its only use is inside a non-Linux-only test). The gate that actually fires is the privatekfd_readable. Relatedly, the PR body's basis for "no doc edit required" — thatkfd_accessibledoes not appear outside the three changed files — is wrong: it is a live private symbol at the line above, in a file this diff does not touch, whose own doc comment independently asserts the pre-flight is mandatory.- Nothing mechanically stops a seventh launch site landing unguarded.
resolve_amd_smi_binarystayspuband directly spawnable, and no xtask check, lint or test enforces routing throughamd_smi_json— the claim that "a future launch site inherits the guard" rests on convention plus a doc comment.tests/e2e-cucumber/tests/e2e/serving_steps.rs:306already spawns a bareamd-smiwith neither gate nor timeout, which is the hang this change exists to prevent, on the lane least able to absorb it. - PR body, "Why the fourth site is left alone" —
xtask/src/crate_edges.rsnames exactly one dash-side invariant (rocm-dash-tuimust not depend onrocm-core); an edge fromrocm-dash-collectorswould trip the generic allowlist path, whose own message invites adding the edge "after review if this edge is intentional". Describing the guard as existing to keep the dash crates decoupled overstates a targeted intent the file does not state, in the direction that makes leaving the site alone look more forced than it is. crates/rocm-core/src/lib.rs:8195—drain_on_threaddiscards theread_to_enderror, so a genuine pipe-read failure now surfaces as the vaguer JSON-parse error rather than the distinct collection error it used to. Separately,apps/rocm/src/main.rs:6077,6088can now spend up to 20s in two sequential probes with no progress output; that replaces a previously unbounded stall, so it is a net improvement, but a one-line status print or running the two concurrently would remove the silence.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 877cf06
Requesting changes on two items. The full round, with the mutation evidence and the previous round's status, is in the review comment posted alongside this. The previous round's blocking item — the zombie left by the timeout path — is resolved, and that was confirmed by mutation rather than by reading.
1. The new reaper test's final assertion is satisfied by two materially different outcomes, one of which is a broken timeout path. Removing the kill from the timeout path while keeping the reaper leaves the test passing in under a third of a second: the long-sleeping child is never killed, so the poll loop only ever observes the running state, never the zombie state, and a not-equal assertion holds for the wrong reason. The test therefore proves that reaping happens given the child was killed, not that the timeout path kills and then reaps; the timeout error asserted above it is not evidence of a kill. On a genuinely passing run the observed state is that the process-table entry is gone entirely. Assert that outcome exactly rather than merely "not a zombie": with the exact-match form, the kill-removal mutation fails as it should, the reaper mutations still fail as they already do, and it cannot flake on a slower machine because the surrounding loop already polls to its full deadline before the assertion runs.
This gates rather than being a note because a weak test is never strengthened after merge, and this one sits on the exact regression the previous round was filed about.
2. The contributor rule on user-observable behaviour is neither satisfied nor discharged. The base tip's contributor guide requires that a change altering what a user of the command-line tool can observe — naming which runtime or engine it selects as an example — be covered by a scenario in the end-to-end feature directory, and says in terms that a unit test asserting the internal helper does not discharge it. This change alters serve-time GPU auto-selection on a Linux host whose kernel driver node cannot be opened: two probes now decline to launch instead of launching, and both discard the error. No scenario covers an unreadable-but-present driver node; the nearest existing ones cover absence and device masking, which are different conditions. The rule offers two cheap escapes — name the existing scenario id the change makes pass, or state that only a gated lane can exercise it and name that lane. The description's test section lists three unit tests and takes neither. This is a one-sentence discharge if the second route is the honest one, but it is currently absent.
Two further observations that do not gate but are worth reading in the round: the fourth launch site is guarded once per collector lifetime rather than per launch, which the description's table records more strongly than that; and nothing mechanically stops a seventh unguarded launch site landing, since the binary resolver stays public and directly spawnable and no check enforces routing through the guarded helper.
…ie (EAI-8457)
`assert_ne!(state, Some('Z'))` was satisfied by two different outcomes: a
child the timeout path killed and reaped, and a child it never killed at
all, which sits in `S` while it sleeps on. Only one of those is the
contract, so the assertion could not tell a working timeout path from a
broken one. Assert the `/proc` entry is gone instead, and retry on any
surviving state rather than only on `Z` so neither the post-`SIGKILL`
teardown transient nor the detached reaper reads as a pass.
Mutation-checked: unmutated passes; dropping `child.kill()` fails with
`Some('S')` where the old assertion passed; dropping
`reap_in_background` still fails with `Some('Z')`.
Also record why the declined-launch behaviour has no Gherkin scenario, as
AGENTS.md section 3 requires when one cannot be written. The premise needs a
host that is GPU-present by the KFD/DRM sysfs rule yet cannot open
`/dev/kfd`; no lane is that host and the harness cannot manufacture it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
🔴 Automated review · pr-review-watcher · 9d2024b This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryRoutes the three previously unguarded Prior round
🚫 Blocking (must fix before merge)None. Non-blocking
|
…preflight Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # tests/e2e-cucumber/features/model_serving.feature
…preflight Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # apps/rocmd/src/lib.rs
…preflight Signed-off-by: Roman Sirokov <roman.sirokov@amd.com> # Conflicts: # apps/rocm/src/main.rs # apps/rocmd/src/lib.rs # tests/e2e-cucumber/features/model_serving.feature
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · f096985
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Every finding below is new this round. No objection from an earlier round was still standing. Because the commits since the last round are all merges of main, this round reviewed the whole change rather than only the commits since the last round.
Review — needs work
Full review of the whole change (prw-base...HEAD, merge-base 70d6aa13).
The description names ticket EAI-8457 and claims the pre-flight is now routed through every synchronous amd-smi launch site. That claim holds for the code, except that the change also deletes unrelated code the description never mentions (first Blocking item).
Blocking
-
The last merge deleted the
ROCM_E2E_FORCE_LOW_VRAMtest hook, and the change does not mention it —apps/rocm/src/main.rs:22099-22101(hook removed),apps/rocm/src/main.rs:32909-32930,tests/e2e-cucumber/tests/e2e/serving_steps.rs:865,tests/e2e-cucumber/features/model_serving.feature:283-291- What was removed:
simulated_low_vram_usage()and its#[cfg(feature = "e2e-test-hooks")]call at the top ofgpu_vram_usage(). Both exist onprw-base(added by #251,5ed9365) and in the base-side parent of mergef096985. The merge result drops them, and no earlier branch commit touched them. - Compile failure, reproduced:
cargo check -p rocm --features rocm/e2e-test-hooks --testsfails witherror[E0425]: cannot find function simulated_low_vram_usageatmain.rs:32922and:32929. - Scenario lost: the step at
serving_steps.rs:865still setsROCM_E2E_FORCE_LOW_VRAM=0, but nothing reads it any more.@id:serve-vllm-low-vram-oom-guidance(serve-23) therefore loses its seam on the real-GPU lane, whose cards are mostly free. - Why CI is green: CI builds with the feature (
cargo build --release ... --features rocm/e2e-test-hooks) but never compiles the test target with it, and defaultcargo test/clippy don't enable it. - Description: it says nothing about removing the hook (unmentioned work), and "No doc edit required" and the green test plan were all run on default features.
- Confidence 100 · logic · Fix: restore the function and its call, ahead of
gpu_vram_usage_amd_smi().or_else(gpu_vram_usage_sysfs). Add a CI step or check that compiles the test target withe2e-test-hooks, so a second silent loss like this fails visibly.
- What was removed:
-
amd_smi_preflight_okis a public function with no production caller —crates/rocm-core/src/lib.rs:8259-8261, test-only use at:10321- Evidence: a grep of the whole workspace finds only the definition and one assertion in a test compiled off Linux.
amd_smi_json_withcallskfd_readable(kfd_device)directly. - Contradiction: its doc comment says the check is mandatory before every launch, which tells callers to use it. Nothing does, and the off-Linux test of it asserts nothing independent: it just returns
kfd_readable(...). - Which side governs: the code does.
amd_smi_jsonis the only launch path and takes the check throughkfd_readable. - Confidence 85 · mechanical · Fix: delete it and point the off-Linux test at
kfd_readable, or give it a real caller.
- Evidence: a grep of the whole workspace finds only the definition and one assertion in a test compiled off Linux.
-
The behaviour change has no scenario, and the "cannot be manufactured" argument depends on a seam the PR chose not to add —
tests/e2e-cucumber/features/model_serving.feature:299-339; AGENTS.md §3-
Behaviour: on Linux with an unreadable
/dev/kfd,--gpu autono longer launchesamd-smi. That is user-observable, and §3 says a unit test on the internal helper does not discharge it. AGENTS.md lists internal-only changes as the sole exemption, and this is not one. -
The claim is not actually blocked: it says
KFD_DEVICEhas "no path override", so no lane can reach the declined branch. The repo has established seams for this exact situation:e2e-test-hooks-gatedROCM_E2E_*environment seams (the one the first finding restores, andROCM_E2E_LEMONADE_BACKEND_INSTALL_FAILURE);- the state-file seam
<ROCM_CLI_DATA_DIR>/telemetry/test-clock-offsetfrom #466 (52f4409).
A gated override of the KFD path on a
@requires-gpulane would make the declined branch reachable. -
Hard to assert: I did not establish that "auto-selection degrades to service state alone" can be asserted from the CLI. If it cannot, that is the argument to make, along with a maintainer accepting the exception.
-
WSL coverage claim is wrong: line 321 says "
@requires-wslexercises the wave-through". The@requires-wslscenarios (examine.feature:115,diagnose.feature:244,255,driver_install.feature:12-28,dash.feature:325) never reachserve --gpu autoor therocmdsnapshot, anddash-24goes through the separate collector gate. So the WSL wave-through is covered only by the unit test. -
Confidence 70 · architectural · Fix: add a gated seam plus a scenario, or correct the comment's WSL claim and get the exception explicitly accepted. As written, §3 is not met and the comment overstates the coverage.
-
Non-blocking
-
The
rocmdcopy ofrun_command_with_timeoutremains, with both defects this change fixes (pre-existing, pattern-relevant) —apps/rocmd/src/common.rs:165-206, caller:224- It is the old poll-then-
wait_with_outputbody: it deadlocks once the child fills the 64 KiB pipe buffer, and blocks the caller afterkillon an unkillable child. - It is now a divergent duplicate of the
rocm-corefunction.rocmdalready depends onrocm-core, so it is a one-line swap like the one made atapps/rocm/src/main.rs:15959. - Confidence 85 · architectural.
- It is the old poll-then-
-
Timeout error no longer quotes partial output, for every caller —
crates/rocm-core/src/lib.rs:8396-8409- The old message was
process exceeded Ns timeout: <stderr|stdout|no output>; it is nowprocess exceeded Ns timeout. This reaches the assistant tools atmain.rs:15769/:15867(install_sdk,launch_server, 2-minute timeouts), where the last stderr shows where the stall happened. It is deliberate and documented, but untested. - Both the description and the code comment say why. A shared buffer snapshotted on timeout would keep the diagnostic without joining the child.
- Confidence 80 · logic.
- The old message was
-
a_timed_out_child_is_reaped_rather_than_left_a_zombiedoes not pin the "detached" property (verdicts below)- Replacing
reap_in_background(child)with an inlinechild.wait()keeps it green, because aSIGKILLedshreaps instantly. So nothing asserts that the caller returns without waiting, which is the property the docs lean on. A D-state child cannot be created in a test, so this may be untestable. If so, the comment should say so rather than imply it. - The test also uses a 300 ms timeout against a child that must write its PID file first, and
the_kfd_gate_*writes and execs a script while sibling tests spawn children (possibleETXTBSY). Both are plausible flakes on loaded runners. - Confidence 70 · mechanical.
- Replacing
-
User-facing docs still say the sysfs fallback happens only when amd-smi is absent —
README.md:603-605,docs/vllm.md:226-231,skills/rocm-cli-assistant/SKILL.md:30, plus thegpu_vram_usagedoc comment atmain.rs:22091-22098- The fallback now also applies when
/dev/kfdis unreadable or the 10 s probe times out. The description says "No doc edit required" on the basis of a narrow search for helper names. - Confidence 70 · mechanical.
- The fallback now also applies when
-
rocmdcan leak threads without bound when the gate passes butamd-smistays in D state —crates/rocm-core/src/lib.rs:8448-8454, cadence atapps/rocmd/src/lib.rs:47,52- Each timed-out launch leaves a reaper thread, two reader threads and the child. At the 60 s snapshot interval that is roughly 3 threads and 1 process per minute, unbounded. The doc is honest about the reaper; the cost is that the tick previously blocked once, whereas now it keeps spawning. An atomic "outstanding unreaped child" latch that makes
amd_smi_jsondecline would bound it. - Confidence 70 · architectural.
- Each timed-out launch leaves a reaper thread, two reader threads and the child. At the 60 s snapshot interval that is roughly 3 threads and 1 process per minute, unbounded. The doc is honest about the reaper; the cost is that the tick previously blocked once, whereas now it keeps spawning. An atomic "outstanding unreaped child" latch that makes
-
Commit history is a trail of five merge commits of
origin/main. One of them,f096985, silently carried the first finding. A rebase ontomainwould have surfaced the conflict as an explicit resolution.
Decisions for the author
-
Is a successful
open(/dev/kfd, O_RDONLY)the right pre-flight probe? — tradeoff- For: it matches what the dash collector already does, and it filters the common missing or unpermitted node cases cheaply. It also does not change behaviour for hosts where it passes.
- Against: it is a real
kfd_open, which in upstreamkfd_chardev.c/kfd_process.ccreates a KFD process context and takes a non-interruptible mutex. In the wedged-driver state the doc comment names, the open itself could block theserveorrocmdtick thread uninterruptibly, which is worse than before. The context is tied to the process's address space, not the handle, so a long-livedrocmdwould hold one for its lifetime. Anaccess(2)permission check avoids both costs, and a probe child with a timeout keeps the open but bounds it. The doc's claim that declining is "the only mitigation" overstates what an open can detect (version-mismatched or partly installed drivers usually still open fine). I argued this from kernel source only and could not reproduce it. - Regression risk: unprivileged hosts where
/dev/kfdis 0660 and the user is not inrendermay have hadamd-smitelemetry that now silently disappears. This matters forrocmdtelemetry, less so forserve.
-
Two KFD pre-flights now exist with different WSL rules — tradeoff
rocm-corewaves WSL through unconditionally (is_wsl_host()); the dash collector (rocm-dash-collectors/src/amd_smi.rs:154,apps/rocm/src/dash.rs:89) only waves it through when a usable GPU was found. The description explains why the collector was left alone (layering and async), so this is deliberate, but the policy divergence is not documented in either place.is_wsl_host()matches "wsl" as a substring of/proc/versionor the presence of/dev/dxg; a misfire fails open to pre-PR behaviour.
-
The
serveprobes now have a 10 s timeout where they had none — non-blocking-improvement- A slow multi-GPU
amd-smithat takes more than 10 s now returnsNonesilently, and the sysfs fallback gives nothing on multi-GPU hosts, so VRAM-aware auto-selection degrades without a message. Consider logging the decline or timeout reason.
- A slow multi-GPU
Positive signals
- The WSL wave-through is tested with a bare-metal control (
kfd_readable_with(false, absent)must still decline), so the test cannot pass with the wave-through deleted. the_kfd_gate_stops_amd_smi_before_it_is_spawneduses a marker file plus a readable-device re-run, so the blocked case cannot pass vacuously.- Draining both pipes on threads before the timeout poll fixes a real silent-telemetry-loss hazard that routing the
servepath through the old helper would have introduced.
Deployment notes
- Linux hosts that cannot open
/dev/kfdnow skipamd-smientirely inrocm serveGPU auto-selection and therocmdGPU snapshot. WSL and non-Linux are unchanged. That is a behaviour change for users outside therendergroup. - The
rocm-corepublic API gainsamd_smi_json,amd_smi_preflight_okandrun_command_with_timeout.
What this covered
- Every changed file read completely:
apps/rocm/src/main.rs(all changed hunks and their surrounding functions and callers),apps/rocmd/src/common.rs,crates/rocm-core/src/lib.rs(every added item and test), andtests/e2e-cucumber/features/model_serving.feature. The diff was read in full and compared againstgit show prw-base:<path>. Reviewedprw-base...HEAD(merge-base70d6aa13…, headf0969855…). The work was split across four read-only workers (code pass for the three source files, impact and completeness, agent-instruction/history/comments, design questions); none built anything. - Checks run:
cargo check -p rocm --features rocm/e2e-test-hooks --tests(one build, reproduced the first Blocking item).- The
gh pr viewCI rollup for this head: every check passed or was skipped (clippy, build-and-test, windows-build-and-test, E2E tests, commit signatures, among others). Passing CI does not cover the hook-enabled test target, as noted above. The Skill checks (skillscope) and Sphinx docs build (-W) checks had not reported when this round was handed over.
- Did not run: the full workspace test suite, clippy, or any mutation run; test verdicts below were reached by reading and tracing, not execution. The one exception is
a_timed_out_child_is_reaped_rather_than_left_a_zombie, whose revert behaviour I could not run and argued from the code only.- Test verdicts, reasoned not run.
the_kfd_gate_stops_amd_smi_before_it_is_spawned: fails with the gate reverted; stays green if the publicamd_smi_jsonor a call site stopped routing through the gate, because it calls the_withvariant, and it passes vacuously on WSL.a_child_that_outgrows_the_pipe_buffer_is_not_reported_as_hung: fails with the drain reverted; either pipe's drain removed alone is also caught.a_timed_out_child_is_reaped_rather_than_left_a_zombie: fails with the kill or the reap removed; stays green if the reaper is made synchronous.the_kfd_preflight_waves_through_hosts_that_have_no_kfd(non-Linux): its second assertion cannot fail independently of the first.the_kfd_preflight_waves_through_wsl_hosts: stays green ifkfd_readablehard-coded the WSL flag, because the wiring tois_wsl_host()is unpinned.
- Test verdicts, reasoned not run.
- Prior-changes pass ran against already-merged PRs only (#251 and #466 applied); no earlier pass of this PR was read.
- Not reconciled against any discussion here.
Resolving the `gpu_vram_usage` conflict after #251 was squash-merged, I kept this branch's pre-refactor body and then restored main's dispatcher -- but not `simulated_low_vram_usage()` or its `#[cfg(feature = "e2e-test-hooks")]` call, both of which arrived with #251. The symbol count fell from main's 4 to 2, leaving only the two dangling references in the test module. Nothing in the default build reaches the hook, which is why 21 CI checks stayed green over it. It breaks only under the feature: cargo check -p rocm --features rocm/e2e-test-hooks --tests error[E0425]: cannot find function `simulated_low_vram_usage` and the step at serving_steps.rs:865 still set ROCM_E2E_FORCE_LOW_VRAM with nothing left to read it -- a scenario quietly asserting nothing. Restored verbatim from main: both the hook and its gated call are now byte-identical to origin/main, and the symbol count matches again. Verification note: the Linux devbox is unavailable (its pod has been Pending ~90m behind a 502 from the node proxy) and macOS cannot build this workspace -- `cargo check` stops in rocm-engine-lemonade on `u32 & u16` long before reaching this crate -- so the feature-gated compile has NOT been re-run here. It is pending, and the restored text is identical to the known-good tree. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Confirmed, and fixed in What happened. Why nothing caught it. Exactly as you say — the default build never reaches the hook, so 21 checks stayed green. It breaks only under the feature, which no per-PR job builds with tests: The release-with-hooks build the E2E lane does run compiles fine, because the dangling references are in And your second point is the one that would have hurt most: The fix restores both pieces verbatim from One honest caveat. I have not been able to re-run the feature-gated compile. The Linux devbox is down — its pod has been |
My restore of the low-VRAM hook in a3085af spliced the function in between `gpu_vram_usage`'s doc comment and `gpu_vram_usage` itself. The result compiles, so CI stayed green, but the amd-smi/sysfs fallback prose ended up describing `simulated_low_vram_usage`, `gpu_vram_usage` was left undocumented, and the hook's own twelve-line comment -- explaining why the seam exists and that production builds never see it -- was dropped entirely. Restores both comments to the functions they describe, matching 70d6aa1 (the last commit before the merge that dropped the hook) byte for byte. No executable code changes: `gpu_vram_usage`'s body is moved verbatim, and moving a top-level item is inert. Verified without a build host: the region is byte-identical to 70d6aa1, the diff touches no statement, `rustfmt --check --edition 2024` parses the file and reports no formatting change, and the three intra-doc link targets (`AUTO_FREE_VRAM_FRACTION`, `vram_capacity_is_meaningful`, `read_drm_vram_usage`) all still resolve. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
main narrowed `rocm-core`'s process import to `use std::process::Command` when #528/#483/#487 lifted the last `Stdio` users out of lib.rs. This branch adds new `Stdio` users to that same file, so the merge combined the narrowed import with code that needs the wider one and produced a tree that git saw as conflict-free but rustc did not. Restore `Stdio` to the import. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Two conflicts, both against #584 ("rocm-core: share command_failure_detail across vllm, rocm, rocmd"), which edited the same code this branch moves. apps/rocm/src/main.rs: #584 rewrote the tail of the local run_command_with_timeout to call command_failure_detail; this branch deletes that function outright, having hoisted it into rocm-core. Took the deletion. That leaves #584's `command_failure_detail` import with no caller in this crate -- its only use was the body just deleted -- so drop it from the rocm_core use list, which reflows back to its pre-#584 shape. apps/rocmd/src/common.rs: both sides edited the same rocm_core use list. Union of the two intents: add amd_smi_json and drop resolve_amd_smi_binary (this branch, since capture_amd_smi_json is now a one-line delegation), and keep command_failure_detail (#584), which is still called by the local run_command_with_timeout that this branch leaves in place for run_rocm_capture_for_paths. No overlap in crates/rocm-core/src/lib.rs: #584's command_failure_detail lands near require_nonempty, far from the pre-flight block added here. rocm-core's amd_smi_json keeps its own stderr-else-stdout picker rather than calling command_failure_detail, because its no-output fallback is "exit status {status}", not the helper's literal "no output" -- that is the behaviour capture_amd_smi_json had before this branch and reusing the helper would silently change it. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Pushed Finding 1 — unbounded leaked probe threads (
|
| Path | Probes | Was documented | Actually |
|---|---|---|---|
serve --gpu auto |
2 sequential | ~20s | 2 × (2s + 10s) = ~24s |
--model readiness |
1 | 10s | 2s + 10s = ~12s |
rocmd telemetry poll |
1 | 2s | 2s + 2s = ~4s |
apps/rocm/src/gpu_policy.rs:501-528— theAMD_SMI_SERVE_PROBE_TIMEOUTdoc now has a "Worst-case latency" section with the arithmetic shown, and says plainly that the previous 10s/20s figures omitted the pre-flight.crates/rocm-core/src/amd_smi.rs:244-250—KFD_OPEN_TIMEOUT's doc states the bound is additive to everyamd_smi_jsontimeout, not inside it, and points at the caller that documents a worst case.crates/rocm-core/src/amd_smi.rs:430-436—amd_smi_jsongets a# Latencysection giving the general rule.apps/rocmd/src/common.rs:39-44— the watcher's 2s constant now says a poll can cost 4s, still inside its 60s cadence.
One honest qualifier, stated in the doc rather than quietly assumed: the two bounds are additive in the formula but rarely both paid, because a host whose open hangs is a host that then declines and never spends the amd-smi budget. A ceiling that assumes the good case is not a ceiling, so it is quoted additively.
Merge resolution
main has been running the Phase 6x modularization (ROCMAI-82/91/211/214/215), which moved most of what this branch touches. Several conflict hunks had an empty main side — that meant "moved", not "deleted", so each was resolved by dropping the HEAD block and re-applying this branch's new content onto the module that now owns it. @siloteemu's merge-conflict finding enumerated exactly this and it is what the resolution follows.
| Was | Now |
|---|---|
rocm-core/src/lib.rs — KFD_DEVICE, preflight_kfd_device, kfd_readable/_with, KFD_OPEN_TIMEOUT, device_readable*, amd_smi_json/_with, MAX_QUOTED_OUTPUT_CHARS, all 8 unit tests |
rocm-core/src/amd_smi.rs (the module main created for resolve_amd_smi_binary), re-exported as rocm_core::amd_smi_json |
rocm-core/src/lib.rs — command_failure_detail_or_else |
stays in lib.rs, beside the command_failure_detail it factors |
apps/rocm/src/main.rs — AMD_SMI_SERVE_PROBE_TIMEOUT, gpu_vram_usage_amd_smi, detect_gpu_count |
apps/rocm/src/gpu_policy.rs. main.rs is now byte-identical to main's |
apps/rocmd/src/common.rs |
main's import list with resolve_amd_smi_binary swapped for amd_smi_json |
All eight original tests were relocated, not dropped, and each was re-run by name on Linux (11 passed in amd_smi::tests, including the three pre-existing resolve_amd_smi_binary* ones).
This also lets one thing collapse that could not before: the failure quote now shares process.rs's MAX_QUOTED_OUTPUT_CHARS (made pub(crate)) instead of declaring a second constant. That constant's own doc named capture_amd_smi_json's quote as the one it did not yet cover (#623); the two converge rather than drift, and process.rs's doc is updated to say so.
Scenario renumbering. main took examine-29 (Windows display driver) and examine-30, so this branch's scenario is now examine-31, and the model_serving.feature note that cites it is updated. I checked every other examine-29 reference in the tree (docs/testing.md:1361, docs/manual-testing.md:353, tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:3244) — all three refer to main's Windows driver scenario and are correct as they stand. main also took serve-26/serve-27; this branch adds no numbered serve scenario, only the trailing note, so nothing collided there.
docs/architecture.md's amd_smi.rs entry said "invoking amd-smi stays with its callers", which is no longer true — updated, along with the gpu_policy.rs entry. That addresses the doc-currency item @jussielo-amd and @juhovainio both raised, now that the code has a module of its own rather than sitting in lib.rs.
Verification
The Darwin pre-push hooks cargo-clippy and cargo-test cannot compile this workspace (engines/lemonade reads st_mode as u32; it is u16 on Darwin), so they were skipped on the push — SKIP=cargo-clippy,cargo-test, nothing else, no --no-verify. Everything else ran and passed locally: verify-commits (both merge commits are GPG-signed and DCO signed-off), license-headers, cargo fmt, cargo clippy (e2e harness + steps), YAML/whitespace/shellcheck.
The two skipped checks were run first on the MI300X Linux dev box, against main (11a60691) with this branch's net diff applied, capturing real exit codes:
cargo build --workspace --all-targets # exit 0
cargo clippy --workspace --all-targets -- -D warnings # exit 0
cargo clippy -p e2e-cucumber --test e2e -- -D warnings # exit 0
cargo clippy --workspace --all-targets --features rocm/e2e-test-hooks \
-- -D warnings # exit 0
cargo test --workspace --all-targets --exclude e2e-cucumber # exit 0, 0 failed
cargo test -p rocm-core --lib --features e2e-test-hooks -- amd_smi:: # 11 passed, 0 failed
cargo fmt --all --check # clean
cargo xtask check-crate-edges # clean
Not claimed: that box has no amd-smi and no GPU telemetry despite having KFD nodes, so nothing here attests real-device behaviour. cargo xtask e2e was not re-run (it drives real vLLM serves); examine-31 is unchanged in substance from its last green run, only renumbered.
Still open, deliberately
@jussielo-amd — your blocking item stands and I am not claiming otherwise: rocm-dash-collectors' device_accessible is still a single unbounded open(). It gates the spawn, but it is not bounded the way device_readable now is. Closing it needs either a reviewed crate_edges allowlist entry or a dependency-free leaf crate hosting just the bounded-open primitive — a layering decision rather than a code one, which is why it is not widened into this PR. The PR description now states that asymmetry plainly instead of implying the two gates are equally hardened, which was the narrower half of your finding and @siloteemu's B2. I have left your review in place rather than dismissing it.
@siloteemu — of your blocking set, the merge conflict and the examine-29 collision are fixed here. The host_path-routing / WSL-conjunction mutation finding and the seam test's non-feature-build claim are not; they are real and I would rather address them in their own round than bundle them into a merge resolution. Review left standing.
`rocm-dash-collectors` runs its own pre-flight before spawning `amd-smi` for the dashboard's GPU metrics, on the same `/dev/kfd` and for the same reason as the CLI launch sites. It used a plain `OpenOptions::open(..).is_ok()`, so it stayed the one launch site that a wedged device could still block indefinitely — the exact hazard the rest of EAI-8457 removes. It now shares `rocm_core::device_readable_bounded`, which carries the 2s bound and the in-flight probe budget with it, instead of restating them. A second copy of that primitive would be free to drift from the gate it belongs to. That needs a new first-party edge, `rocm-dash-collectors -> rocm-core`, so `ALLOWLIST` gains it and the module doc records why. The invariant that forbids this edge binds `rocm-dash-tui`, the presentation layer, which makes no such call and keeps the stricter rule. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
juhovainio
left a comment
There was a problem hiding this comment.
Nothing here blocks the fix itself: the gate stops amd-smi from being spawned, and the examine-31 scenario asserts both the message and the resulting state. I'm requesting changes for one docs gap and two points I'd like addressed or answered.
To fix
docs/testing.mdisn't updated for the newROCM_E2E_KFD_DEVICEseam or scenario examine-31. The repo guidelines ask for that in the same change.
To address or answer
- The gate declines on permission denied and on a missing KFD node, but the rationale is amd-smi hanging in D-state. A failed open with an errno can't cause that, so the gate looks broader than the hazard it describes. On hosts where the user can't open
/dev/kfd,detect_gpu_countis lost and has no sysfs fallback (gpu_vram_usagedoes). Erring safe is fine; please confirm it's intended or narrow the claim (inline comment). - Roughly 600 of the ~1100 added lines in
amd_smi.rsare doc comments, and several narrate review history or cite a commit hash. Describing the invariant and leaving history to the PR/commit would make this much easier to maintain (inline comment).
FYI, no change requested
- The dash collector's
device_accessiblestill does an unboundedopen(); the PR body acknowledges this and defers it. - The
cargo xtask e2eitem in the PR test plan is unchecked, and the MI300X / W7900 / Strix Halo Ubuntu lanes were still pending when I looked. Everything that had completed was green.
| /// `amd-smi` launch goes through, and it is **mandatory** before every launch. | ||
| /// | ||
| /// What it actually detects is **inaccessible**, not **unhealthy**. `kfd_open` | ||
| /// refuses when the node is absent, when permission is denied, when the driver |
There was a problem hiding this comment.
The doc says amd-smi blocks in D-state when the node is absent or permission is denied. Those states fail the open with an errno, so they can't park amd-smi in uninterruptible sleep. The gate therefore over-declines: users outside the render group lose detect_gpu_count, which has no sysfs fallback. Could you narrow the claim or the gate, or confirm this is intended?
There was a problem hiding this comment.
Good catch, and you are right that the rationale did not match the gate. A failed open that returns an errno cannot park anything in D-state, so describing all four decline reasons as "the states in which amd-smi blocks in uninterruptible sleep" was simply wrong.
Decision: keep the fail-safe gate, narrow the claim. Narrowing the gate turns out to be the worse of the two options. To decline only on a timeout, the gate would have to treat every errno it did not anticipate as safe to launch into — and the errno a driver returns mid-reset is exactly the thing no test in this repo can pin, because there is no way to manufacture a resetting KFD on any lane we have. An EBUSY/EIO from a device that is about to wedge would then be waved straight through to amd-smi. Not resting on an assumption of that shape is the reason the open is bounded at all (KFD_OPEN_TIMEOUT's doc comment makes that argument), so building an errno taxonomy on top of it would reintroduce the same class of mistake one level up.
On the detect_gpu_count asymmetry — real as you state it, and I went and worked out what it actually costs rather than waving at it. On the hosts where this branch can fire (bare-metal Linux), detect_gpu_count is not what serve resolves ordinals from:
serve_cmd.rscallsrocm_core::usable_amd_gpu_indices(), which counts KFD topology nodes under/sys/class/kfd/kfd/topology/nodesand AMD DRM cards under/sys/class/drm. Both are world-readable and neither needs the device node open, so that set is intact on a host outside the render group.validate_pinned_gpu_indexprefers that set outright over thedetectedcount ("it is authoritative and already in the HIP-ordinal space"), so--gpu <index>validation and the no-usable-GPU fail-fast are unaffected on exactly the host you are describing.
What is genuinely lost is the secondary count --gpu auto ranks with. On a single-GPU host the DRM sysfs VRAM rows still supply it (effective_gpu_count); on a multi-GPU one auto-selection returns no selection and the engine's own device probe pins the first present GPU. That host is also the one rocm diagnose already names, via fix-4-render-group.
I did not give detect_gpu_count a sysfs count fallback of its own: /sys/class/drm counts physical cards while amd-smi list counts HIP-visible devices, and the two can legitimately disagree, so a silent substitute could mis-size tensor parallelism. Given the above makes the loss non-load-bearing, erring safe without inventing a second count source seemed the right trade — but say the word if you would rather have it and I will add it with its own test.
All of this is now in the code rather than only here: kfd_readable has # What the predicate is and # What the broad refusal costs sections saying precisely which class declines and why the broader refusal is deliberate, detect_gpu_count's own doc says it lands here and points at that reasoning, and the error message no longer attributes every decline reason to D-state.
|
|
||
| /// The gate's own probe must not be able to hang. | ||
| /// | ||
| /// `kfd_readable` was reviewed as resting on an unverified assumption: that |
There was a problem hiding this comment.
This comment narrates review history ("was reviewed as resting on..."), and the kfd_readable docs cite a commit hash. Could you describe the invariant instead? The history belongs in the PR or commit message.
There was a problem hiding this comment.
Fair, and the commit hash was worse than noise: b4f98bc2 does not exist in this repository at all. @jussielo-amd caught the same thing in his October review and traced the real commit — d37cfbab, PR #375 / EAI-8450. Both citations are gone. The surviving text states the behavioural reason instead (a WSL host whose ROCDXG plumbing is not ready still hangs, so a bare identity check would wave it through) without naming any commit.
Pruned the rest of the narration too:
device_readable_within_budget's "both of those used to fall back to a direct blockingopen…" archaeology, restated as the invariant: both branches decline, and here is the escalation that makes a fallback wrong.the_kfd_probe_declines_a_device_whose_open_blocks_instead_of_hanging's "kfd_readablewas reviewed as resting on an unverified assumption…".the_kfd_probe_budget_declines_…'s "the escalation this pins is the one the review named".- The dash collector's "this was a plain
OpenOptions::open(..).is_ok()…". gpu_policy.rs's "both figures used to be quoted as 10s and 20s, which omitted the pre-flight".
Net about 100 lines of narration out, which is also why amd_smi.rs's added-comment count drops despite the errno/scope answer in the sibling thread adding roughly 30 lines of genuinely new reasoning.
What I deliberately kept is the reasoning a future maintainer cannot reconstruct from the code:
- why both failure branches decline rather than falling back to a blocking open (the thread budget is exhausted by precisely the device the gate refuses, so the fallback fires when it is most dangerous);
- why the probe budget exists at all;
- why the WSL and off-Linux wave-throughs are load-bearing rather than stubs (reading an absent
/dev/kfdas "unsafe" disables GPU detection on every Windows host); - why the off-Linux
kfd_readablehas to beconst fn— the Windows lane's-D warningsmakesclippy::missing_const_for_fna hard compile error there while the Linux lane never sees it, which is not guessable from the Linux side; - the "what this would catch" note on each test, since several are non-obvious (the FIFO fixture, the watched-thread deadline, the marker file).
Happy to cut further if any of those still read as history rather than invariant.
|
@jussielo-amd — the dash-collector gap is closed. Pushed as
That needed a new first-party edge, Your two non-blocking notes are also handled: the On the @siloteemu — your blocking items from VerificationLinux MI300X box, exact merged tree applied onto
One thing the Linux box could not catch and the pre-push hook did: the first version of Still not reproducible here: real |
The blocking dash-collector gap is closed in 741852a: crates/rocm-dash-collectors/src/amd_smi.rs now calls the shared rocm_core::device_readable_bounded instead of a bare open(), with the new rocm-dash-collectors -> rocm-core edge recorded in xtask/src/crate_edges.rs. Both non-blocking notes are also addressed (latency figures corrected; the spawn-failure fallback removed). Re-requesting your review.
…te's real scope docs/testing.md had no entry for the `ROCM_E2E_KFD_DEVICE` seam or scenario examine-31, which the repo guidelines ask for in the same change. The gate declines on a failed `open` as well as on one that never returns, and the first class cannot cause the D-state hang the rationale described. Keep the fail-safe behaviour -- narrowing it would mean deciding per errno which kernel states are safe to launch into, an unverifiable list of exactly the kind the bounded open exists to avoid resting on -- and say so, including what the broader refusal costs `detect_gpu_count` and what answers in its place. Prune the doc comments to the invariants: drop the review-round narration, the "this used to be X" archaeology and the two citations of a commit hash that does not exist in this repository. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
@juhovainio thanks — all three addressed in
|
| check | exit |
|---|---|
cargo build --workspace --all-targets |
0 |
cargo clippy --workspace --all-targets -- -D warnings |
0 |
cargo clippy -p e2e-cucumber --test e2e -- -D warnings |
0 |
cargo clippy --workspace --all-targets --features rocm/e2e-test-hooks -- -D warnings |
0 |
cargo test --workspace --all-targets --exclude e2e-cucumber |
0, 0 failed |
cargo test -p rocm-core --lib --features e2e-test-hooks -- amd_smi:: |
0, 11 passed |
cargo fmt --all -- --check |
0 |
cargo xtask check-crate-edges |
0 |
cargo xtask e2e -- -n 'examine-31 ' |
0, 1 scenario / 3 steps passed |
And cargo check -p rocm-core --all-targets on macOS, with and without e2e-test-hooks — exit 0 both ways, so the cfg(not(target_os = "linux")) branch still builds (fs is Linux-gated in this module, and that has bitten before).
The clippy and test runs above are what the push hook would have run; it was skipped only for those two, since neither can compile on Darwin. Not verified: the box has no amd-smi, no rocm-smi and no GPU telemetry, so none of this is real-device verification of the gate against an actual wedged /dev/kfd — that remains untestable on any lane we have, which is itself part of the argument for the fail-safe branch above.
|
@jussielo-amd — closing the loop on your 19 Oct review, since it was not fully superseded by your 28 Oct one. Point by point: B1, the fabricated commit-hash citations. You were right and this was still live until now: B2, the overstated parity between the two pre-flights. Closed in B3, PR description contradicting the diff. Rewritten. The seam and scenario sections now match the branch, and this round also rewrote the "why the fourth site is left alone" section, which B4, no test exercises the NB1, no catalog entry for "no KFD GPU node" or "reset in progress". Confirmed and not addressed here. NB2, stale Dismissing on that basis — shout if B4 or NB1 should block rather than become follow-ups. |
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · 741852a
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Review — needs work
Full review of the whole change (base 11a60691...head 741852a). Implements the fix described in EAI-8457 (Jira alias of ROCMAI-342, status In Progress): all four production amd-smi launch sites now run the /dev/kfd pre-flight, matching the ticket's own four-row completeness table.
Blocking
-
docs/architecture.md:101contradicts this same PR's own code change —docs/architecture.md:101,crates/rocm-dash-collectors/Cargo.toml(+1),xtask/src/crate_edges.rs:17-21,66
The doc line added by this PR reads "therocm dashcollectors keep their own pre-flight, since those crates deliberately do not depend on this one" — but this PR's ownxtask/src/crate_edges.rsadds("rocm-dash-collectors", "rocm-core")toALLOWLISTwith a comment explainingrocm-dash-collectorsdoes now depend onrocm-core(EAI-8457), andCargo.toml/Cargo.lockadd that exact dependency.git log --first-parentconfirms the doc line was written in an earlier commit on this branch, before the final commit (741852a8) added the dependency, and the final commit never updated it. This is a direct, traceable self-contradiction within the reviewed diff, and squarely the pattern AGENTS.md §5 calls out ("the same behavior claim... repeats across... docs/*.md... grep for the claim's wording... and check each against the actual code path").
Confidence 90 · mechanical · Fix: reword the sentence to describe the actual relationship (sharesdevice_readable_boundedvia an allowlisted edge, no longer fully independent). Note: this is already fixed in a later commit on the PR (472efacc, not part of the reviewed diff) — flagged here because it is present and wrong at the reviewed head. -
device_readable_bounded's non-Linux branch is new, completely untested code —crates/rocm-core/src/amd_smi.rs:419-429pub fn device_readable_bounded(device: &Path) -> bool { #[cfg(target_os = "linux")] { device_readable(device) } #[cfg(not(target_os = "linux"))] { std::fs::OpenOptions::new().read(true).open(device).is_ok() } }
git grep -n "device_readable_bounded"across the repo turns up only the definition, thelib.rsre-export, and the one call site incrates/rocm-dash-collectors/src/amd_smi.rs:208— no test anywhere calls it, on either platform. The Linux branch is incidentally covered because it forwards to the already-testeddevice_readable, but the#[cfg(not(target_os = "linux"))]branch is new, unbounded code with zero coverage, and this isn't impractical to test:.github/workflows/ci.ymlhas awindows-latestjob (lines 581, 1290). This is exactly the case AGENTS.md's "platform-gated code gets a test that runs on its gate" rule targets, and behaviour this change introduces that no test exercises is blocking per this repo's own test-coverage standard.
Confidence 85 · logic · Fix: add a#[cfg(not(target_os = "linux"))]unit test (readable file →true, nonexistent path →false), mirroring the existing off-Linuxkfd_readablewave-through test.
Non-blocking
-
AmdSmiCollector::runspawns atokio::process::Commandwithoutkill_on_drop(true), under atokio::time::timeout—crates/rocm-dash-collectors/src/amd_smi.rs:148-149(run), called fromrun_json'stimeout(RUN_TIMEOUT, self.run(args))
This repo's own AGENTS.md states: "atokio::processchild awaited under atimeoutsetskill_on_drop(true), so a timeout that fires does not leak it" — and a sibling file,crates/rocm-dash-tui/src/jobs.rs:78, does set it for exactly this reason. A slow-but-not-wedgedamd-smithat exceedsRUN_TIMEOUT(10s) leaks a child on every such poll. This is pre-existing (unchanged by this diff, which only touches this file'sdevice_accessible) and the blast radius is not enlarged by this PR's change — the new bounded device-open probe reduces exposure to the D-state hang this PR targets, it doesn't affect this orthogonal slow-process case — so it is not blocking, but it is squarely on this PR's own subprocess-safety theme and in a file this PR otherwise touches.
Confidence 85 · logic · Fix: add.kill_on_drop(true)to theCommandinrun. -
The WSL wave-through branch of
kfd_readable(is_wsl_host() && has_usable_amd_gpu()) is exercised by no E2E scenario, only by unit tests —crates/rocm-core/src/amd_smi.rs:203-213
No WSL CI lane runsexamine-31or any other new scenario against this branch; coverage is real but confined tocargo test. Non-blocking since the branch is unit-tested and genuinely hard to reach from the e2e harness without real WSL infrastructure.
Confidence 70 · architectural · Fix: none required; consider noting the gap indocs/testing.mdif not already present.
Decisions for the author
tests/e2e-cucumber/tests/e2e/serving_steps.rs:310'svram_mib()spawnsamd-smidirectly, with no/dev/kfdpre-flight and no timeout — answered-by-intent
EAI-8457/ROCMAI-342's own ticket text explicitly scopes this out: "Two further launch sites exist in test code only —tests/e2e-cucumber/tests/e2e/serving_steps.rs:306and:321— and are excluded from the table." Confirmed this is a test-harness helper (probing real VRAM on hardware-backed E2E runs), not production code. Would otherwise have been a non-blocking completeness finding; the ticket already settles that it's intentionally out of scope.
Positive signals
ProbeBudget::claim(crates/rocm-core/src/amd_smi.rs) usesfetch_updaterather thanfetch_add+compensating decrement specifically to avoid the momentary over-the-limit state two racing callers could otherwise observe — correct, and the reasoning is inline.ProbeSlotis moved into the spawned probe thread's closure so the budget slot releases only when the blockingopen()actually returns (or the thread unwinds), verified safe under panic since no workspace crate overridespanic = "abort".- The blocking-open tests use a real
mkfifo'd FIFO with no writer (a genuine blockingopen), watched from a second thread with its own looser watchdog deadline, so a dropped production timeout fails the suite with a message instead of hanging it. examine-31's stub-plus-marker-file design distinguishes "declined before spawn" from "spawned and failed" non-vacuously — stronger than a naive "amd-smi unavailable" assertion.- Differentiated per-caller timeouts (10s for
serve's probe, 2s forrocmd's watcher poll) are justified by real latency-budget asymmetry documented at each constant, not arbitrary.
Deployment notes
- New first-party crate dependency edge
rocm-dash-collectors -> rocm-core, allowlisted inxtask/src/crate_edges.rsand reflected inCargo.lock. KFD_OPEN_TIMEOUT(2s) is now additive to everyamd_smi_jsoncaller's declared timeout; the PR's own description documents the resulting worst case (rocm serve --gpu autorises to ~24s from the prior unbounded.output()call), which replaces an unbounded hang rather than a faster bound, but is worth knowing for anyone with latency expectations aroundservestartup.
What this covered
Full diff 11a606912ed37887fb527251ea8d037fe3af9d25…741852a8dac7223595ebf80ea8b862fe1ee3569b (15 files, +1288/−88), merge-base-verified against the stated base. Three independent fan-out workers each read their file group completely at the reviewed head: (1) core gate — crates/rocm-core/src/amd_smi.rs, lib.rs, hardware_root.rs, process.rs in full, examine.rs's diff hunk plus its enclosing function (its ~8-line diff sits in a 6769-line file, scoped deliberately rather than read whole); (2) call sites — apps/rocm/src/gpu_policy.rs, apps/rocmd/src/common.rs, crates/rocm-dash-collectors/src/amd_smi.rs, Cargo.toml/Cargo.lock, xtask/src/crate_edges.rs, docs/architecture.md, plus a repo-wide Command::new/amd_smi_json sweep confirming every production launch site is gated; (3) e2e — tests/e2e-cucumber/features/examine.feature, model_serving.feature, examine_steps.rs. The driver independently read the core gate diff directly as well, and verified the two blocking findings by direct git grep/git log cross-reference rather than taking a worker's word alone. CI checked live at the reviewed head: 22 success, 3 skipped, 0 failing/pending. Ticket EAI-8457 (→ ROCMAI-342, status In Progress) and the PR description were both read at Step 9, after findings were settled; no mismatch found between the description's claims and the diff.
Did not run: no Rust toolchain was available in this environment, so cargo test/clippy/xtask coverage could not be executed directly; relied on live CI's green result plus direct code reading instead. History pass performed only for crates/rocm-core/src/amd_smi.rs (confirms an earlier-raised "does the timeout path reap its child" concern was already fixed earlier in this branch's own history, before this diff's base). Prior-changes pass not performed — this PR's own review-comment history was deliberately not read, per this method's independence rule. Not reconciled against any GitHub discussion on this PR. One finding above (the docs/architecture.md contradiction) is already fixed in a later commit on this PR (472efacc) that is outside the reviewed diff range; noted for transparency, it does not change the verdict for the reviewed commit.
🤖 Generated with Claude Code
juhovainio
left a comment
There was a problem hiding this comment.
I read the whole diff. The core change looks right to me: one guarded amd_smi_json chokepoint, the spawn is provably prevented in the gate test (marker file plus an allowed-path control), serve gains a bound it didn't have, and the decline note is asserted together with the absence of the spawn in examine-31. CI is green on everything that ran, and the scope matches EAI-8457. I did not build or run anything locally, so the mutation-check and MI300X claims in the PR body are unverified on my side.
Requesting changes for a few things worth fixing before merge (details inline):
- The dashboard collector's
device_accessiblenow callsdevice_readable_bounded, which parks the caller for up to 2s (twice under a simulated root) and is reached from an async fn. That is better than the old unbounded open, but it still blocks a tokio worker; please wrap it inspawn_blockingor document the stall. - The new timeout/budget tests return
Ok(())silently whenmkfifois missing (and on WSL), so they can turn into green no-ops. Please emit a skip notice or assertmkfifoexists on the CI lane. command_failure_detail_or_elseispubbut only used inside rocm-core;pub(crate)keeps the public surface from growing.
Smaller notes, no action required if you disagree:
- On a bare-metal Linux host outside the render group,
detect_gpu_countnow returns None with no sysfs fallback, so--gpu automakes no selection on multi-GPU hosts. This is documented and the gpu_policy unit tests cover the None paths, but there is no Gherkin scenario for the serve side; worth saying so explicitly in the PR text. docs/manual-testing.mdis not touched although serve/rocmd probe behaviour changed, and some of the new doc comments are very long.- Pre-existing and not from this PR: the collector's
run()awaitsCommand::output()under a timeout withoutkill_on_drop(true).
The latency ceiling (~24s on serve), the permanent probe-budget exhaustion while threads stay parked, and the new rocm-dash-collectors -> rocm-core edge all look like deliberate, documented tradeoffs; I'm not asking for changes there.
| /// it, so this site cannot drift away from the gate it is part of. | ||
| fn device_accessible(path: &Path) -> bool { | ||
| std::fs::OpenOptions::new().read(true).open(path).is_ok() | ||
| rocm_core::device_readable_bounded(path) |
There was a problem hiding this comment.
device_readable_bounded blocks the calling thread (up to 2s, and twice under a simulated root), and this is reached from async fn detect_with_binary_inner via preflight_passes, so it parks a tokio worker. It's better than the old unbounded open, but AGENTS.md says blocking work belongs under spawn_blocking. Consider wrapping the pre-flight in spawn_blocking, or noting the 2s stall here.
| let temp_root = | ||
| std::env::temp_dir().join(format!("rocm-cli-kfd-open-{}", unix_time_millis())); | ||
| fs::create_dir_all(&temp_root)?; | ||
| let Some(fifo) = make_fifo(&temp_root, "blocking-kfd") else { |
There was a problem hiding this comment.
When mkfifo is missing this test returns Ok(()) silently, so the timeout test becomes a no-op with a green result. The budget test (make_fifo at the second call site) and the is_wsl_host() early returns do the same. Print a skip notice, or assert mkfifo exists on the CI lane, so a skipped check is visible.
| /// diagnostic left — and inlining the two `is_empty` checks there to get it put | ||
| /// the same rule in two places. `fallback` is a closure because formatting an | ||
| /// `ExitStatus` is wasted work on the overwhelmingly common non-blank path. | ||
| pub fn command_failure_detail_or_else( |
There was a problem hiding this comment.
command_failure_detail_or_else is pub but only amd_smi_json inside rocm-core uses it. pub(crate) would avoid widening the crate's public surface for an internal helper.
ROCMAI-756 moved the rocmd bridge snapshot out of `apps/rocmd/src/common.rs` into the new `apps/rocmd/src/host_snapshot.rs`. The conflict hunks therefore had an empty `origin/main` side: the code moved, it was not deleted. Resolved by taking main's `common.rs` verbatim and re-applying this branch's three edits in the module main moved them to -- the `amd_smi_json` chokepoint in `capture_amd_smi_json`, the probe-timeout doc that states the additive 2s pre-flight bound, and the two note comments naming the pre-flight as the one failure that arrives without `run_with_timeout`'s context. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Review round 3 on #447. - `AmdSmiCollector::new` reached `device_accessible` -> the bounded open from an `async fn`, parking a tokio worker for up to the 2s `KFD_OPEN_TIMEOUT` per probe — ~4s under a simulated host root, where there are two. The pre-flight now runs through `preflight_passes_off_thread`, which short-circuits the two non-device branches before the thread hop and puts the rest on `spawn_blocking`. A `JoinError` reads as "not readable", matching `device_readable_within_budget`: every branch that cannot get an answer declines rather than opening on the caller's thread. - The timeout and budget tests returned `Ok(())` when `mkfifo(1)` was missing, and on WSL, which is indistinguishable from a pass for a test that asserted nothing. Both now announce the skip through a shared `skip_notice`. - `command_failure_detail_or_else` is `pub(crate)`; its only caller outside its module is `amd_smi_json`, in this same crate. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
@juhovainio — all three fixed, plus another 1. Blocking a tokio worker. Real bug, and mine — the dash-collector change introduced it. Fixed rather than documented. The pre-flight now goes through Deliberately no 2. Tests that could turn into green no-ops. You were right that this was a silent pass. Both the missing- 3. On your smaller notes:
VerificationLinux MI300X box, merged tree applied onto
Locally on macOS, because this round touches
Unverified here, as before: no |
All three addressed in 57071f6: the dash-collector pre-flight now runs via preflight_passes_off_thread (spawn_blocking, with the two non-device branches short-circuiting before the thread hop) so no tokio worker is parked; the mkfifo and WSL skips now announce themselves through a shared skip_notice instead of returning a silent Ok(()); and command_failure_detail_or_else is pub(crate). The --gpu auto render-group scenario gap is now stated in the PR body. Re-requesting your review.
juhovainio
left a comment
There was a problem hiding this comment.
Re-review at 57071f6. The approach holds up: both probe failure branches decline, the probe slot moves into the probe thread, the budget test uses a readable file so only a refusing implementation passes, and examine-31 asserts message, device and marker absence together.
Blocking
hardware_root.rsnow says the dash crates "deliberately do not depend on" rocm-core, but this PR adds therocm-dash-collectors->rocm-coreedge (Cargo.toml and thecrate_edges.rsallowlist). The doc is false as written. Please reword it, and grep the other docs that repeat the claim.- The PR body omits the dash-collector
spawn_blockinghop (preflight_passes_off_thread, commit 57071f6) and its two tests. Please update the body from the final diff, rerun the full gate at this head, and confirm the dash tests fail when the target is broken (for example, inliningpreflight_passesshould make the ticker test record 0 ticks).
Non-blocking
- Nothing fails if the two
gpu_policy.rsprobes revert to a raw spawn, which is the regression this PR exists to prevent. A structural guard (for example,resolve_amd_smi_binaryused only inside rocm-core'samd_smi.rs) would pin it. - Declines on the serve path are silent (
.ok()?), including a false decline if the 2s open bound trips on a slow but healthy host. A debug log of the discarded error would help. make_fifois duplicated across two crates, and the latency figures (12s/24s/4s) are repeated across several docs and the PR body, so they will drift.- rocmd now passes the label "amd-smi" to
run_with_timeoutinstead of "process"; worth a mention if intentional.
For the author to decide / state
- The gate only proves
open(/dev/kfd)returns, while the docs attribute the D-state to amd-smi's later ioctls. No real unhealthy-driver run was done, so please say that this is a mitigation heuristic. - The decline is broader than the ticket: an absent
/dev/kfdand EACCES also decline (the ticket says it does not apply with no /dev/kfd), anddetect_gpu_counthas no sysfs fallback.
The PR is also currently conflicting with main and needs a rebase.
| //! | ||
| //! - the `rocm dash` collector's, where `apps/rocm/src/dash.rs` passes the | ||
| //! re-rooted path down to the dash daemon — those crates deliberately do not | ||
| //! depend on this one, so the caller resolves it; |
There was a problem hiding this comment.
This now says the dash crates "deliberately do not depend on this one", but this PR adds rocm-core to rocm-dash-collectors (Cargo.toml and the crate_edges.rs allowlist). Reword to say the collector shares device_readable_bounded but still receives the re-rooted path from the caller, and grep the other docs repeating this claim.
| return None; | ||
| } | ||
| let value: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?; | ||
| let value = rocm_core::amd_smi_json(&["metric", "--json"], AMD_SMI_SERVE_PROBE_TIMEOUT).ok()?; |
There was a problem hiding this comment.
Nothing in the test suite fails if this reverts to a raw Command::new(resolve_amd_smi_binary()), which is the regression this PR exists to prevent, and .ok()? drops the decline silently. Consider a structural guard (e.g. resolve_amd_smi_binary used only inside rocm-core's amd_smi.rs) and/or a debug log of the error.
| return None; | ||
| } | ||
| let value: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?; | ||
| let value = rocm_core::amd_smi_json(&["list", "--json"], AMD_SMI_SERVE_PROBE_TIMEOUT).ok()?; |
There was a problem hiding this comment.
Same as the other probe: a decline here, including a false decline when the 2s open bound trips on a slow but healthy host, is silent. A debug log of the discarded error would make "why did --gpu auto not rank GPUs" diagnosable.
| } | ||
| // Both error arms — timeout and a probe thread that died without sending — | ||
| // mean "no readable answer", which the gate must read as not readable. | ||
| receiver.recv_timeout(timeout).unwrap_or(false) |
There was a problem hiding this comment.
The pre-flight only proves open(/dev/kfd) returns; the docs say the D-state comes from amd-smi's later ioctls. Please state in the PR that no real unhealthy-driver run was done and that this gate is a mitigation heuristic.
| /// A `JoinError` reads as "not readable", matching | ||
| /// `device_readable_within_budget`: every branch that fails to get an answer | ||
| /// declines rather than falling back to a blocking open on the caller's thread. | ||
| async fn preflight_passes_off_thread( |
There was a problem hiding this comment.
The spawn_blocking hop and the two new tests are not in the PR description. Please update the body, and say whether the ticker test was shown to fail with the hop removed (inlining preflight_passes should yield 0 ticks).
This PR added `rocm-dash-collectors -> rocm-core` so the dashboard's `amd-smi` launch could share the bounded `/dev/kfd` probe, which made six doc comments false as written. The invariant that survives binds only the presentation layer: `rocm-dash-tui` has no edge to `rocm-core` and `crate_edges.rs` still forbids it one. The collectors crate does have one, and says what for. `runner.rs` and the collector's own docs additionally explained the threading of `gpu_reachable`/`host_kfd_device` by the missing dependency, which is no longer the reason: the verdict is threaded through because `serve`, `examine` and the dashboard must not reach it independently, not because the collector could not compute it. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…ight The pre-flight protects nothing unless every `amd-smi` launch goes through it, and nothing in the tree failed when one stopped: reverting either `gpu_policy.rs` probe to `Command::new(resolve_amd_smi_binary())` left the whole suite green, because the regression is the absence of a call and no behavioural test can observe that. So guard the ingredient. The managed SDK ships `amd-smi` inside the runtime wheel's bin directory rather than on `PATH`, so `resolve_amd_smi_binary` is the only way to find a binary to launch; a static contract test now fails any file outside a two-entry, justified allowlist that calls it. Bidirectional like the crate-edge allowlist, so an exemption cannot outlive its reason. Verified by mutation: a raw spawn in `detect_gpu_count` fails `only_preflighted_sites_resolve_the_amd_smi_binary` by name. It is a source-text scan, not a call-graph analysis, and the module doc says so rather than overclaiming. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…tencies Both serve-path probes return `Option`, so a decline had nowhere to go and was dropped with `.ok()?`. That silenced the two cases a user wants: an `amd-smi` that ran and failed, and a false decline when the 2s open bound trips on a slow but healthy host. `amd_smi_probe_json` now logs the full error chain at `debug` -- not `warn`, because on a GPU-less host it is the expected answer on every `serve` -- so "why did `--gpu auto` not rank my GPUs?" is answerable from `RUST_LOG=debug`. The gate's 2s bound and 8-slot budget are restated as literal numbers in five places, two of them as derived figures (caller timeout + 2s), none of which can see the constant. A tripwire test now fails on a change to either and names every restatement to update. Also state plainly, in `kfd_readable`'s docs and `docs/testing.md`, what the gate is not: it proves only that `open(/dev/kfd)` returned, while the hang is attributed to amd-smi's later ioctls, and no unhealthy-driver run backs it up. It is a mitigation heuristic. And the decline is broader than the ticket -- an absent node and EACCES decline too, and `detect_gpu_count` has no sysfs fallback. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
main moved the shell/fs probe helpers out of `examine.rs` into `probe_exec.rs`, so the conflict hunk had an empty main side — the code moved rather than went away. This branch's only contribution there is the `truncate_to_chars` doc note recording that `amd_smi` is now a third caller of the shared marker and budget; it moves to `probe_exec.rs` with the function. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Both blocking items addressed in 116cf18: the hardware_root.rs claim (and its copies in architecture.md and the dash module docs) now says the invariant binds rocm-dash-tui, not the collectors crate; and the PR body is rewritten from the final diff including the spawn_blocking hop. Also implemented your structural guard so a launch site that skips the pre-flight fails the build, logged the silent serve-path declines, and pinned the latency figures in one place. The mitigation-heuristic caveat and the broader-than-the-ticket decline are both stated in the PR body. Re-requesting your review.
|
Both blocking items done, plus the structural guard. Pushed as 1. The 2. PR body and falsification. Body rewritten from the final diff, including the Structural guard (your strongest non-blocking point) — implemented. You were right that nothing failed if the two Silent declines on the serve path — also done: the discarded error is now logged, so a false decline from the 2s bound tripping on a slow-but-healthy host is visible rather than indistinguishable from "no GPU". Latency figures — pinned in one place rather than restated across several docs and the PR body, which is what would have let them drift. The two things you asked me to state
VerificationLinux MI300X box, merged tree applied onto
The The merge you noted was already done before this round; this push merges |
juhovainio
left a comment
There was a problem hiding this comment.
Requesting changes on one item: the PR description doesn't cover part of the diff. The code itself looks sound. The pre-flight is bounded and budget-capped, both failure branches decline instead of falling back to a blocking open, and the FIFO-based tests make the blocking open real, with a control case.
Blocking
- The description never mentions the new xtask static contract test (
xtask/src/amd_smi_launch_contract.rs, wired in viaxtask/src/main.rs). It also doesn't say that the dash collector's pre-flight now runs underspawn_blocking, with a ~2s timing test. Both are user-relevant parts of the change; please add them to the body.
Non-blocking
docs/testing.mdand thekfd_readabledoc disagree about what--gpu autodoes when the gate declines (inline comments). Please reconcile with the code and with thedetect_gpu_countdoc ingpu_policy.rs.the_device_preflight_does_not_run_on_the_async_workeruses the realKFD_PROBE_BUDGETand parks one slot for the lifetime of the test binary. The rocm-core tests use private budgets for this reason. It's harmless today but becomes order-dependent if a second FIFO test lands there.- The contract test's
strip_commenttruncates at the first//even inside a string, so a call after a URL literal would be missed. It's documented as fail-open, so I'm just noting it.
Please confirm these are deliberate
- The gate declines on any unopenable
/dev/kfd, not only on a hang. Hosts with no/dev/kfd(containers) or users outside the render group losedetect_gpu_counteven though amd-smi might have answered. This is documented, but it is wider than the ticket's "not applicable when/dev/kfdis absent". - The new
rocm-dash-collectors->rocm-coreedge reverses the earlier "deliberately no dependency" stance and pulls rocm-core's dependency tree into the collectors crate. - Worst-case
serve --gpu autolatency rises to about 24s. It was unbounded before, so this is an improvement.
Nothing here was run against a real unhealthy driver, which the PR says openly. CI is green on everything that had completed when I looked.
| `/dev/kfd`) and `EACCES` for a user outside the `render`/`video` groups. | ||
| `gpu_vram_usage` still falls back to the DRM sysfs counters on such a host, but | ||
| `detect_gpu_count` has **no** sysfs fallback and returns `None`, so `--gpu auto` | ||
| there ranks from the sysfs VRAM rows alone. `kfd_readable`'s doc comment carries |
There was a problem hiding this comment.
This says --gpu auto "ranks from the sysfs VRAM rows alone" on a declined host, but the kfd_readable doc in crates/rocm-core/src/amd_smi.rs says auto-selection returns no selection and the engine's own probe picks. Please check against select_auto_gpu_index and make both (and the detect_gpu_count doc in gpu_policy.rs) say the same thing.
| /// world-readable, neither needing the device node open — and | ||
| /// `validate_pinned_gpu_index` prefers that set outright. What is lost is the | ||
| /// secondary count `--gpu auto` ranks with, so on a *multi*-GPU host in that | ||
| /// state auto-selection returns no selection and the engine's own device probe |
There was a problem hiding this comment.
Counterpart of the docs/testing.md wording: "auto-selection returns no selection and the engine's own device probe picks". Reconcile with docs/testing.md; whichever one matches the code wins.
Summary
amd-smilaunched against a kernel fusion driver in a bad state blocks in uninterruptible kernel sleep. No signal escapes that state, sorocm servehangs and survives both Ctrl-C andSIGKILL; a timeout does not help, because killing a D-state child is a no-op. The repo documents this and calls the/dev/kfdpre-flight mandatory — but three of the four production launch sites skipped it:rocm-dash-collectorscollectorrunrocmdcapture_amd_smi_jsonrocmgpu_vram_usage_amd_smirocmdetect_gpu_countThe two unguarded, untimed ones sit on the normal
rocm servepath for any run that is not CPU-only, sorocm serve --gpu autoreached at least one launch with neither protection.This gives the pre-flight one definition in
rocm-coreand routes the three unguarded sites through a single guarded launcher, so a future launch site inherits the guard rather than having to remember it.What is actually in this diff
crates/rocm-core/src/amd_smi.rs— the gate and the launcher.amd_smi_json(args, timeout)is the one supported way for a synchronous caller to startamd-smi; it runs the pre-flight (KFD_DEVICE,preflight_kfd_device,kfd_readable/kfd_readable_with,device_readable) and only then hands theCommandtorocm_core::process::run_with_timeout. Re-exported asrocm_core::amd_smi_json.apps/rocm/src/gpu_policy.rs—gpu_vram_usage_amd_smianddetect_gpu_countrewritten to call it, with a newAMD_SMI_SERVE_PROBE_TIMEOUT(10s) and its latency rationale.apps/rocmd/src/common.rs—capture_amd_smi_jsonbecomes a one-line call into the same helper with the watcher's own 2s budget.crates/rocm-core/src/lib.rs—command_failure_detail_or_else, so the stderr-then-stdout precedence lives in one place while the amd-smi site supplies its own both-blank fallback (exit status <code>).crates/rocm-core/src/process.rs—MAX_QUOTED_OUTPUT_CHARSbecomespub(crate)so the amd-smi failure quote shares it. That constant's own doc named this quote (Converge the remaining unbounded child-capture helpers onto run_with_timeout #623) as the one it did not yet cover; the two now converge instead of drifting.hardware_root.rs's module contract (two pre-flights, both keeping the real device's veto) anddocs/architecture.md'samd_smi.rs/gpu_policy.rsentries.e2e-test-hookspath seamROCM_E2E_KFD_DEVICEand scenarioexamine-31.What is not in this diff: the pipe-draining / zombie-reaping rewrite of
run_command_with_timeout. That landed separately as #526 (ROCMAI-448) and is already onmain; earlier revisions of this description claimed it, wrongly.The gate is Linux-only, deliberately
/dev/kfdexists on no other platform.amd-smi.exeis a supported Windows binary (managed_sdk_tool_pathresolves it) and there is a livee2e-gpu-strix-windowslane, so a check that merely failed to find the device would have silently disabled GPU detection on every Windows host. Off Linux the pre-flight waves the launch through, and a test pins that.WSL compiles as Linux and has no
/dev/kfdeither, so it needs a runtime wave-through. The condition isis_wsl_host() && has_usable_amd_gpu()— the same conjunctionapps/rocm/src/dash.rs::gpu_reachable_for_preflightfeeds the dashboard — not bare WSL identity, because a WSL host whose ROCDXG plumbing is not ready is one whereamd-smistill hangs.The probe cannot hang, and cannot pile up
The gate's open is not assumed to return promptly. It runs on a probe thread bounded by
KFD_OPEN_TIMEOUT(2s); an open that has not returned by then reads as not readable, so the gate declines rather than inheriting the stall it exists to prevent.A probe that overruns leaves its thread parked in
open, possibly forever. Those threads are capped:MAX_INFLIGHT_KFD_PROBES(8) slots, claimed with afetch_updateand released by a guard moved into the probe thread, so a slot frees when the kernel finally returns rather than when the caller gives up. With the budget spent, a probe answersfalsewithout opening anything.Both of the remaining failure branches — budget exhausted, and thread spawn failed — decline rather than falling back to a direct blocking
openon the caller's thread. The states in which this process runs out of threads are not independent of the state the gate refuses: the way the budget is exhausted here is one parked open per poll against a wedged device, so a fallback would fire exactly when the device is most dangerous, performing the unbounded open the bound exists to prevent, on a thread with nothing left to kill. A refusal is recoverable and visible; a hang is not.Latency
KFD_OPEN_TIMEOUTis additive to each call'stimeout, not inside it:rocm serve --gpu auto--modelreadiness (host_accelerator_memory)rocmdtelemetry pollEarlier revisions quoted 10s and ~20s, which omitted the pre-flight. The two bounds are rarely both paid — a host whose open hangs is a host that then declines and never spends the
amd-smibudget — but a ceiling that assumes the good case is not a ceiling. Before this change bothserveprobes called.output()with no timeout at all, so ~24s is the ceiling replacing "forever".They are deliberately not parallelised:
detect_gpu_countruns insideselect_gpu_indices_under_launch_lockprecisely so the cpu-only and pinned paths never pay for the subprocess, and hoisting it out would make everyrocm serve --cpu-onlyspawn anamd-smiit has no use for.Coverage of the user-observable change (AGENTS.md §3)
On a Linux host that cannot open
/dev/kfd, the probes now decline to launch where before they attempted the spawn; GPU auto-selection degrades to service state alone, which is what it already does whenever amd-smi is unavailable.examine-31(@id:examine-telemetry-declines-an-unopenable-kfd-device) covers this end to end onrocmd's bridge snapshot — the one surface that renders the decline as JSON in a single command. It is non-vacuous twice over: the stubamd-smiwould have answered the probe perfectly and records that it ran, so the marker's absence is what distinguishes "refused before the spawn" from "spawned and failed"; and the note must name the device this run pointed the pre-flight at, so a build that ignored the seam would decline naming/dev/kfdand fail rather than passing on the runner's own missing device.Reaching either branch needs the
e2e-test-hooksseamROCM_E2E_KFD_DEVICE, which moves the device the gate opens rather than skipping the gate.ROCM_CLI_TEST_HOST_ROOTcannot substitute: the gate keeps the real node's veto on purpose, so a simulated root can make it decline but never allow.Still uncovered by a scenario:
rocm serve's and--model's own call sites. Both discard the error with.ok()?by design, so on a GPU-less lane the declined branch is indistinguishable from having no GPU. Named here rather than left silent, and also recorded as a note at the end oftests/e2e-cucumber/features/model_serving.feature.The fourth site: the dash collector
Closed in
741852a8(an earlier draft of this section argued for leaving it; that reasoning no longer applies). The collector keeps its own pre-flight decision (preflight_passes) because it gates an async launch and the syncamd_smi_jsonchokepoint does not fit, but itsdevice_accessibleis no longer a bareopen()— it callsrocm_core::device_readable_bounded, the same bounded, budget-capped proberocm-core's own gate uses. The bound, the budget and the reasoning about why both failure branches decline now live in exactly one place, so the one launch site outside the sync chokepoint cannot drift away from them.That needed the
rocm-dash-collectors -> rocm-coreedge thecrate_edgeslayering guard exists to force review on. It is taken deliberately and recorded as an allowlist entry inxtask/src/crate_edges.rs, withdocs/architecture.mdupdated to say so. The alternative raised in review — a dependency-free leaf crate hosting just the bounded-open primitive — would have cost two reviewed edges instead of one for the same sharing.The site is separately guarded as before, too: #375 ("fix(dash): detect amd-smi through WSL DXG", EAI-8450, on
mainasd37cfbab) replaced its barekfd_accessible()withpreflight_passes(...), fed byrocm_core::has_usable_amd_gpu()fromapps/rocm/src/dash.rs.Tests
Nine unit tests in
crates/rocm-core/src/amd_smi.rs, each confirmed non-vacuous by reverting the behaviour it pins:the_kfd_gate_stops_amd_smi_before_it_is_spawned— a fakeamd-smileaves a marker file; the gate must stop the spawn, not report a failure afterwards. The same fixture is re-run against a readable device to prove the fake would otherwise have executed.the_kfd_probe_declines_a_device_whose_open_blocks_instead_of_hanging— a FIFO with no writer really does block inopen; the probe must convert that into a decline inside its bound. Watched from a second thread with a looser deadline, so a dropped bound fails with a message instead of hanging the binary.the_kfd_probe_budget_declines_instead_of_parking_threads_without_bound— new this round. Spends a one-slot budget on a parked FIFO probe, then points the capped probe at an ordinary readable file: an unbounded probe, or one that fell back to a blocking open, answerstrue; only a probe that refuses on an exhausted budget answersfalse. A control with a free slot must still answertrue. Mutation-checked on the MI300X box — restoring the old blocking-open fallback fails it with exactly the escalation message.the_kfd_preflight_waves_through_hosts_that_have_no_kfd(non-Linux) — guards the Windows lane.the_kfd_preflight_waves_through_wsl_hosts_only_when_the_gpu_is_reachable— pins the conjunction, with the unready-WSL case as the discriminator and bare metal as the control.the_kfd_gate_honours_a_simulated_root_but_keeps_the_real_device_veto— a simulated root may hide the device, never invent one.the_public_amd_smi_entry_point_gates_on_the_real_kfd_device— the production wrapper gates onKFD_DEVICEitself, which the injected helpers cannot show.the_e2e_kfd_path_seam_moves_the_gate_without_disabling_it— the seam relocates the probe instead of skipping it.a_failing_amd_smi_cannot_paste_its_whole_output_into_the_error— a 200K-char payload must not reach the error (and from thererocmd's snapshot note and the rewrite-the-whole-file JSONL logs, which is how a 151K-char log line was produced).Test plan
Run on the MI300X Linux dev box against
main+ this branch's net diff; macOS cannot build this workspace (engines/lemonadeusesst_modeasu32).cargo build --workspace --all-targets— exit 0cargo clippy --workspace --all-targets -- -D warnings— exit 0cargo clippy -p e2e-cucumber --test e2e -- -D warnings— exit 0cargo clippy --workspace --all-targets --features rocm/e2e-test-hooks -- -D warnings— exit 0cargo test --workspace --all-targets --exclude e2e-cucumber— exit 0, 0 failedcargo test -p rocm-core --lib --features e2e-test-hooks -- amd_smi::— 11 passed, 0 failedcargo fmt --all --check,cargo xtask check-crate-edgesRe-run on the same box for the docs/scope round (
472efacc), againstmainat11a60691+ this branch's net diff:cargo build --workspace --all-targets— exit 0cargo clippy --workspace --all-targets -- -D warnings— exit 0cargo clippy -p e2e-cucumber --test e2e -- -D warnings— exit 0cargo clippy --workspace --all-targets --features rocm/e2e-test-hooks -- -D warnings— exit 0cargo test --workspace --all-targets --exclude e2e-cucumber— exit 0, 0 failedcargo test -p rocm-core --lib --features e2e-test-hooks -- amd_smi::— 11 passed, 0 failedcargo fmt --all -- --check— exit 0;cargo xtask check-crate-edges— exit 0cargo check -p rocm-core --all-targetson macOS (both with and withoute2e-test-hooks) — exit 0, so thecfg(not(target_os = "linux"))branch still buildscargo xtask e2e -- -n 'examine-31 'amd-smi/rocm-smiand no GPU telemetry on that box, so nothing here is real-device verification.