IA
6 min de lecture

vLLM sur MSI EdgeXpert (GB10) : cinq bugs avant que ça tourne

Premier déploiement de vLLM sur la nouvelle architecture Blackwell ARM64 de NVIDIA (MSI EdgeXpert / GB10). Cinq bugs à débugger, du driver CUDA jusqu'au client opencode. Notes pour plus tard.

Arthur Zinck
Arthur Zinck
Expert DevOps Kubernetes & Cloud

J’installe une MSI EdgeXpert. NVIDIA GB10, 128 Go de mémoire unifiée, ARM64, DGX OS. Environ 3 700 €. L’objectif est simple : faire tourner vLLM avec un modèle Qwen-Coder, brancher opencode dessus, avoir un assistant code 100 % local pour mes propres besoins – et tester ce que ça donne pour des clients qui ne peuvent pas envoyer leur code à OpenAI ou Anthropic.

Le matériel est arrivé. nvidia-smi répond. La carte est reconnue. Driver 580.142, CUDA 13.0. Tout va bien.

Deux heures de setup plus tard, j’ai un truc qui fonctionne. Voici les cinq bugs traversés, dans l’ordre.

1. La cache AOTAutograd de PyTorch ne pickle pas

Premier vllm serve avec Qwen/Qwen2.5-Coder-32B-Instruct (FP16 base) + --quantization fp8 pour conversion runtime. Le modèle télécharge (~62 Go de poids FP16), se quantifie en FP8 à la volée, finit par occuper ~32 Go en VRAM. Puis crash à l’initialisation du worker, dans le profiling KV cache :

_pickle.PicklingError: Can't pickle <function launcher at 0x...>:
attribute lookup launcher on __main__ failed

PyTorch essaie de sérialiser un graphe inductor compilé vers son cache disque. La fonction launcher est un closure défini dans __main__, pickle refuse.

La trace exacte ne matche aucune issue GitHub publique que j’ai trouvée. Mais la catégorie est documentée – la doc debug vLLM-torch.compile explicite : “vLLM’s compilation cache requires that the code being compiled ends up being serializable. If this is not the case, then it will error out on save.” Et la doc Troubleshooting vLLM donne le fix standard pour cette catégorie de problèmes : --enforce-eager.

Fix : --enforce-eager. On désactive complètement torch.compile et la capture CUDA Graph. On perd 15-20 % de throughput décode mais l’engine démarre. À reprendre en phase de tuning, pas le sujet pour aujourd’hui.

2. Triton “operation not permitted” sur sm_121

Modèle suivant. Cette fois le chargement passe (32 Go en VRAM, OK), puis crash plus tard, sur le sampler :

RuntimeError: Triton Error [CUDA]: operation not permitted

Le kernel Triton du top-k/top-p sampler refuse de se charger. Le GPU est propre, aucun process zombie – ça vient pas de l’environnement.

Web search sur le message d’erreur exact. Cette fois j’ai de la chance : c’est documenté. La vLLM issue #36821 “No sm_121 (Blackwell) support on aarch64” confirme que vLLM n’a pas encore de support officiel pour le compute capability du GB10. Le thread NVIDIA Developer “Triton/vLLM Crash on sm_121” décrit exactement ce que je vois. Et eelbaz/dgx-spark-vllm-setup maintient une recette de contournement.

Le fix qu’on y trouve : forcer le sampler à utiliser FlashInfer au lieu de Triton.

Fix : -e VLLM_USE_FLASHINFER_SAMPLER=1. On bascule vers le sampler FlashInfer, qui contourne le code path cassé. À monitorer jusqu’à ce que sm_121 soit officiellement supporté.

3. Le tool calling sort en <tools> au lieu de <tool_call>

Pour gagner du temps de cold start, j’essaie un checkpoint pré-quantifié FP8 publié par un tiers (RedHatAI/Qwen2.5-Coder-32B-Instruct-FP8-dynamic). Le modèle charge en 10 minutes au lieu de 21. Pratique.

Test du tool calling avec un curl simple. Le modèle reçoit la définition d’un outil calculator. Réponse :

{
  "content": "<tools>\n{\"name\": \"calculator\", \"arguments\": ...}\n</tools>",
  "tool_calls": []
}

