| |
| """ |
| bench_vllm.py β Benchmark vLLM directly via its Python API (LLM class). |
| |
| This bypasses the HTTP server and measures: |
| - Prefill throughput (tokens/s) |
| - Decode throughput (tokens/s) |
| - End-to-end latency |
| - Peak VRAM usage |
| |
| Usage: |
| python scripts/bench_vllm.py --model Qwen/Qwen3.6-27B-FP8 --max-model-len 8192 |
| |
| Note: |
| Requires vLLM installed. This script loads the model in-process, |
| so it will consume VRAM immediately. Make sure no other process |
| is using the GPU. |
| |
| For HTTP-based benchmarking (against a running server), use |
| bench_openai_api.py instead. |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import csv |
| import json |
| import os |
| import sys |
| import time |
| from dataclasses import dataclass, asdict |
| from pathlib import Path |
| from typing import Optional |
|
|
| |
| try: |
| import pynvml |
| HAS_PYNVML = True |
| except ImportError: |
| HAS_PYNVML = False |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class DirectBenchResult: |
| prompt_id: str |
| category: str |
| model: str |
| config_name: str |
| max_model_len: int |
| gpu_memory_util: float |
| dtype: str |
| quantization: str |
| input_tokens: int |
| output_tokens: int |
| prefill_time_s: float |
| decode_time_s: float |
| total_time_s: float |
| prefill_tps: float |
| decode_tps: float |
| output_tps: float |
| vram_peak_mb: float |
| gpu_util_pct: float |
| success: bool |
| error: str |
|
|
|
|
| |
| |
| |
|
|
| class GpuMonitor: |
| def __init__(self): |
| self.use_pynvml = HAS_PYNVML |
| self._peak_vram = 0.0 |
| self._gpu_utils: list[float] = [] |
|
|
| if self.use_pynvml: |
| pynvml.nvmlInit() |
| self._handle = pynvml.nvmlDeviceGetHandleByIndex(0) |
|
|
| def poll(self): |
| if self.use_pynvml: |
| try: |
| info = pynvml.nvmlDeviceGetMemoryInfo(self._handle) |
| vram_mb = info.used / (1024 * 1024) |
| self._peak_vram = max(self._peak_vram, vram_mb) |
| util = pynvml.nvmlDeviceGetUtilizationRates(self._handle) |
| self._gpu_utils.append(util.gpu) |
| except Exception: |
| pass |
|
|
| @property |
| def stats(self) -> dict: |
| return { |
| "vram_peak_mb": round(self._peak_vram, 1), |
| "gpu_util_pct": round(sum(self._gpu_utils) / len(self._gpu_utils), 1) if self._gpu_utils else 0.0, |
| } |
|
|
| def close(self): |
| if self.use_pynvml: |
| try: |
| pynvml.nvmlShutdown() |
| except Exception: |
| pass |
|
|
|
|
| |
| |
| |
|
|
| def load_prompts(prompts_path: str) -> list[dict]: |
| with open(prompts_path, "r", encoding="utf-8") as f: |
| return [json.loads(line) for line in f if line.strip()] |
|
|
|
|
| def run_direct_benchmark( |
| llm, |
| tokenizer, |
| prompts: list[dict], |
| gpu_monitor: GpuMonitor, |
| config_info: dict, |
| warmup: bool = True, |
| ) -> list[DirectBenchResult]: |
| """Run direct vLLM benchmark (no HTTP).""" |
| results: list[DirectBenchResult] = [] |
|
|
| |
| if warmup: |
| print(" π₯ Warmup...") |
| sampling_params = llm.__class__.__module__.split(".")[0] |
| try: |
| from vllm import SamplingParams |
| llm.generate(["Hello"], SamplingParams(max_tokens=10, temperature=0)) |
| print(" β
Warmup OK") |
| except Exception as e: |
| print(f" β οΈ Warmup failed: {e}") |
|
|
| from vllm import SamplingParams |
|
|
| for i, p in enumerate(prompts, 1): |
| prompt_text = p["prompt"] |
| prompt_id = p["id"] |
| max_tokens = p.get("max_tokens", 256) |
|
|
| result = DirectBenchResult( |
| prompt_id=prompt_id, |
| category=p.get("category", "unknown"), |
| **config_info, |
| ) |
|
|
| print(f" [{i}/{len(prompts)}] {prompt_id} ({result.category})...", end=" ", flush=True) |
|
|
| gpu_monitor.poll() |
|
|
| try: |
| |
| input_ids = tokenizer.encode(prompt_text) |
| result.input_tokens = len(input_ids) |
|
|
| sampling_params = SamplingParams( |
| max_tokens=max_tokens, |
| temperature=0.0, |
| ignore_eos=False, |
| ) |
|
|
| t0 = time.perf_counter() |
| outputs = llm.generate([prompt_text], sampling_params, use_tqdm=False) |
| elapsed = time.perf_counter() - t0 |
|
|
| gpu_monitor.poll() |
|
|
| if outputs: |
| out = outputs[0] |
| result.output_tokens = len(out.outputs[0].token_ids) |
|
|
| |
| metrics = out.metrics |
| if hasattr(metrics, 'first_token_time') and metrics.first_token_time is not None: |
| |
| result.prefill_time_s = metrics.first_token_time - getattr(metrics, 'arrival_time', 0) |
| else: |
| |
| result.prefill_time_s = elapsed * 0.3 |
|
|
| result.decode_time_s = elapsed - result.prefill_time_s |
| result.total_time_s = elapsed |
|
|
| if result.prefill_time_s > 0: |
| result.prefill_tps = result.input_tokens / result.prefill_time_s |
| if result.decode_time_s > 0: |
| result.decode_tps = result.output_tokens / result.decode_time_s |
| if elapsed > 0: |
| result.output_tps = result.output_tokens / elapsed |
|
|
| print(f"β
{result.output_tps:.1f} tok/s (prefill={result.prefill_tps:.0f}, decode={result.decode_tps:.0f})") |
|
|
| else: |
| result.success = False |
| result.error = "No output generated" |
| print("β No output") |
|
|
| except Exception as e: |
| result.success = False |
| result.error = str(e)[:500] |
| print(f"β {str(e)[:100]}") |
|
|
| results.append(result) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| DIRECT_CSV_COLUMNS = [ |
| "prompt_id", "category", "model", "config_name", |
| "max_model_len", "gpu_memory_util", "dtype", "quantization", |
| "input_tokens", "output_tokens", |
| "prefill_time_s", "decode_time_s", "total_time_s", |
| "prefill_tps", "decode_tps", "output_tps", |
| "vram_peak_mb", "gpu_util_pct", |
| "success", "error", |
| ] |
|
|
|
|
| def save_results(results: list[DirectBenchResult], csv_path: str): |
| file_exists = os.path.exists(csv_path) |
| os.makedirs(os.path.dirname(csv_path), exist_ok=True) |
| with open(csv_path, "a", newline="", encoding="utf-8") as f: |
| writer = csv.DictWriter(f, fieldnames=DIRECT_CSV_COLUMNS, extrasaction='ignore') |
| if not file_exists or os.path.getsize(csv_path) == 0: |
| writer.writeheader() |
| for r in results: |
| writer.writerow(asdict(r)) |
| print(f"\nπ {len(results)} results saved to {csv_path}") |
|
|
|
|
| def print_summary(results: list[DirectBenchResult]): |
| successful = [r for r in results if r.success] |
| if not successful: |
| print("\nβ No successful results") |
| return |
|
|
| decode_tps_vals = [r.decode_tps for r in successful if r.decode_tps > 0] |
| output_tps_vals = [r.output_tps for r in successful] |
|
|
| print("\n" + "=" * 60) |
| print(" DIRECT VLLM BENCHMARK SUMMARY") |
| print("=" * 60) |
| print(f" Successful: {len(successful)}/{len(results)}") |
| if decode_tps_vals: |
| print(f" Decode TPS: {min(decode_tps_vals):.1f} / {sum(decode_tps_vals)/len(decode_tps_vals):.1f} / {max(decode_tps_vals):.1f} (min/avg/max)") |
| print(f" Output TPS: {min(output_tps_vals):.1f} / {sum(output_tps_vals)/len(output_tps_vals):.1f} / {max(output_tps_vals):.1f} (min/avg/max)") |
|
|
| gpu_results = [r for r in successful if r.vram_peak_mb > 0] |
| if gpu_results: |
| peak_vram = max(r.vram_peak_mb for r in gpu_results) |
| print(f"\n VRAM peak: {peak_vram:.0f} MiB / 24576 MiB") |
| print("=" * 60) |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Benchmark vLLM directly (in-process)") |
| parser.add_argument("--model", default="Qwen/Qwen3.6-27B-FP8", help="Model name or path") |
| parser.add_argument("--max-model-len", type=int, default=8192, help="Max model length") |
| parser.add_argument("--gpu-memory-utilization", type=float, default=0.90, help="GPU memory utilization") |
| parser.add_argument("--dtype", default="auto", help="Data type (auto, float16, bfloat16)") |
| parser.add_argument("--quantization", default="", help="Quantization method") |
| parser.add_argument("--prompts", default=None, help="JSONL prompts file") |
| parser.add_argument("--output", default=None, help="Output CSV path") |
| parser.add_argument("--config", default="direct", help="Config name for logging") |
| parser.add_argument("--max-num-seqs", type=int, default=4, help="Max number of sequences") |
| parser.add_argument("--trust-remote-code", action="store_true", default=True) |
|
|
| args = parser.parse_args() |
|
|
| project_dir = Path(__file__).resolve().parent.parent |
| if args.prompts is None: |
| args.prompts = str(project_dir / "prompts" / "bench_prompts.jsonl") |
| if args.output is None: |
| args.output = str(project_dir / "reports" / "results_direct.csv") |
|
|
| print("=" * 60) |
| print(" Qwen SpeedLab β Direct vLLM Benchmark") |
| print("=" * 60) |
| print(f" Model: {args.model}") |
| print(f" Max len: {args.max_model_len}") |
| print(f" GPU mem: {args.gpu_memory_utilization}") |
| print(f" Dtype: {args.dtype}") |
| print(f" Prompts: {args.prompts}") |
| print("=" * 60) |
| print() |
|
|
| |
| print("π¦ Importing vLLM...") |
| try: |
| from vllm import LLM, SamplingParams |
| except ImportError: |
| print("β vLLM not installed. Run: pip install vllm") |
| sys.exit(1) |
|
|
| |
| if not os.path.exists(args.prompts): |
| print(f"β Prompts file not found: {args.prompts}") |
| sys.exit(1) |
| prompts = load_prompts(args.prompts) |
| print(f"π Loaded {len(prompts)} prompts") |
|
|
| |
| gpu_monitor = GpuMonitor() |
| gpu_monitor.poll() |
|
|
| |
| print(f"\nπ Loading model: {args.model}") |
| print(f" max_model_len={args.max_model_len}, gpu_memory_utilization={args.gpu_memory_utilization}") |
| t0 = time.perf_counter() |
|
|
| try: |
| llm = LLM( |
| model=args.model, |
| max_model_len=args.max_model_len, |
| gpu_memory_utilization=args.gpu_memory_utilization, |
| dtype=args.dtype, |
| quantization=args.quantization if args.quantization else None, |
| trust_remote_code=args.trust_remote_code, |
| max_num_seqs=args.max_num_seqs, |
| tensor_parallel_size=1, |
| enable_prefix_caching=True, |
| ) |
| load_time = time.perf_counter() - t0 |
| print(f"β
Model loaded in {load_time:.1f}s") |
|
|
| |
| tokenizer = llm.get_tokenizer() |
|
|
| gpu_monitor.poll() |
|
|
| except Exception as e: |
| print(f"β Failed to load model: {e}") |
| gpu_monitor.close() |
| sys.exit(1) |
|
|
| |
| config_info = { |
| "model": args.model, |
| "config_name": args.config, |
| "max_model_len": args.max_model_len, |
| "gpu_memory_util": args.gpu_memory_utilization, |
| "dtype": args.dtype, |
| "quantization": args.quantization, |
| } |
|
|
| print(f"\nπ Running benchmark ({len(prompts)} prompts)...\n") |
| results = run_direct_benchmark( |
| llm=llm, |
| tokenizer=tokenizer, |
| prompts=prompts, |
| gpu_monitor=gpu_monitor, |
| config_info=config_info, |
| warmup=True, |
| ) |
|
|
| gpu_monitor.close() |
|
|
| |
| save_results(results, args.output) |
|
|
| |
| print_summary(results) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|