Skip to content

Capture backend identity for Lemonade managed-serve failures - #912

Open
jussielo-amd wants to merge 24 commits into
mainfrom
e2e/lemonade-serve-failure-evidence
Open

jussielo-amd wants to merge 24 commits into
mainfrom
e2e/lemonade-serve-failure-evidence

Conversation

@jussielo-amd

@jussielo-amd jussielo-amd commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Strix Halo Ubuntu managed serves return success without exposing the model, with logs showing argument rejection, backend-ready/router-unavailable, or download failure. Existing xfails attribute all three to an immediate shutdown.

  • Diagnostics: Capture runtime version, configured pins, candidate backend paths, SHA-256 fingerprints, and version/help output alongside archived service logs. The report reads files first and executes a backend second: the manifest, pins, candidate paths, fingerprints and version.txt are read before the service is stopped, and the llama-server --version and --help probes run after the stop returns, with their own 20 second budget. Both steps and the stop sit in one function, backend_evidence_around_stop, which runs the stop exactly once for every engine. The runtime manifest is read through AppPaths::engine_install_manifest_path, the same helper the Lemonade installer writes it with, so the two cannot drift. Candidates are searched under rocm-stable, rocm-nightly, rocm and vulkan down to three directory levels (the cpu backend is deliberately left out). The search is bounded as a whole: the inventory's 20 second deadline starts at entry and is checked at every directory and entry, at most 128 directories and 2048 entries are read across all backend directories, and the walk ends as soon as it has eight candidates, in sorted order. Symlinks are never followed (a link cycle ends, and a link out of the backend directory is not read; the report counts them), and a search cut short by the clock or a limit says which and never emits the completed-empty-search text, which is reserved for a search that finished and found nothing. The manifest and pin reads, the search and each fingerprint run on blocking threads with that in-traversal deadline. An empty search lists the locations it covered and what bin/llamacpp holds (16 names out of at most 64 entries read, with the omitted count), and does not guess at a cause. Bound probe duration and retained output; kill and reap timed-out children. The engine is read from both --engine X and --engine=X, falls back to the printed serve plan, and the failure report says so when no engine can be determined. A value that starts with -- after --engine (for example serve m --engine --managed) counts as no engine named, so the report still prints the engine unknown line instead of silently skipping the inventory. Executables up to 512 MiB are fingerprinted (streamed 64 KiB at a time under the inventory's time budget); a larger one is still listed and probed, and its entry says sha256 unavailable: executable exceeds 512 MiB hash limit.
  • Expectations: Replace shutdown-only attribution with the observed failure signatures and cover serve-40's shared serve precondition (main now carries the serve-40 row itself, so this PR keeps that single row and only the other rows' attribution changes). Retain intermittent-failure handling and existing platform scope. The header keeps both the rule that flaky is set per row and is not derived from bug, and the caveat to revisit before deleting any of these rows until a failed run on the lane has carried the new backend evidence. The regression tests include Linux hosts with no GPU, with a gfx target but no usable GPU, and with a GPU but no gfx target, so removing either the therock_family or the has_amd_gpu guard from the bench and chat rows turns a test red; the earlier mock-capability case could not, because its os_family is other and the os = "linux" condition excluded it first.
  • Scope: Diagnostic coverage, not a confirmed backend fix. Preserve the runtime pin and asynchronous managed-launch contract; distinguish configured pins and candidate binaries from proof of what actually ran. The only change outside the e2e harness is the small AppPaths::engine_install_manifest_path helper in rocm-core, which the Lemonade engine's own manifest_path now calls; it returns the same path as before.
  • Scenario coverage: This changes the e2e harness's failure reporting and xfail expectations only, not anything the CLI prints or does, so there is no new user-observable behavior to cover with a scenario. The existing serving scenarios keep running; their xfail attribution is what changes.

Verification. The search bounds are pinned by unit tests that go red when each guard is removed: the clock read during the walk (driven by an injected clock, no sleeping), the whole-search entry and directory limits, the cap ending the walk when reached, symlink handling on Unix with a real link cycle, the bounded listing, and the scan running off the async executor. The ordering is pinned by unit tests in lemonade_diagnostics.rs: the stop step deletes the candidate, so a fingerprint in the report shows the file was read before the stop, and a recording probe shows the probes ran after it; moving either step to the other side of the stop turns the test red. An engine test writes a manifest and asserts it lands at the shared path with the field names the report reads, and it goes red when the engine builds a different path. The probe's timeout, kill and reap path now runs on every platform: the unit tests re-run the test binary itself as the child (an ignored helper test selected by an environment variable), let the probe time out, and require the child to be gone afterwards (run with the child on one test thread and on several, because libtest labels a single-threaded child's output), and leaking the child instead of killing it turns that test red. The shell-script llama-server stand-ins used by the other probe tests remain unix-only. Not covered anywhere is the kill/reap failed branch, which needs an operating system that refuses to kill a child it just started. I could not run the Windows lane locally; clippy for the Windows target passes for both the library and the e2e test target, and the Windows lane on the push is what runs the new child-process tests there. The e2e scenarios themselves were not run locally (no GPU).

