Pourquoi mon LLM produit des résultats incohérents
Cinq requêtes identiques, cinq réponses différentes — un run dit « pas Python » quand quatre disent Python. Seed fixé, et le batch bascule un token à temp=0.
Fig 1 — cinq runs, cinq réponses, une auto-contradiction. Le run 2 (estompé) fait office de reçu : même fichier, même machine, même prompt, ouverture opposée.
Pourquoi mon LLM donne-t-il une réponse différente à chaque fois ?
Le modèle ne « possède » jamais de réponse. Pour chaque token à produire, il tient une liste de probabilités, la température remodèle la liste, et un token remporte le tirage. Le gagnant est ajouté au texte, et l’enchère suivante s’ouvre sur une liste différente — car le texte qu’il continue a changé. Une divergence précoce se propage : le run 2 a dit « not » là où les runs 1, 3, 4 et 5 ont dit « Python », et depuis ce seul caractère, les cinq réponses vivent dans des univers distincts. Cinq runs, cinq tirages indépendants — pas une réponse imprimée cinq fois.
Comment obtenir des réponses identiques à l’octet près ?
Nommez le lancer. Nous avons rejoué la requête identique trois fois avec seed: 42 à température 0.7 — le réglage non déterministe — et obtenu 3/3 réponses identiques à l’octet près (la vitesse de génération a oscillé de 6.18 → 6.74 → 7.63 tok/s ; les mots n’ont pas vacillé d’un iota). Le seed fige les dés même pendant qu’on les lance.
curl http://localhost:8080/v1/chat/completions -d '{
"model": "qwen3:4b",
"messages": [{"role": "user", "content": "Explain GGUF quantization"}],
"temperature": 0.7,
"seed": 42
}'
verify: même prompt + même seed → réponse identique à l’octet près, 3/3 au banc (temp 0.7, pas que le glouton) · det-results-a.json
Le même réglage existe partout où vous travaillez déjà : options.seed dans le /api/generate d’ollama, seed dans l’API de chat compatible OpenAI, --seed en ligne de commande llama.cpp. Le runner d’ollama, c’est llama-server avec une veste par-dessus, donc le reçu sous le capot est le même — le seed arrive au même sampler.
Pourquoi la température 0 reste-t-elle non déterministe ?
Parce que le décodage glouton retire les dés. Il ne retire pas l’arrondi. Dans le réseau, chaque score de token, ce sont des milliers d’additions, et l’addition en virgule flottante n’est pas associative : additionnez une longue colonne de gauche à droite, puis par paquets — mêmes nombres, dernier chiffre possiblement différent. Quand les deux premiers candidats sont assez proches, l’arrondi est la décision. Et l’ordre des sommes dépend de tout ce que le batch traitera d’un coup.
Alors nous avons tout verrouillé — temp=0, seed=42, même serveur, même prompt — et changé exactement un drapeau, la largeur du micro-batch :
| requête | -ub | temp | seed | la sortie ouvre sur | vs -ub 512 |
|---|---|---|---|---|---|
| 1–2 | 512 | 0 | 42 | “ Python. It is used in many…” | référence, 2/2 identiques |
| 1 | 8192 | 0 | 42 | “ Python. It’s used in many…” | BASCULÉ — un token |
| 2–3 | 8192 | 0 | 42 | “ Python. It is used in many…” | retour à la référence, 2/2 |
Le dé pipé, en direct : tous les réglages juraient déterminisme, la forme du batch a déplacé un token quand même — et uniquement sur la première requête après le boot.
Lisez la troisième ligne deux fois. La bascule est apparue une seule fois, à la première requête du serveur froid, et les deux requêtes suivantes, réglages identiques, ont rendu le texte de référence. L’état du serveur chaud — le cache de prompts, le placement des slots — rejoint l’ordre des sommes sur la liste des choses dont dépend une réponse « déterministe ». Nous avons attrapé la bascule ; nous ne prétendrons pas avoir attrapé le coupable. Les dés sont pipés. La main reste cachée.
Combien coûte la variance d’un run à l’autre ?
L’addition, fois cinq. Nous avons demandé un hello en exactement trois mots, cinq fois, seed libre, budget de 256 tokens. Le modèle a répondu zéro fois sur cinq — chaque run a brûlé tout le budget en réflexion avant que le plafond ne le coupe, et la réflexion elle-même vacillait : 884 · 954 · 986 · 915 · 885 caractères pour la requête identique.
Fig 2 — le vacillement est le propos : 102 caractères d’écart sur une requête figée. L’addition de réflexion du post LFM3 d’octobre, cinq fois de suite.
Nous avons demandé un hello en trois mots et reçu 884 à 986 caractères d’introspection, sans aucun hello. Le modèle n’est pas indécis sur sa réponse — il est indécis sur tout ce qui précède la réponse, et vous payez pour ça, réponse arrivée ou pas. Toute requête « réponse exacte » est un budget : plafonnez la réflexion (think: false, ou l’interrupteur du modèle lui-même) ou c’est le plafond qui vous dévore le premier.
La liste des correctifs
| symptôme | ce qui se passe | le correctif |
|---|---|---|
| une réponse différente à chaque run | pas de seed ; temp > 0 remodèle la liste à chaque tirage | fixez seed ; la réponse se rejoue à l’octet près |
| réponses différentes à temp=0 | forme du batch / état du serveur chaud : l’ordre des sommes a changé | mêmes -ub/-b, même build du moteur, serveur chaud avant de mesurer |
| la réponse n’arrive jamais | la réflexion a mangé le budget de tokens (0/5 ici) | plafonnez ou coupez la réflexion ; lisez completion_tokens, pas vos tripes |
run 1: “ Python. Python is widely used in data science, web…”
run 2: “not Python, Java, or C++. It’s actually a language that’s
not even a progr…”
run 3: “ Python. But there are other languages that are…”
run 4: “ Python. But why? Python is a high-level, interpreted…”
run 5: “ Python. Python has a lot of applications in web…”
Quatre runs ont dit Python. Un run a ouvert en contestant la prémisse. Mêmes poids, même machine, même chaîne en entrée. Sur les cinq, un seul se trompe de se croire une machine déterministe ; les quatre autres ont simplement eu de la chance.
dernier test 2026-11-11
Qwen3-4B Q4_K_M · 2,497,280,256 B · sha256 7485fe6f…df5 · llama-server 0.9.0-dev (b12110) · une VM Arch toute neuve, 4 threads — banc CPU, aucune prétention VRAM