Skip to content

EAI-8240: Fix diagnose emitting invalid UNKNOWN render-group remediation - #302

Merged
r0x0r merged 1 commit into
mainfrom
fix-diagnose-unknown-render-group
Aug 27, 2026
Merged

EAI-8240: Fix diagnose emitting invalid UNKNOWN render-group remediation#302
r0x0r merged 1 commit into
mainfrom
fix-diagnose-unknown-render-group

Conversation

@r0x0r

@r0x0r r0x0r commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

rocm diagnose built the render-group remediation command from the /dev/kfd
owner group, obtained via stat -c %G /dev/kfd. When the device GID has no
matching group name, stat prints the literal string UNKNOWN. The guard in
check_4_render_group only rejected empty strings, so UNKNOWN flowed straight
into the suggested fix:

sudo usermod -a -G UNKNOWN,video "$USER"

No group named UNKNOWN exists, so the command fails if a user copy-pastes it.
The bad command appeared in the human-readable plan, the summary, and the
machine-readable diagnose --json output.

Root cause

In crates/rocm-core/src/diagnose.rs (check_4_render_group), the group was
selected with a filter that only rejects empty strings (!g.is_empty()), not the
literal UNKNOWN that stat -c %G emits when the GID has no name.

Fix

Reject UNKNOWN (case-insensitive) alongside empty values so the group falls back
to render. The emitted command is now:

sudo usermod -a -G render,video "$USER"

This makes diagnose's fallback coincide with the render,video that
fix.rs hardcodes in the UNKNOWN/empty case. It does not unify the two
group sources: diagnose and fix still compute the group independently, and
they still diverge whenever /dev/kfd has a real group name that is not render
(diagnose suggests that name; rocm fix fix-4-render-group still runs
render,video). That pre-existing divergence is out of scope here.

Reproduction

On any container where /dev/kfd's GID is not mapped to a group name:

$ stat -c %G /dev/kfd
UNKNOWN

Before the fix, rocm diagnose (and --json) emitted the invalid UNKNOWN,video
command.

Testing

  • Added regression test unknown_kfd_group_falls_back_to_render asserting both
    the UNKNOWN and unknown sentinel casings produce the render fallback and
    never appear in the command or summary.
  • cargo test -p rocm-core diagnose:: — all 23 diagnose tests pass.
  • cargo clippy -p rocm-core --all-targets -- -D warnings — clean.

Gherkin scenario deferred to follow-up (AGENTS.md §3)

This changes user-observable CLI output (the remediation command in the plan,
summary, and --json), which §3 requires a Gherkin scenario for. An earlier
version of this section claimed the condition "cannot be staged on the CI lanes";
that was incorrect and has been removed.

The condition is reachable end-to-end. stat_device
(crates/rocm-core/src/examine.rs) populates the owner group by shelling out to a
bare stat (Command::new("stat"), no env_clear), which resolves through the
child's PATH. So a stat shim printing an UNKNOWN owner group — the same
PATH-shim technique tests/e2e-cucumber/tests/e2e/dependency_guard_steps.rs
already uses for apt-get/ldconfig/sudo — drives the exact condition through
the real CLI, black-box, with no product-crate test seam.

One genuine constraint makes it a lane gate rather than an impossibility:
stat_device short-circuits on a real Path::new("/dev/kfd").exists(), a direct
std::fs call a PATH shim cannot intercept, so /dev/kfd must actually be
present. The scenario therefore belongs on the @requires-bare-metal lane that
the self-hosted Linux GPU runners exercise as part of the full suite — exactly the
"scenario can only run on a gated lane, say so and name the lane" case §3
describes.

Rather than land it here, the scenario is deferred to the follow-up that also
carries the non-blocking diagnose/fix clean-ups (the coherent
GID-capture/group-unification class). In the meantime the behavior is covered by
the unit-level regression test above, which reproduces the UNKNOWN GID directly.

@r0x0r
r0x0r requested a review from a team as a code owner August 24, 2026 08:40
@r0x0r
r0x0r requested a review from rominf August 24, 2026 08:40
@volen-silo

Copy link
Copy Markdown
Collaborator

Review: changes requested (both items are PR-text edits, not code changes)

The code change itself is correct, minimal, and well-tested. I ran cargo test -p rocm-core diagnose:: (23/23 pass) and cargo clippy -p rocm-core --all-targets -- -D warnings (clean) locally, and CI is green across the matrix. Two required items below, then some non-blocking notes.

Required

1. AGENTS.md §3 — user-observable behavior change with no scenario and no justification.

The PR body itself states the bad command appeared "in the human-readable plan, the summary, and the machine-readable diagnose --json output" — that is user-observable CLI output, which §3 says needs a Gherkin scenario in tests/e2e-cucumber/features/, and explicitly says "a unit test asserting the internal helper does NOT discharge this."

I checked: none of the 12 scenarios in tests/e2e-cucumber/features/diagnose.feature cover this. The file's own header says the scenarios "assert the SHAPE of a diagnosis", and the closest step — every reported cause comes with a command that applies it (tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:233-249) — only counts apply with: rocm fix lines, never inspects their content. A regression reintroducing UNKNOWN,video would sail through e2e.

I don't think a new scenario is the right answer here — the trigger (an unmapped /dev/kfd GID) can't be staged on the CI lanes, so a scenario would be vacuous. §3 anticipates that and requires the PR text to say so. A sentence in the PR body naming the gap and why it can't be covered would close this.

