File size: 69,636 Bytes
f9e2591
2454f65
 
 
 
 
 
8d4b987
 
 
 
 
 
 
 
f9e2591
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2454f65
 
 
f9e2591
 
 
2454f65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d4b987
 
 
2454f65
 
 
8d4b987
 
2454f65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452403b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cef7c89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6cf1796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc75885
 
 
 
 
 
 
 
 
 
 
 
3a70529
 
 
 
 
 
 
cd81fd1
 
 
 
 
 
 
 
 
 
 
 
c6a5dde
 
 
 
 
 
 
 
 
5fdb82b
 
 
 
 
 
 
 
 
 
 
 
 
 
b89df6e
 
 
 
 
 
 
 
 
 
 
 
a86c4f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d4b987
 
 
 
 
 
 
 
 
 
 
304cb86
 
 
 
 
 
 
 
52caca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
004e09c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2534a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d4b987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
# Endpoint Anthropic pour Claude Code, sur une RTX 6000 Ada à 0,84 $/h

Un pod RunPod, deux modèles au choix, une API compatible Anthropic. Le pod ne
connaît qu'une URL : toute la configuration vit dans `vllm_bootstrap.sh` publié
sur le Hub, et **publier une nouvelle version suffit à reconfigurer la machine

sans jamais la recréer**.


