patdev/k3-a40-bootstrap
01.3k
1# Ornith-1.5-35B-A3B sur 2x A40 — mesures du 22/08/2026
2
3Pod RunPod `slw5uz06ucdm51` (2x A40, 0,88 $/h), image precompilee
4`registry.hf.space/patdev-ornith-vllm-a40` (v2), bootstrap v39, vLLM 0.27.1,
5contexte 1M (YaRN x4), `ulkaa/Ornith-1.5-35B-A3B-AWQ-INT4`. Banc :
6`pod_bench.py` via le pont Anthropic (`/v1/messages`, SSE), 256 jetons de
7sortie, prompt different par session (le cache de prefixe ne joue pas).
8
9## Base : KV bf16, sans speculation
10
11| Sessions | j/s agreges | par session | OK |
12|---:|---:|---:|---:|
13| 1 | 100,2 | 100,2 | 1/1 |
14| 4 | 320,7 | 80,2 | 4/4 |
15| 8 | 580,5 | 72,6 | 8/8 |
16| 16 | 927,0 | 57,9 | 16/16 |
17| **32** | **1 379,8** | 43,1 | 32/32 |
18
19KV alloue : 2 785 939 jetons (`--disable-custom-all-reduce` en coute ~37 k).
20**1 000 j/s agreges sont depasses des 20 sessions** sur le materiel reel.
21
22## TurboQuant k3v4 (`--kv-cache-dtype turboquant_k3v4_nc`)
23
24| Sessions | bf16 | TurboQuant | ecart |
25|---:|---:|---:|---:|
26| 1 | 100,2 | 107,0 / 113,7 (75,8 au 1er appel : autotune Triton) | +7 a +13 % |
27| 2 | -- | 190,6 | |
28| 4 | 320,7 | 322,1 | 0 % |
29| 8 | 580,5 | 537,2 | -7 % |
30| 16 | 927,0 | 904,4 | -2 % |
31| 32 | 1 379,8 | 1 302,6 | -6 % |
32| **KV alloue** | 2 785 939 | **12 076 923** | **x4,33** |
33
34Seules les 10 couches d'attention pleine sont quantifiees (KVQuantMode vit sur
35AttentionSpec, pas MambaSpec) ; les etats DeltaNet restent intacts. Le debit
36est quasi inchange ; le cache est multiplie par 4,33 : **douze sessions a 1M
37de contexte simultanees sur deux A40**, ou le 1M entier sur UNE A40.
38Premier appel apres (re)demarrage lent (autotune) : prevoir un appel de rodage.
39
40## DSpark + TurboQuant : incompatibles dans vLLM 0.27.1 (sm86)
41
42Avec `kv_cache_dtype=turboquant_k3v4_nc` ET `speculative_config.method=dspark`
43(brouillon `RedHatAI/Qwen3.6-35B-A3B-speculator.dspark`), l'initialisation
44meurt dans `load_dspark_model` : `ValueError: No valid attention backend found
45for cuda with AttentionSelectorConfig(head_size=256, dtype=bf16,
46kv_cache_dtype=turboquant_k3v4_nc, ...)`. Le brouillon DSpark exige un backend
47d'attention que le KV TurboQuant n'a pas sur cette carte -- meme conflit que
48fp8+DFlash note dans DEPLOY.md. Sur A40 il faut choisir : **TurboQuant (x4,33
49de KV) ou DSpark (spéculation)**, pas les deux. Mesure DSpark seul ci-dessous.
50
51## DSpark seul (`RedHatAI/Qwen3.6-35B-A3B-speculator.dspark`, 8 jetons proposes, KV bf16)
52
53| Sessions | bf16 sans spec | **DSpark** | ecart |
54|---:|---:|---:|---:|
55| 1 | 100,2 | **171,5** (96,0 au 1er appel : rodage) | **+71 %** |
56| 4 | 320,7 | 425,2 | +33 % |
57| 8 | 580,5 | 710,9 | +22 % |
58| 16 | 927,0 | 995,7 | +7 % |
59| 32 | 1 379,8 | 1 359,7 | -1 % |
60| KV alloue | 2 785 939 | 2 614 156 | -6 % |
61
62Telemetrie vLLM (`SpecDecoding metrics`) : longueur acceptee moyenne **3,3-3,5**,
63acceptation par position 0,76 / 0,54 / 0,38 / 0,27 / 0,18 / 0,13 / 0,08 / 0,04,
64taux global ~29-31 %. Le brouillon etait entraine contre Qwen3.6-35B-A3B, pas
65Ornith : la longueur acceptee tient (3,57 publie sur sa cible), le taux global
66est plus bas parce que les positions 5-8 n'acceptent presque rien — ce qui ne
67coute rien grace a la tete de confiance, qui coupe la proposition tot.
68
69C'est exactement le comportement que le bareme ngram codé à la main n'avait pas
70su produire : **gros gain en solo, aucune penalite a 32 sessions**. La
71verification adaptative (`enable_confidence_head`) fait le travail toute seule.
72
73## Leviers de la doc vLLM (v43d) et banc officiel `vllm bench serve`
74
75`--numa-bind --numa-bind-nodes 0 1` (membind refuse par le conteneur, faute de
76SYS_NICE ; cpunodebind applique) + `--enable-expert-parallel` + TurboQuant :
7799 / 308 / 551 / 880 / **1 291** a 1/4/8/16/32 sessions, KV 12 122 171. Face a
78TurboQuant seul (107-114 / 322 / 537 / 904 / 1 303) : **neutre**, dans le bruit.
79`--optimization-level 3` = O2 (defaut) dans les sources. `VLLM_USE_FASTOKENS`
80exige le paquet fastokens, absent de l'image v2 (ajoute dans la v3, non mesure).
81
82Banc OFFICIEL (`vllm bench serve`, dataset aleatoire **1024 jetons d'entree /
83256 de sortie**, direct sur vLLM, config v43d) :
84
85| Concurrence | Debit sortie (j/s) | TTFT median | TPOT median | ITL median |
86|---:|---:|---:|---:|---:|
87| 1 | 100,8 | 135 ms | 9,2 ms | 9,3 ms |
88| 8 | 340,4 | 691 ms (P99 7,2 s) | 14,6 ms | 14,5 ms |
89| 32 | 875,7 | 1,67 s (P99 2,7 s) | 29,4 ms | 26,3 ms |
90
91Plus bas que pod_bench.py a 8/32 sessions parce que chaque requete porte ici
921 024 jetons de prefill qui partagent le GPU avec le decodage -- c'est la forme
93reelle d'une session Claude Code a prompt long. Le solo (100,8 j/s, 9,2 ms par
94jeton) confirme le banc maison au jeton pres.
95
96## v45 : fastokens et `--kv-cache-memory-bytes`
97
98| | v44 (reference) | v45 |
99|---|---:|---:|
100| Demarrage (args -> pret) | ~3 min 04 | ~4 min 40 (dont ~1 min d'installation de fastokens a la volee) |
101| Profilage memoire | oui | saute (`reserved 26.5 GiB`) — mais le warmup de 65 s reste : **aucun gain visible** |
102| KV | 12 081 447 | 11 977 375 (marge de 0,22 GiB) |
103| Banc officiel solo : debit / TTFT median / TPOT | 100,8 j/s / 135 ms / 9,2 ms | 102,3 j/s / **119 ms** / 8,9 ms |
104
105fastokens : -12 % de TTFT median, +1,5 % de debit solo — petit mais positif,
106garde par defaut (l'image v3 l'embarque, plus d'installation au boot).
107`--kv-cache-memory-bytes` : retire (v46) — pas de gain, et une valeur fausse
108(autre modele, brouillon DSpark) fait mourir le moteur.
109
110## TensorRT-LLM : mesure, disqualifie
111
112Voir RESULTS_QWEN38.md : les deux INT4 communautaires sont refuses par le
113lecteur de config de TRT-LLM 1.3.0rc24 ; le FP8 officiel charge sur L40S mais
114genere du bruit (`" entraî match[conguàmh correctnessness…"`). Aucun chiffre
115de debit TRT-LLM n'est valide pour cette architecture a ce jour.
116
117## Qwen3.8-27B sur la meme machine, et la decision
118
119| Sessions | Qwen3.8-27B INT4 + MTP k=3 | Ornith base | Ornith DSpark | Ornith TurboQuant |
120|---:|---:|---:|---:|---:|
121| 1 | 79 | 100 | **171** | 107-114 |
122| 8 | 410 | 581 | 711 | 537 |
123| 32 | 727 | **1 380** | 1 360 | 1 303 |
124| KV | 0,79 M | 2,79 M | 2,61 M | **12,08 M** |
125
126Le dense 27B est compute-bound sur Ampere : ses 999 j/s de la L40S venaient du
127calcul Ada. Sur A40, Ornith fait 1,9x l'agrege, 2,2x le solo (DSpark), 3,5x le
128KV, meme famille architecturale et meme recette de quantification.
129
130**Configuration retenue (bootstrap v41)** : Ornith, `VL_KV=turboquant_k3v4_nc`,
131`VL_SPEC=off` — douze sessions a 1M simultanees, debit quasi inchange. Pour une
132session unique rapide : `VL_KV=` + `VL_SPEC=dspark` (171 j/s solo). Les deux
133ne se combinent pas sur sm86. Le changement se fait en editant le bloc
134EXPERIENCE du bootstrap et en republiant : le pod recharge a chaud en ~4 min,
135sans retelecharger les poids.
136
137## Ce qu'il a fallu regler avant de mesurer (deux hotes, une soiree)
138
1391. `start` d'un pod arrete refuse ("not enough free GPUs on the host") →
140 recreation depuis l'image precompilee (15 s de bootstrap au lieu de 8 min,
141 mais ~10 min de pull la premiere fois).
1422. **Spin NCCL apres la capture des graphes CUDA** : GPU a 100 % SM / 0 %
143 memoire, workers en futex, "No available shared memory broadcast block"
144 toutes les 60 s. Les deux A40 sont sur deux noeuds NUMA (`topo` = SYS).
145 `export NCCL_P2P_DISABLE=1` ne suffit PAS : la variable est dans
146 l'environnement de `vllm serve` mais **absente de celui des workers**
147 (verifie dans `/proc/<pid>/environ`). Ce qui marche : `~/.nccl.conf` (lu
148 par NCCL a l'init, independant de l'env) + `--disable-custom-all-reduce`.
1493. Premier hote : la premiere generation a tue un worker (`CUDA error:
150 unspecified launch failure`) puis un GPU est devenu invisible pour CUDA et
151 `restart` expirait cote RunPod → pod supprime, recree ailleurs.
152
153## ONNX Runtime GenAI : export reussi, graphe invalide
154
155Export INT4 + QMoE sur HF Jobs cpu-xl (18,9 Go, ~25 min, pousse sur
156`patdev/Ornith-1.5-35B-A3B-ONNX-INT4`). Au chargement sur L40S :
157`Node (/model/layers.0/moe/Add) Op (Add) [ShapeInferenceError] Incompatible
158dimensions` — l'exporteur 0.15.2 produit un graphe faux pour cette variante
159MoE (256 experts de 512 + expert partage de 512). Meme s'il chargeait, ORT
160GenAI n'a pas de batching continu : la voie ONNX ne peut pas atteindre le
161debit agrege ci-dessus. Abandonnee avec cette preuve.
162
163## Aiguille Ornith a 634 243 jetons AVEC TurboQuant — VALIDEE (22/08, 17:04)
164
165Pod neuf `y26flhzhufrkb3`, bootstrap v50 (TurboQuant k3v4, util 0,85, KV
16610 466 063 jetons). Prompt reel **634 243 jetons** (2,4x le natif), aiguille au
167milieu : `MARMOTTE-7391` retrouve en **244,9 s** (~2 600 j/s de prefill a
168froid, TurboQuant compris). La config par defaut est donc validee de bout en
169bout : 1M par YaRN, KV quantifie, aiguille au-dela du natif. (Hier en bf16 :
170meme aiguille trouvee en ~3 min.)
171
172## Prefill Ornith (TurboQuant, util 0,85, 2x A40), prompts sales — 22/08 soir
173
174| Entree | a froid | a chaud (cache de prefixe) |
175|---:|---:|---:|
176| ~8,6 k | 7 246 j/s | 12 956 j/s |
177| ~35 k | 7 821 j/s | 43 426 j/s |
178| ~143 k | 5 909 j/s | **115 655 j/s** (24,2 s -> 1,2 s) |
179| 634 k (aiguille) | ~2 600 j/s | -- |
180
181Ornith prefill **3,5x plus vite que Qwen3.8** a taille egale (7,8 k vs 2,2 k a
18235 k ; 5,9 k vs 2,0 k a ~130 k). Le cache de prefixe fonctionne avec
183TurboQuant + mamba align (metrique vLLM `prefix_cache_hits_total` en hausse,
184x20 a chaud a 143 k). Pour un prompt systeme Claude Code de 30-100 k, seul le
185premier tour paie.
186
187## Etat final (22/08, 03:40 UTC)
188
189Bootstrap **v50** : Ornith + TurboQuant + fastokens, util 0,85, batched 16 384.
190Pod arrete (volume 0 -> 0 $). Redemarrage : `create_pod.py` (image precompilee
191v3, ~10 min de pull + 4 min de bootstrap) ou `start` si l'hote a encore les GPU.
192Reste a faire (non mesure) : mesurer le prefill Ornith sale (3 800 j/s estime
193d'apres l'aiguille de la veille).
194
195## Banc "6 agents x 4 tours x ~80 k de contexte" (agent_bench.py, 22/08 soir)
196
197Flux SSE sur `/v1/messages` natif, thinking actif (budget 150), 300 jetons de
198sortie par tour, prompts sales distincts, +~1 k par tour. Decodage par flux =
199jetons de sortie / duree apres le premier jeton.
200
201| Version | Config | Decodage / flux | Agrege | Prefill reel | Cache | Mur |
202|---|---|---:|---:|---:|---:|---:|
203| v59 | bf16, pas 4096, FA2 | 26,4 j/s | 55 j/s | 115 j/s | 99 % | 116 s |
204| v60 | bf16, pas 4096, FlashInfer | 28,5 | 61 | 150 | 99 % | 108 s |
205| v61 | TurboQuant k3v4, pas 4096 | 15,0 | 40 | 352 | 97 % | 145 s |
206
207Solo (1 agent) : 100 j/s a 85 k, 117 j/s a 1 k. Prefill a froid des 6 x 80 k en
208serie a ~6,7 k j/s (TTFT en escalier 13 -> 72 s), ensuite TTFT 2-4 s.
209Avant (v55, TurboQuant + pas 16384, agents reels) : 2-12 j/s par flux, cache 62 %.
210Lecture : la concurrence en long contexte est bornee par la lecture du KV a
211chaque pas (5 Go/pas en bf16 par A40) et le noyau de decodage ; TurboQuant
212perd plus en dequantification qu'il ne gagne en octets ; FlashInfer = bruit.
213DSpark impossible avec les niveaux d'effort (runner V2). Config retenue : v62 =
214bf16, pas 4096, sans speculation.
215
216## MTP native sur le quant INT4 (v64, 22/08 soir) — tete BF16 chargee, PERD PARTOUT
217
218Correction d'un diagnostic errone : la tete MTP de `ulkaa/Ornith-1.5-35B-A3B-AWQ-INT4`
219n'est PAS quantifiee (785 tenseurs `mtp.*` BF16) ; ce qui cassait etait la liste
220`quantization_config.ignore` du config.json, sans motif `mtp`. Le bootstrap patche
221le snapshot (`ignore += "re:.*mtp\..*"`) et `--speculative-config '{"method":"mtp",
222"num_speculative_tokens":2}'` charge (runner V1, compatible effort). KV 2,41 M
223(2,84 M sans). Mesure (agent_bench.py) :
224
225| | sans spec (v62) | MTP n=2 (v64) |
226|---|---:|---:|
227| solo 1 k | 117 j/s | 100,7 |
228| solo 85 k | 100 | **27,6** |
229| 6 agents x 85 k, par flux | 26,4 | 14,2 |
230| acceptation | - | pos0 55 %, pos1 9 % (0,64 jeton/brouillon) |
231
232La tete BF16 predit mal sur les etats caches decales par l'INT4, et sa couche
233d'attention pleine paie les 85 k a chaque pas. Config par defaut : sans
234speculation (v65). `VL_SPEC=mtp` reste disponible.
235
236## Banc "agents lecteurs" (agent_bench.py 6 3 50 300 15) : A/B du pas, 22/08 nuit
237
2386 agents, +65 k de contexte par tour (80 -> 144 k), 300 jetons de sortie, flux SSE.
239
240| Pas | Decodage / flux | Agrege | TTFT cache chaud | Mur |
241|---|---:|---:|---:|---:|
242| 2048 (v66) | 7,0 j/s | 15 | 37 s | 330 s |
243| 4096 (v67) | 7,1 j/s | 15 | 41 s | 304 s |
244
245Borne par le calcul de prefill (390 k jetons par tour de table). Pendant un vrai audit
246(5 agents a 150 k lisant des fichiers) : +177 k jetons de prompt / 30 s, 22 j/s agrege.
247Coupure proxy RunPod mesuree a TTFT 104 s ("Response ended prematurely") -> pont (pings
248SSE 15 s) par defaut depuis v68.
249
250## Débit solo ressenti (agent unique, 23/08)
251
2521 agent qui tourne : generation 86-106 tok/s, GPU 97 %, cache de prefixe 94 %, 1 requete en
253cours. C'est le plafond SOLO du modele sur A40 (mesure constante toute la session) ; il ne monte
254qu'en parallelisant (6 agents ~= 150 j/s agrege). Le ressenti "lent" vient d'un seul flux avec
255beaucoup de raisonnement (traces jusqu'a 13 959 car. ~= 3 900 jetons en un tour = ~43 s a
25690 tok/s) ponctue d'appels d'outils (GPU inactif pendant l'execution locale). Leviers cote
257client : effort low, parallelisme, sorties courtes.
258 