2. The PR body and commit message claim something the diff does not do.

This also unifies the group source so diagnose and fix no longer disagree.

There is no shared group source to unify. crates/rocm-core/src/fix.rs:129 and run_render_group (fix.rs:563-623, e.g. line 586) hardcode "render,video" unconditionally and never read Examination.kfd.owner_group; the two paths are looked up independently by fix_id with no Examination passed in. The patch only makes diagnose's independently-computed fallback coincide with fix.rs's hardcoded value in the UNKNOWN/empty case. The paths still diverge whenever /dev/kfd has a real group name that isn't renderdiagnose suggests that name, rocm fix fix-4-render-group still runs render,video. That divergence predates this PR, but the claim as written overstates the change. Please soften the wording.

Non-blocking

3. The fallback is syntactically valid but not necessarily effective. Nothing in the codebase ever captures the numeric GID — stat_device asks for %A|%U|%G (never %g), and Device (crates/rocm-core/src/examine.rs:85-94) has no GID field. In the exact container case this PR targets, the local render group's GID usually won't match the unmapped GID on /dev/kfd, so sudo usermod -a -G render,video "$USER" can still leave the user without access — while the summary asserts it will fix it. That is a silent fallback that reads as a remediation. Worth considering: capture %g and either name the numeric GID or add a note when the group name is unresolvable, so the guidance is honest about the container case. (check_10_container_devices has the same limitation with its hardcoded --group-add render at diagnose.rs:1013 — pre-existing.)

4. Consider normalizing the sentinel at the parse boundary instead. stat -c %U emits the same UNKNOWN literal for an unmapped UID, and probe_devices populates render_devices[].owner_group through the same stat_device call. I verified there is no second leak path today (owner_user only feeds mode_access; render_devices is only checked for .is_empty()), so this is future-proofing rather than a bug — but mapping UNKNOWN to "" in stat_device would protect every present and future consumer by construction instead of requiring each call site to remember the guard.

5. The case-insensitive arm is untested. The new test only sets owner_group: "UNKNOWN" (uppercase), so swapping eq_ignore_ascii_case("UNKNOWN") for == "UNKNOWN" would keep all 23 tests green even though the PR body specifically claims case-insensitive rejection. Either add a lowercase case, or drop to an exact match — stat's sentinel is a fixed uppercase literal, and the loose comparison also discards a real group genuinely named unknown (unlikely, but a behavior difference bought for nothing).

6. Adjacent, same class, while you're here. check_6_path_missing (diagnose.rs:670-758) splices bin_dir unvalidated into directly runnable command strings — export PATH={bin_dir}:$PATH, echo 'export PATH={bin_dir}:$PATH' >> ~/.bashrc, setx PATH "%PATH%;{bin_dir}". Same "probed value flows into a suggested command" pattern as the bug being fixed here, with a much larger input surface (an arbitrary filesystem path that may contain spaces or shell metacharacters) and no filter at all. Pre-existing and out of scope, but a reasonable follow-up if this class is being cleaned up.

Positive

  • Leaving the raw value in the evidence line (diagnose.rs:566-569, "group UNKNOWN") is the right call — evidence should report what was observed; only the remediation has to be actionable. The test correctly makes no assertion about it, which shows the scoping was deliberate rather than an oversight.
  • The regression test asserts the actual command content and the negative (summary must not contain the bogus name), rather than just "a fix exists". It genuinely fails before the change.
  • Single logical change, signed commit with DCO sign-off, no AI-generated footers.

@r0x0r
r0x0r force-pushed the fix-diagnose-unknown-render-group branch from b329901 to 8af2cb5 Compare August 27, 2026 08:05
@r0x0r

r0x0r commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — both required items addressed, plus the untested-arm note. Force-pushed 8af2cb5 (amend of the single commit) and updated the PR body.

1. §3 justification (required): added a "No Gherkin scenario" section to the PR body. The trigger is an unmapped /dev/kfd GID (stat -c %GUNKNOWN), which the e2e harness can't stage — there's no way to present a device whose GID has no group name — so a scenario would be vacuous. The unit regression test reproduces the UNKNOWN GID directly, and §3's carve-out for behavior that can't run on the available lanes is now named explicitly rather than left implicit.

2. Softened the overclaim (required): the "unifies the group source" sentence is gone. The PR body and the amended commit message now say the fix makes diagnose's fallback coincide with the render,video that fix.rs hardcodes in the UNKNOWN/empty case only, and explicitly note the two sources are still computed independently and diverge when /dev/kfd has a real non-render group name — a pre-existing divergence out of scope here. (For the record, the "unifies" phrasing was only in the PR body; the original commit message said "matching the group that fix.rs hardcodes", which I've also made more precise.)

5. Case-insensitive arm now tested (non-blocking): unknown_kfd_group_falls_back_to_render now loops over both UNKNOWN and unknown, so dropping eq_ignore_ascii_case to an exact match would fail the lowercase case. Kept the case-insensitive guard rather than tightening to exact match. 23/23 diagnose tests pass.

3, 4, 6 (non-blocking, follow-ups): agreed and out of scope for this one logical change:

I'll file these as a follow-up so the class gets cleaned up deliberately rather than piecemeal.

