Skip to content

Hnimrama/interactivity graph - #276

Open
hnimra-amd wants to merge 110 commits into
dev/dtnifrom
hnimrama/interactivity_graph
Open

Hnimrama/interactivity graph#276
hnimra-amd wants to merge 110 commits into
dev/dtnifrom
hnimrama/interactivity_graph

Conversation

@hnimra-amd

@hnimra-amd hnimra-amd commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

InferenceX interactivity viewer and run provenance

Base branch: dev/dtni
Head branch: hnimrama/interactivity_graph

Summary

Adds a SemiAnalysis-style interactivity chart to the inference interactive HTML viewer and improves run deck reproducibility metadata (git ref, container image, launch commands). Render-only reporting changes.

Interactivity chart (interactive viewer)

  • Token Throughput per GPU vs. Interactivity (one line per ISL/OSL shape; points by concurrency).
  • Interactivity = 1000 / mean TPOT (tok/s/user), aligned with InferenceX.
  • Y-axis: total token throughput per GPU.
  • Hover detail card (click point to pin): date, image, throughput split, TP/GPUs, concurrency, precision, CI/upstream link.
  • Zoom/pan: scroll zoom, shift+drag pan, drag box-zoom, reset / double-click.
  • Layout: Filters → Overview → Interactivity → other sweep panels.

Run deck & provenance

  • Git ref, container image display, image digest at launch, IX launch command provenance.
  • Run deck surfaces reproducibility fields; bundle copy skip when artifact already present.

Test plan

  • pytest cvs/lib/report/unittests/test_viewer_scaffold.py cvs/lib/report/unittests/test_provenance.py cvs/lib/report/unittests/test_inference_report.py
  • pytest cvs/lib/inference/unittests/test_inferencex_atom_launch.py cvs/core/orchestrators/unittests/test_container.py
  • Open *_viewer.html from a sweep report: interactivity chart, pin card, links, filters.

Sample reports

atnair-amd and others added 30 commits July 27, 2026 09:47
* 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>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@amd.com>
Signed-off-by: amd-droy <droy@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.
Plot interactivity (1/TPOT) vs per-GPU throughput in the interactive sweep explorer with log-scale Y, concurrency labels, and TPOT selector.
Inference suite run-deck files are written directly into log_dir; add_html_to_report now registers them without shutil.copy2 on the same path.
Add branch/dirty git ref formatting, image digest display helpers, and a session store for runtime provenance fields.
Resolve running image ID and repo digest on the head host after setup_containers and bind it into report session provenance.
Derive launch summary and representative server/benchmark commands from the variant config for suite report rendering.
Merge runtime image digest and launch commands into provenance, highlight git ref/image/launch in the run card, and add a launch commands panel to the HTML report.
@hnimra-amd
hnimra-amd force-pushed the hnimrama/interactivity_graph branch from ae637f0 to 83ec2d9 Compare July 27, 2026 21:09
@solaiys

solaiys commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Review (head 3af5952f, base dev/dtni)

Regular review (blocking issues only) plus the requested analysis of the CVS core-file changes.

Blocking issues

  • 🔴 str | None annotation breaks import on Python 3.9 (a declared-supported version)cvs/core/orchestrators/container.py:549

    container.py has no from __future__ import annotations, so the new def capture_image_provenance(self, container_name: str | None = None) annotation is evaluated eagerly at import time. PEP 604 X | None unions only work at runtime on Python 3.10+, but setup.py declares python_requires=">=3.9"; on 3.9 this raises TypeError while importing container.py. Since ContainerOrchestrator is core and imported by every container use case (training and inference), this breaks all of them on 3.9 — not just the new inference feature.

    Fix: add from __future__ import annotations at the top of container.py (consistent with provenance.py, which already does this), or use Optional[str] from typing. (If 3.9 is no longer actually supported, bump python_requires instead — separate decision.)

