qwen-speedlab / scripts /sweep_advanced.py
simonlesaumon's picture
Upload folder using huggingface_hub
3cabd3a verified
Raw
History Blame Contribute Delete
29.3 kB
#!/usr/bin/env python3
"""
sweep_advanced.py โ€” Advanced vLLM optimization sweep for RTX 3090.
Tests each optimization dimension independently to measure its impact:
1. Attention backend: flash-attn vs flashinfer
2. KV cache dtype: auto vs fp8
3. Chunked prefill: on vs off
4. Block size: 8 vs 16 vs 32
5. GPU memory utilization: 0.85 vs 0.88 vs 0.90 vs 0.92
6. max_num_seqs vs max_batched_tokens grid
7. AWQ vs GPTQ vs FP8 model comparison
8. Speculative decoding (optional, if draft model available)
Usage:
# Baseline: test each optimization in isolation
python scripts/sweep_advanced.py --mode baseline
# Model shootout: compare AWQ vs GPTQ vs FP8
python scripts/sweep_advanced.py --mode shootout
# Full grid: exhaustively test combinations
python scripts/sweep_advanced.py --mode grid
# Single test with full instrumentation
python scripts/sweep_advanced.py --mode single --config awq-flashinfer-fp8kv
"""
from __future__ import annotations
import argparse
import json
import os
import signal
import socket
import subprocess
import sys
import time
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path
from typing import Optional
import httpx
PROJECT_DIR = Path(__file__).resolve().parent.parent
REPORTS_DIR = PROJECT_DIR / "reports"
PROMPTS_FILE = PROJECT_DIR / "prompts" / "bench_prompts.jsonl"
RESULTS_CSV = REPORTS_DIR / "results_advanced.csv"
SUMMARY_MD = REPORTS_DIR / "summary_advanced.md"
# ---- GPU warmup / profiler ----
try:
import pynvml
HAS_PYNVML = True
except ImportError:
HAS_PYNVML = False
# ---- vLLM availability ----
VLLM_AVAILABLE = False
try:
import vllm
VLLM_AVAILABLE = True
except ImportError:
pass
@dataclass
class OptimizedConfig:
"""A single vLLM config to benchmark."""
name: str
model_path: str
quant_method: str # awq, fp8, gptq, none
max_model_len: int = 8192
gpu_memory_utilization: float = 0.90
dtype: str = "auto"
max_num_seqs: int = 8
max_num_batched_tokens: int = 32768
attention_backend: str = "flashinfer" # flashinfer, flash-attn, auto
kv_cache_dtype: str = "auto" # auto, fp8
enable_chunked_prefill: bool = False
block_size: int = 16
swap_space: int = 4
enable_prefix_caching: bool = True
max_seq_len_to_capture: int = 8192
# Speculative decoding
speculative_model: str = ""
num_speculative_tokens: int = 5
@property
def label(self) -> str:
parts = [self.quant_method, f"len{self.max_model_len}"]
if self.attention_backend != "auto":
parts.append(self.attention_backend.replace("-", ""))
if self.kv_cache_dtype == "fp8":
parts.append("fp8kv")
if self.enable_chunked_prefill:
parts.append("chunk")
parts.append(f"bs{self.block_size}")
parts.append(f"s{self.max_num_seqs}")
return "-".join(parts)
def to_server_flags(self, port: int = 8000) -> list[str]:
"""Convert to vLLM CLI arguments."""
flags = [
"--model", self.model_path,
"--host", "0.0.0.0",
"--port", str(port),
"--max-model-len", str(self.max_model_len),
"--gpu-memory-utilization", str(self.gpu_memory_utilization),
"--dtype", self.dtype,
"--max-num-seqs", str(self.max_num_seqs),
"--max-num-batched-tokens", str(self.max_num_batched_tokens),
"--tensor-parallel-size", "1",
"--trust-remote-code",
"--block-size", str(self.block_size),
"--swap-space", str(self.swap_space),
"--max-seq-len-to-capture", str(self.max_seq_len_to_capture),
]
if self.enable_prefix_caching:
flags.append("--enable-prefix-caching")
if self.enable_chunked_prefill:
flags.append("--enable-chunked-prefill")
if self.attention_backend not in ("auto", ""):
flags.extend(["--attention-backend", self.attention_backend])
if self.kv_cache_dtype not in ("auto", ""):
flags.extend(["--kv-cache-dtype", self.kv_cache_dtype])
if self.speculative_model:
flags.extend(["--speculative-model", self.speculative_model])
flags.extend(["--num-speculative-tokens", str(self.num_speculative_tokens)])
return flags
# ============================================================
# Config presets for each sweep mode
# ============================================================
# Models โ€” best to worst for RTX 3090
MODELS = {
"awq": "QuantTrio/Qwen3.6-27B-AWQ",
"gptq": "groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit",
"fp8": "Qwen/Qwen3.6-27B-FP8",
"base": "Qwen/Qwen3.6-27B",
}
def baseline_configs() -> list[OptimizedConfig]:
"""
A/B test each optimization dimension independently.
All configs use AWQ as the base model (best for Ampere).
"""
BASE = dict(
model_path=MODELS["awq"], quant_method="awq",
max_model_len=8192, gpu_memory_utilization=0.90,
max_num_seqs=8, max_num_batched_tokens=32768,
)
configs = []
# ---- 1. Attention backend shootout ----
for attn in ["flashinfer", "flash-attn"]:
c = OptimizedConfig(name=f"attn-{attn}", attention_backend=attn, **BASE)
configs.append(c)
# ---- 2. FP8 KV cache (VRAM saving) ----
for kv in ["auto", "fp8"]:
c = OptimizedConfig(name=f"kv-{kv}", kv_cache_dtype=kv, **BASE)
configs.append(c)
# ---- 3. Chunked prefill ----
for cp in [False, True]:
tag = "chunk-on" if cp else "chunk-off"
c = OptimizedConfig(name=tag, enable_chunked_prefill=cp, **BASE)
configs.append(c)
# ---- 4. Block size sweep ----
for bs in [8, 16, 32]:
c = OptimizedConfig(name=f"blksz-{bs}", block_size=bs, **BASE)
configs.append(c)
# ---- 5. GPU memory utilization sweep ----
for mem in [0.85, 0.88, 0.90, 0.92]:
c = OptimizedConfig(name=f"gpumem-{int(mem*100)}", gpu_memory_utilization=mem, **BASE)
configs.append(c)
# ---- 6. Batch size sweep ----
for seqs, batched in [(4, 16384), (8, 32768), (12, 49152)]:
c = OptimizedConfig(
name=f"batch-s{seqs}-b{batched}",
max_num_seqs=seqs, max_num_batched_tokens=batched, **BASE,
)
configs.append(c)
# ---- 7. Combined best guess ----
c = OptimizedConfig(
name="combined-best",
attention_backend="flashinfer",
kv_cache_dtype="fp8",
enable_chunked_prefill=True,
block_size=16,
max_num_seqs=8,
max_num_batched_tokens=32768,
**BASE,
)
configs.append(c)
return configs
def model_shootout_configs() -> list[OptimizedConfig]:
"""Compare AWQ vs GPTQ vs FP8 on equal footing."""
configs = []
COMMON = dict(
max_model_len=8192, gpu_memory_utilization=0.90,
max_num_seqs=8, max_num_batched_tokens=32768,
attention_backend="flashinfer", kv_cache_dtype="fp8",
enable_chunked_prefill=True,
)
# AWQ โ€” expected best
configs.append(OptimizedConfig(
name="shootout-awq", model_path=MODELS["awq"], quant_method="awq", **COMMON,
))
# GPTQ Marlin โ€” potentially faster on Ampere
configs.append(OptimizedConfig(
name="shootout-gptq", model_path=MODELS["gptq"], quant_method="gptq", **COMMON,
))
# FP8 โ€” higher precision, tighter on VRAM
configs.append(OptimizedConfig(
name="shootout-fp8", model_path=MODELS["fp8"], quant_method="fp8",
max_num_seqs=6, max_num_batched_tokens=16384, **COMMON,
))
# FP8 + FP8 KV cache
configs.append(OptimizedConfig(
name="shootout-fp8-fp8kv", model_path=MODELS["fp8"], quant_method="fp8",
max_num_seqs=8, max_num_batched_tokens=32768, **COMMON,
))
return configs
def grid_configs() -> list[OptimizedConfig]:
"""Full grid search over the most impactful dimensions. ~20 configs."""
configs = []
for model_key, model_path in [("awq", MODELS["awq"]), ("gptq", MODELS["gptq"])]:
for attn in ["flashinfer", "flash-attn"]:
for kv in ["fp8", "auto"]:
for bs in [16, 32]:
for seqs in [8, 12]:
c = OptimizedConfig(
name=f"grid-{model_key}-{attn}-{kv}kv-bs{bs}-s{seqs}",
model_path=model_path,
quant_method=model_key,
attention_backend=attn,
kv_cache_dtype=kv,
block_size=bs,
max_num_seqs=seqs,
max_num_batched_tokens=seqs * 4096,
max_model_len=8192,
gpu_memory_utilization=0.90,
enable_chunked_prefill=True,
)
configs.append(c)
return configs
# ============================================================
# Server lifecycle
# ============================================================
class VllmServer:
"""Manage a vLLM server process."""
def __init__(self, config: OptimizedConfig, port: int = 8000):
self.config = config
self.port = port
self.process: Optional[subprocess.Popen] = None
self.logfile = Path(f"/tmp/vllm_adv_{config.name}.log")
def start(self):
cmd = [sys.executable, "-m", "vllm.entrypoints.openai.api_server"]
cmd.extend(self.config.to_server_flags(port=self.port))
print(f" {' '.join(cmd)}")
self.logfile.parent.mkdir(parents=True, exist_ok=True)
fout = open(str(self.logfile), "w")
self.process = subprocess.Popen(
cmd, stdout=fout, stderr=subprocess.STDOUT,
start_new_session=True,
env={**os.environ, "VLLM_ATTENTION_BACKEND": self.config.attention_backend},
)
return True
def wait_ready(self, timeout: int = 600) -> tuple[bool, str]:
client = httpx.Client(base_url=f"http://localhost:{self.port}", timeout=httpx.Timeout(8.0))
deadline = time.time() + timeout
while time.time() < deadline:
if self.process and self.process.poll() is not None:
tail = self._log_tail(60)
return False, f"CRASH rc={self.process.returncode}\n{tail}"
try:
resp = client.get("/v1/models")
if resp.status_code == 200:
client.close()
return True, "OK"
except Exception:
pass
time.sleep(5)
client.close()
return False, f"TIMEOUT ({timeout}s)\n{self._log_tail(30)}"
def stop(self):
if not self.process:
return
print(" โน๏ธ Stopping server...")
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
self.process.wait(timeout=30)
except subprocess.TimeoutExpired:
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
self.process.wait(timeout=10)
except Exception:
pass
self.process = None
time.sleep(5)
def _log_tail(self, lines: int = 40) -> str:
if not self.logfile.exists():
return "(no log)"
try:
with open(self.logfile, "r") as f:
return "".join(f.readlines()[-lines:])
except Exception:
return "(error)"
# ============================================================
# Benchmark integration
# ============================================================
@dataclass
class SweepResult:
config: OptimizedConfig
success: bool
error: str = ""
avg_output_tps: float = 0.0
avg_total_tps: float = 0.0
avg_ttft_ms: float = 0.0
avg_tpot_ms: float = 0.0
peak_vram_mb: float = 0.0
avg_gpu_util_pct: float = 0.0
avg_power_w: float = 0.0
ok: int = 0
fail: int = 0
duration_s: float = 0.0
def parse_csv_for_config(csv_path: str, config_name: str) -> Optional[dict]:
"""Aggregate results from CSV for a config."""
if not os.path.exists(csv_path):
return None
import csv
try:
with open(csv_path, "r") as f:
rows = [r for r in csv.DictReader(f) if r.get("config_name") == config_name]
except Exception:
return None
if not rows:
# Fallback: take last 30 entries
with open(csv_path, "r") as f:
rows = list(csv.DictReader(f))[-30:]
ok = [r for r in rows if r.get("success") == "True"]
if not ok:
return {"ok": 0, "fail": len(rows)}
def avg(key):
vals = [float(r[key]) for r in ok if r.get(key)]
return sum(vals) / len(vals) if vals else 0.0
def pct(key):
vals = [float(r[key]) for r in ok if r.get(key) and float(r.get(key)) > 0]
return sum(vals) / len(vals) if vals else 0.0
def maxv(key):
vals = [float(r[key]) for r in ok if r.get(key)]
return max(vals) if vals else 0.0
return {
"ok": len(ok),
"fail": len(rows) - len(ok),
"avg_output_tps": avg("output_tps"),
"avg_total_tps": avg("total_tps"),
"avg_ttft_ms": avg("ttft_ms"),
"avg_tpot_ms": avg("tpot_ms"),
"peak_vram_mb": maxv("vram_peak_mb"),
"avg_gpu_util_pct": pct("gpu_util_pct"),
"avg_power_w": pct("power_w"),
}
def run_benchmark_subprocess(config: OptimizedConfig, port: int = 8000) -> tuple[bool, str]:
"""Run bench_openai_api.py as a subprocess."""
bench_script = PROJECT_DIR / "scripts" / "bench_openai_api.py"
cmd = [
sys.executable, str(bench_script),
"--url", f"http://localhost:{port}",
"--prompts", str(PROMPTS_FILE),
"--output", str(RESULTS_CSV),
"--model", config.model_path,
"--backend", "vllm",
"--config", config.name,
"--max-model-len", str(config.max_model_len),
"--gpu-mem-util", str(config.gpu_memory_utilization),
"--dtype", config.dtype,
"--quantization", config.quant_method,
"--repeat", "2",
]
print(f" ๐Ÿ”ฌ Running benchmark...")
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if result.returncode == 0:
# Extract key lines
for line in result.stdout.split("\n"):
if any(kw in line for kw in ["Output TPS:", "TTFT", "SUMMARY", "Success"]):
print(f" {line.strip()}")
return True, ""
else:
err = result.stderr[-500:] if result.stderr else result.stdout[-500:]
return False, err
except subprocess.TimeoutExpired:
return False, "Benchmark subprocess timed out"
except Exception as e:
return False, str(e)
# ============================================================
# Orchestrator
# ============================================================
def sweep(configs: list[OptimizedConfig], port: int = 8000) -> list[SweepResult]:
results: list[SweepResult] = []
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
# Backup old CSV
if RESULTS_CSV.exists():
RESULTS_CSV.rename(RESULTS_CSV.with_suffix(".csv.bak"))
print(f"\n{'='*70}")
print(f" ADVANCED OPTIMIZATION SWEEP โ€” {len(configs)} configs")
print(f"{'='*70}")
for i, c in enumerate(configs, 1):
print(f" [{i:2d}] {c.name:50s} ({c.label[:70]})")
print(f"{'='*70}\n")
for idx, config in enumerate(configs, 1):
result = SweepResult(config=config, success=False)
t0 = time.time()
print(f"\n{'#'*70}")
print(f" [{idx}/{len(configs)}] {config.name}")
print(f" attn={config.attention_backend} kv={config.kv_cache_dtype} "
f"chunk={config.enable_chunked_prefill} bs={config.block_size} "
f"seqs={config.max_num_seqs}")
print(f"{'#'*70}")
# Start server
server = VllmServer(config, port=port)
try:
server.start()
except Exception as e:
result.error = f"START FAIL: {e}"
results.append(result)
continue
# Wait for ready
ok, msg = server.wait_ready()
if not ok:
result.error = msg[:500]
result.duration_s = time.time() - t0
server.stop()
results.append(result)
print(f" โŒ Failed to start: {msg[:200]}")
continue
print(f" โœ… Server ready")
# Run benchmark
bench_ok, bench_err = run_benchmark_subprocess(config, port=port)
# Parse results
metrics = parse_csv_for_config(str(RESULTS_CSV), config.name)
if metrics:
result.success = metrics.get("ok", 0) > 0
result.avg_output_tps = metrics.get("avg_output_tps", 0)
result.avg_total_tps = metrics.get("avg_total_tps", 0)
result.avg_ttft_ms = metrics.get("avg_ttft_ms", 0)
result.avg_tpot_ms = metrics.get("avg_tpot_ms", 0)
result.peak_vram_mb = metrics.get("peak_vram_mb", 0)
result.avg_gpu_util_pct = metrics.get("avg_gpu_util_pct", 0)
result.avg_power_w = metrics.get("avg_power_w", 0)
result.ok = metrics.get("ok", 0)
result.fail = metrics.get("fail", 0)
if not bench_ok:
result.error = bench_err[:500]
if not result.success:
result.error = bench_err[:500] or result.error
result.duration_s = time.time() - t0
server.stop()
status = "โœ…" if result.success else "โŒ"
print(f" {status} TPS={result.avg_output_tps:.1f} TTFT={result.avg_ttft_ms:.0f}ms "
f"VRAM={result.peak_vram_mb:.0f}MB [{result.ok}/{result.ok + result.fail}] "
f"({result.duration_s:.0f}s)")
results.append(result)
return results
# ============================================================
# Report generation
# ============================================================
def generate_report(results: list[SweepResult], mode: str):
ok = [r for r in results if r.success]
fail = [r for r in results if not r.success]
ok_sorted = sorted(ok, key=lambda r: r.avg_output_tps, reverse=True)
lines = [
f"# Qwen SpeedLab โ€” Advanced Optimization Report",
f"",
f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"**GPU:** NVIDIA GeForce RTX 3090 (24 GB, SM 8.6)",
f"**Mode:** {mode}",
f"**Configs:** {len(results)} tested โ€” {len(ok)} passed, {len(fail)} failed",
f"",
"---",
f"",
]
if ok_sorted:
best = ok_sorted[0]
lines += [
f"## ๐Ÿ† Best Configuration",
f"",
f"```",
f" Config name: {best.config.name}",
f" Model: {best.config.model_path}",
f" Attention: {best.config.attention_backend}",
f" KV cache dtype: {best.config.kv_cache_dtype}",
f" Chunked prefill: {best.config.enable_chunked_prefill}",
f" Block size: {best.config.block_size}",
f" Max seqs: {best.config.max_num_seqs}",
f" Max batched tok: {best.config.max_num_batched_tokens}",
f" GPU mem util: {best.config.gpu_memory_utilization}",
f"```",
f"",
f"| Metric | Value |",
f"|--------|-------|",
f"| **Output TPS** | **{best.avg_output_tps:.1f} tok/s** |",
f"| Total TPS | {best.avg_total_tps:.1f} tok/s |",
f"| TTFT | {best.avg_ttft_ms:.0f} ms |",
f"| TPOT | {best.avg_tpot_ms:.1f} ms |",
f"| Peak VRAM | {best.peak_vram_mb:.0f} MB |",
f"| GPU util | {best.avg_gpu_util_pct:.1f}% |",
f"| Power | {best.avg_power_w:.1f} W |",
f"| Prompt success | {best.ok}/{best.ok + best.fail} |",
f"| Test duration | {best.duration_s:.0f}s |",
f"",
]
# ---- Optimization dimension analysis ----
lines += [
"## ๐Ÿ“Š Optimization Impact Analysis",
"",
"Each table shows the effect of toggling one optimization dimension.",
"",
]
# Group by dimension
dimensions = {
"attention_backend": "Attention Backend",
"kv_cache_dtype": "KV Cache Dtype",
"enable_chunked_prefill": "Chunked Prefill",
"block_size": "Block Size",
"gpu_memory_utilization": "GPU Memory Utilization",
"max_num_seqs": "Max Concurrent Sequences",
}
for dim, title in dimensions.items():
groups: dict[str, list[SweepResult]] = {}
for r in ok:
key = str(getattr(r.config, dim, "?"))
groups.setdefault(key, []).append(r)
if len(groups) <= 1:
continue
lines += [f"### {title}", ""]
lines += ["| Value | Avg TPS | TTFT ms | VRAM MB | Tests |",
"|-------|---------|---------|---------|-------|"]
for key in sorted(groups.keys()):
grp = groups[key]
avg_tps = sum(r.avg_output_tps for r in grp) / len(grp)
avg_ttft = sum(r.avg_ttft_ms for r in grp) / len(grp)
avg_vram = sum(r.peak_vram_mb for r in grp) / len(grp)
n = sum(r.ok for r in grp)
lines.append(f"| `{key}` | {avg_tps:.1f} | {avg_ttft:.0f} | {avg_vram:.0f} | {n} |")
lines.append("")
# ---- Full results table ----
lines += [
"## ๐Ÿ“‹ All Configurations",
"",
"| # | Name | Attn | KV | Chunk | BS | Seqs | TPS | TTFT | VRAM | Status |",
"|---|------|------|----|-------|----|------|-----|------|------|--------|",
]
for i, r in enumerate(results, 1):
if r.success:
tps = f"{r.avg_output_tps:.1f}"
ttft = f"{r.avg_ttft_ms:.0f}"
vram = f"{r.peak_vram_mb:.0f}"
status = "โœ…"
else:
tps = ttft = vram = "-"
status = "โŒ"
lines.append(
f"| {i} | {r.config.name} | {r.config.attention_backend} | {r.config.kv_cache_dtype} | "
f"{r.config.enable_chunked_prefill} | {r.config.block_size} | {r.config.max_num_seqs} | "
f"{tps} | {ttft} | {vram} | {status} |"
)
lines.append("")
# ---- Failed ----
if fail:
lines += ["## โŒ Failed Configurations", ""]
for r in fail:
lines.append(f"- **{r.config.name}**: `{r.error[:200]}`")
lines.append("")
# ---- Recommendations ----
lines += [
"## ๐Ÿ’ก Recommendations for RTX 3090",
"",
]
# Analyze what worked best
if ok_sorted:
flashinfer_ok = [r for r in ok if r.config.attention_backend == "flashinfer"]
flashinfer_tps = sum(r.avg_output_tps for r in flashinfer_ok) / len(flashinfer_ok) if flashinfer_ok else 0
flashattn_ok = [r for r in ok if r.config.attention_backend == "flash-attn"]
flashattn_tps = sum(r.avg_output_tps for r in flashattn_ok) / len(flashattn_ok) if flashattn_ok else 0
fp8kv_ok = [r for r in ok if r.config.kv_cache_dtype == "fp8"]
fp8kv_tps = sum(r.avg_output_tps for r in fp8kv_ok) / len(fp8kv_ok) if fp8kv_ok else 0
auto_ok = [r for r in ok if r.config.kv_cache_dtype == "auto"]
auto_tps = sum(r.avg_output_tps for r in auto_ok) / len(auto_ok) if auto_ok else 0
lines.append("1. **Attention backend**:")
if flashinfer_tps > flashattn_tps:
lines.append(f" FlashInfer is faster ({flashinfer_tps:.1f} vs {flashattn_tps:.1f} tok/s). Use `--attention-backend flashinfer`.")
else:
lines.append(f" FlashAttention is comparable ({flashattn_tps:.1f} vs {flashinfer_tps:.1f} tok/s). Default is fine.")
lines.append("")
lines.append("2. **KV Cache dtype**:")
if fp8kv_tps > auto_tps * 0.95:
lines.append(f" FP8 KV cache saves VRAM with minimal quality loss ({fp8kv_tps:.1f} vs {auto_tps:.1f} tok/s). Use `--kv-cache-dtype fp8`.")
else:
lines.append(f" FP8 KV cache costs speed ({fp8kv_tps:.1f} vs {auto_tps:.1f} tok/s). Keep auto unless you need the VRAM.")
best_quant = ok_sorted[0].config.quant_method
lines.append("")
lines.append(f"3. **Best quantization**: `{best_quant}` โ€” based on measured throughput.")
best_bs = max(set(r.config.block_size for r in ok), key=lambda bs: sum(r.avg_output_tps for r in ok if r.config.block_size == bs) / max(1, len([r for r in ok if r.config.block_size == bs])))
lines.append(f"4. **Optimal block size**: `{best_bs}`")
best_seqs = max(set(r.config.max_num_seqs for r in ok), key=lambda s: sum(r.avg_output_tps for r in ok if r.config.max_num_seqs == s) / max(1, len([r for r in ok if r.config.max_num_seqs == s])))
lines.append(f"5. **Optimal max_num_seqs**: `{best_seqs}`")
lines += [
"",
"6. **Always enable**: prefix caching, trust-remote-code",
"7. **Chunked prefill**: helps throughput under concurrent load",
"8. **Swap space**: 4GB CPU swap acts as a safety net against OOM",
"",
"---",
f"*Report generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*",
"",
]
SUMMARY_MD.parent.mkdir(parents=True, exist_ok=True)
with open(SUMMARY_MD, "w") as f:
f.write("\n".join(lines))
print(f"\n๐Ÿ“„ Report: {SUMMARY_MD}")
# ============================================================
# CLI
# ============================================================
def main():
parser = argparse.ArgumentParser(description="Advanced vLLM optimization sweep for RTX 3090")
parser.add_argument("--mode", default="baseline",
choices=["baseline", "shootout", "grid", "single", "list"],
help="baseline=A/B each opt, shootout=AWQ vs GPTQ vs FP8, grid=full combos")
parser.add_argument("--config", type=str, help="Single config name (for --mode single)")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--timeout", type=int, default=600)
parser.add_argument("--dry-run", action="store_true", help="List configs, don't run")
args = parser.parse_args()
# Config selection
if args.mode == "baseline":
configs = baseline_configs()
elif args.mode == "shootout":
configs = model_shootout_configs()
elif args.mode == "grid":
configs = grid_configs()
elif args.mode == "single":
all_configs = baseline_configs() + model_shootout_configs()
configs = [c for c in all_configs if c.name == args.config]
if not configs:
print(f"Unknown config: {args.config}")
print(f"Available: {sorted(set(c.name for c in all_configs))}")
sys.exit(1)
else: # list
for c in baseline_configs() + model_shootout_configs():
print(f" {c.name:45s} {c.label}")
return
print(f"\n{'='*70}")
print(f" ADVANCED SWEEP โ€” {args.mode} mode โ€” {len(configs)} config(s)")
print(f"{'='*70}")
if args.dry_run:
for c in configs:
print(f" {c.name}")
print(f" model={c.model_path}")
print(f" attn={c.attention_backend} kv={c.kv_cache_dtype} chunk={c.enable_chunked_prefill}")
print(f" bs={c.block_size} seqs={c.max_num_seqs} batched={c.max_num_batched_tokens}")
return
# Preflight
print("๐Ÿ” Preflight checks...")
# GPU
try:
result = subprocess.run(
["nvidia-smi", "--query-gpu=memory.free", "--format=csv,noheader"],
capture_output=True, text=True, timeout=10,
)
free_mb = int(result.stdout.strip())
print(f" GPU free: {free_mb} MiB")
if free_mb < 20000:
print(" โš ๏ธ Less than 20GB free โ€” old processes?")
except Exception as e:
print(f" โš ๏ธ GPU check failed: {e}")
# Port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if sock.connect_ex(('localhost', args.port)) == 0:
print(f" โš ๏ธ Port {args.port} already in use!")
sock.close()
# HF_TOKEN
if not os.environ.get("HF_TOKEN"):
print(" โš ๏ธ HF_TOKEN not set")
print(" โœ… Ready\n")
# Run
results = sweep(configs, port=args.port)
# Report
generate_report(results, args.mode)
# Final summary
ok = [r for r in results if r.success]
print(f"\n{'='*70}")
print(f" SWEEP COMPLETE")
print(f" Configs: {len(results)} | Passed: {len(ok)} | Failed: {len(results) - len(ok)}")
if ok:
best = max(ok, key=lambda r: r.avg_output_tps)
print(f" ๐Ÿ† Best: {best.config.name} โ€” {best.avg_output_tps:.1f} tok/s")
print(f" attn={best.config.attention_backend} kv={best.config.kv_cache_dtype} "
f"bs={best.config.block_size} seqs={best.config.max_num_seqs}")
print(f"{'='*70}")
if __name__ == "__main__":
main()