@volen-silo

Copy link
Copy Markdown
Collaborator

Follow-up review at 8af2cb5 — one item remains, and it is mine to own

Re-verified against the current head (8af2cb5, force-pushed amend), not the state at the time of the first review.

Gates, run locally at this head (exit codes, not grepped output):

  • cargo fmt --all -- --check — exit 0
  • cargo clippy --workspace --all-targets -- -D warnings — exit 0
  • cargo test --workspace --all-targets — exit 0

Per finding

2. Overclaim softened — resolved. Verified in both surfaces. The commit message now reads "This makes diagnose's fallback coincide with the render,video that fix.rs hardcodes in the UNKNOWN/empty case; it does not unify the two group sources", and the PR body says the same. I re-checked the underlying facts at this head: crates/rocm-core/src/fix.rs:129 and run_render_group (fix.rs:586,597) still hardcode "render,video" and never read Examination.kfd.owner_group, so the softened wording is accurate. Nothing overstated remains.

5. Case-insensitive arm — resolved, and I confirmed it by mutation rather than by reading. The test now loops for sentinel in ["UNKNOWN", "unknown"]. I killed two mutants locally at this head:

  • dropping the guard back to .filter(|g| !g.is_empty()) → fails with left: ["sudo usermod -a -G UNKNOWN,video \"$USER\""]
  • swapping eq_ignore_ascii_case("UNKNOWN") for g != "UNKNOWN" → fails on the lowercase pass with left: ["sudo usermod -a -G unknown,video \"$USER\""]

Both mutants are caught. The test is real, and the case-insensitive claim is now actually load-bearing.

3, 4, 6 (non-blocking) — fine as deferred. Agreed these are a coherent class and better cleaned up deliberately than piecemeal. No objection to them leaving this PR.

1. §3 scenario justification — the item I asked for is done, but the premise I gave you was wrong, and the text now states something untrue.

You did exactly what I asked: the PR body has a "No Gherkin scenario" section. The problem is that I told you the trigger "can't be staged on the CI lanes", you reasonably took that at face value, and I was wrong. I checked properly this time.

The harness can stage it, and the technique is already in the repo:

  • stat_device (crates/rocm-core/src/examine.rs:876-885) populates owner_group by shelling out — run("stat", &["-c", "%A|%U|%G", path], SHORT) — and run (examine.rs:330) uses Command::new("stat"), a bare program name with no env_clear, so it resolves through the child's PATH.
  • run_rocm_with_env (tests/e2e-cucumber/tests/e2e.rs:527) sets arbitrary env vars on the CLI under test, PATH included.
  • dependency_guard_steps.rs:39,116 already does precisely this: writes executable shims (apt-get, ldconfig, sudo) into a temp dir and runs the CLI with PATH pointed at it.

So a stat shim printing crw-rw----|root|UNKNOWN gets the exact condition through the real CLI, black-box, with no test seam in the product crates.

One genuine constraint, which is a lane gate rather than an impossibility: stat_device short-circuits on exists: Path::new(path).exists() (examine.rs:879), a direct std::fs call that a PATH shim cannot intercept. /dev/kfd has to really be there — so this belongs on @requires-bare-metal, which the self-hosted Linux GPU lanes run as part of the full suite (tests/e2e-cucumber/README.md, tags table). That is the case §3 covers explicitly: "if the scenario can only run on a gated lane, say so in the PR text and name the lane that will exercise it." §3 has no "cannot be staged at all" carve-out — I invented that in the first review and the PR body now cites it back.

What I think should happen: I am not going to block this a second time on a requirement I waived myself. The fix is correct, minimal, and now genuinely well-tested, and re-litigating it would cost more than it buys. But the justification paragraph should not merge as written, because it asserts a harness limitation that does not exist and will be quoted as precedent the next time someone wants to skip a scenario. Minimum: replace "the e2e harness has no way to present a device whose GID has no group name" with the accurate version — the condition is reachable via a stat PATH shim on a bare-metal lane, and the scenario is being deferred to the follow-up you already committed to. Better: fold the scenario into that follow-up alongside items 3/4/6 and say so. Whether the scenario must land here or can ride the follow-up is a maintainer call, and given I am the reason it was skipped, I would not hold the PR for it.

CI

Three checks are red at this head — all three are pre-existing, none attributable to this diff. Flagging so nobody blames the PR:

  • E2E tests (GPU): 81 scenarios (72 passed, 9 failed) with 4 unexpected regressions (bench-load-real-serve, chat-end-to-end-local-model, serve-vllm-inference, serve-vllm-default-on-instinct). The most recent completed run on main shows the identical count and the identical four. All 12 diagnose scenarios passed here.
  • E2E tests (Strix Halo, Ubuntu): 35m timeout, stuck after Model serving / Scenario 1 panicked with no 'resolved model' in output on empty stdout. Same timeout, same panic, same spot on a main run a day earlier.
  • Read the Docs: fails on unrelated PRs against main too; the repo has no .readthedocs.yaml at all. Systemic, not this change.

Verdict

Blocking items 1 and 2 as originally stated are both discharged, and non-blocking item 5 is fixed and verified. The code is good. The one thing I would not merge as-is is the §3 justification text, because it is factually wrong about the harness — and that error started with me, not with you. Sorry for the bad steer.

