feat(diagnose): answer whether a model will run before downloading it - #407
Conversation
28e1d91 to
095c501
Compare
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 095c501
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 rocm diagnose --model <ref>, a pre-download READY/DEGRADED/BLOCKED/UNDETERMINED verdict computed from the curated recipe catalog plus host facts. The core logic and its tests are unusually solid; the defects are all in the host-fact layer that feeds it, where three paths produce a confident verdict from a measurement that does not mean what the code treats it as meaning — outcome: Needs work. Verified: mutation-tested all nine new tests on a throwaway copy (twelve mutations, eleven killed) — the engine is genuinely passed in, not re-derived (re-deriving from recipe.preferred_engines fails the named cross-check test), UNDETERMINED and BLOCKED are separately pinned, declared-alternative filtering is pinned, the renderer's claimed prefix coupling with the diagnosis renderer is accurate, the "8 unit tests" count is exact, and tracing every call on the path (catalog load, config discovery, GPU summary, amd-smi) found only local file reads and local subprocesses, so "fetches nothing" holds; a leak and injection scan over the whole diff was clean. Checks at review time: 24 success, 3 failures, 1 cancelled. Blocking: 3 · Non-blocking: 5.
🚫 Blocking (must fix before merge)
apps/rocm/src/main.rs:2853 — the APU pool is total system RAM, which over-reports what the engine can actually allocate, and over-reporting yields READY for a model that will not fit.
The doc comment three lines above correctly explains that on an APU the allocator draws from GTT-backed system memory rather than the BIOS carve-out — and then supplies detect_system_ram_gib(), the full RAM figure, as that pool. GTT is not system RAM: the amdgpu driver caps the aperture well below total RAM by default, and the rest of RAM is in use by the OS and everything else on the box. This repository already knows the difference — the self-hosted preflight in .github/workflows/e2e-selfhosted.yml was changed on the base branch specifically to stop applying a floor to a pool the engine never draws on, and it measures the GTT total explicitly rather than assuming it equals RAM. A model sized between the real GTT cap and total RAM therefore reads READY here and then fails to allocate when served — precisely the wasted download this feature exists to prevent, on the exact hardware class (gfx1151) the feature's own doc comment uses as its motivating example. Fix: read the GTT pool total the way the preflight does and use that as the UnifiedSystemMemory figure; where no GTT pool can be read, return AcceleratorMemory::Unknown so the answer lands in UNDETERMINED rather than in an optimistic READY. A conservative fraction of RAM is an acceptable fallback if reading GTT is impractical, but the unreadable case must still degrade to Unknown, not to total RAM. Note that a real GTT read does not regress the motivating case — a 22 GiB model on a 128 GiB Strix Halo still fits comfortably inside a default aperture.
apps/rocm/src/main.rs:21146 — a VRAM total of 0 is accepted as a measurement, producing a confident BLOCKED where the honest answer is UNDETERMINED.
parse_gpu_vram_usage requires only that total_vram parse as a u64, and 0 parses. gpu_vram_usage()'s emptiness guard does not catch this because the row exists; host_accelerator_memory then sums to 0.0, vram_capacity_is_meaningful is true for a single discrete card, and the result is AcceleratorMemory::Dedicated(0.0). In the core, measured_gib() returns Some(0.0) rather than None, so the AcceleratorMemoryUnknown branch is skipped entirely and the host is told "this host offers 0 GiB" and BLOCKED. This directly contradicts the module's own stated non-collapse that unmeasured memory is not absent memory, and it is not hypothetical: the same zero-total reading from a wedged driver was observed and explicitly rejected on the workflow side of this repository. Fix: reject non-positive totals in parse_gpu_vram_usage (filter the row, or make a zero total yield None), so an all-zero reading reaches the Unknown path instead of the Dedicated one.
apps/rocm/src/main.rs:2840-2844 — on WSL the Unknown branch is unreachable, so an unmeasurable GPU is reported as no GPU.
The branch distinguishes "no GPU" from "GPU present but unmeasured" using examination.has_amd_gpu. That field is assigned in exactly one place, summarise_gpu_categories, which is called on the Linux and Windows probe paths only; the WSL path returns early before it and never populates gpus at all, so has_amd_gpu stays at its false default on every WSL host regardless of hardware. Consequently, on WSL, whenever amd-smi cannot be read — the case this branch exists for — the verdict is AcceleratorMemory::None, which the core turns into BLOCKED with "no GPU is visible to ROCm… there is no CPU fallback", on a machine whose GPU works. The root cause predates this change, but this is the first caller that converts it into a user-visible wrong verdict, on a platform with dedicated lanes here. Fix inside this diff's own function: when the examination reports WSL, treat an unreadable amd-smi as AcceleratorMemory::Unknown rather than None, since has_amd_gpu carries no information on that path. (A WSL distro with genuinely no passthrough then reports UNDETERMINED instead of BLOCKED — the correct trade, and the safe direction, since it cannot manufacture a READY.)
Non-blocking
apps/rocm/src/main.rs:2882— forcingunsupported_heretoNonehere kills no test: the platform gate is pinned only at the core level from hand-built fixtures, so a regression that stopped the integration from populating it at all would go undetected. One assertion over a Windows-plus-vLLM host closes it.README.md:262— the "Diagnose and fix" section's exhaustive signature still readsrocm diagnose [--symptom TEXT] [--top N] [--json] [--distro [NAME]], omitting--modeleven though the same file documents the flag a few sections later; the contributor rules require the flag surface to move in the same change.apps/rocm/src/main.rs:2745— the--model+--distrorefusal has no coverage at all, neither unit nor scenario, despite being a user-facing error path.tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:1419— the READY scenario's assertions return early when no GPU memory was measured, so on the blocking GPU-less lane the whole scenario degenerates into re-testing the no-GPU path the BLOCKED scenario already covers; READY is proven only on non-blocking lanes.crates/rocm-core/src/model_readiness.rs:11— the feature answers "will this run" but never checks free disk space, although artifact records already carrysize_bytesand a disk-space module exists; a host that fits the model in memory but not on disk still gets READY.
juhovainio
left a comment
There was a problem hiding this comment.
Reviewed this against the PR description. There's already a review here from another reviewer flagging real problems in the host-fact layer (APU memory misclassification, WSL GPU detection) — I checked two of those independently by reading the code and they hold up, so I won't repeat them. Here's what I found on top of that:
The one that matters most: assess_model_on_this_host calls detect_host_gpu_summary(None) for its own GPU read, but a few lines later it separately calls AppPaths::discover() for the config lookup. That means the gfx-target fallback that comes from reading the TheRock SDK manifest (which serve's own engine-selection path uses) never gets applied here. On a host that only resolves its GPU target through that fallback, rocm diagnose --model and rocm serve can end up naming different engines for the same model — which is exactly the inconsistency this PR's own doc comment says assess_model_for_host exists to prevent. Left as an inline comment.
Docs didn't keep up with the new command. README.md got a new section (nice writeup), but docs/testing.md's WSL Preflight section and docs/manual-testing.md weren't touched, even though this adds new observable CLI behavior worth a manual-test entry.
Test count in the PR description doesn't match the diff. It says 3 new tests in apps/rocm; I only found 1 (the_engine_doctor_names_is_the_engine_serve_would_select). Not a big deal, just flagging so the description gets fixed or I'm missing something.
Quiet behavior change. The /model command's VRAM-missing message text changed from pointing at /examine to pointing at amd-smi metric --json. Probably the right call, but it's not called out anywhere in the PR body, and it's a message an existing command already prints today, not new-command scope.
E2E coverage gap. The 3 new scenarios cover BLOCKED, READY, and one flavor of UNDETERMINED, but not DEGRADED or the ModelNotCurated reason — both are verdict words a user can actually see.
Smaller stuff, not blocking: model_readiness.rs's core functions all take the same 4-5 params together (model_ref, catalog, host, engine_for) which might want bundling into a context type down the road; a few of the #[allow(dead_code)] helpers in main.rs got refactored to delegate into model_readiness but are still unreachable — worth double-checking they're actually wired up somewhere; and there's a theoretical edge case where a cpu_only recipe with a declared min_gpu_mem_gb on a true no-GPU host might report Undetermined instead of Blocked, though I couldn't confirm real catalog data hits this.
Requesting changes mainly for the engine-selection consistency bug and the docs gap — the rest is worth a look but not blocking on its own.
| model_ref: &str, | ||
| examination: &rocm_core::Examination, | ||
| ) -> ModelReadiness { | ||
| let host_gpu_summary = detect_host_gpu_summary(None); |
There was a problem hiding this comment.
This calls detect_host_gpu_summary(None) directly, but AppPaths::discover() is resolved a few lines below for the config lookup. serve's engine selection uses the TheRock-SDK-manifest gfx-target fallback that only kicks in when AppPaths is available — this path never gets it, so on a host that depends on that fallback, this can name a different engine than rocm serve would actually pick. Worth calling AppPaths::discover() once up front and threading it into detect_host_gpu_summary too, if that's what unlocks the fallback.
There was a problem hiding this comment.
Fixed: assess_model_on_this_host now calls AppPaths::discover() once and threads the result into both detect_host_gpu_summary and the config lookup (assess_model_with_host_paths), so they can never resolve different paths. A regression test (the_configured_default_engine_reaches_the_readiness_verdict_from_the_same_paths_as_the_gpu_summary) drives both call sites and compares.
|
|
||
| Supported engines: `lemonade`, `vllm`. | ||
|
|
||
| ### Will a model run here? |
There was a problem hiding this comment.
Good writeup here, but docs/testing.md (WSL Preflight section) and docs/manual-testing.md weren't updated to mention rocm diagnose --model, despite it being new observable behavior worth a manual-test entry.
There was a problem hiding this comment.
Fair gap, reporting rather than fixing: docs/testing.md now has a "Model Fit Preflight" section covering --model, but I left docs/manual-testing.md alone. That file's sections are interactive smoke-test walkthroughs for things without automated coverage (SDK install, server records, ComfyUI); diagnose --model already has unit, integration, and 5 e2e scenarios, so there's no manual-only gap to fill there today. Happy to add an entry if you'd rather have it regardless.
| let _ = writeln!( | ||
| output, | ||
| " action: run /examine or refresh GPU telemetry, then retry /model {}", | ||
| " action: make `amd-smi metric --json` work on this host, then retry /model {}", |
There was a problem hiding this comment.
This changes the wording of an existing /model message (VRAM-missing action line, pointing at amd-smi instead of /examine) rather than adding new text. Reasonable change, but it's not mentioned in the PR description and isn't really in scope for a diagnose --model PR — might be worth its own commit/PR note.
There was a problem hiding this comment.
Agreed it's a scope edge, but it's not incidental: the old /examine pointer was actively wrong (that command emits a static snapshot with no VRAM figure), and diagnose --model needed to name the correct remediation (amd-smi metric --json) for the same gap it just introduced — leaving /model pointing at a dead end while the new command pointed at the real fix would have been an inconsistency in the same release. Called out explicitly in the PR description's review-fixes list rather than a separate PR, since it's a one-line, low-risk correction directly motivated by this change.
| /// both real call sites are driven with the same inputs and compared, rather | ||
| /// than either being called with literal arguments. | ||
| #[test] | ||
| fn the_engine_doctor_names_is_the_engine_serve_would_select() { |
There was a problem hiding this comment.
The PR description says 3 new tests were added in apps/rocm, but this is the only new #[test] I could find in this diff. Worth fixing the description, or let me know if I'm missing two more somewhere.
There was a problem hiding this comment.
Description was stale, fixed it. Current diff has 9 new #[test]s in rocm-core/src/model_readiness.rs and 13 in apps/rocm/src/main.rs (both counts grew again from further review rounds since your pass); description now reads '9 unit tests in rocm-core, 13 in apps/rocm, and 5 end-to-end scenarios' to match.
| When the user asks the CLI whether that model would run, in machine-readable form | ||
| Then the CLI reports that it could not determine the answer | ||
| And the reason given is the unreachable catalog, not the model | ||
| And nothing is claimed about whether the model fits this machine |
There was a problem hiding this comment.
These three scenarios cover Blocked, Ready, and one Undetermined reason (unreachable catalog), but not Degraded or UndeterminedReason::ModelNotCurated — both are verdict words a user can see in the actual output. Worth adding at least one scenario for each before merge, per the Gherkin-coverage expectation for new observable behavior.
There was a problem hiding this comment.
Already fixed: scenarios diagnose-24 (UndeterminedReason::ModelNotCurated) and diagnose-25 (Degraded, via a synthetic signed catalog since no built-in recipe can produce it against an arbitrary real host) were added covering exactly these two gaps.
095c501 to
ab289c4
Compare
ab289c4 to
b1bad70
Compare
|
🔴 Automated review · pr-review-watcher · f0a8a68 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. SummaryAdds Previous round
🚫 Blocking (must fix before merge)1. 2. 3. Non-blocking
|
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · b1bad70
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.
Three items block. The full detail is in the review comment; the essentials are below.
1. The APU capacity figure is total system RAM, not the pool an engine can allocate from
apps/rocm/src/main.rs:2938, surfaced at crates/rocm-core/src/model_readiness.rs:412. On the unified-memory path host_accelerator_memory returns UnifiedSystemMemory(detect_system_ram_gib()), and detect_system_ram_gib reads MemTotal — total installed RAM, not free RAM and not the GTT aperture. model_readiness.rs:412 then states that number to the user as fact: "its engine allocates from {X} of GTT-backed system memory, which is the figure compared below".
The repository already knows these are different numbers and already measures the real one: xtask/src/workflow_contract.rs enforces that every APU-class preflight block runs rocm-smi --showmeminfo vram gtt, and its own fixtures pair a small carve-out with a much smaller GTT pool on the same host class this code would report at full installed capacity. The error direction is the dangerous one — a false READY for a model that will not fit, which is exactly the failed multi-gigabyte download this command exists to prevent.
Fix: read the GTT aperture (the same figure the preflight contract already requires) and compare against that; if it cannot be read, return Unknown so the verdict becomes Undetermined rather than an optimistic Ready. At minimum, stop asserting the number as the engine's allocation pool in user-facing evidence.
2. an_apu_is_judged_on_the_memory_its_engine_allocates_from cannot fail for the defect its name claims to catch
crates/rocm-core/src/model_readiness.rs:960. The test hand-constructs AcceleratorMemory::UnifiedSystemMemory(128.0) and asserts the verdict and evidence string. The judgement it names — deciding that this host is an APU and choosing which figure represents its memory — lives in host_accelerator_memory in a different crate, which rocm-core cannot call, so the classifier is structurally unreachable from this test. host_accelerator_memory has no test anywhere, and reverting it to unconditionally return Dedicated(total_gib) — deleting the whole APU behaviour — leaves every test green. That also refutes the description's claim that "each invariant test was checked by mutation: the production code was broken deliberately and the test confirmed to go red"; it does not hold for this one.
Fix: split the pure part of host_accelerator_memory so it takes the parsed VRAM rows and the gfx target as arguments, and test that — asserting both that an APU target yields the unified branch and that a discrete target does not.
3. A new scenario asserts degraded where this PR's own gate returns blocked
tests/e2e-cucumber/tests/e2e/diagnose_steps.rs around line 490, with tests/e2e-cucumber/features/diagnose.feature:362. The synthetic recipe declares "preferred_engines": ["vllm"]. engine_ruled_out_by_platform rules vLLM out on native Windows, and model_readiness.rs:440 returns Blocked on unsupported_here before the RAM-softening branch is reached. The step early-returns only when GPU memory could not be measured, so on a Windows host that measures one it asserts degraded against an actual blocked and fails. Unlike its siblings in the same file, none of the five new scenarios carries a platform or GPU tag, and the file header states these run on every lane.
Fix: declare a platform-neutral engine such as lemonade in the fixture — which preserves the coverage on every platform — rather than tagging the scenario away from Windows.
The non-blocking observations are in the review comment and none of them gate.
b1bad70 to
f0a8a68
Compare
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · f0a8a68
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 rocm diagnose --model <ref>, which answers "will this model run here" from the curated recipe catalog and local host facts without fetching anything, reporting one of four verdicts. Outcome: Needs work — the round-3 blockers are genuinely fixed, but the fix for the APU one left behind an unreachable code path that the module still documents as live, and the new path now gives APU users remediation advice that cannot work. Verified: ran the targeted unit suites for the two touched crates (cargo test -p rocm-core model_readiness and filtered cargo test -p rocm --bin rocm runs) green on scratch copies, then mutation-tested each new test against the exact production line it names — the full test suite and the e2e suite were not run here; independently confirmed that no production code anywhere in the workspace constructs AcceleratorMemory::UnifiedSystemMemory, that HostFacts has exactly one production construction site, that the degraded e2e fixture now declares lemonade, that the catalog loader opens no socket (so the "no network call" claim holds), that the commit carries a DCO sign-off, and that nothing in the diff leaks internal references or contains prompt-injection text; the review worked from check counts success=26, failure=1, pending=1, with the single failure on a GPU hardware lane of a family that is intermittently red on trunk, so it is not attributed to this diff. Blocking: 3 · Non-blocking: 5.
Previous round
- Item 1 (APU capacity figure was total system RAM): resolved.
classify_accelerator_memory(apps/rocm/src/main.rs:2937) now returnsAcceleratorMemory::Unknownfor an APU instead of fabricating a figure. Mutating that arm back to a total-RAM-derivedUnifiedSystemMemoryturnsclassify_accelerator_memory_reports_unknown_not_total_ram_for_an_apured, so the regression is pinned. - Item 2 (the APU test could not fail for the defect it named): partially resolved. The classifier is now split into a pure function with five new unit tests in apps/rocm, which is exactly what was asked for. But the rocm-core test that carried the bad name was left untouched, and the remediation turned it into a test of a branch production can no longer reach — see blocking 2.
- Item 3 (e2e scenario asserted degraded where the platform gate returns blocked): resolved. tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:1600 now declares
"preferred_engines": ["lemonade"], which no lane excludes, so the Windows lane can no longer reach the wrong assertion. - The prior round's non-blocking observations were not re-checked item by item in this round; only the three items the change request itself raised were tracked above.
🚫 Blocking (must fix before merge)
1. crates/rocm-core/src/model_readiness.rs:483-499 — the "could not measure your GPU" remediation is wrong for the machine class this change is about.
On an APU, amd-smi runs fine and returns a figure; classify_accelerator_memory deliberately discards it and returns Unknown because the carve-out is the wrong pool. That lands in the (Some(required), None) arm, which tells the user "the GPU memory on this host could not be read", then hands them a plan of "let the CLI read this host's GPU memory, then ask again", the command amd-smi metric --json, and the note "the reading comes from amd-smi; if it is missing or failing, the ROCm install is what needs attention first". For a Strix Halo user none of that is true: amd-smi is not missing, the ROCm install needs no attention, and re-running the command returns the identical answer forever. This is a user-facing remediation that cannot work, on a platform this project treats as first class, and it blocks because the whole point of the command is that its answer is actionable. AcceleratorMemory::Unknown currently conflates "no telemetry at all" with "telemetry exists but names the wrong pool", so assess cannot tell them apart. Fix: carry the distinction — a second unknown variant, or a fourth UndeterminedReason — and give the APU case its own text saying plainly that this host has no dedicated VRAM and the CLI cannot yet read the pool its engine allocates from, with no command to run for it today. Pin both branches with a test.
2. crates/rocm-core/src/model_readiness.rs:24-25, 82-92, 412-418, 959-982 — UnifiedSystemMemory is unreachable, but the module presents it as the design.
Nothing in the workspace constructs AcceleratorMemory::UnifiedSystemMemory any more: the only production HostFacts site is apps/rocm/src/main.rs:2857, whose classifier now returns Unknown for every APU. Three surfaces still say otherwise. The module doc lists "The memory an APU reports is not the memory its engine allocates from" as the third of "three distinctions [that] carry the design", pointing at the variant. The evidence branch at :412 formats a line ("its engine allocates from {} of GTT-backed system memory, which is the figure compared below") that no user can ever see. And the test an_apu_is_judged_on_the_memory_its_engine_allocates_from at :959 hand-builds the variant and asserts Ready plus that evidence string, under a doc comment saying "the pool the engine actually uses is what gets compared" — which is the opposite of what ships. Mutation confirms the test only pins the measured_gib arm that serves the dead variant; deleting the whole APU classification in apps/rocm does not touch it. This blocks on two counts: it is dead code with live-looking documentation, and a reader arriving at this module will conclude APU hosts are judged on a GTT figure, which is the confusion the round-3 finding already cost a round to unpick and which will recur. Fix: delete the variant, the evidence branch, the doc bullet and that test, and say in the module doc that the APU pool is not readable today so those hosts report Unknown; if the variant is kept for a near-term reader, the doc and the test name must state that no production path constructs it.
3. apps/rocm/src/main.rs:2993-2998 — the platform-gate wiring has no test on any platform.
The PR body raises this to a design guarantee: "An engine ruled out by the platform blocks the model. It is not silently swapped for another engine." The code that delivers it is the unsupported_here closure in assess_model_for_host, and deleting it outright — making unsupported_here unconditionally None — leaves every test in both crates green. The rocm-core test an_engine_the_platform_rules_out_blocks_a_model_that_would_otherwise_fit (crates/rocm-core/src/model_readiness.rs:1010) injects unsupported_here: Some(...) as a literal, so it proves rocm-core's handling of the flag and never touches the code that sets it; and engine_ruled_out_by_platform itself short-circuits on runtime_is_windows(), so gutting it to always-false also survives on Linux. The PR's own limitation note ("the native-Windows engine gate is covered by a unit test that injects the condition") reads as coverage of this and is the disclosure being measured here — it is inaccurate in the direction that makes the change look safer. Fix, platform-independently: extract the closure body into a pure helper taking the selection and a ruled-out boolean, and assert both branches — that a ruled-out engine yields a populated unsupported_here naming that engine, and that an allowed one yields None.
Non-blocking
crates/rocm-core/src/model_readiness.rs:644-647, 672, 697— of the four verdict labels onlyBLOCKEDsurvives a full-string mutation;READY,DEGRADEDandUNDETERMINEDare unpinned, as are the" - "evidence prefix and the" verify after fix: "prefix, despite the renderer's doc calling those prefixes the contract that stops a second report shape drifting.crates/rocm-core/src/model_readiness.rs:217-239, 597-620— the declared-alternatives-first ordering, the cap of three fallbacks, and the Ready-before-Degraded two-pass all survive mutation untouched; only thefitsfilter the PR body highlights is actually pinned.apps/rocm/src/main.rs:2776-2791— the--modelplus--distrorefusal is disclosed as untestable, butinspected_remotelyis a pure function of the examination; extracting it into a named helper makes the guard unit-testable with a constructedExaminationand no WSL host at all.docs/testing.md(new "Model Fit Preflight" section) — "The e2e suite ... exercises all four verdicts" now holds only on a lane with a discrete GPU: after the APU change an APU lane reports no measurement, so the ready half of diagnose-22 and the degraded half of diagnose-25 both early-return there.tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:1529-1547—workspace_rootandxtask_commandare copy-pasted from artifact_steps.rs rather than hoisted into the shared e2e module the way the other cross-file helpers are.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · f0a8a68
Requesting changes on three defects found at this head. The full round, with the evidence and the previous round's status, is in the review comment posted alongside this.
1. The "could not measure your GPU" remediation cannot work on the machine class this change is about. On an APU the telemetry tool runs fine and returns a figure; the classifier deliberately discards it because it names the wrong memory pool, and the result lands in the branch that tells the user the GPU memory could not be read and hands them a command to run, plus a note that the install is what needs attention. None of that is true there: the tool is present, the install is fine, and re-running returns the identical answer forever. The unknown state currently conflates "no telemetry at all" with "telemetry exists but names the wrong pool", so the assessment cannot tell them apart. Carry the distinction and give that case its own text, with both branches pinned by a test.
2. A memory variant is unreachable while three surfaces still present it as the design. No production path constructs it any more. The module doc still lists the distinction it represents as one of three that "carry the design"; an evidence branch formats a line no user can ever see; and a test hand-builds the variant and asserts a verdict under a doc comment saying the pool the engine actually uses is what gets compared, which is the opposite of what ships. Deleting the whole APU classification in the application does not touch that test. This is dead code with live-looking documentation, and it will lead the next reader to exactly the wrong conclusion — the same confusion the previous round already spent a round unpicking.
3. The platform gate that the description raises to a guarantee has no test on any platform. The description states that an engine ruled out by the platform blocks the model and is not silently swapped for another. Deleting the code that delivers that, so the ruled-out condition is never set, leaves every test in both crates green: the unit test that looks like coverage injects the condition as a literal and never reaches the code that sets it, and the setter short-circuits on a platform check, so gutting it survives elsewhere too. The description's own limitation note reads as coverage of this and is inaccurate in the direction that makes the change look safer. Extract the condition into a pure helper and assert both branches.
Item 3, and the residue carried over from the previous round's item 2, are test-coverage defects rather than behaviour defects, and they gate for the reason a weak test always does: nobody reopens a landed change to strengthen one.
f0a8a68 to
bade1c9
Compare
|
Addressed all 3 blocking items from the automated review at f0a8a68:
The two active reviews from this bot (the CHANGES_REQUESTED one, and this COMMENTED one) are being closed out — dismissing the former; this comment stands in for the latter since GitHub's API refuses to dismiss a COMMENTED-state review. On the 5 non-blocking items also raised here: reporting rather than fixing, since none are behavior defects or unfailable tests, only coverage/completeness observations —
Also fixed, from juhovainio's review: the PR description's stale test count (now 9 rocm-core / 13 apps/rocm / 5 e2e, replied inline with the count breakdown). |
|
🔴 Automated review · pr-review-watcher · bade1c9 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. SummaryAdds 🚫 Blocking (must fix before merge)
Non-blocking
|
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · bade1c9
Requesting changes at this head. One blocking finding; the full round, including the five non-blocking items and what was and was not verified, is in the review comment posted alongside this.
🚫 Blocking (must fix before merge)
tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:1326-1335 — the scenarios assert the one undetermined reason this change stopped producing on APU hosts, so on the project's own APU lanes three scenarios fail on a correct verdict.
The "undetermined" arm of assert_unmeasured_machine_is_told_why is an assert_eq! against the literal "accelerator_memory_unknown". This head introduces a second reason — the APU case where telemetry exists but names the wrong pool — and routes every single-GPU host whose gfx target is in the APU family (gfx115x) to it: the classifier at apps/rocm/src/main.rs:2972 returns the unified-unreadable variant, which reports no measured figure, so the step's measured_gpu_gib guard does not return early and control reaches that arm with "unified_memory_unreadable". The step is used by diagnose-21 (tests/e2e-cucumber/features/diagnose.feature:315), diagnose-22 (:330) and diagnose-25 (:367), none of which carries a platform or hardware tag — unlike their tagged siblings in the same file — and the repository runs three dedicated gfx115x APU lanes on the full suite. So the change's own correct output is what makes its own scenarios red, on exactly the hardware class the change was reworked to serve; that is also the most plausible reading of the two red checks. The same omission has a second, quieter face: the step comment two lines above enumerates only two machines ("no GPU at all" / "a GPU whose memory it could not read"), and the fixture doc at tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:1143-1145 still claims any machine that can measure a GPU at all can serve the smallest recipe, "so the ready half of diagnose-21 holds on every GPU lane" — a prose guarantee this change made false. Fix: accept both reasons in that arm and assert something reason-specific for each — for the unified case, that the evidence names the absence of dedicated VRAM and that no fix plan is attached, mirroring the unit test at crates/rocm-core/src/model_readiness.rs:988; then correct the step comment and the fixture doc to name the third machine class. Running the adversarial pass over that fix: simply widening the assertion to a set membership would be the naive version and would NOT hold — it would let a future regression that collapses the two reasons back together pass silently, which is precisely the distinction this round of work exists to create, so the per-reason assertion is load-bearing rather than decoration. Separately, this is a fix applied to one of two sites: the new reason reached the core, the classifier and the unit tests but not the scenario layer, so it is worth grepping for any other place that enumerates undetermined reasons before landing.
Checks at review time: success=25, failure=2, pending=1. The two red checks are judged plausibly this change's own, for the reason above; mapping them to particular jobs is inference and is not confirmed here.
The three blocking items from our previous round are genuinely discharged -- this request is about the new gap the latest remediation opened, not a re-statement of them.
bade1c9 to
b6b874e
Compare
|
Addressed the blocking finding at bade1c9 (now b6b874e).
This is deliberately not the naive set-membership widening the review called out — a mutation that collapses the two reasons back together (correct reason string, but evidence/fix reverted to the other shape) fails this assertion. I verified that directly: copied the reason-specific match arm into a standalone scratch crate (outside the repo, discarded after use) with plain Also fixed the two stale-prose spots: the step's own two-machine comment and the Grepped the repo for every other site enumerating Verification at the new head:
Left alone: the pre-existing |
|
🔴 Automated review · pr-review-watcher · b6b874e 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. SummaryAdds Prior round
🚫 Blocking (must fix before merge)
The arm hard-asserts Fix: give the Non-blocking
|
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · b6b874e
Requesting changes on one item. The previous round's blocking item is genuinely and well discharged — the per-reason arms are exactly the form that round asked for rather than the naive widening it warned against. The full round, including four claims of ours that this round examined (two of which were wrong, and are corrected there), is in the comment posted alongside this.
The fix went into one of two arms, so the same defect shape survives one branch over (tests/e2e-cucumber/tests/e2e/diagnose_steps.rs, the blocked arm of the unmeasured-machine step, around lines 1324-1336).
The blocked arm hard-asserts that the evidence contains "no GPU is visible to ROCm", on the premise that the only way a host with no measurement reaches a blocked verdict is an absent GPU. This diff makes that premise false. In the readiness module the engine gate is deliberately placed before the memory gate and returns blocked early with its own, different evidence, naming an engine that has no adapter on native Windows and suggesting WSL or Linux instead.
Traced end to end: the scenario's subject declares only one engine and has no alternate recipe, so with no explicit or configured engine it resolves to that engine; the platform gate rules it out whenever the runtime is Windows; on an integrated GPU the memory classifier returns the unified-unreadable state, which reports no figure, so the step's opening early-return does not fire and control reaches this arm carrying the engine evidence. The sibling assertion still passes; the evidence-string assertion fails. The scenario is untagged, consistent with its host-agnostic siblings — that is not itself the defect — and no expectations entry exempts it, so this is an unexpected failure on a lane the project runs. Two neighbouring scenarios are unaffected, because their subjects use a recipe the gate does not rule out.
This is also the strongest available explanation for the one red check on this head. Mapping it to a particular named job is inference and cannot be confirmed from here.
Why it gates. It is the hard-blocker shape twice over: an assertion that now fails for a correct verdict, and a closed enumeration of causes that a production change has outgrown without anything forcing it to be revisited. It is the second time a new cause has reached production and the unit tests but not the scenario layer.
Suggested fix. Give the blocked arm the same per-cause shape the undetermined arm now has: discriminate the two causes and assert something cause-specific for each — the GPU-visibility refusal must name the absent GPU and carry its fix; the engine refusal must name the engine it ruled out and offer the other-platform remedy — keeping the no-figure assertion common to both. Correct the scenario's own comment, which still enumerates only two halves.
We ran the same adversarial pass over that remedy as over the finding, and it changed the answer: the tempting shortcut of widening to an either-string match would not hold. It would let a future change that collapses one refusal into the other, or that emits either message for the wrong cause, pass silently — which is precisely the distinction the engine gate's own ordering comment exists to create. Discriminating first and asserting per cause is load-bearing, not decoration.
Worth grepping for any other closed enumeration of verdict causes before landing.
b6b874e to
c63f0fd
Compare
|
Fixed at c63f0fd (amended into the one existing commit, force-pushed). What changed (
The I did not widen to an either-string match. I wrote a standalone throwaway harness (outside the repo, since the
Other enumeration sites: grepped the whole repo for Does this explain the red Verified: Dismissing now that the fix is confirmed present at the new head. |
Addressed at c63f0fd: the blocked arm now discriminates both causes (fix.summary) and asserts cause-specific content for each, verified by a mutation harness; the scenario comment is corrected; no other enumeration site found. See PR comment for details.
juhovainio
left a comment
There was a problem hiding this comment.
I reviewed this PR and found several issues worth fixing before merge, left inline below.
The most important one: on a multi-GPU host the VRAM figures are summed into a single number and compared against the model's minimum. Since rocm serve pins exactly one ordinal, that reports READY for models that will OOM on every individual card. That lands in the direction this PR's own description warns against, "a verdict that pretended otherwise would be wrong in the user's favour".
Two more that undercut stated goals: the rewritten /model remediation points at a command that cannot clear the condition that printed it, because every production caller discards the reading before the function sees it. And the new --model plus --distro refusal keys on whether the examination happened to be remote rather than on the flag, so some --distro runs fall through and answer for the local host under a heading naming another machine.
Plenty here is solid and I confirmed it rather than assuming: the curated_alternatives refactor is provably equivalent, the select_serve_engine call-site drift produces identical outcomes, examination_reports_amd_gpu keying on wsl.is_some() is a fix and not a regression, every Fix{} command exists with the flags given, and the exactly-fits boundary is handled correctly. I also chased the parse_gpu_vram_usage zero-row change and it does not regress any consumer.
c63f0fd to
36c173d
Compare
2b71e32 to
988c5d5
Compare
`rocm diagnose --model <ref>` composes a curated recipe with this host's
GPU memory and the engine `serve` would select, and reports ready,
degraded, blocked or undetermined. It fetches nothing, so the answer
costs seconds instead of a failed download.
Undetermined is kept distinct from blocked deliberately. A recipe catalog
that could not be read, a GPU whose memory could not be measured, and a
model the catalog does not carry all say nothing about whether the model
fits; reporting any of them as an incompatibility produces a wrong answer
that reads like a real one. An APU is judged on the memory its engine
allocates from rather than the BIOS carve-out amd-smi reports, for the
same reason.
A blocked verdict names curated models that would run here instead, and
each candidate is put through the same assessment against the same host,
so a user is never pointed at a second model they also cannot run.
Engine selection is passed in from `select_serve_engine` rather than
re-derived, so the two commands cannot come to disagree about which
engine would serve a model. The alternative-selection policy moves into
rocm-core and is shared with `rocm model --verbose`, which keeps its own
fit predicate and its current output.
Review fixes:
- The engine consistency guarantee had a gap: the doctor path derived the
configured engine from its own `AppPaths::discover()`, separate from the
one behind the GPU summary, so a config write racing a lookup could name
a different engine than `rocm serve` would pick for the same model.
`assess_model_on_this_host` now threads a single discovered `AppPaths`
to both the GPU summary and the config lookup, and a regression test
drives both call sites and compares.
- WSL GPU detection always reported `has_amd_gpu: false`, which fed a
false "no usable GPU" into every verdict on a WSL host. Fixed.
- A GPU reporting zero VRAM total was accepted as a valid measurement
instead of being treated as "could not measure", which could produce a
confident-looking blocked verdict from a bad reading. Fixed.
- Added end-to-end coverage for the two verdicts the existing scenarios
did not reach: a model outside the curated catalog (undetermined, not
blocked) and a model whose system RAM recommendation exceeds the host's
(degraded, not blocked), the latter via a synthetic signed catalog since
no built-in recipe can produce it against an arbitrary real host.
- `/model`'s missing-VRAM-reading message no longer points at `/examine`,
which is a static snapshot with no VRAM figure in it; it now names
`amd-smi metric --json` directly, matching what `diagnose --model`
already names for the same gap.
- Documented `--model` in the README's exhaustive flag listing and added
a Model Fit Preflight section to docs/testing.md.
- The APU fallback fabricated `UnifiedSystemMemory` from total installed
RAM, which is not the pool the engine allocates from -- the real GTT
aperture is capped below installed RAM by BIOS/kernel policy, and this
binary has no way to read it. Asserting installed RAM in its place could
produce a false `Ready` for a model that does not actually fit. The
classification is now a pure, unit-tested function separated from the
`amd-smi` subprocess call, and the APU case reports `Unknown` instead of
a fabricated figure. The degraded-vs-blocked e2e fixture also named
`vllm` as its preferred engine, which the platform gate rules out on
native Windows ahead of the RAM-softening logic the scenario exists to
exercise, turning the expected `degraded` into `blocked` on that lane;
swapped to `lemonade`, which no lane this suite runs excludes.
- The APU fix above still left two gaps. First, reporting plain `Unknown`
for an APU sent it down the same remediation as a genuine telemetry
gap ("run `amd-smi metric --json`"), which is a dead end for an APU:
amd-smi already ran and answered, it just named the BIOS carve-out
instead of the pool the engine allocates from, and running it again
produces the same wrong figure forever. `AcceleratorMemory` gains a
`UnifiedMemoryUnreadable` variant and `UndeterminedReason` a matching
case, so the APU path now says plainly that there is no dedicated VRAM
and no command that makes the real pool readable today, with no fix
offered -- and stays distinct from the genuine no-telemetry case, which
keeps its real amd-smi remediation. Second, the now-dead
`UnifiedSystemMemory` variant was still documented and still had a test
pinning it; both are removed. Third, the platform-gate `unsupported_here`
message had no test coverage on any single platform -- deleting the
closure body left every test green -- so its logic is now a pure
`unsupported_here_for(engine, ruled_out)` helper, unit-tested on both
the ruled-out and allowed branches directly rather than depending on
which OS the test happens to run on.
- The `UnifiedMemoryUnreadable` reason above reached rocm-core, the
classifier and the unit tests, but not the e2e scenario layer: the
"told why" step still asserted the single reason string that existed
before it, so an APU host now fails that assertion on output that is
correct. The step's undetermined arm is now reason-specific instead of
a single `assert_eq!`: the no-telemetry-at-all reason must say the
memory could not be read and carry a fix, the APU reason must name the
missing dedicated-VRAM pool and carry no fix, and any other reason
string panics rather than passing by coincidence -- so a future
regression that collapses the two back together fails this step
instead of going unnoticed. Two stale comments this variant made false
are corrected to name the APU case as a third machine class alongside
the two they used to enumerate.
- The same defect shape survived one branch over: the blocked arm of the
same step still hard-asserted a single evidence string ("no GPU is
visible to ROCm"), on the premise that the only way a host with no
measurement reaches a blocked verdict is an absent GPU. The engine
platform gate runs before the memory gate and returns blocked early
with its own evidence, naming an engine with no adapter on this
platform -- a vllm-only recipe on native Windows reaches this arm with
that evidence instead, and the premise no longer held. The blocked arm
is now reason-specific the same way the undetermined arm already is:
the two causes are discriminated by `fix.summary`, written
independently at the two call sites that set it, so a collapse in one
cause's evidence text cannot borrow the other's identity and still
pass; each cause is then asserted on its own terms -- the
engine-platform refusal must name the ruled-out engine and offer the
other-platform remedy, the GPU-visibility refusal must name the absent
GPU and still carry its fix -- with a closed `other => panic!`
fallback for a cause this step does not know about yet. The scenario's
own comment, which still enumerated only two halves, now names all
three unmeasured sub-causes. This is the change that explains the
`E2E tests (Strix Halo, Windows)` regression on this PR: the failing
job's own log carries the pre-fix panic text verbatim against a
vllm/native-Windows payload naming the engine-platform cause, so it is
not a flake.
- A fresh review round after the rebase onto main caught a real correctness
bug: `classify_accelerator_memory` summed VRAM across every GPU rather
than comparing against the single ordinal `rocm serve` actually pins, so
a homogeneous multi-GPU host (e.g. 8x192 GiB) could report READY for a
model that OOMs on every individual card. It now narrows to the
mask-visible rows (`rocm_core::usable_amd_gpu_indices`) and takes the
largest one -- the best case `--gpu auto` could land on. Two new tests
prove it: one against an 8x192 GiB host (would have reported ~1536 GiB
summed; now reports 192 GiB) and one proving a masked-out card's
capacity is ignored.
- The rewritten `/model` missing-VRAM-reading remediation (`amd-smi metric
--json`) was itself a dead end: every production caller of this
rendering path hardcodes `aggregate_gpu_vram_gib = None`, so running
amd-smi changes nothing this function ever sees. It now points at `rocm
diagnose --model <ref>` instead, the command this PR adds that does
thread a live reading through.
- `--model` with `--distro` was refused only when the probe happened to
come back looking remote (`inspected_remotely`, derived from
`examination.wsl`), not on whether `--distro` was actually passed -- a
correct-by-coincidence check with no test. It is now keyed on the flag
directly, checked before any probe runs, which also means the refusal no
longer needs a reachable WSL distribution to exercise: added
`diagnose-26` (feature + step defs), which runs on every lane.
- `format_gib`'s fractional branch rounded to nearest, so a card that
reports a hair under its nameplate size (e.g. 8176 MiB = 7.9844 GiB)
could print "8.0 GiB" against an "8 GiB" recipe minimum on a BLOCKED
verdict -- evidence that reads as an exact match while the verdict says
otherwise. It now floors instead; `required`/`recommended` values are
always exact integers so they are unaffected.
- The verdict table's `UNDETERMINED` row was missing its fourth reason
(`UnifiedMemoryUnreadable`, the APU case); the PR description now lists
all four.
- Corrected a stale off-by-one in two scenario doc comments
(`diagnose-20`/`diagnose-21` meant `diagnose-21`/`diagnose-22`) and
added a `docs/architecture.md` mention for the new `model_readiness.rs`
module.
- `rocm_doctor_skill.feature`'s field-sync check (new on main since this
branch was cut) caught a second merged-tree-only gap: `skills/rocm-doctor/
reference.md` never names the new `model` field `diagnose --json` now
emits, so an agent following the skill would not read it. Documented it
there, alongside the new `--distro`/`--model` flags in the command
synopsis.
- The multi-GPU fix's own tests proved `classify_accelerator_memory` alone,
not that picking the wrong figure would have changed a verdict a user
sees. Added `a_host_whose_vram_sum_clears_the_minimum_but_no_single_card_does_is_blocked_not_ready`,
which drives the real pipeline end to end: two 8 GiB cards (16 GiB
summed, comfortably above qwen3.5-4b's 12 GiB minimum) against the real
catalog, asserting the verdict is `Blocked`. Confirmed non-vacuous by
temporarily restoring the sum and re-running it: the verdict reads
`Ready` with the sum, `Blocked` with the fix.
The remote doctor's test fixture builds a DiagnoseReport literal, so it
names the new field too. That file arrived on main after this branch was
cut, which is why the break appeared only in the merged tree: neither
side is wrong alone, and no local build on either branch can see it.
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
988c5d5 to
6450959
Compare
juhovainio
left a comment
There was a problem hiding this comment.
Re-reviewed after the fixes — all of the substantive issues from the last round are resolved (multi-GPU VRAM max-not-sum, the /model remediation dead end, the --distro+--model keying bug, the BLOCKED rounding contradiction, the disclosed UnifiedMemoryUnreadable variant). One of my original findings was actually wrong: model_registry_adapter_availability_note isn't dead code, it has live call sites — apologies for the noise there; the stale #[allow(dead_code)] on it is a harmless leftover worth a quick cleanup but not blocking. Nothing new turned up elsewhere in the diff. Looks safe to approve after CI.
…and check #407 landed `render_model_readiness_text`, whose first line is `rocm diagnose --model <ref>: <verdict>`: a heading naming the command that produced the report, not advice to run anything. The advised-command check reads it as an invocation, substitutes both placeholders, and fails because `<verdict>` becomes a second positional argument. That is what ejected this change from the merge queue once main gained #407. Exempt it in NOT_INVOCATIONS with that reason, beside the other status headings. It is the only new hit from rebasing onto current main. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
What this adds
rocm diagnose --model <ref>answers "will this model run on this machine" fromthe curated recipe catalog and the host's own facts. It fetches nothing, so the
answer costs seconds instead of a failed multi-gigabyte download.
The environment diagnosis is unchanged. The model verdict rides on top of it.
The four verdicts
READYDEGRADEDBLOCKEDUNDETERMINEDUnifiedMemoryUnreadable)UNDETERMINEDis kept distinct fromBLOCKEDon purpose. A catalog that couldnot be read is a gap in what the CLI can see, not a fact about the model.
Collapsing the two would tell users a model is incompatible on the strength of a
failed read, which is the one answer here that is worse than no answer.
Design decisions worth review
Engine selection is passed in, not re-derived.
select_serve_engineremainsthe single place that answers "which engine serves this model on this host".
Doctor calls it rather than reimplementing the preference order, so it cannot
name an engine that
rocm servewould not pick. A test drives both call sitesand compares.
An engine ruled out by the platform blocks the model. It is not silently
swapped for another engine.
rocm servedoes not fall back either, and a verdictthat pretended otherwise would be wrong in the user's favour.
Alternatives are filtered, not just forwarded. A recipe's declared
alternatives are editorial: an entry can name something smaller that is still too
large for this machine. Offering it unfiltered sends the user into a second
failed download, which is the failure this command exists to prevent.
GPU memory is the single most-capable visible card, not the sum of every
card.
rocm servepins exactly one GPU ordinal; there is no tensor-parallelpath anywhere in this binary. A homogeneous multi-GPU host (e.g. 8x192 GiB)
summing to ~1536 GiB would have reported
READYfor a model that OOMs on everyindividual card. The figure is now the largest mask-visible card
(
rocm_core::usable_amd_gpu_indices), the best case--gpu autocould actuallyland on.
--modelis refused with--distro. The verdict is about this machine, soanswering it for a declared distro would be answering a different question.
Keyed on whether
--distrowas passed, checked before any probe runs -- not ona derived examination property -- so the refusal cannot depend on probe
internals and needs no reachable WSL distribution to exercise.
/model's missing-VRAM-reading message points atrocm diagnose --model.Every production caller of this rendering path hardcodes
aggregate_gpu_vram_gib = None, so a prior wording namingamd-smi metric --jsondirectly was itself a dead end: nothing the user runs changes what thisfunction sees. It now points at the command this PR adds, which does thread a
live reading through.
A measured GiB figure is floored, not rounded, for display. A card reporting
a hair under its nameplate size (8176 MiB = 7.9844 GiB) used to round to "8.0
GiB" against an "8 GiB" recipe minimum on a BLOCKED verdict -- evidence that
read as an exact match while the verdict said otherwise.
required/recommendedvalues are always exact integers, so only measured figures areaffected.
Testing
rocm-core, 15 inapps/rocm, and 6 end-to-end scenarios (diagnose-21throughdiagnose-26).cargo test --workspace --all-targets,cargo clippy --workspace --all-targets -- -D warnings,cargo clippy -p e2e-cucumber --test e2e -- -D warnings, andcargo fmt --all -- --checkare clean.cargo xtask e2e -- -n diagnose-2passes all 7 scenarios / 34 steps.--distrokeying fix, confirmed to makediagnose-26fail for the predicted reason) and the test confirmed to go red, then restored.Known limitations
readyandblocked-with-a-measurement paths ran only in unit tests and on the no-GPU branch of the scenarios. The scenarios are written host-agnostically, so both halves are real assertions on a lane that can reach them.Follow-up, not in this PR
GPU memory is read through
amd-smi. A directamdgpusysfs fallback would letthe check still answer on hosts where
amd-smiis missing, at the cost of notcovering APU unified memory. Worth doing as its own change.