Skip to content

fix(cli): accept negative float flag values in space form (EAI-8243) - #306

Merged
r0x0r merged 3 commits into
mainfrom
fix/negative-float-flags-space-form
Aug 27, 2026
Merged

fix(cli): accept negative float flag values in space form (EAI-8243)#306
r0x0r merged 3 commits into
mainfrom
fix/negative-float-flags-space-form

Conversation

@r0x0r

@r0x0r r0x0r commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

rocm serve --temperature -1 (space-separated form) was rejected by clap with the
confusing error unexpected argument '-1', because clap parsed the leading-dash
token as a flag rather than a value. Only the equals form (--temperature=-1)
reached the value validator that reports the valid range.

This is the classic negative-number-as-flag gotcha. It is a tokenizer-level
ambiguity that fires before any value parser runs, so it is not float-specific —
it affected every numeric value flag: --temperature and --top-p (float) and
--max-tokens (positive integer)
on both chat and serve.

Fix

Enable clap's allow_negative_numbers on the affected numeric flags so the space
form reaches the value parser. Both forms now validate identically and surface a
clear range/parse message instead of an unexpected-argument error.

allow_negative_numbers is preferred over allow_hyphen_values here: it only
accepts leading-dash tokens that parse as numbers, so it does not swallow the next
flag when a value is missing (the tradeoff called out for --gpu-memory-utilization).

Testing

  • Gherkin scenario (AGENTS.md §3): @id:serve-negative-temperature-rejected
    in tests/e2e-cucumber/features/model_serving.feature covers the user-observable
    stderr change end-to-end. It is ungated — the value parser runs inside argument
    parsing before engine selection or GPU pre-flight, so it needs no GPU and no
    engine and gates every PR (mirrors the ungated @id:fix-position-argument-rejected
    precedent). The scenario is now numbered (Scenario: 15 - ...) for consistency
    with the rest of the suite.
  • Extended the space-form regression tests (serve/chat) to cover
    --temperature, --top-p, and --max-tokens in both space and equals
    forms, asserting they reach ValueValidation (the value parser) rather than
    being rejected as an unexpected argument.
  • cargo test -p rocm --bin rocm (touched sampling tests pass); cargo clippy -p rocm --all-targets -- -D warnings; cargo fmt -p rocm.

Deliberate scope-out (follow-up)

rocm fix --device-index -1 has the same gotcha with its own dedicated
--device-index must be >= 0 message (crates/rocm-core/src/fix.rs), but it is
not the only remaining instance. The same negative-as-flag rejection still
fires on a wider class of single-value numeric flags across the tree — for
example rocm serve --port -1, rocm bench load --isl -1 (and
--osl/--requests/--batch-sizes/--keep), rocm diagnose --top -5, and
rocm comfyui logs --lines -5 — roughly eight flags, not one. These are left to
a follow-up to keep this PR one logical change. The durable fix for the recurring
class is a whole-tree guard test walking Cli::command() to assert every
single-value numeric arg sets allow_negative_numbers/allow_hyphen_values; it
belongs with that follow-up, which is "fix the class" rather than "fix
--device-index".

The space form of negative-number float flags (e.g. `serve --temperature
-1`) was rejected by clap with a confusing "unexpected argument '-1'"
error, because clap parsed the leading-dash token as a flag rather than a
value. Only the equals form (`--temperature=-1`) reached the range
validator that reports the valid range.

Enable `allow_negative_numbers` on the affected float flags
(`--temperature` and `--top-p` on both `chat` and `serve`) so the space
form reaches the value parser and both forms validate identically,
surfacing a clear range-validation message instead of an unexpected
argument error.

Add regression tests covering both the space and equals forms for
`--temperature` and `--top-p` on `chat` and `serve`.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
@r0x0r
r0x0r requested a review from a team as a code owner August 24, 2026 10:44
@r0x0r
r0x0r requested a review from michaelroy-amd August 24, 2026 10:44
@r0x0r r0x0r changed the title fix(cli): accept negative float flag values in space form fix(cli): accept negative float flag values in space form (EAI-8243) Aug 24, 2026
@volen-silo

Copy link
Copy Markdown
Collaborator

Code review — changes requested

The fix itself is correct and well-chosen. I verified the mechanism against the pinned clap (4.6.1) and against the built binary on this branch: rocm serve foo --temperature -1 now reports invalid value '-1' for '--temperature <TEMPERATURE>': temperature must be a finite value >= 0.0, and pre-fix the same input returns ErrorKind::UnknownArgument, so the new tests genuinely discriminate the bug rather than tautologically passing. Preferring allow_negative_numbers over allow_hyphen_values is the right call and the PR body explains why — confirmed in clap source that it only fires when the whole token after - parses as a number, so --temperature --top-p 0.5 still errors with "a value is required" instead of swallowing the next flag.

Two things to resolve before merge.

1. Missing Gherkin scenario (AGENTS.md §3)

AGENTS.md §3 states that if a change alters what a user of the CLI can observe — command output, exit codes — the behavior "must be covered by a Gherkin scenario in tests/e2e-cucumber/features/", that "a unit test asserting the internal helper does NOT discharge this", and that the carve-out is for "purely internal changes (refactors, CI plumbing, docs)".

This change alters CLI stderr for a command users type directly, so the carve-out does not apply, and the PR body's justification ("Behavior is CLI argument parsing only... The new unit tests assert the user-observable error kind") is the exact substitution the rule forbids. No scenario in tests/e2e-cucumber/features/ mentions temperature, top-p, or max-tokens today, so there is no existing @id: to name either.

