simonlesaumon commited on
Commit
3cabd3a
·
verified ·
1 Parent(s): e20fa25

Upload folder using huggingface_hub

Browse files
.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()