check_4_render_group built the render-group remediation command from the
/dev/kfd owner group obtained via `stat -c %G`. When the device GID has no
matching group name, stat prints the literal "UNKNOWN", and the existing
guard only rejected empty strings. That let UNKNOWN leak into the suggested
fix, producing `sudo usermod -a -G UNKNOWN,video "$USER"` in the plan,
summary, and --json output -- a command that fails because no such group
exists.

Reject "UNKNOWN" (case-insensitive) alongside empty values so the group
falls back to "render". This makes diagnose's fallback coincide with the
"render,video" that fix.rs hardcodes in the UNKNOWN/empty case; it does not
unify the two group sources, which are still computed independently and can
diverge when /dev/kfd has a real group name that is not "render".

Add a regression test covering both the "UNKNOWN" and "unknown" sentinel
casings falling back to the render group.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
@r0x0r
r0x0r force-pushed the fix-diagnose-unknown-render-group branch from 8af2cb5 to aa65da1 Compare August 27, 2026 12:31
@r0x0r

r0x0r commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Appreciated — and no worries about the earlier steer.

I've corrected the §3 justification in the PR body. It no longer claims the condition "cannot be staged"; instead it documents that the UNKNOWN owner group is reachable end-to-end via a stat PATH shim (the technique dependency_guard_steps.rs already uses for apt-get/ldconfig/sudo), that stat_device's real Path::new("/dev/kfd").exists() short-circuit makes it a @requires-bare-metal lane gate rather than an impossibility, and that the scenario is deferred to the follow-up alongside the coherent items 3/4/6 — with the unit-level regression test covering it in the meantime. No code change; body text only.

Note on history: the commit was signed-off but not cryptographically signed, so it was failing the commit-signatures gate. I re-signed it and force-pushed with lease; git range-diff confirms the content is byte-identical (signature only), and verify-commits/clippy/fmt/test pass at the new head.

@r0x0r
r0x0r enabled auto-merge August 27, 2026 12:45
@r0x0r
r0x0r added this pull request to the merge queue Aug 27, 2026
Merged via the queue into main with commit 3cb433c Aug 27, 2026
19 of 20 checks passed
@r0x0r
r0x0r deleted the fix-diagnose-unknown-render-group branch August 27, 2026 14:13
pmoutsias-amd pushed a commit that referenced this pull request Aug 27, 2026
check_4_render_group built the render-group remediation command from the
/dev/kfd owner group obtained via `stat -c %G`. When the device GID has no
matching group name, stat prints the literal "UNKNOWN", and the existing
guard only rejected empty strings. That let UNKNOWN leak into the suggested
fix, producing `sudo usermod -a -G UNKNOWN,video "$USER"` in the plan,
summary, and --json output -- a command that fails because no such group
exists.

Reject "UNKNOWN" (case-insensitive) alongside empty values so the group
falls back to "render". This makes diagnose's fallback coincide with the
"render,video" that fix.rs hardcodes in the UNKNOWN/empty case; it does not
unify the two group sources, which are still computed independently and can
diverge when /dev/kfd has a real group name that is not "render".

Add a regression test covering both the "UNKNOWN" and "unknown" sentinel
casings falling back to the render group.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>
mikeroySoft pushed a commit to mikeroySoft/rocm-cli that referenced this pull request Sep 1, 2026
* Add Sphinx documentation site

Adds a Sphinx documentation site under docs/rocm-docs/, single-sourced
from README.md and CONTRIBUTING.md via MyST {include} directives so
the docs and repo content stay in sync automatically. Covers
installation, getting started, command reference, demos, and
contributing/license, and is set up to publish with the rocm-docs-core
theme. Updates some links in README.md and CONTRIBUTING.md to be
absolute so they render correctly when reused via {include}.

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* Single-source the platform-support table in the install page

Anchor the table in README.md and pull it into
docs/rocm-docs/install/installation.md via {include} instead of
duplicating it by hand.

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* fix(docs): address review feedback on Sphinx docs site

- Add the MIT license header to the 7 new files hawkeye flagged as
  missing it.
- Fix the WSL2 platform-support-table link to docs/wsl.md: it's now an
  absolute GitHub URL so it resolves when the table is pulled into
  install/installation.md via MyST include, matching the pattern
  already used for the other README anchors.
- Pin rocm-docs-core to the exact version that resolved locally
  (1.40.0) instead of an open->= range.

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* fix(docs): add ReadTheDocs config and a CI job to build docs with -W

- Add .readthedocs.yaml so RTD builds against docs/rocm-docs/conf.py
  with the same pinned rocm-docs-core toolchain CI uses.
- Add a docs-build CI job that runs sphinx-build -W (warnings as
  errors) against the same source RTD points at, gated on the new
  `docs` path-filter category so it only runs when doc sources,
  README.md/CONTRIBUTING.md (single-sourced via MyST includes), or
  the build config change.
- Suppress the myst.header warning from :start-after:/:end-before:
  README includes stripping the anchor heading, which is cosmetic:
  docutils re-normalizes the resulting heading depth in the rendered
  output.
- Scope external_projects to [] instead of the implicit "all" default,
  which was fetching intersphinx inventories for every project in
  rocm_docs' bundled catalog (~90 projects) - several of which 404 or
  have moved upstream. No page here uses a cross-project intersphinx
  role, so this is lossless and makes -W builds deterministic instead
  of flaky against infrastructure this repo doesn't control.