Lane evidence. At the previous head, the Strix Halo Ubuntu nightly and release jobs both printed the new report five times each (E2E tests (Strix Halo, Ubuntu, nightly, current) and E2E tests (Strix Halo, Ubuntu, release, current) in the E2E self-hosted run for that head): the manifest was read from the data dir, the rocm-stable b12109 llama-server was found one directory below its backend directory and fingerprinted under the old 32 MiB cap, and --version and --help exited 0. That run used the old probe order (probes before the stop), so it shows the report is populated and the manifest path is right, not that the new order changes anything. The same two jobs on this push are what confirm the new order: the report's probes, run after the service stop was attempted section should follow its stop section. The W7900 and MI300X lane jobs had not reported at the time of writing.

Known trade-offs, stated so they are not read as more than they are. serve-40 (serve-stop-leaves-no-background-helper) is xfail and flaky on lemonade/linux because it needs the same real serve, so until #897 closes its "no background helper" assertion carries no signal on that lane: a pass there does not show the scenario's behaviour is healthy and a failure is not attributed to it. The guard itself stays covered by the unit test background_helper_not_spawned_when_no_automation_is_enabled in apps/rocm/src/service_launch.rs (which checks the gate function, not the serve call site) and by scenario automations-11 on the mock lane (the helper's own lifecycle). Whether a llama-server --version or --help run initialises a GPU backend has not been observed on a lane, which is why the probes now run after the stop and not next to the router; a stop that fails or finds no record can still leave the router running, and the report's stop section says so. The 512 MiB hash cap is an assumption about builds larger than the one observed (a statically linked multi-target build), not a measured size.

Supersedes #899, which had the same content under a bot author; this is the same change started from main at the time, plus one commit per review fix and merges of main (the branch is merged rather than rebased so the existing review context keeps matching).

  • If this PR fixes a bug, searched tests/e2e-cucumber/expectations.toml for the fixed ticket ID and removed/narrowed any now-stale xfail rows.
  • If this PR adds a new subcommand or subsystem, its domain implementation lives in its own file per docs/architecture.md (the clap declaration and dispatch wiring staying in main.rs/lib.rs is expected, not a violation).
  • Every new or changed user-facing message was read against the code path that runs after it, and its test asserts the resulting state — not only the wording — per AGENTS.md §3. Covers claims of an outcome, remediation advice naming a command, and promises that something will not happen.

Refs #897

Strix Halo Ubuntu managed serves can return success without exposing the
model, with logs showing argument rejection, backend-ready/router-unavailable,
or download failure. The existing xfails attribute all three to an immediate
shutdown.

Capture the runtime version, configured pins, candidate backend paths,
SHA-256 fingerprints and version/help output alongside the archived service
logs before cleanup. Probe duration and retained output are bounded, hashing
is capped and runs off the async executor, and timed-out children are killed
and reaped. The engine is read from both `--engine X` and `--engine=X`, falls
back to the printed serve plan, and the failure report says so when no engine
can be determined. Probe capture and kill/reap results are printed only when
they fail.

Replace the shutdown-only xfail attribution with the observed failure
signatures and cover serve-40's shared serve precondition. Intermittent-failure
handling and platform scope are unchanged.

This is diagnostic coverage, not a confirmed backend fix: configured pins and
candidate binaries are evidence of what could have run, not proof of what did.

Refs #897

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
@jussielo-amd
jussielo-amd requested a balanced review from Copilot November 5, 2026 11:28
@jussielo-amd
jussielo-amd marked this pull request as ready for review November 5, 2026 11:28
@jussielo-amd
jussielo-amd requested a review from a team as a code owner November 5, 2026 11:28
@jussielo-amd
jussielo-amd requested a review from rominf November 5, 2026 11:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The regression test uses a non-Linux mock capability, so it skips the relevant xfail guards and needs Linux/no-GPU cases, including one with a reported gfx target, to protect MockServer-backed chat scenarios.

1 open issue
What changed in this PR

This PR improves E2E diagnostics for Lemonade managed-serve failures tracked in #897 without changing runtime pins or the CLI’s asynchronous launch behavior.

