Repository navigation
fix(lemonade): restore packaged backend pins when a kept alignment pin stops installing - #754
Conversation
nowycondro
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · bd917f4
Review — needs work
Full review of the whole change (base efb9bc37…head bd917f4d, 3 files, 367/-2 lines). No ticket named; the PR body is a self-contained CI-failure writeup, not a tracker reference.
Blocking
-
Description and docs claim the original error survives a failed retry; the code reports the retry's error instead —
docs/engine-plugins.md:77,engines/lemonade/src/backend_alignment.rs:259-268
The PR description anddocs/engine-plugins.md:76-77both state: "If the file already holds the packaged values, or the second attempt fails too, the original install error is reported unchanged." Tracinginstall_pinned_backend_or_restore_packaged_pins: whenrestore_packaged_pinsreturnsOk(true)and the forced retry (fallback_install(manifest, true)?at line 267) itself fails, the?propagates that error, not the capturedinstall_error. The PR's own testequal_pin_still_failing_after_the_restore_reports_the_retry_failure(backend_alignment.rs:1705) confirms this directly: it asserts the surfaced error contains "packaged pins" (the retry failure's text) and that it does not contain the first failure's text. Only theOk(false)andErr(restore_error)branches actually report the original error unchanged; the third documented case does not. The code's behavior (surfacing the retry failure, which is what's actually actionable once packaged defaults are in play) looks like the right choice — the description and doc are what should change to match it.
Confidence 90 · logic · Fix: correct the wording in the PR description anddocs/engine-plugins.mdto say the retry's own error is reported when the forced reinstall fails after a successful restore. -
The new forced reinstall skips this file's own established half-install safety net —
engines/lemonade/src/backend_alignment.rs:267vs.:398-407,:433-448,:1047-1097
Every other forced reinstall in this file (align_llamacpp_backend_to_version's revert fallback at:398-407,try_llamacpp_backend_alignmentat:433-448) wraps the install inset_aside_rocm_llamacpp_backend_dirs/resolve_rocm_llamacpp_backend_dirs_aside(:1047,:1087), whose own doc comments explain why: Lemonade extracts each build into its own build-numbered directory rather than replacing one in place, so a rejected/failed attempt can leave a stray directory thatfind_binary_in's priority-ordered lookup later picks up instead of (or alongside) the intended build — or leave the host with no working backend at all.install_pinned_backend_or_restore_packaged_pins's own comment names the identical hazard ("the failed attempt may have left a half-installed backend for the withdrawn pin") but callsfallback_install(manifest, true)?directly, with no aside/resolve wrapping. A retry that fails after a successful restore can leave exactly the stale/shadowing directory state this machinery exists to prevent.
Confidence 80 · architectural · Fix: wrap the forced retry (and arguably the first attempt too) with the sameset_aside_rocm_llamacpp_backend_dirs/resolve_rocm_llamacpp_backend_dirs_asidepair used elsewhere in this file. -
No Gherkin scenario for user-observable behavior this change introduces, and the stated justification doesn't fit AGENTS.md's own exceptions —
AGENTS.md§3, PR description "Not covered / gaps"
AGENTS.md §3 requires: "If the change alters what a user of the CLI can observe — command output, exit codes, files or paths the CLI creates, or which runtime/engine it selects — then that behavior must be covered by a Gherkin scenario intests/e2e-cucumber/features/," with only three outs: an existing scenario (cited by@id), a new scenario gated to@requires-gpu/@nightly(named in the PR text), or a purely-internal change. This change adds new stderr warnings, rewrites a file the CLI manages, and turns a previously-failing install into a succeeding one — squarely in scope, and not purely internal. The PR's own gap note reasons that such a scenario "can only be a gated GPU or nightly scenario" and points at "the Windows lane's prewarm step" as "the live guard" — but that's an existing CI job, not a tagged Gherkin scenario, so even on the author's own reasoning the gated-lane exception's required action (add the scenario, tag it, name the lane) was never taken. Coverage is unit tests over injected closures plus one archive-round-trip test, which prove the decision logic and the file-rewrite mechanics but not the CLI-observable outcome end to end.
Confidence 80 · architectural · Fix: either add a@requires-gpu/@nightly-gated scenario exercising the restore-and-retry path, or make the case explicitly for why this is exempt and get that judgment call confirmed rather than asserted.
Non-blocking
-
Archive-extraction/root-finding sequence duplicated —
engines/lemonade/src/install.rs:171-188(prepare_embeddable_with) and:291-306(restore_packaged_backend_versions_with)
Both functions independently do "create a timestamped extract dir,extract_archive,find_embeddable_root, clean up the extract dir" with only the follow-up step (copy the whole tree vs. read one file) differing. Not a correctness issue — reuses already-hardened extraction code — but a reasonable candidate for a small shared helper.
Confidence 70 · mechanical · Fix: factor the extract-into-temp-dir-and-find-root steps into one shared helper returning the embeddable root path. -
The restore function's own failure branches, and the new warning text, are untested —
engines/lemonade/src/install.rs:273-311,engines/lemonade/src/backend_alignment.rs:259-268
restore_packaged_backend_versions_with's own new logic (digest mismatch viaensure_cached_archive, a packaged archive missing the resource entry) has no direct test — only the happy-path round trip (restore_packaged_backend_versions_replaces_only_a_pin_file_that_drifted,install.rs:1767) is exercised; the underlyingensure_cached_archive/verify_sha256primitives are separately well-tested elsewhere, which limits the real exposure here. Separately, the new warning strings ("restored the pins Lemonade ships and trying again", "could not restore the pins Lemonade ships") are never referenced by any test (zero matches repo-wide), so AGENTS.md §3's "a message about the CLI's own behavior is asserted together with the behavior" guidance isn't exercised either way for this string — not a false-pinning risk since nothing pins it at all, just an uncovered edge.
Confidence 70 · test-coverage. -
Doc's new recovery paragraph isn't cross-referenced from the earlier summary it extends —
docs/engine-plugins.md:46vs.:69-79
The pre-existing sentence "skipped when the pin already matches the active version" (line 46) is still accurate (it describes the alignment rewrite being skipped, which this PR doesn't change) but a reader stopping there would not know the install in that case now has a recovery path, described eleven lines further down with no forward pointer. Not a contradiction, just a completeness/discoverability gap.
Confidence 70 · mechanical.
Decisions for the author
- Full archive re-extraction just to read one JSON file — tradeoff
restore_packaged_backend_versions_withextracts the entire embeddable archive to a temp directory solely to readresources/backend_versions.jsonout of it, rather than reading just that one archive entry directly. The current approach reuses the same hardened, tested extraction pathprepare_embeddable_withuses (consistent traversal/safety guarantees) at the cost of needless I/O on a failure-recovery path that should be rare. A leaner single-entry read would avoid the extra work but would duplicate (or need to factor out) the archive-format-specific entry-reading logic instead. Confirm this tradeoff was made deliberately rather than by default.
Positive signals
- Digest verification is enforced before any restored byte is trusted (
ensure_cached_archive's existing lock/verify path is reused, not re-implemented) — matches the archive-handling discipline already established in this file. - Comparing the pin file as parsed JSON rather than raw bytes (
install.rs:308-312) correctly avoids a spurious "restore" when a prior write only re-serialized equivalent content — a detail that's easy to get wrong and is handled carefully here, with a test for it. - The five new
backend_alignment.rsunit tests exhaustively drive the equal-pin branch's four outcomes (clean install, restorable drift, already-packaged, restore-fails, retry-fails-after-restore) through injected closures, without needing a reallemondspawn or network access.
Deployment notes
- This only changes behavior on the "pin already equals active ROCm version but install fails" path;
ROCM_CLI_DISABLE_LEMONADE_BACKEND_ALIGNMENTcontinues to bypass it exactly as before (verified: the disabled-check inprepare_llamacpp_backend_for_active_rocm_implreturns before this branch is ever reached). No migration, no new dependency, no config surface added. - At review time the two CI lanes this PR directly targets —
E2E tests (Strix Halo, Windows, current)andE2E tests (Strix Halo, WSL2, current)— had just finished and both report SUCCESS on this exact head commit, consistent with the fix working against the real failure mode described in the PR. The GPU-hardware lanes (MI300X, W7900, Strix Halo Ubuntu release/nightly) were still QUEUED at review time; confirm they finished green before merge.
What this covered
Read in full: engines/lemonade/src/backend_alignment.rs, engines/lemonade/src/install.rs, docs/engine-plugins.md at the reviewed commit, plus the unchanged call sites and helpers they depend on (ensure_cached_archive, extract_archive, find_embeddable_root, ensure_best_llamacpp_backend, install_response, set_aside_rocm_llamacpp_backend_dirs/resolve_rocm_llamacpp_backend_dirs_aside) and AGENTS.md in full. Diff reviewed: efb9bc3767…bd917f4d72 (the PR's merge-base to its head). Two independent blind passes were run in addition to this driving pass — one a code-correctness pass over the two Rust files, one an agent-instruction-adherence/impact-analysis pass over AGENTS.md and the doc — and their candidate findings were independently re-verified against the source before inclusion above; every finding reported here was confirmed directly against the code, not taken on their word.
Did not run: cargo test/clippy/fmt locally (no Rust toolchain available in this review environment) — relied on the PR's own stated local verification and on live CI status instead, both checked immediately before publishing. History (git log/blame) on the touched files showed no prior related work conflicting with this change. No prior reviews existed on this PR to reconcile against.
juhovainio
left a comment
There was a problem hiding this comment.
The fix does what it says: when a kept alignment pin stops installing, the packaged pins are restored and the install is retried once, forced. I found nothing that breaks that path, but a few things should change before merge.
- The docs and PR description say that if the second attempt also fails, the original install error is reported. The code returns the retry's error, and a test pins that. Reporting the retry error is fine; the docs and PR body should say so.
- Any install failure triggers the restore, including transient network or disk errors. If the retry then fails too,
backend_versions.jsonstays rewritten and a good kept pin is lost. It self-heals on the next run, but it is a surprising side effect. Inline comment below. - The restore can re-download the embeddable archive when the cache is gone. The docs should mention that.
- The pin-file write is not atomic.
- The new warning says the packaged pins were restored, but no test ties that message to the resulting state, and there is no Gherkin scenario. The PR text justifies the missing scenario and names the Windows prewarm lane. It would help to state which gated lane exercises it, and to assert the message together with the pin-file state in one test.
Inline comments cover the first three code and docs points.
| from (digest-verified; only that file is rewritten, so installed backends are | ||
| untouched) and installs once more against Lemonade's own pins, printing a | ||
| warning that says so. If the file already holds the packaged values, or the | ||
| second attempt fails too, the original install error is reported unchanged. |
There was a problem hiding this comment.
The code returns the retry's error here (fallback_install(manifest, true)?, and a test pins that), not the original one. Please reword, e.g. "the second attempt's error is reported", and fix the same claim in the PR body. Also worth noting that the restore may re-download the embeddable archive if it is no longer cached.
| ); | ||
| // Forced: the failed attempt may have left a half-installed backend | ||
| // for the withdrawn pin. | ||
| fallback_install(manifest, true)?; |
There was a problem hiding this comment.
If this retry fails, the pin file stays rewritten even when the first failure was transient (network or disk), discarding a kept pin that may have been fine. Consider restoring the previous bytes when the retry fails, or only treating pin-specific failures as restorable.
| { | ||
| return Ok(false); | ||
| } | ||
| fs::write(&target, &packaged) |
There was a problem hiding this comment.
Non-atomic write of the pin file: a crash mid-write leaves invalid JSON that the next run reads. Writing to a temp file and renaming is cheap.
bd917f4 to
4dfbb9a
Compare
|
Thanks for the careful review. The branch is rebased onto current 1. Original error vs the retry's error. You were right: the code returns the retry's error, and a test pins that. I reworded 2. A failed retry left the pin file rewritten. Fixed. The restore now hands back a backup of the bytes it replaced (or of "no file"), and a failed retry puts them back and says so. I did not try to limit the restore to pin-specific failures, because telling those apart from transient ones would mean matching error text, which is fragile; putting the previous file back removes the side effect either way. 3. Re-download. The docs now say the restore reads the cached archive and downloads it again if the cache no longer has it. 4. Non-atomic write. The restore and the put-back both write a sibling temp file and rename it over the pin file. The existing alignment code's own edit of that file is unchanged. 5. The message and the state. Two new tests assert the printed message together with the real pin-file content: a successful retry reports the pins as restored and the file holds the packaged values; a failed retry reports that the previous file was put back and the file holds the kept pin. I removed the put-back and the second test failed, then restored it. To make that testable the message goes through a small callback instead of a direct print. On the lane: the PR text now names |
nowycondro
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 4dfbb9a
Review
Read docs/engine-plugins.md, engines/lemonade/src/backend_alignment.rs, and engines/lemonade/src/install.rs in full at the current head (merge-base fe1dbabe…head 4dfbb9a6), plus the unchanged helpers the new code calls (ensure_cached_archive, extract_archive, find_embeddable_root, set_aside_rocm_llamacpp_backend_dirs/resolve_rocm_llamacpp_backend_dirs_aside). This head is a force-pushed rebase that answers the two earlier review rounds on this PR (one automated, one human) at commit bd917f4d: the doc now matches the code on which error is reported after a failed retry, the restore takes a backup and puts it back when the retry fails, the pin file is written atomically, and two new tests tie each new warning message to the resulting pin-file state. Two points from those earlier rounds are still open at this head, detailed below.
Blocking
-
The forced retry bypasses this file's own half-install safety net —
engines/lemonade/src/backend_alignment.rs:280vs.:427-435,:1075-1115
install_pinned_backend_or_restore_packaged_pinscallsfallback_install(manifest, true)directly after a successful restore. Its own comment at line 278-279 names the hazard ("the failed attempt may have left a half-installed backend for the withdrawn pin"), but every other forced reinstall in this file — the revert fallback insidealign_llamacpp_backend_to_version(lines 427-435) andtry_llamacpp_backend_alignment(line 461) — wraps that same call inset_aside_rocm_llamacpp_backend_dirs/resolve_rocm_llamacpp_backend_dirs_aside(defined lines 1075-1115), whose doc comments explain why: each build extracts into its own build-numbered directory, so a failed attempt can leave a stray directory that the binary lookup later picks up instead of, or alongside, the intended build. The new retry path recognizes the same risk in prose but doesn't apply the existing mitigation.
Confidence 80 · architectural · Fix: wrap the forced retry (and arguably the first attempt) with the same aside/resolve pair used elsewhere in this file. -
No Gherkin scenario for the user-observable behavior this change introduces —
AGENTS.md§3, PR description "Not covered / gaps"
The project's own contribution guide requires a Gherkin scenario undertests/e2e-cucumber/features/for any change that alters CLI-observable output, exit codes, or files/paths the CLI creates — with only three outs: an existing scenario, a new scenario gated to@requires-gpu/@nightlyand named in the PR text, or a change that's purely internal. This adds new stderr warnings and turns a previously-failing install into a succeeding one, so it isn't purely internal. The PR names the CI lane whose prewarm step exercises this path, and the author's follow-up comment explains why a scenario would need to fake runner state that can't easily be reproduced in a normal test run. That's a reasonable practical constraint, but naming an existing CI job is not the same as adding and tagging a scenario, so the project's own rule isn't satisfied on its own terms yet. Coverage today is unit tests over injected closures plus one archive round-trip test, which exercise the decision logic but not the CLI-observable outcome end to end.
Confidence 75 · architectural · Fix: either add a gated scenario, or get explicit maintainer sign-off that the CI-lane argument is accepted as the exception here.
Non-blocking
-
Archive-extraction/root-finding logic is duplicated —
engines/lemonade/src/install.rs:135-188(prepare_embeddable_with) and:275-311(restore_packaged_backend_versions_with)
Both independently extract the embeddable archive to a timestamped temp dir, callfind_embeddable_root, then clean up — differing only in what they do with the result (copy the tree vs. read one file). Not a correctness problem, just a candidate for a shared helper. -
Some of the new warning strings and restore failure branches remain untested —
engines/lemonade/src/backend_alignment.rs:301and:290,engines/lemonade/src/install.rs:275-311
The new tests assert the "restored the pins" and "put the previous" messages against real pin-file state, which is good, but the "could not restore the pins Lemonade ships" and "could not be put back" messages have no test referencing them, andrestore_packaged_backend_versions_with's own failure branches (digest mismatch viaensure_cached_archive, a packaged archive missing the resource entry) are only indirectly covered through already-tested lower-level primitives. -
The new recovery paragraph isn't cross-referenced from the summary it extends —
docs/engine-plugins.md:46vs.:69-83
Line 46 still reads "skipped when the pin already matches the active version" (still accurate for the alignment rewrite), but a reader stopping there won't know the install in that case now has a recovery path described 23 lines later, with no forward pointer.
Decisions for the author
- Full archive re-extraction just to read one JSON file — tradeoff
restore_packaged_backend_versions_withextracts the whole embeddable archive to read a single entry out of it, reusing the same hardened extraction pathprepare_embeddable_withuses rather than reading one archive entry directly. That trades some I/O on a failure-recovery path that should be rare for reuse of already-tested code. Worth confirming this was a deliberate choice.
Positive signals
- The restore takes a verified-JSON-equal check before deciding anything changed (
install.rs:290-296), so a prior write that only re-serialized equivalent content doesn't trigger a spurious restore — handled carefully, with a test for it. - The pin file write now goes through a temp-file-and-rename (
write_file_atomic), and the backup/put-back path is tested against a real file on disk, including the "no file existed" case. - Digest verification is enforced before any restored byte is trusted, reusing the existing
ensure_cached_archivelock/verify path rather than re-implementing it.
Deployment notes
- At review time, all required checks on this head pass except
E2E testsandwindows-build-and-test, both still pending/running — these are exactly the lanes this change targets, so confirm they finish green before merge. - This only changes behavior on the "pin already equals the active version but install fails" path;
ROCM_CLI_DISABLE_LEMONADE_BACKEND_ALIGNMENTcontinues to bypass it, unchanged. No migration or new configuration surface.
What this covered
Read in full at head 4dfbb9a6: docs/engine-plugins.md, engines/lemonade/src/backend_alignment.rs, engines/lemonade/src/install.rs, diffed against merge-base fe1dbabe, plus the unchanged call sites/helpers named above and the project's AGENTS.md. Did not run cargo test/clippy/fmt locally — no Rust toolchain available in this review environment; relied on GitHub's reported CI status instead, re-checked immediately before publishing. Checked git log on the touched files and found no other recent related work. This review did read the PR description and the two earlier review rounds on this PR (an automated review and a human review, both at the prior head bd917f4d) and the author's reply to them, specifically to establish which of their points this force-push already addresses versus what's still open — the two blocking items above are the ones that remain.
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 4dfbb9a
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
Review — needs work
Full review of the whole change.
No ticket or issue named. The description matches the diff and justifies the missing Gherkin scenario under its AGENTS.md §3 gaps section.
All findings below are new (first automated round on this PR).
Blocking
-
Two of the four new user-facing warnings are not tested, and one of them claims an outcome (new) —
engines/lemonade/src/backend_alignment.rs(install_pinned_backend_or_restore_packaged_pins: theErr(restore_error)arm's "could not restore the pins Lemonade ships…", and theErr(put_back_error)arm's "…could not be put back: …")
AGENTS.md §3 says a line the CLI prints about what it did must be asserted together with the resulting state.equal_pin_whose_restore_fails_reports_the_original_failuregoes throughprepare_with_equal_pin, which sendswarntoeprintln!. So it checks the returned error, but neither the message nor the pin file.
Two mutations ran on this head, and the lemonade suite stayed green on both (146/146):- replacing the restore-error
warn(format!(…))withdrop(format!(…)); - changing the put-back-error message text.
The other two warnings are properly covered: removing
put_back()failsa_failed_retry_puts_the_previous_pin_file_back_and_says_so.
Confidence 100 · logic · Fix: drive both arms throughinstall_pinned_backend_or_restore_packaged_pinswith a capturedwarn. Assert the message, plus the pin file being unchanged (restore failed) or still holding the packaged bytes (put-back failed, e.g. the target made unwritable). - replacing the restore-error
-
The new forced retry skips the backend-directory set-aside that its twin, the revert path, uses for the same operation (new) —
engines/lemonade/src/backend_alignment.rs(fallback_install(manifest, true)ininstall_pinned_backend_or_restore_packaged_pins); twin atalign_llamacpp_backend_to_version~L426-437; lookup inengines/lemonade/src/direct_llama.rsfind_binary_in- What the twin does: after rewriting the pins, it wraps its forced reinstall in
set_aside_rocm_llamacpp_backend_dirs/resolve_rocm_llamacpp_backend_dirs_aside. - Why, per the twin's own doc comment: installing a different
therock.version/llama.cpp tag "can otherwise leave two builds side by side".find_binary_intakes the firstread_dirsubdirectory, sorocm serve"may resolve to whichever build" is found first. Also, "a failed fallback install must not leave the host with no ROCm backend at all". - Why the new path matches that case: it rewrites the pins and then force-installs a different build. The new comment ("the failed attempt may have left a half-installed backend") admits there may be leftovers. But forcing the install does not remove a leftover in another build-numbered directory.
- What happens on failure: if the retry fails, nothing restores a backend that the forced install removed.
Not run on hardware. Traced from the code.
Confidence 70 · logic · Fix: wrap the retry the same way as the revert path. Passtrue/falsetoresolve_rocm_llamacpp_backend_dirs_asideaccording to the retry result. - What the twin does: after rewriting the pins, it wraps its forced reinstall in
Non-blocking
-
The new atomic-write helper is a third copy, and weaker than the existing one (new) —
engines/lemonade/src/install.rswrite_file_atomic; existing helpers atcrates/rocm-core/src/lib.rswrite_atomicandapps/rocmd/src/sandbox.rswrite_file_atomically- The new helper always uses the fixed temp name
<path>.rocm-tmp, and does not remove the temp file whenfs::writefails. rocm_core::write_atomicadds a millis+pid suffix. Its doc says this exists because two processes writing at the same moment would otherwise overwrite each other's temp file. The runtime tree here is documented as shared.- Lemonade already depends on
rocm-core, butwrite_atomicispub(crate)there.
Confidence 85 · mechanical · Fix: export and reuse
rocm_core::write_atomic, or at least copy its unique temp name and its cleanup when the write fails. - The new helper always uses the fixed temp name
-
A doc comment the change made stale (new) —
engines/lemonade/src/backend_alignment.rsdoc onprepare_llamacpp_backend_for_active_rocm(~L69-79)
It still says that when "the pinned version already matches", this "falls back to … today's unmodified pinned-version install — never a regression". That branch can now rewritebackend_versions.jsonand force a second install. The code governs here, so the comment is the side to fix.
Confidence 85 · mechanical · Fix: mention the restore-and-retry on that branch. -
The new warning wraps advice that contradicts it (new) —
engines/lemonade/src/backend_alignment.rs({install_error:#}in the "restored the pins…" warning); source of the advice atinstall_llamacpp_backend_with_retry~L903-906
install_errorcarries the context "runrocm engines install lemonade --reinstalland then retryrocm serve". The same line then says the CLI restored the pins and is retrying by itself.
Confidence 70 · mechanical · Fix: print the root cause (install_error.root_cause(), or the chain without the outer context) in this warning. -
"Installed backends are untouched" reads as true of the whole recovery, but the retry is a forced reinstall (new) —
docs/engine-plugins.mdnew paragraph; restore doc inengines/lemonade/src/install.rs(restore_packaged_backend_versions); the PR description's "Change" section
The statement is true of the restore step alone. The next step,fallback_install(manifest, true), reinstalls the selected backend into the shared tree.
Confidence 70 · mechanical · Fix: limit the claim to the restore step, and say that the retry forces a reinstall.
Decisions for the author
- Any install failure, including a transient one, can permanently replace a pin that was working — tradeoff
- What happens: the inner
install_llamacpp_backend_with_retryhas already retried once. If the restored retry then succeeds, the kept aligned pin is gone. The next plain install sees pinned ≠ target and runs alignment again (forced multi-GB reinstalls). - Where the user hears about it: only the stderr warning. Nothing goes into
InstallResponse.warnings. - The other side: a stale pin otherwise blocks every install until someone runs
--reinstall. The docs do say that any failure starts the restore.
- What happens: the inner
- The restore rewrites the whole file, so hand edits to keys alignment never touches are also dropped — tradeoff
- Before this change: with
therock.versionequal to the active SDK, the file was never rewritten without--reinstall. - Now: any failure there can replace hand pins for other recipes.
- The other side:
ROCM_CLI_DISABLE_LEMONADE_BACKEND_ALIGNMENTis checked before this branch (verified: thedisabledearly return comes first), and the docs point users who edit by hand at it. Restoring only thetherock/llamacpp.rocm-stablekeys would keep other edits, at the cost of a pin file that is not exactly what Lemonade ships.
- Before this change: with
Positive signals
- Recovery is limited to the equal-pin branch, and a no-op when the file already holds the packaged values (compared as JSON, not bytes). So a genuinely bad packaged pin still reports its original error and is not hidden behind a retry.
- The archive is read through
ensure_cached_archive, so the bytes are digest-checked before they are trusted. A backup is put back when the retry fails, so a transient error cannot throw away a pin that was fine.
Verdicts for every test the change adds (mutations run on this head, lemonade lib suite, 146 tests):
| Test | Fails with the fix hunk reverted? | Branch mutations it catches |
|---|---|---|
equal_pin_kept_from_an_earlier_alignment_is_restored_when_it_no_longer_installs |
yes | retry not forced |
equal_pin_still_failing_after_the_restore_reports_the_retry_failure |
yes | retry error replaced by the first error |
equal_pin_that_is_already_the_packaged_pin_reports_the_original_failure |
yes | Ok(None) arm turned into a retry |
equal_pin_whose_restore_fails_reports_the_original_failure |
no (pins pre-existing behaviour) | restore error returned instead of the original; not its warning (see Blocking) |
equal_pin_that_installs_never_touches_the_pin_file |
no (no-op path) | restore count asserted |
the_restored_message_is_true_of_the_pin_file_when_the_retry_succeeds |
depends on the new helper | retry not forced |
a_failed_retry_puts_the_previous_pin_file_back_and_says_so |
depends on the new helper | put-back removed; first error reported |
restore_packaged_backend_versions_replaces_only_a_pin_file_that_drifted |
depends on the new helper | JSON-equality skip removed; extract cleanup removed |
a_backup_of_a_missing_pin_file_removes_the_file_again |
depends on the new helper | removal skipped |
Mutations that survived:
- the restore-error warning dropped (Blocking above);
- the put-back-error message changed (Blocking above);
write_file_atomicmade non-atomic — acceptable, as a crash in mid-write is not practical to test.
Deployment notes
- The lane the description names as exercising this,
E2E tests (Strix Halo, Windows, current), does not appear in this head's check rollup. So no run on hardware has shown the fix on a runner holding the stale state. The description says the lane goes green only once the change is on the branch it runs.
What this covered
The full diff 24e405a7…4dfbb9a6 (one commit), plus the callers and twins it touches:
- the
prepare_llamacpp_backend_for_active_rocm{,_impl}gate chain; - the alignment revert and set-aside code;
install_llamacpp_backend_with_retryandensure_best_llamacpp_backend;ensure_cached_archive,prepare_embeddable_withandfind_binary_in;- the only production caller, in
engines/lemonade/src/lib.rs.
What was run:
cargo test -p rocm-engine-lemonade --lib: 146 tests passed, including all 9 new ones by name.- 12 single-branch mutations, each rebuilt and run separately.
Fan-out ran with 2 workers: a code/design pass, and an agent-instruction/history/comments pass. The design questions (Step 7) were folded into the first worker because of the 2-worker cap, not run as separate workers.
Checks and sign-off:
- Sign-off: checked with
check_signoff.py, which exited 0 (1 commit checked, none missing). - CI: for this head, all reported checks succeeded. "Skill checks (skillscope)" and "Sphinx docs build (-W)" were SKIPPED. No GPU lane appears in the rollup.
Did not run:
- Prior-changes pass: earlier PR reviews were not consulted, by design.
- Workspace-wide gates:
cargo test --workspace, clippy, the Windows cross-check andsmoke_local.pywere not run locally. CI's clippy and Windows build-and-test passed.
No prompt-injection content found. Not reconciled against any PR discussion here.
Why this requests changes: both blocking findings are defects with a consequence once merged — an outcome-claiming warning whose removal no test notices (the repo's AGENTS.md §3 rule), and a forced reinstall that can leave two backend builds side by side or no ROCm backend after a failed retry, unlike its twin. Resolving them: add the two captured-warn tests, and wrap the retry in the same set-aside/resolve as the revert path (or say why it is not needed here). This automation will withdraw the request once a later round confirms both.
4dfbb9a to
d9796e4
Compare
|
Thanks for the thorough review. Both blocking findings were right. The branch is rebased onto current Blocking 1: two warnings were untested. Added tests that drive both arms through Blocking 2: the forced retry skipped the set-aside its twin uses. Agreed. The retry now sets the ROCm backend directories aside before the forced install, discards them on success, and puts them back on failure, using the same two functions as the revert path. Tests: a success leaves only the new build (no two builds side by side); a failed retry puts the old build back, removes a stray directory the failed install left under another ROCm name, and puts the previous pin file back. Removing the set-aside fails both. Non-blocking.
Decisions.
On the lane: |
Addressed in d9796e4; point-by-point reply: #754 (comment)
juhovainio
left a comment
There was a problem hiding this comment.
I went through this at d9796e4. The fix itself matches the PR description, and the tests assert each warning together with the real pin-file and backend-directory state, which I appreciate. I'm requesting changes for the items below; the first three are inline.
- The backend directories are set aside only after the first install attempt has already failed. A partial rocm-* directory left by that attempt is what would be "put back" if the retry also fails. Setting aside before attempt one, or a test where attempt one leaves a partial directory, would close this.
- Any install failure now triggers the restore path, which reads or re-downloads the embeddable archive even when the pins are already the packaged ones. On the common failure (for example offline) that adds a download attempt and a second warning before the original error. A cheap short-circuit would avoid it.
- In install.rs, the current pin file is read before the archive is verified and extracted, so a concurrent writer in between would be overwritten and the backup would hold stale bytes.
- The retry is forced on any first failure, including transient ones, and moves the working ROCm backend aside. A kill between set-aside and put-back leaves only a .pre-alignment directory, which is now reachable from a plain install.
- Docs: only docs/engine-plugins.md was updated, but
rocm engines install lemonadenow prints a new warning and rewrites pins. Please grep README.md, docs/manual-testing.md and docs/testing.md for the repeated "alignment" claim and update them in this change. - The Strix Halo Windows lane this fix targets is not in the check list for this head, and the Windows repro was done on an earlier version of the change. Please re-run it on the current head, or say which lane will exercise it.
Not asking for a change: hand-edited pins being lost on a successful retry is disclosed in the PR and the env var is the escape hatch.
| )); | ||
| // Forced: the failed attempt may have left a half-installed backend | ||
| // for the withdrawn pin. | ||
| let aside_dirs = set_aside_rocm_llamacpp_backend_dirs(manifest); |
There was a problem hiding this comment.
The backend dirs are set aside only after the first install already failed, so a half-installed rocm-* dir left by that attempt is what gets put back if the retry fails too. Consider capturing before the first attempt, or add a test where attempt 1 leaves a partial dir.
| mut restore_packaged_pins: impl FnMut(&LemonadeInstallManifest) -> Result<Option<PinFileBackup>>, | ||
| warn: &mut impl FnMut(String), | ||
| ) -> Result<Option<String>> { | ||
| let Err(install_error) = fallback_install(manifest, false) else { |
There was a problem hiding this comment.
Any failure (including a plain network error) now triggers restore, which reads or downloads the embeddable archive even when the pins are already packaged. Consider short-circuiting before the archive work when the pins already match, or limiting this to failures that look like a withdrawn asset.
| .join("downloads") | ||
| .join(embeddable_archive_name(&manifest.version)); | ||
| let target = manifest.runtime_dir.join(resource); | ||
| let current = fs::read(&target).ok(); |
There was a problem hiding this comment.
current is read before the archive is verified and extracted. If alignment in another process writes between this read and the write_atomic below, its write is overwritten and the backup holds stale bytes.
nowycondro
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · d9796e4
Review
Restores the two backend-alignment pins (therock.version, llamacpp.rocm-stable) in resources/backend_versions.json from the packaged embeddable archive and retries once, forced, when the already-pinned Lemonade backend fails to install — recovering from a stale pin an earlier alignment kept that now names a withdrawn build. The core restore/retry logic, its error-reporting branches, and the backend-directory aside/resolve wrapping were traced through every branch and are correct and well tested against real files and directories. One real documentation-accuracy defect blocks; one narrow data-loss edge case and the stated AGENTS.md scenario gap are not.
Blocking
write_atomic's visibility bump contradictsdocs/architecture.md, which still calls it crate-private and slated for deletion —crates/rocm-core/src/lib.rs:362,docs/architecture.md(thewrite_atomicbullet, ~line 110), new consumer atengines/lemonade/src/install.rs:332and:407.
docs/architecture.mdreads, unchanged by this PR: "write_atomic— not permanent. It is the crate-private temp-then-rename writerconfig.rs,automation.rs, andservice_record.rsuse for their atomic replaces. ... Once #610 lands, the automation and service-record callers follow andwrite_atomicis deleted (ROCMAI-724)." This PR changes the function frompub(crate)topuband gives it a brand-new, permanent cross-crate consumer (rocm_core::write_atomicfromengines/lemonade), which the doc's deletion plan never accounted for. The doc is now wrong on two counts: the function is no longer crate-private, and deleting it per ROCMAI-724 would break this new caller. AGENTS.md §5 requires keeping docs and behavior claims in sync while editing.
Confidence 85 · architectural · Fix: update thewrite_atomicbullet indocs/architecture.mdto drop "crate-private", name the lemonade consumer, and note how this affects (or supersedes) the #610/ROCMAI-724 deletion plan.
Non-blocking
- A process kill during the forced retry window can permanently lose a hand-edited pin file —
engines/lemonade/src/backend_alignment.rs:282-314,engines/lemonade/src/install.rs:398-411(PinFileBackup::put_back).
restore_packaged_pinsatomically overwritesbackend_versions.jsonwith the packaged values and hands back a backup whosepreviousbytes live only in an in-processVec<u8>. The code then runs a full forced reinstall (spawnslemond, waits up to 30s, network-bound backend install) beforeput_back()ever runs, and only on theErrarm. A SIGKILL, OOM-kill, or power loss during that window leaves the packaged values on disk with no way to recover whatever was there before — a window that did not exist before this change, since the equal-pin branch previously never touched the file. Narrow (needs an actual process kill, not an ordinary failure) and partly mitigated by the PR's own stated design: these two keys are already meant to be reset if a user's hand-edit of them causes an install failure, crash or not.
Confidence 70 · architectural · Fix: either persist the backup to a sibling file before the retry (cleaned up afterput_back), or note the accepted risk explicitly in the function's doc comment.
Decisions for the author
- No Gherkin scenario covers the new restore-and-retry path — answered-by-intent.
engines/lemonade/src/backend_alignment.rs:280-311adds a new stderr warning, rewritesbackend_versions.json, and can change which llama.cpp build gets installed — all three AGENTS.md §3 triggers for "needs a scenario, not only a unit test," and no feature file intests/e2e-cucumber/features/exercises this path (only the pre-existing Tier1/Tier2/revert alignment is covered there). The PR description's "Not covered / gaps (AGENTS.md section 3)" section answers this directly: "The behaviour needs a runtime tree that already holds a stale pin and a live backend download, so it could only be a gated scenario. The lane that exercises it isE2E tests (Strix Halo, Windows, current)... That lane did not appear in this head's check list at the last look, so no hardware run has shown the fix on a runner holding the stale state." AGENTS.md §3 explicitly permits exactly this: state the gap and name the gated lane. Would otherwise have been blocking. Confirm that lane is still the intended coverage path once this lands, and that someone will check it actually goes green there (it isn't in this PR's own check list). - The restore-and-retry recovery only triggers from the
pinned_version == target_versionbranch — tradeoff. Otherfallback_install(manifest, false)call sites in the same function (no active managed SDK version, an unreadable pin, aselected_backend_lookuperror) don't get the same recovery, so an identically stale pin reached via one of those rarer paths stays unrecovered. Plausible but speculative — confirm this narrower scope (the one failure mode actually seen in CI) was the deliberate choice rather than an oversight.
Positive signals
- The restore/retry control flow (
install_pinned_backend_or_restore_packaged_pins) was traced through every branch — install succeeds; fails then restore+retry succeeds; fails then restore succeeds but retry fails (put-back succeeds or itself fails); restore is a no-op because the pins already match packaged; restore itself errors — and each is correct, with the ROCm backend-directory aside/resolve pairing (set_aside_rocm_llamacpp_backend_dirs/resolve_rocm_llamacpp_backend_dirs_aside) never leaked or double-resolved in any outcome. - Tests exercise realistic failure scenarios against real pin files and real backend directories (not just mocked closures), including a simulated stray directory from a failed install and the exact wording/root-cause-vs-chain distinction in the warning messages, each asserted together with the resulting on-disk state per AGENTS.md §3's message-assertion rule.
docs/engine-plugins.md's new paragraph was checked clause-by-clause against the code and is accurate on every point, including the subtle ones (original error reported unchanged when there's nothing to restore, the warning names only the root cause and not the chain's--reinstalladvice, the retry is forced, aside/resolve pairing correct in every branch).
Deployment notes
None — no new dependency, no migration, no config change. write_atomic's visibility change is additive (widens crate-private to crate-public) and doesn't alter its existing callers' behavior.
What this covered
Full review of the whole change: engines/lemonade/src/install.rs, engines/lemonade/src/backend_alignment.rs, crates/rocm-core/src/lib.rs, and docs/engine-plugins.md, each read in full (not just the diff hunks), plus AGENTS.md, docs/architecture.md, relevant git log/git blame on the touched regions, and prior commits touching the backend-directory aside/resolve and archive-cache-locking helpers these functions build on. Commit range: merge-base of origin/main and pr-754 (24e405a7) through d9796e48, the single commit on this PR. CI re-checked live via gh pr checks 754: all checks pass or are intentionally skipped; no E2E Strix Halo (gated, hardware) run appears in this head's check list, consistent with the PR's own description. Did not run cargo test/cargo clippy locally — relied on tracing the code plus the PR's own stated local verification and CI's green result. Not reconciled against this PR's existing review threads or its prior nowycondro review (at an older commit) — this is a fresh, blind pass against the current head.
d9796e4 to
620a872
Compare
|
Thanks, these were good catches. The branch is rebased onto current 1. Partial directory from the first attempt. Real, fixed, but not by setting the directories aside before the first attempt: that would move a working backend on every plain install, including the ones that succeed. Instead the ROCm backend entries (the 2. Restore re-downloading the archive. Done, and stricter than a short-circuit: the restore is now cache-only. It never downloads. If the archive is not cached or its digest does not verify, the restore is skipped with a warning and the original install error is reported. I did not short-circuit "before the archive work when the pins already match", because the packaged values come from the archive; reading the cached archive is a local extraction, so on the common offline failure the cost is now a local read, not a download. 3. Pin file read before the archive work. Fixed: the file is read after the archive work, immediately before computing and writing, and that read is what the backup holds. Test: a writer changes the pin file between the archive work and the read (through a test-only hook that runs at that point); the backup holds the writer's bytes, the writer's other entries survive, and put-back restores them. Moving the read back to the top makes it fail. 4. Kill between set-aside and put-back. I did not implement orphan recovery. The runtime tree is shared between processes, so a leftover 5. Docs. I grepped README.md, docs/*.md, tests/e2e-cucumber/README.md and skills/ for alignment, backend_versions, pinned and reinstall claims. Nothing else claimed that a plain install never rewrites the pins or leaves backends untouched (the alignment text in 6. Windows lane and reproduction. I re-ran the reproduction on this Windows Strix Halo host with the final code, with an The second automated review on the same head, for completeness:
|
nowycondro
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 620a872
Review — needs work
Full review of the whole change (single commit 620a872e, base ea86403c). No ticket named for this change; the PR description's own account of scope matches what this review found — no overclaim or undisclosed work detected.
Blocking
-
PinFileBackup's backup conflates "pin file absent" with "pin file present but unreadable," so a failed recovery can delete a real file instead of restoring it —engines/lemonade/src/install.rs:324(let current = fs::read(&target).ok();),:346(None => packaged),:352(previous: current),:406-409(doc comment:`None` when the file did not exist, so putting it back removes it),:421-432(PinFileBackup::put_back).
fs::read(&target).ok()discards the specific error kind, so any read failure other than "file doesn't exist" (e.g. a Windows sharing violation from a concurrent writer — a scenario this very function's own neighboring doc comment says it's designed around, and whichread_manifest/alignment's own revert path treat as a real possibility) is treated identically to "no file was ever there." ThePinFileBackupbuilt from that read then carriesprevious: None, directly contradicting its own doc comment's contract (`None` when the file did not exist). If the subsequent forced retry then also fails,put_back()takes theNonebranch and callsfs::remove_file, deletingresources/backend_versions.jsonoutright, rather than restoring the real (merely unread) content that was there. No test exercises this through the realrestore_packaged_backend_versions_withfunction — the only "missing file" test constructsPinFileBackup::new(.., None)directly, bypassing this code path entirely.
Confidence 85 · architectural · Fix: match onio::ErrorKind— treatNotFoundas "no previous file", and propagate (bail!/?) any other read error instead of silently folding it into "nothing to restore." -
Two self-hosted E2E hardware checks are failing on this head — CI:
E2E tests (Strix Halo, Ubuntu, release, current)andE2E tests (Strix Halo, Ubuntu, nightly, current)both reportFAILUREas of this review (statusCheckRollupfor620a872e). The directly-relevant lane for this fix,E2E tests (Strix Halo, Windows, current), passed. Job logs were not retrievable from this review environment (403 on the Actions API), so whether the two Ubuntu failures are a regression from this change or pre-existing/runner flakiness (the PR's own description documents this general class of self-hosted-lane flakiness) could not be determined here.
Confidence 100 (directly read from live CI status) · correctness/CI · Fix: inspect the two failing jobs' logs before merge and confirm they're unrelated to this change (or fix/rerun if not).
Non-blocking
- Pre-existing, pattern-relevant: three sibling pin-writer functions in the same file still use non-atomic
fs::writefor the samebackend_versions.jsonthis change newly treats atomically —engines/lemonade/src/backend_alignment.rs:614(write_backend_versions_llamacpp_tag),:631(remove_backend_versions_llamacpp_tag),:686(write_backend_versions_therock_version). Not introduced by this diff, and not wrong on its own, but this change's own framing emphasizes "writes are atomic" for this exact file and makesrocm_core::write_atomicpubspecifically to reach it from here — the three older writers to the same file are the obvious next candidates and are left as-is.
Confidence 85 · mechanical · Fix: consider migrating these three towrite_atomicin a follow-up, or note why they're excluded.
Decisions for the author
None
Positive signals
restore_reads_the_pin_file_after_the_archive_work_not_beforeininstall.rsis a genuinely rigorous test: it simulates a concurrent writer mutating the pin file mid-restore and asserts the backup holds exactly what was on disk at write time, not a stale earlier read — a real race condition, actually exercised.- The new forced-retry path reuses the existing
set_aside_rocm_llamacpp_backend_dirs/resolve_rocm_llamacpp_backend_dirs_asidemachinery rather than reimplementing directory set-aside/restore, keeping the new recovery consistent with alignment's existing revert semantics instead of introducing a second, subtly different implementation. - The warning text surfaces
install_error.root_cause()rather than the full context chain, specifically avoiding repeating the confusing--reinstalladvice the CLI is already acting on — verified by a dedicated test (the_restored_warning_names_the_root_cause_not_the_reinstall_advice).
Deployment notes
- Behavior change on the happy-path-adjacent failure case: a
rocm engines install lemonadethat previously failed loudly and leftbackend_versions.json/the installed backend untouched can now, on a kept-but-stale pin, print a warning, rewrite the two alignment-owned pins, and replace the selected backend — exit 0 instead of exit 1. Operators or scripts depending on the old "hard fail, no mutation" behavior should account for this. ROCM_CLI_DISABLE_LEMONADE_BACKEND_ALIGNMENTdisables this new recovery path along with alignment itself (verified in code: thedisabledbranch returns before the new logic is ever reached).- See the two failing self-hosted E2E checks under Blocking before merging.
What this covered
Every file in the change read completely at 620a872e against base ea86403c (engines/lemonade/src/backend_alignment.rs, engines/lemonade/src/install.rs, crates/rocm-core/src/lib.rs, README.md, docs/architecture.md, docs/engine-plugins.md, docs/manual-testing.md, docs/testing.md), plus the pre-existing sibling helpers each new function calls (directory set-aside/restore, the pin read/write helpers, the cached-archive digest/lock machinery, write_atomic and its other callers) and cross-file consistency checks (pin-key literals, public-item usage, crate-edge/dependency direction). Run as two independent fan-out workers (one per primary Rust file, each also covering the docs most tied to its file) plus direct verification of both files and all five doc files by the driver; candidates were consolidated and re-verified against source before scoring, and several sub-70-confidence candidates from the workers (a bare-backtick doc citation matching existing file convention; a shared-constant suggestion for pin-key names; a speculative one-level-deep pruning gap; an already-self-documented warning-noise tradeoff; an already-self-healing kill-window overlap) were checked and dropped as not meeting the keep bar.
Did not run: cargo test/cargo clippy directly (read-only clone, PR head never checked out per method; relied on the PR's own CI instead). Prior-changes pass did not run — this review has no access to this PR's own discussion thread or any earlier review round by design, so could not check whether earlier feedback was addressed. The PR description's mutation-testing claims (reverting specific lines makes the new tests fail) were not independently reproduced. Not reconciled against any discussion on this PR.
juhovainio
left a comment
There was a problem hiding this comment.
No blocking issues from my read of the diff. The fix matches the stated root cause (a kept pin equal to the active ROCm version means alignment never reruns), and the tests assert each message together with the state it describes. A few non-blocking notes, with the first three also inline:
fs::read(&target).ok()treats any read error as a missing file. Only NotFound should.- The scratch extract dir name is millisecond-only; a pid suffix would avoid a collision.
- Question on Windows: what happens when the set-aside rename fails because lemond holds the backend dir?
- The PR is large for one recovery branch. A lighter option would be to restore the two pins and call the existing forced reinstall, without the prune step. Not a request, just something to weigh.
- CI: Strix Halo Ubuntu release and nightly are red at bench-06 "a model is being served on GPU" (device state not drained after 120s), although the backend install itself succeeded. It looks like runner contention rather than this diff, but a rerun to confirm would be good. MI300X and W7900 had not reported when I looked.
This reflects a read of the diff only, not a local build or run.
| // computation and the backup: a pin file written by another process | ||
| // while the archive was being verified and extracted is what gets | ||
| // restored over and backed up, not a stale earlier read. | ||
| let current = fs::read(&target).ok(); |
There was a problem hiding this comment.
fs::read(&target).ok() collapses any read error (permission, Windows sharing violation) into "file missing". In that case the code writes the whole packaged file with a None backup, and a later put-back deletes it. Consider treating only NotFound as missing and returning an error for other kinds, so the caller reports the original install error and the pin file is left alone.
| archive.display() | ||
| ) | ||
| })?; | ||
| let extract_root = root |
There was a problem hiding this comment.
Nit: the scratch dir is keyed by milliseconds only, so two processes sharing the runtime tree could pick the same directory. Unlikely, but a pid suffix is free.
| )); | ||
| // Forced: the failed attempt may have left a half-installed backend | ||
| // for the withdrawn pin. | ||
| let aside_dirs = set_aside_rocm_llamacpp_backend_dirs(manifest); |
There was a problem hiding this comment.
Question: on Windows, if a running lemond or llama-server holds one of these backend dirs open, does set_aside_rocm_llamacpp_backend_dirs fail the rename, and if so what does the forced reinstall do next? The new tests don't seem to cover that case.
|
🔴 Automated review · pr-review-watcher · d9796e4 This automation never files a GitHub approval, so no approving review will
Review — needs workFull review of the whole change at d9796e4. The PR head has since moved to 620a872, so this review is of an older commit. Blocking
Non-blocking
Decisions for the author
Positive signals
Deployment notes
What this covered
|
620a872 to
4a45d1b
Compare
|
Thanks. Both blocking points were checked against the code; the fix is in Unreadable pin file read as absent. Confirmed. Ubuntu Strix Halo lanes. Not caused by this change. Both lanes report 8 failed scenarios: 7 are expected failures with rows in I also took the older review's two test gaps, which were still true: restore tests for a packaged file without Not changed: the three older Not run: |
|
🔴 Automated review · pr-review-watcher · 620a872 This automation never files a GitHub approval, so no approving review will
Review — needs workFull review of the whole change at 620a872. That commit is no longer the PR head: the PR now points at 4a45d1b, which this review did not read. Blocking
Non-blocking
Decisions for the author
Positive signals
Deployment notes
What this coveredI read the whole diff
Two read-only workers ran: one on the coupled Rust files, and one covering agent-instruction adherence, doc accuracy, history and code comments. I ran consolidation, the design questions (not fanned out further, because of the two-worker cap) and Step 9 myself. The reviewed commit is stale. The live PR head is 4a45d1b. According to its description, that head adds the remove-a-pin test, makes an unreadable pin file an error, and changes how the backup read works. This report does not cover 4a45d1b, and the description's claims about it were not checked against code. Re-run the review on the current head. Did not run:
The commit sign-off check ran and passed (1 commit, none missing). This report was not reconciled against any PR discussion. |
…n stops installing A runner's shared Lemonade tree can hold backend_versions.json pins left by an earlier successful ROCm alignment (the active ROCm version plus a newer llama.cpp tag). When the TheRock tarball those pins name is later withdrawn, every `rocm engines install lemonade` fails with HTTP 403: the pin already equals the active ROCm version, so alignment, whose revert is the only other thing that rewrites the pin, is never attempted, and the advertised recovery is unavailable to the prewarm step that does not pass --reinstall. When installing the already-pinned backend fails, restore the two pins that alignment writes (therock.version and llamacpp.rocm-stable) from the digest-verified cached embeddable archive and install once more, forced. The restore never downloads: the same install has just verified that archive into the cache, and if it is gone or fails its digest the restore is skipped and the original install error is reported. The same holds when the pin file exists but cannot be read: only a missing file counts as absent, so an unreadable one is left alone instead of being backed up as nothing (which a failed retry would then have deleted). Every other entry of the file is kept, the file is rewritten through rocm_core::write_atomic, and it is read only after the archive work, so a concurrent writer is neither overwritten blindly nor missing from the backup. The forced retry is wrapped the way alignment's own revert is: the existing ROCm backend directories are set aside first and put back if the retry fails. The ROCm backend entries are listed before the first attempt, and after a failed retry anything not in that list (a partial build the failed attempts left) is removed once the directories are back. If the retry fails, its error is reported and the previous pin file is put back, because any install failure starts the restore, including a transient one. If the pins already hold the packaged values, or the restore itself fails, there is no second attempt and the original error is reported. The warning names the root cause, not the --reinstall advice that the first attempt's error carries. A process killed between the set-aside and the put-back leaves the working backend under a .pre-alignment name, and loses the in-memory copy of the previous pin file; this is the same window alignment's own revert has and is documented rather than recovered, because the runtime tree is shared and a leftover aside cannot be told from another process's install in progress. Signed-off-by: Eugene Volen <eugene.volen@amd.com>
4a45d1b to
d71fab4
Compare
|
Follow-up on the review of
|
Problem
E2E tests (Strix Halo, Windows, current)fails in its prewarm step, before any scenario runs:rocm engines install lemonadeexits 1 withHTTP error 403downloadingtherock-dist-windows-gfx1151-7.14.1.tar.gz.The 403 is not the cause. The runner's shared Lemonade tree keeps a patched
resources/backend_versions.jsonfrom an earlier successful ROCm alignment (therock.version=7.14.1and a newer llama.cpp tag). The TheRock tarball that pin names is not served, so the install fails. Alignment never runs again because the pin already equals the active ROCm version, and the alignment revert is the only other thing that rewrites the pin. The message tells the user to run--reinstall, but the prewarm step does not pass it, so the failure repeats on every run.Evidence, from the logs of 8 runs of that lane: the runs that start from a stale first-attempt tag fail, and the runs that start from the packaged tag hit the same 403 and 404 responses, revert to the packaged 7.13.0 defaults and pass. That is why the lane looks flaky. It has failed on main pushes as well as on PRs, depending on which runner state it landed on.
Change
When installing the already-pinned backend fails, the install restores the two pins that backend alignment writes (
therock.versionandllamacpp.rocm-stable) from the digest-verified cached embeddable archive, and installs once more, forced.docs/engine-plugins.mdis updated, with short notes inREADME.md,docs/testing.mdanddocs/manual-testing.md;docs/architecture.md'swrite_atomicentry now says the function ispuband has a Lemonade caller.This is a change to what a plain
rocm engines install lemonadedoes: on a failed install of a kept pin it can print a warning, rewrite those two pins and replace the selected backend, where it used to report the failure and leave both alone.downloads/(every install does, before the backend step), so the cache is normally there. If it is gone or does not match its digest, the restore is skipped, a warning says why, and the original install error is reported.backend_versions.jsonis kept, so a hand edit to another entry survives. If the file is missing or is not JSON, the packaged file is written whole. If the two pins already hold the packaged values, nothing is rewritten. The pin file is read after the archive work, immediately before the rewrite, and that read is also what the backup holds, so a pin written by another process in between is not overwritten blindly.rocm serveto choose between. The ROCm backend entries are listed before the first attempt, and after a failed retry anything that was not in that list (for example a partial build the first attempt left) is removed once the directories are back. The directories are not set aside before the first attempt, because that would move a working backend on every plain install.--reinstall, which the CLI is already doing.rocm_core::write_atomic, which is nowpubfor this.ROCM_CLI_DISABLE_LEMONADE_BACKEND_ALIGNMENTdisables this path too. The existing alignment code's own edit of the pin file is unchanged.Tradeoffs and known limitations, stated rather than hidden:
--reinstall.ROCM_CLI_DISABLE_LEMONADE_BACKEND_ALIGNMENTis the supported way to keep them..pre-alignmentname; a process killed there leaves it there. The previous pin file is held in memory only, so a kill during the retry also leaves the packaged pins on disk (the next install runs alignment against them again). Alignment's own revert path has the same window. It is not recovered automatically because the runtime tree is shared between processes and a leftover aside cannot be told from another process's install in progress without a lock. It is documented indocs/engine-plugins.md;--reinstallrebuilds the tree.Verification
Reproduced on a Windows Strix Halo host on the code of the previous head
620a872e(the amend since then only changes how the pin file is read for the backup and adds tests; the reproduction was not repeated on it) (debug builds ofrocmandrocm-engine-lemonade; the base binary isorigin/mainatea86403c), each in its own isolated config/data/cache directory:rocm engines install lemonadeonce, cleanly (no managed SDK in that data dir): exit 0, packaged pins7.13.0/b9752, backend installed.7.14.1the active managed ROCm version (this is a synthetic record, not a real SDK install; it only feeds the version lookup). Thenbackend_versions.jsonis patched totherock.version=7.14.1,llamacpp.rocm-stable=b11783plus an unrelated keyhand-edited-unrelated, and the installed ROCm backend directory is removed.rocm engines install lemonadeagain:therock-dist-windows-gfx1151-7.14.1.tar.gz,Lemonade backend installation failed again; run rocm engines install lemonade --reinstall ..., exit 1; the pins are left at7.14.1/b11783and no backend is installed.Warning: the llama.cpp backend pinned in resources/backend_versions.json could not be installed (...); restored the pins Lemonade ships and trying again., exit 0, pins back to7.13.0/b9752, theb9752backend installed, and the unrelated key still present.Unit tests in
backend_alignment.rsandinstall.rs, each asserting the message together with the real state it claims:--reinstalladvice;llamacpp.rocm-stableremoves that key from the current file, and a drift in only that key triggers the restore;I changed the production code under each new test and it fails: removing the prune after a failed retry, removing the missing-cache check (the restore then reports a digest mismatch instead of the not-cached error), moving the pin-file read back before the archive work, treating every read error as an absent file, making the remove-a-pin arm a no-op, and making the drift check ignore
llamacpp.rocm-stable; the equal-pin restore-failure test now asserts the restore was attempted and fails when the restore step is skipped. The earlier tests were mutation-checked on the previous version of this change. Each mutation was reverted and the file compared byte-identical.On Windows:
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo clippy -p e2e-cucumber --test e2e -- -D warnings,cargo xtask check-crate-edgesare clean, andcargo nextest run -p rocm-core -p rocm-engine-lemonadepasses 922 tests (one reported as leaky:rocm-core process::tests::a_timed_out_child_has_its_output_quoted_within_a_bound, which this change does not touch). Under WSL2,cargo test -p rocm-engine-lemonade --lib -- restore pin backup retrypasses (33 tests).Not covered / gaps (AGENTS.md section 3)
E2E tests (Strix Halo, Windows, current): its prewarm step runsrocm engines install lemonadeagainst the runner's shared tree, and it is the step that fails today. It only exercises this change on a runner that still holds the stale state. That lane is not in this head's check list, so no CI hardware run has shown the fix; the manual reproduction above is the evidence, and the lane's result after merge is what confirms it.cargo test --workspace(single process) andscripts/smoke_local.pyoutside the pre-push hook;cargo xtask e2e; the missing-cache branch against a real install (every install refills the cache first, so it is covered by the unit tests only); the reproduction used debug builds and a synthetic registry record, not a real 7.14.1 SDK install.