> **Mis à jour le 23/08/2026 au soir.** Le modèle servi est désormais Nemotron
> et non Ornith, la RTX 6000 Ada est passée à **0,74 $/h**, et la configuration
> retenue a changé sur deux drapeaux. Voir
> [« Configuration retenue »](#configuration-retenue-23082026-soir) en fin de
> document — les sections antérieures restent valables pour l'historique et le
> raisonnement, pas comme consigne de déploiement.

## Choix de la machine (mesures du 23/08)

| | 2×A40 | **1× RTX 6000 Ada** |
|---|---:|---:|
| prix | 0,88 $/h | **0,84 $/h** |
| solo, contexte court | 117 tok/s | **134,1** |
| solo à 85 k | 100 | **109,4** |
| 6 agents (par flux) | **26,4** | 24,7 |
| contexte max | **1 M** | 786 k (KV 957 122 jetons) |

L'Ada gagne en solo et coûte moins cher ; la paire d'A40 garde l'avantage en multi-agents
et sur le 1 M. Une seule carte supprime aussi le TP=2 sans P2P, qui coûtait du rendement
(0,25 tok/s par Go/s sur une carte seule contre 0,17 sur la paire).

## Bureau Linux distant

`VL_DESKTOP=on` (ou `bash /opt/desktop.sh` à chaud) lance Xvfb + x11vnc + websockify/noVNC +
XFCE. Accès navigateur, aucun client VNC nécessaire :

```

https://<podid>-6080.proxy.runpod.net/vnc.html?autoconnect=1&resize=remote

mot de passe : $VNC_PASSWORD (défaut SECRET)

```

x11vnc n'écoute que sur la loopback (`-localhost`) ; tout passe par websockify, et le port 6080
doit être exposé en `http` sur le pod. Détails de la pile et raison de chaque option dans
`desktop_setup.sh`.

**Après un restart de conteneur**, les paquets survivent mais les processus meurent : relancer
`bash /opt/desktop.sh`. (Un *Stop*, lui, efface tout le disque conteneur.)

## Mise en service

```

image            registry.hf.space/patdev-ornith-pod:latest  (vLLM precompile)

GPU              1x RTX 6000 Ada (49 Go, sm_89)   0,84 $/h

disque           200 Go  (conteneur ; efface au Stop, pas au restart)

port expose      8080/http

commande         bash -c 'apt-get update -qq >/dev/null 2>&1;

                          apt-get install -y -qq curl ca-certificates >/dev/null 2>&1;

                          curl -sL https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/vllm_bootstrap.sh -o /run.sh;

                          bash /run.sh'

env              HF_TOKEN, HF_XET_HIGH_PERFORMANCE=1, VL_MODEL=qwen|kimi

```

`create-pod` de l'API RunPod **n'expose pas de champ `args`** : il faut passer
par `create-template` avec `dockerStartCmd`, puis déployer avec `templateId`.
Si la création échoue sur « This machine does not have the resources », c'est
transitoire — préciser `dataCenterIds: ["EU-SE-1"]` suffit généralement.

### Épingler la version CUDA de l'hôte — indispensable

RunPod place le pod sur une machine en CUDA 12.4, 12.8 ou 13.0 **au hasard**.
Le vLLM installé par pip embarque un torch compilé pour CUDA 13.x, qui meurt à
l'initialisation du moteur sur un hôte 12.8 :

```

RuntimeError: The NVIDIA driver on your system is too old (found version 12080)

```

Le même script marche ou échoue selon le tirage. L'outil MCP n'expose pas le
champ, mais **l'API REST l'accepte** :

```bash

curl -X POST https://rest.runpod.io/v1/pods   -H "Authorization: Bearer $RUNPOD_API_KEY" -H "Content-Type: application/json"   -d '{"name":"a40","templateId":"<id>","gpuTypeIds":["NVIDIA A40"],

       "gpuCount":1,"cloudType":"SECURE","dataCenterIds":["EU-SE-1"],

       "allowedCudaVersions":["13.0"]}'

```

Vérifier ensuite `cudaVersion` dans la réponse : il doit valoir `13.0`. Le
bootstrap journalise aussi `pilote <version>, CUDA runtime <x>` au démarrage.

## Branchement de Claude Code

```

ANTHROPIC_BASE_URL=https://<podId>-8080.proxy.runpod.net

ANTHROPIC_API_KEY=peu-importe          # le pont ne verifie rien

```

Le port exposé sert l'API **Anthropic** (`/v1/messages`), c'est-à-dire celle que
Claude Code consomme. vLLM tourne en interne sur 8081 ; le pont relaie aussi
`/v1/chat/completions`, `/v1/completions` et `/metrics` pour les clients OpenAI
et les outils de mesure.

## Les deux modèles

| | Qwen3-Coder-30B-A3B | Kimi-Linear-48B-A3B |
|---|---|---|
| dépôt | `cyankiwi/Qwen3-Coder-30B-A3B-Instruct-AWQ-4bit` | `cyankiwi/Kimi-Linear-48B-A3B-Instruct-AWQ-4bit` |
| poids | 16,9 Gio | 28,4 Gio |
| contexte | 262 144 | **1 048 576** |
| cache de préfixe | **oui (16×)** | non |
| spéculation | **oui (+86 % en édition de code)** | non — elle corrompt le code |
| `VL_MODEL` | `qwen` | `kimi` |

**Ils ne tiennent pas ensemble sur une seule A40** : 16,9 + 28,4 = 45,3 Gio de
poids avant le moindre KV, sur une carte de 46 Go. `VL_MODEL` choisit lequel est
servi ; un endpoint bi-modèle simultané demande deux cartes.

## Réglages, par variable d'environnement

| variable | défaut | effet |
|---|---|---|
| `VL_MODEL` | `qwen` | `qwen` ou `kimi` |
| `VL_CTX` | 262144 / 1048576 | longueur de contexte |
| `VL_SPEC` | `auto` | `off` pour couper la spéculation |
| `VL_SPEC_N` | 12 | profondeur des brouillons |
| `VL_LOOK_MIN` / `VL_LOOK_MAX` | 3 / 8 | fenêtre de correspondance n-gram |
| `VL_BATCHED` | 16384 | `max-num-batched-tokens` |
| `VL_SEQS` | 32 | flux concurrents |
| `VL_UTIL` | 0.90 | fraction de VRAM. **Ne pas monter à 0,93** : l'OOM tombe au tout dernier pas, dans le rejection sampler |

Changer une valeur demande de recréer le pod (les env sont figées) ; changer un
**défaut dans le script** et le publier se propage en 30 s sans rien recréer.

## Correctifs indispensables, et pourquoi

**Tokenizer Kimi.** `tokenization_kimi.py` importe `bytes_to_unicode` depuis
`transformers.convert_slow_tokenizer`, supprimé en transformers ≥ 5.5.3 —
qu'exige tout vLLM ≥ 0.24. Aucun dépôt Kimi-Linear ne fournit de
`tokenizer.json` rapide, donc le tokenizer Python est obligatoire. Corrigé par
`patdev/kimi-linear-tokenizer-fix` : mêmes fichiers, avec une redéfinition
locale de repli, vérifiée identique octet pour octet à la référence transformers
avant tout déploiement.

**Parser d'outils.** `hermes` ne produit aucun appel sur Kimi-Linear. Son
gabarit de chat utilise `<|tool_calls_section_begin|>` / `<|tool_call_begin|>`,
le schéma **K2** — donc `kimi_k2`. (`kimi_k3` vise `<|open|>`/`<|close|>`/
`<|sep|>` et ne s'applique pas.) Pour Qwen, c'est `qwen3_coder`.

**Guillemets.** `--speculative-config` prend du JSON, et les `args` d'un pod
RunPod transitent par un shell qui **mange les guillemets** : il faut entourer
le JSON d'apostrophes et n'y mettre aucun espace. Le bootstrap le fait pour
vous ; le piège ne concerne que les configurations passées en dur.

**Tensor parallelism.** Sur une paire d'A40 RunPod, `NCCL_P2P_DISABLE=1` est
nécessaire, sinon l'initialisation NCCL ne rend jamais la main.

## Image précompilée (depuis le 21/08/2026)

Le pod ne réinstalle plus rien au démarrage : l'image Docker est construite
par le Space `patdev/ornith-vllm-a40` (sdk docker, gratuit, sur CPU) et tirée
par RunPod depuis `registry.hf.space/patdev-ornith-vllm-a40:latest` avec un
`containerRegistryAuthId` (user `patdev`, mot de passe = token HF). Elle
contient vLLM 0.27.1 (cu13) dans `/opt/venv`, `cuda-compat-13-0`, `nvcc` et
les en-têtes CUDA 13. Le bootstrap (v35+) lit `/opt/.image_prebaked` et saute
la phase d'installation : **~15 s** entre le démarrage du conteneur et
`vllm serve`, contre ~8 min avant.

Création : `scratchpad/create_pod.py` (template `ornith-vllm-a40-prebaked`,
`dockerStartCmd: ["/start_pod.sh"]` = sshd en arrière-plan + bootstrap du Hub).

Pièges :
- **Un pod arrêté peut refuser de redémarrer** ("not enough free GPUs on the
  host machine") : l'hôte a été reloué. Recréer, le disque est perdu de toute
  façon (volume 0).
- **Premier pull ~9 min** (18 Go, une seule couche) ; ensuite l'hôte le met en
  cache. L'image v2 sépare les couches et retire le torch cu128 de la base.
- **`NCCL_P2P_DISABLE=1` est obligatoire** (bootstrap v37) : sur deux A40
  placées sur deux nœuds NUMA (`nvidia-smi topo` = SYS), le premier collectif
  après la capture des graphes CUDA tourne en attente active — GPU à 100 %,
  0 % d'activité mémoire, workers en `futex_wait`, "No available shared memory
  broadcast block" toutes les 60 s. Rien dans le journal ne dit "NCCL".
- **Le watcher de rechargement n'est actif qu'une fois vLLM sain.** Si vLLM
  est bloqué au démarrage, publier une nouvelle version ne suffit pas : tuer
  le serveur (`pkill -9 -f "vllm [s]erve"` — les crochets évitent que pkill se
  tue lui-même) ; le bootstrap passe en `fatal`, scrute le Hub et recharge.
- **Hôte malade, signature complète** (pod `udpfa0l4z65m09`, 21/08) : première
  génération → `CUDA error: unspecified launch failure` dans `synchronize()` ;
  ensuite un GPU disparaît pour CUDA (`No CUDA GPUs are available` en le
  ciblant seul, `nvidia-smi` le liste encore, pstate P0 avec puissance N/A) ;
  `torch.cuda.device_count()` dit 2 mais `get_device_name(1)` échoue ; vLLM
  TP=2 meurt sur "DP adjusted local rank 1 is out of bounds for 1 devices" ;
  et `POST /pods/{id}/restart` répond 500 "context deadline exceeded" vers
  `hapi.runpod.net`. Rien de tout ça ne se répare depuis le conteneur : supprimer
  le pod et recréer (la création choisit un autre hôte).
- `vllm --version` dans un Dockerfile meurt sur un constructeur sans GPU
  ("Failed to infer device type") ; vérifier via `importlib.metadata`.

## Leviers mesurés (bootstrap v41, bloc EXPERIENCE en tête de script)

| Variable | Valeurs | Effet mesuré (2×A40, Ornith, 22/08) |
|---|---|---|
| `VL_KV` | `""` (bf16) / `turboquant_k3v4_nc` (défaut) / `turboquant_k8v4` / `fp8` | TurboQuant : KV ×4,33 (12,08 M jetons), débit −6 % à 32 sessions, +7 % solo |
| `VL_SPEC` | `off` (défaut) / `dspark` / `mtp` / `on` (ngram) | DSpark : +71 % solo, −1 % à 32 sessions, KV −6 % ; incompatible avec TurboQuant |
| `VL_DSPARK_N` | 8 | jetons proposés (tête de confiance adaptative) |
| `VL_SWAP` | `ornith:ornithnvfp4,qwen38:qwen38nvfp4` (défaut) | v70 : noms servis → clés de modèle interchangeables ; une requête pour un modèle non chargé déclenche le swap (`/travail/modele_demande`, re-exec avec `VL_MODEL`). Mesuré 28/08 : 368 s à la 1re compilation, pings SSE pendant l'attente (pont v71) |
| `VL_SPEC_AUTO` | `on` (défaut) / `off` | v70 : spéculation par modèle — MTP k=3 sur Qwen3.8-27B (73–83 j/s solo sur PRO 5000 NVFP4), off sur Ornith ; `off` rend la main à `VL_SPEC` |
| `VL_PRECHARGE` | dépôts HF séparés par des espaces | v70 : poids préchargés en tâche de fond après le premier service, pour un swap sans téléchargement |
| `VL_FLASHINFER` | `off` (défaut) | le sampler flashinfer a produit une faute CUDA sur un hôte 580 |
| `VL_NUMA`, `VL_EP`, `VL_OPT` | `off` / `off` / `""` (défaut) | mesurés neutres (v43d) ; membind refusé sans `SYS_NICE` ; O3 == O2 |
| `VL_FASTOK` | `1` (défaut) | fastokens (tokenizer Rust) : −12 % TTFT médian, +1,5 % solo ; installé à la volée si absent |
| `VL_KVBYTES` | `""` (défaut) | `--kv-cache-memory-bytes` : **+22 % de capacité KV** sur Flash-Next (26/08), voir « Le cache KV » ; une valeur trop haute tue le moteur en service, pas au démarrage |
| `VL_MM_IMAGES` | `4` | plafond d'images par requête. **À `0`, vLLM rejette toute capture par un 400** dont le texte est le plafond lui-même ; voir « Les images » |
| `VL_DP` | `1` (défaut) | parallélisme de données (un réplica par GPU) : +15 % à 32 sessions sur Qwen3.8, perd le solo et la moitié du KV |

Modifier les défauts, republier `vllm_bootstrap.sh` sur `patdev/k3-a40-bootstrap` :
le pod recharge à chaud (~4 min, poids conservés). `scratchpad/drive_pod.py`
automatise publication → attente → banc (`pod_bench.py`).

## Pièges d'exploitation

- Le **proxy HTTP de RunPod coupe à ~125 s** (Cloudflare, `error code: 524`).
  Un prefill à froid plus long ne passe pas en un seul appel — découper le
  prompt, chaque requête étendant le préfixe caché.
- Un **`wait` sans argument** dans le script attend aussi le serveur lancé en
  arrière-plan, indéfiniment : le serveur répond, mais le watcher ne démarre
  jamais et les mises à jour publiées sont ignorées en silence. Toujours
  `wait "$pid"` explicite. Idem pour `%1`, qui désigne le job du serveur.
- **Redémarrer un pod efface son disque conteneur.** Le rechargement en place
  par le watcher évite de retélécharger les poids.

---

## Branchement de Claude Code — validé de bout en bout

```bash

export ANTHROPIC_BASE_URL=https://<podId>-8080.proxy.runpod.net

export ANTHROPIC_API_KEY=dummy          # le pont ne verifie rien

export CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT=1

claude --model claude-kimi-k3

```

**Les identifiants de modèle doivent commencer par `claude-`.** Claude Code
valide le nom avant d'émettre la requête et refuse tout le reste. Le pont expose
donc des alias conformes et ignore le nom reçu pour router vers l'unique modèle
chargé : le client choisit une étiquette, pas un moteur.

```

claude-kimi-k3          claude-kimi-k3-linear     claude-qwen3-coder

claude-sonnet-4-5       claude-3-5-haiku

```

Les deux derniers existent parce que certains clients codent en dur un modèle
« rapide » et un « lent ».

`CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT=1` évite que Claude Code
plafonne la session à 200k alors que l'endpoint en sert 262 144. L'alternative
propre est de déclarer le modèle dans le réglage `modelOverrides`.

### Preuve d'exécution

Édition réelle d'un fichier via les outils, en 45 s :

```python

# avant

def divide(a, b):

    return a / b



# apres

def divide(a: Union[int, float], b: Union[int, float]) -> Union[int, float]:

    """Divide two numbers.



    Raises:

        ZeroDivisionError: If b is zero

    """

    if b == 0:

        raise ZeroDivisionError("Cannot divide by zero")

    return a / b

```

Claude Code a lu le fichier, appelé les outils, appliqué les annotations, ajouté
la garde et documenté. Deux avertissements bénins subsistent : le modèle n'est
pas au catalogue (fenêtre supposée), et l'outil Advisor se désactive faute de
rang dans ce catalogue.

### `/v1/models` sert deux protocoles

La distinction se fait sur l'en-tête `anthropic-version` :

```

avec l'en-tete    {"data":[{"type":"model","id":"claude-kimi-k3",...}], "has_more":false}

sans              la reponse native de vLLM, dont les bancs lisent max_model_len

```

## Configuration finale (bootstrap v50, 22/08/2026)

Ornith-1.5-35B-A3B AWQ, TP=2, YaRN ×4 (1M validé à 634 k en bf16),
`VL_KV=turboquant_k3v4_nc` (KV ~10,6 M à util 0,85 ; 12,08 M à 0,93),
`VL_SPEC=off`, `VL_UTIL=0.85` (marge VRAM pour le prefill TurboQuant),
`VL_BATCHED=16384`, fastokens on, numa/EP/O3/kvbytes off (mesurés neutres).
Variante vitesse solo : `VL_KV=` + `VL_SPEC=dspark` (171 j/s, KV 2,6 M).
Qwen3.8-27B : `FORCE_MODEL="dense"` (W4A16, MTP k=3, 262 k natif ; 1M possible
avec YaRN+TurboQuant mais ~12 min de prefill et 3,7 sessions).

Pièges de la nuit du 22 :
- **TurboQuant + prefill long = marge VRAM obligatoire** : le backend déquantifie
  le K en cache en bf16 dense à chaque tranche (`turboquant_attn.py

  _continuation_prefill`) ; à 0,93 d'utilisation, un prompt de 700 k tue le

  moteur (OOM 516 Mio). 0,85 le règle pour −12 % de KV.

- **Banc de prefill et cache de préfixe** : des textes qui partagent un préfixe

  avec des requêtes précédentes donnent un « froid » à 40 k j/s, faux. Saler

  chaque prompt (`prefill_bench.py` le fait).
- `--max-num-batched-tokens 65536` : neutre sur A40 (prefill borné par le
  calcul). `--numa-bind` demande `--numa-bind-nodes 0 1` dans un conteneur, et
  le membind est refusé sans `SYS_NICE`.
- Le proxy RunPod renvoie **403 à l'User-Agent Python par défaut** : toujours
  poser un `User-Agent` dans les clients de test.

## Raisonnement : trois correctifs du pont (22/08 soir, mesurés)

1. **Le raisonnement n'arrivait jamais à Claude Code.** vLLM 0.27.1 diffuse le
   raisonnement en flux sous `delta.reasoning` (et `reasoning_content` en
   non-flux) ; le pont ne lisait que le second → jetons dépensés, rien montré.
   Corrigé : les deux clés. Mesure : 0 → 246 car. de bloc `thinking` sur la même
   question.
2. **Le niveau de raisonnement (budget) n'était pas transmis.** Anthropic
   `thinking.budget_tokens` → vLLM `thinking_token_budget` (sampling params, il
   ferme le bloc à ce nombre de jetons). Mesure sur un problème à étapes :
   budget 64 → 168 car. de raisonnement, réponse fausse ; 512 → 1 155 car.,
   juste ; 8 000 → 1 443 car. (le modèle s'arrête seul), juste. `disabled` →
   0 car., 49 jetons de sortie. `adaptive` (ce qu'envoie Claude Code) = actif
   sans plafond.
3. **Le raisonnement de l'historique était jeté.** Dans les boucles d'outils,
   Claude Code renvoie le bloc `thinking` du tour précédent ; le pont l'ignorait
   → le modèle perdait son plan entre deux appels. Corrigé : les blocs
   `thinking` d'un tour assistant deviennent `reasoning_content` du message
   OpenAI ; le gabarit Qwen3.5 le conserve pour le dernier tour (même
   sémantique qu'Anthropic). Vérifié : historique thinking + tool_use +

   tool_result accepté par vLLM, réponse cohérente.

## Audit du pont (22/08 soir) — `audit_pont.py`, 13 cas contre le pod



Bugs trouvés et corrigés (tous vérifiés avant/après) :

- **non-flux** : le raisonnement n'était pas rendu en bloc `thinking` (seul le

  flux l'était) → `to_anthropic` le fait, avec `signature`.
- **`cache_read_input_tokens` jamais exposé** : deux causes — le pont ne lisait

  pas `prompt_tokens_details.cached_tokens`, et vLLM ne le renseigne qu'avec

  `--enable-prompt-tokens-details` (bootstrap v51). Claude Code affichait 0 %

  de cache alors que vLLM en servait ~68 %.

- **`count_tokens` ignorait système et outils** (len/4 des seuls messages : 13
  jetons pour un appel qui en pesait 398) → compte réel via `/tokenize` de
  vLLM en forme chat, repli sur l'estimation.
- **`stop_sequences`** : la chaîne coupait bien mais `stop_reason` disait

  `end_turn` → `stop_sequence` + champ `stop_sequence` (vLLM le rapporte dans

  `choice.stop_reason`).

- **`signature_delta`** absent avant la fermeture des blocs thinking → émis aux
  deux points de fermeture (passage au texte, fin de flux).

Vérifiés OK sans changement : `max_tokens` → `stop_reason=max_tokens`, système
en liste de blocs + `cache_control`, `tool_use` en flux (`input_json_delta`
reconstitue un JSON valide, `stop_reason=tool_use`), `tool_result.is_error`,
`redacted_thinking` dans l'historique, `temperature/top_p/top_k/metadata`,
préremplissage par un dernier message assistant, modèle inconnu (repli sur le
modèle servi, 200).

Vérifié après v51 : `cache_read_input_tokens` = 23 360 sur 24 047 au second
appel d'un prompt de 24 k (bloc de cache = 784 jetons en mode mamba align : un
prompt d'un seul bloc ne rapporte rien, c'est normal). Audit : **13/13**.

### Le pont est-il encore nécessaire ? (vLLM 0.27.1 sert `/v1/messages` nativement)

Oui, vLLM expose déjà `/v1/messages` et `/v1/messages/count_tokens` sur le même
port que l'API OpenAI (`vllm/entrypoints/anthropic/`) : blocs `thinking` avec
signature, `tool_use`/`input_json_delta`, images, `stop_sequence`, `ping`,
`cache_read_input_tokens`, `redacted_thinking`, raisonnement de l'historique
→ `reasoning`, fusion des messages système inline. Testé sur le pod :
`curl 127.0.0.1:18081/v1/messages` répond correctement.

Ce que le pont ajoute encore (mesuré ou vérifié dans les sources) :
- le **niveau de raisonnement** : `thinking.budget_tokens` → `thinking_token_budget`
  (absent du serveur natif, qui ne connaît que `output_config.effort`) ;
- les **alias** `claude-*` / `[1m]` avec `context_window` dans `/v1/models`
  (Claude Code lit `id`/`display_name` pour la découverte) ;
- le **keepalive pendant le préremplissage** (ping toutes les 15 s : le proxy
  RunPod coupe à ~125 s de silence — un prefill de 634 k dure 4 min) ;
- le **garde agentique** (refus des commandes destructrices, 4/4 au test), la
  console, la trace par requête, la transmission intacte des erreurs amont.
Pour un usage direct sans RunPod ni garde, le natif suffit : pointer
`ANTHROPIC_BASE_URL` sur le port vLLM et `ANTHROPIC_MODEL=ornith`.

## Pont optionnel (bootstrap v52) : `VL_BRIDGE`



| `VL_BRIDGE` | Ce qui écoute sur 8080 | Ce que ça donne |
|---|---|---|
| `on` | le pont (`anthropic_proxy.py`), vLLM derrière en 18081 | budget de raisonnement respecté, keepalive de prefill (proxy RunPod), `/v1/models` au format Anthropic, garde agentique, console, trace |
| `off` | vLLM lui-même (natif) | `/v1/messages` + `count_tokens` natifs vLLM (thinking + signature, outils, images, ping, cache), `/v1/models` au format OpenAI ; **pas** de budget de raisonnement ; keepalive de prefill non garanti derrière le proxy RunPod |

Dans les deux modes vLLM sert les alias `--served-model-name ornith claude-ornith
"claude-ornith[1m]"` : `ANTHROPIC_MODEL=claude-ornith[1m]` marche partout, et

`connect.ps1` aussi (il vérifie l'`id` dans `/v1/models`). Bascule : une ligne du

bloc EXPERIENCE + republication (vLLM redémarre, ~4 min).



Test du mode natif (`VL_BRIDGE=off`, 22/08 19:13, via le proxy RunPod) :
`/v1/models` liste `ornith`, `claude-ornith`, `claude-ornith[1m]` (max_model_len
1 000 000) ; `/v1/messages` non-flux → blocs `thinking` + `text`, usage avec
`cache_read_input_tokens` ; flux → `message_start … thinking_delta … message_stop` ;
`count_tokens` compte le système. `budget_tokens: 64` → 267 car. de raisonnement :
**le budget est ignoré en natif**, comme prévu. Défaut final v53 : `VL_BRIDGE=on`.

### 22/08 soir : natif par défaut (v54), garde corrigé

Sur demande, `VL_BRIDGE=off` est le défaut : vLLM écoute 8080 directement. Comme
vLLM natif ne répond qu'aux ids servis, `VL_ALIASES` ajoute les noms que Claude
Code et ses agents envoient : `claude-3-5-haiku claude-haiku-4-5 claude-sonnet-5
claude-opus-5 deepseek-v4-flash deepseek-v4-pro` (tous → le modèle servi).
Le garde-fou du pont (si `VL_BRIDGE=on`) ne couvre plus que les écritures de
fichier entier (`write/write_file/create_file`) et ne réagit qu'aux marqueurs
explicites (`... rest of file`, `[... previous ...]`, `(reste du fichier`,
`<unchanged>`) — les faux positifs `[...]` / `# ...` observés en session réelle
sont retirés.

### Temps de démarrage (mesuré v54, caches chauds) : 2 min 33

36 s init API server (tokenizer 248 k, processeurs image/vidéo, appels Hub) ·
24 s NCCL/TP + noyaux · 32 s poids · 7 s compile (cache ; 71 s à froid) · 2 s
warmup (65 s à froid) · 5 s graphes CUDA. v55 (préparée, non publiée) :
`--limit-mm-per-prompt '{"video":0}'` et `HF_HUB_OFFLINE=1` quand le modèle est
en cache → visé ~1 min 45. Le vrai levier reste le nombre de redémarrages : en
natif tout changement de flag/alias en coûte un ; le pont rechargeait en 5 s.

### Niveau de raisonnement en natif : patch vLLM (v55)

Constat (sources 0.27.1) : le serveur Anthropic natif ne lit pas `thinking`
(champ absent de `AnthropicMessagesRequest`, jeté par pydantic) et ne traduit
`output_config.effort` qu'en `reasoning_effort`, que seul Harmony/GPT-OSS
honore. Sur Ornith, effort low = max, et `disabled` n'agit pas.

`vllm_anthropic_effort_patch.py` (appliqué par le bootstrap avant `vllm serve`,
idempotent, `VL_EFFORT_PATCH=off` pour le couper) ajoute le champ `thinking` et
mappe : effort low/medium/high/xhigh/max → `thinking_token_budget`
1 024 / 4 096 / 16 384 / 32 768 / illimité ; `thinking.budget_tokens` explicite
gagne ; `thinking.type=disabled` → `enable_thinking=false`. Même mécanisme que
le pont (le bloc de raisonnement est fermé au plafond côté échantillonnage).

Mesure après v55 (natif, patch actif, problème à étapes « trains ») :
`thinking disabled` → 0 car. de raisonnement (avant : ignoré) ; `budget_tokens 64`
→ 164 car., réponse fausse (plafond appliqué) ; effort low/medium/max → ~1 125
car. chacun, justes : le modèle n'a eu besoin que d'~300 jetons de raisonnement,
sous le plafond `low` (1 024) — la plomberie agit (preuve par budget/disabled),
le niveau ne devient visible que sur des problèmes plus longs. Seuils
modifiables dans `vllm_anthropic_effort_patch.py` (`_BUDGET`).
Démarrage v55 : 4 min 37 — le changement de `--limit-mm-per-prompt` a invalidé
le cache torch.compile (73 s) et le warmup (67 s) une fois ; la phase avant les
poids est passée de 85 s à 64 s (Hub hors-ligne + vidéo off). Prochain
redémarrage à config identique attendu ~2 min.

## Régime multi-agents (22/08 soir, bootstrap v56 → v62) — le diagnostic par `/metrics`

Symptôme : six sous-agents Claude Code (65–120 k de contexte chacun) « lents ».
Ce que disaient les métriques vLLM (`GET /metrics` passe par le proxy RunPod) :
6 requêtes en cours, 0 en attente, KV à 7 %, **génération agrégée 2–85 j/s**,
prefill 5–8,6 k j/s quasi continu, cache de préfixe 62 % → ~25 k jetons
re-prefillés par tour, et pendant chaque tranche de 16 384 jetons les autres
flux n'avançaient que d'un jeton par pas (~2,5 s). Un rejeu de boucle agent
(thinking + tool_use + tool_result × 7) a montré que **le préfixe ne se casse

pas** : hits exactement sur les multiples du bloc. Les ratés sont réels
(gros `tool_result`, démarrages à froid, alignement du bloc).

Deux faits de structure :
- avec TurboQuant, vLLM monte le **bloc d'attention à 4 672 jetons** pour égaler
  la page Mamba (784 en bf16) : granularité du cache et re-prefill par tour ;
- `--max-num-batched-tokens 16384` gèle les décodes pendant chaque tranche.

Banc `agent_bench.py` (6 agents × 4 tours × ~80 k, flux SSE, `claude-ornith[1m]`) :

| Version | Config | Décodage / flux | Agrégé | Cache | TTFT (cache chaud) |
|---|---|---:|---:|---:|---:|
| v55 (avant) | TurboQuant, pas 16 384 | 2–12 j/s (agents réels) | ~25 | 62 % | — |
| **v59 = v62** | **bf16, pas 4 096, FA2** | **26,4** | **55** | **99 %** | 2–3 s |
| v60 | bf16, pas 4 096, FlashInfer | 28,5 | 61 | 99 % | 2–4 s |
| v61 | TurboQuant, pas 4 096 | 15,0 | 40 | 97 % | 2–4 s |

Un flux seul fait **100 j/s à 85 k** comme 117 à 1 k : le long contexte ne
coûte rien seul. En concurrence, 6 × 85 k × 10 Ko de KV bf16 par GPU = 5 Go lus
à chaque pas sur une A40 à 696 Go/s, plus le noyau de décodage : ni FlashInfer
(= bruit) ni TurboQuant (déquantification à chaque pas, pire) ne lèvent la
limite. Au-delà, c'est la bande passante de la carte, pas un flag.
Le prefill à froid de 6 × 80 k se fait en série (~6,7 k j/s, 12 s chacun) :
`--max-num-partial-prefills` ne changerait pas le total (borné par le calcul).

DSpark (v56–v58) a été essayé puis **écarté** : vLLM 0.27.1 ne l'implémente que
dans le Model Runner V2, et V2 **rejette toute requête** portant
`thinking_token_budget` (`VLLMValidationError … not yet supported by the V2
model runner`) — or le patch effort en pose un dès que Claude Code envoie
`effort`/`budget` : Claude Code recevait des 500. DSpark et niveaux d'effort
s'excluent ; `VL_SPEC=dspark` coupe désormais le patch effort automatiquement.
À 0,93 d'utilisation, DSpark a aussi frôlé l'OOM à la capture des graphes
(220 Mio libres) : 0,90 dans ce cas.

Deux bugs de bootstrap corrigés au passage : le brouillon DSpark n'était pas
téléchargeable parce que `HF_HUB_OFFLINE=1` était posé dès qu'Ornith était en
cache (v57 : pré-téléchargement) ; et `exec bash /run.sh` **hérite de

l'environnement**, donc le `HF_HUB_OFFLINE=1` du run précédent survivait au
rechargement (v58 : `unset` en tête).

Effort en v59+ (natif, patch actif, problème « trains ») : `disabled` → 0 car.,
`budget 64` → 179 car., `low` → 1 569, `max` → 3 463 (plafonné par
max_tokens) : les niveaux se distinguent.



Configuration par défaut depuis v62 : `VL_KV=` (bf16), `VL_SPEC=off`,

`VL_BATCHED=4096`, `VL_ATTN=` (FA2), util 0,93, KV 2,84 M jetons (1M solo

tient ; ~20 agents à 120 k aussi). `VL_KV=turboquant_k3v4_nc` reste là pour
>10 sessions à 1M simultanées (util 0,85 automatique).
Une requête restée **silencieuse** plus de ~125 s prend un 524 du proxy RunPod.

> **Précision du 23/08.** Le critère n'est pas « non streamée » mais « aucun octet
> émis » : vLLM n'envoie rien avant la fin du prefill, donc une requête *streamée*
> avec un gros prompt froid se fait couper aussi. Mesuré : 352 230 jetons passent
> en 97 s, ~427 k rendent un 524 à 126 s, et le délai est le même quelle que soit
> la taille. Ce qui protège Claude Code n'est pas le streaming, c'est le **ping

> SSE du pont sur `/v1/messages`** — vérifié à ~500 k de prompt froid : premier
> jeton à 221,7 s avec 30 pings, et à ~780 k premier jeton à **538,8 s** avec
> 72 pings, soit une connexion tenue neuf minutes. Le relais
> `/v1/chat/completions`, lui, n'a pas ce keepalive.

### MTP native (v63–v64) : chargée, mesurée, écartée

La tête MTP d'`ulkaa/Ornith-1.5-35B-A3B-AWQ-INT4` est en BF16 ; seul le `ignore` du
config.json manquait (`re:.*mtp\..*`). Le bootstrap patche le snapshot (glob sur `$HUBDIR`,
idempotent, trace `/tmp/mtp_patch.log`) quand `VL_SPEC=mtp`. Mesure n=2 : solo 1 k 100,7 j/s
(117 sans), solo 85 k **27,6** (100 sans), 6 agents 14,2/flux (26,4 sans) ; acceptation pos0
55 %, pos1 9 %. Défaut : `VL_SPEC=off` (v65). Détail dans `reports/`.

### Agents « lecteurs », A/B du pas, coupure proxy, pont par défaut (v66 → v68)

Régime observé pendant un vrai audit (5 sous-agents à ~150 k qui lisent des fichiers) :
+177 k jetons de prompt en 30 s (5,9 k j/s = le max de la carte) pour 667 jetons générés
(22 j/s agrégé, ~5 j/s par agent) — **le GPU est saturé par le prefill**, pas bloqué (0 en
attente, 0 abandon, TTFT < 20 s). Banc `agent_bench.py 6 3 50 300 15` (6 agents, +65 k de
contexte par tour, 80 → 144 k) :

| Pas | Décodage / flux | Agrégé | TTFT (cache chaud) | Mur |
|---|---:|---:|---:|---:|
| 2 048 (v66) | 7,0 j/s | 15 | 37 s | 330 s |
| 4 096 (v67) | 7,1 j/s | 15 | 41 s | 304 s |

Identique : ce régime est borné par le **calcul** de prefill (390 k jetons par tour de table ≈
60 s), pas par l'ordonnanceur. Pas gardé à 4 096.

**Coupure proxy** : quand le TTFT dépasse ~100 s (file de prefill), le proxy RunPod ferme la
connexion (« Response ended prematurely » ; 524 à 125 s en non-flux). vLLM natif n'émet rien
avant le premier jeton. Le pont envoie un `ping` SSE toutes les 15 s → **`VL_BRIDGE=on` par

défaut depuis v68** ; le pont mappe désormais aussi `output_config.effort` (mêmes seuils que le

patch natif). `VL_BRIDGE=off` = natif + patch effort, pour un usage léger.



Leviers réels pour ce régime : moins de réflexion pour les sous-agents (`effort: low`), moins

d'agents en parallèle, outils qui renvoient moins de texte ; au-delà, du calcul GPU

(`reports/2026-08-22-materiel-prix-runpod-hf.md`).



### Pont muet sous charge (22/08 nuit) : fuite de flux amont, pool httpx saturé



Symptôme : agents Claude Code à 0 jeton / « failed », `/metrics` via le proxy en timeout ; sur

le pod : vLLM sain (health 2 ms, 0 requête, GPU 0 %), **pont vivant (3 % CPU, 117 fd) mais

`/health` en timeout**. Journal du pont : 189 `POST /v1/messages` pour 100 traces de fin ;

`ss` : 45 connexions amont vers vLLM pour 2 clients. Les flux abandonnés côté client (retry de

Claude Code, coupure proxy) gardaient leur connexion amont — vLLM générait pour personne — et à

100 connexions (limite httpx par défaut) le pool bloquait tout, `/health` compris (il passe par

l'amont).



Correctifs : (1) `stream_anthropic` reçoit la `Request` et **ferme l'amont quand le client est
parti** (`request.is_disconnected()` à chaque ping et tous les 32 jetons) → vLLM annule la
génération ; (2) `httpx.Limits(max_connections=512, max_keepalive_connections=64)` et
`httpx.Timeout(TIMEOUT, connect=10, pool=10)` → une saturation devient un 503 rapide, plus un
blocage ; (3) chien de garde dans le watcher du bootstrap (v69) : deux `/health` KO d'affilée
→ relance du pont seul, journal conservé dans `/tmp/proxy.hung.*.log` (et, posé à la main ce
soir, `/opt/pont_watchdog.sh`). Le pont se recharge à chaud depuis le Hub sans toucher à vLLM.

## Intégration Claude Code : les quatre pièges côté pont/client (23/08)

Une fois le débit compris, les problèmes restants étaient côté pont ou côté Claude Code, jamais
le modèle. Détail et mesures : `reports/2026-08-23-pont-et-integration-claude-code.md`.

1. **Pont muet sous charge** — flux abandonnés par le client → connexions amont fuitées (45 pour
   2 clients, 189 requêtes / 100 fins) → pool httpx (100) saturé → `/health` bloqué, agents à
   0 jeton. Corrigé : `request.is_disconnected()` ferme l'amont, `httpx.Limits(512)` +
   `Timeout(pool=10)`, chien de garde du pont (relance sur 2 `/health` KO). Diagnostic :
   `ss -tn | grep -c :18081` vs nombre de clients.
2. **`[1m]` obligatoire dans le nom de modèle** — sans lui, Claude Code suppose ~200 k et lance
   la compaction automatique à ~150 k. Sous-agents = `deepseek-v4-flash[1m]` /
   `deepseek-v4-pro[1m]` ; alias `[1m]` ajoutés à `VL_ALIASES` (v69).
3. **Réponse sans texte → compaction vide** — le modèle mettait tout dans le raisonnement
   (`texte=0c`). Le pont a un repli : pas de texte ni d'outil → raisonnement rendu en texte.
4. **Compteur de jetons figé** — le pont n'émettait qu'un `message_delta` final ; il émet
   désormais un `message_delta` toutes les 0,5 s avec `output_tokens` cumulé
   (`continuous_usage_stats`), comme l'API Anthropic (11 événements au lieu de 1).

**Débit solo** : un agent seul = 86–106 tok/s (le plafond solo d'Ornith sur A40), cache de
préfixe 94 %. Ça paraît lent parce que c'est un seul flux avec beaucoup de raisonnement (jusqu'à
~3 900 jetons de réflexion en un tour = ~43 s) ; le débit ne monte qu'en parallélisant. Leviers
côté client : `effort: low`, plus d'agents en parallèle, sorties/outils moins bavards.

**v69 (préparé, non publié)** rassemble : chien de garde du pont, alias `[1m]`, correction de la
fuite de connexions, repli réponse vide, usage incrémental. À publier hors charge (un `publish`
relance vLLM). Les correctifs du pont ont été déployés à chaud sur le pod entre-temps.


## Bascule NVFP4 (bootstrap v74, 23/08/2026) — configuration servie aujourd'hui

    VL_MODEL=ornithnvfp4   VL_SPEC=off   VL_CTX=1048576


`ornith-ai/Ornith-1.5-35B-A3B-NVFP4` remplace `ulkaa/…-AWQ-INT4`. Raison, calculée
sur les formes de tenseurs (octets **lus par jeton** en décodage, tour visuelle
exclue) :

| dépôt | dense | `lm_head` | 8/256 experts | actif | plafond |
|---|---:|---:|---:|---:|---:|
| `ulkaa` AWQ-INT4 | 2,86 | 1,02 | 0,63 | 4,51 Go | 213 t/s |
| `ornith-ai` FP8 | 2,46 | 1,02 | 1,06 | 4,54 Go | 211 t/s |
| **`ornith-ai` NVFP4** | **1,40** | **0,29** | **0,62** | **2,30 Go** | **416 t/s** |

L'AWQ ne quantifiait que les experts routés — 531 entrées dans `ignore` — donc
**86 % de ce qui est lu à chaque jeton restait en BF16**. Le FP8 officiel est un
piège symétrique : il grossit les experts, qui ne pèsent que 14 % de la lecture,
sans alléger le dense.

**Ce que vLLM en fait sur sm89** : `MarlinNvFp4LinearKernel` et backend MoE
`MARLIN` (les autres candidats exigent Blackwell). Poids 21,94 Gio, et la config
modelopt impose un **KV en fp8** : cache de **2 103 356 jetons**, soit 2,01× de
concurrence à 1 048 576. Le vrai 1 M tient sur une carte, sans TurboQuant.

### Débits mesurés (sortie contrôlée en 4-grammes, 0,82 à 0,99)

| session unique | 3 k | 32 k | 114 k | 282 k | 1 011 609 |
|---|---:|---:|---:|---:|---:|
| AWQ-INT4 | 132,3 | 125,4 | 103,1 | 73,2 | *impossible* |
| **NVFP4** | **201,0** | **187,2** | **156,5** | **119,7** | **58,9** |

| sessions | 1 | 2 | 4 | 8 | 16 |
|---|---:|---:|---:|---:|---:|
| agrégé | 127 | 274 | 398 | 646 | **1 108** |

Loi de décodage : `t(L) = 4,936 ms + 0,01274 µs × L`, vérifiée à 5 % près à 1 M.

### `VL_SPEC` doit rester à `off`



La MTP avait déjà été écartée sur l'AWQ (v63–v65). Elle reste perdante sur le

NVFP4, et plus nettement : **66 tok/s contre 201**. La chaîne est causale et vaut

la peine d'être retenue — le KV fp8 interdit FA2 sur sm89, vLLM bascule sur

FlashInfer, et FlashInfer refuse `CUDAGraphMode.FULL_AND_PIECEWISE` sous

spéculation ; vLLM coupe en plus `--async-scheduling`. La spéculation émettait

pourtant 1,93 jeton par passe (acceptation 75,7 / 15,4 / 1,8 % par position) :

c'est le surcoût par passe, ×3,9, qui noie le gain.



### Nouveautés du bootstrap v74



- `FORCE_MODEL="${VL_MODEL:-ornith}"` : le défaut ne change pas, mais `VL_MODEL`
  redevient surchargeable — **seul levier quand on n'a pas de shell sur le pod**.
- **Journal distant** : la fin de `/tmp/boot.log` et de `/tmp/vllm.log` est
  republiée toutes les 25 s sur
  `patdev/k3-a40-bootstrap/etat/<POD_ID>.log`, tant que `/v1/models` ne répond
  pas. Sans lui, un démarrage raté est invisible dès que SSH est indisponible.
- Le correctif de config MTP couvre aussi `ornithnvfp4` (sans effet : ce dépôt
  exclut déjà `mtp*` de la quantification).

### Ce qui est ferme, et qu'il ne faut plus retenter

- **TRT-LLM sur MoE Ada** : noyau MoE 4 bits réservé à sm90.
- **EP TensorRT sur l'ONNX d'Ornith** : il ne prend que 181 nœuds sur 988, tous
  de plomberie ; les 441 qui portent les poids restent sur CUDA, et le graphe
  partagé interdit la capture CUDA.
- **Réécriture QDQ du MoE** : TensorRT gagne 2,14× à index d'experts fixe, mais
  refuse de construire le moteur dès qu'un `Gather` s'intercale entre la
  constante quantifiée et son `DequantizeLinear`. La sparsité dynamique est
  incompatible avec un moteur compilé statiquement.

Détail complet et chiffré :
`reports/2026-08-23-plafond-solo-et-dtype-du-chemin-dense.md`.

---

# Configuration retenue (23/08/2026, soir)

Ce que 23 mesures de la journée désignent, toutes sur
`nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4`.

## Branchement de Claude Code — la version qui marche

```powershell

Remove-Item Env:\ANTHROPIC_API_KEY -ErrorAction SilentlyContinue

$env:ANTHROPIC_BASE_URL   = "https://<podId>-8080.proxy.runpod.net"

$env:ANTHROPIC_AUTH_TOKEN = "x"

$env:ANTHROPIC_MODEL      = "claude-nemotron[1m]"

$env:CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT = "1"

$env:CLAUDE_CODE_AUTO_COMPACT_WINDOW = "1000000"

$env:ANTHROPIC_SMALL_FAST_MODEL     = "claude-3-5-haiku"

$env:ANTHROPIC_DEFAULT_HAIKU_MODEL  = "claude-3-5-haiku"

$env:ANTHROPIC_DEFAULT_SONNET_MODEL = "claude-nemotron[1m]"

$env:ANTHROPIC_DEFAULT_OPUS_MODEL   = "claude-nemotron[1m]"

claude

```

Ou simplement `.\connect.ps1`, qui lit l'identifiant du pod dans
`.pod-nemotron.json`, vérifie l'endpoint, choisit le meilleur identifiant servi
et lance Claude Code.

**`ANTHROPIC_API_KEY` doit être effacé.** Il prend le pas sur `AUTH_TOKEN` et
envoie les requêtes vers l'API officielle : on croit tester le modèle local, et
la facture arrive avec. C'est ce qui faisait que Claude Code « ne voyait pas »
les modèles servis.

**Le 1 M exige les deux variables `CLAUDE_CODE_*`.** Claude Code suppose 200 k

pour tout identifiant inconnu et compacte à cette limite ; la première lève la

présomption, la seconde est plafonnée par elle et reste sans effet seule. Le

suffixe `[1m]` ne suffit pas non plus.



**Ne rien publier sur `patdev/k3-a40-bootstrap` pendant qu'un pod sert.** Le
bootstrap surveille sa propre URL et se recharge : publier coupe l'endpoint
pendant 3 à 5 minutes.

## Cycle de vie du pod

```

python pod.py create      # cree (--gpu, --cloud, --replis, --max-prix, --sans-volume)

python pod.py wait        # bloque jusqu'au service

python pod.py status      # etat, cout, endpoint

python pod.py stop        # arrete -- facturation A LA MINUTE, couper tot economise

python pod.py stock       # cartes disponibles, avec leurs id RunPod exacts

```

## La commande

```

vllm serve nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4 \

  --trust-remote-code \

  --max-model-len 1048576 \

  --moe-backend marlin \

  --kv-cache-dtype fp8 \

  --enable-prefix-caching \

  --gpu-memory-utilization 0.85 \

  --mamba-backend flashinfer \

  --mamba-cache-mode align \

  --async-scheduling \

  --max-num-batched-tokens 8192 \

  --reasoning-parser nemotron_v3 \

  --tool-call-parser qwen3_coder \

  --enable-auto-tool-choice

```

Par variables du bootstrap : `VL_MODEL=nemotron VL_SPEC=off VL_CTX=1048576`
**`VL_BATCHED=8192`** (défaut actuel 4096) `VL_ASYNC=on` (déjà le défaut).



**Un seul changement par rapport à ce qui tournait** : `VL_BATCHED` de 4096 à
8192. Tout le reste était déjà en place.

## Ce que chaque drapeau vaut, mesuré

Dix leviers testés un par un sur RTX PRO 6000, contexte 131 072 :

| levier | solo | agrégé@8 | prefill j/s |
|---|---:|---:|---:|
| référence | 275,3 | 910 | 22 015 |
| **`--async-scheduling`** | 281,4 | **995** | 23 845 |
| **`--max-num-batched-tokens 8192`** | 276,9 | **1 003** | **24 050** |
| `-O3` | 279,0 | 979 | 23 757 |
| graphes CUDA `FULL` | 280,3 | 853 | 21 799 |
| `--attention-backend TRITON_ATTN` | 280,5 | 902 | **14 918** |
| `--mamba-cache-mode all` | 271,3 | 877 | 22 093 |
| `--kv-cache-dtype auto` | 277,8 | 904 | 21 700 |
| `--max-num-seqs 8` | 278,9 | 830 | 21 935 |
| sans cache de préfixe | **283,9** | 911 | 23 021 |

**Le débit solo ne bouge pas** : 4,6 % d'écart entre le meilleur et le pire,
l'ordre du bruit. Les deux drapeaux retenus rendent ~+10 % **en agrégé et en

prefill uniquement** — ils agissent sur l'ordonnancement, pas sur le calcul par
jeton.

Trois pièges dans ce tableau :

- **Ne jamais forcer `TRITON_ATTN`** : −32 % de prefill pour un décodage

  identique. Notre ancien banc Blackwell le forçait, ce qui explique une partie

  de ses 237,7 périmés.

- **`--kv-cache-dtype fp8` ne rend rien** (277,8 en `auto` contre 275,3). Il est

  gardé pour la capacité KV, pas pour la vitesse.

- **Le meilleur solo est un piège.** « Sans cache de préfixe » donne 283,9, mais

  le cache rend **×16 sur le TTFT** dès que le préfixe est réutilisé : 2,90 s à

  froid contre 0,18 s à chaud, sur 63 895 jetons lus dans `usage.prompt_tokens`.

  Claude Code renvoie le même prompt système à chaque tour. On garde le cache.



## `mamba-cache-mode` : garder `align` partout



**Correction du 23/08 au soir.** Une version antérieure de ce document
recommandait `all` sur Ada pour ses « +11 % ». **C'était faux**, et l'erreur
était une comparaison à deux variables : les 238,5 étaient en mode `all` avec le
dtype de cache SSM par défaut, les 215,2 en mode `align` **avec

`mamba-ssm-cache-dtype float16`** — et à des contextes différents, 1 M contre
131 k. L'écart a été attribué au mode de cache alors qu'il ne l'était pas.

Mesure où le mode de cache est la **seule** variable (Ada, recette + v83,
contexte 131 072) :

| mode | solo | agrégé@8 | cache KV |
|---|---:|---:|---:|
| `align` | **209,9** | 648 | **5 108 531** |
| `all` | 208,5 | 638 | 1 257 472 |

`all` ne rapporte rien et divise la capacité KV par quatre. **Garder `align`

partout.** Sur Blackwell le constat était déjà celui-là (271,3 contre 275,3).

Lequel des autres facteurs portait les +11 % n'est pas établi — le dtype du
cache SSM est le suspect le plus probable, mais il n'a pas été isolé.

## La machine

Même modèle, même banc, mode `align`, contexte 1 M :

| carte | $/h | solo | agrégé | remarque |
|---|---:|---:|---:|---|
| RTX 6000 Ada | **0,84** | 209,9 | 648 @8 | contrôle direct, config v83, ctx 131 072 |
| RTX PRO 6000 Blackwell | 1,69 | **282,0** | 1 045 @8 | +31 % de solo |
| A100 PCIe 80 Go | 1,19 | 154,2 | 2 257 @48 | 18,5 M de KV, 17,64× à 1 M |
| H200 SXM | 3,59 | **351,6** | — | meilleur absolu, hors budget |

**Sous 2 $/h et pour la latence d'une session unique : RTX PRO 6000 à 1,69 $/h.**
Pour le meilleur rapport qualité-prix : l'Ada à 0,74 $/h.

**Réserve levée le 23/08 au soir.** Contrôle exécuté sur un pod RunPod avec
l'image publique `vllm/vllm-openai:v0.27.1` — la même que les jobs — et la
commande identique, contexte 131 072 :

| | Ada | RTX PRO 6000 | écart |
|---|---:|---:|---:|
| solo | 200,7 | 275,3 | **+37,2 %** |
| agrégé @8 | 627 | 910 | **+45,2 %** |
| prefill j/s | 11 556 | 22 015 | **+90,5 %** |

Les +31 % annoncés étaient donc **conservateurs** : l'écart réel de silicium est
de +37 %, et le prefill de la Blackwell est presque le double. Pile de noyaux
identique des deux côtés (`FLASHINFER` / `MARLIN` / `MarlinNvFp4LinearKernel`).

À noter : ces 200,7 sont *inférieurs* aux 215,2 de notre ancienne configuration
de pod (`gpu-memory-utilization 0.93`, `max-num-batched-tokens 4096`,
`mamba-ssm-cache-dtype float16`, contexte 1 M), qui était donc mieux réglée pour
cette carte que la recette NVIDIA brute.

## Ce qui est fermé, et qu'il ne faut plus retenter

- **Toute spéculation.** MTP, DSpark, DFlash, à la recette exacte de NVIDIA, sur
  Ada **et** sur Blackwell — l'architecture que NVIDIA déclare supportée :
  −62 % à −73 % de débit solo. DSpark n=5 émet *plus* de jetons par passe que
  n=3 (3,25 contre 2,72) et rend *moins* de débit : le brouillon fait son
  travail, c'est la passe avant qui s'effondre quand vLLM coupe
  `--async-scheduling` et les graphes CUDA complets. `VL_SPEC` reste à `off`.
- **Chercher un autre noyau.** `--moe-backend humming` et
  `--linear-backend humming` donnent le même chiffre que Marlin à 1 % près, sur
  trois architectures. Le noyau MoE porte 36 % du socle actif, le linéaire 64 % :
  les deux moitiés sont insensibles.
- **Requantifier le dépôt en W4A4.** Le format débloquerait cinq noyaux, et les
  cinq sont plus lents (−20 % à −33 %). Vérifié à 0,61 $ sur
  `nvidia/Qwen3-8B-FP4`, déjà au bon format, au lieu de 5 à 10 $ de conversion.
- **Compter sur une carte plus large.** Rendement mémoire 69,3 % sur Ada, 47,5 %
  sur Blackwell, **22,6 % sur H200** : le socle actif est de 2,880 Gio par jeton,
  et le terme dominant n'est pas la mémoire mais un coût fixe par jeton.

Détail chiffré : `reports/2026-08-23-plafond-solo-et-dtype-du-chemin-dense.md`,
sections 22 à 28.

---

# Qwen3.8-Flash-Next sur UNE carte, derrière le pont Anthropic (26/08/2026)

Validé de bout en bout : Claude Code parle à `claude-flashnext`, 262 144 de
contexte natif, sur **une seule** RTX PRO 6000 à 1,69 $/h.

## Reconstruire le pod

`dockerStartCmd` :

```

curl -sL https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/lancer_flashnext.sh -o /run.sh && bash /run.sh

```

Exigences du pod : image `vllm/vllm-openai:qwen38-flash-next`, 1 GPU de 96 Go,
**plus de 110 Go de RAM**, 200 Go de disque conteneur, port `8080/http`.
Env : `HF_TOKEN`. Compter ~25 min au premier démarrage (135 Go à télécharger),
~6 min ensuite.

## Les quatre réglages sans lesquels rien ne marche

| réglage | ce qui arrive sans lui |
|---|---|
| `VLLM_PLE_CPU_OFFLOAD=1` | 125,9 Go de poids pour 96 Go de VRAM |
| `--distributed-executor-backend mp` | aucun ouvrier PLE, blocage **muet et sans fin** après la capture des graphes |
| `patch_ple.py` | échec au 205e shard sur 206, `ngram_embedding.weight_scale` |
| `--gpu-memory-utilization 0.93` | à 0,88 le cache KV plafonne à 309 594 jetons et les requêtes s'empilent pour cause de capacité |

Ne **pas** poser `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True` :
`pidfd_getfd: Operation not permitted`, faute de `CAP_SYS_PTRACE`.

### Le cache KV, et pourquoi ne pas suivre la suggestion de vLLM

Mesuré le 26/08, une seule variable changée :

| `--gpu-memory-utilization` | cache KV | jetons | concurrence max à 262 k |
|---|---|---|---|
| 0.88 | 7,52 GiB | 309 594 | 1,18× |
| **0.93** | **12,26 GiB** | **504 841** | **1,93×** |

À 0,88, vLLM refusait d'admettre des requêtes —
`num_requests_waiting_by_reason{reason="capacity"} = 5` avec 2 seulement en
cours. À 0,93 la file est vide.

Ne **pas** suivre le `--kv-cache-memory` « to fully utilize gpu memory » que
vLLM propose (17,92 GiB ici) : il ne garde aucune marge au-dessus du pic
d'activation mesuré à vide (1,55 GiB), et sous charge réelle ce pic est plus
haut — c'est cette valeur-là qui avait tué le serveur en plein service.

### Mais l'intervalle, lui, se prend : `--kv-cache-memory-bytes`

Mesuré le 26/08. **Le KV est strictement linéaire en octets**, à 26 078 octets
par jeton aux trois points :

| réglage | cache KV | jetons | concurrence à 262 k | tient 2 prefills pleins ? |
|---|---|---|---|---|
| `--gpu-memory-utilization 0.93` | 12,26 GiB | 504 841 | 1,93× | oui |
| **`--kv-cache-memory-bytes 16106127360`** | **15 GiB** | **617 633** | **2,36×** | **oui — retenu** |
| `--kv-cache-memory-bytes 18253611008` | 17 GiB | 700 087 | 2,67× | **NON — `EngineDeadError`** |

**Le test qui décide n'est pas un prefill, c'est deux.** À 17 GiB, une requête
à pleine fenêtre passe très bien ; deux simultanées tuent le moteur. La VRAM
libre le dit :

```

             au repos   1 prefill   2 prefills simultanes

17 Gio        269 Mo      273 Mo      83 Mo  -> EngineDeadError

15 Gio       4353 Mo     2289 Mo    1137 Mo  -> OK

```

Deux prefills concurrents consomment **~3,2 GiB transitoires**. La réserve
`1.55 GiB for peak activation` annoncée par vLLM en couvre à peu près *un* :
elle est profilée sur une forme de lot, **ce n'est pas une borne pire-cas**.
Ne jamais valider une capacité sur une seule requête.

### `--kv-sharing-fast-prefill` : RETIRE le 27/08, il tuait le moteur

**Ne pas l'activer.** Mesure et contre-mesure, dans cet ordre :

| | KV | solo | agrégé à 8 | prefill chaud | aiguilles |
|---|---|---|---|---|---|
| 15 GiB | 617 633 | 83,2 | 426,0 | 259 858 | 5/5 |
| **15 GiB + `--kv-sharing-fast-prefill`** | **617 633** | **87,3** | **445,4** | **297 085** | **5/5** |

Capacité identique au jeton près. **Attention au piège de mesure :** sous
`--gpu-memory-utilization`, ce même drapeau semblait coûter −8,6 % de cache
(504 841 → 461 280), parce que c'est vLLM qui arbitre et que ses tampons
sortent du budget KV. Sous `--kv-cache-memory-bytes`, le cache est *fixé* et
le surcoût sort de la marge. Les deux mesures sont exactes et racontent
l'inverse : le mode d'allocation est une seconde variable, invisible dans la
ligne de commande.

Exactitude vérifiée en A/B — cinq aiguilles à 10/30/50/70/90 % de profondeur
dans 120 906 jetons, raisonnement coupé : **5/5 des deux côtés**.

**Et pourtant il est retiré.** Trois morts du moteur en douze heures, toutes
avec ce drapeau actif, aucune sans, sur des dizaines de configurations :

```

File "vllm/v1/ple_offload/connector.py", line 389, in _launch

    self._request_queue.put_nowait(request)     # queue.Queue(maxsize=1)

queue.Full

```

Le commentaire du code dit l'invariante : *« each forward consumes its output

before the next launch »*. Elle suppose des forwards **sérialisés** ; ce drapeau
modifie l'ordonnancement du prefill juste assez pour la casser sous concurrence.

Preuve par contraste, mesurée le 27/08 :

| | concurrence atteinte | survie |
|---|---|---|
| avec `--kv-sharing-fast-prefill` | 4 requêtes | **14 min → `queue.Full`** |
| sans | **5 à 6 requêtes** | 49 min et plus, zéro incident |

La charge sans le drapeau était **supérieure de moitié** à celle qui avait tué
le moteur avec. Une seule paire d'observations, donc pas une preuve formelle —
mais 4,6 % d'agrégé ne paient pas un moteur qui meurt sous la charge réelle.

**Piège de lecture** : pendant 40 min après le retrait, la concurrence n'a
jamais dépassé 3. Les « quarante minutes sans incident » ne prouvaient alors
rien du tout — comparer deux régimes de charge différents ne vaut pas mieux ici
qu'ailleurs. Le résultat n'existe qu'à partir du moment où la charge a repassé
le seuil.

### Le bug qui tue le moteur : la file du déchargement PLE

Une requête très au-delà de la fenêtre (mesurée à 580 000 jetons, 2,2×) ne
rend pas une erreur — elle **tue le moteur** :

```

File "vllm/v1/ple_offload/connector.py", line 389, in _launch

    self._request_queue.put_nowait(request)

queue.Full

```

```python

# PLE rejects DBO, and each forward consumes its output before the

# next launch, so one pending request is sufficient.

self._request_queue = queue.Queue(maxsize=1)

```

C'est une **invariante violée**, pas une file sous-dimensionnée : augmenter
`maxsize` ferait courir deux requêtes sur des tampons de sortie partagés.

Non atteignable depuis Claude Code (`CLAUDE_CODE_MAX_CONTEXT_TOKENS = 262144`).
Juste au-dessus de la limite, à 262 085 jetons, le serveur rejette proprement
avec un `BadRequestError` transmis intact dans le flux SSE, et reste debout.
Le symptôme à reconnaître : `/v1/models` répond `200` alors que toute vraie
requête rend `500` — le pont survit au moteur qu'il sert.

### Ne pas activer la spéculation MTP sur une seule carte

Le modèle embarque bien sa tête brouillon (31 tenseurs `mtp.*` dans le dépôt,
`hc_count=4`, `mtp_num_hidden_layers=1`) et vLLM l'accepte. Mais elle consomme
**5,79 GiB de VRAM** : le cache KV tombe de 12,26 à 6,47 GiB, alors qu'une
seule requête à 262 144 en réclame 7,25 — le moteur refuse de démarrer.

```

ValueError: To serve at least one request with the model's max seq len (262144),

7.25 GiB KV cache is needed, which is larger than the available KV cache

memory (6.47 GiB). ... estimated maximum model length is 231264.

```

Même au maximum de mémoire, on obtiendrait ~423 000 jetons contre 504 841 sans
MTP : au moins 16 % de capacité cédés pour un gain de décodage plafonné à 2×,
alors que la capacité est justement ce qui bride le serveur. Les seules portes
de sortie sont de descendre `--max-model-len` à 231 264 ou moins, ou d'ajouter
une deuxième carte.

## Côté client

```powershell

Remove-Item Env:ANTHROPIC_API_KEY -ErrorAction SilentlyContinue

$env:ANTHROPIC_BASE_URL   = "https://<pod>-8080.proxy.runpod.net"

$env:ANTHROPIC_AUTH_TOKEN = "x"



# Alias NU, sans prefixe claude- : voir ci-dessous, c'est ce qui debloque 262 k

$env:ANTHROPIC_MODEL                = "flashnext"

$env:ANTHROPIC_DEFAULT_OPUS_MODEL   = "flashnext"

$env:ANTHROPIC_DEFAULT_SONNET_MODEL = "flashnext"

$env:ANTHROPIC_DEFAULT_HAIKU_MODEL  = "flashnext"

$env:ANTHROPIC_SMALL_FAST_MODEL     = "flashnext"



$env:CLAUDE_CODE_MAX_CONTEXT_TOKENS = "262144"

$env:CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT = "1"

claude

```

`ANTHROPIC_API_KEY` doit être effacée : elle prime sur `ANTHROPIC_AUTH_TOKEN` et
renvoie vers l'API officielle, facturée, sans le dire.

**Le nom servi doit être NU.** Corrigé le 26/08 en décompilant le binaire
2.1.239 : la fonction `JFd` refuse toute fenêtre personnalisée à un identifiant
commençant par `claude-`.

```js

let n = CLAUDE_CODE_MAX_CONTEXT_TOKENS;

if (n !== undefined && n > 0 && !id.startsWith("claude-")) return n;

return /* defaut */ 200_000

```

Avec `claude-flashnext`, ni `CLAUDE_CODE_MAX_CONTEXT_TOKENS` ni
`CLAUDE_CODE_AUTO_COMPACT_WINDOW` n'ont le moindre effet et `/context` affiche
200 k quoi qu'on fasse. La bonne variable est
**`CLAUDE_CODE_MAX_CONTEXT_TOKENS`**, pas `AUTO_COMPACT_WINDOW` ; il en faut
toujours deux, car la présomption de 200 k est rabattue tant que
l'application de la règle est active. Le pont ajoute donc `MODEL` nu en tête
d'`ALIASES` (`anthropic_proxy.py`).

Ne pas utiliser le suffixe `[1m]` : d'autres branches de `JFd` renvoient `1e6`
en dur, ce qui ferait croire à Claude Code qu'il dispose d'un million alors que
le moteur plafonne à 262 144, et ferait compacter trop tard.

**Contrepartie de l'alias nu.** Tout outil qui déduit la fenêtre du NOM du
modèle casse. Une ligne d'état qui attend `claude-<fournisseur>/<modele>`
retombe sur son défaut de 200 k et affiche `172k/200k` pendant que `/context`
affiche 262,1 k. Le correctif est de consulter la même source que Claude Code,
avec sa condition :

```js

const env = Number(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS);

if (Number.isFinite(env) && env > 0 && !base.startsWith("claude-")) return env;

return 200_000;

```

## Ce qu'on obtient

Configuration retenue, corrigée le **27/08** après trois morts du moteur en
service :

```

--kv-cache-memory-bytes 16106127360   (15 Gio)

--enable-flashinfer-autotune

```

`--kv-sharing-fast-prefill` a été **retiré** — voir plus haut : il rendait
+4,6 % d'agrégé et déclenchait `queue.Full` dans le connecteur PLE sous
concurrence. Les chiffres du 26/08 le comprenaient ; ceux-ci ne le comprennent
plus.

```

                    26/08 matin      27/08 retenu   gain

solo                 75,3 tok/s      83,2           +10,5 %

agrege a 8          335,2 tok/s     426,0           +27,1 %

KV                  297 148 jetons  617 633        +107,9 %

concurrence a 262k    1,13x           2,36x        +108,8 %

prefill froid         8 535 j/s      11 200         +31,2 %

prefill chaud             -         259 858 j/s    (x23 sur le froid)

```

Tenue en charge vérifiée le 27/08 : **5 à 6 requêtes concurrentes** soutenues,
1 228 j/s de prefill et 322 j/s de génération, sans incident — contre 14 min de
survie à 4 requêtes avec le drapeau retiré.

**Le prefill froid n'a été amélioré par aucun levier de la soirée** — les 31 %
viennent de `0.93` et de l'autotune, acquis plus tôt. Tous les leviers qui le
visaient sont fermés : déchargement KV et `prefix-match-unit` par l'alignement
Mamba (groupes `[800, 4, 800, 800, 800, 800]`), `indexer_budget` par le noyau
`persistent_topk` qui n'accepte que k ∈ {512, 1024, 2048}, soit un budget déjà
au plancher.

Le vrai gain de prefill est **indirect** : le contrôle d'éviction donne
`gain_eviction = 1,00` — un bloc chassé du GPU est définitivement perdu. Seule
la taille du cache protège un préfixe. À ~130 k par session, on passe de ~2,3 à
~4,7 sessions dont chaque tour coûte 1,14 s au lieu de 21 s.

Trois fois et demie plus lent que Nemotron en solo. Le pari reste que la
capacité compense.

Détail complet : `reports/2026-08-23-plafond-solo-et-dtype-du-chemin-dense.md`,
sections 33 à 36.

## Les images : un 400 qui accuse le modèle et désigne notre réglage

### Le symptôme

Une capture collée dans Claude Code fait tomber le tour entier :

```

API Error: 400 {"error":{"message":"At most 0 image(s) may be provided in one

prompt. (parameter=image)","type":"BadRequestError","param":"image","code":400}}

```

Le message ressemble à une limite du modèle. C'est **notre** drapeau, lu à voix
haute : `--limit-mm-per-prompt '{"image":0,"video":0}'`. vLLM le confirme au
démarrage, en clair, dans son propre journal :

```

All limits of multimodal modalities supported by the model are set to 0,

running in text-only mode.

```

Le piège de diagnostic est là : l'erreur est renvoyée au client comme une
erreur de requête, alors que la cause est une option de service. Chercher dans
le modèle ou dans le pont fait perdre l'heure ; il faut lire le journal de
démarrage du moteur.

### Ce qui n'était PAS en cause

Deux couches vérifiées avant de toucher au moteur, et toutes deux saines :

* **le pont** traduit déjà correctement les blocs Anthropic vers OpenAI
  (`_image_part`, `anthropic_proxy.py`) — `source.base64` devient une URI de
  données, `source.url` passe tel quel, et les images portées par un
  `tool_result` sont extraites pour être rattachées au tour utilisateur ;
* **les poids de vision sont dans le dépôt** : 333 tenseurs `model.visual.*`,
  soit **0,90 Go sur les 135,16** du checkpoint (0,449 B paramètres BF16).
  Décompte complet par catégorie :

| | paramètres | taille |
|---|---:|---:|
| experts MoE | 67,948 B | 67,95 Go |
| table PLE n-grammes | 51,200 B | 51,20 Go |
| reste (attention, embeddings, `lm_head`) | 4,948 B | 9,90 Go |
| tête MTP (non chargée) | 2,607 B | 5,21 Go |
| **tour de vision** | **0,449 B** | **0,90 Go** |

La tour est donc déjà résidente : le plafond ne gouvernait que la validation
des requêtes et le profilage, pas le chargement.

### Pourquoi il a fallu rendre 0,5 GiB de KV en même temps

Mesure sur le pod en service avant bascule :

```

VRAM : 96 612 MiB utilisés sur 97 887   ->  1,25 GiB libres

```

Et surtout, dans le journal :

```

reserved 15.0 GiB memory for KV Cache as specified by kv_cache_memory_bytes

config and skipped memory profiling.

```

**`--kv-cache-memory-bytes` fait sauter le profilage mémoire.** vLLM ne
recalculera donc rien tout seul : il réservera ses 15 GiB quoi qu'il arrive, y
compris si l'encodage d'images demande des activations transitoires qu'on n'a
jamais mesurées ici. Avec 1,25 GiB libres et trois morts de ce moteur déjà au
compteur pour avoir visé trop haut, on rend la marge :

| | avant | après |
|---|---:|---:|
| `--kv-cache-memory-bytes` | 16 106 127 360 (15,0 GiB) | **15 569 256 448 (14,5 GiB)** |
| cache KV | 617 633 jetons | ~597 000 |
| concurrence à 262 144 | 2,36× | ~2,28× |
| images par requête | **0** | **4** |

−3,3 % de capacité pour que les captures fonctionnent. La vidéo reste à `0` :
`temporal_patch_size 2` et plusieurs images par seconde feraient exploser le
budget d'activation, et Claude Code n'en envoie jamais.

### Les deux pièges de la manœuvre

**Le chien de garde doit porter le même drapeau.** `chien.py` a sa propre
`MOTEUR_CMD`. Si on ne la met pas à jour, la première « réparation » relance le
moteur en mode texte seul et la panne revient sans que rien ne l'explique.

**Aucun `pkill -f` sur ce pod.** L'agent qui exécute les commandes porte le
**texte complet du script** dans sa ligne de commande : tout motif présent
littéralement dans le fichier matche l'agent et le tue — constaté, `[FIN]
code=-15` dès la première ligne. Soit on brise l'égalité littérale
(`vllm [s]erve`, à appliquer à *chaque* motif), soit — plus sûr quand le script
arrête plusieurs choses — on lit `/proc/*/cmdline` et on exclut toute son
ascendance en remontant `ppid`. Recette dans `bascule_images.sh`.

### La vraie cause : `transformers` ne connaît pas le modèle du tout

Le 400 levé, les images sont acceptées et encodées — 223 jetons pour un
448×448 — mais le modèle **ne voit rien de juste** : 0/4 sur des couleurs
unies, 0/3 sur du comptage de carrés, et « droite » que le carré soit à gauche
ou à droite. Un 200 ne prouvait rien.

Premier indice, dans le journal du moteur, répété cinq fois :

```

[transformers] Unrecognized keys in `rope_parameters` for 'rope_type'='default':

               {'mrope_section', 'mrope_interleaved'}

```

J'ai d'abord conclu « le mRoPE est jeté ». **C'était incomplet**, et l'écart
compte : un avertissement sur des clés inconnues ne prouve pas qu'elles sont
supprimées. En sondant le vrai chemin de code sur le pod :

```

ValueError: The checkpoint you are trying to load has model type `qwen4_exp`

            but Transformers does not recognize this architecture.

processeur      : Qwen3VLProcessor        <-- repli sur un AUTRE modèle

image_processor : Qwen2VLImageProcessor

```

`transformers` **5.15.1**, embarqué dans l'image vLLM d'aperçu, ne connaît pas
`qwen4_exp` : `AutoConfig` **lève**, au lieu de construire un `Qwen4ExpConfig`.
Le modèle ne reçoit donc jamais sa configuration propre — `mrope_section

[11, 11, 10]` compris, qui est ce qui donne aux patchs d'image leurs positions

en 3D. Le texte n'en souffre pas ; l'image perd toute géométrie, tandis que le

nombre de jetons, lui, reste plausible.



**Deux fausses pistes que la réparation a démenties, et qu'il faut nommer :**



* j'ai d'abord accusé `AutoProcessor`, qui tombait en repli sur

  `Qwen3VLProcessor`. **Faux** : après correction, le processeur est *toujours*

  `Qwen3VLProcessor` / `Qwen2VLImageProcessor` et la vision marche — c'est

  simplement le processeur légitime de ce modèle ;

* l'avertissement `Unrecognized keys in rope_parameters` **subsiste** dans le
  journal avec 5.16.1, vision fonctionnelle à l'appui. Il n'a jamais été le
  mécanisme. C'était le symptôme le plus visible, et le plus trompeur.

C'est la signature à retenir : *le compte de jetons peut être juste pendant que

le contenu est faux*. Le seul test qui tranche est un test qui **discrimine** —
trois couleurs distinctes, deux comptages, deux latéralités.

Deux fausses pistes écartées au passage : la tour de vision est **exclue de la

quantification** (`model.visual.*` dans `exclude_modules` de
`hf_quant_config.json`), donc intacte en BF16 ; et le pont traduisait déjà
correctement les blocs Anthropic. Le `smoke_report.json` du dépôt RadixArk est
d'ailleurs **purement textuel** : personne n'avait validé la vision de ce quant.

**Le correctif est une montée de version** : `transformers 5.16.1` (publié sur
PyPI) contient `transformers/models/qwen4_exp/`. L'opération se fait dans le
conteneur, avec **revert automatique** vers 5.15.1 si la reconnaissance échoue
ou si le moteur ne redémarre pas — un endpoint texte qui marche vaut mieux
qu'une vision hypothétique. Script : `monter_transformers.sh`, aides dans
`aides/` sur le dépôt bootstrap.

### Résultat mesuré (27/08, 19 h 31)

```

transformers   5.15.1 -> 5.16.1        tokenizers 0.22.2 -> 0.23.1

config class   ValueError -> Qwen4ExpConfig / Qwen4ExpTextConfig

mrope_section  absent -> [11, 11, 10]

KV             596 630 jetons, concurrence 2,28x

redemarrage    200 s

```

| épreuve | avant | après |
|---|---:|---:|
| couleurs unies (rouge, vert, bleu) | 0/3 | **3/3** |
| comptage (1 et 3 carrés) | 0/2 | **2/2** |
| latéralité (gauche, droite) | 0/2 | **2/2** |
| **total** | **0/7** | **7/7** |

Confirmé indépendamment depuis l'extérieur, par le proxy RunPod — la sonde du
pod tapait sur `127.0.0.1`, ce qui ne prouve pas le chemin client. Sur une
scène composite : « deux carrés noirs situés à gauche, l'un au-dessus de
l'autre », plus le carré rouge ; et une forme en T lue comme un T.

### Deuxième épreuve : lire une vraie capture d'écran

Les 7/7 ci-dessus portent sur de la **géométrie** — couleurs, comptage,
latéralité. Ils prouvent que le pipeline d'image est correctement câblé, pas
que le modèle sait lire un terminal à 14 px. Ce sont deux régimes différents,
et c'est le second que Claude Code envoie. Rendu sur le pod avec Pillow et
DejaVuSansMono, à des densités réalistes :

| épreuve | image | jetons | résultat |
|---|---|---:|---|
| erreur de build (terminal sombre, 16 px) | 1200×400 | 501 | **OK** — `TS2345 \| src/services/auth.ts \| 142` |
| extrait de code (fond clair, 16 px) | 1200×400 | 499 | **OK** — `fibonacci` / `4096` |
| transcription à **13 px** | 1200×200 | 258 | **OK** — mot pour mot |
| disposition d'interface (deux boutons) | 900×300 | 287 | **OK** — « Enregistrer », bleu |

**4/4.** Le troisième est le plus significatif : à 13 px, la ligne
`EngineDeadError: ple_offload/connector.py queue full` est restituée
exactement, sans approximation.

**Budget à connaître : une capture plein écran coûte ~500 jetons de contexte.**
Sur un cache de 596 630 jetons partagé entre agents, une dizaine de captures
dans une conversation restent négligeables ; une centaine ne le sont plus.

**Coût de l'opération** : `--kv-cache-memory-bytes` 15,0 → 14,5 GiB, soit
617 633 → 596 630 jetons de cache (−3,4 %). C'est le prix de la marge pour
l'encodeur d'images, pas celui du multimodal lui-même.

Le redémarrage coûte plus cher que d'habitude : changer `--limit-mm-per-prompt`
**invalide le cache `torch.compile` et le warmup**, soit ~4-5 min au lieu de
2 min 30. Une seule fois ; le redémarrage suivant à configuration identique
retrouve son temps normal.