There is a direct precedent for this class in the repo — @id:fix-position-argument-rejected (tests/e2e-cucumber/features/diagnose.feature:60) is a confusing-argument-error-becomes-clear fix that got an ungated scenario.

Adding one here is cheap and needs no gating tags: the value parser runs inside try_parse_from before model resolution or any GPU pre-flight, verified by running the binary with a bogus model name and no --engine. Then serving is refused before any engine starts already exists (tests/e2e-cucumber/tests/e2e/serving_steps.rs:784) and is reusable verbatim; only one new When and one new Then are needed (~16 lines, mirroring user_serves_gpu_required at serving_steps.rs:709 and assert_no_gpu_message at serving_steps.rs:797). No expectations.toml row — that file is for known-bug xfails only. Something like:

  @id:serve-negative-temperature-rejected
  Scenario: Serving with a temperature below zero is refused with a clear reason
    When the user serves a model with a negative sampling temperature
    Then serving is refused before any engine starts
    And the CLI explains that temperature cannot be negative

2. --max-tokens has the identical bug, on the same two commands

apps/rocm/src/main.rs:269 (chat) and :413 (serve) declare --max-tokens with value_parser = parse_positive_u32 and no allow_negative_numbers. Verified on the binary built from this branch:

$ rocm serve foo --max-tokens -1
error: unexpected argument '-1' found

...while --max-tokens=-1 reaches the parser and reports max-tokens must be greater than 0 (main.rs:8237). That is exactly the asymmetry this PR fixes, three lines away in the same struct, with a dedicated message that stays unreachable in the space form.

The PR body's claim ("all float flags that accept a value") is literally accurate, but the underlying gotcha is not float-specific — it is a tokenizer-level ambiguity that fires before any value parser runs, so the argument that "u32 has no legitimate negative value" does not make the user-facing symptom any less confusing. Please either include --max-tokens (plus a test case) or state the scope-out explicitly in the PR body so it is a deliberate decision rather than an oversight.

Design note — this gotcha has now recurred three times

git log -S allow_hyphen_values / -S allow_negative_numbers shows two prior independent fixes for the same clap gotcha in this file (32acc52d, 7cdc30c1) before this one, and two instances are still open (--max-tokens, and --device-index on fix). Per-flag annotation has not prevented recurrence.

A whole-tree guard test would close the class durably, and the repo already has the idiom: cli_command_definition_is_valid (main.rs:17694) and hidden_subcommands_excluded_from_completions_tree already walk Cli::command(). Confirmed the needed getters are public in clap_builder 4.6.0 — Arg::is_allow_negative_numbers_set (arg.rs:4462), is_allow_hyphen_values_set (arg.rs:4457), get_value_parser (arg.rs:4488) — so a ~30-line recursive test asserting "every single-value arg with a numeric value parser sets allow_negative_numbers or allow_hyphen_values" is writable with public API only. Worth considering here or as a follow-up; not blocking this PR.

Non-blocking

  • Comment overclaims slightly. --temperature -0.5abc still yields error: unexpected argument '-0' found, because clap only treats a token as a negative number when the entire remainder parses as one. The comment's "instead of clap rejecting -1 as an unexpected argument" is fine for the literal case it names, but the general framing does not hold for malformed negative-looking input.
  • Comment coverage. The 3-line explanation sits above --temperature in both structs (main.rs:260, :404) but never above --top-p (:266, :410), which gets the same treatment for the same reason. A reader landing on --top-p sees the attribute with no rationale. Either repeat it or write it once covering both flags — the verbatim duplication across the two structs is also a small maintenance smell.
  • Test comment asymmetry. serve_negative_sampling_space_form_reaches_range_validator carries the explanation; the adjacent, materially identical chat_... test has none.
  • Equals-form cases in the new tests are not regression guards. --temperature=-1 / --top-p=-0.5 already passed before the fix (clap resolves --opt=value through parse_opt_value unconditionally once the arg matches), and serve_rejects_out_of_range_sampling (main.rs:18239) already exercises the equals path. Keeping them as an explicit "both forms behave identically" assertion is defensible — just noting they add no new discriminating coverage.
  • Adjacent, out of scope: rocm fix --device-index -1 has the same gotcha, and crates/rocm-core/src/fix.rs:852 has a dedicated --device-index must be >= 0 message that the space form can never reach. Worth a follow-up.

Positive signals

  • Choosing allow_negative_numbers over the broader allow_hyphen_values, and calling out in the PR body why (it will not swallow the next flag when a value is missing) — that tradeoff is the non-obvious part and it is documented in the right place.
  • The regression tests fail before the fix and pass after, verified rather than assumed.
  • All CI checks are green on the current head.

@r0x0r

r0x0r commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — thorough review. Addressed in 862b0e0.

1. Gherkin scenario (blocking, §3): added @id:serve-negative-temperature-rejected to model_serving.feature, ungated per your @id:fix-position-argument-rejected precedent — the value parser runs inside try_parse_from before engine selection or GPU pre-flight, so it needs no GPU/engine and gates every PR. It reuses serving is refused before any engine starts verbatim and adds one When (mirrors user_serves_gpu_required) and one Then (mirrors assert_no_gpu_message) asserting the temperature must be a finite value >= 0.0 message. e2e-cucumber compiles with the new steps.

2. --max-tokens (blocking): included it. Added allow_negative_numbers to --max-tokens on both chat and serve, and extended both space-form regression tests with --max-tokens -1 / --max-tokens=-1 (they now reach parse_positive_u32ValueValidation instead of UnknownArgument). PR body updated to say the fix covers all numeric value flags, not just floats.

