Hnimrama/interactivity graph - #276
Conversation
…n, lib refactor map
* 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.
ae637f0 to
83ec2d9
Compare
Review (head
|
amd-droy
left a comment
There was a problem hiding this comment.
looks good to me. Thanks Humna.
| @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, | ||
| } | ||
|
|
There was a problem hiding this comment.
is this really needed? couldnt you do orch.exec commands for this?
| '''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" |
There was a problem hiding this comment.
should not be bundled in orch
| 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) |
There was a problem hiding this comment.
duplication. differs by one fn
| 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 |
There was a problem hiding this comment.
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)
| 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 | ||
|
|
||
|
|
There was a problem hiding this comment.
why is this needed for the interactivity graph?
atnair-amd
left a comment
There was a problem hiding this comment.
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
RepoDigestsdo not render docker's error text as the digest — docker exits 1 there, and the existingexit_codeguard catches it. Verified against a live daemon. _example_job(orch=SimpleNamespace())is safe, and is the right call — no argv builder touchesorch(bytecode-checked), andSimpleNamespaceraises 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) { |
There was a problem hiding this comment.
🔴 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) { |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
🔴 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.runshas nomin_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) andvllm_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]: |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
🟠 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", |
There was a problem hiding this comment.
🟠 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_atom → inferencex_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: |
There was a problem hiding this comment.
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 || '—'], |
There was a problem hiding this comment.
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 commit → Git 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.
2821e97 to
146c077
Compare
InferenceX interactivity viewer and run provenance
Base branch:
dev/dtniHead branch:
hnimrama/interactivity_graphSummary
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)
1000 / mean TPOT(tok/s/user), aligned with InferenceX.Run deck & provenance
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.pypytest cvs/lib/inference/unittests/test_inferencex_atom_launch.py cvs/core/orchestrators/unittests/test_container.py*_viewer.htmlfrom a sweep report: interactivity chart, pin card, links, filters.Sample reports