Skip to content

feat(examine): report every code object manager library, not just the first - #384

Open
volen-silo wants to merge 1 commit into
mainfrom
feat/report-comgr-copies
Open

volen-silo wants to merge 1 commit into
mainfrom
feat/report-comgr-copies

Conversation

@volen-silo

@volen-silo volen-silo commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

What

HIP compiles device code at run time through libamd_comgr, and a machine can hold more than one copy — a system ROCm install and a ROCm Python wheel each ship one. When the copy the loader picks does not belong to the active runtime, compilation fails with a general error naming neither the library nor the second copy.

This CLI installs the second copy itself. Its install path puts ROCm wheels into a managed environment, so a user who follows it on a host that already has system ROCm ends up holding both, having done nothing unusual and been warned about nothing.

Nothing looked past the first match, so the second copy was invisible. The inspection now records every copy in loader search order, which one would load, and its version.

Reporting only. The conflict rule and the catalog entry that states the user's options follow in a second PR. That is where the risk of a false report on a healthy managed install sits — every managed environment this CLI creates legitimately holds two copies — so it belongs with the rule it threatens rather than bundled with an uncontroversial probe.

Non-obvious decisions

No dlopen. The ticket specified reading the version via amd_comgr_get_version through dlopen. That runs the library's ELF initialisers — code from an unknown library, on a machine the tool was called to precisely because something is already wrong — and pulls a possibly conflicting HIP stack permanently into the process. The version is read from the versioned soname instead. A renamed file yields an empty version, which is honest; the conflict rule never needs the version, only the report text does.

Deduplicated on the resolved path. ROCm ships libamd_comgr.so.2.8.0 beside an unversioned symlink. Counting those as two copies would invent a conflict on an ordinary install — the false report that matters most, since it would fire on healthy machines.

It emulates the loader; it is not the loader. No account is taken of RUNPATH/RPATH, ld.so.preload, or a container remapping paths. The doc comment says so, and the list of copies is the evidence for the verdict rather than a guarantee.

It runs on WSL2 too — a deliberate departure from the ticket's "Linux" scope. The WSL2 early return exists for kernel driver questions, and the code says so while still running the framework probe. A shadowed comgr copy is a run-time compilation failure inside the framework, and ROCm on WSL2 is a supported configuration where the two copies collide identically. Skipping it would leave a WSL user unable to see a conflict that is really there.

Four existing symbols, plus two struct fields, were made pub(crate) rather than restating where a managed runtime keeps its libraries: ldconfig_cache, managed_therock_sdk_probe_candidates, collect_managed_runtime_library_paths, collect_sdk_library_paths, and TheRockSdkProbeCandidate's site_packages/root_path fields. A second description of that layout is a second thing to keep correct.

collect_libraries_in_dir now sorts its results. That is a behavior change, not purely additive: where a directory holds more than one file matching libamd_comgr/libamdhip64, the HIP probe's "first hit" can now be a different file than the OS-arbitrary read_dir order it used before. Deterministic is the better answer for a report two people compare side by side, but it is worth stating plainly rather than filing this PR as behavior-preserving outside the one field it says it touches.

Corrections to the ticket

  • The ticket says examine already reports the active runtime's owning installation. It does not — Examination records the system install only. That knowledge lives in a separate rocm examine surface. Part 2 needs it, and rocm-core's own runtime helpers can resolve managed runtime roots, so no cross-crate dependency is required.
  • The ticket asks for the copies to appear in the human-readable report. Examination has no text renderer — it surfaces through --json and feeds diagnose. Putting them in the separate text report would mean a second, independent probe. Deferred to part 2, where the diagnosis puts the conflict in front of the user in prose.

Verification

  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo clippy -p e2e-cucumber --test e2e -- -D warnings (the harness = false target --all-targets skips) — all clean
  • cargo test --workspace --all-targets — clean, run twice after the review round below to rule out the one unrelated parallel-execution flake (restarting_a_keyless_public_service_names_the_public_bind_not_the_key_flag, a filesystem path race under full-suite parallelism, confirmed to pass in isolation) as masking a real failure
  • GPU e2e lanes (examine-finds-the-managed-runtimes-own-compilation-library, @requires-gpu @requires-os:linux) run in CI, not locally

Every new or changed unit test was verified to fail, not just to pass, each against a deliberately broken version of the thing it covers, then restored. See "Review remediation" below for the round added after the first review.

Coverage boundary

The e2e suite cannot install a second ROCm stack, so it cannot prove the two-copy case. It proves the surface every lane has: the inspection answers the question, the reported copies carry enough to act on, and the selected copy agrees with the list. The two-copy, symlink-dedupe, version-parsing, active-runtime-priority, and attribution rules are proven by unit tests that build the directory layout directly. This is a real boundary, not an oversight.

Risk

Medium, not Low as originally assessed: a first review round found the LD_LIBRARY_PATH/loader-cache search could name the wrong copy as "would load" on a managed install, and a fallback path that could never actually resolve on a real one. Both are fixed now, each with a regression test confirmed to fail against the old behavior first (see "Review remediation"). Still additive in shape — four new fields and a probe that only reads — but no longer risk-free by construction the way the first version was.

The Examination field set is a frozen wire contract with a test guarding it; that test fired on this change and was updated deliberately, which is the guard working as intended.

Review remediation

A first review round found four issues, all addressed in this commit:

  1. comgr_selected and the multi-copy note named the ld.so.cache system copy as "would load" whenever a managed wheel runtime was active but this process's own LD_LIBRARY_PATH was empty — wrong, because rocm serve/rocm chat prepend the active runtime's own library directories onto LD_LIBRARY_PATH before launching the engine, so the wheel copy is what a served process actually loads. The search now takes the active runtime's interpreter (the same one already threaded through the framework probe) and searches its library directories first, ahead of the ambient environment.
  2. The site_packages: None fallback in collect_managed_runtime_library_paths guessed a venv layout (root/lib/<python>/site-packages) the installer never produces — root is one of the runtime's own _rocm_sdk_* package directories, not a venv root, so the guess always found nothing. It now reads root.parent() instead, which is the site-packages directory the guess could never reach. The test fixture was built from the same wrong venv shape and so could not have caught this; it is rebuilt from the real installer layout, and confirmed to fail against the old fallback before passing against the fix.
  3. probe_comgr's internals had no unit tests at all, so the source ordering, the ldconfig -p parsing, the /opt/rocm* scan, the selection, and the multi-copy note could all break with the rest of the suite still green. The ordering/selection logic is split into a pure find_comgr_copies, the ldconfig -p line parsing into parse_ldconfig_cache_paths, and the /opt scan into rocm_install_siblings (the directory to scan is now a parameter, not hardcoded, so a test can point it at a temp folder); each is unit-tested directly.
  4. This PR's commit message previously claimed a HIP copy list, a search that works with no registry record (true now, after item 2's fix — it wasn't, before), and a single shared description of the loader path launched processes use (true only for the SDK-tool-probe path, not the served-engine path, which still has its own separate implementation). Corrected in the commit message.

A second round, one automated and one human, reaching the same place by different routes:

  1. Item 2's fallback fix was itself confirmed unreachable: ROCM_SDK_PROBE_SCRIPT has recorded site_packages unconditionally since its initial version, so every manifest that parses at all carries Some, and the test for it called the function directly with None rather than through a real candidate either way. Rather than keep a reachable-looking branch and a comment describing behavior the code does not have, the None arm is now a no-op and the test is removed; the neighboring root-format test, which already called the function directly with None, is what actually exercises that arm's contract and is kept.
  2. Three of the new unit tests (item 3) had no #[cfg(unix)] gate and fed a path into the :-split LD_LIBRARY_PATH parser, failing the required windows-build-and-test lane — the entire Windows failure on this branch. Gated, along with the helper they share.
  3. Item 1's active-runtime-priority fix regressed examine-finds-the-managed-runtimes-own-compilation-library on a healthy host: once a managed runtime is active, its directory is found first and labelled active-runtime, and this search's existing dedup-by-resolved-path then drops the later managed-runtime hit for the identical file — the scenario's step asserted on the label that fix makes unreachable. The step now accepts either label, and a new unit test pins that an overlapping directory keeps exactly one label, confirmed to fail when the active-runtime search is reordered to run last.

Full detail is in the commit message's two Review: paragraphs.

@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch 3 times, most recently from 4e5fe59 to 6f617c8 Compare September 15, 2026 13:18
@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch from 6f617c8 to 09476f8 Compare September 28, 2026 07:53
@volen-silo
volen-silo marked this pull request as ready for review September 28, 2026 10:04
@volen-silo
volen-silo requested a review from a team as a code owner September 28, 2026 10:04
@volen-silo
volen-silo requested a review from rominf September 28, 2026 10:04

@siloteemu siloteemu left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Automated review · pr-review-watcher · 09476f8

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