CVS core-file changes: inference-specific or all use cases?

  • cvs/core/orchestrators/container.py — functionally inference-specific, but with repo-wide side effects.

    • The new capture_image_provenance() (+ _ssh_command_output helper) is called only from cvs/lib/inference/utils/inference_suite_lifecycle.py (test_launch_container), wrapped in try/except so a failure only warns. As a feature it serves the inference run-deck provenance use case exclusively; it is additive and doesn't alter the existing container lifecycle.
    • However, two things now apply to all container use cases regardless of inference:
      1. The module-level from cvs.lib.report.provenance import format_image_display loads whenever ContainerOrchestrator is imported. This is not a circular import (provenance.py imports only subprocess/typing), so it's safe — but it introduces a cvs.core → cvs.lib.report layering dependency (a core orchestrator depending on the reporting layer). Non-blocking; consider a lazy import inside the method.
      2. The str | None annotation above — the blocking issue, which affects all container use cases on 3.9.
  • cvs/lib/report_plugins.py — applies to ALL use cases (shared HtmlReportManager), and is a net improvement.

    • add_html_to_report now .resolve()s paths and skips the copy when source == dest (the "bundle copy skip when artifact already present") — safe and beneficial for every report-producing suite.
    • generate_reports_section renames the local html string var to reports_html, fixing a latent bug: the old code shadowed the imported html module, so html.escape(...) would raise AttributeError whenever any suite registered a custom test report. General bug fix, not inference-specific.

So the report_plugins.py change is genuinely cross-cutting (and fixes a real bug for all suites); the container.py method is inference-only in purpose, but its import and the str | None annotation land on every container use case.

Reviewed 1 PR: 1 with a blocking issue.

Comment thread cvs/core/orchestrators/container.py Outdated
Comment thread cvs/lib/inference/inferencex_atom/inferencex_atom_launch.py Outdated
Comment thread cvs/lib/report/viewer/interactive.html
Comment thread cvs/lib/report/viewer/interactive.html Outdated
Comment thread cvs/lib/report/viewer/interactive.html Outdated
@hnimra-amd
hnimra-amd requested a review from amd-droy July 29, 2026 20:01

@amd-droy amd-droy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks good to me. Thanks Humna.

Comment on lines +538 to +589
@staticmethod
def _ssh_command_output(result) -> str:
if not isinstance(result, dict):
return ""
for value in result.values():
if isinstance(value, dict):
if value.get("exit_code") not in (0, None):
continue
return str(value.get("output") or "").strip()
if value:
return str(value).strip()
return ""

def capture_image_provenance(self, container_name: str | None = None) -> dict[str, str]:
"""Resolve the running container image tag, ID, and digest on the head host."""
name = container_name or self.container_id
if not name:
return {}

image_tag = str(self.container_config.get("image") or "")
head = self.runtime.orchestrator.head
quoted = shlex.quote(name)

image_id = self._ssh_command_output(
head.exec(
f"sudo docker inspect --format '{{{{.Image}}}}' {quoted}",
timeout=30,
detailed=True,
)
)
image_digest = ""
if image_id:
image_digest = self._ssh_command_output(
head.exec(
f"sudo docker image inspect --format '{{{{index .RepoDigests 0}}}}' {shlex.quote(image_id)}",
timeout=30,
detailed=True,
)
)

