Repository navigation
fix(serve): run the KFD pre-flight at every amd-smi launch site (EAI-8457) - #447
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>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
All three fixed, plus 1. Windows CI. Fixed. You were right to suspect later errors were hiding behind it. Rather than find them one CI round at a time, I ran clippy across every crate that builds on Darwin ( 2. The description. Fixed, and I should own that this is the second round you have asked for it — last time I reported it done without checking, and it wasn't. It now names 3. Docs saying The
|
Addressed in 1925548: Windows CI green (device_accessible cfg-split, plus clippy run across every Darwin-buildable crate to find anything hiding behind it); the PR description now names host_snapshot.rs and covers the xtask contract guard and the spawn_blocking hop, verified against the live body; and architecture.md/testing.md no longer say debug. Your &&-short-circuit point was a real regression my own extraction introduced — the GPU probe is taken unevaluated now. Re-requesting review.
jussielo-amd
left a comment
There was a problem hiding this comment.
Thanks for the thorough round, and for fixing the description, the exit status text, the && short-circuit, and the debug wording. I checked your claims against the code: the WSL truth-table and slot-freed tests do pin what they say, and the ceiling wording is now accurate.
Blocking
- Windows CI is still red on this head, now from the
device_accessiblewave-through: two collector tests that assume a real open fail on Windows (details inline). Please gate or adjust them. Small fix; the core gate design looks good to me.
Non-blocking (inline): decline text/visibility on slow hosts, a test for the blank-output failure path, a few stale doc comments, the --gpu auto multi-GPU wording in docs/testing.md, and the contract wording still open from last round.
Happy to approve once Windows is green.
🤖 by agenthub on AMD AgentHub
| /// Off-Linux counterpart of [`device_accessible`]: see that function for why | ||
| /// the pair is split rather than written once. | ||
| #[cfg(not(target_os = "linux"))] | ||
| const fn device_accessible(_path: &Path) -> bool { |
There was a problem hiding this comment.
Blocking: this wave-through makes the Windows lane red for a different reason than before. With device_accessible returning true off Linux, preflight_passes(false, false, &missing, &missing) is now true on Windows, so the two existing, non-cfg-gated tests missing_kfd_and_no_reachability_fails_preflight and real_kfd_keeps_a_veto_over_the_host_root_view fail there (panics at lines 539 and 558; Windows build and test (debug): 134 passed, 2 failed). They pass on main, where the helper does a real open, so this comes from the branch.
Fix: gate those two tests with #[cfg(target_os = "linux")] and add a small non-Linux test pinning the wave-through. Also worth a sentence in the description: the dash collector used to never detect amd-smi off Linux (the /dev/kfd open failed) and now will try it on Windows. Is that intended?
| if !kfd_readable(kfd_device) { | ||
| bail!( | ||
| "declined to launch `amd-smi {}`: {} could not be opened for reading (absent, \ | ||
| permission denied, no KFD GPU node, or a reset in progress); the pre-flight \ |
There was a problem hiding this comment.
Non-blocking (UX): the decline text only lists device-state reasons ("absent, permission denied, no KFD GPU node, or a reset in progress"). The same decline is also produced by the 2s open timeout on a slow-but-healthy host and by an exhausted probe budget, and in those cases the message points the user at the wrong cause. Adding "did not answer within 2s" and "too many probes already waiting" to the list would keep it honest. Related: the info! goes to the log file, so on serve --gpu auto the user sees nothing on the terminal and a false decline still looks like "no GPU". One stderr line when ranking is skipped would help.
| "amd-smi {} failed: {}", | ||
| args.join(" "), | ||
| crate::probe_exec::truncate_to_chars( | ||
| crate::command_failure_detail_or_else(&output, || output.status.to_string()), |
There was a problem hiding this comment.
Non-blocking: thanks for fixing the doubled "exit status". Nothing reaches this both-blank branch yet, though (a fake amd-smi that exits 1 with no output and an assertion on failed: exit status), so the message could regress unnoticed.
| /// the bound, the budget and the reasoning on `device_readable_within_budget` | ||
| /// stay in one place; a second copy would be free to drift from all three. | ||
| /// | ||
| /// Off Linux this is a direct open, matching [`kfd_readable`]'s wave-through: |
There was a problem hiding this comment.
Non-blocking (docs): a few comments still describe the old behaviour. This one says "Off Linux this is a direct open", but the non-Linux function now just returns true; the same stale claim is in the dash collector test doc ("off Linux device_readable_bounded is a direct unbounded open", crates/rocm-dash-collectors/src/amd_smi.rs ~612). Also, line 712 here and the note in model_serving.feature (~408) say the serve probes drop the error with .ok()? "silently", which is no longer true now that the decline is logged at info!.
| `/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.
Non-blocking (docs): this says --gpu auto on a declined host "ranks from the sysfs VRAM rows alone" and that kfd_readable's doc says the same. The code (select_auto_gpu_index in gpu_policy.rs) only has sysfs rows to rank on a single-GPU host; on a multi-GPU host there is no selection and the engine probe picks. Suggest "single-GPU: ranks from the sysfs rows; multi-GPU: no selection, the engine picks", so the docs match the PR description.
| //! no behavioural test can observe. | ||
| //! | ||
| //! So the guard is on the ingredient rather than the behaviour. A launch needs | ||
| //! a binary to launch, and [`RESOLVER`] is the only way to find one — the |
There was a problem hiding this comment.
Non-blocking: still open from the earlier round, and I understand it is deferred on purpose. The rationale says the resolver is "the only way to find" the binary, but runner.rs (~281) and a collector test fall back to a literal "amd-smi". Until a scan for literal Command::new("amd-smi") lands, I'd soften this wording (and the matching line in docs/testing.md) so it doesn't claim more than the check enforces.
| /// gets past the open and a later ioctl wedges it into uninterruptible sleep, | ||
| /// `run_with_timeout`'s post-kill `wait` does not return either, and no number | ||
| /// quoted here applies — the pre-flight is a mitigation for the reachable | ||
| /// case, not a proof about the whole call. Running them concurrently is not free: |
There was a problem hiding this comment.
Nit: the sentence "Running them concurrently is not free:" is left dangling after the inserted ceiling paragraph, so it now reads as if it belongs to the previous thought. A paragraph break above it would fix the flow.
The previous round made the off-Linux branch return `true`, on the reasoning that it should mirror `kfd_readable`'s wave-through as its doc claimed. That was the wrong half to fix. The two answer different questions: `kfd_readable` gates this crate's own `amd-smi` launch, which off Linux has no `/dev/kfd` to reason about; `device_readable_bounded` is the shared probe `rocm-dash-collectors` calls for a pre-flight that is expected to decline a missing device on every platform. Windows CI caught it: `missing_kfd_and_no_reachability_fails_preflight` and `real_kfd_keeps_a_veto_over_the_host_root_view` both went green while losing the veto they exist to protect. The off-Linux branch is a direct open again and the doc now says why it does not wave through. That also removes the `const`-able body, so the `missing_const_for_fn` split added for it is gone from both crates. Both tests verified passing on Darwin, which compiles the same non-Linux branch Windows does. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Windows CI caught a real bug of mine, and I had fixed it backwards. Pushed as Last round I made The result was the exact failure mode this PR exists to prevent elsewhere: The off-Linux branch is a direct open again, and the doc now states why it does not wave through, naming those two tests so the next person does not repeat it. That also removed the Verified the two tests pass on Darwin, which compiles the same non-Linux branch Windows does, rather than inferring it. Full gate on the Linux MI300X box with the merged tree: Worth recording plainly: this is the third round on this PR where a change of mine was wrong in a way only CI could see, all of it in |
|
Windows is green. Your blocking item was fixed in All three Windows lanes now pass on this head: To be explicit about what the fix was, since "gate or adjust the tests" was the natural reading and I did neither: the two collector tests were right and my change was wrong. Your non-blocking list is noted and still open: the decline text and its visibility on slow hosts, a test for the blank-output failure path, the remaining stale doc comments, the Linux gate on the merged tree, for completeness: clippy |
Windows is green on this head. The blocking item was fixed in 1e091a3, pushed shortly before your review landed. Rather than gate the two collector tests, I reverted my own change: they were correct and the off-Linux wave-through I added made them pass while removing the veto they protect. device_readable_bounded does a direct open off Linux again, with the doc naming those tests. All three Windows lanes now SUCCESS, no failures, nothing pending. Your non-blocking items remain open and are acknowledged as such. Re-requesting your review.
jussielo-amd
left a comment
There was a problem hiding this comment.
Thanks for the thorough rounds. I re-checked the earlier blocker: the Windows lane is green now (the off-Linux collector pre-flight is a direct open again, so the two veto tests assert what they claim), and CI on this head is green across the board, including E2E. The && short-circuit, the debug/info! wording and the description fixes also look right. Nothing blocking.
Still open from before / new, all non-blocking (details inline):
- Decline text is misleading in some cases, and
serve --gpu autoshows nothing on the terminal during the up-to-24s wait. - The both-blank
exit statuspath has no test. examine-31should probably be@requires-bare-metal.- Docs:
--gpu automulti-GPU wording indocs/testing.md, and a stale.ok()?claim plus an orphaned comment block inmodel_serving.feature.
Housekeeping you may want to do while you're in there:
- The PR description says the
serveprobes discard the error with.ok()?and thatgpu_policy.rs's architecture entry was updated; the code now logs atinfo!, and the architecture sentence landed in thefreeform_planner.rsbullet (docs/architecture.mdline 58) instead. It also narrates review rounds and commit hashes that will go stale after merge. - The
rocmdsnapshot doc says a poll costs up to 4s, but it makes two sequential probes (about 8s worst case). - Comments in
rocm-dash-collectors(Cargo.tomldev-deps note,winperf.rs) still say the crate doesn't depend onrocm-core; therocm-coredev-dependency is now redundant. - Several comments narrate review history ("an earlier revision...") rather than the invariant.
🤖 by agenthub on AMD AgentHub
| probe = purpose, | ||
| args = args.join(" "), | ||
| error = format!("{error:#}"), | ||
| "amd-smi probe declined or failed; GPU selection degrades to service state alone" |
There was a problem hiding this comment.
Non-blocking (UX): two things here. (1) This only goes to the log file, so on rocm serve --gpu auto the user sees nothing for up to ~12-24s and a false decline still looks like "no GPU"; one stderr line when a probe is declined/slow (pointing at the log) would help. (2) "GPU selection degrades to service state alone" isn't accurate for every caller: the VRAM probe falls back to sysfs and --model readiness does no selection. Wording it per probe would keep it honest.
| bail!( | ||
| "declined to launch `amd-smi {}`: {} could not be opened for reading (absent, \ | ||
| permission denied, no KFD GPU node, or a reset in progress); the pre-flight \ | ||
| refuses an unreachable device because amd-smi can block unkillably in \ |
There was a problem hiding this comment.
Non-blocking (UX): one message covers an absent node, permission denied, an unready WSL host and a hung open. On a plain GPU-less host (where "absent" is normal) it warns about uninterruptible sleep, and on WSL it blames /dev/kfd, which WSL never has. This text also lands in rocmd's snapshot note. Returning the reason from the probe (not found / permission / timed out / budget spent) would let the hang hazard be mentioned only where it applies.
| "amd-smi {} failed: {}", | ||
| args.join(" "), | ||
| crate::probe_exec::truncate_to_chars( | ||
| crate::command_failure_detail_or_else(&output, || output.status.to_string()), |
There was a problem hiding this comment.
Non-blocking: still open from before. Nothing reaches this both-blank fallback (a fake amd-smi that exits non-zero with no output, asserting failed: exit status: 1), so the doubled-prefix fix could regress unnoticed. A small test would pin it, plus a one-liner for command_failure_detail_or_else.
| # examine-24/25/26 unable to reach amd-smi on a GPU-less runner at all. | ||
| # Linux-only, like its three siblings: the stub is a shell script, and the | ||
| # gate is a wave-through on every other platform. | ||
| @id:examine-telemetry-declines-an-unopenable-kfd-device @requires-os:linux |
There was a problem hiding this comment.
Non-blocking: on a WSL host with a ready GPU the gate waves through before the seam is consulted, so the stub runs and this scenario would fail there. @requires-os:linux can't express "not WSL" (see the note near examine-10); @requires-bare-metal is the tag for that, and the comment/docs could name WSL among the wave-through cases. No WSL lane ran for this PR, so it's a risk rather than an observed failure.
| `/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.
Non-blocking (docs): still says --gpu auto after a decline "ranks from the sysfs VRAM rows alone". That only holds on a single-GPU host; on multi-GPU the DRM rows are withheld, so there is no selection and the engine's probe picks, as the kfd_readable doc already says. Please make the two agree.
| # they previously attempted the spawn. The same chokepoint serves `rocmd`'s | ||
| # bridge snapshot, and that surface is the one a scenario can observe | ||
| # directly: the snapshot reports `amd_smi_available` and a note that says why, | ||
| # as JSON, in one command. `serve`'s probes discard their error with `.ok()?` |
There was a problem hiding this comment.
Non-blocking (docs): "serve's probes discard their error with .ok()?" is no longer true now that the decline is logged at info!. Also this 28-line comment block follows serve-27 with no scenario of its own and runs straight into serve-28's header, so it reads as that scenario's comment. Dropping it (examine-31 and testing.md carry the reasoning) or separating and correcting it would help.
Both sides added tests to the collector's `mod tests`: main's `power_limits_prefer_current_cap_and_skip_unreadable` and `a_zero_current_cap_falls_through_to_the_maximum`, and this branch's `make_fifo` helper with the two off-thread pre-flight tests. Kept both. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Doc-only: main extended the gpu_policy.rs architecture entry again; this branch's sentence about both amd-smi probes going through the /dev/kfd pre-flight is kept alongside it. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
juhovainio
left a comment
There was a problem hiding this comment.
I went through this PR and found one thing worth fixing before merge, plus a few smaller points, left as inline comments.
Needs a fix: the new amd-smi / AMD_SMI_SERVE_PROBE_TIMEOUT sentence in docs/architecture.md was added to the freeform_planner.rs bullet, but both probes live in gpu_policy.rs, whose entry is untouched. The PR description says the gpu_policy.rs entry was updated, so that needs correcting as well.
Smaller points:
- examine-31 is tagged Linux-only, but on a WSL host with a usable GPU the gate waves through, so the scenario would fail there.
docs/testing.mdand thekfd_readabledoc comment disagree about what--gpu autodoes on a multi-GPU host without render-group access. The PR body repeats the second version.- On WSL,
has_usable_amd_gpu()now runs on every launch. If it is not cheap, the documented 12s/24s ceilings are understated. - The rewritten call sites in
gpu_policy.rsare guarded only by the xtask text scan.docs/testing.mdsays unit tests cover them, but none are added.
For the author to weigh: the new rocm-dash-collectors -> rocm-core edge, a decline wider than the ticket asks (absent /dev/kfd or EACCES also declines), the overall size of the PR, and long doc comments that repeat the same latency figures in several places.
The gate tests are non-vacuous (marker file plus control run), the probe is bounded, and examine-31 asserts message, device and no-spawn together. CI is green.
| - [`freeform_answer.rs`](../apps/rocm/src/freeform_answer.rs) — mechanical relocation, no owned types: the answers a freeform request can get without running a command — `render_freeform_read_only_answer`, which answers a single approval-free `examine`, `comfyui status` or `comfyui logs` action, and `render_freeform_examine_answer`, the plain-language ROCm status it prints for `examine`. `run_freeform` in [`main.rs`](../apps/rocm/src/main.rs) is the only caller of the first, which is `pub(crate)` and re-imported there; the second is private. Besides `chat.rs` (`any_substring`), it depends on `comfyui.rs`, `runtimes_cmd.rs` and `therock.rs`, none of which calls back into it. No unit test calls either function directly. | ||
| - [`freeform_execution.rs`](../apps/rocm/src/freeform_execution.rs) — mechanical relocation, dispatch-adjacent, owns a `pub(crate)` type (as of Phase 6e's follow-up, ROCMAI-726; see the Phase 6a variant note above for the owned-types pattern). Holds the freeform natural-language planner's plan-*execution*/consent half that [`freeform_planner.rs`](../apps/rocm/src/freeform_planner.rs) below deliberately left out: `validate_freeform_execution_action()`, `prepare_freeform_execution()`, `apply_freeform_execution_consent()`, `render_freeform_execution_header()`, and the `pub(crate)` `FreeformExecution` struct that `prepare_freeform_execution` returns and `render_freeform_execution_header` consumes. `main.rs`'s `execute_freeform_next_action()` and `run_freeform()` stay in `main.rs` — both are dispatch-level, not part of deciding whether a plan's next action is safe to execute — and `execute_freeform_next_action()` calls in via the unqualified re-import `use crate::freeform_execution::{prepare_freeform_execution, render_freeform_execution_header};` (`run_freeform()` only calls `execute_freeform_next_action()`, never this module directly), matching [`driver_install.rs`](../apps/rocm/src/driver_install.rs)'s dispatch-adjacent form rather than `freeform_planner.rs`'s own qualified-path form. `prepare_freeform_execution` takes the already-built `plan` by value rather than rebuilding it from `request`/`paths`/`config` (ROCMAI-725's single-build guarantee, inherited unchanged from `main.rs`); `prepare_freeform_execution`/`render_freeform_execution_header` are `pub(crate)` for that cross-module call; `FreeformExecution` was already `pub(crate)` (read directly by `main.rs` and this module's own tests); `validate_freeform_execution_action`/`apply_freeform_execution_consent` have no caller outside this module and stay module-private, unchanged from before the move. The cluster's six regression tests moved out of `main.rs`'s shared `mod tests` into this module's own `mod tests`, inheriting the qualified `freeform_planner::` path they already used to reach `freeform_planner.rs`'s items as `main.rs`'s former tests — this module reaches in from outside using the same qualified path `main.rs` and `providers.rs`'s tests use (unlike `chat.rs`/`comfyui.rs`, which import it unqualified). | ||
| - [`freeform_planner.rs`](../apps/rocm/src/freeform_planner.rs) — full domain extraction (Phase 6e, ROCMAI-87). Owns the freeform natural-language planner cluster's types (`StructuredRequestPlan`, `PlannerIntent`, `PlannedToolCall`, `FreeformPlanAction`, `ProviderPlannerResponse`, `ProviderPlannerToolCall`, all module-private except `FreeformPlanAction`, `StructuredRequestPlan` and `PlannedToolCall`, which are `pub(crate)`, and `ProviderPlannerToolCall`, `pub(crate)` together with `validate_provider_planner_tool_call` and `planner_failure_note` so [`chat.rs`](../apps/rocm/src/chat.rs)'s command-preview property tests can drive the planner's guards), the deterministic plan builder (`build_freeform_plan*`), the provider-assisted path (`resolve_freeform_plan_with_provider`, `build_provider_planner_prompt`, `provider_planner_response_to_plan`, `validate_provider_planner_tool_call`), and plan rendering (`render_structured_request_plan`). [`main.rs`](../apps/rocm/src/main.rs) calls `build_freeform_plan_with_context`/`render_structured_request_plan` directly for the dispatch/review flow ([`examine_report_text.rs`](../apps/rocm/src/examine_report_text.rs)'s `render_launch_summary` → `host_default_engine` is another call into this module, unrelated to planning: bare `rocm`'s non-interactive launch summary), while [`freeform_execution.rs`](../apps/rocm/src/freeform_execution.rs) above calls `plan_next_action`/`format_structured_tool_call` for the execution flow and reads `FreeformPlanAction`/`StructuredRequestPlan` by type for its execution-validation functions — those live there, not here, because they decide whether a plan's next action is safe to *execute*, a dispatch-adjacent concern distinct from *building* the plan; `main.rs`'s `run_freeform` builds a plan exactly once via `build_freeform_plan_with_context` and threads that same value through `execute_freeform_next_action` to `prepare_freeform_execution` rather than rebuilding it (ROCMAI-725: building a plan can call a configured cloud `planner_provider`, which is not guaranteed deterministic, so a second independent build could silently disagree with the first). [`chat.rs`](../apps/rocm/src/chat.rs)'s `natural_language_plan` MCP tool gets the same single-build guarantee from `plan_freeform_request_with_context`, which builds once and returns both the rendered text and the next action; `chat.rs` also calls `format_structured_tool_call` directly, like [`comfyui.rs`](../apps/rocm/src/comfyui.rs) does, and its `validate_chat_rocm_command_safety` reads `serve_args_request_cpu_device` directly as a no-CPU-fallback safety check on provider-proposed `serve` args. `host_default_engine`, `build_freeform_plan_with_context`, `render_structured_request_plan`, `plan_next_action`, `plan_freeform_request_with_context`, `provider_planner_response_to_plan`, `format_structured_tool_call`, and `serve_args_request_cpu_device` are `pub(crate)` for these reasons, as are `infer_engine_from_request` (read from `chat.rs`) and `plan_driver_request` (read from [`driver_plan.rs`](../apps/rocm/src/driver_plan.rs)'s test module); `freeform_plan_next_action` and `render_freeform_plan` are test-only (`#[cfg(test)]`) and `FreeformPlanAction` was already `pub(crate)` before the split. `parse_freeform_invocation`/`should_treat_as_freeform` (deciding whether an invocation is freeform at all) stay in `main.rs`, a dispatch-level concern outside both this module and `freeform_execution.rs`. The cluster's regression tests live in `freeform_planner.rs`'s own `mod tests`; the handful of `main.rs` tests that exercise the invocation classifier/clap-suggestion-avoidance stay in `main.rs`'s shared `mod tests`, since neither is specific to this module or to `freeform_execution.rs` (and neither touches `freeform_planner::` at all), and the ROCMAI-725 regression tests proving the single-build guarantee live in `providers.rs`'s `mod tests` (reusing its mock local-provider server), reaching this module's items through the qualified `freeform_planner::` path like any other external caller. | ||
| - [`freeform_planner.rs`](../apps/rocm/src/freeform_planner.rs) — full domain extraction (Phase 6e, ROCMAI-87). Both `amd-smi` probes go through `rocm_core::amd_smi_json` (the `/dev/kfd` pre-flight) with this module's own `AMD_SMI_SERVE_PROBE_TIMEOUT`, whose doc records the resulting worst-case `serve` latency. Owns the freeform natural-language planner cluster's types (`StructuredRequestPlan`, `PlannerIntent`, `PlannedToolCall`, `FreeformPlanAction`, `ProviderPlannerResponse`, `ProviderPlannerToolCall`, all module-private except `FreeformPlanAction`, `StructuredRequestPlan` and `PlannedToolCall`, which are `pub(crate)`, and `ProviderPlannerToolCall`, `pub(crate)` together with `validate_provider_planner_tool_call` and `planner_failure_note` so [`chat.rs`](../apps/rocm/src/chat.rs)'s command-preview property tests can drive the planner's guards), the deterministic plan builder (`build_freeform_plan*`), the provider-assisted path (`resolve_freeform_plan_with_provider`, `build_provider_planner_prompt`, `provider_planner_response_to_plan`, `validate_provider_planner_tool_call`), and plan rendering (`render_structured_request_plan`). [`main.rs`](../apps/rocm/src/main.rs) calls `build_freeform_plan_with_context`/`render_structured_request_plan` directly for the dispatch/review flow ([`examine_report_text.rs`](../apps/rocm/src/examine_report_text.rs)'s `render_launch_summary` → `host_default_engine` is another call into this module, unrelated to planning: bare `rocm`'s non-interactive launch summary), while [`freeform_execution.rs`](../apps/rocm/src/freeform_execution.rs) above calls `plan_next_action`/`format_structured_tool_call` for the execution flow and reads `FreeformPlanAction`/`StructuredRequestPlan` by type for its execution-validation functions — those live there, not here, because they decide whether a plan's next action is safe to *execute*, a dispatch-adjacent concern distinct from *building* the plan; `main.rs`'s `run_freeform` builds a plan exactly once via `build_freeform_plan_with_context` and threads that same value through `execute_freeform_next_action` to `prepare_freeform_execution` rather than rebuilding it (ROCMAI-725: building a plan can call a configured cloud `planner_provider`, which is not guaranteed deterministic, so a second independent build could silently disagree with the first). [`chat.rs`](../apps/rocm/src/chat.rs)'s `natural_language_plan` MCP tool gets the same single-build guarantee from `plan_freeform_request_with_context`, which builds once and returns both the rendered text and the next action; `chat.rs` also calls `format_structured_tool_call` directly, like [`comfyui.rs`](../apps/rocm/src/comfyui.rs) does, and its `validate_chat_rocm_command_safety` reads `serve_args_request_cpu_device` directly as a no-CPU-fallback safety check on provider-proposed `serve` args. `host_default_engine`, `build_freeform_plan_with_context`, `render_structured_request_plan`, `plan_next_action`, `plan_freeform_request_with_context`, `provider_planner_response_to_plan`, `format_structured_tool_call`, and `serve_args_request_cpu_device` are `pub(crate)` for these reasons, as are `infer_engine_from_request` (read from `chat.rs`) and `plan_driver_request` (read from [`driver_plan.rs`](../apps/rocm/src/driver_plan.rs)'s test module); `freeform_plan_next_action` and `render_freeform_plan` are test-only (`#[cfg(test)]`) and `FreeformPlanAction` was already `pub(crate)` before the split. `parse_freeform_invocation`/`should_treat_as_freeform` (deciding whether an invocation is freeform at all) stay in `main.rs`, a dispatch-level concern outside both this module and `freeform_execution.rs`. The cluster's regression tests live in `freeform_planner.rs`'s own `mod tests`; the handful of `main.rs` tests that exercise the invocation classifier/clap-suggestion-avoidance stay in `main.rs`'s shared `mod tests`, since neither is specific to this module or to `freeform_execution.rs` (and neither touches `freeform_planner::` at all), and the ROCMAI-725 regression tests proving the single-build guarantee live in `providers.rs`'s `mod tests` (reusing its mock local-provider server), reaching this module's items through the qualified `freeform_planner::` path like any other external caller. |
There was a problem hiding this comment.
This sentence about the two amd-smi probes and AMD_SMI_SERVE_PROBE_TIMEOUT sits in the freeform_planner.rs bullet, but both live in gpu_policy.rs, whose bullet is untouched. Please move it there.
| # Linux-only, like its three siblings: the stub is a shell script, and the | ||
| # gate is a wave-through on every other platform. | ||
| @id:examine-telemetry-declines-an-unopenable-kfd-device @requires-os:linux | ||
| Scenario: examine-31 - An unopenable KFD device stops amd-smi being launched at all |
There was a problem hiding this comment.
This is @requires-os:linux only. On a WSL host with a usable GPU the gate waves through regardless of ROCM_E2E_KFD_DEVICE, so the stub runs and the marker assertion fails; the unit tests skip on WSL for this reason. Consider a WSL skip or a note.
| `/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, while the kfd_readable doc says auto-selection returns no selection and the engine's own probe picks. Please make both match what serve actually does.
| // evaluations, so `has_usable_amd_gpu` was running on every Linux | ||
| // launch rather than on WSL only. Passing the probe unevaluated keeps | ||
| // the original behaviour and still lets the condition be tested. | ||
| wsl_wave_through(is_wsl_host(), has_usable_amd_gpu), |
There was a problem hiding this comment.
On WSL this now evaluates has_usable_amd_gpu() on every launch (rocmd polls every 60s). If it is not cheap, the documented 12s/24s ceilings are understated; consider confirming its cost or reusing the verdict.
Doc-only again: main keeps extending the gpu_policy.rs architecture entry; this branch's sentence about both amd-smi probes going through the /dev/kfd pre-flight is kept alongside it. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…wo claims overreaching Review round on #447. - The `AMD_SMI_SERVE_PROBE_TIMEOUT` sentence had drifted into `architecture.md`'s `freeform_planner.rs` bullet across several merge resolutions. Both probes live in `gpu_policy.rs`; it sits there now, next to `detect_gpu_count`. - `amd_smi.rs` said that losing the amd-smi count leaves `--gpu auto` with no selection. It does not: `effective_gpu_count` falls through to the DRM sysfs VRAM rows, which is what `docs/testing.md` already said. The code was right and this doc was wrong, so the doc now matches and names the function. - `docs/testing.md` claimed unit tests cover the rewritten call sites. They cover what those sites do with a *missing count* (`effective_gpu_count_prefers_amd_smi_then_falls_back_to_vram_rows`); that they route through the chokepoint at all is guarded only by the xtask contract scan. Both halves are now stated separately. - examine-31 was `@requires-os:linux`, but WSL2 reports that os_family and `kfd_readable` waves through there on a reachable GPU, so the decline it asserts never happens. Now `@requires-bare-metal` too. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
All four fixed. Pushed as The blocking one was mine, and you traced it exactly right. The examine-31 on WSL — real gap, fixed. WSL2 reports The " On WSL, Still open, deliberatelyThe Verification on the Linux MI300X box with the merged tree: One note on the push: this branch now also skips the |
All four addressed in 2d2b8f0. The architecture.md sentence is in the gpu_policy.rs bullet now — it had drifted into freeform_planner.rs across repeated resolutions of that same conflict. examine-31 gains @requires-bare-metal, since WSL2 reports os_family linux but the gate waves through there. The --gpu auto contradiction is resolved against the code: effective_gpu_count falls through to the sysfs VRAM rows, so docs/testing.md was right and the amd_smi.rs doc was wrong. And the testing.md coverage claim is split into its two halves — the missing-count behaviour is unit-tested, the routing is guarded only by the xtask scan. Re-requesting your review.
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/host_snapshot.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.xtask/src/amd_smi_launch_contract.rs(wired in viaxtask/src/main.rs) — a static contract test that fails the build when any file outside a two-entry allowlist callsresolve_amd_smi_binary. Without it nothing stopped the twogpu_policy.rsprobes reverting to a raw spawn, which is the regression this PR exists to prevent; the guard is structural rather than a convention.crates/rocm-dash-collectors/src/amd_smi.rs— the dashboard collector shared the bounded probe (rocm_core::device_readable_bounded, a new allowlistedrocm-dash-collectors -> rocm-coreedge), and then moved it off the async worker:preflight_passes_off_threadshort-circuits the two non-device branches before hopping tospawn_blocking, so a tokio worker is never parked for the 2s bound (~4s under a simulated root). Covered by a timing test.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
Unit tests in
crates/rocm-core/src/amd_smi.rs(11 at the current head), each confirmed non-vacuous by reverting the behaviour it pins, plus the dash-collector timing test and the xtask contract test: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.Scope and limits, stated rather than implied
open(/dev/kfd)returns within a bound. The D-state hang it is aimed at is attributed toamd-smi's later ioctls, and nothing here has been run against a genuinely unhealthy driver — the dev box has noamd-smiand no GPU telemetry. If a later ioctl wedges,run_with_timeout's post-killwaitdoes not return either, and the ~24s figure below does not apply./dev/kfd(containers) andEACCES(outside the render group) both decline, where the ticket says the gate does not apply with no/dev/kfd. Narrowing it means deciding per errno which kernel states are safe to launch into, and the mid-reset errno is exactly what no lane here can manufacture. The cost:detect_gpu_countis lost with no sysfs fallback, so--gpu automakes no selection on a multi-GPU host.serve's--gpuvalidation and the no-GPU fail-fast survive, since they resolve fromusable_amd_gpu_indices()(KFD topology + DRM sysfs, no device open). Thegpu_policy.rsunit tests cover theNonepaths; there is no Gherkin scenario for the serve side.serve --gpu autolatency is ~24s, up from unbounded.