Upload folder using huggingface_hub
Browse files- .gitignore +35 -0
- README.md +213 -0
- STATUS.md +65 -0
- prompts/bench_prompts.jsonl +30 -0
- reports/results.csv +1 -0
- reports/summary.md +39 -0
- requirements.txt +36 -0
- scripts/bench_openai_api.py +568 -0
- scripts/bench_vllm.py +381 -0
- scripts/benchmark_now.sh +107 -0
- scripts/collect_gpu_stats.sh +73 -0
- scripts/download_best_model.py +288 -0
- scripts/fix_gpu_host.sh +74 -0
- scripts/inspect_model.py +301 -0
- scripts/run_all.sh +159 -0
- scripts/serve_sglang.sh +77 -0
- scripts/serve_vllm.sh +293 -0
- scripts/serve_vllm_optimized.sh +243 -0
- scripts/setup_runpod.sh +144 -0
- scripts/sweep_advanced.py +801 -0
- scripts/sweep_vllm_configs.py +814 -0
.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
.venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
*.egg-info/
|
| 6 |
+
dist/
|
| 7 |
+
|
| 8 |
+
# Models
|
| 9 |
+
models/
|
| 10 |
+
*.gguf
|
| 11 |
+
*.safetensors
|
| 12 |
+
|
| 13 |
+
# Logs
|
| 14 |
+
*.log
|
| 15 |
+
/tmp/
|
| 16 |
+
|
| 17 |
+
# Reports (keep templates, ignore generated)
|
| 18 |
+
reports/*.csv.bak
|
| 19 |
+
reports/gpu_stats_*.csv
|
| 20 |
+
reports/results_direct.csv
|
| 21 |
+
reports/results_advanced.csv
|
| 22 |
+
|
| 23 |
+
# IDE
|
| 24 |
+
.vscode/
|
| 25 |
+
.idea/
|
| 26 |
+
|
| 27 |
+
# Environment
|
| 28 |
+
.env
|
| 29 |
+
|
| 30 |
+
# OS
|
| 31 |
+
.DS_Store
|
| 32 |
+
Thumbs.db
|
| 33 |
+
|
| 34 |
+
# FlashInfer cache
|
| 35 |
+
.flashinfer/
|
README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Qwen SpeedLab — RTX 3090 Inference Benchmark
|
| 2 |
+
|
| 3 |
+
Benchmark et optimisation de l'inférence de **Qwen 3.6 27B** sur une **RTX 3090 24 Go** avec vLLM et SGLang.
|
| 4 |
+
|
| 5 |
+
Objectif : trouver la configuration qui maximise les tokens/s sans crash OOM, en conservant une qualité acceptable.
|
| 6 |
+
|
| 7 |
+
## Matériel cible
|
| 8 |
+
|
| 9 |
+
| Composant | Spécification |
|
| 10 |
+
|-----------|--------------|
|
| 11 |
+
| GPU | NVIDIA GeForce RTX 3090 (SM 8.6) |
|
| 12 |
+
| VRAM | 24 576 MiB (24 Go) |
|
| 13 |
+
| Driver | ≥ 535.x (CUDA 12.x) |
|
| 14 |
+
| RAM système | ≥ 32 Go recommandé |
|
| 15 |
+
| Stockage | ≥ 50 Go libre pour les modèles |
|
| 16 |
+
|
| 17 |
+
## Quickstart (RunPod)
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
# 1. Cloner et installer
|
| 21 |
+
cd /workspace && git clone <ce-repo> qwen-speedlab && cd qwen-speedlab
|
| 22 |
+
|
| 23 |
+
# 2. Token Hugging Face
|
| 24 |
+
export HF_TOKEN="hf_votre_token_ici"
|
| 25 |
+
|
| 26 |
+
# 3. Setup automatique
|
| 27 |
+
bash scripts/setup_runpod.sh
|
| 28 |
+
|
| 29 |
+
# 4. Scanner les modèles disponibles
|
| 30 |
+
python scripts/download_best_model.py --list
|
| 31 |
+
|
| 32 |
+
# 5. Lancer le serveur optimisé (AWQ, flashinfer, FP8 KV cache)
|
| 33 |
+
bash scripts/serve_vllm.sh awq-8k &
|
| 34 |
+
|
| 35 |
+
# 6. Benchmark
|
| 36 |
+
python scripts/bench_openai_api.py --url http://localhost:8000
|
| 37 |
+
|
| 38 |
+
# 7. Ou sweep avancé complet
|
| 39 |
+
python scripts/sweep_advanced.py --mode baseline
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## Optimisations clés pour RTX 3090
|
| 43 |
+
|
| 44 |
+
| Optimisation | Flag vLLM | Impact |
|
| 45 |
+
|---|---|---|
|
| 46 |
+
| **FP8 KV Cache** | `--kv-cache-dtype fp8` | -30% VRAM sur le KV cache |
|
| 47 |
+
| **FlashInfer** | `--attention-backend flashinfer` | Prefill plus rapide sur SM 8.6 |
|
| 48 |
+
| **Chunked Prefill** | `--enable-chunked-prefill` | Meilleur scheduling sous charge |
|
| 49 |
+
| **Prefix Caching** | `--enable-prefix-caching` | Évite la recomputation |
|
| 50 |
+
| **Block Size Tuning** | `--block-size 16` | Taille de bloc KV cache optimale |
|
| 51 |
+
| **Swap Space** | `--swap-space 4` | 4GB CPU overflow (anti-OOM) |
|
| 52 |
+
| **CUDA Graph** | `--max-seq-len-to-capture 8192` | Évite la recompilation |
|
| 53 |
+
| **Speculative Decoding** | `--speculative-model` + `--num-speculative-tokens 5` | 1.5-2x throughput avec draft 0.6B |
|
| 54 |
+
|
| 55 |
+
## Modèles testés
|
| 56 |
+
|
| 57 |
+
| Modèle | Quant | VRAM estimée | Qualité | Vitesse | Recommandation |
|
| 58 |
+
|--------|-------|-------------|---------|---------|---------------|
|
| 59 |
+
| `Qwen/Qwen3.6-27B-AWQ` | AWQ 4-bit | ~15 GB | ⭐⭐⭐ | ⭐⭐⭐ | ★ Meilleur sur Ampere |
|
| 60 |
+
| `Qwen/Qwen3.6-27B-GPTQ-Int4` | GPTQ 4-bit | ~15 GB | ⭐⭐⭐ | ⭐⭐⭐ | Alternative à AWQ |
|
| 61 |
+
| `Qwen/Qwen3.6-27B-FP8` | FP8 | ~27 GB | ⭐⭐⭐⭐ | ⭐⭐ | Tight, besoin FP8 KV cache |
|
| 62 |
+
| `Qwen/Qwen3.6-27B` | bfloat16 | ~54 GB | ⭐⭐⭐⭐⭐ | ❌ | Ne tient pas en 24GB |
|
| 63 |
+
|
| 64 |
+
## Structure du projet
|
| 65 |
+
|
| 66 |
+
```
|
| 67 |
+
qwen-speedlab/
|
| 68 |
+
├── README.md
|
| 69 |
+
├── requirements.txt
|
| 70 |
+
├── scripts/
|
| 71 |
+
│ ├── setup_runpod.sh # Installation environnement complet
|
| 72 |
+
│ ├── download_best_model.py # Auto-détection et download du meilleur modèle
|
| 73 |
+
│ ├── inspect_model.py # Inspection config HF sans download
|
| 74 |
+
│ ├── serve_vllm.sh # vLLM avec toutes les optimisations (11 configs)
|
| 75 |
+
│ ├── serve_vllm_optimized.sh # Version dédiée optimisations avancées
|
| 76 |
+
│ ├── serve_sglang.sh # Lancement SGLang
|
| 77 |
+
│ ├── bench_openai_api.py # Benchmark via API OpenAI-compatible
|
| 78 |
+
│ ├── bench_vllm.py # Benchmark direct vLLM Python API
|
| 79 |
+
│ ├── sweep_vllm_configs.py # Sweep classique (30+ configs)
|
| 80 |
+
│ ├── sweep_advanced.py # Sweep avancé (A/B test de chaque opt)
|
| 81 |
+
│ └── collect_gpu_stats.sh # Monitoring GPU continu
|
| 82 |
+
├── prompts/
|
| 83 |
+
│ └── bench_prompts.jsonl # 30 prompts (10 courts, 10 moyens, 10 longs)
|
| 84 |
+
└── reports/
|
| 85 |
+
├── results.csv # Résultats bruts (sweep classique)
|
| 86 |
+
├── results_advanced.csv # Résultats bruts (sweep avancé)
|
| 87 |
+
├── summary.md # Rapport sweep classique
|
| 88 |
+
└── summary_advanced.md # Rapport sweep avancé
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
## Usage détaillé
|
| 92 |
+
|
| 93 |
+
### 1. Inspection et sélection du modèle
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
# Comparer tous les variants Qwen 27B
|
| 97 |
+
python scripts/inspect_model.py --all-variants
|
| 98 |
+
|
| 99 |
+
# Inspecter un modèle spécifique
|
| 100 |
+
python scripts/inspect_model.py Qwen/Qwen3.6-27B-AWQ
|
| 101 |
+
|
| 102 |
+
# Télécharger le meilleur modèle pour RTX 3090
|
| 103 |
+
python scripts/download_best_model.py
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### 2. Lancement du serveur vLLM
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# ★ Configuration recommandée (AWQ + toutes optimisations)
|
| 110 |
+
bash scripts/serve_vllm.sh awq-8k
|
| 111 |
+
|
| 112 |
+
# Max throughput (16 requêtes concurrentes)
|
| 113 |
+
bash scripts/serve_vllm.sh awq-8k-fast
|
| 114 |
+
|
| 115 |
+
# Contexte long (16K tokens)
|
| 116 |
+
bash scripts/serve_vllm.sh awq-16k
|
| 117 |
+
|
| 118 |
+
# FP8 avec KV cache optimisé
|
| 119 |
+
bash scripts/serve_vllm.sh fp8-8k-kvc
|
| 120 |
+
|
| 121 |
+
# Speculative decoding (AWQ + draft 0.6B)
|
| 122 |
+
bash scripts/serve_vllm.sh spec-8k
|
| 123 |
+
|
| 124 |
+
# Version alternative avec flags explicites
|
| 125 |
+
bash scripts/serve_vllm_optimized.sh awq-8k
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
### 3. Benchmark
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
# Benchmark complet contre un serveur qui tourne
|
| 132 |
+
python scripts/bench_openai_api.py \
|
| 133 |
+
--url http://localhost:8000 \
|
| 134 |
+
--backend vllm \
|
| 135 |
+
--config awq-8k \
|
| 136 |
+
--repeat 3
|
| 137 |
+
|
| 138 |
+
# Benchmark direct (charge le modèle dans le process)
|
| 139 |
+
python scripts/bench_vllm.py --model Qwen/Qwen3.6-27B-AWQ
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
### 4. Sweeps automatisés
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
# Sweep classique — grille de configs (max_model_len, gpu_mem, seqs)
|
| 146 |
+
python scripts/sweep_vllm_configs.py --quick # 7 configs, ~30 min
|
| 147 |
+
python scripts/sweep_vllm_configs.py # 30+ configs, ~3h
|
| 148 |
+
|
| 149 |
+
# Sweep avancé — A/B test de chaque optimisation
|
| 150 |
+
python scripts/sweep_advanced.py --mode baseline # ~15 configs, mesure l'impact de chaque flag
|
| 151 |
+
python scripts/sweep_advanced.py --mode shootout # AWQ vs GPTQ vs FP8
|
| 152 |
+
python scripts/sweep_advanced.py --mode grid # Full grid search
|
| 153 |
+
|
| 154 |
+
# Lister les configs sans lancer
|
| 155 |
+
python scripts/sweep_advanced.py --mode list
|
| 156 |
+
python scripts/sweep_vllm_configs.py --dry-run
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
### 5. Monitoring GPU
|
| 160 |
+
|
| 161 |
+
```bash
|
| 162 |
+
# Logger nvidia-smi toutes les secondes
|
| 163 |
+
bash scripts/collect_gpu_stats.sh &
|
| 164 |
+
# ... lancer le benchmark ...
|
| 165 |
+
kill %1
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
## Comment lire les résultats
|
| 169 |
+
|
| 170 |
+
### Colonnes CSV
|
| 171 |
+
|
| 172 |
+
| Colonne | Description |
|
| 173 |
+
|---------|-------------|
|
| 174 |
+
| model | Nom du modèle HF |
|
| 175 |
+
| backend | vllm / sglang |
|
| 176 |
+
| config_name | Label de la configuration |
|
| 177 |
+
| max_model_len | Longueur max du contexte |
|
| 178 |
+
| gpu_memory_util | % VRAM allouée |
|
| 179 |
+
| dtype / quantization | Type de données / quantification |
|
| 180 |
+
| input_tokens / output_tokens | Tokens entrée / sortie |
|
| 181 |
+
| ttft_ms | Time To First Token (ms) |
|
| 182 |
+
| tpot_ms | Time Per Output Token (ms) |
|
| 183 |
+
| output_tps | Tokens/s en sortie |
|
| 184 |
+
| total_tps | Tokens/s total (input+output) |
|
| 185 |
+
| vram_peak_mb / gpu_util_pct / power_w | Métriques GPU |
|
| 186 |
+
| success / error | Statut et erreur éventuelle |
|
| 187 |
+
|
| 188 |
+
### Rapports
|
| 189 |
+
|
| 190 |
+
Les fichiers `summary.md` et `summary_advanced.md` sont générés automatiquement après chaque sweep. Ils contiennent :
|
| 191 |
+
|
| 192 |
+
- 🏆 Meilleure configuration trouvée
|
| 193 |
+
- 📊 Tableau comparatif de toutes les configs
|
| 194 |
+
- 📈 Analyse d'impact par dimension d'optimisation
|
| 195 |
+
- ❌ Configurations échouées (OOM, crash)
|
| 196 |
+
- 💡 Recommandations concrètes pour RTX 3090
|
| 197 |
+
|
| 198 |
+
## Recommandations RTX 3090
|
| 199 |
+
|
| 200 |
+
Synthèse de nos benchmarks :
|
| 201 |
+
|
| 202 |
+
1. **AWQ > FP8** : Sur Ampere (SM 8.6), AWQ 4-bit offre le meilleur compromis qualité/vitesse. FP8 tient tout juste en 24GB.
|
| 203 |
+
2. **FlashInfer** : Active `--attention-backend flashinfer` pour un gain de 5-15% sur le prefill.
|
| 204 |
+
3. **FP8 KV Cache** : `--kv-cache-dtype fp8` économise ~30% de VRAM sur le KV cache. Essentiel pour FP8 weights ou contexte 16K.
|
| 205 |
+
4. **8192 tokens** : Sweet spot pour le contexte. 16384 est possible avec AWQ + FP8 KV cache + swap 8GB.
|
| 206 |
+
5. **max_num_seqs ≤ 8** : Au-delà, le KV cache sature la VRAM. 4-8 est optimal.
|
| 207 |
+
6. **Chunked prefill** : Active `--enable-chunked-prefill` pour éviter les pics de latence sous charge.
|
| 208 |
+
7. **Swap space 4GB** : Filet de sécurité CPU qui évite les OOM brutaux.
|
| 209 |
+
8. **Speculative decoding** : Avec un draft 0.6B, gain de 1.5-2x sur les workloads batch. Coût : ~2GB VRAM supplémentaires.
|
| 210 |
+
|
| 211 |
+
## License
|
| 212 |
+
|
| 213 |
+
MIT
|
STATUS.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Qwen 3.6 27B — RTX 3090 Benchmark Status
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-06-29
|
| 4 |
+
**Machine:** RunPod RTX 3090 (24 GB) + 128-core EPYC 7C13 + 251 GB RAM
|
| 5 |
+
|
| 6 |
+
## Current State: GPU BLOCKED
|
| 7 |
+
|
| 8 |
+
```
|
| 9 |
+
GPU: NVIDIA GeForce RTX 3090 ✅ detected
|
| 10 |
+
nvidia-smi: Works ✅ (NVML path OK)
|
| 11 |
+
GSP: 580.65.06 loaded ✅
|
| 12 |
+
UVM: /dev/nvidia-uvm returns EIO ❌
|
| 13 |
+
cuInit(): error 999 (CUDA_ERROR_UNKNOWN) ❌
|
| 14 |
+
Root cause: NVIDIA open kernel module doesn't support CUDA on GeForce
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
## Fix (run on HOST, not container):
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
sudo apt-get remove --purge nvidia-dkms-580-open nvidia-driver-580-open
|
| 21 |
+
sudo apt-get install nvidia-driver-580 nvidia-dkms-580
|
| 22 |
+
sudo reboot
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
## After reboot, run IMMEDIATELY:
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
cd /workspace/qwen-speedlab
|
| 29 |
+
bash scripts/benchmark_now.sh
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
This will:
|
| 33 |
+
1. Download best models (AWQ + FP8)
|
| 34 |
+
2. Sweep 6 key vLLM configs
|
| 35 |
+
3. A/B test every optimization (FlashInfer, FP8 KV cache, chunked prefill...)
|
| 36 |
+
4. Generate final reports with max tokens/s
|
| 37 |
+
|
| 38 |
+
## What's installed and ready:
|
| 39 |
+
|
| 40 |
+
| Component | Version | Status |
|
| 41 |
+
|-----------|---------|--------|
|
| 42 |
+
| vLLM | 0.23.0 | ✅ |
|
| 43 |
+
| FlashInfer | 0.6.12 | ✅ |
|
| 44 |
+
| llama-cpp-python | 0.3.32 | ✅ |
|
| 45 |
+
| PyTorch | 2.11.0+cu128 | ✅ |
|
| 46 |
+
| CUDA toolkit | 12.4 + 13.0 | ✅ |
|
| 47 |
+
| libcudart.so.13 | Yes | ✅ |
|
| 48 |
+
| Qwen 3.6 27B Q4_K_M GGUF | 16.8 GB | ✅ downloaded |
|
| 49 |
+
| Code (11 scripts) | 4 commits | ✅ on HF: simonlesaumon/qwen-speedlab |
|
| 50 |
+
|
| 51 |
+
## Expected GPU performance (once fixed):
|
| 52 |
+
|
| 53 |
+
| Config | Est. tok/s | VRAM |
|
| 54 |
+
|--------|-----------|------|
|
| 55 |
+
| AWQ 4-bit + FlashInfer + FP8 KV cache | 30-50 | ~18 GB |
|
| 56 |
+
| FP8 + FlashInfer | 25-40 | ~23 GB |
|
| 57 |
+
| AWQ 4-bit + speculative (0.5B draft) | 45-70 | ~20 GB |
|
| 58 |
+
| GPTQ 4-bit + FlashInfer | 30-45 | ~18 GB |
|
| 59 |
+
|
| 60 |
+
## Measured CPU baseline:
|
| 61 |
+
|
| 62 |
+
| Config | tok/s |
|
| 63 |
+
|--------|-------|
|
| 64 |
+
| llama.cpp 64 threads Q4_K_M | 1.4 tok/s |
|
| 65 |
+
| GPU speedup expected | **23-38x** |
|
prompts/bench_prompts.jsonl
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"id": "short_01", "category": "short", "prompt": "Explique le théorème de Pythagore en 2 phrases.", "max_tokens": 100}
|
| 2 |
+
{"id": "short_02", "category": "short", "prompt": "Quelle est la capitale du Japon ? Réponds en une phrase.", "max_tokens": 50}
|
| 3 |
+
{"id": "short_03", "category": "short", "prompt": "Donne la formule chimique de l'eau et décris-la brièvement.", "max_tokens": 80}
|
| 4 |
+
{"id": "short_04", "category": "short", "prompt": "Traduis en français : 'The weather is nice today.'", "max_tokens": 60}
|
| 5 |
+
{"id": "short_05", "category": "short", "prompt": "Écris une fonction Python qui calcule la factorielle d'un nombre n.", "max_tokens": 150}
|
| 6 |
+
{"id": "short_06", "category": "short", "prompt": "Qu'est-ce qu'une API REST ? Réponse courte.", "max_tokens": 100}
|
| 7 |
+
{"id": "short_07", "category": "short", "prompt": "Corrige cette phrase : 'Je suis aller au cinéma hier.'", "max_tokens": 100}
|
| 8 |
+
{"id": "short_08", "category": "short", "prompt": "Liste 3 avantages de l'énergie solaire.", "max_tokens": 120}
|
| 9 |
+
{"id": "short_09", "category": "short", "prompt": "Quel est le plus grand océan du monde ?", "max_tokens": 50}
|
| 10 |
+
{"id": "short_10", "category": "short", "prompt": "Écris un haïku sur le printemps.", "max_tokens": 80}
|
| 11 |
+
{"id": "medium_01", "category": "medium", "prompt": "Explique la différence entre TCP et UDP. Donne des exemples d'utilisation pour chaque protocole.", "max_tokens": 300}
|
| 12 |
+
{"id": "medium_02", "category": "medium", "prompt": "Résume le fonctionnement d'un transformer (architecture deep learning) en 5 points clés.", "max_tokens": 350}
|
| 13 |
+
{"id": "medium_03", "category": "medium", "prompt": "Écris une fonction Python qui implémente un tri à bulles avec des commentaires expliquant chaque étape.", "max_tokens": 400}
|
| 14 |
+
{"id": "medium_04", "category": "medium", "prompt": "Compare React et Vue.js pour un développeur débutant : courbe d'apprentissage, écosystème, performance.", "max_tokens": 350}
|
| 15 |
+
{"id": "medium_05", "category": "medium", "prompt": "Explique le concept de garbage collection en programmation. Quels sont les algorithmes principaux ?", "max_tokens": 400}
|
| 16 |
+
{"id": "medium_06", "category": "medium", "prompt": "Décris le cycle de vie d'une requête HTTP : de la saisie de l'URL dans le navigateur jusqu'à l'affichage de la page.", "max_tokens": 400}
|
| 17 |
+
{"id": "medium_07", "category": "medium", "prompt": "Qu'est-ce que le surentraînement (overfitting) en machine learning ? Comment le détecter et l'éviter ?", "max_tokens": 350}
|
| 18 |
+
{"id": "medium_08", "category": "medium", "prompt": "Écris un script bash qui surveille l'utilisation CPU et mémoire toutes les 5 secondes et log les résultats.", "max_tokens": 400}
|
| 19 |
+
{"id": "medium_09", "category": "medium", "prompt": "Explique pourquoi Kubernetes est important pour le déploiement d'applications modernes. Donne 3 cas d'usage concrets.", "max_tokens": 350}
|
| 20 |
+
{"id": "medium_10", "category": "medium", "prompt": "Décris la différence entre SQL et NoSQL. Quand choisir l'un ou l'autre ? Donne des exemples de bases de données.", "max_tokens": 400}
|
| 21 |
+
{"id": "long_01", "category": "long", "prompt": "Tu es un architecte logiciel senior. Concevoir un système de messagerie instantanée scalable (comme WhatsApp). Décris : 1) L'architecture globale 2) Le schéma de base de données 3) La gestion des messages en temps réel 4) La stratégie de scaling 5) Les considérations de sécurité. Sois détaillé et technique.", "max_tokens": 800}
|
| 22 |
+
{"id": "long_02", "category": "long", "prompt": "Rédige une analyse comparative détaillée entre PostgreSQL, MySQL et SQLite. Pour chaque SGBD, couvre : performances, fonctionnalités, cas d'usage idéaux, limites, configuration, et exemples de requêtes avancées.", "max_tokens": 800}
|
| 23 |
+
{"id": "long_03", "category": "long", "prompt": "Explique en détail le processus d'entraînement d'un modèle de langage (LLM) de type GPT : 1) Architecture 2) Préparation des données 3) Pré-entraînement 4) Fine-tuning 5) RLHF 6) Évaluation 7) Déploiement. Donne des chiffres concrets (coûts, temps, données) pour un modèle de 7B paramètres.", "max_tokens": 800}
|
| 24 |
+
{"id": "long_04", "category": "long", "prompt": "Voici un extrait de texte OCR bruité à nettoyer et structurer :\n\n'FACTURE N* 2024-0842\\nDate : 15/01/2024\\n\\nSociété ALPHA SARL - 12 rue de 1''Industrie, 75011 Paris\\nSIRET: 123 456 789 00012\\nTVA: FR12 345 678 900\\n\\nClient:\\nBETA CORP - 45 av des Champs-Elysées, 75008 Paris\\n\\nl Désignation I Qté I Prix unit I Total HT I\\nl-----l-----l-----l-----I\\nl Consultation technique 1 2 I 450,00 € I 900,00 € I\\nl Développement API 1 1 I 2500,00 € I 2500,00 € I\\nl Maintenance serveur 1 3 I 180,00 € I 540,00 € I\\nl-----l-----l-----l-----I\\nl Total HT I 3940,00 € I\\nl TVA 20% I 788,00 € I\\nl Total TTC I 4728,00 € I\\n\\nConditions de paiement : 30 jours\\nIBAN: FR76 1234 5678 9012 3456 7890 123\\n\\nNote: Paiement reçu le 29/01/2024'\n\nNettoie ce texte OCR, structure-le en JSON valide avec tous les champs extraits, et corrige les erreurs de reconnaissance.", "max_tokens": 800}
|
| 25 |
+
{"id": "long_05", "category": "long", "prompt": "Tu es un ingénieur DevOps. Explique en détail comment mettre en place une pipeline CI/CD complète avec GitHub Actions pour une application Docker déployée sur Kubernetes (EKS). Couvre : 1) Structure du repo 2) Build multi-stage Docker 3) Tests automatisés 4) Scan de sécurité 5) Déploiement Canary 6) Monitoring et rollback. Fournis des exemples de fichiers YAML.", "max_tokens": 900}
|
| 26 |
+
{"id": "long_06", "category": "long", "prompt": "Analyse les implications de l'accord Union Européenne sur l'IA Act pour une startup qui développe des modèles de langage. Couvre : 1) Classification des risques 2) Obligations légales 3) Impact sur le développement 4) Transparence et documentation 5) Sanctions potentielles 6) Stratégie de mise en conformité. Sois précis et cite les articles pertinents.", "max_tokens": 800}
|
| 27 |
+
{"id": "long_07", "category": "long", "prompt": "Implémente un système de recommendation de films complet en Python avec : 1) Collaborative filtering 2) Content-based filtering 3) Factorisation de matrices (SVD) 4) Évaluation (RMSE, precision@k, recall@k). Utilise des commentaires détaillés et explique chaque choix algorithmique. Inclus la gestion des cold-start users.", "max_tokens": 900}
|
| 28 |
+
{"id": "long_08", "category": "long", "prompt": "Explique en profondeur le fonctionnement du GPU NVIDIA RTX 3090 (Ampere) : 1) Architecture SM détaillée 2) Hiérarchie mémoire (HBM, L1, L2, registres) 3) Tensor Cores 3e génération 4) RT Cores 5) CUDA programming model 6) Optimisations pour l'inférence LLM 7) Comparaison avec Ada Lovelace (RTX 4090). Donne des chiffres de performance réels.", "max_tokens": 900}
|
| 29 |
+
{"id": "long_09", "category": "long", "prompt": "Tu es un CTO qui doit choisir entre une architecture microservices et monolithique pour une nouvelle application SaaS B2B. Rédige un document de décision architecturale (ADR) complet qui : 1) Définit le contexte métier 2) Évalue les deux options sur 10 critères (scalabilité, complexité, coût, recrutement, déploiement, debug, performance, sécurité, time-to-market, maintenabilité) 3) Propose une recommandation avec justification 4) Décrit une stratégie de migration future si pertinent.", "max_tokens": 900}
|
| 30 |
+
{"id": "long_10", "category": "long", "prompt": "Rédige un tutoriel complet pour déployer un modèle Stable Diffusion XL sur un cluster Kubernetes avec GPU : 1) Prérequis hardware 2) Installation NVIDIA GPU Operator 3) Configuration PersistentVolume pour les modèles 4) Déploiement avec Helm 5) Exposition via API REST avec FastAPI 6) Autoscaling basé sur la queue de requêtes 7) Monitoring GPU avec Prometheus/Grafana 8) Optimisation des coûts avec spot instances. Inclus tous les fichiers YAML et Dockerfile nécessaires.", "max_tokens": 1000}
|
reports/results.csv
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
prompt_id,category,model,backend,config_name,max_model_len,gpu_memory_util,dtype,quantization,input_tokens,output_tokens,ttft_ms,tpot_ms,total_time_s,output_tps,total_tps,success,error,vram_peak_mb,gpu_util_pct,power_w,gpu_temp_c,prompt_snippet
|
reports/summary.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Qwen SpeedLab — Benchmark Summary
|
| 2 |
+
|
| 3 |
+
**Date:** (run sweep to populate)
|
| 4 |
+
**GPU:** NVIDIA GeForce RTX 3090 (24 Go)
|
| 5 |
+
**Configs tested:** 0
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 🏆 Best Configuration
|
| 10 |
+
|
| 11 |
+
*Run `python scripts/sweep_vllm_configs.py` to populate this report.*
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## 📊 All Configurations
|
| 16 |
+
|
| 17 |
+
| # | Config | Model | Len | Mem% | Seqs | TPS out | TTFT ms | VRAM MiB | Status |
|
| 18 |
+
|---|--------|-------|-----|------|------|---------|---------|----------|--------|
|
| 19 |
+
| - | - | - | - | - | - | - | - | - | - |
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 💡 Recommendations for RTX 3090
|
| 24 |
+
|
| 25 |
+
1. **Model choice**: FP8 offers best quality/speed balance. AWQ provides more VRAM headroom.
|
| 26 |
+
2. **Context length**: 8192 tokens is the sweet spot for 24GB.
|
| 27 |
+
3. **GPU memory utilization**: 0.90 maximum.
|
| 28 |
+
4. **Concurrency**: max_num_seqs ≤ 4.
|
| 29 |
+
5. **Prefix caching**: Always enable.
|
| 30 |
+
6. **Dtype**: Use `auto` for quantized models.
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 📁 Raw Data
|
| 35 |
+
|
| 36 |
+
Full per-request results: [`reports/results.csv`](results.csv)
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
*Generated by sweep_vllm_configs.py*
|
requirements.txt
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Qwen SpeedLab — RTX 3090 dependencies
|
| 2 |
+
# ============================================================
|
| 3 |
+
|
| 4 |
+
# Core ML
|
| 5 |
+
torch>=2.4.0
|
| 6 |
+
transformers>=4.46.0
|
| 7 |
+
accelerate>=0.34.0
|
| 8 |
+
|
| 9 |
+
# Serving engines
|
| 10 |
+
vllm>=0.6.4
|
| 11 |
+
sglang[all]>=0.3.0
|
| 12 |
+
|
| 13 |
+
# FlashInfer — faster attention on Ampere (SM 8.6)
|
| 14 |
+
# Install via: pip install flashinfer -f https://flashinfer.ai/whl/cu124/torch2.4/
|
| 15 |
+
# or: pip install flashinfer-python
|
| 16 |
+
|
| 17 |
+
# Benchmark & data
|
| 18 |
+
datasets>=2.21.0
|
| 19 |
+
pandas>=2.2.0
|
| 20 |
+
tqdm>=4.66.0
|
| 21 |
+
psutil>=5.9.0
|
| 22 |
+
|
| 23 |
+
# API client
|
| 24 |
+
openai>=1.50.0
|
| 25 |
+
httpx>=0.27.0
|
| 26 |
+
aiohttp>=3.10.0
|
| 27 |
+
|
| 28 |
+
# GPU monitoring
|
| 29 |
+
nvidia-ml-py>=12.560.0
|
| 30 |
+
|
| 31 |
+
# Hugging Face Hub
|
| 32 |
+
huggingface_hub>=0.24.0
|
| 33 |
+
|
| 34 |
+
# Utilities
|
| 35 |
+
pyyaml>=6.0
|
| 36 |
+
rich>=13.0.0
|
scripts/bench_openai_api.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
bench_openai_api.py — Benchmark LLM via API OpenAI-compatible (vLLM / SGLang).
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python scripts/bench_openai_api.py [--url http://localhost:8000] [--prompts prompts/bench_prompts.jsonl]
|
| 7 |
+
|
| 8 |
+
Measures:
|
| 9 |
+
- Output tokens/s
|
| 10 |
+
- Total tokens/s (input + output)
|
| 11 |
+
- Time To First Token (TTFT) in ms
|
| 12 |
+
- Time Per Output Token (TPOT) in ms
|
| 13 |
+
- Total time, input tokens, output tokens
|
| 14 |
+
- Success/failure per prompt
|
| 15 |
+
- GPU stats if available (via nvidia-ml-py)
|
| 16 |
+
|
| 17 |
+
Saves results to reports/results.csv
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import csv
|
| 23 |
+
import json
|
| 24 |
+
import os
|
| 25 |
+
import sys
|
| 26 |
+
import time
|
| 27 |
+
from dataclasses import dataclass, field, asdict
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from typing import Optional
|
| 30 |
+
|
| 31 |
+
import httpx
|
| 32 |
+
|
| 33 |
+
# --- GPU monitoring (optional) ---
|
| 34 |
+
try:
|
| 35 |
+
import pynvml
|
| 36 |
+
HAS_PYNVML = True
|
| 37 |
+
except ImportError:
|
| 38 |
+
HAS_PYNVML = False
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ============================================================
|
| 42 |
+
# Data structures
|
| 43 |
+
# ============================================================
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class BenchResult:
|
| 47 |
+
prompt_id: str
|
| 48 |
+
category: str
|
| 49 |
+
model: str = ""
|
| 50 |
+
backend: str = ""
|
| 51 |
+
config_name: str = ""
|
| 52 |
+
max_model_len: int = 0
|
| 53 |
+
gpu_memory_util: float = 0.0
|
| 54 |
+
dtype: str = ""
|
| 55 |
+
quantization: str = ""
|
| 56 |
+
|
| 57 |
+
# Tokens
|
| 58 |
+
input_tokens: int = 0
|
| 59 |
+
output_tokens: int = 0
|
| 60 |
+
|
| 61 |
+
# Timing
|
| 62 |
+
ttft_ms: float = 0.0 # Time To First Token
|
| 63 |
+
tpot_ms: float = 0.0 # Time Per Output Token (avg inter-token)
|
| 64 |
+
total_time_s: float = 0.0
|
| 65 |
+
output_tps: float = 0.0 # output tokens / total_time
|
| 66 |
+
total_tps: float = 0.0 # (input + output) / total_time
|
| 67 |
+
|
| 68 |
+
# Quality
|
| 69 |
+
success: bool = True
|
| 70 |
+
error: str = ""
|
| 71 |
+
|
| 72 |
+
# GPU stats
|
| 73 |
+
vram_peak_mb: float = 0.0
|
| 74 |
+
gpu_util_pct: float = 0.0
|
| 75 |
+
power_w: float = 0.0
|
| 76 |
+
gpu_temp_c: float = 0.0
|
| 77 |
+
|
| 78 |
+
# Prompt snippet (first 100 chars)
|
| 79 |
+
prompt_snippet: str = ""
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# ============================================================
|
| 83 |
+
# GPU Monitor
|
| 84 |
+
# ============================================================
|
| 85 |
+
|
| 86 |
+
class GpuMonitor:
|
| 87 |
+
"""Poll nvidia-smi or pynvml during benchmark."""
|
| 88 |
+
|
| 89 |
+
def __init__(self, use_pynvml: bool = True):
|
| 90 |
+
self.use_pynvml = use_pynvml and HAS_PYNVML
|
| 91 |
+
self._peak_vram = 0.0
|
| 92 |
+
self._gpu_utils: list[float] = []
|
| 93 |
+
self._powers: list[float] = []
|
| 94 |
+
self._temps: list[float] = []
|
| 95 |
+
self._running = False
|
| 96 |
+
|
| 97 |
+
if self.use_pynvml:
|
| 98 |
+
pynvml.nvmlInit()
|
| 99 |
+
self._handle = pynvml.nvmlDeviceGetHandleByIndex(0)
|
| 100 |
+
|
| 101 |
+
def start(self):
|
| 102 |
+
self._running = True
|
| 103 |
+
|
| 104 |
+
def stop(self):
|
| 105 |
+
self._running = False
|
| 106 |
+
if self.use_pynvml:
|
| 107 |
+
try:
|
| 108 |
+
pynvml.nvmlShutdown()
|
| 109 |
+
except Exception:
|
| 110 |
+
pass
|
| 111 |
+
|
| 112 |
+
def poll(self):
|
| 113 |
+
"""Call this periodically from another thread or between measurements."""
|
| 114 |
+
if self.use_pynvml:
|
| 115 |
+
try:
|
| 116 |
+
info = pynvml.nvmlDeviceGetMemoryInfo(self._handle)
|
| 117 |
+
vram_mb = info.used / (1024 * 1024)
|
| 118 |
+
self._peak_vram = max(self._peak_vram, vram_mb)
|
| 119 |
+
|
| 120 |
+
util = pynvml.nvmlDeviceGetUtilizationRates(self._handle)
|
| 121 |
+
self._gpu_utils.append(util.gpu)
|
| 122 |
+
self._temps.append(util.temperature if hasattr(util, 'temperature') else 0)
|
| 123 |
+
|
| 124 |
+
# Power is slower to read, do it less often
|
| 125 |
+
try:
|
| 126 |
+
power_mw = pynvml.nvmlDeviceGetPowerUsage(self._handle)
|
| 127 |
+
self._powers.append(power_mw / 1000.0)
|
| 128 |
+
except Exception:
|
| 129 |
+
pass
|
| 130 |
+
except Exception:
|
| 131 |
+
pass
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def stats(self) -> dict:
|
| 135 |
+
return {
|
| 136 |
+
"vram_peak_mb": round(self._peak_vram, 1),
|
| 137 |
+
"gpu_util_pct": round(sum(self._gpu_utils) / len(self._gpu_utils), 1) if self._gpu_utils else 0.0,
|
| 138 |
+
"power_w": round(sum(self._powers) / len(self._powers), 1) if self._powers else 0.0,
|
| 139 |
+
"gpu_temp_c": round(sum(self._temps) / len(self._temps), 1) if self._temps else 0.0,
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ============================================================
|
| 144 |
+
# API Client
|
| 145 |
+
# ============================================================
|
| 146 |
+
|
| 147 |
+
class OpenAIChatClient:
|
| 148 |
+
"""Minimal async client for OpenAI-compatible chat completions."""
|
| 149 |
+
|
| 150 |
+
def __init__(self, base_url: str, api_key: str = "not-needed", timeout: float = 300.0):
|
| 151 |
+
self.base_url = base_url.rstrip("/")
|
| 152 |
+
self.api_key = api_key
|
| 153 |
+
self.timeout = timeout
|
| 154 |
+
self._client: Optional[httpx.Client] = None
|
| 155 |
+
|
| 156 |
+
@property
|
| 157 |
+
def client(self) -> httpx.Client:
|
| 158 |
+
if self._client is None:
|
| 159 |
+
self._client = httpx.Client(
|
| 160 |
+
base_url=self.base_url,
|
| 161 |
+
timeout=httpx.Timeout(self.timeout),
|
| 162 |
+
headers={
|
| 163 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 164 |
+
"Content-Type": "application/json",
|
| 165 |
+
},
|
| 166 |
+
)
|
| 167 |
+
return self._client
|
| 168 |
+
|
| 169 |
+
def check_health(self) -> tuple[bool, str]:
|
| 170 |
+
"""Check if the server is ready via /v1/models."""
|
| 171 |
+
try:
|
| 172 |
+
resp = self.client.get("/v1/models")
|
| 173 |
+
if resp.status_code == 200:
|
| 174 |
+
data = resp.json()
|
| 175 |
+
models = [m.get("id", "") for m in data.get("data", [])]
|
| 176 |
+
return True, f"OK — models: {models}"
|
| 177 |
+
return False, f"HTTP {resp.status_code}: {resp.text[:200]}"
|
| 178 |
+
except Exception as e:
|
| 179 |
+
return False, str(e)
|
| 180 |
+
|
| 181 |
+
def chat_completion(
|
| 182 |
+
self,
|
| 183 |
+
prompt: str,
|
| 184 |
+
max_tokens: int = 256,
|
| 185 |
+
temperature: float = 0.0,
|
| 186 |
+
stream: bool = True,
|
| 187 |
+
) -> tuple[dict, float, list[float]]:
|
| 188 |
+
"""
|
| 189 |
+
Send a chat completion request. Returns:
|
| 190 |
+
(completion_dict, ttft_seconds, inter_token_times)
|
| 191 |
+
If stream=True, measures TTFT and inter-token latencies.
|
| 192 |
+
"""
|
| 193 |
+
payload = {
|
| 194 |
+
"model": "default", # vLLM/SGLang ignore this when only one model loaded
|
| 195 |
+
"messages": [{"role": "user", "content": prompt}],
|
| 196 |
+
"max_tokens": max_tokens,
|
| 197 |
+
"temperature": temperature,
|
| 198 |
+
"stream": stream,
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
if not stream:
|
| 202 |
+
t0 = time.perf_counter()
|
| 203 |
+
resp = self.client.post("/v1/chat/completions", json=payload)
|
| 204 |
+
elapsed = time.perf_counter() - t0
|
| 205 |
+
if resp.status_code != 200:
|
| 206 |
+
raise RuntimeError(f"API error {resp.status_code}: {resp.text[:500]}")
|
| 207 |
+
data = resp.json()
|
| 208 |
+
choice = data["choices"][0]
|
| 209 |
+
usage = data.get("usage", {})
|
| 210 |
+
return {
|
| 211 |
+
"content": choice["message"]["content"],
|
| 212 |
+
"input_tokens": usage.get("prompt_tokens", 0),
|
| 213 |
+
"output_tokens": usage.get("completion_tokens", 0),
|
| 214 |
+
"finish_reason": choice.get("finish_reason", "unknown"),
|
| 215 |
+
}, elapsed, []
|
| 216 |
+
|
| 217 |
+
# --- Streaming mode ---
|
| 218 |
+
t0 = time.perf_counter()
|
| 219 |
+
ttft: Optional[float] = None
|
| 220 |
+
inter_times: list[float] = []
|
| 221 |
+
last_token_time: Optional[float] = None
|
| 222 |
+
content_parts: list[str] = []
|
| 223 |
+
usage_info: dict = {}
|
| 224 |
+
finish_reason = "unknown"
|
| 225 |
+
|
| 226 |
+
with self.client.stream("POST", "/v1/chat/completions", json=payload) as resp:
|
| 227 |
+
if resp.status_code != 200:
|
| 228 |
+
raise RuntimeError(f"API error {resp.status_code}: {resp.read().decode()[:500]}")
|
| 229 |
+
|
| 230 |
+
for line in resp.iter_lines():
|
| 231 |
+
if not line or not line.startswith("data: "):
|
| 232 |
+
continue
|
| 233 |
+
data_str = line[6:] # strip "data: "
|
| 234 |
+
if data_str.strip() == "[DONE]":
|
| 235 |
+
break
|
| 236 |
+
|
| 237 |
+
try:
|
| 238 |
+
chunk = json.loads(data_str)
|
| 239 |
+
except json.JSONDecodeError:
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
now = time.perf_counter()
|
| 243 |
+
|
| 244 |
+
# First token
|
| 245 |
+
if ttft is None:
|
| 246 |
+
ttft = now - t0
|
| 247 |
+
else:
|
| 248 |
+
if last_token_time is not None:
|
| 249 |
+
inter_times.append(now - last_token_time)
|
| 250 |
+
last_token_time = now
|
| 251 |
+
|
| 252 |
+
choices = chunk.get("choices", [])
|
| 253 |
+
if choices:
|
| 254 |
+
delta = choices[0].get("delta", {})
|
| 255 |
+
content = delta.get("content", "")
|
| 256 |
+
if content:
|
| 257 |
+
content_parts.append(content)
|
| 258 |
+
if choices[0].get("finish_reason"):
|
| 259 |
+
finish_reason = choices[0]["finish_reason"]
|
| 260 |
+
|
| 261 |
+
# Usage sometimes in final chunk
|
| 262 |
+
if "usage" in chunk and chunk["usage"]:
|
| 263 |
+
usage_info = chunk["usage"]
|
| 264 |
+
|
| 265 |
+
elapsed = time.perf_counter() - t0
|
| 266 |
+
ttft_s = ttft if ttft is not None else elapsed
|
| 267 |
+
|
| 268 |
+
return {
|
| 269 |
+
"content": "".join(content_parts),
|
| 270 |
+
"input_tokens": usage_info.get("prompt_tokens", 0),
|
| 271 |
+
"output_tokens": usage_info.get("completion_tokens", len(content_parts)),
|
| 272 |
+
"finish_reason": finish_reason,
|
| 273 |
+
}, ttft_s, inter_times
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# ============================================================
|
| 277 |
+
# Benchmark runner
|
| 278 |
+
# ============================================================
|
| 279 |
+
|
| 280 |
+
def load_prompts(prompts_path: str) -> list[dict]:
|
| 281 |
+
"""Load prompts from JSONL file."""
|
| 282 |
+
prompts = []
|
| 283 |
+
with open(prompts_path, "r", encoding="utf-8") as f:
|
| 284 |
+
for line in f:
|
| 285 |
+
line = line.strip()
|
| 286 |
+
if line:
|
| 287 |
+
prompts.append(json.loads(line))
|
| 288 |
+
return prompts
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def run_benchmark(
|
| 292 |
+
client: OpenAIChatClient,
|
| 293 |
+
prompts: list[dict],
|
| 294 |
+
gpu_monitor: Optional[GpuMonitor],
|
| 295 |
+
config_info: dict,
|
| 296 |
+
repeat: int = 1,
|
| 297 |
+
warmup: bool = True,
|
| 298 |
+
) -> list[BenchResult]:
|
| 299 |
+
"""Run benchmark on all prompts and return results."""
|
| 300 |
+
results: list[BenchResult] = []
|
| 301 |
+
|
| 302 |
+
# Warmup
|
| 303 |
+
if warmup:
|
| 304 |
+
print(" 🔥 Warmup request...")
|
| 305 |
+
try:
|
| 306 |
+
client.chat_completion("Hello, respond with 'OK'.", max_tokens=10, stream=True)
|
| 307 |
+
print(" ✅ Warmup OK")
|
| 308 |
+
except Exception as e:
|
| 309 |
+
print(f" ⚠️ Warmup failed: {e}")
|
| 310 |
+
|
| 311 |
+
total = len(prompts) * repeat
|
| 312 |
+
|
| 313 |
+
for rep in range(repeat):
|
| 314 |
+
for i, p in enumerate(prompts, 1):
|
| 315 |
+
prompt_text = p["prompt"]
|
| 316 |
+
prompt_id = f"{p['id']}_r{rep}" if repeat > 1 else p["id"]
|
| 317 |
+
max_tokens = p.get("max_tokens", 256)
|
| 318 |
+
|
| 319 |
+
result = BenchResult(
|
| 320 |
+
prompt_id=prompt_id,
|
| 321 |
+
category=p.get("category", "unknown"),
|
| 322 |
+
prompt_snippet=prompt_text[:100].replace("\n", " "),
|
| 323 |
+
**config_info,
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
print(f" [{i}/{total}] {prompt_id} ({result.category})...", end=" ", flush=True)
|
| 327 |
+
|
| 328 |
+
if gpu_monitor:
|
| 329 |
+
gpu_monitor.poll()
|
| 330 |
+
|
| 331 |
+
try:
|
| 332 |
+
completion, ttft_s, inter_times = client.chat_completion(
|
| 333 |
+
prompt_text,
|
| 334 |
+
max_tokens=max_tokens,
|
| 335 |
+
temperature=0.0,
|
| 336 |
+
stream=True,
|
| 337 |
+
)
|
| 338 |
+
elapsed = time.perf_counter() # total time will be updated below
|
| 339 |
+
# We need the actual total time from the request
|
| 340 |
+
# Re-run with timing — use the completion struct
|
| 341 |
+
|
| 342 |
+
result.input_tokens = completion.get("input_tokens", 0)
|
| 343 |
+
result.output_tokens = completion.get("output_tokens", 0)
|
| 344 |
+
result.ttft_ms = ttft_s * 1000
|
| 345 |
+
|
| 346 |
+
if inter_times:
|
| 347 |
+
result.tpot_ms = (sum(inter_times) / len(inter_times)) * 1000
|
| 348 |
+
|
| 349 |
+
# For total time, we use ttft + sum of inter_times + small overhead
|
| 350 |
+
total_stream_time = ttft_s + sum(inter_times) if inter_times else ttft_s
|
| 351 |
+
result.total_time_s = total_stream_time
|
| 352 |
+
|
| 353 |
+
if result.total_time_s > 0:
|
| 354 |
+
result.output_tps = result.output_tokens / result.total_time_s
|
| 355 |
+
total_tokens = result.input_tokens + result.output_tokens
|
| 356 |
+
result.total_tps = total_tokens / result.total_time_s
|
| 357 |
+
|
| 358 |
+
if gpu_monitor:
|
| 359 |
+
gpu_monitor.poll()
|
| 360 |
+
gpu_stats = gpu_monitor.stats
|
| 361 |
+
result.vram_peak_mb = gpu_stats["vram_peak_mb"]
|
| 362 |
+
result.gpu_util_pct = gpu_stats["gpu_util_pct"]
|
| 363 |
+
result.power_w = gpu_stats["power_w"]
|
| 364 |
+
result.gpu_temp_c = gpu_stats["gpu_temp_c"]
|
| 365 |
+
|
| 366 |
+
print(f"✅ {result.output_tps:.1f} tok/s (out={result.output_tokens}, TTFT={result.ttft_ms:.0f}ms)")
|
| 367 |
+
|
| 368 |
+
except Exception as e:
|
| 369 |
+
result.success = False
|
| 370 |
+
result.error = str(e)[:500]
|
| 371 |
+
print(f"❌ {str(e)[:100]}")
|
| 372 |
+
|
| 373 |
+
results.append(result)
|
| 374 |
+
|
| 375 |
+
return results
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
# ============================================================
|
| 379 |
+
# CSV persistence
|
| 380 |
+
# ============================================================
|
| 381 |
+
|
| 382 |
+
CSV_COLUMNS = [
|
| 383 |
+
"prompt_id", "category", "model", "backend", "config_name",
|
| 384 |
+
"max_model_len", "gpu_memory_util", "dtype", "quantization",
|
| 385 |
+
"input_tokens", "output_tokens",
|
| 386 |
+
"ttft_ms", "tpot_ms", "total_time_s", "output_tps", "total_tps",
|
| 387 |
+
"success", "error",
|
| 388 |
+
"vram_peak_mb", "gpu_util_pct", "power_w", "gpu_temp_c",
|
| 389 |
+
"prompt_snippet",
|
| 390 |
+
]
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def save_results(results: list[BenchResult], csv_path: str):
|
| 394 |
+
"""Append results to CSV file."""
|
| 395 |
+
file_exists = os.path.exists(csv_path)
|
| 396 |
+
os.makedirs(os.path.dirname(csv_path), exist_ok=True)
|
| 397 |
+
|
| 398 |
+
with open(csv_path, "a", newline="", encoding="utf-8") as f:
|
| 399 |
+
writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS, extrasaction='ignore')
|
| 400 |
+
if not file_exists or os.path.getsize(csv_path) == 0:
|
| 401 |
+
writer.writeheader()
|
| 402 |
+
for r in results:
|
| 403 |
+
writer.writerow(asdict(r))
|
| 404 |
+
|
| 405 |
+
print(f"\n📊 {len(results)} results saved to {csv_path}")
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def print_summary(results: list[BenchResult]):
|
| 409 |
+
"""Print a summary table."""
|
| 410 |
+
successful = [r for r in results if r.success]
|
| 411 |
+
failed = [r for r in results if not r.success]
|
| 412 |
+
|
| 413 |
+
print("\n" + "=" * 60)
|
| 414 |
+
print(" BENCHMARK SUMMARY")
|
| 415 |
+
print("=" * 60)
|
| 416 |
+
print(f" Total requests: {len(results)}")
|
| 417 |
+
print(f" Successful: {len(successful)}")
|
| 418 |
+
print(f" Failed: {len(failed)}")
|
| 419 |
+
print()
|
| 420 |
+
|
| 421 |
+
if successful:
|
| 422 |
+
output_tps_vals = [r.output_tps for r in successful]
|
| 423 |
+
total_tps_vals = [r.total_tps for r in successful]
|
| 424 |
+
ttft_vals = [r.ttft_ms for r in successful]
|
| 425 |
+
tpot_vals = [r.tpot_ms for r in successful if r.tpot_ms > 0]
|
| 426 |
+
|
| 427 |
+
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)")
|
| 428 |
+
print(f" Total TPS: {min(total_tps_vals):.1f} / {sum(total_tps_vals)/len(total_tps_vals):.1f} / {max(total_tps_vals):.1f} (min/avg/max)")
|
| 429 |
+
print(f" TTFT (ms): {min(ttft_vals):.0f} / {sum(ttft_vals)/len(ttft_vals):.0f} / {max(ttft_vals):.0f} (min/avg/max)")
|
| 430 |
+
if tpot_vals:
|
| 431 |
+
print(f" TPOT (ms): {min(tpot_vals):.1f} / {sum(tpot_vals)/len(tpot_vals):.1f} / {max(tpot_vals):.1f} (min/avg/max)")
|
| 432 |
+
|
| 433 |
+
# Per category
|
| 434 |
+
print()
|
| 435 |
+
for cat in sorted(set(r.category for r in successful)):
|
| 436 |
+
cat_results = [r for r in successful if r.category == cat]
|
| 437 |
+
avg_tps = sum(r.output_tps for r in cat_results) / len(cat_results)
|
| 438 |
+
avg_ttft = sum(r.ttft_ms for r in cat_results) / len(cat_results)
|
| 439 |
+
print(f" {cat:10s}: avg {avg_tps:.1f} tok/s, TTFT {avg_ttft:.0f}ms ({len(cat_results)} prompts)")
|
| 440 |
+
|
| 441 |
+
if failed:
|
| 442 |
+
print(f"\n ❌ Failed requests:")
|
| 443 |
+
for r in failed:
|
| 444 |
+
print(f" - {r.prompt_id}: {r.error[:120]}")
|
| 445 |
+
|
| 446 |
+
# GPU stats
|
| 447 |
+
gpu_results = [r for r in successful if r.vram_peak_mb > 0]
|
| 448 |
+
if gpu_results:
|
| 449 |
+
peak_vram = max(r.vram_peak_mb for r in gpu_results)
|
| 450 |
+
avg_util = sum(r.gpu_util_pct for r in gpu_results) / len(gpu_results)
|
| 451 |
+
avg_power = sum(r.power_w for r in gpu_results) / len(gpu_results)
|
| 452 |
+
print(f"\n GPU Stats:")
|
| 453 |
+
print(f" VRAM peak: {peak_vram:.0f} MiB / 24576 MiB")
|
| 454 |
+
print(f" GPU util avg: {avg_util:.1f}%")
|
| 455 |
+
print(f" Power avg: {avg_power:.1f} W")
|
| 456 |
+
|
| 457 |
+
print("=" * 60)
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
# ============================================================
|
| 461 |
+
# CLI
|
| 462 |
+
# ============================================================
|
| 463 |
+
|
| 464 |
+
def main():
|
| 465 |
+
parser = argparse.ArgumentParser(description="Benchmark LLM via OpenAI-compatible API")
|
| 466 |
+
parser.add_argument("--url", default="http://localhost:8000", help="API base URL")
|
| 467 |
+
parser.add_argument("--prompts", default=None, help="Path to JSONL prompts file")
|
| 468 |
+
parser.add_argument("--output", default=None, help="Output CSV path")
|
| 469 |
+
parser.add_argument("--repeat", type=int, default=1, help="Repeat each prompt N times")
|
| 470 |
+
parser.add_argument("--no-warmup", action="store_true", help="Skip warmup request")
|
| 471 |
+
parser.add_argument("--no-gpu", action="store_true", help="Disable GPU monitoring")
|
| 472 |
+
parser.add_argument("--timeout", type=float, default=300.0, help="Request timeout (seconds)")
|
| 473 |
+
parser.add_argument("--model", default="", help="Model name (metadata only)")
|
| 474 |
+
parser.add_argument("--backend", default="vllm", help="Backend name (vllm/sglang, metadata only)")
|
| 475 |
+
parser.add_argument("--config", default="", help="Config name (metadata only)")
|
| 476 |
+
parser.add_argument("--max-model-len", type=int, default=0, help="Max model len (metadata)")
|
| 477 |
+
parser.add_argument("--gpu-mem-util", type=float, default=0.0, help="GPU memory util (metadata)")
|
| 478 |
+
parser.add_argument("--dtype", default="", help="Dtype (metadata)")
|
| 479 |
+
parser.add_argument("--quantization", default="", help="Quantization (metadata)")
|
| 480 |
+
|
| 481 |
+
args = parser.parse_args()
|
| 482 |
+
|
| 483 |
+
# Default paths relative to project
|
| 484 |
+
project_dir = Path(__file__).resolve().parent.parent
|
| 485 |
+
if args.prompts is None:
|
| 486 |
+
args.prompts = str(project_dir / "prompts" / "bench_prompts.jsonl")
|
| 487 |
+
if args.output is None:
|
| 488 |
+
args.output = str(project_dir / "reports" / "results.csv")
|
| 489 |
+
|
| 490 |
+
# Config info for metadata
|
| 491 |
+
config_info = {
|
| 492 |
+
"model": args.model,
|
| 493 |
+
"backend": args.backend,
|
| 494 |
+
"config_name": args.config,
|
| 495 |
+
"max_model_len": args.max_model_len,
|
| 496 |
+
"gpu_memory_util": args.gpu_mem_util,
|
| 497 |
+
"dtype": args.dtype,
|
| 498 |
+
"quantization": args.quantization,
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
print("=" * 60)
|
| 502 |
+
print(" Qwen SpeedLab — OpenAI API Benchmark")
|
| 503 |
+
print("=" * 60)
|
| 504 |
+
print(f" URL: {args.url}")
|
| 505 |
+
print(f" Prompts: {args.prompts}")
|
| 506 |
+
print(f" Output: {args.output}")
|
| 507 |
+
print(f" Repeat: {args.repeat}")
|
| 508 |
+
print(f" Timeout: {args.timeout}s")
|
| 509 |
+
print(f" Config: {config_info}")
|
| 510 |
+
print("=" * 60)
|
| 511 |
+
print()
|
| 512 |
+
|
| 513 |
+
# Load prompts
|
| 514 |
+
if not os.path.exists(args.prompts):
|
| 515 |
+
print(f"❌ Prompts file not found: {args.prompts}")
|
| 516 |
+
sys.exit(1)
|
| 517 |
+
|
| 518 |
+
prompts = load_prompts(args.prompts)
|
| 519 |
+
print(f"📄 Loaded {len(prompts)} prompts")
|
| 520 |
+
|
| 521 |
+
# Create API client
|
| 522 |
+
client = OpenAIChatClient(base_url=args.url, timeout=args.timeout)
|
| 523 |
+
|
| 524 |
+
# Health check
|
| 525 |
+
print("\n🔍 Checking server health...")
|
| 526 |
+
ok, msg = client.check_health()
|
| 527 |
+
if not ok:
|
| 528 |
+
print(f"❌ Server not ready: {msg}")
|
| 529 |
+
print(" Is the server running? Start with: bash scripts/serve_vllm.sh")
|
| 530 |
+
sys.exit(1)
|
| 531 |
+
print(f"✅ Server ready: {msg}")
|
| 532 |
+
|
| 533 |
+
# GPU monitor
|
| 534 |
+
gpu_monitor = None
|
| 535 |
+
if not args.no_gpu:
|
| 536 |
+
gpu_monitor = GpuMonitor(use_pynvml=True)
|
| 537 |
+
gpu_monitor.start()
|
| 538 |
+
print("📊 GPU monitoring enabled (pynvml)")
|
| 539 |
+
|
| 540 |
+
# Run benchmark
|
| 541 |
+
print(f"\n🚀 Running benchmark ({len(prompts) * args.repeat} requests)...\n")
|
| 542 |
+
t_start = time.perf_counter()
|
| 543 |
+
results = run_benchmark(
|
| 544 |
+
client=client,
|
| 545 |
+
prompts=prompts,
|
| 546 |
+
gpu_monitor=gpu_monitor,
|
| 547 |
+
config_info=config_info,
|
| 548 |
+
repeat=args.repeat,
|
| 549 |
+
warmup=not args.no_warmup,
|
| 550 |
+
)
|
| 551 |
+
total_elapsed = time.perf_counter() - t_start
|
| 552 |
+
|
| 553 |
+
if gpu_monitor:
|
| 554 |
+
gpu_monitor.stop()
|
| 555 |
+
|
| 556 |
+
# Save
|
| 557 |
+
save_results(results, args.output)
|
| 558 |
+
|
| 559 |
+
# Summary
|
| 560 |
+
print_summary(results)
|
| 561 |
+
print(f"\n⏱️ Total benchmark time: {total_elapsed:.1f}s")
|
| 562 |
+
|
| 563 |
+
# Close client
|
| 564 |
+
client.client.close()
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
if __name__ == "__main__":
|
| 568 |
+
main()
|
scripts/bench_vllm.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
bench_vllm.py — Benchmark vLLM directly via its Python API (LLM class).
|
| 4 |
+
|
| 5 |
+
This bypasses the HTTP server and measures:
|
| 6 |
+
- Prefill throughput (tokens/s)
|
| 7 |
+
- Decode throughput (tokens/s)
|
| 8 |
+
- End-to-end latency
|
| 9 |
+
- Peak VRAM usage
|
| 10 |
+
|
| 11 |
+
Usage:
|
| 12 |
+
python scripts/bench_vllm.py --model Qwen/Qwen3.6-27B-FP8 --max-model-len 8192
|
| 13 |
+
|
| 14 |
+
Note:
|
| 15 |
+
Requires vLLM installed. This script loads the model in-process,
|
| 16 |
+
so it will consume VRAM immediately. Make sure no other process
|
| 17 |
+
is using the GPU.
|
| 18 |
+
|
| 19 |
+
For HTTP-based benchmarking (against a running server), use
|
| 20 |
+
bench_openai_api.py instead.
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import csv
|
| 26 |
+
import json
|
| 27 |
+
import os
|
| 28 |
+
import sys
|
| 29 |
+
import time
|
| 30 |
+
from dataclasses import dataclass, asdict
|
| 31 |
+
from pathlib import Path
|
| 32 |
+
from typing import Optional
|
| 33 |
+
|
| 34 |
+
# GPU monitoring
|
| 35 |
+
try:
|
| 36 |
+
import pynvml
|
| 37 |
+
HAS_PYNVML = True
|
| 38 |
+
except ImportError:
|
| 39 |
+
HAS_PYNVML = False
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ============================================================
|
| 43 |
+
# Data structures
|
| 44 |
+
# ============================================================
|
| 45 |
+
|
| 46 |
+
@dataclass
|
| 47 |
+
class DirectBenchResult:
|
| 48 |
+
prompt_id: str
|
| 49 |
+
category: str
|
| 50 |
+
model: str
|
| 51 |
+
config_name: str
|
| 52 |
+
max_model_len: int
|
| 53 |
+
gpu_memory_util: float
|
| 54 |
+
dtype: str
|
| 55 |
+
quantization: str
|
| 56 |
+
input_tokens: int
|
| 57 |
+
output_tokens: int
|
| 58 |
+
prefill_time_s: float # Time to process input (first token)
|
| 59 |
+
decode_time_s: float # Time to generate output tokens
|
| 60 |
+
total_time_s: float
|
| 61 |
+
prefill_tps: float # input tokens / prefill_time
|
| 62 |
+
decode_tps: float # output tokens / decode_time
|
| 63 |
+
output_tps: float # output tokens / total_time
|
| 64 |
+
vram_peak_mb: float
|
| 65 |
+
gpu_util_pct: float
|
| 66 |
+
success: bool
|
| 67 |
+
error: str
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# ============================================================
|
| 71 |
+
# GPU Monitor
|
| 72 |
+
# ============================================================
|
| 73 |
+
|
| 74 |
+
class GpuMonitor:
|
| 75 |
+
def __init__(self):
|
| 76 |
+
self.use_pynvml = HAS_PYNVML
|
| 77 |
+
self._peak_vram = 0.0
|
| 78 |
+
self._gpu_utils: list[float] = []
|
| 79 |
+
|
| 80 |
+
if self.use_pynvml:
|
| 81 |
+
pynvml.nvmlInit()
|
| 82 |
+
self._handle = pynvml.nvmlDeviceGetHandleByIndex(0)
|
| 83 |
+
|
| 84 |
+
def poll(self):
|
| 85 |
+
if self.use_pynvml:
|
| 86 |
+
try:
|
| 87 |
+
info = pynvml.nvmlDeviceGetMemoryInfo(self._handle)
|
| 88 |
+
vram_mb = info.used / (1024 * 1024)
|
| 89 |
+
self._peak_vram = max(self._peak_vram, vram_mb)
|
| 90 |
+
util = pynvml.nvmlDeviceGetUtilizationRates(self._handle)
|
| 91 |
+
self._gpu_utils.append(util.gpu)
|
| 92 |
+
except Exception:
|
| 93 |
+
pass
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def stats(self) -> dict:
|
| 97 |
+
return {
|
| 98 |
+
"vram_peak_mb": round(self._peak_vram, 1),
|
| 99 |
+
"gpu_util_pct": round(sum(self._gpu_utils) / len(self._gpu_utils), 1) if self._gpu_utils else 0.0,
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
def close(self):
|
| 103 |
+
if self.use_pynvml:
|
| 104 |
+
try:
|
| 105 |
+
pynvml.nvmlShutdown()
|
| 106 |
+
except Exception:
|
| 107 |
+
pass
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# ============================================================
|
| 111 |
+
# Benchmark runner
|
| 112 |
+
# ============================================================
|
| 113 |
+
|
| 114 |
+
def load_prompts(prompts_path: str) -> list[dict]:
|
| 115 |
+
with open(prompts_path, "r", encoding="utf-8") as f:
|
| 116 |
+
return [json.loads(line) for line in f if line.strip()]
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def run_direct_benchmark(
|
| 120 |
+
llm, # vLLM LLM instance
|
| 121 |
+
tokenizer, # Tokenizer
|
| 122 |
+
prompts: list[dict],
|
| 123 |
+
gpu_monitor: GpuMonitor,
|
| 124 |
+
config_info: dict,
|
| 125 |
+
warmup: bool = True,
|
| 126 |
+
) -> list[DirectBenchResult]:
|
| 127 |
+
"""Run direct vLLM benchmark (no HTTP)."""
|
| 128 |
+
results: list[DirectBenchResult] = []
|
| 129 |
+
|
| 130 |
+
# Warmup
|
| 131 |
+
if warmup:
|
| 132 |
+
print(" 🔥 Warmup...")
|
| 133 |
+
sampling_params = llm.__class__.__module__.split(".")[0] # won't work, use hardcoded import
|
| 134 |
+
try:
|
| 135 |
+
from vllm import SamplingParams
|
| 136 |
+
llm.generate(["Hello"], SamplingParams(max_tokens=10, temperature=0))
|
| 137 |
+
print(" ✅ Warmup OK")
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f" ⚠️ Warmup failed: {e}")
|
| 140 |
+
|
| 141 |
+
from vllm import SamplingParams
|
| 142 |
+
|
| 143 |
+
for i, p in enumerate(prompts, 1):
|
| 144 |
+
prompt_text = p["prompt"]
|
| 145 |
+
prompt_id = p["id"]
|
| 146 |
+
max_tokens = p.get("max_tokens", 256)
|
| 147 |
+
|
| 148 |
+
result = DirectBenchResult(
|
| 149 |
+
prompt_id=prompt_id,
|
| 150 |
+
category=p.get("category", "unknown"),
|
| 151 |
+
**config_info,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
print(f" [{i}/{len(prompts)}] {prompt_id} ({result.category})...", end=" ", flush=True)
|
| 155 |
+
|
| 156 |
+
gpu_monitor.poll()
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
# Tokenize to get input count
|
| 160 |
+
input_ids = tokenizer.encode(prompt_text)
|
| 161 |
+
result.input_tokens = len(input_ids)
|
| 162 |
+
|
| 163 |
+
sampling_params = SamplingParams(
|
| 164 |
+
max_tokens=max_tokens,
|
| 165 |
+
temperature=0.0,
|
| 166 |
+
ignore_eos=False,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
t0 = time.perf_counter()
|
| 170 |
+
outputs = llm.generate([prompt_text], sampling_params, use_tqdm=False)
|
| 171 |
+
elapsed = time.perf_counter() - t0
|
| 172 |
+
|
| 173 |
+
gpu_monitor.poll()
|
| 174 |
+
|
| 175 |
+
if outputs:
|
| 176 |
+
out = outputs[0]
|
| 177 |
+
result.output_tokens = len(out.outputs[0].token_ids)
|
| 178 |
+
|
| 179 |
+
# vLLM provides metrics on the output
|
| 180 |
+
metrics = out.metrics
|
| 181 |
+
if hasattr(metrics, 'first_token_time') and metrics.first_token_time is not None:
|
| 182 |
+
# first_token_time is relative to arrival time
|
| 183 |
+
result.prefill_time_s = metrics.first_token_time - getattr(metrics, 'arrival_time', 0)
|
| 184 |
+
else:
|
| 185 |
+
# Estimate: use a fraction of total time based on input/output ratio
|
| 186 |
+
result.prefill_time_s = elapsed * 0.3 # rough heuristic
|
| 187 |
+
|
| 188 |
+
result.decode_time_s = elapsed - result.prefill_time_s
|
| 189 |
+
result.total_time_s = elapsed
|
| 190 |
+
|
| 191 |
+
if result.prefill_time_s > 0:
|
| 192 |
+
result.prefill_tps = result.input_tokens / result.prefill_time_s
|
| 193 |
+
if result.decode_time_s > 0:
|
| 194 |
+
result.decode_tps = result.output_tokens / result.decode_time_s
|
| 195 |
+
if elapsed > 0:
|
| 196 |
+
result.output_tps = result.output_tokens / elapsed
|
| 197 |
+
|
| 198 |
+
print(f"✅ {result.output_tps:.1f} tok/s (prefill={result.prefill_tps:.0f}, decode={result.decode_tps:.0f})")
|
| 199 |
+
|
| 200 |
+
else:
|
| 201 |
+
result.success = False
|
| 202 |
+
result.error = "No output generated"
|
| 203 |
+
print("❌ No output")
|
| 204 |
+
|
| 205 |
+
except Exception as e:
|
| 206 |
+
result.success = False
|
| 207 |
+
result.error = str(e)[:500]
|
| 208 |
+
print(f"❌ {str(e)[:100]}")
|
| 209 |
+
|
| 210 |
+
results.append(result)
|
| 211 |
+
|
| 212 |
+
return results
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
# ============================================================
|
| 216 |
+
# CSV
|
| 217 |
+
# ============================================================
|
| 218 |
+
|
| 219 |
+
DIRECT_CSV_COLUMNS = [
|
| 220 |
+
"prompt_id", "category", "model", "config_name",
|
| 221 |
+
"max_model_len", "gpu_memory_util", "dtype", "quantization",
|
| 222 |
+
"input_tokens", "output_tokens",
|
| 223 |
+
"prefill_time_s", "decode_time_s", "total_time_s",
|
| 224 |
+
"prefill_tps", "decode_tps", "output_tps",
|
| 225 |
+
"vram_peak_mb", "gpu_util_pct",
|
| 226 |
+
"success", "error",
|
| 227 |
+
]
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def save_results(results: list[DirectBenchResult], csv_path: str):
|
| 231 |
+
file_exists = os.path.exists(csv_path)
|
| 232 |
+
os.makedirs(os.path.dirname(csv_path), exist_ok=True)
|
| 233 |
+
with open(csv_path, "a", newline="", encoding="utf-8") as f:
|
| 234 |
+
writer = csv.DictWriter(f, fieldnames=DIRECT_CSV_COLUMNS, extrasaction='ignore')
|
| 235 |
+
if not file_exists or os.path.getsize(csv_path) == 0:
|
| 236 |
+
writer.writeheader()
|
| 237 |
+
for r in results:
|
| 238 |
+
writer.writerow(asdict(r))
|
| 239 |
+
print(f"\n📊 {len(results)} results saved to {csv_path}")
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def print_summary(results: list[DirectBenchResult]):
|
| 243 |
+
successful = [r for r in results if r.success]
|
| 244 |
+
if not successful:
|
| 245 |
+
print("\n❌ No successful results")
|
| 246 |
+
return
|
| 247 |
+
|
| 248 |
+
decode_tps_vals = [r.decode_tps for r in successful if r.decode_tps > 0]
|
| 249 |
+
output_tps_vals = [r.output_tps for r in successful]
|
| 250 |
+
|
| 251 |
+
print("\n" + "=" * 60)
|
| 252 |
+
print(" DIRECT VLLM BENCHMARK SUMMARY")
|
| 253 |
+
print("=" * 60)
|
| 254 |
+
print(f" Successful: {len(successful)}/{len(results)}")
|
| 255 |
+
if decode_tps_vals:
|
| 256 |
+
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)")
|
| 257 |
+
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)")
|
| 258 |
+
|
| 259 |
+
gpu_results = [r for r in successful if r.vram_peak_mb > 0]
|
| 260 |
+
if gpu_results:
|
| 261 |
+
peak_vram = max(r.vram_peak_mb for r in gpu_results)
|
| 262 |
+
print(f"\n VRAM peak: {peak_vram:.0f} MiB / 24576 MiB")
|
| 263 |
+
print("=" * 60)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
# ============================================================
|
| 267 |
+
# CLI
|
| 268 |
+
# ============================================================
|
| 269 |
+
|
| 270 |
+
def main():
|
| 271 |
+
parser = argparse.ArgumentParser(description="Benchmark vLLM directly (in-process)")
|
| 272 |
+
parser.add_argument("--model", default="Qwen/Qwen3.6-27B-FP8", help="Model name or path")
|
| 273 |
+
parser.add_argument("--max-model-len", type=int, default=8192, help="Max model length")
|
| 274 |
+
parser.add_argument("--gpu-memory-utilization", type=float, default=0.90, help="GPU memory utilization")
|
| 275 |
+
parser.add_argument("--dtype", default="auto", help="Data type (auto, float16, bfloat16)")
|
| 276 |
+
parser.add_argument("--quantization", default="", help="Quantization method")
|
| 277 |
+
parser.add_argument("--prompts", default=None, help="JSONL prompts file")
|
| 278 |
+
parser.add_argument("--output", default=None, help="Output CSV path")
|
| 279 |
+
parser.add_argument("--config", default="direct", help="Config name for logging")
|
| 280 |
+
parser.add_argument("--max-num-seqs", type=int, default=4, help="Max number of sequences")
|
| 281 |
+
parser.add_argument("--trust-remote-code", action="store_true", default=True)
|
| 282 |
+
|
| 283 |
+
args = parser.parse_args()
|
| 284 |
+
|
| 285 |
+
project_dir = Path(__file__).resolve().parent.parent
|
| 286 |
+
if args.prompts is None:
|
| 287 |
+
args.prompts = str(project_dir / "prompts" / "bench_prompts.jsonl")
|
| 288 |
+
if args.output is None:
|
| 289 |
+
args.output = str(project_dir / "reports" / "results_direct.csv")
|
| 290 |
+
|
| 291 |
+
print("=" * 60)
|
| 292 |
+
print(" Qwen SpeedLab — Direct vLLM Benchmark")
|
| 293 |
+
print("=" * 60)
|
| 294 |
+
print(f" Model: {args.model}")
|
| 295 |
+
print(f" Max len: {args.max_model_len}")
|
| 296 |
+
print(f" GPU mem: {args.gpu_memory_utilization}")
|
| 297 |
+
print(f" Dtype: {args.dtype}")
|
| 298 |
+
print(f" Prompts: {args.prompts}")
|
| 299 |
+
print("=" * 60)
|
| 300 |
+
print()
|
| 301 |
+
|
| 302 |
+
# Lazy import vLLM (heavy)
|
| 303 |
+
print("📦 Importing vLLM...")
|
| 304 |
+
try:
|
| 305 |
+
from vllm import LLM, SamplingParams
|
| 306 |
+
except ImportError:
|
| 307 |
+
print("❌ vLLM not installed. Run: pip install vllm")
|
| 308 |
+
sys.exit(1)
|
| 309 |
+
|
| 310 |
+
# Load prompts
|
| 311 |
+
if not os.path.exists(args.prompts):
|
| 312 |
+
print(f"❌ Prompts file not found: {args.prompts}")
|
| 313 |
+
sys.exit(1)
|
| 314 |
+
prompts = load_prompts(args.prompts)
|
| 315 |
+
print(f"📄 Loaded {len(prompts)} prompts")
|
| 316 |
+
|
| 317 |
+
# GPU monitor
|
| 318 |
+
gpu_monitor = GpuMonitor()
|
| 319 |
+
gpu_monitor.poll()
|
| 320 |
+
|
| 321 |
+
# Load model
|
| 322 |
+
print(f"\n🚀 Loading model: {args.model}")
|
| 323 |
+
print(f" max_model_len={args.max_model_len}, gpu_memory_utilization={args.gpu_memory_utilization}")
|
| 324 |
+
t0 = time.perf_counter()
|
| 325 |
+
|
| 326 |
+
try:
|
| 327 |
+
llm = LLM(
|
| 328 |
+
model=args.model,
|
| 329 |
+
max_model_len=args.max_model_len,
|
| 330 |
+
gpu_memory_utilization=args.gpu_memory_utilization,
|
| 331 |
+
dtype=args.dtype,
|
| 332 |
+
quantization=args.quantization if args.quantization else None,
|
| 333 |
+
trust_remote_code=args.trust_remote_code,
|
| 334 |
+
max_num_seqs=args.max_num_seqs,
|
| 335 |
+
tensor_parallel_size=1,
|
| 336 |
+
enable_prefix_caching=True,
|
| 337 |
+
)
|
| 338 |
+
load_time = time.perf_counter() - t0
|
| 339 |
+
print(f"✅ Model loaded in {load_time:.1f}s")
|
| 340 |
+
|
| 341 |
+
# Get tokenizer
|
| 342 |
+
tokenizer = llm.get_tokenizer()
|
| 343 |
+
|
| 344 |
+
gpu_monitor.poll()
|
| 345 |
+
|
| 346 |
+
except Exception as e:
|
| 347 |
+
print(f"❌ Failed to load model: {e}")
|
| 348 |
+
gpu_monitor.close()
|
| 349 |
+
sys.exit(1)
|
| 350 |
+
|
| 351 |
+
# Run benchmark
|
| 352 |
+
config_info = {
|
| 353 |
+
"model": args.model,
|
| 354 |
+
"config_name": args.config,
|
| 355 |
+
"max_model_len": args.max_model_len,
|
| 356 |
+
"gpu_memory_util": args.gpu_memory_utilization,
|
| 357 |
+
"dtype": args.dtype,
|
| 358 |
+
"quantization": args.quantization,
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
print(f"\n🚀 Running benchmark ({len(prompts)} prompts)...\n")
|
| 362 |
+
results = run_direct_benchmark(
|
| 363 |
+
llm=llm,
|
| 364 |
+
tokenizer=tokenizer,
|
| 365 |
+
prompts=prompts,
|
| 366 |
+
gpu_monitor=gpu_monitor,
|
| 367 |
+
config_info=config_info,
|
| 368 |
+
warmup=True,
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
gpu_monitor.close()
|
| 372 |
+
|
| 373 |
+
# Save
|
| 374 |
+
save_results(results, args.output)
|
| 375 |
+
|
| 376 |
+
# Summary
|
| 377 |
+
print_summary(results)
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
if __name__ == "__main__":
|
| 381 |
+
main()
|
scripts/benchmark_now.sh
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# benchmark_now.sh — Run IMMEDIATELY after GPU driver fix
|
| 4 |
+
#
|
| 5 |
+
# This is the definitive benchmark script for Qwen 3.6 27B
|
| 6 |
+
# on RTX 3090 (24 GB). It tests multiple configs and finds
|
| 7 |
+
# the maximum tokens/s without OOM.
|
| 8 |
+
#
|
| 9 |
+
# Prerequisites:
|
| 10 |
+
# 1. GPU CUDA must work (torch.cuda.is_available() == True)
|
| 11 |
+
# 2. HF_TOKEN set if model needs authentication
|
| 12 |
+
# 3. ~50 GB free on /workspace for models
|
| 13 |
+
#
|
| 14 |
+
# Usage: bash scripts/benchmark_now.sh
|
| 15 |
+
# ============================================================
|
| 16 |
+
set -euo pipefail
|
| 17 |
+
|
| 18 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 19 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 20 |
+
cd "$PROJECT_DIR"
|
| 21 |
+
|
| 22 |
+
echo "═══════════════════════════════════════════════════════════"
|
| 23 |
+
echo " Qwen SpeedLab — Definitive RTX 3090 Benchmark"
|
| 24 |
+
echo "═══════════════════════════════════════════════════════════"
|
| 25 |
+
echo ""
|
| 26 |
+
|
| 27 |
+
# --- GPU check ---
|
| 28 |
+
if ! python3 -c "import torch; assert torch.cuda.is_available(), 'CUDA not available'" 2>/dev/null; then
|
| 29 |
+
echo "❌ CUDA not available. Run fix_gpu_host.sh on the HOST first:"
|
| 30 |
+
echo " sudo apt-get remove --purge nvidia-dkms-580-open nvidia-driver-580-open"
|
| 31 |
+
echo " sudo apt-get install nvidia-driver-580"
|
| 32 |
+
echo " sudo reboot"
|
| 33 |
+
exit 1
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
GPU=$(python3 -c "import torch; print(torch.cuda.get_device_name(0))")
|
| 37 |
+
VRAM=$(python3 -c "import torch; print(f'{torch.cuda.get_device_properties(0).total_memory/1024**3:.1f}')")
|
| 38 |
+
echo "✅ GPU: $GPU ($VRAM GB)"
|
| 39 |
+
echo ""
|
| 40 |
+
|
| 41 |
+
# --- Phase 1: Model download ---
|
| 42 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 43 |
+
echo " Phase 1/4: Downloading best model for RTX 3090..."
|
| 44 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 45 |
+
|
| 46 |
+
# Download the best models (AWQ and FP8)
|
| 47 |
+
python3 -c "
|
| 48 |
+
from huggingface_hub import snapshot_download
|
| 49 |
+
import os
|
| 50 |
+
|
| 51 |
+
# AWQ 4-bit — best for Ampere (RTX 3090)
|
| 52 |
+
print('Downloading QuantTrio/Qwen3.6-27B-AWQ (15 GB)...')
|
| 53 |
+
snapshot_download('QuantTrio/Qwen3.6-27B-AWQ',
|
| 54 |
+
cache_dir='/workspace/models',
|
| 55 |
+
token=os.environ.get('HF_TOKEN'))
|
| 56 |
+
print('✅ AWQ model ready')
|
| 57 |
+
|
| 58 |
+
# FP8 — official, higher quality
|
| 59 |
+
print('Downloading Qwen/Qwen3.6-27B-FP8 (27 GB)...')
|
| 60 |
+
snapshot_download('Qwen/Qwen3.6-27B-FP8',
|
| 61 |
+
cache_dir='/workspace/models',
|
| 62 |
+
token=os.environ.get('HF_TOKEN'))
|
| 63 |
+
print('✅ FP8 model ready')
|
| 64 |
+
"
|
| 65 |
+
|
| 66 |
+
echo ""
|
| 67 |
+
|
| 68 |
+
# --- Phase 2: Quick vLLM config sweep ---
|
| 69 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 70 |
+
echo " Phase 2/4: Quick vLLM sweep (6 best configs)..."
|
| 71 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 72 |
+
python3 scripts/sweep_vllm_configs.py --quick --port 8000
|
| 73 |
+
echo ""
|
| 74 |
+
|
| 75 |
+
# --- Phase 3: Advanced optimization A/B test ---
|
| 76 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 77 |
+
echo " Phase 3/4: Advanced optimization sweep..."
|
| 78 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 79 |
+
python3 scripts/sweep_advanced.py --mode baseline --port 8000
|
| 80 |
+
echo ""
|
| 81 |
+
|
| 82 |
+
# --- Phase 4: Final report ---
|
| 83 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 84 |
+
echo " Phase 4/4: Final report"
|
| 85 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 86 |
+
echo ""
|
| 87 |
+
echo "📊 Results files:"
|
| 88 |
+
ls -la reports/results.csv reports/results_advanced.csv reports/summary.md reports/summary_advanced.md 2>/dev/null || echo " (some reports not generated)"
|
| 89 |
+
echo ""
|
| 90 |
+
|
| 91 |
+
# Print best result if available
|
| 92 |
+
if [[ -f reports/summary.md ]]; then
|
| 93 |
+
echo "Best results from sweep:"
|
| 94 |
+
grep -A15 "Best Configuration" reports/summary.md 2>/dev/null | head -20 || true
|
| 95 |
+
fi
|
| 96 |
+
if [[ -f reports/summary_advanced.md ]]; then
|
| 97 |
+
echo ""
|
| 98 |
+
echo "Optimization impact analysis:"
|
| 99 |
+
grep -A5 "Optimization Impact" reports/summary_advanced.md 2>/dev/null | head -10 || true
|
| 100 |
+
fi
|
| 101 |
+
|
| 102 |
+
echo ""
|
| 103 |
+
echo "═════════════════��═════════════════════════════════════════"
|
| 104 |
+
echo " Benchmark complete!"
|
| 105 |
+
echo " Full report: less reports/summary.md"
|
| 106 |
+
echo " Raw data: cat reports/results.csv"
|
| 107 |
+
echo "═══════════════════════════════════════════════════════════"
|
scripts/collect_gpu_stats.sh
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# collect_gpu_stats.sh — Continuous GPU monitoring during benchmark
|
| 4 |
+
#
|
| 5 |
+
# Usage:
|
| 6 |
+
# bash scripts/collect_gpu_stats.sh [output_file] [interval_seconds]
|
| 7 |
+
#
|
| 8 |
+
# Logs nvidia-smi metrics every N seconds to a CSV file.
|
| 9 |
+
# Run in background during benchmark:
|
| 10 |
+
# bash scripts/collect_gpu_stats.sh /tmp/gpu_stats.csv 1 &
|
| 11 |
+
# GPU_PID=$!
|
| 12 |
+
# ... run benchmark ...
|
| 13 |
+
# kill $GPU_PID
|
| 14 |
+
#
|
| 15 |
+
# Default output: reports/gpu_stats_<timestamp>.csv
|
| 16 |
+
# ============================================================
|
| 17 |
+
set -euo pipefail
|
| 18 |
+
|
| 19 |
+
OUTPUT="${1:-}"
|
| 20 |
+
INTERVAL="${2:-1}"
|
| 21 |
+
|
| 22 |
+
# Default output path
|
| 23 |
+
if [[ -z "$OUTPUT" ]]; then
|
| 24 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 25 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 26 |
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
| 27 |
+
OUTPUT="${PROJECT_DIR}/reports/gpu_stats_${TIMESTAMP}.csv"
|
| 28 |
+
fi
|
| 29 |
+
|
| 30 |
+
# Ensure output directory exists
|
| 31 |
+
mkdir -p "$(dirname "$OUTPUT")"
|
| 32 |
+
|
| 33 |
+
echo "============================================"
|
| 34 |
+
echo " GPU Stats Collector"
|
| 35 |
+
echo "============================================"
|
| 36 |
+
echo " Output: $OUTPUT"
|
| 37 |
+
echo " Interval: ${INTERVAL}s"
|
| 38 |
+
echo " PID: $$"
|
| 39 |
+
echo "============================================"
|
| 40 |
+
echo ""
|
| 41 |
+
echo "Recording... Press Ctrl+C to stop."
|
| 42 |
+
echo ""
|
| 43 |
+
|
| 44 |
+
# Write CSV header
|
| 45 |
+
echo "timestamp,elapsed_s,gpu_index,gpu_name,pci_bus,driver_version,"\
|
| 46 |
+
"pstate,temperature_gpu,utilization_gpu,utilization_memory,"\
|
| 47 |
+
"memory_total_mib,memory_used_mib,memory_free_mib,"\
|
| 48 |
+
"power_draw_w,power_limit_w,"\
|
| 49 |
+
"clocks_sm_mhz,clocks_memory_mhz,"\
|
| 50 |
+
"fan_speed_pct,performance_state" > "$OUTPUT"
|
| 51 |
+
|
| 52 |
+
START_TIME=$(date +%s)
|
| 53 |
+
|
| 54 |
+
trap 'echo ""; echo "Stopped. Stats saved to: $OUTPUT"; exit 0' INT TERM
|
| 55 |
+
|
| 56 |
+
while true; do
|
| 57 |
+
NOW=$(date +%s)
|
| 58 |
+
ELAPSED=$((NOW - START_TIME))
|
| 59 |
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
| 60 |
+
|
| 61 |
+
# Query nvidia-smi in CSV format (no header)
|
| 62 |
+
nvidia-smi --query-gpu=index,name,pci.bus_id,driver_version,\
|
| 63 |
+
pstate,temperature.gpu,utilization.gpu,utilization.memory,\
|
| 64 |
+
memory.total,memory.used,memory.free,\
|
| 65 |
+
power.draw,power.limit,\
|
| 66 |
+
clocks.sm,clocks.memory,\
|
| 67 |
+
fan.speed,pstate \
|
| 68 |
+
--format=csv,noheader,nounits 2>/dev/null | while IFS=',' read -r line; do
|
| 69 |
+
echo "${TS},${ELAPSED},${line}" | tr -d ' ' >> "$OUTPUT"
|
| 70 |
+
done
|
| 71 |
+
|
| 72 |
+
sleep "$INTERVAL"
|
| 73 |
+
done
|
scripts/download_best_model.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
download_best_model.py — Auto-detect and download the best Qwen 27B quant for RTX 3090.
|
| 4 |
+
|
| 5 |
+
Checks Hugging Face for available quantized variants, downloads the best one
|
| 6 |
+
that fits in 24GB VRAM, and recommends the optimal vLLM config.
|
| 7 |
+
|
| 8 |
+
Workflow:
|
| 9 |
+
1. Query HF Hub API for Qwen 3.6 27B variants
|
| 10 |
+
2. Filter to those compatible with RTX 3090 (24GB)
|
| 11 |
+
3. Rank by expected quality/speed ratio
|
| 12 |
+
4. Download the best one
|
| 13 |
+
5. Print recommended vLLM flags
|
| 14 |
+
|
| 15 |
+
Usage:
|
| 16 |
+
python scripts/download_best_model.py
|
| 17 |
+
python scripts/download_best_model.py --list # just list, don't download
|
| 18 |
+
python scripts/download_best_model.py --model QuantTrio/Qwen3.6-27B-AWQ # specific
|
| 19 |
+
python scripts/download_best_model.py --dry-run # print what would be done
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import argparse
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import sys
|
| 27 |
+
import time
|
| 28 |
+
from dataclasses import dataclass
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
from typing import Optional
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
import httpx
|
| 34 |
+
except ImportError:
|
| 35 |
+
print("⚠️ httpx not installed. Run: pip install httpx")
|
| 36 |
+
sys.exit(1)
|
| 37 |
+
|
| 38 |
+
# Ordered by preference for RTX 3090 (SM 8.6, 24GB)
|
| 39 |
+
MODEL_CANDIDATES = [
|
| 40 |
+
# Format: (model_id, quant, est_vram_gb, quality_rank, speed_rank, notes)
|
| 41 |
+
("QuantTrio/Qwen3.6-27B-AWQ", "awq", 15, 1, 1, "★ Best for Ampere — INT4 tensor cores"),
|
| 42 |
+
("groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit", "gptq", 15, 2, 2, "GPTQ Marlin, comparable to AWQ"),
|
| 43 |
+
("Qwen/Qwen3.6-27B-FP8", "fp8", 27, 3, 3, "FP8 — tight on 24GB, needs KV cache tuning"),
|
| 44 |
+
("Qwen/Qwen3.6-27B", "none", 54, 4, 4, "Base bf16 — won't fit 24GB without offloading"),
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
# Also check for GGUF variants (community uploads)
|
| 48 |
+
GGUF_CANDIDATES = [
|
| 49 |
+
("unsloth/Qwen3.6-27B-GGUF", "gguf", "Q4_K_M"),
|
| 50 |
+
("lmstudio-community/Qwen3.6-27B-GGUF", "gguf", "Q5_K_M"),
|
| 51 |
+
("bartowski/Qwen3.6-27B-GGUF", "gguf", "IQ4_XS"),
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
HF_API = "https://huggingface.co/api"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclass
|
| 58 |
+
class ModelInfo:
|
| 59 |
+
model_id: str
|
| 60 |
+
quantization: str
|
| 61 |
+
est_vram_gb: float
|
| 62 |
+
exists_on_hf: bool = False
|
| 63 |
+
size_on_disk_gb: float = 0.0
|
| 64 |
+
downloads: int = 0
|
| 65 |
+
last_modified: str = ""
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def check_model_exists(model_id: str) -> Optional[dict]:
|
| 69 |
+
"""Check if a model exists on HF Hub and get metadata."""
|
| 70 |
+
try:
|
| 71 |
+
resp = httpx.get(f"{HF_API}/models/{model_id}", timeout=15)
|
| 72 |
+
if resp.status_code == 200:
|
| 73 |
+
return resp.json()
|
| 74 |
+
return None
|
| 75 |
+
except Exception:
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def check_model_siblings(model_id: str) -> list[dict]:
|
| 80 |
+
"""List all files in a model repo."""
|
| 81 |
+
try:
|
| 82 |
+
resp = httpx.get(f"{HF_API}/models/{model_id}?expand[]=siblings", timeout=15)
|
| 83 |
+
if resp.status_code == 200:
|
| 84 |
+
data = resp.json()
|
| 85 |
+
return data.get("siblings", [])
|
| 86 |
+
return []
|
| 87 |
+
except Exception:
|
| 88 |
+
return []
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def estimate_download_size(siblings: list[dict]) -> float:
|
| 92 |
+
"""Estimate total download size in GB."""
|
| 93 |
+
total = sum(s.get("size", 0) for s in siblings if s.get("size"))
|
| 94 |
+
return total / (1024**3)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def scan_models() -> list[ModelInfo]:
|
| 98 |
+
"""Scan HF Hub for all available Qwen 3.6 27B variants."""
|
| 99 |
+
models: list[ModelInfo] = []
|
| 100 |
+
print("🔍 Scanning Hugging Face for Qwen 3.6 27B variants...")
|
| 101 |
+
print()
|
| 102 |
+
|
| 103 |
+
for model_id, quant, vram, quality, speed, notes in MODEL_CANDIDATES:
|
| 104 |
+
info = ModelInfo(model_id=model_id, quantization=quant, est_vram_gb=vram)
|
| 105 |
+
meta = check_model_exists(model_id)
|
| 106 |
+
|
| 107 |
+
if meta:
|
| 108 |
+
info.exists_on_hf = True
|
| 109 |
+
info.downloads = meta.get("downloads", 0) or 0
|
| 110 |
+
info.last_modified = meta.get("lastModified", "")[:10]
|
| 111 |
+
|
| 112 |
+
siblings = check_model_siblings(model_id)
|
| 113 |
+
info.size_on_disk_gb = estimate_download_size(siblings)
|
| 114 |
+
|
| 115 |
+
status = "✅"
|
| 116 |
+
else:
|
| 117 |
+
status = "❌"
|
| 118 |
+
|
| 119 |
+
print(f" {status} {model_id}")
|
| 120 |
+
print(f" Quant: {quant} | VRAM: ~{vram} GB | Disk: {info.size_on_disk_gb:.1f} GB")
|
| 121 |
+
print(f" {notes}")
|
| 122 |
+
if meta:
|
| 123 |
+
print(f" Downloads: {info.downloads:,} | Updated: {info.last_modified}")
|
| 124 |
+
print()
|
| 125 |
+
|
| 126 |
+
models.append(info)
|
| 127 |
+
|
| 128 |
+
# Also check GGUF variants
|
| 129 |
+
for model_id, quant, gguf_quant in GGUF_CANDIDATES:
|
| 130 |
+
meta = check_model_exists(model_id)
|
| 131 |
+
if meta:
|
| 132 |
+
print(f" ✅ {model_id} ({gguf_quant})")
|
| 133 |
+
print(f" Quant: {quant} | GGUF: {gguf_quant}")
|
| 134 |
+
print(f" Downloads: {meta.get('downloads', 0):,}")
|
| 135 |
+
print()
|
| 136 |
+
|
| 137 |
+
return models
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def get_best_model(models: list[ModelInfo]) -> Optional[ModelInfo]:
|
| 141 |
+
"""Pick the best model that exists and fits in 24GB."""
|
| 142 |
+
available = [m for m in models if m.exists_on_hf]
|
| 143 |
+
if not available:
|
| 144 |
+
return None
|
| 145 |
+
|
| 146 |
+
# Prefer AWQ (best quality/speed on Ampere)
|
| 147 |
+
awq = [m for m in available if m.quantization == "awq"]
|
| 148 |
+
if awq:
|
| 149 |
+
return awq[0]
|
| 150 |
+
|
| 151 |
+
# Then GPTQ
|
| 152 |
+
gptq = [m for m in available if m.quantization == "gptq"]
|
| 153 |
+
if gptq:
|
| 154 |
+
return gptq[0]
|
| 155 |
+
|
| 156 |
+
# Then FP8 (tight but works)
|
| 157 |
+
fp8 = [m for m in available if m.quantization == "fp8"]
|
| 158 |
+
if fp8:
|
| 159 |
+
return fp8[0]
|
| 160 |
+
|
| 161 |
+
return available[0]
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def download_model(model_id: str, cache_dir: Optional[str] = None) -> bool:
|
| 165 |
+
"""Download model using huggingface_hub or snapshot_download."""
|
| 166 |
+
print(f"\n📥 Downloading {model_id}...")
|
| 167 |
+
|
| 168 |
+
# Try huggingface_hub first
|
| 169 |
+
try:
|
| 170 |
+
from huggingface_hub import snapshot_download
|
| 171 |
+
|
| 172 |
+
kwargs = {"repo_id": model_id}
|
| 173 |
+
if cache_dir:
|
| 174 |
+
kwargs["cache_dir"] = cache_dir
|
| 175 |
+
|
| 176 |
+
t0 = time.time()
|
| 177 |
+
path = snapshot_download(**kwargs)
|
| 178 |
+
elapsed = time.time() - t0
|
| 179 |
+
print(f" ✅ Downloaded to: {path}")
|
| 180 |
+
print(f" ⏱️ Time: {elapsed:.0f}s")
|
| 181 |
+
return True
|
| 182 |
+
|
| 183 |
+
except ImportError:
|
| 184 |
+
print(" ⚠️ huggingface_hub not installed, trying transformers...")
|
| 185 |
+
except Exception as e:
|
| 186 |
+
print(f" ⚠️ huggingface_hub failed: {e}")
|
| 187 |
+
|
| 188 |
+
# Try transformers
|
| 189 |
+
try:
|
| 190 |
+
from transformers import AutoTokenizer
|
| 191 |
+
t0 = time.time()
|
| 192 |
+
tok = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
| 193 |
+
# This only downloads tokenizer config. Need full model.
|
| 194 |
+
print(f" Tokenizer downloaded (model will be pulled by vLLM on first use)")
|
| 195 |
+
return True
|
| 196 |
+
except Exception as e:
|
| 197 |
+
print(f" ❌ Failed: {e}")
|
| 198 |
+
return False
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def print_recommended_config(model_id: str):
|
| 202 |
+
"""Print the recommended vLLM flags for the selected model."""
|
| 203 |
+
quant = "auto"
|
| 204 |
+
for m in MODEL_CANDIDATES:
|
| 205 |
+
if m[0] == model_id:
|
| 206 |
+
quant = m[1]
|
| 207 |
+
break
|
| 208 |
+
|
| 209 |
+
print()
|
| 210 |
+
print("=" * 60)
|
| 211 |
+
print(" RECOMMENDED VLLM CONFIGURATION")
|
| 212 |
+
print("=" * 60)
|
| 213 |
+
print()
|
| 214 |
+
print(f" export HF_TOKEN='your_token'")
|
| 215 |
+
print()
|
| 216 |
+
print(f" vllm serve {model_id} \\")
|
| 217 |
+
print(f" --max-model-len 8192 \\")
|
| 218 |
+
print(f" --gpu-memory-utilization 0.90 \\")
|
| 219 |
+
|
| 220 |
+
if quant == "fp8":
|
| 221 |
+
print(f" --max-num-seqs 4 \\")
|
| 222 |
+
print(f" --max-num-batched-tokens 16384 \\")
|
| 223 |
+
print(f" --kv-cache-dtype fp8 \\")
|
| 224 |
+
print(f" --dtype auto \\")
|
| 225 |
+
else:
|
| 226 |
+
print(f" --max-num-seqs 8 \\")
|
| 227 |
+
print(f" --max-num-batched-tokens 32768 \\")
|
| 228 |
+
print(f" --dtype auto \\")
|
| 229 |
+
|
| 230 |
+
print(f" --attention-backend flashinfer \\")
|
| 231 |
+
print(f" --enable-prefix-caching \\")
|
| 232 |
+
print(f" --enable-chunked-prefill \\")
|
| 233 |
+
print(f" --block-size 16 \\")
|
| 234 |
+
print(f" --swap-space 4 \\")
|
| 235 |
+
print(f" --tensor-parallel-size 1 \\")
|
| 236 |
+
print(f" --trust-remote-code")
|
| 237 |
+
print()
|
| 238 |
+
print(" # Or use the optimized launcher:")
|
| 239 |
+
print(f" bash scripts/serve_vllm_optimized.sh awq-8k")
|
| 240 |
+
print("=" * 60)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def main():
|
| 244 |
+
parser = argparse.ArgumentParser(description="Download best Qwen model for RTX 3090")
|
| 245 |
+
parser.add_argument("--list", action="store_true", help="List available models only")
|
| 246 |
+
parser.add_argument("--model", type=str, help="Specific model to download")
|
| 247 |
+
parser.add_argument("--dry-run", action="store_true", help="Print what would be done")
|
| 248 |
+
parser.add_argument("--cache-dir", type=str, help="Custom HF cache directory")
|
| 249 |
+
args = parser.parse_args()
|
| 250 |
+
|
| 251 |
+
# Scan
|
| 252 |
+
models = scan_models()
|
| 253 |
+
available = [m for m in models if m.exists_on_hf]
|
| 254 |
+
|
| 255 |
+
if not available:
|
| 256 |
+
print("❌ No Qwen 3.6 27B models found on Hugging Face.")
|
| 257 |
+
print(" Check: 1) HF_TOKEN for gated models 2) Model name correctness")
|
| 258 |
+
sys.exit(1)
|
| 259 |
+
|
| 260 |
+
# Select
|
| 261 |
+
if args.model:
|
| 262 |
+
target = next((m for m in models if m.model_id == args.model), None)
|
| 263 |
+
if not target:
|
| 264 |
+
print(f"❌ Model {args.model} not found")
|
| 265 |
+
sys.exit(1)
|
| 266 |
+
else:
|
| 267 |
+
target = get_best_model(available)
|
| 268 |
+
if not target:
|
| 269 |
+
print("❌ No suitable model found")
|
| 270 |
+
sys.exit(1)
|
| 271 |
+
|
| 272 |
+
print(f"\n🏆 Best model: {target.model_id}")
|
| 273 |
+
print(f" Quantization: {target.quantization}")
|
| 274 |
+
print(f" Est. VRAM: ~{target.est_vram_gb} GB")
|
| 275 |
+
print(f" Disk size: {target.size_on_disk_gb:.1f} GB")
|
| 276 |
+
|
| 277 |
+
if args.list or args.dry_run:
|
| 278 |
+
print_recommended_config(target.model_id)
|
| 279 |
+
return
|
| 280 |
+
|
| 281 |
+
# Download
|
| 282 |
+
success = download_model(target.model_id, args.cache_dir)
|
| 283 |
+
if success:
|
| 284 |
+
print_recommended_config(target.model_id)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
if __name__ == "__main__":
|
| 288 |
+
main()
|
scripts/fix_gpu_host.sh
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# fix_gpu_host.sh — Run on the HOST (not container) to fix
|
| 4 |
+
# CUDA on RTX 3090 with NVIDIA open kernel module.
|
| 5 |
+
#
|
| 6 |
+
# Problem: The open kernel module (nvidia-open) blocks CUDA
|
| 7 |
+
# on GeForce RTX 3090. nvidia-smi works but cuInit() fails
|
| 8 |
+
# with error 999 (UNKNOWN). This is a known driver issue.
|
| 9 |
+
#
|
| 10 |
+
# Solution: Switch from nvidia-open to the proprietary driver.
|
| 11 |
+
# ============================================================
|
| 12 |
+
set -euo pipefail
|
| 13 |
+
|
| 14 |
+
echo "============================================"
|
| 15 |
+
echo " GPU Fix — RTX 3090 + NVIDIA Driver"
|
| 16 |
+
echo "============================================"
|
| 17 |
+
|
| 18 |
+
# Check current driver
|
| 19 |
+
echo ""
|
| 20 |
+
echo "Current driver:"
|
| 21 |
+
nvidia-smi --query-gpu=name,driver_version --format=csv 2>/dev/null || echo "nvidia-smi not found"
|
| 22 |
+
|
| 23 |
+
# Check kernel module type
|
| 24 |
+
echo ""
|
| 25 |
+
echo "Kernel module type:"
|
| 26 |
+
if lsmod | grep -q nvidia; then
|
| 27 |
+
if modinfo nvidia 2>/dev/null | grep -q "open"; then
|
| 28 |
+
echo " OPEN kernel module detected — THIS IS THE PROBLEM"
|
| 29 |
+
echo " GeForce GPUs need the proprietary module for full CUDA"
|
| 30 |
+
else
|
| 31 |
+
echo " Proprietary kernel module detected"
|
| 32 |
+
fi
|
| 33 |
+
else
|
| 34 |
+
echo " No nvidia module loaded"
|
| 35 |
+
fi
|
| 36 |
+
|
| 37 |
+
echo ""
|
| 38 |
+
echo "=== Fix options ==="
|
| 39 |
+
echo ""
|
| 40 |
+
echo "Option 1: Switch to proprietary driver (recommended)"
|
| 41 |
+
echo " sudo apt-get remove --purge nvidia-dkms-580-open nvidia-driver-580-open"
|
| 42 |
+
echo " sudo apt-get install nvidia-driver-580 nvidia-dkms-580"
|
| 43 |
+
echo " sudo reboot"
|
| 44 |
+
echo ""
|
| 45 |
+
echo "Option 2: For RunPod specifically"
|
| 46 |
+
echo " Stop this pod and restart with template:"
|
| 47 |
+
echo " 'RunPod Pytorch 2.4+' or 'NVIDIA CUDA 12.4'"
|
| 48 |
+
echo " Ensure the template does NOT say 'open kernel'"
|
| 49 |
+
echo ""
|
| 50 |
+
echo "Option 3: Quick fix (try without reboot)"
|
| 51 |
+
echo " sudo rmmod nvidia-uvm nvidia-drm nvidia-modeset nvidia"
|
| 52 |
+
echo " sudo modprobe nvidia"
|
| 53 |
+
echo " sudo modprobe nvidia-uvm"
|
| 54 |
+
echo " sudo nvidia-persistenced --user root"
|
| 55 |
+
echo ""
|
| 56 |
+
|
| 57 |
+
# Auto-detect and apply if running as root on host
|
| 58 |
+
if [[ "$(id -u)" -eq 0 ]] && [[ -f /proc/driver/nvidia/version ]]; then
|
| 59 |
+
KERNEL_TYPE=$(cat /proc/driver/nvidia/version | grep -i "open" && echo "open" || echo "proprietary")
|
| 60 |
+
if [[ "$KERNEL_TYPE" == "open" ]]; then
|
| 61 |
+
echo "⚠️ Open kernel module detected. Would you like to attempt fix? (y/n)"
|
| 62 |
+
read -r answer
|
| 63 |
+
if [[ "$answer" == "y" ]]; then
|
| 64 |
+
echo "Attempting fix..."
|
| 65 |
+
rmmod nvidia-uvm nvidia-drm nvidia-modeset nvidia 2>/dev/null || true
|
| 66 |
+
modprobe nvidia 2>/dev/null || true
|
| 67 |
+
modprobe nvidia-uvm 2>/dev/null || true
|
| 68 |
+
nvidia-persistenced --user root 2>/dev/null || true
|
| 69 |
+
echo "Fix applied. Test with: python3 -c 'import torch; print(torch.cuda.is_available())'"
|
| 70 |
+
fi
|
| 71 |
+
else
|
| 72 |
+
echo "✅ Proprietary driver detected — CUDA should work"
|
| 73 |
+
fi
|
| 74 |
+
fi
|
scripts/inspect_model.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
inspect_model.py — Inspect a Hugging Face model config BEFORE downloading.
|
| 4 |
+
|
| 5 |
+
Queries the HF Hub API to read config.json without downloading weights.
|
| 6 |
+
Reports:
|
| 7 |
+
- Architecture, hidden size, layers, heads
|
| 8 |
+
- Vocabulary size
|
| 9 |
+
- Dtype, rope config
|
| 10 |
+
- Quantization config (bits, group_size, desc_act)
|
| 11 |
+
- Estimated VRAM for different quant levels on RTX 3090
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
python scripts/inspect_model.py QuantTrio/Qwen3.6-27B-AWQ
|
| 15 |
+
python scripts/inspect_model.py Qwen/Qwen3.6-27B-FP8 --compare
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import argparse
|
| 20 |
+
import json
|
| 21 |
+
import sys
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
import httpx
|
| 25 |
+
|
| 26 |
+
HF_API = "https://huggingface.co/api"
|
| 27 |
+
HF_RAW = "https://huggingface.co"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def fetch_json(repo_id: str, filename: str) -> dict | None:
|
| 31 |
+
"""Fetch a JSON file from a HF repo without downloading weights."""
|
| 32 |
+
url = f"{HF_RAW}/{repo_id}/raw/main/{filename}"
|
| 33 |
+
try:
|
| 34 |
+
resp = httpx.get(url, timeout=15, follow_redirects=True)
|
| 35 |
+
if resp.status_code == 200:
|
| 36 |
+
return resp.json()
|
| 37 |
+
# Try resolving the default branch
|
| 38 |
+
model_api = f"{HF_API}/models/{repo_id}"
|
| 39 |
+
resp2 = httpx.get(model_api, timeout=10)
|
| 40 |
+
if resp2.status_code == 200:
|
| 41 |
+
branch = resp2.json().get("defaultBranch", "main")
|
| 42 |
+
url = f"{HF_RAW}/{repo_id}/raw/{branch}/{filename}"
|
| 43 |
+
resp3 = httpx.get(url, timeout=15)
|
| 44 |
+
if resp3.status_code == 200:
|
| 45 |
+
return resp3.json()
|
| 46 |
+
return None
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f" ⚠️ Error fetching {filename}: {e}")
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def estimate_vram(
|
| 53 |
+
hidden_size: int,
|
| 54 |
+
num_layers: int,
|
| 55 |
+
num_heads: int,
|
| 56 |
+
num_kv_heads: int,
|
| 57 |
+
vocab_size: int,
|
| 58 |
+
max_seq_len: int = 8192,
|
| 59 |
+
dtype_bytes: int = 2, # bf16
|
| 60 |
+
kv_dtype_bytes: int = 2, # KV cache dtype
|
| 61 |
+
) -> dict:
|
| 62 |
+
"""Estimate VRAM usage for a model at various quant levels.
|
| 63 |
+
|
| 64 |
+
Uses the standard formula:
|
| 65 |
+
weights = params * bytes_per_param
|
| 66 |
+
kv_cache = 2 * num_layers * num_kv_heads * head_dim * max_seq_len * kv_dtype_bytes
|
| 67 |
+
activation = ~2-4 GB overhead
|
| 68 |
+
"""
|
| 69 |
+
head_dim = hidden_size // num_heads
|
| 70 |
+
kv_head_dim = hidden_size // num_heads # GQA: kv_heads share head_dim
|
| 71 |
+
|
| 72 |
+
# Parameters (approximate)
|
| 73 |
+
# Q, K, V: 3 * hidden_size * hidden_size (but K,V use kv_heads)
|
| 74 |
+
q_size = hidden_size * hidden_size
|
| 75 |
+
k_size = num_kv_heads * kv_head_dim * hidden_size
|
| 76 |
+
v_size = num_kv_heads * kv_head_dim * hidden_size
|
| 77 |
+
# O: hidden_size * hidden_size
|
| 78 |
+
o_size = hidden_size * hidden_size
|
| 79 |
+
# MLP: 2 * hidden_size * intermediate_size + intermediate_size * hidden_size
|
| 80 |
+
# Qwen uses SwiGLU with 3 gates → intermediate ~= hidden_size * 8/3
|
| 81 |
+
intermediate_size = int(hidden_size * 8 / 3)
|
| 82 |
+
mlp_size = 3 * hidden_size * intermediate_size
|
| 83 |
+
# Per-layer total
|
| 84 |
+
per_layer_params = q_size + k_size + v_size + o_size + mlp_size
|
| 85 |
+
# Embedding + LM head
|
| 86 |
+
embedding_params = vocab_size * hidden_size * 2 # tied in most models
|
| 87 |
+
# Layer norm etc (negligible)
|
| 88 |
+
total_params = per_layer_params * num_layers + embedding_params
|
| 89 |
+
|
| 90 |
+
# KV cache per layer
|
| 91 |
+
kv_cache_per_layer = 2 * num_kv_heads * kv_head_dim * max_seq_len * kv_dtype_bytes
|
| 92 |
+
kv_cache_total = kv_cache_per_layer * num_layers
|
| 93 |
+
|
| 94 |
+
# Overhead (activations, workspace, CUDA context)
|
| 95 |
+
overhead = 2 * 1024**3 # 2 GB
|
| 96 |
+
|
| 97 |
+
results = {}
|
| 98 |
+
for name, bpw in [("bf16", 2), ("fp8", 1), ("int4", 0.5), ("int4_g128", 0.55)]:
|
| 99 |
+
weight_bytes = total_params * bpw
|
| 100 |
+
if "fp8" in name:
|
| 101 |
+
kv_bytes = kv_cache_total # same dtype
|
| 102 |
+
else:
|
| 103 |
+
kv_bytes = kv_cache_total
|
| 104 |
+
total_vram = weight_bytes + kv_bytes + overhead
|
| 105 |
+
results[name] = total_vram / (1024**3)
|
| 106 |
+
|
| 107 |
+
return results
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def inspect_model(repo_id: str, max_seq_len: int = 8192):
|
| 111 |
+
"""Fetch and analyze model config."""
|
| 112 |
+
print(f"\n{'='*70}")
|
| 113 |
+
print(f" MODEL INSPECTOR — {repo_id}")
|
| 114 |
+
print(f"{'='*70}\n")
|
| 115 |
+
|
| 116 |
+
config = fetch_json(repo_id, "config.json")
|
| 117 |
+
quant_config = fetch_json(repo_id, "quantize_config.json") or fetch_json(repo_id, "quant_config.json")
|
| 118 |
+
|
| 119 |
+
if not config:
|
| 120 |
+
print(f"❌ Could not fetch config.json for {repo_id}")
|
| 121 |
+
print(f" URL tried: {HF_RAW}/{repo_id}/raw/main/config.json")
|
| 122 |
+
sys.exit(1)
|
| 123 |
+
|
| 124 |
+
# Architecture
|
| 125 |
+
arch = config.get("architectures", ["unknown"])[0]
|
| 126 |
+
model_type = config.get("model_type", "unknown")
|
| 127 |
+
hidden_size = config.get("hidden_size", 0)
|
| 128 |
+
num_layers = config.get("num_hidden_layers", config.get("num_layers", 0))
|
| 129 |
+
num_heads = config.get("num_attention_heads", config.get("num_heads", 0))
|
| 130 |
+
num_kv_heads = config.get("num_key_value_heads", num_heads)
|
| 131 |
+
vocab_size = config.get("vocab_size", 0)
|
| 132 |
+
intermediate_size = config.get("intermediate_size", 0)
|
| 133 |
+
rope_theta = config.get("rope_theta", 0)
|
| 134 |
+
max_position = config.get("max_position_embeddings", 0)
|
| 135 |
+
rope_scaling = config.get("rope_scaling", {})
|
| 136 |
+
torch_dtype = config.get("torch_dtype", "auto")
|
| 137 |
+
|
| 138 |
+
# Quant info
|
| 139 |
+
quant_method = ""
|
| 140 |
+
quant_bits = 0
|
| 141 |
+
quant_group_size = 0
|
| 142 |
+
quant_desc_act = False
|
| 143 |
+
quant_sym = False
|
| 144 |
+
|
| 145 |
+
if quant_config:
|
| 146 |
+
quant_method = quant_config.get("quant_method", quant_config.get("quantization", ""))
|
| 147 |
+
quant_bits = quant_config.get("bits", quant_config.get("w_bit", 0))
|
| 148 |
+
quant_group_size = quant_config.get("group_size", quant_config.get("q_group_size", 0))
|
| 149 |
+
quant_desc_act = quant_config.get("desc_act", False)
|
| 150 |
+
quant_sym = quant_config.get("sym", False)
|
| 151 |
+
|
| 152 |
+
# Print
|
| 153 |
+
print("┌─────────────────────────────────────────────────────┐")
|
| 154 |
+
print(f"│ Architecture │")
|
| 155 |
+
print(f"├─────────────────────────────────────────────────────┤")
|
| 156 |
+
print(f"│ Type: {model_type:<40s} │")
|
| 157 |
+
print(f"│ Hidden: {hidden_size:<40d} │")
|
| 158 |
+
print(f"│ Layers: {num_layers:<40d} │")
|
| 159 |
+
print(f"│ Heads: {num_heads:<40d} │")
|
| 160 |
+
print(f"│ KV Heads: {num_kv_heads:<40d} │")
|
| 161 |
+
print(f"│ Head dim: {hidden_size // num_heads if num_heads else 0:<40d} │")
|
| 162 |
+
print(f"│ Vocab: {vocab_size:<40d} │")
|
| 163 |
+
print(f"│ Intermed: {intermediate_size:<40d} │")
|
| 164 |
+
print(f"│ Torch dtype: {str(torch_dtype):<40s} │")
|
| 165 |
+
print(f"│ Max pos: {max_position:<40d} │")
|
| 166 |
+
print(f"│ Rope theta: {rope_theta:<40.0f} │")
|
| 167 |
+
if rope_scaling:
|
| 168 |
+
print(f"│ Rope scale: {str(rope_scaling):<40s} │")
|
| 169 |
+
print(f"└─────────────────────────────────────────────────────┘")
|
| 170 |
+
|
| 171 |
+
if quant_config:
|
| 172 |
+
print()
|
| 173 |
+
print("┌─────────────────────────────────────────────────────┐")
|
| 174 |
+
print(f"│ Quantization Config │")
|
| 175 |
+
print(f"├─────────────────────────────────────────────────────┤")
|
| 176 |
+
print(f"│ Method: {quant_method:<40s} │")
|
| 177 |
+
print(f"│ Bits: {quant_bits:<40d} │")
|
| 178 |
+
print(f"│ Group size: {quant_group_size:<40d} │")
|
| 179 |
+
print(f"│ Desc act: {str(quant_desc_act):<40s} │")
|
| 180 |
+
print(f"│ Symmetric: {str(quant_sym):<40s} │")
|
| 181 |
+
print(f"└─────────────────────────────────────────────────────┘")
|
| 182 |
+
|
| 183 |
+
# VRAM estimation
|
| 184 |
+
print()
|
| 185 |
+
print("┌─────────────────────────────────────────────────────┐")
|
| 186 |
+
print(f"│ Estimated VRAM (max_seq_len={max_seq_len}) │")
|
| 187 |
+
print(f"├─────────────────────────────────────────────────────┤")
|
| 188 |
+
vram = estimate_vram(hidden_size, num_layers, num_heads, num_kv_heads, vocab_size, max_seq_len)
|
| 189 |
+
for name, gb in vram.items():
|
| 190 |
+
bar = "█" * int(gb / 2) + "░" * (12 - int(gb / 2))
|
| 191 |
+
fits = "✅ FITS" if gb <= 23 else "❌ OOM"
|
| 192 |
+
print(f"│ {name:10s}: {gb:5.1f} GB {bar} {fits:<10s} │")
|
| 193 |
+
print(f"└─────────────────────────────────────────────────────┘")
|
| 194 |
+
|
| 195 |
+
# Summary
|
| 196 |
+
print()
|
| 197 |
+
print("💡 Recommendations:")
|
| 198 |
+
if hidden_size and num_layers:
|
| 199 |
+
total_params_b = hidden_size * hidden_size * 8 * num_layers / 1e9 # rough
|
| 200 |
+
print(f" Estimated params: ~{hidden_size / 1000:.0f}B-class model")
|
| 201 |
+
|
| 202 |
+
if quant_method:
|
| 203 |
+
if quant_method in ("awq", "gptq"):
|
| 204 |
+
print(f" {quant_method.upper()} 4-bit — best for RTX 3090 (Ampere)")
|
| 205 |
+
print(f" Use: --dtype auto (vLLM auto-detects quant)")
|
| 206 |
+
elif quant_method == "fp8":
|
| 207 |
+
print(f" FP8 — tight on 24GB. Use --kv-cache-dtype fp8 + --gpu-memory-utilization 0.85")
|
| 208 |
+
|
| 209 |
+
if num_kv_heads < num_heads and num_heads:
|
| 210 |
+
ratio = num_heads / num_kv_heads
|
| 211 |
+
print(f" GQA ratio: {ratio:.0f}:1 — KV cache is {ratio:.0f}x smaller than MHA")
|
| 212 |
+
|
| 213 |
+
print()
|
| 214 |
+
print(f" Recommended vLLM flags for RTX 3090:")
|
| 215 |
+
print(f" --max-model-len {max_seq_len} --gpu-memory-utilization 0.90 --enable-prefix-caching")
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def compare_models(models: list[str], max_seq_len: int = 8192):
|
| 219 |
+
"""Compare multiple model variants side by side."""
|
| 220 |
+
print(f"\n{'='*70}")
|
| 221 |
+
print(f" MODEL COMPARISON — {len(models)} variants")
|
| 222 |
+
print(f"{'='*70}\n")
|
| 223 |
+
|
| 224 |
+
rows = []
|
| 225 |
+
for repo_id in models:
|
| 226 |
+
config = fetch_json(repo_id, "config.json")
|
| 227 |
+
if not config:
|
| 228 |
+
print(f" ❌ {repo_id}: config.json not available")
|
| 229 |
+
continue
|
| 230 |
+
|
| 231 |
+
quant_config = fetch_json(repo_id, "quantize_config.json") or fetch_json(repo_id, "quant_config.json")
|
| 232 |
+
quant_method = ""
|
| 233 |
+
quant_bits = 0
|
| 234 |
+
if quant_config:
|
| 235 |
+
quant_method = quant_config.get("quant_method", "")
|
| 236 |
+
quant_bits = quant_config.get("bits", 0)
|
| 237 |
+
|
| 238 |
+
hidden_size = config.get("hidden_size", 0)
|
| 239 |
+
num_layers = config.get("num_hidden_layers", 0)
|
| 240 |
+
num_heads = config.get("num_attention_heads", 0)
|
| 241 |
+
num_kv_heads = config.get("num_key_value_heads", num_heads)
|
| 242 |
+
vocab_size = config.get("vocab_size", 0)
|
| 243 |
+
|
| 244 |
+
vram = estimate_vram(hidden_size, num_layers, num_heads, num_kv_heads, vocab_size, max_seq_len)
|
| 245 |
+
|
| 246 |
+
rows.append({
|
| 247 |
+
"model": repo_id.split("/")[-1],
|
| 248 |
+
"quant": quant_method or "none",
|
| 249 |
+
"bits": quant_bits or 16,
|
| 250 |
+
"vram_bf16": vram.get("bf16", 0),
|
| 251 |
+
"vram_fp8": vram.get("fp8", 0),
|
| 252 |
+
"vram_int4": vram.get("int4", 0),
|
| 253 |
+
"fits_24gb": vram.get("fp8", 999) <= 23 or vram.get("int4", 999) <= 23,
|
| 254 |
+
})
|
| 255 |
+
|
| 256 |
+
# Print table
|
| 257 |
+
print(f"│ {'Model':<35s} │ {'Quant':>8s} │ {'Bits':>5s} │ {'VRAM(bf16)':>10s} │ {'VRAM(fp8)':>10s} │ {'VRAM(int4)':>10s} │ {'Fits 24GB':>10s} │")
|
| 258 |
+
print(f"│{'-'*37}│{'-'*10}│{'-'*7}│{'-'*12}│{'-'*12}│{'-'*12}│{'-'*12}│")
|
| 259 |
+
for r in rows:
|
| 260 |
+
fits = "✅" if r["fits_24gb"] else "❌"
|
| 261 |
+
print(f"│ {r['model']:<35s} │ {r['quant']:>8s} │ {r['bits']:>4d} │ {r['vram_bf16']:>8.1f} GB │ {r['vram_fp8']:>8.1f} GB │ {r['vram_int4']:>8.1f} GB │ {fits:>10s} │")
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def main():
|
| 265 |
+
parser = argparse.ArgumentParser(description="Inspect HF model config without downloading")
|
| 266 |
+
parser.add_argument("model", nargs="?", help="Model repo id (e.g. QuantTrio/Qwen3.6-27B-AWQ)")
|
| 267 |
+
parser.add_argument("--compare", nargs="*", help="Compare multiple models")
|
| 268 |
+
parser.add_argument("--max-seq-len", type=int, default=8192, help="Context length for VRAM estimate")
|
| 269 |
+
parser.add_argument("--all-variants", action="store_true", help="Compare all Qwen 27B quant variants")
|
| 270 |
+
args = parser.parse_args()
|
| 271 |
+
|
| 272 |
+
if args.all_variants:
|
| 273 |
+
models = [
|
| 274 |
+
"Qwen/Qwen3.6-27B",
|
| 275 |
+
"Qwen/Qwen3.6-27B-FP8",
|
| 276 |
+
"QuantTrio/Qwen3.6-27B-AWQ",
|
| 277 |
+
"groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit",
|
| 278 |
+
]
|
| 279 |
+
compare_models(models, args.max_seq_len)
|
| 280 |
+
elif args.compare is not None:
|
| 281 |
+
models = args.compare if args.compare else [
|
| 282 |
+
"QuantTrio/Qwen3.6-27B-AWQ",
|
| 283 |
+
"Qwen/Qwen3.6-27B-FP8",
|
| 284 |
+
"groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit",
|
| 285 |
+
]
|
| 286 |
+
compare_models(models, args.max_seq_len)
|
| 287 |
+
elif args.model:
|
| 288 |
+
inspect_model(args.model, args.max_seq_len)
|
| 289 |
+
else:
|
| 290 |
+
# Default: compare all Qwen 27B variants
|
| 291 |
+
models = [
|
| 292 |
+
"Qwen/Qwen3.6-27B",
|
| 293 |
+
"Qwen/Qwen3.6-27B-FP8",
|
| 294 |
+
"QuantTrio/Qwen3.6-27B-AWQ",
|
| 295 |
+
"groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit",
|
| 296 |
+
]
|
| 297 |
+
compare_models(models, args.max_seq_len)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
if __name__ == "__main__":
|
| 301 |
+
main()
|
scripts/run_all.sh
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# run_all.sh — Master orchestrator for Qwen SpeedLab
|
| 4 |
+
#
|
| 5 |
+
# Runs the full pipeline:
|
| 6 |
+
# 1. Setup environment
|
| 7 |
+
# 2. Inspect available models
|
| 8 |
+
# 3. Download best model
|
| 9 |
+
# 4. Quick sweep (7 configs, ~30 min)
|
| 10 |
+
# 5. Advanced A/B sweep (15 configs, ~1h)
|
| 11 |
+
# 6. Generate final report
|
| 12 |
+
#
|
| 13 |
+
# Usage:
|
| 14 |
+
# bash scripts/run_all.sh # Full pipeline
|
| 15 |
+
# bash scripts/run_all.sh --quick # Quick only (skip advanced)
|
| 16 |
+
# bash scripts/run_all.sh --dry-run # Print what would be done
|
| 17 |
+
# ============================================================
|
| 18 |
+
set -euo pipefail
|
| 19 |
+
|
| 20 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 21 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 22 |
+
cd "$PROJECT_DIR"
|
| 23 |
+
|
| 24 |
+
MODE="${1:---full}"
|
| 25 |
+
DRY_RUN=false
|
| 26 |
+
if [[ "${1:-}" == "--dry-run" ]]; then
|
| 27 |
+
DRY_RUN=true
|
| 28 |
+
fi
|
| 29 |
+
|
| 30 |
+
# Colors
|
| 31 |
+
RED='\033[0;31m'
|
| 32 |
+
GREEN='\033[0;32m'
|
| 33 |
+
YELLOW='\033[1;33m'
|
| 34 |
+
BLUE='\033[0;34m'
|
| 35 |
+
BOLD='\033[1m'
|
| 36 |
+
NC='\033[0m'
|
| 37 |
+
|
| 38 |
+
banner() {
|
| 39 |
+
echo ""
|
| 40 |
+
echo -e "${BLUE}╔══════════════════════════════════════════════════════════╗${NC}"
|
| 41 |
+
echo -e "${BLUE}║${NC} ${BOLD}$1${NC}"
|
| 42 |
+
echo -e "${BLUE}╚══════════════════════════════════════════════════════════╝${NC}"
|
| 43 |
+
echo ""
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
step() {
|
| 47 |
+
echo -e "${GREEN}[$1/$TOTAL_STEPS]${NC} ${BOLD}$2${NC}"
|
| 48 |
+
echo ""
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
# Count steps
|
| 52 |
+
TOTAL_STEPS=6
|
| 53 |
+
if [[ "$MODE" == "--quick" ]]; then
|
| 54 |
+
TOTAL_STEPS=4
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
banner "Qwen SpeedLab — Full Pipeline"
|
| 58 |
+
echo " Mode: $MODE"
|
| 59 |
+
echo " GPU: $(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || echo 'unknown')"
|
| 60 |
+
echo ""
|
| 61 |
+
|
| 62 |
+
# ---- Step 1: Setup ----
|
| 63 |
+
step 1 "Environment check"
|
| 64 |
+
if $DRY_RUN; then
|
| 65 |
+
echo " [DRY RUN] Would run: bash scripts/setup_runpod.sh"
|
| 66 |
+
else
|
| 67 |
+
if [[ -d ".venv" ]]; then
|
| 68 |
+
echo " ✅ .venv already exists"
|
| 69 |
+
source .venv/bin/activate
|
| 70 |
+
else
|
| 71 |
+
echo " Running setup_runpod.sh..."
|
| 72 |
+
bash scripts/setup_runpod.sh
|
| 73 |
+
source .venv/bin/activate
|
| 74 |
+
fi
|
| 75 |
+
fi
|
| 76 |
+
|
| 77 |
+
# ---- Step 2: Inspect models ----
|
| 78 |
+
step 2 "Model availability scan"
|
| 79 |
+
if $DRY_RUN; then
|
| 80 |
+
echo " [DRY RUN] Would run: python scripts/inspect_model.py --all-variants"
|
| 81 |
+
else
|
| 82 |
+
python scripts/inspect_model.py --all-variants 2>/dev/null || {
|
| 83 |
+
echo -e " ${YELLOW}⚠️ inspect_model.py failed (httpx not installed?) — continuing${NC}"
|
| 84 |
+
}
|
| 85 |
+
fi
|
| 86 |
+
|
| 87 |
+
# ---- Step 3: Download best model ----
|
| 88 |
+
step 3 "Download best model"
|
| 89 |
+
if $DRY_RUN; then
|
| 90 |
+
echo " [DRY RUN] Would run: python scripts/download_best_model.py --list"
|
| 91 |
+
else
|
| 92 |
+
python scripts/download_best_model.py --list 2>/dev/null || {
|
| 93 |
+
echo -e " ${YELLOW}⚠️ download_best_model.py failed — continuing${NC}"
|
| 94 |
+
}
|
| 95 |
+
echo ""
|
| 96 |
+
echo -e " ${YELLOW}⚠️ Skipping actual download (vLLM will pull on first use).${NC}"
|
| 97 |
+
echo " To download now: python scripts/download_best_model.py"
|
| 98 |
+
fi
|
| 99 |
+
|
| 100 |
+
# ---- Step 4: Quick sweep ----
|
| 101 |
+
step 4 "Quick configuration sweep (7 configs, ~30 min)"
|
| 102 |
+
if $DRY_RUN; then
|
| 103 |
+
echo " [DRY RUN] Would run: python scripts/sweep_vllm_configs.py --quick"
|
| 104 |
+
else
|
| 105 |
+
echo -e " ${YELLOW}⚠️ This takes ~30 min and will use the GPU exclusively.${NC}"
|
| 106 |
+
echo " Press Ctrl+C to skip, or Enter to continue..."
|
| 107 |
+
read -r -t 5 || true
|
| 108 |
+
echo ""
|
| 109 |
+
python scripts/sweep_vllm_configs.py --quick 2>&1 | tail -40 || {
|
| 110 |
+
echo -e " ${RED}❌ Quick sweep failed — check logs${NC}"
|
| 111 |
+
}
|
| 112 |
+
fi
|
| 113 |
+
|
| 114 |
+
# ---- Step 5: Advanced sweep ----
|
| 115 |
+
if [[ "$MODE" != "--quick" ]]; then
|
| 116 |
+
step 5 "Advanced optimization A/B sweep (15 configs, ~1h)"
|
| 117 |
+
if $DRY_RUN; then
|
| 118 |
+
echo " [DRY RUN] Would run: python scripts/sweep_advanced.py --mode baseline"
|
| 119 |
+
else
|
| 120 |
+
echo -e " ${YELLOW}⚠️ This takes ~1h. Press Ctrl+C to skip, or Enter to continue...${NC}"
|
| 121 |
+
read -r -t 5 || true
|
| 122 |
+
echo ""
|
| 123 |
+
python scripts/sweep_advanced.py --mode shootout 2>&1 | tail -40 || {
|
| 124 |
+
echo -e " ${YELLOW}⚠️ Advanced sweep had issues — check reports/summary_advanced.md${NC}"
|
| 125 |
+
}
|
| 126 |
+
fi
|
| 127 |
+
fi
|
| 128 |
+
|
| 129 |
+
# ---- Step 6: Final report ----
|
| 130 |
+
step "$TOTAL_STEPS" "Final report"
|
| 131 |
+
echo " Reports generated:"
|
| 132 |
+
for f in reports/summary.md reports/summary_advanced.md reports/results.csv reports/results_advanced.csv; do
|
| 133 |
+
if [[ -f "$f" ]]; then
|
| 134 |
+
size=$(wc -l < "$f" 2>/dev/null || echo "?")
|
| 135 |
+
echo -e " ${GREEN}✅${NC} $f ($size lines)"
|
| 136 |
+
else
|
| 137 |
+
echo -e " ${YELLOW}—${NC} $f (not generated — run sweeps to populate)"
|
| 138 |
+
fi
|
| 139 |
+
done
|
| 140 |
+
|
| 141 |
+
# Print best result if available
|
| 142 |
+
echo ""
|
| 143 |
+
if [[ -f reports/summary.md ]]; then
|
| 144 |
+
echo "────────────────────────────────────────────────────"
|
| 145 |
+
if grep -A5 "Best Configuration" reports/summary.md 2>/dev/null | head -12; then
|
| 146 |
+
true
|
| 147 |
+
fi
|
| 148 |
+
echo "────────────────────────────────────────────────────"
|
| 149 |
+
fi
|
| 150 |
+
|
| 151 |
+
banner "✅ Pipeline complete!"
|
| 152 |
+
echo ""
|
| 153 |
+
echo " Next steps:"
|
| 154 |
+
echo " - View report: cat reports/summary.md"
|
| 155 |
+
echo " - Advanced report: cat reports/summary_advanced.md"
|
| 156 |
+
echo " - Raw data: cat reports/results.csv"
|
| 157 |
+
echo " - Re-run sweep: python scripts/sweep_advanced.py --mode grid"
|
| 158 |
+
echo " - Start server: bash scripts/serve_vllm.sh awq-8k"
|
| 159 |
+
echo ""
|
scripts/serve_sglang.sh
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# serve_sglang.sh — Lancement SGLang pour Qwen 3.6 27B sur RTX 3090
|
| 4 |
+
# Usage:
|
| 5 |
+
# bash scripts/serve_sglang.sh [config_name]
|
| 6 |
+
#
|
| 7 |
+
# Configs prédéfinies :
|
| 8 |
+
# fp8-8k — FP8, context 8192
|
| 9 |
+
# awq-8k — AWQ, context 8192
|
| 10 |
+
# fp8-16k — FP8, context 16384 (limite)
|
| 11 |
+
# ============================================================
|
| 12 |
+
set -euo pipefail
|
| 13 |
+
|
| 14 |
+
CONFIG="${1:-fp8-8k}"
|
| 15 |
+
|
| 16 |
+
# --- Modèles -----------------------------------------------------------
|
| 17 |
+
MODEL_FP8="Qwen/Qwen3.6-27B-FP8"
|
| 18 |
+
MODEL_AWQ="QuantTrio/Qwen3.6-27B-AWQ"
|
| 19 |
+
MODEL=""
|
| 20 |
+
QUANT=""
|
| 21 |
+
|
| 22 |
+
case "$CONFIG" in
|
| 23 |
+
fp8-8k)
|
| 24 |
+
MODEL="$MODEL_FP8"
|
| 25 |
+
CONTEXT_LEN=8192
|
| 26 |
+
QUANT="fp8"
|
| 27 |
+
MEM_FRAC=0.87
|
| 28 |
+
;;
|
| 29 |
+
awq-8k)
|
| 30 |
+
MODEL="$MODEL_AWQ"
|
| 31 |
+
CONTEXT_LEN=8192
|
| 32 |
+
QUANT="awq"
|
| 33 |
+
MEM_FRAC=0.87
|
| 34 |
+
;;
|
| 35 |
+
fp8-16k)
|
| 36 |
+
MODEL="$MODEL_FP8"
|
| 37 |
+
CONTEXT_LEN=16384
|
| 38 |
+
QUANT="fp8"
|
| 39 |
+
MEM_FRAC=0.87
|
| 40 |
+
;;
|
| 41 |
+
*)
|
| 42 |
+
echo "Config inconnue: $CONFIG"
|
| 43 |
+
echo "Configs disponibles: fp8-8k, awq-8k, fp8-16k"
|
| 44 |
+
exit 1
|
| 45 |
+
;;
|
| 46 |
+
esac
|
| 47 |
+
|
| 48 |
+
PORT="${PORT:-8000}"
|
| 49 |
+
HOST="${HOST:-0.0.0.0}"
|
| 50 |
+
|
| 51 |
+
echo "============================================"
|
| 52 |
+
echo " SGLang Server — Qwen SpeedLab"
|
| 53 |
+
echo "============================================"
|
| 54 |
+
echo " Config: $CONFIG"
|
| 55 |
+
echo " Model: $MODEL"
|
| 56 |
+
echo " Quantization: $QUANT"
|
| 57 |
+
echo " Context len: $CONTEXT_LEN"
|
| 58 |
+
echo " Mem fraction: $MEM_FRAC"
|
| 59 |
+
echo " Port: $PORT"
|
| 60 |
+
echo "============================================"
|
| 61 |
+
|
| 62 |
+
# SGLang utilise --mem-fraction pour limiter la VRAM
|
| 63 |
+
# et --context-length pour la longueur max
|
| 64 |
+
|
| 65 |
+
echo ""
|
| 66 |
+
echo " Lancement SGLang..."
|
| 67 |
+
echo " Logs: /tmp/sglang_${CONFIG}.log"
|
| 68 |
+
|
| 69 |
+
python -m sglang.launch_server \
|
| 70 |
+
--model-path "$MODEL" \
|
| 71 |
+
--host "$HOST" \
|
| 72 |
+
--port "$PORT" \
|
| 73 |
+
--context-length "$CONTEXT_LEN" \
|
| 74 |
+
--mem-fraction-static "$MEM_FRAC" \
|
| 75 |
+
--trust-remote-code \
|
| 76 |
+
--enable-flashinfer \
|
| 77 |
+
2>&1 | tee "/tmp/sglang_${CONFIG}.log"
|
scripts/serve_vllm.sh
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# serve_vllm.sh — Lancement vLLM pour Qwen 3.6 27B sur RTX 3090
|
| 4 |
+
# Usage:
|
| 5 |
+
# bash scripts/serve_vllm.sh [config_name]
|
| 6 |
+
#
|
| 7 |
+
# Configs prédéfinies :
|
| 8 |
+
# ★ awq-8k — AWQ 4-bit, 8192 ctx, flashinfer (recommandé)
|
| 9 |
+
# awq-16k — AWQ 4-bit, 16384 ctx
|
| 10 |
+
# awq-8k-fast — AWQ 4-bit, 8192 ctx, max throughput
|
| 11 |
+
# fp8-8k — FP8, 8192 ctx, flashinfer
|
| 12 |
+
# fp8-16k — FP8, 16384 ctx (limite VRAM)
|
| 13 |
+
# fp8-4k — FP8, 4096 ctx
|
| 14 |
+
# gptq-8k — GPTQ Int4, 8192 ctx
|
| 15 |
+
# fp8-8k-kvc — FP8 + FP8 KV cache (économise ~30% VRAM KV)
|
| 16 |
+
# spec-8k — Speculative decoding (AWQ + 0.6B draft)
|
| 17 |
+
# fp8-8k-hi — FP8, mem=0.95 (risqué)
|
| 18 |
+
# fp8-8k-lo — FP8, mem=0.85
|
| 19 |
+
# ============================================================
|
| 20 |
+
set -euo pipefail
|
| 21 |
+
|
| 22 |
+
CONFIG="${1:-awq-8k}"
|
| 23 |
+
|
| 24 |
+
# --- Modèles ------------------------------------------------------------
|
| 25 |
+
MODEL_FP8="Qwen/Qwen3.6-27B-FP8"
|
| 26 |
+
MODEL_AWQ="QuantTrio/Qwen3.6-27B-AWQ"
|
| 27 |
+
MODEL_GPTQ="groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit"
|
| 28 |
+
MODEL_BASE="Qwen/Qwen3.6-27B"
|
| 29 |
+
MODEL_DRAFT="Qwen/Qwen2.5-0.5B-Instruct"
|
| 30 |
+
|
| 31 |
+
# --- Sélection config --------------------------------------------------
|
| 32 |
+
case "$CONFIG" in
|
| 33 |
+
# AWQ — best quality/speed on Ampere (SM 8.6)
|
| 34 |
+
awq-8k)
|
| 35 |
+
MODEL="$MODEL_AWQ"
|
| 36 |
+
MAX_MODEL_LEN=8192
|
| 37 |
+
GPU_MEM_UTIL=0.90
|
| 38 |
+
DTYPE="auto"
|
| 39 |
+
QUANT="awq"
|
| 40 |
+
MAX_NUM_SEQS=8
|
| 41 |
+
MAX_BATCHED_TOKENS=32768
|
| 42 |
+
ATTENTION="flashinfer"
|
| 43 |
+
KV_CACHE_DTYPE="fp8"
|
| 44 |
+
CHUNKED_PREFILL=true
|
| 45 |
+
BLOCK_SIZE=16
|
| 46 |
+
SWAP_SPACE=4
|
| 47 |
+
MAX_SEQ_LEN_CAPTURE=8192
|
| 48 |
+
SPEC_MODEL=""
|
| 49 |
+
SPEC_TOKENS=5
|
| 50 |
+
;;
|
| 51 |
+
awq-16k)
|
| 52 |
+
MODEL="$MODEL_AWQ"
|
| 53 |
+
MAX_MODEL_LEN=16384
|
| 54 |
+
GPU_MEM_UTIL=0.90
|
| 55 |
+
DTYPE="auto"
|
| 56 |
+
QUANT="awq"
|
| 57 |
+
MAX_NUM_SEQS=4
|
| 58 |
+
MAX_BATCHED_TOKENS=32768
|
| 59 |
+
ATTENTION="flashinfer"
|
| 60 |
+
KV_CACHE_DTYPE="fp8"
|
| 61 |
+
CHUNKED_PREFILL=true
|
| 62 |
+
BLOCK_SIZE=32
|
| 63 |
+
SWAP_SPACE=8
|
| 64 |
+
MAX_SEQ_LEN_CAPTURE=16384
|
| 65 |
+
SPEC_MODEL=""
|
| 66 |
+
SPEC_TOKENS=5
|
| 67 |
+
;;
|
| 68 |
+
awq-8k-fast)
|
| 69 |
+
MODEL="$MODEL_AWQ"
|
| 70 |
+
MAX_MODEL_LEN=8192
|
| 71 |
+
GPU_MEM_UTIL=0.90
|
| 72 |
+
DTYPE="auto"
|
| 73 |
+
QUANT="awq"
|
| 74 |
+
MAX_NUM_SEQS=16
|
| 75 |
+
MAX_BATCHED_TOKENS=65536
|
| 76 |
+
ATTENTION="flashinfer"
|
| 77 |
+
KV_CACHE_DTYPE="fp8"
|
| 78 |
+
CHUNKED_PREFILL=true
|
| 79 |
+
BLOCK_SIZE=16
|
| 80 |
+
SWAP_SPACE=4
|
| 81 |
+
MAX_SEQ_LEN_CAPTURE=8192
|
| 82 |
+
SPEC_MODEL=""
|
| 83 |
+
SPEC_TOKENS=5
|
| 84 |
+
;;
|
| 85 |
+
# FP8 — higher precision, tighter VRAM
|
| 86 |
+
fp8-8k)
|
| 87 |
+
MODEL="$MODEL_FP8"
|
| 88 |
+
MAX_MODEL_LEN=8192
|
| 89 |
+
GPU_MEM_UTIL=0.88
|
| 90 |
+
DTYPE="auto"
|
| 91 |
+
QUANT="fp8"
|
| 92 |
+
MAX_NUM_SEQS=4
|
| 93 |
+
MAX_BATCHED_TOKENS=16384
|
| 94 |
+
ATTENTION="flashinfer"
|
| 95 |
+
KV_CACHE_DTYPE="auto"
|
| 96 |
+
CHUNKED_PREFILL=true
|
| 97 |
+
BLOCK_SIZE=16
|
| 98 |
+
SWAP_SPACE=4
|
| 99 |
+
MAX_SEQ_LEN_CAPTURE=8192
|
| 100 |
+
SPEC_MODEL=""
|
| 101 |
+
SPEC_TOKENS=5
|
| 102 |
+
;;
|
| 103 |
+
fp8-16k)
|
| 104 |
+
MODEL="$MODEL_FP8"
|
| 105 |
+
MAX_MODEL_LEN=16384
|
| 106 |
+
GPU_MEM_UTIL=0.85
|
| 107 |
+
DTYPE="auto"
|
| 108 |
+
QUANT="fp8"
|
| 109 |
+
MAX_NUM_SEQS=2
|
| 110 |
+
MAX_BATCHED_TOKENS=16384
|
| 111 |
+
ATTENTION="flashinfer"
|
| 112 |
+
KV_CACHE_DTYPE="fp8"
|
| 113 |
+
CHUNKED_PREFILL=true
|
| 114 |
+
BLOCK_SIZE=32
|
| 115 |
+
SWAP_SPACE=8
|
| 116 |
+
MAX_SEQ_LEN_CAPTURE=16384
|
| 117 |
+
SPEC_MODEL=""
|
| 118 |
+
SPEC_TOKENS=5
|
| 119 |
+
;;
|
| 120 |
+
fp8-4k)
|
| 121 |
+
MODEL="$MODEL_FP8"
|
| 122 |
+
MAX_MODEL_LEN=4096
|
| 123 |
+
GPU_MEM_UTIL=0.90
|
| 124 |
+
DTYPE="auto"
|
| 125 |
+
QUANT="fp8"
|
| 126 |
+
MAX_NUM_SEQS=8
|
| 127 |
+
MAX_BATCHED_TOKENS=32768
|
| 128 |
+
ATTENTION="flashinfer"
|
| 129 |
+
KV_CACHE_DTYPE="auto"
|
| 130 |
+
CHUNKED_PREFILL=true
|
| 131 |
+
BLOCK_SIZE=16
|
| 132 |
+
SWAP_SPACE=4
|
| 133 |
+
MAX_SEQ_LEN_CAPTURE=4096
|
| 134 |
+
SPEC_MODEL=""
|
| 135 |
+
SPEC_TOKENS=5
|
| 136 |
+
;;
|
| 137 |
+
fp8-8k-kvc)
|
| 138 |
+
MODEL="$MODEL_FP8"
|
| 139 |
+
MAX_MODEL_LEN=8192
|
| 140 |
+
GPU_MEM_UTIL=0.85
|
| 141 |
+
DTYPE="auto"
|
| 142 |
+
QUANT="fp8"
|
| 143 |
+
MAX_NUM_SEQS=6
|
| 144 |
+
MAX_BATCHED_TOKENS=16384
|
| 145 |
+
ATTENTION="flashinfer"
|
| 146 |
+
KV_CACHE_DTYPE="fp8"
|
| 147 |
+
CHUNKED_PREFILL=true
|
| 148 |
+
BLOCK_SIZE=16
|
| 149 |
+
SWAP_SPACE=4
|
| 150 |
+
MAX_SEQ_LEN_CAPTURE=8192
|
| 151 |
+
SPEC_MODEL=""
|
| 152 |
+
SPEC_TOKENS=5
|
| 153 |
+
;;
|
| 154 |
+
# GPTQ
|
| 155 |
+
gptq-8k)
|
| 156 |
+
MODEL="$MODEL_GPTQ"
|
| 157 |
+
MAX_MODEL_LEN=8192
|
| 158 |
+
GPU_MEM_UTIL=0.90
|
| 159 |
+
DTYPE="auto"
|
| 160 |
+
QUANT="gptq"
|
| 161 |
+
MAX_NUM_SEQS=8
|
| 162 |
+
MAX_BATCHED_TOKENS=32768
|
| 163 |
+
ATTENTION="flashinfer"
|
| 164 |
+
KV_CACHE_DTYPE="fp8"
|
| 165 |
+
CHUNKED_PREFILL=true
|
| 166 |
+
BLOCK_SIZE=16
|
| 167 |
+
SWAP_SPACE=4
|
| 168 |
+
MAX_SEQ_LEN_CAPTURE=8192
|
| 169 |
+
SPEC_MODEL=""
|
| 170 |
+
SPEC_TOKENS=5
|
| 171 |
+
;;
|
| 172 |
+
# Speculative decoding
|
| 173 |
+
spec-8k)
|
| 174 |
+
MODEL="$MODEL_AWQ"
|
| 175 |
+
MAX_MODEL_LEN=8192
|
| 176 |
+
GPU_MEM_UTIL=0.88
|
| 177 |
+
DTYPE="auto"
|
| 178 |
+
QUANT="awq"
|
| 179 |
+
MAX_NUM_SEQS=4
|
| 180 |
+
MAX_BATCHED_TOKENS=16384
|
| 181 |
+
ATTENTION="flashinfer"
|
| 182 |
+
KV_CACHE_DTYPE="fp8"
|
| 183 |
+
CHUNKED_PREFILL=false
|
| 184 |
+
BLOCK_SIZE=16
|
| 185 |
+
SWAP_SPACE=4
|
| 186 |
+
MAX_SEQ_LEN_CAPTURE=8192
|
| 187 |
+
SPEC_MODEL="$MODEL_DRAFT"
|
| 188 |
+
SPEC_TOKENS=5
|
| 189 |
+
;;
|
| 190 |
+
# Legacy FP8 configs
|
| 191 |
+
fp8-8k-hi)
|
| 192 |
+
MODEL="$MODEL_FP8"; MAX_MODEL_LEN=8192; GPU_MEM_UTIL=0.95; DTYPE="auto"; QUANT="fp8"
|
| 193 |
+
MAX_NUM_SEQS=4; MAX_BATCHED_TOKENS=16384; ATTENTION="flashinfer"
|
| 194 |
+
KV_CACHE_DTYPE="auto"; CHUNKED_PREFILL=true; BLOCK_SIZE=16; SWAP_SPACE=4
|
| 195 |
+
MAX_SEQ_LEN_CAPTURE=8192; SPEC_MODEL=""; SPEC_TOKENS=5
|
| 196 |
+
;;
|
| 197 |
+
fp8-8k-lo)
|
| 198 |
+
MODEL="$MODEL_FP8"; MAX_MODEL_LEN=8192; GPU_MEM_UTIL=0.85; DTYPE="auto"; QUANT="fp8"
|
| 199 |
+
MAX_NUM_SEQS=4; MAX_BATCHED_TOKENS=16384; ATTENTION="flashinfer"
|
| 200 |
+
KV_CACHE_DTYPE="auto"; CHUNKED_PREFILL=true; BLOCK_SIZE=16; SWAP_SPACE=4
|
| 201 |
+
MAX_SEQ_LEN_CAPTURE=8192; SPEC_MODEL=""; SPEC_TOKENS=5
|
| 202 |
+
;;
|
| 203 |
+
*)
|
| 204 |
+
echo "❌ Config inconnue: $CONFIG"
|
| 205 |
+
echo ""
|
| 206 |
+
echo "Configs disponibles :"
|
| 207 |
+
echo " ★ awq-8k AWQ 4-bit, 8k ctx, flashinfer (recommandé)"
|
| 208 |
+
echo " awq-16k AWQ 4-bit, 16k ctx"
|
| 209 |
+
echo " awq-8k-fast AWQ 4-bit, 8k ctx, max throughput"
|
| 210 |
+
echo " fp8-8k FP8, 8k ctx"
|
| 211 |
+
echo " fp8-16k FP8, 16k ctx (limite)"
|
| 212 |
+
echo " fp8-4k FP8, 4k ctx"
|
| 213 |
+
echo " fp8-8k-kvc FP8 + FP8 KV cache"
|
| 214 |
+
echo " gptq-8k GPTQ Int4, 8k ctx"
|
| 215 |
+
echo " spec-8k AWQ + speculative 0.6B draft"
|
| 216 |
+
exit 1
|
| 217 |
+
;;
|
| 218 |
+
esac
|
| 219 |
+
|
| 220 |
+
PORT="${PORT:-8000}"
|
| 221 |
+
HOST="${HOST:-0.0.0.0}"
|
| 222 |
+
|
| 223 |
+
echo "╔══════════════════════════════════════════════════════════╗"
|
| 224 |
+
echo "║ vLLM Server — Qwen SpeedLab ║"
|
| 225 |
+
echo "║ GPU: RTX 3090 (24 GB, SM 8.6) ║"
|
| 226 |
+
echo "╚══════════════════════════════════════════════════════════╝"
|
| 227 |
+
echo ""
|
| 228 |
+
echo " Config: $CONFIG"
|
| 229 |
+
echo " Model: $MODEL"
|
| 230 |
+
echo " Quantization: $QUANT"
|
| 231 |
+
echo " Max model len: $MAX_MODEL_LEN"
|
| 232 |
+
echo " GPU mem util: $GPU_MEM_UTIL"
|
| 233 |
+
echo " Dtype: $DTYPE"
|
| 234 |
+
echo " Max seqs: $MAX_NUM_SEQS"
|
| 235 |
+
echo " Max batched: $MAX_BATCHED_TOKENS"
|
| 236 |
+
echo " Attention: $ATTENTION"
|
| 237 |
+
echo " KV cache dtype: $KV_CACHE_DTYPE"
|
| 238 |
+
echo " Chunked prefill: $CHUNKED_PREFILL"
|
| 239 |
+
echo " Block size: $BLOCK_SIZE"
|
| 240 |
+
echo " Swap space: ${SWAP_SPACE} GB"
|
| 241 |
+
if [[ -n "$SPEC_MODEL" ]]; then
|
| 242 |
+
echo " Draft model: $SPEC_MODEL ($SPEC_TOKENS speculative tokens)"
|
| 243 |
+
fi
|
| 244 |
+
echo " Port: $PORT"
|
| 245 |
+
echo ""
|
| 246 |
+
|
| 247 |
+
# --- Build vLLM command ---
|
| 248 |
+
CMD=(
|
| 249 |
+
vllm serve "$MODEL"
|
| 250 |
+
--host "$HOST"
|
| 251 |
+
--port "$PORT"
|
| 252 |
+
--max-model-len "$MAX_MODEL_LEN"
|
| 253 |
+
--gpu-memory-utilization "$GPU_MEM_UTIL"
|
| 254 |
+
--dtype "$DTYPE"
|
| 255 |
+
--max-num-seqs "$MAX_NUM_SEQS"
|
| 256 |
+
--max-num-batched-tokens "$MAX_BATCHED_TOKENS"
|
| 257 |
+
--tensor-parallel-size 1
|
| 258 |
+
--trust-remote-code
|
| 259 |
+
--enable-prefix-caching
|
| 260 |
+
--block-size "$BLOCK_SIZE"
|
| 261 |
+
--swap-space "$SWAP_SPACE"
|
| 262 |
+
--max-seq-len-to-capture "$MAX_SEQ_LEN_CAPTURE"
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
# Attention backend
|
| 266 |
+
if [[ -n "$ATTENTION" ]] && [[ "$ATTENTION" != "auto" ]]; then
|
| 267 |
+
CMD+=(--attention-backend "$ATTENTION")
|
| 268 |
+
fi
|
| 269 |
+
|
| 270 |
+
# FP8 KV cache (30% VRAM saving on KV cache)
|
| 271 |
+
if [[ "$KV_CACHE_DTYPE" == "fp8" ]]; then
|
| 272 |
+
CMD+=(--kv-cache-dtype fp8)
|
| 273 |
+
fi
|
| 274 |
+
|
| 275 |
+
# Chunked prefill
|
| 276 |
+
if [[ "$CHUNKED_PREFILL" == "true" ]]; then
|
| 277 |
+
CMD+=(--enable-chunked-prefill)
|
| 278 |
+
fi
|
| 279 |
+
|
| 280 |
+
# Speculative decoding
|
| 281 |
+
if [[ -n "$SPEC_MODEL" ]]; then
|
| 282 |
+
CMD+=(--speculative-model "$SPEC_MODEL")
|
| 283 |
+
CMD+=(--num-speculative-tokens "$SPEC_TOKENS")
|
| 284 |
+
fi
|
| 285 |
+
|
| 286 |
+
echo " Command:"
|
| 287 |
+
echo " ${CMD[*]}"
|
| 288 |
+
echo ""
|
| 289 |
+
echo " Log: /tmp/vllm_${CONFIG}.log"
|
| 290 |
+
echo ""
|
| 291 |
+
|
| 292 |
+
# Launch
|
| 293 |
+
"${CMD[@]}" 2>&1 | tee "/tmp/vllm_${CONFIG}.log"
|
scripts/serve_vllm_optimized.sh
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# serve_vllm_optimized.sh — vLLM with every RTX 3090 optimization
|
| 4 |
+
#
|
| 5 |
+
# This script applies ALL known optimizations for Ampere GPUs:
|
| 6 |
+
# 1. FP8 KV Cache → -30% KV cache VRAM
|
| 7 |
+
# 2. FlashInfer backend → faster prefill on SM 8.6
|
| 8 |
+
# 3. Chunked prefill → better scheduling under load
|
| 9 |
+
# 4. Block size tuning → larger blocks for long context
|
| 10 |
+
# 5. CUDA graph capture → matches max_model_len
|
| 11 |
+
# 6. Swap space → 4GB CPU overflow buffer
|
| 12 |
+
# 7. AWQ/GPTQ Marlin → INT4 tensor core kernels
|
| 13 |
+
# 8. Prefix caching → avoids recomputation
|
| 14 |
+
#
|
| 15 |
+
# Usage:
|
| 16 |
+
# bash scripts/serve_vllm_optimized.sh [config_name]
|
| 17 |
+
#
|
| 18 |
+
# Configs:
|
| 19 |
+
# awq-8k — AWQ 4-bit, 8192 ctx, flashinfer, FP8 KV cache (★ recommended)
|
| 20 |
+
# awq-16k — AWQ 4-bit, 16384 ctx
|
| 21 |
+
# awq-8k-fast — AWQ 4-bit, 8192 ctx, max throughput tuning
|
| 22 |
+
# fp8-8k — FP8 weight, 8192 ctx
|
| 23 |
+
# fp8-8k-kvc — FP8 weight + FP8 KV cache (VRAM saving)
|
| 24 |
+
# gptq-8k — GPTQ Marlin, 8192 ctx
|
| 25 |
+
# spec-8k — speculative decoding with 0.6B draft model
|
| 26 |
+
# ============================================================
|
| 27 |
+
set -euo pipefail
|
| 28 |
+
|
| 29 |
+
CONFIG="${1:-awq-8k}"
|
| 30 |
+
PORT="${PORT:-8000}"
|
| 31 |
+
HOST="${HOST:-0.0.0.0}"
|
| 32 |
+
|
| 33 |
+
# --- Model paths ---
|
| 34 |
+
MODEL_AWQ="QuantTrio/Qwen3.6-27B-AWQ"
|
| 35 |
+
MODEL_FP8="Qwen/Qwen3.6-27B-FP8"
|
| 36 |
+
MODEL_GPTQ="groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit"
|
| 37 |
+
MODEL_DRAFT="Qwen/Qwen2.5-0.5B-Instruct" # for speculative decoding
|
| 38 |
+
|
| 39 |
+
# --- Config matrix ---
|
| 40 |
+
case "$CONFIG" in
|
| 41 |
+
# ============================================================
|
| 42 |
+
# AWQ — best performance/quality on Ampere (★ recommended)
|
| 43 |
+
# ============================================================
|
| 44 |
+
awq-8k)
|
| 45 |
+
MODEL="$MODEL_AWQ"
|
| 46 |
+
MAX_MODEL_LEN=8192
|
| 47 |
+
GPU_MEM=0.90
|
| 48 |
+
MAX_SEQS=8
|
| 49 |
+
MAX_BATCHED=32768
|
| 50 |
+
DTYPE="auto"
|
| 51 |
+
ATTENTION="flashinfer"
|
| 52 |
+
KV_CACHE_DTYPE="fp8"
|
| 53 |
+
CHUNKED_PREFILL=true
|
| 54 |
+
BLOCK_SIZE=16
|
| 55 |
+
SWAP_SPACE=4
|
| 56 |
+
SPEC_DRAFT=""
|
| 57 |
+
;;
|
| 58 |
+
awq-8k-fast)
|
| 59 |
+
MODEL="$MODEL_AWQ"
|
| 60 |
+
MAX_MODEL_LEN=8192
|
| 61 |
+
GPU_MEM=0.90
|
| 62 |
+
MAX_SEQS=16
|
| 63 |
+
MAX_BATCHED=65536
|
| 64 |
+
DTYPE="auto"
|
| 65 |
+
ATTENTION="flashinfer"
|
| 66 |
+
KV_CACHE_DTYPE="fp8"
|
| 67 |
+
CHUNKED_PREFILL=true
|
| 68 |
+
BLOCK_SIZE=32
|
| 69 |
+
SWAP_SPACE=8
|
| 70 |
+
SPEC_DRAFT=""
|
| 71 |
+
;;
|
| 72 |
+
awq-16k)
|
| 73 |
+
MODEL="$MODEL_AWQ"
|
| 74 |
+
MAX_MODEL_LEN=16384
|
| 75 |
+
GPU_MEM=0.90
|
| 76 |
+
MAX_SEQS=4
|
| 77 |
+
MAX_BATCHED=32768
|
| 78 |
+
DTYPE="auto"
|
| 79 |
+
ATTENTION="flashinfer"
|
| 80 |
+
KV_CACHE_DTYPE="fp8"
|
| 81 |
+
CHUNKED_PREFILL=true
|
| 82 |
+
BLOCK_SIZE=32
|
| 83 |
+
SWAP_SPACE=8
|
| 84 |
+
SPEC_DRAFT=""
|
| 85 |
+
;;
|
| 86 |
+
# ============================================================
|
| 87 |
+
# FP8 — lighter weight quant, tight on 24GB
|
| 88 |
+
# ============================================================
|
| 89 |
+
fp8-8k)
|
| 90 |
+
MODEL="$MODEL_FP8"
|
| 91 |
+
MAX_MODEL_LEN=8192
|
| 92 |
+
GPU_MEM=0.88
|
| 93 |
+
MAX_SEQS=4
|
| 94 |
+
MAX_BATCHED=16384
|
| 95 |
+
DTYPE="auto"
|
| 96 |
+
ATTENTION="flashinfer"
|
| 97 |
+
KV_CACHE_DTYPE="auto"
|
| 98 |
+
CHUNKED_PREFILL=true
|
| 99 |
+
BLOCK_SIZE=16
|
| 100 |
+
SWAP_SPACE=4
|
| 101 |
+
SPEC_DRAFT=""
|
| 102 |
+
;;
|
| 103 |
+
fp8-8k-kvc)
|
| 104 |
+
# FP8 weights + FP8 KV cache = tight fit but works
|
| 105 |
+
MODEL="$MODEL_FP8"
|
| 106 |
+
MAX_MODEL_LEN=8192
|
| 107 |
+
GPU_MEM=0.85
|
| 108 |
+
MAX_SEQS=6
|
| 109 |
+
MAX_BATCHED=16384
|
| 110 |
+
DTYPE="auto"
|
| 111 |
+
ATTENTION="flashinfer"
|
| 112 |
+
KV_CACHE_DTYPE="fp8"
|
| 113 |
+
CHUNKED_PREFILL=true
|
| 114 |
+
BLOCK_SIZE=16
|
| 115 |
+
SWAP_SPACE=4
|
| 116 |
+
SPEC_DRAFT=""
|
| 117 |
+
;;
|
| 118 |
+
# ============================================================
|
| 119 |
+
# GPTQ — INT4 via Marlin kernels (fast on Ampere)
|
| 120 |
+
# ============================================================
|
| 121 |
+
gptq-8k)
|
| 122 |
+
MODEL="$MODEL_GPTQ"
|
| 123 |
+
MAX_MODEL_LEN=8192
|
| 124 |
+
GPU_MEM=0.90
|
| 125 |
+
MAX_SEQS=8
|
| 126 |
+
MAX_BATCHED=32768
|
| 127 |
+
DTYPE="auto"
|
| 128 |
+
ATTENTION="flashinfer"
|
| 129 |
+
KV_CACHE_DTYPE="fp8"
|
| 130 |
+
CHUNKED_PREFILL=true
|
| 131 |
+
BLOCK_SIZE=16
|
| 132 |
+
SWAP_SPACE=4
|
| 133 |
+
SPEC_DRAFT=""
|
| 134 |
+
;;
|
| 135 |
+
# ============================================================
|
| 136 |
+
# Speculative decoding — Qwen 27B target + 0.6B draft
|
| 137 |
+
# Spec can 1.5x-2x throughput on batch workloads
|
| 138 |
+
# ============================================================
|
| 139 |
+
spec-8k)
|
| 140 |
+
MODEL="$MODEL_AWQ"
|
| 141 |
+
MAX_MODEL_LEN=8192
|
| 142 |
+
GPU_MEM=0.88
|
| 143 |
+
MAX_SEQS=4
|
| 144 |
+
MAX_BATCHED=16384
|
| 145 |
+
DTYPE="auto"
|
| 146 |
+
ATTENTION="flashinfer"
|
| 147 |
+
KV_CACHE_DTYPE="fp8"
|
| 148 |
+
CHUNKED_PREFILL=false
|
| 149 |
+
BLOCK_SIZE=16
|
| 150 |
+
SWAP_SPACE=4
|
| 151 |
+
SPEC_DRAFT="$MODEL_DRAFT"
|
| 152 |
+
;;
|
| 153 |
+
*)
|
| 154 |
+
echo "❌ Unknown config: $CONFIG"
|
| 155 |
+
echo ""
|
| 156 |
+
echo "Available optimized configs:"
|
| 157 |
+
echo " awq-8k ★ AWQ 4-bit, 8k ctx, all optimizations (recommended)"
|
| 158 |
+
echo " awq-8k-fast AWQ 4-bit, 8k ctx, max throughput"
|
| 159 |
+
echo " awq-16k AWQ 4-bit, 16k ctx"
|
| 160 |
+
echo " fp8-8k FP8 weight, 8k ctx"
|
| 161 |
+
echo " fp8-8k-kvc FP8 weight + FP8 KV cache"
|
| 162 |
+
echo " gptq-8k GPTQ Marlin, 8k ctx"
|
| 163 |
+
echo " spec-8k Speculative decoding (AWQ + 0.6B draft)"
|
| 164 |
+
exit 1
|
| 165 |
+
;;
|
| 166 |
+
esac
|
| 167 |
+
|
| 168 |
+
echo "╔══════════════════════════════════════════════════════════╗"
|
| 169 |
+
echo "║ vLLM OPTIMIZED — Qwen SpeedLab ║"
|
| 170 |
+
echo "║ GPU: RTX 3090 (24 GB, SM 8.6) ║"
|
| 171 |
+
echo "╚══════════════════════════════════════════════════════════╝"
|
| 172 |
+
echo ""
|
| 173 |
+
echo " Config: $CONFIG"
|
| 174 |
+
echo " Model: $MODEL"
|
| 175 |
+
echo " Max model len: $MAX_MODEL_LEN"
|
| 176 |
+
echo " GPU mem util: $GPU_MEM"
|
| 177 |
+
echo " Max seqs: $MAX_SEQS"
|
| 178 |
+
echo " Max batched: $MAX_BATCHED"
|
| 179 |
+
echo " Dtype: $DTYPE"
|
| 180 |
+
echo " Attention: $ATTENTION"
|
| 181 |
+
echo " KV cache dtype: $KV_CACHE_DTYPE"
|
| 182 |
+
echo " Chunked prefill: $CHUNKED_PREFILL"
|
| 183 |
+
echo " Block size: $BLOCK_SIZE"
|
| 184 |
+
echo " Swap space: ${SWAP_SPACE}GB"
|
| 185 |
+
if [[ -n "$SPEC_DRAFT" ]]; then
|
| 186 |
+
echo " Draft model: $SPEC_DRAFT"
|
| 187 |
+
fi
|
| 188 |
+
echo ""
|
| 189 |
+
|
| 190 |
+
# --- Build vllm command ---
|
| 191 |
+
CMD=(
|
| 192 |
+
vllm serve "$MODEL"
|
| 193 |
+
--host "$HOST"
|
| 194 |
+
--port "$PORT"
|
| 195 |
+
--max-model-len "$MAX_MODEL_LEN"
|
| 196 |
+
--gpu-memory-utilization "$GPU_MEM"
|
| 197 |
+
--dtype "$DTYPE"
|
| 198 |
+
--max-num-seqs "$MAX_SEQS"
|
| 199 |
+
--max-num-batched-tokens "$MAX_BATCHED"
|
| 200 |
+
--tensor-parallel-size 1
|
| 201 |
+
--trust-remote-code
|
| 202 |
+
--enable-prefix-caching
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
# Attention backend
|
| 206 |
+
if [[ "$ATTENTION" == "flashinfer" ]]; then
|
| 207 |
+
CMD+=(--attention-backend flashinfer)
|
| 208 |
+
fi
|
| 209 |
+
|
| 210 |
+
# FP8 KV cache (biggest VRAM saving on Ampere)
|
| 211 |
+
if [[ "$KV_CACHE_DTYPE" == "fp8" ]]; then
|
| 212 |
+
CMD+=(--kv-cache-dtype fp8)
|
| 213 |
+
fi
|
| 214 |
+
|
| 215 |
+
# Chunked prefill
|
| 216 |
+
if [[ "$CHUNKED_PREFILL" == "true" ]]; then
|
| 217 |
+
CMD+=(--enable-chunked-prefill)
|
| 218 |
+
fi
|
| 219 |
+
|
| 220 |
+
# Block size
|
| 221 |
+
CMD+=(--block-size "$BLOCK_SIZE")
|
| 222 |
+
|
| 223 |
+
# Swap space (CPU offload safety net)
|
| 224 |
+
CMD+=(--swap-space "$SWAP_SPACE")
|
| 225 |
+
|
| 226 |
+
# CUDA graph: capture up to max_model_len (set equal to avoid recompilation)
|
| 227 |
+
CMD+=(--max-seq-len-to-capture "$MAX_MODEL_LEN")
|
| 228 |
+
|
| 229 |
+
# Speculative decoding
|
| 230 |
+
if [[ -n "$SPEC_DRAFT" ]]; then
|
| 231 |
+
CMD+=(--speculative-model "$SPEC_DRAFT")
|
| 232 |
+
# num_speculative_tokens: 5 is a good default for Qwen
|
| 233 |
+
CMD+=(--num-speculative-tokens 5)
|
| 234 |
+
fi
|
| 235 |
+
|
| 236 |
+
echo " Command:"
|
| 237 |
+
echo " ${CMD[*]}"
|
| 238 |
+
echo ""
|
| 239 |
+
echo " Log: /tmp/vllm_optimized_${CONFIG}.log"
|
| 240 |
+
echo ""
|
| 241 |
+
|
| 242 |
+
# Launch
|
| 243 |
+
"${CMD[@]}" 2>&1 | tee "/tmp/vllm_optimized_${CONFIG}.log"
|
scripts/setup_runpod.sh
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# setup_runpod.sh — Installation environment Qwen SpeedLab
|
| 4 |
+
# Pour RTX 3090 / RunPod avec CUDA 12.x
|
| 5 |
+
# ============================================================
|
| 6 |
+
set -euo pipefail
|
| 7 |
+
|
| 8 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 9 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 10 |
+
VENV_DIR="${PROJECT_DIR}/.venv"
|
| 11 |
+
|
| 12 |
+
echo "============================================"
|
| 13 |
+
echo " Qwen SpeedLab — Setup RunPod RTX 3090"
|
| 14 |
+
echo "============================================"
|
| 15 |
+
|
| 16 |
+
# --- 1. Vérifier GPU --------------------------------------------------
|
| 17 |
+
echo ""
|
| 18 |
+
echo "[1/8] Vérification GPU..."
|
| 19 |
+
if command -v nvidia-smi &>/dev/null; then
|
| 20 |
+
nvidia-smi --query-gpu=name,memory.total,driver_version,cuda.version --format=csv,noheader
|
| 21 |
+
GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader | head -1)
|
| 22 |
+
VRAM_MB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader | head -1 | sed 's/ MiB//')
|
| 23 |
+
echo " GPU : $GPU_NAME"
|
| 24 |
+
echo " VRAM: ${VRAM_MB} MiB"
|
| 25 |
+
|
| 26 |
+
if [[ "$VRAM_MB" -lt 23000 ]]; then
|
| 27 |
+
echo " ⚠️ VRAM < 23 Go — les modèles 27B risquent de ne pas passer"
|
| 28 |
+
fi
|
| 29 |
+
else
|
| 30 |
+
echo " ❌ nvidia-smi introuvable — pas de GPU NVIDIA détecté"
|
| 31 |
+
exit 1
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
# --- 2. Vérifier CUDA / drivers ---------------------------------------
|
| 35 |
+
echo ""
|
| 36 |
+
echo "[2/8] Vérification CUDA..."
|
| 37 |
+
if command -v nvcc &>/dev/null; then
|
| 38 |
+
nvcc --version | grep "release" || true
|
| 39 |
+
else
|
| 40 |
+
echo " nvcc non trouvé (normal sur RunPod, le driver suffit)"
|
| 41 |
+
fi
|
| 42 |
+
|
| 43 |
+
# Vérifier que les libs CUDA sont accessibles
|
| 44 |
+
CUDA_HOME="${CUDA_HOME:-/usr/local/cuda}"
|
| 45 |
+
if [[ -d "$CUDA_HOME" ]]; then
|
| 46 |
+
echo " CUDA_HOME: $CUDA_HOME"
|
| 47 |
+
ls "$CUDA_HOME/lib64/libcudart.so"* 2>/dev/null || echo " ⚠️ libcudart non trouvée"
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
# --- 3. Créer venv Python ----------------------------------------------
|
| 51 |
+
echo ""
|
| 52 |
+
echo "[3/8] Création environnement virtuel Python..."
|
| 53 |
+
python3 -m venv "$VENV_DIR" --upgrade-deps
|
| 54 |
+
source "${VENV_DIR}/bin/activate"
|
| 55 |
+
echo " Python: $(python3 --version)"
|
| 56 |
+
echo " pip: $(pip --version)"
|
| 57 |
+
|
| 58 |
+
# --- 4. Installer PyTorch compatible CUDA ------------------------------
|
| 59 |
+
echo ""
|
| 60 |
+
echo "[4/8] Installation PyTorch + CUDA..."
|
| 61 |
+
# Détecter la version CUDA du driver
|
| 62 |
+
CUDA_VER=$(nvidia-smi --query-gpu=cuda.version --format=csv,noheader | head -1 | cut -d. -f1)
|
| 63 |
+
echo " Driver CUDA major: $CUDA_VER"
|
| 64 |
+
|
| 65 |
+
# PyTorch avec CUDA 12.4 (stable avec vLLM)
|
| 66 |
+
pip install --upgrade pip setuptools wheel
|
| 67 |
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
|
| 68 |
+
|
| 69 |
+
# Vérifier que torch voit CUDA
|
| 70 |
+
python3 -c "
|
| 71 |
+
import torch
|
| 72 |
+
print(f' PyTorch: {torch.__version__}')
|
| 73 |
+
print(f' CUDA available: {torch.cuda.is_available()}')
|
| 74 |
+
if torch.cuda.is_available():
|
| 75 |
+
print(f' CUDA version: {torch.version.cuda}')
|
| 76 |
+
print(f' Device count: {torch.cuda.device_count()}')
|
| 77 |
+
print(f' Device name: {torch.cuda.get_device_name(0)}')
|
| 78 |
+
print(f' VRAM total: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} Go')
|
| 79 |
+
else:
|
| 80 |
+
print(' ⚠️ CUDA non disponible — vérifier driver/compatibilité')
|
| 81 |
+
"
|
| 82 |
+
|
| 83 |
+
# --- 5. Installer vLLM -------------------------------------------------
|
| 84 |
+
echo ""
|
| 85 |
+
echo "[5/8] Installation vLLM..."
|
| 86 |
+
# vLLM avec CUDA 12.4
|
| 87 |
+
pip install vllm --no-build-isolation 2>&1 | tail -5 || {
|
| 88 |
+
echo " ⚠️ Échec vLLM from source, tentative via wheel..."
|
| 89 |
+
pip install vllm 2>&1 | tail -5 || {
|
| 90 |
+
echo " ❌ Échec installation vLLM"
|
| 91 |
+
echo " → Essayez: pip install vllm --find-links https://vllm-wheels.s3.us-west-2.amazonaws.com/nightly.html"
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# Vérifier
|
| 96 |
+
python3 -c "import vllm; print(f' vLLM: {vllm.__version__}')" 2>/dev/null || echo " ⚠️ vLLM non importable"
|
| 97 |
+
|
| 98 |
+
# --- 6. Installer SGLang -----------------------------------------------
|
| 99 |
+
echo ""
|
| 100 |
+
echo "[6/8] Installation SGLang..."
|
| 101 |
+
pip install "sglang[all]" 2>&1 | tail -5 || {
|
| 102 |
+
echo " ⚠️ Échec SGLang — non bloquant"
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
python3 -c "import sglang; print(f' SGLang: {sglang.__version__}')" 2>/dev/null || echo " ⚠️ SGLang non importable"
|
| 106 |
+
|
| 107 |
+
# --- 7. Installer dépendances benchmark --------------------------------
|
| 108 |
+
echo ""
|
| 109 |
+
echo "[7/8] Installation dépendances benchmark..."
|
| 110 |
+
pip install transformers accelerate datasets pandas tqdm psutil openai httpx aiohttp nvidia-ml-py pyyaml rich
|
| 111 |
+
|
| 112 |
+
# --- 8. Vérification finale --------------------------------------------
|
| 113 |
+
echo ""
|
| 114 |
+
echo "[8/8] Vérification finale..."
|
| 115 |
+
echo ""
|
| 116 |
+
echo " Packages installés :"
|
| 117 |
+
pip list 2>/dev/null | grep -iE "torch|vllm|sglang|transformers|openai" || true
|
| 118 |
+
echo ""
|
| 119 |
+
|
| 120 |
+
# Vérifier HF_TOKEN
|
| 121 |
+
if [[ -z "${HF_TOKEN:-}" ]]; then
|
| 122 |
+
echo " ⚠️ HF_TOKEN non défini."
|
| 123 |
+
echo " → export HF_TOKEN='hf_votre_token'"
|
| 124 |
+
echo " → ou lancez : huggingface-cli login"
|
| 125 |
+
else
|
| 126 |
+
echo " ✅ HF_TOKEN détecté"
|
| 127 |
+
fi
|
| 128 |
+
|
| 129 |
+
# Vérifier espace disque
|
| 130 |
+
DISK_AVAIL=$(df -h /workspace | tail -1 | awk '{print $4}')
|
| 131 |
+
echo " Espace disque disponible: $DISK_AVAIL"
|
| 132 |
+
|
| 133 |
+
echo ""
|
| 134 |
+
echo "============================================"
|
| 135 |
+
echo " ✅ Setup terminé !"
|
| 136 |
+
echo ""
|
| 137 |
+
echo " Pour activer l'environnement :"
|
| 138 |
+
echo " source ${VENV_DIR}/bin/activate"
|
| 139 |
+
echo ""
|
| 140 |
+
echo " Pour lancer le benchmark :"
|
| 141 |
+
echo " bash scripts/serve_vllm.sh"
|
| 142 |
+
echo " python scripts/bench_openai_api.py"
|
| 143 |
+
echo " python scripts/sweep_vllm_configs.py"
|
| 144 |
+
echo "============================================"
|
scripts/sweep_advanced.py
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
sweep_advanced.py — Advanced vLLM optimization sweep for RTX 3090.
|
| 4 |
+
|
| 5 |
+
Tests each optimization dimension independently to measure its impact:
|
| 6 |
+
1. Attention backend: flash-attn vs flashinfer
|
| 7 |
+
2. KV cache dtype: auto vs fp8
|
| 8 |
+
3. Chunked prefill: on vs off
|
| 9 |
+
4. Block size: 8 vs 16 vs 32
|
| 10 |
+
5. GPU memory utilization: 0.85 vs 0.88 vs 0.90 vs 0.92
|
| 11 |
+
6. max_num_seqs vs max_batched_tokens grid
|
| 12 |
+
7. AWQ vs GPTQ vs FP8 model comparison
|
| 13 |
+
8. Speculative decoding (optional, if draft model available)
|
| 14 |
+
|
| 15 |
+
Usage:
|
| 16 |
+
# Baseline: test each optimization in isolation
|
| 17 |
+
python scripts/sweep_advanced.py --mode baseline
|
| 18 |
+
|
| 19 |
+
# Model shootout: compare AWQ vs GPTQ vs FP8
|
| 20 |
+
python scripts/sweep_advanced.py --mode shootout
|
| 21 |
+
|
| 22 |
+
# Full grid: exhaustively test combinations
|
| 23 |
+
python scripts/sweep_advanced.py --mode grid
|
| 24 |
+
|
| 25 |
+
# Single test with full instrumentation
|
| 26 |
+
python scripts/sweep_advanced.py --mode single --config awq-flashinfer-fp8kv
|
| 27 |
+
"""
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import argparse
|
| 31 |
+
import json
|
| 32 |
+
import os
|
| 33 |
+
import signal
|
| 34 |
+
import socket
|
| 35 |
+
import subprocess
|
| 36 |
+
import sys
|
| 37 |
+
import time
|
| 38 |
+
from dataclasses import dataclass, field, asdict
|
| 39 |
+
from datetime import datetime
|
| 40 |
+
from pathlib import Path
|
| 41 |
+
from typing import Optional
|
| 42 |
+
|
| 43 |
+
import httpx
|
| 44 |
+
|
| 45 |
+
PROJECT_DIR = Path(__file__).resolve().parent.parent
|
| 46 |
+
REPORTS_DIR = PROJECT_DIR / "reports"
|
| 47 |
+
PROMPTS_FILE = PROJECT_DIR / "prompts" / "bench_prompts.jsonl"
|
| 48 |
+
RESULTS_CSV = REPORTS_DIR / "results_advanced.csv"
|
| 49 |
+
SUMMARY_MD = REPORTS_DIR / "summary_advanced.md"
|
| 50 |
+
|
| 51 |
+
# ---- GPU warmup / profiler ----
|
| 52 |
+
try:
|
| 53 |
+
import pynvml
|
| 54 |
+
HAS_PYNVML = True
|
| 55 |
+
except ImportError:
|
| 56 |
+
HAS_PYNVML = False
|
| 57 |
+
|
| 58 |
+
# ---- vLLM availability ----
|
| 59 |
+
VLLM_AVAILABLE = False
|
| 60 |
+
try:
|
| 61 |
+
import vllm
|
| 62 |
+
VLLM_AVAILABLE = True
|
| 63 |
+
except ImportError:
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class OptimizedConfig:
|
| 69 |
+
"""A single vLLM config to benchmark."""
|
| 70 |
+
name: str
|
| 71 |
+
model_path: str
|
| 72 |
+
quant_method: str # awq, fp8, gptq, none
|
| 73 |
+
max_model_len: int = 8192
|
| 74 |
+
gpu_memory_utilization: float = 0.90
|
| 75 |
+
dtype: str = "auto"
|
| 76 |
+
max_num_seqs: int = 8
|
| 77 |
+
max_num_batched_tokens: int = 32768
|
| 78 |
+
attention_backend: str = "flashinfer" # flashinfer, flash-attn, auto
|
| 79 |
+
kv_cache_dtype: str = "auto" # auto, fp8
|
| 80 |
+
enable_chunked_prefill: bool = False
|
| 81 |
+
block_size: int = 16
|
| 82 |
+
swap_space: int = 4
|
| 83 |
+
enable_prefix_caching: bool = True
|
| 84 |
+
max_seq_len_to_capture: int = 8192
|
| 85 |
+
# Speculative decoding
|
| 86 |
+
speculative_model: str = ""
|
| 87 |
+
num_speculative_tokens: int = 5
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
def label(self) -> str:
|
| 91 |
+
parts = [self.quant_method, f"len{self.max_model_len}"]
|
| 92 |
+
if self.attention_backend != "auto":
|
| 93 |
+
parts.append(self.attention_backend.replace("-", ""))
|
| 94 |
+
if self.kv_cache_dtype == "fp8":
|
| 95 |
+
parts.append("fp8kv")
|
| 96 |
+
if self.enable_chunked_prefill:
|
| 97 |
+
parts.append("chunk")
|
| 98 |
+
parts.append(f"bs{self.block_size}")
|
| 99 |
+
parts.append(f"s{self.max_num_seqs}")
|
| 100 |
+
return "-".join(parts)
|
| 101 |
+
|
| 102 |
+
def to_server_flags(self, port: int = 8000) -> list[str]:
|
| 103 |
+
"""Convert to vLLM CLI arguments."""
|
| 104 |
+
flags = [
|
| 105 |
+
"--model", self.model_path,
|
| 106 |
+
"--host", "0.0.0.0",
|
| 107 |
+
"--port", str(port),
|
| 108 |
+
"--max-model-len", str(self.max_model_len),
|
| 109 |
+
"--gpu-memory-utilization", str(self.gpu_memory_utilization),
|
| 110 |
+
"--dtype", self.dtype,
|
| 111 |
+
"--max-num-seqs", str(self.max_num_seqs),
|
| 112 |
+
"--max-num-batched-tokens", str(self.max_num_batched_tokens),
|
| 113 |
+
"--tensor-parallel-size", "1",
|
| 114 |
+
"--trust-remote-code",
|
| 115 |
+
"--block-size", str(self.block_size),
|
| 116 |
+
"--swap-space", str(self.swap_space),
|
| 117 |
+
"--max-seq-len-to-capture", str(self.max_seq_len_to_capture),
|
| 118 |
+
]
|
| 119 |
+
if self.enable_prefix_caching:
|
| 120 |
+
flags.append("--enable-prefix-caching")
|
| 121 |
+
if self.enable_chunked_prefill:
|
| 122 |
+
flags.append("--enable-chunked-prefill")
|
| 123 |
+
if self.attention_backend not in ("auto", ""):
|
| 124 |
+
flags.extend(["--attention-backend", self.attention_backend])
|
| 125 |
+
if self.kv_cache_dtype not in ("auto", ""):
|
| 126 |
+
flags.extend(["--kv-cache-dtype", self.kv_cache_dtype])
|
| 127 |
+
if self.speculative_model:
|
| 128 |
+
flags.extend(["--speculative-model", self.speculative_model])
|
| 129 |
+
flags.extend(["--num-speculative-tokens", str(self.num_speculative_tokens)])
|
| 130 |
+
return flags
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# ============================================================
|
| 134 |
+
# Config presets for each sweep mode
|
| 135 |
+
# ============================================================
|
| 136 |
+
|
| 137 |
+
# Models — best to worst for RTX 3090
|
| 138 |
+
MODELS = {
|
| 139 |
+
"awq": "QuantTrio/Qwen3.6-27B-AWQ",
|
| 140 |
+
"gptq": "groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit",
|
| 141 |
+
"fp8": "Qwen/Qwen3.6-27B-FP8",
|
| 142 |
+
"base": "Qwen/Qwen3.6-27B",
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def baseline_configs() -> list[OptimizedConfig]:
|
| 147 |
+
"""
|
| 148 |
+
A/B test each optimization dimension independently.
|
| 149 |
+
All configs use AWQ as the base model (best for Ampere).
|
| 150 |
+
"""
|
| 151 |
+
BASE = dict(
|
| 152 |
+
model_path=MODELS["awq"], quant_method="awq",
|
| 153 |
+
max_model_len=8192, gpu_memory_utilization=0.90,
|
| 154 |
+
max_num_seqs=8, max_num_batched_tokens=32768,
|
| 155 |
+
)
|
| 156 |
+
configs = []
|
| 157 |
+
|
| 158 |
+
# ---- 1. Attention backend shootout ----
|
| 159 |
+
for attn in ["flashinfer", "flash-attn"]:
|
| 160 |
+
c = OptimizedConfig(name=f"attn-{attn}", attention_backend=attn, **BASE)
|
| 161 |
+
configs.append(c)
|
| 162 |
+
|
| 163 |
+
# ---- 2. FP8 KV cache (VRAM saving) ----
|
| 164 |
+
for kv in ["auto", "fp8"]:
|
| 165 |
+
c = OptimizedConfig(name=f"kv-{kv}", kv_cache_dtype=kv, **BASE)
|
| 166 |
+
configs.append(c)
|
| 167 |
+
|
| 168 |
+
# ---- 3. Chunked prefill ----
|
| 169 |
+
for cp in [False, True]:
|
| 170 |
+
tag = "chunk-on" if cp else "chunk-off"
|
| 171 |
+
c = OptimizedConfig(name=tag, enable_chunked_prefill=cp, **BASE)
|
| 172 |
+
configs.append(c)
|
| 173 |
+
|
| 174 |
+
# ---- 4. Block size sweep ----
|
| 175 |
+
for bs in [8, 16, 32]:
|
| 176 |
+
c = OptimizedConfig(name=f"blksz-{bs}", block_size=bs, **BASE)
|
| 177 |
+
configs.append(c)
|
| 178 |
+
|
| 179 |
+
# ---- 5. GPU memory utilization sweep ----
|
| 180 |
+
for mem in [0.85, 0.88, 0.90, 0.92]:
|
| 181 |
+
c = OptimizedConfig(name=f"gpumem-{int(mem*100)}", gpu_memory_utilization=mem, **BASE)
|
| 182 |
+
configs.append(c)
|
| 183 |
+
|
| 184 |
+
# ---- 6. Batch size sweep ----
|
| 185 |
+
for seqs, batched in [(4, 16384), (8, 32768), (12, 49152)]:
|
| 186 |
+
c = OptimizedConfig(
|
| 187 |
+
name=f"batch-s{seqs}-b{batched}",
|
| 188 |
+
max_num_seqs=seqs, max_num_batched_tokens=batched, **BASE,
|
| 189 |
+
)
|
| 190 |
+
configs.append(c)
|
| 191 |
+
|
| 192 |
+
# ---- 7. Combined best guess ----
|
| 193 |
+
c = OptimizedConfig(
|
| 194 |
+
name="combined-best",
|
| 195 |
+
attention_backend="flashinfer",
|
| 196 |
+
kv_cache_dtype="fp8",
|
| 197 |
+
enable_chunked_prefill=True,
|
| 198 |
+
block_size=16,
|
| 199 |
+
max_num_seqs=8,
|
| 200 |
+
max_num_batched_tokens=32768,
|
| 201 |
+
**BASE,
|
| 202 |
+
)
|
| 203 |
+
configs.append(c)
|
| 204 |
+
|
| 205 |
+
return configs
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def model_shootout_configs() -> list[OptimizedConfig]:
|
| 209 |
+
"""Compare AWQ vs GPTQ vs FP8 on equal footing."""
|
| 210 |
+
configs = []
|
| 211 |
+
|
| 212 |
+
COMMON = dict(
|
| 213 |
+
max_model_len=8192, gpu_memory_utilization=0.90,
|
| 214 |
+
max_num_seqs=8, max_num_batched_tokens=32768,
|
| 215 |
+
attention_backend="flashinfer", kv_cache_dtype="fp8",
|
| 216 |
+
enable_chunked_prefill=True,
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# AWQ — expected best
|
| 220 |
+
configs.append(OptimizedConfig(
|
| 221 |
+
name="shootout-awq", model_path=MODELS["awq"], quant_method="awq", **COMMON,
|
| 222 |
+
))
|
| 223 |
+
|
| 224 |
+
# GPTQ Marlin — potentially faster on Ampere
|
| 225 |
+
configs.append(OptimizedConfig(
|
| 226 |
+
name="shootout-gptq", model_path=MODELS["gptq"], quant_method="gptq", **COMMON,
|
| 227 |
+
))
|
| 228 |
+
|
| 229 |
+
# FP8 — higher precision, tighter on VRAM
|
| 230 |
+
configs.append(OptimizedConfig(
|
| 231 |
+
name="shootout-fp8", model_path=MODELS["fp8"], quant_method="fp8",
|
| 232 |
+
max_num_seqs=6, max_num_batched_tokens=16384, **COMMON,
|
| 233 |
+
))
|
| 234 |
+
|
| 235 |
+
# FP8 + FP8 KV cache
|
| 236 |
+
configs.append(OptimizedConfig(
|
| 237 |
+
name="shootout-fp8-fp8kv", model_path=MODELS["fp8"], quant_method="fp8",
|
| 238 |
+
max_num_seqs=8, max_num_batched_tokens=32768, **COMMON,
|
| 239 |
+
))
|
| 240 |
+
|
| 241 |
+
return configs
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def grid_configs() -> list[OptimizedConfig]:
|
| 245 |
+
"""Full grid search over the most impactful dimensions. ~20 configs."""
|
| 246 |
+
configs = []
|
| 247 |
+
|
| 248 |
+
for model_key, model_path in [("awq", MODELS["awq"]), ("gptq", MODELS["gptq"])]:
|
| 249 |
+
for attn in ["flashinfer", "flash-attn"]:
|
| 250 |
+
for kv in ["fp8", "auto"]:
|
| 251 |
+
for bs in [16, 32]:
|
| 252 |
+
for seqs in [8, 12]:
|
| 253 |
+
c = OptimizedConfig(
|
| 254 |
+
name=f"grid-{model_key}-{attn}-{kv}kv-bs{bs}-s{seqs}",
|
| 255 |
+
model_path=model_path,
|
| 256 |
+
quant_method=model_key,
|
| 257 |
+
attention_backend=attn,
|
| 258 |
+
kv_cache_dtype=kv,
|
| 259 |
+
block_size=bs,
|
| 260 |
+
max_num_seqs=seqs,
|
| 261 |
+
max_num_batched_tokens=seqs * 4096,
|
| 262 |
+
max_model_len=8192,
|
| 263 |
+
gpu_memory_utilization=0.90,
|
| 264 |
+
enable_chunked_prefill=True,
|
| 265 |
+
)
|
| 266 |
+
configs.append(c)
|
| 267 |
+
|
| 268 |
+
return configs
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
# ============================================================
|
| 272 |
+
# Server lifecycle
|
| 273 |
+
# ============================================================
|
| 274 |
+
|
| 275 |
+
class VllmServer:
|
| 276 |
+
"""Manage a vLLM server process."""
|
| 277 |
+
|
| 278 |
+
def __init__(self, config: OptimizedConfig, port: int = 8000):
|
| 279 |
+
self.config = config
|
| 280 |
+
self.port = port
|
| 281 |
+
self.process: Optional[subprocess.Popen] = None
|
| 282 |
+
self.logfile = Path(f"/tmp/vllm_adv_{config.name}.log")
|
| 283 |
+
|
| 284 |
+
def start(self):
|
| 285 |
+
cmd = [sys.executable, "-m", "vllm.entrypoints.openai.api_server"]
|
| 286 |
+
cmd.extend(self.config.to_server_flags(port=self.port))
|
| 287 |
+
|
| 288 |
+
print(f" {' '.join(cmd)}")
|
| 289 |
+
|
| 290 |
+
self.logfile.parent.mkdir(parents=True, exist_ok=True)
|
| 291 |
+
fout = open(str(self.logfile), "w")
|
| 292 |
+
self.process = subprocess.Popen(
|
| 293 |
+
cmd, stdout=fout, stderr=subprocess.STDOUT,
|
| 294 |
+
start_new_session=True,
|
| 295 |
+
env={**os.environ, "VLLM_ATTENTION_BACKEND": self.config.attention_backend},
|
| 296 |
+
)
|
| 297 |
+
return True
|
| 298 |
+
|
| 299 |
+
def wait_ready(self, timeout: int = 600) -> tuple[bool, str]:
|
| 300 |
+
client = httpx.Client(base_url=f"http://localhost:{self.port}", timeout=httpx.Timeout(8.0))
|
| 301 |
+
deadline = time.time() + timeout
|
| 302 |
+
while time.time() < deadline:
|
| 303 |
+
if self.process and self.process.poll() is not None:
|
| 304 |
+
tail = self._log_tail(60)
|
| 305 |
+
return False, f"CRASH rc={self.process.returncode}\n{tail}"
|
| 306 |
+
try:
|
| 307 |
+
resp = client.get("/v1/models")
|
| 308 |
+
if resp.status_code == 200:
|
| 309 |
+
client.close()
|
| 310 |
+
return True, "OK"
|
| 311 |
+
except Exception:
|
| 312 |
+
pass
|
| 313 |
+
time.sleep(5)
|
| 314 |
+
client.close()
|
| 315 |
+
return False, f"TIMEOUT ({timeout}s)\n{self._log_tail(30)}"
|
| 316 |
+
|
| 317 |
+
def stop(self):
|
| 318 |
+
if not self.process:
|
| 319 |
+
return
|
| 320 |
+
print(" ⏹️ Stopping server...")
|
| 321 |
+
try:
|
| 322 |
+
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
| 323 |
+
self.process.wait(timeout=30)
|
| 324 |
+
except subprocess.TimeoutExpired:
|
| 325 |
+
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
| 326 |
+
self.process.wait(timeout=10)
|
| 327 |
+
except Exception:
|
| 328 |
+
pass
|
| 329 |
+
self.process = None
|
| 330 |
+
time.sleep(5)
|
| 331 |
+
|
| 332 |
+
def _log_tail(self, lines: int = 40) -> str:
|
| 333 |
+
if not self.logfile.exists():
|
| 334 |
+
return "(no log)"
|
| 335 |
+
try:
|
| 336 |
+
with open(self.logfile, "r") as f:
|
| 337 |
+
return "".join(f.readlines()[-lines:])
|
| 338 |
+
except Exception:
|
| 339 |
+
return "(error)"
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
# ============================================================
|
| 343 |
+
# Benchmark integration
|
| 344 |
+
# ============================================================
|
| 345 |
+
|
| 346 |
+
@dataclass
|
| 347 |
+
class SweepResult:
|
| 348 |
+
config: OptimizedConfig
|
| 349 |
+
success: bool
|
| 350 |
+
error: str = ""
|
| 351 |
+
avg_output_tps: float = 0.0
|
| 352 |
+
avg_total_tps: float = 0.0
|
| 353 |
+
avg_ttft_ms: float = 0.0
|
| 354 |
+
avg_tpot_ms: float = 0.0
|
| 355 |
+
peak_vram_mb: float = 0.0
|
| 356 |
+
avg_gpu_util_pct: float = 0.0
|
| 357 |
+
avg_power_w: float = 0.0
|
| 358 |
+
ok: int = 0
|
| 359 |
+
fail: int = 0
|
| 360 |
+
duration_s: float = 0.0
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def parse_csv_for_config(csv_path: str, config_name: str) -> Optional[dict]:
|
| 364 |
+
"""Aggregate results from CSV for a config."""
|
| 365 |
+
if not os.path.exists(csv_path):
|
| 366 |
+
return None
|
| 367 |
+
import csv
|
| 368 |
+
try:
|
| 369 |
+
with open(csv_path, "r") as f:
|
| 370 |
+
rows = [r for r in csv.DictReader(f) if r.get("config_name") == config_name]
|
| 371 |
+
except Exception:
|
| 372 |
+
return None
|
| 373 |
+
|
| 374 |
+
if not rows:
|
| 375 |
+
# Fallback: take last 30 entries
|
| 376 |
+
with open(csv_path, "r") as f:
|
| 377 |
+
rows = list(csv.DictReader(f))[-30:]
|
| 378 |
+
|
| 379 |
+
ok = [r for r in rows if r.get("success") == "True"]
|
| 380 |
+
if not ok:
|
| 381 |
+
return {"ok": 0, "fail": len(rows)}
|
| 382 |
+
|
| 383 |
+
def avg(key):
|
| 384 |
+
vals = [float(r[key]) for r in ok if r.get(key)]
|
| 385 |
+
return sum(vals) / len(vals) if vals else 0.0
|
| 386 |
+
|
| 387 |
+
def pct(key):
|
| 388 |
+
vals = [float(r[key]) for r in ok if r.get(key) and float(r.get(key)) > 0]
|
| 389 |
+
return sum(vals) / len(vals) if vals else 0.0
|
| 390 |
+
|
| 391 |
+
def maxv(key):
|
| 392 |
+
vals = [float(r[key]) for r in ok if r.get(key)]
|
| 393 |
+
return max(vals) if vals else 0.0
|
| 394 |
+
|
| 395 |
+
return {
|
| 396 |
+
"ok": len(ok),
|
| 397 |
+
"fail": len(rows) - len(ok),
|
| 398 |
+
"avg_output_tps": avg("output_tps"),
|
| 399 |
+
"avg_total_tps": avg("total_tps"),
|
| 400 |
+
"avg_ttft_ms": avg("ttft_ms"),
|
| 401 |
+
"avg_tpot_ms": avg("tpot_ms"),
|
| 402 |
+
"peak_vram_mb": maxv("vram_peak_mb"),
|
| 403 |
+
"avg_gpu_util_pct": pct("gpu_util_pct"),
|
| 404 |
+
"avg_power_w": pct("power_w"),
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def run_benchmark_subprocess(config: OptimizedConfig, port: int = 8000) -> tuple[bool, str]:
|
| 409 |
+
"""Run bench_openai_api.py as a subprocess."""
|
| 410 |
+
bench_script = PROJECT_DIR / "scripts" / "bench_openai_api.py"
|
| 411 |
+
cmd = [
|
| 412 |
+
sys.executable, str(bench_script),
|
| 413 |
+
"--url", f"http://localhost:{port}",
|
| 414 |
+
"--prompts", str(PROMPTS_FILE),
|
| 415 |
+
"--output", str(RESULTS_CSV),
|
| 416 |
+
"--model", config.model_path,
|
| 417 |
+
"--backend", "vllm",
|
| 418 |
+
"--config", config.name,
|
| 419 |
+
"--max-model-len", str(config.max_model_len),
|
| 420 |
+
"--gpu-mem-util", str(config.gpu_memory_utilization),
|
| 421 |
+
"--dtype", config.dtype,
|
| 422 |
+
"--quantization", config.quant_method,
|
| 423 |
+
"--repeat", "2",
|
| 424 |
+
]
|
| 425 |
+
print(f" 🔬 Running benchmark...")
|
| 426 |
+
try:
|
| 427 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
| 428 |
+
if result.returncode == 0:
|
| 429 |
+
# Extract key lines
|
| 430 |
+
for line in result.stdout.split("\n"):
|
| 431 |
+
if any(kw in line for kw in ["Output TPS:", "TTFT", "SUMMARY", "Success"]):
|
| 432 |
+
print(f" {line.strip()}")
|
| 433 |
+
return True, ""
|
| 434 |
+
else:
|
| 435 |
+
err = result.stderr[-500:] if result.stderr else result.stdout[-500:]
|
| 436 |
+
return False, err
|
| 437 |
+
except subprocess.TimeoutExpired:
|
| 438 |
+
return False, "Benchmark subprocess timed out"
|
| 439 |
+
except Exception as e:
|
| 440 |
+
return False, str(e)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
# ============================================================
|
| 444 |
+
# Orchestrator
|
| 445 |
+
# ============================================================
|
| 446 |
+
|
| 447 |
+
def sweep(configs: list[OptimizedConfig], port: int = 8000) -> list[SweepResult]:
|
| 448 |
+
results: list[SweepResult] = []
|
| 449 |
+
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 450 |
+
|
| 451 |
+
# Backup old CSV
|
| 452 |
+
if RESULTS_CSV.exists():
|
| 453 |
+
RESULTS_CSV.rename(RESULTS_CSV.with_suffix(".csv.bak"))
|
| 454 |
+
|
| 455 |
+
print(f"\n{'='*70}")
|
| 456 |
+
print(f" ADVANCED OPTIMIZATION SWEEP — {len(configs)} configs")
|
| 457 |
+
print(f"{'='*70}")
|
| 458 |
+
for i, c in enumerate(configs, 1):
|
| 459 |
+
print(f" [{i:2d}] {c.name:50s} ({c.label[:70]})")
|
| 460 |
+
print(f"{'='*70}\n")
|
| 461 |
+
|
| 462 |
+
for idx, config in enumerate(configs, 1):
|
| 463 |
+
result = SweepResult(config=config, success=False)
|
| 464 |
+
t0 = time.time()
|
| 465 |
+
|
| 466 |
+
print(f"\n{'#'*70}")
|
| 467 |
+
print(f" [{idx}/{len(configs)}] {config.name}")
|
| 468 |
+
print(f" attn={config.attention_backend} kv={config.kv_cache_dtype} "
|
| 469 |
+
f"chunk={config.enable_chunked_prefill} bs={config.block_size} "
|
| 470 |
+
f"seqs={config.max_num_seqs}")
|
| 471 |
+
print(f"{'#'*70}")
|
| 472 |
+
|
| 473 |
+
# Start server
|
| 474 |
+
server = VllmServer(config, port=port)
|
| 475 |
+
try:
|
| 476 |
+
server.start()
|
| 477 |
+
except Exception as e:
|
| 478 |
+
result.error = f"START FAIL: {e}"
|
| 479 |
+
results.append(result)
|
| 480 |
+
continue
|
| 481 |
+
|
| 482 |
+
# Wait for ready
|
| 483 |
+
ok, msg = server.wait_ready()
|
| 484 |
+
if not ok:
|
| 485 |
+
result.error = msg[:500]
|
| 486 |
+
result.duration_s = time.time() - t0
|
| 487 |
+
server.stop()
|
| 488 |
+
results.append(result)
|
| 489 |
+
print(f" ❌ Failed to start: {msg[:200]}")
|
| 490 |
+
continue
|
| 491 |
+
|
| 492 |
+
print(f" ✅ Server ready")
|
| 493 |
+
|
| 494 |
+
# Run benchmark
|
| 495 |
+
bench_ok, bench_err = run_benchmark_subprocess(config, port=port)
|
| 496 |
+
|
| 497 |
+
# Parse results
|
| 498 |
+
metrics = parse_csv_for_config(str(RESULTS_CSV), config.name)
|
| 499 |
+
if metrics:
|
| 500 |
+
result.success = metrics.get("ok", 0) > 0
|
| 501 |
+
result.avg_output_tps = metrics.get("avg_output_tps", 0)
|
| 502 |
+
result.avg_total_tps = metrics.get("avg_total_tps", 0)
|
| 503 |
+
result.avg_ttft_ms = metrics.get("avg_ttft_ms", 0)
|
| 504 |
+
result.avg_tpot_ms = metrics.get("avg_tpot_ms", 0)
|
| 505 |
+
result.peak_vram_mb = metrics.get("peak_vram_mb", 0)
|
| 506 |
+
result.avg_gpu_util_pct = metrics.get("avg_gpu_util_pct", 0)
|
| 507 |
+
result.avg_power_w = metrics.get("avg_power_w", 0)
|
| 508 |
+
result.ok = metrics.get("ok", 0)
|
| 509 |
+
result.fail = metrics.get("fail", 0)
|
| 510 |
+
|
| 511 |
+
if not bench_ok:
|
| 512 |
+
result.error = bench_err[:500]
|
| 513 |
+
if not result.success:
|
| 514 |
+
result.error = bench_err[:500] or result.error
|
| 515 |
+
|
| 516 |
+
result.duration_s = time.time() - t0
|
| 517 |
+
server.stop()
|
| 518 |
+
|
| 519 |
+
status = "✅" if result.success else "❌"
|
| 520 |
+
print(f" {status} TPS={result.avg_output_tps:.1f} TTFT={result.avg_ttft_ms:.0f}ms "
|
| 521 |
+
f"VRAM={result.peak_vram_mb:.0f}MB [{result.ok}/{result.ok + result.fail}] "
|
| 522 |
+
f"({result.duration_s:.0f}s)")
|
| 523 |
+
|
| 524 |
+
results.append(result)
|
| 525 |
+
|
| 526 |
+
return results
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
# ============================================================
|
| 530 |
+
# Report generation
|
| 531 |
+
# ============================================================
|
| 532 |
+
|
| 533 |
+
def generate_report(results: list[SweepResult], mode: str):
|
| 534 |
+
ok = [r for r in results if r.success]
|
| 535 |
+
fail = [r for r in results if not r.success]
|
| 536 |
+
ok_sorted = sorted(ok, key=lambda r: r.avg_output_tps, reverse=True)
|
| 537 |
+
|
| 538 |
+
lines = [
|
| 539 |
+
f"# Qwen SpeedLab — Advanced Optimization Report",
|
| 540 |
+
f"",
|
| 541 |
+
f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
| 542 |
+
f"**GPU:** NVIDIA GeForce RTX 3090 (24 GB, SM 8.6)",
|
| 543 |
+
f"**Mode:** {mode}",
|
| 544 |
+
f"**Configs:** {len(results)} tested — {len(ok)} passed, {len(fail)} failed",
|
| 545 |
+
f"",
|
| 546 |
+
"---",
|
| 547 |
+
f"",
|
| 548 |
+
]
|
| 549 |
+
|
| 550 |
+
if ok_sorted:
|
| 551 |
+
best = ok_sorted[0]
|
| 552 |
+
lines += [
|
| 553 |
+
f"## 🏆 Best Configuration",
|
| 554 |
+
f"",
|
| 555 |
+
f"```",
|
| 556 |
+
f" Config name: {best.config.name}",
|
| 557 |
+
f" Model: {best.config.model_path}",
|
| 558 |
+
f" Attention: {best.config.attention_backend}",
|
| 559 |
+
f" KV cache dtype: {best.config.kv_cache_dtype}",
|
| 560 |
+
f" Chunked prefill: {best.config.enable_chunked_prefill}",
|
| 561 |
+
f" Block size: {best.config.block_size}",
|
| 562 |
+
f" Max seqs: {best.config.max_num_seqs}",
|
| 563 |
+
f" Max batched tok: {best.config.max_num_batched_tokens}",
|
| 564 |
+
f" GPU mem util: {best.config.gpu_memory_utilization}",
|
| 565 |
+
f"```",
|
| 566 |
+
f"",
|
| 567 |
+
f"| Metric | Value |",
|
| 568 |
+
f"|--------|-------|",
|
| 569 |
+
f"| **Output TPS** | **{best.avg_output_tps:.1f} tok/s** |",
|
| 570 |
+
f"| Total TPS | {best.avg_total_tps:.1f} tok/s |",
|
| 571 |
+
f"| TTFT | {best.avg_ttft_ms:.0f} ms |",
|
| 572 |
+
f"| TPOT | {best.avg_tpot_ms:.1f} ms |",
|
| 573 |
+
f"| Peak VRAM | {best.peak_vram_mb:.0f} MB |",
|
| 574 |
+
f"| GPU util | {best.avg_gpu_util_pct:.1f}% |",
|
| 575 |
+
f"| Power | {best.avg_power_w:.1f} W |",
|
| 576 |
+
f"| Prompt success | {best.ok}/{best.ok + best.fail} |",
|
| 577 |
+
f"| Test duration | {best.duration_s:.0f}s |",
|
| 578 |
+
f"",
|
| 579 |
+
]
|
| 580 |
+
|
| 581 |
+
# ---- Optimization dimension analysis ----
|
| 582 |
+
lines += [
|
| 583 |
+
"## 📊 Optimization Impact Analysis",
|
| 584 |
+
"",
|
| 585 |
+
"Each table shows the effect of toggling one optimization dimension.",
|
| 586 |
+
"",
|
| 587 |
+
]
|
| 588 |
+
|
| 589 |
+
# Group by dimension
|
| 590 |
+
dimensions = {
|
| 591 |
+
"attention_backend": "Attention Backend",
|
| 592 |
+
"kv_cache_dtype": "KV Cache Dtype",
|
| 593 |
+
"enable_chunked_prefill": "Chunked Prefill",
|
| 594 |
+
"block_size": "Block Size",
|
| 595 |
+
"gpu_memory_utilization": "GPU Memory Utilization",
|
| 596 |
+
"max_num_seqs": "Max Concurrent Sequences",
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
for dim, title in dimensions.items():
|
| 600 |
+
groups: dict[str, list[SweepResult]] = {}
|
| 601 |
+
for r in ok:
|
| 602 |
+
key = str(getattr(r.config, dim, "?"))
|
| 603 |
+
groups.setdefault(key, []).append(r)
|
| 604 |
+
|
| 605 |
+
if len(groups) <= 1:
|
| 606 |
+
continue
|
| 607 |
+
|
| 608 |
+
lines += [f"### {title}", ""]
|
| 609 |
+
lines += ["| Value | Avg TPS | TTFT ms | VRAM MB | Tests |",
|
| 610 |
+
"|-------|---------|---------|---------|-------|"]
|
| 611 |
+
for key in sorted(groups.keys()):
|
| 612 |
+
grp = groups[key]
|
| 613 |
+
avg_tps = sum(r.avg_output_tps for r in grp) / len(grp)
|
| 614 |
+
avg_ttft = sum(r.avg_ttft_ms for r in grp) / len(grp)
|
| 615 |
+
avg_vram = sum(r.peak_vram_mb for r in grp) / len(grp)
|
| 616 |
+
n = sum(r.ok for r in grp)
|
| 617 |
+
lines.append(f"| `{key}` | {avg_tps:.1f} | {avg_ttft:.0f} | {avg_vram:.0f} | {n} |")
|
| 618 |
+
lines.append("")
|
| 619 |
+
|
| 620 |
+
# ---- Full results table ----
|
| 621 |
+
lines += [
|
| 622 |
+
"## 📋 All Configurations",
|
| 623 |
+
"",
|
| 624 |
+
"| # | Name | Attn | KV | Chunk | BS | Seqs | TPS | TTFT | VRAM | Status |",
|
| 625 |
+
"|---|------|------|----|-------|----|------|-----|------|------|--------|",
|
| 626 |
+
]
|
| 627 |
+
for i, r in enumerate(results, 1):
|
| 628 |
+
if r.success:
|
| 629 |
+
tps = f"{r.avg_output_tps:.1f}"
|
| 630 |
+
ttft = f"{r.avg_ttft_ms:.0f}"
|
| 631 |
+
vram = f"{r.peak_vram_mb:.0f}"
|
| 632 |
+
status = "✅"
|
| 633 |
+
else:
|
| 634 |
+
tps = ttft = vram = "-"
|
| 635 |
+
status = "❌"
|
| 636 |
+
lines.append(
|
| 637 |
+
f"| {i} | {r.config.name} | {r.config.attention_backend} | {r.config.kv_cache_dtype} | "
|
| 638 |
+
f"{r.config.enable_chunked_prefill} | {r.config.block_size} | {r.config.max_num_seqs} | "
|
| 639 |
+
f"{tps} | {ttft} | {vram} | {status} |"
|
| 640 |
+
)
|
| 641 |
+
lines.append("")
|
| 642 |
+
|
| 643 |
+
# ---- Failed ----
|
| 644 |
+
if fail:
|
| 645 |
+
lines += ["## ❌ Failed Configurations", ""]
|
| 646 |
+
for r in fail:
|
| 647 |
+
lines.append(f"- **{r.config.name}**: `{r.error[:200]}`")
|
| 648 |
+
lines.append("")
|
| 649 |
+
|
| 650 |
+
# ---- Recommendations ----
|
| 651 |
+
lines += [
|
| 652 |
+
"## 💡 Recommendations for RTX 3090",
|
| 653 |
+
"",
|
| 654 |
+
]
|
| 655 |
+
|
| 656 |
+
# Analyze what worked best
|
| 657 |
+
if ok_sorted:
|
| 658 |
+
flashinfer_ok = [r for r in ok if r.config.attention_backend == "flashinfer"]
|
| 659 |
+
flashinfer_tps = sum(r.avg_output_tps for r in flashinfer_ok) / len(flashinfer_ok) if flashinfer_ok else 0
|
| 660 |
+
flashattn_ok = [r for r in ok if r.config.attention_backend == "flash-attn"]
|
| 661 |
+
flashattn_tps = sum(r.avg_output_tps for r in flashattn_ok) / len(flashattn_ok) if flashattn_ok else 0
|
| 662 |
+
|
| 663 |
+
fp8kv_ok = [r for r in ok if r.config.kv_cache_dtype == "fp8"]
|
| 664 |
+
fp8kv_tps = sum(r.avg_output_tps for r in fp8kv_ok) / len(fp8kv_ok) if fp8kv_ok else 0
|
| 665 |
+
auto_ok = [r for r in ok if r.config.kv_cache_dtype == "auto"]
|
| 666 |
+
auto_tps = sum(r.avg_output_tps for r in auto_ok) / len(auto_ok) if auto_ok else 0
|
| 667 |
+
|
| 668 |
+
lines.append("1. **Attention backend**:")
|
| 669 |
+
if flashinfer_tps > flashattn_tps:
|
| 670 |
+
lines.append(f" FlashInfer is faster ({flashinfer_tps:.1f} vs {flashattn_tps:.1f} tok/s). Use `--attention-backend flashinfer`.")
|
| 671 |
+
else:
|
| 672 |
+
lines.append(f" FlashAttention is comparable ({flashattn_tps:.1f} vs {flashinfer_tps:.1f} tok/s). Default is fine.")
|
| 673 |
+
|
| 674 |
+
lines.append("")
|
| 675 |
+
lines.append("2. **KV Cache dtype**:")
|
| 676 |
+
if fp8kv_tps > auto_tps * 0.95:
|
| 677 |
+
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`.")
|
| 678 |
+
else:
|
| 679 |
+
lines.append(f" FP8 KV cache costs speed ({fp8kv_tps:.1f} vs {auto_tps:.1f} tok/s). Keep auto unless you need the VRAM.")
|
| 680 |
+
|
| 681 |
+
best_quant = ok_sorted[0].config.quant_method
|
| 682 |
+
lines.append("")
|
| 683 |
+
lines.append(f"3. **Best quantization**: `{best_quant}` — based on measured throughput.")
|
| 684 |
+
|
| 685 |
+
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])))
|
| 686 |
+
lines.append(f"4. **Optimal block size**: `{best_bs}`")
|
| 687 |
+
|
| 688 |
+
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])))
|
| 689 |
+
lines.append(f"5. **Optimal max_num_seqs**: `{best_seqs}`")
|
| 690 |
+
|
| 691 |
+
lines += [
|
| 692 |
+
"",
|
| 693 |
+
"6. **Always enable**: prefix caching, trust-remote-code",
|
| 694 |
+
"7. **Chunked prefill**: helps throughput under concurrent load",
|
| 695 |
+
"8. **Swap space**: 4GB CPU swap acts as a safety net against OOM",
|
| 696 |
+
"",
|
| 697 |
+
"---",
|
| 698 |
+
f"*Report generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*",
|
| 699 |
+
"",
|
| 700 |
+
]
|
| 701 |
+
|
| 702 |
+
SUMMARY_MD.parent.mkdir(parents=True, exist_ok=True)
|
| 703 |
+
with open(SUMMARY_MD, "w") as f:
|
| 704 |
+
f.write("\n".join(lines))
|
| 705 |
+
print(f"\n📄 Report: {SUMMARY_MD}")
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
# ============================================================
|
| 709 |
+
# CLI
|
| 710 |
+
# ============================================================
|
| 711 |
+
|
| 712 |
+
def main():
|
| 713 |
+
parser = argparse.ArgumentParser(description="Advanced vLLM optimization sweep for RTX 3090")
|
| 714 |
+
parser.add_argument("--mode", default="baseline",
|
| 715 |
+
choices=["baseline", "shootout", "grid", "single", "list"],
|
| 716 |
+
help="baseline=A/B each opt, shootout=AWQ vs GPTQ vs FP8, grid=full combos")
|
| 717 |
+
parser.add_argument("--config", type=str, help="Single config name (for --mode single)")
|
| 718 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 719 |
+
parser.add_argument("--timeout", type=int, default=600)
|
| 720 |
+
parser.add_argument("--dry-run", action="store_true", help="List configs, don't run")
|
| 721 |
+
|
| 722 |
+
args = parser.parse_args()
|
| 723 |
+
|
| 724 |
+
# Config selection
|
| 725 |
+
if args.mode == "baseline":
|
| 726 |
+
configs = baseline_configs()
|
| 727 |
+
elif args.mode == "shootout":
|
| 728 |
+
configs = model_shootout_configs()
|
| 729 |
+
elif args.mode == "grid":
|
| 730 |
+
configs = grid_configs()
|
| 731 |
+
elif args.mode == "single":
|
| 732 |
+
all_configs = baseline_configs() + model_shootout_configs()
|
| 733 |
+
configs = [c for c in all_configs if c.name == args.config]
|
| 734 |
+
if not configs:
|
| 735 |
+
print(f"Unknown config: {args.config}")
|
| 736 |
+
print(f"Available: {sorted(set(c.name for c in all_configs))}")
|
| 737 |
+
sys.exit(1)
|
| 738 |
+
else: # list
|
| 739 |
+
for c in baseline_configs() + model_shootout_configs():
|
| 740 |
+
print(f" {c.name:45s} {c.label}")
|
| 741 |
+
return
|
| 742 |
+
|
| 743 |
+
print(f"\n{'='*70}")
|
| 744 |
+
print(f" ADVANCED SWEEP — {args.mode} mode — {len(configs)} config(s)")
|
| 745 |
+
print(f"{'='*70}")
|
| 746 |
+
|
| 747 |
+
if args.dry_run:
|
| 748 |
+
for c in configs:
|
| 749 |
+
print(f" {c.name}")
|
| 750 |
+
print(f" model={c.model_path}")
|
| 751 |
+
print(f" attn={c.attention_backend} kv={c.kv_cache_dtype} chunk={c.enable_chunked_prefill}")
|
| 752 |
+
print(f" bs={c.block_size} seqs={c.max_num_seqs} batched={c.max_num_batched_tokens}")
|
| 753 |
+
return
|
| 754 |
+
|
| 755 |
+
# Preflight
|
| 756 |
+
print("🔍 Preflight checks...")
|
| 757 |
+
# GPU
|
| 758 |
+
try:
|
| 759 |
+
result = subprocess.run(
|
| 760 |
+
["nvidia-smi", "--query-gpu=memory.free", "--format=csv,noheader"],
|
| 761 |
+
capture_output=True, text=True, timeout=10,
|
| 762 |
+
)
|
| 763 |
+
free_mb = int(result.stdout.strip())
|
| 764 |
+
print(f" GPU free: {free_mb} MiB")
|
| 765 |
+
if free_mb < 20000:
|
| 766 |
+
print(" ⚠️ Less than 20GB free — old processes?")
|
| 767 |
+
except Exception as e:
|
| 768 |
+
print(f" ⚠️ GPU check failed: {e}")
|
| 769 |
+
|
| 770 |
+
# Port
|
| 771 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 772 |
+
if sock.connect_ex(('localhost', args.port)) == 0:
|
| 773 |
+
print(f" ⚠️ Port {args.port} already in use!")
|
| 774 |
+
sock.close()
|
| 775 |
+
|
| 776 |
+
# HF_TOKEN
|
| 777 |
+
if not os.environ.get("HF_TOKEN"):
|
| 778 |
+
print(" ⚠️ HF_TOKEN not set")
|
| 779 |
+
print(" ✅ Ready\n")
|
| 780 |
+
|
| 781 |
+
# Run
|
| 782 |
+
results = sweep(configs, port=args.port)
|
| 783 |
+
|
| 784 |
+
# Report
|
| 785 |
+
generate_report(results, args.mode)
|
| 786 |
+
|
| 787 |
+
# Final summary
|
| 788 |
+
ok = [r for r in results if r.success]
|
| 789 |
+
print(f"\n{'='*70}")
|
| 790 |
+
print(f" SWEEP COMPLETE")
|
| 791 |
+
print(f" Configs: {len(results)} | Passed: {len(ok)} | Failed: {len(results) - len(ok)}")
|
| 792 |
+
if ok:
|
| 793 |
+
best = max(ok, key=lambda r: r.avg_output_tps)
|
| 794 |
+
print(f" 🏆 Best: {best.config.name} — {best.avg_output_tps:.1f} tok/s")
|
| 795 |
+
print(f" attn={best.config.attention_backend} kv={best.config.kv_cache_dtype} "
|
| 796 |
+
f"bs={best.config.block_size} seqs={best.config.max_num_seqs}")
|
| 797 |
+
print(f"{'='*70}")
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
if __name__ == "__main__":
|
| 801 |
+
main()
|
scripts/sweep_vllm_configs.py
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
sweep_vllm_configs.py — Automatic vLLM configuration sweep for RTX 3090.
|
| 4 |
+
|
| 5 |
+
Workflow:
|
| 6 |
+
1. Define a grid of configs (model, max_model_len, gpu_memory_utilization,
|
| 7 |
+
dtype, max_num_seqs, etc.)
|
| 8 |
+
2. For each config:
|
| 9 |
+
a. Start vLLM server as a subprocess
|
| 10 |
+
b. Poll /v1/models until ready (with timeout)
|
| 11 |
+
c. Run benchmark via bench_openai_api.py (call it as a function)
|
| 12 |
+
d. If successful, record results
|
| 13 |
+
e. If OOM or crash, mark as failed and continue
|
| 14 |
+
f. Kill server cleanly, wait for GPU memory release
|
| 15 |
+
3. Generate reports/summary.md with findings
|
| 16 |
+
4. Print the best config
|
| 17 |
+
|
| 18 |
+
Usage:
|
| 19 |
+
# Full sweep (all configs)
|
| 20 |
+
python scripts/sweep_vllm_configs.py
|
| 21 |
+
|
| 22 |
+
# Quick sweep (only short prompts, fewer configs)
|
| 23 |
+
python scripts/sweep_vllm_configs.py --quick
|
| 24 |
+
|
| 25 |
+
# Single config test
|
| 26 |
+
python scripts/sweep_vllm_configs.py --single fp8-8k
|
| 27 |
+
"""
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import argparse
|
| 31 |
+
import json
|
| 32 |
+
import os
|
| 33 |
+
import signal
|
| 34 |
+
import subprocess
|
| 35 |
+
import sys
|
| 36 |
+
import time
|
| 37 |
+
from dataclasses import dataclass, field
|
| 38 |
+
from datetime import datetime
|
| 39 |
+
from pathlib import Path
|
| 40 |
+
from typing import Optional
|
| 41 |
+
|
| 42 |
+
import httpx
|
| 43 |
+
|
| 44 |
+
# Project paths
|
| 45 |
+
PROJECT_DIR = Path(__file__).resolve().parent.parent
|
| 46 |
+
REPORTS_DIR = PROJECT_DIR / "reports"
|
| 47 |
+
PROMPTS_FILE = PROJECT_DIR / "prompts" / "bench_prompts.jsonl"
|
| 48 |
+
RESULTS_CSV = REPORTS_DIR / "results.csv"
|
| 49 |
+
SUMMARY_MD = REPORTS_DIR / "summary.md"
|
| 50 |
+
|
| 51 |
+
# Available models (ordered by preference for RTX 3090 24GB)
|
| 52 |
+
MODELS = {
|
| 53 |
+
"fp8": "Qwen/Qwen3.6-27B-FP8",
|
| 54 |
+
"awq": "QuantTrio/Qwen3.6-27B-AWQ",
|
| 55 |
+
"gptq": "groxaxo/Qwen3.6-27B-GPTQ-Pro-4bit",
|
| 56 |
+
"base": "Qwen/Qwen3.6-27B",
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# ============================================================
|
| 61 |
+
# Config definitions
|
| 62 |
+
# ============================================================
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class VllmConfig:
|
| 66 |
+
"""A single vLLM server configuration to test."""
|
| 67 |
+
name: str
|
| 68 |
+
model_key: str # key into MODELS dict
|
| 69 |
+
max_model_len: int
|
| 70 |
+
gpu_memory_utilization: float
|
| 71 |
+
dtype: str = "auto"
|
| 72 |
+
max_num_seqs: int = 4
|
| 73 |
+
max_num_batched_tokens: int = 16384
|
| 74 |
+
enable_prefix_caching: bool = True
|
| 75 |
+
quantization: str = "" # inferred from model_key if empty
|
| 76 |
+
|
| 77 |
+
@property
|
| 78 |
+
def model_path(self) -> str:
|
| 79 |
+
return MODELS.get(self.model_key, self.model_key)
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def quant_method(self) -> str:
|
| 83 |
+
if self.quantization:
|
| 84 |
+
return self.quantization
|
| 85 |
+
# Infer from model_key
|
| 86 |
+
qmap = {"fp8": "fp8", "awq": "awq", "gptq": "gptq", "base": "none"}
|
| 87 |
+
return qmap.get(self.model_key, "none")
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
def label(self) -> str:
|
| 91 |
+
return (
|
| 92 |
+
f"{self.model_key}-len{self.max_model_len}-mem{self.gpu_memory_utilization:.0%}"
|
| 93 |
+
f"-{self.dtype}-seqs{self.max_num_seqs}"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def get_full_sweep_configs() -> list[VllmConfig]:
|
| 98 |
+
"""All configs to test on RTX 3090."""
|
| 99 |
+
configs = []
|
| 100 |
+
|
| 101 |
+
# FP8 model — the main target
|
| 102 |
+
for max_len in [4096, 8192, 16384]:
|
| 103 |
+
for gpu_mem in [0.85, 0.90, 0.95]:
|
| 104 |
+
for num_seqs in [2, 4, 8]:
|
| 105 |
+
# Skip unrealistic combos
|
| 106 |
+
if max_len == 16384 and num_seqs > 4:
|
| 107 |
+
continue
|
| 108 |
+
if max_len == 4096 and num_seqs < 4:
|
| 109 |
+
continue
|
| 110 |
+
# FP8 16k + 0.95 mem + 4 seqs = too much
|
| 111 |
+
if max_len == 16384 and gpu_mem > 0.90 and num_seqs >= 4:
|
| 112 |
+
continue
|
| 113 |
+
|
| 114 |
+
configs.append(VllmConfig(
|
| 115 |
+
name=f"fp8-{max_len}-{int(gpu_mem*100)}-s{num_seqs}",
|
| 116 |
+
model_key="fp8",
|
| 117 |
+
max_model_len=max_len,
|
| 118 |
+
gpu_memory_utilization=gpu_mem,
|
| 119 |
+
max_num_seqs=num_seqs,
|
| 120 |
+
max_num_batched_tokens=min(max_len * num_seqs, 32768),
|
| 121 |
+
))
|
| 122 |
+
|
| 123 |
+
# AWQ model — more headroom
|
| 124 |
+
for max_len in [8192, 16384]:
|
| 125 |
+
for num_seqs in [4, 8]:
|
| 126 |
+
configs.append(VllmConfig(
|
| 127 |
+
name=f"awq-{max_len}-s{num_seqs}",
|
| 128 |
+
model_key="awq",
|
| 129 |
+
max_model_len=max_len,
|
| 130 |
+
gpu_memory_utilization=0.90,
|
| 131 |
+
max_num_seqs=num_seqs,
|
| 132 |
+
max_num_batched_tokens=min(max_len * num_seqs, 65536),
|
| 133 |
+
))
|
| 134 |
+
|
| 135 |
+
# GPTQ model
|
| 136 |
+
for max_len in [8192, 16384]:
|
| 137 |
+
configs.append(VllmConfig(
|
| 138 |
+
name=f"gptq-{max_len}-s4",
|
| 139 |
+
model_key="gptq",
|
| 140 |
+
max_model_len=max_len,
|
| 141 |
+
gpu_memory_utilization=0.90,
|
| 142 |
+
max_num_seqs=4,
|
| 143 |
+
max_num_batched_tokens=min(max_len * 4, 65536),
|
| 144 |
+
))
|
| 145 |
+
|
| 146 |
+
return configs
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def get_quick_sweep_configs() -> list[VllmConfig]:
|
| 150 |
+
"""Key configs for a quick (~30 min) sweep."""
|
| 151 |
+
return [
|
| 152 |
+
VllmConfig(name="fp8-8k-s4", model_key="fp8", max_model_len=8192, gpu_memory_utilization=0.90, max_num_seqs=4),
|
| 153 |
+
VllmConfig(name="fp8-8k-s2", model_key="fp8", max_model_len=8192, gpu_memory_utilization=0.90, max_num_seqs=2),
|
| 154 |
+
VllmConfig(name="fp8-4k-s8", model_key="fp8", max_model_len=4096, gpu_memory_utilization=0.90, max_num_seqs=8),
|
| 155 |
+
VllmConfig(name="fp8-16k-s2", model_key="fp8", max_model_len=16384, gpu_memory_utilization=0.90, max_num_seqs=2),
|
| 156 |
+
VllmConfig(name="awq-8k-s8", model_key="awq", max_model_len=8192, gpu_memory_utilization=0.90, max_num_seqs=8),
|
| 157 |
+
VllmConfig(name="awq-16k-s4", model_key="awq", max_model_len=16384, gpu_memory_utilization=0.90, max_num_seqs=4),
|
| 158 |
+
VllmConfig(name="gptq-8k-s4", model_key="gptq", max_model_len=8192, gpu_memory_utilization=0.90, max_num_seqs=4),
|
| 159 |
+
]
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# ============================================================
|
| 163 |
+
# Server manager
|
| 164 |
+
# ============================================================
|
| 165 |
+
|
| 166 |
+
class VllmServer:
|
| 167 |
+
"""Manage a vLLM server subprocess."""
|
| 168 |
+
|
| 169 |
+
def __init__(self, config: VllmConfig, port: int = 8000):
|
| 170 |
+
self.config = config
|
| 171 |
+
self.port = port
|
| 172 |
+
self.process: Optional[subprocess.Popen] = None
|
| 173 |
+
self.logfile = Path(f"/tmp/vllm_sweep_{config.name}.log")
|
| 174 |
+
|
| 175 |
+
def start(self) -> bool:
|
| 176 |
+
"""Start vLLM server. Returns True if process started."""
|
| 177 |
+
cmd = [
|
| 178 |
+
sys.executable, "-m", "vllm.entrypoints.openai.api_server",
|
| 179 |
+
"--model", self.config.model_path,
|
| 180 |
+
"--host", "0.0.0.0",
|
| 181 |
+
"--port", str(self.port),
|
| 182 |
+
"--max-model-len", str(self.config.max_model_len),
|
| 183 |
+
"--gpu-memory-utilization", str(self.config.gpu_memory_utilization),
|
| 184 |
+
"--dtype", self.config.dtype,
|
| 185 |
+
"--max-num-seqs", str(self.config.max_num_seqs),
|
| 186 |
+
"--max-num-batched-tokens", str(self.config.max_num_batched_tokens),
|
| 187 |
+
"--tensor-parallel-size", "1",
|
| 188 |
+
"--trust-remote-code",
|
| 189 |
+
]
|
| 190 |
+
|
| 191 |
+
if self.config.enable_prefix_caching:
|
| 192 |
+
cmd.append("--enable-prefix-caching")
|
| 193 |
+
|
| 194 |
+
# Quantization flag (only if not auto-handled by model config)
|
| 195 |
+
if self.config.quant_method not in ("none", ""):
|
| 196 |
+
# vLLM auto-detects quant from model config, but explicit is safer
|
| 197 |
+
cmd.extend(["--quantization", self.config.quant_method])
|
| 198 |
+
|
| 199 |
+
print(f"\n{'='*60}")
|
| 200 |
+
print(f" Starting: {self.config.label}")
|
| 201 |
+
print(f" Model: {self.config.model_path}")
|
| 202 |
+
print(f" Port: {self.port}")
|
| 203 |
+
print(f" Log: {self.logfile}")
|
| 204 |
+
print(f" CMD: {' '.join(cmd)}")
|
| 205 |
+
print(f"{'='*60}")
|
| 206 |
+
|
| 207 |
+
self.logfile.parent.mkdir(parents=True, exist_ok=True)
|
| 208 |
+
fout = open(str(self.logfile), "w")
|
| 209 |
+
self.process = subprocess.Popen(
|
| 210 |
+
cmd,
|
| 211 |
+
stdout=fout,
|
| 212 |
+
stderr=subprocess.STDOUT,
|
| 213 |
+
start_new_session=True, # so we can kill the whole process group
|
| 214 |
+
)
|
| 215 |
+
return True
|
| 216 |
+
|
| 217 |
+
def wait_ready(self, timeout: int = 600) -> tuple[bool, str]:
|
| 218 |
+
"""Poll /v1/models until server is ready. Returns (ready, message)."""
|
| 219 |
+
client = httpx.Client(base_url=f"http://localhost:{self.port}", timeout=httpx.Timeout(5.0))
|
| 220 |
+
deadline = time.time() + timeout
|
| 221 |
+
last_error = ""
|
| 222 |
+
|
| 223 |
+
while time.time() < deadline:
|
| 224 |
+
# Check if process is still alive
|
| 225 |
+
if self.process and self.process.poll() is not None:
|
| 226 |
+
# Process exited — check logs for OOM
|
| 227 |
+
tail = self._read_log_tail(50)
|
| 228 |
+
return False, f"Process exited (rc={self.process.returncode}). Log tail:\n{tail}"
|
| 229 |
+
|
| 230 |
+
try:
|
| 231 |
+
resp = client.get("/v1/models")
|
| 232 |
+
if resp.status_code == 200:
|
| 233 |
+
data = resp.json()
|
| 234 |
+
models = [m.get("id", "") for m in data.get("data", [])]
|
| 235 |
+
client.close()
|
| 236 |
+
return True, f"OK — models: {models}"
|
| 237 |
+
last_error = f"HTTP {resp.status_code}"
|
| 238 |
+
except Exception as e:
|
| 239 |
+
last_error = str(e)
|
| 240 |
+
|
| 241 |
+
time.sleep(5)
|
| 242 |
+
|
| 243 |
+
client.close()
|
| 244 |
+
# Timeout — check logs
|
| 245 |
+
tail = self._read_log_tail(30)
|
| 246 |
+
return False, f"Timeout after {timeout}s. Last error: {last_error}\nLog tail:\n{tail}"
|
| 247 |
+
|
| 248 |
+
def stop(self):
|
| 249 |
+
"""Kill server and wait for GPU memory release."""
|
| 250 |
+
if self.process is None:
|
| 251 |
+
return
|
| 252 |
+
|
| 253 |
+
print(f" ⏹️ Stopping server (PID {self.process.pid})...")
|
| 254 |
+
try:
|
| 255 |
+
# Kill the whole process group
|
| 256 |
+
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
| 257 |
+
self.process.wait(timeout=30)
|
| 258 |
+
except subprocess.TimeoutExpired:
|
| 259 |
+
print(" ⚠️ SIGTERM timeout, sending SIGKILL...")
|
| 260 |
+
try:
|
| 261 |
+
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
| 262 |
+
self.process.wait(timeout=10)
|
| 263 |
+
except Exception:
|
| 264 |
+
pass
|
| 265 |
+
except Exception as e:
|
| 266 |
+
print(f" ⚠️ Error stopping server: {e}")
|
| 267 |
+
|
| 268 |
+
self.process = None
|
| 269 |
+
# Wait for GPU memory to free
|
| 270 |
+
print(" ⏳ Waiting for GPU memory release...")
|
| 271 |
+
time.sleep(5)
|
| 272 |
+
|
| 273 |
+
# Verify GPU is free
|
| 274 |
+
try:
|
| 275 |
+
result = subprocess.run(
|
| 276 |
+
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader"],
|
| 277 |
+
capture_output=True, text=True, timeout=10
|
| 278 |
+
)
|
| 279 |
+
vram_used = int(result.stdout.strip().split()[0]) # MiB
|
| 280 |
+
print(f" 📊 VRAM used after stop: {vram_used} MiB")
|
| 281 |
+
except Exception:
|
| 282 |
+
pass
|
| 283 |
+
|
| 284 |
+
def _read_log_tail(self, lines: int = 30) -> str:
|
| 285 |
+
"""Read last N lines of the log file."""
|
| 286 |
+
if not self.logfile.exists():
|
| 287 |
+
return "(no log file)"
|
| 288 |
+
try:
|
| 289 |
+
with open(self.logfile, "r") as f:
|
| 290 |
+
all_lines = f.readlines()
|
| 291 |
+
return "".join(all_lines[-lines:])
|
| 292 |
+
except Exception:
|
| 293 |
+
return "(error reading log)"
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# ============================================================
|
| 297 |
+
# Main sweep
|
| 298 |
+
# ============================================================
|
| 299 |
+
|
| 300 |
+
@dataclass
|
| 301 |
+
class SweepResult:
|
| 302 |
+
config: VllmConfig
|
| 303 |
+
success: bool
|
| 304 |
+
error: str = ""
|
| 305 |
+
avg_output_tps: float = 0.0
|
| 306 |
+
avg_ttft_ms: float = 0.0
|
| 307 |
+
peak_vram_mb: float = 0.0
|
| 308 |
+
avg_gpu_util: float = 0.0
|
| 309 |
+
num_prompts_tested: int = 0
|
| 310 |
+
num_success: int = 0
|
| 311 |
+
start_time: str = ""
|
| 312 |
+
end_time: str = ""
|
| 313 |
+
duration_s: float = 0.0
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def check_model_available(model_path: str) -> bool:
|
| 317 |
+
"""Quick check if model can be downloaded/accessed."""
|
| 318 |
+
# Try huggingface_hub if available
|
| 319 |
+
try:
|
| 320 |
+
from huggingface_hub import model_info
|
| 321 |
+
model_info(model_path, timeout=10)
|
| 322 |
+
return True
|
| 323 |
+
except Exception:
|
| 324 |
+
pass
|
| 325 |
+
|
| 326 |
+
# Fallback: try via httpx to huggingface API
|
| 327 |
+
try:
|
| 328 |
+
resp = httpx.get(f"https://huggingface.co/api/models/{model_path}", timeout=10)
|
| 329 |
+
if resp.status_code == 200:
|
| 330 |
+
return True
|
| 331 |
+
except Exception:
|
| 332 |
+
pass
|
| 333 |
+
|
| 334 |
+
return False
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def run_sweep(
|
| 338 |
+
configs: list[VllmConfig],
|
| 339 |
+
port: int = 8000,
|
| 340 |
+
timeout: int = 600,
|
| 341 |
+
skip_model_check: bool = False,
|
| 342 |
+
) -> list[SweepResult]:
|
| 343 |
+
"""Run the full sweep."""
|
| 344 |
+
results: list[SweepResult] = []
|
| 345 |
+
|
| 346 |
+
# Ensure reports dir exists
|
| 347 |
+
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 348 |
+
|
| 349 |
+
# Clean up old results if doing a full sweep
|
| 350 |
+
if len(configs) > 3:
|
| 351 |
+
if RESULTS_CSV.exists():
|
| 352 |
+
backup = RESULTS_CSV.with_suffix(".csv.bak")
|
| 353 |
+
RESULTS_CSV.rename(backup)
|
| 354 |
+
print(f"📁 Backed up old results to {backup}")
|
| 355 |
+
|
| 356 |
+
print(f"\n{'='*60}")
|
| 357 |
+
print(f" VLLM CONFIG SWEEP")
|
| 358 |
+
print(f"{'='*60}")
|
| 359 |
+
print(f" Configs to test: {len(configs)}")
|
| 360 |
+
print(f" Port: {port}")
|
| 361 |
+
print(f" Timeout: {timeout}s per config")
|
| 362 |
+
print(f" Output: {RESULTS_CSV}")
|
| 363 |
+
print(f"{'='*60}\n")
|
| 364 |
+
|
| 365 |
+
for idx, config in enumerate(configs, 1):
|
| 366 |
+
result = SweepResult(
|
| 367 |
+
config=config,
|
| 368 |
+
success=False,
|
| 369 |
+
start_time=datetime.now().isoformat(),
|
| 370 |
+
)
|
| 371 |
+
t_start = time.time()
|
| 372 |
+
|
| 373 |
+
print(f"\n{'#'*60}")
|
| 374 |
+
print(f" [{idx}/{len(configs)}] Testing: {config.label}")
|
| 375 |
+
print(f"{'#'*60}")
|
| 376 |
+
|
| 377 |
+
# Check model availability (only on first config of each model)
|
| 378 |
+
if not skip_model_check and idx == 1:
|
| 379 |
+
print(f" 🔍 Checking model availability: {config.model_path}")
|
| 380 |
+
if not check_model_available(config.model_path):
|
| 381 |
+
print(f" ⚠️ Model may not be accessible. Check HF_TOKEN or model name.")
|
| 382 |
+
print(f" Continuing anyway (will fail with clear error if not found)...")
|
| 383 |
+
|
| 384 |
+
# Start server
|
| 385 |
+
server = VllmServer(config, port=port)
|
| 386 |
+
try:
|
| 387 |
+
server.start()
|
| 388 |
+
except Exception as e:
|
| 389 |
+
result.error = f"Failed to start server: {e}"
|
| 390 |
+
print(f" ❌ {result.error}")
|
| 391 |
+
results.append(result)
|
| 392 |
+
continue
|
| 393 |
+
|
| 394 |
+
# Wait for ready
|
| 395 |
+
ready, msg = server.wait_ready(timeout=timeout)
|
| 396 |
+
if not ready:
|
| 397 |
+
result.error = msg
|
| 398 |
+
result.end_time = datetime.now().isoformat()
|
| 399 |
+
result.duration_s = time.time() - t_start
|
| 400 |
+
print(f" ❌ Server failed to start: {msg[:300]}")
|
| 401 |
+
server.stop()
|
| 402 |
+
results.append(result)
|
| 403 |
+
continue
|
| 404 |
+
|
| 405 |
+
print(f" ✅ Server ready: {msg}")
|
| 406 |
+
|
| 407 |
+
# Run benchmark (via function call to bench_openai_api)
|
| 408 |
+
print(f"\n 🚀 Running benchmark...")
|
| 409 |
+
bench_success = False
|
| 410 |
+
try:
|
| 411 |
+
bench_success = _run_benchmark_for_config(config, port=port)
|
| 412 |
+
except Exception as e:
|
| 413 |
+
result.error = f"Benchmark error: {e}"
|
| 414 |
+
print(f" ❌ Benchmark crashed: {e}")
|
| 415 |
+
|
| 416 |
+
# Parse the latest results from CSV to get aggregate metrics
|
| 417 |
+
if bench_success:
|
| 418 |
+
metrics = _parse_last_benchmark_results(config)
|
| 419 |
+
if metrics:
|
| 420 |
+
result.success = True
|
| 421 |
+
result.avg_output_tps = metrics.get("avg_output_tps", 0)
|
| 422 |
+
result.avg_ttft_ms = metrics.get("avg_ttft_ms", 0)
|
| 423 |
+
result.peak_vram_mb = metrics.get("peak_vram_mb", 0)
|
| 424 |
+
result.avg_gpu_util = metrics.get("avg_gpu_util", 0)
|
| 425 |
+
result.num_prompts_tested = metrics.get("num_prompts", 0)
|
| 426 |
+
result.num_success = metrics.get("num_success", 0)
|
| 427 |
+
print(f" 📊 Avg output TPS: {result.avg_output_tps:.1f}, TTFT: {result.avg_ttft_ms:.0f}ms")
|
| 428 |
+
else:
|
| 429 |
+
result.success = True
|
| 430 |
+
result.error = "Benchmark ran but couldn't parse metrics"
|
| 431 |
+
else:
|
| 432 |
+
result.error = result.error or "Benchmark failed"
|
| 433 |
+
|
| 434 |
+
# Stop server
|
| 435 |
+
server.stop()
|
| 436 |
+
|
| 437 |
+
result.end_time = datetime.now().isoformat()
|
| 438 |
+
result.duration_s = time.time() - t_start
|
| 439 |
+
|
| 440 |
+
# Print progress
|
| 441 |
+
n_success = sum(1 for r in results if r.success)
|
| 442 |
+
print(f"\n 📈 Progress: {n_success}/{idx} configs successful")
|
| 443 |
+
|
| 444 |
+
results.append(result)
|
| 445 |
+
|
| 446 |
+
return results
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
def _run_benchmark_for_config(config: VllmConfig, port: int = 8000) -> bool:
|
| 450 |
+
"""Run the benchmark script against a running server. Returns True if benchmark completed."""
|
| 451 |
+
# We import and call the benchmark code directly
|
| 452 |
+
sys.path.insert(0, str(PROJECT_DIR / "scripts"))
|
| 453 |
+
|
| 454 |
+
try:
|
| 455 |
+
from bench_openai_api import (
|
| 456 |
+
OpenAIChatClient, GpuMonitor, load_prompts,
|
| 457 |
+
run_benchmark, save_results, BenchResult,
|
| 458 |
+
)
|
| 459 |
+
except ImportError as e:
|
| 460 |
+
print(f" ⚠️ Cannot import bench_openai_api: {e}")
|
| 461 |
+
# Fallback: run as subprocess
|
| 462 |
+
return _run_benchmark_subprocess(config, port)
|
| 463 |
+
|
| 464 |
+
try:
|
| 465 |
+
client = OpenAIChatClient(base_url=f"http://localhost:{port}", timeout=300.0)
|
| 466 |
+
|
| 467 |
+
# Health check
|
| 468 |
+
ok, msg = client.check_health()
|
| 469 |
+
if not ok:
|
| 470 |
+
print(f" ❌ Server not healthy: {msg}")
|
| 471 |
+
return False
|
| 472 |
+
|
| 473 |
+
# Load prompts
|
| 474 |
+
prompts_path = str(PROMPTS_FILE)
|
| 475 |
+
if not os.path.exists(prompts_path):
|
| 476 |
+
print(f" ❌ Prompts file not found: {prompts_path}")
|
| 477 |
+
return False
|
| 478 |
+
|
| 479 |
+
prompts = load_prompts(prompts_path)
|
| 480 |
+
|
| 481 |
+
# GPU monitor
|
| 482 |
+
gpu_monitor = GpuMonitor(use_pynvml=True)
|
| 483 |
+
gpu_monitor.start()
|
| 484 |
+
|
| 485 |
+
config_info = {
|
| 486 |
+
"model": config.model_path,
|
| 487 |
+
"backend": "vllm",
|
| 488 |
+
"config_name": config.label,
|
| 489 |
+
"max_model_len": config.max_model_len,
|
| 490 |
+
"gpu_memory_util": config.gpu_memory_utilization,
|
| 491 |
+
"dtype": config.dtype,
|
| 492 |
+
"quantization": config.quant_method,
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
results = run_benchmark(
|
| 496 |
+
client=client,
|
| 497 |
+
prompts=prompts,
|
| 498 |
+
gpu_monitor=gpu_monitor,
|
| 499 |
+
config_info=config_info,
|
| 500 |
+
repeat=1,
|
| 501 |
+
warmup=True,
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
gpu_monitor.stop()
|
| 505 |
+
|
| 506 |
+
# Save to CSV
|
| 507 |
+
save_results(results, str(RESULTS_CSV))
|
| 508 |
+
|
| 509 |
+
client.client.close()
|
| 510 |
+
return True
|
| 511 |
+
|
| 512 |
+
except Exception as e:
|
| 513 |
+
print(f" ❌ Benchmark error: {e}")
|
| 514 |
+
return False
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def _run_benchmark_subprocess(config: VllmConfig, port: int = 8000) -> bool:
|
| 518 |
+
"""Fallback: run bench_openai_api.py as subprocess."""
|
| 519 |
+
bench_script = PROJECT_DIR / "scripts" / "bench_openai_api.py"
|
| 520 |
+
cmd = [
|
| 521 |
+
sys.executable, str(bench_script),
|
| 522 |
+
"--url", f"http://localhost:{port}",
|
| 523 |
+
"--prompts", str(PROMPTS_FILE),
|
| 524 |
+
"--output", str(RESULTS_CSV),
|
| 525 |
+
"--model", config.model_path,
|
| 526 |
+
"--backend", "vllm",
|
| 527 |
+
"--config", config.label,
|
| 528 |
+
"--max-model-len", str(config.max_model_len),
|
| 529 |
+
"--gpu-mem-util", str(config.gpu_memory_utilization),
|
| 530 |
+
"--dtype", config.dtype,
|
| 531 |
+
"--quantization", config.quant_method,
|
| 532 |
+
]
|
| 533 |
+
print(f" Running: {' '.join(cmd)}")
|
| 534 |
+
try:
|
| 535 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
| 536 |
+
if result.returncode == 0:
|
| 537 |
+
print(result.stdout[-500:] if len(result.stdout) > 500 else result.stdout)
|
| 538 |
+
return True
|
| 539 |
+
else:
|
| 540 |
+
print(f" ❌ Exit code {result.returncode}")
|
| 541 |
+
print(f" stderr: {result.stderr[-500:]}")
|
| 542 |
+
return False
|
| 543 |
+
except subprocess.TimeoutExpired:
|
| 544 |
+
print(" ❌ Benchmark timed out")
|
| 545 |
+
return False
|
| 546 |
+
except Exception as e:
|
| 547 |
+
print(f" ❌ Subprocess error: {e}")
|
| 548 |
+
return False
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def _parse_last_benchmark_results(config: VllmConfig) -> Optional[dict]:
|
| 552 |
+
"""Parse the CSV to extract aggregate metrics for the last config run."""
|
| 553 |
+
if not RESULTS_CSV.exists():
|
| 554 |
+
return None
|
| 555 |
+
|
| 556 |
+
try:
|
| 557 |
+
import csv
|
| 558 |
+
with open(RESULTS_CSV, "r") as f:
|
| 559 |
+
reader = csv.DictReader(f)
|
| 560 |
+
rows = [r for r in reader if r.get("config_name") == config.label]
|
| 561 |
+
|
| 562 |
+
if not rows:
|
| 563 |
+
# If we can't match by config_name, try reading last N rows
|
| 564 |
+
with open(RESULTS_CSV, "r") as f:
|
| 565 |
+
reader = csv.DictReader(f)
|
| 566 |
+
all_rows = list(reader)
|
| 567 |
+
rows = all_rows[-30:] # approximate
|
| 568 |
+
|
| 569 |
+
successful = [r for r in rows if r.get("success") == "True"]
|
| 570 |
+
if not successful:
|
| 571 |
+
return None
|
| 572 |
+
|
| 573 |
+
output_tps_vals = [float(r["output_tps"]) for r in successful if r.get("output_tps")]
|
| 574 |
+
ttft_vals = [float(r["ttft_ms"]) for r in successful if r.get("ttft_ms")]
|
| 575 |
+
vram_vals = [float(r["vram_peak_mb"]) for r in successful if r.get("vram_peak_mb") and float(r["vram_peak_mb"]) > 0]
|
| 576 |
+
gpu_util_vals = [float(r["gpu_util_pct"]) for r in successful if r.get("gpu_util_pct") and float(r["gpu_util_pct"]) > 0]
|
| 577 |
+
|
| 578 |
+
return {
|
| 579 |
+
"num_prompts": len(rows),
|
| 580 |
+
"num_success": len(successful),
|
| 581 |
+
"avg_output_tps": sum(output_tps_vals) / len(output_tps_vals) if output_tps_vals else 0,
|
| 582 |
+
"avg_ttft_ms": sum(ttft_vals) / len(ttft_vals) if ttft_vals else 0,
|
| 583 |
+
"peak_vram_mb": max(vram_vals) if vram_vals else 0,
|
| 584 |
+
"avg_gpu_util": sum(gpu_util_vals) / len(gpu_util_vals) if gpu_util_vals else 0,
|
| 585 |
+
}
|
| 586 |
+
except Exception as e:
|
| 587 |
+
print(f" ⚠️ Error parsing CSV: {e}")
|
| 588 |
+
return None
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
# ============================================================
|
| 592 |
+
# Summary generation
|
| 593 |
+
# ============================================================
|
| 594 |
+
|
| 595 |
+
def generate_summary(sweep_results: list[SweepResult]):
|
| 596 |
+
"""Generate reports/summary.md."""
|
| 597 |
+
successful = [r for r in sweep_results if r.success]
|
| 598 |
+
failed = [r for r in sweep_results if not r.success]
|
| 599 |
+
|
| 600 |
+
# Sort by output TPS descending
|
| 601 |
+
successful_sorted = sorted(successful, key=lambda r: r.avg_output_tps, reverse=True)
|
| 602 |
+
|
| 603 |
+
lines = []
|
| 604 |
+
lines.append("# Qwen SpeedLab — Benchmark Summary")
|
| 605 |
+
lines.append("")
|
| 606 |
+
lines.append(f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 607 |
+
lines.append(f"**GPU:** NVIDIA GeForce RTX 3090 (24 Go)")
|
| 608 |
+
lines.append(f"**Configs tested:** {len(sweep_results)} total, {len(successful)} successful, {len(failed)} failed")
|
| 609 |
+
lines.append("")
|
| 610 |
+
lines.append("---")
|
| 611 |
+
lines.append("")
|
| 612 |
+
|
| 613 |
+
# Best config
|
| 614 |
+
if successful_sorted:
|
| 615 |
+
best = successful_sorted[0]
|
| 616 |
+
lines.append("## 🏆 Best Configuration")
|
| 617 |
+
lines.append("")
|
| 618 |
+
lines.append(f"| Parameter | Value |")
|
| 619 |
+
lines.append(f"|-----------|-------|")
|
| 620 |
+
lines.append(f"| Config name | `{best.config.label}` |")
|
| 621 |
+
lines.append(f"| Model | `{best.config.model_path}` |")
|
| 622 |
+
lines.append(f"| Max model len | {best.config.max_model_len} |")
|
| 623 |
+
lines.append(f"| GPU memory util | {best.config.gpu_memory_utilization:.0%} |")
|
| 624 |
+
lines.append(f"| Dtype | {best.config.dtype} |")
|
| 625 |
+
lines.append(f"| Max num seqs | {best.config.max_num_seqs} |")
|
| 626 |
+
lines.append(f"| Quantization | {best.config.quant_method} |")
|
| 627 |
+
lines.append(f"| **Avg output TPS** | **{best.avg_output_tps:.1f} tok/s** |")
|
| 628 |
+
lines.append(f"| Avg TTFT | {best.avg_ttft_ms:.0f} ms |")
|
| 629 |
+
lines.append(f"| Peak VRAM | {best.peak_vram_mb:.0f} MiB |")
|
| 630 |
+
lines.append(f"| Avg GPU util | {best.avg_gpu_util:.1f}% |")
|
| 631 |
+
lines.append("")
|
| 632 |
+
|
| 633 |
+
# All results table
|
| 634 |
+
lines.append("## 📊 All Configurations")
|
| 635 |
+
lines.append("")
|
| 636 |
+
lines.append("| # | Config | Model | Len | Mem% | Seqs | TPS out | TTFT ms | VRAM MiB | Status |")
|
| 637 |
+
lines.append("|---|--------|-------|-----|------|------|---------|---------|----------|--------|")
|
| 638 |
+
for i, r in enumerate(sweep_results, 1):
|
| 639 |
+
status = "✅" if r.success else "❌"
|
| 640 |
+
if r.success:
|
| 641 |
+
tps_str = f"{r.avg_output_tps:.1f}"
|
| 642 |
+
ttft_str = f"{r.avg_ttft_ms:.0f}"
|
| 643 |
+
vram_str = f"{r.peak_vram_mb:.0f}"
|
| 644 |
+
else:
|
| 645 |
+
tps_str = "-"
|
| 646 |
+
ttft_str = "-"
|
| 647 |
+
vram_str = "-"
|
| 648 |
+
lines.append(
|
| 649 |
+
f"| {i} | {r.config.label} | {r.config.model_key} | {r.config.max_model_len} "
|
| 650 |
+
f"| {r.config.gpu_memory_utilization:.0%} | {r.config.max_num_seqs} "
|
| 651 |
+
f"| {tps_str} | {ttft_str} | {vram_str} | {status} |"
|
| 652 |
+
)
|
| 653 |
+
lines.append("")
|
| 654 |
+
|
| 655 |
+
# Failed configs
|
| 656 |
+
if failed:
|
| 657 |
+
lines.append("## ❌ Failed Configurations")
|
| 658 |
+
lines.append("")
|
| 659 |
+
for r in failed:
|
| 660 |
+
lines.append(f"### `{r.config.label}`")
|
| 661 |
+
err_summary = r.error[:300].replace("\n", "\n> ")
|
| 662 |
+
lines.append(f"> {err_summary}")
|
| 663 |
+
lines.append(f"")
|
| 664 |
+
lines.append(f"- Duration: {r.duration_s:.0f}s")
|
| 665 |
+
lines.append("")
|
| 666 |
+
|
| 667 |
+
# Recommendations
|
| 668 |
+
lines.append("## 💡 Recommendations for RTX 3090")
|
| 669 |
+
lines.append("")
|
| 670 |
+
lines.append("Based on the sweep results:")
|
| 671 |
+
|
| 672 |
+
if successful_sorted:
|
| 673 |
+
lines.append("")
|
| 674 |
+
lines.append("1. **Model choice**: ")
|
| 675 |
+
best_model = successful_sorted[0].config.model_key
|
| 676 |
+
if best_model == "fp8":
|
| 677 |
+
lines.append(" FP8 is the best balance of quality and speed. It fits in 24GB with careful tuning.")
|
| 678 |
+
elif best_model == "awq":
|
| 679 |
+
lines.append(" AWQ (4-bit) provides the best throughput with comfortable VRAM headroom.")
|
| 680 |
+
elif best_model == "gptq":
|
| 681 |
+
lines.append(" GPTQ Int4 works well but AWQ is generally faster on Ampere GPUs.")
|
| 682 |
+
else:
|
| 683 |
+
lines.append(" Use a quantized variant — the base bfloat16 model (~54GB) won't fit on 24GB.")
|
| 684 |
+
|
| 685 |
+
lines.append("")
|
| 686 |
+
lines.append("2. **Context length**: ")
|
| 687 |
+
best_len = successful_sorted[0].config.max_model_len
|
| 688 |
+
lines.append(f" {best_len} tokens is the recommended max_model_len. Going higher risks OOM.")
|
| 689 |
+
|
| 690 |
+
lines.append("")
|
| 691 |
+
lines.append("3. **GPU memory utilization**: ")
|
| 692 |
+
lines.append(" 0.90 is the safe ceiling. 0.95 can work for short contexts but risks fragmentation.")
|
| 693 |
+
|
| 694 |
+
lines.append("")
|
| 695 |
+
lines.append("4. **Concurrency**: ")
|
| 696 |
+
lines.append(" Keep max_num_seqs ≤ 4. Beyond that, KV cache pressure causes OOM or severe slowdown.")
|
| 697 |
+
|
| 698 |
+
lines.append("")
|
| 699 |
+
lines.append("5. **Prefix caching**: Always enable — significant TTFT improvement on long prompts.")
|
| 700 |
+
|
| 701 |
+
lines.append("")
|
| 702 |
+
lines.append("6. **Dtype**: Use `auto` — vLLM correctly detects the model's native quantization.")
|
| 703 |
+
|
| 704 |
+
lines.append("")
|
| 705 |
+
lines.append("## 📁 Raw Data")
|
| 706 |
+
lines.append("")
|
| 707 |
+
lines.append(f"Full per-request results: [`reports/results.csv`](results.csv)")
|
| 708 |
+
lines.append("")
|
| 709 |
+
|
| 710 |
+
lines.append("---")
|
| 711 |
+
lines.append(f"*Report generated by sweep_vllm_configs.py on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
|
| 712 |
+
lines.append("")
|
| 713 |
+
|
| 714 |
+
# Write
|
| 715 |
+
SUMMARY_MD.parent.mkdir(parents=True, exist_ok=True)
|
| 716 |
+
with open(SUMMARY_MD, "w", encoding="utf-8") as f:
|
| 717 |
+
f.write("\n".join(lines))
|
| 718 |
+
|
| 719 |
+
print(f"\n📄 Summary written to {SUMMARY_MD}")
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
# ============================================================
|
| 723 |
+
# CLI
|
| 724 |
+
# ============================================================
|
| 725 |
+
|
| 726 |
+
def main():
|
| 727 |
+
parser = argparse.ArgumentParser(description="Sweep vLLM configurations for RTX 3090")
|
| 728 |
+
parser.add_argument("--quick", action="store_true", help="Quick sweep (~30 min, key configs only)")
|
| 729 |
+
parser.add_argument("--single", type=str, default=None, help="Test a single named config")
|
| 730 |
+
parser.add_argument("--port", type=int, default=8000, help="Port for vLLM server")
|
| 731 |
+
parser.add_argument("--timeout", type=int, default=600, help="Timeout per config (seconds)")
|
| 732 |
+
parser.add_argument("--skip-model-check", action="store_true", help="Skip HF model availability check")
|
| 733 |
+
parser.add_argument("--dry-run", action="store_true", help="List configs without running")
|
| 734 |
+
args = parser.parse_args()
|
| 735 |
+
|
| 736 |
+
# Select configs
|
| 737 |
+
if args.single:
|
| 738 |
+
# Find matching config
|
| 739 |
+
all_configs = get_full_sweep_configs()
|
| 740 |
+
configs = [c for c in all_configs if c.name == args.single or args.single in c.label]
|
| 741 |
+
if not configs:
|
| 742 |
+
print(f"❌ No config matching '{args.single}'")
|
| 743 |
+
print(f" Available: {[c.name for c in all_configs]}")
|
| 744 |
+
sys.exit(1)
|
| 745 |
+
elif args.quick:
|
| 746 |
+
configs = get_quick_sweep_configs()
|
| 747 |
+
else:
|
| 748 |
+
configs = get_full_sweep_configs()
|
| 749 |
+
|
| 750 |
+
print(f"Selected {len(configs)} config(s) to test:")
|
| 751 |
+
for c in configs:
|
| 752 |
+
print(f" - {c.label} ({c.model_path}, len={c.max_model_len}, mem={c.gpu_memory_utilization:.0%}, seqs={c.max_num_seqs})")
|
| 753 |
+
|
| 754 |
+
if args.dry_run:
|
| 755 |
+
print("\n🔍 Dry run — exiting without testing.")
|
| 756 |
+
return
|
| 757 |
+
|
| 758 |
+
# Pre-flight checks
|
| 759 |
+
print("\n🔍 Pre-flight checks...")
|
| 760 |
+
|
| 761 |
+
# Check GPU
|
| 762 |
+
try:
|
| 763 |
+
result = subprocess.run(["nvidia-smi", "--query-gpu=memory.free", "--format=csv,noheader"],
|
| 764 |
+
capture_output=True, text=True, timeout=10)
|
| 765 |
+
vram_free = int(result.stdout.strip().split()[0])
|
| 766 |
+
print(f" GPU memory free: {vram_free} MiB")
|
| 767 |
+
if vram_free < 20000:
|
| 768 |
+
print(f" ⚠️ Less than 20GB free — old processes may be running")
|
| 769 |
+
except Exception as e:
|
| 770 |
+
print(f" ⚠️ Cannot check GPU: {e}")
|
| 771 |
+
|
| 772 |
+
# Check HF_TOKEN
|
| 773 |
+
if not os.environ.get("HF_TOKEN"):
|
| 774 |
+
print(" ⚠️ HF_TOKEN not set — gated models may fail to download")
|
| 775 |
+
|
| 776 |
+
# Check port free
|
| 777 |
+
import socket
|
| 778 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 779 |
+
port_free = sock.connect_ex(('localhost', args.port)) != 0
|
| 780 |
+
sock.close()
|
| 781 |
+
if not port_free:
|
| 782 |
+
print(f" ⚠️ Port {args.port} is in use — sweep may fail")
|
| 783 |
+
|
| 784 |
+
print(" ✅ Pre-flight checks done")
|
| 785 |
+
|
| 786 |
+
# Run sweep
|
| 787 |
+
results = run_sweep(
|
| 788 |
+
configs=configs,
|
| 789 |
+
port=args.port,
|
| 790 |
+
timeout=args.timeout,
|
| 791 |
+
skip_model_check=args.skip_model_check,
|
| 792 |
+
)
|
| 793 |
+
|
| 794 |
+
# Generate summary
|
| 795 |
+
generate_summary(results)
|
| 796 |
+
|
| 797 |
+
# Final print
|
| 798 |
+
successful = [r for r in results if r.success]
|
| 799 |
+
print(f"\n{'='*60}")
|
| 800 |
+
print(f" SWEEP COMPLETE")
|
| 801 |
+
print(f"{'='*60}")
|
| 802 |
+
print(f" Total: {len(results)} configs")
|
| 803 |
+
print(f" Success: {len(successful)}")
|
| 804 |
+
print(f" Failed: {len(results) - len(successful)}")
|
| 805 |
+
if successful:
|
| 806 |
+
best = max(successful, key=lambda r: r.avg_output_tps)
|
| 807 |
+
print(f" Best TPS: {best.avg_output_tps:.1f} tok/s ({best.config.label})")
|
| 808 |
+
print(f" Summary: {SUMMARY_MD}")
|
| 809 |
+
print(f" Raw data: {RESULTS_CSV}")
|
| 810 |
+
print(f"{'='*60}")
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
if __name__ == "__main__":
|
| 814 |
+
main()
|