@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / f849ee739c
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 13, 03:15 UTC

Adding a benchmark for batch invariance (#28161)

Signed-off-by: Bram Wasti <bwasti@meta.com> Signed-off-by: Bram Wasti <bwasti@fb.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

Bram Wasti committed Nov 16, 2025 at 00:22 UTC f849ee739cdb3d82fce1660a6fd91806e8ae9bff
1 file changed +380
benchmarks/benchmark_batch_invariance.py new
+380
@@ -0,0 +1,380 @@
1 +#!/usr/bin/env python3
2 +# SPDX-License-Identifier: Apache-2.0
3 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4 +"""
5 +Benchmark to measure the performance overhead of VLLM_BATCH_INVARIANT mode.
6 +
7 +This benchmark runs the same workload twice:
8 +1. With VLLM_BATCH_INVARIANT=0 (baseline)
9 +2. With VLLM_BATCH_INVARIANT=1 (batch invariant mode)
10 +
11 +And reports the timing and throughput metrics for comparison.
12 +
13 +Environment variables:
14 + VLLM_BENCH_MODEL: Model to benchmark (default: "Qwen/Qwen3-1.7B")
15 + VLLM_BENCH_TP_SIZE: Tensor parallel size (default: 1, use 8 for deepseek)
16 + VLLM_BENCH_BATCH_SIZE: Max batch size (default: 128)
17 + VLLM_BENCH_NUM_TRIALS: Number of trials to run (default: 5)
18 + VLLM_BENCH_MIN_PROMPT: Min prompt length in words (default: 1024)
19 + VLLM_BENCH_MAX_PROMPT: Max prompt length in words (default: 2048)
20 + VLLM_BENCH_MAX_TOKENS: Max tokens to generate (default: 128)
21 + VLLM_BENCH_TEMPERATURE: Temperature for sampling (default: 0.0)
22 + VLLM_BENCH_GPU_MEMORY_UTILIZATION: GPU memory utilization (default: 0.4)
23 + VLLM_BENCH_MAX_MODEL_LEN: Max model length (default: 5120)
24 + VLLM_BENCH_BACKEND: Attention backend (default: FLASH_ATTN)
25 +
26 +Example usage:
27 + # Benchmark qwen3 (default)
28 + python benchmarks/benchmark_batch_invariance.py
29 +
30 + # Benchmark deepseek with 8 GPUs
31 + VLLM_BENCH_MODEL="deepseek-ai/DeepSeek-V3" VLLM_BENCH_TP_SIZE=8 \\
32 + python benchmarks/benchmark_batch_invariance.py
33 +
34 + # Quick test with fewer trials
35 + VLLM_BENCH_NUM_TRIALS=2 VLLM_BENCH_BATCH_SIZE=32 \\
36 + python benchmarks/benchmark_batch_invariance.py
37 +"""
38 +
39 +import contextlib
40 +import os
41 +import random
42 +import time
43 +
44 +from vllm import LLM, SamplingParams
45 +from vllm.platforms import current_platform
46 +
47 +
48 +def _random_prompt(min_words: int = 1024, max_words: int = 1024 * 2) -> str:
49 + """Generate a random prompt for benchmarking."""
50 + prompt_templates = [
51 + "Question: What is the capital of France?\nAnswer: The capital of France is",
52 + "Q: How does photosynthesis work?\nA: Photosynthesis is the process by which",
53 + "User: Can you explain quantum mechanics?\nAssistant: Quantum mechanics is",
54 + "Once upon a time in a distant galaxy, there lived",
55 + "The old man walked slowly down the street, remembering",
56 + "In the year 2157, humanity finally discovered",
57 + "To implement a binary search tree in Python, first we need to",
58 + "The algorithm works by iterating through the array and",
59 + "Here's how to optimize database queries using indexing:",
60 + "The Renaissance was a period in European history that",
61 + "Climate change is caused by several factors including",
62 + "The human brain contains approximately 86 billion neurons which",
63 + "I've been thinking about getting a new laptop because",
64 + "Yesterday I went to the store and bought",
65 + "My favorite thing about summer is definitely",
66 + ]
67 +
68 + base_prompt = random.choice(prompt_templates)
69 +
70 + if max_words < min_words:
71 + max_words = min_words
72 + target_words = random.randint(min_words, max_words)
73 +
74 + if target_words > 50:
75 + padding_text = (
76 + " This is an interesting topic that deserves more explanation. "
77 + * (target_words // 50)
78 + )
79 + base_prompt = base_prompt + padding_text
80 +
81 + return base_prompt
82 +
83 +
84 +def run_benchmark_with_batch_invariant(
85 + model: str,
86 + tp_size: int,
87 + max_batch_size: int,
88 + num_trials: int,
89 + min_prompt: int,
90 + max_prompt: int,
91 + max_tokens: int,
92 + temperature: float,
93 + gpu_mem_util: float,
94 + max_model_len: int,
95 + backend: str,
96 + batch_invariant: bool,
97 + seed: int = 12345,
98 +) -> dict:
99 + """
100 + Run the benchmark with the specified configuration.
101 +
102 + Returns a dict with timing and throughput metrics.
103 + """
104 + random.seed(seed)
105 +
106 + # Set environment variables
107 + os.environ["VLLM_ATTENTION_BACKEND"] = backend
108 + if batch_invariant:
109 + os.environ["VLLM_BATCH_INVARIANT"] = "1"
110 + else:
111 + os.environ["VLLM_BATCH_INVARIANT"] = "0"
112 +
113 + print(f"\n{'=' * 80}")
114 + print(f"BENCHMARK: VLLM_BATCH_INVARIANT={int(batch_invariant)}")
115 + print(f" Model: {model}")
116 + print(f" TP Size: {tp_size}")
117 + print(f" Backend: {backend}")
118 + print(f" Max Batch Size: {max_batch_size}")
119 + print(f" Trials: {num_trials}")
120 + print(f" Max Tokens: {max_tokens}")
121 + print(f"{'=' * 80}\n")
122 +
123 + sampling = SamplingParams(
124 + temperature=temperature,
125 + top_p=0.95,
126 + max_tokens=max_tokens,
127 + seed=20240919,
128 + )
129 +
130 + needle_prompt = "There once was a "
131 +
132 + llm = None
133 + try:
134 + # Create LLM engine
135 + start_init = time.perf_counter()
136 + llm = LLM(
137 + model=model,
138 + max_num_seqs=max_batch_size,
139 + gpu_memory_utilization=gpu_mem_util,
140 + max_model_len=max_model_len,
141 + dtype="bfloat16",
142 + tensor_parallel_size=tp_size,
143 + enable_prefix_caching=False,
144 + )
145 + init_time = time.perf_counter() - start_init
146 + print(f"Engine initialization time: {init_time:.2f}s\n")
147 +
148 + # Generate baseline
149 + print("Generating baseline (warmup)...")
150 + baseline_out = llm.generate([needle_prompt], sampling)
151 + assert len(baseline_out) == 1
152 + baseline_text = baseline_out[0].outputs[0].text
153 + print(f"Baseline output: '{baseline_text[:50]}...'\n")
154 +
155 + # Run trials and measure timing
156 + trial_times: list[float] = []
157 + total_tokens = 0
158 + total_prompts = 0
159 +
160 + for trial in range(num_trials):
161 + # Create a batch
162 + prompts: list[str] = []
163 + batch_size = random.randint(max_batch_size // 2, max_batch_size)
164 + needle_pos = random.randint(0, batch_size - 1)
165 + for i in range(batch_size):
166 + if i == needle_pos:
167 + prompts.append(needle_prompt)
168 + else:
169 + prompts.append(_random_prompt(min_prompt, max_prompt))
170 +
171 + # Measure time for this trial
172 + start_time = time.perf_counter()
173 + outputs = llm.generate(prompts, sampling)
174 + trial_time = time.perf_counter() - start_time
175 +
176 + trial_times.append(trial_time)
177 + total_prompts += len(prompts)
178 +
179 + # Count tokens
180 + for output in outputs:
181 + if output.outputs:
182 + total_tokens += len(output.outputs[0].token_ids)
183 +
184 + print(
185 + f"Trial {trial + 1}/{num_trials}: "
186 + f"batch_size={batch_size}, "
187 + f"time={trial_time:.2f}s"
188 + )
189 +
190 + # Verify needle output still matches
191 + needle_output = outputs[needle_pos]
192 + assert needle_output.prompt == needle_prompt
193 +
194 + # Compute statistics
195 + avg_time = sum(trial_times) / len(trial_times)
196 + min_time = min(trial_times)
197 + max_time = max(trial_times)
198 + throughput = total_tokens / sum(trial_times)
199 + prompts_per_sec = total_prompts / sum(trial_times)
200 +
201 + print(f"\n{'=' * 80}")
202 + print("RESULTS:")
203 + print(f" Average time per trial: {avg_time:.2f}s")
204 + print(f" Min time: {min_time:.2f}s")
205 + print(f" Max time: {max_time:.2f}s")
206 + print(f" Total tokens generated: {total_tokens}")
207 + print(f" Total prompts processed: {total_prompts}")
208 + print(f" Throughput: {throughput:.2f} tokens/s")
209 + print(f" Prompts/s: {prompts_per_sec:.2f}")
210 + print(f"{'=' * 80}\n")
211 +
212 + return {
213 + "init_time": init_time,
214 + "avg_time": avg_time,
215 + "min_time": min_time,
216 + "max_time": max_time,
217 + "total_tokens": total_tokens,
218 + "total_prompts": total_prompts,
219 + "throughput": throughput,
220 + "prompts_per_sec": prompts_per_sec,
221 + "trial_times": trial_times,
222 + }
223 +
224 + finally:
225 + # Cleanup
226 + if llm is not None:
227 + with contextlib.suppress(Exception):
228 + llm.shutdown()
229 +
230 +
231 +def main():
232 + # Check platform support
233 + if not (current_platform.is_cuda() and current_platform.has_device_capability(90)):
234 + print("ERROR: Requires CUDA and >= Hopper (SM90)")
235 + print(f"Current platform: {current_platform.device_type}")
236 + if current_platform.is_cuda():
237 + print(f"Device capability: {current_platform.get_device_capability()}")
238 + return 1
239 +
240 + # Read configuration from environment
241 + model = os.getenv("VLLM_BENCH_MODEL", "Qwen/Qwen3-1.7B")
242 + tp_size = int(os.getenv("VLLM_BENCH_TP_SIZE", "1"))
243 + max_batch_size = int(os.getenv("VLLM_BENCH_BATCH_SIZE", "128"))
244 + num_trials = int(os.getenv("VLLM_BENCH_NUM_TRIALS", "5"))
245 + min_prompt = int(os.getenv("VLLM_BENCH_MIN_PROMPT", "1024"))
246 + max_prompt = int(os.getenv("VLLM_BENCH_MAX_PROMPT", "2048"))
247 + max_tokens = int(os.getenv("VLLM_BENCH_MAX_TOKENS", "128"))
248 + temperature = float(os.getenv("VLLM_BENCH_TEMPERATURE", "0.0"))
249 + gpu_mem_util = float(os.getenv("VLLM_BENCH_GPU_MEMORY_UTILIZATION", "0.4"))
250 + max_model_len = int(os.getenv("VLLM_BENCH_MAX_MODEL_LEN", "5120"))
251 + backend = os.getenv("VLLM_BENCH_BACKEND", "FLASH_ATTN")
252 +
253 + print("\n" + "=" * 80)
254 + print("VLLM BATCH INVARIANCE BENCHMARK")
255 + print("=" * 80)
256 + print("\nConfiguration:")
257 + print(f" Model: {model}")
258 + print(f" Tensor Parallel Size: {tp_size}")
259 + print(f" Attention Backend: {backend}")
260 + print(f" Max Batch Size: {max_batch_size}")
261 + print(f" Number of Trials: {num_trials}")
262 + print(f" Prompt Length Range: {min_prompt}-{max_prompt} words")
263 + print(f" Max Tokens to Generate: {max_tokens}")
264 + print(f" Temperature: {temperature}")
265 + print(f" GPU Memory Utilization: {gpu_mem_util}")
266 + print(f" Max Model Length: {max_model_len}")
267 + print("=" * 80)
268 +
269 + # Run benchmark WITHOUT batch invariance (baseline)
270 + print("\n" + "=" * 80)
271 + print("PHASE 1: Running WITHOUT batch invariance (baseline)")
272 + print("=" * 80)
273 + baseline_results = run_benchmark_with_batch_invariant(
274 + model=model,
275 + tp_size=tp_size,
276 + max_batch_size=max_batch_size,
277 + num_trials=num_trials,
278 + min_prompt=min_prompt,
279 + max_prompt=max_prompt,
280 + max_tokens=max_tokens,
281 + temperature=temperature,
282 + gpu_mem_util=gpu_mem_util,
283 + max_model_len=max_model_len,
284 + backend=backend,
285 + batch_invariant=False,
286 + )
287 +
288 + # Run benchmark WITH batch invariance
289 + print("\n" + "=" * 80)
290 + print("PHASE 2: Running WITH batch invariance")
291 + print("=" * 80)
292 + batch_inv_results = run_benchmark_with_batch_invariant(
293 + model=model,
294 + tp_size=tp_size,
295 + max_batch_size=max_batch_size,
296 + num_trials=num_trials,
297 + min_prompt=min_prompt,
298 + max_prompt=max_prompt,
299 + max_tokens=max_tokens,
300 + temperature=temperature,
301 + gpu_mem_util=gpu_mem_util,
302 + max_model_len=max_model_len,
303 + backend=backend,
304 + batch_invariant=True,
305 + )
306 +
307 + # Compare results
308 + print("\n" + "=" * 80)
309 + print("COMPARISON: Batch Invariance vs Baseline")
310 + print("=" * 80)
311 +
312 + init_overhead_pct = (
313 + (batch_inv_results["init_time"] - baseline_results["init_time"])
314 + / baseline_results["init_time"]
315 + * 100
316 + )
317 + time_overhead_pct = (
318 + (batch_inv_results["avg_time"] - baseline_results["avg_time"])
319 + / baseline_results["avg_time"]
320 + * 100
321 + )
322 + throughput_change_pct = (
323 + (batch_inv_results["throughput"] - baseline_results["throughput"])
324 + / baseline_results["throughput"]
325 + * 100
326 + )
327 +
328 + print("\nInitialization Time:")
329 + print(f" Baseline: {baseline_results['init_time']:.2f}s")
330 + print(f" Batch Invariant: {batch_inv_results['init_time']:.2f}s")
331 + print(f" Overhead: {init_overhead_pct:+.2f}%")
332 +
333 + print("\nAverage Trial Time:")
334 + print(f" Baseline: {baseline_results['avg_time']:.2f}s")
335 + print(f" Batch Invariant: {batch_inv_results['avg_time']:.2f}s")
336 + print(f" Overhead: {time_overhead_pct:+.2f}%")
337 +
338 + print("\nThroughput (tokens/s):")
339 + print(f" Baseline: {baseline_results['throughput']:.2f}")
340 + print(f" Batch Invariant: {batch_inv_results['throughput']:.2f}")
341 + print(f" Change: {throughput_change_pct:+.2f}%")
342 +
343 + print("\nPrompts/s:")
344 + print(f" Baseline: {baseline_results['prompts_per_sec']:.2f}")
345 + print(f" Batch Invariant: {batch_inv_results['prompts_per_sec']:.2f}")
346 +
347 + print("\n" + "=" * 80)
348 + print("SUMMARY")
349 + print("=" * 80)
350 + if time_overhead_pct > 0:
351 + print(
352 + f"Batch invariance mode adds approximately {time_overhead_pct:.1f}% "
353 + "overhead"
354 + )
355 + else:
356 + print(
357 + f"Batch invariance mode is approximately {-time_overhead_pct:.1f}% "
358 + "faster (unexpected!)"
359 + )
360 +
361 + if abs(throughput_change_pct) < 1.0:
362 + print("Throughput difference is negligible (< 1%)")
363 + elif throughput_change_pct < 0:
364 + print(
365 + f"Throughput decreased by {-throughput_change_pct:.1f}% "
366 + "with batch invariance"
367 + )
368 + else:
369 + print(
370 + f"Throughput increased by {throughput_change_pct:.1f}% "
371 + "with batch invariance (unexpected!)"
372 + )
373 +
374 + print("=" * 80 + "\n")
375 +
376 + return 0
377 +
378 +
379 +if __name__ == "__main__":
380 + exit(main())