image_display = format_image_display(
image_tag=image_tag,
image_digest=image_digest,
image_id=image_id,
)
return {
"image_tag": image_tag,
"image_id": image_id,
"image_digest": image_digest,
"image_display": image_display,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is this really needed? couldnt you do orch.exec commands for this?

Comment thread cvs/core/image_display.py
Comment on lines +1 to +17
'''Container image tag / digest display strings (shared by orchestration and reports).'''

from __future__ import annotations


def format_image_display(*, image_tag: str = "", image_digest: str = "", image_id: str = "") -> str:
digest = image_digest or image_id
if digest:
short = digest
if "@" in short:
short = short.split("@", 1)[1]
if short.startswith("sha256:") and len(short) > 19:
short = f"{short[:19]}\u2026"
if image_tag:
return f"{image_tag} @ {short}"
return short
return image_tag or "\u2014"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should not be bundled in orch

Comment on lines +51 to +70
def server_command(variant: Any) -> str:
example_cell, isl, osl, conc = _first_sweep_coords(variant)
del example_cell
job = _example_job(variant, isl=isl, osl=osl, concurrency=conc)
if job.driver == "atom":
argv = job._atom_server_argv()
else:
argv = job._server_argv()
return " ".join(shlex.quote(str(arg)) for arg in argv)


def bench_command(variant: Any) -> str:
example_cell, isl, osl, conc = _first_sweep_coords(variant)
del example_cell
job = _example_job(variant, isl=isl, osl=osl, concurrency=conc)
if job.driver == "atom":
argv = job._atom_client_argv()
else:
argv = job._vllm_client_argv()
return " ".join(shlex.quote(str(arg)) for arg in argv)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplication. differs by one fn

Comment on lines +74 to +81
example_cell, _isl, _osl, _conc = _first_sweep_coords(variant)
out = {
"launch_summary": launch_summary(variant),
"launch_server_cmd": server_command(variant),
"launch_bench_cmd": bench_command(variant),
}
if example_cell:
out["launch_example_cell"] = example_cell

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

confused why _first_sweep_coords is called here just for example_cell. i think build_launch_provenance, server_command, bench_command could be refactored to be cleaner.

Unless there is a need for _first_sweep_coords to be this way (i could've missed something)

Comment on lines +28 to +64
def git_commit_short() -> str:
return _git_run(["rev-parse", "--short", "HEAD"])


def git_branch_name() -> str:
branch = _git_run(["rev-parse", "--abbrev-ref", "HEAD"])
return "" if branch == "HEAD" else branch


def git_worktree_dirty() -> bool:
try:
result = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True,
text=True,
check=True,
timeout=2,
)
return bool(result.stdout.strip())
except (OSError, subprocess.SubprocessError):
return False


def format_git_ref(*, commit: str = "", branch: str = "", dirty: bool = False) -> str:
if not commit and not branch:
return ""
parts: list[str] = []
if commit:
parts.append(commit)
if branch:
parts.append(branch)
ref = " @ ".join(parts) if len(parts) == 2 else parts[0]
if dirty:
ref = f"{ref} (dirty)"
return ref


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why is this needed for the interactivity graph?

@atnair-amd atnair-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Second-pass review at c7cfc65c. Each finding was independently re-verified by attempting to refute it; two initial findings were dropped when the refutation succeeded, and I've noted those below so the negative results are on record too.

The earlier review's two points (py3.9 str | None, core→report layering) are both resolved at this commit — extracting cvs/core/image_display.py is a cleaner fix than the lazy import that was suggested.

Two correctness bugs (inline on interactive.html and inferencex_atom_launch.py), and make ut is currently red with the tests that would have guarded both of them not running at all.

Checked and found not to be problems:

  • Locally-built images with no RepoDigests do not render docker's error text as the digest — docker exits 1 there, and the existing exit_code guard catches it. Verified against a live daemon.
  • _example_job(orch=SimpleNamespace()) is safe, and is the right call — no argv builder touches orch (bytecode-checked), and SimpleNamespace raises loudly rather than silently stringifying a stub.

Also worth noting as a genuine win: the report_plugins.py rename of the local html var to reports_html fixes a real latent bug — the old code shadowed the html module, so html.escape(...) would AttributeError for any suite registering a custom test report.

return tput / tp;
}

function outputPerGpuThroughput(cell) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Per-GPU throughput ignores pipeline parallelism.

outputPerGpuThroughput() reads client.output_tput_per_gpu, which only ATOM emits (inferencex_atom_parsing.py:73). vLLM never sets it — I confirmed by executing to_client_metrics across a tp/pp grid rather than grepping — so for vLLM the /tp fallback always fires, overstating by exactly a factor of PP.

On the shipped mi300x_vllm_llama31-70b_fp8_distributed.json (TP=8 at :59, PP=2 at :60), total 4000 / output 2000 tok/s:

Row Shown Correct
Total GPUs 16 16 ✓
Token Throughput per GPU 250 250 ✓
Output Token Throughput per GPU 250 125
Input Token Throughput per GPU 0 125

The same tooltip prints Total GPUs: 16 and Pipeline Parallelism: 2 (:741), and :725 already computes tp * pp — so PP is available right there; these rows just don't use it.

Note the chart's Y value is not affected: perGpuThroughput() (:895-897) hits the PP-aware direct metric first, so the plot is correct and only the tooltip is wrong. That makes this easy to ship unnoticed.

Suggested fix, in both this function and inputPerGpuThroughput:

const gpus = tp * (ppFromCellId(cell.cell_id) || 1);
return tput / gpus;

return tput / tp;
}

