Épisode 7 — j’avais filé l’issue #187 et la PR #188 chez Project-HAMi/HAMi-core avec le fix des 6 hooks cuCtxGetDevice. Et puis… rien. La PR est restée OPEN, sans review. La saga s’arrêtait là, en attendant que quelqu’un côté HAMi maintainer s’y intéresse.
Sept jours plus tard, toujours pas review. Mais entre temps, j’ai eu une idée toute simple : pourquoi attendre que ça merge upstream alors que je peux compiler mon libvgpu.so patché et le hot-swap directement sur le node Olares ? La PR bénéficierait à toute la communauté HAMi quand mergée — mais en attendant, je débloque mon Olares moi-même.
Spoiler : ça a marché. Et au bout de la chaîne, Lucebox tourne à 88,5 t/s sur Qwen3.6-27B, ce qui bat de 0,5 t/s mon vLLM Turbo (88). Voilà comment.
Phase 1 : hot-swap libvgpu.so
Je rebuild HAMi-core depuis ma branche fix/cumemcreate-uninit-dev (les commits de la PR #188), sortie : libvgpu.so 676 KB, ELF amd64. La version Olares originale fait 863 KB (build différent, plus d’options). Pas grave, l’ABI est compatible.
Sur le node Olares, le libvgpu master est à /usr/local/vgpu/libvgpu.so. Backup, swap :
ssh olares@olares-one "sudo cp /usr/local/vgpu/libvgpu.so /usr/local/vgpu/libvgpu.so.backup-original
sudo cp /tmp/libvgpu.so.patched /usr/local/vgpu/libvgpu.so"
Vérification du sha256 : 85b2ebe47bfe... (notre patché). Bingo.
Caveat : c’est node-wide. Tous les pods GPU qui démarreront après le swap vont LD_PRELOAD la version patchée. Les pods déjà running gardent l’ancienne en mémoire (LD_PRELOAD lit le fichier au process load). Donc zero impact sur ce qui tourne, mais à chaque restart Olares HAMi DaemonSet, le master peut se faire écraser. Risk modéré, réversible (backup en place).
Phase 2 : Lucebox boot… et trois layers de bugs
Je réinstalle lucedflashqwen36one v1.3.0 (chart classique du repo). Le bug “Illegal device id” de l’épisode 5 disparaît. HAMi est débloqué.
Sauf que. Le pod crash quand même. CrashLoop. Avec un nouveau message :
Layer 1 : gguf_init_from_file_ptr: invalid magic characters: 'p???', expected 'GGUF'
J’utilisais le drafter spiritbuun/Qwen3.6-27B-DFlash-GGUF dans le chart (héritage de mon expérience buun-llama-cpp parallèle). Mais Lucebox a son propre loader custom pour les safetensors BF16 de z-lab — il ne lit pas le GGUF de spiritbuun. Format incompatible.
Fix : revert au drafter z-lab BF16 (huggingface-cli download z-lab/Qwen3.6-27B-DFlash --local-dir models/draft/) qui télécharge model.safetensors + config.json.
Layer 2 : Total VRAM: 0 MiB puis cuMemoryAllocate failed res=2
Le pod boot, charge le target sur GPU… puis OOM immédiat. Logs HAMi :
[HAMI-core Warn]: invalid device memory limit CUDA_DEVICE_MEMORY_LIMIT_0=0m
ggml_cuda_init: found 1 CUDA devices (Total VRAM: 0 MiB):
Olares set CUDA_DEVICE_MEMORY_LIMIT_0=0m par défaut (ce qui devrait signifier “no limit”). Mais HAMi-core parse “0m” comme “limite invalide” et set la mémoire dispo à 0 bytes. Sur le path Runtime API (cudaMalloc) ce check est bypass. Sur le path Driver API que test_dflash utilise, c’est strict. Résultat : 0 MiB visible.
Diagnostic dans la source HAMi-core, fonction get_limit_from_env :
if (scaled_res == 0) {
if (env_name[12]=='M'){
LOG_WARN("invalid device memory limit %s=%s",env_name,env_limit);
}
return 0;
}
“0m” → res=0 → scaled_res=0 → log “invalid” → return 0. Le caller traite 0 comme “no memory available”. Bug HAMi original, pas dans ma PR #188.
Fix : override dans l’entrypoint avant de lancer server.py :
export CUDA_DEVICE_MEMORY_LIMIT_0=24000m # 24 GB, juste sous le hardware 24463
Layer 3 : prefix_cache.startup_sync() timeout 10s
Le pod boot maintenant correctement, le target charge (851 tensors, 14,99 GiB sur GPU), le drafter charge (3,5 GiB). Puis :
File "/opt/lucebox/src/dflash/scripts/prefix_cache.py", line 670, in startup_sync
reply = await self._await_reply("[snap] slots=")
asyncio.exceptions.TimeoutError
server.py lance le daemon test_dflash puis attend qu’il imprime [snap] slots= sur stdout. Timeout codé en dur à 10 secondes. Mais le daemon, sur premier boot, doit JIT-compiler ses kernels CUDA pour sm_120 (compute capability 12.0 = Blackwell consumer). Ça prend 30-60 secondes. Le timeout fire avant le daemon ready.
Fix : disable la prefix-cache et la prefill-cache (qui invoquent startup_sync) :
python3 scripts/server.py \
--prefix-cache-slots 0 \
--prefill-cache-slots 0 \
...
Trade-off : pas de réutilisation de KV cache cross-request. Pour bench solo c’est OK ; pour usage agent en production, faudrait fix le timeout dans la source (PR à filer plus tard).
Phase 3 : OOM round 2
Boot OK ! Server.py log :
Luce DFlash OpenAI server on http://0.0.0.0:8000
target = /models/Qwen3.6-27B-Q4_K_M.gguf
draft = /models/draft/model.safetensors
bin = /opt/lucebox/dflash-build/test_dflash
budget = 22
[daemon] [target] target loaded: 851 tensors on GPU 14.99 GiB
[daemon] [draft] loaded
[daemon] [daemon] ready
Premier curl sur /v1/chat/completions :
500 Internal Server Error
{"detail":"dflash daemon has exited unexpectedly"}
Aïe. Le daemon a crashé sur la première inférence. Logs :
[HAMI-core ERROR allocator.c:56]: Device 0 OOM 24232551680 / 24117248000
ggml_backend_cuda_buffer_type_alloc_buffer: allocating 2428.00 MiB on device 0: cudaMalloc failed: out of memory
24232 MB requis vs 24117 MB autorisé (notre 23000m… wait, j’avais set 23000m ? Non, 24000m) → ah ok, c’est 24117 MB qui correspond à 23000 * 1024 * 1024 si je calcule. J’avais oublié de bump correctement.
Bump à 24000m (= 24117 * 1024 * 1024 réel = 25165824000 octets, soit 24,03 GiB ~ 150 MB sous le hardware 24463 MiB). Marge serrée mais devrait suffire.
Phase 4 : le bench
Patch deployment runtime via kubectl patch (au lieu de re-bumper le chart) :
kubectl patch deployment lucedflashqwen36one ... '/spec/.../args/0' avec sed 23000m → 24000m
Pod recreate. 4m24s plus tard, 2/2 Running, 0 restarts.
Bench Space Invaders × 3 (max_tokens=800, temp=0,6) via python3 urllib.request à l’intérieur du container (curl pas dans l’image Lucebox, mais Python si) :
Run 1: 800 tok in 9,19s = 87,05 t/s
Run 2: 800 tok in 8,93s = 89,54 t/s
Run 3: 800 tok in 9,00s = 88,88 t/s
AVG 88,5 t/s [87-89,5].
Le classement final sur Olares One (4 mai 2026)
| Backend | Stack | t/s avg |
|---|---|---|
| llama.cpp standard | UD-Q4_K_XL, pas de spec | 33-36 |
| buun-llama-cpp DFlash | HEAD + Q8_0 GGUF drafter | 80 |
| vLLM Turbo | v0.20.0 + Genesis + TurboQuant K8V4 + MTP n=3 | 88,0 |
| Lucebox DFlash HTTP | scripts/server.py + test_dflash + TQ3_0 KV | 88,5 🏆 |
| vLLM vanilla (autre app) | 0.19.1 + AutoRound INT4 + MTP n=3 | 99 peak |
Lucebox devient le nouveau champion sur Olares One, devant vLLM Turbo de 0,5 t/s. Pas un blowout, mais c’est la première fois qu’un path llama.cpp-based sur Blackwell consumer mobile bat le path vLLM custom. Et c’est avec le drafter mismatched 3.5 qui acceptance moyenne. La PR #94 de Lucebox du 4 mai annonce 106 t/s sur RTX 4090 (Ada sm_89) en activant le matched 3.6 draft avec support SWA. Sur Blackwell sm_120, l’extrapolation donne 100+ t/s estimés dès qu’on rebuild avec PR #94 mergé.
La leçon de cet épisode
La saga avait un cliffhanger naturel à l’épisode 7 : “PR filed, attendons”. Avec un peu d’agressivité (compiler son propre libvgpu.so au lieu d’attendre upstream), on peut transformer “attendons” en “ça marche”. Trois workarounds plus tard. Aucun n’est très propre :
- Hot-swap node-wide : risque que HAMi DaemonSet rewrite mon master à la prochaine update Olares
- Override
CUDA_DEVICE_MEMORY_LIMIT_0=24000m: workaround d’un bug HAMi original qui mériterait sa propre PR (“0m means no limit”) - Disable prefix-cache : perd la réutilisation de KV cache. À fix dans
server.py(bumper le timeout)
Mais 88,5 t/s sur 24 Go consumer mobile via le path Lucebox HTTP — c’est la première démo publique. Suffit à valider que le path est viable. Le polish vient ensuite.
Prochaines étapes
- Rebuild image quand PR #94 (matched 3.6 draft + SWA) est mergée → bench → expect ~100 t/s
- Filer une PR à Lucebox pour bumper le timeout
startup_sync(le 10s en dur est trop court sur sm_120) - Filer une PR à HAMi-core pour fix la sémantique “0m = no limit” sur memory_limit (séparée de PR #188)
- Tester PFlash (10× prefill speedup) en combo avec DFlash decode
Voilà ! Si vous tournez sur 5090M, 4080M ou 3090 24 Go et que vous reproduisez ces chiffres (ou battez 100+ t/s avec PR #94), je veux savoir comment vous avez fait. À 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.