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

Gemma 4 26B-A4B vision via vLLM — 135 t/s à 128K pour un office workhorse sur 24 GB

Un peer user d'Olares One a partagé un patch Discord pour restaurer la vision sur la chart gemma426ba4bone. 24 heures plus tard, j'avais shippé un variant vLLM à 135 t/s à 128K de contexte — et le même user l'a validé en production. L'histoire d'une boucle community-driven, quatre configs llama.cpp benchées en parallèle, et le moment où turbo3 KV a cessé d'être la réponse.

Cette histoire a commencé sur Discord. Un peer user d’Olares One (même hardware RTX 5090M 24 GB sm_120 que moi) m’a envoyé le genre de message qui vaut son pesant d’or :

« I was looking for a vision capable model with decent context size and downloaded gemma426ba4bone but realized that one doesn’t have vision. So I tried a few things and came out with a pretty good result I wanted to share. »

Il avait hand-patché ma chart gemma426ba4bone v1.0.9 (variant text-only MTP) pour restaurer la vision : ajouté mmproj-F16, retiré MTP (incompatible avec multimodal en llama.cpp), retiré --mlock (son host ne pouvait pas lock 19 GB), et re-activé GGML_CUDA_GRAPH_OPT=1. 125 t/s avg avec vision + 128K de contexte.

24 heures plus tard, j’avais shippé vllmgemma426ba4bvisionone v1.0.1 à 135 t/s, 128K de contexte, vision validée end-to-end, range 0,54 — et le même user a validé en production : « This one is really a perfect alrounder which many people will appreciate. »

Voici comment quatre configs llama.cpp et un pivot vers vLLM nous ont menés là, et pourquoi turbo3 KV a cessé d’être la réponse sur Gemma 4.

Le framing du use case

Au milieu du thread le peer a lâché son vrai objectif, le framing le plus concret d’un workload LLM local que j’ai vu en quelques mois :

« A tech stack that allows decent office work. A workhorse for the everyday use in a company. 2-3 concurrent user → vllm for PagedAttention. 128k or higher context window to allow decent multiple user and long(er) pdf processing. Vision to process scans and images. Performance > 100 t/s to make it acceptable to work with. »

Quatre contraintes : PagedAttention (donc vLLM, pas llama.cpp), 128K de contexte (long PDFs + multi-user reservation), vision (scan/image), >100 t/s. Je creuse chacune dans l’ordre.

Étape 1 — Ship le patch tel quel

Premier réflexe : shipper sa config patchée exactement comme il l’avait envoyée, pour que la communauté puisse installer en un clic via Olares Studio. Création de llamacppgemma426ba4bvisionone v1.0.3 (atomic fork llama.cpp + mmproj F16 + q4_0 KV + no spec + --parallel 2 + GGML_CUDA_GRAPH_OPT=1). Bench 10 runs Space Invaders HTML, 2000 tokens chacun.

Mais j’avais aussi 3 choses à tester qu’il n’avait pas fait :

Donc quatre configs :

VerKVSpec—parallelAVG t/sRange
v1.0.0turbo3ngram-cache2115,930,23
v1.0.1turbo32116,200,15
v1.0.2q8_02123,970,22
v1.0.3q4_01121,810,26

Findings :

  1. ngram-cache ne contribue à rien sur HTML generation. v1.0.0 → v1.0.1 : drop ngram-cache, gain marginal +0,27 t/s. Le drafter ne fire jamais sur des workloads à output variable — pure overhead. C’était ma première hypothèse à valider. Confirmée.
  2. turbo3 KV a un vrai overhead sur Gemma 4 — environ 6,7 %. v1.0.1 → v1.0.2 : swap turbo3 → q8_0, gain +6,7 %. Inattendu. Sur Qwen3.6 27B avec BeeLlama, turbo3 KV est essentiel (économise 50 % de mémoire, permet 262K). Sur Gemma 4, le coût de dequant Hadamard rotation dépasse l’économie de bandwidth. La classe de modèle compte plus que la taille du KV pour ce hardware.
  3. q4_0 KV est légèrement plus lent que q8_0 mais libère 3,5 GB de marge, dont j’avais besoin pour le compute buffer d’image à 128K. v1.0.3 garde q4_0 pour ça. Trade-off : -1,7 % de throughput pour la safety margin.

Ce dernier point est ce qui a shippé en v1.0.3, le setup du peer amélioré d’~3 t/s et validé end-to-end avec image input (un PNG 64×64 gradient → “Red” correctement identifié, 0,73 s wall time vision processing incluse).

Étape 2 — Le pivot vLLM

La vraie contrainte du peer était PagedAttention pour 2-3 users concurrents, ce que llama.cpp ne fait pas vraiment (le flag --parallel N existe mais c’est de la réservation de slots, pas du scheduling multi-request vLLM-grade). Et sa préoccupation était que le fork Atomic que j’utilisais est maintenu off-master, donc les futurs updates llama.cpp ne touchent pas facilement ce path.

