Salut les amis !
Petit rappel pour ceux qui débarquent : Genesis, c’est la stack vLLM custom maintenue par Sandermage (un dev qui a sorti une suite de 28 patches Python pour faire tenir Qwen3.6-27B en speculative decoding sur consumer Blackwell). Sur mon Olares One c’est ce qui me tient à 88 tokens/seconde, mon record actuel. Mais c’est lourd à maintenir : image Docker custom de 5 Go, et à chaque nouvelle version de vLLM faut re-patcher en chassant les changements upstream.
Hier matin (5 mai 2026, 00:14 UTC), JartX intègre dans vLLM main la PR #39931 : “[Feature] TurboQuant: support hybrid models and uniform quantization”. Cette PR corrige nativement exactement ce que la moitié des Genesis patches font à la main. Bingo, on peut peut-être virer Genesis ?
Spoiler : oui MAIS pas complètement. 72,55 t/s sans Genesis vs 88 avec. Voilà l’histoire complète.
La PR #39931 — ce qu’elle fixe vraiment
Lecture du body de #39931 :
- Hybrid models : TurboQuant plantait
NotImplementedErrordès qu’il croisait une couche Mamba sur Qwen3.5/3.6/Qwen3-Next. Maintenant il applique TurboQuant uniquement sur les couchesfull_attentionet passe les Mamba/SWA en transparent. - Page-size planner : le planner hybrid utilisait la formule standard, qui ne match pas le layout K|V packed de TurboQuant → assertions au merge des pages. Réparé.
- Backend selector : les couches exclues (sliding-window, Mamba, skipped) forçaient encore le backend
TURBOQUANTà tort. Maintenant elles tombent sur le backend par défaut. - ROCm
flash_attn_varlen_func: wrapper pour l’incompatout=(pas pour nous, on est CUDA).
Côté Genesis (rappel : la suite de patches dont je parlais en intro), c’est exactement ce que trois patches couvrent :
- P60 — fixe le ngram pour les modèles GDN (les couches Mamba)
- P65 — fait baisser le mode CUDA graph quand on est en speculative decoding
- P66 — filtre les tailles invalides au moment de la capture CUDA graph
Si #39931 fait le job nativement, ces trois-là deviennent caducs.
Trouver la bonne image
Le seul détail emmerdant : vLLM v0.20.1 est sorti le 4 mai à 10:36 UTC — soit ~14h avant le merge de #39931. La release officielle ne l’a donc pas. Et le tag nightly standard est resté coincé à 2026-05-04 06:08 UTC (commit 01d4d1ad du 04:33 UTC, prédate la PR de 20h). Le job nightly a soit planté, soit il n’a pas tourné ce jour-là.
Mais sur Docker Hub, il y avait ce tag bizarre : vllm/vllm-openai:gemma4-0505-cu130 pushé à 18:27 UTC le 5 mai — soit 18h après #39931. Le tag est probablement un build spécial pour le day-zero de Gemma 4 (qui sortait ce jour-là). Mais c’est un build main HEAD complet, donc il a la PR.
8.67 Go en amd64 + cu130 (CUDA 13.0). Exactement ce qu’il me faut pour sm_120 natif.
Premier crash : HAMi 0m
Pod déployé. Logs :
[HAMI-core Warn] invalid device memory limit CUDA_DEVICE_MEMORY_LIMIT_0=0m
...
File ".../vllm/v1/spec_decode/llm_base_proposer.py", line 151, in __init__
self.input_ids = torch.zeros(
RuntimeError: CUDA driver error: invalid argument
Aïe. Le bug HAMi-core que je connais déjà bien : Olares set CUDA_DEVICE_MEMORY_LIMIT_0=0m (intention : “pas de limite”) mais HAMi parse “0m” comme “0 bytes” et toute alloc CUDA crash. Je pensais que vLLM Runtime API y échappait (contrairement à Lucebox qui passe par Driver API). Faux : EagleProposer.__init__ (le drafter MTP) déclenche aussi le path HAMi intercepté.
Workaround connu : CUDA_DEVICE_MEMORY_LIMIT_0=24000m en env var override (même fix que lucedflashqwen36one v1.4.2+).
Deuxième crash : CUDA graph + TurboQuant + spec decoding
Pod redémarre. Cette fois on passe le boot, le model AutoRound se télécharge (17.69 GiB), TurboQuant détecte les couches d’attention pleine :
INFO config.py:195 TQ hybrid: full-attention layers [3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63]
INFO Resolved architecture: Qwen3_5MTP
INFO Using TURBOQUANT attention backend out of potential backends: ['TURBOQUANT'].
INFO Mamba cache mode is set to 'align' for Qwen3_5ForConditionalGeneration when prefix caching is enabled
Génial, PR #39931 + PR #40454 (Mamba ‘align’ mode) sont actifs nativement. Sans aucun Genesis patch.
Puis pendant la capture CUDA graph :
File ".../vllm/v1/attention/backends/turboquant_attn.py", line 583, in _prefill_attention
qsl = query_start_loc.tolist()
RuntimeError: Cannot copy between CPU and CUDA tensors during CUDA graph capture
unless the CPU tensor is pinned. Please use tensor.pin_memory() or allocate
the tensor with pin_memory=True.
Aïe bis. C’est précisément ce que Genesis P65 TURBOQUANT_SPEC_CG_DOWNGRADE monkey-patche. PR #39931 a NOT réglé ce point — elle ciblait l’aspect hybrid models, pas le CUDA graphs + spec decoding.
Workaround minimal : --enforce-eager (skip TOUS les CUDA graphs). On va perdre du speedup mais le pod va booter.
Le bench
Ajout du flag, redémarrage, le pod passe READY 1/1 après 3min37s. Trois prompts Space Invaders standards (HTML+CSS+JS, 800 tokens max, temp=0.6, top_p=0.95, MTP n=3 actif) :
Run 1: 800 tok in 11.12s = 71.97 t/s
Run 2: 800 tok in 10.84s = 73.81 t/s
Run 3: 800 tok in 11.13s = 71.87 t/s
AVG = 72.55 t/s [71.87 – 73.81]
Comparaison sur Olares One (RTX 5090M 24 Go sm_120)
| Stack | t/s | Patches custom |
|---|---|---|
Vanilla vLLM main HEAD + #39931 + --enforce-eager | 72.55 | 0 |
| llama.cpp standard (sans spec) | 33-36 | upstream pur |
| llama.cpp + MTP PR #22673 | 78.1 | 1 PR open |
| buun-llama-cpp DFlash + Q8_0 drafter | 80 | fork llama.cpp |
| Genesis 28 patches + image custom (baseline) | 88.0 | 28 patches + disable_p8 |
| Lucebox v1.4.4 (DFlash test_dflash + libvgpu hot-swap) | 88.5 | engine custom |
vLLM no-Genesis = -17.5% vs Genesis. C’est mieux que llama.cpp standard, mais en dessous de tout ce qui va plus loin que vanilla.
Pourquoi -17% : on perd les CUDA graphs
--enforce-eager désactive toutes les captures CUDA graph. Or sur les batches de décode 1-token (le cas commun en single-user), les CUDA graphs apportent typiquement +20-30% sur Blackwell.
Genesis P65 fait plus malin : il downgrade _cudagraph_support de UNIFORM_BATCH → UNIFORM_SINGLE_TOKEN_DECODE uniquement quand speculative_config est actif. Résultat : les batches de spec-verify K+1 (ceux qui crashent) tombent en eager, mais les batches de décode 1-token gardent leur CUDA graph.
Code du P65 (très propre, vraiment upstream-able) :
@classmethod
def get_cudagraph_support(cls, vllm_config, kv_cache_spec) -> AttentionCGSupport:
"""Context-aware downgrade for spec-decode only."""
if vllm_config.speculative_config is not None:
return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
return cls._cudagraph_support
C’est tout. Trois lignes ajoutées à la classe TurboQuantMetadataBuilder dans vllm/v1/attention/backends/turboquant_attn.py.
L’issue upstream existe déjà : #40807
En fouillant je tombe sur vLLM issue #40807 ouverte par noonghunna le 24 avril 2026 : “TurboQuant KV + spec-decode + chunked-prefill crashes CUDA graph capture at query_start_loc.tolist()”. C’est exactement notre crash, exactement la même ligne.
L’issue contient un thread fascinant :
- noonghunna confirme sur RTX 3090 (Ampere) avec Qwen3.6-27B
- Sandermage (auteur Genesis) commente le 24 avril, partage ses Patches 23/44/22/38 qui fixent + propose explicitement “Happy to extract Patch 23 + Patch 44 into an actual upstream PR if the core team finds the approach acceptable”
- xyehya confirme sur RTX 5080 (Blackwell consumer) avec Qwen3.5-9b NVFP4
- Mais 12 jours après l’offre de Sandermage : aucune PR upstream filée
Et avec mon test, on ajoute un quatrième hardware confirmé : RTX 5090M sm_120 mobile (Olares One). Le bug se reproduit sur Ampere, Blackwell consumer desktop, et Blackwell consumer mobile.
Ce qui marche déjà nativement (le bon côté)
PR #39931 a quand même livré du concret :
✅ TQ hybrid: full-attention layers [...] — TurboQuant skip Mamba/SWA tout seul
✅ Using TURBOQUANT attention backend — backend selector OK
✅ Resolved architecture: Qwen3_5MTP — l’arch MTP est native, pas besoin de Genesis P64
✅ Mamba cache align mode (PR #40454) — le cache de spec decoding hybrid est rangé
✅ TurboQuant overriding flash_attn_version to 2 — auto-fallback FA3→FA2
Côté Genesis, ça veut dire que P60 (GDN ngram fix) et P64 (Qwen3 Coder MTP streaming) sont caducs. P65/P66 (CUDA graph + spec) restent utiles.
Étape suivante : appliquer Patch 65 seul (et constater que ça ne suffit pas)
Curieux, j’ai écrit un init script qui applique le P65 de Sandermage en text-patch sur turboquant_attn.py au démarrage du pod, image vanilla gemma4-0505-cu130. Le boot passe, le P65 est bien détecté par vLLM :
P65 applied: TurboQuantMetadataBuilder.get_cudagraph_support added
WARNING [compilation.py:1390] CUDAGraphMode.FULL_AND_PIECEWISE is not supported with spec-decode for attention backend TurboQuantAttentionBackend (support: AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE); setting cudagraph_mode=PIECEWISE
Capturing CUDA graphs (mixed prefill-decode, PIECEWISE): 100%|██████████| 4/4
init engine ... took 100.32 s (compilation: 39.30 s)
Application startup complete.
CUDA graphs capturés sans crash, server READY 1/1. Bingo… non. À la première requête de bench, l’engine crash :
File ".../vllm/v1/attention/backends/turboquant_attn.py", line 862, in _decode_attention
current_workspace_manager().get_simultaneous(...)
AssertionError: Workspace is locked but allocation from 'turboquant_attn.py:862:
_decode_attention' requires 0.76 MB, current size is 0.00 MB. Workspace growth
is not allowed after locking.
Aïe. P65 fixe la capture CUDA graph — mais pas le workspace lock qui pète à la première inférence. C’est précisément ce que Sandermage décrivait dans son commentaire #40807 : “profiler-invisible torch.empty inside _continuation_prefill — the allocation happens after profile_run finishes, so vLLM’s KV sizing never accounts for it”. Ses Patches 22 + 38 pré-allouent ce buffer pour qu’il existe avant le lock.
Donc P65 seul ne suffit pas. Pour quitter Genesis pour de bon, il faut au minimum cinq patches du même paquet :
- P65 — la 3-ligner qui baisse le mode CUDA graph en spec-decode (celui qu’on vient de tester)
- P22 — partage du buffer de déquantization + pré-allocation du workspace K/V 4-D (gros morceau)
- P38 — la machine d’états mémoire pour la phase de “continuation prefill”
- Peut-être P44 — le buffer de sortie de l’attention pour rester idempotent sous capture
- Peut-être P23 — la pré-allocation de
cu_seqlens
C’est plus qu’un simple “extract P23+P44 to upstream PR” comme Sandermage l’avait initialement proposé. C’est un effort sérieux qui mérite probablement plusieurs PRs distinctes.
Verdict provisoire
On ne peut pas drop Genesis aujourd’hui sans perdre 17% de débit (workaround --enforce-eager) ou sans porter ~5 patches upstream non-triviaux. Le path est connu, mais il manque encore la suite de fixes CUDA graph + TurboQuant + spec + workspace dans vLLM main.
Mon plan immédiat :
- Commenter #40807 avec deux datapoints Blackwell consumer mobile inédits dans le thread :
- vanilla #39931 +
--enforce-eager= 72.55 t/s (vs 88 baseline Genesis) - vanilla #39931 + Patch 65 seul = boot OK + CG OK mais workspace lock crash à la 1ère inférence
- vanilla #39931 +
- Pousser Sandermage vers une série de PRs upstream — pas juste P23+P44, plutôt P22+P38+P65 minimum
- Tester le pack complet (P22+P38+P44+P65) sur ma 5090M quand Sandermage les extrait — pour valider que ça remonte à 88+ t/s
- Quand les PRs mergent : bumper l’image
vllmqwen36turbo27boneà vanillavllm/vllm-openai:nightlyet virer Genesis pour de bon
Pour suivre ça
- Issue #40807 — TurboQuant + spec + CG capture
- PR #39931 — la moitié-fix mergée
- Repo Sandermage/genesis-vllm-patches — où vivent P23/P44 en attendant upstream
- vLLM
v0.20.2à venir (probablement la première release stable avec #39931)
Crédits
- JartX pour PR #39931 et la fix hybrid TurboQuant
- Sandermage (Sander Barzov, Odessa) pour Genesis et les patches Patch 22/23/38/44 qui ont identifié le root cause
- noonghunna pour avoir filé #40807 avec un repro détaillé
- xyehya pour la confirmation Blackwell consumer (sa 5080)
- JartX pour le travail FP16 rotation upstream et la PR
Voilà ! Si vous tournez Qwen3.5/3.6 + TurboQuant + spec decoding sur n’importe quel hardware Blackwell consumer (5070/5080/5090/5090M) et que vous reproduisez ces 72 t/s en --enforce-eager, ou idéalement 88+ t/s avec les Patches Sandermage appliqués, envoyez-moi vos chiffres dans les commentaires. À très vite !
Disclosure — Tous les benchmarks de ce post tournent sur mon propre Olares One. Si le contenu vous a été utile et que vous envisagez d’en acheter un, commander via ce lien de parrainage vous donne 400 $ de réduction (3 599 $ au lieu de 3 999 $) et me rapporte 200 $. Je le mentionne par transparence — et oui, accessoirement, ça aide à faire vivre le blog (hébergement, domaine, et le temps que je passe à écrire ici). Lien valable jusqu’à fin juin 2026 environ.