A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 7, 12:45 UTC
[Benchmark] Improve UX of sweep scripts (#35600)
Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>
Cyrus Leung committed
Feb 28, 2026 at 16:36 UTC
4292e3b807a51507f60f43b3829b5e5e918f5b87
6 files changed
+191
-128
docs/benchmarking/sweeps.md
+14
-10
@@ -72,7 +72,7 @@ Follow these steps to run the script:
72
]
73
```
74
75
-5. Determine where you want to save the results, and pass that to `--output-dir`.
75
+5. Set `--output-dir` and optionally `--experiment-name` to control where to save the results.
76
77
Example command:
78
@@ -82,7 +82,8 @@ vllm bench sweep serve \
82
--bench-cmd 'vllm bench serve --model meta-llama/Llama-2-7b-chat-hf --backend vllm --endpoint /v1/completions --dataset-name sharegpt --dataset-path benchmarks/ShareGPT_V3_unfiltered_cleaned_split.json' \
83
--serve-params benchmarks/serve_hparams.json \
84
--bench-params benchmarks/bench_hparams.json \
85
- -o benchmarks/results
85
+ --output-dir benchmarks/results \
86
+ --experiment-name demo
87
```
88
89
By default, each parameter combination is benchmarked 3 times to make the results more reliable. You can adjust the number of runs by setting `--num-runs`.
@@ -118,7 +119,8 @@ vllm bench sweep serve_workload \
119
--serve-params benchmarks/serve_hparams.json \
120
--bench-params benchmarks/bench_hparams.json \
121
--num-runs 1 \
121
- -o benchmarks/results
122
+ --output-dir benchmarks/results \
123
+ --experiment-name demo
124
```
125
126
The algorithm for exploring different workload levels can be summarized as follows:
@@ -186,7 +188,8 @@ vllm bench sweep startup \
188
--startup-cmd 'vllm bench startup --model Qwen/Qwen3-0.6B' \
189
--serve-params benchmarks/serve_hparams.json \
190
--startup-params benchmarks/startup_hparams.json \
189
- -o benchmarks/results
191
+ --output-dir benchmarks/results \
192
+ --experiment-name demo
193
```
194
195
!!! important
@@ -204,11 +207,10 @@ Control the variables to plot via `--var-x` and `--var-y`, optionally applying `
207
Example commands for visualizing [Workload Explorer](#workload-explorer) results:
208
209
```bash
207
-# Name of the directory that stores the results
208
-TIMESTAMP=$1
210
+EXPERIMENT_DIR=${1:-"benchmarks/results/demo"}
211
212
# Latency increases as the workload increases
211
-vllm bench sweep plot benchmarks/results/$TIMESTAMP \
213
+vllm bench sweep plot $EXPERIMENT_DIR \
214
--var-x max_concurrency \
215
--var-y median_ttft_ms \
216
--col-by _benchmark_name \
@@ -216,7 +218,7 @@ vllm bench sweep plot benchmarks/results/$TIMESTAMP \
218
--fig-name latency_curve
219
220
# Throughput saturates as workload increases
219
-vllm bench sweep plot benchmarks/results/$TIMESTAMP \
221
+vllm bench sweep plot $EXPERIMENT_DIR \
222
--var-x max_concurrency \
223
--var-y total_token_throughput \
224
--col-by _benchmark_name \
@@ -224,7 +226,7 @@ vllm bench sweep plot benchmarks/results/$TIMESTAMP \
226
--fig-name throughput_curve
227
228
# Tradeoff between latency and throughput
227
-vllm bench sweep plot benchmarks/results/$TIMESTAMP \
229
+vllm bench sweep plot $EXPERIMENT_DIR \
230
--var-x total_token_throughput \
231
--var-y median_ttft_ms \
232
--col-by _benchmark_name \
@@ -249,7 +251,9 @@ Higher concurrency or batch size can raise GPU efficiency (per-GPU), but can add
251
Example:
252
253
```bash
252
-vllm bench sweep plot_pareto benchmarks/results/<timestamp> \
254
+EXPERIMENT_DIR=${1:-"benchmarks/results/demo"}
255
+
256
+vllm bench sweep plot_pareto $EXPERIMENT_DIR \
257
--label-by max_concurrency,tensor_parallel_size,pipeline_parallel_size
258
```
259
vllm/benchmarks/sweep/plot.py
+3
-5
@@ -499,7 +499,7 @@ class SweepPlotArgs:
499
500
@classmethod
501
def from_cli_args(cls, args: argparse.Namespace):
502
- output_dir = Path(args.OUTPUT_DIR)
502
+ output_dir = Path(args.EXPERIMENT_DIR)
503
if not output_dir.exists():
504
raise ValueError(f"No parameter sweep results under {output_dir}")
505
@@ -531,11 +531,9 @@ class SweepPlotArgs:
531
@classmethod
532
def add_cli_args(cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
533
parser.add_argument(
534
- "OUTPUT_DIR",
534
+ "EXPERIMENT_DIR",
535
type=str,
536
- default="results",
537
- help="The directory containing the results to plot, "
538
- "i.e., the `--output-dir` argument to the parameter sweep script.",
536
+ help="The directory containing the sweep results to plot.",
537
)
538
parser.add_argument(
539
"--fig-dir",
vllm/benchmarks/sweep/plot_pareto.py
+2
-3
@@ -325,7 +325,7 @@ class SweepPlotParetoArgs:
325
326
@classmethod
327
def from_cli_args(cls, args: argparse.Namespace):
328
- output_dir = Path(args.OUTPUT_DIR)
328
+ output_dir = Path(args.EXPERIMENT_DIR)
329
if not output_dir.exists():
330
raise ValueError(f"No parameter sweep results under {output_dir}")
331
@@ -342,9 +342,8 @@ class SweepPlotParetoArgs:
342
@classmethod
343
def add_cli_args(cls, parser: argparse.ArgumentParser):
344
parser.add_argument(
345
- "OUTPUT_DIR",
345
+ "EXPERIMENT_DIR",
346
type=str,
347
- default="results",
347
help="The directory containing the sweep results to plot.",
348
)
349
parser.add_argument(
vllm/benchmarks/sweep/serve.py
+80
-45
@@ -4,6 +4,7 @@ import argparse
4
import contextlib
5
import json
6
import shlex
7
+from contextlib import contextmanager
8
from dataclasses import dataclass
9
from datetime import datetime
10
from pathlib import Path
@@ -135,7 +136,7 @@ def run_benchmark(
136
137
138
def _get_comb_base_path(
138
- output_dir: Path,
139
+ experiment_dir: Path,
140
serve_comb: ParameterSweepItem,
141
bench_comb: ParameterSweepItem,
142
*,
@@ -149,7 +150,7 @@ def _get_comb_base_path(
150
if extra_parts:
151
parts.extend(extra_parts)
152
152
- return output_dir / sanitize_filename("-".join(parts))
153
+ return experiment_dir / sanitize_filename("-".join(parts))
154
155
156
def _get_comb_run_path(base_path: Path, run_number: int | None):
@@ -162,10 +163,10 @@ def _get_comb_run_path(base_path: Path, run_number: int | None):
163
def _comb_needs_server(
164
serve_comb: ParameterSweepItem,
165
bench_combs: ParameterSweep,
165
- output_dir: Path,
166
+ experiment_dir: Path,
167
):
168
for bench_comb in bench_combs:
168
- base_path = _get_comb_base_path(output_dir, serve_comb, bench_comb)
169
+ base_path = _get_comb_base_path(experiment_dir, serve_comb, bench_comb)
170
if not _get_comb_run_path(base_path, run_number=None).exists():
171
return True
172
@@ -179,11 +180,11 @@ def server_ctx(
180
show_stdout: bool,
181
serve_comb: ParameterSweepItem,
182
bench_params: ParameterSweep,
182
- output_dir: Path,
183
+ experiment_dir: Path,
184
dry_run: bool,
185
server_ready_timeout: int = 300,
186
):
186
- if not _comb_needs_server(serve_comb, bench_params, output_dir):
187
+ if not _comb_needs_server(serve_comb, bench_params, experiment_dir):
188
return contextlib.nullcontext()
189
190
return run_server(
@@ -215,10 +216,10 @@ def run_comb(
216
*,
217
serve_comb: ParameterSweepItem,
218
bench_comb: ParameterSweepItem,
219
+ link_vars: list[tuple[str, str]],
220
base_path: Path,
221
num_runs: int,
222
dry_run: bool,
221
- link_vars: list[tuple[str, str]],
223
):
224
if not _comb_is_valid(serve_comb, bench_comb, link_vars):
225
return None
@@ -257,10 +258,10 @@ def run_combs(
258
server_ready_timeout: int,
259
serve_params: ParameterSweep,
260
bench_params: ParameterSweep,
260
- output_dir: Path,
261
+ link_vars: list[tuple[str, str]],
262
+ experiment_dir: Path,
263
num_runs: int,
264
dry_run: bool,
263
- link_vars: list[tuple[str, str]],
265
):
266
all_data = list[dict[str, object]]()
267
for serve_comb in serve_params:
@@ -270,22 +271,22 @@ def run_combs(
271
show_stdout=show_stdout,
272
serve_comb=serve_comb,
273
bench_params=bench_params,
273
- output_dir=output_dir,
274
+ experiment_dir=experiment_dir,
275
dry_run=dry_run,
276
server_ready_timeout=server_ready_timeout,
277
) as server:
278
for bench_comb in bench_params:
278
- base_path = _get_comb_base_path(output_dir, serve_comb, bench_comb)
279
+ base_path = _get_comb_base_path(experiment_dir, serve_comb, bench_comb)
280
281
comb_data = run_comb(
282
server,
283
bench_cmd,
284
serve_comb=serve_comb,
285
bench_comb=bench_comb,
286
+ link_vars=link_vars,
287
base_path=base_path,
288
num_runs=num_runs,
289
dry_run=dry_run,
288
- link_vars=link_vars,
290
)
291
292
if comb_data is not None:
@@ -295,7 +296,7 @@ def run_combs(
296
return None
297
298
combined_df = pd.DataFrame.from_records(all_data)
298
- combined_df.to_csv(output_dir / "summary.csv")
299
+ combined_df.to_csv(experiment_dir / "summary.csv")
300
301
return combined_df
302
@@ -309,11 +310,12 @@ class SweepServeArgs:
310
server_ready_timeout: int
311
serve_params: ParameterSweep
312
bench_params: ParameterSweep
313
+ link_vars: list[tuple[str, str]]
314
output_dir: Path
315
+ experiment_name: str
316
num_runs: int
317
dry_run: bool
315
- resume: str | None
316
- link_vars: list[tuple[str, str]]
318
+ resume: bool
319
320
parser_name: ClassVar[str] = "serve"
321
parser_help: ClassVar[str] = "Run vLLM server benchmark under multiple settings."
@@ -340,6 +342,11 @@ class SweepServeArgs:
342
343
link_vars = cls.parse_link_vars(args.link_vars)
344
345
+ if args.experiment_name:
346
+ experiment_name = args.experiment_name
347
+ else:
348
+ experiment_name = datetime.now().strftime("%Y%m%d_%H%M%S")
349
+
350
num_runs = args.num_runs
351
if num_runs < 1:
352
raise ValueError("`num_runs` should be at least 1.")
@@ -351,11 +358,12 @@ class SweepServeArgs:
358
show_stdout=args.show_stdout,
359
serve_params=serve_params,
360
bench_params=bench_params,
361
+ link_vars=link_vars,
362
output_dir=Path(args.output_dir),
363
+ experiment_name=experiment_name,
364
num_runs=num_runs,
365
dry_run=args.dry_run,
366
resume=args.resume,
358
- link_vars=link_vars,
367
server_ready_timeout=args.server_ready_timeout,
368
)
369
@@ -392,6 +400,7 @@ class SweepServeArgs:
400
default=300,
401
help="Timeout in seconds to wait for the server to become ready.",
402
)
403
+
404
parser.add_argument(
405
"--serve-params",
406
type=str,
@@ -402,6 +411,16 @@ class SweepServeArgs:
411
"If both `serve_params` and `bench_params` are given, "
412
"this script will iterate over their Cartesian product.",
413
)
414
+ parser.add_argument(
415
+ "--link-vars",
416
+ type=str,
417
+ default="",
418
+ help=(
419
+ "Comma-separated list of linked variables between serve and bench, "
420
+ "e.g. max_num_seqs=max_concurrency,max_model_len=random_input_len"
421
+ ),
422
+ )
423
+
424
parser.add_argument(
425
"--bench-params",
426
type=str,
@@ -417,7 +436,15 @@ class SweepServeArgs:
436
"--output-dir",
437
type=str,
438
default="results",
420
- help="The directory to which results are written.",
439
+ help="The main directory to which results are written.",
440
+ )
441
+ parser.add_argument(
442
+ "-e",
443
+ "--experiment-name",
444
+ type=str,
445
+ default=None,
446
+ help="The name of this experiment (defaults to current timestamp). "
447
+ "Results will be stored under `output_dir/experiment_name`.",
448
)
449
parser.add_argument(
450
"--num-runs",
@@ -433,21 +460,10 @@ class SweepServeArgs:
460
)
461
parser.add_argument(
462
"--resume",
436
- type=str,
437
- default=None,
438
- help="Set this to the name of a directory under `output_dir` (which is a "
439
- "timestamp) to resume a previous execution of this script, i.e., only run "
440
- "parameter combinations for which there are still no output files.",
441
- )
442
-
443
- parser.add_argument(
444
- "--link-vars",
445
- type=str,
446
- default="",
447
- help=(
448
- "Comma-separated list of linked variables between serve and bench, "
449
- "e.g. max_num_seqs=max_concurrency,max_model_len=random_input_len"
450
- ),
463
+ action="store_true",
464
+ help="Resume a previous execution of this script, i.e., only run "
465
+ "parameter combinations for which there are still no output files "
466
+ "under `output_dir/experiment_name`.",
467
)
468
469
return parser
@@ -462,33 +478,52 @@ class SweepServeArgs:
478
pairs.append((a.strip(), b.strip()))
479
return pairs
480
481
+ def resolve_experiment_dir(self) -> Path:
482
+ experiment_dir = self.output_dir / self.experiment_name
483
466
-def run_main(args: SweepServeArgs):
467
- timestamp = args.resume or datetime.now().strftime("%Y%m%d_%H%M%S")
468
- output_dir = args.output_dir / timestamp
484
+ if self.resume:
485
+ if not experiment_dir.exists():
486
+ raise ValueError(f"Cannot resume from non-existent {experiment_dir=}")
487
+ else:
488
+ if experiment_dir.exists():
489
+ raise ValueError(f"Cannot overwrite existing {experiment_dir=}")
490
+
491
+ return experiment_dir
492
470
- if args.resume and not output_dir.exists():
471
- raise ValueError(f"Cannot resume from non-existent directory ({output_dir})")
493
+ @contextmanager
494
+ def run_ctx(self, experiment_dir: Path):
495
+ if self.dry_run:
496
+ yield
497
+ print(f"Experiment will be saved at: {experiment_dir}")
498
+ return
499
473
- try:
500
+ try:
501
+ yield
502
+ print(f"Experiment has been saved at: {experiment_dir}")
503
+ except BaseException as exc:
504
+ raise RuntimeError(
505
+ "The script was terminated early. Use `--resume` "
506
+ "to continue the script from its last checkpoint."
507
+ ) from exc
508
+
509
+
510
+def run_main(args: SweepServeArgs):
511
+ experiment_dir = args.resolve_experiment_dir()
512
+
513
+ with args.run_ctx(experiment_dir):
514
return run_combs(
515
serve_cmd=args.serve_cmd,
516
bench_cmd=args.bench_cmd,
517
+ link_vars=args.link_vars,
518
after_bench_cmd=args.after_bench_cmd,
519
show_stdout=args.show_stdout,
520
server_ready_timeout=args.server_ready_timeout,
521
serve_params=args.serve_params,
522
bench_params=args.bench_params,
482
- output_dir=output_dir,
523
+ experiment_dir=experiment_dir,
524
num_runs=args.num_runs,
525
dry_run=args.dry_run,
485
- link_vars=args.link_vars,
526
)
487
- except BaseException as exc:
488
- raise RuntimeError(
489
- f"The script was terminated early. Use `--resume {timestamp}` "
490
- f"to continue the script from its last checkpoint."
491
- ) from exc
527
528
529
def main(args: argparse.Namespace):
vllm/benchmarks/sweep/serve_workload.py
+22
-32
@@ -3,7 +3,6 @@
3
import argparse
4
import math
5
from dataclasses import asdict, dataclass
6
-from datetime import datetime
6
from pathlib import Path
7
from typing import ClassVar, Literal, get_args
8
@@ -59,10 +58,10 @@ def run_comb_workload(
58
*,
59
serve_comb: ParameterSweepItem,
60
bench_comb: ParameterSweepItem,
62
- output_dir: Path,
61
+ link_vars: list[tuple[str, str]],
62
+ experiment_dir: Path,
63
num_runs: int,
64
dry_run: bool,
65
- link_vars: list[tuple[str, str]],
65
workload_var: WorkloadVariable,
66
workload_value: int,
67
) -> list[dict[str, object]] | None:
@@ -73,15 +72,15 @@ def run_comb_workload(
72
bench_cmd,
73
serve_comb=serve_comb,
74
bench_comb=bench_comb_workload,
75
+ link_vars=link_vars,
76
base_path=_get_comb_base_path(
77
- output_dir,
77
+ experiment_dir,
78
serve_comb,
79
bench_comb,
80
extra_parts=("WL-", f"{workload_var}={workload_value}"),
81
),
82
num_runs=num_runs,
83
dry_run=dry_run,
84
- link_vars=link_vars,
84
)
85
86
@@ -91,12 +90,12 @@ def explore_comb_workloads(
90
*,
91
serve_comb: ParameterSweepItem,
92
bench_comb: ParameterSweepItem,
93
+ link_vars: list[tuple[str, str]],
94
workload_var: WorkloadVariable,
95
workload_iters: int,
96
- output_dir: Path,
96
+ experiment_dir: Path,
97
num_runs: int,
98
dry_run: bool,
99
- link_vars: list[tuple[str, str]],
99
):
100
print("[WL START]")
101
print(f"Serve parameters: {serve_comb.as_text() or '(None)'}")
@@ -125,10 +124,10 @@ def explore_comb_workloads(
124
bench_cmd,
125
serve_comb=serve_comb,
126
bench_comb=bench_comb | {"max_concurrency": 1},
128
- output_dir=output_dir,
127
+ link_vars=link_vars,
128
+ experiment_dir=experiment_dir,
129
num_runs=num_runs,
130
dry_run=dry_run,
131
- link_vars=link_vars,
131
workload_var=workload_var,
132
workload_value=1,
133
)
@@ -137,10 +136,10 @@ def explore_comb_workloads(
136
bench_cmd,
137
serve_comb=serve_comb,
138
bench_comb=bench_comb | {"max_concurrency": dataset_size},
140
- output_dir=output_dir,
139
+ link_vars=link_vars,
140
+ experiment_dir=experiment_dir,
141
num_runs=num_runs,
142
dry_run=dry_run,
143
- link_vars=link_vars,
143
workload_var=workload_var,
144
workload_value=dataset_size,
145
)
@@ -177,10 +176,10 @@ def explore_comb_workloads(
176
bench_cmd,
177
serve_comb=serve_comb,
178
bench_comb=bench_comb,
180
- output_dir=output_dir,
179
+ link_vars=link_vars,
180
+ experiment_dir=experiment_dir,
181
num_runs=num_runs,
182
dry_run=dry_run,
183
- link_vars=link_vars,
183
workload_var=workload_var,
184
workload_value=inter_workload_value,
185
)
@@ -201,12 +200,12 @@ def explore_combs_workloads(
200
server_ready_timeout: int,
201
serve_params: ParameterSweep,
202
bench_params: ParameterSweep,
203
+ link_vars: list[tuple[str, str]],
204
workload_var: WorkloadVariable,
205
workload_iters: int,
206
- output_dir: Path,
206
+ experiment_dir: Path,
207
num_runs: int,
208
dry_run: bool,
209
- link_vars: list[tuple[str, str]],
209
):
210
if any(bench_comb.has_param(workload_var) for bench_comb in bench_params):
211
raise ValueError(
@@ -223,7 +222,7 @@ def explore_combs_workloads(
222
server_ready_timeout=server_ready_timeout,
223
serve_comb=serve_comb,
224
bench_params=bench_params,
226
- output_dir=output_dir,
225
+ experiment_dir=experiment_dir,
226
dry_run=dry_run,
227
) as server:
228
for bench_comb in bench_params:
@@ -232,12 +231,12 @@ def explore_combs_workloads(
231
bench_cmd,
232
serve_comb=serve_comb,
233
bench_comb=bench_comb,
234
+ link_vars=link_vars,
235
workload_var=workload_var,
236
workload_iters=workload_iters,
237
- output_dir=output_dir,
237
+ experiment_dir=experiment_dir,
238
num_runs=num_runs,
239
dry_run=dry_run,
240
- link_vars=link_vars,
240
)
241
242
if comb_data is not None:
@@ -247,7 +246,7 @@ def explore_combs_workloads(
246
return None
247
248
combined_df = pd.DataFrame.from_records(all_data)
250
- combined_df.to_csv(output_dir / "summary.csv")
249
+ combined_df.to_csv(experiment_dir / "summary.csv")
250
251
return combined_df
252
@@ -298,13 +297,9 @@ class SweepServeWorkloadArgs(SweepServeArgs):
297
298
299
def run_main(args: SweepServeWorkloadArgs):
301
- timestamp = args.resume or datetime.now().strftime("%Y%m%d_%H%M%S")
302
- output_dir = args.output_dir / timestamp
303
-
304
- if args.resume and not output_dir.exists():
305
- raise ValueError(f"Cannot resume from non-existent directory ({output_dir})")
300
+ experiment_dir = args.resolve_experiment_dir()
301
307
- try:
302
+ with args.run_ctx(experiment_dir):
303
return explore_combs_workloads(
304
serve_cmd=args.serve_cmd,
305
bench_cmd=args.bench_cmd,
@@ -313,18 +308,13 @@ def run_main(args: SweepServeWorkloadArgs):
308
server_ready_timeout=args.server_ready_timeout,
309
serve_params=args.serve_params,
310
bench_params=args.bench_params,
311
+ link_vars=args.link_vars,
312
workload_var=args.workload_var,
313
workload_iters=args.workload_iters,
318
- output_dir=output_dir,
314
+ experiment_dir=experiment_dir,
315
num_runs=args.num_runs,
316
dry_run=args.dry_run,
321
- link_vars=args.link_vars,
317
)
323
- except BaseException as exc:
324
- raise RuntimeError(
325
- f"The script was terminated early. Use `--resume {timestamp}` "
326
- f"to continue the script from its last checkpoint."
327
- ) from exc
318
319
320
def main(args: argparse.Namespace):
vllm/benchmarks/sweep/startup.py
+70
-33
@@ -4,6 +4,7 @@ import argparse
4
import json
5
import shlex
6
import subprocess
7
+from contextlib import contextmanager
8
from dataclasses import dataclass
9
from datetime import datetime
10
from functools import lru_cache
@@ -111,7 +112,7 @@ def _apply_output_json(cmd: list[str], output_path: Path) -> list[str]:
112
113
114
def _get_comb_base_path(
114
- output_dir: Path,
115
+ experiment_dir: Path,
116
serve_comb: ParameterSweepItem,
117
startup_comb: ParameterSweepItem,
118
) -> Path:
@@ -120,7 +121,8 @@ def _get_comb_base_path(
121
parts.extend(("SERVE-", serve_comb.name))
122
if startup_comb:
123
parts.extend(("STARTUP-", startup_comb.name))
123
- return output_dir / sanitize_filename("-".join(parts))
124
+
125
+ return experiment_dir / sanitize_filename("-".join(parts))
126
127
128
def _get_comb_run_path(base_path: Path, run_number: int | None) -> Path:
@@ -225,7 +227,7 @@ def run_combs(
227
*,
228
serve_params: ParameterSweep,
229
startup_params: ParameterSweep,
228
- output_dir: Path,
230
+ experiment_dir: Path,
231
num_runs: int,
232
show_stdout: bool,
233
dry_run: bool,
@@ -233,7 +235,7 @@ def run_combs(
235
all_data = list[dict[str, object]]()
236
for serve_comb in serve_params:
237
for startup_comb in startup_params:
236
- base_path = _get_comb_base_path(output_dir, serve_comb, startup_comb)
238
+ base_path = _get_comb_base_path(experiment_dir, serve_comb, startup_comb)
239
comb_data = run_comb(
240
startup_cmd,
241
serve_comb=serve_comb,
@@ -250,7 +252,7 @@ def run_combs(
252
return None
253
254
combined_df = pd.DataFrame.from_records(all_data)
253
- combined_df.to_csv(output_dir / "summary.csv")
255
+ combined_df.to_csv(experiment_dir / "summary.csv")
256
return combined_df
257
258
@@ -260,11 +262,11 @@ class SweepStartupArgs:
262
serve_params: ParameterSweep
263
startup_params: ParameterSweep
264
output_dir: Path
265
+ experiment_name: str
266
num_runs: int
267
show_stdout: bool
268
dry_run: bool
266
- resume: str | None
267
- strict_params: bool
269
+ resume: bool
270
271
parser_name: ClassVar[str] = "startup"
272
parser_help: ClassVar[str] = (
@@ -286,13 +288,19 @@ class SweepStartupArgs:
288
startup_params = ParameterSweep.from_records([{}])
289
290
supported = _get_supported_startup_keys()
291
+ strict_params = args.strict_params
292
serve_params = _filter_params(
290
- serve_params, supported=supported, strict=args.strict_params
293
+ serve_params, supported=supported, strict=strict_params
294
)
295
startup_params = _filter_params(
293
- startup_params, supported=supported, strict=args.strict_params
296
+ startup_params, supported=supported, strict=strict_params
297
)
298
299
+ if args.experiment_name:
300
+ experiment_name = args.experiment_name
301
+ else:
302
+ experiment_name = datetime.now().strftime("%Y%m%d_%H%M%S")
303
+
304
if args.num_runs < 1:
305
raise ValueError("`num_runs` should be at least 1.")
306
@@ -301,11 +309,11 @@ class SweepStartupArgs:
309
serve_params=serve_params,
310
startup_params=startup_params,
311
output_dir=Path(args.output_dir),
312
+ experiment_name=experiment_name,
313
num_runs=args.num_runs,
314
show_stdout=args.show_stdout,
315
dry_run=args.dry_run,
316
resume=args.resume,
308
- strict_params=args.strict_params,
317
)
318
319
@classmethod
@@ -316,6 +324,7 @@ class SweepStartupArgs:
324
default="vllm bench startup",
325
help="The command used to run the startup benchmark.",
326
)
327
+
328
parser.add_argument(
329
"--serve-params",
330
type=str,
@@ -331,12 +340,27 @@ class SweepStartupArgs:
340
help="Path to JSON file containing parameter combinations "
341
"for the `vllm bench startup` command.",
342
)
343
+ parser.add_argument(
344
+ "--strict-params",
345
+ action="store_true",
346
+ help="If set, unknown parameters in sweep files raise an error "
347
+ "instead of being ignored.",
348
+ )
349
+
350
parser.add_argument(
351
"-o",
352
"--output-dir",
353
type=str,
354
default="results",
339
- help="The directory to which results are written.",
355
+ help="The main directory to which results are written.",
356
+ )
357
+ parser.add_argument(
358
+ "-e",
359
+ "--experiment-name",
360
+ type=str,
361
+ default=None,
362
+ help="The name of this experiment (defaults to current timestamp). "
363
+ "Results will be stored under `output_dir/experiment_name`.",
364
)
365
parser.add_argument(
366
"--num-runs",
@@ -357,43 +381,56 @@ class SweepStartupArgs:
381
)
382
parser.add_argument(
383
"--resume",
360
- type=str,
361
- default=None,
362
- help="Set this to the name of a directory under `output_dir` (which is a "
363
- "timestamp) to resume a previous execution of this script, i.e., only run "
364
- "parameter combinations for which there are still no output files.",
365
- )
366
- parser.add_argument(
367
- "--strict-params",
384
action="store_true",
369
- help="If set, unknown parameters in sweep files raise an error "
370
- "instead of being ignored.",
385
+ help="Resume a previous execution of this script, i.e., only run "
386
+ "parameter combinations for which there are still no output files "
387
+ "under `output_dir/experiment_name`.",
388
)
389
+
390
return parser
391
392
+ def resolve_experiment_dir(self) -> Path:
393
+ experiment_dir = self.output_dir / self.experiment_name
394
375
-def run_main(args: SweepStartupArgs):
376
- timestamp = args.resume or datetime.now().strftime("%Y%m%d_%H%M%S")
377
- output_dir = args.output_dir / timestamp
395
+ if self.resume:
396
+ if not experiment_dir.exists():
397
+ raise ValueError(f"Cannot resume from non-existent {experiment_dir=}")
398
+ else:
399
+ if experiment_dir.exists():
400
+ raise ValueError(f"Cannot overwrite existing {experiment_dir=}")
401
+
402
+ return experiment_dir
403
+
404
+ @contextmanager
405
+ def run_ctx(self, experiment_dir: Path):
406
+ if self.dry_run:
407
+ yield
408
+ print(f"Experiment will be saved at: {experiment_dir}")
409
+ return
410
379
- if args.resume and not output_dir.exists():
380
- raise ValueError(f"Cannot resume from non-existent directory ({output_dir})")
411
+ try:
412
+ yield
413
+ print(f"Experiment has been saved at: {experiment_dir}")
414
+ except BaseException as exc:
415
+ raise RuntimeError(
416
+ "The script was terminated early. Use `--resume` "
417
+ "to continue the script from its last checkpoint."
418
+ ) from exc
419
+
420
+
421
+def run_main(args: SweepStartupArgs):
422
+ experiment_dir = args.resolve_experiment_dir()
423
382
- try:
424
+ with args.run_ctx(experiment_dir):
425
return run_combs(
426
startup_cmd=args.startup_cmd,
427
serve_params=args.serve_params,
428
startup_params=args.startup_params,
387
- output_dir=output_dir,
429
+ experiment_dir=experiment_dir,
430
num_runs=args.num_runs,
431
show_stdout=args.show_stdout,
432
dry_run=args.dry_run,
433
)
392
- except BaseException as exc:
393
- raise RuntimeError(
394
- f"The script was terminated early. Use `--resume {timestamp}` "
395
- f"to continue the script from its last checkpoint."
396
- ) from exc
434
435
436
def main(args: argparse.Namespace):