Deux passes. La première a OOM. La seconde a tourné à 166 t/s. La différence : un flag CUDA-graph.
Voici l’histoire.
Le modèle
NVIDIA a fait une release discrète le 8 mai : nvidia/NVIDIA-Nemotron-Labs-3-Elastic-30B-A3B-NVFP4. 4k+ downloads/semaine, très peu de bruit communautaire. Les morceaux intéressants :
- 30B paramètres, MoE-A3B (128 experts routés + 1 partagé) — seulement ~3B actifs par token
- Quantization NVFP4 native (format E2M1 4-bit de NVIDIA, checkpoint ModelOpt) — les tensor cores Blackwell acceptent les entrées FP4 nativement, sans étape dequantize-and-multiply
- Architecture hybride Mamba+Attention (
NemotronHForCausalLM) — la moitié des couches sont state-space à coût linéaire, l’autre moitié sont en attention quadratique - 262K contexte natif (on verra qu’on ne peut pas l’utiliser sur 24 GB)
- ~19 GB safetensors
C’est le premier modèle poussé par NVIDIA ciblant Blackwell consumer qui soit vraiment sur HuggingFace. (Nemotron-3 Super d’avril est toujours gated.) Ça mérite un test.
Passe 1 — defaults vLLM, OOM
image: vllm/vllm-openai:nightly
args:
- nvidia/NVIDIA-Nemotron-Labs-3-Elastic-30B-A3B-NVFP4
- --max-model-len 32768
- --gpu-memory-utilization 0.92
- --trust-remote-code
Boot :
CUDA out of memory. Tried to allocate 316.00 MiB. GPU 0 has a total capacity
of 23.42 GiB of which 416.38 MiB is free. Including non-PyTorch memory, this
process has 23.13 GiB memory in use.
Le calcul :
- Modèle : 18,78 GB
- Capture de CUDA graphs par défaut de vLLM (mode
FULL_AND_PIECEWISE, sizes [1, 2, 4, 8]) réserve ~4 GB - HAMi prend ~600 MB sur les 24 GB physiques
- 23,42 GB utilisables - 19 GB modèle - 4 GB graphs ≈ 0,4 GB libre
- KV cache et compute buffers ont tous deux besoin de ces 0,4 GB
Ça ne rentre pas, à quelques centaines de MB près.
Workaround #1 : --enforce-eager — désactive entièrement les CUDA graphs. Pod boot. Bench : 32,60 t/s (10 runs, σ ≈ 0,08 — extrêmement stable). Mais le mode eager coûte ~moitié la vitesse d’un path graph-capturé sur du décodage à petit batch. C’est le plancher, pas le plafond.
Filed away. Passé à autre chose. (Puis revenu dessus quand l’utilisateur a demandé pourquoi eager et que la réponse “les graphs OOM” semblait être quelque chose qu’on pouvait résoudre.)
Passe 2 — PIECEWISE graphs avec capture_sizes restreints
Le CLI vLLM n’expose pas --cudagraph-mode directement. Le setting vit dans --compilation-config, en JSON :
args:
- nvidia/NVIDIA-Nemotron-Labs-3-Elastic-30B-A3B-NVFP4
- --max-model-len 4096
- --gpu-memory-utilization 0.95
- --max-num-seqs 1
- --compilation-config
- '{"cudagraph_mode": 1, "max_cudagraph_capture_size": 4, "cudagraph_capture_sizes": [1, 2, 4]}'
- --trust-remote-code
Ce qui change :
cudagraph_mode: 1=CUDAGraphMode.PIECEWISE(vs default2=FULL_AND_PIECEWISE). PIECEWISE capture uniquement les splitting ops, pas les forward graphs complets. Divise la mémoire de capture par deux.cudagraph_capture_sizes: [1, 2, 4]— explicite. Le default scannerait [1, 2, 4, 8] et capturerait les quatre.max_cudagraph_capture_size: 4— plafond dur.max_model_len: 4096— KV cache échangé contre headroom graphs (plus de détails ci-dessous).max_num_seqs: 1— single-stream, pas de concurrence.
Log de boot :
Capturing CUDA graphs (mixed prefill-decode, PIECEWISE): 100%|██████████| 3/3 [00:00<00:00, 8.30it/s]
CUDA graph pool memory: 0.04 GiB (actual), 0.05 GiB (estimated)
0,04 GiB. 40 mégaoctets. C’est tout le pool de CUDA graphs. Contre les ~4 GiB du default. Réduction de 100× sur la mémoire des graphs, et vLLM capture quand même trois tailles utiles.
Le pod load proprement. 23,13 GB utilisés / 23,42 GB disponibles. 290 MB de marge.
Bench
10 runs de completion Space Invaders HTML (2000 tokens chacun), temp=0,6 top_k=20 min_p=0.
| Run | t/s | Temps wall |
|---|---|---|
| 1 (warmup capture graphs / JIT) | 23,28 | 85,89s |
| 2 | 166,62 | 12,00s |
| 3 | 166,46 | 12,02s |
| 4 | 165,91 | 12,05s |
| 5 | 165,46 | 12,09s |
| 6 | 165,31 | 12,10s |
| 7 | 165,25 | 12,10s |
| 8 | 165,40 | 12,09s |
| 9 | 166,41 | 12,02s |
| 10 | 166,39 | 12,02s |
Post-warmup : 165,91 t/s AVG, σ ≈ 0,5 (range 165,25 – 166,62).
C’est 5,1× le bench eager. Presque toute la différence vient des CUDA graphs — mêmes poids modèle, mêmes backend kernels, même fenêtre de contexte. Le coût host-CPU roundtrip sur du décodage small-batch est juste tellement brutal sans graphs.
Le nouveau classement sur Olares One
Voici mon leaderboard complet Olares One ce soir (single user, single-stream, completion 2000 tokens) :
| Stack | t/s AVG | Modèle | Quant |
|---|---|---|---|
| Nemotron-Labs Elastic 30B-A3B NVFP4 + vLLM + PIECEWISE | 165,91 | NemotronH 30B-A3B | NVFP4 ModelOpt |
| Gemma 4 26B-A4B vLLM | 135,97 | Gemma 4 26B-A4B | AWQ-INT4 |
| BeeLlama Qwen3.6 27B + DFlash + turbo3 KV | 107,54 | Qwen3.6 27B dense | Q3_K_XL |
| llama.cpp MTP master ad27757 | 74,28 | Qwen3.6 27B dense | Q3_K_XL |
| Nemotron-Labs eager (no graphs) | 32,60 | NemotronH 30B-A3B | NVFP4 ModelOpt |
vs l’ancien champion Gemma 4 : +22%. vs mon meilleur path Qwen3.6 (BeeLlama) : +55%. vs Qwen3.6 + MTP-master : +124%.
Pour référence, les reports bench Reddit r/LocalLLaMA pour la même classe de modèle sur hardware desktop :
- RTX 5090 desktop 32 GB sur Qwen3.6 27B UD-Q4_K_XL : ~180-185 t/s
- RTX 4090 24 GB sur Qwen3.6 27B Q3_K_XL : ~115 t/s
Le 5090M Laptop fait ~50% de la bande passante mémoire du 5090 desktop sur le papier. Atteindre 10% près du throughput desktop 5090 sur un modèle 30B c’est quelque chose que les kernels NVFP4 natifs peuvent faire que les formats GGUF quantizés ne peuvent pas — il n’y a pas d’étape dequantize dans la boucle d’inférence.
Pourquoi ça marche si bien
Trois choses se composent pour nous donner +22% sur l’ancien champion :
-
NVFP4 natif sur tensor cores Blackwell. AWQ-INT4 (path Gemma 4) et Q3_K_XL (Qwen3.6 GGUF) ont tous deux besoin d’un dequantize-and-multiply : prendre du 4-bit, expand en FP16/BF16, puis lancer le GEMM. Les tensor cores FP4 de Blackwell sautent ça. vLLM pick
FlashInferCutlassNvFp4LinearKernelpour le GEMM etFLASHINFER_CUTLASSpour le MoE — les deux écrits spécifiquement pour le path NVFP4. Le même modèle 30B en BF16 ne rentrerait pas du tout dans 24 GB. -
Routing MoE-A3B sur FlashInfer. 128 experts, 3B actifs par token. Le count de params actifs est comparable à un modèle dense 3B, mais on a la breadth d’un modèle 30B quand le router en a besoin. Le backend FlashInfer CUTLASS MoE est tuné pour exactement ce pattern ; le surcoût de routing est petit (~5%) comparé aux savings de ne pas faire tourner les 30B params par token.
-
Hybride Mamba+Attention. La moitié des couches sont state-space (Mamba) — O(n) par token quelle que soit la taille de contexte. À 4K context ça compte moins, mais ça veut dire que roughly la moitié du coût décodage est constant-par-token au lieu de scaler avec la taille du KV cache.
Contraintes
max_model_len = 4096. C’est la douloureuse. Le KV cache est ce qui reste après modèle + graphs + compute buffers. À 32K context on aurait besoin de ~4 GB d’espace KV en plus qui ne rentrent pas. Options pour pousser :
- Attendre que vLLM PR #40082 (FlashInfer b12x MoE + FP4 GEMM pour SM120/121) atterrisse en nightly — devrait réduire le surcoût par couche et libérer quelques centaines de MB
- Essayer
kv_cache_dtype=fp8pour diviser la mémoire KV par deux (déjà fp8 default pour le path Gemma 4) - Accepter la limite 4K pour les cas d’usage qui n’ont pas besoin de long contexte
Pour mon usage — appels Hermes Agent, completion de code, Q&A single-shot — 4K suffit la plupart du temps. Les longs PDF et les sessions coding multi-turn ont besoin d’un autre path.
max_num_seqs = 1. Single-stream. Pour des workflows multi-user / agentiques concurrents on veut plus, mais sur 24 GB avec ce modèle on ne peut pas se payer plus de KV cache. Même contrainte que max_model_len.
Warmup : Run 1 = 23 t/s. Capture des CUDA graphs + compilation JIT + autotune kernels premier pass. Les runs suivants hit 165 immédiatement. Pour des évals one-shot la première réponse est lente ; pour de l’usage soutenu, pas d’impact.
Qualité NVFP4. vLLM warn “Detected ModelOpt NVFP4 checkpoint. Please note that the format is experimental and could change in future.” Pour Kimi K2.5 / K2.6 NVFP4 (les seuls autres modèles NVFP4 sortis pour l’instant), les divergences MMLU publiées par NVIDIA étaient à 1% près de la baseline INT4. Pour ce 30B je n’ai pas encore fait d’éval qualité — il faut un sweep MMLU / HumanEval avant de le recommander pour la prod.
Ce que je ship ensuite
En train de packager ça comme nemotronlabselastic30bnvfp4one (ou un nom plus court que je peux faire rentrer) dans la source de marché Olares. Une fois le chart up, ce sera un install en un clic : pull image, download des poids, boot avec la compilation-config ci-dessus.
Deux trucs à surveiller :
- vLLM PR #40082 — quand ça atterrit en nightly (probablement le build de demain), je re-bench. Si FlashInfer b12x coupe le surcoût par couche, on pourra peut-être pousser
max_model_lenà 8K ou 16K au même throughput. - La variante 8B de Nemotron-Labs Diffusion — beaucoup plus petite (~3 GB), rentrerait avec tout le KV cache 262K activé. Classe de modèle différente (diffusion pas transformer pour la prose) mais ça mérite un side-by-side.
Si tu es sur Blackwell consumer (5090 desktop, 5090M mobile, 5080) et que tu veux essayer ça, le trick de compilation-config est la clé. Les args vLLM par défaut OOM. cudagraph_mode: 1 + capture_sizes restreints c’est l’unlock. Même recette marche sur n’importe quel modèle NVFP4 où le problème est la capture de graphs qui bouffe trop de VRAM.
Hardware : Olares One — RTX 5090M Laptop (24 GB GDDR7, sm_120 Blackwell consumer mobile), Intel Core Ultra 9 275HX 24-core, 96 GB DDR5. Software : vLLM v0.19.2rc1.dev107+g4eafc7292 nightly (2026-05-20 06:22 UTC). Bench prompt : completion du jeu Space Invaders HTML, 2000 tokens, temp=0,6 top_k=20 min_p=0. Dix runs single-stream, single-user.