function inputPerGpuThroughput(cell, totalPg, outputPg) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Follow-on from the outputPerGpuThroughput comment above: this mixes a PP-aware value (totalPg, from the direct client.per_gpu_throughput metric, computed as total/(tp*pp)) with a PP-blind one (outputPg, from the /tp fallback).

For the shipped TP=8/PP=2 config that yields 250 - 250 = 0. With an OSL-heavy shape (total 2200 / output 2000) it renders −112.5 — a negative throughput on screen.

Fixing outputPerGpuThroughput to divide by tp * pp fixes this row too, since the subtraction then operates on two consistent quantities.

match = _CELL_RE.search(cells[0])
if match:
return cells[0], match.group("isl"), match.group("osl"), match.group("conc")
return "", "1024", "1024", "128"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 This fallback fabricates a sweep point and it reaches the published report.

The ("", "1024", "1024", "128") tuple is undocumented — no constant name, no comment — and to a run-deck reader is indistinguishable from a real swept point.

The miss branch is reachable from a config that loads and runs correctly. inferencex_atom_config_loader.py:86 builds the cell key by interpolating isl / osl / tensor_parallelism, all declared as bare str with no digit constraint, while _CELL_RE here demands \d+ in all three positions — the regex is stricter than the schema.

Concretely: params.tensor_parallelism: "8x2" is a plausible label for a 2-node deployment, and on driver: atom that field is labeling only (real TP comes from roles.server.atom_args). Such a config loads under enforce_thresholds: true, runs ISL=7168 @ conc 128 — and the deck publishes ISL=1024 @ conc 128, with a --result-dir .../isl1024_osl1024_conc128 naming a directory that never existed on the node. Zero warnings, no log line.

Two related notes:

  • sweep.runs has no min_length (inferencing_config_loader.py:141), so "runs": [] loads fine and still emits all three launch keys.
  • The NNODES= alternative in this regex is dead — no generator anywhere emits it. The only two are ATOM :86 (never PP) and vllm_config_loader.py:228-231 (PP only when pp>1).

Segment order is also fragile — PP=2,TP=8 (swapped), DP=, and EP= all miss.

The durable fix is to stop re-deriving coordinates from a stringified key and read sweep.runs directly, the same source cell_key uses.

return " ".join(shlex.quote(str(arg)) for arg in argv)


def build_launch_provenance(variant: Any) -> dict[str, str]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is why omitting launch_example_cell doesn't contain the fabricated coordinates: example_cell is computed here but discarded, and server_command / bench_command each re-call _first_sweep_coords independently, using the fallback values.

_build_panels (inference_payload.py:199-206) gates the whole Launch panel on launch_server_cmd, which is always present — so the panel always renders. With empty cells the literal output is:

{'launch_bench_cmd': 'vllm bench serve ... --random-input-len 1024 '
                     '--random-output-len 1024 --max-concurrency 128 ...',
 'launch_summary': 'vllm · TP=8 · max_model_len=8192'}
launch_example_cell present? False

Suppressing the label but keeping the commands is the worst of both — the reader gets a concrete-looking command with nothing marking it synthetic.

Suggest omitting launch_server_cmd and launch_bench_cmd entirely (not just launch_example_cell) when no first cell resolves.

For what it's worth the run-card Launch row is fine — provenance.py:125-126 builds it from variant.params only, so it isn't fabricated. The issue is confined to the panel's command blocks.

container={"launch": True, "image": "x"},
)