Adds a Linux probe that records every libamd_comgr copy on the machine (path, resolved path, version, source), plus four new Examination JSON fields, four unit tests and one Gherkin scenario — outcome: Needs work (one blocking issue). Verified: ran the rocm-core examine test module (41 pass) on a throwaway copy and mutated it per branch — reverting the multi-hit walk, the resolve-path dedup and the digits-only version filter each fails exactly one test, but removing the intra-directory sort(), flipping copies.first() to .last(), and gutting probe_comgr to an immediate return all leave every test green; confirmed examine --json flattens Examination so the new keys really surface, that no other consumer reads the new fields or hip_libs_on_ld_path, that the managed-runtime layout claim matches runtime.rs, and that no comgr probe existed on the base at all (so the title's "not just the first" describes a new field rather than a widened one). Checks at review time: 24 success, 2 failures, 1 skipped, 1 cancelled. Blocking: 1 · Non-blocking: 4.

🚫 Blocking (must fix before merge)

crates/rocm-core/src/examine.rs:1982 — comgr_paths_in_loader_cache gates on which("ldconfig"), and which (same file, ~line 616) walks only the process PATH with no /sbin fallback. This re-introduces a bug the same crate already fixed and documented: crates/rocm-core/src/lib.rs:2543-2560 defines ldconfig_cache() whose doc comment says in as many words that "ldconfig lives in /sbin, which is not on a non-root user's PATH on Debian and derivatives. Looking it up by bare name there yields nothing, and an empty cache is indistinguishable from a cache that does not list the library" — and records the user-visible harm it caused last time (a correctly installed library read as "not registered with the linker"). Its fix is to try ["ldconfig", "/sbin/ldconfig", "/usr/sbin/ldconfig"].

Why it blocks: on those hosts the whole loader-cache tier silently contributes nothing, and that tier is the one that finds the copy the loader actually resolves. The consequences are exactly the ones this feature exists to prevent — a copy registered only in ld.so.cache (a ROCm install outside /opt, or one whose directory is on ld.so.conf but not LD_LIBRARY_PATH) is either missed entirely, producing the "no libamd_comgr found …" note on a machine that has one, or is ranked below a rocm-install/managed-runtime entry so comgr_selected names the wrong copy. It also silently degrades rather than reporting "could not ask", which is the distinction the sibling function was written to preserve. AGENTS.md §5 requires checking sibling implementations before adding a probe.

Fix: drop the which gate and reuse the existing helper — make ldconfig_cache() pub(crate) (the same treatment this PR already applies to collect_sdk_library_paths) and parse its Option<String>, treating None as "could not ask" rather than "no copies". It is timeout-bounded (capture_optional_command → OPTIONAL_COMMAND_TIMEOUT), so this does not trade away the 5s bound the current run(..., SHORT) gives. A smaller variant — mirroring the three-candidate list locally — works too but leaves two descriptions of the same trap to keep in sync.

Non-blocking

  • crates/rocm-core/src/examine.rs:236,239,1875 — "in loader search order" / "the copy the loader would pick" overclaims: the rocm-install and managed-runtime tiers are not loader search locations at all unless they also appear on LD_LIBRARY_PATH/ld.so.conf, yet the hedge names only RUNPATH, ld.so.preload and container remapping; one sentence saying the last two tiers are evidence of what exists rather than of what loads would make the source field's purpose explicit and keep the future conflict rule honest.
  • crates/rocm-core/src/examine.rs:1889-1917 — probe_comgr itself has no test: replacing its body with an immediate return leaves all 41 examine tests green, and the new scenario passes too (the fields serialise as []/null from Default), so on every lane without ROCm present the scenario proves only that two keys exist; the copies.first() selection rule and the multi-copy note are unproven, and a variant taking its search roots as a parameter would make both testable.
  • crates/rocm-core/src/examine.rs:1861 — deleting matches.sort() fails nothing, so the "sorted so the result does not depend on directory iteration order" promise has no test; the ordering test uses two separate directories and never exercises intra-directory order.
  • crates/rocm-core/src/examine.rs:457 — the comment justifying the call site says the search consults LD_LIBRARY_PATH "(read by probe_env)", but probe_comgr re-reads the variable itself and uses nothing probe_env stored; only the probe_rocm_install half of the stated ordering dependency is real.
  • crates/rocm-core/src/examine.rs:1907 — the "N copies … would load" note fires on any host holding a system ROCm install alongside a runtime this CLI installed, which the commit message itself calls the normal case; harmless while Examination.notes is JSON-only (no consumer prints it today), but it will read as a warning the moment one does.

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice write-up, and the reasoning behind the non-obvious calls (no dlopen, resolved-path dedup, WSL2 inclusion) all held up.

One thing I think is a real gap: the managed-runtime search path doesn't look like it actually covers the wheel-install case this PR is meant to catch. managed_sdk_ld_library_path (the existing helper) finds a wheel's comgr copy by walking site_packages for _rocm_sdk_* siblings, because the actual libraries live in a sibling package, not under the root it's handed. comgr_search_dirs's new managed_runtime_roots() path reuses collect_sdk_library_paths on the root directly but skips that site_packages walk entirely - so for a wheel-format install this probe likely never finds the copy that's the PR's own headline scenario. Left an inline comment at the spot.

Two smaller notes, not blocking:

  • The /opt/rocm* sibling-install scan isn't mentioned in the PR description, which frames this as strictly system-vs-wheel. Worth a line in the write-up.
  • collect_libraries_in_dir now sorts before taking the first match, which does change the HIP probe's tie-break order when a directory has multiple matching files - minor, but the Risk section's "unchanged" claim isn't quite true for that edge case.

Also a couple of judgement-call code-smell notes worth a look when convenient (not blockers): the same 4-line "Unix-only" test comment is duplicated across three tests in examine.rs, and ComgrCopy.source is a closed 4-value set that reads like it wants to be an enum rather than a bare String.

Comment thread crates/rocm-core/src/examine.rs Outdated
// managed runtime keeps its libraries is a second thing to keep correct.
for root in managed_runtime_roots() {
let mut paths = Vec::new();
crate::collect_sdk_library_paths(&root, &mut paths);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only walks root via collect_sdk_library_paths. The existing managed_sdk_ld_library_path (lib.rs) additionally walks candidate.site_packages for _rocm_sdk_* sibling packages, because that's where the actual libraries live for a wheel install - this loop doesn't have (and doesn't look up) an equivalent site_packages value for managed_runtime_roots()'s roots, so it likely misses a wheel-installed comgr copy entirely.

Comment thread crates/rocm-core/src/examine.rs Outdated
// Sibling ROCm installs the active one does not cover. A versioned install
// left behind by an upgrade is one of the two copies this entry exists to
// find.
if let Ok(entries) = std::fs::read_dir("/opt") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This /opt/rocm* sibling-install scan isn't mentioned in the PR description (which frames the conflict as system-install vs. wheel-install only) - worth a line explaining it's in scope too.

.filter(|entry| entry.file_name().to_string_lossy().starts_with(prefix))
.map(|entry| entry.path().to_string_lossy().into_owned())
.collect();
matches.sort();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sort changes the HIP probe's tie-break order when a directory has more than one matching file (previously OS-arbitrary read_dir order, now sorted). The Risk section says "the HIP probe still takes the first hit and its field is unchanged" - true for the single-match case, not quite for this edge case.

@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch 2 times, most recently from d9b0a75 to f2bd0c9 Compare September 29, 2026 12:05
@volen-silo
volen-silo dismissed stale reviews from siloteemu and juhovainio September 29, 2026 12:18

Addressed

@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch 2 times, most recently from 6ea3a21 to ee48870 Compare September 29, 2026 13:23
@siloteemu

Copy link
Copy Markdown

🔴 Automated review · pr-review-watcher · ee48870

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 change generalises the code object manager scan from find-first to find-all, adding four Examination fields plus a shared library-path walker; the plurality itself holds up end to end, but a descoped ownership feature left behind an assertion and a doc claim that the shipped code cannot satisfy — Needs work. Verified: ran the touched crate's unit tests (51 pass) and five targeted mutations on a scratch copy — reverting the walker to first-hit kills exactly the_library_path_is_searched_left_to_right_and_every_copy_is_kept (the PR's headline claim, confirmed), breaking canonicalisation kills the symlink test, breaking the version parser kills two, dropping the no-registry fallback kills its test; also confirmed ComgrCopy has exactly four fields (no install_root), that comgr_selected/comgr_version reading index 0 is the intended loader semantics and the e2e step pins selected == copies[0], that the zero-copy and one-copy paths are both handled, that no deny_unknown_fields exists and #[serde(default)] genuinely suffices for the cross-machine deserialiser (checked against a real fixture lacking the fields), that the new note cannot alter status, that no test asserts exact notes contents, and that the PR body's "Examination has no text renderer" is accurate (the human report is built from a different type). The full suite was not run here. Checks at review time: 19 success, 1 failure, 1 skipped. Blocking: 2 · Non-blocking: 4.

On the red check: I did find a defect in this diff that would deterministically fail any run that actually executes the new GPU-gated scenario (blocker 1). That scenario resolves to skip rather than fail where no AMD GPU is present, so whether it is the failing check depends on the lane; I cannot confirm which check failed and am not inferring one. Either way the defect is real and independent of the base branch's intermittency.

🚫 Blocking (must fix before merge)

  • tests/e2e-cucumber/tests/e2e/examine_steps.rs:859-872 — the new step for scenario examine-17 asserts that every copy whose source is managed-runtime carries a non-empty install_root. ComgrCopy (crates/rocm-core/src/examine.rs:159-177) has exactly four fields — path, real_path, version, source — and nothing anywhere in the workspace emits install_root on a comgr copy. copy.get("install_root") therefore always yields None, unwrap_or_default() gives "", and the assertion fails on every execution. This is a test that cannot pass for the reason its message states, and it is the headline coverage the PR text offers for the managed case ("examine-17 asserts that on a GPU lane"), so the case is in fact uncovered and the scenario is a guaranteed red wherever a GPU is present. Fix: delete the for copy in managed { … } loop and its preceding comment (lines 859-872). What remains — copies non-empty, and at least one with source == "managed-runtime" — is still a non-vacuous assertion of the thing the scenario names. If the owning-install data is wanted now rather than in the follow-up, the alternative is to add the field to ComgrCopy and populate it in record_comgr_copy, but that is the deferred work and should not be bolted on here.

  • crates/rocm-core/src/lib.rs:4172-4177 — the doc comment on collect_sdk_package_library_paths states "Attribution is by longest known root and no _rocm_sdk_* directory is a known root, so each resolves to its runtime — which is what keeps a healthy managed install from looking like several installations in conflict." No attribution-by-longest-root mechanism exists in this diff or in the base; nothing computes an owning installation for a comgr copy, and comgr_matches_runtime is documented as permanently unset. The comment presents a safety property as provided when the code provides nothing of the kind, which is precisely the guard-in-prose-only pattern — and it is the same descoped feature that produced blocker 1. The commit body carries the same claim twice ("recording every copy with the installation that owns it, plus the same for the HIP runtime so the two can be compared" and "Ownership is matched against known installation roots, longest first, and never derived by walking up from the file"); a maintainer reading either will believe attribution shipped. Fix: cut the second and third sentences of the doc comment, keeping "They belong to the runtime that contains them, not to themselves.", and drop the two ownership paragraphs from the commit body when the branch is next amended.

Non-blocking

  • crates/rocm-core/src/examine.rs:1876 — removing matches.sort(); fails no test at all (verified by mutation), yet the comment beside it correctly flags it as a deliberate change to the existing HIP probe's tie-break, not a no-op. A two-file-in-one-directory case would pin it cheaply.
  • The PR text's "Nothing existing changes behaviour except the LD_LIBRARY_PATH walker" understates the diff: managed_sdk_ld_library_path gains a new fallback that scans the predictable venv layout when no site-packages was recorded, which changes the loader path handed to the agent enumerator during gfx-target detection. The shared helper's new branch is tested; its effect through that caller is not, and both existing tests there populate site_packages.
  • "One existing helper was made pub(crate)" — four items were: the probe-candidate builder, the SDK library-path collector, the candidate struct and two of its fields, plus the new shared helper.
  • comgr_matches_runtime ships permanently None and no diagnosis check reads any of the four new fields, so the data is inert on the wire today. Deferring is disclosed and defensible; worth confirming the follow-up is what makes it non-inert. Relatedly, examine-16 passes vacuously on a host holding zero copies — the PR discloses this accurately, and the zero/selection consistency it does assert is real.

@siloteemu siloteemu left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Automated review · pr-review-watcher · ee48870

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)

  • tests/e2e-cucumber/tests/e2e/examine_steps.rs:859-872 — the new step for scenario examine-17 asserts that every copy whose source is managed-runtime carries a non-empty install_root. ComgrCopy (crates/rocm-core/src/examine.rs:159-177) has exactly four fields — path, real_path, version, source — and nothing anywhere in the workspace emits install_root on a comgr copy. copy.get("install_root") therefore always yields None, unwrap_or_default() gives "", and the assertion fails on every execution. This is a test that cannot pass for the reason its message states, and it is the headline coverage the PR text offers for the managed case ("examine-17 asserts that on a GPU lane"), so the case is in fact uncovered and the scenario is a guaranteed red wherever a GPU is present. Fix: delete the for copy in managed { … } loop and its preceding comment (lines 859-872). What remains — copies non-empty, and at least one with source == "managed-runtime" — is still a non-vacuous assertion of the thing the scenario names. If the owning-install data is wanted now rather than in the follow-up, the alternative is to add the field to ComgrCopy and populate it in record_comgr_copy, but that is the deferred work and should not be bolted on here.

  • crates/rocm-core/src/lib.rs:4172-4177 — the doc comment on collect_sdk_package_library_paths states "Attribution is by longest known root and no _rocm_sdk_* directory is a known root, so each resolves to its runtime — which is what keeps a healthy managed install from looking like several installations in conflict." No attribution-by-longest-root mechanism exists in this diff or in the base; nothing computes an owning installation for a comgr copy, and comgr_matches_runtime is documented as permanently unset. The comment presents a safety property as provided when the code provides nothing of the kind, which is precisely the guard-in-prose-only pattern — and it is the same descoped feature that produced blocker 1. The commit body carries the same claim twice ("recording every copy with the installation that owns it, plus the same for the HIP runtime so the two can be compared" and "Ownership is matched against known installation roots, longest first, and never derived by walking up from the file"); a maintainer reading either will believe attribution shipped. Fix: cut the second and third sentences of the doc comment, keeping "They belong to the runtime that contains them, not to themselves.", and drop the two ownership paragraphs from the commit body when the branch is next amended.

@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch from ee48870 to a8749a3 Compare September 30, 2026 06:49
@siloteemu
siloteemu dismissed their stale review September 30, 2026 08:10

Superseded: this objection was filed against an earlier commit and is replaced by a fresh round at the current head. The earlier blocking item is discharged in the tree; a new, unrelated blocking finding is filed separately.

@siloteemu

Copy link
Copy Markdown

🔴 Automated review · pr-review-watcher · a8749a3

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 change replaces the first-match-only libamd_comgr lookup with a walker that records every copy on the machine, in the order the search consults, together with which one would load and its version; it is reporting only, with the conflict rule deferred. Outcome: Needs work — one scenario does not exercise the code it names on the lanes where it is the only coverage. Verified: the eight new unit tests in the core crate were run on a scratch copy and pass, then production lines were mutated one at a time on that copy — dropping the recorded-site-packages arm kills two tests, dropping the no-record fallback kills one, deduplicating on the given path instead of the resolved one kills the symlink test, removing the digits-and-dots version guard kills the version table, and restoring stop-at-first-hit kills the ordering test, so the headline defect and the managed-runtime layout are genuinely covered; removing the new matches.sort() kills nothing; and with both probe_comgr call sites deleted, replaying examine-16's exact assertion sequence against the serialized examination still passes. Both items from the previous round are discharged in the tree — the install_root loop is gone and what replaced it is non-vacuous, and the attribution-by-longest-root sentences are gone from both the doc comment and the commit body. The previous round's non-blocking notes were not re-checked in this round. The full test suite, the e2e suite and clippy were not run here. Checks at review time: success=16, skipped=6, failure=4, pending=2; at publication, success=16, skipped=6, failure=5, pending=1. Blocking: 1 · Non-blocking: 5.

🚫 Blocking (must fix before merge)

  • tests/e2e-cucumber/features/examine.feature:184-194 — examine-16 is untagged, so it runs on every lane, and its comment claims "What every lane can prove is that the inspection answers the question at all rather than staying silent, and that finding none is reported as a finding rather than a failure — which is the case the mock lane actually has." The code does not provide that; the struct defaults do. All four new fields are #[serde(default)] and Examination::default() already yields comgr_paths: [] and comgr_selected: null, which is exactly what both steps assert when no copy is found. Measured, not read: with both probe_comgr(&mut e) calls deleted, the assertion sequence of assert_comgr_copies_reported and assert_comgr_selection_is_stated (tests/e2e-cucumber/tests/e2e/examine_steps.rs:773, :800) still passes against the serialized examination. That is the state of every Windows lane, where the probe is never called at all, and of the mock lane the comment names. So on precisely the lanes where examine-16 is the only coverage of this change, it passes with the change reverted, while its prose says it proves the opposite — and examine-17 cannot compensate because it is @requires-gpu. Fix: tag the scenario @requires-os:linux (the probe is Linux and WSL2 only, so the Windows pass is vacuous by construction), and in the empty branch of assert_comgr_selection_is_stated assert that notes contains the "no libamd_comgr found ..." entry that crates/rocm-core/src/examine.rs:1927-1930 pushes, in addition to comgr_selected being null. That note is produced only by probe_comgr running and finding nothing, so it distinguishes "probed, found none" from "never probed" — which is the distinction the scenario's own sentence claims to make, and the same distinction the PR already builds into comgr_matches_runtime. Assert on a substring of the note rather than the whole string so the wording stays free to change; do not assert it in the non-empty branch, where a single copy pushes no note at all.

Non-blocking

  • crates/rocm-core/src/examine.rs:246,249 — the "emulates the loader, is not the loader" caveat lives only on the private function; the two public field docs state "in loader search order", "the one that would load" and "the copy the loader would pick" as fact, and the rocm-install and managed-runtime tiers are not loader-searched at all absent RPATH/RUNPATH, so a consumer reading only the wire contract gets a stronger promise than the code makes.
  • crates/rocm-core/src/examine.rs:1876 — the commit body calls the new sort out as "a change, not a no-op", but no test pins it: removing matches.sort() leaves all eight new unit tests green, because every fixture directory holds at most one matching file.
  • crates/rocm-core/src/examine.rs:1996 — the loader-cache tier has no test at any level, so the /sbin gating fix the commit body describes is entirely unverified; note also that the base branch has no which("ldconfig") gate and this function is new, so that paragraph reads as fixing shipped code when it describes an earlier revision of this branch.
  • tests/e2e-cucumber/tests/e2e/examine_steps.rs:400 — the new When step runs examine --json and stores stdout in cli_output, which is exactly what user_inspects_both_ways at :389 already does; two step names for one action is the second description this change elsewhere takes care to avoid.
  • tests/e2e-cucumber/tests/e2e/examine_steps.rs:818-828 — asserting comgr_selected.path == comgr_paths[0].path restates the single line that assigns it, so it can only fail if the two fields are decoupled later; worth keeping as a contract check, but it is not evidence that the search order itself is right.

@siloteemu siloteemu left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Automated review · pr-review-watcher · a8749a3

Change request filed by the automated review round at this commit. The full round, including non-blocking notes, is in the comment posted alongside it.

🚫 Blocking (must fix before merge)

  • tests/e2e-cucumber/features/examine.feature:184-194 — examine-16 is untagged, so it runs on every lane, and its comment claims "What every lane can prove is that the inspection answers the question at all rather than staying silent, and that finding none is reported as a finding rather than a failure — which is the case the mock lane actually has." The code does not provide that; the struct defaults do. All four new fields are #[serde(default)] and Examination::default() already yields comgr_paths: [] and comgr_selected: null, which is exactly what both steps assert when no copy is found. Measured, not read: with both probe_comgr(&mut e) calls deleted, the assertion sequence of assert_comgr_copies_reported and assert_comgr_selection_is_stated (tests/e2e-cucumber/tests/e2e/examine_steps.rs:773, :800) still passes against the serialized examination. That is the state of every Windows lane, where the probe is never called at all, and of the mock lane the comment names. So on precisely the lanes where examine-16 is the only coverage of this change, it passes with the change reverted, while its prose says it proves the opposite — and examine-17 cannot compensate because it is @requires-gpu. Fix: tag the scenario @requires-os:linux (the probe is Linux and WSL2 only, so the Windows pass is vacuous by construction), and in the empty branch of assert_comgr_selection_is_stated assert that notes contains the "no libamd_comgr found ..." entry that crates/rocm-core/src/examine.rs:1927-1930 pushes, in addition to comgr_selected being null. That note is produced only by probe_comgr running and finding nothing, so it distinguishes "probed, found none" from "never probed" — which is the distinction the scenario's own sentence claims to make, and the same distinction the PR already builds into comgr_matches_runtime. Assert on a substring of the note rather than the whole string so the wording stays free to change; do not assert it in the non-empty branch, where a single copy pushes no note at all.

volen-silo added a commit that referenced this pull request Oct 1, 2026
…s own

Builds on the copy search: with every copy of libamd_comgr known, the
catalog can say when the one that loads belongs to a different
installation than the HIP runtime that loads, and device code
compilation therefore fails with an error naming neither.

The rule is symmetric, and that is what makes it safe. The design note
for this entry proposed a special case -- "when the active runtime is
the managed runtime, the matching copy is the wheel copy" -- without
which it "fires on every healthy CLI installation". That is a patch over
a rule stated asymmetrically. Asking one question of both libraries
instead, does the libamd_comgr that would load come from the same
installation as the libamdhip64 that would load, makes every case fall
out of the rule: a healthy managed install takes both from the managed
runtime, so nothing differs and nothing is reported. A control that has
to be written as an exception is a rule that has not been stated
correctly yet.

That turns on attributing a copy to its installation correctly. A
managed runtime spreads its libraries across separate _rocm_sdk_*
packages, so ownership is decided by matching against known installation
roots, longest first -- never by walking up from the file, which would
call each package its own install and fire on the most common install we
ship. That is the property the mutation check targets.

Two further guards on firing: an unattributed copy is a gap in what the
search knows rather than a finding about the machine, and the runtime
must actually ship a copy of its own, or there is nothing to point the
user at.

The entry is print-only and ranks neither remedy. Removing a stack and
reordering the search path can each break a working Python environment,
and which is right depends on which stack the user means to keep.

The finding says out loud that it describes the environment outside the
managed runtimes. `rocm serve` puts a managed runtime's libraries first
on purpose and gets a different, correct answer, and a report that did
not name the environment it examined would read as a claim about one it
never looked at.

The rule above is now stated exactly once, as `comgr_matches_runtime` in
examine.rs, and both this entry and `rocm examine --json`'s field of the
same name call it rather than each restating the conditions -- so the
two surfaces cannot disagree about one machine, and a regression test
pins that. Fix option (b)'s advice matched an earlier, asymmetric
statement of the rule; it now says to prefer the wheel's own copy of
both libraries, which is what the symmetric rule actually asks the user
to do. The machine-readable inspection now carries the same
`install_root` attribution for the HIP runtime side that it already
carried for the code object manager side, since the conflict question is
about both libraries and a reader could not previously check either the
attribution or the selection for one of them.

Known roots are deduplicated across the whole list, not only where
repeats happen to land next to each other. `dedup_by` drops consecutive
duplicates, and these roots arrive from three independent sources -- the
active install, a sorted /opt scan, and the managed runtimes -- so the
active install colliding with a sibling was caught or missed depending on
where the path sorted. A root recorded twice can make a machine holding
one stack read as a machine holding two, which is the false conflict this
entry exists not to report.

hip_paths and hip_selected are `serde(default)` for the same reason the
comgr fields are: an examination read back over the remote path may have
been produced by an older CLI that never wrote them.

Fix CI: the managed-runtime comgr/HIP search resolved the data directory
through `crate::runtime::default_data_dir` (`$HOME/.rocm` or the OS
default) instead of `AppPaths::discover`, so it disagreed with the rest
of the CLI, and found nothing at all, on any host where
`ROCM_CLI_DATA_DIR` relocates the data directory -- which is exactly what
every GPU e2e scenario does for isolation. That is why the search saw no
managed runtime whatsoever and the GPU e2e lane
(`examine-finds-the-managed-runtimes-own-compilation-library`) failed
identically on every GPU family. Separately, once a runtime is found, its
root resolves to its `_rocm_sdk_devel` package directory, and the search
then guessed a venv layout (`root/lib/<python>/site-packages`) to find
its sibling packages; that guess does not hold for a real install either,
so the search now reads back the library directories the SDK probe
already recorded (`library_paths`) instead of re-deriving them. Also
fixes a needless-`collect` clippy lint in the e2e step, and tags
diagnose-21 `@requires-os:linux`: fix-18-comgr-conflict is registered for
linux/wsl only, so applying it for real on a native Windows lane hit the
fix's own platform gate before ever reaching the advisory behavior under
test.

That data-dir fix cleared the scenario everywhere except the Strix Halo
Windows lane, where it still failed CI after the rest of this commit
landed: `comgr_paths`/`hip_paths` came back empty even with the managed
runtime found and the right data dir in hand, because the whole search --
`LD_LIBRARY_PATH`, the loader cache, and the `lib/` directory scan -- is
built around ELF shared-object names (`libamd_comgr`, `libamdhip64`).
Native Windows ships the equivalent libraries under different names and
has neither `LD_LIBRARY_PATH` nor a loader cache, so none of it applies
there; WSL is unaffected, since it reports `os_family` "linux" and the
runtime underneath really does carry a `.so`. This is the same boundary
`fix-18-comgr-conflict` is already gated on (`&["linux", "wsl"]`,
addressed above for diagnose-21), just missed for this scenario when it
was introduced in #384 -- so examine-17 now carries the same
`@requires-os:linux` tag, pre-existing gap, not a regression from this
commit.

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch from a8749a3 to 51a20ff Compare October 1, 2026 13:17
volen-silo added a commit that referenced this pull request Oct 1, 2026
…s own

Builds on the copy search: with every copy of libamd_comgr known, the
catalog can say when the one that loads belongs to a different
installation than the HIP runtime that loads, and device code
compilation therefore fails with an error naming neither.

The rule is symmetric, and that is what makes it safe. The design note
for this entry proposed a special case -- "when the active runtime is
the managed runtime, the matching copy is the wheel copy" -- without
which it "fires on every healthy CLI installation". That is a patch over
a rule stated asymmetrically. Asking one question of both libraries
instead, does the libamd_comgr that would load come from the same
installation as the libamdhip64 that would load, makes every case fall
out of the rule: a healthy managed install takes both from the managed
runtime, so nothing differs and nothing is reported. A control that has
to be written as an exception is a rule that has not been stated
correctly yet.

That turns on attributing a copy to its installation correctly. A
managed runtime spreads its libraries across separate _rocm_sdk_*
packages, so ownership is decided by matching against known installation
roots, longest first -- never by walking up from the file, which would
call each package its own install and fire on the most common install we
ship. That is the property the mutation check targets.

Two further guards on firing: an unattributed copy is a gap in what the
search knows rather than a finding about the machine, and the runtime
must actually ship a copy of its own, or there is nothing to point the
user at.

The entry is print-only and ranks neither remedy. Removing a stack and
reordering the search path can each break a working Python environment,
and which is right depends on which stack the user means to keep.

The finding says out loud that it describes the environment outside the
managed runtimes. `rocm serve` puts a managed runtime's libraries first
on purpose and gets a different, correct answer, and a report that did
not name the environment it examined would read as a claim about one it
never looked at.

The rule above is now stated exactly once, as `comgr_matches_runtime` in
examine.rs, and both this entry and `rocm examine --json`'s field of the
same name call it rather than each restating the conditions -- so the
two surfaces cannot disagree about one machine, and a regression test
pins that. Fix option (b)'s advice matched an earlier, asymmetric
statement of the rule; it now says to prefer the wheel's own copy of
both libraries, which is what the symmetric rule actually asks the user
to do. The machine-readable inspection now carries the same
`install_root` attribution for the HIP runtime side that it already
carried for the code object manager side, since the conflict question is
about both libraries and a reader could not previously check either the
attribution or the selection for one of them.

Known roots are deduplicated across the whole list, not only where
repeats happen to land next to each other. `dedup_by` drops consecutive
duplicates, and these roots arrive from three independent sources -- the
active install, a sorted /opt scan, and the managed runtimes -- so the
active install colliding with a sibling was caught or missed depending on
where the path sorted. A root recorded twice can make a machine holding
one stack read as a machine holding two, which is the false conflict this
entry exists not to report.

hip_paths and hip_selected are `serde(default)` for the same reason the
comgr fields are: an examination read back over the remote path may have
been produced by an older CLI that never wrote them.

Once a managed runtime is found, its root resolves to its
`_rocm_sdk_devel` package directory, and the search used to guess a venv
layout (`root/lib/<python>/site-packages`) to find its sibling packages;
that guess does not hold for a real install, so the search now reads
back the library directories the SDK probe already recorded
(`library_paths`) instead of re-deriving them. This generalises the
walker introduced in #384 (`find_library_copies`/`install_library_dirs`)
so it serves the HIP side too, which is why `managed_runtime_roots` now
returns `library_paths` plural rather than a single `site_packages`
guess.

The AppPaths::discover data-dir fix and the @requires-os:linux platform
tags this commit originally carried for examine-17/18 belong with the
scenario they correct, which #384 introduced -- moved there so the
parent doesn't depend on the child to pass its own CI, and this commit
now only rebases on top of that fix rather than restating it.

Sweeping the same gap once it was visible: examine-17 (the
host-independent code-object-manager scenario, renumbered by #384's
rebase) asserts `hip_paths`/`hip_selected` the same way it asserts
`comgr_paths`/`comgr_selected`, and the empty branch had the identical
problem #384's review flagged for the comgr side -- `hip_paths: []` /
`hip_selected: null` also hold by nothing more than `Examination`'s own
defaults, so the assertion could not tell "probed, found none" from
"never probed". `probe_comgr` now pushes a "no libamdhip64 found" note
the same way it already did for libamd_comgr, and the HIP assertion now
requires it in the empty branch. Verified by temporarily suppressing the
note and watching the scenario go red, then restoring it.

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
@volen-silo
volen-silo dismissed siloteemu’s stale review October 1, 2026 13:41

Addressed: examine-17 (renumbered) is now tagged @requires-os:linux and its empty-result assertion requires the "no libamd_comgr found" note, so it can no longer pass with probe_comgr reverted or never called. Verified by mutation-testing the note (suppressed it, watched the scenario fail, restored it).

@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch from 51a20ff to c5c15f7 Compare October 2, 2026 05:57
volen-silo added a commit that referenced this pull request Oct 2, 2026
…s own

Builds on the copy search: with every copy of libamd_comgr known, the
catalog can say when the one that loads belongs to a different
installation than the HIP runtime that loads, and device code
compilation therefore fails with an error naming neither.

The rule is symmetric, and that is what makes it safe. The design note
for this entry proposed a special case -- "when the active runtime is
the managed runtime, the matching copy is the wheel copy" -- without
which it "fires on every healthy CLI installation". That is a patch over
a rule stated asymmetrically. Asking one question of both libraries
instead, does the libamd_comgr that would load come from the same
installation as the libamdhip64 that would load, makes every case fall
out of the rule: a healthy managed install takes both from the managed
runtime, so nothing differs and nothing is reported. A control that has
to be written as an exception is a rule that has not been stated
correctly yet.

That turns on attributing a copy to its installation correctly. A
managed runtime spreads its libraries across separate _rocm_sdk_*
packages, so ownership is decided by matching against known installation
roots, longest first -- never by walking up from the file, which would
call each package its own install and fire on the most common install we
ship. That is the property the mutation check targets.

Two further guards on firing: an unattributed copy is a gap in what the
search knows rather than a finding about the machine, and the runtime
must actually ship a copy of its own, or there is nothing to point the
user at.

The entry is print-only and ranks neither remedy. Removing a stack and
reordering the search path can each break a working Python environment,
and which is right depends on which stack the user means to keep.

The finding says out loud that it describes the environment outside the
managed runtimes. `rocm serve` puts a managed runtime's libraries first
on purpose and gets a different, correct answer, and a report that did
not name the environment it examined would read as a claim about one it
never looked at.

The rule above is now stated exactly once, as `comgr_matches_runtime` in
examine.rs, and both this entry and `rocm examine --json`'s field of the
same name call it rather than each restating the conditions -- so the
two surfaces cannot disagree about one machine, and a regression test
pins that. Fix option (b)'s advice matched an earlier, asymmetric
statement of the rule; it now says to prefer the wheel's own copy of
both libraries, which is what the symmetric rule actually asks the user
to do. The machine-readable inspection now carries the same
`install_root` attribution for the HIP runtime side that it already
carried for the code object manager side, since the conflict question is
about both libraries and a reader could not previously check either the
attribution or the selection for one of them.

Known roots are deduplicated across the whole list, not only where
repeats happen to land next to each other. `dedup_by` drops consecutive
duplicates, and these roots arrive from three independent sources -- the
active install, a sorted /opt scan, and the managed runtimes -- so the
active install colliding with a sibling was caught or missed depending on
where the path sorted. A root recorded twice can make a machine holding
one stack read as a machine holding two, which is the false conflict this
entry exists not to report.

hip_paths and hip_selected are `serde(default)` for the same reason the
comgr fields are: an examination read back over the remote path may have
been produced by an older CLI that never wrote them.

Once a managed runtime is found, its root resolves to its
`_rocm_sdk_devel` package directory, and the search used to guess a venv
layout (`root/lib/<python>/site-packages`) to find its sibling packages;
that guess does not hold for a real install, so the search now reads
back the library directories the SDK probe already recorded
(`library_paths`) instead of re-deriving them. This generalises the
walker introduced in #384 (`find_library_copies`/`install_library_dirs`)
so it serves the HIP side too, which is why `managed_runtime_roots` now
returns `library_paths` plural rather than a single `site_packages`
guess.

The AppPaths::discover data-dir fix and the @requires-os:linux platform
tags this commit originally carried for examine-17/18 belong with the
scenario they correct, which #384 introduced -- moved there so the
parent doesn't depend on the child to pass its own CI, and this commit
now only rebases on top of that fix rather than restating it.

Sweeping the same gap once it was visible: examine-17 (the
host-independent code-object-manager scenario, renumbered by #384's
rebase) asserts `hip_paths`/`hip_selected` the same way it asserts
`comgr_paths`/`comgr_selected`, and the empty branch had the identical
problem #384's review flagged for the comgr side -- `hip_paths: []` /
`hip_selected: null` also hold by nothing more than `Examination`'s own
defaults, so the assertion could not tell "probed, found none" from
"never probed". `probe_comgr` now pushes a "no libamdhip64 found" note
the same way it already did for libamd_comgr, and the HIP assertion now
requires it in the empty branch. Verified by temporarily suppressing the
note and watching the scenario go red, then restoring it.

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

rominf commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

🔴 Automated review · pr-review-watcher · e33f8ee

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 changes rocm examine to report every libamd_comgr copy it finds, in loader search order, and to say which one would load. The managed-runtime library-dir search is now shared with managed_sdk_ld_library_path. Outcome: Needs work. Three new unit tests are not gated for Windows and fail there, and the new GPU scenario examine-19 cannot pass when it runs. Reviewed: the whole change (main...e33f8ee7, one commit, 4 files: crates/rocm-core/src/{examine.rs,lib.rs}, tests/e2e-cucumber/features/examine.feature, tests/e2e-cucumber/tests/e2e/examine_steps.rs), plus its blast radius: active_managed_framework_interpreter, ROCM_SDK_PROBE_SCRIPT, the lemonade LD_LIBRARY_PATH prepend, and feature_naming.rs. This was a full pass rather than a follow-up, because the previously reviewed commit c5c15f73 is gone after the force-push. Verified: on Linux, cargo test -p rocm-core --lib examine:: passes (63 tests) and cargo clippy -p rocm-core -p e2e-cucumber --all-targets -D warnings is clean. Tracing the code confirms that serve prepends library_entries onto LD_LIBRARY_PATH, which justifies ranking active-runtime first, and that the probe writes nothing. The Windows and GPU lanes were not run locally. CI's windows-build-and-test on this head fails on exactly the three tests in blocking item 1, with the assertion values that item predicts. Blocking: 2 · Non-blocking: 11.

🚫 Blocking (must fix before merge)

  1. crates/rocm-core/src/examine.rs:3584, :3620, :3650: three new tests fail on Windows, and they are what turns the required windows-build-and-test check red. The tests are the_active_runtimes_own_copy_outranks_the_ambient_environment, ld_library_path_outranks_loader_cache_and_search_dirs and the_multi_copy_note_only_fires_past_one_copy, and none has a #[cfg(unix)] gate.
    • Each one passes a temp_dir() path as the ld_library_path string into find_comgr_copies → libraries_on_ld_path, which splits on ':'.
    • A Windows path like C:\Users\...\Temp\... is split into "C" plus a drive-relative \Users\..., so the "ambient"/"ld"/"only" directory is lost.
    • The assertions then fail: one_copy.len() == 1 gets 0, the first source is loader-cache rather than ld-library-path, and len() == 3 gets 2.
    • The code under test only runs on Linux/WSL (probe_comgr is called only from those branches), so gating the tests loses no coverage.
    • Fix: add #[cfg(unix)] (or #[cfg(target_os = "linux")]) to all three, as the neighbouring LD-path tests at :3149/:3179/:3199 already have.
  2. tests/e2e-cucumber/tests/e2e/examine_steps.rs:1103-1126 (examine-19, examine.feature:270-271), together with crates/rocm-core/src/examine.rs:2252-2321 (find_comgr_copies / record_comgr_copy): examine-19 cannot pass on the lane it is gated to.
    • examine --json passes active_managed_framework_interpreter(...) (apps/rocm/src/main.rs:2695).
    • That interpreter's library_paths come from managed_therock_environment_from_record (lib.rs:4068) and include the SDK probe's recorded library_paths.
    • ROCM_SDK_PROBE_SCRIPT's add_runtime_root (apps/rocm/src/therock.rs:5259-5301) records each package root's lib, including _rocm_sdk_core/lib. That is exactly where managed_comgr_dirs finds the managed copy.
    • find_comgr_copies searches the active-runtime dirs first, and record_comgr_copy dedupes on the canonical path, keeping the first label it saw. So under examine-19's own precondition ("a managed runtime is active"), the copy is always labelled active-runtime and the later managed-runtime hit is dropped.
    • The assertion any(source == "managed-runtime") therefore fails. No unit test feeds find_comgr_copies overlapping dirs.
    • Fix: the scenario's stated intent is to find the CLI-installed copy, so make the step accept either active-runtime or managed-runtime. Better, assert that some copy's real_path lies under the active runtime's recorded site_packages/library_paths. If managed-runtime is meant to mean "a managed runtime other than the active one", say so in the ComgrCopy::source doc and add a unit test with overlapping dirs.

Earlier change request (review 5392128801, filed at c5c15f73)

This round read every file that change request was about.

  • Items 2–4 were re-reviewed and not re-found. The site_packages: None fallback now reads root.parent() (lib.rs:4219-4229), and the fixture uses the installer layout. The commit message no longer claims a HIP copy list. find_comgr_copies, parse_ldconfig_cache_paths and rocm_install_siblings are split out and unit-tested.
  • Item 1 is half addressed. The active runtime's own library dirs are now searched ahead of the ambient environment, so the wheel copy outranks the ld.so.cache system copy.
  • Item 1's second half still holds. It said that directories on no loader path (/opt/rocm*/lib, managed-runtime) can also become "would load". find_comgr_copies (examine.rs:2252-2290) still selects copies.first() whatever its source. On a host with no active runtime, nothing on LD_LIBRARY_PATH and no cache entry, the first /opt/rocm*/lib or managed-runtime hit is still set as comgr_selected and named in "N copies … X would load", even though no loader consults that directory.
    • Fix: either select only from the active-runtime/ld-library-path/loader-cache sources and leave comgr_selected empty otherwise, or word the note so it does not claim a load for a search-dir hit.
    • That change request therefore stays standing.

Non-blocking

  • crates/rocm-core/src/examine.rs:466-468: the comment says probe_comgr must run after probe_env because it reads the LD_LIBRARY_PATH that probe_env stored. It doesn't: probe_comgr reads the env var itself (:2229). Its only real ordering dependency is e.rocm_path from probe_rocm_install.
  • crates/rocm-core/src/examine.rs:2380-2387: parse_ldconfig_cache_paths matches with line.contains(prefix) across the whole line, including the => /path part, while the directory scan uses starts_with on the file name. Matching only the soname token before the first space would make the two consistent.
  • crates/rocm-core/src/examine.rs:2380-2387: ldconfig -p output is not filtered by architecture, so a libc6 (32-bit) entry would be recorded and could be selected if it comes first. Low risk, but the "emulates the loader" doc should mention the gap.
  • crates/rocm-core/src/lib.rs:4204-4229: root is itself a _rocm_sdk_* directory under the scanned parent, so its lib dirs are pushed twice into the SDK-tool LD_LIBRARY_PATH. The Some(site_packages) branch already did this, and the new None fallback now does too. Harmless, but a dedupe (as push_existing_runtime_path does) would be cleaner.
  • crates/rocm-core/src/lib.rs:4168-4189: managed_sdk_ld_library_path behaves differently when site_packages is None: it now also adds the parent's sibling packages. No test exercises that function directly; the change is only covered through managed_comgr_dirs.
  • crates/rocm-core/src/lib.rs:4252: collect_sdk_library_paths was made pub(crate) but is only called inside lib.rs, so it can go back to private.
  • crates/rocm-core/src/lib.rs:4191-4198: the doc says "One description of the layout … every search shares it", but collect_runtime_environment_paths (the served-engine path) still restates the layout without the sibling scan, as the commit body admits. Name the two actual callers instead.
  • tests/e2e-cucumber/features/examine.feature:176: this deletes an unrelated blank line between examine-15 and the examine-16 comment block; every other scenario boundary in the file keeps one.
  • crates/rocm-core/src/examine.rs:3145, :3175, :3195: the same 4-line "Unix-only" comment is pasted above three tests. One shared comment would do.
  • Commit message: about half of it narrates review rounds ("Review: four more issues…", "Fix CI…", the renumbering history) rather than describing the change. The repo squash-merges, so that history ends up permanently in the final commit. Rewrite it as a description of the change.
  • crates/rocm-core/src/examine.rs:2181-2420: the new functions are inconsistent about paths. Some use fully qualified std::path::Path/PathBuf, while probe_comgr and find_comgr_copies, added in the same diff, use the imported names.

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Automated review · pr-review-watcher · c5c15f7

Change request filed by the automated review round at this commit. The full round, including the detail for each item and the non-blocking notes, is in the comment posted alongside it. It will be withdrawn by the automation once these are addressed.

🚫 Blocking (must fix before merge)

  1. crates/rocm-core/src/examine.rs:2214-2244: comgr_selected and the "N copies … X would load" note name the ld.so.cache system copy when a managed wheel runtime is present and the shell's LD_LIBRARY_PATH is empty. The processes the CLI launches against that runtime prepend its library_entries (engines/lemonade/src/process.rs ~L220-229), so the wheel copy is what actually loads. Directories that are not on any loader path (/opt/rocm*/lib, managed-runtime) can also become "would load". Restrict the selection to real loader-path copies, or compute it under the managed runtime's environment, and pin the chosen behaviour with a unit test.
  2. crates/rocm-core/src/lib.rs:4159-4168: the site_packages: None fallback scans root/lib{,64}/*/site-packages. The installer records root_path as <site-packages>/_rocm_sdk_devel (apps/rocm/src/therock.rs:5279), so that path never exists. The test fixture (examine.rs:3383) uses a venv-root layout the installer never produces. Either fix the fallback (use root.parent() for _rocm_sdk_* roots) and build the fixture from the real shape, or drop the fallback and its test.
  3. The commit message, which is the PR's only description, claims things the diff does not do: a HIP copy list, a search that works with no registry record, and a single shared description of the loader path that launched processes use. Make the message match the code.
  4. probe_comgr, comgr_paths_in_loader_cache and comgr_search_dirs (examine.rs:2214-2371) have no tests. Source ordering, ldconfig -p parsing, the /opt/rocm* scan, the selection and the multi-copy note can all break with every test still green. Split out a pure function and unit-test it.

@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch from c5c15f7 to a6d7af6 Compare October 2, 2026 13:10

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and found one substantive issue plus some description and structure notes, left inline below.

The substantive one: the site_packages: None fallback is unreachable, and its directory shape could not match even if it were reached. It ships a seven-line comment explaining that it rescues a host whose registry is missing or stale, which it cannot do, since a missing registry produces zero candidates upstream. The test that appears to cover it only passes because its fixture inverts the real parent/child relation, and the description offers those tests as the substitute for e2e coverage.

The rest: collect_libraries_in_dir now sorts, which can change which library the HIP probe picks, and the description says that field is unchanged. The visibility change is four symbols and two struct fields rather than one helper. And examine-19 looks fragile on a healthy host.

Things I checked and did not flag: the #[serde(default)] justification holds, mid-struct field insertion is fine here, the probe_env HIP rewrite is equivalent, and the @requires-os:linux tagging is right.

paths: &mut Vec<PathBuf>,
) {
collect_sdk_library_paths(root, paths);
match site_packages {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The None arm of this match is unreachable, and its directory shape could not match if it were reached.

Unreachable: ROCM_SDK_PROBE_SCRIPT sets site_packages unconditionally, outside the try, at apps/rocm/src/therock.rs:5228, and has done since the initial import 837067fd. No candidate reaches here with None.

Could not match anyway: root_path is already inside site-packages, pinned by the probe's own contract test at apps/rocm/src/therock.rs:9696-9698. So the fallback's root/lib/<python>/site-packages is a directory no managed runtime has.

The seven-line comment above it says it rescues "a host whose registry is missing or stale". It cannot: a missing registry yields zero candidates at lib.rs:4060, so there is nothing for this arm to run on.

Deleting the arm and taking site_packages: &Path would make the signature say what is actually true.

Comment thread crates/rocm-core/src/examine.rs Outdated
/// nothing wrong rather than a machine we failed to inspect.
#[cfg(target_os = "linux")]
#[test]
fn a_wheel_runtime_with_no_registry_record_is_still_found() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only looks like it exercises the fallback above. Its fixture puts site_packages under root_path, which is the reverse of the real relation, where root_path lives inside site-packages (pinned at apps/rocm/src/therock.rs:9696-9698).

That matters more than usual here because the description offers these unit tests as the substitute for e2e coverage of this path. Right now they certify a shape production never produces.

/// An unreadable directory contributes nothing and is not an error: the library
/// path routinely names directories that do not exist, and a probe that failed
/// on one would report nothing about the machine it was asked to describe.
fn collect_libraries_in_dir(dir: &std::path::Path, prefix: &str, found: &mut Vec<String>) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect_libraries_in_dir now sorts its results, so the HIP probe taking "the first hit" can select a different library than before on a directory holding more than one match. The description says "The HIP probe still takes the first hit and its field is unchanged", which understates this: the mechanism is unchanged, the outcome need not be.

Worth either stating the ordering change in the description or confirming explicitly that no multi-match directory exists in practice.

/// not become a second change to the wire contract, and so a consumer can
/// tell "not yet answered" from "answered no".
#[serde(default)]
pub comgr_matches_runtime: Option<bool>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comgr_matches_runtime is always None as of this PR, with the consumer arriving in #385. That is a reasonable way to split a stack, it just means this field ships inert, and anything reading rocm examine --json in between sees a null that means "not computed" rather than "no mismatch". Worth a line in the description.

Two structural notes while here. The description says "One existing helper was made pub(crate)", but it is four symbols plus two struct fields. And flattening TheRockSdkProbeCandidate into anonymous tuples is what forces those exports: the fields travel together everywhere, so keeping the struct and passing it whole would cost fewer exports and read better at the call sites.

Last one, minor: roughly 60 new lines go into lib.rs, which is already past 13,000 lines. AGENTS.md section 6 asks for new subsystems to get their own file, and while this is an extension rather than a new subsystem, the managed-runtime path-collection helpers now form a coherent group that would sit well on its own.

# still runs on the WSL lane, where the managed runtime really does carry a
# `.so`.
@id:examine-finds-the-managed-runtimes-own-compilation-library @requires-gpu @requires-os:linux
Scenario: examine-19 - The inspection finds the compilation library the CLI installed itself

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scenario looks fragile on a healthy host. Dedupe keeps the first label, so if the managed runtime's library directory is on LD_LIBRARY_PATH, which is a normal state after serving, the copy is labelled ld-library-path rather than as the managed runtime and the scenario fails.

Separately, the description's e2e count of 15 does not reconcile with the 19 scenarios in the file.

@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch from a6d7af6 to e33f8ee Compare October 2, 2026 14:07
volen-silo added a commit that referenced this pull request Oct 2, 2026
…s own

Builds on the copy search: with every copy of libamd_comgr known, the
catalog can say when the one that loads belongs to a different
installation than the HIP runtime that loads, and device code
compilation therefore fails with an error naming neither.

The rule is symmetric, and that is what makes it safe. The design note
for this entry proposed a special case -- "when the active runtime is
the managed runtime, the matching copy is the wheel copy" -- without
which it "fires on every healthy CLI installation". That is a patch over
a rule stated asymmetrically. Asking one question of both libraries
instead, does the libamd_comgr that would load come from the same
installation as the libamdhip64 that would load, makes every case fall
out of the rule: a healthy managed install takes both from the managed
runtime, so nothing differs and nothing is reported. A control that has
to be written as an exception is a rule that has not been stated
correctly yet.

That turns on attributing a copy to its installation correctly. A
managed runtime spreads its libraries across separate _rocm_sdk_*
packages that sit beside each other under one site-packages, not nested
under the runtime's own root at all, so ownership is decided by looking a
found file's directory up against the same directories
install_library_dirs already recorded for each root -- never by walking
up from the file, which would call each package its own install and fire
on the most common install we ship. That is the property the mutation
check targets.

Two further guards on firing: an unattributed copy is a gap in what the
search knows rather than a finding about the machine, and the runtime
must actually ship a copy of its own, or there is nothing to point the
user at.

The entry is print-only and ranks neither remedy. Removing a stack and
reordering the search path can each break a working Python environment,
and which is right depends on which stack the user means to keep.

The finding says out loud that it describes the environment outside the
managed runtimes. `rocm serve` puts a managed runtime's libraries first
on purpose and gets a different, correct answer, and a report that did
not name the environment it examined would read as a claim about one it
never looked at.

The rule above is now stated exactly once, as `comgr_matches_runtime` in
examine.rs, and both this entry and `rocm examine --json`'s field of the
same name call it rather than each restating the conditions -- so the
two surfaces cannot disagree about one machine, and a regression test
pins that. Fix option (b)'s advice matched an earlier, asymmetric
statement of the rule; it now says to prefer the wheel's own copy of
both libraries, which is what the symmetric rule actually asks the user
to do. The machine-readable inspection now carries the same
`install_root` attribution for the HIP runtime side that it already
carried for the code object manager side, since the conflict question is
about both libraries and a reader could not previously check either the
attribution or the selection for one of them.

Known roots are deduplicated across the whole list, not only where
repeats happen to land next to each other. `dedup_by` drops consecutive
duplicates, and these roots arrive from three independent sources -- the
active install, a sorted /opt scan, and the managed runtimes -- so the
active install colliding with a sibling was caught or missed depending on
where the path sorted. A root recorded twice can make a machine holding
one stack read as a machine holding two, which is the false conflict this
entry exists not to report.

hip_paths and hip_selected are `serde(default)` for the same reason the
comgr fields are: an examination read back over the remote path may have
been produced by an older CLI that never wrote them.

Once a managed runtime is found, its root resolves to its
`_rocm_sdk_devel` package directory, and the search used to guess a venv
layout (`root/lib/<python>/site-packages`) to find its sibling packages;
that guess does not hold for a real install, so the search now reads
back the library directories the SDK probe already recorded
(`library_paths`) instead of re-deriving them. This generalises the
walker introduced in #384 (`find_library_copies`/`install_library_dirs`)
so it serves the HIP side too, which is why `managed_runtime_roots` now
returns `library_paths` plural rather than a single `site_packages`
guess.

The AppPaths::discover data-dir fix and the @requires-os:linux platform
tags this commit originally carried for examine-17/18 belong with the
scenario they correct, which #384 introduced -- moved there so the
parent doesn't depend on the child to pass its own CI, and this commit
now only rebases on top of that fix rather than restating it.

Sweeping the same gap once it was visible: examine-17 (the
host-independent code-object-manager scenario, renumbered by #384's
rebase) asserts `hip_paths`/`hip_selected` the same way it asserts
`comgr_paths`/`comgr_selected`, and the empty branch had the identical
problem #384's review flagged for the comgr side -- `hip_paths: []` /
`hip_selected: null` also hold by nothing more than `Examination`'s own
defaults, so the assertion could not tell "probed, found none" from
"never probed". `probe_comgr` now pushes a "no libamdhip64 found" note
the same way it already did for libamd_comgr, and the HIP assertion now
requires it in the empty branch. Verified by temporarily suppressing the
note and watching the scenario go red, then restoring it.

Review: the attribution half still did not survive a realistic managed
runtime. `known_install_roots` registers a managed runtime by
`candidate.root_path` alone -- `_rocm_sdk_devel` when the `devel` extra
is installed -- and `libamd_comgr`/`libamdhip64` live in the sibling
`_rocm_sdk_core` package, which sits *beside* `root_path`, not under it.
A string-prefix match against the bare root therefore attributed nothing
on exactly this shape: both libraries came back `install_root: ""`,
`comgr_matches_runtime` hit its empty-root guard and returned `None`
instead of `true`, and `check_18_comgr_conflict` could not fire for that
runtime at all. The two unit tests meant to catch this
(`a_library_deep_inside_a_managed_runtime_belongs_to_the_runtime`,
`a_healthy_managed_installation_raises_no_report`) built their fixtures
from a venv root (`/data/runtimes/therock/default`,
`.../lib/python3.12/site-packages/_rocm_sdk_core/...`) that production
never produces, so they passed regardless.

Fixed at the root: `install_library_dirs` already walks, for every known
root, exactly the directories that root's copies live in (that is the
discovery-side fix this commit already made). `find_library_copies` now
builds a directory -> owning-root map from that same walk and attributes
each found file by looking its directory up in it, rather than by
string-prefix match against the bare root. A sibling package's directory
is simply one more entry in the map, keyed to the runtime that recorded
it, so it attributes correctly without being nested under anything.
`owning_install_root` and `record_library_copy` take that map instead of
the bare root list; `the_nearest_enclosing_installation_claims_a_library`
is removed because the "longest prefix wins" ambiguity it guarded against
cannot arise under exact directory lookup. Both fixture tests above were
rebuilt from the real installer shape (sibling `_rocm_sdk_*` packages
under one `site-packages`, matching `apps/rocm/src/therock.rs`'s
`ROCM_SDK_PROBE_SCRIPT`), and
`a_library_deep_inside_a_managed_runtime_belongs_to_the_runtime` was
confirmed to fail against the old string-prefix attribution before
passing against the fix.

`find_library_copies` also gained the `active_runtime_dirs` parameter
#384's review added to the comgr-only search: the active managed
runtime's own library directories are now searched first, ahead of the
ambient `LD_LIBRARY_PATH`, for both comgr and HIP, since `rocm serve`
prepends them for both libraries equally. The loader cache and the
directory walk are now gathered once in `probe_comgr` and passed to both
the comgr and HIP searches, rather than each search reading
`LD_LIBRARY_PATH`, spawning `ldconfig -p`, and walking every managed
runtime's directories again on every invocation -- neither depends on
which library is being searched for, so the duplication cost an extra
subprocess and directory walk on every `rocm examine`/`rocm diagnose`
for no reason. `multi_copy_note` is split out as its own pure function
so the "N copies ... would load" note stays independently testable now
that `find_library_copies` only returns copies.

Two doc-comment splices, both from a missing blank `///` line between two
unrelated items: `check_18_comgr_conflict`'s doc paragraph had absorbed
check_17's above it, leaving check_17 undocumented; the same shape left
`known_install_roots` undocumented under a paragraph describing
`dedup_roots_keeping_first`. Both are split back onto the function they
actually describe.

`docs/wsl.md` said `fix-19-shm-too-small` was the only non-WSL entry that
applies on WSL; registering `check_18_comgr_conflict` for `["linux",
"wsl"]` made that false the moment this entry existed. Updated to name
both.

The "Neither option is recommended..." sentence was stated independently
in `fix.rs`'s static recipe and `diagnose.rs`'s dynamically-generated one,
in slightly different words -- already drifted, and only the `fix.rs`
copy was pinned by an e2e assertion, so the `diagnose.rs` copy could drift
further with nothing to catch it. Both now read
`fix::COMGR_CONFLICT_NEITHER_OPTION_RECOMMENDED`, so they cannot
disagree.

Not changed: `fix.rs`'s option (b) wording and `comgr_matches_runtime`'s
parity with `check_18_comgr_conflict` were flagged in an earlier review
round and were already correct by the time this round started --
verified against the current code rather than re-fixed.

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

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Automated review · pr-review-watcher · e33f8ee

Change request filed by the automated review round at this commit. The detail for each item, and the non-blocking notes, are in the round's report comment. The automation will withdraw this request once these items are addressed. The earlier change request (filed at c5c15f73) also stays standing: part of its item 1 still holds, as the report explains.

🚫 Blocking (must fix before merge)

  1. crates/rocm-core/src/examine.rs:3584, :3620, :3650: the_active_runtimes_own_copy_outranks_the_ambient_environment, ld_library_path_outranks_loader_cache_and_search_dirs and the_multi_copy_note_only_fires_past_one_copy have no #[cfg(unix)] gate.
    • They feed a Windows temp path into a ':'-split LD_LIBRARY_PATH parser, so they fail on the required windows-build-and-test lane. They are the three failures on this head.
    • Fix: gate them #[cfg(unix)], like the neighbouring LD-path tests.
  2. examine-19 (tests/e2e-cucumber/tests/e2e/examine_steps.rs:1103-1126) cannot pass on its GPU lane.
    • With a managed runtime active, its _rocm_sdk_core/lib is already among the active interpreter's library_paths. So find_comgr_copies labels the copy active-runtime, and dedupes away the later managed-runtime hit the step asserts on.
    • Fix: assert that the CLI-installed copy is found, whichever of those two labels it carries, or pin the intended label semantics with a unit test over overlapping dirs.

… first

HIP compiles device code at run time through libamd_comgr, and a machine
can hold more than one copy. The loader picks one. When the copy it picks
does not belong to the active HIP runtime, compilation fails with an
error naming neither the library nor the second copy.

The search stopped at the first match, so the second copy could not be
seen at all. It now records every copy found, in loader search order.
Attributing each copy to the installation that owns it, and doing the
same search for the HIP runtime so the two can be compared, is deferred;
comgr_matches_runtime stays permanently unset until that lands.

The managed search now reads the runtime registry rather than listing
<data>/runtimes on disk. A directory listing yields a root and nothing
else, and the root alone does not locate a wheel runtime's libraries --
those live in a sibling _rocm_sdk_* package inside the interpreter's
site-packages, which is why `managed_sdk_ld_library_path` (the
environment the CLI builds to run an SDK tool, such as
rocm_agent_enumerator, inside a managed runtime) walks that directory.
Searching the root alone therefore missed the copy this CLI installs
itself, which is the case the entry exists for.

That knowledge now lives in one place for this search:
collect_managed_runtime_library_paths is shared by the comgr search here
and by managed_sdk_ld_library_path, so that one layout is described once
there rather than twice. A third description, collect_runtime_environment_paths,
still builds the environment a served engine process gets; unifying all
three is follow-up work, not part of this search.

Listing the directory was also a second answer to "which runtimes exist",
where the registry is the first one, and only the registry records which
site-packages an interpreter uses.

The record is preferred but not required. A read-only probe cannot depend
on one being present and current, and a host whose registry is missing or
stale is exactly the kind this command is called on: reporting no copies
there would read as a machine with nothing wrong rather than one we
failed to inspect. With no record, root's own `_rocm_sdk_*` name is read
to find its parent -- the site-packages directory its siblings share --
and a miss costs a directory that is simply not reported.

No dlopen. Reading the version through amd_comgr_get_version would run an
unknown library's initialisers on a machine called on precisely because
something is already wrong, and load a possibly conflicting HIP stack
permanently into the process. The version is read from the versioned
soname instead, and a name carrying none yields an empty version rather
than a confident wrong one.

Directory matches are now sorted, which makes the HIP probe's tie-break
deterministic where one directory holds several matching files. It used
to take whatever read_dir yielded first. Better, but a change, not a
no-op.

The loader-cache tier used to gate on which("ldconfig"), which only walks
$PATH -- and ldconfig lives in /sbin, off a non-root user's PATH on Debian
and derivatives. That silently dropped the one tier that finds a copy
registered only in ld.so.cache; it now reuses the fallback search
ldconfig_cache() already does for the same reason.

Covered at two levels, because neither is sufficient alone. Unit tests
build a wheel-format layout directly, which proves the search understands
a layout we described; only a real managed runtime proves it matches the
one the installer produces, so examine-19 asserts that on a GPU lane.

The four new fields are `serde(default)`. This structure is read back
from another machine: `rocm remote doctor` deserializes an examination
the remote's own CLI produced, and that CLI may predate these fields.
Without a default, adding one here refuses every remote running an older
build, reported as "the remote CLI is probably a different version" --
true, and useless, since the older CLI is the one that cannot be changed.

Fix CI: managed_runtime_roots resolved the data directory through
crate::runtime::default_data_dir (`$HOME/.rocm` or the OS default)
instead of AppPaths::discover, so it disagreed with the rest of the CLI
and found nothing at all on any host where ROCM_CLI_DATA_DIR relocates
the data directory -- which is exactly what every GPU e2e lane sets for
isolation. That is why the new GPU scenario
(examine-finds-the-managed-runtimes-own-compilation-library) failed
identically on every GPU family; it now resolves through AppPaths::discover
like everything else in the CLI. Also fixes a needless-collect clippy
lint in the e2e step, and tags that scenario @requires-os:linux: the
search looks only for libamd_comgr by its ELF soname, which native
Windows does not ship.

Rebasing onto main picked up two unrelated scenarios that claimed the same
numbers in turn -- examine-16 (EAI-8950, #444), then examine-17 (EAI-8449,
#434) -- so this commit's two new scenarios are renumbered to examine-18
and examine-19 to keep every scenario number in the file unique.

Review: examine-18 (the host-independent code-object-manager scenario) was
untagged and its empty-result assertion matched `Examination::default()`
alone, so it would pass with `probe_comgr` deleted outright -- on native
Windows, where the probe never runs, that is not a hypothetical. Tagged
@requires-os:linux and the assertion now also requires the "no libamd_comgr
found" note that only a probe which actually ran and came up empty pushes.
Verified the new assertion fails by temporarily suppressing that note and
watching the scenario go red, then restored it.

Review: four more issues. (1) comgr_selected and the multi-copy note named
the ld.so.cache system copy as "would load" whenever a managed wheel
runtime was active but this process's own LD_LIBRARY_PATH was empty --
wrong, because rocm serve/rocm chat prepend the active runtime's own
library directories onto LD_LIBRARY_PATH before launching the engine, so
the wheel copy is what a served process actually loads. probe_comgr now
takes the active runtime's interpreter (the same FrameworkInterpreter
already threaded through probe_framework) and searches its library_paths
first, ahead of the ambient environment; pinned by
the_active_runtimes_own_copy_outranks_the_ambient_environment. (2) The
site_packages: None fallback in collect_managed_runtime_library_paths
guessed a venv layout (root/lib/<python>/site-packages) that the installer
never produces -- root is one of the runtime's own _rocm_sdk_* package
directories, not a venv root, so the guess always found nothing. It now
reads root.parent() instead, which is the site-packages directory the
guessed layout could never reach. The test fixture
(wheel_runtime_on_disk) was built from the same wrong venv shape and so
could not have caught this; rebuilt it from the real installer layout
(ROCM_SDK_PROBE_SCRIPT's root_path), and confirmed
a_wheel_runtime_with_no_registry_record_is_still_found fails against the
old fallback before passing against the fix. (3) probe_comgr,
comgr_paths_in_loader_cache, and comgr_search_dirs had no tests at all, so
the source ordering, the ldconfig -p parsing, the /opt/rocm* scan, the
selection, and the multi-copy note could all break with the rest of the
suite still green. Split the ordering/selection logic into a pure
find_comgr_copies, the ldconfig -p line parsing into
parse_ldconfig_cache_paths, and the /opt scan into rocm_install_siblings
(opt directory taken as a parameter rather than hardcoded, so a test can
point it at a temp folder instead of the real /opt on the machine running
the suite); each is unit-tested directly, and each new test was confirmed
to fail against a deliberately broken version of the function it covers
before being restored. (4) This message previously claimed a HIP copy
list and a single shared description of the loader path launched
processes use; neither matches this diff. Corrected above.

Review: a second round, one automated and one human, reaching the same
place by different routes.

The human reviewer confirmed, independently, that the site_packages:
None fallback added in the prior round could not actually fire:
ROCM_SDK_PROBE_SCRIPT has recorded site_packages unconditionally, outside
its try, since the probe's initial version, so every manifest that
parses at all carries Some there. The test built to cover it could not
have caught that either way, since it called the function directly with
None rather than through a real candidate. Rather than keep a reachable-
looking branch and a comment describing behaviour the code does not
have, the None arm is now a no-op (root's own directories, already
collected unconditionally above, are what a hand-constructed None caller
still gets) and the test is removed. The neighbouring root-format test
is kept and re-targeted: it already called the same function directly
with None, so it is what actually exercises that arm's contract, not the
removed test.

The automated reviewer caught a real regression the first round
introduced: three new unit tests feed a path into the `:`-split
LD_LIBRARY_PATH parser without a unix cfg gate, same as the pattern the
file already uses for that reason, and that broke the required
windows-build-and-test lane. Gated, along with the helper they share
(which would otherwise be unused, and clippy-denied, once its only
callers are gated).

It also found that the active-runtime-priority fix regresses
examine-finds-the-managed-runtimes-own-compilation-library on a healthy
host: once a managed runtime is active, its directory is found first and
labelled active-runtime, and the dedup this search has always done on
resolved path then drops the later managed-runtime hit for the identical
file -- the scenario's step asserted on the label that fix makes
unreachable. The step now accepts either label as proof the CLI's own
installed copy was found, and a new unit test
(an_active_runtime_directory_that_is_also_a_managed_root_keeps_one_label)
pins that an overlapping directory keeps exactly one label rather than
being reported twice; confirmed to fail when the active-runtime search is
moved after the others, then restored.

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
@volen-silo
volen-silo force-pushed the feat/report-comgr-copies branch from e33f8ee to ba1d7fb Compare October 2, 2026 15:27
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.

4 participants