Repository navigation
Capture backend identity for Lemonade managed-serve failures - #912
jussielo-amd wants to merge 24 commits into
Conversation
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>
There was a problem hiding this comment.
🟡 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
| for platform in ["mock", "strix-windows"] { | ||
| assert_eq!( | ||
| resolve(&scenario, &cap(platform), &matrix, Included::default()), | ||
| Expectation::ExpectPass, | ||
| "{id} on {platform}", | ||
| ); | ||
| } |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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/--helpprobes 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-serverbinary 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() |
There was a problem hiding this comment.
--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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
…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>
|
Pushed 5f37981 with fixes for the inline notes (see the replies on each) and a merge of On your three questions:
|
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>
|
The Windows lane found a bug in a test I added in the last push, and c05b62c fixes it. |
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>
|
Merged |
juhovainio
left a comment
There was a problem hiding this comment.
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/--helpprobes 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.jsonmanifest path matches what the CLI writes, and evidence is taken before the stop. A wrong path degrades silently tounavailable:on the real lane.
Smaller points
backend_candidatessearches 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.tomlheader; it is still true until the new evidence lands. - Please confirm a repo-wide
grep EAI-7423is 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
|
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).
|
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).
There was a problem hiding this comment.
🟡 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>
There was a problem hiding this comment.
🟡 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>
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>
There was a problem hiding this comment.
🟡 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) { |


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.
version.txtare read before the service is stopped, and thellama-server --versionand--helpprobes 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 throughAppPaths::engine_install_manifest_path, the same helper the Lemonade installer writes it with, so the two cannot drift. Candidates are searched underrocm-stable,rocm-nightly,rocmandvulkandown 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 whatbin/llamacppholds (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 Xand--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 exampleserve m --engine --managed) counts as no engine named, so the report still prints theengine unknownline 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 sayssha256 unavailable: executable exceeds 512 MiB hash limit.flakyis set per row and is not derived frombug, 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 thetherock_familyor thehas_amd_gpuguard from the bench and chat rows turns a test red; the earlier mock-capability case could not, because itsos_familyisotherand theos = "linux"condition excluded it first.AppPaths::engine_install_manifest_pathhelper inrocm-core, which the Lemonade engine's ownmanifest_pathnow calls; it returns the same path as before.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-scriptllama-serverstand-ins used by the other probe tests remain unix-only. Not covered anywhere is thekill/reap failedbranch, which needs an operating system that refuses to kill a child it just started. I could not run the Windows lane locally;clippyfor the Windows target passes for both the library and thee2etest 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)andE2E tests (Strix Halo, Ubuntu, release, current)in the E2E self-hosted run for that head): the manifest was read from the data dir, therocm-stableb12109llama-serverwas found one directory below its backend directory and fingerprinted under the old 32 MiB cap, and--versionand--helpexited 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'sprobes, run after the service stop was attemptedsection should follow itsstopsection. 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 testbackground_helper_not_spawned_when_no_automation_is_enabledinapps/rocm/src/service_launch.rs(which checks the gate function, not theservecall site) and by scenarioautomations-11on the mock lane (the helper's own lifecycle). Whether allama-server --versionor--helprun 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'sstopsection 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
mainat the time, plus one commit per review fix and merges ofmain(the branch is merged rather than rebased so the existing review context keeps matching).tests/e2e-cucumber/expectations.tomlfor the fixed ticket ID and removed/narrowed any now-stale xfail rows.docs/architecture.md(the clap declaration and dispatch wiring staying inmain.rs/lib.rsis expected, not a violation).Refs #897