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
69.6 kB
| # 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. | |