Aller au contenu
Sylvain Joffraud l4w
Retour

Gemma 4 sous MLX : le cache à fenêtre glissante se relit

Avec Gemma 4 sous MLX, le prompt cache de mlx_lm.server ne servait à rien d’une conversation à l’autre : les couches à fenêtre glissante (sliding window) de Gemma 4 ont un cache qui ne se tronque pas, donc le serveur ne pouvait pas reprendre au milieu d’un prompt déjà lu. Gemma 4 12B relisait ~2 800 jetons et mettait ~20 s à écrire son premier mot. Le correctif : un petit script, servir_socle.py, qui fait du socle (le prompt système commun) un segment de cache à lui seul. Une nouvelle conversation ne lit plus que 160 à 230 jetons, et le premier mot tombe de 20,7 s à 2,2 s.

Le contexte : des candidats servis par mlx_lm.server

Je cherche un modèle plus léger pour l’assistant du Journal des Nuisances, une application de suivi de nuisances que j’ai co-fondée. Les candidats tournent sur mac-train, un Mac mini M2 Pro 16 Go sous macOS 26.3, avec mlx-lm 0.32.0, et passent le banc d’essai : 22 cas pilotes, notés par un LLM-juge, en moyenne de trois passages.

Chaque requête porte deux messages système : le socle (~2 700 jetons, le même pour tout le monde), puis le contexte de la personne, propre à chaque personne et à chaque heure. mlx_lm.server garde un cache de prompt à la fin des messages système, donc après le contexte : un point de reprise qui ne sert à aucune autre conversation.

Pourquoi Ministral s’en sortait et pas Gemma 4

Le même serveur donnait un premier mot en 1,4 s à Ministral 3 8B : son cache se tronque à la longueur commune avec la conversation précédente, socle compris.

Gemma 4 alterne des couches d’attention globale et des couches à fenêtre glissante, qui ne regardent que les derniers jetons. Sous MLX, le cache de ces couches ne se tronque pas : on ne peut pas le ramener à « la fin du socle ». Résultat au banc du 16 octobre, Gemma 4 12B QAT : premier mot 20,7 s, réponse complète 28 s.

Avec un modèle à fenêtre glissante, le cache de préfixe ne se coupe pas après coup : il faut un point de cache exactement là où les conversations divergent.

C’est la même famille de problème que côté llama.cpp, où Gemma 4 demande --swa-full pour réutiliser le préfixe du prompt, un réglage que j’avais aussi posé pour les mesures llama-server sur Snapdragon. Sous MLX, pas d’option équivalente : j’ai changé l’endroit où le serveur coupe.

Le correctif : faire du socle un segment de cache

servir_socle.py remplace ResponseGenerator._tokenize de mlx_lm.server sans toucher à mlx-lm. Il cherche où le prompt cesse de coïncider avec le socle seul, et coupe le premier segment à cet endroit :

fin = fin_du_socle(tokenizer, tete, prompt, kwargs)
# Le socle doit tomber à l'intérieur du premier segment, sans le remplir.
if not 0 < fin < len(segments[0]):
    return prompt, segments, types, etat
if types[0] == "system" and len(segments) > 1:
    segments = [segments[0][:fin], segments[0][fin:] + segments[1], *segments[2:]]
    types = ["system", *types[1:]]

Le segment socle + contexte disparaît, fondu dans le suivant : à ~0,4 Go par cache pour Gemma 4 12B, il chassait celui du socle, puisque le serveur évince d’abord les plus anciens caches système. Le plafond --prompt-cache-bytes est passé de 1 à 2 Go dans servir-candidat.sh.

Qwen 3.5 et Ministral 3 refusent un second message système : socle et contexte arrivent fondus. Le script prend alors pour socle la partie commune avec le message système précédent, coupée entre deux paragraphes pour que les jetons tombent comme dans le message entier :

fin = systeme.rfind("\n\n", 0, len(os.path.commonprefix([precedent, systeme])))
if fin < SOCLE_MIN:  # 1000 caractères : en dessous, un début de contexte commun
    return None

Gemma 4 MLX : le prompt cache, avant et après

Mesures des 16 et 17 octobre 2026, moyenne de trois passages, latences en médiane mesurées depuis le PC à travers le mesh :

ModèleLecture d’une nouvelle conversationPremier motRéponse complète
Gemma 4 12B QAT, sans segment socle~2 800 jetons20,7 s28 s
Gemma 4 12B QAT, segment socle160 à 230 jetons2,2 s9,0 s
Qwen 3.5 9B, segment socle (message unique)100 à 300 jetons1,2 s4,7 s

Dix fois plus rapide au premier mot, trois fois pour la réponse complète. Seul le tout premier cas d’un passage paie encore la lecture à froid du socle (21,1 s). Côté mémoire, le journal du serveur a relevé 2,85 Go de cache de prompt pour un plafond de 2 Go : ça tient sur les 16 Go du Mac, mais le plafond n’est pas une limite stricte.

Une nuance : j’avais noté que qwen3.6:35b-a3b, modèle hybride, ne réutilise pas le préfixe sous Ollama. Ici, c’est Qwen 3.5 9B sous mlx_lm.server, avec un point de cache posé à la main : autre modèle, autre moteur, la mesure ne se transpose pas.

Lessons learned


Partager cet article :

Article précédent
Coolify webhook : un push, cinq déploiements
Article suivant
`navigator.share` après un PDF : `NotAllowedError`