CoolFace
Modelpublic

patdev/k3-a40-bootstrap

sourceHugging Faceotherupdated 19d agoView on Hugging Face
0likes1.3kdownloads
vllm_bootstrap.sh1404 linesDownload Raw Back to root
1#!/usr/bin/env bash2# Un seul pod, reconfigurable sans jamais le recreer.3#4# Les `args` d'un pod Runpod sont figes a la creation : changer un flag vLLM5# imposait de detruire et recreer la machine, ce qui a coute plusieurs pods6# dans cette session. Ici le pod ne connait qu'une URL ; toute la configuration7# vit dans ce fichier, publie sur le Hub. Publier une nouvelle version suffit :8# le watcher la detecte en 30 s, tue le serveur et se reexecute en place. Le9# disque conteneur survit tant que le pod n'est pas arrete, donc les poids10# deja telecharges ne le sont pas deux fois.11set -uo pipefail12 13BOOTSTRAP_URL="https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/vllm_bootstrap.sh"14# vLLM, interne. PAS 8081 : les images runpod/pytorch embarquent un nginx qui15# ecoute deja sur 3001, 7270, 7861, 8001, 8081 et 9091 pour le proxy de ports de16# la console. vLLM y mourait sur "OSError: [Errno 98] Address already in use",17# message qui laisse croire a un residu de la tentative precedente alors que le18# port n'a jamais ete libre. 8080 reste libre pour le pont.19# ------------------------------------------------------------------ EXPERIENCE20# Defauts des deux leviers mesures sur le pod. Le pod recharge ce script a chaud21# des qu'il change sur le Hub : modifier ici et republier suffit, sans recreer22# la machine ni retelecharger les poids.23#   VL_KV   : "" (bf16) | turboquant_k3v4_nc | turboquant_k8v4 | fp824#   VL_SPEC : off | on (ngram adaptatif) | dspark (brouillon RedHatAI, tete25#             de confiance = verification adaptative) | mtp26#27# MESURE 22/08 sur 2x A40, Ornith (RESULTS_ORNITH_A40.md) :28#   base bf16        100 j/s solo   1 380 a 32 sessions   KV 2,79 M29#   turboquant_k3v4  107-114        1 303                 KV 12,08 M  <- defaut30#   dspark seul      171,5          1 360                 KV 2,61 M31#   dspark+turboquant : echec init ("No valid attention backend"), sm86.32# MESURE 22/08 soir, 6 agents Claude Code a 65-120 k de contexte (metriques33# vLLM en direct) : generation agregee 2-85 j/s, prefill 5-8,6 k j/s en34# continu, cache de prefixe 62 %. Deux causes : (1) avec TurboQuant le bloc35# d'attention monte a 4 672 jetons pour egaler la page Mamba (784 en bf16) et36# chaque tour re-prefill jusqu'a 4,7 k ; (2) un pas de 16 384 jetons bloque37# les decodes des autres sessions ~2,5 s par tranche. Defaut depuis v56 :38# bf16 sans speculation (blocs de 784, pas de dequantification TurboQuant au39# prefill) et pas de 4 096. KV 2,7 M jetons : 1M en solo tient, ~20 agents a40# 120 k aussi ; remettre VL_KV=turboquant_k3v4_nc seulement pour >10 sessions41# a 1M simultanees.42# DSPARK EXCLU PAR DEFAUT (v58, mesure) : vLLM 0.27.1 n'implemente DSpark que43# dans le Model Runner V2, et V2 rejette toute requete portant44# thinking_token_budget ("not yet supported by the V2 model runner", erreur45# 400 renvoyee a Claude Code des qu'il envoie effort/budget). Donc DSpark et46# les niveaux d'effort s'excluent : VL_SPEC=dspark coupe le patch effort.47# Le 0,93 d'utilisation avec DSpark a aussi frole l'OOM a la capture des48# graphes (220 Mio libres) : prendre 0,90 dans ce cas.49: "${VL_KV:=}"50: "${VL_SPEC:=off}"51: "${VL_MTP_N:=2}"52# MTP native MESUREE (v64, tete BF16 chargee grace au patch config, n=2) : PERD53# PARTOUT sur ce quant. Solo 1 k : 100,7 j/s (117 sans) ; solo 85 k : 27,6 j/s54# (100 sans) ; 6 agents x 85 k : 14,2 j/s/flux (26,4 sans). Acceptation :55# position 0 = 55 %, position 1 = 9 % (0,64 jeton accepte par brouillon) -- la56# tete BF16 predit mal sur les etats caches decales par l'INT4 (avertissement57# mattbucci confirme), et sa couche d'attention pleine paie les 85 k a chaque58# pas. KV 2,41 M au lieu de 2,84 M. VL_SPEC=mtp reste disponible (patch config59# automatique), mais ce n'est pas un levier ici.60# BANC "6 agents x 4 tours x ~80 k de contexte" (agent_bench.py, 22/08 soir),61# decodage par flux / agrege / cache :62#   v59 bf16 FA2, pas 4096           26,4 j/s / 55 / 99 %   <- retenu (v62)63#   v60 bf16 FlashInfer, pas 4096    28,5 j/s / 61 / 99 %   (= bruit, et64#                                    incompatible TurboQuant)65#   v61 TurboQuant k3v4, pas 4096    15,0 j/s / 40 / 97 %   (dequantification a66#                                    chaque pas > gain de bande passante sur sm86 ;67#                                    blocs de 4 672 -> 4,6-9 k re-prefill par tour)68#   v55 TurboQuant, pas 16384        2-12 j/s (agents reels, affames par le prefill)69# Un flux seul fait 100 j/s a 85 k comme a 1 k : la limite est la lecture du KV70# de 6 contextes a chaque pas (5 Go/pas en bf16 sur une A40 a 696 Go/s) plus le71# noyau de decodage ; ni FlashInfer ni TurboQuant ne la levent. Au-dela, c'est72# la bande passante de la carte (A100/H100), pas un flag.73# Backend d'attention des couches pleines (VL_ATTN vide = choix vLLM, FA2 sur74# sm86). Mesure v59, 6 flux x 85 k de contexte : 25 j/s par flux (150 agrege)75# alors qu'un flux seul a 85 k fait 100 j/s -> le decodage concurrent a long76# contexte est limite par le noyau, pas par la memoire. Essai FLASHINFER (v60).77: "${VL_ATTN:=}"   # FLASHINFER mesure = FA2 (v60) ; incompatible TurboQuant78[ -n "$VL_KV" ] && case "$VL_KV" in turboquant*) VL_ATTN="";; esac79: "${VL_DSPARK_MODEL:=RedHatAI/Qwen3.6-35B-A3B-speculator.dspark}"80: "${VL_DSPARK_N:=8}"81# Echantillonneur flashinfer : compile a la volee des que nvcc est present82# (image precompilee). Mesure neutre en debit sur A40 ; et le 21/08, pod83# udpfa0l4z65m09 (pilote 580, CUDA 13.0 natif), la premiere generation tuait le84# worker sur "CUDA error: unspecified launch failure" avec lui actif, alors que85# la meme configuration sans lui avait servi 4 h la veille. Off par defaut.86: "${VL_FLASHINFER:=off}"87# Demarrage plus court (mesure v54 : 2 min 33, dont 36 s d'init API server =88# tokenizer + processeurs image/video + appels HF Hub). Deux leviers sans effet89# sur le service : pas de processeur video (on garde les images pour la vision90# de Claude Code), et Hub hors-ligne quand les poids sont deja en cache.91: "${VL_NOVIDEO:=on}"92: "${VL_HFOFFLINE:=auto}"   # auto = hors-ligne si le modele est deja dans le cache HF93: "${VL_ALIASES:=claude-3-5-haiku claude-haiku-4-5 claude-sonnet-5 claude-opus-5 deepseek-v4-flash deepseek-v4-pro deepseek-v4-flash[1m] deepseek-v4-pro[1m] claude-sonnet-5[1m] claude-opus-5[1m]}"94# Suffixe [1m] : Claude Code en deduit une fenetre de 1M ; sans lui il suppose95# ~200 k pour un nom inconnu et declenche la compaction automatique (22/08 nuit :96# "Prompt is too long / summarization produced empty response" sur un agent a97# ~170 k). Le pont accepte n'importe quel nom, mais autant les lister.   # noms acceptes par vLLM en natif (tous -> le modele servi)98# Pont Anthropic optionnel. vLLM 0.27.1 sert deja /v1/messages et99# /v1/messages/count_tokens (thinking+signature, outils, images, ping, cache).100#   on  : le pont ecoute sur 8080 et relaie vers vLLM (18081) -- budget de101#         raisonnement (thinking_token_budget), keepalive de prefill derriere102#         le proxy RunPod, /v1/models au format Anthropic, garde agentique.103#   off : vLLM ecoute DIRECTEMENT sur 8080 (le seul port expose), natif.104# Dans les deux cas vLLM sert les alias claude-<nom> et claude-<nom>[1m].105: "${VL_BRIDGE:=on}"    # on par defaut depuis v68 : le proxy RunPod COUPE une106                        # connexion silencieuse ~100 s (mesure : TTFT 104 s en107                        # file de prefill -> "Response ended prematurely"), et108                        # vLLM natif n'emet rien avant le premier jeton. Le pont109                        # envoie un ping SSE toutes les 15 s, mappe effort et110                        # budget. VL_BRIDGE=off = natif (patch effort).111# Marge VRAM. Le backend TurboQuant de vLLM 0.27.1 DEQUANTIFIE le K en cache en112# bf16 dense a chaque tranche de prefill (turboquant_attn.py113# _continuation_prefill) : a ~700 k jetons sur Qwen3.8 il lui a manque 516 Mio114# (OOM, moteur tue, aiguille perdue) avec 0,93. 0,85 laisse ~3,5 Gio par GPU ;115# le KV perd ~12 %, le prefill long passe.116: "${VL_UTIL:=${VL_KV:+0.85}}"; : "${VL_UTIL:=0.93}"   # 0,85 seulement avec TurboQuant117# Prefill long (doc vLLM "optimization") :118#   VL_BATCHED     : jetons par pas d'ordonnancement (16384 defaut vLLM). Plus119#                    haut = moins de tranches pour un gros prompt = prefill a120#                    froid plus rapide, au prix de la latence inter-jeton des121#                    autres sessions pendant ce temps.122#   VL_LONGPREFILL : seuil (jetons) au-dela duquel une requete est "longue"123#   VL_PARTIAL     : nombre max de prefills partiels concurrents par pas124# Vides = defauts vLLM. Mesures avec prefill_bench.py (8k/32k/128k, froid/chaud).125: "${VL_BATCHED:=8192}"    # v83 (23/08 soir) : 4096 -> 8192. Balayage de 10 leviers126                           # sur RTX PRO 6000, Nemotron NVFP4, contexte 131072 :127                           # 8192 rend +10,2 % en agrege (1003 vs 910 a 8 sessions)128                           # et +9,2 % en prefill (24050 vs 22015 j/s), pour un129                           # solo inchange (276,9 vs 275,3, dans le bruit).130                           # Le regime "agents lecteurs" ci-dessous restait131                           # indifferent entre 2048 et 4096 ; il n'avait pas ete132                           # teste au-dela. 16384/65536 : prefill identique, mais les decodes des133                           # autres sessions gelent pendant chaque tranche (v56).134                           # A/B v66 (2048) vs v67 (4096), banc "agents lecteurs"135                           # (6 agents, +65 k par tour, 80 -> 144 k) : 7,0 vs 7,1 j/s136                           # par flux, 15 agrege, TTFT 37 vs 41 s -> identique : ce137                           # regime est borne par le CALCUL de prefill (390 k jetons138                           # par tour de table = 60 s), pas par l'ordonnanceur.139: "${VL_LONGPREFILL:=}"140: "${VL_PARTIAL:=}"141# Leviers de docs.vllm.ai/configuration/optimization (v0.27.1), mesures en v43 :142#   VL_OPT   : --optimization-level 0..3 (3 = compile max, demarrage plus long)143#   VL_NUMA  : --numa-bind, workers epingles au noeud NUMA de leur GPU (les deux144#              A40 sont sur deux noeuds : c'est exactement notre cas)145#   VL_EP    : --enable-expert-parallel, experts MoE repartis entre les GPU au146#              lieu du TP sur chaque expert147#   VLLM_USE_FASTOKENS=1 : tokenizer Rust (prefill / TTFT)148: "${VL_OPT:=}"       # O3 == O2 (defaut) dans les sources 0.27.1149: "${VL_NUMA:=off}"   # mesure neutre (v43d), et membind refuse sans SYS_NICE150# Le conteneur ne laisse pas vLLM deduire la topologie GPU->NUMA (v43b :151# "could not detect the GPU-to-NUMA topology automatically") : on la donne,152# GPU0 -> noeud 0, GPU1 -> noeud 1 d'apres `nvidia-smi topo -m`. Les valeurs153# sont passees a `numactl --membind`, installe plus bas s'il manque.154: "${VL_NUMA_NODES:=0 1}"   # liste separee par des ESPACES (nargs), pas de virgule155: "${VL_EP:=off}"     # mesure neutre (v43d)156# fastokens n'est PAS dans l'image (ImportError "The 'fastokens' package157# (>= 0.2.0) is required when VLLM_USE_FASTOKENS=1", v43). Off tant que l'image158# ne l'embarque pas ; a ajouter au Dockerfile pour le mesurer.159: "${VL_FASTOK:=1}"   # installe a la volee plus bas si absent (image v2) ; image v3 l'embarque160# --kv-cache-memory-bytes : saute le profilage memoire au (re)demarrage. Valeur161# MESUREE sur cette config exacte (Ornith, TP=2, TurboQuant, sans brouillon) :162# "Available KV cache memory: 26.72 GiB" par GPU -> 26,5 GiB pour garder une marge.163# Ne s'applique qu'a cette config : un brouillon DSpark ou un autre modele164# changent la memoire libre, et une valeur trop haute fait mourir le moteur.165: "${VL_KVBYTES:=}"   # MESURE v45 : aucun gain de demarrage (le warmup de 65 s reste), et une valeur fausse tue le moteur -> off166: "${VL_DP:=1}"167# Paire d'A40 sur deux noeuds NUMA (nvidia-smi topo : SYS, aucun P2P). Sans ce168# drapeau, le premier collectif NCCL apres la capture des graphes CUDA tourne169# en attente active : deux GPU a 100 % d'occupation SM, 0 % d'activite memoire,170# workers en futex_wait, et "No available shared memory broadcast block" toutes171# les 60 s. Observe le 21/08 sur le pod udpfa0l4z65m09 ; documente dans172# DEPLOY.md mais jamais pose ici -- l'ancien hote avait un P2P qui marchait.173export NCCL_P2P_DISABLE=1 NCCL_IB_DISABLE=1174# La variable n'atteint PAS les workers (verifie dans /proc/<worker>/environ :175# presente pour `vllm serve`, absente pour VLLM::Worker_TP0), et le spin est176# revenu a l'identique le 22/08 sur un second hote. NCCL lit aussi177# ~/.nccl.conf a l'initialisation, independamment de l'environnement : c'est178# la voie qui traverse tout. Et --disable-custom-all-reduce coupe l'autre179# chemin P2P (l'all-reduce maison de vLLM), au cas ou c'est lui qui spinne.180printf 'NCCL_P2P_DISABLE=1181NCCL_IB_DISABLE=1182' > /root/.nccl.conf183PORT="${VL_PORT:-18081}"184# v80 : surchargeable — le frontend HTTP du mode MP de LMCache prend 8080 par185# defaut et volait le port du pont (bind clash sur deux hotes de suite).186PROXY_PORT="${VL_PROXY_PORT:-8080}"      # pont Anthropic, seul port expose par le pod187[ "$VL_BRIDGE" = off ] && PORT="$PROXY_PORT"   # natif : vLLM prend le port expose188# v85 : VL_BRIDGE=keepalive. vLLM sert /v1/messages NATIVEMENT (il retire les189# blocs x-anthropic-billing-header qui detruisaient notre cache de prefixe :190# 0 % avec l'ancien pont, 98,5 % en natif), mais le proxy RunPod coupe toute191# connexion silencieuse a ~120 s et vLLM n'emet rien avant le premier jeton192# (mesure : prompt de 480 K tue a 126,5 s, zero jeton). Le petit pont ne fait193# QUE le battement SSE, sans toucher au JSON. vLLM reste donc en interne.194# Banniere de version : sans elle, impossible de distinguer "la correction n'a195# pas marche" de "la correction n'est pas encore arrivee", et on debogue le196# mauvais probleme.197SCRIPT_VERSION="v86-journal-runtime-continu"198# v74 : le journal est double dans un fichier pour etre republie sur le Hub.199log() { echo "[VL] $(date -u +%H:%M:%S) $*" | tee -a /tmp/boot.log; }200 201fatal() {202  log "FATAL $*"; log "conteneur maintenu ouvert; scrutation d'une mise a jour"203  # v71 : publier la fin du journal vLLM, sinon la cause est invisible depuis204  # l'exterieur (le journal distant appartient au processus precedent).205  { echo "FATAL $*"; echo "--- /tmp/vllm.log (fin) ---"; tail -120 /tmp/vllm.log 2>/dev/null; } > /tmp/fatal.log206  python3 - /tmp/fatal.log "etat/fatal-${RUNPOD_POD_ID:-pod}.log" <<'PYF' >/dev/null 2>&1 || "$VENV/bin/python" - /tmp/fatal.log "etat/fatal-${RUNPOD_POD_ID:-pod}.log" <<'PYF2' >/dev/null 2>&1207import os, sys208from huggingface_hub import HfApi209HfApi().upload_file(path_or_fileobj=sys.argv[1], path_in_repo=sys.argv[2], repo_id="patdev/k3-a40-bootstrap", token=os.environ.get("HF_TOKEN"))210PYF211import os, sys212from huggingface_hub import HfApi213HfApi().upload_file(path_or_fileobj=sys.argv[1], path_in_repo=sys.argv[2], repo_id="patdev/k3-a40-bootstrap", token=os.environ.get("HF_TOKEN"))214PYF2215  local cur new; cur=$(md5sum /run.sh 2>/dev/null | cut -d' ' -f1)216  while sleep 30; do217    curl -sL "$BOOTSTRAP_URL" -o /run.next 2>/dev/null || continue218    [ -s /run.next ] || continue219    new=$(md5sum /run.next | cut -d' ' -f1)220    [ "$new" != "$cur" ] && { log "mise a jour, reexecution"; mv /run.next /run.sh; exec bash /run.sh; }221  done222}223 224log "bootstrap $SCRIPT_VERSION"225export DEBIAN_FRONTEND=noninteractive HF_XET_HIGH_PERFORMANCE=1 HF_HUB_DISABLE_PROGRESS_BARS=1226 227# ------------------------------------------------------------------ dependances228# Le pilote de l'hote est une LOTERIE : Runpod place le pod sur une machine en229# CUDA 12.4, 12.8 ou 13.0 selon la disponibilite. Le vLLM de PyPI embarque un230# torch compile pour CUDA 13.x, qui meurt sur un hote 12.8 avec :231#   RuntimeError: The NVIDIA driver on your system is too old (found version 12080)232# Le meme script marchait hier et pas aujourd'hui, uniquement parce que le233# premier tirage etait tombe sur un hote 13.0.234#235# `uv pip install --torch-backend=auto` detecte le pilote et prend le torch236# correspondant, ce qui rend le demarrage independant de l'hote obtenu.237DRIVER=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1)238CUDA_RT=$(nvidia-smi 2>/dev/null | grep -o "CUDA Version: [0-9.]*" | head -1 | cut -d' ' -f3)239log "pilote $DRIVER, CUDA runtime ${CUDA_RT:-inconnu}"240 241# Environnement dedie. Les images Ubuntu 24.04 / Python 3.12 marquent le Python242# systeme "externally-managed" (PEP 668) : `pip install` ET `uv pip --system`243# refusent tous les deux, avec un message qui parle de pipx et de venv sans244# jamais dire "installation ignoree". Le bootstrap continuait donc jusqu'a245# `vllm: command not found` 12 secondes plus tard, ou la cause reelle n'est246# plus visible. Un venv evite le probleme au lieu de le contourner avec247# --break-system-packages, qui casserait le Python de l'image.248VENV=/opt/venv249export PATH="$VENV/bin:$PATH"250 251# Compatibilite ascendante CUDA. Depuis la 0.25, TOUTE version de vLLM qui252# connait l'architecture qwen3_5 depend de nvidia-cutlass-dsl[cu13] et253# humming-kernels[cu13] : la roue est compilee pour CUDA 13, point. Or Runpod254# place le pod sur un hote en 12.4, 12.8 ou 13.0 selon la disponibilite, et255# rien ne permet d'epingler la version CUDA a la creation d'un pod.256#257# Sur un hote 12.8 le symptome est "ImportError: libcudart.so.13", et si on258# corrige en forçant un torch cu12 (--torch-backend=auto) on obtient l'autre259# moitie du probleme : torch se charge mais le binaire vLLM reclame toujours260# cudart 13.261#262# La sortie n'est pas de choisir son hote mais de rendre le pod indifferent :263# cuda-compat-13-0 installe un userspace pilote 580 a cote du noyau 570, ce que264# NVIDIA supporte officiellement sur les cartes datacenter comme l'A40.265# VERIFIE sur cet hote : sans lui torch.cuda.is_available() est False266# ("driver too old, found version 12080") ; avec lui, 2 cartes, matmul bf16 OK,267# capacite (8,6). On ne l'installe QUE si le pilote est anterieur a 580 : par-268# dessus un pilote plus recent, la bibliotheque de compatibilite serait plus269# ancienne que l'hote et NVIDIA deconseille de la prefixer.270DRIVER_MAJOR=${DRIVER%%.*}271if [ -n "${DRIVER_MAJOR:-}" ] && [ "$DRIVER_MAJOR" -lt 580 ] 2>/dev/null; then272  if [ ! -d /usr/local/cuda-13.0/compat ]; then273    log "pilote $DRIVER < 580 : installation de cuda-compat-13-0"274    apt-get update -qq >/dev/null 2>&1275    DEBIAN_FRONTEND=noninteractive apt-get install -y -qq cuda-compat-13-0 >/dev/null 2>&1       || log "WARNING cuda-compat-13-0 indisponible"276  fi277  if [ -d /usr/local/cuda-13.0/compat ]; then278    export LD_LIBRARY_PATH="/usr/local/cuda-13.0/compat:${LD_LIBRARY_PATH:-}"279    log "compat CUDA 13 active (LD_LIBRARY_PATH)"280  fi281 282  # Compilateur CUDA 13. L'image runpod/pytorch n'embarque AUCUN nvcc, et283  # flashinfer compile ses noyaux a la volee contre des en-tetes CUDA 13 :284  #   error "CUDA compiler and CUDA toolkit headers are incompatible"285  #   RuntimeError: Ninja build failed286  # Le defaut ne se voit pas tout de suite -- le cache de compilation de vLLM287  # (~1,2 Go dans ~/.cache/vllm) couvre les cas deja rencontres. Il ressort des288  # qu'un changement de configuration invalide une cle de cache, ce qui donne289  # l'illusion d'une regression alors que le compilateur a toujours manque.290  if [ ! -x /usr/local/cuda-13.0/bin/nvcc ]; then291    log "installation de cuda-nvcc-13-0 (aucun nvcc dans l'image)"292    DEBIAN_FRONTEND=noninteractive apt-get install -y -qq cuda-nvcc-13-0 >/dev/null 2>&1       || log "WARNING cuda-nvcc-13-0 indisponible"293  fi294  if [ -x /usr/local/cuda-13.0/bin/nvcc ]; then295    export CUDA_HOME=/usr/local/cuda-13.0296    export PATH="$CUDA_HOME/bin:$PATH"297    log "nvcc $(/usr/local/cuda-13.0/bin/nvcc --version | grep -o 'V[0-9.]*' | tail -1)"298    # Les en-tetes que reclament les noyaux compiles a la volee. Sans curand.h,299    # flashinfer echoue sur son noyau d'echantillonnage :300    #   flashinfer/sampling.cuh:20:10: fatal error: curand.h: No such file301    # Chaque en-tete manquante fait tomber le serveur entier, d'ou le garde-fou302    # ci-dessous plutot qu'une course a l'en-tete suivante.303    if [ ! -f /usr/local/cuda-13.0/include/curand.h ]; then304      log "installation des en-tetes CUDA 13 (curand, cublas, cusparse, cusolver)"305      # Un par un : un seul nom errone fait echouer TOUT le lot apt, et le306      # message ne dit pas lequel. C'est ce qui s'est passe avec307      # "cuda-curand-dev-13-0", qui n'existe pas -- le nom est libcurand-dev-13-0.308      for pkg in libcurand-dev-13-0 libcublas-dev-13-0 cuda-cccl-13-0                  cuda-cudart-dev-13-0 libcusparse-dev-13-0 libcusolver-dev-13-0; do309        DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$pkg" >/dev/null 2>&1           || log "WARNING $pkg indisponible"310      done311    fi312  fi313 314  # Filet de securite : l'echantillonneur flashinfer est une OPTIMISATION, mais315  # sa compilation a la volee est un point de defaillance unique -- une seule316  # en-tete absente et le moteur ne demarre pas du tout. On le coupe si les317  # en-tetes ne sont pas toutes la, plutot que de parier sur leur presence.318  # VL_FLASHINFER=off force la coupure meme si les en-tetes sont la : la319  # presence des en-tetes ne garantit pas que la compilation aboutira, et sans320  # cet interrupteur il faudrait rediter le script pour revenir en arriere.321  if [ ! -f /usr/local/cuda-13.0/include/curand.h ] || [ "${VL_FLASHINFER:-on}" = "off" ]; then322    export VLLM_USE_FLASHINFER_SAMPLER=0323    log "echantillonneur flashinfer desactive"324  else325    log "echantillonneur flashinfer actif (en-tetes CUDA 13 presentes)"326  fi327else328  log "pilote $DRIVER : compat CUDA non necessaire"329fi330 331NEED_INSTALL=1332# Image precompilee (Space patdev/ornith-vllm-a40) : vLLM, cuda-compat, nvcc et333# en-tetes sont deja la. Le marqueur dispense de la comparaison avec CUDA_RT,334# qui varie selon l'hote et forcerait une reinstallation inutile -- et non335# epinglee, donc susceptible de tirer une autre version que celle mesuree.336if [ -f /opt/.image_prebaked ] && [ -x "$VENV/bin/vllm" ]; then337  log "image precompilee : $(cat /opt/.image_prebaked)"338  NEED_INSTALL=0339elif [ -x "$VENV/bin/vllm" ] && [ -f /opt/.vllm_cuda ]    && [ "$(cat /opt/.vllm_cuda)" = "${CUDA_RT:-x}" ]; then340  NEED_INSTALL=0341fi342 343if [ "$NEED_INSTALL" = 1 ]; then344  log "PHASE deps (torch adapte au pilote de cet hote)"345  apt-get update -qq >/dev/null 2>&1346  apt-get install -y -qq python3 python3-pip curl ca-certificates jq >/dev/null 2>&1 || fatal apt347  command -v uv >/dev/null 2>&1 || pip install -q -U uv --break-system-packages >/dev/null 2>&1348  [ -x "$VENV/bin/python" ] || uv venv "$VENV" --system-site-packages 2>&1 | tail -2 ||       python3 -m venv --system-site-packages "$VENV" || fatal "creation du venv"349  # PAS de --torch-backend=auto ici : il choisirait un torch cu12 sur cet hote,350  # alors que le binaire vLLM reclame cudart 13 de toute facon. On prend donc le351  # torch cu13 par defaut, et c'est cuda-compat-13-0 ci-dessus qui fait le pont352  # avec le pilote de l'hote.353  uv pip install -q --python "$VENV/bin/python" "vllm==0.27.1" 2>&1 | tail -5 || fatal "installation de vllm"354  [ -x "$VENV/bin/vllm" ] || fatal "vllm absent du venv apres installation"355fi356 357# Bibliotheques CUDA embarquees dans le venv. Le chemin FP8/Marlin compile ses358# noyaux a la volee via NVRTC, qui fait un dlopen de libnvrtc-builtins.so.13.0 :359# le fichier EST livre avec torch (nvidia/cu13/lib) mais n'est pas sur le chemin360# du chargeur, d'ou "nvrtc: error: failed to open libnvrtc-builtins.so.13.0"361# puis "NVRTCCompiler run failed". Le quant int4 ne compilait rien et ne362# rencontrait donc jamais le probleme.363# On ajoute APRES cuda-compat, qui doit rester prioritaire pour libcuda.364# NE PAS ajouter les bibliotheques CUDA du venv a LD_LIBRARY_PATH.365#366# C'etait necessaire au chemin FP8 (NVRTC ne trouvait pas ses builtins), mais367# le FP8 s'est heurte plus loin a l'absence de nvcc 13 et a ete abandonne. Or368# cet ajout CASSE le chemin int4, qui marchait : rendre les en-tetes CUDA 13 du369# venv visibles pousse flashinfer a compiler a la volee avec le nvcc 12.8 de370# l'image, d'ou371#   error "CUDA compiler and CUDA toolkit headers are incompatible"372#   RuntimeError: Ninja build failed373# Autrement dit un correctif utile a une configuration abandonnee a fait tomber374# la configuration retenue. Ne le remettre que si le toolkit CUDA 13 complet375# (nvcc compris) est installe.376 377 378# Echouer ICI plutot que 30 s plus tard dans un worker vLLM, ou la cause reelle379# est enfouie sous "Engine core initialization failed". On verifie a CHAQUE380# demarrage, pas seulement apres installation : le chemin des bibliotheques381# vient d'etre modifie et c'est precisement ce qu'on veut valider.382"$VENV/bin/python" -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null   || fatal "torch ne voit pas le GPU (pilote $DRIVER vs torch cu13, compat absente ?)"383log "torch $("$VENV/bin/python" -c 'import torch;print(torch.__version__)') voit $("$VENV/bin/python" -c 'import torch;print(torch.cuda.device_count())') carte(s)"384echo "${CUDA_RT:-x}" > /opt/.vllm_cuda385log "vllm $(vllm --version 2>/dev/null | tail -1)"386command -v jq >/dev/null || apt-get install -y -qq jq >/dev/null 2>&1387if [ "$VL_FASTOK" = 1 ] && ! "$VENV/bin/python" -c "import fastokens" >/dev/null 2>&1; then388# ---- extras CUDA de humming (v82) -------------------------------------------389# `humming-kernels` arrive comme dependance transitive de vLLM, donc SANS ses390# extras. Or son compilateur resout CUDA en deux etapes independantes :391# `filter_cuda_paths` choisit une racine dont la majeure correspond a392# torch.version.cuda et qui contient nvrtc.h, puis `_find_nvrtc_lib_dir` y393# cherche un libnvrtc.so* -- rien ne garantit que la bibliotheque retenue, ses394# builtins et les en-tetes passes au compilateur viennent du meme paquet. D'ou395# les deux symptomes observes :396#     pod  : cuda_fp8.hpp: this declaration has no storage class  (NVRTC trop397#            ancien pour des en-tetes CUDA 13)398#     A100 : nvrtc: failed to open libnvrtc-builtins.so.13.0399# L'extra cu13 declare le jeu coherent : nvidia-cuda-{runtime,cccl,nvcc,nvrtc}.400# `nvidia-cuda-cccl` est indispensable : le compilateur de humming injecte401# #include <cuda/std/climits>, <cuda/std/cstdint>, <cuda/std/type_traits>, qui402# sont des en-tetes CCCL, et le code les cherche dans un sous-repertoire cccl/.403if [ "${VL_HUMMING_EXTRAS:-on}" = on ]; then404  log "installation de humming-kernels[cu13] (extras CUDA coherents)"405  uv pip install -q --python "$VENV/bin/python" "humming-kernels[cu13]"     >/dev/null 2>&1 || log "WARNING humming-kernels[cu13] non installable"406fi407 408  uv pip install -q --python "$VENV/bin/python" "fastokens>=0.2.0" >/dev/null 2>&1 || { log "WARNING fastokens non installable, tokenizer standard"; VL_FASTOK=0; }409fi410export VLLM_USE_FASTOKENS="$VL_FASTOK"; log "fastokens=$VL_FASTOK"411# Niveau de raisonnement en natif : le serveur Anthropic de vLLM 0.27.1 ignore412# thinking.budget_tokens / disabled et n'applique effort qu'a Harmony. Ce patch413# (idempotent) mappe effort et budget vers thinking_token_budget. Voir414# vllm_anthropic_effort_patch.py. Desactivable : VL_EFFORT_PATCH=off.415if [ "$VL_SPEC" = dspark ] && [ "${VL_EFFORT_PATCH:-on}" = on ]; then416  VL_EFFORT_PATCH=off; log "DSpark => runner V2 => thinking_token_budget refuse : patch effort coupe (niveaux d'effort ignores)"417fi418if [ "${VL_EFFORT_PATCH:-on}" = on ]; then419  curl -sL "https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/vllm_anthropic_effort_patch.py" -o /opt/vllm_anthropic_effort_patch.py 2>/dev/null420  [ -s /opt/vllm_anthropic_effort_patch.py ] && "$VENV/bin/python" /opt/vllm_anthropic_effort_patch.py 2>&1 | sed "s/^/[VL] patch effort : /" || log "WARNING patch effort non applique"421fi422[ "${VL_NUMA:-off}" = on ] && ! command -v numactl >/dev/null 2>&1 && { apt-get update -qq >/dev/null 2>&1; apt-get install -y -qq numactl >/dev/null 2>&1 || log "WARNING numactl indisponible"; }423 424# Bureau Linux distant (v71), en OPTION : VL_DESKTOP=on installe Xvnc + XFCE +425# noVNC et sert le bureau sur le port 6080 (expose en HTTP par Runpod). Lance en426# arriere-plan pour ne pas retarder d'une seconde le demarrage de vLLM, qui427# reste la raison d'etre du pod. Script : desktop_setup.sh sur le Hub.428if [ "${VL_DESKTOP:-off}" = on ]; then429  ( curl -sL "https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/desktop_setup.sh" -o /opt/desktop_setup.sh     && bash /opt/desktop_setup.sh >>/tmp/desktop.log 2>&1 ) &430  log "bureau distant demande (VL_DESKTOP=on) : installation en arriere-plan, journal /tmp/desktop.log"431fi432 433nvidia-smi --query-gpu=index,name,memory.total --format=csv,noheader434 435# Contexte adapte a la VRAM REELLE (v72 ; en v70 il etait place APRES le bloc qui436# construit deja --max-model-len, donc sans effet). Ornith consomme ~20 Ko de KV par437# jeton : 1M de contexte demande ~20 Gio de cache, ce qui suppose au moins deux438# cartes de 48 Go (26 Go de poids + 20 de KV + marge). Sur UNE carte, vLLM439# refuserait de demarrer ("max seq len larger than the maximum number of tokens440# that can be stored in KV cache"). On calcule donc un plafond soutenable.441if [ -z "${VL_CTX:-}" ]; then442  VRAM_MO=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | head -1)443  NG=$(nvidia-smi --query-gpu=index --format=csv,noheader 2>/dev/null | wc -l)444  [ "${NG:-0}" -ge 1 ] 2>/dev/null || NG=1445  VRAM_TOT=$(( VRAM_MO * NG ))446  # poids ~26 Go pour ornith int4 ; 0,90 d'utilisation ; 2 Go de marge/graphes447  DISPO=$(( VRAM_TOT * 90 / 100 - 26000 - 2000 ))448  [ "$DISPO" -lt 2000 ] && DISPO=2000449  # 20 Ko/jeton -> jetons = Mo * 1024 / 20450  CTX_MAX=$(( DISPO * 1024 / 20 ))451  if [ "$CTX_MAX" -ge 1000000 ]; then VL_CTX=1000000452  elif [ "$CTX_MAX" -ge 786432 ]; then VL_CTX=786432453  elif [ "$CTX_MAX" -ge 524288 ]; then VL_CTX=524288454  elif [ "$CTX_MAX" -ge 262144 ]; then VL_CTX=262144455  else VL_CTX=131072; fi456  export VL_CTX457  log "VRAM ${VRAM_TOT} Mo sur $NG carte(s) -> contexte $VL_CTX (plafond estime $CTX_MAX)"458fi459# Le brouillon DSpark doit etre en cache AVANT de passer hors-ligne : v56 a460# echoue sur un pod recree ("Invalid repository ID" sur le brouillon) parce que461# HF_HUB_OFFLINE=1 etait pose des qu'Ornith etait en cache.462HUBDIR="${HF_HOME:-$HOME/.cache/huggingface}/hub"463# `exec bash /run.sh` herite de l'environnement : le HF_HUB_OFFLINE=1 exporte par464# le run precedent survivait au rechargement (v57 : brouillon non telecharge,465# vLLM hors-ligne, meme erreur qu'en v56). On repart toujours en ligne.466unset HF_HUB_OFFLINE467if [ "$VL_SPEC" = dspark ] && ! ls "$HUBDIR" 2>/dev/null | grep -q "models--$(echo "$VL_DSPARK_MODEL" | tr / -)"; then468  log "telechargement du brouillon DSpark $VL_DSPARK_MODEL"469  "$VENV/bin/python" -c "from huggingface_hub import snapshot_download as s; s('$VL_DSPARK_MODEL')" >/dev/null 2>&1 || log "WARNING brouillon DSpark non telecharge"470fi471if [ "$VL_HFOFFLINE" = auto ] && ls "$HUBDIR" 2>/dev/null | grep -q "models--$(echo "$MODEL" | tr / -)"    && { [ "$VL_SPEC" != dspark ] || ls "$HUBDIR" | grep -q "models--$(echo "$VL_DSPARK_MODEL" | tr / -)"; }; then472  export HF_HUB_OFFLINE=1; log "poids en cache : HF_HUB_OFFLINE=1 (pas d'appel Hub au demarrage)"473elif [ "$VL_HFOFFLINE" = on ]; then export HF_HUB_OFFLINE=1; fi474 475# ----------------------------------------------------------------- configuration476# Le seul bloc a modifier pour un nouvel essai. Tout le reste est de la plomberie.477#478# Les deux modeles du dispositif. Ils ne tiennent PAS ensemble sur une A40479# (16,9 + 28,4 = 45,3 Gio de poids, avant le moindre KV), donc K3_WHICH choisit480# lequel est servi. Un endpoint bi-modele demande deux cartes, ou un echange a481# la demande.482# Surcharge pilotee depuis le Hub. Les variables d'environnement d'un pod sont483# figees a sa creation : sans ce cran, changer de modele imposerait de recreer484# la machine, ce que tout ce dispositif cherche precisement a eviter. Laisser485# vide pour rendre la main a VL_MODEL.486# v74 : FORCE_MODEL restait cable en dur, ce qui rendait VL_MODEL inoperant --487# or l'environnement du pod est le SEUL levier quand on n'a pas de shell488# dessus. Le defaut ne change pas ; il devient seulement surchargeable.489FORCE_MODEL="${VL_MODEL:-ornith}"490WHICH="${FORCE_MODEL:-${VL_MODEL:-qwen}}"491 492# ---------------------------------------------------------------- SWAP (v70)493# Deux modeles servis a tour de role sur la meme carte : le pont ecrit la cle494# demandee dans $SWAP_DIR/modele_demande, la boucle de surveillance arrete vLLM495# et reexecute ce script avec VL_MODEL=<cle>. Le pont survit au swap (il lit496# $SWAP_DIR/modele_courant a chaque requete) ; seul vLLM redemarre.497# La speculation est un ATTRIBUT DU MODELE, mesuree : MTP k=3 rend +75 % solo498# et +32 % a 8 sessions sur Qwen3.8-27B (tete BF16) ; elle PERD partout sur499# Ornith (v64 : 14,2 j/s/flux contre 26,4 sans). VL_SPEC_AUTO=off pour forcer500# a la main (banc).501SWAP_DIR="${VL_SWAP_DIR:-/travail}"; mkdir -p "$SWAP_DIR"502export VL_SWAP_DIR="$SWAP_DIR" VL_MODEL_KEY="$WHICH"503export VL_SWAP="${VL_SWAP:-ornith:ornithnvfp4,qwen38:qwen38nvfp4}"504if [ "${VL_SPEC_AUTO:-on}" = on ]; then505  case "$WHICH" in506    qwen38nvfp4|dense) VL_SPEC=mtp; VL_MTP_N="${VL_MTP_N_QWEN38:-3}" ;;507    ornith|ornithnvfp4) VL_SPEC=off ;;508  esac509  log "speculation par modele ($WHICH) : VL_SPEC=$VL_SPEC"510fi511 512case "$WHICH" in513  ornithfp8)514    # Quant OFFICIEL de l'editeur, au lieu de ulkaa/...AWQ-INT4 (depot d'un seul515    # auteur, publie trois jours avant usage). Deux raisons :516    #517    # 1. Sa liste `ignore` contient "re:.*mtp\..*" : la tete MTP reste dense.518    #    L'AWQ int4, lui, l'avait quantifiee, et vLLM refusait de la charger519    #    ("no module or parameter named 'fc.weight' in520    #    Qwen3_5MultiTokenPredictor" -- il ne trouvait que fc.weight_packed).521    #    La spéculation est donc possible ICI et seulement ici.522    # 2. Il vient des auteurs du modele.523    #524    # Le checkpoint est en W8A8, ce qui demanderait sm_89 -- mais vLLM retombe525    # tout seul sur CompressedTensorsW8A16Fp8 (compressed_tensors.py:835), dont526    # get_min_capability() vaut 75, "turing and up". Une A40 est en 86 : ca527    # passe par Marlin, en dequantifiant les poids.528    #529    # Cout : 39,4 Go contre 26,1, soit ~6,7 Go lus par token contre 4,4. Environ530    # 33 % plus lent en brut ; c'est la MTP qui doit compenser. Il reste ~1,9 M531    # tokens de KV, donc le 1M n'est pas menace.532    MODEL="ornith-ai/Ornith-1.5-35B-A3B-FP8"533    NAME=ornith534    EXTRA=( --tool-call-parser qwen3_xml --reasoning-parser qwen3 --trust-remote-code )535    if [ "${VL_YARN:-on}" = "on" ]; then536      export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1537      EXTRA+=( --hf-overrides '{"rope_scaling":{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":262144}}'538               --max-model-len "${VL_CTX:-1000000}" )539      log "YaRN facteur 4.0 -> contexte ${VL_CTX:-1000000}"540    else541      EXTRA+=( --max-model-len "${VL_CTX:-262144}" )542    fi543    ;;544  nemotron)545    # v75. NVIDIA-Nemotron-3.5-Lightning-30B-A3B en NVFP4 officiel NVIDIA.546    #547    # Raison chiffree, socle actif calcule sur les formes de tenseurs (octets LUS548    # par jeton en decodage, tete MTP exclue puisque la speculation est coupee) :549    #550    #   Ornith NVFP4 (servi)   2,25 Go -> plafond 426 t/s551    #   Nemotron Lightning     1,66 Go -> plafond 578 t/s   (+35 %)552    #553    # Trois atouts par rapport a Ornith :554    #   - contexte NATIF de 1 048 576 : pas de YaRN, donc pas de reechelonnement555    #     des positions courtes et pas la perte de qualite qui va avec ;556    #   - vocabulaire de 131 072 au lieu de 248 320, d'ou un lm_head de 0,20 Go557    #     contre 0,29 -- et le lm_head est lu a CHAQUE jeton ;558    #   - 128 experts en top-6 (au lieu de 256 en top-8) sur un intermediaire de559    #     1856 : moins d'experts lus par jeton.560    #561    # Quantification modelopt, activations en 8 bits (W8A8), donc chemin Marlin562    # utilisable sur sm89 -- contrairement aux NVFP4 de Kimi-Linear et de563    # Qwen3.8, qui sont en W4A4 et exigent Blackwell.564    #565    # Le gabarit de discussion emet <tool_call> et <function=>, exactement ce que566    # lit le parseur qwen3_xml, et <think>/</think> pour qwen3.567    #568    # RISQUE ASSUME : `NemotronHForCausalLM` est une architecture hybride569    # distincte de Qwen3.5 ; si vLLM 0.27.1 ne la connait pas, repli immediat par570    # VL_MODEL=ornithnvfp4.571    MODEL="nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4"572    # v84 : le nom servi etait reste `ornith`, herite du modele precedent. Les573    # identifiants exposes annoncaient donc un modele qui n'est pas charge --574    # `claude-ornith[1m]` pour du Nemotron. Corrige : `nemotron`,575    # `claude-nemotron` et `claude-nemotron[1m]`. Les alias generiques576    # (claude-sonnet-5, etc.) sont inchanges et continuent de fonctionner.577    NAME=nemotron578    # v76 : recette OFFICIELLE NVIDIA pour materiel de classe Ampere (W4A16).579    # Notre configuration par defaut donnait 238,5 tok/s en solo, soit 41 % du580    # plafond de 578 -- moins bien qu'Ornith qui en tire 47 %. NVIDIA prescrit :581    #   - `humming` pour le MoE ET les couches lineaires : Ada n'a pas d'unites582    #     FP4, donc c'est le chemin W4A16 qu'il faut, et vLLM choisissait Marlin583    #     tout seul (les deux figuraient dans sa liste de candidats) ;584    #   - `--quantization modelopt_fp4` pour forcer ce chemin ;585    #   - backend Mamba flashinfer, cache `align` et SSU `simple` : sans quoi586    #     vLLM met le cache en mode `all` des que le cache de prefixe est actif,587    #     ce qu'il signale lui-meme comme experimental ;588    #   - `nemotron_v3` pour le raisonnement -- ce n'est PAS qwen3, et l'utiliser589    #     laisse fuir le raisonnement dans le contenu ;590    #   - `qwen3_coder` pour les outils, le gabarit emettant <tool_call> et591    #     <function=>.592    # Les agents de code doivent en outre passer593    # `chat_template_kwargs: {force_nonempty_content: true}` dans leurs requetes.594    # v77 : `humming` ECHOUE sur cette image, et ce n'est pas une limite d'Ada.595    # Humming compile ses noyaux a la volee via NVRTC, et NVRTC ne sait pas lire596    # les en-tetes CUDA de l'image :597    #     /usr/local/cuda/include/cuda_fp8.hpp: this declaration has no storage598    #     class or type specifier ... nvrtc_compile: NVRTC_ERROR_COMPILATION599    # Les en-tetes sont ceux de CUDA 13 (les types fp4/fp6 en sont une600    # nouveaute) mais le NVRTC qui les lit est plus ancien. C'est le desaccord de601    # versions decrit plus haut, que `cuda-compat-13-0` ne resout que pour le602    # pilote, pas pour la chaine de compilation. Marlin, lui, est PRECOMPILE et603    # ne souffre pas du probleme.604    # Defaut : on laisse vLLM choisir (il prend MARLIN). VL_MOE_BACKEND=humming605    # reste possible si l'image est un jour alignee sur une seule version CUDA.606    # v78 : `--mamba-ssu-algorithm simple` RETIRE du defaut. Mesure a trois607    # variables changees d'un coup (align + ssu simple + parseurs NVIDIA) :608    # -12 % de debit solo (238,5 -> 210,0) mais x4,8 de cache KV (1,68 M ->609    # 8,13 M jetons) et TTFT divise par deux. Le gain de cache vient d'`align`,610    # qui evite de garder les etats Mamba de tous les blocs caches. La perte de611    # debit est probablement due a `simple` : NVIDIA ne la prescrit que pour le612    # chemin Ampere, et utilise `horizontal` sur H100 en debit maximal. On la613    # retire pour isoler, VL_SSU permet de la remettre.614    # v79 : etats Mamba en float16 au lieu de float32. Mesure : `align` coute615    # -12 % de debit solo (238,5 -> 211,4) mais multiplie le cache par 4,8616    # (1,68 M -> 8,16 M jetons, soit 7,75 sessions pleines a 1 M au lieu d'une et617    # demie). `--mamba-ssu-algorithm simple` s'etant revele sans effet (210,0618    # contre 211,4, du bruit), le float16 est le levier suivant : il halve le619    # trafic memoire des etats Mamba a chaque pas, et NVIDIA le prescrit avec620    # l'arrondi stochastique pour compenser la perte de precision.621    # v80 : ReplaySSM rendu activable. La doc vLLM le decrit comme un noyau de622    # decodage Mamba2 qui "met en cache les entrees SSM recentes et evite le623    # stockage complet de l'etat a chaque pas, n'ecrivant le point de reprise624    # qu'au vidage". C'est exactement le surcout par pas qui domine notre625    # decodage a un seul jeton. Il exige `mamba_cache_mode` en 'none' ou 'align'626    # -- acquis -- ET le backend Mamba Triton, donc les deux variables bougent627    # ensemble : VL_MAMBA_BACKEND=triton VL_REPLAYSSM=1.628    # Mesures cumulees sur ce modele (banc identique, par flux en solo) :629    #   defaut (mamba all)      238,5 | agrege 16 : 911  | cache 1,68 M630    #   + align                 211,4 | agrege 16 : 899  | cache 8,16 M631    #   + mamba float16         215,2 | agrege 16 : 995  | cache 8,28 M632    # `--mamba-ssu-algorithm simple` : sans effet, retire.633    # v81 : l'arrondi stochastique devient optionnel. Avec le backend Mamba634    # TRITON il est FATAL sur sm_89 :635    #     ptxas: Feature '.rs' not supported on .target 'sm_89'636    # `.rs` est l'instruction PTX d'arrondi stochastique, introduite avec637    # Blackwell. En FlashInfer (v79) le drapeau passait sans emettre cette638    # instruction ; en Triton il la declenche. Sur Ada, on ne peut donc PAS639    # cumuler Triton et arrondi stochastique -- et sans arrondi, le float16 perd640    # sa compensation de precision, d'ou VL_MAMBA_DTYPE=float32 par prudence641    # quand on passe en Triton.642    EXTRA=( --tool-call-parser qwen3_coder643            --reasoning-parser nemotron_v3644            --trust-remote-code645            ${VL_MOE_BACKEND:+--moe-backend "$VL_MOE_BACKEND"}646            ${VL_LINEAR_BACKEND:+--linear-backend "$VL_LINEAR_BACKEND"}647            --mamba-backend "${VL_MAMBA_BACKEND:-flashinfer}"648            ${VL_REPLAYSSM:+--use-replayssm}649            --mamba-cache-mode align650            --mamba-ssm-cache-dtype "${VL_MAMBA_DTYPE:-float16}"651            ${VL_STOCHASTIC:+--enable-mamba-cache-stochastic-rounding}652            ${VL_STOCHASTIC:+--mamba-cache-philox-rounds 5}653            ${VL_SSU:+--mamba-ssu-algorithm "$VL_SSU"}654            --max-model-len "${VL_CTX:-1048576}" )655    log "Nemotron Lightning A3B, parseurs NVIDIA + Mamba align, contexte natif ${VL_CTX:-1048576}"656    ;;657  ornithnvfp4)658    # v74. Quant NVFP4 officiel de l'editeur. Raison chiffree, calculee sur les659    # formes de tenseurs des trois depots (octets LUS par jeton en decodage,660    # tour visuelle exclue puisqu'elle ne sert pas en texte) :661    #662    #   depot                dense   lm_head  8/256 experts   ACTIF    plafond663    #   ulkaa AWQ-INT4        2,86     1,02       0,63        4,51 Go  213 t/s664    #   ornith-ai FP8         2,46     1,02       1,06        4,54 Go  211 t/s665    #   ornith-ai NVFP4       1,40     0,29       0,62        2,30 Go  416 t/s666    #667    # (plafond = 960 Go/s de la RTX 6000 Ada divises par le socle actif.)668    #669    # L'AWQ ne quantifie QUE les experts routes : sa liste `ignore` compte 531670    # entrees -- linear_attn, self_attn, shared_expert, lm_head -- donc 86 % de671    # ce qui est lu a chaque jeton reste en BF16. Le depot s'appelle INT4 mais672    # ne l'est presque pas. Le FP8 officiel est un piege symetrique : il grossit673    # les experts, qui ne pesent que 14 % de la lecture, sans alleger le dense.674    # Socle inchange, et 39,4 Go qui etouffent le cache KV.675    #676    # Le NVFP4 quantifie exactement ce qu'il faut : attention et linear_attn en677    # FP8 W8A8 (natif sm89), puis experts, shared_expert et lm_head en FP4678    # POIDS SEULS (`input_activations: null`, group_size 16). Ce n'est PAS du679    # W4A4, qui exigerait Blackwell : les activations restent en 16 bits, donc680    # le chemin Marlin FP4 de vLLM (sm80+) s'applique. A l'inverse,681    # protoLabsAI/...-NVFP4 est bien W4A4 et reste inutilisable ici.682    #683    # Effet de bord recherche : 23,4 Go de poids au lieu de 26,1 liberent 2,7 Go,684    # soit ~138 k jetons de cache a 20 Ko/jeton. Le palier automatique de685    # CTX_MAX passe donc de 786432 a 1000000 -- le vrai 1M tient sur UNE carte,686    # sans TurboQuant, dont l'en-tete de ce script mesure qu'il coute la moitie687    # du debit (v61 : 15,0 j/s contre 26,4 en bf16).688    #689    # RISQUE ASSUME : le noyau Marlin FP4 n'a jamais ete exerce sur sm89 ici. Si690    # vLLM refuse, repli immediat par VL_MODEL=ornith.691    MODEL="ornith-ai/Ornith-1.5-35B-A3B-NVFP4"692    NAME=ornith693    EXTRA=( --tool-call-parser qwen3_xml694            --reasoning-parser qwen3695            --trust-remote-code )696    if [ "${VL_YARN:-on}" = "on" ]; then697      export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1698      EXTRA+=( --hf-overrides '{"rope_scaling":{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":262144}}'699               --max-model-len "${VL_CTX:-1000000}" )700      log "YaRN facteur 4.0 -> contexte ${VL_CTX:-1000000}"701    else702      EXTRA+=( --max-model-len "${VL_CTX:-262144}" )703      log "contexte natif ${VL_CTX:-262144} (YaRN desactive)"704    fi705    ;;706  ornith)707    # Ornith-1.5-35B-A3B (publie le 18/08/2026, MIT) : meme famille708    # architecturale que Qwen3.8 -- Gated DeltaNet hybride, 40 couches dont709    # SEULEMENT 10 en attention pleine, 256 experts dont 8 actifs.710    #711    # Ce qui le rend interessant ici, mesure sur les poids eux-memes :712    #   KV                 20 Ko/token  (10 x 2 x 2 tetes x 256)713    #   octets lus/token   4,42 Go      contre 16,70 pour Qwen3.8-27B dense714    # Sur DEUX A40 (96 Gio) le budget KV atteint ~58 Gio, soit ~2,8 M tokens :715    # le 1M tient largement, AVEC de la place pour plusieurs sessions.716    #717    # C'est pour ca qu'on reste en KV BF16 et qu'on ne met PAS --kv-cache-dtype718    # fp8 : verifie dans les sources de vLLM 0.27.1,719    # fa_utils.flash_attn_supports_kv_cache_dtype n'accepte un KV quantifie que720    # pour FA3/sm_90 ou FA4/sm_100. Sur une A40 (sm_86) le fp8 forcerait le721    # repli sur TRITON_ATTN, or speculative.py note que DFlash exige un backend722    # non causal type FLASH_ATTN. A une seule carte les deux s'excluaient ; a723    # deux cartes le KV BF16 suffit, donc on garde FLASH_ATTN et la porte724    # ouverte a DFlash.725    #726    # Le format d'outil du template est <tool_call><function=><parameter=>,727    # c'est-a-dire exactement celui que lit le parser qwen3_coder.728    MODEL="ulkaa/Ornith-1.5-35B-A3B-AWQ-INT4"729    NAME=ornith730    # Contexte. Le modele est natif 262144 -- PAS 1M : partir a 1048576 le fait731    # mourir sur "User-specified max_model_len is greater than the derived732    # max_model_len (max_position_embeddings=262144)".733    #734    # L'editeur documente le passage a 1M par YaRN facteur 4.0. C'est un735    # compromis, pas un cadeau : le YaRN statique reechelonne TOUTES les736    # positions, y compris les courtes, donc il peut couter un peu de qualite737    # sur les sessions qui n'atteindront jamais 262 k -- c'est-a-dire la738    # plupart. VL_YARN=off rend le contexte natif et sa qualite pleine.739    #740    # VLLM_ALLOW_LONG_MAX_MODEL_LEN seul, SANS le rope_scaling, serait un piege :741    # vLLM accepterait la longueur et le RoPE produirait des nan au-dela de742    # 262 k. Les deux vont ensemble ou pas du tout.743    EXTRA=( --tool-call-parser qwen3_xml   # meme classe que qwen3_coder, nom de l'editeur744            --reasoning-parser qwen3       # sans lui, le raisonnement fuit dans content745            --trust-remote-code )746    if [ "${VL_YARN:-on}" = "on" ]; then747      export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1748      EXTRA+=( --hf-overrides '{"rope_scaling":{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":262144}}'749               --max-model-len "${VL_CTX:-1000000}" )750      log "YaRN facteur 4.0 -> contexte ${VL_CTX:-1000000}"751    else752      EXTRA+=( --max-model-len "${VL_CTX:-262144}" )753      log "contexte natif ${VL_CTX:-262144} (YaRN desactive)"754    fi755    ;;756  qwen)757    MODEL="cyankiwi/Qwen3-Coder-30B-A3B-Instruct-AWQ-4bit"758    NAME=qwen759    EXTRA=( --kv-cache-dtype fp8 --max-model-len "${VL_CTX:-262144}"760            --tool-call-parser qwen3_coder )761    ;;762  kimi)763    MODEL="cyankiwi/Kimi-Linear-48B-A3B-Instruct-AWQ-4bit"764    NAME=kimi765    # tokenizer corrige : tokenization_kimi.py importe bytes_to_unicode depuis766    # transformers.convert_slow_tokenizer, supprime en transformers >= 5.5.3767    # qu'exige tout vLLM >= 0.24, et aucun depot Kimi-Linear ne fournit de768    # tokenizer.json rapide.769    EXTRA=( --tokenizer patdev/kimi-linear-tokenizer-fix --trust-remote-code770            --max-model-len "${VL_CTX:-1048576}"771            --tool-call-parser kimi_k2 )772    ;;773  reap)774    # Qwen3-Coder elague par Cerebras : 103 experts au lieu de 128 (-20 %),775    # 12,9 Gio en AWQ contre 16,9.776    #777    # MESURE : aucun gain de debit. 124,9 / 511,8 / 1171,6 tok/s a 1, 8 et 32778    # sessions, contre 122,9 / 512,1 / 1159,7 pour le modele complet -- identique779    # au bruit malgre 24 % de poids en moins. On n'est donc PAS borne par la780    # lecture des poids, et l'elagage coute 20 % des experts pour rien.781    # Conserve uniquement pour liberer 4 Go de VRAM si le KV manque.782    MODEL="mattbucci/Qwen3-Coder-REAP-25B-A3B-AWQ"783    NAME=qwen784    EXTRA=( --kv-cache-dtype fp8 --max-model-len "${VL_CTX:-262144}"785            --tool-call-parser qwen3_coder )786    ;;787  dense)788    # Qwen3.8-27B dense, INT4 AWQ G128 asym (Marlin sm86), tete MTP en BF16789    # intacte -- c'est ce qui rend la speculation rentable ici (+25 % a k=1,790    # +75 % a k=3 mesures sur L40S), a l'inverse d'Ornith AWQ. Natif 262144,791    # 64 Ko/jeton de KV (16 couches pleines sur 64). Sans YaRN ici : on mesure792    # le modele, pas le contexte.793    MODEL="philbert440/Qwen3.8-27B-W4A16-AWQ"794    NAME=qwen38795    # En DP=2 chaque A40 porte les 17,8 Gio de poids : il reste 17,56 Gio de KV,796    # et UNE requete a 262144 en demande 17,79 (64 Ko/jeton). 131072 suffit au banc.797    EXTRA=( --tool-call-parser qwen3_xml --reasoning-parser qwen3 --trust-remote-code )798    # Meme recette que pour Ornith : YaRN x4 sur le natif 262144 -> 1M, a799    # VALIDER par une aiguille au-dela de 262 k (valide_1m.py). Le KV bf16 de800    # Qwen3.8 (64 Ko/jeton, 794 k jetons sur 2x A40) ne tient pas une requete a801    # 1M : TurboQuant est OBLIGATOIRE ici, pas optionnel.802    if [ "${VL_YARN:-on}" = on ]; then803      export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1804      EXTRA+=( --hf-overrides '{"rope_scaling":{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":262144}}'805               --max-model-len "${VL_CTX:-1000000}" )806      log "Qwen3.8 : YaRN facteur 4.0 -> contexte ${VL_CTX:-1000000}"807    else808      EXTRA+=( --max-model-len "${VL_CTX:-262144}" )809    fi810    ;;811  qwen38nvfp4)812    # Qwen3.8-27B en NVFP4 (unsloth), pour Blackwell : +52 % mesure sur Ornith813    # entre W4A16 et NVFP4 sur la meme carte. La tete MTP y est laissee en BF16814    # (`ignore: re:^mtp.*`), condition du +75 % de la speculation MTP k=3.815    # Natif 262144, 64 Ko/jeton de KV : sur 48 Go il reste ~22 Go de KV, soit816    # ~360 k jetons -- UNE requete a 250 k a la fois, pas six.817    MODEL="unsloth/Qwen3.8-27B-NVFP4"818    NAME=qwen38819    EXTRA=( --tool-call-parser qwen3_xml --reasoning-parser qwen3 --trust-remote-code )820    # v82 : YaRN aussi pour qwen38 (la famille Qwen3.5+ le documente). Facteur821    # 2.0 par defaut (plafond 524288) : moindre reechelonnement des positions822    # courtes que le 4.0 d'Ornith. La VRAIE limite est le pool KV (~452K a fp8,823    # util 0.90) : demander VL_CTX <= pool, sinon vLLM refuse au boot.824    if [ "${VL_YARN:-on}" = "on" ] && [ "${VL_CTX:-262144}" -gt 262144 ]; then825      export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1826      EXTRA+=( --hf-overrides "{\"rope_scaling\":{\"rope_type\":\"yarn\",\"factor\":${VL_YARN_FACTOR:-2.0},\"original_max_position_embeddings\":262144}}"827               --max-model-len "${VL_CTX}" )828      log "YaRN facteur ${VL_YARN_FACTOR:-2.0} -> contexte ${VL_CTX} (qwen38)"829    else830      EXTRA+=( --max-model-len "${VL_CTX:-262144}" )831    fi832    # 64 Ko/jeton en bf16 : 262144 x 64 Ko = 16,8 Go pour UNE requete, sur les833    # ~19 Go qui restent apres 23,4 Go de poids et les graphes -- trop juste avec834    # la tete MTP. KV fp8 (Blackwell) : 32 Ko/jeton, ~600 k jetons.835    [ -z "$VL_KV" ] && VL_KV=fp8836    ;;837  *) fatal "VL_MODEL inconnu: $WHICH" ;;838esac839 840# Parallelisme tensoriel. Par defaut on prend toutes les cartes du pod : un pod841# 2x A40 laisse par erreur en TP=1 gaspille la moitie de sa VRAM ET de sa bande842# passante, sans que rien ne le signale dans les logs.843NGPU=$(nvidia-smi --query-gpu=index --format=csv,noheader 2>/dev/null | wc -l)844TP="${VL_TP:-$NGPU}"845# Parallelisme de DONNEES : un replica complet par carte, aucun all-reduce846# entre GPU. A forte concurrence le dense 27B est borne par le calcul ; TP=2847# le partage (727 j/s a 32 sessions mesure), DP=2 le double. Le KV par replica848# est moitie moindre, c'est le prix. VL_DP=2 force TP=1.849DP="${VL_DP:-1}"850[ "$DP" -gt 1 ] && TP=1851[ "$TP" -ge 1 ] 2>/dev/null || TP=1852log "cartes detectees: $NGPU, tensor-parallel-size=$TP"853 854# Speculation. Mesure sur Qwen3-Coder-30B, meme materiel :855#   generation pure   102,21 -> 58,49 tok/s   (-43 %, rien a deviner)856#   edition de code   130,16 -> 242,39 tok/s  (+86 %)857# C'est un pari sur la repetition, pas un gain gratuit ; Claude Code vit dans858# le second regime. Sur Kimi-Linear en revanche elle CORROMPT le code de facon859# reproductible (fibonacci(n-1(n-1)) et divise le debit par 3 a 4, donc elle y860# est desactivee par defaut.861# Profondeur mesuree en edition de code, meme materiel :862#   n=5   259,2 tok/s   acceptation 68,2 %   longueur acceptee 3,41863#   n=12  363,9 tok/s   acceptation 26,9 %   longueur acceptee 3,23864# Le taux d'acceptation BAISSE avec la profondeur mais le debit MONTE : ce qui865# compte est le nombre de tokens emis par passe avant, pas la fraction acceptee.866# A n=12 la queue par position restait plate (257/118/82/72/71/63/61/59/59/58/867# 58/51), donc la profondeur n'etait pas saturee.868SPEC_N="${VL_SPEC_N:-24}"869 870LOOK_MAX="${VL_LOOK_MAX:-16}"871LOOK_MIN="${VL_LOOK_MIN:-2}"872# Defaut : DESACTIVEE. Mesure sur ce meme materiel, sessions agentiques873# simultanees partageant un prompt systeme :874#875#   sessions      avec spec n=24        sans spec        ecart876#      1          575 tok/s (rafale d'edition de code)877#      2       187,5 agr. / 93,8      204,2 / 102,1       +9 %878#      4       240,4 agr. / 60,1      333,9 /  83,5      +39 %879#      8       323,8 agr. / 40,5      503,2 /  62,9      +55 %880#881# La speculation ne gagne QUE si une seule session tourne. Verifie a DEUX882# profondeurs, pour ecarter l'idee qu'une profondeur plus faible menagerait le883# budget de batch :884#   sessions   sans spec   spec n=24   spec n=4885#      4         83,5        60,1        25,3886#      8         64,0        40,5        22,1887#     16         48,5         -          23,1888# n=4 est encore PIRE que n=24 : ce n'est pas le budget de batch qui est en889# cause, c'est le cout de verification paye sur chaque sequence. Mettre890# VL_SPEC=on uniquement pour un usage strictement solo.891SPEC=()892# Defaut : ACTIVEE, l'usage vise etant mono-session. Le compromis est net et893# mesure : 575 tok/s en edition de code seul (contre 123 sans), mais -35 a -65 %894# des deux sessions simultanees. Passer VL_SPEC=off pour servir plusieurs895# clients a la fois.896# Defaut par modele. Sur ornith la tete MTP est DANS les poids : contrairement a897# ngram (qui devine par repetition) ou a DFlash (qui charge un second modele et898# dispute la VRAM au cache KV), elle ne coute ni memoire ni pari. On l'active899# donc par defaut, et VL_SPEC=off la coupe si la concurrence en pâtit.900# MESURE sans speculation, 2x A40 : 116,2 / 204,3 / 357,2 / 576,6 tok/s agreges901# a 1 / 2 / 4 / 8 sessions. C'est le solo (116) que la MTP doit ameliorer.902SPEC_DEFAULT=off903# La MTP n'est utilisable que sur le quant FP8 officiel.904[ "$WHICH" = ornithfp8 ] && SPEC_DEFAULT=on905# ornith int4 : speculation DESACTIVEE, et ce n'est pas un oubli.906#907# La speculation ngram adaptative a ete implementee, deployee et MESUREE sur ce908# modele. Elle perd partout, agrege en tok/s :909#     sessions      1       2       4       8910#     sans spec   116,2   204,3   357,2   576,6911#     avec spec    58,1    89,7   139,4   409,3912#     ecart       -50 %   -56 %   -61 %   -29 %913# Elle coute en plus 259 k jetons de cache KV (2,56 M contre 2,82 M), donc de la914# concurrence.915#916# POURQUOI le bareme etait faux : il reposait sur +367 % en solo, chiffre mesure917# sur Qwen3-Coder en EDITION DE CODE -- un regime ou le ngram devine bien parce918# que le texte se repete. Rien ne transferait a Ornith en generation. Et surtout,919# vLLM desactive --async-scheduling des qu'une speculation est configuree, meme920# quand le bareme la ramene a zero jeton : on perd l'ordonnancement asynchrone a921# TOUTES les tailles de lot pour un gain nul.922#923# Ne remettre a "on" qu'avec une mesure sur CE modele et CE regime.924[ "$WHICH" = ornith ] && SPEC_DEFAULT=off925# ornith (int4) : la tete MTP EST en BF16 dans ulkaa/Ornith-1.5-35B-A3B-AWQ-INT4926# (785 tenseurs mtp.* BF16 verifies dans les en-tetes safetensors, 22/08 soir).927# Ce qui cassait (v4x) : la liste `ignore` du config.json n'exclut pas `mtp`,928# donc vLLM construit des modules QUANTIFIES pour la tete et ne trouve pas le929# `fc.weight` dense. Le bloc "patch config MTP" ci-dessous ajoute930# "re:.*mtp\..*" a `ignore` dans le snapshot (idempotent). Ancienne erreur :931#   ValueError: There is no module or parameter named 'fc.weight' in932#   Qwen3_5MultiTokenPredictor. The available parameters belonging to fc are:933#   {'fc.weight_packed', 'fc.weight_scale', 'fc.weight_shape', ...}934# A comparer avec cyankiwi/Qwen3.8-27B-AWQ-INT4, qui liste `mtp.fc` et935# `mtp.layers.0.*` dans son `ignore` et les laisse donc en BF16 -- c'est ce936# qu'il aurait fallu faire. Le depot embarque bien un model-mtp.safetensors de937# 1,69 Go, ce qui donne l'illusion que la spéculation est disponible.938# (diagnostic errone "tete quantifiee" corrige en v63 : c'etait la config.)939# KV quantifie (TurboQuant). Contrat verifie dans vLLM 0.27.1 : KVQuantMode vit940# sur AttentionSpec, jamais sur MambaSpec -- seules les 10 couches d'attention941# pleine d'Ornith sont touchees, les etats DeltaNet restent intacts. Les noyaux942# TurboQuant sont en Triton, sans contrainte de capacite (sm_86 OK), a la943# difference du fp8 qui force le repli TRITON_ATTN sur cette carte.944if [ -n "$VL_KV" ]; then945  EXTRA+=( --kv-cache-dtype "$VL_KV" )946  log "KV quantifie : $VL_KV"947fi948 949if [ "$VL_SPEC" = dspark ] && [ "$WHICH" != kimi ]; then950  # DSpark : brouillon ENTRAINE contre Qwen3.6-35B-A3B (dont Ornith est un951  # affinage RL). Mesure publiee sur sa propre cible : acceptation 0,628,952  # longueur acceptee 3,57. Sur Ornith c'est un plafond, pas une prediction.953  # `enable_confidence_head` = la verification adaptative que le bareme ngram954  # tentait d'imiter a la main. Architecture Qwen3DSparkModel : vLLM route955  # tout seul vers method=dspark.956  SPEC=( --speculative-config "{\"method\":\"dspark\",\"model\":\"$VL_DSPARK_MODEL\",\"num_speculative_tokens\":$VL_DSPARK_N}" )957  log "speculation DSPARK $VL_DSPARK_MODEL n=$VL_DSPARK_N"958elif [ "$VL_SPEC" = dflash ]; then959  # v76. DFlash2 (z-lab) : brouillon par diffusion de bloc, fusionne dans vLLM960  # le 21/08 (PR 52816) -- exige une image nightly >= ce commit. Le brouillon961  # pese 3,8 Go de VRAM, a comparer au MTP natif qui n'en coute aucun.962  SPEC=( --speculative-config "{\"method\":\"dflash\",\"model\":\"${VL_DFLASH2_MODEL:-z-lab/Qwen3.8-27B-DFlash2}\",\"num_speculative_tokens\":${VL_DFLASH2_N:-7}}" )963  log "speculation DFLASH2 ${VL_DFLASH2_MODEL:-z-lab/Qwen3.8-27B-DFlash2} n=${VL_DFLASH2_N:-7}"964elif [ "$VL_SPEC" = mtp ] && [ "$WHICH" != kimi ]; then965  SPEC=( --speculative-config "{\"method\":\"mtp\",\"num_speculative_tokens\":${VL_MTP_N:-3}}" )966  log "speculation MTP n=${VL_MTP_N:-3}"967elif [ "${VL_SPEC:-$SPEC_DEFAULT}" != "on" ] || [ "$WHICH" = kimi ]; then968  log "speculation desactivee"969else970  if [ "$WHICH" = ornith ]; then971    # SPECULATION ADAPTATIVE. vLLM 0.27.1 accepte un bareme par taille de lot :972    # `num_speculative_tokens_per_batch_size` prend des triplets973    # (debut_de_plage, fin_de_plage, jetons_speculatifs), plages inclusives.974    # Le moteur choisit la profondeur A CHAQUE PAS d'ordonnancement, donc il975    # remonte tout seul des qu'un agent s'arrete -- sans redemarrage.976    #977    # Le bareme suit NOS mesures, pas une intuition. Sur ce meme materiel :978    #   1 session   speculation +367 % (575 contre 123 tok/s en edition de code)979    #   4 sessions  speculation  -39 % (60,1 contre 83,5 par session)980    #   8 sessions  speculation  -55 % (40,5 contre 62,9 par session)981    # La speculation paie la verification sur CHAQUE sequence : ce qui est982    # gratuit quand le GPU est inoccupe devient un impot quand il est plein.983    # D'ou une decroissance jusqu'a zero, et non un simple interrupteur.984    SPEC=( --speculative-config "{\"method\":\"ngram\",\"num_speculative_tokens\":$SPEC_N,\"num_speculative_tokens_per_batch_size\":[[1,1,$SPEC_N],[2,3,8],[4,7,3],[8,100000,0]],\"prompt_lookup_max\":$LOOK_MAX,\"prompt_lookup_min\":$LOOK_MIN}" )985    log "speculation ngram ADAPTATIVE : $SPEC_N jetons en solo, 8 a 2-3 sessions, 3 a 4-7, coupee au-dela"986  elif [ "$WHICH" = ornithfp8 ]; then987    # Ornith embarque sa tete MTP dans les poids (model-mtp.safetensors,988    # 1,69 Go) : contrairement a EAGLE-3 ou DFlash, aucun second modele ne989    # vient disputer sa VRAM au cache KV. C'est donc la spéculation a essayer990    # en premier ; DFlash (z-lab/Qwen3.6-35B-A3B-DFlash, structurellement991    # compatible : 40 couches cibles, hidden 2048, meme tokenizer) reste une992    # experience dont le taux d'acceptation est l'inconnue.993    SPEC=( --speculative-config "{\"method\":\"mtp\",\"num_speculative_tokens\":${VL_MTP_N:-3}}" )994    log "speculation MTP n=${VL_MTP_N:-3}"995  else996    SPEC=( --speculative-config "{\"method\":\"ngram\",\"num_speculative_tokens\":$SPEC_N,\"prompt_lookup_max\":$LOOK_MAX,\"prompt_lookup_min\":$LOOK_MIN}" )997    log "speculation ngram n=$SPEC_N lookup=$LOOK_MIN..$LOOK_MAX"998  fi999fi1000 1001# vLLM rabaisse max_num_batched_tokens a 2048 quand la speculation est active et1002# previent lui-meme que c'est sous-optimal : les brouillons devorent le budget de1003# batch. Mesure sans le corriger : 32 flux tombaient de 1166 a 296 tok/s.1004# L'ordonnancement asynchrone est incompatible avec la speculation : on ne1005# l'active donc que lorsqu'elle est coupee.1006ASYNC_FLAG=""1007[ "${#SPEC[@]}" -eq 0 ] && [ "${VL_ASYNC:-on}" = "on" ] && ASYNC_FLAG="--async-scheduling"1008[ -n "$ASYNC_FLAG" ] && log "ordonnancement asynchrone active"1009 1010# v77 : dechargement KV via LMCache pour les hybrides GDN. Le backend natif1011# ecrit mais ne relit jamais (l'etat DeltaNet vit hors des blocs pagines) ;1012# LMCache enregistre cet etat comme page opaque et le rend au retour de session.1013# VL_LMCACHE=<GiB de RAM> ; chunk = taille de bloc unifiee du modele (1600 pour1014# qwen38-27B, lisible dans "Setting attention block size to N").1015LMCACHE_CFG=""1016if [ -n "${VL_LMCACHE:-}" ]; then1017  # l'image officielle n'a PAS de pip dans le venv (gere par uv) : uv d'abord.1018  { command -v uv >/dev/null 2>&1 && uv pip install --python "$VENV/bin/python" -q lmcache >>/tmp/boot.log 2>&1; }     || "$VENV/bin/python" -m pip install -q lmcache >>/tmp/boot.log 2>&1 && {1019    # v81 : le connecteur MP EXIGE le serveur separe (ZMQ 5555 :1020    # "Cannot reach the LMCache MP server at tcp://localhost:5555"). Son1021    # frontend HTTP prend 8080 par defaut — c'est POUR CA que le pont est1022    # passe sur 8088 (VL_PROXY_PORT, v80). Les deux coexistent desormais.1023    "$VENV/bin/lmcache" server --chunk-size "${VL_LMCACHE_CHUNK:-1600}" --separate-object-groups         --l1-size-gb "$VL_LMCACHE" --eviction-policy LRU > /tmp/lmcache.log 2>&1 &1024    sleep 31025    LMCACHE_CFG='{"kv_connector":"LMCacheMPConnector","kv_role":"kv_both"}'1026    log "LMCache actif : L1 ${VL_LMCACHE} GiB, chunk ${VL_LMCACHE_CHUNK:-1600}"1027  } || log "WARNING pip lmcache a echoue, dechargement ignore"1028fi1029 1030COMMON=(1031  "$MODEL"1032  --served-model-name "$NAME" "claude-$NAME" "claude-$NAME[1m]" ${VL_ALIASES:-claude-3-5-haiku claude-haiku-4-5 claude-sonnet-5 claude-opus-5 deepseek-v4-flash deepseek-v4-pro}1033  --host 0.0.0.0 --port "$PORT"1034  --tensor-parallel-size "$TP" --data-parallel-size "$DP" --disable-custom-all-reduce   $( [ "$VL_NOVIDEO" = on ] && echo --limit-mm-per-prompt '{"video":0}' ) --enable-prompt-tokens-details   ${VL_OPT:+--optimization-level "$VL_OPT"}   $( [ "${VL_NUMA:-off}" = on ] && echo --numa-bind --numa-bind-nodes ${VL_NUMA_NODES:-0 1} )   $( [ "${VL_EP:-off}" = on ] && [ "$TP" -gt 1 ] && echo --enable-expert-parallel )   $( [ "$WHICH" = ornith ] && [ "$TP" = 2 ] && [ "$VL_KV" = turboquant_k3v4_nc ] && [ "$VL_SPEC" = off ] && [ -n "$VL_KVBYTES" ] && echo --kv-cache-memory-bytes "$VL_KVBYTES" )1035  --gpu-memory-utilization "${VL_UTIL:-0.93}"1036  --max-num-batched-tokens "${VL_BATCHED:-16384}"   ${VL_LONGPREFILL:+--long-prefill-token-threshold "$VL_LONGPREFILL"}   ${VL_PARTIAL:+--max-num-partial-prefills "$VL_PARTIAL"}1037  --max-num-seqs "${VL_SEQS:-64}"   ${VL_ATTN:+--attention-backend "$VL_ATTN"}1038  ${VL_OFFLOAD:+--kv-offloading-size "$VL_OFFLOAD"}1039  ${LMCACHE_CFG:+--kv-transfer-config "$LMCACHE_CFG"}1040  --enable-prefix-caching1041  # vLLM refusait l'ordonnancement asynchrone tant que la speculation ngram1042  # etait active ("Async scheduling not supported with ngram-based speculative1043  # decoding and will be disabled") : il recouvre l'ordonnancement CPU avec le1044  # calcul GPU, donc il ne rapporte que si on ne le desactive pas soi-meme.1045  ${ASYNC_FLAG}1046  --enable-auto-tool-choice1047  "${EXTRA[@]}"1048  "${SPEC[@]}"1049)1050 1051if ss -lptn 2>/dev/null | grep -q ":$PORT "; then1052  log "le port $PORT est deja pris :"; ss -lptn 2>/dev/null | grep ":$PORT "1053  fatal "port $PORT occupe (nginx du conteneur ?) -- fixer VL_PORT"1054fi1055 1056log "PHASE serve  modele=$WHICH"1057printf '[VL] flag: %s\n' "${COMMON[@]}"1058 1059# ---- patch config MTP (v63/v64) : exclure la tete mtp.* de la quantification dans1060# le snapshot local, pour que vLLM la charge dense. Idempotent. v63 passait par1061# snapshot_download(local_files_only) qui n'a pas trouve le cache : glob direct1062# sur $HUBDIR (le meme chemin que la detection hors-ligne). Trace dans1063# /tmp/mtp_patch.log.1064if [ "$VL_SPEC" = mtp ] && { [ "$WHICH" = ornith ] || [ "$WHICH" = ornithnvfp4 ]; }; then1065  "$VENV/bin/python" - "$HUBDIR" "$MODEL" > /tmp/mtp_patch.log 2>&1 <<'PYEOF'1066import json, sys, os, glob1067hub, repo = sys.argv[1], sys.argv[2]1068files = glob.glob(os.path.join(hub, "models--" + repo.replace("/", "--"), "snapshots", "*", "config.json"))1069print("config.json :", files)1070for p in files:1071    real = os.path.realpath(p)1072    c = json.load(open(real, encoding="utf-8"))1073    q = c.get("quantization_config", {}); ig = q.setdefault("ignore", [])1074    if not any("mtp" in x for x in ig):1075        ig.append(r"re:.*mtp\..*"); json.dump(c, open(real, "w", encoding="utf-8"), indent=1)1076        print("patche :", real)1077    else:1078        print("deja patche :", real)1079PYEOF1080  sed "s/^/[VL] patch MTP : /" /tmp/mtp_patch.log1081fi1082# ---- cache de compilation (v73). Mesure : sur un demarrage a chaud, tout le1083# reste est rapide (poids en cache local 25 s, image deja tiree) et il reste1084# ~3 min de torch.compile + capture des graphes CUDA. Ce cache est specifique a1085# (modele, carte, version de vLLM, drapeaux) : on le range donc sous une cle qui1086# reprend exactement ces elements, dans un depot HF. Restaure avant de servir,1087# publie apres, et seulement s'il n'existait pas.1088#1089# NB : precompiler les POIDS dans l'image serait contre-productif -- le pull1090# d'image tourne a ~46 Mo/s, le telechargement HF avec xet a 200-390 Mo/s.1091CACHE_REPO="${VL_CACHE_REPO:-patdev/k3-compile-cache}"1092CARTE=$(nvidia-smi --query-gpu=name --format=csv,noheader | head -1 | tr ' ' '_')1093VVER=$("$VENV/bin/python" -c "import importlib.metadata as m;print(m.version('vllm'))" 2>/dev/null)1094CLE=$(printf '%s|%s|%s|%s|%s|%s|%s|%s' "$MODEL" "$CARTE" "$VVER" "${VL_KV:-bf16}" "$VL_SPEC"         "${VL_CTX:-def}" "$VL_BATCHED" "$TP" | sha1sum | cut -c1-16)1095CACHE_FIC="compile-${CLE}.tar.gz"1096export VLLM_CACHE_ROOT="${VLLM_CACHE_ROOT:-/root/.cache/vllm}"1097CACHE_RESTAURE=01098if [ "${VL_CACHE:-on}" = on ]; then1099  log "cache de compilation, cle $CLE ($CARTE, vllm $VVER)"1100  if "$VENV/bin/python" - "$CACHE_REPO" "$CACHE_FIC" "$VLLM_CACHE_ROOT" <<'PYEOF' 2>>/tmp/cache.log1101import os, sys, tarfile1102from huggingface_hub import hf_hub_download1103repo, fic, dest = sys.argv[1], sys.argv[2], sys.argv[3]1104p = hf_hub_download(repo_id=repo, filename=fic, repo_type="dataset")1105os.makedirs(dest, exist_ok=True)1106with tarfile.open(p, "r:gz") as t:1107    t.extractall(dest)1108print("restaure")1109PYEOF1110  then CACHE_RESTAURE=1; log "cache de compilation RESTAURE (demarrage court attendu)"1111  else log "pas de cache pour cette cle : premiere compilation, il sera publie ensuite"1112  fi1113fi1114 1115# ---- journal distant (v74) ---------------------------------------------------1116# Sans shell sur le pod -- une cle SSH a passphrase et un agent mort suffisent a1117# couper l'acces -- le seul canal d'observation restant est HTTP. Or le1118# demarrage est justement le moment ou l'on a le plus besoin de voir, et celui1119# ou l'endpoint ne repond pas encore. On republie donc la fin du journal dans le1120# depot du bootstrap, sous une cle propre au pod, tant que le serveur n'est pas1121# debout. Un dernier envoi suit la mise en service, puis la boucle s'arrete :1122# pas de commit inutile une fois que /v1/models repond et sert de temoin.1123if [ -n "${HF_TOKEN:-}" ] && [ "${VL_JOURNAL:-on}" = on ]; then1124  CLE_JOURNAL="etat/${RUNPOD_POD_ID:-pod}.log"1125  log "journal distant : https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/$CLE_JOURNAL"1126  (1127    publier() {1128      { echo "=== bootstrap $SCRIPT_VERSION | $(date -u +%H:%M:%S) UTC ==="1129        tail -c 25000 /tmp/boot.log 2>/dev/null1130        echo; echo "=== nvidia-smi ==="1131        nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader 2>/dev/null1132        echo; echo "=== vllm.log (fin) ==="1133        tail -c 60000 /tmp/vllm.log 2>/dev/null1134        echo; echo "=== proxy.log (fin) ==="1135        tail -c 20000 /tmp/proxy.log 2>/dev/null1136      } > /tmp/journal_publie.log 2>/dev/null1137      "$VENV/bin/python" - "$CLE_JOURNAL" >/dev/null 2>&1 <<'PYJOURNAL'1138import os, sys1139from huggingface_hub import HfApi1140HfApi().upload_file(path_or_fileobj="/tmp/journal_publie.log",1141                    path_in_repo=sys.argv[1],1142                    repo_id="patdev/k3-a40-bootstrap",1143                    commit_message="journal de demarrage",1144                    token=os.environ["HF_TOKEN"])1145PYJOURNAL1146    }1147    # v72 : sonder vLLM ($PORT), pas le pont (8080) -- pendant un swap le pont1148    # reste en vie, et sonder 8080 arretait la publication des la 1re passe :1149    # le journal du modele suivant n'etait jamais publie.1150    for _ in $(seq 1 120); do1151      curl -sf -m 2 "http://127.0.0.1:$PORT/health" >/dev/null 2>&1 && break1152      publier; sleep 251153    done1154    sleep 5; publier1155    # v86 : continuer a publier APRES la mise en service. Sans cela le journal1156    # s'arretait au demarrage et le pod devenait une boite noire : le 05/09, le1157    # debit est tombe de 110 a 6,5 tok/s et il n'existait aucun moyen de voir la1158    # file d'attente -- deux redemarrages a l'aveugle. vLLM ecrit periodiquement1159    # « Running: N reqs, Waiting: M reqs, GPU KV cache usage: X% » dans son1160    # journal : c'est precisement ce qu'il faut lire. VL_JOURNAL_PERIODE=01161    # retablit l'ancien comportement.1162    PERIODE="${VL_JOURNAL_PERIODE:-120}"1163    if [ "$PERIODE" -gt 0 ] 2>/dev/null; then1164      while sleep "$PERIODE"; do publier; done1165    fi1166  ) &1167fi1168# v85. `set -m` place le job d'arriere-plan dans SON PROPRE groupe de processus1169# (PGID = PID). Sans lui, en bash NON interactif, un `&` laisse l'enfant dans le1170# groupe du script : `kill -- -$SRV_PID` tuerait alors le bootstrap lui-meme, ce1171# qui arrete le conteneur. Avec lui, on peut tuer d'un coup vLLM ET sa1172# descendance -- EngineCore et les workers, qui survivent a un kill du parent et1173# RETIENNENT LA VRAM, faisant tomber le vLLM suivant en OOM.1174set -m1175vllm serve "${COMMON[@]}" >/tmp/vllm.log 2>&1 &1176SRV_PID=$!1177set +m1178 1179# Arret complet et borne. Tout appelant doit passer par ici : les trois sites1180# d'arret divergeaient, et seul l'un des trois pensait a EngineCore.1181arreter_vllm() {1182  # `kill -- -PID` echoue si le groupe n'existe pas (si `set -m` n'a pas pris) :1183  # on retombe alors sur le parent seul plutot que de viser un groupe au hasard.1184  kill -TERM -- -"$SRV_PID" 2>/dev/null || kill -TERM "$SRV_PID" 2>/dev/null1185  for _ in $(seq 1 20); do kill -0 "$SRV_PID" 2>/dev/null || break; sleep 1; done1186  if kill -0 "$SRV_PID" 2>/dev/null; then1187    log "le serveur ne repond pas a SIGTERM apres 20 s, SIGKILL du groupe"1188    kill -KILL -- -"$SRV_PID" 2>/dev/null || kill -9 "$SRV_PID" 2>/dev/null1189  fi1190  # Ceinture et bretelles : EngineCore renomme son processus, donc un pkill sur1191  # "vllm serve" ne l'attrape pas. Mesure passee : VRAM retenue apres l'arret.1192  pkill -9 -f "VLLM::EngineCore" 2>/dev/null1193  wait "$SRV_PID" 2>/dev/null1194  # Ne pas rendre la main avant que la carte soit reellement libre, sinon le1195  # vLLM suivant profile une VRAM encore occupee et se dimensionne trop petit.1196  for _ in $(seq 1 45); do1197    VRAM_LIBRE=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | head -1)1198    [ -n "$VRAM_LIBRE" ] && [ "$VRAM_LIBRE" -lt 3000 ] && break; sleep 21199  done1200  log "vLLM arrete, VRAM a ${VRAM_LIBRE:-?} Mio"

Showing the first 1,200 of 1404 lines. Download the file for the rest.