def test_capture_image_provenance_resolves_digest_on_head(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 This test errors out and never exercises capture_image_provenance.

It was appended inside class TestResolveContainerLifetime (opens at :299), but self._make() is defined on TestContainerOrchestrator (:77). There's no inheritance, mixin, or setUp linking them:

AttributeError: 'TestResolveContainerLifetime' object has no attribute '_make'

It fails on line 358 — the first line of the body — so container.py:551 gets zero coverage, and this is currently the only unit coverage of capture_image_provenance.

It is collected by run_all_unittests.py (813 tests discovered, this among them), so make ut is red on this branch.

The good news: this is purely class placement, not a logic problem. I grafted setUp/_make onto a subclass and ran the body unmodified — it passes, and mutating format_image_display or _ssh_command_output makes it fail, so the assertions genuinely exercise the two-step docker-inspect chain. Just move it into TestContainerOrchestrator; the body needs no changes.

def test_launch_summary_includes_driver_tp_and_max_model_len():
root = Path("cvs")
variant = load_variant(
root / "input/config_file/inference/inferencex_atom/mi300x_inferencex-atom-single_deepseek-r1_fp8_smoke_config.json",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 Both tests in this file fail, and the gate can't see them.

The path is input/config_file/inference/inferencex_atom/, but the only directory on disk is inferencex_atom_single. Both tests raise:

FileNotFoundError: variant config not found:
  cvs/input/config_file/inference/inferencex_atom/mi300x_inferencex-atom-single_deepseek-r1_fp8_smoke_config.json

Worth flagging: the referenced filename exists on no branch. There's a rename commit (ae2b6474) going inferencex_atom_single/inferencex_atom/, but it isn't an ancestor of this branch or dev/dtni — it lives on the unmerged multinode branch, and that branch's directory has no *smoke* file. Looks like the new dir name from one branch got combined with the old filename from another.

Separately, these are bare pytest-style functions in a unittests/ directory, which run_all_unittests.py:12 scans with unittest.TestLoader.discover. That collects 0 tests from this module — so make ut stays green while this file has zero effective coverage. That's precisely why the regex/schema mismatch in _first_sweep_coords shipped.

Two fixes needed: inferencex_atominferencex_atom_single, and convert to unittest.TestCase so the gate actually runs them. While there, the sibling test_inferencex_atom_config_loader.py anchors its root as Path(__file__).resolve().parents[3] — this file's cwd-relative Path("cvs") breaks when pytest runs from anywhere but the repo root.

return True

@staticmethod
def _ssh_command_output(result) -> str:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor / low priority.

pssh._process_output (pssh.py:190-197) appends stdout and stderr into the same output string, with exit_code set independently. So a docker command that exits 0 while warning — e.g. a malformed root ~/.docker/config.json — yields a multi-line image_id. I reproduced this:

rc=0
out=[WARNING: Error loading config file: /tmp/dkrcfg/config.json: invalid character 'N' ...
sha256:90630909f007ad4b58dd7b8271911f7b184aa97368ae35cd90eecce11bd98520]

That multi-line value is then shlex.quoted into the second docker call, which fails — so the digest is silently lost and it falls back to the image id.

Impact is limited: format_image_display truncates sha256:-prefixed values over 19 chars, which decapitates the warning, so the rendered row looks identical to the clean case. The residual is that a short or non-sha256: digest would leak the warning verbatim.

If you want to harden it, take the last non-empty line — docker writes the warning before stdout, so taking the first would be wrong.

Not a blocker: this matches existing repo practice (docker.py:210-218 does the same read-output/strip/check-exit_code without line-splitting), and the call is already wrapped in try/except.

title: gpu + ' (' + driver + ')',
rows: [
['Date', reportDateLabel()],
['Image', prov.image_display || rc.Image || '—'],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor. This row is permanently for vLLM.

bind_runtime_provenance has exactly one call site in the tree (inference_suite_lifecycle.py:98), and vLLM defines its own test_launch_container (vllm.py:131) rather than importing the shared one — the two function bodies are identical apart from the 6 new provenance lines, so this reads as drift rather than deliberate divergence. The only fallback (inference.py:198) reads run_card.atom_image_pin, and the vLLM VariantConfig has no run_card field at all (it inherits extra="forbid", so it can't even be hand-added to the JSON).

To be clear this is not a regression — vLLM never had an Image row; on dev/dtni provenance_run_card_rows had no image branch and cvs/core/image_display.py didn't exist. vLLM strictly gains a row here (Git commitGit ref). It's an un-extended feature.

The one new wrinkle is this line: since interactive_viewer defaults to True and vLLM doesn't override it, a large-enough vLLM sweep now renders an Image row that is always an em-dash. An always-empty field is arguably worse cosmetically than no field.

Heads-up if you close the gap: falling back to variant_config.container.image won't work as-is — both shipped vLLM configs have "image": "<changeme>" at :20. It'd need the runtime-resolved tag.

@solaiys
solaiys force-pushed the dev/dtni branch 2 times, most recently from 2821e97 to 146c077 Compare August 13, 2026 07:41
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.

5 participants