Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2f3b7ae
docs(dtni): add vllm_single PoC plan and DTNI suite developer guide
atnair-amd Jun 9, 2026
54399d2
docs(dtni): expand dev guide with Background, before/after, config/th…
atnair-amd Jun 9, 2026
d90a78c
docs(dtni): rewrite dev guide with lifecycle flowcharts, test skeleto…
atnair-amd Jun 9, 2026
7aad78c
fix(cli): exclude conftest.py and _-prefixed files from cvs list [AIM…
atnair-amd Jun 15, 2026
114be01
vLLM single node refactor (#223)
atnair-amd Jun 16, 2026
857bff5
feat(dtni): move vLLM bench client to stock vllm bench serve (Spec 0)…
atnair-amd Jun 17, 2026
a22ef31
Merge pull request #225 from ROCm/hnimrama/inferencemax-uplift
hnimra-amd Jun 18, 2026
e06c1c7
Revert "Merge pull request #225 from ROCm/hnimrama/inferencemax-uplif…
hnimra-amd Jun 18, 2026
de9cbe9
Restructure shared/inference libs + sweep selector + artifact-based v…
atnair-amd Jun 24, 2026
57e1b13
SGLang framework update (#234)
amd-droy Jun 15, 2026
20acbbb
xdit
amd-droy Jun 22, 2026
7e4ffa0
Hnimrama/ix atom (#238) AIMVT-244/Add ATOM framework and inference …
hnimra-amd Jun 29, 2026
76414ad
docs(plans): add inference suite architecture changes presentation doc
atnair-amd Jul 1, 2026
7b5cf5e
Revert "docs(plans): add inference suite architecture changes present…
atnair-amd Jul 1, 2026
4c6c933
Hnimrama/Added ix atom reporting run deck module (#244)
hnimra-amd Jul 8, 2026
05e47f6
refactor(inference): move shared suite helpers under inference/utils …
hnimra-amd Jul 14, 2026
a7733d0
[CVS] GPU metrics polling integration for inference validation suites…
atnair-amd Jul 15, 2026
9fd33c9
Integrating nodesmoke tier 1 tests using Primus cli (#250)
urtiwari Jul 16, 2026
c2c0c7b
feat(vllm): unify vllm_single + vllm_distributed into one topology-pa…
atnair-amd Jul 17, 2026
8fb7df5
feat(vllm): wire unified vLLM suite into inference report engine (#261)
atnair-amd Jul 20, 2026
d0f2044
feat(vllm): GPU metrics polling + fix per_gpu_throughput pp-undercoun…
atnair-amd Jul 23, 2026
e43306e
Add OpenAI-compatible smoke test to unified vllm suite
atnair-amd Jul 22, 2026
6f38720
fix(vllm): give smoke test its own adequate max-model-len
atnair-amd Jul 24, 2026
567f7f3
Capture vllm server log into test_vllm_inference output
atnair-amd Jul 23, 2026
9f4a82e
SGLANG - orchestration and reporting changes (#275)
amd-droy Jul 28, 2026
66a0493
feat(inference): lm-eval-harness based accuracy evaluation + vllm sui…
atnair-amd Jul 30, 2026
3675050
feat(vllm): add Prometheus /metrics-derived latency metrics (queue/pr…
atnair-amd Jul 30, 2026
eaa82be
Urtiwari/tier2 (#285)
urtiwari Aug 3, 2026
83c6339
fix(accuracy): install lm-eval math extra and probe it by capability …
atnair-amd Aug 3, 2026
3e46f5b
Stop the vLLM suite from logging bulk command output three times over…
atnair-amd Aug 5, 2026
12630e3
squashing all commits to clean (#294)
amd-droy Aug 7, 2026
0bf589a
Close SSH sessions left open by temporary subset Pssh handles
atnair-amd Aug 5, 2026
7219e33
report: comment out Value/Unit columns in inference suite HTML reports
atnair-amd Aug 7, 2026
f653814
Hnimrama/atom multinode (#296)
hnimra-amd Aug 7, 2026
30b9299
Apply ruff formatting to atom inference modules
hnimra-amd Aug 11, 2026
2b8eecf
docs(plans): remove stale atom CVS automation plan
hnimra-amd Aug 11, 2026
4471aa2
test(atom): align multinode unit tests with session key and fabric di…
hnimra-amd Aug 11, 2026
29e0c74
Hnimrama/atom test readme (#305)
hnimra-amd Aug 11, 2026
f919fe8
reformatted to pass ruff formatter check in the CI (#302)
urtiwari Aug 11, 2026
9a20e3d
feat(vllm): add MI300X workload config set (14 models, single + distr…
atnair-amd Aug 12, 2026
2d792e8
feat(JAX)[Training] Orch refactored JAX MaxText single & distributed …
solaiys Aug 12, 2026
0a49745
fix(docker): use sudo_prefix() for pull_image, repair two failing UTs
atnair-amd Aug 12, 2026
87f5edb
Document the unified vLLM suite (#297)
atnair-amd Aug 12, 2026
ab8ec76
feat (Megatron) Orch refactored Megatron Single-Node & Distributed Tr…
sukesh-amd Aug 12, 2026
146c077
feat (Torchtitan) Orch refactored TorchtitanSingle-Node & Distributed…
amd-rthummal Aug 12, 2026
a3324f1
fix(ruff format) Fixed ruff formating issues.
solaiys Aug 13, 2026
c5efa0f
fix(ruff lint) Fixed lint issues.
solaiys Aug 13, 2026
4df77de
fix[inference] Fixed stale ModelSpec/threshold_json config-loader tests
solaiys Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ docs/sphinx/_toc.yml

# Build distributions
dist/

# Local sample output
sample_reports/
10 changes: 8 additions & 2 deletions cvs/cli_plugins/list_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,14 @@ def discover_tests():
# Prune non-suite dirs in place so os.walk skips descending them.
dirs[:] = [d for d in dirs if d not in skip_dirs]
for file in files:
# conftest.py holds fixtures/hooks, not a runnable suite.
if file.endswith(".py") and file not in ("__init__.py", "conftest.py"):
# Skip pytest infra (conftest.py) and private helpers
# (e.g. _shared.py): they are not selectable suites.
if (
file.endswith(".py")
and file != "__init__.py"
and file != "conftest.py"
and not file.startswith("_")
):
rel_path = os.path.relpath(os.path.join(root, file), tests_dir)
module_parts = os.path.splitext(rel_path)[0].split(os.sep)
# Module path: <tests_path>.<test_name>
Expand Down
8 changes: 6 additions & 2 deletions cvs/cli_plugins/run_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ def get_parser(self, subparsers):
)
parser.add_argument(
"--log-file",
default="/tmp/cvs/test.log",
help="Pytest: Path to file for logging output (default: /tmp/cvs/test.log)",
default=None,
metavar="PATH",
help=(
"Pytest: write logging output to this file (optional). "
"Parent directories are created automatically when set."
),
)
parser.add_argument(
"--log-level",
Expand Down
30 changes: 30 additions & 0 deletions cvs/cli_plugins/unittests/test_run_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,36 @@ def test_run_test_multiple_functions(self, mock_exit, mock_pytest_main):
mock_pytest_main.assert_called_once_with(expected_args)
mock_exit.assert_called_once_with(0)

@patch("cvs.cli_plugins.run_plugin.pytest.main")
@patch("cvs.cli_plugins.run_plugin.sys.exit")
def test_run_test_omits_log_file_when_not_set(self, mock_exit, mock_pytest_main):
"""No --log-file is passed to pytest when the user does not request file logging."""
args = MagicMock()
args.test = "agfhc_cvs"
args.function = []
args.cluster_file = "/path/to/cluster.json"
args.config_file = "/path/to/config.json"
args.html = None
args.self_contained_html = False
args.log_file = None
args.log_level = None
args.capture = None
args.extra_pytest_args = []

mock_pytest_main.return_value = 0

with patch.object(self.plugin, "get_test_file", return_value="/mock/path/test.py"):
with patch.object(self.plugin, "_validate_json_config"):
self.plugin.run(args)

expected_args = [
"/mock/path/test.py",
"--cluster_file=/path/to/cluster.json",
"--config_file=/path/to/config.json",
]
mock_pytest_main.assert_called_once_with(expected_args)
mock_exit.assert_called_once_with(0)


class TestRunPluginJsonValidation(unittest.TestCase):
"""Tests for RunPlugin._validate_json_config pre-flight checks."""
Expand Down
119 changes: 113 additions & 6 deletions cvs/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

import importlib.metadata
import json
import logging
from pathlib import Path

import pytest

from cvs.lib.report_plugins import HtmlReportManager, cli_option_value

log = logging.getLogger(__name__)


def _maybe_autocollect_html(config, suite_name):
'''
Expand Down Expand Up @@ -79,8 +82,8 @@ def _maybe_autocollect_html(config, suite_name):
return


@pytest.hookimpl(tryfirst=True)
def pytest_configure(config):
def _sync_suite_name_from_args(config):
"""Derive suite stem from the first ``*.py`` target in ``config.args``."""
suite_name = "test"
for arg in config.args:
bare = arg.split("::")[0]
Expand All @@ -89,8 +92,97 @@ def pytest_configure(config):
break
config._suite_name = suite_name
config._test_html_dir = f"{suite_name}_html"
_maybe_autocollect_html(config, suite_name)


def _ensure_html_report_manager(config):
"""Create ``HtmlReportManager`` once; safe if ``pytest_configure`` did not run."""
_sync_suite_name_from_args(config)
mgr = getattr(config, "_html_report_manager", None)
if mgr is not None:
return mgr

_maybe_autocollect_html(config, config._suite_name)
config._html_report_manager = HtmlReportManager(config)
return config._html_report_manager


def _auto_register_inference_suite_report(config):
from cvs.lib.report.auto_register import try_auto_register_inference_suite_report

_sync_suite_name_from_args(config)
return try_auto_register_inference_suite_report(config)


@pytest.hookimpl(tryfirst=True)
def pytest_configure(config):
_ensure_html_report_manager(config)
_auto_register_inference_suite_report(config)


@pytest.fixture(scope="session", autouse=True)
def _cvs_inference_suite_report_session(request):
"""Initialize the session report store when a suite preset is registered."""
from cvs.lib.report.registry import clear_session_results, get_suite_report_config
from cvs.lib.report.types import InferenceReportConfig

if not isinstance(get_suite_report_config(request.config), InferenceReportConfig):
yield
return

clear_session_results()
yield


@pytest.fixture(scope="module", autouse=True)
def _cvs_inference_suite_report_bind_module(request, _cvs_inference_suite_report_session):
"""Bind module-scoped suite fixtures into the session store at module teardown."""
from cvs.lib.report.registry import bind_session_results, get_suite_report_config
from cvs.lib.report.types import InferenceReportConfig

if not isinstance(get_suite_report_config(request.config), InferenceReportConfig):
yield
return

inf_res_dict = None
variant_config = None
lifecycle = None
try:
inf_res_dict = request.getfixturevalue("inf_res_dict")
except pytest.FixtureLookupError:
log.warning(
"Inference suite report preset registered but inf_res_dict fixture is missing; "
"session-end report will be skipped"
)
yield
return
try:
variant_config = request.getfixturevalue("variant_config")
except pytest.FixtureLookupError:
log.warning(
"Inference suite report preset registered but variant_config fixture is missing; "
"session-end report will be skipped"
)
yield
return
try:
lifecycle = request.getfixturevalue("lifecycle")
except pytest.FixtureLookupError:
log.warning(
"Inference suite report preset registered but lifecycle fixture is missing; "
"session-end report will be skipped"
)
yield
return

def _bind_at_module_end():
bind_session_results(
inf_res_dict=inf_res_dict,
variant_config=variant_config,
lifecycle=lifecycle,
)

request.addfinalizer(_bind_at_module_end)
yield


# Add all additional cmd line arguments for the script
Expand Down Expand Up @@ -156,15 +248,28 @@ def pytest_metadata(metadata):

# Prepare a clean per-run log directory before tests start.
def pytest_sessionstart(session):
session.config._html_report_manager.setup_log_dir()
_auto_register_inference_suite_report(session.config)
_ensure_html_report_manager(session.config).setup_log_dir()


# Capture each test report and attach a per-test external log link.
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call): # noqa: ARG001
outcome = yield
report = outcome.get_result()
report.extras = item.config._html_report_manager.write_test_log(report, item.originalname)
report.extras = _ensure_html_report_manager(item.config).write_test_log(report, item.originalname)

from cvs.lib.report.registry import get_suite_report_config
from cvs.lib.report.types import InferenceReportConfig

if isinstance(get_suite_report_config(item.config), InferenceReportConfig):
from cvs.lib.report.inference_wiring import (
attach_inference_suite_lifecycle_table,
attach_inference_suite_report_row_extra,
)

attach_inference_suite_lifecycle_table(item, report)
attach_inference_suite_report_row_extra(item, report)


# Replace inline pytest-html log content with a short externalized-log message.
Expand All @@ -181,4 +286,6 @@ def pytest_html_results_summary(prefix, summary, postfix):
@pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(session, exitstatus): # noqa: ARG001
yield # wait for pytest-html and all other plugins to finish writing the report
session.config._html_report_manager.create_zip_bundle(session)
mgr = _ensure_html_report_manager(session.config)
mgr.generate_suite_reports(session)
mgr.create_zip_bundle(session)
21 changes: 15 additions & 6 deletions cvs/core/orchestrators/baremetal.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(self, log, config, stop_on_errors=False):
stop_on_errors=self.stop_on_errors,
)

def exec(self, cmd, hosts=None, timeout=None, detailed=False):
def exec(self, cmd, hosts=None, timeout=None, detailed=False, print_console=True):
"""
Execute command across hosts via SSH (baremetal execution).

Expand All @@ -85,6 +85,8 @@ def exec(self, cmd, hosts=None, timeout=None, detailed=False):
timeout: Command timeout
detailed: If True, return detailed execution info including
exit_code (mirrors ContainerOrchestrator.exec).
print_console: If False, the command's output is returned but not
logged. Use for bulk data the caller parses itself.

Returns:
Dictionary mapping hosts to execution results
Expand All @@ -94,7 +96,7 @@ def exec(self, cmd, hosts=None, timeout=None, detailed=False):

# Use appropriate handle based on target hosts
if set(hosts) == set(self.hosts):
return self.all.exec(cmd, timeout=timeout, detailed=detailed)
return self.all.exec(cmd, timeout=timeout, detailed=detailed, print_console=print_console)
else:
# For arbitrary subset (including head node), create temporary handle
pssh = Pssh(
Expand All @@ -106,7 +108,10 @@ def exec(self, cmd, hosts=None, timeout=None, detailed=False):
host_key_check=False,
stop_on_errors=self.stop_on_errors,
)
return pssh.exec(cmd, timeout=timeout, detailed=detailed)
try:
return pssh.exec(cmd, timeout=timeout, detailed=detailed, print_console=print_console)
finally:
pssh.destroy_clients()

def sudo_prefix(self):
"""
Expand All @@ -130,19 +135,20 @@ def sudo_prefix(self):
self._needs_sudo = sudo_status.get(self.head_node, False)
return 'sudo -n ' if self._needs_sudo else ''

def exec_on_head(self, cmd, timeout=None, detailed=False):
def exec_on_head(self, cmd, timeout=None, detailed=False, print_console=True):
"""
Execute command on head node only via SSH.

Args:
cmd: Command to execute
timeout: Command timeout
detailed: See exec().
print_console: See exec().

Returns:
Dictionary mapping head node to execution result
"""
return self.head.exec(cmd, timeout=timeout, detailed=detailed)
return self.head.exec(cmd, timeout=timeout, detailed=detailed, print_console=print_console)

def setup_env(self, hosts, env_script=None):
"""Set up environment on hosts."""
Expand All @@ -167,7 +173,10 @@ def setup_env(self, hosts, env_script=None):
host_key_check=False,
stop_on_errors=self.stop_on_errors,
)
result = pssh.exec(f"bash {env_script}", timeout=60, detailed=True)
try:
result = pssh.exec(f"bash {env_script}", timeout=60, detailed=True)
finally:
pssh.destroy_clients()

# Check if all hosts succeeded
success = all(output['exit_code'] == 0 for output in result.values())
Expand Down
3 changes: 2 additions & 1 deletion cvs/core/orchestrators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ def exec(self, cmd, hosts=None, timeout=None):
pass

@abstractmethod
def exec_on_head(self, cmd, timeout=None):
def exec_on_head(self, cmd, timeout=None, detailed=False):
"""
Execute command on head node only.

Args:
cmd: Command to execute
timeout: Command timeout in seconds
detailed: If True, return detailed execution info including exit_code

"""
pass
Expand Down
Loading
Loading