Le modèle a bien fait l’appel d’outil – mais avec les balises <tools> (pluriel) au lieu de <tool_call> (singulier). Aucun parser de vLLM ne matche. opencode reçoit du texte sans tool_calls, croit que rien ne s’est passé, lance une boucle de compaction.

Premier réflexe : “le pré-quant tiers a un chat template cassé”. Je switche vers le checkpoint officiel Qwen/Qwen2.5-Coder-32B-Instruct-GPTQ-Int8 (org Qwen directement, tokenizer canonique). Re-test. Même résultat : <tools> pluriel, tool_calls: []. Pas un problème de pré-quant, donc.

Web search sur “vLLM Qwen2.5-Coder tool_calls empty”. Réponse trouvée dans les issues vLLM #29192 et #32926, qui décrivent exactement le même comportement.

L’explication, en clair :

  • Qwen2.5-Coder ne sait pas faire des tool calls en natif. Son entraînement ne lui a pas appris ce format.
  • Pour quand même supporter les tools, son chat template lui montre des exemples sous forme de balises <tools> dans le prompt système – en espérant qu’il imite.
  • Le modèle imite, oui. Mais il imite avec les balises <tools> qu’il a vues dans les exemples, pas avec <tool_call> que les parsers de vLLM (hermes, qwen3_coder, qwen3_xml) cherchent.
  • Résultat : tool calls bien émis, mais dans un format que personne ne sait extraire.

Un parser communautaire (hanXen/vllm-qwen2.5-coder-tool-parser) existe pour matcher ce format <tools>. Je ne l’ai pas testé – le bug suivant offre un fix plus simple.

Leçon générale : un checkpoint “qui charge dans vLLM” n’est pas “production-ready”. Toujours faire un smoke test tool-calling end-to-end avant de qualifier un modèle pour usage client. Et la qualification se fait en couple parser × modèle, pas l’un sans l’autre.

4. Le bon parser + le bon modèle

Le parser qwen3_coder est conçu pour la famille Qwen3-Coder (qui est entraînée avec des tokens de tool calling natifs et sort <tool_call>...</tool_call>), pas pour Qwen2.5-Coder. Donc le fix complet du tool calling demande deux choses : trouver le nom exact du parser, et changer de modèle vers la bonne famille.

La doc Tool Calling vLLM et le Qwen3-Coder Usage Guide donnent directement le bon nom : --tool-call-parser qwen3_coder (avec tiret bas). Lecture rapide, fix immédiat – une fois qu’on pense à aller voir la doc.

Test du qwen3_coder parser sur Qwen2.5-Coder-32B-Instruct-GPTQ-Int8 : toujours <tools> pluriel. Logique en y réfléchissant – ce parser cherche <tool_call> singulier, le modèle 2.5-Coder ne sait pas le sortir. C’est cohérent avec l’analyse du bug #3 : Qwen2.5-Coder n’a pas le format dans son training.

Fix complet : swap vers Qwen/Qwen3-Coder-Next-FP8 (famille Qwen3-Coder, FP8 natif, chat template aligné avec le format <tool_call> que le parser sait extraire) + parser qwen3_coder. Re-test avec le curl tool-calling :

{
  "role": "assistant",
  "content": null,
  "tool_calls": [{
    "id": "...",
    "type": "function",
    "function": {"name": "calculator", "arguments": "{\"a\":2,\"b\":2}"}
  }]
}

tool_calls: [...] peuplé. opencode peut enfin faire son boulot.

5. opencode demande 32 000 tokens en sortie par défaut

vLLM répond correctement. Je relance opencode. Boucle de compaction infinie. Chaque tour : 25-30 secondes, le modèle “résume” la conversation et ne fait rien.

Trace dans opencode :

This model's maximum context length is 32768 tokens.
However, you requested 32000 output tokens and your prompt
contains at least 769 input tokens, for a total of at least
32769 tokens.

opencode demande 32 000 tokens de sortie par défaut. Mon --max-model-len est à 32 768. N’importe quel prompt non vide fait dépasser le budget. vLLM renvoie 400. opencode interprète comme “contexte saturé”, lance une compaction pour réduire, retente avec les mêmes 32 000 tokens, redéborde, boucle.