Non-blocking, addressed:

  • Comment coverage / overclaim: reworked the shared rationale comment to cover all three numeric flags in one place (above --temperature in both structs), and it now states clap only treats a -… token as a number when the whole remainder parses as one — so a missing value (--temperature --top-p 0.5) or a malformed token (--temperature -0.5abc) still errors at parse time. That removes the earlier overclaim.
  • Test comment asymmetry: the chat_… test now carries a symmetric comment pointing back to the serve_… one.
  • Equals-form cases: kept as an explicit "both forms behave identically" assertion, as you noted they're defensible even though they aren't new discriminating coverage.

Deliberate follow-up (not in this PR): rocm fix --device-index -1 shares the gotcha with its own --device-index must be >= 0 message. I scoped it out in the PR body to keep this one logical change. The whole-tree Cli::command() guard test is the right durable fix for the recurring class, but it would necessarily fail until --device-index is also fixed, so it belongs with that follow-up rather than here.

@volen-silo

Copy link
Copy Markdown
Collaborator

Follow-up review — both blocking findings resolved

Re-verified against the current head 862b0e03 (changed from a5003c0c, which is what the original review covered). Checked out that commit locally and ran the gates — cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace --all-targets all exit 0, no failures.

1. Missing Gherkin scenario — resolved

Not taken on trust: I ran it. @id:serve-negative-temperature-rejected executes ungated on a no-GPU WSL host and passes all three steps, with the harness reporting 0 xfail, 0 XPASS, 0 unexpected failure(s) — so the "gates every PR" claim holds, and no expectations.toml row is needed. It also genuinely discriminates the bug: pre-fix the CLI emits unexpected argument '-1' found, so serving is refused before any engine starts would still pass on the non-zero exit but the CLI explains that temperature cannot be negative would fail. The asserted literal matches the parser's message exactly (main.rs:8222). Step definitions follow the surrounding style — serve_output reuse, assertion-message format, the lowercase-then-match idiom that assert_no_gpu_message already uses.

2. --max-tokensresolved

allow_negative_numbers is now on --max-tokens in both structs (main.rs:272, :419). Verified on a binary built from this head:

$ rocm serve foo --max-tokens -1
error: invalid value '-1' for '--max-tokens <MAX_TOKENS>': `-1` is not a valid positive integer
$ rocm chat --prompt hi --max-tokens -1
error: invalid value '-1' for '--max-tokens <MAX_TOKENS>': `-1` is not a valid positive integer

I also went looking for a regression and did not find one. allow_negative_numbers is evaluated per-Arg in clap 4.6, not command-wide, so it cannot leak into the model positional or interact with allow_hyphen_values on --gpu-memory-utilization in the same struct. Confirmed on the binary: serve -1 … still rejects the positional, --max-tokens 100 with trailing flags parses fine, and serve foo --max-tokens (missing value) still reports a value is required rather than swallowing the next token. The new space-form test case is the discriminating one (pre-fix it returns UnknownArgument, not ValueValidation); the equals-form case is the "both forms behave identically" assertion, as agreed.

Non-blocking items from the original review

  • Comment overclaim — resolved, and the new wording is accurate. Checked both halves against clap 4.6 and the binary: --temperature --top-p 0.5 gives a value is required for '--temperature' (a long flag is always a new arg regardless of the setting), and --temperature -0.5abc gives unexpected argument '-0' found. Both "error at parse time" as the comment now says.
  • Comment coverage — resolved. One block covering all three flags. Minor and not worth changing: the block sits above --temperature while --max-tokens is six lines further down past --top-p's own doc comment.
  • Test comment asymmetry — resolved.
  • Equals-form cases — your call, and it's the right one. Keeping them as an explicit both-forms assertion is fine; no change wanted.
  • --device-index scope-out — accepted. Stating it in the PR body is what I asked for, and the guard test belongs with it rather than here.

One correction to the scope-out (not blocking, doesn't change the merge decision)

The PR body reads as though --device-index is the one remaining instance. It isn't — the class is wider than either of us said. On the binary built from this head:

$ rocm serve foo --port -1        → error: unexpected argument '-1' found
$ rocm bench load --isl -1        → error: unexpected argument '-1' found
$ rocm diagnose --top -5          → error: unexpected argument '-5' found
$ rocm comfyui logs --lines -5    → error: unexpected argument '-5' found

--osl, --requests, --batch-sizes and --keep look the same by inspection. That matters mainly for the follow-up's sizing: the whole-tree Cli::command() guard would fail on roughly eight flags, not one, so the follow-up is "fix the class" rather than "fix --device-index". Worth reflecting in the body so the next person doesn't scope it as a one-liner.

One new nit introduced by 862b0e03

model_serving.feature:143 is the only unnumbered scenario title in the suite. Every other scenario across all the feature files — including @id:fix-position-argument-rejected, the precedent this PR cites — is Scenario: <N> - <Title>. Worth a one-word fix (Scenario: 15 - Serving with a temperature below zero…) to keep the file consistent.

Verdict

LGTM. Both blocking findings are genuinely resolved, verified rather than accepted. The scenario-number nit and the PR-body scope-out correction are worth doing but neither blocks merge.

Note on CI: E2E tests (GPU) and the Read the Docs build are failing here, but they fail identically on main's latest run and on every other currently open PR, so they are pre-existing and unrelated to this change.

r0x0r added 2 commits August 27, 2026 12:20
…nario

The negative-number-as-flag gotcha is a tokenizer-level ambiguity that
fires before any value parser runs, so it is not float-specific:
`--max-tokens -1` (space form) on both chat and serve was still rejected
as an unexpected argument while `--max-tokens=-1` reached parse_positive_u32.
Add allow_negative_numbers to --max-tokens on both commands so the space
form reaches the value parser and reports a clear error, matching the
--temperature/--top-p behavior fixed here.

Per AGENTS.md §3, the user-observable stderr change is now covered by an
ungated Gherkin scenario, @id:serve-negative-temperature-rejected, in
model_serving.feature. The value parser runs inside argument parsing
before engine selection or GPU pre-flight, so the scenario needs no GPU
and no engine and gates every PR (mirrors the ungated
@id:fix-position-argument-rejected precedent).

Also: reword the shared rationale comment to cover all three numeric value
flags (and note clap only treats a token as negative when the whole
remainder parses as one, so missing/malformed values still error at parse
time), give the chat regression test a symmetric comment, and extend both
space-form regression tests with the --max-tokens cases.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
The new scenario was the only unnumbered title in model_serving.feature;
every other scenario uses 'Scenario: <N> - <Title>'. Assign the next free
number (15) to keep the file consistent. No behavior change.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
@r0x0r
r0x0r force-pushed the fix/negative-float-flags-space-form branch from 862b0e0 to 96ac1de Compare August 27, 2026 12:23
@r0x0r

r0x0r commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — both follow-ups are pushed.

  • Scenario number: model_serving.feature:143 is now Scenario: 15 - Serving with a temperature below zero is refused with a clear reason (15 was the next free number in the suite), so it matches the Scenario: <N> - <Title> convention every other scenario uses.
  • Scope-out correction: the PR body no longer implies --device-index is the last instance. It now names the wider class you found — --port, --isl/--osl/--requests/--batch-sizes/--keep, --top, --lines, plus --device-index (~eight flags) — and frames the follow-up (the whole-tree Cli::command() guard test) as "fix the class" rather than a one-liner.

Note on history: the branch's earlier commit was signed-off but not cryptographically signed, so it was failing the commit-signatures gate and blocking the push hook. I re-signed the range and force-pushed with lease; git range-diff confirms the content is byte-identical (signatures only). verify-commits, clippy, fmt and test all pass at the new head.

@r0x0r
r0x0r added this pull request to the merge queue Aug 27, 2026
Merged via the queue into main with commit ee6c1f9 Aug 27, 2026
22 of 25 checks passed
@r0x0r
r0x0r deleted the fix/negative-float-flags-space-form branch August 27, 2026 13:19
pmoutsias-amd pushed a commit that referenced this pull request Aug 27, 2026
…306)

* fix(cli): accept negative float flag values in space form

The space form of negative-number float flags (e.g. `serve --temperature
-1`) was rejected by clap with a confusing "unexpected argument '-1'"
error, because clap parsed the leading-dash token as a flag rather than a
value. Only the equals form (`--temperature=-1`) reached the range
validator that reports the valid range.

Enable `allow_negative_numbers` on the affected float flags
(`--temperature` and `--top-p` on both `chat` and `serve`) so the space
form reaches the value parser and both forms validate identically,
surfacing a clear range-validation message instead of an unexpected
argument error.

Add regression tests covering both the space and equals forms for
`--temperature` and `--top-p` on `chat` and `serve`.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* fix(cli): extend negative-value fix to --max-tokens and add serve scenario

The negative-number-as-flag gotcha is a tokenizer-level ambiguity that
fires before any value parser runs, so it is not float-specific:
`--max-tokens -1` (space form) on both chat and serve was still rejected
as an unexpected argument while `--max-tokens=-1` reached parse_positive_u32.
Add allow_negative_numbers to --max-tokens on both commands so the space
form reaches the value parser and reports a clear error, matching the
--temperature/--top-p behavior fixed here.

Per AGENTS.md §3, the user-observable stderr change is now covered by an
ungated Gherkin scenario, @id:serve-negative-temperature-rejected, in
model_serving.feature. The value parser runs inside argument parsing
before engine selection or GPU pre-flight, so the scenario needs no GPU
and no engine and gates every PR (mirrors the ungated
@id:fix-position-argument-rejected precedent).

Also: reword the shared rationale comment to cover all three numeric value
flags (and note clap only treats a token as negative when the whole
remainder parses as one, so missing/malformed values still error at parse
time), give the chat regression test a symmetric comment, and extend both
space-form regression tests with the --max-tokens cases.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* test(e2e): number the negative-temperature serve scenario

The new scenario was the only unnumbered title in model_serving.feature;
every other scenario uses 'Scenario: <N> - <Title>'. Assign the next free
number (15) to keep the file consistent. No behavior change.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>
mikeroySoft pushed a commit to mikeroySoft/rocm-cli that referenced this pull request Sep 1, 2026
* Add Sphinx documentation site

Adds a Sphinx documentation site under docs/rocm-docs/, single-sourced
from README.md and CONTRIBUTING.md via MyST {include} directives so
the docs and repo content stay in sync automatically. Covers
installation, getting started, command reference, demos, and
contributing/license, and is set up to publish with the rocm-docs-core
theme. Updates some links in README.md and CONTRIBUTING.md to be
absolute so they render correctly when reused via {include}.

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* Single-source the platform-support table in the install page

Anchor the table in README.md and pull it into
docs/rocm-docs/install/installation.md via {include} instead of
duplicating it by hand.

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* fix(docs): address review feedback on Sphinx docs site

- Add the MIT license header to the 7 new files hawkeye flagged as
  missing it.
- Fix the WSL2 platform-support-table link to docs/wsl.md: it's now an
  absolute GitHub URL so it resolves when the table is pulled into
  install/installation.md via MyST include, matching the pattern
  already used for the other README anchors.