vLLM supporte Gemma 4 multimodal nativement via l’architecture Gemma4ForConditionalGeneration dans cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit (compressed-tensors WNA16-Marlin, ~17 GB sur GPU, vision encoder bundled, pas de mmproj séparé). À tester.

Les diffs de config :

Image: vllm/vllm-openai:tokenspeed-preview-x86_64-ubuntu2404
Model: cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit (multimodal natif)
Attention: triton_attn (Gemma 4 head_dim hétérogène force ça — flash_attn errore)
KV cache: fp8 (vLLM natif)
Spec: aucun (le DFlash drafter a un bug de cycle 5-fast/4-slow, plus loin)
max_num_seqs: 4
max_num_batched_tokens: 8192
gpu_memory_utilization: 0.85 (initialement)
max_model_len: 16384 (initialement)

Bench, 10 runs, même prompt Space Invaders HTML :

AVG 135,97 t/s
MIN 135,67, MAX 136,23
Range 0,56

+9,7 % sur la meilleure config llama.cpp (v1.0.2), avec une stabilité exceptionnelle. Vision validée identiquement — gradient image → “Red”. Shippé en vllmgemma426ba4bvisionone v1.0.0. Suppression du variant llama.cpp (une app par use case, le peer était clair sur le fait de vouloir une seule app Gemma vision optimisée).

Étape 3 — Pousser le contexte de 16K à 128K

v1.0.0 shippé avec 16K, valeur conservative récupérée de vllmgemma4dflashone. Le use case office workhorse du peer demande 128K (PDF processing long sur 2-3 slots concurrents).

Math sur 24 GB :

Target loaded:           16,6 GB
vLLM compute reserve:    ~3-5 GB
Available KV cache:      ~3-4 GB (à gpu_mem 0,85)
fp8 KV par token:        ~80-100 KB pour Gemma 4 hybrid (mixed SSM + attn)
KV cache pour 16K:       ~1,4 GB
KV cache pour 128K:      ~10 GB  ← ne fit pas à 0,85

L’utilization 0,85 laissait 3,7 GB libre à 16K, insuffisant pour 128K. Bump gpu_memory_utilization à 0,92 (déjà validé safe sur vllmgemma4dflashone v1.0.4). Ça libère assez de marge pour fit 128K + la headroom pour le cudagraph profiling et la réservation multi-seq.

Patché et benché, 10 runs :

AVG 135,85 t/s @ 128K context
MIN 135,52, MAX 136,06
Range 0,54

Identique au bench 16K (135,97 t/s, range 0,56) dans le bruit de mesure. 8× le contexte pour zéro régression throughput. fp8 KV dans vLLM est vraiment efficace sur Gemma 4 hybrid — la pression bandwidth reste gérable même à 128K.

Shippé en vllmgemma426ba4bvisionone v1.0.1 dans l’heure. Temps total de “peer mentionne 128K” à “v1.0.1 en production” — environ 2 heures incluant le bench.

Pourquoi turbo3 a cessé d’être la réponse

C’est le finding que je veux highlight. En démarrant ce travail j’avais un prior fort : turbo3 KV (3-bit Walsh-Hadamard rotated cache) est toujours le bon choix sur Blackwell consumer mobile parce que (a) il économise 50 % de mémoire vs q4_0 et (b) BeeLlama a hit 262K sur Qwen3.6 27B précisément parce que turbo3 était si compact.

Ce prior était faux sur Gemma 4. La progression du bench est brutale :

v1.0.0 turbo3 KV + ngram-cache  → 115,93 t/s
v1.0.1 turbo3 KV + no spec      → 116,20 t/s
v1.0.2 q8_0 KV   + no spec      → 123,97 t/s  ← +6,7 % juste en switchant turbo3 → q8_0

Ce qui se passe : sur Gemma 4 dense / MoE-3,8B-active, le dequant Hadamard rotation ajoute ~120 micro-ops par token par layer. Sur Qwen3.6 hybrid arch (Gated DeltaNet + SSM), il y a moins de couches attention (les couches SSM n’utilisent pas le KV traditionnel), donc l’overhead rotation est amorti sur moins de boulot. La même primitive qui win sur Qwen3.6 lose sur Gemma 4.

Et dans vLLM la question équivalente est FP8 KV vs no KV quant. FP8 win facilement sur Gemma 4 : 1,7× la capacity, ~zéro quality loss per documentation NVIDIA, ET le dequant est fused dans le matmul sur les tensor cores H100/Blackwell donc pas d’overhead mesurable. C’est pourquoi le push à 128K marche sans coût throughput — fp8 KV est la bonne primitive pour cette classe de modèle.