- Filter the "current project not found in projects" warning: rocm-cli
  isn't registered in rocm-docs-core's shared projects catalog yet, so
  external_projects_current_project can never resolve until that's
  added upstream. rocm_docs.projects handles the unresolved project as
  None everywhere it's used, so this is informational, not a defect.

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* docs: use docs-relative cross-page links consistently

README.md's own #model-serving and #interactive-interfaces anchors were
rewritten to absolute GitHub URLs so they'd still work once pulled into
the docs site, but that breaks in-page anchor scrolling when the README
is viewed on GitHub. Revert both to relative anchors and instead apply
the cut-and-reauthor pattern already used for the CONTRIBUTING.md link
in installation.md: narrow the surrounding {include} in getting-started.md
and commands.md around each sentence and hand-author a docs-relative
replacement (commands.md#model-serving, getting-started.md#interactive-interfaces).

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* docs: use descriptive nav title for Demos section

Avoids the sidebar rendering as duplicated Demos > Demos for the
single-entry section by overriding the nav link text to match the
homepage tile's wording.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs: fix sentence fragments in intro blurb

Joins the "single prebuilt binary" fragments into one complete
sentence in both README.md and the standalone index.rst homepage
copy, and adds a lead-in sentence before the bare `rocm` code fence
in the Getting Started "First run" section.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs: style guide pass on README and CONTRIBUTING

Sentence-case headings, expand DCO acronym, replace banned/informal
wording (may, e.g.), normalize slash usage, lowercase version
placeholder.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(lemonade): retry interrupted backend setup (ROCm#249)

* fix(lemonade): retry interrupted backend setup

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

* ci(e2e): build prebuilt lanes with the test-hooks feature

The suite's deterministic failure seams are compiled out without
`rocm/e2e-test-hooks`. `cargo xtask e2e` passes the feature only when it
builds the binaries itself, so every lane that pre-builds and exports
ROCM_CLI_BINARY silently tested a binary without those seams. The
scripted-Lemonade-failure scenario then never reached its premise and
failed as a regression on the one lane that selects it.

Pass the feature on all prebuilt e2e lanes, and add a workflow-contract
test so a lane cannot drift back. Release and packaging builds keep the
feature off — a shipped binary must not carry a failure-injection seam.

Also name the cause in the retry assertion, which otherwise reports a
baffling missing announcement rather than the real misconfiguration.

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

---------

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* ci(dependabot): track pre-commit hook versions (ROCm#235)

The hook pins in .pre-commit-config.yaml were bumped by hand and drifted
between releases. Dependabot's pre-commit ecosystem resolves each rev
against the hook repository's tags, and skips the builtin and local
blocks, which have no upstream release to track.

Weekly and grouped, with the same 7-day cooldown as the other two
ecosystems: a hook is executable code that runs on every contributor's
machine at commit time and in the prek CI job, so a compromised release
would run before anyone reads the bump.

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix: raise the supported Linux minimum to Ubuntu 24.04 (ROCm#261)

* fix: raise the supported Linux minimum to Ubuntu 24.04

Every published Lemonade embeddable, v10.2.0 through v11.5.2, is linked
against GLIBC_2.38 and GLIBCXX_3.4.32. Ubuntu 22.04 ships glibc 2.35, so
the Lemonade engine cannot start there at all, yet README.md advertised
Linux x86_64 as "full support ... both inference engines" and docs/wsl.md
named 22.04 as a supported WSL base.

Document a single minimum of Ubuntu 24.04 for Linux and WSL2, keeping the
glibc number as the reason so the requirement stays meaningful on other
distributions, and drop 22.04 from the WSL preflight's supported set so the
automated check matches the prerequisite it enforces.

Fixes ROCm#258

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>

* fix(wsl): accept Ubuntu releases after 24.04

Signed-off-by: Michael Roy <michael.roy@amd.com>

---------

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: Michael Roy <michael.roy@amd.com>
Co-authored-by: Michael Roy <michael.roy@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* ci(e2e): repair a pre-warm tree that records a folder elsewhere (ROCm#316)

A scenario reaches the shared pre-warm tree through a symlink at its own
`data/runtimes`. An `install sdk` run that way writes the link's path — a
per-scenario temp dir — into the manifest that lands in the SHARED
registry, and into the venv's console-script shebangs. The temp dir goes
away with the scenario and the shared runtime is left naming a folder
that no longer exists, so later, unrelated runs fail.

Drop such a runtime before deciding whether the tree is fresh, so the
pre-warm reinstalls instead of serving a dead one. `rocm update` cannot
see this: it compares versions against the index, not the runtime
against the disk.

Removing the folder is the repair, not a precaution. A poisoned venv
keeps a working `bin/python` — a symlink to the base interpreter, still
present — so the install reuses it and audits already-satisfied packages
rather than reinstalling them, leaving every shebang pointing at the
folder that went away. Measured on uv 0.9.30: reinstalling over a
poisoned venv reports success and repairs nothing.

The signal is an install root outside the tree, deliberately not
`status=unusable`. Unusable has many causes — a missing rocm_sdk probe
block alone reports it — and this deletes what it selects, so a healthy
multi-GiB runtime must not hang on a validation detail. Read-only
runtimes are exempt: `runtimes adopt` records an external folder on
purpose.

Refs: ROCm#315

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(install): record the folder the files land in, not the link (ROCm#317)

* fix(install): record the folder the files land in, not the link

`install sdk` built the install root by joining onto the data dir and
recorded whatever that produced. Reaching `data/runtimes` through a
symlink is enough to make that name a route rather than a place: correct
while the link exists, and wrong the moment it goes.

The path outlives the command. It goes into the registry manifest, the
sidecar beside the runtime, and — through uv — the `#!` line of every
console script in the environment. So a runtime installed that way keeps
reporting itself installed at a folder that is not there, while the files
sit untouched next door and every entry point fails to start.

Resolve the root before anything is written to it. Only the root:
`python_executable` is derived from it and must keep the venv's own
`bin/python`, which is itself a symlink to the base interpreter, so
resolving that would record the system Python instead. The adopt path
already draws the line in the same place, canonicalizing the install root
next to `absolute_existing_file_path_preserving_symlink` for the
interpreter; this gives the install path the same treatment, `--prefix`
included.

The E2E harness creates exactly this shape when a scenario opts into the
shared pre-warmed runtime, so the shared tree on a runner was what got
poisoned — but nothing about the fault is test-only. Any data dir reached
through a link has it.

Refs: ROCm#315
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

* fix(vllm): name the missing interpreter behind a failed launch

Two messages described a stale runtime as an absence.

A spawn that fails on a console script whose `#!` interpreter is gone
reports ENOENT against the script, so the error named a file that is
plainly there. Say which interpreter is missing when that is what
happened, and stay quiet when the ordinary reading is right — a genuinely
absent file, a binary, or a live interpreter.

Resolution drops a registered runtime whose recorded interpreter is not
there. That is correct — a runtime that cannot run is not a candidate —
but it left the failure describing an empty registry while `rocm runtimes
list` prints the entry. Name the manifests that were passed over and the
interpreters they record.

Both are additive: the note only ever decorates an error already being
returned, and a registry it cannot read yields no note rather than
replacing the original failure.

First tests for this area, which had none.

Refs: ROCm#315
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

* test(install): compare resolved paths, not verbatim ones, on Windows

The Windows lane failed on the prefix rather than the behaviour.
`canonicalize` hands back `\\?\C:\…` there, so a test that built its
expectation that way compared a verbatim path against the plain one the
resolver deliberately returns, and three assertions turned on the
difference.

Build the expectations with the resolver instead, which strips the prefix
for the same reason the product does: a stored path is later compared
against ordinary ones, and `\\?\C:\…` never `starts_with`-matches `C:\…`.
That the prefix really is stripped stays asserted on its own, so this does
not become the function agreeing with itself.

This also removes a second Windows-only trap: `temp_dir()` there can
return an 8.3 short path, which `canonicalize` expands, so the two sides
disagreed on the folder name as well as the prefix.

Same fault in the scenario's step definitions, where the comparison would
have failed only on the Strix Windows lane. That crate cannot reach
`rocm-core`, and a dependency for six lines of string handling is the
worse trade, so it strips the prefix locally with the reason recorded.

Refs: ROCm#315
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

* fix(install): give up on a parent component consistently across hosts

The guard against resolving a `..` past a missing directory never fired
on Windows, because the two platforms disagree about what such a path
means. Windows collapses `..` lexically, so `<root>/missing/..` "exists"
and canonicalizes to `<root>`; Unix walks the path through the filesystem
and finds nothing. The walk therefore stopped a level early on Windows,
resolved to the wrong folder, and re-attached the tail — recording a
different folder depending on the host, which is precisely what this
function exists to prevent.

Check for the parent component before asking whether the candidate
exists, so both hosts give up in the same place.

The give-up stays narrow: a `..` whose parent is really there is
unambiguous everywhere and still resolves, which is now pinned by its own
test so a future tightening cannot quietly leave ordinary paths
unresolved.

Found by the Windows lane; it does not reproduce on Linux.

Refs: ROCm#315
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

---------

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(install): resolve driver repo version and package release in plans (ROCm#307)

* fix(install): resolve driver repo version and package release in plans

The driver install plan stored the repo version and package release as
raw shell parameter-expansion templates (${ROCM_CLI_AMDGPU_VERSION:-7.2.4}
and ${ROCM_CLI_AMDGPU_PACKAGE_RELEASE:-70204}). Two problems followed.
The human-readable plan summary printed the version template verbatim on
its "repo_version:" line, leaking an unexpanded shell placeholder into
user-facing dry-run output. And the Debian/Ubuntu apt source line wrapped
the template in single quotes, so the shell never expanded it and the
literal placeholder would have been written into
/etc/apt/sources.list.d/amdgpu.list.

Resolve both templates once, at plan-build time, to their effective
values (the env var when set and non-empty, matching shell :- default
semantics, otherwise the documented default) and bake the concrete values
into every rendered URL, repo path, and command. The resolver is
generalized to resolve_shell_default_template and hardened to pass bare
${VAR} and nested-default shapes through unchanged rather than emitting a
partially rewritten value.

Add unit tests for the resolver (default, env override, empty-as-unset,
non-template pass-through, bare-var and nested-default pass-through) and
update every distro plan test to assert the concrete values, plus a
render-level regression test and an end-to-end scenario asserting the
dry-run summary shows the resolved version rather than the raw
placeholder. Tests that read these process env vars serialize on a shared
lock and save/restore prior values so they stay deterministic under
edition-2024 env semantics.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* test(install): guard the two remaining env-reading driver-plan tests

build_driver_install_plan now resolves the repo-version template via
std::env::var at the top of the function, so every test calling it reads
process env. The sweep that added ScopedTestEnv::with_amd_overrides_cleared()
to the other call sites missed windows_install_driver_is_validate_only and
wsl_install_driver_uses_rocdxg_guidance_without_dkms, leaving two unguarded
readers racing the guarded mutators in the same test binary. Add the guard to
both. No production change.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(cli): accept negative float flag values in space form (EAI-8243) (ROCm#306)

* fix(cli): accept negative float flag values in space form

The space form of negative-number float flags (e.g. `serve --temperature
-1`) was rejected by clap with a confusing "unexpected argument '-1'"
error, because clap parsed the leading-dash token as a flag rather than a
value. Only the equals form (`--temperature=-1`) reached the range
validator that reports the valid range.

Enable `allow_negative_numbers` on the affected float flags
(`--temperature` and `--top-p` on both `chat` and `serve`) so the space
form reaches the value parser and both forms validate identically,
surfacing a clear range-validation message instead of an unexpected
argument error.

Add regression tests covering both the space and equals forms for
`--temperature` and `--top-p` on `chat` and `serve`.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* fix(cli): extend negative-value fix to --max-tokens and add serve scenario

The negative-number-as-flag gotcha is a tokenizer-level ambiguity that
fires before any value parser runs, so it is not float-specific:
`--max-tokens -1` (space form) on both chat and serve was still rejected
as an unexpected argument while `--max-tokens=-1` reached parse_positive_u32.
Add allow_negative_numbers to --max-tokens on both commands so the space
form reaches the value parser and reports a clear error, matching the
--temperature/--top-p behavior fixed here.

Per AGENTS.md §3, the user-observable stderr change is now covered by an
ungated Gherkin scenario, @id:serve-negative-temperature-rejected, in
model_serving.feature. The value parser runs inside argument parsing
before engine selection or GPU pre-flight, so the scenario needs no GPU
and no engine and gates every PR (mirrors the ungated
@id:fix-position-argument-rejected precedent).

Also: reword the shared rationale comment to cover all three numeric value
flags (and note clap only treats a token as negative when the whole
remainder parses as one, so missing/malformed values still error at parse
time), give the chat regression test a symmetric comment, and extend both
space-form regression tests with the --max-tokens cases.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* test(e2e): number the negative-temperature serve scenario

The new scenario was the only unnumbered title in model_serving.feature;
every other scenario uses 'Scenario: <N> - <Title>'. Assign the next free
number (15) to keep the file consistent. No behavior change.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(serve): validate --device via clap possible values (exit 2) (ROCm#304)

* fix(serve): validate --device via clap possible values (exit 2)

Model `rocm serve --device` as a clap `ValueEnum` (`DevicePolicyArg`) so an
invalid value is rejected by clap's usage validation (exit code 2) with the
valid choices listed, instead of parsing as a free-form string and failing
later in application logic with a generic "unsupported device policy" error
(exit code 1).

This makes `--device` consistent with every other enum-style argument in the
CLI (`--engine`, config set-*, engines install, etc.), all of which use clap
possible values. The historical aliases (`auto`/`gpu` for gpu_required, `cpu`
for cpu_only) stay accepted for backward compatibility but are hidden from the
advertised list, and the intentional cpu_only app-level rejection (exit 1 with
a GPU-required message) is preserved.

Adds regression tests asserting invalid `--device` is a clap InvalidValue usage
error and that every value and alias still parses, and reworks the device
possible-values sync test to read clap's structural possible values rather than
a hand-written doc string.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* fix(serve): correct --device help text and hide cpu_only choice

The per-variant ValueEnum doc comments render in `serve --help`, shell
completions, and the invalid-value suggestion list. `gpu_preferred`
previously read "Prefer a ROCm GPU when one is available", which is
misleading: parse_device_policy maps gpu_preferred to GpuRequired and
rocm serve has no CPU fallback path. Reword it to state it behaves
identically to gpu_required.

cpu_only is always rejected (rocm serve requires GPU execution), so hide
it from the advertised choices with #[value(hide = true)] while keeping it
parseable so the deliberate rejection message is preserved.

Add device_policy_arg_maps_through_parse_device_policy to guard the
DevicePolicyArg::as_policy_str -> parse_device_policy mapping (gpu_required
and gpu_preferred resolve to GpuRequired; cpu_only is rejected), closing
the previously untested conversion path.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* docs(serve): stop advertising hidden cpu_only device value

Hiding `cpu_only` from `--device` help/completion left three surfaces still
presenting it as a real choice. Align them with what the CLI now advertises:

- README serve synopsis dropped `cpu_only` from the --device value list.
- Removed the '--gpu is ignored with --device cpu_only (the model runs on CPU)'
  sentence: cpu_only is rejected (exit 1), never runs on CPU, so the line
  advertised a fallback path that does not exist (AGENTS.md §6).
- Corrected the drifted test comment to note cpu_only is #[value(hide = true)]
  and that serve_device_help_lists_match_device_policy_names compares against
  the full DevicePolicy set (hidden entries included).

Doc/comment only; no behavior change.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(dash): launcher shows live serving instances; amd-smi detection off critical path (eai-8190) (ROCm#295)

* fix(dash): seed launcher front door with live serving instances

The launcher front door built an empty AppState, so it always rendered
the idle variant even when a model was actively serving. Read the
managed-service registry (the same authority `rocm services` reads)
once per hub-loop pass and seed the AppState's instances from it.

Also treat `Ready` as serving everywhere the dashboard counts running
instances (`is_serving()`), matching the `Running`+`Ready` treatment
already used elsewhere (e.g. home.rs) -- a served model reports
`Ready`, not `Running`, so the count previously undercounted actual
serving models.

Adds apps/rocm's direct dependency on rocm-dash-core (previously only a
transitive dep) so the launcher can build `Instance`s from the
registry's `DiscoveredService` records.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* fix(dash-daemon): run amd-smi detection off the run loop's critical path

Detecting amd-smi (`amd-smi version` plus the first `system_info()`)
can take up to ~15s on real hardware. Running it inline before the run
loop's first tick blocked managed-service discovery and the first
snapshot broadcast behind it, so an already-running model did not
surface in the dashboard until GPU detection finished -- a visible
~15-20s "0 models running" lag while `rocm services` already reported
it live.

Spawn detection in the background and adopt the result via a oneshot
channel the moment it lands, without ever blocking the loop while it
is in flight. The loop now starts ticking immediately, so serving
instances appear within one discovery tick; GPU metrics fill in once
detection completes.

Adds a regression test asserting on ordering (the instance must
surface in a snapshot whose gpu_system_info is still None) rather than
wall-clock timing, since a pure "arrived within Ns" check would be
flaky under subscriber starvation.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* test(dash): exercise off-critical-path detection and launcher front door

Address review feedback on the amd-smi-off-critical-path change.

- Gate the "amd-smi unavailable" warning behind gpu_init_done so a healthy
  host never flashes it during the detection window; settle as unavailable
  (and say so once) if the detection task ever ends without a result.
- Add an amd_smi_skip_kfd_preflight test seam so the daemon regression test
  drives a fake amd-smi through the real detection path instead of
  short-circuiting on a GPU-less CI host (the /dev/kfd guard stays mandatory
  in production). The test now genuinely fails if detection moves back onto
  the critical path, and asserts the surfaced snapshot carries no premature
  "amd-smi unavailable" warning.
- Add direct unit tests for launcher_serving_instances (ready record maps to
  a live Instance; unbound :0 record is dropped) and a behavioural launcher
  scenario driving bare rocm through a PTY to prove the front door shows
  "Serving <model>" rather than "Idle" for a live registry service.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* Fix diagnose emitting invalid UNKNOWN render-group remediation (ROCm#302)

check_4_render_group built the render-group remediation command from the
/dev/kfd owner group obtained via `stat -c %G`. When the device GID has no
matching group name, stat prints the literal "UNKNOWN", and the existing
guard only rejected empty strings. That let UNKNOWN leak into the suggested
fix, producing `sudo usermod -a -G UNKNOWN,video "$USER"` in the plan,
summary, and --json output -- a command that fails because no such group
exists.

Reject "UNKNOWN" (case-insensitive) alongside empty values so the group
falls back to "render". This makes diagnose's fallback coincide with the
"render,video" that fix.rs hardcodes in the UNKNOWN/empty case; it does not
unify the two group sources, which are still computed independently and can
diverge when /dev/kfd has a real group name that is not "render".

Add a regression test covering both the "UNKNOWN" and "unknown" sentinel
casings falling back to the render group.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(readme): reframe install channels around published release, document rocm examine, describe Configuration section

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(readme): remove rocm model from command reference

Not one of the commands being hardened for this release; team decision
was to drop it from the reference for now rather than document it.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(conf): switch to rocm-ai flavor, enable repo and download buttons

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(conf): revert repository/download header buttons

use_repository_button and use_download_button rely on rocm-docs-core
resolving a repository_url from the current git branch. GitHub
Actions' pull_request checkout leaves a detached HEAD on the
synthetic merge ref, so get_branch() returns an empty URL, and
sphinx_book_theme crashes trying to unpack it, failing the -W build.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(conf): fix flavor fallback and pin repository buttons

- Broaden the known-warning suppression filter to also cover the
  "rocm-ai" flavor, which isn't in the published rocm-docs-core
  release yet and would otherwise fail the -W build even though
  rocm_docs.theme already falls back to "rocm" gracefully.
- Re-enable use_repository_button/use_download_button, pinning
  repository_url/repository_branch explicitly instead of letting
  rocm-docs-core infer them from the local git branch. CI's
  pull_request checkout leaves a detached HEAD on the synthetic merge
  ref, which resolves to an empty repository_url and crashes
  sphinx_book_theme's repository button; use_download_button was
  never actually affected by this, it only reads local source files.

Verified by building against the pinned rocm-docs-core==1.40.0 in a
clean venv (matching CI) -- build succeeds with 0 warnings.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

---------

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: Michael Roy <michael.roy@amd.com>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Co-authored-by: Eugene Volen <150254791+volen-silo@users.noreply.github.com>
Co-authored-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Co-authored-by: Michael Roy <michael.roy@amd.com>
Co-authored-by: Roman <roman.sirokov@amd.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants