Skip to content

Adding llama configs, subtests and moving common functions to the common library - #312

Open
amd-droy wants to merge 46 commits into
dev/dtnifrom
dr_sgl_sub
Open

Adding llama configs, subtests and moving common functions to the common library#312
amd-droy wants to merge 46 commits into
dev/dtnifrom
dr_sgl_sub

Conversation

@amd-droy

@amd-droy amd-droy commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

This PR extends the SGLang inference suite with Llama 70B sample configs (single-node, distributed, and disaggregated), adds pytest subtests for per-node/per-metric performance verification, and refactors duplicated logic from the SGLang controller libs into sglang_common.py.

It also improves reporting by introducing a benchmark metric registry and perf-metric table rendering, wired into the SGLang report presets and report plugins so threshold results show up more clearly in HTML output.

atnair-amd and others added 30 commits August 9, 2026 09:29
* feat(dtni): vllm_single PoC — typed configs + orch-driven VllmJob

Replace 4 byte-similar vllm_single wrappers with a single parametrized
suite, per-variant config + threshold dirs, a typed pydantic loader, and
a new orch-driven VllmJob whose container lifecycle is owned entirely by
ContainerOrchestrator (launch:true).

- cvs/lib/dtni/{verdict,config_loader}.py — 5 threshold kinds; pydantic v2
  models with extra=forbid; 3-pass placeholder substitution; model.remote=1
  raises NotImplementedError pointing at v1 resource_resolver.
- cvs/lib/inference/vllm_orch.py — standalone VllmJob driven by orch.exec.
  Drops dead self.port_no distributed branch, random_range_ration typo,
  globals.error_list indirection, silent-skip in verify_inference_results.
- cvs/tests/inference/vllm/{conftest,_shared,vllm_single}.py — orch fixture
  owns container lifetime; test_print_results_table moved to _shared.
- cvs/input/dtni/vllm_single/{4 variants}/{config,threshold}.json — all
  variants pinned to rocm/vllm-dev:nightly for the PoC; thresholds carry
  MI300X-realistic floors (~1/3 of MI355X totals) for the verification node.
- cvs/input/cluster_file/mi300x_g21u37.json — single-node MI300X cluster
  for verification on 10.245.135.13.
- Delete the 4 old per-model wrappers and mi355x_vllm_single.json.

Verification (offline gates): pytest --collect-only enumerates 9 parametric
cells + test_print_results_table; cvs list vllm_single discovers both test
functions; load_variant of a missing-models-dir variant resolves the
expected /models/{id} path; pydantic ValidationError fires on a
percentile_metrics typo. On-hardware verification is deferred: target node
lacks pre-fetched models, HF token, and benchmark server scripts.

Legacy cvs/lib/inference/{base,vllm,inference_max}.py untouched; other
suites (sglang, inferencemax, pytorch_xdit, megatron, jax) unaffected.

* fix(dtni): cluster file uses devbox-correct key path

The devbox /data/atnair is /data/atnair (not /home/atnair), and the node
10.245.135.13 authenticates with id_ed25519 (not id_rsa). Update the
verification cluster file so the orch fixture authenticates first try.
Confirmed via a lifecycle smoke that brought up an alpine container on
the node and tore it down at the right boundaries.

* fix(dtni): remediate vllm_single lifecycle review findings

- is_ready: grep in-container instead of cat-ing the whole server log
- thresholds: fail at load if sweep cells lack a threshold entry; hard-error
  (not silent skip) on per-cell verdict miss
- HTML report: per-test timing rows with explicit units (no cross-row leak)
- client failure: treat only a nonzero failed-request count as failure
- pin HF cache to the mounted models dir; shlex-quote shell interpolation
- raise server readiness budget to 60min for remote model pulls
- remove legacy cvs.lib.inference.vllm.VllmJob (no remaining importers)

* refactor(dtni): collapse vllm_single to single W1 config + generic cluster file

- Replace 4 model variants under input/dtni/vllm_single/ with one W1 config
  (Llama 3.1 70B FP8-KV, TP=8) at input/config_file/inference/vllm_single/.
- Rename cluster file to mi300x_vllm_single.json with <changeme> placeholders
  so it is generic/shareable rather than node-specific.
- config_loader: add enforce_thresholds gate (record-only scaffolds),
  glob threshold sibling, drop enumerate_variants, note generalization seam.
- Move pytest_generate_tests into vllm_single test module; drop aa/ab/zz
  lifecycle-ordering prefixes from test names.
- Drop unused imports / apply ruff formatting across touched lib + test files.

* fix(dtni): address vllm_single PR review findings

- config_loader: drop dead BenchmarkParams class + unused benchmark_params
  field (and the matching key in the w1 config)
- config_loader: raise FileNotFoundError/ValueError instead of AssertionError
  in load_variant (AssertionError is stripped under python -O)
- config_loader: collapse _resolve_cluster_mapping; clarify the container
  runtime docstring and the intentional model_validator ordering
- vllm_orch: build the bench client command as a shlex.quote-d arg list so a
  model id or path containing a space or $ cannot break the inner bash layer

* fix(dtni): address second-round vllm_single review

- conftest: scope inf_res_dict per-module to match sibling fixtures and
  avoid cross-module result-table bleed
- test_model_fetch: split the offline/pre-staged path from the download
  poll loop; presence-check with retries so a slow mount that reads 0 on
  the first du does not false-fail a model that is present
- start_server: shlex.quote scripts_dir/server_script/server_log, matching
  the per-path quoting used elsewhere in the file
- test_teardown: set lifecycle.torn_down only after verifying the container
  is gone so the orch finalizer retries an incomplete teardown
- _clone_bench_serving: document why the bench_serving URL is a hardcoded
  calibration fork (not stock vLLM, unpinned HEAD), kept for legacy parity
…#227)

* feat(dtni): move vllm bench client to stock vllm bench serve

Drop the kimbochen/bench_serving fork clone in VllmJob.run_client and
invoke the in-image stock "vllm bench serve" CLI instead. The fork was
cloned at unpinned HEAD; pinning the client to the run image tag gives
Spec 1 a stable artifact contract to parse.

- run_client: remove _clone_bench_serving call; head tokens become
  vllm/bench/serve; drop the now-dead cd /app in client_cmd
- remove _clone_bench_serving and its (false) calibration comment
- drop Params.bench_serv_script (extra=forbid) and the matching key in
  the vllm_single config; rename client_log to client.log

base.py / inferencemax still use the fork (separate workload, untouched).
Source-only change; no metrics added (Spec 1). enforce_thresholds=false.

* fix(dtni): robust client completion + launch-failure detection for stock bench

Harden wait_client_complete after the move to stock vllm bench serve:

- COMPLETION_RE: key off the unconditional "Serving Benchmark Result"
  banner instead of the "End-to-end Latency" metric header. Stock prints
  metric headers only when the metric is in --percentile-metrics, so a
  config omitting e2el would never be detected as complete and would spin
  to the poll cap (~90 min) on an otherwise-successful run.
- add CLIENT_LAUNCH_FAIL_RE: a CLI launch failure (bad/renamed flag,
  missing bench subcommand, vllm not on PATH) exits before any summary and
  is neither a Python traceback nor a Failed-requests line, so it too would
  hang the poll cap. Treat it as a hard failure, like a crash.
- drop dead export RESULT_FILENAME=results: consumed only by the removed
  fork client; stock takes the name via --result-filename.
Hnimrama/inferencemax uplift

Refactors InferenceMax for the DTNI pytest layout (inferencemax_single): ContainerOrchestrator-based
conftest, suite/threshold JSON loading, benchmark model selection, and tighter server/client
lifecycle handling against current InferenceX upstream.

Benchmarking: stop cloning third-party bench_serving; resolve benchmark_serving.py from the
installed vllm package (BENCH_SCRIPT) for InferenceMax and vLLM single paths. Host-mounted server
entrypoints live under cvs.lib.dtni.vllm_benchmark_scripts (vllm_serve_mi300x.sh); samples and docs
use <changeme> container placeholders, legacy benchmark_script_repo called out as ignored, and
volume_dict guidance avoids duplicate Docker :/workspace mounts.

vLLM single (vllm_orch): align with dev/dtni completion and client-failure detection while keeping
python3 "$BENCH_SCRIPT" invocation.

Misc: optional run_plugin --log-file; sglang_disagg total_generated_tokens key; log redaction and
small review fixes from PR feedback.

Test with cvs run inferencemax_single (cluster + suite JSON, HF token) and spot-check vllm_single
if configs touch shared modules.
…llm result path (#233)

* refactor(lib): rename dtni -> utils and split out generic config machinery

Rename cvs/lib/dtni to cvs/lib/utils ("utils" says what it is: pure
functions any lib can call; "dtni" was a leftover project codename).
verdict.py moves unchanged.

config_loader.py is trimmed to the framework-agnostic half: the
paths/model/image/container schema, the 3-pass placeholder substitution,
the enforce_thresholds gate on a new BaseVariantConfig, and a
substitute_config() helper (file read + substitution + sibling-threshold
discovery). The inference-only schema moves to a sibling module in a
later commit.

* refactor(inference): move vllm_parsing into cvs/lib/inference/utils

The client.* metric parser (to_client_metrics + CLIENT_METRICS surface)
is inference-specific and should not sit in the shared utils dir. Move it
under a new cvs/lib/inference/utils package. Content unchanged.

* feat(inference): inference config schema with named-combo sweep selector

The inference half of the old config_loader: GoodputSlo, SeqCombo, Sweep,
Params, Roles, and VariantConfig(BaseVariantConfig) with cell_key and the
threshold-coverage check. load_variant() delegates the file read and
placeholder substitution to the generic substitute_config().

Replaces the sequence_combinations x concurrency_levels cartesian with a
named-combo + explicit runs[] selector: each run is a {combo, concurrency}
pair, so the config enumerates exactly the cells to run (no NxM explosion).
A model_validator rejects duplicate combo names and runs referencing an
unknown combo at load time.

* feat(inference): self-contained server cmd + artifact-based result parsing

build_server_cmd/start_server assemble a `vllm serve` arg list in Python
(mirroring run_client) instead of cloning and running an external .sh, so
a run needs no hand-staged script. --max-model-len is derived per cell
from isl/osl/random_range_ratio so any sweep change stays self-consistent.

parse_results reads the stock extensionless `results` JSON artifact that
`vllm bench serve` writes to --result-dir and delegates namespacing +
derived-metric math to vllm_parsing.to_client_metrics, replacing the
brittle console-log regex table. Missing/empty/unparseable artifacts
hard-fail the cell rather than recording a silently-green empty row.

Adds the optional --goodput SLO gate (per-cell, omitted when no SLO) and
threads goodput_slo through run().

* feat(suite): per-metric result rows, Value/Unit columns, sweep selector

pytest_generate_tests now drives parametrization from the named-combo +
runs[] selector instead of the cartesian. test_vllm_inference only runs
the benchmark and stashes results; the verdict moves into a new
test_metric (one pytest test = one HTML row per metric per cell), with
inline Value/Unit columns added via pytest_html hooks in conftest.

test_setup_sshd gates its 2224 probe on len(orch.hosts) > 1, mirroring
the single-node orchestrator guard: single-node runs skip the in-container
sshd (it exists only for inter-node MPI) and must not probe for it.

Import paths follow the dtni -> utils / inference.utils moves.

* chore(config): rename vllm_single config pair, adopt selector, drop cluster file

Rename the config/threshold pair to {model}_{precision}_{config|threshold}
.json and convert the sweep to the named-combo + runs[] selector. Pin the
image to rocm/vllm-dev:nightly (the previously pinned :nightly-sshd tag
does not exist on Docker Hub, and single-node runs skip in-container sshd).

Delete cvs/input/cluster_file/mi300x_vllm_single.json: a cluster file only
needs node IP + user/key/orchestrator; the variant config supplies the
container block, so the bespoke per-suite cluster file is redundant.

* test(inference): unit tests for parser, sweep selector, and verdict guards

Cover to_client_metrics purity + derived metrics, the named-combo/runs
sweep selector (expansion, unknown-combo and duplicate-name rejection),
the run_client goodput/metric-percentiles flags, table-cell rendering, and
the verdict None-guards. Adds JSON fixtures for the stock results artifact.

* docs: suite-authoring guide + AGENTS.md for shared and inference helpers

Add a human reference guide (plans/building-a-cvs-test-suite.md) that walks
the six-layer suite architecture using vllm_single as the worked example:
the generic <-> framework config seam, the named-combo + runs[] sweep
selector, the self-contained Python-built server cmd, lifecycle-as-tests,
and a checklist for authoring a new inference or training suite.

Add per-package AGENTS.md docs naming the public entry points, the seam,
and the non-obvious gotchas:
  - cvs/lib/utils: substitute_config / BaseVariantConfig / evaluate_all,
    the 3-pass placeholder order, sibling-glob threshold discovery,
    parent-first validator ordering.
  - cvs/lib/inference/utils: load_variant / to_client_metrics / CLIENT_METRICS,
    the cell_key single-source-of-truth, the coverage check that prevents a
    silent green, and the validators mirrored in pytest_generate_tests.

* docs: expand suite guide with lib restructure, drop redundant section rules

Document the dtni -> utils rename and the shared (cvs/lib/utils) vs
domain-specific (cvs/lib/inference/utils) split: what lives where, the rule
for placing a new helper, and the directory map. Note training is not yet
ported and this guide is the blueprint for that port.

Remove the manual --- horizontal rules between sections: heading levels
already render their own bottom border, so the extra rules produced a
double-underline. Minor prose/format cleanups.

* docs: demote headings so GitHub stops underlining sections

* removing old plan

* fix(config): re-key W1 threshold to the swept CONC=16 cell

The threshold file carried placeholder CONC=64/128/256 entries while the
sweep's only run is concurrency 16, so cell_key() matched no threshold.
The mismatch was masked by enforce_thresholds=false (warned, not raised)
and would have failed load the instant enforcement was flipped on. Re-key
to the single CONC=16 cell the runs selector actually enumerates.

* fix(inference): drop dead server-env exports from build_server_cmd

MODEL/ISL/OSL/MAX_MODEL_LEN/RANDOM_RANGE_RATIO/TP/CONC/PORT were exported
into /tmp/server_env_script.sh but read by nothing after the .sh->Python
server command refactor -- both _server_argv and run_client pass these as
explicit flags. Keep only the env the vllm process actually consumes
(HF token, HF cache pin, AITER flags). Also drops the second
_derive_max_model_len call that fed the dead MAX_MODEL_LEN export.

* fix(inference): move --kv-cache-dtype out of the driver into config

_server_argv hard-coded --kv-cache-dtype fp8, baking a per-model property
into the shared orchestrator -- a non-fp8-KV model dropped into the suite
would be served wrong with no config recourse (extra_serve_args can only
add, so an override would pass the flag twice). Declare it in the W1
config's roles.server.extra_serve_args instead; the driver stays
model-agnostic and 'new model = new config' holds.

* refactor(inference): share the sweep-selector validator across load and collection

pytest_generate_tests hand-reimplemented the duplicate-name and
unknown-run.combo checks that Sweep._check_runs_reference_known_combos
already enforces, with divergent semantics (first-failure raise vs
all-at-once). Extract validate_sweep_selector() as the single home and
call it from both the typed validator (load time) and the collection-time
raw-JSON path so the rule can't drift.

* fix(inference): key the per-cell out_dir by isl/osl/conc

out_dir was fixed per job, so a multi-cell sweep would overwrite each
cell's `results` and client.log, and parse_results could cat a prior
cell's stale artifact if the current cell's client failed to write one.
Key it by cell. Latent today (the shipped sweep has one cell).

* refactor(inference): normalize goodput_slo to dict-only

run_client accepted goodput_slo as either a dict (.get) or an object
(getattr) via a per-key hasattr branch, but the only production caller
passes a raw dict -- the object path existed solely for a unit test, and
the dual path meant the typed GoodputSlo's validation never reached the
command builder. Consume the dict only and drop the object-form test.

* test(inference): drop unused _fake_variant parameter

goodput_slo_unused was never read (goodput is threaded through _make_job).

* chore(inference): placeholder personal/image refs in example config

The committed vllm_single example config carried a personal hf-token path
and a concrete image tag (duplicated in image.tag and container.image).
Replace all three with <changeme> so the file is a template a new user must
fill in -- it still loads (collection works) and only fails at run time when
an unedited value is read, which is the intended signal.

* refactor(inference): server serve_args as a {flag: value} map

The per-model server knobs were a flat [flag, value, flag, value] list
(roles.server.extra_serve_args), which reads poorly. Replace with a
roles.server.serve_args {flag: value} map (flag without the leading --):
a scalar renders --flag value, True a bare --flag, a list the flag once per
element -- so it stays readable while still covering vllm bare/repeatable
flags. _server_argv flattens the map via a new _flatten_serve_args helper;
the derived flags (tp/max-model-len/port) stay code-built.

Also repoints a stale unit test that asserted MAX_MODEL_LEN in the env
script (it moved to the --max-model-len flag in an earlier commit) to assert
against the server argv instead.

* feat(verdict): add unit-agnostic max threshold kind

The only ceiling kind was max_ms, whose message hard-codes ms. A count
metric like client.failed needs an upper bound without the unit lie; add
a plain max with the same comparison and an honest message.

* feat(inference): enforce a declared gated-metric SLO contract

Previously only cell-presence was validated: a cell could exist while a
given metric had no spec, and test_metric (spec is None -> return) would
silently report a green record-only row even under enforce_thresholds=true.
A new perf metric was thus unvalidated by default.

Declare GATED_METRICS beside CLIENT_METRICS -- the perf+health subset that
must assert (throughput, mean+p99 latency, success_rate/failed) -- and
extend _check_thresholds_cover_sweep to require a spec for every gated
metric in every present cell, reusing the same enforce-vs-warn path. A new
metric is record-only until added to the set; once gated, the loader forces
a spec in every cell before the suite can run green. Inputs, totals, and
derived diagnostics stay record-only by design.

* fix(inference): single image source on container.image

The image was declared twice: top-level image.tag (live -- conftest copied
it onto the container block) and container.image (dead -- overwritten by that
copy). The duplicate forced a top-level image block whose remote field was
unused and whose tag silently shadowed container.image.

Make container.image the single source: drop the top-level ImageSpec block
from the generic BaseVariantConfig, drop the conftest overwrite so the merged
container.image is used as-is, and remove the now-schemaless image block from
the example config.

* feat(inference): gate the full latency distribution

Expand GATED_METRICS from the mean+p99 subset to every emitted latency
quantile (mean/median/p90/p95/p99) for ttft, tpot, itl, and e2el -- itl
omits p90 as CLIENT_METRICS has no producer for it. Throughput and
success_rate/failed health are unchanged. Inputs, totals, secondary
throughputs, and derived diagnostics stay record-only.

The example threshold file gains a placeholder spec for each newly gated
metric (23 total) so the loader gated-coverage check passes.

* docs: reflect image collapse, serve_args rename, max kind, GATED_METRICS

- utils/AGENTS.md: drop top-level image (now container.image); add max verdict kind
- inference/utils/AGENTS.md: document GATED_METRICS contract + dual-axis coverage check
- building-a-cvs-test-suite.md: container.image, serve_args map, max kind, GATED_METRICS
- dtni-dev-guide.md: SUPERSEDED banner pointing to building guide + AGENTS.md

* refactor(inference): rename vllm_orch → vllm_single throughout

The module name vllm_orch.py implied disaggregated orchestration; this is a
single-node suite. Align the module name with the suite file and suite name.
Update all import sites, AGENTS.md prose, and the plan doc.

* fix(config): drop ModelSpec.precision, make threshold_json an explicit field

ModelSpec.precision was an unvalidated free-text field with no downstream use;
the kv-cache-dtype flag belongs in serve_args. Removing it prevents configs
silently carrying a stale or misleading label.

Replace the sibling-glob threshold discovery (glob('*threshold.json') next to
the config) with an explicit threshold_json field on BaseVariantConfig. The
glob was fragile: ambiguous when multiple threshold files coexist, and
invisible in the config spec. An explicit absolute path is transparent,
repo-portable, and validated as part of the schema.

Update the example config to add threshold_json: "<changeme>" and drop
model.precision.

* test(config): unit tests for ModelSpec, BaseVariantConfig, substitute_config

Cover the contracts changed by the precision-removal and threshold_json-explicit
commits: ModelSpec forbids precision and extra keys; BaseVariantConfig requires
threshold_json; substitute_config reads the threshold via raw['threshold_json']
as a literal absolute path, not by globbing the sibling directory; a sibling
*threshold.json must NOT be auto-discovered (regression guard for old behavior);
comment keys are stripped. No hardware; pure filesystem via tempfile.

* refactor(inference): O(n) duplicate detection in validate_sweep_selector

Replace list.count() inside the loop (O(n²)) with Counter: one pass to build
the frequency map, one comprehension to collect duplicates. Suggested in review.

* test(inference): address review — setUpClass, if __name__ at end of file

Convert per-test module loads to setUpClass so each heavy import runs once
per class, not once per test method: TestTableCellRendering._cell() (loads
_shared.py + stubs tabulate), TestKeyConsistency._producer_keys() (runs
parse_results), TestMetricTests.setUp() (_load_vllm_single). Suggested in
review for TestTableCellRendering; applied the same fix to the other two
classes that had the identical problem.

Move `if __name__ == "__main__": unittest.main()` from mid-file (line 282)
to the very end. Test classes defined after the guard were still discovered
by both pytest and python -m unittest (Python parses the full file first),
but the placement looked like dead code. Flagged in review.

* test(inference): address review + expand coverage for config loader models

Review items: remove dead `vc = _variant(sw)` assignment that was never read;
move `import warnings` from inline to top-level imports.

New test classes covering contracts changed in this PR: TestModelSpecNoPrecision
(extra field rejected), TestThresholdJsonField (required field, constructs with
it), TestCellCoverageAxis (missing cell and extra threshold-key axes, warn/raise
modes), TestExpectedCellsBoundaries (empty runs, unreferenced combo),
TestGoodputSlo (construction, missing fields, forbid extra keys, optional on
SeqCombo), TestSeqComboForbid (required fields, extra keys). All no-hardware.

* feat(config): expand w1 llama31_70b_fp8kv sweep to 5 ISL/OSL cells at conc=16

Replace the single ISL=128/OSL=2048 placeholder cell with the full
ISL/OSL matrix requested in review:

  ISL=1024 / OSL=1024
  ISL=8192 / OSL=1024
  ISL=1K   / OSL=8192
  ISL=1K   / OSL=4096
  ISL=5000 / OSL=1024

All five cells run at concurrency=16, TP=8, with record-only placeholder
thresholds (enforce_thresholds=false). threshold.json carries a spec for
every GATED_METRICS member per cell so the loader coverage check passes.

* fix(config): use literal ISL/OSL values (1000/8000/4000 not 1024/8192/4096)

* style(inference): collapse multi-line string concat in launch cmd

Co-Authored-By: Claude <noreply@anthropic.com>

* adding documentation

* renaming config and threshold files

---------

Co-authored-by: Claude <noreply@anthropic.com>
Squashed from 74 incremental commits merged via PR #234: SGLang multinode
orchestration, smoke tests, hellaswag accuracy runs, and iterative framework
updates. Full per-commit history preserved in the backup branch/tag
solaiys/dev-dtni-presquash-2026-08-02.
Signed-off-by: amd-droy <droy@amd.com>
…epseek r1 model

* Restore InferenceMax uplift reverted by #228

Reverts commit 4a8425f, restoring the changes from PR #225 on dev/dtni.

* fix(inference): run vLLM bench client with vLLM interpreter

Probe python3.13..python3 for import vllm; export BENCH_PY and BENCH_SCRIPT. Use shlex.quote for docker exec bash -c. Align InferenceMax client completion with Serving Benchmark Result or End-to-end Latency.

* fix(dtni): broaden vLLM benchmark script discovery

Search site-packages and ancestor paths, verify the file is readable, and document vllm[bench] when wheels omit benchmarks/.

* fix(inference): harden InferenceMax server startup and GPU mem env

Use CVS_GPU_MEMORY_UTIL in sample config and serve script to avoid vLLM unknown-env warnings. Extend default readiness poll budget to 60 and grep full server logs so Uvicorn ready is not missed after long model loads.

* fix(dtni): fall back to vllm bench serve when benchmark script is absent

Wheels often omit vllm/benchmarks; resolve the driver via eval exports, run python -m vllm.entrypoints.cli.main bench serve when needed, and fail fast on missing-script log patterns in InferenceMax and base polling.

* fix(dtni): clamp bench random-range to max_model_length

vLLM random workloads scale (ISL+OSL)*(1+r); clamp ratio when it would exceed MML, pass --temperature 0 for greedy parity, and forward --metric-percentiles in InferenceMax and vllm_single clients.

* fix(inference): extend InferenceMax bench client poll budget

Read client_poll_count and client_poll_wait_time from benchmark_params (defaults 50/60), document them and fix the inferencemax.rst table, and surface the keys in sample MI300X/MI355X configs.

* feat(inference): add bench_max_failed_requests cap and completion-first polling

Gate benchmark success on Failed requests only after the summary is present;
tail more client log lines for InferenceMax. Variant and benchmark_params accept
bench_max_failed_requests (default 0 remains strict for CI).

* feat(inference): add typed InferenceMax config loader (Phase 1)

Move InferenceMax loading onto substitute_config and a typed InferenceMaxVariantConfig with legacy adapters for InferenceMaxJob until the driver is ported.

* feat(inference): migrate InferenceMax configs to schema_version 1 (Phase 2)

Flatten MI300X and MI355X variant configs to paths/model/container/roles/params/sweep and client.* threshold specs with enforce_thresholds false until recalibrated.

* test(inference): wire inferencemax_single to typed config and sweep (Phase 2)

Use variant_config and legacy adapter fixtures, parametrization from sweep.runs, and unit tests for load_variant and threshold adapters.

* docs(inference): update InferenceMax config reference for schema_version 1 (Phase 2)

Point loader and threshold docs at inferencemax_config_loader.load_variant and the client.* sweep cell format.

* docs: fix stale dtni.config_loader references (Phase 1 tail)

Point run-cvs-tests and dtni-dev-guide at cvs.lib.utils and inference/utils loaders.

* feat(inference): rewrite InferenceMaxJob like VllmJob (Phase 3)

Standalone driver uses Python-built vllm serve, vllm bench serve, and artifact parsing. Drop legacy InferenceBaseJob path and factory construction.

* feat(inference): move InferenceMax server flags to roles.server.serve_args (Phase 3)

MI300X and MI355X variants drop host-script and bench_serving params in favor of Python serve args.

* test(inference): align inferencemax_single suite with VllmJob pattern (Phase 3)

Add model_fetch, test_metric, and new InferenceMaxJob lifecycle. Update conftest and unit tests for typed config.

* docs(inference): update InferenceMax reference for Phase 3 driver (Phase 3)

Document Python serve, client.* metrics, and expanded lifecycle test stages.

* chore(inference): remove unused inferencemax_host_scripts (Phase 5)

Host script staging was dropped when InferenceMaxJob moved to Python-built vllm serve.

* docs: clarify vllm_benchmark_scripts are legacy-only (Phase 5)

InferenceMax and vllm_single build vllm serve in Python; this package remains for InferenceBaseJob paths.

* docs(inference): rewrite InferenceMax reference for schema_version 1 (Phase 5)

Replace legacy config/benchmark_params table with typed blocks and client.* thresholds. Document inferencemax_config_loader in AGENTS.md.

* test(inference): add InferenceMaxJob parse_results unit test (Phase 5)

Verify stock results artifact maps to client.* metrics via FakeOrch.

* refactor(inference): rename inferencemax_single to inferencex_atom_single

Adopt InferenceX ATOM as the framework identity while the suite is still internal. Renames the driver, config loader, pytest suite, variant configs, and documentation to inferencex_atom_single.

* docs(plan): add InferenceX ATOM automation plan (MI300X + MI355X)

Align the implementation plan with DTNI Validation Tracker workloads W1-W18, MI300X calibration seeds, and gsm8k accuracy gates. MI300X and MI355X variant dirs ship in parallel; MI355X thresholds stay record-only until lab calibration. Milestone 1 targets ATOM backend plus W1 on both arches.

* docs(plan): add MI355X W1 calibration seeds from ATOM CI

Document section 4.3 thresholds from ROCm/ATOM run 27912164002 and align M1 scope for dual-arch W1 calibration.

* feat(inference): Phase 0 ATOM driver and W1 DeepSeek R1 variants

Swap InferenceXAtomJob to atom.entrypoints.openai_server and atom.benchmarks.benchmark_serving when params.driver=atom. Add MI300X/MI355X W1 config+threshold dirs and cluster examples for deepseek-ai/DeepSeek-R1-0528.

* feat(inference): complete Phase 0 W1 DeepSeek R1 recipe pins and MTP3 variants

Add ix_recipes.json registry, ix_recipe_id/run_card in config loader, MTP3 variant dirs for MI300X/MI355X, copy-config dtni root, and run-card logging in tests.

* feat(inference): add MI300X W1 DeepSeek R1 smoke variant

Single-cell smoke config (C=128, 128 prompts) for shorter first lab validation before full atom_perf calibration.

* FIX: underscore-prefixed keys (including _comment) are now stripped from the config dict before Pydantic validation, same as for thresholds.

* Move InferenceX ATOM W1 configs to config_file layout and calibrate MI300X perf gates.

Relocate DeepSeek R1 variants from input/dtni to the standard inference config tree, enable enforce_thresholds with lab-calibrated thresholds, and clear stale results.json before each benchmark run.

* docs: replace section symbol with plain Section references.

Use readable Section/Sections wording in plans and InferenceX ATOM variant config comments instead of the section sign character.

* feat(inference): calibrate MI355X W1 thresholds from ATOM CI seeds.

Apply the same 10% margin as MI300X lab gates to perf and MTP3 variant thresholds from ROCm/ATOM run 27912164002, document copy-config flow in README, and add config-loader unit tests.

* fix(inference): address PR #229 review on inferencex_atom_single suite.

Drop the post-launch sleep, fail model fetch on du errors instead of treating them as an empty cache, scope inf_res_dict to module like vllm_single, and document the bounded client poll timeout.

* fix(inference): unbreak W1 Phase A threshold checks for ATOM artifacts.

Pin DeepSeek W1 container names and derive failed/success_rate when ATOM omits failed; skip threshold enforcement for metrics the benchmark did not emit.

* docs(plan): MI355X lab pending without blocking MI300X spine.

Add Section 1.2 hardware policy, revise M1/Phase A exit criteria, and update milestone diagrams so MI355X confirmation is optional until nodes are available.

* docs(plan): refresh IX-atom plan with accuracy, metrics, and CVS backlog.

Align branch state and phases with ATOM driver reality, add accuracy test
catalog, metric tiers, and platform enhancements; update W1 README lab notes.

* docs(plan): add Section 12 coverage for variants, parity frameworks, and metrics.

Document perf variant modes, workload-specific accuracy tests, inferencex_atom_vllm/sglang parity suites, supplemental and MTP metrics, and CI compare keys.

* align: restore config_loader threshold discovery after dev/dtni rebase

Re-apply merge resolutions from PR #233 alignment (dual threshold layout, vllm_single imports) that were lost when replaying commits onto e47df5a.

* refactor(inference): deprecate InferenceMax legacy factory paths

Route inferencemax_repo and framework=inferencemax to inferencex_atom with a warning. InferenceMax host jobs remain a placeholder that points callers at inferencex_atom_single.

* refactor(inference): extract shared threshold sweep validation

Add validate_thresholds_cover_sweep() for reuse by vllm_single and inferencex_atom loaders. Optional gated_metrics parameter allows framework-specific SLO sets.

* refactor(inferencex): flatten configs and adopt vllm_single naming

Rename W1 and GPT-OSS variant stems to {gpu}_inferencex-atom-single_{model}_{precision}[_{mode}] and remove per-variant subfolders. Update README and docs with new copy-config paths and W1 threshold gates for per-GPU throughput and tail latencies.

* feat(inferencex): gate W1 per-GPU and tail latency metrics on ATOM path

Add inferencex_atom_parsing with IX-specific GATED_METRICS (per_gpu_throughput, output_tput_per_gpu) without changing vllm_single. Wire InferenceXAtomJob and the suite to atom parsing; default metric_percentiles to 95,99 for p95 TPOT and p99 TTFT.

* docs(inferencex): document ATOM-specific parsing vs vllm_single

Clarify that W1 GATED_METRICS and output_tput_per_gpu live in inferencex_atom_parsing so vllm_single stays untouched until parity.

* docs(inferencex): simplify variant README for lab users

Drop vllm_single comparisons and internal threshold tables; keep naming pattern, variant list, and copy/run commands.

* chore(utils): silence cluster placeholder resolution logs

* test(config): align config_loader tests with sibling threshold discovery

* feat(inferencex): add W1 metric tiers for tiered threshold gates

* feat(inferencex): reuse server across sweep cells and config-driven waits

* feat(inferencex): replace per-metric tests with tiered test_cell_metrics

* chore(inferencex): enable server reuse on perf configs and shorter smoke waits

* fix(inferencex): align cluster container names with variant configs

* fix(inferencex): tighten W1 perf health gates when enforcing thresholds

* test(inferencex): add GATED_METRICS parity and health gate coverage tests

* test(inferencex): add GATED_METRICS parity and health gate coverage tests

* docs(inferencex): update plan and README for flat layout and tiered gates

* docs(inferencex): update plan and README for flat layout and tiered gates

* chore(inferencex): trim expand_sweep docstring

* docs(inferencex): clarify lab layout, launcher host, and results paths

Document per-variant ~/input subdirs to avoid ambiguous threshold discovery,
remote launcher vs GPU node prerequisites, and ~/cvs_results output paths.

* docs(plan): prioritize multi-node as M5 after framework parity

Elevate scaling to P1 milestone M5 immediately after M4 parity when
hardware and suite recipes support nnodes>1; defer MTP+P2 widen to M6.

* fix(inferencex): align W1 tpot tier with ATOM bench output

Gate p99_tpot_ms instead of absent p95_tpot_ms, skip missing tier metrics in actuals, and recalibrate MI300X perf thresholds from the 2026-06-25 lab run.

* chore(inferencex): use portable W1 perf thresholds on MI300X

Replace per-node calibrated gates with conservative throughput floors and loose latency caps so healthy runs pass across lab nodes without recalibration.

* test(inferencex): cover parse_results errors and client log failure paths

* fix(inferencex): detect ATOM server early failures during wait_ready

* refactor(inferencex): extract sweep reuse helpers and safer collection defaults

* feat(inferencex): add explicit threshold_json paths to variant configs

* chore(inferencex): polish conftest docs and simplify CLIENT_METRICS build

* refactor(inferencex): inline atom_args and remove ix_recipe indirection

* docs(plan): sync IX atom plan with inline atom_args config layout
* Add IX Run Deck reporting stack on top of dev/dtni.

Rebased onto dev/dtni after IX-atom merge (#238). Keeps only Run Deck wiring and add-only doc updates; no unrelated IX-atom or utils doc churn.

* Expand sweep charts with per ISL/OSL grouping and add vLLM preset.

Group concurrency charts by sequence shape so multi-shape sweeps stay readable, add tail-latency chart presets, and wire vllm_single auto-registration.

* fix(report): register inference suite preset before session bind

Re-sync suite stem from pytest args at session start so auto-discovery works when tests run from the installed package. Log when no preset is registered instead of failing silently, and wire inferencex_atom explicitly in suite conftest as a fallback.

* fix(report): link run-card pytest HTML and log via sibling basenames.

* feat(report): add chart_comparison payload for multi-shape sweeps.

Expose cross-shape series on a shared concurrency axis so the interactive viewer can render grouped comparisons without duplicating chart_series logic.

* feat(report): add cross-shape sweep chart render helpers.

Add grouped-bar and line comparison renderers plus shared tooltip CSS for static HTML chart surfaces.

* feat(report): improve static run deck bar charts with axis grid.

Add y/x gridlines, aligned tick labels, hover tooltips, and a viewer banner so cross-shape comparison stays in the interactive sidecar.

* feat(report): expand interactive viewer with cross-shape comparison.

Add grouped bar and line comparison modes, filter-aware heatmap and gates, and overview layout updates backed by chart_comparison payload tests.

* feat(report): expose sweep_ttft_metric in viewer payload.

Lets the interactive viewer default the throughput-vs-latency Y-axis to the suite TTFT metric.

* feat(report): add percentile fans, gate margins, and heatmap toggle to viewer.

Add P90/P95/P99 fan charts and gate margin vs concurrency, move throughput-vs-latency to the bottom, and let the heatmap switch between throughput and latency metrics.

* refactor(report): drop unused static cross-shape chart renderers.

Keep sweep_charts as tooltip CSS only; cross-shape comparison lives in the Chart.js viewer.

* refactor(report): dedupe viewer charts and simplify multi-shape banner.

Use sweep_has_multi_shape_comparison for the static deck banner, limit per-shape viewer lines to throughput and means, and document viewer chart surfaces in README.

* Restore CI one-page summary HTML for inference suite reports.

Bring back {report_basename}_summary.html generation and pytest bundle links, and extend the IX atom sample generator with a multi-shape variant.

* Restore viewer baseline comparison and consolidate report docs.

Bring back prev-run delta columns and payload wiring, remove ADDING_A_SUITE.md, and point suite/report docs at cvs/lib/report/README.md.

* refactor(report): consolidate shared sweep shape and comparison helpers.

Deduplicate shape grouping, prev-run comparison, preset builders, and viewer chart utilities without changing report behavior.

* docs(inference): restore ADDING_A_SUITE.md with Run Deck step.

Keep the existing suite guide on dev/dtni and retain the optional Step 8 report wiring section instead of deleting the file.

* refactor(report): trim PR scope by deferring non-IX-atom extras.

Remove the local sample generator and vLLM preset, shorten README, and drop unused prev-run HTML renderer and panel_shell helper.

* refactor(report): rely on auto-register for IX-atom preset wiring.

Drop explicit pytest_configure registration from the inferencex_atom suite conftest and expose a single stem-matched preset export.

* refactor(report): dedupe cell-card CSS and drop chart_comparison pre-bake.

Unify pytest and static cell-card themes, build cross-shape comparison only in the viewer, and trim overlapping unit tests.

* fix(report): use bundle-relative links and omit config run-card notes.

Write run deck artifacts into the pytest bundle folder, link pytest HTML and run logs correctly after zip extract, and stop auto-rendering variant run_card.notes in static HTML.

* fix(report): address PR #244 review feedback.

Merge duplicate pytest_configure hooks, escape HTML in report logs, and tighten report payload binding and tests.

* fix(conftest): rename report bind fixture to match module scope.

Addresses PR #244 fixture-scope review: name no longer implies session-scoped pytest fixture.

* fix(report): align PR #244 inline review fixes.

Use tryfirst pytest_configure, session-scoped report store with module bind merge, and key-is-None results column offset.

* fix(report): sanitize absolute paths in link_or_text_html.

Only treat ../ paths as bundle-relative so local filesystem paths reduce to basename in HTML reports.
…255)

* refactor(inference): move shared suite helpers under inference/utils

Colocate lifecycle, cache probe, and results table modules with other inference utilities and update import paths across IX atom suites and report wiring.

* style: apply ruff formatting for make test fmt-check

Run ruff format on 26 files across inference, report, and test modules so fmt-check passes before merging into dev/dtni.

* fix(lint): resolve ruff check issues for make lint gate

Move cache_probe import to module top, remove unused variables/imports, and add noqa for pytest side-effect imports so ruff check and pylint both pass.

* refactor(inferencex_atom): colocate lib modules under inferencex_atom package

* refactor(inferencex_atom): update imports for inferencex_atom package paths

* moved lib import
…#241)

* Updating sglanf for multinode

Signed-off-by: amd-droy <droy@amd.com>

* feat(gpu): add GPU polling loop, metrics, and threshold gates

- gpu.py: parse_gpu_metrics, capture_gpu_metrics, _mean, agg_readings, poll_gpu_metrics
- VllmJob.is_client_done(): non-raising completion predicate
- vllm_single test: poll GPU while client runs, write gpu_poll.log, derive 5 metrics
- _shared.py: Peak VRAM / Compute % / BW % columns in results table
- test_gpu.py: TestMean, TestAggReadings, TestPollGpuMetrics unit test classes
- threshold JSON: gpu.* placeholder SLO entries for all 5 cells
- test_vllm_orch_parse: update threshold path + exclude gpu.* from client key guard

* fix(vllm_single): add missing gpu_metrics_snap module-scope fixture

The fixture was referenced in test_vllm_inference's parameter list but
never defined, causing a setup Error before any inference ran.

* fix(gpu): use exec_on_head for amd-smi; mkdir gpu_poll.log parent dir

amd-smi is a host-side tool — running it via orch.exec() sends it into
the container where it doesn't exist. Switch capture_gpu_metrics to
orch.exec_on_head() so the command runs on the bare-metal node.

Also ensure the out_dir exists before poll_gpu_metrics attempts to write
gpu_poll.log, since the directory is created lazily by the job setup.

Update unit test mocks from exec to exec_on_head to match.

* fix(vllm_single): write gpu_poll.log to tmp then copy to node via exec_on_head

out_dir is an NFS path on the node, not mounted on the devbox.
Write the log to a local tempdir, then base64-encode it and push
it to the node via exec_on_head so it lands in the bundle.

* fix(gpu): move deferred imports to module level; fix test_gpu_metric rank

Move import time/logging/pathlib from inside poll_gpu_metrics body to
module top-level. Add test_gpu_metric at rank 4 in conftest sort table
so it runs before test_teardown, not after.

* docs(gpu): add gpu.py section to AGENTS.md and integration guide

Add gpu.py API reference to cvs/lib/utils/AGENTS.md: public symbols,
poll_gpu_metrics parameter table, 5-metric derivation table, required
conftest fixtures (gpu_metrics_snap), two wiring patterns (sync poll /
threaded poll), pytest_generate_tests parametrize branch, collection
sort rank table, and gotchas (threshold key prefix, capture can raise,
or-None semantics, full actuals for evaluate_all, GATED_METRICS).

Add cvs/lib/utils/docs/gpu-metrics.md: user-facing integration guide
covering the 5 derived metrics, polling lifecycle, 5-step integration
walkthrough, gpu_poll.log format, failure/None handling table, and
cross-references to ADDING_A_SUITE.md and threshold-kinds.md.

* fix(vllm_single): write gpu_poll.log to local report dir so it lands in zip bundle

Previously the log was written to a tempfile then uploaded to the NFS out_dir;
because the zip plugin only bundles the local html report directory, the log
never appeared in the run archive. Now it is written directly into the _test_html_dir
folder (e.g. vllm_single_html/) so every run archive contains the poll log alongside
the per-test HTML files. The NFS upload is kept for cluster-side access.

Update gpu-metrics.md integration guide to match the correct log_path pattern
and to describe where the log lands.

* fix(vllm): guard is_client_done cat against missing log file

orch.exec captures stderr; 'cat client.log' when the file does not yet
exist emits 'No such file or directory' to stderr, which matches
CLIENT_LAUNCH_FAIL_RE and causes is_client_done to return True
immediately after run_client(). poll_gpu_metrics then exits with 0
readings.

Replace bare cat with 'test -f ... && cat ... || true' so a missing
file produces empty output (False) instead of a false launch-failure
signal.

* feat(gpu): add multi-node polling via nodes= param

capture_gpu_metrics and poll_gpu_metrics now accept an optional
nodes=[(label, phdl)] list. When provided, amd-smi is fanned out
to all phdl handles and their GPU entries are merged before
aggregation (sum VRAM, mean utilisation) — same output dict shape
as the single-node path. Log lines are tagged with node labels;
the summary block adds per-label used_vram lines.

nodes=None (default) preserves the existing exec_on_head single-node
path unchanged. 75 unit tests pass (66 existing + 9 new).

* feat(sglang-disagg): wire GPU polling into performance benchmark test

Threads poll_gpu_metrics alongside benchserv_test_random (Pattern B)
using nodes=[('prefill-0', p_phdl), ('decode-0', d_phdl)] so both
node groups are polled. is_done_fn cats the benchmark results log on
the b_phdl node. gpu_poll_disagg.log lands in the local HTML report
dir for bundle inclusion.

Adds test_gpu_metric parametrized over the 5 GPU_METRICS keys, with
gpu_metrics_snap fixture and pytest_generate_tests in conftest.
test_gpu_metric ordered at rank 14 (after test_disagg_gpu_topology).

* refactor(gpu): drop vllm_single wiring, keep gpu.py scope-focused

The vllm_single suite has been substantially rewritten upstream since
this branch split off; wiring GPU polling into it here was scope creep
on a feature that's really about the gpu.py library + sglang-disagg
integration. Revert all vllm_single/vllm test/lib/threshold changes to
their current dev/dtni state and generalize the remaining vllm-specific
references in gpu.py, its tests, and docs.

* refactor(gpu): remove sglang-disagg wiring, gpu.py stays suite-agnostic

Per feedback: this branch should not touch any suite. Revert the
sglang-disagg GPU polling integration back to dev/dtni; only the
gpu.py library, its unit tests, and docs remain.

* refactor(gpu): move gpu.py to inference/utils, use orch.exec(hosts=) for multi-node

Multi-node polling now targets host lists through the existing Orchestrator.exec(cmd,
hosts=...) contract instead of requiring callers to hand-construct per-role Pssh/phdl
handles, mirroring but generalizing the pattern in sglang_disagg_lib.py. Also fixes two
poll_gpu_metrics bugs: is_done_fn() exceptions were previously misattributed as amd-smi
failures, and multi-node polls ran two exec rounds (merged + per-node) instead of one.

* refactor(gpu): move gpu.py back to cvs/lib/utils (suite-agnostic, not inference-only)

gpu.py has no inference-specific logic — it shells out to amd-smi via an
Orchestrator and parses/aggregates results. Training suites will need GPU
polling too, so it belongs in cvs/lib/utils/ alongside the other
framework-agnostic machinery (config_loader.py, verdict.py), not nested
under cvs/lib/inference/.

* addressing nits: failure cap bypass and incorrect docstrings

---------

Signed-off-by: amd-droy <droy@amd.com>
Co-authored-by: amd-droy <droy@amd.com>
* Integrating nodesmoke tier 1 tests using Primus cli
* Fixed the braces
* Updated the README
* Added the robustness improvement
Signed-off-by: Urvashi Tiwari <urtiwari.com>
…rametrized suite (#257)

* Merge pull request #225 from ROCm/hnimrama/inferencemax-uplift

Hnimrama/inferencemax uplift

Refactors InferenceMax for the DTNI pytest layout (inferencemax_single): ContainerOrchestrator-based
conftest, suite/threshold JSON loading, benchmark model selection, and tighter server/client
lifecycle handling against current InferenceX upstream.

Benchmarking: stop cloning third-party bench_serving; resolve benchmark_serving.py from the
installed vllm package (BENCH_SCRIPT) for InferenceMax and vLLM single paths. Host-mounted server
entrypoints live under cvs.lib.dtni.vllm_benchmark_scripts (vllm_serve_mi300x.sh); samples and docs
use <changeme> container placeholders, legacy benchmark_script_repo called out as ignored, and
volume_dict guidance avoids duplicate Docker :/workspace mounts.

vLLM single (vllm_orch): align with dev/dtni completion and client-failure detection while keeping
python3 "$BENCH_SCRIPT" invocation.

Misc: optional run_plugin --log-file; sglang_disagg total_generated_tokens key; log redaction and
small review fixes from PR feedback.

Test with cvs run inferencemax_single (cluster + suite JSON, HF token) and spot-check vllm_single
if configs touch shared modules.

* Revert "Merge pull request #225 from ROCm/hnimrama/inferencemax-uplift" (#228)

This reverts commit 5798f4e.

* Hnimrama/ix atom (#238)   AIMVT-244/Add ATOM framework and inference Deepseek r1 model

* Restore InferenceMax uplift reverted by #228

Reverts commit 4a8425f, restoring the changes from PR #225 on dev/dtni.

* fix(inference): run vLLM bench client with vLLM interpreter

Probe python3.13..python3 for import vllm; export BENCH_PY and BENCH_SCRIPT. Use shlex.quote for docker exec bash -c. Align InferenceMax client completion with Serving Benchmark Result or End-to-end Latency.

* fix(dtni): broaden vLLM benchmark script discovery

Search site-packages and ancestor paths, verify the file is readable, and document vllm[bench] when wheels omit benchmarks/.

* fix(inference): harden InferenceMax server startup and GPU mem env

Use CVS_GPU_MEMORY_UTIL in sample config and serve script to avoid vLLM unknown-env warnings. Extend default readiness poll budget to 60 and grep full server logs so Uvicorn ready is not missed after long model loads.

* fix(dtni): fall back to vllm bench serve when benchmark script is absent

Wheels often omit vllm/benchmarks; resolve the driver via eval exports, run python -m vllm.entrypoints.cli.main bench serve when needed, and fail fast on missing-script log patterns in InferenceMax and base polling.

* fix(dtni): clamp bench random-range to max_model_length

vLLM random workloads scale (ISL+OSL)*(1+r); clamp ratio when it would exceed MML, pass --temperature 0 for greedy parity, and forward --metric-percentiles in InferenceMax and vllm_single clients.

* fix(inference): extend InferenceMax bench client poll budget

Read client_poll_count and client_poll_wait_time from benchmark_params (defaults 50/60), document them and fix the inferencemax.rst table, and surface the keys in sample MI300X/MI355X configs.

* feat(inference): add bench_max_failed_requests cap and completion-first polling

Gate benchmark success on Failed requests only after the summary is present;
tail more client log lines for InferenceMax. Variant and benchmark_params accept
bench_max_failed_requests (default 0 remains strict for CI).

* feat(inference): add typed InferenceMax config loader (Phase 1)

Move InferenceMax loading onto substitute_config and a typed InferenceMaxVariantConfig with legacy adapters for InferenceMaxJob until the driver is ported.

* feat(inference): migrate InferenceMax configs to schema_version 1 (Phase 2)

Flatten MI300X and MI355X variant configs to paths/model/container/roles/params/sweep and client.* threshold specs with enforce_thresholds false until recalibrated.

* test(inference): wire inferencemax_single to typed config and sweep (Phase 2)

Use variant_config and legacy adapter fixtures, parametrization from sweep.runs, and unit tests for load_variant and threshold adapters.

* docs(inference): update InferenceMax config reference for schema_version 1 (Phase 2)

Point loader and threshold docs at inferencemax_config_loader.load_variant and the client.* sweep cell format.

* docs: fix stale dtni.config_loader references (Phase 1 tail)

Point run-cvs-tests and dtni-dev-guide at cvs.lib.utils and inference/utils loaders.

* feat(inference): rewrite InferenceMaxJob like VllmJob (Phase 3)

Standalone driver uses Python-built vllm serve, vllm bench serve, and artifact parsing. Drop legacy InferenceBaseJob path and factory construction.

* feat(inference): move InferenceMax server flags to roles.server.serve_args (Phase 3)

MI300X and MI355X variants drop host-script and bench_serving params in favor of Python serve args.

* test(inference): align inferencemax_single suite with VllmJob pattern (Phase 3)

Add model_fetch, test_metric, and new InferenceMaxJob lifecycle. Update conftest and unit tests for typed config.

* docs(inference): update InferenceMax reference for Phase 3 driver (Phase 3)

Document Python serve, client.* metrics, and expanded lifecycle test stages.

* chore(inference): remove unused inferencemax_host_scripts (Phase 5)

Host script staging was dropped when InferenceMaxJob moved to Python-built vllm serve.

* docs: clarify vllm_benchmark_scripts are legacy-only (Phase 5)

InferenceMax and vllm_single build vllm serve in Python; this package remains for InferenceBaseJob paths.

* docs(inference): rewrite InferenceMax reference for schema_version 1 (Phase 5)

Replace legacy config/benchmark_params table with typed blocks and client.* thresholds. Document inferencemax_config_loader in AGENTS.md.

* test(inference): add InferenceMaxJob parse_results unit test (Phase 5)

Verify stock results artifact maps to client.* metrics via FakeOrch.

* refactor(inference): rename inferencemax_single to inferencex_atom_single

Adopt InferenceX ATOM as the framework identity while the suite is still internal. Renames the driver, config loader, pytest suite, variant configs, and documentation to inferencex_atom_single.

* docs(plan): add InferenceX ATOM automation plan (MI300X + MI355X)

Align the implementation plan with DTNI Validation Tracker workloads W1-W18, MI300X calibration seeds, and gsm8k accuracy gates. MI300X and MI355X variant dirs ship in parallel; MI355X thresholds stay record-only until lab calibration. Milestone 1 targets ATOM backend plus W1 on both arches.

* docs(plan): add MI355X W1 calibration seeds from ATOM CI

Document section 4.3 thresholds from ROCm/ATOM run 27912164002 and align M1 scope for dual-arch W1 calibration.

* feat(inference): Phase 0 ATOM driver and W1 DeepSeek R1 variants

Swap InferenceXAtomJob to atom.entrypoints.openai_server and atom.benchmarks.benchmark_serving when params.driver=atom. Add MI300X/MI355X W1 config+threshold dirs and cluster examples for deepseek-ai/DeepSeek-R1-0528.

* feat(inference): complete Phase 0 W1 DeepSeek R1 recipe pins and MTP3 variants

Add ix_recipes.json registry, ix_recipe_id/run_card in config loader, MTP3 variant dirs for MI300X/MI355X, copy-config dtni root, and run-card logging in tests.

* feat(inference): add MI300X W1 DeepSeek R1 smoke variant

Single-cell smoke config (C=128, 128 prompts) for shorter first lab validation before full atom_perf calibration.

* FIX: underscore-prefixed keys (including _comment) are now stripped from the config dict before Pydantic validation, same as for thresholds.

* Move InferenceX ATOM W1 configs to config_file layout and calibrate MI300X perf gates.

Relocate DeepSeek R1 variants from input/dtni to the standard inference config tree, enable enforce_thresholds with lab-calibrated thresholds, and clear stale results.json before each benchmark run.

* docs: replace section symbol with plain Section references.

Use readable Section/Sections wording in plans and InferenceX ATOM variant config comments instead of the section sign character.

* feat(inference): calibrate MI355X W1 thresholds from ATOM CI seeds.

Apply the same 10% margin as MI300X lab gates to perf and MTP3 variant thresholds from ROCm/ATOM run 27912164002, document copy-config flow in README, and add config-loader unit tests.

* fix(inference): address PR #229 review on inferencex_atom_single suite.

Drop the post-launch sleep, fail model fetch on du errors instead of treating them as an empty cache, scope inf_res_dict to module like vllm_single, and document the bounded client poll timeout.

* fix(inference): unbreak W1 Phase A threshold checks for ATOM artifacts.

Pin DeepSeek W1 container names and derive failed/success_rate when ATOM omits failed; skip threshold enforcement for metrics the benchmark did not emit.

* docs(plan): MI355X lab pending without blocking MI300X spine.

Add Section 1.2 hardware policy, revise M1/Phase A exit criteria, and update milestone diagrams so MI355X confirmation is optional until nodes are available.

* docs(plan): refresh IX-atom plan with accuracy, metrics, and CVS backlog.

Align branch state and phases with ATOM driver reality, add accuracy test
catalog, metric tiers, and platform enhancements; update W1 README lab notes.

* docs(plan): add Section 12 coverage for variants, parity frameworks, and metrics.

Document perf variant modes, workload-specific accuracy tests, inferencex_atom_vllm/sglang parity suites, supplemental and MTP metrics, and CI compare keys.

* align: restore config_loader threshold discovery after dev/dtni rebase

Re-apply merge resolutions from PR #233 alignment (dual threshold layout, vllm_single imports) that were lost when replaying commits onto e47df5a.

* refactor(inference): deprecate InferenceMax legacy factory paths

Route inferencemax_repo and framework=inferencemax to inferencex_atom with a warning. InferenceMax host jobs remain a placeholder that points callers at inferencex_atom_single.

* refactor(inference): extract shared threshold sweep validation

Add validate_thresholds_cover_sweep() for reuse by vllm_single and inferencex_atom loaders. Optional gated_metrics parameter allows framework-specific SLO sets.

* refactor(inferencex): flatten configs and adopt vllm_single naming

Rename W1 and GPT-OSS variant stems to {gpu}_inferencex-atom-single_{model}_{precision}[_{mode}] and remove per-variant subfolders. Update README and docs with new copy-config paths and W1 threshold gates for per-GPU throughput and tail latencies.

* feat(inferencex): gate W1 per-GPU and tail latency metrics on ATOM path

Add inferencex_atom_parsing with IX-specific GATED_METRICS (per_gpu_throughput, output_tput_per_gpu) without changing vllm_single. Wire InferenceXAtomJob and the suite to atom parsing; default metric_percentiles to 95,99 for p95 TPOT and p99 TTFT.

* docs(inferencex): document ATOM-specific parsing vs vllm_single

Clarify that W1 GATED_METRICS and output_tput_per_gpu live in inferencex_atom_parsing so vllm_single stays untouched until parity.

* docs(inferencex): simplify variant README for lab users

Drop vllm_single comparisons and internal threshold tables; keep naming pattern, variant list, and copy/run commands.

* chore(utils): silence cluster placeholder resolution logs

* test(config): align config_loader tests with sibling threshold discovery

* feat(inferencex): add W1 metric tiers for tiered threshold gates

* feat(inferencex): reuse server across sweep cells and config-driven waits

* feat(inferencex): replace per-metric tests with tiered test_cell_metrics

* chore(inferencex): enable server reuse on perf configs and shorter smoke waits

* fix(inferencex): align cluster container names with variant configs

* fix(inferencex): tighten W1 perf health gates when enforcing thresholds

* test(inferencex): add GATED_METRICS parity and health gate coverage tests

* test(inferencex): add GATED_METRICS parity and health gate coverage tests

* docs(inferencex): update plan and README for flat layout and tiered gates

* docs(inferencex): update plan and README for flat layout and tiered gates

* chore(inferencex): trim expand_sweep docstring

* docs(inferencex): clarify lab layout, launcher host, and results paths

Document per-variant ~/input subdirs to avoid ambiguous threshold discovery,
remote launcher vs GPU node prerequisites, and ~/cvs_results output paths.

* docs(plan): prioritize multi-node as M5 after framework parity

Elevate scaling to P1 milestone M5 immediately after M4 parity when
hardware and suite recipes support nnodes>1; defer MTP+P2 widen to M6.

* fix(inferencex): align W1 tpot tier with ATOM bench output

Gate p99_tpot_ms instead of absent p95_tpot_ms, skip missing tier metrics in actuals, and recalibrate MI300X perf thresholds from the 2026-06-25 lab run.

* chore(inferencex): use portable W1 perf thresholds on MI300X

Replace per-node calibrated gates with conservative throughput floors and loose latency caps so healthy runs pass across lab nodes without recalibration.

* test(inferencex): cover parse_results errors and client log failure paths

* fix(inferencex): detect ATOM server early failures during wait_ready

* refactor(inferencex): extract sweep reuse helpers and safer collection defaults

* feat(inferencex): add explicit threshold_json paths to variant configs

* chore(inferencex): polish conftest docs and simplify CLIENT_METRICS build

* refactor(inferencex): inline atom_args and remove ix_recipe indirection

* docs(plan): sync IX atom plan with inline atom_args config layout

* feat(dtni): add vllm_distributed CVS suite for 2-node MI300X multinode inference

Introduces vllm_distributed, a new CVS inference validation framework for
2-node MI300X clusters running vLLM with tensor parallelism (TP=8) and
pipeline parallelism (PP=2) across 16 GPUs total via the multiprocessing
distributed executor backend.

New files:
  cvs/lib/inference/vllm_distributed.py          VllmDistributedJob class:
    - build_server_cmd applies 5 in-container patches per run to fix upstream
      vLLM bugs in the rocm/ufb-private nightlies image:
        Patch 0:  delete stale multiproc_executor.pyc and core.pyc
        Patch 0b: replace assert in multiproc_executor.py:collective_rpc
                  (rpc_broadcast_mq is None on PP follower nodes); return
                  safe default instead of crashing
        Patch 1:  guard _initialize_kv_caches() for follower nodes; use
                  dummy KVCacheConfig(num_blocks=1) to skip collective_rpc
        Patch 2:  stub Scheduler() with _F on follower nodes to skip
                  KVCacheManager/HybridKVCacheCoordinator assert
        Patch 3:  fix get_supported_tasks() to return ("generate",) for
                  follower nodes (SupportedTask is Literal, not Enum)
    - is_ready() / wait_ready(): per-poll readiness with fatal-log detection
    - run_client(): bench serve head-only via exec_on_head
    - postcheck(): validates server log, client log, result file
    - collect_logs(): zips node logs and HTML artifacts
  cvs/lib/inference/utils/vllm_distributed_config_loader.py  config schema
  cvs/lib/inference/unittests/test_vllm_distributed.py        52 unit tests
  cvs/tests/inference/vllm_distributed/                       pytest suite
  cvs/input/config_file/inference/vllm_distributed/           config + thresholds

Modified files:
  cvs/core/orchestrators/container.py    openssh-server fallback install for
                                         images without sshd; per-cmd timeout
  cvs/lib/inference_lib.py               register vllm_distributed framework
  cvs/lib/inference/unittests/test_vllm_orch_parse.py  fix threshold JSON path

Validated on 10.245.135.15 (g21u43, head) + 10.245.135.115 (h16u07, worker)
with amd/Llama-3.1-70B-Instruct-FP8-KV, ISL=1000 OSL=1000 concurrency=16.

Signed-off-by: Atul Nair <Atul.Nair@amd.com>

* style: apply ruff formatting to vllm_distributed suite files

Signed-off-by: Atul Nair <Atul.Nair@amd.com>

* fix(dtni): address review feedback on vllm_distributed suite

- Revert cvs/core/orchestrators/container.py: the openssh-server
  fallback install should not be in core; the ufb-private image already
  ships sshd (confirmed by v7a7 validation pass)
- Replace VllmDistributedJob alias with direct use: test suite imported
  VllmDistributedJob as VllmJob; now uses the class name directly
- Scrub personal references from config: threshold_json absolute path,
  master_addr IP, and GLOO/TP/NCCL_SOCKET_IFNAME NIC name replaced
  with <changeme> placeholders
- Remove VllmDistributedJob from InferenceJobFactory registry:
  VllmDistributedJob's constructor (orch, variant, ...) is incompatible
  with create_job's calling convention (c_phdl, s_phdl, ...) so the
  entry was unreachable dead code

* fix(dtni): remove test_setup_sshd from vllm_distributed suite

vLLM with --distributed-executor-backend mp uses PyTorch distributed
(TCPStore on master_addr:master_port) for inter-node rendezvous -- no
SSH between containers is required. The sshd test was cargo-culted from
MPI-based suites and fails on images that do not ship openssh-server.

* feat(vllm): unified vllm suite replacing vllm_single + vllm_distributed

New files:
- cvs/lib/utils/ib_discovery.py: discover_ib_hca_names() via ibv_devinfo -l;
  fails loudly on empty nodes or asymmetric HCA lists across nodes
- cvs/lib/inference/utils/vllm_config_loader.py: unified VariantConfig for
  single-node (nnodes=1, pp=1) and distributed (nnodes>1, pp>1); ib_netdev
  required when nnodes>1; cell_key emits PP= segment only when pp>1
- cvs/lib/inference/vllm_job.py: unified VllmJob; distributed flags added iff
  nnodes>1; run_client/wait_client_complete/parse_results use exec_on_head;
  IB devices (ib_hcas, ib_netdev) written into env script; no runtime patches
- cvs/tests/inference/vllm/vllm.py: unified suite with test_discover_topology
  lifecycle step; single-node skips discovery; distributed validates config
  ib_hca_devices list at preflight
- cvs/input/config_file/inference/vllm/: single and distributed config templates

Modified:
- cvs/tests/inference/vllm/conftest.py: switch to vllm_config_loader,
  add test_discover_topology to rank map, fix hf_token for remote=0

* fix(vllm): address suite bugs found during core42 validation run

Three issues surfaced by live 2-node run on 10.245.135.11/13:

1. ib_discovery: add /sys/class/infiniband sysfs fallback when ibv_devinfo
   is absent from the image. ROCm vLLM images ship without libibverbs-dev
   but sysfs always reflects the same HCA names NCCL_IB_HCA needs.

2. vllm_job: fix is_ready() and wait_ready() to use per-rank log paths.
   Previously broadcast self.server_log (node0 path) to all nodes — node1
   always got exit_code!=0 (file not found), causing a 60-minute timeout
   on any follower failure. Now checks _rank_log(rank) on each host and
   fails fast via _check_early_failure() each poll iteration.

3. vllm: skip test_setup_sshd entirely. vLLM distributed uses
   --distributed-executor-backend mp + NCCL over host network; no
   inter-container sshd is needed (unlike MPI-based suites). The
   vLLM image also does not ship openssh-server.

* fix(vllm): headless worker ranks + head-only readiness for distributed

Worker ranks (rank > 0) launch with --headless, and is_ready() only greps the
head rank for the startup pattern when nnodes > 1 (worker ranks have no API
server and never log 'Application startup complete').

* perf(vllm): reuse server across cells with identical server args

The unified vLLM suite restarted vllm serve (full weight reload + warmup) for
every sweep cell, even when consecutive cells differed only in concurrency — a
client-only knob that never changes the server command. For a multi-concurrency
sweep this paid one ~9-minute weight reload per cell for no reason.

- Add VllmJob.server_signature(): the rank-agnostic server identity (argv minus
  --node-rank, plus the env map), excluding client-only knobs. Cells with the
  same signature can share one running server.
- test_vllm_inference reuses the live server when the signature matches the one
  recorded on the lifecycle object, skipping stop/start/wait_ready; a failed
  cell clears the recorded signature so the next cell does a clean bringup.
- Fix a duplicate --max-model-len: _server_argv emitted a derived value AND the
  config's serve_args value, so vllm saw the flag twice (config silently won).
  Now the derived value is only emitted when serve_args does not pin it.

Adds test_vllm_job_server_reuse.py covering the dedup and signature behavior
(concurrency-invariant; ISL/OSL-sensitive when max-model-len is derived).

* chore(vllm): remove legacy vllm_single + vllm_distributed suites

The unified vllm suite (cvs/tests/inference/vllm/vllm.py) parametrizes both
single-node (nnodes=1) and multinode distributed (nnodes>1, PP across nodes)
runs, fully replacing the two legacy suites. Nothing outside the deleted files
imported the legacy classes/loaders.

Removed:
- suites: tests/inference/vllm/vllm_single.py, tests/inference/vllm_distributed/
- lib: vllm_single.py, vllm_distributed.py
- loaders: inferencing_config_loader.py, vllm_distributed_config_loader.py
  (both superseded by vllm_config_loader.py)
- legacy unit tests: test_vllm_orch_parse.py, test_vllm_distributed.py,
  test_inferencing_config_loader.py
- sample configs under input/config_file/inference/vllm_{single,distributed}/

Kept _shared.py (the unified suite imports it) and fixed two now-stale
docstrings. `cvs list` shows only `vllm`; remaining unit tests pass.

* fix(vllm): create cell out_dir in run_client for the server-reuse path

The server-reuse path skips build_server_cmd (which did the per-cell
`mkdir -p out_dir`), so a reused cell's client wrote client.log/results into a
directory that never existed -> 'No such file or directory' and the cell failed.
run_client now ensures its own out_dir on the head node, so it is correct
whether the server was freshly built or reused. Regression test added.

* fix(vllm): pass --trust-remote-code to bench client when server enables it

The bench client loads the tokenizer from --model to count tokens. Models
whose tokenizer_config declares a custom tokenizer via auto_map (e.g.
Kimi-K2.6) fail this load with ValueError unless trust-remote-code is set.
The server already honored serve_args trust-remote-code; mirror it on the
client so the same tokenizer loads. Validated on 2-node Kimi-K2.6-MXFP4
(TP8xPP2): 3 cells, 0 failed requests, 297/530/830 tok/s.

* style(vllm): satisfy ruff lint + format gate

Remove 4 unused imports and apply ruff formatter to the report and
inference modules introduced on this branch, so `make build`
(fmt-check + lint) passes. No behavior change. Verified: ruff check
clean, ruff format clean, pylint 10/10, 415 unit tests + 38 CLI tests pass.

* feat(vllm): surface server log content and validate serve_args at load time

- _check_early_failure: add emit_tail param; precheck and warmup calls
  log the tail -30 snapshot at INFO so startup/engine-load lines appear
  in the CVS capture without re-emitting on every poll iteration
- wait_ready: log readiness poll iter=N/M at each iteration for progress
  visibility during the up-to-60-minute poll window
- _flatten_serve_args: False values now omit the flag entirely (previously
  emitted --flag False which argparse rejected as unrecognized argument)
- EARLY_FAILURE_RE: extended with argparse error patterns so a server CLI
  parse failure raises on the precheck call instead of spinning to the cap
- RoleServer: add field_validator for serve_args.log-level; invalid values
  raise ValidationError at config-load time rather than at server launch
- Tests: FakeOrchWithOutput + _make_job_for_check helper; 7 new cases
  covering False omission, True flag-only, log-level pass-through,
  emit_tail logging, CLI parse error raise, and the log-level validator

* fix(vllm): restore inferencing_config_loader.py deleted in error

The rebase onto dev/dtni's PR #244 dropped this module believing it was
legacy vllm_single-only, but dev/dtni's InferenceX ATOM suite (added
independently via PR #244) imports Sweep/SeqCombo/GoodputSlo/Run from it
via inferencex_atom_config_loader.py. Restoring it and its test file
verbatim from dev/dtni fixes the ModuleNotFoundError.

* feat(vllm): add Ray distributed-executor-backend support

Adds ray as a distributed-executor-backend option for multi-node vLLM
serving (nnodes>1, pp=1), alongside the existing mp backend. Bootstraps
a Ray cluster (head + workers) before vllm serve, runs vllm serve only
on the head, and tears down via ray stop on stop_server.

* fix(inference): remove stale duplicate left by dev/dtni rebase

cvs/lib/inference/inference_suite_lifecycle.py was renamed to
cvs/lib/inference/utils/inference_suite_lifecycle.py upstream (dev/dtni
#255). Rebasing onto dev/dtni with -X ours left a corrupted duplicate
at the old path with no remaining references; delete it.

* fix(vllm): stop overriding configured num_prompts with magic constants

test_vllm_inference always overrode variant_config.params.num_prompts
(a documented config field, default 3200) with a hardcoded
concurrency*20/50 heuristic keyed on osl >= 8192. Use the configured
value directly instead.

* fix(inference): remove old-path duplicates reintroduced from a stale rebase

dev/dtni PR #255 moved cache_probe.py, inferencex_atom_orch.py,
inference_suite_results_table.py, inferencex_atom_config_loader.py, and
inferencex_atom_parsing.py to their inferencex_atom/ and utils/
locations. This branch's rebase onto dev/dtni left the old-path copies
in place alongside the moved ones, landing divergent duplicate source
modules. No code references the old paths; deleting them.

* fix(vllm): use correct exec() output key in fatal-error check

detailed=True exec() results carry the log text under "output", not
"stdout" (every other call site in this file already uses "output").
The wrong key meant FATAL_LOG_RE could never match, so a fatal
RuntimeError in the server log would go undetected until the full
readiness poll cap instead of failing fast.

* style(vllm): apply ruff format to PR files

Wraps two over-width lines in test_vllm_job_ray_backend.py to satisfy
ruff format --check; no logic changes.

* style(utils): apply ruff format/lint fixes to gpu.py + test_gpu.py

Pre-existing drift unrelated to the vllm unification PR: ruff format
line-wrapping and E401 multiple-imports-on-one-line in test_gpu.py.
No logic changes.

---------

Signed-off-by: Atul Nair <Atul.Nair@amd.com>
Co-authored-by: Hamna Nimra <hnimrama@amd.com>
* feat(vllm): wire unified vLLM suite into inference report engine

The generic inference report engine (cvs/lib/report/) already landed on
this branch via dev/dtni, but no suite preset existed for `vllm`, so
`cvs run vllm` never rendered a run deck. Add the missing preset plus
its vLLM-specific tier/column glue:

- vllm_parsing.py: VLLM_RESULTS_COLUMNS (mirrors _shared.py's console
  table), METRIC_TIERS/METRIC_TIER_ORDER seeded from the existing
  GATED_METRICS set so the report's gate matrix partition exactly
  matches what the suite already enforces, and tier_metric_specs().
- report/presets/vllm.py: registers VLLM_REPORT_CONFIG under stem
  "vllm" (cvs/tests/inference/vllm/vllm.py), with explicit lifecycle
  labels overriding the builder defaults -- this suite records
  topology_discovery instead of sshd_setup/client_complete.
- tests/inference/vllm/conftest.py: remove the suite-local lifecycle
  table hookwrapper, now redundant with root conftest's
  attach_inference_suite_lifecycle_table once the preset is registered
  (confirmed byte-for-byte equivalent rendering logic).
- unittests/test_vllm_report_preset.py: pins the tier partition
  bijection with GATED_METRICS and the auto-register wiring.

Verified with an offline dry render (synthetic VariantConfig +
inf_res_dict via write_report()): HTML/JSON/viewer artifacts generate,
gate matrix tiers compute correctly, and the lifecycle timeline shows
only the stages this suite actually records.

* fix(vllm): correct stale hook reference in lifecycle docstring

The docstring pointed to pytest_runtest_makereport, which no longer
renders these rows after the vLLM suite was wired into the generic
inference report engine; attach_inference_suite_lifecycle_table does
it now.
…ting (#258)

* fix(vllm): account for pipeline-parallel size in per_gpu_throughput

* fix(vllm): account for pipeline-parallel size in per_gpu_throughput

* fix(inferencex_atom): pass pp=1 to to_client_metrics after pp became required

ATOM has no pipeline-parallel concept (single-node TP only), so pp=1
preserves the prior per_gpu_throughput numeric behavior. Also widen the
Ray-backend regression test to assert pp explicitly across pp=1/2 so a
broken pp passthrough can't slip through silently again.

* Revert "fix(inferencex_atom): pass pp=1 to to_client_metrics after pp became required"

This reverts commit a2eb3c4.

* feat(vllm): wire GPU metrics polling into the vLLM inference suite

Adds test_gpu_metric (one HTML row per GPU metric per sweep cell),
GPU pre/post-load VRAM snapshots and background amd-smi polling
during test_vllm_inference, and extends threshold-coverage validation
to gate gpu.* metrics alongside client.* metrics.

amd-smi runs fine from inside the benchmark container (GPU device
files are passed through), so gpu.py calls orch.exec/exec_on_head
directly like every other command in the suite -- no host-vs-container
bypass routing needed.

* chore(vllm): remove drive-plan ledger artifact accidentally committed to PR

spec-a1-ledger.md is an internal drive-plan tracking file, not part of the
shipped test suite; it doesn't belong under cvs/lib/inference/unittests/.

Signed-off-by: Atul Nair <Atul.Nair@amd.com>

* fix(vllm): default pp="1" in to_client_metrics to unbreak ATOM

Making pp a required keyword-only arg broke InferenceX ATOM at runtime:
its wrapper (inferencex_atom_parsing.to_client_metrics) calls this
function without pp, so every ATOM run raised TypeError in
parse_results(). ATOM has no pipeline-parallel concept, so defaulting
pp to "1" here reproduces the pre-fix tp-only formula for those
callers while vLLM call sites keep passing pp explicitly.

Addresses blocking review feedback on PR #258.

Signed-off-by: Atul Nair <Atul.Nair@amd.com>

* chore(vllm): remove unused gpu_metrics_snap fixture

Never consumed by any test -- test_vllm_inference tracks GPU snapshots
via local pre_snap/post_snap variables instead. Leftover from the
generic gpu-metrics.md fixture template that the vLLM implementation
diverged from.

Addresses non-blocking review feedback on PR #258.

Signed-off-by: Atul Nair <Atul.Nair@amd.com>

* fix(vllm): replace thread-based GPU poller with detached remote script

poll_gpu_metrics() spawned a second OS thread that shared the
orchestrator's gevent-based SSH transport with the main thread's
client-log-tail polling, causing a real HW-observed
SessionError(OutOfBoundaryError()) race. Replace it with
start_gpu_poller()/stop_and_collect_gpu_poller(): a detached remote
background script per node writes amd-smi snapshots to a file, read
back via ordinary sequential exec/exec_on_head calls, extending to
real multi-node round-aligned collection.

* style(vllm): ruff-format the new gpu poller test classes

* fix(gpu): scope poller /tmp files by user and clean them up after collection

start_gpu_poller()/stop_and_collect_gpu_poller() write a script and log
file to /tmp on each polled node, keyed only by a sanitized run_id (e.g.
a pytest node id). On shared hardware nodes (SSH-based orch.exec, not a
per-container /tmp), two users running a similarly-named test can collide
on the same path, and the files were never removed afterward -- the same
failure mode reported for Fremont's conda-based CVS runs where /tmp is
mounted through from the host.

Scope the marker by the local SSH user (getpass.getuser()) in addition to
run_id, truncate any stale log at launch instead of only appending, and
rm -f both the script and log on each node after stop_and_collect_gpu_poller
reads them back -- following the same never-raises degrade pattern already
used for the pkill broadcast and read-back.

---------

Signed-off-by: Atul Nair <Atul.Nair@amd.com>
Adds VllmJob.probe_openai_endpoints(), reusing the shared OpenAIProbe
helper already used by the sglang suite, but driven through orch.exec_on_head
instead of docker exec/Pssh. Wires it in as a new lifecycle stage
(test_openai_compatible_smoke) that brings up a short-lived server at
a small fixed cell and checks GET/POST /v1/models,
/v1/chat/completions, /v1/completions, and structured JSON output
before the full sweep runs.
test_openai_compatible_smoke derived max-model-len from the unrelated
_SMOKE_ISL/_SMOKE_OSL sweep-cell constants (296 tokens), but the
OpenAI-compatible probe sends its own fixed-content requests -- the
structured-output-book probe alone needs ~50 prompt + 256 response
tokens, exceeding that budget and failing every run with HTTP 400.
Set serve_args["max-model-len"] explicitly so the flawed derivation
is bypassed for this test path.
_check_early_failure() only tails the server log during startup, so a
mid-benchmark crash (e.g. EngineDeadError) currently vanishes from the
captured pytest output. Add VllmJob.dump_server_log(), mirroring the
existing dump_client_log() pattern but per-rank, skipping ray-backend
headless workers (rank > 0) which never run vllm serve.

Wire it into test_vllm_inference's failure path only, tracking
lifecycle.live_server_job so the dump targets whichever job actually
owns the running server -- on the reuse path (cells that only differ
by concurrency) that's an earlier cell's job, not the current one.
* Orchectration and Reporting. Signed-off-by: amd-droy <droy@amd.com>
…te integration (#268)

* feat(inference): add AccuracyTask/AccuracyConfig schema for accuracy harness

First unit of the lm-eval-harness accuracy evaluation system: the
config.json-side task selection schema, split from threshold/gating
values which will be joined in at runtime by a later unit.

* feat(inference): add lm_eval_parsing.py auto-discover projector

Second unit of the accuracy harness: pure JSON -> {scalar: float}
projector for lm-eval-harness results.json payloads. Walks every
numeric metric with no per-task registry, so group tasks (e.g.
RULER's per-seq-length metrics) fall out of the same walk.

* feat(inference): add lm_eval_job.py command builder + orchestration runner

* feat(inference): add test_accuracy_eval lifecycle stage (step 4)

* Wire AccuracyConfig and test_accuracy_eval into vllm and inferencex_atom suites

Adds the accuracy field to both suites' VariantConfig classes (defaulting to
an empty AccuracyConfig so existing configs load unchanged), imports the
shared test_accuracy_eval lifecycle stage into each suite's test module, and
places it in collection order right after the perf-metric stage.

* fix(accuracy): address adversarial review findings for accuracy harness

- exclude the top-level "accuracy" threshold key from sweep-cell coverage
  checks (was tripping the extra-key/typo detector); delegate
  vllm_config_loader's inline check to the shared validator
- gate accuracy threshold evaluation on enforce_thresholds, matching
  test_metric's existing record-only convention
- route chat-template tasks to /v1/chat/completions instead of always
  using /v1/completions
- wrap exec_on_head tuple-unpack and JSON parse failures in
  run_accuracy_tasks as RuntimeError instead of letting raw
  ValueError/JSONDecodeError propagate
- select the newest results*.json by mtime instead of an arbitrary find
  ordering, guarding against stale results from a prior run

* fix(accuracy): pass --apply_chat_template to lm_eval for chat-template tasks

build_lm_eval_cmd switched the model backend/endpoint to local-chat-completions
for tasks with apply_chat_template=True but never passed lm-eval's own
--apply_chat_template flag, so lm-eval sent plain-string prompts and the
chat-completions client asserted on every request. Verified on a live 2N
DeepSeek-R1 accuracy run where this aborted mmlu_pro and blocked all
subsequent tasks in the same test_accuracy_eval invocation.

* revert(accuracy): un-wire test_accuracy_eval from inferencex_atom_single

Scope this feature to vllm only until it's been validated against ATOM
hardware. Removes the accuracy field from InferenceXAtomVariantConfig,
the test_accuracy_eval import/collection-order entry in the ATOM suite,
and the corresponding ATOM-specific unit tests. The shared
lm_eval_job.py/lm_eval_parsing.py/accuracy_config.py machinery and
test_accuracy_eval itself are untouched -- vllm's wiring is unaffected.

* feat(accuracy): parametrize test_accuracy_eval by task

Each accuracy task now gets its own pytest node (test_accuracy_eval[<id>])
instead of one collapsed row covering every configured task, matching the
test_metric/test_gpu_metric per-metric row convention. Task nodes are gated
independently: a run failure or threshold violation in one task no longer
sets the shared lifecycle.failed flag, so sibling tasks still execute rather
than being skipped by a prior task's outcome. pytest_generate_tests
parametrizes accuracy_task from config.json's accuracy.tasks, including the
empty-list case (auto-skips a single node, same UX as before).

* test(accuracy): use gsm8k instead of mmlu as the default task fixture

mmlu spans 57 subjects and is unnecessarily slow for a placeholder task
in schema-validation tests; gsm8k exercises the same construction paths
without implying a real eval choice.

* fix(accuracy): propagate server env, trust_remote_code, and exit-code checks to lm-eval

lm_eval was launched without sourcing /tmp/server_env_script.sh, so
HF_HUB_CACHE/HF_TOKEN set during server setup never reached it -- tokenizer
resolution could fail on a fresh head node even though it happened to work
on hosts with a pre-warmed cache. Source the script the same way
VllmJob's client-launch path already does.

Models with custom tokenizer code (Qwen, ChatGLM, Phi, MPT, ...) need
trust_remote_code=True or they fail to load; add it unconditionally to
model_args since it's a no-op for models that don't need it.

run_accuracy_tasks treated any results*.json under the output dir as
success, even a stale one from a prior run, without checking lm-eval's own
exit status. ContainerOrchestrator.exec_on_head/DockerRuntime.exec_on_head
didn't support detailed=True (unlike their sibling exec() methods and
BaremetalOrchestrator.exec_on_head), so exit codes were unreachable on the
container-runtime path the vLLM/accuracy suite actually uses -- add the
missing detailed param end to end and check exit_code before falling
through to the results-file lookup.
…efill p50/p95) (#274)

* feat(vllm): add Prometheus /metrics-derived latency metrics (queue/prefill p50/p95)

Scrapes vLLM's own /metrics endpoint before/after each client run, diffs
the queue-time and prefill-time histograms to isolate one cell's
observations, and interpolates p50/p95 quantiles matching PromQL's
histogram_quantile(). Gated as prom.* metrics via a parallel
GATED_PROM_METRICS set (mirroring GATED_GPU_METRICS) to keep out of the
locked client.* tiering partition.

* fix(vllm): include gpu/prom metric rows in report cell-card extras

test_gpu_metric and test_prom_metric rows were missing the pytest-html
cell-card attachment that test_metric rows get, since row_card_test_names
only listed test_metric.

* fix(vllm): pin test_prom_metric in the lifecycle order rank

test_prom_metric was missing from pytest_collection_modifyitems' rank
dict, so it fell through to the default rank (99) and collected after
test_teardown -- running the prom metric assertions against an already
torn-down container instead of alongside test_metric/test_gpu_metric.

* fix(vllm): clamp histogram_quantile to the highest finite bound in +Inf bucket

PromQL cannot linearly interpolate past the last finite bucket boundary,
so it clamps a quantile that falls in the unbounded "+Inf" bucket to that
boundary instead of extrapolating to infinity. Our implementation
returned +Inf in that case, diverging from the PromQL parity the
docstring claims. Under the overload scenarios these metrics target, a
p95 exceeding every finite bucket would surface as `inf` ms instead of a
finite value (and risk `Infinity` in strict-JSON serialization if gated).

The one exception (a "+Inf"-only histogram with no finite boundary to
clamp to) still returns +Inf, matching PromQL.

* fix(vllm): stop requiring every gated client/gpu/prom metric in threshold.json

_check_thresholds_cover_sweep previously raised (under enforce_thresholds:
true) if a cell's threshold entry omitted ANY client.*/gpu.*/prom.* gated
metric, making the full 20+-metric union mandatory per cell. Downstream
evaluation (test_metric/test_gpu_metric/test_prom_metric) already treats
an absent spec as "don't gate this metric" -- the completeness check
existed only at load time and served no evaluation-time purpose.

This meant a user who only wants to gate a couple of metrics (e.g. just
prom.queue_time_p50_ms) was forced to also author specs for every other
client/gpu/prom metric, or drop to enforce_thresholds: false and lose
gating entirely. Adding prom.* to the gated union (this PR) would have
made this worse for any downstream threshold.json with
enforce_thresholds: true, since it would now also need 4 new prom.*
specs per cell just to keep loading.

Drops the completeness check; keeps the still-needed sweep-cell-coverage
check (every sweep cell must have SOME threshold entry, cell keys must
not be stray/typo'd).

* style(vllm): ruff format prom-metrics files (line-length wraps)

* docs(vllm): remove dangling VLLM_PROMETHEUS_METRICS_SPEC.md references

The spec doc was never added to the repo/PR, so its ~10 citations across
comments/docstrings pointed readers at a nonexistent file. Drop the
references, keep the underlying rationale inline.
* Added optional Tier 2 node_smoke support to preflight via config (tier2_perf: true).

When enabled, preflight passes --tier2-perf and thresholds for:

GEMM TFLOPS floor
HBM D2D bandwidth
local multi-GPU RCCL all-reduce
Updated: node_smoke.py, preflight config/schema/docs, report summary, and unit tests.

Signed-off-by: Urvashi Tiwari <urtiwari.com>

* Fixed the default setting

Signed-off-by: Urvashi Tiwari <urtiwari.com>

---------

Signed-off-by: Urvashi Tiwari <urtiwari.com>
Co-authored-by: Urvashi Tiwari <urtiwari.com>
…284)

leaderboard_math_hard imports math_verify when building its task prompt
templates. The install guard requested only the `api` extra, so the module
was absent and the task died with ModuleNotFoundError after the server was
already up -- observed on GLM-5.2-FP8 (TP8/PP2, ROCm 7.14), where 8 of 10
accuracy tasks scored and math_hard failed on the missing dependency.

Request `lm-eval[api,math]`, which pulls math-verify, sympy>=1.12 and the
pinned antlr4 runtime.

Also replace the `pip list | grep lm_eval` presence check with an import
probe. The old guard tested only that some lm_eval existed, not that it
carried the needed extras: on an image preinstalling bare lm-eval it would
short-circuit, skip the math extra, and reproduce this same failure while
the install line looked correct. Probing `import lm_eval, math_verify`
fails closed instead.

Unit-tested only -- the in-container install path is not exercised by the
suite and still needs a hardware rerun of leaderboard_math_hard to confirm.
…#278)

* Stop double-logging every remote command's output

pssh's own host_logger emits every remote stdout/stderr line, tagged with
the host. Upstream keeps it quiet behind a NullHandler unless
enable_host_logger() is called -- which CVS never does -- but
cvs/lib/globals.py binds `log` to the ROOT logger, so propagation delivers
those lines to CVS's handlers anyway. Pssh._process_output then logs each
line a second time.

Measured on a 2-node DeepSeek R1 TP16/PP1 vLLM run: 3,747,478 duplicate
pairs, 49.8% of all lines in a 491 MB cvs.log.

Detaching the third-party logger keeps the _process_output copy, which is
the one that honors print_console and can therefore be suppressed for
bulk-data commands. Host attribution is preserved by the existing
"Host == <host> ==" banner that _process_output prints per host.

* Thread print_console through the orchestrator and runtime layers

Pssh.exec has always accepted print_console, but ContainerOrchestrator and
DockerRuntime dropped it, so a caller asking for a quiet bulk read still
got every line logged. Passing the kwarg from a call site raised TypeError
before this change.

Added as a keyword argument defaulting to True in last position, mirroring
the existing `detailed` parameter, so every current call site is
unaffected. Also aligned the ContainerRuntime protocol, BaremetalOrchestrator,
and the enroot stub.

MultiProcessPssh (what cvs/core actually gets from the parallel_ssh_lib
shim) already accepts and forwards print_console on both its sharded and
delegating paths, so no change was needed below the runtime layer.

Tests pin forwarding on all three DockerRuntime exec paths and all three
BaremetalOrchestrator paths -- a dropped kwarg fails silently, so the
default-stays-verbose case is pinned too.

* Stop logging raw amd-smi JSON from the GPU metrics reads

The GPU poller writes amd-smi JSON to a file on each node every 15s;
stop_and_collect_gpu_poller then cats the whole accumulated file back and
parses it. The cat output was logged in full.

On a 2h14m 2-node DeepSeek R1 TP16/PP1 run that was 7,457,416 lines /
486 MB from two cat calls -- 99.1% of a 491 MB cvs.log. The pre/post
snapshot calls added another 2.1 MB.

Nothing is lost. The digest side-file (gpu_poll_isl*_osl*_conc*.log,
23 KB) already carries every value any consumer reads, as do the
peak_gpu_memory_mb / gpu_compute_util_pct / gpu_bandwidth_util_pct
aggregates. The suppression is applied inside gpu.py, where "this is bulk
data" is locally true, rather than left to each caller.

* Emit each vLLM log once instead of two or three times

_check_early_failure tails each rank's server log on every readiness poll
and, under emit_tail, re-logs it with host+rank labels. With the transport
also logging it, each line landed up to three times. Across 75 polls that
was 30,159 emitted lines carrying only 974 distinct ones -- 97% repeats.

The same pattern appeared in dump_client_log, dump_server_log,
parse_results, and the client-completion poll: each reads content and then
either emits it under its own label or parses it.

Suppressing transport-level logging on these reads keeps exactly one copy,
and it is the best-labeled one -- dump_server_log's crash dump (PR #270)
is now easier to read, not harder. dump_client_log runs on every exit path
of the completion poll, so the per-iteration tail added nothing.

* Preserve failure diagnostics the quieting silenced

Suppressing console echo on the vLLM log reads removed the only copy of
some output that mattered on failure paths:

- test_openai_compatible_smoke never dumped its server log, relying on
  the poll loop's echoed tails. A bringup timeout now left nothing from
  the failing server behind; it dumps like the sweep does.
- parse_results raised on a malformed artifact without quoting it, so
  the content was no longer anywhere in the log.

Also gives ContainerOrchestrator.exec_on_head the detailed parameter its
baremetal counterpart already had. build_mpi_cmd calls exec_on_head with
detailed=True, which raised TypeError on the container path; unreachable
today since distribute_using_mpi has no in-tree caller, but this PR is
already reshaping that signature.

The host-subset forwarding test asserted against the same MagicMock for
both the subset and all-hosts handles, so it passed even with the branch
it guards removed. It now pins the constructed host list.

* Filter pssh host_logger instead of clearing its propagate flag

The previous fix set propagate=False on pssh.host_logger. That works in a
plain process but not under pytest, which is how CVS actually runs:
_pytest.logging.catching_logs attaches its capture handler to root AND to
every non-propagating logger, so clearing propagate makes pytest attach
directly to host_logger and the duplicate line survives.

A filter drops the record before any handler is consulted, so it holds
however the handler was attached. It also survives a level reset, which a
setLevel(CRITICAL) approach would not.

The old unit test asserted on the propagate flag in a plain process, so it
verified the mechanism intended rather than the outcome wanted. The new
tests assert no pssh.host_logger record is captured -- once with a handler
bolted straight onto the logger, once inside a real catching_logs -- plus a
guard that other loggers are still captured.

* Sync the Orchestrator ABC and pin container-layer forwarding

Review follow-up on #278.

The ABC advertised a narrower contract than every class implementing it:
`print_console` (this PR) and `detailed` (pre-existing) were accepted by
BaremetalOrchestrator and ContainerOrchestrator but declared by neither
abstract method. Python's abstractmethod enforces method names only, never
parameter lists, so nothing raised and no test caught it -- the gap only
surfaces when someone writes a new backend from the ABC and omits the
kwarg, and a dropped print_console is silent: the command still works, it
just logs hundreds of MB again.

test_abc_signature_matches_implementation compares the ABC's parameters
against the concrete implementation's so this drift cannot recur quietly.

The container forwarding pins mirror the existing baremetal ones onto the
path the vLLM suite actually runs, and which previously dropped both
kwargs. They read arguments by keyword or position, so they keep holding
if the runtime signature grows; verified by mutation (removing either
forward fails four of them with a named-argument message, not IndexError).

* Address review nits; back out the ABC signature change

Revert the Orchestrator ABC signature widening from 95907bd. The ABC's
sole subclass is BaremetalOrchestrator, which ContainerOrchestrator
inherits from, so the concrete signatures are already the contract every
call site sees. The ABC/impl drift also pre-dates this PR.

Three review nits:

- globals.py: name the host_logger suppression filter instead of an
  anonymous lambda, so it is identifiable in
  logging.getLogger('pssh.host_logger').filters when debugging log
  routing on a live node.

- vllm_job.py: repr() the artifact snippet carried into the
  parse_results error. The artifact can be a stack trace or an HTML
  error page; raw newlines there break up CI output and pasted ticket
  bodies.

- container.py: forward detailed/print_console to the runtime by
  keyword rather than positionally.

Also mirror the exec/exec_on_head forwarding pins from
test_baremetal.py into test_container.py -- container is the path the
vLLM suite actually runs on, and the one that previously dropped both
kwargs. Validated by mutation: deleting the forwarding args in
container.py fails 4 of the 6 pins.
hnimra-amd and others added 11 commits August 11, 2026 09:23
Fixes fmt-check failures without changing runtime behavior.
Drop the outdated branch-scoped plan doc now that multinode atom work has landed on dev/dtni.
…scovery (#303)

* test(atom): align multinode unit tests with session key and fabric discovery

Update server_session_key fixtures with nnodes/pp/master fields and stub IB topology in build_server_cmd tests so FakeOrch runs match the lab lifecycle path.

* test(atom): fix RUF012 ClassVar lint in recording orch helper
* docs(atom): add inference suite README for tests/inference/atom

Document suite layout, lifecycle, sweeps, metric tiers, and quick-start commands in the same style as the JAX MaxText training suite README.

* docs(atom): rewrite config README in jaxmaxtext reference style

Restructure the input-config guide with file inventory, must-change table, config/threshold reference, and condensed lab workflow; point to the new tests/inference/atom README.

* docs(atom): scope READMEs to ATOM drivers and trim clutter

Drop sglang/vllm placeholder references, migration notes, and redundant related-doc links from the config and suite guides.
* reformatted to pass ruff formatter check in the CI

* removed the unused line from the code

Signed-off-by: Urvashi Tiwari <urtiwari.com>
Co-authored-by: Urvashi Tiwari <urtiwari.com>
…ibuted) (#298)

* feat(vllm): add MI325X workload config set (single + distributed)

Adds 14 vllm inference workloads for MI325X, each as a single/distributed
pair -- 28 configs with 28 sibling threshold files, plus a README.

Topology follows the requested uniform shape: single is TP8/PP1/1 node,
distributed is TP8/PP2/2 nodes. Several workloads in the source list specify
TP=4; TP=8 is used throughout per the directive and the deviation is
documented in the README.

Every sweep carries the three concurrency-16 shapes (1k1k, 1k8k, 8k1k) as
sequence_combinations, but only 1k1k is referenced by sweep.runs, so a run
executes exactly one cell. random_range_ratio is 0.0 so ISL/OSL are exact.

enforce_thresholds is false on all configs and each threshold file covers the
selected cell with permissive placeholders for all 25 gated client.* metrics,
satisfying the coverage check without warnings until MI325X is calibrated.

Environment-specific values (model.id, container.image, models mount,
ib_netdev, master_addr) are redacted to <changeme>.

All 28 configs verified to load cleanly through vllm_config_loader.load_variant
with zero warnings, emitting the expected cell keys:
  single      ISL=1024,OSL=1024,TP=8,CONC=16
  distributed ISL=1024,OSL=1024,TP=8,PP=2,CONC=16

* fix(vllm): use per-model TP for MI325X workloads

DeepSeek V4 Flash, Kimi K2.6, Kimi K2.5 and gpt-oss-20b run at TP=4 per the
source workload list, instead of the previous uniform TP=8. TP=8 is unchanged
for the other ten workloads.

Distributed variants keep PP=2 across 2 nodes regardless of TP, so a TP=4
distributed run uses 4 GPUs per node.

Threshold cell keys follow automatically (ISL=1024,OSL=1024,TP=4,CONC=16 and
the PP=2 form). All 28 configs still load through load_variant with zero
warnings.

* docs(vllm): require a routable IPv4 when picking ib_netdev

An interface that merely exists is not enough: NCCL_SOCKET_IFNAME,
GLOO_SOCKET_IFNAME and TP_SOCKET_IFNAME all take this name, and gloo
fails engine init with "Unable to find address for: <name>" when the
interface is DOWN or has no IPv4. Point at `ip -o -4 addr show` so the
check catches that case.

* fix(vllm): set fp8 kv-cache for DeepSeek V4 workloads

DeepseekV4ForCausalLM uses the fp8_ds_mla attention layout, which
asserts "only supports fp8 kv-cache, got auto" and fails engine
initialization under the default kv-cache dtype. Hit on hardware with
DeepSeek-V4-Flash-FP8; V4-Pro shares the architecture.

* fix(vllm): enable AITER for DeepSeek V4 workloads

DeepSeek V4's sparse attention indexer has no non-AITER ROCm path and
refuses to initialize without VLLM_ROCM_USE_AITER=1. Confirmed on
hardware: with it unset the worker dies with "Sparse attention indexer
ROCm path is only supported on AITER".

* fix(vllm): enable AITER + GPU_ARCHS for sparse-attention workloads

GLM-5.1/5.2 (GlmMoeDsaForCausalLM) reach the same sparse attention indexer
as DeepSeek V4 via deepseek_v2.py -> mla.py -> sparse_attn_indexer.forward_hip,
which raises 'only supported on AITER' without VLLM_ROCM_USE_AITER=1.

AITER's JIT kernel build then fails with "One of GPU archs of [''] is invalid"
because GPU_ARCHS is empty in the image; MI325X is gfx942.

Scoped to the 4 stems whose model config carries the DSA indexer keys
(index_topk / index_head_dim / index_n_heads).

* feat(vllm): add gpu/prom threshold placeholders, zero all values

Threshold files covered only the client.* family. Add the gpu.* (5) and
prom.* (4) families so every metric the suite records has a visible slot,
taking 32 specs per file.

Set every value to 0 across all three families. These are uncalibrated
placeholders, not measurements. Note a 0 on a max/max_ms kind is an
impossible bound, so enabling enforce_thresholds before calibration fails
loudly rather than passing silently -- the previous 1e9 values did the
opposite.

Kinds follow the metric's unit: max for MB/s counts, min for utilization
floors, max_ms only for genuine milliseconds.

* feat(vllm): add accuracy placeholder blocks to MI325X workload set

Accuracy is the one gated family split across both files: config.json's
`accuracy.tasks` selects which lm-eval tasks run, and threshold.json's
`accuracy` block holds the gating values keyed by task id, then by lm-eval
metric key. Because those keys derive from the task ids chosen in the config,
they cannot be pre-enumerated the way client.*/gpu.*/prom.* were -- so both
blocks ship empty, as visible slots rather than absent ones, with the fill-in
shape documented inline and in the README.

No behavior change: `pytest_generate_tests` reads `raw.get("accuracy", {})`,
so an empty block and an absent one both parametrize on an empty list and the
node is auto-skipped. The `accuracy` threshold key is exempt from the
sweep-cell coverage check via NON_SWEEP_THRESHOLD_KEYS.

Verified: all 28 configs load through `load_variant` with 0 failures and 0
warnings; the exemption is discriminating, not vacuous (a typo'd "accuracie"
still warns). 74 accuracy unit tests pass. Full suite 1084 tests / 4 failures,
identical to baseline. fmt-check and lint are red on 23 pre-existing Python
files, identical count with and without this change (proven by stash) -- this
diff is JSON and Markdown only.

* refactor(vllm): retarget workload config set from MI325X to MI300X

Renames the directory, all 56 file names, and the gpu_arch / threshold_json /
container.name fields. Content-neutral: normalizing the arch token shows zero
files differing beyond the name, so no threshold, sweep or serve_arg changed.

GPU_ARCHS stays gfx942 -- MI300X and MI325X are both CDNA3 and report the same
arch, so the AITER env for the four sparse-attention workloads is correct for
either, and the runs already validated on MI325X hardware remain applicable.

Verified: all 28 configs load through load_variant with 0 failures and 0
warnings; every threshold_json resolves to a file that exists; leak scan clean
and all 28 configs still carry <changeme>. Unit tests 1084 / 4 pre-existing
failures, fmt-check 23 files, both identical to baseline.

* chore: drop local gate marker from the workload config branch

.last-gate-pass is a per-worktree marker the pre-push hook touches; it was
swept in by a git add -A and is not deliverable content.

* docs(vllm): clarify num_prompts, threshold coverage, and suite cross-links

Review feedback on the MI300X workload set.

num_prompts is 320 while the shipped vllm/ examples and the schema default
are 3200, which reads as a full-length benchmark unless you check. Say so in
the README and in each config's _comment, with the reason and how to undo it.

The Thresholds section listed the 32-slot grid without saying whether the
loader wants it. It only checks cell coverage -- an absent metric spec means
"don't gate this metric" -- so the grid is calibration convenience, not a
requirement.

Add a See also pointing at the vLLM suite reference and how-to, so the
threshold-kind and multinode material lives in one place.

---------

Co-authored-by: Atul Nair <atnair@amd.com>
…training suites (#300)

* [JAX] Adding Jax-maxtext training test refactor
Updating the Jax maxtext with Orchestrator instead of direct pssh.

* [JAX] Updated Time per Step - mean/p50/p95(ms)
* [JAX] Added Scaling Efficiency calculation for training.
* [JAX] Added Convergence check and validation loss/training loss check.
* [JAX] Added Loss curve plot for the Training loss.
It also checks for the loss curve slop for decrease in slope.
Added jaxmaxtext unittests.

* [JAX] Renamed config files and paths to match naming conventions.
* [JAX] Added one metric results html file for all metrics tests.
* [JAX] Enabled training sweeps run for few parameters.
* [JAX] Separate test files for single & distributed tests.
Also updated the sweep label (short) name with 3 params:
[PRECISION+SEQLEN+BATCH]
ex: BF16-SL4096-B3

* [JAX] Enabled error patterns from config file.
List of error patterns to be scanned in the training log is
added as user editable list via config file.
If this list i empty, it will fallback to the existing list of
error patterns in the code.

* feat(JAX) Added README files for jax test and config files.
* [JAX] Updated config files and ruff formatting fixes.

* [JAX] Fixed self review comments
1. num_gpus no longer assumes 8 GPUs/node
Added gpus_per_node: int = 8 to TrainingConfig (schema), and the job now computes
self.num_gpus = self.num_nodes * self.gpus_per_node from config (via getattr, default 8)
with a comment explaining it feeds tokens_per_sec_total → scaling efficiency.
Made it explicit and editable in all three configs ("gpus_per_node": 8), and documented
it in both READMEs (config table + a "must change per cluster" row).

* [JAX] Review comment fix and updates
1. Signle and distributed test files are now proper pytest files with
   direct pytest methods with clear docstring instead of tricky bind
   method.
2. Version-flexible train script path (train_script → train_script_paths)
3. User-namespaced scratch dir (/tmp/jax → /tmp/<user>/jax)
   /tmp/{user-id}/jax/TRAINING_LOGS/

* [JAX] Address PR #300 review comments

* [JAX] Add node dmesg error scan around training

Gated on a new training.verify_dmesg config flag (default true; disable on
clusters without passwordless sudo for dmesg). Best-effort: an infra failure of
the scan itself is logged and swallowed so it never masks the training result.
verify_lib is imported lazily so the training lib stays importable without the
broader utils stack. Wired into _common.training_run after parse_results.

Signed-off-by: Saravanan Solaiyappan <saravanan.solaiyappan@amd.com>
pull_image was the only one of twelve docker invocations in DockerRuntime
that hardcoded `sudo` instead of orchestrator.sudo_prefix(). Two
consequences:

  - Credential mismatch. registry_login() does honor sudo_prefix(), so on
    a cluster without passwordless sudo it authenticates as the SSH user
    while the pull runs as root, which reads an empty
    /root/.docker/config.json. A private image then fails with "pull
    access denied" despite a successful login.
  - Lost -n. Bare `sudo` drops the non-interactive flag, so a password
    prompt blocks until the 600s timeout instead of failing fast.

This matters now that setup_containers() pulls unconditionally whenever
check_image_exists() reports the image missing, which puts the pull on
the startup path of every container-orchestrator suite, including the
RVS and AGFHC health suites.

Also fixes the two unit tests failing on this branch:

  - test_pulls_image_when_missing_before_run asserted a bare
    "docker run" prefix against a MagicMock orchestrator, whose
    sudo_prefix() returned a Mock rather than a string. Stub it
    explicitly and assert the rendered prefix.
  - test_run_test_omits_log_file_when_not_set did not patch
    _validate_json_config, so the new pre-flight check called
    sys.exit on the mock config path. Patch it, matching the two
    sibling tests.

Adds test_pull_uses_sudo_prefix_not_hardcoded_sudo to pin the fix for
both the sudo and no-sudo cluster shapes.

Signed-off-by: Atul Nair <Atul.Nair@amd.com>
* Document the unified vLLM suite

The vLLM docs described a schema the suite no longer has: a `config` +
`benchmark_params.<model>` layout, a `vllm_single` suite name, and a
config path under inference/vllm_single/<variant>/. None of those exist
on dev/dtni, and the reference page named a config file that ships
nowhere in the repo.

- Add reference/configuration-files/vllm.rst covering the current schema:
  every config block, the four distinct "backend" settings, the container
  and Docker keys, sweep cell keys and server reuse, all six threshold
  kinds and both coverage axes, and all four metric namespaces
  (37 client / 5 gpu / 4 prom / accuracy)
- Add how-to/run-vllm-benchmarks.rst as the task-shaped entry point,
  including the mp-vs-ray multinode split
- Delete vllm_singlenode_mi355x.rst; it documented the removed schema and
  also failed to build (unknown target name at line 113)
- Correct the stale suite name and config path in run-cvs-tests.rst, with
  the test listing taken from live `cvs list vllm` output

Ray is documented as opt-in rather than required: mp is the default
multinode backend, and ray's effect is to relax the pipeline-parallelism
requirement, not to enable multinode.

* docs: correct vLLM threshold coverage semantics

Threshold files are validated on cell coverage only. The vLLM loader passes an
empty gated set, so no metric is mandatory in a cell; a metric is asserted only
where its cell carries a spec for it. Gated marks the designated pass/fail
criteria that seed the report gate matrix, not a required spec.

Point the install guide at the shipped vLLM configs and their current schema
keys, replacing a config file that no longer exists.
…aining Suites- (#307)

*Added Megatron test suite with Orch refactoring, untouched the legacy Megatron files.

This PR introduces a complete Megatron-LM pre-training validation suite
for MI325X GPUs, covering single-node and distributed (multi-node) runs. 
The suite drives Megatron-LM training jobs inside a container, parses training logs, 
and gates results against configurable per-combo performance and correctness 
thresholds with a linked HTML report.

---------

Signed-off-by: sukesh kalla <skalla@amd.com>
… Training Suites- (#306)

*Added Torchtitan test suite with Orch refactoring, untouched the legacy Torchtitan files.

This PR introduces a complete Torchtitan pre-training validation suite
for MI355X GPUs, covering single-node and distributed (multi-node) runs. 
The suite drives Torchtitan training jobs inside a container, parses training logs, 
and gates results against configurable per-combo performance and correctness 
thresholds with a linked HTML report.

---------

Signed-off-by: Rajesh Thummala <rthummal@amd.com>

@hnimra-amd hnimra-amd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Config review notes on threshold_file and model_num_params.

"llama-70b":
{
"backend": "sglang",
"threshold_file": "cvs/input/config_file/inference/sglang/mi30x_sglang_deepseek_r1_0528_threshold.json",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This points to the DeepSeek R1 threshold file, but the disaggregated config uses mi30x_sglang_llama_70b_threshold.json. Single and distributed would run Llama 70B with DeepSeek thresholds, and those files have different throughput values. Should this be the Llama threshold file instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated.

"llama-70b":
{
"backend": "sglang",
"threshold_file": "cvs/input/config_file/inference/sglang/mi30x_sglang_deepseek_r1_0528_threshold.json",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same as single: this still points to the DeepSeek R1 threshold file while disaggregated uses the Llama file. Single/distributed would pick up DeepSeek thresholds, which differ from the Llama values. Can we align this with mi30x_sglang_llama_70b_threshold.json?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated.

"data_set_name": "random",
"num_prompts": "25",
"random_range_ratio": "0.5",
"model_num_params": "671000000000",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks copied from the DeepSeek configs (671B). For Llama 3.1 70B it should be closer to 70000000000. It only affects MFU calculation, not the core perf gates, but worth fixing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated.

"data_set_name": "random",
"num_prompts": "100",
"random_range_ratio": "0.5",
"model_num_params": "671000000000",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same DeepSeek copy-paste: 671000000000 is 671B params. For Llama 3.1 70B use something like 70000000000. Lower priority since this only affects MFU, not throughput/latency gates.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated.

"bench_serv_generated_shared_prefix":
{
"backend": "sglang",
"gsp_num_groups": "1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here: 671000000000 looks like it came from the DeepSeek configs. For Llama 3.1 70B this should be closer to 70000000000. Only affects MFU, not the main perf thresholds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated.

@atnair-amd

Copy link
Copy Markdown
Collaborator

Code review

Found 2 issues:

  1. New library code has no unit tests (AGENTS.md, this repo's root CLAUDE.md doc, says under Unit Testing Standards: "All library functions and classes must have corresponding unit tests in the tested module's own unittests/ dir" and under Do Not: "Never add library functions without unit tests"). This PR adds substantial new logic to sglang_common.py (parse_inference_bench_results, verify_inference_results, poll_for_inference_completion, metric_threshold_violation, etc.) plus two new files, benchmark_metric_registry.py and perf_metric_table.py, with no corresponding tests in cvs/lib/inference/unittests/ or cvs/lib/report/unittests/ — both of which are active, populated test directories for these packages already (e.g. test_vllm_parsing.py, test_atom_parsing.py), so this isn't a case of the module having no prior test convention.

'''Shared helpers for SGLang single-node and disaggregated inference libs.'''
from __future__ import annotations
import base64
import json
import re
import shlex
import time
from typing import Any, Callable, Mapping, Optional
from cvs.lib import globals
from cvs.lib.utils.model_query_lib import LmEvalBenchmark, OpenAIProbe
from cvs.lib.utils_lib import fail_test
from cvs.lib.utils.verdict import ThresholdViolation, _check_one, evaluate_all
log = globals.log
DEFAULT_GPU_MEM_THRESHOLD_MB = 5000
AMD_SMI_METRIC_CMD = "sudo amd-smi metric --json"
_SERVER_READY_RE = re.compile(
r"server is fired up and ready to roll",
re.I,
)
def textwrap_for_yml(msg_string: str) -> str:
return '\n'.join([m.lstrip() for m in msg_string.split('\n')])

'''Shared storage for benchmark metric pass/fail rows used by pytest-html hooks.
Metric rows must live in a normal import path (``cvs.lib...``), not only on the
sglang ``conftest`` module. Pytest loads directory ``conftest.py`` files under a
different module name than ``cvs.tests.inference.sglang.conftest``, so a dict on
the conftest module is invisible to hooks while ``record_benchmark_metric_rows()``
writes to the package import path.
'''
from __future__ import annotations
import html
import json
import re
from pathlib import Path
from typing import Any
from _pytest.stash import StashKey
from cvs.lib.report.render.perf_metric_table import (
dedupe_metric_rows,
is_benchmark_metrics_extra,
render_benchmark_metrics_html,
)
BENCHMARK_METRIC_ROWS_KEY = StashKey[list[dict[str, Any]]]()
BENCHMARK_METRIC_ROWS_USER_PROPERTY = 'cvs_benchmark_metric_rows'
DEFAULT_BENCHMARK_TEST_NAME = 'test_run_performance_benchmark_test'
_ROWS_BY_NODEID: dict[str, list[dict[str, Any]]] = {}

  1. Global HTML-report CSS/JS regression outside SGLang's scope. HtmlReportManager.inject_style_overrides is invoked unconditionally for every suite via the root cvs/conftest.py's pytest_html_results_summary hook, not just SGLang. This PR removes .collapse { display: none !important; } and .collapsible td:not(.col-links) { cursor: default !important; } from REPORT_STYLE_OVERRIDES and replaces them only with rules scoped to the new cvs-benchmark-collapsible class used by SGLang's benchmark-metric rows. Nothing re-establishes the suppression for the general case, so pytest-html's built-in "Show all details / Hide all details" control bar and per-row toggle cursor will reappear in every other suite's report (vLLM, ATOM, RCCL, training, etc.) as an apparently unintended side effect of an SGLang-only feature. The docstring on inject_style_overrides ("Inject CSS to hide show/hide details UI elements") is also now stale, since it no longer does that for non-benchmark rows and doesn't mention the new REPORT_BENCHMARK_METRICS_SCRIPT JS injection.

inf_res_dict=inf_res_dict,
lifecycle_report=store.get("lifecycle_report") or {},
report_manager=self,
pytest_config=session.config,
)
@staticmethod
def inject_style_overrides(prefix):
"""Inject CSS to hide show/hide details UI elements."""
prefix.extend([REPORT_STYLE_OVERRIDES, REPORT_BENCHMARK_METRICS_SCRIPT])

@amd-droy
amd-droy force-pushed the dr_sgl_sub branch 2 times, most recently from 01b879d to a8ab4db Compare August 12, 2026 22:48
@amd-droy

Copy link
Copy Markdown
Contributor Author

Code review

Found 2 issues:

  1. New library code has no unit tests (AGENTS.md, this repo's root CLAUDE.md doc, says under Unit Testing Standards: "All library functions and classes must have corresponding unit tests in the tested module's own unittests/ dir" and under Do Not: "Never add library functions without unit tests"). This PR adds substantial new logic to sglang_common.py (parse_inference_bench_results, verify_inference_results, poll_for_inference_completion, metric_threshold_violation, etc.) plus two new files, benchmark_metric_registry.py and perf_metric_table.py, with no corresponding tests in cvs/lib/inference/unittests/ or cvs/lib/report/unittests/ — both of which are active, populated test directories for these packages already (e.g. test_vllm_parsing.py, test_atom_parsing.py), so this isn't a case of the module having no prior test convention.

'''Shared helpers for SGLang single-node and disaggregated inference libs.'''
from __future__ import annotations
import base64
import json
import re
import shlex
import time
from typing import Any, Callable, Mapping, Optional
from cvs.lib import globals
from cvs.lib.utils.model_query_lib import LmEvalBenchmark, OpenAIProbe
from cvs.lib.utils_lib import fail_test
from cvs.lib.utils.verdict import ThresholdViolation, _check_one, evaluate_all
log = globals.log
DEFAULT_GPU_MEM_THRESHOLD_MB = 5000
AMD_SMI_METRIC_CMD = "sudo amd-smi metric --json"
_SERVER_READY_RE = re.compile(
r"server is fired up and ready to roll",
re.I,
)
def textwrap_for_yml(msg_string: str) -> str:
return '\n'.join([m.lstrip() for m in msg_string.split('\n')])

'''Shared storage for benchmark metric pass/fail rows used by pytest-html hooks.
Metric rows must live in a normal import path (``cvs.lib...``), not only on the
sglang ``conftest`` module. Pytest loads directory ``conftest.py`` files under a
different module name than ``cvs.tests.inference.sglang.conftest``, so a dict on
the conftest module is invisible to hooks while ``record_benchmark_metric_rows()``
writes to the package import path.
'''
from __future__ import annotations
import html
import json
import re
from pathlib import Path
from typing import Any
from _pytest.stash import StashKey
from cvs.lib.report.render.perf_metric_table import (
dedupe_metric_rows,
is_benchmark_metrics_extra,
render_benchmark_metrics_html,
)
BENCHMARK_METRIC_ROWS_KEY = StashKey[list[dict[str, Any]]]()
BENCHMARK_METRIC_ROWS_USER_PROPERTY = 'cvs_benchmark_metric_rows'
DEFAULT_BENCHMARK_TEST_NAME = 'test_run_performance_benchmark_test'
_ROWS_BY_NODEID: dict[str, list[dict[str, Any]]] = {}

  1. Global HTML-report CSS/JS regression outside SGLang's scope. HtmlReportManager.inject_style_overrides is invoked unconditionally for every suite via the root cvs/conftest.py's pytest_html_results_summary hook, not just SGLang. This PR removes .collapse { display: none !important; } and .collapsible td:not(.col-links) { cursor: default !important; } from REPORT_STYLE_OVERRIDES and replaces them only with rules scoped to the new cvs-benchmark-collapsible class used by SGLang's benchmark-metric rows. Nothing re-establishes the suppression for the general case, so pytest-html's built-in "Show all details / Hide all details" control bar and per-row toggle cursor will reappear in every other suite's report (vLLM, ATOM, RCCL, training, etc.) as an apparently unintended side effect of an SGLang-only feature. The docstring on inject_style_overrides ("Inject CSS to hide show/hide details UI elements") is also now stale, since it no longer does that for non-benchmark rows and doesn't mention the new REPORT_BENCHMARK_METRICS_SCRIPT JS injection.

inf_res_dict=inf_res_dict,
lifecycle_report=store.get("lifecycle_report") or {},
report_manager=self,
pytest_config=session.config,
)
@staticmethod
def inject_style_overrides(prefix):
"""Inject CSS to hide show/hide details UI elements."""
prefix.extend([REPORT_STYLE_OVERRIDES, REPORT_BENCHMARK_METRICS_SCRIPT])

  1. 3 unit tests added.
  2. Handled. Now, vLLM, ATOM, RCCL, training, and the rest of the reports should look the same as before this PR.

@amd-droy
amd-droy requested a review from hnimra-amd August 12, 2026 22:54
@hnimra-amd

Copy link
Copy Markdown

LGTM

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.

7 participants