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

Qwen3.6-27B + MTP CUDA OOM à 262K sur 24Go — réglé en descendant un cran de quant UD

Un utilisateur a tapé un CUDA OOM reproductible en MTP draft sur ma v1.0.5 de Qwen3.6-27B à 262K de contexte. Boot OK, draft scale au-delà de l'estimation statique, exit 139 dans common_speculative_state_mtp draft. Réglé en descendant havenoammo UD-Q3_K_XL (14.9 Go) vers UD-Q2_K_XL (12.3 Go). Bench direct valide v1.0.7 à 72.14 t/s stable, 262K full, zéro OOM. Bonus : test pour drop les patches Genesis via NVFP4. Spoiler : ça marche pas.

Salut les amis ! Il y a deux nuits, j’ai shippé llamacppqwen36mtpone v1.0.5 sur le catalogue orales-one-market : havenoammo UD-Q3_K_XL (14.9 Go) à 262K de contexte avec MTP speculative decoding n=5. Mon bench de validation sortait 77 t/s soutenus à 262K avec 75-80 % d’acceptance du drafter — clean win sur la v1.0.3 (65 t/s @ 128K).

Puis un utilisateur m’a reporté la réalité :

the pod crashed frequently with exit code 139 due to a CUDA OOM during MTP drafting:

/src/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu:97: CUDA error
  CUDA error: out of memory
  current device: 0, in function ggml_cuda_graph_evaluate_and_capture
...
common_speculative_state_mtp::draft

J’ai désactivé MTP et le LLM tourne rock-stable. Même avec contexte > 200K il reste de la place pour réactiver MTP (c’est tight mais OK). Mais si je le fais et que MTP devient actif, CUDA crash de nouveau.

C’est le gap classique entre estimation mémoire statique au boot et pire cas en runtime. Les tailles de capture de CUDA graph que le moteur exerce vraiment pendant l’inférence dépassent l’estimation initiale.

TL;DR

Cause racine : le buffer de MTP draft scale avec les tailles de capture cudagraph

Quand le MTP de llama.cpp tourne, il capture des CUDA graphs pour le calcul du draft head. Les shapes capturées dépendent des patterns de batch + séquence que le moteur rencontre vraiment en inférence. L’estimation au boot --ctx-size 262144 --spec-draft-n-max 5 --cache-type-{k,v} q4_0 suffit pour les captures statiques de l’init, mais pas pour l’ensemble plus large de shapes que les vrais prompts déclenchent.

En chiffres, sur un 24 Go sm_120 mobile (5090M, RTX 5090 Laptop) :

Même problème documenté dans le thread llama.cpp MTP PR #22673 : contexte long + MTP bouffe plus de VRAM que ce que le calcul KV au boot suggère. Pas de flag --mtp-draft-buf-size pour limiter l’allocation runtime — le seul levier aujourd’hui c’est la taille du quant target.

Le fix : descendre d’un cran de quant Unsloth Dynamic

Le repo havenoammo/Qwen3.6-27B-MTP-UD-GGUF ship une ladder Unsloth Dynamic complète incluant UD-Q2_K_XL à 12.3 Go (vs Q3_K_XL 14.9 Go). Les 2.6 Go récupérés, c’est exactement ce dont les captures cudagraph MTP draft ont besoin en runtime.

Unsloth Dynamic préserve les couches critiques en précision plus haute, et (selon la recette de havenoammo) garde le MTP head en Q8_0 même dans les variantes Q2 — du coup la baisse de qualité benchmark est autour de 5-8 % vs Q3_K_XL plutôt que la baisse plus violente qu’on aurait avec un Q3_K_M → Q2_K K-quants standards.

Bench de validation direct

J’ai testé les DEUX : havenoammo UD-Q2_K_XL (celui que v1.0.7 ship) ET le tout fraîchement sorti unsloth/Qwen3.6-27B-GGUF-MTP UD-Q2_K_XL (même tier de quant, Unsloth officiel) sur Olares One.

Stack :

havenoammo UD-Q2_K_XL @ 262K + MTP n=5 (ce que v1.0.7 ship) :

runs: 68.60, 71.25, 75.24, 68.46, 76.25, 71.30, 73.12, 68.51, 73.92, 74.70 t/s
AVG = 72.14 t/s   MIN = 68.46   MAX = 76.25
ZÉRO CUDA OOM   PAS de cycle de dégradation   10 runs propres

unsloth UD-Q2_K_XL @ 262K + MTP n=5 (l’officiel Unsloth sorti aujourd’hui) :