- Pin rocm-docs-core to the exact version that resolved locally
  (1.40.0) instead of an open->= range.

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* fix(docs): add ReadTheDocs config and a CI job to build docs with -W

- Add .readthedocs.yaml so RTD builds against docs/rocm-docs/conf.py
  with the same pinned rocm-docs-core toolchain CI uses.
- Add a docs-build CI job that runs sphinx-build -W (warnings as
  errors) against the same source RTD points at, gated on the new
  `docs` path-filter category so it only runs when doc sources,
  README.md/CONTRIBUTING.md (single-sourced via MyST includes), or
  the build config change.
- Suppress the myst.header warning from :start-after:/:end-before:
  README includes stripping the anchor heading, which is cosmetic:
  docutils re-normalizes the resulting heading depth in the rendered
  output.
- Scope external_projects to [] instead of the implicit "all" default,
  which was fetching intersphinx inventories for every project in
  rocm_docs' bundled catalog (~90 projects) - several of which 404 or
  have moved upstream. No page here uses a cross-project intersphinx
  role, so this is lossless and makes -W builds deterministic instead
  of flaky against infrastructure this repo doesn't control.
- Filter the "current project not found in projects" warning: rocm-cli
  isn't registered in rocm-docs-core's shared projects catalog yet, so
  external_projects_current_project can never resolve until that's
  added upstream. rocm_docs.projects handles the unresolved project as
  None everywhere it's used, so this is informational, not a defect.

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* docs: use docs-relative cross-page links consistently

