Aller au contenu
17/30Chapitre 17 sur 30

Température, top-p et le déterminisme que vous n’avez pas

La température divise les logits avant la softmax : fini le mythe du cadran de créativité. Même appel greedy, deux réponses.

Dans cet article

Voici la même requête envoyée cinq fois au même modèle. Mêmes poids, même prompt, même machine, même graine aléatoire. La seule chose qui change est un nombre.

TEXT
prompt: "Q: What is the capital of France?\nA:"

T = 0.0   " Paris\nWhat is the question and does the answer answer it? The
           question is: What is the capital of France?..."

T = 0.7   " Paris\nWhat is the question: Which city is the capital of
           France?..."

T = 1.0   " Paris\nWhat is a good geographical qualifier for describing
           Paris concerning its location?\nA: Near the Mediterranean Sea..."

T = 1.5   " Paris\nWhat clue from premise allows we to conclude that Godwin
           was &, He chose Healing Crimson Colour No:white flour Pure..."

T = 2.0   "安全感金华.ITEMT]]];\naims assume parental.st-importe.valtermination
           Screens قطر_Zeroหมายเลข-zA ('$ספטמבר..."

Rien n’a cassé. Chaque token de la dernière ligne a été tiré légitimement depuis la distribution de probabilité du modèle lui-même sur son vocabulaire de 151 936 entrées. Le nombre qui a changé s’appelle la température ; la plupart des documentations le décrivent comme un cadran de créativité, et cette description est fausse d’une manière que ce chapitre peut démontrer plutôt qu’affirmer.

C’est aussi le chapitre où trois promesses précédentes arrivent à échéance. Le chapitre 4 a défini le logit sans vraiment s’en servir. L’encadré sur les nombres à virgule flottante du chapitre 2 se terminait par une consigne — souvenez-vous-en lorsque le chapitre 17 demandera pourquoi le même prompt, le même modèle et la même graine peuvent produire des tokens différents. Et l’encadré sur les mixture-of-experts du chapitre 9 promettait un catalogue de quatre causes de non-déterminisme. Les trois arrivent ci-dessous.

Le chapitre 4 a présenté le logit comme un score réel non normalisé, un par classe. Le chapitre 8 a fait produire à un modèle de langage un logit par entrée du vocabulaire. La softmax transforme ce vecteur z\mathbf{z} en probabilités :

pi=ezijezjp_i = \frac{e^{z_i}}{\sum_j e^{z_j}}

La température intervient ici — le nom vient de la physique statistique, où le même paramètre contrôle la concentration plus ou moins forte d’une distribution de Boltzmann sur ses états de basse énergie1 — et elle divise les logits avant l’exponentielle :

pi(T)=ezi/Tjezj/Tp_i(T) = \frac{e^{z_i/T}}{\sum_j e^{z_j/T}}

Cet emplacement est tout le mécanisme, et deux lignes d’algèbre suffisent à comprendre pourquoi il ne pouvait pas être ailleurs. Supposons que vous essayiez d’appliquer la température aux probabilités à la place — les multiplier par 1/T1/T puis renormaliser. Vous obtiendriez

pi/Tjpj/T=pijpj=pi\frac{p_i/T}{\sum_j p_j/T} = \frac{p_i}{\sum_j p_j} = p_i

La constante s’annule. Mettre les probabilités à l’échelle ne change absolument rien ; la distribution revient inchangée. La température n’a d’effet que parce qu’elle agit sur l’exposant, où diviser par TT avant l’exponentiation revient à élever chaque probabilité à la puissance 1/T1/T — un remodelage non linéaire qui change les rapports entre les entrées plutôt que leur échelle commune.

À partir de cet emplacement, les deux limites suivent sans travail supplémentaire. Quand T0T \to 0, le plus grand logit s’échappe du reste et pp s’effondre sur le token ayant le score le plus élevé : greedy decoding. Quand TT augmente, chaque zi/Tz_i/T tend vers zéro, chaque exponentielle tend vers 1, et la distribution s’aplatit vers une distribution uniforme sur tout le vocabulaire. Exactement à T=0T = 0, la formule divise par zéro ; toutes les implémentations en font donc un cas particulier et utilisent le maximum arithmétique — y compris le widget ci-dessous, qui bascule vers argmax à T0.001T \le 0.001.

Un avertissement, car la collision des noms crée une vraie confusion. Il existe en machine learning une deuxième notion sans rapport appelée température : temperature scaling, une méthode de calibration qui ajuste une valeur sur un jeu de validation pour que la confiance d’un classifieur corresponde à sa précision.2 Même formule, rien à voir avec la génération. Quand des articles disent « température », ils parlent souvent de celle-là ; ce chapitre, jamais.

Voici cette distribution, avec le calcul sous vos yeux. Les logits sont fixes et plausibles, donc les nombres du texte ci-dessous peuvent être vérifiés par rapport à ce que vous voyez :

  • ␣Paris96.9%
  • ␣the1.3%
  • ␣located0.8%
  • ␣a0.5%
  • ␣Lyon0.2%
  • ␣called0.1%
  • ␣home0.1%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

10 sur 10 tokens passent le filtre et se partagent la probabilité.

Voir les données sous forme de tableau
TokenlogitAprès la températureAprès le filtrage
␣Paris⁨9.4⁩96.90%96.90%
␣the⁨5.1⁩1.31%1.31%
␣located⁨4.6⁩0.80%0.80%
␣a⁨4.1⁩0.48%0.48%
␣Lyon⁨3.2⁩0.20%0.20%
␣called⁨2.9⁩0.15%0.15%
␣home⁨2.4⁩0.09%0.09%
␣Marseille⁨1.8⁩0.05%0.05%
␣not⁨1.1⁩0.02%0.02%
␣banana⁨-2.6⁩0.00%0.00%
Échantillonnage : température, top-p et top-k

Dix continuations candidates de The capital of France is, à température 1 sans coupe. ␣Paris détient 96,90 % de la masse ; ␣banana, tout en bas avec un logit de 2.6-2.6, reçoit 0,00 %. Faites glisser la température à 0 et un seul token survit avec 100 %. Faites-la glisser à 2 et ␣Paris tombe à 69,81 %, tandis que ␣banana grimpe à 0,17 % — le token rejeté par le modèle, doté d’une vraie probabilité par un bouton tourné par le lecteur.

La température n’est pas un cadran de créativité

Lien vers la section : La température n’est pas un cadran de créativité

Le nombre ␣banana résume tout l’argument : augmenter la température ne peut pas donner à un modèle une idée qu’il n’avait pas. Les logits sont déjà calculés, le classement est déjà fixé, et la température le préserve exactement — aucune quantité de chaleur ne fera jamais passer un token moins bien noté au-dessus d’un token mieux noté. Elle ne fait que redistribuer la masse vers le bas du classement que le modèle a lui-même produit. Une température élevée ne rend pas un modèle plus inventif ; elle augmente la probabilité qu’il émette les tokens qu’il a jugés mauvais.

Sur un vrai vocabulaire, cela cesse d’être une curiosité et devient la raison pour laquelle les sorties à haute température sont inutilisables. Mesuré sur Qwen/Qwen2.5-0.5B-Instruct, une forward pass, le prompt ci-dessus, en comptant le nombre de tokens nécessaires pour accumuler une part donnée de la masse de probabilité :

températureprobabilité top-1entropietokens portant 80 %90 %95 %99 %
0,599,98 %0,00 nats1111
0,799,65 %0,03 nats1111
1,096,01 %0,30 nats11114
1,288,20 %0,88 nats1213252
1,562,83 %3,07 nats293532 67226 787
2,016,62 %8,19 nats13 51632 96655 231101 205

Lisez lentement la dernière ligne. À T=2T = 2, sur une question qui a exactement une bonne réponse, 32 966 tokens différents se partagent les 90 % supérieurs de la masse de probabilité. Ce n’est pas un espace créatif plus vaste. C’est un modèle auquel l’arithmétique a demandé de traiter une particule coréenne et un identifiant C++ comme des options actives pour le mot après A:. Les déchets du bloc d’ouverture en sont la conséquence directe, et ce n’est pas un bug du modèle ni de la bibliothèque — c’est ce que la requête demandait.

La plage utile est étroite et dépend de la tâche plutôt que du goût. Sur une question factuelle, la réponse est un token, et toute chaleur au-dessus d’environ 1,2 injecte de l’erreur pour rien. Sur une question ouverte, il existe vraiment plusieurs bonnes continuations, et un peu de chaleur apporte une variété qui reste fluide :

TEXT
"Write a two-sentence story about a lighthouse."

T = 0.0  "The lighthouse stood tall and proud, its beacon illuminating the
          night sky above. A lone sailor, his eyes fixed on the distant
          horizon..."

T = 0.7  "In the quiet, stormy waters of the sea, a lighthouse stood
          sentinel over the horizon, its golden dome casting a warm glow
          on the fog-shrouded streets below..."

T = 1.0  "In the quiet night, a lone lighthouse stood sentinel over the
          sea, its shining beacon a beacon of hope and solace for sailors
          and fishermen across the vast and endless ocean..."

T = 1.3  "In the gentle sunlight, now reflecting upon the opening of Jack's
          lighthouse, Jim Trahan, a small-time individual difficult to
          define in paperwork, wondered about a career where simplicity
          reigns..."

À 1,3, le modèle a inventé un nom propre et une phrase qui ne se parse pas. La bande entre « identique à chaque fois » et « incohérent » est à peu près de 0,6 à 1,1 pour ce modèle sur cette tâche, et le conseil honnête est de la trouver en mesurant sur votre tâche, pas en copiant un nombre depuis un article de blog.

Pourquoi le texte le plus probable est un mauvais texte

Lien vers la section : Pourquoi le texte le plus probable est un mauvais texte

Une question évidente se cache sous tout cela : si le modèle possède une distribution de probabilité et qu’un token est le plus probable, pourquoi ne pas toujours le prendre ? Le greedy decoding est gratuit, reproductible et ne demande aucun paramètre.

Parce que le résultat est ceci :

TEXT
prompt: "In a shocking finding, scientists discovered a herd of unicorns
         living in a remote valley."

greedy: " The unicorns were so rare that they were not even recognized by
         the local people. The unicorns were so rare that they were not
         even recognized by the local people. The unicorns were so rare
         that they were not even recognized by the local people. ..."

         repeated 4-grams: 87.6 %

Huit phrases, une phrase. Près de neuf fenêtres de quatre tokens sur dix étaient déjà apparues plus tôt dans la même sortie. C’est la dégénérescence du texte neuronal, nommée et expliquée par Holtzman et al. dans l’article qui a introduit top-p.3 Le modèle n’est pas cassé ; maximiser la probabilité de séquence est simplement le mauvais objectif pour du texte ouvert. L’écriture humaine n’est pas la suite de mots la plus probable — elle contient de la surprise, sa probabilité par token erre, plonge et remonte — tandis que le chemin de probabilité maximale est un point fixe qui, une fois atteint, n’a aucune raison d’en sortir.

C’est pour cela que l’échantillonnage existe. Et c’est aussi — la partie qu’on oublie souvent — pas une loi universelle. Le chapitre 12 a mesuré 24 réponses correctes sur 24 à des problèmes de mots en deux étapes avec un simple greedy decoding, et l’échantillonnage à température 0,8 a fait tomber ce résultat à 81 % ; la self-consistency a ensuite dépensé six fois plus de tokens pour revenir là où le greedy était déjà. Les deux faits sont vrais en même temps :

Génération ouverte. Il n’existe pas de continuation unique correcte, donc la plus probable est un piège — elle boucle, et 87,6 % de celle-ci est copiée depuis elle-même. Échantillonnez.

Tâches avec une seule bonne réponse. Il existe une continuation unique correcte, donc tirer autre chose revient à tirer une erreur. Le 100 % du chapitre 12 est devenu 81 % exactement pour cette raison. N’échantillonnez pas.

La plupart des prompts de production sont du deuxième type et sont configurés comme le premier, parce que la température a été laissée à la valeur utilisée par l’exemple de code.

Deux façons de couper, dont une seule s’adapte

Lien vers la section : Deux façons de couper, dont une seule s’adapte

Échantillonner depuis la distribution complète n’est pas ce que qui que ce soit fait réellement, car la queue est immense et pleine de non-sens. Il faut couper quelque chose. Il existe deux réponses classiques, et elles diffèrent sur un point qui décide de tout.

Top-k conserve un nombre fixe de candidats. Triez par probabilité, gardez les kk premiers, écartez le reste, renormalisez.4 Top-p, aussi appelé nucleus sampling, conserve une quantité fixe de masse : prenez les tokens par ordre décroissant jusqu’à ce que leur probabilité cumulée atteigne pp, puis arrêtez.3 Formellement, le noyau est le plus petit ensemble VpV_p tel que

iVppip\sum_{i \in V_p} p_i \ge p

La différence paraît cosmétique, mais ne l’est pas, parce que les deux prompts que vous envoyez dans la même minute ont des formes de distribution complètement différentes. Les deux utilisent le même modèle à température 1 :

Q: What is the capital of France?\nA:Once upon a time,
probabilité top-196,01 %25,39 %
tokens portant 90 % de la masse1467
top-k = 40 conserve99,61 % de la masse78,87 % de la masse
masse dans les rangs 2 à 403,61 %53,48 %
token au rang 40␣Av, 0,0093 %␣Dr, 0,128 %

Un kk fixe, deux échecs en sens opposés. Sur le prompt factuel, k=40k = 40 admet 39 tokens qui valent ensemble 3,6 % — il laisse passer des déchets, y compris un candidat à neuf millièmes de pour cent, parce que la règle compte des emplacements et non des preuves. Sur le prompt d’histoire, le même k=40k = 40 jette 21 % de la masse que le modèle avait réellement attribuée, parce que le vrai noyau y mesure 467 tokens de large.

Top-p fait accomplir les deux tâches à un seul nombre. Définissez p=0.9p = 0.9 et il conserve 1 token sur le premier prompt et 467 sur le second, parce qu’il pose une question sur la distribution au lieu de lui imposer un nombre. Observez directement cette adaptation — même coupe, quatre températures :

  • ␣Paris91.1%
  • ␣the5.2%
  • ␣located3.7%
  • ␣a0.0%
  • ␣Lyon0.0%
  • ␣called0.0%
  • ␣home0.0%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

3 sur 10 tokens passent le filtre et se partagent la probabilité.

Voir les données sous forme de tableau
TokenlogitAprès la températureAprès le filtrage
␣Paris⁨9.4⁩85.03%91.10%
␣the⁨5.1⁩4.84%5.18%
␣located⁨4.6⁩3.47%3.71%
␣a⁨4.1⁩2.48%
␣Lyon⁨3.2⁩1.36%
␣called⁨2.9⁩1.12%
␣home⁨2.4⁩0.80%
␣Marseille⁨1.8⁩0.54%
␣not⁨1.1⁩0.34%
␣banana⁨-2.6⁩0.03%
Échantillonnage : température, top-p et top-k

Top-p à 0,90 avec la température à 1,5 : trois des dix tokens survivent et se partagent la masse, ␣Paris renormalisé à 91,10 %. Maintenant, ne déplacez que la température. À 0,7, le même 0,90 ne laisse qu’un survivant — un noyau aussi étroit est du greedy decoding sous un autre nom. À 2,0, il en laisse cinq. La coupe n’a jamais bougé ; la forme en dessous, oui.

Ce widget règle aussi un malentendu qu’il faut nommer, parce qu’il coûte réellement de l’argent. Sur une distribution confiante, top_p = 0.9 n’est pas « un peu de variété ». C’est du greedy. À température 1, le token de tête détient ici 96,90 %, déjà au-dessus de 0,9 ; le noyau ne fait donc qu’un token de large et rien d’autre ne peut jamais être tiré. Des équipes règlent top_p à 0,9 en pensant avoir desserré quelque chose, puis se demandent pourquoi chaque réponse est identique.

Réglez top-k à la place, et l’échec opposé est tout aussi visible :

  • ␣Paris97.2%
  • ␣the1.3%
  • ␣located0.8%
  • ␣a0.5%
  • ␣Lyon0.2%
  • ␣called0.0%
  • ␣home0.0%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

5 sur 10 tokens passent le filtre et se partagent la probabilité.

Voir les données sous forme de tableau
TokenlogitAprès la températureAprès le filtrage
␣Paris⁨9.4⁩96.90%97.20%
␣the⁨5.1⁩1.31%1.32%
␣located⁨4.6⁩0.80%0.80%
␣a⁨4.1⁩0.48%0.49%
␣Lyon⁨3.2⁩0.20%0.20%
␣called⁨2.9⁩0.15%
␣home⁨2.4⁩0.09%
␣Marseille⁨1.8⁩0.05%
␣not⁨1.1⁩0.02%
␣banana⁨-2.6⁩0.00%
Échantillonnage : température, top-p et top-k

Top-k à 5, sans top-p. Cinq tokens survivent à chaque température, parce que cinq est ce qui a été demandé. À température 1, comme montré, les quatre candidats sous ␣Paris valent ensemble 2,79 %. Descendez à 0,7 et les quatre mêmes valent 0,38 % — la coupe est du théâtre, et le modèle est effectivement greedy. Montez à 2,0 et ils valent 22,54 %. Même réglage, même nombre de survivants, trois comportements complètement différents, et rien dans la requête ne vous dit lequel vous obtenez.

Les pénalités, avec les formules, parce que les confondre est endémique

Lien vers la section : Les pénalités, avec les formules, parce que les confondre est endémique

Trois mécanismes différents circulent sous des noms similaires, ils font des choses différentes, et la différence se mesure. Soit cic_i le nombre de fois où le token ii est déjà apparu.

ziziα1[ci>0]z_i \leftarrow z_i - \alpha \cdot \mathbb{1}[c_i > 0]

Soustrait une constante à tout token déjà apparu au moins une fois. Apparaître une fois et apparaître quarante fois sont pénalisés de manière identique. C’est un interrupteur, pas un cadran.

ziziβciz_i \leftarrow z_i - \beta \, c_i

Soustrait proportionnellement au compte. Un token utilisé quatre fois est pénalisé quatre fois plus qu’un token utilisé une fois, et la pression se compose à mesure que le texte grandit.

zi{zi/ρif zi>0ziρif zi0z_i \leftarrow \begin{cases} z_i / \rho & \text{if } z_i > 0 \\ z_i \cdot \rho & \text{if } z_i \le 0 \end{cases}

L’originale, issue de l’article CTRL.7 Elle divise au lieu de soustraire, avec le cas du signe nécessaire parce que diviser un logit négatif le rendrait plus grand. Sa force dépend donc de la magnitude du logit, ce qui signifie que le même ρ\rho frappe différemment à différents endroits d’une même phrase.

La même continuation dégénérée que plus haut, avec chacune de ces pénalités appliquée. « Étapes modifiées » compte combien des 120 étapes de génération ont choisi un token différent de celui que le modèle non pénalisé aurait choisi. L’exécution fait ici 120 étapes contre 140 dans le bloc ci-dessus, ce qui explique pourquoi la baseline non pénalisée indique 85,5 % plutôt que 87,6 % :

réglage4-grams répétésétapes modifiées
rien85,5 %0 / 120
presence 0,565,0 %3 / 120
presence 1,03,4 %11 / 120
frequency 0,56,0 %12 / 120
frequency 1,00,0 %20 / 120
repetition 1,2 (CTRL)0,0 %35 / 120

Trois choses en découlent. Presence à 0,5 a changé trois décisions sur 120 et réduit la répétition d’un quart — la boucle tenait grâce à une poignée de tokens. Frequency à 0,5 a changé quatre fois plus de décisions pour un effet beaucoup plus grand, parce que le multiplicateur du compte continue de croître alors que la constante de presence ne le fait pas. Et la pénalité CTRL à la valeur largement copiée de 1,2 a réécrit 35 décisions sur 120, ce qui n’est pas un coup de pouce ; c’est un autre modèle.

Ce dernier nombre prépare l’échec dont personne ne vous avertit.

Ce que les pénalités font au texte censé se répéter

Lien vers la section : Ce que les pénalités font au texte censé se répéter

Le code répète. Les tableaux répètent. Les listes répètent. Les sorties structurées répètent par définition — c’est cela, la structure. Une pénalité ne sait pas faire la différence entre un modèle coincé dans une boucle et un modèle qui émet correctement la quatrième ligne d’un tableau, parce que les deux ressemblent à un token qui réapparaît.

Les trois mêmes tâches, générées de trois façons :

tâcherienfrequency 0,5repetition 1,2
tableau markdown, 6 lignes0 / 56 étapes modifiées0 / 562 / 62
fonction Python0 / 930 / 9310 / 110
liste à puces, 1 à 120 / 500 / 500 / 50

La frequency penalty à 0,5 s’est révélée inoffensive sur les trois, ce qui est un résultat utile et légèrement surprenant, et qui dit quelque chose de précis : puisqu’aucune décision n’a changé, les tokens structurels devaient gagner leurs positions avec une marge supérieure à la pénalité soustraite, même après être apparus cinq ou six fois. La pénalité CTRL, qui divise à la place, les déloge bel et bien, et voici ce qu’elle a produit :

TEXT
repetition 1.2, markdown table:
  | n | 2^n |
  | --- | --- |
  | 0 | 1      |
  | 1 | 2       |
  | 2 | 4       |

L’alignement s’effondre : la quantité de remplissage à l’intérieur de chaque cellule change d’une ligne à l’autre, parce que la séquence d’espaces avant la barre verticale fermante est exactement le type de répétition que la pénalité est conçue pour casser. Cosmétique, et cela a coûté six tokens supplémentaires. Le cas Python n’est pas cosmétique :

TEXT
nothing / frequency 0.5:
      total = 0
      for i in range(1, n + 1):
          total += i ** 2
      return total

repetition 1.2:
      # Initialize total_sum with 0
      total_sum = 0
      # Loop through numbers from 1 to n, incrementing by 2 each time
      for i in range(1, n + 1,

La pénalité a poussé le modèle hors de total — déjà utilisé dans la docstring — vers total_sum, a rembourré la sortie avec des commentaires inventés pour dépenser son budget sur des tokens inutilisés, puis l’a conduit dans un range à trois arguments avec un pas. Le commentaire dit incrementing by 2 each time, ce qui est faux pour une somme de carrés de 1 à nn. Une repetition penalty a produit du code incorrect à partir d’un prompt auquel il avait été correctement répondu sans elle.

La règle qui suit est courte : les pénalités sont faites pour de la prose ouverte, et elles doivent être désactivées pour le code, les sorties structurées, les données tabulaires et tout ce qui a un schéma. Le chapitre 18 porte exactement sur cette seconde catégorie.

L’ordre d’application, et pourquoi il change la réponse

Lien vers la section : L’ordre d’application, et pourquoi il change la réponse

Toute vraie implémentation les applique dans une séquence précise :

pénalités → température → top-k → top-p → échantillonnage

Ce n’est pas de la comptabilité arbitraire, et inverser deux étapes produit réellement des distributions différentes. Deux mesures, toutes deux sur le prompt factuel.

Couper avant ou après la température. Le noyau est calculé sur la distribution qu’on lui donne, et la température change radicalement cette distribution :

top-p 0,9 après températuretop-p 0,9 avant température
T=1.0T = 1.01 token1 token
T=1.5T = 1.5353 tokens1 token
T=2.0T = 2.032 966 tokens1 token

À T=2T = 2, le même réglage nominal donne un ensemble de candidats de 32 966 ou de 1, uniquement selon l’étape qui s’exécute en premier. Si vous vous êtes déjà demandé pourquoi augmenter la température « ne fait rien » chez un fournisseur et détruit la sortie chez un autre avec les deux mêmes nombres, ce tableau est une réponse plausible.

Pénaliser avant ou après la température. Soustraire une pénalité α\alpha puis diviser par TT donne une pénalité effective de α/T\alpha/T ; diviser d’abord puis soustraire donne α\alpha. Avec une presence penalty de 1,0 appliquée au token de tête :

températurepénaliser, puis tempérertempérer, puis pénaliser
0,599,858 %99,948 %
1,089,839 %89,839 %
2,010,783 %6,830 %

Identiques à T=1T = 1, comme elles doivent l’être. Écartées d’un facteur 1,58 à T=2T = 2. « Presence penalty 1.0 » n’est pas une quantité de pénalité bien définie à moins de savoir aussi où la température est appliquée, et aucune API ne documente cela.

Afficher les détails

Optionnel : tout le pipeline, dans l’ordre ci-dessus.

Seize lignes, et tout ce chapitre y tient. C’est le même calcul que celui effectué par le widget, sur un vrai vecteur de logits plutôt que sur dix nombres fixes.

sample.pyPYTHON
def sample(logits, counts, presence=0.0, frequency=0.0,
           temperature=1.0, top_k=0, top_p=1.0, generator=None):
    z = logits.clone()

    idx = torch.tensor(list(counts))                       # 1. penalties
    if len(idx):
        z[idx] -= presence
        z[idx] -= frequency * torch.tensor([float(c) for c in counts.values()])

    if temperature <= 0:                                   # 2. temperature
        return int(z.argmax())                             #    T=0 is argmax
    p = torch.softmax(z / temperature, -1)

    p, order = p.sort(descending=True)
    if top_k:                                              # 3. top-k
        p[top_k:] = 0
    p = p * ((p.cumsum(0) - p) < top_p)                     # 4. top-p

    p = p / p.sum()                                        # 5. renormalise
    return int(order[torch.multinomial(p, 1, generator=generator)])

Le cumsum(0) - p dans la ligne top-p est la masse cumulée hors token courant, ce qui fait que le noyau inclut le token qui franchit le seuil au lieu de s’arrêter juste avant. Décalez cela d’une unité et top_p = 0.9 devient silencieusement une coupe légèrement plus serrée que toutes les autres implémentations.

C’est l’un des rares endroits dans la seconde moitié du cours où Python est le bon langage, et la raison est structurelle plutôt que stylistique : chaque ligne ci-dessus exige d’avoir le vecteur complet de logits entre les mains, et via une API HTTP, ce vecteur n’existe pas. Vous pouvez envoyer temperature et top_p à un fournisseur ; vous ne pouvez pas les implémenter, et vous ne pouvez pas voir ce qu’ils ont fait.

Il n’existe pas d’API d’échantillonnage universelle

Lien vers la section : Il n’existe pas d’API d’échantillonnage universelle

Chaque fournisseur accepte un sous-ensemble différent de ces contrôles, avec des plages différentes, et ignore silencieusement le reste. Ce n’est pas une plainte abstraite. Toute application qui propose un choix de modèle doit noter les différences quelque part, et le fichier où elle le fait est une carte de l’incompatibilité. Voici ce que déclare l’un de ces catalogues pour un seul paramètre sur les neuf sources de texte qu’il prend en charge :

plage de température déclaréesources
0 à 1Anthropic, Google, Meta, Cerebras, PaLM
0 à 1,5Mistral
0 à 2OpenAI, DeepSeek, xAI

Le mot est le même ; l’échelle ne l’est pas. Une « température de 1 » est la distribution non modifiée chez l’un et la chaleur maximale autorisée chez l’autre, et la moitié du catalogue ne peut pas exprimer la valeur que l’autre moitié traite comme neutre-plus-un-peu. Les autres boutons sont tout aussi inégaux : les entrées OpenAI, DeepSeek et xAI acceptent des pénalités presence et frequency mais pas topK ; les entrées Google, Meta, Cerebras et PaLM acceptent topK et aucune pénalité ; Anthropic accepte topK, topP et des séquences d’arrêt, mais aucune pénalité ; et exactement une des neuf — Mistral — accepte une graine. Envoyer un paramètre qu’un fournisseur n’implémente pas ne produit généralement aucune erreur : la requête réussit, le bouton ne fait rien, et vous concluez que le réglage n’a aucun effet.

Et remarquez ce qu’est un tel fichier : une affirmation sur l’API de quelqu’un d’autre, écrite un jour précis, que rien ne vérifie ensuite. Un catalogue qui indique 0 à 1 pour un fournisseur acceptant désormais 0 à 2 plafonnera silencieusement chaque requête.

Deux autres contrôles appartiennent à la même famille. logprobs, lorsqu’il est proposé, renvoie les log-probabilités du token choisi et souvent les quelques meilleures alternatives — la seule fenêtre que vous avez sur la distribution dont parle ce chapitre, et la base de toutes les heuristiques de confiance construites sur un modèle fermé. Et les maximum tokens plus les séquences d’arrêt terminent la génération sans aucune référence à la probabilité : une limite dure et une correspondance de chaîne. Les deux apparaissent comme le finish_reason du chapitre 14, où length signifie que votre réponse a été coupée au milieu d’une phrase par un budget, pas terminée par le modèle.

La graine, et le déterminisme que vous n’avez pas

Lien vers la section : La graine, et le déterminisme que vous n’avez pas

Définissez une graine et l’échantillonnage devient reproductible. Cette partie est réelle, et facile à vérifier :

TEXT
seed = 1234  " Paris\nWhat is a good geographical qualifier for describing
               Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 1234  " Paris\nWhat is a good geographical qualifier for describing
               Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 7     " Paris is the capital of France. The appellation of Paris is
               \"Île de Paris\"."
seed = 7     " Paris is the capital of France. The appellation of Paris is
               \"Île de Paris\"."

Identiques au byte près pour une même graine, différents d’une graine à l’autre, exactement comme annoncé. Ce que la graine fixe, donc, c’est le tirage aléatoire dans la dernière ligne de cette fonction sample — le token choisi étant donnée une distribution.

Ce qu’elle ne fixe pas, c’est la distribution. Et c’est là que les problèmes commencent, parce que le vecteur de logits produit par votre modèle n’est pas un objet mathématique ; c’est le résultat de milliards d’additions en virgule flottante, et ces additions ont un ordre.

Le chapitre 2 avait préparé cette expérience. Le même million de nombres float32, sommés selon des groupements différents :

TEXT
sequential          998.564270020    error vs float64: 6.393e-03
pairwise (numpy)    998.570556641    error vs float64: 1.061e-04
in 4 chunks         998.570495605    error vs float64: 1.672e-04
in 8 chunks         998.570556641    error vs float64: 1.061e-04
in 16 chunks        998.570678711    error vs float64: 1.594e-05

sequential == pairwise?  False
4 chunks == 8 chunks?    False

Regardez la dernière ligne. Le nombre de morceaux change la réponse. Ce n’est pas une curiosité à propos de numpy ; c’est le mécanisme, parce que lorsqu’un serveur d’inférence répartit une réduction sur plus ou moins d’unités parallèles, il fait précisément cela. Et un serveur répartit selon le nombre de requêtes qu’il est en train de servir.

Voici cet effet sur le modèle lui-même. Le même prompt, la même forward pass, la seule différence étant le nombre d’autres requêtes qui se trouvaient dans le batch :

TEXT
20 identical forward passes, batch of 1:  20 / 20 bit-for-bit identical

the same prompt inside a batch of  2:  147,321 of 151,936 logits differ
the same prompt inside a batch of  4:  146,515 of 151,936 logits differ
the same prompt inside a batch of  8:  146,515 of 151,936 logits differ
the same prompt inside a batch of 16:  147,321 of 151,936 logits differ

largest change to any logit: 2.5e-05

Exécuté seul, le modèle est parfaitement déterministe — vingt passages, identiques au bit près. Placez le prompt identique dans un batch avec des requêtes sans rapport et 97 % de ses logits changent. Rien dans votre requête n’a changé. La requête de quelqu’un d’autre est arrivée.

Maintenant, la partie honnête, parce que cela est souvent raconté comme si c’était la fin de l’histoire. Un changement de 2.5×1052.5 \times 10^{-5} ne modifie la sortie que si deux tokens candidats étaient à cette distance l’un de l’autre. Sur 717 étapes de génération réparties sur douze prompts, le plus petit écart entre les deux premiers logits était 2.5×1032.5 \times 10^{-3} — cent fois plus grand que la perturbation — et aucune étape n’était assez proche pour basculer. Donc sur ce modèle, en float32, sur un ordinateur portable, le batching a déplacé chaque logit et n’a changé aucun token.

C’est la description de conditions favorables, pas une garantie rassurante, et un seul changement de ces conditions suffit :

TEXT
same weights, same prompts, greedy decoding, no seed involved
float32 vs bfloat16:   6 of 8 answers diverge
                       first divergence at step 23, on average

  float32: "...it is scattered and dispersed into different colors,
            including blue. The blue light is scattered more than other
            colors, so it appears to come from the sky."

  bfloat16: "...it is scattered and scattered, causing the colors of the
             sun to be scattered and scattered, creating the appearance
             of a blue color."

Six réponses sur huit divergent, et l’une se dégrade fortement. Le tableau du chapitre 2 explique pourquoi : bfloat16 conserve 7 bits de mantisse, donc près d’une magnitude de logit de 16, les valeurs représentables sont espacées de 0,125 — 16,0, puis 16,125, puis 16,25 — et l’arrondi peut déplacer un logit jusqu’à 0,0625. Pendant ce temps, 4,7 % des étapes de génération mesurées ci-dessus avaient un écart top-two inférieur à 0,1. Toute la différence entre les deux expériences est là : en float32, la perturbation était cent fois plus petite que la décision la plus proche, et en bfloat16, elle est de même ordre. L’inférence de production s’exécute en 16-bit, sur du matériel avec des kernels fusionnés et des ordres de réduction que personne ne promet de conserver. Savoir si « le bruit numérique est négligeable » est une question de précision et de matériel, pas de modèle.

Donc, les quatre causes, cataloguées comme promis au chapitre 9 :

L’addition en virgule flottante n’est pas associative

Lien vers la section : L’addition en virgule flottante n’est pas associative

L’encadré du chapitre 2. L’ordre d’une somme change sa valeur, donc tout changement dans la façon dont une réduction est découpée change les logits. C’est le substrat ; les trois autres sont des façons de changer l’ordre.

Le batching dynamique regroupe votre requête avec celles d’inconnus

Lien vers la section : Le batching dynamique regroupe votre requête avec celles d’inconnus

Le continuous batching, vu au chapitre 13, est ce qui rend l’inférence abordable — et il signifie que la forme des matrices traversées par vos tokens dépend du trafic. Mesuré ci-dessus : 147 321 logits ont bougé parce que la taille du batch a changé.

L’encadré du chapitre 9 le disait déjà. Le router fait un choix discret par token et par couche, soumis à des limites de capacité par expert calculées sur le batch. Un token qui serait allé seul vers l’expert 7 va vers l’expert 12 en compagnie. Ce n’est pas une différence d’arrondi ; c’est un ensemble de poids différent.

Une chaîne de version comme -latest est un pointeur, et les pointeurs sont repointés. Les fournisseurs mettent aussi à jour la pile de service sous un identifiant de version fixe. Aucun de ces changements n’est annoncé à une granularité qui vous permettrait de le corréler à vos propres sorties qui changent.

Le paramètre seed d’OpenAI est honnête à ce sujet de la seule manière possible : il arrive avec un champ system_fingerprint qui identifie la configuration backend, et la documentation indique que le déterminisme est best-effort et qu’un fingerprint modifié signifie que les résultats peuvent différer. Lisez cela pour ce que c’est — un fournisseur qui vous dit qu’il contrôle les quatre causes ci-dessus, que vous n’en contrôlez aucune, et que la seule chose qu’il peut offrir est de vous dire après coup que quelque chose a bougé.

Tout ce qui précède portait sur un bouton et ses conséquences. Reculez d’un niveau et le problème plus difficile apparaît : l’objet que nous réglions est une distribution de probabilité, et les distributions de probabilité n’ont pas d’interface.

Un function call en a une. Une ligne de base de données en a une. Un handler POST attendant un corps JSON avec trois champs obligatoires en a une, et il rejettera tout le reste. Entre le modèle et chaque autre composant de votre système se trouve un contrat dont l’une des parties ne peut pas faire de promesses : le modèle produira quelque chose, tiré d’une distribution que vous avez façonnée mais pas fixée, et le code de l’autre côté a besoin d’une valeur d’un type connu, sinon il lève une erreur.

Le pont entre ces deux mondes se construit avec la matière de ce chapitre plutôt qu’avec du parsing et des retries. Si un token casserait la structure requise, vous ne l’échantillonnez pas en espérant — vous réglez son logit à -\infty avant même que la softmax ne le voie. Le constrained decoding est un masque sur le même vecteur que nous avons passé ce chapitre à remodeler, et il transforme « veuillez répondre en JSON » d’une requête en garantie.

Le chapitre 18 est ce contrat : tool calling, JSON Schema, sorties structurées, et ce qu’il faut pour rendre un système déterministe sûr à construire au-dessus d’un système probabiliste.


Toutes les mesures de ce chapitre viennent de Qwen/Qwen2.5-0.5B-Instruct sur CPU, en float32 sauf indication contraire, avec l’échantillonnage implémenté comme écrit dans la section optionnelle plutôt que délégué à une bibliothèque. C’est un petit modèle, et les valeurs spécifiques sont les siennes ; les mécanismes, non. How to generate text with different decoding methods de Von Platen (Hugging Face, 2020) est l’article par rapport auquel celui-ci est mesuré, et reste la meilleure courte introduction au même sujet. Pour la section sur le déterminisme : les notes de reproductibilité de PyTorch décrivent ce qu’une graine fixe et ne fixe pas sur une seule machine, la documentation OpenAI de seed et system_fingerprint décrit ce qu’un fournisseur peut et ne peut pas promettre, et la discussion de Thinking Machines en 2025 sur les kernels invariants au batch est l’explication publique la plus claire de la raison pour laquelle corriger cela au niveau du serveur d’inférence est possible, mais pas gratuit.

  1. Ackley, D. H., Hinton, G. E. and Sejnowski, T. J. A Learning Algorithm for Boltzmann Machines. Cognitive Science 9(1), pp. 147–169 (1985), où la température dans une softmax vient de la physique statistique. Hinton, G., Vinyals, O. and Dean, J., Distilling the Knowledge in a Neural Network, arXiv:1503.02531 (2015), section 2, est l’endroit où le même paramètre réapparaît dans le deep learning moderne — comme moyen d’exposer toute la distribution d’un enseignant, ce qui correspond aux soft labels du chapitre 13 plutôt qu’à l’échantillonnage de ce chapitre.

  2. Guo, C., Pleiss, G., Sun, Y. and Weinberger, K. Q. On Calibration of Modern Neural Networks. arXiv:1706.04599 (2017). Ne confondez pas cela avec la température de ce chapitre. Le temperature scaling ajuste une seule valeur sur un jeu de validation pour que la confiance du modèle corresponde à sa précision ; c’est une méthode de calibration post-hoc appliquée aux sorties d’un classifieur. Le temperature sampling est un contrôle à l’exécution sur la façon dont un générateur tire des tokens. Même formule, objectif différent, et aucune valeur commune.

  3. Holtzman, A., Buys, J., Du, L., Forbes, M. and Choi, Y. The Curious Case of Neural Text Degeneration. arXiv:1904.09751 (2019). Introduit le nucleus sampling et la mesure montrant que le décodage fondé sur la maximisation produit un texte dont le profil de probabilité n’a rien à voir avec le texte humain. 2

  4. Fan, A., Lewis, M. and Dauphin, Y. Hierarchical Neural Story Generation. arXiv:1805.04833 (2018). L’article qui a popularisé le top-k sampling.

  5. Nguyen, M. et al. Turning Up the Heat: Min-p Sampling for Creative and Coherent LLM Outputs. arXiv:2407.01082 (2024).

  6. Meister, C., Pimentel, T., Wiher, G. and Cotterell, R. Locally Typical Sampling. arXiv:2202.00666 (2022).

  7. Keskar, N. S., McCann, B., Varshney, L. R., Xiong, C. and Socher, R. CTRL: A Conditional Transformer Language Model for Controllable Generation. arXiv:1909.05858 (2019). La section 4.1 présente la repetition penalty originale — celle qui divise.


Créé par

David Vicente Campos

Fondateur de NeuraLIA Labs et cofondateur de MyRealFood

Je suis ingénieur en informatique, diplômé de l’Université de León. J’ai cofondé MyRealFood, où, en tant que CTO, j’ai construit l’application que des millions de personnes ont utilisée pour mieux manger, et j’ai fondé NeuraLIA Labs, où je développe des produits d’IA. Ici, j’écris sur ce que j’ai dû comprendre en chemin, comme j’aurais aimé qu’on me l’explique.

En savoir plus sur l’auteur

Publié par NeuraLIA Labs.

Recevez les nouveaux articles dans votre boîte

Actualités IA, guides et nouveautés produit — un court e-mail quand nous publions quelque chose qui vaut votre temps.

Index du cours

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev14 min de lecture

Le modèle d’IA Jev est conçu pour les décisions, pas pour la prose

Jev de TypeSafe AI attire l’attention parce qu’il traite l’intelligence logicielle comme un problème de probabilité : choisir la bonne branche, y associer un niveau de confiance et éviter de payer un LLM pour rédiger du texte quand le code a besoin d’une décision.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering15 min de lecture

Ingénierie du contexte pour agents IA de longue durée

Les agents de longue durée n’échouent pas seulement parce que la fenêtre est petite. Ils échouent lorsque les fichiers, les sorties d’outils et l’historique obsolète évincent la tâche que l’agent était censé terminer.

Prêt à laisser LIA choisir à votre place ?

Créez avec tous les modèles d'IA au même endroit — commencez gratuitement dès aujourd'hui.