runs: 66.01, 66.37, 64.46, 62.54, 65.48, 64.47, 62.88, 64.05, 61.58, 63.77 t/s
AVG = 64.16 t/s   MIN = 61.58   MAX = 66.37
ZÉRO CUDA OOM   PAS de cycle de dégradation   10 runs propres

havenoammo gagne de +12 % au même tier de quant. Intégration MTP / metadata différente. v1.0.7 garde havenoammo pour cette raison ; je revisiterai si l’intégration MTP d’Unsloth s’améliore.

Tableau comparatif

Stackt/sStabilitéContexte
v1.0.5 — havenoammo Q3_K_XL @ 262K + MTP n=577 (bench validation)❌ OOM runtime MTP draft262K
v1.0.6 — havenoammo Q3_K_XL @ 128K + MTP n=4 (transitoire, jamais shippée)~65 (safe prouvé)✅ stable128K
v1.0.7 — havenoammo Q2_K_XL @ 262K + MTP n=5 (ship maintenant)72.14✅ rock-stable262K
unsloth UD-Q2_K_XL @ 262K + MTP n=564.16✅ rock-stable262K

v1.0.7 est strictement meilleur que v1.0.5 pour qui valorise la stabilité — même tier de speedup, contexte full, zéro crash.

Bonus : tentative de drop-Genesis via NVFP4

Pendant que j’avais le GPU libre, j’ai testé sudo-0x2a/Qwen3.6-27B-NVFP4-GPTQ (NVFP4 + SmoothQuant + GPTQ, MTP / lm_head / embeddings préservés en BF16) sur vllm/vllm-openai:tokenspeed-preview-x86_64-ubuntu2404 stock — pas de Genesis.

vllmqwen36turbo27bone en prod tourne à 88 t/s avec aamsellem/vllm-qwen36-blackwell:0.20.0-genesis-v3 (vLLM modifié par 28 patches Genesis). La question : NVFP4 permet-il de drop ce build custom ?

Phase 1 — NVFP4 sans spec decoding : boot clean sur vLLM stock. 31.79 t/s STABLE sur 10 runs (0.2 % spread, ZÉRO outlier, ultra-déterministe). max-model-len a dû descendre de 88000 à 28000 pour le budget KV cache (3 Go nécessaires, 1.2 Go free après le load model).

Phase 2 — NVFP4 + MTP n=3 : CUDA OOM au chargement du MTP head : il faut 2.37 Go free, il y en a 1.0. Les patches Genesis libèrent ces ~2-3 Go quelque part — sûrement buffer reuse / cudagraph pruning / réordonnancement d’activations. Sans Genesis, le MTP head ne tient pas avec le model NVFP4 + KV + overhead non-PyTorch sur 24 Go.

Verdict : on garde Genesis en prod. Drop-Genesis coûte tout le speedup MTP. 88 t/s avec Genesis vs 31.79 t/s sans = -64 % de régression ; la simplification du build chain ne vaut pas ça.

Observation bonus à creuser : le run NVFP4-seul (sans spec decoding) a montré 0.2 % de spread sur 10 runs. Contraste avec mon résultat Gemma 4 DFlash où la même image montrait un cycle reproductible 5-fast/4-slow. Ça affine mon hypothèse sur le cycle DFlash : c’est spécifiquement un effet d’adaptive throttling du spec-decode (matchant le pattern “low-yield MTP fallback” documenté dans llama.cpp PR #22931, closed), PAS un bug moteur vLLM. Quand l’acceptance du drafter s’effondre temporairement, l’amplification 4.7× du verify path produit la phase slow, et la récupération arrive une fois l’état du drafter réaligné.

Reproductible

Chart Helm, image exact, tous les flags, harness de bench — tout est dans orales-one-market. Pin : v1.0.7.

Watch list

L’image custom aamsellem/llamacpp-mtp:0.1.0 deviendra inutile une fois la PR MTP de am17an #22673 mergée upstream — imminente après que sa préreq #22838 (parallel drafting) ait mergé dans master à 16:09 UTC le 11 mai. Le rebase est en flight. Quand le merge land, v1.0.8 swap vers ghcr.io/ggml-org/llama.cpp:server-cuda13-bNNNN mainline avec flags identiques.

Si quelqu’un run Qwen3.6-27B + MTP sur 24 Go et voit un crash reproductible similaire, descendez à Q2_K_XL — c’est le levier. Et pingez-moi si vous avez une variante Unsloth Dynamic tierce qui bat la marge +12 % de havenoammo au tier Q2.

Share this post on:

Commentaires