Salut les amis !
Ça fait sept jours.
Sept jours que j’ai filé l’issue #187 et la PR #188 chez Project-HAMi/HAMi-core, avec un fix propre des 6 hooks cuCtxGetDevice qui crashent silencieusement (le diag complet est dans l’épisode 7). La PR est OPEN, pas review, pas de commentaire des maintainers. Sept jours de silence côté upstream. Et moi, mon Olares One avec son RTX 5090M, il tourne toujours sur la lib HAMi originale qui crashe avec Illegal device id: -644371744 dès qu’un pod GPU bootote.
La saga s’arrêtait là, en théorie. J’attendais. Et puis ce matin, en buvant mon café, une idée toute simple : pourquoi est-ce que j’attends que ça merge upstream alors que je peux compiler ma lib patchée et la swap directement sur le node Olares ? La PR bénéficiera à toute la communauté HAMi le jour où elle merge. Mais en attendant, je débloque mon Olares moi-même.
Spoiler du soir : ça a marché. Et au bout du chemin, Lucebox tape 88,5 t/s sur Qwen3.6-27B, soit 0,5 t/s de plus que mon vLLM Turbo (88). C’est la première fois qu’un path llama.cpp-based passe devant vLLM Turbo sur ce hardware. Voilà l’histoire.
Petit rappel pour ceux qui débarquent
Lucebox c’est un fork de llama.cpp tuné par sandropuppo qui fait tourner DFlash speculative decoding avec des kernels CUDA custom. HAMi (Heterogeneous AI computing Virtualization Middleware) c’est la couche d’isolation GPU qui tourne sous les pods Kubernetes d’Olares. Et le bug que j’ai filé épisode 7, c’est six hooks dans HAMi-core qui lisent une variable dev non initialisée → crash random à chaque boot d’un pod qui utilise le path Driver API CUDA (Lucebox, llama.cpp récent, vLLM en async).
Le matin
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 vit à /usr/local/vgpu/libvgpu.so. Je fais un backup, puis je 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"
sha256 vérification : 85b2ebe47bfe.... C’est ma lib patchée.
Petit caveat à se rappeler : le swap est node-wide. Tous les pods GPU qui bootent après vont LD_PRELOAD ma version. Les pods déjà running gardent l’ancienne en mémoire (LD_PRELOAD lit le fichier au process load). Donc zéro impact sur ce qui tourne, mais à chaque restart du HAMi DaemonSet, le master peut se faire écraser. Risque modéré, réversible (backup en place).
L’instant où Lucebox boot… et trois bugs nouveaux
Je redéploie 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 crashe quand même. CrashLoop. Avec un nouveau message :
Couche 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 mes expériences buun-llama-cpp parallèles). 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/).
Couche 2 : Total VRAM: 0 MiB
Le pod boote, charge le target sur GPU… et 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 (intention : “pas de limite”). Mais HAMi-core parse “0m” comme “limite invalide” et la mémoire dispo retourne à 0 octet. C’est un autre bug HAMi original, séparé de ma PR #188.
Fix dans l’entrypoint avant de lancer server.py :
export CUDA_DEVICE_MEMORY_LIMIT_0=24000m
Couche 3 : prefix_cache.startup_sync() timeout 10 s
Le pod boote correctement maintenant, le target charge (851 tensors, 14,99 GiB sur GPU), le drafter charge (3,5 GiB). Puis :
File ".../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 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 que le daemon soit prêt.
Workaround : disable la prefix-cache et la prefill-cache (qui invoquent toutes deux 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, faudra fix le timeout dans la source (PR à filer plus tard).
L’après-midi : 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
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
24 232 Mo demandés vs 24 117 Mo autorisés (mon 23000m apparemment, mauvais calcul). Bump à 24 000 m (= 25 165 824 000 octets, soit 24,03 GiB ~150 Mo sous le hardware 24 463 MiB). Marge serrée mais devrait suffire.
Patch via kubectl patch deployment runtime (au lieu de re-bumper le chart) :
kubectl patch deployment lucedflashqwen36one ... '/spec/.../args/0' avec sed 23000m → 24000m
Pod recreate. 4 min 24 s plus tard, 2/2 Running, 0 restarts.
L’instant
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,19 s = 87,05 t/s
Run 2: 800 tok in 8,93 s = 89,54 t/s
Run 3: 800 tok in 9,00 s = 88,88 t/s
AVG 88,5 t/s [87-89,5].
Je relis les chiffres deux fois. C’est la première fois que je vois Lucebox déboucher sur du Blackwell consumer mobile depuis qu’on a commencé la saga il y a sept jours. Et c’est légèrement au-dessus de mon vLLM Turbo (Genesis 28 patches + TurboQuant K8V4 + MTP n=3) qui tourne à 88 t/s.
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 | Genesis + TurboQuant K8V4 + MTP n=3 | 88,0 |
| Lucebox DFlash HTTP | scripts/server.py + test_dflash + TQ3_0 KV | 88,5 🏆 |
Lucebox devient le nouveau champion sur Olares One. Pas un blowout — 0,5 t/s, c’est la marge de bruit. 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 a une 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. Si ça extrapole linéairement à Blackwell sm_120, on devrait taper 100+ t/s estimés dès qu’on rebuild avec PR #94 mergé.
(Spoiler de l’épisode 9 : ça n’extrapole pas linéairement. Mais c’est une autre histoire.)
La leçon de cet épisode
La saga avait un cliffhanger naturel à l’épisode 7 : “PR filed, on attend”. Avec un peu d’agressivité — compiler son propre libvgpu.so au lieu d’attendre upstream — on transforme “on attend” en “ça marche”. Aucun des trois workarounds 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 - 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 → expected ~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” (séparée de ma PR #188)
- Tester PFlash (10× prefill speedup) en combo avec DFlash decode
Épisode 9 — la tentative à 106 t/s qui s’écrase à 88,7 t/s, et pourquoi. À 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.