Le tradeoff KV quant per-model est réel. Je traitais turbo3 comme un default universel. C’est faux. La bonne règle est : turbo3 pour Qwen3.6 (sauve la situation à 262K), q8_0 pour Gemma 4 + fork AtomicBot, fp8 pour les modèles vLLM-servés. Il n’y a pas de “best KV format” unique — ça dépend du kernel dequant utilisé par le moteur d’inférence.

Le retest DFlash drafter

Une question ouverte à creuser : est-ce qu’ajouter DFlash spec decoding pousse le throughput au-delà de 135 t/s ? vLLM a un drafter z-lab DFlash pour Gemma 4 spécifiquement (z-lab/gemma-4-26B-A4B-it-DFlash). Mon app text-only vllmgemma4dflashone faisait 224 t/s avec.

Mais j’avais documenté un “cycle 5-fast/4-slow” sur ce déploiement DFlash text-only : distribution bimodale où 60 % des requests faisaient 215-235 t/s et 40 % faisaient 60-100 t/s, sans aucune stratégie de récupération qui marchait (enforce-eager, prefix-caching off, max-num-seqs=1 — tous essayés, aucun ne fixait).

Aujourd’hui vLLM a mergé PR #42692 [Bugfix] DFlash FP8 KV-Cache. Hopeful, j’ai switché à v0.21.0-x86_64-cu129-ubuntu2404 (la dernière stable avec le fix), ajouté le DFlash drafter, et bench. 10 runs à 32K (pas fit 128K + DFlash drafter sur 24 GB — le drafter prend ~3 GB de footprint supplémentaire) :

runs: 218, 229, 62, 223, 215, 103, 62, 224, 62, 221
AVG: 161,94 (trompeur)
MIN: 61,66, MAX: 228,61
Range: 167

Même cycle bimodal. PR #42692 a fixé un autre bug DFlash + FP8 (correctness/precision probable), pas le adaptive spec throttling qui cause notre distribution. Le cycle est un issue scheduler upstream vLLM — quand DFlash détecte “draft acceptance dropping” il fallback en no-spec target-only pour quelques requests, puis re-teste. Sur un workload où DFlash always wins (Gemma 4 + cyankiwi AWQ), cette oscillation est pure UX damage.

UX math : avec DFlash, 40 % des user requests font 60 t/s (pire que la baseline no-spec 136). Sans DFlash, chaque request fait 135. La consistency win. Le variant DFlash reste off jusqu’à ce qu’upstream vLLM addresse le throttling cycle spécifiquement.

Ce qui a shippé

vllmgemma426ba4bvisionone v1.0.1 sur le catalog orales-one-market. Config finale :

Image:  vllm/vllm-openai:tokenspeed-preview-x86_64-ubuntu2404
Model:  cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit
KV:     fp8 (vLLM natif)
Attn:   triton_attn (Gemma 4 head_dim hetero force ça)
Spec:   disabled (DFlash cycle bug, pas de MTP pour multimodal)
ctx:    131072 (128K)
gpu_mem: 0.92
max_num_seqs: 4
prefix-caching: enabled
multimodal: native (vision + audio config bundled, audio path pas encore wiré)

Le peer a validé en production dans les heures suivant le ship de v1.0.1 : « I’ve already downloaded and briefly tested vllmgemma426ba4bvisionone — works like a charm 🙂 Will do more in-depth tests later… This one is really a perfect alrounder which many people will appreciate, I am sure. »

C’est la full feedback loop. Discord patch → app shipped → bench iteration → peer validates → community can install. Temps total écoulé environ 30 heures.

La suite pour v1.0.2

Deux paths à surveiller :

  1. Weights Gemma 4 NVFP4. NVIDIA a release Kimi K2.6 NVFP4 il y a quelques jours, validant NVFP4 comme production-grade pour des modèles externes. Si un Gemma-4-26B-A4B-NVFP4 land (soit NVIDIA eux-mêmes soit community port), on économiserait ~5 GB sur le footprint modèle, ce qui pourrait pousser max_model_len de 128K à 250K+ à la même gpu_memory_utilization. C’est le path “très long PDF batch” que le peer a mentionné vouloir eventually.

  2. vLLM upstream fix pour le adaptive spec throttling cycle. Quand ça land, on revisit DFlash et on vise 200+ t/s steady-state avec vision. Probablement une v1.0.2 juste pour ça.

Pour l’instant le path office workhorse est shippé, peer-validé, et serving. Vision + 128K + >100 t/s + PagedAttention sur un 24 GB consumer Blackwell mobile est atteignable aujourd’hui.

Reproductibilité

Si tu run ce setup sur une autre carte sm_120, les cartes 32 GB ont la marge pour pousser max_model_len à 256K+ trivialement. Dis-moi tes chiffres.

Share this post on:

Commentaires