Changes:

  • Captures candidate backend identities, configured pins, and bounded version/help output before service cleanup.
  • Updates expected-failure attribution to cover observed failure signatures and serve-40’s shared precondition.
  • Adds diagnostic and expectation tests, plus documentation of evidence limits.
File Description
tests/​e2e-cucumber/​tests/​e2e/​serving_steps.rs Collects backend evidence before stopping failed services.
tests/​e2e-cucumber/​src/​serve_log.rs Adds engine detection and backend evidence to reports; bounds reads.
tests/​e2e-cucumber/​src/​lib.rs Exposes the diagnostics module.
tests/​e2e-cucumber/​src/​lemonade_diagnostics.rs Implements backend inventory, fingerprints, bounded probes, and tests.
tests/​e2e-cucumber/​src/​expectation.rs Tests updated failure attribution and platform scope.
tests/​e2e-cucumber/​features/​model_serving.feature Updates a comment’s xfail reference to #897.
tests/​e2e-cucumber/​expectations.toml Revises failure descriptions and adds serve-40 coverage.
tests/​e2e-cucumber/​Cargo.toml Adds hashing and asynchronous process/I/O dependencies.
docs/​testing.md Documents collected evidence, limitations, and focused tests.
Cargo.lock Records the E2E crate’s hashing dependency.

Review effort: Balanced

