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::draftJ’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
- v1.0.5 (havenoammo UD-Q3_K_XL @ 262K + MTP n=5) : boot OK, 77 t/s sur validation, CUDA OOM runtime dans le MTP draft sur de vrais prompts
- v1.0.7 (havenoammo UD-Q2_K_XL @ 262K + MTP n=5, ce qui ship maintenant) : 72.14 t/s AVG sur 10 runs propres, ZÉRO OOM, contexte 262K préservé
- Trade-off : seulement -6 % de t/s pour la stabilité et le contexte full. Strictement meilleur que v1.0.5.
- Bonus : j’ai testé
sudo-0x2a/Qwen3.6-27B-NVFP4-GPTQsur vLLM stock pour voir si NVFP4 permettait de drop les patches Genesis. Verdict : non — NVFP4 seul donne 31.79 t/s (-64 % vs 88 t/s Genesis), et ajouter MTP n=3 OOM en 24 Go.
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) :
- Target Q3_K_XL : 14.92 Go
- Cache KV q4_0 à 262K : ~4.3 Go
- Init vLLM + buffers compute : ~3-4 Go
- Captures cudagraph MTP draft : il faut ~2-3 Go de plus que l’estimation boot
- Résultat : VRAM épuisée en runtime, exit 139 dans
ggml_cuda_graph_evaluate_and_capture.
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 :
- Matos : Olares One — RTX 5090 Laptop, 24 Go GDDR7, 896 Go/s, sm_120 Blackwell mobile
- Image :
aamsellem/llamacpp-mtp:0.1.0(build custom depuis la branche MTP de am17an ; deviendra droppable une fois #22673 mergée) - Args :
--ctx-size 262144 --cache-type-k q4_0 --cache-type-v q4_0 --batch-size 512 --ubatch-size 512 --parallel 1 --flash-attn on --spec-type mtp --spec-draft-n-max 5 --chat-template-kwargs '{"enable_thinking": false}' - Prompt : Space Invaders HTML, 2000 tokens, temp 0.6 top_p 0.95
- Méthodo : 2 warmups + 10 runs mesurés
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
| Stack | t/s | Stabilité | Contexte |
|---|---|---|---|
| v1.0.5 — havenoammo Q3_K_XL @ 262K + MTP n=5 | 77 (bench validation) | ❌ OOM runtime MTP draft | 262K |
| v1.0.6 — havenoammo Q3_K_XL @ 128K + MTP n=4 (transitoire, jamais shippée) | ~65 (safe prouvé) | ✅ stable | 128K |
| v1.0.7 — havenoammo Q2_K_XL @ 262K + MTP n=5 (ship maintenant) | 72.14 | ✅ rock-stable | 262K |
| unsloth UD-Q2_K_XL @ 262K + MTP n=5 | 64.16 | ✅ rock-stable | 262K |
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.