README.md's own #model-serving and #interactive-interfaces anchors were
rewritten to absolute GitHub URLs so they'd still work once pulled into
the docs site, but that breaks in-page anchor scrolling when the README
is viewed on GitHub. Revert both to relative anchors and instead apply
the cut-and-reauthor pattern already used for the CONTRIBUTING.md link
in installation.md: narrow the surrounding {include} in getting-started.md
and commands.md around each sentence and hand-author a docs-relative
replacement (commands.md#model-serving, getting-started.md#interactive-interfaces).

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>

* docs: use descriptive nav title for Demos section

Avoids the sidebar rendering as duplicated Demos > Demos for the
single-entry section by overriding the nav link text to match the
homepage tile's wording.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs: fix sentence fragments in intro blurb

Joins the "single prebuilt binary" fragments into one complete
sentence in both README.md and the standalone index.rst homepage
copy, and adds a lead-in sentence before the bare `rocm` code fence
in the Getting Started "First run" section.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs: style guide pass on README and CONTRIBUTING

Sentence-case headings, expand DCO acronym, replace banned/informal
wording (may, e.g.), normalize slash usage, lowercase version
placeholder.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(lemonade): retry interrupted backend setup (ROCm#249)

* fix(lemonade): retry interrupted backend setup

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

* ci(e2e): build prebuilt lanes with the test-hooks feature

The suite's deterministic failure seams are compiled out without
`rocm/e2e-test-hooks`. `cargo xtask e2e` passes the feature only when it
builds the binaries itself, so every lane that pre-builds and exports
ROCM_CLI_BINARY silently tested a binary without those seams. The
scripted-Lemonade-failure scenario then never reached its premise and
failed as a regression on the one lane that selects it.

Pass the feature on all prebuilt e2e lanes, and add a workflow-contract
test so a lane cannot drift back. Release and packaging builds keep the
feature off — a shipped binary must not carry a failure-injection seam.

Also name the cause in the retry assertion, which otherwise reports a
baffling missing announcement rather than the real misconfiguration.

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

---------

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

* ci(dependabot): track pre-commit hook versions (ROCm#235)

The hook pins in .pre-commit-config.yaml were bumped by hand and drifted
between releases. Dependabot's pre-commit ecosystem resolves each rev
against the hook repository's tags, and skips the builtin and local
blocks, which have no upstream release to track.

Weekly and grouped, with the same 7-day cooldown as the other two
ecosystems: a hook is executable code that runs on every contributor's
machine at commit time and in the prek CI job, so a compromised release
would run before anyone reads the bump.

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix: raise the supported Linux minimum to Ubuntu 24.04 (ROCm#261)

* fix: raise the supported Linux minimum to Ubuntu 24.04

Every published Lemonade embeddable, v10.2.0 through v11.5.2, is linked
against GLIBC_2.38 and GLIBCXX_3.4.32. Ubuntu 22.04 ships glibc 2.35, so
the Lemonade engine cannot start there at all, yet README.md advertised
Linux x86_64 as "full support ... both inference engines" and docs/wsl.md
named 22.04 as a supported WSL base.

Document a single minimum of Ubuntu 24.04 for Linux and WSL2, keeping the
glibc number as the reason so the requirement stays meaningful on other
distributions, and drop 22.04 from the WSL preflight's supported set so the
automated check matches the prerequisite it enforces.

Fixes ROCm#258

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>

* fix(wsl): accept Ubuntu releases after 24.04

Signed-off-by: Michael Roy <michael.roy@amd.com>

---------

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: Michael Roy <michael.roy@amd.com>
Co-authored-by: Michael Roy <michael.roy@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* ci(e2e): repair a pre-warm tree that records a folder elsewhere (ROCm#316)

A scenario reaches the shared pre-warm tree through a symlink at its own
`data/runtimes`. An `install sdk` run that way writes the link's path — a
per-scenario temp dir — into the manifest that lands in the SHARED
registry, and into the venv's console-script shebangs. The temp dir goes
away with the scenario and the shared runtime is left naming a folder
that no longer exists, so later, unrelated runs fail.

Drop such a runtime before deciding whether the tree is fresh, so the
pre-warm reinstalls instead of serving a dead one. `rocm update` cannot
see this: it compares versions against the index, not the runtime
against the disk.

Removing the folder is the repair, not a precaution. A poisoned venv
keeps a working `bin/python` — a symlink to the base interpreter, still
present — so the install reuses it and audits already-satisfied packages
rather than reinstalling them, leaving every shebang pointing at the
folder that went away. Measured on uv 0.9.30: reinstalling over a
poisoned venv reports success and repairs nothing.

The signal is an install root outside the tree, deliberately not
`status=unusable`. Unusable has many causes — a missing rocm_sdk probe
block alone reports it — and this deletes what it selects, so a healthy
multi-GiB runtime must not hang on a validation detail. Read-only
runtimes are exempt: `runtimes adopt` records an external folder on
purpose.

Refs: ROCm#315

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

* fix(install): record the folder the files land in, not the link (ROCm#317)

* fix(install): record the folder the files land in, not the link

`install sdk` built the install root by joining onto the data dir and
recorded whatever that produced. Reaching `data/runtimes` through a
symlink is enough to make that name a route rather than a place: correct
while the link exists, and wrong the moment it goes.

The path outlives the command. It goes into the registry manifest, the
sidecar beside the runtime, and — through uv — the `#!` line of every
console script in the environment. So a runtime installed that way keeps
reporting itself installed at a folder that is not there, while the files
sit untouched next door and every entry point fails to start.

Resolve the root before anything is written to it. Only the root:
`python_executable` is derived from it and must keep the venv's own
`bin/python`, which is itself a symlink to the base interpreter, so
resolving that would record the system Python instead. The adopt path
already draws the line in the same place, canonicalizing the install root
next to `absolute_existing_file_path_preserving_symlink` for the
interpreter; this gives the install path the same treatment, `--prefix`
included.

The E2E harness creates exactly this shape when a scenario opts into the
shared pre-warmed runtime, so the shared tree on a runner was what got
poisoned — but nothing about the fault is test-only. Any data dir reached
through a link has it.

Refs: ROCm#315
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

* fix(vllm): name the missing interpreter behind a failed launch

Two messages described a stale runtime as an absence.

A spawn that fails on a console script whose `#!` interpreter is gone
reports ENOENT against the script, so the error named a file that is
plainly there. Say which interpreter is missing when that is what
happened, and stay quiet when the ordinary reading is right — a genuinely
absent file, a binary, or a live interpreter.

Resolution drops a registered runtime whose recorded interpreter is not
there. That is correct — a runtime that cannot run is not a candidate —
but it left the failure describing an empty registry while `rocm runtimes
list` prints the entry. Name the manifests that were passed over and the
interpreters they record.

Both are additive: the note only ever decorates an error already being
returned, and a registry it cannot read yields no note rather than
replacing the original failure.

First tests for this area, which had none.

Refs: ROCm#315
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

* test(install): compare resolved paths, not verbatim ones, on Windows

The Windows lane failed on the prefix rather than the behaviour.
`canonicalize` hands back `\\?\C:\…` there, so a test that built its
expectation that way compared a verbatim path against the plain one the
resolver deliberately returns, and three assertions turned on the
difference.

Build the expectations with the resolver instead, which strips the prefix
for the same reason the product does: a stored path is later compared
against ordinary ones, and `\\?\C:\…` never `starts_with`-matches `C:\…`.
That the prefix really is stripped stays asserted on its own, so this does
not become the function agreeing with itself.

This also removes a second Windows-only trap: `temp_dir()` there can
return an 8.3 short path, which `canonicalize` expands, so the two sides
disagreed on the folder name as well as the prefix.

Same fault in the scenario's step definitions, where the comparison would
have failed only on the Strix Windows lane. That crate cannot reach
`rocm-core`, and a dependency for six lines of string handling is the
worse trade, so it strips the prefix locally with the reason recorded.

Refs: ROCm#315
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

* fix(install): give up on a parent component consistently across hosts

The guard against resolving a `..` past a missing directory never fired
on Windows, because the two platforms disagree about what such a path
means. Windows collapses `..` lexically, so `<root>/missing/..` "exists"
and canonicalizes to `<root>`; Unix walks the path through the filesystem
and finds nothing. The walk therefore stopped a level early on Windows,
resolved to the wrong folder, and re-attached the tail — recording a
different folder depending on the host, which is precisely what this
function exists to prevent.

Check for the parent component before asking whether the candidate
exists, so both hosts give up in the same place.

The give-up stays narrow: a `..` whose parent is really there is
unambiguous everywhere and still resolves, which is now pinned by its own
test so a future tightening cannot quietly leave ordinary paths
unresolved.

Found by the Windows lane; it does not reproduce on Linux.

Refs: ROCm#315
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>

---------

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

* fix(install): resolve driver repo version and package release in plans (ROCm#307)

* fix(install): resolve driver repo version and package release in plans

The driver install plan stored the repo version and package release as
raw shell parameter-expansion templates (${ROCM_CLI_AMDGPU_VERSION:-7.2.4}
and ${ROCM_CLI_AMDGPU_PACKAGE_RELEASE:-70204}). Two problems followed.
The human-readable plan summary printed the version template verbatim on
its "repo_version:" line, leaking an unexpanded shell placeholder into
user-facing dry-run output. And the Debian/Ubuntu apt source line wrapped
the template in single quotes, so the shell never expanded it and the
literal placeholder would have been written into
/etc/apt/sources.list.d/amdgpu.list.

Resolve both templates once, at plan-build time, to their effective
values (the env var when set and non-empty, matching shell :- default
semantics, otherwise the documented default) and bake the concrete values
into every rendered URL, repo path, and command. The resolver is
generalized to resolve_shell_default_template and hardened to pass bare
${VAR} and nested-default shapes through unchanged rather than emitting a
partially rewritten value.

Add unit tests for the resolver (default, env override, empty-as-unset,
non-template pass-through, bare-var and nested-default pass-through) and
update every distro plan test to assert the concrete values, plus a
render-level regression test and an end-to-end scenario asserting the
dry-run summary shows the resolved version rather than the raw
placeholder. Tests that read these process env vars serialize on a shared
lock and save/restore prior values so they stay deterministic under
edition-2024 env semantics.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* test(install): guard the two remaining env-reading driver-plan tests

build_driver_install_plan now resolves the repo-version template via
std::env::var at the top of the function, so every test calling it reads
process env. The sweep that added ScopedTestEnv::with_amd_overrides_cleared()
to the other call sites missed windows_install_driver_is_validate_only and
wsl_install_driver_uses_rocdxg_guidance_without_dkms, leaving two unguarded
readers racing the guarded mutators in the same test binary. Add the guard to
both. No production change.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(cli): accept negative float flag values in space form (EAI-8243) (ROCm#306)

* fix(cli): accept negative float flag values in space form

The space form of negative-number float flags (e.g. `serve --temperature
-1`) was rejected by clap with a confusing "unexpected argument '-1'"
error, because clap parsed the leading-dash token as a flag rather than a
value. Only the equals form (`--temperature=-1`) reached the range
validator that reports the valid range.

Enable `allow_negative_numbers` on the affected float flags
(`--temperature` and `--top-p` on both `chat` and `serve`) so the space
form reaches the value parser and both forms validate identically,
surfacing a clear range-validation message instead of an unexpected
argument error.

Add regression tests covering both the space and equals forms for
`--temperature` and `--top-p` on `chat` and `serve`.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* fix(cli): extend negative-value fix to --max-tokens and add serve scenario

The negative-number-as-flag gotcha is a tokenizer-level ambiguity that
fires before any value parser runs, so it is not float-specific:
`--max-tokens -1` (space form) on both chat and serve was still rejected
as an unexpected argument while `--max-tokens=-1` reached parse_positive_u32.
Add allow_negative_numbers to --max-tokens on both commands so the space
form reaches the value parser and reports a clear error, matching the
--temperature/--top-p behavior fixed here.

Per AGENTS.md §3, the user-observable stderr change is now covered by an
ungated Gherkin scenario, @id:serve-negative-temperature-rejected, in
model_serving.feature. The value parser runs inside argument parsing
before engine selection or GPU pre-flight, so the scenario needs no GPU
and no engine and gates every PR (mirrors the ungated
@id:fix-position-argument-rejected precedent).

Also: reword the shared rationale comment to cover all three numeric value
flags (and note clap only treats a token as negative when the whole
remainder parses as one, so missing/malformed values still error at parse
time), give the chat regression test a symmetric comment, and extend both
space-form regression tests with the --max-tokens cases.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* test(e2e): number the negative-temperature serve scenario

The new scenario was the only unnumbered title in model_serving.feature;
every other scenario uses 'Scenario: <N> - <Title>'. Assign the next free
number (15) to keep the file consistent. No behavior change.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(serve): validate --device via clap possible values (exit 2) (ROCm#304)

* fix(serve): validate --device via clap possible values (exit 2)

Model `rocm serve --device` as a clap `ValueEnum` (`DevicePolicyArg`) so an
invalid value is rejected by clap's usage validation (exit code 2) with the
valid choices listed, instead of parsing as a free-form string and failing
later in application logic with a generic "unsupported device policy" error
(exit code 1).

This makes `--device` consistent with every other enum-style argument in the
CLI (`--engine`, config set-*, engines install, etc.), all of which use clap
possible values. The historical aliases (`auto`/`gpu` for gpu_required, `cpu`
for cpu_only) stay accepted for backward compatibility but are hidden from the
advertised list, and the intentional cpu_only app-level rejection (exit 1 with
a GPU-required message) is preserved.

Adds regression tests asserting invalid `--device` is a clap InvalidValue usage
error and that every value and alias still parses, and reworks the device
possible-values sync test to read clap's structural possible values rather than
a hand-written doc string.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* fix(serve): correct --device help text and hide cpu_only choice

The per-variant ValueEnum doc comments render in `serve --help`, shell
completions, and the invalid-value suggestion list. `gpu_preferred`
previously read "Prefer a ROCm GPU when one is available", which is
misleading: parse_device_policy maps gpu_preferred to GpuRequired and
rocm serve has no CPU fallback path. Reword it to state it behaves
identically to gpu_required.

cpu_only is always rejected (rocm serve requires GPU execution), so hide
it from the advertised choices with #[value(hide = true)] while keeping it
parseable so the deliberate rejection message is preserved.

Add device_policy_arg_maps_through_parse_device_policy to guard the
DevicePolicyArg::as_policy_str -> parse_device_policy mapping (gpu_required
and gpu_preferred resolve to GpuRequired; cpu_only is rejected), closing
the previously untested conversion path.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* docs(serve): stop advertising hidden cpu_only device value

Hiding `cpu_only` from `--device` help/completion left three surfaces still
presenting it as a real choice. Align them with what the CLI now advertises:

- README serve synopsis dropped `cpu_only` from the --device value list.
- Removed the '--gpu is ignored with --device cpu_only (the model runs on CPU)'
  sentence: cpu_only is rejected (exit 1), never runs on CPU, so the line
  advertised a fallback path that does not exist (AGENTS.md §6).
- Corrected the drifted test comment to note cpu_only is #[value(hide = true)]
  and that serve_device_help_lists_match_device_policy_names compares against
  the full DevicePolicy set (hidden entries included).

Doc/comment only; no behavior change.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* fix(dash): launcher shows live serving instances; amd-smi detection off critical path (eai-8190) (ROCm#295)

* fix(dash): seed launcher front door with live serving instances

The launcher front door built an empty AppState, so it always rendered
the idle variant even when a model was actively serving. Read the
managed-service registry (the same authority `rocm services` reads)
once per hub-loop pass and seed the AppState's instances from it.

Also treat `Ready` as serving everywhere the dashboard counts running
instances (`is_serving()`), matching the `Running`+`Ready` treatment
already used elsewhere (e.g. home.rs) -- a served model reports
`Ready`, not `Running`, so the count previously undercounted actual
serving models.

Adds apps/rocm's direct dependency on rocm-dash-core (previously only a
transitive dep) so the launcher can build `Instance`s from the
registry's `DiscoveredService` records.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* fix(dash-daemon): run amd-smi detection off the run loop's critical path

Detecting amd-smi (`amd-smi version` plus the first `system_info()`)
can take up to ~15s on real hardware. Running it inline before the run
loop's first tick blocked managed-service discovery and the first
snapshot broadcast behind it, so an already-running model did not
surface in the dashboard until GPU detection finished -- a visible
~15-20s "0 models running" lag while `rocm services` already reported
it live.

Spawn detection in the background and adopt the result via a oneshot
channel the moment it lands, without ever blocking the loop while it
is in flight. The loop now starts ticking immediately, so serving
instances appear within one discovery tick; GPU metrics fill in once
detection completes.

Adds a regression test asserting on ordering (the instance must
surface in a snapshot whose gpu_system_info is still None) rather than
wall-clock timing, since a pure "arrived within Ns" check would be
flaky under subscriber starvation.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

* test(dash): exercise off-critical-path detection and launcher front door

Address review feedback on the amd-smi-off-critical-path change.

- Gate the "amd-smi unavailable" warning behind gpu_init_done so a healthy
  host never flashes it during the detection window; settle as unavailable
  (and say so once) if the detection task ever ends without a result.
- Add an amd_smi_skip_kfd_preflight test seam so the daemon regression test
  drives a fake amd-smi through the real detection path instead of
  short-circuiting on a GPU-less CI host (the /dev/kfd guard stays mandatory
  in production). The test now genuinely fails if detection moves back onto
  the critical path, and asserts the surfaced snapshot carries no premature
  "amd-smi unavailable" warning.
- Add direct unit tests for launcher_serving_instances (ready record maps to
  a live Instance; unbound :0 record is dropped) and a behavioural launcher
  scenario driving bare rocm through a PTY to prove the front door shows
  "Serving <model>" rather than "Idle" for a live registry service.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>

---------

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* Fix diagnose emitting invalid UNKNOWN render-group remediation (ROCm#302)

check_4_render_group built the render-group remediation command from the
/dev/kfd owner group obtained via `stat -c %G`. When the device GID has no
matching group name, stat prints the literal "UNKNOWN", and the existing
guard only rejected empty strings. That let UNKNOWN leak into the suggested
fix, producing `sudo usermod -a -G UNKNOWN,video "$USER"` in the plan,
summary, and --json output -- a command that fails because no such group
exists.

Reject "UNKNOWN" (case-insensitive) alongside empty values so the group
falls back to "render". This makes diagnose's fallback coincide with the
"render,video" that fix.rs hardcodes in the UNKNOWN/empty case; it does not
unify the two group sources, which are still computed independently and can
diverge when /dev/kfd has a real group name that is not "render".

Add a regression test covering both the "UNKNOWN" and "unknown" sentinel
casings falling back to the render group.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(readme): reframe install channels around published release, document rocm examine, describe Configuration section

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(readme): remove rocm model from command reference

Not one of the commands being hardened for this release; team decision
was to drop it from the reference for now rather than document it.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(conf): switch to rocm-ai flavor, enable repo and download buttons

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(conf): revert repository/download header buttons

use_repository_button and use_download_button rely on rocm-docs-core
resolving a repository_url from the current git branch. GitHub
Actions' pull_request checkout leaves a detached HEAD on the
synthetic merge ref, so get_branch() returns an empty URL, and
sphinx_book_theme crashes trying to unpack it, failing the -W build.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

* docs(conf): fix flavor fallback and pin repository buttons

- Broaden the known-warning suppression filter to also cover the
  "rocm-ai" flavor, which isn't in the published rocm-docs-core
  release yet and would otherwise fail the -W build even though
  rocm_docs.theme already falls back to "rocm" gracefully.
- Re-enable use_repository_button/use_download_button, pinning
  repository_url/repository_branch explicitly instead of letting
  rocm-docs-core infer them from the local git branch. CI's
  pull_request checkout leaves a detached HEAD on the synthetic merge
  ref, which resolves to an empty repository_url and crashes
  sphinx_book_theme's repository button; use_download_button was
  never actually affected by this, it only reads local source files.

Verified by building against the pinned rocm-docs-core==1.40.0 in a
clean venv (matching CI) -- build succeeds with 0 warnings.

Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>

---------

Signed-off-by: pmoutsia_amdeng <peter.moutsias@amd.com>
Signed-off-by: pmoutsias-amd <peter.moutsias@amd.com>
Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: Michael Roy <michael.roy@amd.com>
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Co-authored-by: Eugene Volen <150254791+volen-silo@users.noreply.github.com>
Co-authored-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Co-authored-by: Michael Roy <michael.roy@amd.com>
Co-authored-by: Roman <roman.sirokov@amd.com>
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.

2 participants