Instructions to use patdev/k3-a40-bootstrap with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use patdev/k3-a40-bootstrap with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf patdev/k3-a40-bootstrap:BF16 # Run inference directly in the terminal: llama cli -hf patdev/k3-a40-bootstrap:BF16
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf patdev/k3-a40-bootstrap:BF16 # Run inference directly in the terminal: llama cli -hf patdev/k3-a40-bootstrap:BF16
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf patdev/k3-a40-bootstrap:BF16 # Run inference directly in the terminal: ./llama-cli -hf patdev/k3-a40-bootstrap:BF16
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf patdev/k3-a40-bootstrap:BF16 # Run inference directly in the terminal: ./build/bin/llama-cli -hf patdev/k3-a40-bootstrap:BF16
Use Docker
docker model run hf.co/patdev/k3-a40-bootstrap:BF16
- LM Studio
- Jan
- Ollama
How to use patdev/k3-a40-bootstrap with Ollama:
ollama run hf.co/patdev/k3-a40-bootstrap:BF16
- Unsloth Desktop
- Docker Model Runner
How to use patdev/k3-a40-bootstrap with Docker Model Runner:
docker model run hf.co/patdev/k3-a40-bootstrap:BF16
- Lemonade
How to use patdev/k3-a40-bootstrap with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull patdev/k3-a40-bootstrap:BF16
Run and chat with the model
lemonade run user.k3-a40-bootstrap-BF16
List all available models
lemonade list
- Atomic Chat
Download DEPLOY.md from patdev/k3-a40-bootstrap: direct link, hf CLI and curl.
- Browser
- Download file 69.6 kB
-
https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/DEPLOY.md
- Command line
-
hf download hf://patdev/k3-a40-bootstrap/DEPLOY.md
-
curl -L -o DEPLOY.md https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/DEPLOY.md
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 » 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 :
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=1est 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 enfutex_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 enfatal, scrute le Hub et recharge. - Hôte malade, signature complète (pod
udpfa0l4z65m09, 21/08) : première génération →CUDA error: unspecified launch failuredanssynchronize(); ensuite un GPU disparaît pour CUDA (No CUDA GPUs are availableen le ciblant seul,nvidia-smile liste encore, pstate P0 avec puissance N/A) ;torch.cuda.device_count()dit 2 maisget_device_name(1)échoue ; vLLM TP=2 meurt sur "DP adjusted local rank 1 is out of bounds for 1 devices" ; etPOST /pods/{id}/restartrépond 500 "context deadline exceeded" vershapi.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 --versiondans un Dockerfile meurt sur un constructeur sans GPU ("Failed to infer device type") ; vérifier viaimportlib.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
waitsans 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. Toujourswait "$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
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 :
# 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.pyle fait). --max-num-batched-tokens 65536: neutre sur A40 (prefill borné par le calcul).--numa-binddemande--numa-bind-nodes 0 1dans un conteneur, et le membind est refusé sansSYS_NICE.- Le proxy RunPod renvoie 403 à l'User-Agent Python par défaut : toujours
poser un
User-Agentdans les clients de test.
Raisonnement : trois correctifs du pont (22/08 soir, mesurés)
- Le raisonnement n'arrivait jamais à Claude Code. vLLM 0.27.1 diffuse le
raisonnement en flux sous
delta.reasoning(etreasoning_contenten 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 blocthinkingsur la même question. - Le niveau de raisonnement (budget) n'était pas transmis. Anthropic
thinking.budget_tokens→ vLLMthinking_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. - Le raisonnement de l'historique était jeté. Dans les boucles d'outils,
Claude Code renvoie le bloc
thinkingdu tour précédent ; le pont l'ignorait → le modèle perdait son plan entre deux appels. Corrigé : les blocsthinkingd'un tour assistant deviennentreasoning_contentdu 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_anthropicle fait, avecsignature. cache_read_input_tokensjamais exposé : deux causes — le pont ne lisait pasprompt_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_tokensignorait système et outils (len/4 des seuls messages : 13 jetons pour un appel qui en pesait 398) → compte réel via/tokenizede vLLM en forme chat, repli sur l'estimation.stop_sequences: la chaîne coupait bien maisstop_reasondisaitend_turn→stop_sequence+ champstop_sequence(vLLM le rapporte danschoice.stop_reason).signature_deltaabsent 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 queoutput_config.effort) ; - les alias
claude-*/[1m]aveccontext_windowdans/v1/models(Claude Code litid/display_namepour 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_URLsur le port vLLM etANTHROPIC_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 16384gè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.
- 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é →
/healthbloqué, agents à 0 jeton. Corrigé :request.is_disconnected()ferme l'amont,httpx.Limits(512)+Timeout(pool=10), chien de garde du pont (relance sur 2/healthKO). Diagnostic :ss -tn | grep -c :18081vs nombre de clients. [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).- 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. - Compteur de jetons figé — le pont n'émettait qu'un
message_deltafinal ; il émet désormais unmessage_deltatoutes les 0,5 s avecoutput_tokenscumulé (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, maisVL_MODELredevient surchargeable — seul levier quand on n'a pas de shell sur le pod.- Journal distant : la fin de
/tmp/boot.loget de/tmp/vllm.logest republiée toutes les 25 s surpatdev/k3-a40-bootstrap/etat/<POD_ID>.log, tant que/v1/modelsne 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
Gathers'intercale entre la constante quantifiée et sonDequantizeLinear. 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
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 fp8ne rend rien (277,8 enautocontre 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-schedulinget les graphes CUDA complets.VL_SPECreste àoff. - Chercher un autre noyau.
--moe-backend humminget--linear-backend hummingdonnent 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
# 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
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-.
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 :
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.base64devient une URI de données,source.urlpasse tel quel, et les images portées par untool_resultsont 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 surQwen3VLProcessor. Faux : après correction, le processeur est toujoursQwen3VLProcessor/Qwen2VLImageProcessoret la vision marche — c'est simplement le processeur légitime de ce modèle ; - l'avertissement
Unrecognized keys in rope_parameterssubsiste 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.