@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 6fb2788163
Fork 0

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 14, 05:30 UTC

[CI/Build][Doc] Fully deprecate old bench scripts for serving / throughput / latency (#24411)

Signed-off-by: Ye (Charlotte) Qi <yeq@meta.com>

Ye (Charlotte) Qi committed Sep 9, 2025 at 03:02 UTC 6fb27881634d89c2e70e9e5fbad1b918c0d916cf
4 files changed +35 -2240
benchmarks/README.md
+2 -2
@@ -694,7 +694,7 @@ python -m vllm.entrypoints.openai.api_server \
694 Send requests with images:
695
696 ```bash
697 -python benchmarks/benchmark_serving.py \
697 +vllm bench serve \
698 --backend openai-chat \
699 --model Qwen/Qwen2.5-VL-7B-Instruct \
700 --dataset-name sharegpt \
@@ -721,7 +721,7 @@ python -m vllm.entrypoints.openai.api_server \
721 Send requests with videos:
722
723 ```bash
724 -python benchmarks/benchmark_serving.py \
724 +vllm bench serve \
725 --backend openai-chat \
726 --model Qwen/Qwen2.5-VL-7B-Instruct \
727 --dataset-name sharegpt \
benchmarks/benchmark_latency.py
+11 -185
@@ -1,191 +1,17 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Benchmark the latency of processing a single batch of requests."""
3 +import sys
4
5 -import argparse
6 -import dataclasses
7 -import json
8 -import os
9 -import time
10 -from typing import Any, Optional
11 -
12 -import numpy as np
13 -from tqdm import tqdm
14 -from typing_extensions import deprecated
15 -
16 -import vllm.envs as envs
17 -from benchmark_utils import convert_to_pytorch_benchmark_format, write_to_json
18 -from vllm import LLM, SamplingParams
19 -from vllm.engine.arg_utils import EngineArgs
20 -from vllm.inputs import PromptType
21 -from vllm.sampling_params import BeamSearchParams
22 -from vllm.utils import FlexibleArgumentParser
23 -
24 -
25 -def save_to_pytorch_benchmark_format(
26 - args: argparse.Namespace, results: dict[str, Any]
27 -) -> None:
28 - pt_records = convert_to_pytorch_benchmark_format(
29 - args=args,
30 - metrics={"latency": results["latencies"]},
31 - extra_info={k: results[k] for k in ["avg_latency", "percentiles"]},
32 - )
33 - if pt_records:
34 - pt_file = f"{os.path.splitext(args.output_json)[0]}.pytorch.json"
35 - write_to_json(pt_file, pt_records)
36 -
37 -
38 -@deprecated(
39 - "benchmark_latency.py is deprecated and will be removed in a "
40 - "future version. Please use 'vllm bench latency' instead.",
41 -)
42 -def main(args: argparse.Namespace):
43 - print(args)
44 -
45 - engine_args = EngineArgs.from_cli_args(args)
46 -
47 - # NOTE(woosuk): If the request cannot be processed in a single batch,
48 - # the engine will automatically process the request in multiple batches.
49 - llm = LLM(**dataclasses.asdict(engine_args))
50 - assert llm.llm_engine.model_config.max_model_len >= (
51 - args.input_len + args.output_len
52 - ), (
53 - "Please ensure that max_model_len is greater than"
54 - " the sum of input_len and output_len."
55 - )
56 -
57 - sampling_params = SamplingParams(
58 - n=args.n,
59 - temperature=1.0,
60 - top_p=1.0,
61 - ignore_eos=True,
62 - max_tokens=args.output_len,
63 - detokenize=not args.disable_detokenize,
64 - )
65 - print(sampling_params)
66 - dummy_prompt_token_ids = np.random.randint(
67 - 10000, size=(args.batch_size, args.input_len)
68 - )
69 - dummy_prompts: list[PromptType] = [
70 - {"prompt_token_ids": batch} for batch in dummy_prompt_token_ids.tolist()
71 - ]
72 -
73 - def llm_generate():
74 - if not args.use_beam_search:
75 - llm.generate(dummy_prompts, sampling_params=sampling_params, use_tqdm=False)
76 - else:
77 - llm.beam_search(
78 - dummy_prompts,
79 - BeamSearchParams(
80 - beam_width=args.n,
81 - max_tokens=args.output_len,
82 - ignore_eos=True,
83 - ),
84 - )
85 -
86 - def run_to_completion(profile_dir: Optional[str] = None):
87 - if profile_dir:
88 - llm.start_profile()
89 - llm_generate()
90 - llm.stop_profile()
91 - else:
92 - start_time = time.perf_counter()
93 - llm_generate()
94 - end_time = time.perf_counter()
95 - latency = end_time - start_time
96 - return latency
97 -
98 - print("Warming up...")
99 - for _ in tqdm(range(args.num_iters_warmup), desc="Warmup iterations"):
100 - run_to_completion(profile_dir=None)
101 -
102 - if args.profile:
103 - profile_dir = envs.VLLM_TORCH_PROFILER_DIR
104 - print(f"Profiling (results will be saved to '{profile_dir}')...")
105 - run_to_completion(profile_dir=profile_dir)
106 - return
107 -
108 - # Benchmark.
109 - latencies = []
110 - for _ in tqdm(range(args.num_iters), desc="Profiling iterations"):
111 - latencies.append(run_to_completion(profile_dir=None))
112 - latencies = np.array(latencies)
113 - percentages = [10, 25, 50, 75, 90, 99]
114 - percentiles = np.percentile(latencies, percentages)
115 - print(f"Avg latency: {np.mean(latencies)} seconds")
116 - for percentage, percentile in zip(percentages, percentiles):
117 - print(f"{percentage}% percentile latency: {percentile} seconds")
118 -
119 - # Output JSON results if specified
120 - if args.output_json:
121 - results = {
122 - "avg_latency": np.mean(latencies),
123 - "latencies": latencies.tolist(),
124 - "percentiles": dict(zip(percentages, percentiles.tolist())),
125 - }
126 - with open(args.output_json, "w") as f:
127 - json.dump(results, f, indent=4)
128 - save_to_pytorch_benchmark_format(args, results)
129 -
130 -
131 -def create_argument_parser():
132 - parser = FlexibleArgumentParser(
133 - description="Benchmark the latency of processing a single batch of "
134 - "requests till completion."
135 - )
136 - parser.add_argument("--input-len", type=int, default=32)
137 - parser.add_argument("--output-len", type=int, default=128)
138 - parser.add_argument("--batch-size", type=int, default=8)
139 - parser.add_argument(
140 - "--n",
141 - type=int,
142 - default=1,
143 - help="Number of generated sequences per prompt.",
144 - )
145 - parser.add_argument("--use-beam-search", action="store_true")
146 - parser.add_argument(
147 - "--num-iters-warmup",
148 - type=int,
149 - default=10,
150 - help="Number of iterations to run for warmup.",
151 - )
152 - parser.add_argument(
153 - "--num-iters", type=int, default=30, help="Number of iterations to run."
154 - )
155 - parser.add_argument(
156 - "--profile",
157 - action="store_true",
158 - help="profile the generation process of a single batch",
159 - )
160 - parser.add_argument(
161 - "--output-json",
162 - type=str,
163 - default=None,
164 - help="Path to save the latency results in JSON format.",
165 - )
166 - parser.add_argument(
167 - "--disable-detokenize",
168 - action="store_true",
169 - help=(
170 - "Do not detokenize responses (i.e. do not include "
171 - "detokenization time in the latency measurement)"
172 - ),
173 - )
174 -
175 - parser = EngineArgs.add_cli_args(parser)
176 - # V1 enables prefix caching by default which skews the latency
177 - # numbers. We need to disable prefix caching by default.
178 - parser.set_defaults(enable_prefix_caching=False)
5 +if __name__ == "__main__":
6 + print("""DEPRECATED: This script has been moved to the vLLM CLI.
7
180 - return parser
8 +Please use the following command instead:
9 + vllm bench latency
10
11 +For help with the new command, run:
12 + vllm bench latency --help
13
183 -if __name__ == "__main__":
184 - parser = create_argument_parser()
185 - args = parser.parse_args()
186 - if args.profile and not envs.VLLM_TORCH_PROFILER_DIR:
187 - raise OSError(
188 - "The environment variable 'VLLM_TORCH_PROFILER_DIR' is not set. "
189 - "Please set it to a valid path to use torch profiler."
190 - )
191 - main(args)
14 +Alternatively, you can run the new command directly with:
15 + python -m vllm.entrypoints.cli.main bench latency --help
16 +""")
17 + sys.exit(1)
benchmarks/benchmark_serving.py
+11 -1318
@@ -1,1324 +1,17 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -r"""Benchmark online serving throughput.
3 +import sys
4
5 -On the server side, run one of the following commands:
6 - vLLM OpenAI API server
7 - vllm serve <your_model> \
8 - --swap-space 16
9 -
10 -On the client side, run:
11 - python benchmarks/benchmark_serving.py \
12 - --backend <backend> \
13 - --model <your_model> \
14 - --dataset-name sharegpt \
15 - --dataset-path <path to dataset> \
16 - --request-rate <request_rate> \ # By default <request_rate> is inf
17 - --num-prompts <num_prompts> # By default <num_prompts> is 1000
18 -
19 - when using tgi backend, add
20 - --endpoint /generate_stream
21 - to the end of the command above.
22 -"""
23 -
24 -import argparse
25 -import asyncio
26 -import gc
27 -import json
28 -import os
29 -import random
30 -import time
31 -import warnings
32 -from collections.abc import Iterable
33 -from dataclasses import dataclass
34 -from datetime import datetime
35 -from typing import Any, Literal, Optional
36 -
37 -import numpy as np
38 -from tqdm.asyncio import tqdm
39 -from transformers import PreTrainedTokenizerBase
40 -from typing_extensions import deprecated
41 -
42 -from backend_request_func import (
43 - ASYNC_REQUEST_FUNCS,
44 - OPENAI_COMPATIBLE_BACKENDS,
45 - RequestFuncInput,
46 - RequestFuncOutput,
47 -)
48 -
49 -try:
50 - from vllm.transformers_utils.tokenizer import get_tokenizer
51 -except ImportError:
52 - from backend_request_func import get_tokenizer
53 -
54 -try:
55 - from vllm.utils import FlexibleArgumentParser
56 -except ImportError:
57 - from argparse import ArgumentParser as FlexibleArgumentParser
58 -
59 -from benchmark_dataset import (
60 - AIMODataset,
61 - ASRDataset,
62 - BurstGPTDataset,
63 - ConversationDataset,
64 - CustomDataset,
65 - HuggingFaceDataset,
66 - InstructCoderDataset,
67 - MTBenchDataset,
68 - NextEditPredictionDataset,
69 - RandomDataset,
70 - SampleRequest,
71 - ShareGPTDataset,
72 - SonnetDataset,
73 - VisionArenaDataset,
74 -)
75 -from benchmark_utils import convert_to_pytorch_benchmark_format, write_to_json
76 -from vllm.benchmarks.serve import get_request
77 -
78 -MILLISECONDS_TO_SECONDS_CONVERSION = 1000
79 -
80 -
81 -@dataclass
82 -class BenchmarkMetrics:
83 - completed: int
84 - total_input: int
85 - total_output: int
86 - request_throughput: float
87 - request_goodput: float
88 - output_throughput: float
89 - total_token_throughput: float
90 - mean_ttft_ms: float
91 - median_ttft_ms: float
92 - std_ttft_ms: float
93 - percentiles_ttft_ms: list[tuple[float, float]]
94 - mean_tpot_ms: float
95 - median_tpot_ms: float
96 - std_tpot_ms: float
97 - percentiles_tpot_ms: list[tuple[float, float]]
98 - mean_itl_ms: float
99 - median_itl_ms: float
100 - std_itl_ms: float
101 - percentiles_itl_ms: list[tuple[float, float]]
102 - # E2EL stands for end-to-end latency per request.
103 - # It is the time taken on the client side from sending
104 - # a request to receiving a complete response.
105 - mean_e2el_ms: float
106 - median_e2el_ms: float
107 - std_e2el_ms: float
108 - percentiles_e2el_ms: list[tuple[float, float]]
109 -
110 -
111 -def calculate_metrics(
112 - input_requests: list[SampleRequest],
113 - outputs: list[RequestFuncOutput],
114 - dur_s: float,
115 - tokenizer: PreTrainedTokenizerBase,
116 - selected_percentile_metrics: list[str],
117 - selected_percentiles: list[float],
118 - goodput_config_dict: dict[str, float],
119 -) -> tuple[BenchmarkMetrics, list[int]]:
120 - actual_output_lens: list[int] = []
121 - total_input = 0
122 - completed = 0
123 - good_completed = 0
124 - itls: list[float] = []
125 - tpots: list[float] = []
126 - all_tpots: list[float] = []
127 - ttfts: list[float] = []
128 - e2els: list[float] = []
129 - for i in range(len(outputs)):
130 - if outputs[i].success:
131 - output_len = outputs[i].output_tokens
132 -
133 - if not output_len:
134 - # We use the tokenizer to count the number of output tokens
135 - # for some serving backends instead of looking at
136 - # len(outputs[i].itl) since multiple output tokens may be
137 - # bundled together
138 - # Note : this may inflate the output token count slightly
139 - output_len = len(
140 - tokenizer(
141 - outputs[i].generated_text, add_special_tokens=False
142 - ).input_ids
143 - )
144 - actual_output_lens.append(output_len)
145 - total_input += input_requests[i].prompt_len
146 - tpot = 0
147 - if output_len > 1:
148 - latency_minus_ttft = outputs[i].latency - outputs[i].ttft
149 - tpot = latency_minus_ttft / (output_len - 1)
150 - tpots.append(tpot)
151 - # Note: if output_len <= 1, we regard tpot as 0 for goodput
152 - all_tpots.append(tpot)
153 - itls += outputs[i].itl
154 - ttfts.append(outputs[i].ttft)
155 - e2els.append(outputs[i].latency)
156 - completed += 1
157 - else:
158 - actual_output_lens.append(0)
159 -
160 - if goodput_config_dict:
161 - valid_metrics = []
162 - slo_values = []
163 -
164 - if "ttft" in goodput_config_dict:
165 - valid_metrics.append(ttfts)
166 - slo_values.append(
167 - goodput_config_dict["ttft"] / MILLISECONDS_TO_SECONDS_CONVERSION
168 - )
169 - if "tpot" in goodput_config_dict:
170 - valid_metrics.append(all_tpots)
171 - slo_values.append(
172 - goodput_config_dict["tpot"] / MILLISECONDS_TO_SECONDS_CONVERSION
173 - )
174 - if "e2el" in goodput_config_dict:
175 - valid_metrics.append(e2els)
176 - slo_values.append(
177 - goodput_config_dict["e2el"] / MILLISECONDS_TO_SECONDS_CONVERSION
178 - )
179 -
180 - for req_metric in zip(*valid_metrics):
181 - is_good_req = all([s >= r for s, r in zip(slo_values, req_metric)])
182 - if is_good_req:
183 - good_completed += 1
184 -
185 - if completed == 0:
186 - warnings.warn(
187 - "All requests failed. This is likely due to a misconfiguration "
188 - "on the benchmark arguments.",
189 - stacklevel=2,
190 - )
191 - metrics = BenchmarkMetrics(
192 - completed=completed,
193 - total_input=total_input,
194 - total_output=sum(actual_output_lens),
195 - request_throughput=completed / dur_s,
196 - request_goodput=good_completed / dur_s,
197 - output_throughput=sum(actual_output_lens) / dur_s,
198 - total_token_throughput=(total_input + sum(actual_output_lens)) / dur_s,
199 - mean_ttft_ms=np.mean(ttfts or 0)
200 - * 1000, # ttfts is empty if streaming is not supported by backend
201 - std_ttft_ms=np.std(ttfts or 0) * 1000,
202 - median_ttft_ms=np.median(ttfts or 0) * 1000,
203 - percentiles_ttft_ms=[
204 - (p, np.percentile(ttfts or 0, p) * 1000) for p in selected_percentiles
205 - ],
206 - mean_tpot_ms=np.mean(tpots or 0) * 1000,
207 - std_tpot_ms=np.std(tpots or 0) * 1000,
208 - median_tpot_ms=np.median(tpots or 0) * 1000,
209 - percentiles_tpot_ms=[
210 - (p, np.percentile(tpots or 0, p) * 1000) for p in selected_percentiles
211 - ],
212 - mean_itl_ms=np.mean(itls or 0) * 1000,
213 - std_itl_ms=np.std(itls or 0) * 1000,
214 - median_itl_ms=np.median(itls or 0) * 1000,
215 - percentiles_itl_ms=[
216 - (p, np.percentile(itls or 0, p) * 1000) for p in selected_percentiles
217 - ],
218 - mean_e2el_ms=np.mean(e2els or 0) * 1000,
219 - std_e2el_ms=np.std(e2els or 0) * 1000,
220 - median_e2el_ms=np.median(e2els or 0) * 1000,
221 - percentiles_e2el_ms=[
222 - (p, np.percentile(e2els or 0, p) * 1000) for p in selected_percentiles
223 - ],
224 - )
225 -
226 - return metrics, actual_output_lens
227 -
228 -
229 -async def benchmark(
230 - backend: str,
231 - api_url: str,
232 - base_url: str,
233 - model_id: str,
234 - model_name: str,
235 - tokenizer: PreTrainedTokenizerBase,
236 - input_requests: list[SampleRequest],
237 - logprobs: Optional[int],
238 - request_rate: float,
239 - burstiness: float,
240 - disable_tqdm: bool,
241 - profile: bool,
242 - selected_percentile_metrics: list[str],
243 - selected_percentiles: list[float],
244 - ignore_eos: bool,
245 - goodput_config_dict: dict[str, float],
246 - max_concurrency: Optional[int],
247 - lora_modules: Optional[Iterable[str]],
248 - extra_body: Optional[dict],
249 - ramp_up_strategy: Optional[Literal["linear", "exponential"]] = None,
250 - ramp_up_start_rps: Optional[int] = None,
251 - ramp_up_end_rps: Optional[int] = None,
252 -):
253 - if backend in ASYNC_REQUEST_FUNCS:
254 - request_func = ASYNC_REQUEST_FUNCS[backend]
255 - else:
256 - raise ValueError(f"Unknown backend: {backend}")
257 -
258 - print("Starting initial single prompt test run...")
259 - test_prompt, test_prompt_len, test_output_len, test_mm_content = (
260 - input_requests[0].prompt,
261 - input_requests[0].prompt_len,
262 - input_requests[0].expected_output_len,
263 - input_requests[0].multi_modal_data,
264 - )
265 -
266 - assert (
267 - test_mm_content is None
268 - or isinstance(test_mm_content, dict)
269 - or (
270 - isinstance(test_mm_content, list)
271 - and all(isinstance(item, dict) for item in test_mm_content)
272 - )
273 - ), "multi_modal_data must be a dict or list[dict]"
274 - test_input = RequestFuncInput(
275 - model=model_id,
276 - model_name=model_name,
277 - prompt=test_prompt,
278 - api_url=api_url,
279 - prompt_len=test_prompt_len,
280 - output_len=test_output_len,
281 - logprobs=logprobs,
282 - multi_modal_content=test_mm_content,
283 - ignore_eos=ignore_eos,
284 - extra_body=extra_body,
285 - )
286 -
287 - test_output = await request_func(request_func_input=test_input)
288 - if not test_output.success:
289 - raise ValueError(
290 - "Initial test run failed - Please make sure benchmark arguments "
291 - f"are correctly specified. Error: {test_output.error}"
292 - )
293 - else:
294 - print("Initial test run completed. Starting main benchmark run...")
295 -
296 - if lora_modules:
297 - # For each input request, choose a LoRA module at random.
298 - lora_modules = iter(
299 - [random.choice(lora_modules) for _ in range(len(input_requests))]
300 - )
301 -
302 - if profile:
303 - print("Starting profiler...")
304 - profile_input = RequestFuncInput(
305 - model=model_id,
306 - model_name=model_name,
307 - prompt=test_prompt,
308 - api_url=base_url + "/start_profile",
309 - prompt_len=test_prompt_len,
310 - output_len=test_output_len,
311 - logprobs=logprobs,
312 - multi_modal_content=test_mm_content,
313 - ignore_eos=ignore_eos,
314 - extra_body=extra_body,
315 - )
316 - profile_output = await request_func(request_func_input=profile_input)
317 - if profile_output.success:
318 - print("Profiler started")
319 -
320 - distribution = "Poisson process" if burstiness == 1.0 else "Gamma distribution"
321 -
322 - if ramp_up_strategy is not None:
323 - print(
324 - f"Traffic ramp-up strategy: {ramp_up_strategy}. Will increase "
325 - f"RPS from {ramp_up_start_rps} to {ramp_up_end_rps} RPS over "
326 - "the duration of the benchmark."
327 - )
328 - else:
329 - print(f"Traffic request rate: {request_rate} RPS.")
330 -
331 - print(f"Burstiness factor: {burstiness} ({distribution})")
332 - print(f"Maximum request concurrency: {max_concurrency}")
333 -
334 - pbar = None if disable_tqdm else tqdm(total=len(input_requests))
335 -
336 - # This can be used once the minimum Python version is 3.10 or higher,
337 - # and it will simplify the code in limited_request_func.
338 - # semaphore = (asyncio.Semaphore(max_concurrency)
339 - # if max_concurrency else contextlib.nullcontext())
340 - semaphore = asyncio.Semaphore(max_concurrency) if max_concurrency else None
341 -
342 - async def limited_request_func(request_func_input, pbar):
343 - if semaphore is None:
344 - return await request_func(request_func_input=request_func_input, pbar=pbar)
345 - async with semaphore:
346 - return await request_func(request_func_input=request_func_input, pbar=pbar)
347 -
348 - benchmark_start_time = time.perf_counter()
349 - tasks: list[asyncio.Task] = []
350 -
351 - rps_change_events = []
352 - last_int_rps = -1
353 - if ramp_up_strategy is not None and ramp_up_start_rps is not None:
354 - last_int_rps = ramp_up_start_rps
355 - rps_change_events.append(
356 - {
357 - "rps": last_int_rps,
358 - "timestamp": datetime.now().isoformat(),
359 - }
360 - )
361 -
362 - async for request, current_request_rate in get_request(
363 - input_requests,
364 - request_rate,
365 - burstiness,
366 - ramp_up_strategy,
367 - ramp_up_start_rps,
368 - ramp_up_end_rps,
369 - ):
370 - if ramp_up_strategy is not None:
371 - current_int_rps = int(current_request_rate)
372 - if current_int_rps > last_int_rps:
373 - timestamp = datetime.now().isoformat()
374 - for rps_val in range(last_int_rps + 1, current_int_rps + 1):
375 - rps_change_events.append({"rps": rps_val, "timestamp": timestamp})
376 - last_int_rps = current_int_rps
377 -
378 - prompt, prompt_len, output_len, mm_content, request_id = (
379 - request.prompt,
380 - request.prompt_len,
381 - request.expected_output_len,
382 - request.multi_modal_data,
383 - request.request_id,
384 - )
385 - req_model_id, req_model_name = model_id, model_name
386 - if lora_modules:
387 - req_lora_module = next(lora_modules)
388 - req_model_id, req_model_name = req_lora_module, req_lora_module
389 -
390 - request_func_input = RequestFuncInput(
391 - model=req_model_id,
392 - model_name=req_model_name,
393 - prompt=prompt,
394 - api_url=api_url,
395 - prompt_len=prompt_len,
396 - output_len=output_len,
397 - logprobs=logprobs,
398 - multi_modal_content=mm_content,
399 - ignore_eos=ignore_eos,
400 - extra_body=extra_body,
401 - request_id=request_id,
402 - )
403 - task = limited_request_func(request_func_input=request_func_input, pbar=pbar)
404 - tasks.append(asyncio.create_task(task))
405 - outputs: list[RequestFuncOutput] = await asyncio.gather(*tasks)
406 -
407 - if pbar is not None:
408 - pbar.close()
409 -
410 - benchmark_duration = time.perf_counter() - benchmark_start_time
411 -
412 - metrics, actual_output_lens = calculate_metrics(
413 - input_requests=input_requests,
414 - outputs=outputs,
415 - dur_s=benchmark_duration,
416 - tokenizer=tokenizer,
417 - selected_percentile_metrics=selected_percentile_metrics,
418 - selected_percentiles=selected_percentiles,
419 - goodput_config_dict=goodput_config_dict,
420 - )
421 -
422 - print("{s:{c}^{n}}".format(s=" Serving Benchmark Result ", n=50, c="="))
423 - print("{:<40} {:<10}".format("Successful requests:", metrics.completed))
424 - if max_concurrency is not None:
425 - print("{:<40} {:<10}".format("Maximum request concurrency:", max_concurrency))
426 - if request_rate != float("inf"):
427 - print("{:<40} {:<10.2f}".format("Request rate configured (RPS):", request_rate))
428 - print("{:<40} {:<10.2f}".format("Benchmark duration (s):", benchmark_duration))
429 - print("{:<40} {:<10}".format("Total input tokens:", metrics.total_input))
430 - print("{:<40} {:<10}".format("Total generated tokens:", metrics.total_output))
431 - print(
432 - "{:<40} {:<10.2f}".format(
433 - "Request throughput (req/s):", metrics.request_throughput
434 - )
435 - )
436 - if goodput_config_dict:
437 - print(
438 - "{:<40} {:<10.2f}".format(
439 - "Request goodput (req/s):", metrics.request_goodput
440 - )
441 - )
442 - print(
443 - "{:<40} {:<10.2f}".format(
444 - "Output token throughput (tok/s):", metrics.output_throughput
445 - )
446 - )
447 - print(
448 - "{:<40} {:<10.2f}".format(
449 - "Total Token throughput (tok/s):", metrics.total_token_throughput
450 - )
451 - )
452 -
453 - result = {
454 - "duration": benchmark_duration,
455 - "completed": metrics.completed,
456 - "total_input_tokens": metrics.total_input,
457 - "total_output_tokens": metrics.total_output,
458 - "request_throughput": metrics.request_throughput,
459 - "request_goodput": metrics.request_goodput if goodput_config_dict else None,
460 - "output_throughput": metrics.output_throughput,
461 - "total_token_throughput": metrics.total_token_throughput,
462 - "input_lens": [output.prompt_len for output in outputs],
463 - "output_lens": actual_output_lens,
464 - "ttfts": [output.ttft for output in outputs],
465 - "itls": [output.itl for output in outputs],
466 - "generated_texts": [output.generated_text for output in outputs],
467 - "errors": [output.error for output in outputs],
468 - }
469 -
470 - if rps_change_events:
471 - result["rps_change_events"] = rps_change_events
472 -
473 - def process_one_metric(
474 - # E.g., "ttft"
475 - metric_attribute_name: str,
476 - # E.g., "TTFT"
477 - metric_name: str,
478 - # E.g., "Time to First Token"
479 - metric_header: str,
480 - ):
481 - # This function prints and adds statistics of the specified
482 - # metric.
483 - if metric_attribute_name not in selected_percentile_metrics:
484 - return
485 - print("{s:{c}^{n}}".format(s=metric_header, n=50, c="-"))
486 - print(
487 - "{:<40} {:<10.2f}".format(
488 - f"Mean {metric_name} (ms):",
489 - getattr(metrics, f"mean_{metric_attribute_name}_ms"),
490 - )
491 - )
492 - print(
493 - "{:<40} {:<10.2f}".format(
494 - f"Median {metric_name} (ms):",
495 - getattr(metrics, f"median_{metric_attribute_name}_ms"),
496 - )
497 - )
498 - result[f"mean_{metric_attribute_name}_ms"] = getattr(
499 - metrics, f"mean_{metric_attribute_name}_ms"
500 - )
501 - result[f"median_{metric_attribute_name}_ms"] = getattr(
502 - metrics, f"median_{metric_attribute_name}_ms"
503 - )
504 - result[f"std_{metric_attribute_name}_ms"] = getattr(
505 - metrics, f"std_{metric_attribute_name}_ms"
506 - )
507 - for p, value in getattr(metrics, f"percentiles_{metric_attribute_name}_ms"):
508 - p_word = str(int(p)) if int(p) == p else str(p)
509 - print("{:<40} {:<10.2f}".format(f"P{p_word} {metric_name} (ms):", value))
510 - result[f"p{p_word}_{metric_attribute_name}_ms"] = value
511 -
512 - process_one_metric("ttft", "TTFT", "Time to First Token")
513 - process_one_metric("tpot", "TPOT", "Time per Output Token (excl. 1st token)")
514 - process_one_metric("itl", "ITL", "Inter-token Latency")
515 - process_one_metric("e2el", "E2EL", "End-to-end Latency")
516 -
517 - print("=" * 50)
518 -
519 - if profile:
520 - print("Stopping profiler...")
521 - profile_input = RequestFuncInput(
522 - model=model_id,
523 - prompt=test_prompt,
524 - api_url=base_url + "/stop_profile",
525 - prompt_len=test_prompt_len,
526 - output_len=test_output_len,
527 - logprobs=logprobs,
528 - )
529 - profile_output = await request_func(request_func_input=profile_input)
530 - if profile_output.success:
531 - print("Profiler stopped")
532 -
533 - return result
534 -
535 -
536 -def check_goodput_args(args):
537 - # Check and parse goodput arguments
538 - goodput_config_dict = {}
539 - VALID_NAMES = ["ttft", "tpot", "e2el"]
540 - if args.goodput:
541 - goodput_config_dict = parse_goodput(args.goodput)
542 - for slo_name, slo_val in goodput_config_dict.items():
543 - if slo_name not in VALID_NAMES:
544 - raise ValueError(
545 - f"Invalid metric name found, {slo_name}: {slo_val}. "
546 - "The service level objective name should be one of "
547 - f"{str(VALID_NAMES)}. "
548 - )
549 - if slo_val < 0:
550 - raise ValueError(
551 - f"Invalid value found, {slo_name}: {slo_val}. "
552 - "The service level objective value should be "
553 - "non-negative."
554 - )
555 - return goodput_config_dict
556 -
557 -
558 -def parse_goodput(slo_pairs):
559 - goodput_config_dict = {}
560 - try:
561 - for slo_pair in slo_pairs:
562 - slo_name, slo_val = slo_pair.split(":")
563 - goodput_config_dict[slo_name] = float(slo_val)
564 - except ValueError as err:
565 - raise argparse.ArgumentTypeError(
566 - "Invalid format found for service level objectives. "
567 - 'Specify service level objectives for goodput as "KEY:VALUE" '
568 - "pairs, where the key is a metric name, and the value is a "
569 - "number in milliseconds."
570 - ) from err
571 - return goodput_config_dict
572 -
573 -
574 -def save_to_pytorch_benchmark_format(
575 - args: argparse.Namespace, results: dict[str, Any], file_name: str
576 -) -> None:
577 - metrics = [
578 - "median_ttft_ms",
579 - "mean_ttft_ms",
580 - "std_ttft_ms",
581 - "p99_ttft_ms",
582 - "mean_tpot_ms",
583 - "median_tpot_ms",
584 - "std_tpot_ms",
585 - "p99_tpot_ms",
586 - "median_itl_ms",
587 - "mean_itl_ms",
588 - "std_itl_ms",
589 - "p99_itl_ms",
590 - ]
591 - # These raw data might be useful, but they are rather big. They can be added
592 - # later if needed
593 - ignored_metrics = ["ttfts", "itls", "generated_texts", "errors"]
594 - pt_records = convert_to_pytorch_benchmark_format(
595 - args=args,
596 - metrics={k: [results[k]] for k in metrics},
597 - extra_info={
598 - k: results[k]
599 - for k in results
600 - if k not in metrics and k not in ignored_metrics
601 - },
602 - )
603 - if pt_records:
604 - # Don't use json suffix here as we don't want CI to pick it up
605 - pt_file = f"{os.path.splitext(file_name)[0]}.pytorch.json"
606 - write_to_json(pt_file, pt_records)
607 -
608 -
609 -@deprecated(
610 - "benchmark_serving.py is deprecated and will be removed in a future "
611 - "version. Please use 'vllm bench serve' instead.",
612 -)
613 -def main(args: argparse.Namespace):
614 - print(args)
615 - random.seed(args.seed)
616 - np.random.seed(args.seed)
617 -
618 - backend = args.backend
619 - model_id = args.model
620 - model_name = args.served_model_name
621 - tokenizer_id = args.tokenizer if args.tokenizer is not None else args.model
622 - tokenizer_mode = args.tokenizer_mode
623 -
624 - # Validate ramp-up arguments
625 - if args.ramp_up_strategy is not None:
626 - if args.request_rate != float("inf"):
627 - raise ValueError(
628 - "When using ramp-up, do not specify --request-rate. "
629 - "The request rate will be controlled by ramp-up parameters. "
630 - "Please remove the --request-rate argument."
631 - )
632 - if args.ramp_up_start_rps is None or args.ramp_up_end_rps is None:
633 - raise ValueError(
634 - "When using --ramp-up-strategy, both --ramp-up-start-rps and "
635 - "--ramp-up-end-rps must be specified"
636 - )
637 - if args.ramp_up_start_rps < 0 or args.ramp_up_end_rps < 0:
638 - raise ValueError("Ramp-up start and end RPS must be non-negative")
639 - if args.ramp_up_start_rps > args.ramp_up_end_rps:
640 - raise ValueError("Ramp-up start RPS must be less than end RPS")
641 - if args.ramp_up_strategy == "exponential" and args.ramp_up_start_rps == 0:
642 - raise ValueError("For exponential ramp-up, the start RPS cannot be 0.")
643 -
644 - if args.base_url is not None:
645 - api_url = f"{args.base_url}{args.endpoint}"
646 - base_url = f"{args.base_url}"
647 - else:
648 - api_url = f"http://{args.host}:{args.port}{args.endpoint}"
649 - base_url = f"http://{args.host}:{args.port}"
650 -
651 - tokenizer = get_tokenizer(
652 - tokenizer_id,
653 - tokenizer_mode=tokenizer_mode,
654 - trust_remote_code=args.trust_remote_code,
655 - )
656 -
657 - if args.dataset_name is None:
658 - raise ValueError(
659 - "Please specify '--dataset-name' and the corresponding "
660 - "'--dataset-path' if required."
661 - )
662 -
663 - if args.dataset_name == "custom":
664 - dataset = CustomDataset(dataset_path=args.dataset_path)
665 - input_requests = dataset.sample(
666 - num_requests=args.num_prompts,
667 - tokenizer=tokenizer,
668 - output_len=args.custom_output_len,
669 - skip_chat_template=args.custom_skip_chat_template,
670 - request_id_prefix=args.request_id_prefix,
671 - )
672 -
673 - elif args.dataset_name == "sonnet":
674 - dataset = SonnetDataset(dataset_path=args.dataset_path)
675 - # For the "sonnet" dataset, formatting depends on the backend.
676 - if args.backend == "openai-chat":
677 - input_requests = dataset.sample(
678 - num_requests=args.num_prompts,
679 - input_len=args.sonnet_input_len,
680 - output_len=args.sonnet_output_len,
681 - prefix_len=args.sonnet_prefix_len,
682 - tokenizer=tokenizer,
683 - return_prompt_formatted=False,
684 - request_id_prefix=args.request_id_prefix,
685 - )
686 - else:
687 - assert tokenizer.chat_template or tokenizer.default_chat_template, (
688 - "Tokenizer/model must have chat template for sonnet dataset."
689 - )
690 - input_requests = dataset.sample(
691 - num_requests=args.num_prompts,
692 - input_len=args.sonnet_input_len,
693 - output_len=args.sonnet_output_len,
694 - prefix_len=args.sonnet_prefix_len,
695 - tokenizer=tokenizer,
696 - return_prompt_formatted=True,
697 - request_id_prefix=args.request_id_prefix,
698 - )
699 -
700 - elif args.dataset_name == "hf":
701 - # all following datasets are implemented from the
702 - # HuggingFaceDataset base class
703 - if args.dataset_path in VisionArenaDataset.SUPPORTED_DATASET_PATHS:
704 - dataset_class = VisionArenaDataset
705 - args.hf_split = "train"
706 - args.hf_subset = None
707 - elif args.dataset_path in InstructCoderDataset.SUPPORTED_DATASET_PATHS:
708 - dataset_class = InstructCoderDataset
709 - args.hf_split = "train"
710 - elif args.dataset_path in MTBenchDataset.SUPPORTED_DATASET_PATHS:
711 - dataset_class = MTBenchDataset
712 - args.hf_split = "train"
713 - elif args.dataset_path in ConversationDataset.SUPPORTED_DATASET_PATHS:
714 - dataset_class = ConversationDataset
715 - elif args.dataset_path in AIMODataset.SUPPORTED_DATASET_PATHS:
716 - dataset_class = AIMODataset
717 - args.hf_split = "train"
718 - elif args.dataset_path in NextEditPredictionDataset.SUPPORTED_DATASET_PATHS: # noqa: E501
719 - dataset_class = NextEditPredictionDataset
720 - args.hf_split = "train"
721 - elif args.dataset_path in ASRDataset.SUPPORTED_DATASET_PATHS:
722 - dataset_class = ASRDataset
723 - args.hf_split = "train"
724 - else:
725 - supported_datasets = set(
726 - [
727 - dataset_name
728 - for cls in HuggingFaceDataset.__subclasses__()
729 - for dataset_name in cls.SUPPORTED_DATASET_PATHS
730 - ]
731 - )
732 - raise ValueError(
733 - f"Unsupported dataset path: {args.dataset_path}. "
734 - "Huggingface dataset only supports dataset_path"
735 - f" from one of following: {supported_datasets}. "
736 - "Please consider contributing if you would "
737 - "like to add support for additional dataset formats."
738 - )
739 -
740 - if dataset_class.IS_MULTIMODAL and backend not in [
741 - "openai-chat",
742 - "openai-audio",
743 - ]:
744 - # multi-modal benchmark is only available on OpenAI Chat backend.
745 - raise ValueError(
746 - "Multi-modal content is only supported on 'openai-chat' and "
747 - "'openai-audio' backend."
748 - )
749 - input_requests = dataset_class(
750 - dataset_path=args.dataset_path,
751 - dataset_subset=args.hf_subset,
752 - dataset_split=args.hf_split,
753 - random_seed=args.seed,
754 - no_stream=args.no_stream,
755 - ).sample(
756 - num_requests=args.num_prompts,
757 - tokenizer=tokenizer,
758 - output_len=args.hf_output_len,
759 - request_id_prefix=args.request_id_prefix,
760 - )
761 -
762 - else:
763 - # For datasets that follow a similar structure, use a mapping.
764 - dataset_mapping = {
765 - "sharegpt": lambda: ShareGPTDataset(
766 - random_seed=args.seed, dataset_path=args.dataset_path
767 - ).sample(
768 - tokenizer=tokenizer,
769 - num_requests=args.num_prompts,
770 - output_len=args.sharegpt_output_len,
771 - request_id_prefix=args.request_id_prefix,
772 - ),
773 - "burstgpt": lambda: BurstGPTDataset(
774 - random_seed=args.seed, dataset_path=args.dataset_path
775 - ).sample(
776 - tokenizer=tokenizer,
777 - num_requests=args.num_prompts,
778 - request_id_prefix=args.request_id_prefix,
779 - ),
780 - "random": lambda: RandomDataset(dataset_path=args.dataset_path).sample(
781 - tokenizer=tokenizer,
782 - num_requests=args.num_prompts,
783 - prefix_len=args.random_prefix_len,
784 - input_len=args.random_input_len,
785 - output_len=args.random_output_len,
786 - range_ratio=args.random_range_ratio,
787 - request_id_prefix=args.request_id_prefix,
788 - ),
789 - }
790 -
791 - try:
792 - input_requests = dataset_mapping[args.dataset_name]()
793 - except KeyError as err:
794 - raise ValueError(f"Unknown dataset: {args.dataset_name}") from err
795 - goodput_config_dict = check_goodput_args(args)
796 -
797 - # Collect the sampling parameters.
798 - sampling_params = {
799 - k: v
800 - for k, v in {
801 - "top_p": args.top_p,
802 - "top_k": args.top_k,
803 - "min_p": args.min_p,
804 - "temperature": args.temperature,
805 - }.items()
806 - if v is not None
807 - }
808 -
809 - # Sampling parameters are only supported by openai-compatible backend.
810 - if sampling_params and args.backend not in OPENAI_COMPATIBLE_BACKENDS:
811 - raise ValueError(
812 - "Sampling parameters are only supported by openai-compatible backends."
813 - )
814 -
815 - if "temperature" not in sampling_params:
816 - sampling_params["temperature"] = 0.0 # Default to greedy decoding.
817 -
818 - if args.backend == "llama.cpp":
819 - # Disable prompt caching in llama.cpp backend
820 - sampling_params["cache_prompt"] = False
821 -
822 - # Avoid GC processing "static" data - reduce pause times.
823 - gc.collect()
824 - gc.freeze()
825 -
826 - benchmark_result = asyncio.run(
827 - benchmark(
828 - backend=backend,
829 - api_url=api_url,
830 - base_url=base_url,
831 - model_id=model_id,
832 - model_name=model_name,
833 - tokenizer=tokenizer,
834 - input_requests=input_requests,
835 - logprobs=args.logprobs,
836 - request_rate=args.request_rate,
837 - burstiness=args.burstiness,
838 - disable_tqdm=args.disable_tqdm,
839 - profile=args.profile,
840 - selected_percentile_metrics=args.percentile_metrics.split(","),
841 - selected_percentiles=[float(p) for p in args.metric_percentiles.split(",")],
842 - ignore_eos=args.ignore_eos,
843 - goodput_config_dict=goodput_config_dict,
844 - max_concurrency=args.max_concurrency,
845 - lora_modules=args.lora_modules,
846 - extra_body=sampling_params,
847 - ramp_up_strategy=args.ramp_up_strategy,
848 - ramp_up_start_rps=args.ramp_up_start_rps,
849 - ramp_up_end_rps=args.ramp_up_end_rps,
850 - )
851 - )
852 -
853 - # Save config and results to json
854 - if args.save_result or args.append_result:
855 - result_json: dict[str, Any] = {}
856 -
857 - # Setup
858 - current_dt = datetime.now().strftime("%Y%m%d-%H%M%S")
859 - result_json["date"] = current_dt
860 - result_json["backend"] = backend
861 - result_json["model_id"] = model_id
862 - result_json["tokenizer_id"] = tokenizer_id
863 - result_json["num_prompts"] = args.num_prompts
864 -
865 - # Metadata
866 - if args.metadata:
867 - for item in args.metadata:
868 - if "=" in item:
869 - kvstring = item.split("=")
870 - result_json[kvstring[0].strip()] = kvstring[1].strip()
871 - else:
872 - raise ValueError(
873 - "Invalid metadata format. Please use KEY=VALUE format."
874 - )
875 - # Traffic
876 - result_json["request_rate"] = (
877 - args.request_rate if args.request_rate < float("inf") else "inf"
878 - )
879 - result_json["burstiness"] = args.burstiness
880 - result_json["max_concurrency"] = args.max_concurrency
881 -
882 - if args.ramp_up_strategy is not None:
883 - result_json["ramp_up_strategy"] = args.ramp_up_strategy
884 - result_json["ramp_up_start_rps"] = args.ramp_up_start_rps
885 - result_json["ramp_up_end_rps"] = args.ramp_up_end_rps
886 -
887 - # Merge with benchmark result
888 - result_json = {**result_json, **benchmark_result}
889 -
890 - if not args.save_detailed:
891 - # Remove fields with too many data points
892 - for field in [
893 - "input_lens",
894 - "output_lens",
895 - "ttfts",
896 - "itls",
897 - "generated_texts",
898 - "errors",
899 - ]:
900 - if field in result_json:
901 - del result_json[field]
902 - if field in benchmark_result:
903 - del benchmark_result[field]
904 -
905 - # Save to file
906 - base_model_id = model_id.split("/")[-1]
907 - max_concurrency_str = (
908 - f"-concurrency{args.max_concurrency}"
909 - if args.max_concurrency is not None
910 - else ""
911 - )
912 - if args.ramp_up_strategy is not None:
913 - file_name = f"{backend}-ramp-up-{args.ramp_up_strategy}-{args.ramp_up_start_rps}qps-{args.ramp_up_end_rps}qps{max_concurrency_str}-{base_model_id}-{current_dt}.json" # noqa
914 - else:
915 - file_name = f"{backend}-{args.request_rate}qps{max_concurrency_str}-{base_model_id}-{current_dt}.json" # noqa
916 - if args.result_filename:
917 - file_name = args.result_filename
918 - if args.result_dir:
919 - os.makedirs(args.result_dir, exist_ok=True)
920 - file_name = os.path.join(args.result_dir, file_name)
921 - with open(
922 - file_name, mode="a+" if args.append_result else "w", encoding="utf-8"
923 - ) as outfile:
924 - # Append a newline.
925 - if args.append_result and outfile.tell() != 0:
926 - outfile.write("\n")
927 - json.dump(result_json, outfile)
928 - save_to_pytorch_benchmark_format(args, result_json, file_name)
929 -
930 -
931 -def create_argument_parser():
932 - parser = FlexibleArgumentParser(
933 - description="Benchmark the online serving throughput."
934 - )
935 - parser.add_argument(
936 - "--backend",
937 - type=str,
938 - default="vllm",
939 - choices=list(ASYNC_REQUEST_FUNCS.keys()),
940 - )
941 - parser.add_argument(
942 - "--base-url",
943 - type=str,
944 - default=None,
945 - help="Server or API base url if not using http host and port.",
946 - )
947 - # Use 127.0.0.1 here instead of localhost to force the use of ipv4
948 - parser.add_argument("--host", type=str, default="127.0.0.1")
949 - parser.add_argument("--port", type=int, default=8000)
950 - parser.add_argument(
951 - "--endpoint",
952 - type=str,
953 - default="/v1/completions",
954 - help="API endpoint.",
955 - )
956 - parser.add_argument(
957 - "--dataset-name",
958 - type=str,
959 - default="sharegpt",
960 - choices=["sharegpt", "burstgpt", "sonnet", "random", "hf", "custom"],
961 - help="Name of the dataset to benchmark on.",
962 - )
963 - parser.add_argument(
964 - "--dataset-path",
965 - type=str,
966 - default=None,
967 - help="Path to the sharegpt/sonnet dataset. "
968 - "Or the huggingface dataset ID if using HF dataset.",
969 - )
970 - parser.add_argument(
971 - "--no-stream",
972 - action="store_true",
973 - help="Do not load the dataset in streaming mode.",
974 - )
975 - parser.add_argument(
976 - "--max-concurrency",
977 - type=int,
978 - default=None,
979 - help="Maximum number of concurrent requests. This can be used "
980 - "to help simulate an environment where a higher level component "
981 - "is enforcing a maximum number of concurrent requests. While the "
982 - "--request-rate argument controls the rate at which requests are "
983 - "initiated, this argument will control how many are actually allowed "
984 - "to execute at a time. This means that when used in combination, the "
985 - "actual request rate may be lower than specified with --request-rate, "
986 - "if the server is not processing requests fast enough to keep up.",
987 - )
988 -
989 - parser.add_argument(
990 - "--model",
991 - type=str,
992 - required=True,
993 - help="Name of the model.",
994 - )
995 - parser.add_argument(
996 - "--tokenizer",
997 - type=str,
998 - help="Name or path of the tokenizer, if not using the default tokenizer.", # noqa: E501
999 - )
1000 - parser.add_argument("--use-beam-search", action="store_true")
1001 - parser.add_argument(
1002 - "--num-prompts",
1003 - type=int,
1004 - default=1000,
1005 - help="Number of prompts to process.",
1006 - )
1007 - parser.add_argument(
1008 - "--logprobs",
1009 - type=int,
1010 - default=None,
1011 - help=(
1012 - "Number of logprobs-per-token to compute & return as part of "
1013 - "the request. If unspecified, then either (1) if beam search "
1014 - "is disabled, no logprobs are computed & a single dummy "
1015 - "logprob is returned for each token; or (2) if beam search "
1016 - "is enabled 1 logprob per token is computed"
1017 - ),
1018 - )
1019 - parser.add_argument(
1020 - "--request-rate",
1021 - type=float,
1022 - default=float("inf"),
1023 - help="Number of requests per second. If this is inf, "
1024 - "then all the requests are sent at time 0. "
1025 - "Otherwise, we use Poisson process or gamma distribution "
1026 - "to synthesize the request arrival times.",
1027 - )
1028 - parser.add_argument(
1029 - "--burstiness",
1030 - type=float,
1031 - default=1.0,
1032 - help="Burstiness factor of the request generation. "
1033 - "Only take effect when request_rate is not inf. "
1034 - "Default value is 1, which follows Poisson process. "
1035 - "Otherwise, the request intervals follow a gamma distribution. "
1036 - "A lower burstiness value (0 < burstiness < 1) results in more "
1037 - "bursty requests. A higher burstiness value (burstiness > 1) "
1038 - "results in a more uniform arrival of requests.",
1039 - )
1040 - parser.add_argument("--seed", type=int, default=0)
1041 - parser.add_argument(
1042 - "--trust-remote-code",
1043 - action="store_true",
1044 - help="Trust remote code from huggingface",
1045 - )
1046 - parser.add_argument(
1047 - "--disable-tqdm",
1048 - action="store_true",
1049 - help="Specify to disable tqdm progress bar.",
1050 - )
1051 - parser.add_argument(
1052 - "--profile",
1053 - action="store_true",
1054 - help="Use Torch Profiler. The endpoint must be launched with "
1055 - "VLLM_TORCH_PROFILER_DIR to enable profiler.",
1056 - )
1057 - parser.add_argument(
1058 - "--save-result",
1059 - action="store_true",
1060 - help="Specify to save benchmark results to a json file",
1061 - )
1062 - parser.add_argument(
1063 - "--save-detailed",
1064 - action="store_true",
1065 - help="When saving the results, whether to include per request "
1066 - "information such as response, error, ttfs, tpots, etc.",
1067 - )
1068 - parser.add_argument(
1069 - "--append-result",
1070 - action="store_true",
1071 - help="Append the benchmark result to the existing json file.",
1072 - )
1073 - parser.add_argument(
1074 - "--metadata",
1075 - metavar="KEY=VALUE",
1076 - nargs="*",
1077 - help="Key-value pairs (e.g, --metadata version=0.3.3 tp=1) "
1078 - "for metadata of this run to be saved in the result JSON file "
1079 - "for record keeping purposes.",
1080 - )
1081 - parser.add_argument(
1082 - "--result-dir",
1083 - type=str,
1084 - default=None,
1085 - help="Specify directory to save benchmark json results."
1086 - "If not specified, results are saved in the current directory.",
1087 - )
1088 - parser.add_argument(
1089 - "--result-filename",
1090 - type=str,
1091 - default=None,
1092 - help="Specify the filename to save benchmark json results."
1093 - "If not specified, results will be saved in "
1094 - "{backend}-{args.request_rate}qps-{base_model_id}-{current_dt}.json"
1095 - " format.",
1096 - )
1097 - parser.add_argument(
1098 - "--ignore-eos",
1099 - action="store_true",
1100 - help="Set ignore_eos flag when sending the benchmark request."
1101 - "Warning: ignore_eos is not supported in deepspeed_mii and tgi.",
1102 - )
1103 - parser.add_argument(
1104 - "--percentile-metrics",
1105 - type=str,
1106 - default="ttft,tpot,itl",
1107 - help="Comma-separated list of selected metrics to report percentiles. "
1108 - "This argument specifies the metrics to report percentiles. "
1109 - 'Allowed metric names are "ttft", "tpot", "itl", "e2el". '
1110 - 'Default value is "ttft,tpot,itl".',
1111 - )
1112 - parser.add_argument(
1113 - "--metric-percentiles",
1114 - type=str,
1115 - default="99",
1116 - help="Comma-separated list of percentiles for selected metrics. "
1117 - 'To report 25-th, 50-th, and 75-th percentiles, use "25,50,75". '
1118 - 'Default value is "99". '
1119 - 'Use "--percentile-metrics" to select metrics.',
1120 - )
1121 - parser.add_argument(
1122 - "--goodput",
1123 - nargs="+",
1124 - required=False,
1125 - help='Specify service level objectives for goodput as "KEY:VALUE" '
1126 - "pairs, where the key is a metric name, and the value is in "
1127 - 'milliseconds. Multiple "KEY:VALUE" pairs can be provided, '
1128 - "separated by spaces. Allowed request level metric names are "
1129 - '"ttft", "tpot", "e2el". For more context on the definition of '
1130 - "goodput, refer to DistServe paper: https://arxiv.org/pdf/2401.09670 "
1131 - "and the blog: https://hao-ai-lab.github.io/blogs/distserve",
1132 - )
1133 - parser.add_argument(
1134 - "--request-id-prefix",
1135 - type=str,
1136 - required=False,
1137 - default="benchmark-serving",
1138 - help="Specify the prefix of request id.",
1139 - )
1140 -
1141 - # group for dataset specific arguments
1142 - custom_group = parser.add_argument_group("custom dataset options")
1143 - custom_group.add_argument(
1144 - "--custom-output-len",
1145 - type=int,
1146 - default=256,
1147 - help="Number of output tokens per request, used only for custom dataset.",
1148 - )
1149 - custom_group.add_argument(
1150 - "--custom-skip-chat-template",
1151 - action="store_true",
1152 - help="Skip applying chat template to prompt, used only for custom dataset.",
1153 - )
1154 -
1155 - sonnet_group = parser.add_argument_group("sonnet dataset options")
1156 - sonnet_group.add_argument(
1157 - "--sonnet-input-len",
1158 - type=int,
1159 - default=550,
1160 - help="Number of input tokens per request, used only for sonnet dataset.",
1161 - )
1162 - sonnet_group.add_argument(
1163 - "--sonnet-output-len",
1164 - type=int,
1165 - default=150,
1166 - help="Number of output tokens per request, used only for sonnet dataset.",
1167 - )
1168 - sonnet_group.add_argument(
1169 - "--sonnet-prefix-len",
1170 - type=int,
1171 - default=200,
1172 - help="Number of prefix tokens per request, used only for sonnet dataset.",
1173 - )
1174 -
1175 - sharegpt_group = parser.add_argument_group("sharegpt dataset options")
1176 - sharegpt_group.add_argument(
1177 - "--sharegpt-output-len",
1178 - type=int,
1179 - default=None,
1180 - help="Output length for each request. Overrides the output length "
1181 - "from the ShareGPT dataset.",
1182 - )
1183 -
1184 - random_group = parser.add_argument_group("random dataset options")
1185 - random_group.add_argument(
1186 - "--random-input-len",
1187 - type=int,
1188 - default=1024,
1189 - help="Number of input tokens per request, used only for random sampling.",
1190 - )
1191 - random_group.add_argument(
1192 - "--random-output-len",
1193 - type=int,
1194 - default=128,
1195 - help="Number of output tokens per request, used only for random sampling.",
1196 - )
1197 - random_group.add_argument(
1198 - "--random-range-ratio",
1199 - type=float,
1200 - default=0.0,
1201 - help="Range ratio for sampling input/output length, "
1202 - "used only for random sampling. Must be in the range [0, 1) to define "
1203 - "a symmetric sampling range"
1204 - "[length * (1 - range_ratio), length * (1 + range_ratio)].",
1205 - )
1206 - random_group.add_argument(
1207 - "--random-prefix-len",
1208 - type=int,
1209 - default=0,
1210 - help=(
1211 - "Number of fixed prefix tokens before the random context "
1212 - "in a request. "
1213 - "The total input length is the sum of `random-prefix-len` and "
1214 - "a random "
1215 - "context length sampled from [input_len * (1 - range_ratio), "
1216 - "input_len * (1 + range_ratio)]."
1217 - ),
1218 - )
1219 -
1220 - hf_group = parser.add_argument_group("hf dataset options")
1221 - hf_group.add_argument(
1222 - "--hf-subset", type=str, default=None, help="Subset of the HF dataset."
1223 - )
1224 - hf_group.add_argument(
1225 - "--hf-split", type=str, default=None, help="Split of the HF dataset."
1226 - )
1227 - hf_group.add_argument(
1228 - "--hf-output-len",
1229 - type=int,
1230 - default=None,
1231 - help="Output length for each request. Overrides the output lengths "
1232 - "from the sampled HF dataset.",
1233 - )
1234 -
1235 - sampling_group = parser.add_argument_group("sampling parameters")
1236 - sampling_group.add_argument(
1237 - "--top-p",
1238 - type=float,
1239 - default=None,
1240 - help="Top-p sampling parameter. Only has effect on openai-compatible backends.",
1241 - )
1242 - sampling_group.add_argument(
1243 - "--top-k",
1244 - type=int,
1245 - default=None,
1246 - help="Top-k sampling parameter. Only has effect on openai-compatible backends.",
1247 - )
1248 - sampling_group.add_argument(
1249 - "--min-p",
1250 - type=float,
1251 - default=None,
1252 - help="Min-p sampling parameter. Only has effect on openai-compatible backends.",
1253 - )
1254 - sampling_group.add_argument(
1255 - "--temperature",
1256 - type=float,
1257 - default=None,
1258 - help="Temperature sampling parameter. Only has effect on "
1259 - "openai-compatible backends. If not specified, default to greedy "
1260 - "decoding (i.e. temperature==0.0).",
1261 - )
1262 -
1263 - parser.add_argument(
1264 - "--tokenizer-mode",
1265 - type=str,
1266 - default="auto",
1267 - choices=["auto", "slow", "mistral", "custom"],
1268 - help='The tokenizer mode.\n\n* "auto" will use the '
1269 - 'fast tokenizer if available.\n* "slow" will '
1270 - "always use the slow tokenizer. \n* "
1271 - '"mistral" will always use the `mistral_common` tokenizer. \n*'
1272 - '"custom" will use --tokenizer to select the preregistered tokenizer.',
1273 - )
1274 -
1275 - parser.add_argument(
1276 - "--served-model-name",
1277 - type=str,
1278 - default=None,
1279 - help="The model name used in the API. "
1280 - "If not specified, the model name will be the "
1281 - "same as the ``--model`` argument. ",
1282 - )
1283 -
1284 - parser.add_argument(
1285 - "--lora-modules",
1286 - nargs="+",
1287 - default=None,
1288 - help="A subset of LoRA module names passed in when "
1289 - "launching the server. For each request, the "
1290 - "script chooses a LoRA module at random.",
1291 - )
1292 -
1293 - parser.add_argument(
1294 - "--ramp-up-strategy",
1295 - type=str,
1296 - default=None,
1297 - choices=["linear", "exponential"],
1298 - help="The ramp-up strategy. This would be used to "
1299 - "ramp up the request rate from initial RPS to final "
1300 - "RPS rate (specified by --ramp-up-start-rps and --ramp-up-end-rps). "
1301 - "over the duration of the benchmark.",
1302 - )
1303 - parser.add_argument(
1304 - "--ramp-up-start-rps",
1305 - type=int,
1306 - default=None,
1307 - help="The starting request rate for ramp-up (RPS). "
1308 - "Needs to be specified when --ramp-up-strategy is used.",
1309 - )
1310 - parser.add_argument(
1311 - "--ramp-up-end-rps",
1312 - type=int,
1313 - default=None,
1314 - help="The ending request rate for ramp-up (RPS). "
1315 - "Needs to be specified when --ramp-up-strategy is used.",
1316 - )
5 +if __name__ == "__main__":
6 + print("""DEPRECATED: This script has been moved to the vLLM CLI.
7
1318 - return parser
8 +Please use the following command instead:
9 + vllm bench serve
10
11 +For help with the new command, run:
12 + vllm bench serve --help
13
1321 -if __name__ == "__main__":
1322 - parser = create_argument_parser()
1323 - args = parser.parse_args()
1324 - main(args)
14 +Alternatively, you can run the new command directly with:
15 + python -m vllm.entrypoints.cli.main bench serve --help
16 +""")
17 + sys.exit(1)
benchmarks/benchmark_throughput.py
+11 -735
@@ -1,741 +1,17 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Benchmark offline inference throughput."""
3 +import sys
4
5 -import argparse
6 -import dataclasses
7 -import json
8 -import os
9 -import random
10 -import time
11 -import warnings
12 -from typing import Any, Optional, Union
13 -
14 -import torch
15 -import uvloop
16 -from tqdm import tqdm
17 -from transformers import AutoModelForCausalLM, AutoTokenizer, PreTrainedTokenizerBase
18 -from typing_extensions import deprecated
19 -
20 -from benchmark_dataset import (
21 - AIMODataset,
22 - BurstGPTDataset,
23 - ConversationDataset,
24 - InstructCoderDataset,
25 - RandomDataset,
26 - SampleRequest,
27 - ShareGPTDataset,
28 - SonnetDataset,
29 - VisionArenaDataset,
30 -)
31 -from benchmark_utils import convert_to_pytorch_benchmark_format, write_to_json
32 -from vllm.engine.arg_utils import AsyncEngineArgs, EngineArgs
33 -from vllm.entrypoints.openai.api_server import (
34 - build_async_engine_client_from_engine_args,
35 -)
36 -from vllm.inputs import TextPrompt, TokensPrompt
37 -from vllm.lora.request import LoRARequest
38 -from vllm.outputs import RequestOutput
39 -from vllm.sampling_params import BeamSearchParams
40 -from vllm.utils import FlexibleArgumentParser, merge_async_iterators
41 -
42 -
43 -def run_vllm(
44 - requests: list[SampleRequest],
45 - n: int,
46 - engine_args: EngineArgs,
47 - disable_detokenize: bool = False,
48 -) -> tuple[float, Optional[list[RequestOutput]]]:
49 - from vllm import LLM, SamplingParams
50 -
51 - llm = LLM(**dataclasses.asdict(engine_args))
52 - assert all(
53 - llm.llm_engine.model_config.max_model_len
54 - >= (request.prompt_len + request.expected_output_len)
55 - for request in requests
56 - ), (
57 - "Please ensure that max_model_len is greater than the sum of"
58 - " prompt_len and expected_output_len for all requests."
59 - )
60 - # Add the requests to the engine.
61 - prompts: list[Union[TextPrompt, TokensPrompt]] = []
62 - sampling_params: list[SamplingParams] = []
63 - for request in requests:
64 - prompts.append(
65 - TokensPrompt(
66 - prompt_token_ids=request.prompt["prompt_token_ids"],
67 - multi_modal_data=request.multi_modal_data,
68 - )
69 - if "prompt_token_ids" in request.prompt
70 - else TextPrompt(
71 - prompt=request.prompt, multi_modal_data=request.multi_modal_data
72 - )
73 - )
74 - sampling_params.append(
75 - SamplingParams(
76 - n=n,
77 - temperature=1.0,
78 - top_p=1.0,
79 - ignore_eos=True,
80 - max_tokens=request.expected_output_len,
81 - detokenize=not disable_detokenize,
82 - )
83 - )
84 - lora_requests: Optional[list[LoRARequest]] = None
85 - if engine_args.enable_lora:
86 - lora_requests = [request.lora_request for request in requests]
87 -
88 - use_beam_search = False
89 -
90 - outputs = None
91 - if not use_beam_search:
92 - start = time.perf_counter()
93 - outputs = llm.generate(
94 - prompts, sampling_params, lora_request=lora_requests, use_tqdm=True
95 - )
96 - end = time.perf_counter()
97 - else:
98 - assert lora_requests is None, "BeamSearch API does not support LoRA"
99 - # output_len should be the same for all requests.
100 - output_len = requests[0].expected_output_len
101 - for request in requests:
102 - assert request.expected_output_len == output_len
103 - start = time.perf_counter()
104 - llm.beam_search(
105 - prompts,
106 - BeamSearchParams(
107 - beam_width=n,
108 - max_tokens=output_len,
109 - ignore_eos=True,
110 - ),
111 - )
112 - end = time.perf_counter()
113 - return end - start, outputs
114 -
115 -
116 -def run_vllm_chat(
117 - requests: list[SampleRequest],
118 - n: int,
119 - engine_args: EngineArgs,
120 - disable_detokenize: bool = False,
121 -) -> tuple[float, list[RequestOutput]]:
122 - """
123 - Run vLLM chat benchmark. This function is recommended ONLY for benchmarking
124 - multimodal models as it properly handles multimodal inputs and chat
125 - formatting. For non-multimodal models, use run_vllm() instead.
126 - """
127 - from vllm import LLM, SamplingParams
128 -
129 - llm = LLM(**dataclasses.asdict(engine_args))
130 -
131 - assert all(
132 - llm.llm_engine.model_config.max_model_len
133 - >= (request.prompt_len + request.expected_output_len)
134 - for request in requests
135 - ), (
136 - "Please ensure that max_model_len is greater than the sum of "
137 - "prompt_len and expected_output_len for all requests."
138 - )
139 -
140 - prompts = []
141 - sampling_params: list[SamplingParams] = []
142 - for request in requests:
143 - prompts.append(request.prompt)
144 - sampling_params.append(
145 - SamplingParams(
146 - n=n,
147 - temperature=1.0,
148 - top_p=1.0,
149 - ignore_eos=True,
150 - max_tokens=request.expected_output_len,
151 - detokenize=not disable_detokenize,
152 - )
153 - )
154 - start = time.perf_counter()
155 - outputs = llm.chat(prompts, sampling_params, use_tqdm=True)
156 - end = time.perf_counter()
157 - return end - start, outputs
158 -
159 -
160 -async def run_vllm_async(
161 - requests: list[SampleRequest],
162 - n: int,
163 - engine_args: AsyncEngineArgs,
164 - disable_frontend_multiprocessing: bool = False,
165 - disable_detokenize: bool = False,
166 -) -> float:
167 - from vllm import SamplingParams
168 -
169 - async with build_async_engine_client_from_engine_args(
170 - engine_args,
171 - disable_frontend_multiprocessing=disable_frontend_multiprocessing,
172 - ) as llm:
173 - model_config = await llm.get_model_config()
174 - assert all(
175 - model_config.max_model_len
176 - >= (request.prompt_len + request.expected_output_len)
177 - for request in requests
178 - ), (
179 - "Please ensure that max_model_len is greater than the sum of"
180 - " prompt_len and expected_output_len for all requests."
181 - )
182 -
183 - # Add the requests to the engine.
184 - prompts: list[Union[TextPrompt, TokensPrompt]] = []
185 - sampling_params: list[SamplingParams] = []
186 - lora_requests: list[Optional[LoRARequest]] = []
187 - for request in requests:
188 - prompts.append(
189 - TokensPrompt(
190 - prompt_token_ids=request.prompt["prompt_token_ids"],
191 - multi_modal_data=request.multi_modal_data,
192 - )
193 - if "prompt_token_ids" in request.prompt
194 - else TextPrompt(
195 - prompt=request.prompt, multi_modal_data=request.multi_modal_data
196 - )
197 - )
198 - sampling_params.append(
199 - SamplingParams(
200 - n=n,
201 - temperature=1.0,
202 - top_p=1.0,
203 - ignore_eos=True,
204 - max_tokens=request.expected_output_len,
205 - detokenize=not disable_detokenize,
206 - )
207 - )
208 - lora_requests.append(request.lora_request)
209 -
210 - generators = []
211 - start = time.perf_counter()
212 - for i, (prompt, sp, lr) in enumerate(
213 - zip(prompts, sampling_params, lora_requests)
214 - ):
215 - generator = llm.generate(prompt, sp, lora_request=lr, request_id=f"test{i}")
216 - generators.append(generator)
217 - all_gens = merge_async_iterators(*generators)
218 - async for i, res in all_gens:
219 - pass
220 - end = time.perf_counter()
221 - return end - start
222 -
223 -
224 -def run_hf(
225 - requests: list[SampleRequest],
226 - model: str,
227 - tokenizer: PreTrainedTokenizerBase,
228 - n: int,
229 - max_batch_size: int,
230 - trust_remote_code: bool,
231 - disable_detokenize: bool = False,
232 -) -> float:
233 - llm = AutoModelForCausalLM.from_pretrained(
234 - model, torch_dtype=torch.float16, trust_remote_code=trust_remote_code
235 - )
236 - if llm.config.model_type == "llama":
237 - # To enable padding in the HF backend.
238 - tokenizer.pad_token = tokenizer.eos_token
239 - llm = llm.cuda()
240 -
241 - pbar = tqdm(total=len(requests))
242 - start = time.perf_counter()
243 - batch: list[str] = []
244 - max_prompt_len = 0
245 - max_output_len = 0
246 - for i in range(len(requests)):
247 - prompt = requests[i].prompt
248 - prompt_len = requests[i].prompt_len
249 - output_len = requests[i].expected_output_len
250 - # Add the prompt to the batch.
251 - batch.append(prompt)
252 - max_prompt_len = max(max_prompt_len, prompt_len)
253 - max_output_len = max(max_output_len, output_len)
254 - if len(batch) < max_batch_size and i != len(requests) - 1:
255 - # Check if we can add more requests to the batch.
256 - next_prompt_len = requests[i + 1].prompt_len
257 - next_output_len = requests[i + 1].expected_output_len
258 - if (
259 - max(max_prompt_len, next_prompt_len)
260 - + max(max_output_len, next_output_len)
261 - ) <= 2048:
262 - # We can add more requests to the batch.
263 - continue
264 -
265 - # Generate the sequences.
266 - input_ids = tokenizer(batch, return_tensors="pt", padding=True).input_ids
267 - llm_outputs = llm.generate(
268 - input_ids=input_ids.cuda(),
269 - do_sample=True,
270 - num_return_sequences=n,
271 - temperature=1.0,
272 - top_p=1.0,
273 - use_cache=True,
274 - max_new_tokens=max_output_len,
275 - )
276 - if not disable_detokenize:
277 - # Include the decoding time.
278 - tokenizer.batch_decode(llm_outputs, skip_special_tokens=True)
279 - pbar.update(len(batch))
280 -
281 - # Clear the batch.
282 - batch = []
283 - max_prompt_len = 0
284 - max_output_len = 0
285 - end = time.perf_counter()
286 - return end - start
287 -
288 -
289 -def run_mii(
290 - requests: list[SampleRequest],
291 - model: str,
292 - tensor_parallel_size: int,
293 - output_len: int,
294 -) -> float:
295 - from mii import client, serve
296 -
297 - llm = serve(model, tensor_parallel=tensor_parallel_size)
298 - prompts = [request.prompt for request in requests]
299 -
300 - start = time.perf_counter()
301 - llm.generate(prompts, max_new_tokens=output_len)
302 - end = time.perf_counter()
303 - client = client(model)
304 - client.terminate_server()
305 - return end - start
306 -
307 -
308 -def save_to_pytorch_benchmark_format(
309 - args: argparse.Namespace, results: dict[str, Any]
310 -) -> None:
311 - pt_records = convert_to_pytorch_benchmark_format(
312 - args=args,
313 - metrics={
314 - "requests_per_second": [results["requests_per_second"]],
315 - "tokens_per_second": [results["tokens_per_second"]],
316 - },
317 - extra_info={
318 - k: results[k] for k in ["elapsed_time", "num_requests", "total_num_tokens"]
319 - },
320 - )
321 - if pt_records:
322 - # Don't use json suffix here as we don't want CI to pick it up
323 - pt_file = f"{os.path.splitext(args.output_json)[0]}.pytorch.json"
324 - write_to_json(pt_file, pt_records)
325 -
326 -
327 -def get_requests(args, tokenizer):
328 - # Common parameters for all dataset types.
329 - common_kwargs = {
330 - "dataset_path": args.dataset_path,
331 - "random_seed": args.seed,
332 - }
333 - sample_kwargs = {
334 - "tokenizer": tokenizer,
335 - "lora_path": args.lora_path,
336 - "max_loras": args.max_loras,
337 - "num_requests": args.num_prompts,
338 - "input_len": args.input_len,
339 - "output_len": args.output_len,
340 - }
341 -
342 - if args.dataset_path is None or args.dataset_name == "random":
343 - sample_kwargs["range_ratio"] = args.random_range_ratio
344 - sample_kwargs["prefix_len"] = args.prefix_len
345 - dataset_cls = RandomDataset
346 - elif args.dataset_name == "sharegpt":
347 - dataset_cls = ShareGPTDataset
348 - if args.backend == "vllm-chat":
349 - sample_kwargs["enable_multimodal_chat"] = True
350 - elif args.dataset_name == "sonnet":
351 - assert tokenizer.chat_template or tokenizer.default_chat_template, (
352 - "Tokenizer/model must have chat template for sonnet dataset."
353 - )
354 - dataset_cls = SonnetDataset
355 - sample_kwargs["prefix_len"] = args.prefix_len
356 - sample_kwargs["return_prompt_formatted"] = True
357 - elif args.dataset_name == "burstgpt":
358 - dataset_cls = BurstGPTDataset
359 - elif args.dataset_name == "hf":
360 - common_kwargs["no_stream"] = args.no_stream
361 - if args.dataset_path in VisionArenaDataset.SUPPORTED_DATASET_PATHS:
362 - dataset_cls = VisionArenaDataset
363 - common_kwargs["dataset_subset"] = None
364 - common_kwargs["dataset_split"] = "train"
365 - sample_kwargs["enable_multimodal_chat"] = True
366 - elif args.dataset_path in InstructCoderDataset.SUPPORTED_DATASET_PATHS:
367 - dataset_cls = InstructCoderDataset
368 - common_kwargs["dataset_split"] = "train"
369 - elif args.dataset_path in ConversationDataset.SUPPORTED_DATASET_PATHS:
370 - dataset_cls = ConversationDataset
371 - common_kwargs["dataset_subset"] = args.hf_subset
372 - common_kwargs["dataset_split"] = args.hf_split
373 - sample_kwargs["enable_multimodal_chat"] = True
374 - elif args.dataset_path in AIMODataset.SUPPORTED_DATASET_PATHS:
375 - dataset_cls = AIMODataset
376 - common_kwargs["dataset_subset"] = None
377 - common_kwargs["dataset_split"] = "train"
378 - else:
379 - raise ValueError(f"Unknown dataset name: {args.dataset_name}")
380 - # Remove None values
381 - sample_kwargs = {k: v for k, v in sample_kwargs.items() if v is not None}
382 - return dataset_cls(**common_kwargs).sample(**sample_kwargs)
383 -
384 -
385 -@deprecated(
386 - "benchmark_throughput.py is deprecated and will be removed in a "
387 - "future version. Please use 'vllm bench throughput' instead.",
388 -)
389 -def main(args: argparse.Namespace):
390 - if args.seed is None:
391 - args.seed = 0
392 - print(args)
393 - random.seed(args.seed)
394 - # Sample the requests.
395 - tokenizer = AutoTokenizer.from_pretrained(
396 - args.tokenizer, trust_remote_code=args.trust_remote_code
397 - )
398 - requests = get_requests(args, tokenizer)
399 - is_multi_modal = any(request.multi_modal_data is not None for request in requests)
400 - request_outputs: Optional[list[RequestOutput]] = None
401 - if args.backend == "vllm":
402 - if args.async_engine:
403 - elapsed_time = uvloop.run(
404 - run_vllm_async(
405 - requests,
406 - args.n,
407 - AsyncEngineArgs.from_cli_args(args),
408 - args.disable_frontend_multiprocessing,
409 - args.disable_detokenize,
410 - )
411 - )
412 - else:
413 - elapsed_time, request_outputs = run_vllm(
414 - requests,
415 - args.n,
416 - EngineArgs.from_cli_args(args),
417 - args.disable_detokenize,
418 - )
419 - elif args.backend == "hf":
420 - assert args.tensor_parallel_size == 1
421 - elapsed_time = run_hf(
422 - requests,
423 - args.model,
424 - tokenizer,
425 - args.n,
426 - args.hf_max_batch_size,
427 - args.trust_remote_code,
428 - args.disable_detokenize,
429 - )
430 - elif args.backend == "mii":
431 - elapsed_time = run_mii(
432 - requests, args.model, args.tensor_parallel_size, args.output_len
433 - )
434 - elif args.backend == "vllm-chat":
435 - elapsed_time, request_outputs = run_vllm_chat(
436 - requests, args.n, EngineArgs.from_cli_args(args), args.disable_detokenize
437 - )
438 - else:
439 - raise ValueError(f"Unknown backend: {args.backend}")
440 -
441 - if request_outputs:
442 - # Note: with the vllm and vllm-chat backends,
443 - # we have request_outputs, which we use to count tokens.
444 - total_prompt_tokens = 0
445 - total_output_tokens = 0
446 - for ro in request_outputs:
447 - if not isinstance(ro, RequestOutput):
448 - continue
449 - total_prompt_tokens += (
450 - len(ro.prompt_token_ids) if ro.prompt_token_ids else 0
451 - )
452 - total_output_tokens += sum(len(o.token_ids) for o in ro.outputs if o)
453 - total_num_tokens = total_prompt_tokens + total_output_tokens
454 - else:
455 - total_num_tokens = sum(r.prompt_len + r.expected_output_len for r in requests)
456 - total_output_tokens = sum(r.expected_output_len for r in requests)
457 - total_prompt_tokens = total_num_tokens - total_output_tokens
458 -
459 - if is_multi_modal and args.backend != "vllm-chat":
460 - print(
461 - "\033[91mWARNING\033[0m: Multi-modal request with "
462 - f"{args.backend} backend detected. The "
463 - "following metrics are not accurate because image tokens are not"
464 - " counted. See vllm-project/vllm/issues/9778 for details."
465 - )
466 - # TODO(vllm-project/vllm/issues/9778): Count multi-modal token length.
467 - # vllm-chat backend counts the image tokens now
468 -
469 - print(
470 - f"Throughput: {len(requests) / elapsed_time:.2f} requests/s, "
471 - f"{total_num_tokens / elapsed_time:.2f} total tokens/s, "
472 - f"{total_output_tokens / elapsed_time:.2f} output tokens/s"
473 - )
474 - print(f"Total num prompt tokens: {total_prompt_tokens}")
475 - print(f"Total num output tokens: {total_output_tokens}")
476 -
477 - # Output JSON results if specified
478 - if args.output_json:
479 - results = {
480 - "elapsed_time": elapsed_time,
481 - "num_requests": len(requests),
482 - "total_num_tokens": total_num_tokens,
483 - "requests_per_second": len(requests) / elapsed_time,
484 - "tokens_per_second": total_num_tokens / elapsed_time,
485 - }
486 - with open(args.output_json, "w") as f:
487 - json.dump(results, f, indent=4)
488 - save_to_pytorch_benchmark_format(args, results)
489 -
490 -
491 -def validate_args(args):
492 - """
493 - Validate command-line arguments.
494 - """
495 -
496 - # === Deprecation and Defaulting ===
497 - if args.dataset is not None:
498 - warnings.warn(
499 - "The '--dataset' argument will be deprecated in the next release. "
500 - "Please use '--dataset-name' and '--dataset-path' instead.",
501 - stacklevel=2,
502 - )
503 - args.dataset_path = args.dataset
504 -
505 - if not getattr(args, "tokenizer", None):
506 - args.tokenizer = args.model
507 -
508 - # === Backend Validation ===
509 - valid_backends = {"vllm", "hf", "mii", "vllm-chat"}
510 - if args.backend not in valid_backends:
511 - raise ValueError(f"Unsupported backend: {args.backend}")
512 -
513 - # === Dataset Configuration ===
514 - if not args.dataset and not args.dataset_path:
515 - print("When dataset path is not set, it will default to random dataset")
516 - args.dataset_name = "random"
517 - if args.input_len is None:
518 - raise ValueError("input_len must be provided for a random dataset")
519 -
520 - # === Dataset Name Specific Checks ===
521 - # --hf-subset and --hf-split: only used
522 - # when dataset_name is 'hf'
523 - if args.dataset_name != "hf" and (
524 - getattr(args, "hf_subset", None) is not None
525 - or getattr(args, "hf_split", None) is not None
526 - ):
527 - warnings.warn(
528 - "--hf-subset and --hf-split will be ignored \
529 - since --dataset-name is not 'hf'.",
530 - stacklevel=2,
531 - )
532 - elif args.dataset_name == "hf":
533 - if args.dataset_path in (
534 - VisionArenaDataset.SUPPORTED_DATASET_PATHS.keys()
535 - | ConversationDataset.SUPPORTED_DATASET_PATHS
536 - ):
537 - assert args.backend == "vllm-chat", (
538 - f"{args.dataset_path} needs to use vllm-chat as the backend."
539 - ) # noqa: E501
540 - elif args.dataset_path in (
541 - InstructCoderDataset.SUPPORTED_DATASET_PATHS
542 - | AIMODataset.SUPPORTED_DATASET_PATHS
543 - ):
544 - assert args.backend == "vllm", (
545 - f"{args.dataset_path} needs to use vllm as the backend."
546 - ) # noqa: E501
547 - else:
548 - raise ValueError(f"{args.dataset_path} is not supported by hf dataset.")
549 -
550 - # --random-range-ratio: only used when dataset_name is 'random'
551 - if args.dataset_name != "random" and args.random_range_ratio is not None:
552 - warnings.warn(
553 - "--random-range-ratio will be ignored since \
554 - --dataset-name is not 'random'.",
555 - stacklevel=2,
556 - )
557 -
558 - # --prefix-len: only used when dataset_name is 'random', 'sonnet', or not
559 - # set.
560 - if (
561 - args.dataset_name not in {"random", "sonnet", None}
562 - and args.prefix_len is not None
563 - ):
564 - warnings.warn(
565 - "--prefix-len will be ignored since --dataset-name\
566 - is not 'random', 'sonnet', or not set.",
567 - stacklevel=2,
568 - )
569 -
570 - # === LoRA Settings ===
571 - if getattr(args, "enable_lora", False) and args.backend != "vllm":
572 - raise ValueError("LoRA benchmarking is only supported for vLLM backend")
573 - if getattr(args, "enable_lora", False) and args.lora_path is None:
574 - raise ValueError("LoRA path must be provided when enable_lora is True")
575 -
576 - # === Backend-specific Validations ===
577 - if args.backend == "hf" and args.hf_max_batch_size is None:
578 - raise ValueError("HF max batch size is required for HF backend")
579 - if args.backend != "hf" and args.hf_max_batch_size is not None:
580 - raise ValueError("HF max batch size is only for HF backend.")
581 -
582 - if (
583 - args.backend in {"hf", "mii"}
584 - and getattr(args, "quantization", None) is not None
585 - ):
586 - raise ValueError("Quantization is only for vLLM backend.")
587 -
588 - if args.backend == "mii" and args.dtype != "auto":
589 - raise ValueError("dtype must be auto for MII backend.")
590 - if args.backend == "mii" and args.n != 1:
591 - raise ValueError("n must be 1 for MII backend.")
592 - if args.backend == "mii" and args.tokenizer != args.model:
593 - raise ValueError("Tokenizer must be the same as the model for MII backend.")
594 -
595 - # --data-parallel is not supported currently.
596 - # https://github.com/vllm-project/vllm/issues/16222
597 - if args.data_parallel_size > 1:
598 - raise ValueError(
599 - "Data parallel is not supported in offline benchmark, "
600 - "please use benchmark serving instead"
601 - )
602 -
603 -
604 -def create_argument_parser():
605 - parser = FlexibleArgumentParser(description="Benchmark the throughput.")
606 - parser.add_argument(
607 - "--backend",
608 - type=str,
609 - choices=["vllm", "hf", "mii", "vllm-chat"],
610 - default="vllm",
611 - )
612 - parser.add_argument(
613 - "--dataset-name",
614 - type=str,
615 - choices=["sharegpt", "random", "sonnet", "burstgpt", "hf"],
616 - help="Name of the dataset to benchmark on.",
617 - default="sharegpt",
618 - )
619 - parser.add_argument(
620 - "--no-stream",
621 - action="store_true",
622 - help="Do not load the dataset in streaming mode.",
623 - )
624 - parser.add_argument(
625 - "--dataset",
626 - type=str,
627 - default=None,
628 - help="Path to the ShareGPT dataset, will be deprecated in\
629 - the next release. The dataset is expected to "
630 - "be a json in form of list[dict[..., conversations: "
631 - "list[dict[..., value: <prompt_or_response>]]]]",
632 - )
633 - parser.add_argument(
634 - "--dataset-path", type=str, default=None, help="Path to the dataset"
635 - )
636 - parser.add_argument(
637 - "--input-len",
638 - type=int,
639 - default=None,
640 - help="Input prompt length for each request",
641 - )
642 - parser.add_argument(
643 - "--output-len",
644 - type=int,
645 - default=None,
646 - help="Output length for each request. Overrides the "
647 - "output length from the dataset.",
648 - )
649 - parser.add_argument(
650 - "--n", type=int, default=1, help="Number of generated sequences per prompt."
651 - )
652 - parser.add_argument(
653 - "--num-prompts", type=int, default=1000, help="Number of prompts to process."
654 - )
655 - parser.add_argument(
656 - "--hf-max-batch-size",
657 - type=int,
658 - default=None,
659 - help="Maximum batch size for HF backend.",
660 - )
661 - parser.add_argument(
662 - "--output-json",
663 - type=str,
664 - default=None,
665 - help="Path to save the throughput results in JSON format.",
666 - )
667 - parser.add_argument(
668 - "--async-engine",
669 - action="store_true",
670 - default=False,
671 - help="Use vLLM async engine rather than LLM class.",
672 - )
673 - parser.add_argument(
674 - "--disable-frontend-multiprocessing",
675 - action="store_true",
676 - default=False,
677 - help="Disable decoupled async engine frontend.",
678 - )
679 - parser.add_argument(
680 - "--disable-detokenize",
681 - action="store_true",
682 - help=(
683 - "Do not detokenize the response (i.e. do not include "
684 - "detokenization time in the measurement)"
685 - ),
686 - )
687 - # LoRA
688 - parser.add_argument(
689 - "--lora-path",
690 - type=str,
691 - default=None,
692 - help="Path to the LoRA adapters to use. This can be an absolute path, "
693 - "a relative path, or a Hugging Face model identifier.",
694 - )
695 - parser.add_argument(
696 - "--prefix-len",
697 - type=int,
698 - default=None,
699 - help=f"Number of prefix tokens to be used in RandomDataset "
700 - "and SonnetDataset. For RandomDataset, the total input "
701 - "length is the sum of prefix-len (default: "
702 - f"{RandomDataset.DEFAULT_PREFIX_LEN}) and a random context length "
703 - "sampled from [input_len * (1 - range_ratio), "
704 - "input_len * (1 + range_ratio)]. For SonnetDataset, "
705 - f"prefix_len (default: {SonnetDataset.DEFAULT_PREFIX_LEN}) "
706 - "controls how much of the input is fixed lines versus "
707 - "random lines, but the total input length remains approximately "
708 - "input_len tokens.",
709 - )
710 - # random dataset
711 - parser.add_argument(
712 - "--random-range-ratio",
713 - type=float,
714 - default=None,
715 - help=f"Range ratio (default : {RandomDataset.DEFAULT_RANGE_RATIO}) "
716 - "for sampling input/output length, "
717 - "used only for RandomDataset. Must be in the range [0, 1) to "
718 - "define a symmetric sampling range "
719 - "[length * (1 - range_ratio), length * (1 + range_ratio)].",
720 - )
721 -
722 - # hf dataset
723 - parser.add_argument(
724 - "--hf-subset", type=str, default=None, help="Subset of the HF dataset."
725 - )
726 - parser.add_argument(
727 - "--hf-split", type=str, default=None, help="Split of the HF dataset."
728 - )
729 -
730 - parser = AsyncEngineArgs.add_cli_args(parser)
5 +if __name__ == "__main__":
6 + print("""DEPRECATED: This script has been moved to the vLLM CLI.
7
732 - return parser
8 +Please use the following command instead:
9 + vllm bench throughput
10
11 +For help with the new command, run:
12 + vllm bench throughput --help
13
735 -if __name__ == "__main__":
736 - parser = create_argument_parser()
737 - args = parser.parse_args()
738 - if args.tokenizer is None:
739 - args.tokenizer = args.model
740 - validate_args(args)
741 - main(args)
14 +Alternatively, you can run the new command directly with:
15 + python -m vllm.entrypoints.cli.main bench throughput --help
16 +""")
17 + sys.exit(1)