Skip to content
/ airelien.dev
Go back
Aurélien AMSELLEM

Quitter les 28 patches Genesis sur vLLM ? Bench vanilla : 88 → 72,5 t/s, voilà pourquoi

PR #39931 (TurboQuant hybrid) mergée dans vLLM main hier matin. J'ai testé sur Olares One avec ZÉRO Genesis patch, image vanilla vllm/vllm-openai:gemma4-0505-cu130. Verdict : 72.55 t/s avec --enforce-eager (vs 88 baseline Genesis = -17.5%). Bonus : on a recroisé deux bugs HAMi/CUDA-graph + l'issue #40807 déjà dans le pipe upstream.

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 :

  1. Hybrid models : TurboQuant plantait NotImplementedError dès qu’il croisait une couche Mamba sur Qwen3.5/3.6/Qwen3-Next. Maintenant il applique TurboQuant uniquement sur les couches full_attention et passe les Mamba/SWA en transparent.
  2. 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é.
  3. 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.
  4. ROCm flash_attn_varlen_func : wrapper pour l’incompat out= (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 :

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)

Stackt/sPatches custom
Vanilla vLLM main HEAD + #39931 + --enforce-eager72.550
llama.cpp standard (sans spec)33-36upstream pur
llama.cpp + MTP PR #2267378.11 PR open
buun-llama-cpp DFlash + Q8_0 drafter80fork llama.cpp
Genesis 28 patches + image custom (baseline)88.028 patches + disable_p8
Lucebox v1.4.4 (DFlash test_dflash + libvgpu hot-swap)88.5engine 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_BATCHUNIFORM_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 :

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 :

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 :

  1. 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
  2. Pousser Sandermage vers une série de PRs upstream — pas juste P23+P44, plutôt P22+P38+P65 minimum
  3. Tester le pack complet (P22+P38+P44+P65) sur ma 5090M quand Sandermage les extrait — pour valider que ça remonte à 88+ t/s
  4. Quand les PRs mergent : bumper l’image vllmqwen36turbo27bone à vanilla vllm/vllm-openai:nightly et virer Genesis pour de bon

Pour suivre ça

Crédits

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.

Share this post on:

Commentaires