Fix : - Côté serveur : --max-model-len 65536. La GB10 a 128 Go de mémoire unifiée, le KV cache encaisse largement. - Côté client (~/.config/opencode/opencode.json) : "limit": { "context": 32768, "output": 4096 }. On dit à opencode de demander un budget de sortie raisonnable.

Tout marche. opencode lit des fichiers, écrit du code, exécute des commandes shell. Premier vrai tour de boucle agentique sur ce hardware.

Le setup complet côté client

Le ~/.config/opencode/opencode.json qui pointe sur le vLLM local, pour quelqu’un qui veut reproduire :

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "vllm": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Local vLLM",
      "options": {
        "baseURL": "http://your.ip:8000/v1",
        "apiKey": "not-required-yet"
      },
      "models": {
        "qwen3-coder-next-fp8": {
          "name": "Qwen3-Coder-Next FP8 (Local)",
          "limit": { "context": 32768, "output": 4096 }
        }
      }
    }
  }
}

Deux points à respecter :

  • La clé du dictionnaire models doit matcher exactement la valeur de --served-model-name côté vLLM. C’est ce que opencode envoie comme model: dans le payload OpenAI. Si ça matche pas, vLLM répond “The model xxx does not exist.” et opencode part en boucle.
  • limit.output doit rester confortablement sous --max-model-len. 4 096 sortie + une marge raisonnable d’input couvre la quasi-totalité des tâches dev sans risquer le débordement.

Côté lancement :

cd ~/mon-projet
opencode -m vllm/qwen3-coder-next-fp8

Le préfixe vllm/ correspond au nom du provider dans le JSON.

Ce que ça révèle

Ces cinq bugs représentent deux heures de setup sur un modèle. Tous sont documentés – la doc Tool Calling vLLM, la doc Troubleshooting vLLM, le guide debug torch.compile vLLM, les issues GitHub (#36821, #29192, #32926), les threads NVIDIA Developer, les recipe guides Qwen vLLM. Une fois qu’on cherche au bon endroit avec la bonne formulation, le fix vient en quelques minutes. Le piège c’est que la doc est éparpillée entre ces sources et qu’il faut savoir reconnaître le fil pertinent à partir d’un message d’erreur cryptique – pour le bug #1 par exemple, la trace exacte ne matche rien, mais la catégorie (“compile cache pickle failure”) est dans la doc Troubleshooting et donne le bon fix. C’est le métier : sur du stack émergent, la doc existe, faut juste savoir comment elle est organisée.

C’est exactement pour ça que le local AI managé a une raison d’être.

La promesse du self-hosted – “votre code reste chez vous” – est réelle. RGPD, propriété intellectuelle, contrats clients qui interdisent les API tierces : pour beaucoup d’équipes EU, c’est non-négociable. Mais l’équation “j’achète une carte NVIDIA et j’installe un modèle” cache un travail d’opérations sérieux : choisir le hardware, qualifier les modèles, gérer les mises à jour, monitorer, restaurer après panne.

Mon pari sur les prochains mois : un service géré pour les équipes EU qui veulent du AI local mais pas l’overhead. Matériel installé chez le client, mises à jour modèles mensuelles testées sur banc avant push, configuration opencode prête à l’emploi, supervision distante. Build in public sur ce blog – je documente chaque phase.

Si votre équipe est dans cette situation, ou si vous voulez juste suivre le voyage : la suite arrive ici.


Liens utiles :

Pages de doc vLLM pertinentes pour les bugs de cet article :

Issues GitHub / threads NVIDIA :

Points clés à retenir

  • MSI EdgeXpert (GB10) à 3 700 € : matériel impressionnant, stack logiciel encore brut
  • 5 bugs entre nvidia-smi et un client qui marche : torch.compile, Triton, chat template, parser, fenêtre de contexte
  • Tool calling = qualification couplée parser × modèle ; Qwen2.5-Coder + hermes échoue silencieusement, le fix passe par Qwen3-Coder + qwen3_coder
  • Hosting local maîtrisé = vrai produit pour les boîtes EU qui ne peuvent pas envoyer leur code à OpenAI
ia llm vllm qwen gb10 blackwell local souverainete opencode self-hosted

Partager cet article

Twitter LinkedIn