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.

HUMANpar @msvram23h8 min de lecture
même prompt × 5 · temp 0.7 · sans seed · Qwen3-4B Q4_K_M UN SEUL PROMPT“The most popular programminglanguage in the world is” run 1 · “ Python. Python is widely used in data…” run 2 · “not Python, Java, or C++. It’s actually…” run 3 · “ Python. But there are other languages…” run 4 · “ Python. But why? Python is a high-level…” run 5 · “ Python. Python has a lot of applications…”

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 ?

seed — un lancer de dés enregistré. Chaque tirage d’échantillonnage part d’un nombre ; fixez-le et tout le mélange se rejoue. Omettez-le et le moteur en tire un neuf à chaque requête.
température — jusqu’où le modèle mise. Chaque token suivant est une enchère : le modèle tient une liste de probabilités, la température la remodèle, un seul token remporte le tirage. 0.7, ce sont des dés ordinaires ; 0 signifie «  prendre le favori à chaque fois  » (glouton).
top_p — un second filtre, plus sévère. Il rogne la liste des candidats avant le tirage. Il resserre les dés ; il ne les fige pas.

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-ubtempseedla sortie ouvre survs -ub 512
1–2512042“ Python. It is used in many…”référence, 2/2 identiques
18192042“ Python. It’s used in many…”BASCULÉ — un token
2–38192042“ 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.

caractères de réflexion · “Say hello in exactly three words” × 5 · plafond 256 tokens, englouti ×5 884run 1 954run 2 986run 3 915run 4 885run 5

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ômece qui se passele correctif
une réponse différente à chaque runpas de seed ; temp > 0 remodèle la liste à chaque tiragefixez seed ; la réponse se rejoue à l’octet près
réponses différentes à temp=0forme 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 jamaisla réflexion a mangé le budget de tokens (0/5 ici)plafonnez ou coupez la réflexion ; lisez completion_tokens, pas vos tripes
reçu · le run 2 contredit les runs 1, 3, 4, 5 (même prompt, même machine, sans seed)
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