Comment on lines +852 to +858
for platform in ["mock", "strix-windows"] {
assert_eq!(
resolve(&scenario, &cap(platform), &matrix, Included::default()),
Expectation::ExpectPass,
"{id} on {platform}",
);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2649611: a new test covers Linux hosts with no GPU, with a gfx target only, and with a GPU only, and still expects the xfail for the full GPU host. Removing has_amd_gpu from bench-load-real-serve turns only the new test red, and so does removing therock_family from chat-end-to-end-local-model; the old test stays green in both cases, which confirms your point.

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I went through the whole diff. I found no correctness problems in the new diagnostics; the hash worker's own deadline inside spawn_blocking, the kill-and-reap on the probes, and the boundary tests around the size cap all look right. Two small things are left as inline comments, and I'm requesting changes so they get a look before merge.

Questions for the author:

  • serve-40 is now xfail+flaky on lemonade/linux. The comment in the PR admits its assertion carries no signal on that lane, so a real background-helper regression there stays hidden until #897 closes. Is that the intended trade-off?
  • The llama-server --version/--help probes run while the failed service is still alive. On a shared-memory APU, could a probe that initialises the GPU backend contend with the still-running router? I couldn't verify this either way.
  • A llama-server binary larger than the 32 MiB hash cap gets no fingerprint. That is probably fine for current builds, but I haven't confirmed it.

The branch is also CONFLICTING with main (Cargo.lock, tests/e2e-cucumber/Cargo.toml, tests/e2e-cucumber/src/lib.rs). It needs a rebase, so the green CI so far is on the pre-rebase head.

let value = if let Some(inline) = arg.strip_prefix("--engine=") {
Some(inline)
} else if arg == "--engine" {
args.next()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

--engine followed by another flag (e.g. serve m --engine --managed) returns --managed as the engine here. The report then neither captures backend evidence nor prints the "engine unknown" line, which breaks the promise that a skipped inventory is visible. Treating a value that starts with -- as absent would fix it; a test case for it would be good too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f475592: a value starting with -- after --engine (and --engine=--managed) now counts as no engine named, so the report prints the "engine unknown" line. Both forms are in the test table, and with the guard removed the test fails with left: Some("--managed").

# the model appearing on :11435 (41984503055), and backend download failure
# (41986772728). A backend ready line does not prove a later shutdown.
# EAI-7423 described only the earlier ready-then-unavailable symptom.
# Retain flaky=true because historical runs also passed intermittently; do not

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The header this replaces also said that flaky is per-row and not derived from bug, so a new row has to set it explicitly. That rule is gone now. Worth keeping that sentence for the next person adding a row.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Restored in f237d2d: the header says again that flaky is per-row and not derived from bug, so a new #897 row has to set flaky = true explicitly.

…reports

A serve invocation such as 'serve m --engine --managed' made the engine lookup return '--managed'. The failure report then skipped the backend inventory without printing the 'engine unknown' line that makes a skipped inventory visible. A value that starts with '--' is another flag, not an engine, so it now counts as absent, for both the separate and the '--engine=' forms.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The header comment replaced in this PR also said that flaky is set per row and is not derived from the bug reference, so a new row for the same bug has to set it explicitly. Restore that sentence next to the rule that keeps flaky=true for the #897 rows.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…tion rows

The existing regression test resolves the real-GPU rows against the mock capability, whose os_family is 'other', so the os=linux condition excluded it before the therock_family and has_amd_gpu guards were consulted. Removing either guard left the test green even though Linux MockServer-backed bench and chat scenarios would then be expected to fail. Add Linux hosts with no GPU, with a gfx target but no usable GPU, and with a GPU but no gfx target, and assert that a full real-GPU host is still expected to fail.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…gerprint

The size-cap tests only covered the fingerprint helper. Cover the report itself: a llama-server larger than the hash cap is still listed and probed, and the report prints the reason it has no sha256 line, so the missing fingerprint is visible in the failure output.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Cargo.lock conflicted in the e2e-cucumber dependency list: this branch adds sha2 and main adds shlex. Both are kept, in sorted order, and 'cargo metadata --locked' accepts the result. tests/e2e-cucumber/Cargo.toml and tests/e2e-cucumber/src/lib.rs merged automatically with both sides' additions.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…obe-timing behaviour

Document that a value starting with -- after --engine counts as no engine named, that a llama-server over the hash cap is listed and probed with an explicit 'sha256 unavailable' reason, and that the version/help probes run before the failed service is stopped, with the GPU contention question marked as unchecked.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
@jussielo-amd

Copy link
Copy Markdown
Collaborator Author

Pushed 5f37981 with fixes for the inline notes (see the replies on each) and a merge of main instead of a rebase, so your review context stays valid. The only conflict was Cargo.lock in the e2e-cucumber dependency list: I kept both sha2 from this branch and shlex from main, in sorted order, and cargo metadata --locked accepts it; tests/e2e-cucumber/Cargo.toml and src/lib.rs merged cleanly. The PR body is updated, including dropping the stale "squashed to one commit" claim.

On your three questions:

  1. serve-40 is xfail and flaky on lemonade/linux on purpose: until E2E: lemonade serving fails on the Strix Halo Ubuntu lanes, with a different log signature depending on the run #897 closes the assertion carries no signal on that lane, and the row is to be removed when it does (the expectations.toml comment says so, and the PR body now does too). What covers the guard elsewhere, verified in the code: the unit test background_helper_not_spawned_when_no_automation_is_enabled in service_launch.rs covers the gate function and scenario automations-11 covers the helper's lifecycle on the mock lane. The gap is that neither covers the serve call site itself.
  2. Probe contention: every call site in serving_steps.rs runs the evidence collection only after a non-zero exit or a timed-out readiness poll, and only after the log has been read and archived, so the failing outcome is already decided. The router can still be alive during the probes, which run one at a time (5 s each, 20 s total) in the harness environment, not the engine's. What I cannot verify here (no GPU, no llama-server) is whether --version or --help initialises a GPU backend, so that part is unchecked, and docs/testing.md now says so.
  3. A binary over the 32 MiB cap is still listed and probed, and the report prints sha256 unavailable: executable exceeds 32 MiB hash limit; no partial hash is ever labelled SHA-256. 25e11c5 adds a report-level test for it, which goes red when that line is suppressed.

cargo test -p e2e-cucumber passes (226 lib tests plus the extra targets), and clippy, fmt and cargo xtask manifest --check are clean. Could you take another look?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The new llama-server probes run before a potentially live router is stopped, and their GPU initialization and contention risks remain unvalidated on Strix Halo hardware.

1 open issue

Review effort: Balanced

The oversized-executable test asserted that the report contained the display form of a path it built with join("bin/llamacpp/rocm-stable"), while the inventory joins "bin/llamacpp" first and the backend directory second. On Windows the two spell the same path with different separators, so the substring check failed even though the report was right. Parse the candidate lines back into paths and compare those; Path equality is component-wise, so both spellings agree.

The same helper replaces a negative check in the candidate-cap test that could never fail on Windows: asserting the report did not contain the display form of the skipped candidate passed vacuously there. It now asserts the listed candidates exactly (count, the skipped one absent, the first one present).

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
@jussielo-amd

Copy link
Copy Markdown
Collaborator Author

The Windows lane found a bug in a test I added in the last push, and c05b62c fixes it. oversized_executable_reports_why_it_has_no_fingerprint built its expected candidate path with / separators and compared printed text, while the report on Windows prints the same path with mixed separators (...\bin/llamacpp\rocm-stable\llama-server.exe); the report itself was correct. The test now parses the candidate lines back into paths and compares them component-wise. Auditing the same class in this PR found one more instance: evidence_truncates_candidates_without_hashing_or_probing_the_rest asserted that the skipped candidate's printed path was absent, which could never match on Windows and so passed vacuously. It now asserts the exact set of listed candidates (the count, the absent skipped one and the present first one), and a mutation that lists the wrong eight turns it red. The other new tests use no paths. One gap remains and is not new: five tests in lemonade_diagnostics.rs are unix-only because they use shell-script stand-ins, so the probe timeout and kill path does not run on the Windows lane. I could not run a Windows check locally (the C toolchain for the cross build is missing), so the Windows lane on this push is the confirmation.

tests/e2e-cucumber/expectations.toml conflicted in two places. Main had removed the runtime-path-not-nested row, so it stays removed. Main also added its own serve-stop-leaves-no-background-helper row (lemonade, linux, #897, serve_timeout_secs = 90, flaky = true) with its own explanation; this branch had added an identical-condition row for the same scenario. The two are reconciled to the single row from main, whose fields match what this branch set, so the scenario is not listed twice. The header sentence that flaky is set per row and is not derived from the bug reference is kept, and the bench-load-real-serve row keeps its #897 attribution.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
@jussielo-amd

Copy link
Copy Markdown
Collaborator Author

Merged main into the branch again (5463b83, a plain merge commit) because main moved, including #900, which adds its own serve-stop-leaves-no-background-helper row to expectations.toml. The only conflict was in that file: the merge left two identical rows for that scenario (same lemonade/linux condition, #897, 90 s timeout, flaky = true), so I kept main's row with its fuller comment (it already says that while the row is in place the scenario's own helper check is not exercised, and that the row has to be deleted by hand when #897 closes) and dropped this branch's copy, so the scenario is listed once. The sentence saying flaky is per-row and not derived from bug is kept in the header, runtime-path-not-nested, which main removed, stays removed, and the regression test for the mock scope still passes against the merged row. cargo test -p e2e-cucumber --lib passes (238), clippy and fmt are clean, and the PR body is updated to match.

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall: no blocking problems found; the scope is honest (diagnostics for #897, not a fix) and CI is green. Requesting changes on the points below.

Should address

  • The llama-server --version/--help probes run before the service is stopped and may initialise a GPU backend alongside the live router, perturbing the state being diagnosed. Move the probes after the stop, or confirm on the lane that they are harmless and replace the "has not been checked" note in docs/testing.md.
  • No test ties the new call site to the documented behaviour: the runtime.json manifest path matches what the CLI writes, and evidence is taken before the stop. A wrong path degrades silently to unavailable: on the real lane.

Smaller points

  • backend_candidates searches only one level under four fixed backend dirs, and its empty-result message hints at a download problem.
  • 32 MiB hash cap may be below real ROCm server binaries.
  • The "revisit before deleting" caveat was dropped from the expectations.toml header; it is still true until the new evidence lands.
  • Please confirm a repo-wide grep EAI-7423 is clean.
  • The Windows timeout/kill path of the probe has no test and the PR text does not say so.
  • The strix lane that would exercise the new report was not among the checks; please note which run confirms the report is populated.

&crate::results_path(),
world.current_scenario.as_deref().unwrap_or("scenario"),
);
let backend_evidence = match engine {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Evidence, including the llama-server --version/--help probes, is collected here, before stop_scenario_services below. llama.cpp ROCm/Vulkan builds typically initialise the backend on --version, so the probes may touch the GPU next to the still-running router and perturb the state being diagnosed. Could the probes (not the file inventory/hashes) run after the stop, or has the lane shown they are harmless? docs/testing.md currently says this is unchecked.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in dfb844f. The probes now run after the stop: the manifest, pins, candidate paths, fingerprints and version.txt are still read here, before the stop, and the --version and --help probes execute only after stop_scenario_services returns, with their own 20 second budget. The order lives in one function, backend_evidence_around_stop, so this call site cannot reorder it, and docs/testing.md now describes what the code does instead of saying it was unchecked. I could not show on a lane whether a probe initialises a GPU backend, so I kept them off the router's side of the stop rather than call them harmless. At the previous head the Strix Ubuntu lanes ran the probes before the stop and both exited 0, which says nothing about contention either way.

Some("lemonade") => Some(
e2e_cucumber::lemonade_diagnostics::backend_evidence(
&world
.app_paths()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nothing asserts that this is the file the CLI actually writes, nor that this call precedes the stop. A wrong filename would only show up as unavailable: ... on the real lane, which defeats the feature. Consider sharing the filename constant with the producer or adding a test for the path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 05bcc1c and dfb844f. The filename is now one definition, AppPaths::engine_runtime_manifest_path, which the Lemonade installer's manifest_path and this call site both use, and an engine test writes a manifest and asserts it lands at that path with the field names the report reads (it goes red when the engine builds a different path). For the order, backend_evidence_around_stop takes the stop as a closure, and its test makes the stop delete the candidate: the fingerprint in the report shows the inventory came before the stop and a recording probe shows the probes came after it. Moving either step to the other side of the stop turns that test red. On the lane, the manifest was read correctly at the previous head: the Strix Ubuntu nightly and release jobs both printed the manifest, pins and a fingerprinted llama-server.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correction to my earlier reply: the shared helper is AppPaths::engine_install_manifest_path, not engine_runtime_manifest_path. It was renamed in d4ca134 because the registry writer guard on main (a text match for runtime_manifest_path( in any file that also writes a file) matched the old name inside the Lemonade installer, and the new name also says what it is, the engine's own install manifest and not a runtime registry entry. The path it returns and the engine and harness call sites that share it are unchanged.

report
}

fn backend_candidates(runtime: &Path) -> Vec<PathBuf> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

backend_candidates only looks at depth 0 and 1 under four hard-coded backend dirs, and when nothing is found the report hints at an incomplete download. Consider wording the empty case neutrally and listing the searched locations.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 59d7891 (with a test-order correction in f5bff5f). The search now goes three directory levels below each of the four backend directories, bounded by an entry cap per directory, and the cpu backend is still left out on purpose. An empty result no longer hints at a download: it lists every location searched, the depth, and what bin/llamacpp actually contains (or that it cannot be read), because an interrupted download, an unsearched layout and a build that was never installed look the same. A test with literal layouts shows a build three levels down is found and one four levels down is not, and it goes red when the depth is lowered. For reference, on the Strix Ubuntu lanes the b12109 build sits one level below rocm-stable, so the old depth already found it there.

candidates
}

fn fingerprint(server: &Path, deadline: Instant) -> std::io::Result<String> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

32 MiB hash cap: please confirm real ROCm llama-server builds fit under it, otherwise the fingerprint is absent on exactly the lane this is for.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Checked, and changed anyway as margin, in b064945 and ee08e62. The Strix Halo Ubuntu nightly and release jobs at the previous head both fingerprinted the rocm-stable b12109 llama-server under the old 32 MiB cap, so a current ROCm build fits. A statically linked multi-target build could be larger, so the cap is now 512 MiB; hashing still streams 64 KiB at a time under the inventory time budget, so memory and time stay bounded, and the over-cap line is worded from the constant. The remaining assumption, about builds larger than the observed one, is stated in docs/testing.md and the PR body.

…ine writes it to

The serve failure report reads the installed runtime's manifest to find the
backend directory, and built the path with its own "runtime.json" literal next
to the engine's. A rename on either side would have degraded the report to
"unavailable:" on the one lane it exists for, with nothing failing before then.

AppPaths::engine_runtime_manifest_path is now the one definition. The engine's
manifest_path and the harness both call it, and an engine test writes a manifest
and asserts it lands at that path with the field names the report reads.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…pped

The version and help probes executed a backend binary while the failed serve's
router could still be running. llama.cpp builds commonly initialise a GPU backend
even for --version, so on a shared-memory device a probe could contend with the
router and change the state the report exists to describe. docs/testing.md said
this had not been checked.

The report now reads files first and executes second: manifest, pins, candidate
paths, fingerprints and version.txt are taken before the stop, and the probes run
after it, with their own 20 second budget so a slow stop cannot use it up. The
order lives in one function, backend_evidence_around_stop, which also runs the
stop exactly once for every engine, so the serve step cannot reorder or skip it.
Its tests make the stop delete the candidate, which shows the fingerprint came
before the stop and, through a recording probe, that the probes came after it.
docs/testing.md and the expectations header now state what the code does.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…empty search looked

The inventory looked only at depth 0 and 1 under four backend directories, while
the router chooses how a llama.cpp build is unpacked, so a build one level
deeper would have produced no candidates and no probe. When nothing was found,
the report also suggested the backend download may not have completed, which an
unsearched layout or a never-installed build would equally explain.

The search now goes three directory levels below each backend directory, bounded
by an entry cap per directory. An empty result names every location searched, the
depth, that the cpu backend is deliberately left out, and what bin/llamacpp
actually contains, without naming a cause. docs/testing.md states the search.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…sorts them

The previous commit's depth test listed its two expected candidates in the wrong
order: the search sorts sibling directories by path, so a/b comes before a/build.
The test therefore failed even with the depth limit in place. With the order
corrected it passes, and with the limit lowered to two it fails because the
three-level candidate is no longer found.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
… MiB

A server above the old 32 MiB cap got no fingerprint, so the report would be
missing its identity evidence on exactly the kind of build it is meant to
describe. A dynamically linked llama-server is a few MiB, but one that links the
HIP kernels for several GPU targets statically can be hundreds, and neither the
repository nor this environment has the release asset sizes to settle it.

The cap is now 512 MiB. Hashing already streams 64 KiB at a time and checks its
deadline between reads, so memory stays bounded and time is bounded by the
inventory budget; nothing about the cap changes either. The over-cap line is
worded from the constant so it cannot drift from it, the boundary test uses a
small limit instead of hashing half a gibibyte, and a separate test shows a file
above the old cap is fingerprinted. docs/testing.md states the cap and that it is
an assumption about the builds.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The probe's timeout, kill and reap handling was tested only through shell-script
llama-server stand-ins, which exist on unix, so the Windows lane never ran it and
the change did not say so.

The probe is split into building the llama-server command and running a command
with bounded time and output, so the second part can run any child. Two tests
re-run the unit-test binary itself as that child, through an ignored helper test
selected by an environment variable: one lets the probe time out and requires the
child to be gone afterwards (killed and reaped, not merely abandoned), the other
covers a child that exits and its status and output. Replacing the kill with a
leak turns the first red. The kill/reap failed branch stays untested, because it
needs an operating system that refuses to kill a child it just started, and
docs/testing.md says so.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…s header

The header rewrite for the #897 rows dropped the sentence telling the next
person to revisit before deleting any of these rows, because an XPASS streak is
not evidence of a fix while the lane cannot say which Lemonade build a run used.
That is still true until a failed run there has carried the backend evidence this
branch adds, so it is back, reworded for that. The sentence that flaky is set per
row and not derived from bug was restored earlier and is unchanged.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…ppy lints

The CI clippy runs found what the unit tests could not: the async helper and its
Probe trait were not Send, two helpers could be const, one assertion was on a
constant, and a probe test spelled a minute in seconds. A Send helper cannot
borrow the scenario world, which holds PTY handles and is not Sync, so the serve
step now hands it the isolated root and the app paths by value, and the stop is
split into one function on the world and one on the root path. The ordering
tests use a mutex and an atomic instead of cells, and the size-cap assertion is a
const block.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
… old hash cap

The Strix Halo Ubuntu lanes on this branch fingerprinted the rocm-stable b12109
llama-server under the former 32 MiB cap, so the cap change is margin for larger
static builds and not a repair for current ones. The cap's note said the size was
an assumption; it now says what was observed and what remains an assumption.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
@jussielo-amd

Copy link
Copy Markdown
Collaborator Author

Thanks, all points are addressed on this branch, and I merged main again in 11f338d (no conflicts; the conflicts from the earlier review are already resolved).

  • Probes before the stop: moved after it, dfb844f. File reads stay before the stop; the probes run after it with their own budget. docs/testing.md and the expectations header now state what the code does.
  • No test tied the call site to the documented behaviour: the manifest path is shared with the producer and tested against it (05bcc1c), and the order against the stop is pinned by a test that goes red if either step moves (dfb844f). A wrong path still shows as unavailable: in the report, but it can no longer differ from the one the installer writes.
  • --engine followed by another flag: already fixed in f475592 (a value starting with -- counts as no engine, so the engine unknown line prints), with both forms in the test table.
  • expectations.toml header: the per-row flaky sentence was restored in f237d2d, and the revisit-before-deleting caveat is back in a9bf807, reworded for what is true until a failed run on the lane has carried the new evidence.
  • backend_candidates and its empty-result message: wider search and a neutral, location-listing message, 59d7891.
  • 32 MiB cap: confirmed from the lane that current builds fit, cap raised as margin, b064945 and ee08e62.
  • grep -rn EAI-7423 is not empty, and I left the hits: one is a bare ticket id in this diff, in an expectations.toml comment saying EAI-7423 described only the earlier symptom, and the other two come from main (the serve-40 row's comment from test(e2e): expect serve-40 to fail on lemonade Linux hosts while #897 is open #900, and a fixture in expectation.rs). Bare ticket ids are allowed by the repo's contribution rules, and no new rows use it as bug.
  • Windows timeout and kill path: now tested on every platform in 465912f by re-running the test binary as the child process; leaking the child instead of killing it turns it red. The kill/reap failed branch is still untested, because it needs an OS that refuses to kill a child it just started, and the PR text and docs/testing.md say so. I could not run the Windows lane locally, only clippy for the Windows target, so the Windows job on this push is the confirmation.
  • Strix lane: the Strix Halo Ubuntu nightly and release jobs in the E2E self-hosted run for 5463b83 printed the new report five times each, with manifest, pins, a fingerprinted llama-server and --version and --help exit 0; the W7900 and MI300X jobs were still pending. That run used the old order, so the same two jobs on the next push are what confirm the new one (the probes, run after the service stop was attempted section after stop).
  • The CI clippy runs also needed one lint fix, 1e514a9.

@jussielo-amd
jussielo-amd requested a balanced review from Copilot November 6, 2026 15:35
@jussielo-amd
jussielo-amd dismissed juhovainio’s stale review November 6, 2026 15:35

Fixed: every point is answered on the PR; the probes now run after the stop (dfb844f), the manifest path is shared with its producer and tested (05bcc1c), the candidate search and its empty-result message are reworked (59d7891), the hash cap is raised (b064945), the probe kill path is tested (465912f) and the expectations header caveats are restored (a9bf807).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Candidate discovery runs before the deadline and applies its cap only after recursive search, allowing large or symlinked caches to exhaust time and memory and delay service shutdown, cleanup, and failure reporting.

2 open issues

Review effort: Balanced

…f the async executor

The three-level candidate search added earlier ran before the inventory's
deadline existed, applied the eight-candidate cap only after walking everything,
read directories on an async worker, followed symlinks, and listed an unbounded
bin/llamacpp when it found nothing. A large or linked cache could therefore delay
the stop and the failure report well past the intended budget.

The deadline now starts at inventory entry and is read at every directory and
entry during the walk, through an injected clock so a test drives it without
sleeping. Directory and entry limits (128 and 2048) apply to the whole search, not
per directory, and the walk ends as soon as it has eight candidates, in sorted
order. Symlinks are never entered or taken as a candidate, so a cycle ends and a
link out of the tree is not read; the report counts them. A search cut short by
the clock or a limit says so instead of claiming an empty result, and the listing
of what bin/llamacpp holds reads at most 64 entries, shows 16 and says how many
it left out. The manifest and pin reads, the search and each candidate's
fingerprint run on blocking threads with the in-traversal deadline, since a
timeout around a blocking task only drops its handle. docs/testing.md states the
limits.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The timeout regression test can fail with single-threaded libtest output, and directory- or entry-limited searches can falsely report that all backend trees were searched.

3 open issues
1 resolved since last review

Review effort: Balanced

…arly

The empty-search explanation, which lists every backend directory as searched,
was suppressed only when the clock ended the search. A search stopped by the
directory or entry limit still produced it, so with a real build under
rocm-stable/b and the cap reached before b was entered, the report said all four
backend trees had been searched and held nothing.

The explanation is now emitted only when the search finished, and every early
stop reports its own reason instead. The regression test uses that exact tree
and asserts the empty-result claim is absent for the directory limit, the entry
limit and the clock, and present for a finished search that found nothing. The
candidate cap needs no row: it only stops a search that already has candidates.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…t thread

The probe timeout test reads the pid its helper child prints. With one test
thread (RUST_TEST_THREADS=1, or a one-CPU host) libtest writes the test-name
label first and the child's output continues that line, so the line-start match
for the marker found nothing and the test failed even though the child had been
killed and reaped.

The child now starts a fresh line before the marker, and the test runs the child
both on one test thread and on several (overlapping, so it costs one timeout), so
the single-thread case is exercised wherever the suite runs. With the newline
removed the single-thread run fails; it failed the same way before this change
under RUST_TEST_THREADS=1.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed to evaluate a symlink-handling gap that could expose file contents from outside the backend directory in failure reports.

1 open issue
2 resolved since last review

Review effort: Balanced

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…r guard does not match it

The registry guard in runtime_registry.rs reads each production file as text and
flags one that both writes a file and spells runtime_manifest_path(. The shared
helper added for the Lemonade install manifest was called
engine_runtime_manifest_path, which contains that spelling, and the Lemonade
installer both calls it and writes its manifest, so the guard reported a second
registry writer. It only showed once the guard reached main.

The helper is now AppPaths::engine_install_manifest_path, which also says what it
is: the engine's own install manifest, not an entry in the runtime registry. The
doc comment records why the names must stay apart. The path it returns is
unchanged, and so is the guard.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The diagnostics report can read and embed an outside file through a symlinked version.txt despite reporting the link as skipped, so the marker needs a non-symlink regular-file check and regression coverage.

2 open issues

Review effort: Balanced

}
if let Some(parent) = server.parent() {
let version = parent.join("version.txt");
if let Ok((bytes, note)) = read_clamped(&version, MAX_EVIDENCE_BYTES) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants