vllm-benchmark 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vllm_bench/__init__.py +3 -0
- vllm_bench/artifacts.py +126 -0
- vllm_bench/bench_util.py +124 -0
- vllm_bench/cli.py +336 -0
- vllm_bench/config.py +168 -0
- vllm_bench/contracts.py +31 -0
- vllm_bench/external.py +61 -0
- vllm_bench/gpu.py +92 -0
- vllm_bench/json_io.py +12 -0
- vllm_bench/metrics.py +115 -0
- vllm_bench/processes.py +30 -0
- vllm_bench/progress.py +7 -0
- vllm_bench/providers/__init__.py +29 -0
- vllm_bench/providers/base.py +21 -0
- vllm_bench/providers/compose.py +261 -0
- vllm_bench/providers/context.py +43 -0
- vllm_bench/providers/modal.py +305 -0
- vllm_bench/providers/modal_runtime.py +80 -0
- vllm_bench/providers/paths.py +8 -0
- vllm_bench/reports/__init__.py +5 -0
- vllm_bench/reports/cache_hits.py +14 -0
- vllm_bench/reports/charts.py +44 -0
- vllm_bench/reports/data.py +61 -0
- vllm_bench/reports/engine_activity.py +48 -0
- vllm_bench/reports/engine_work.py +25 -0
- vllm_bench/reports/generator.py +70 -0
- vllm_bench/reports/gpu_usage.py +47 -0
- vllm_bench/reports/preemptions.py +12 -0
- vllm_bench/reports/runtime.py +7 -0
- vllm_bench/reports/throughput.py +18 -0
- vllm_bench/runner.py +159 -0
- vllm_bench/service.py +33 -0
- vllm_benchmark-0.1.0.dist-info/METADATA +363 -0
- vllm_benchmark-0.1.0.dist-info/RECORD +38 -0
- vllm_benchmark-0.1.0.dist-info/WHEEL +5 -0
- vllm_benchmark-0.1.0.dist-info/entry_points.txt +2 -0
- vllm_benchmark-0.1.0.dist-info/licenses/LICENSE +21 -0
- vllm_benchmark-0.1.0.dist-info/top_level.txt +1 -0
vllm_bench/__init__.py
ADDED
vllm_bench/artifacts.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Number result directories and save the exact experiment inputs."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
import zipfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .contracts import Trial
|
|
15
|
+
from .json_io import write_json
|
|
16
|
+
from .progress import log
|
|
17
|
+
|
|
18
|
+
EXCLUDED = {
|
|
19
|
+
".git",
|
|
20
|
+
".venv",
|
|
21
|
+
"venv",
|
|
22
|
+
"env",
|
|
23
|
+
"node_modules",
|
|
24
|
+
"results",
|
|
25
|
+
".vllm-bench",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
".cache",
|
|
28
|
+
".pytest_cache",
|
|
29
|
+
".ruff_cache",
|
|
30
|
+
"build",
|
|
31
|
+
"dist",
|
|
32
|
+
".wheels",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def project_files(root):
|
|
37
|
+
try:
|
|
38
|
+
names = (
|
|
39
|
+
subprocess.check_output(
|
|
40
|
+
["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"],
|
|
41
|
+
cwd=root,
|
|
42
|
+
stderr=subprocess.DEVNULL,
|
|
43
|
+
)
|
|
44
|
+
.decode()
|
|
45
|
+
.split("\0")
|
|
46
|
+
)
|
|
47
|
+
except subprocess.CalledProcessError:
|
|
48
|
+
names = []
|
|
49
|
+
for folder, directories, files in os.walk(root):
|
|
50
|
+
directories[:] = [name for name in directories if name not in EXCLUDED]
|
|
51
|
+
names.extend(str((Path(folder) / name).relative_to(root)) for name in files)
|
|
52
|
+
for name in sorted(set(names)):
|
|
53
|
+
path = root / name
|
|
54
|
+
if name == ".vllm_bench/state.json":
|
|
55
|
+
continue
|
|
56
|
+
if not name or not path.is_file() or path.is_symlink():
|
|
57
|
+
continue
|
|
58
|
+
if any(
|
|
59
|
+
part in EXCLUDED or part.startswith(".env") or part.endswith(".egg-info")
|
|
60
|
+
for part in Path(name).parts
|
|
61
|
+
):
|
|
62
|
+
continue
|
|
63
|
+
if any((root / parent).is_symlink() for parent in Path(name).parents):
|
|
64
|
+
continue
|
|
65
|
+
if path.suffix not in {".pyc", ".so", ".dylib", ".whl"}:
|
|
66
|
+
yield path, name
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def trials(config):
|
|
70
|
+
return [
|
|
71
|
+
Trial(benchmark=name, scenario=scenario, repeat=repeat)
|
|
72
|
+
for name, benchmark in config.benchmarks.items()
|
|
73
|
+
for scenario in benchmark.scenarios
|
|
74
|
+
for repeat in range(1, config.runs + 1)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def prepare_run(config, root):
|
|
79
|
+
parent = root / "results" / f"{config.provider}-{config.name}"
|
|
80
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
last = max(
|
|
82
|
+
(
|
|
83
|
+
int(match[1])
|
|
84
|
+
for path in parent.iterdir()
|
|
85
|
+
if path.is_dir() and (match := re.fullmatch(r"run(\d+)", path.name))
|
|
86
|
+
),
|
|
87
|
+
default=0,
|
|
88
|
+
)
|
|
89
|
+
output = parent / f"run{last + 1:03d}"
|
|
90
|
+
output.mkdir()
|
|
91
|
+
log(f"Result directory: {output}")
|
|
92
|
+
(output / "config.yaml").write_text(
|
|
93
|
+
yaml.safe_dump(config.model_dump(exclude_none=True), sort_keys=False)
|
|
94
|
+
)
|
|
95
|
+
if not config.skip_volume_mount:
|
|
96
|
+
archive = output / "project.zip"
|
|
97
|
+
log("Saving project snapshot (excluding results, environments, and caches)...")
|
|
98
|
+
count = 0
|
|
99
|
+
last_update = time.monotonic()
|
|
100
|
+
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as bundle:
|
|
101
|
+
for path, name in project_files(root):
|
|
102
|
+
bundle.write(path, name)
|
|
103
|
+
count += 1
|
|
104
|
+
if time.monotonic() - last_update >= 5:
|
|
105
|
+
log(f"Project snapshot: {count} files archived so far...")
|
|
106
|
+
last_update = time.monotonic()
|
|
107
|
+
with archive.open("rb") as stream:
|
|
108
|
+
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
109
|
+
(output / "project.sha256").write_text(f"{digest} project.zip\n")
|
|
110
|
+
log(
|
|
111
|
+
f"Snapshot saved: {count} files, {archive.stat().st_size / 1024**2:.1f} MiB"
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
log("Project snapshot skipped: skip_volume_mount is enabled.")
|
|
115
|
+
write_json(
|
|
116
|
+
output / "run.json",
|
|
117
|
+
{
|
|
118
|
+
"schema_version": 1,
|
|
119
|
+
"cli_version": __version__,
|
|
120
|
+
"provider": config.provider,
|
|
121
|
+
"image": config.base_image,
|
|
122
|
+
"trials": [trial.model_dump() for trial in trials(config)],
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
log("Saved effective config and run manifest.")
|
|
126
|
+
return output
|
vllm_bench/bench_util.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""The small public interface used by startup and benchmark scripts."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@lru_cache(maxsize=1)
|
|
11
|
+
def _context():
|
|
12
|
+
return json.loads(Path(os.environ["VLLM_BENCH_CONTEXT"]).read_text())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_global_args():
|
|
16
|
+
return dict(_context()["global_args"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_service_args(name=None):
|
|
20
|
+
"""Global arguments with overrides for the current or named service group."""
|
|
21
|
+
if name is None and (service := get_service()):
|
|
22
|
+
name = service["group"]
|
|
23
|
+
overrides = _context()["service_args"][name] if name is not None else {}
|
|
24
|
+
return {**get_global_args(), **overrides}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_global_arg(name, default=None):
|
|
28
|
+
return _context()["global_args"].get(name, default)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_scenario_arg(name, default=None):
|
|
32
|
+
return _context()["scenario_args"].get(name, default)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_benchmark_arg(name, default=None):
|
|
36
|
+
return _context()["benchmark_args"].get(name, default)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_output_dir():
|
|
40
|
+
path = Path(_context()["output_dir"])
|
|
41
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
return path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_project_dir():
|
|
46
|
+
return Path(_context()["project_dir"])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_service():
|
|
50
|
+
"""Name, role, and replica of the calling startup script (None on the client)."""
|
|
51
|
+
return _context().get("service")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_service_url(name, *, internal=False):
|
|
55
|
+
services = _context()["internal_services" if internal else "services"]
|
|
56
|
+
matches = [
|
|
57
|
+
item for item in services if item["group"] == name or item["name"] == name
|
|
58
|
+
]
|
|
59
|
+
if len(matches) != 1:
|
|
60
|
+
raise ValueError(f"Expected one service named {name}; found {len(matches)}")
|
|
61
|
+
return matches[0]["url"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_leader_url():
|
|
65
|
+
return next(
|
|
66
|
+
item["url"] for item in _context()["services"] if item["role"] == "leader"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_worker_urls():
|
|
71
|
+
return [item["url"] for item in _context()["services"] if item["role"] == "worker"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_service_path(path):
|
|
75
|
+
"""Translate a client project/trial path into the path visible to services."""
|
|
76
|
+
path = Path(path).resolve()
|
|
77
|
+
context = _context()
|
|
78
|
+
if context.get("external_services"):
|
|
79
|
+
# External services may share the client's filesystem; no upload is implied.
|
|
80
|
+
return str(path)
|
|
81
|
+
for local, remote in [
|
|
82
|
+
(context["trial_dir"], "/run/vllm-bench/trial"),
|
|
83
|
+
(context["project_dir"], context["service_project_dir"]),
|
|
84
|
+
]:
|
|
85
|
+
local = Path(local).resolve()
|
|
86
|
+
if path.is_relative_to(local):
|
|
87
|
+
destination = str(Path(remote) / path.relative_to(local))
|
|
88
|
+
if sandbox_id := context.get("modal_sandbox_id"):
|
|
89
|
+
from .providers.modal import upload_path
|
|
90
|
+
|
|
91
|
+
upload_path(sandbox_id, path, destination)
|
|
92
|
+
return destination
|
|
93
|
+
raise ValueError(f"Path must be inside the project or trial directory: {path}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@lru_cache(maxsize=1)
|
|
97
|
+
def get_gpu_count():
|
|
98
|
+
try:
|
|
99
|
+
result = subprocess.run(
|
|
100
|
+
["nvidia-smi", "--query-gpu=uuid", "--format=csv,noheader"],
|
|
101
|
+
capture_output=True,
|
|
102
|
+
text=True,
|
|
103
|
+
check=True,
|
|
104
|
+
timeout=5,
|
|
105
|
+
)
|
|
106
|
+
return len(result.stdout.strip().splitlines())
|
|
107
|
+
except (OSError, subprocess.SubprocessError):
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_gpu_available():
|
|
112
|
+
return get_gpu_count() > 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_cpu():
|
|
116
|
+
return not is_gpu_available()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def __getattr__(name):
|
|
120
|
+
if name == "current_run":
|
|
121
|
+
return _context()["repeat"]
|
|
122
|
+
if name == "current_benchmark":
|
|
123
|
+
return _context()["benchmark"]
|
|
124
|
+
raise AttributeError(name)
|
vllm_bench/cli.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""The user's machine owns scheduling, benchmark scripts, metrics, and reports."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import shlex
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from contextlib import nullcontext
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .artifacts import prepare_run, trials
|
|
14
|
+
from .config import load_config, resolve_config, select_benchmarks
|
|
15
|
+
from .external import prepare_context, service_endpoints
|
|
16
|
+
from .progress import log
|
|
17
|
+
from .providers import get_provider, stop_saved
|
|
18
|
+
from .runner import run_only, run_trial, start_environment, wait_healthy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def interrupt(signum, frame):
|
|
22
|
+
raise KeyboardInterrupt
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_only_command(config, config_path, root, trial, endpoints):
|
|
26
|
+
command = [
|
|
27
|
+
"vllm_bench",
|
|
28
|
+
"run_only",
|
|
29
|
+
"--current-dir",
|
|
30
|
+
str(root),
|
|
31
|
+
"--config",
|
|
32
|
+
str(config_path),
|
|
33
|
+
"--benchmark",
|
|
34
|
+
trial.benchmark,
|
|
35
|
+
"--scenario",
|
|
36
|
+
trial.scenario,
|
|
37
|
+
]
|
|
38
|
+
urls = {}
|
|
39
|
+
for item in endpoints:
|
|
40
|
+
urls.setdefault(item["group"], item["url"])
|
|
41
|
+
for name, url in urls.items():
|
|
42
|
+
command.extend(["--service-url", f"{name}={url}"])
|
|
43
|
+
if dataset := config.benchmarks[trial.benchmark].script_args.get("dataset_path"):
|
|
44
|
+
command.extend(["--dataset-path", str(dataset)])
|
|
45
|
+
return shlex.join(command)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def show_environment(
|
|
49
|
+
provider, endpoints, directory, timeout, started, detach, run_command
|
|
50
|
+
):
|
|
51
|
+
ready = False
|
|
52
|
+
try:
|
|
53
|
+
logs = nullcontext() if detach else provider.follow_logs()
|
|
54
|
+
with logs as process:
|
|
55
|
+
wait_healthy(provider, endpoints, timeout)
|
|
56
|
+
log(f"Environment ready in {time.monotonic() - started:.1f}s.")
|
|
57
|
+
log("Run the benchmark from another terminal:")
|
|
58
|
+
print(run_command, flush=True)
|
|
59
|
+
for item in endpoints:
|
|
60
|
+
log(f"{item['name']} ({item['role']}): {item['url']}")
|
|
61
|
+
log(f"Service output: {directory / 'services'}")
|
|
62
|
+
ready = True
|
|
63
|
+
if detach:
|
|
64
|
+
log("Containers are running. Stop with: vllm_bench down")
|
|
65
|
+
return 0
|
|
66
|
+
log("Following container logs. Press Ctrl+C to stop the environment.")
|
|
67
|
+
while process.poll() is None:
|
|
68
|
+
provider.check_running()
|
|
69
|
+
time.sleep(1)
|
|
70
|
+
if process.returncode:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
f"Container log stream exited with {process.returncode}"
|
|
73
|
+
)
|
|
74
|
+
return 0
|
|
75
|
+
finally:
|
|
76
|
+
if not detach or not ready:
|
|
77
|
+
log("Stopping the environment and saving container logs...")
|
|
78
|
+
provider.stop()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
parser = argparse.ArgumentParser(description="Run script-defined vLLM benchmarks.")
|
|
83
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
84
|
+
parser.add_argument("--config", type=Path, default=Path("local_cpu.yaml"))
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--current-dir",
|
|
87
|
+
type=Path,
|
|
88
|
+
default=Path.cwd(),
|
|
89
|
+
help="Project root containing .vllm_bench",
|
|
90
|
+
)
|
|
91
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
92
|
+
up = commands.add_parser("up", help="Start services and follow their logs")
|
|
93
|
+
up.add_argument(
|
|
94
|
+
"-d",
|
|
95
|
+
"--detach",
|
|
96
|
+
action="store_true",
|
|
97
|
+
help="Leave services running in the background",
|
|
98
|
+
)
|
|
99
|
+
down = commands.add_parser(
|
|
100
|
+
"down", help="Stop the active environment and collect output"
|
|
101
|
+
)
|
|
102
|
+
run = commands.add_parser(
|
|
103
|
+
"run_scenarios",
|
|
104
|
+
help="Run selected trials with fresh managed services each time",
|
|
105
|
+
)
|
|
106
|
+
single = commands.add_parser(
|
|
107
|
+
"run_only", help="Run one benchmark/scenario once against existing service URLs"
|
|
108
|
+
)
|
|
109
|
+
single.add_argument("--benchmark", required=True)
|
|
110
|
+
single.add_argument("--scenario", required=True)
|
|
111
|
+
single.add_argument(
|
|
112
|
+
"--service-url",
|
|
113
|
+
action="append",
|
|
114
|
+
required=True,
|
|
115
|
+
metavar="NAME=URL",
|
|
116
|
+
help="Existing service base URL; repeat for each required service",
|
|
117
|
+
)
|
|
118
|
+
single.add_argument(
|
|
119
|
+
"--dataset-path",
|
|
120
|
+
help="Existing dataset path visible to the services (passed to the benchmark)",
|
|
121
|
+
)
|
|
122
|
+
report = commands.add_parser("report", help="Regenerate reports from saved files")
|
|
123
|
+
report.add_argument("directory", type=Path)
|
|
124
|
+
for command in [up, down, run, single, report]:
|
|
125
|
+
command.add_argument(
|
|
126
|
+
"--current-dir",
|
|
127
|
+
type=Path,
|
|
128
|
+
default=argparse.SUPPRESS,
|
|
129
|
+
help="Project root containing .vllm_bench",
|
|
130
|
+
)
|
|
131
|
+
for command in [up, run, single]:
|
|
132
|
+
command.add_argument(
|
|
133
|
+
"--config",
|
|
134
|
+
type=Path,
|
|
135
|
+
default=argparse.SUPPRESS,
|
|
136
|
+
help="Config name or path, merged over base.yaml (e.g. modal)",
|
|
137
|
+
)
|
|
138
|
+
for command in [up, run]:
|
|
139
|
+
command.add_argument(
|
|
140
|
+
"--benchmark",
|
|
141
|
+
"--benchmarks",
|
|
142
|
+
dest="benchmarks",
|
|
143
|
+
nargs="+",
|
|
144
|
+
required=True,
|
|
145
|
+
help="Benchmark names, or '*' for all benchmarks",
|
|
146
|
+
)
|
|
147
|
+
command.add_argument(
|
|
148
|
+
"--scenario",
|
|
149
|
+
"--scenarios",
|
|
150
|
+
dest="scenarios",
|
|
151
|
+
nargs="+",
|
|
152
|
+
required=True,
|
|
153
|
+
help="Scenario names, or '*' for every scenario in each selected benchmark",
|
|
154
|
+
)
|
|
155
|
+
args = parser.parse_args()
|
|
156
|
+
signal.signal(signal.SIGTERM, interrupt)
|
|
157
|
+
root = args.current_dir.resolve()
|
|
158
|
+
config_path = resolve_config(root / ".vllm_bench/configs" / args.config)
|
|
159
|
+
state_path = root / ".vllm_bench/state.json"
|
|
160
|
+
try:
|
|
161
|
+
if args.command == "report":
|
|
162
|
+
from .reports import generate_reports
|
|
163
|
+
|
|
164
|
+
generate_reports((root / args.directory).resolve())
|
|
165
|
+
return 0
|
|
166
|
+
if not (root / ".vllm_bench").is_dir():
|
|
167
|
+
raise FileNotFoundError(f"Project must contain .vllm_bench: {root}")
|
|
168
|
+
if args.command == "down":
|
|
169
|
+
stop_saved(state_path)
|
|
170
|
+
return 0
|
|
171
|
+
started = time.monotonic()
|
|
172
|
+
if args.command == "up":
|
|
173
|
+
log(f"Reading config: {config_path}")
|
|
174
|
+
external = args.command == "run_only"
|
|
175
|
+
config = select_benchmarks(
|
|
176
|
+
load_config(config_path),
|
|
177
|
+
[args.benchmark] if external else args.benchmarks,
|
|
178
|
+
[args.scenario] if external else args.scenarios,
|
|
179
|
+
)
|
|
180
|
+
if external:
|
|
181
|
+
config = config.model_copy(
|
|
182
|
+
update={
|
|
183
|
+
"provider": "external",
|
|
184
|
+
"runs": 1,
|
|
185
|
+
"skip_volume_mount": True,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
if args.dataset_path:
|
|
189
|
+
benchmark = config.benchmarks[args.benchmark]
|
|
190
|
+
benchmark.script_args = {
|
|
191
|
+
**benchmark.script_args,
|
|
192
|
+
"dataset_path": args.dataset_path,
|
|
193
|
+
}
|
|
194
|
+
experiment = trials(config)
|
|
195
|
+
first = experiment[0]
|
|
196
|
+
if args.command in {"up", "run_only"}:
|
|
197
|
+
pairs = sum(
|
|
198
|
+
len(benchmark.scenarios) for benchmark in config.benchmarks.values()
|
|
199
|
+
)
|
|
200
|
+
if pairs != 1:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f"{args.command} requires exactly one benchmark and one scenario"
|
|
203
|
+
)
|
|
204
|
+
if external:
|
|
205
|
+
endpoints = service_endpoints(config, first, args.service_url)
|
|
206
|
+
if args.command == "up":
|
|
207
|
+
log(f"Configuration valid. Provider: {config.provider}")
|
|
208
|
+
log(f"Starting benchmark '{first.benchmark}', scenario '{first.scenario}'.")
|
|
209
|
+
if model := config.global_args.get("model"):
|
|
210
|
+
log(f"Model: {model}")
|
|
211
|
+
scripts = [benchmark.script for benchmark in config.benchmarks.values()]
|
|
212
|
+
if not external:
|
|
213
|
+
if config.provider == "external":
|
|
214
|
+
raise ValueError("Use run_only for external services")
|
|
215
|
+
if not config.base_image:
|
|
216
|
+
raise ValueError(f"{args.command} requires base_image in the config")
|
|
217
|
+
groups = dict.fromkeys(
|
|
218
|
+
name
|
|
219
|
+
for benchmark_name, benchmark in config.benchmarks.items()
|
|
220
|
+
for scenario in benchmark.scenarios
|
|
221
|
+
for name in config.get_scenario(benchmark_name, scenario).services
|
|
222
|
+
)
|
|
223
|
+
for name in groups:
|
|
224
|
+
script = config.services[name].startup_script
|
|
225
|
+
if not script:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"Managed service '{name}' requires startup_script"
|
|
228
|
+
)
|
|
229
|
+
if not config.skip_volume_mount:
|
|
230
|
+
scripts.append(script)
|
|
231
|
+
for script in scripts:
|
|
232
|
+
if not (root / script).is_file():
|
|
233
|
+
raise FileNotFoundError(f"Script not found: {root / script}")
|
|
234
|
+
if external:
|
|
235
|
+
from .reports import generate_reports
|
|
236
|
+
|
|
237
|
+
output = prepare_run(config, root)
|
|
238
|
+
directory = output / first.directory
|
|
239
|
+
context = prepare_context(config, first, root, directory, endpoints)
|
|
240
|
+
try:
|
|
241
|
+
result = run_only(config, root, first, directory, context, endpoints)
|
|
242
|
+
if result.status == "cancelled":
|
|
243
|
+
return 130
|
|
244
|
+
return 0 if result.status == "ok" else 1
|
|
245
|
+
finally:
|
|
246
|
+
generate_reports(output)
|
|
247
|
+
if state_path.exists():
|
|
248
|
+
if args.command == "up":
|
|
249
|
+
state = json.loads(state_path.read_text())
|
|
250
|
+
directory = Path(state["directory"])
|
|
251
|
+
active = json.loads((directory / ".provider/client.json").read_text())
|
|
252
|
+
if (
|
|
253
|
+
state.get("provider", "local") != config.provider
|
|
254
|
+
or state.get("image") != config.base_image
|
|
255
|
+
or (active["benchmark"], active["scenario"])
|
|
256
|
+
!= (first.benchmark, first.scenario)
|
|
257
|
+
or active["global_args"] != config.global_args
|
|
258
|
+
or active.get("service_args", {})
|
|
259
|
+
!= {
|
|
260
|
+
group: config.services[group].service_args
|
|
261
|
+
for group in config.get_scenario(
|
|
262
|
+
first.benchmark, first.scenario
|
|
263
|
+
).services
|
|
264
|
+
}
|
|
265
|
+
or active["scenario_args"]
|
|
266
|
+
!= config.get_scenario(first.benchmark, first.scenario).args
|
|
267
|
+
):
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"An environment for {active['benchmark']}/"
|
|
270
|
+
f"{active['scenario']} "
|
|
271
|
+
"is already active with different selection "
|
|
272
|
+
"or environment settings. "
|
|
273
|
+
"Run 'vllm_bench down' before switching."
|
|
274
|
+
)
|
|
275
|
+
log(f"Attaching to {first.benchmark}/{first.scenario}: {directory}")
|
|
276
|
+
endpoints = json.loads((directory / "endpoints.json").read_text())
|
|
277
|
+
return show_environment(
|
|
278
|
+
get_provider(config, root),
|
|
279
|
+
endpoints,
|
|
280
|
+
directory,
|
|
281
|
+
config.startup_timeout,
|
|
282
|
+
started,
|
|
283
|
+
args.detach,
|
|
284
|
+
run_only_command(config, config_path, root, first, endpoints),
|
|
285
|
+
)
|
|
286
|
+
stop_saved(state_path)
|
|
287
|
+
output = prepare_run(config, root)
|
|
288
|
+
provider = get_provider(config, root)
|
|
289
|
+
if args.command == "up":
|
|
290
|
+
trial = experiment[0]
|
|
291
|
+
directory = output / trial.directory
|
|
292
|
+
directory.mkdir(parents=True)
|
|
293
|
+
try:
|
|
294
|
+
_, endpoints = start_environment(
|
|
295
|
+
config, provider, trial, directory, wait=False
|
|
296
|
+
)
|
|
297
|
+
except BaseException:
|
|
298
|
+
log("Startup did not complete. Stopping containers and saving logs...")
|
|
299
|
+
provider.stop()
|
|
300
|
+
log(f"Startup output: {directory}")
|
|
301
|
+
raise
|
|
302
|
+
return show_environment(
|
|
303
|
+
provider,
|
|
304
|
+
endpoints,
|
|
305
|
+
directory,
|
|
306
|
+
config.startup_timeout,
|
|
307
|
+
started,
|
|
308
|
+
args.detach,
|
|
309
|
+
run_only_command(config, config_path, root, first, endpoints),
|
|
310
|
+
)
|
|
311
|
+
from .reports import generate_reports
|
|
312
|
+
|
|
313
|
+
failed = False
|
|
314
|
+
try:
|
|
315
|
+
for trial in experiment:
|
|
316
|
+
result = run_trial(
|
|
317
|
+
config, root, provider, trial, output / trial.directory
|
|
318
|
+
)
|
|
319
|
+
if result.status == "cancelled":
|
|
320
|
+
return 130
|
|
321
|
+
failed = failed or result.status != "ok"
|
|
322
|
+
if state_path.exists():
|
|
323
|
+
# Preserve failed cleanup state for recovery; stop scheduling.
|
|
324
|
+
return 1
|
|
325
|
+
return 1 if failed else 0
|
|
326
|
+
finally:
|
|
327
|
+
generate_reports(output)
|
|
328
|
+
except KeyboardInterrupt:
|
|
329
|
+
return 130
|
|
330
|
+
except Exception as exc:
|
|
331
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
332
|
+
return 1
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__":
|
|
336
|
+
raise SystemExit(main())
|