Sari la conținut
17/30Capitolul 17 din 30

Temperature, Top-p și determinismul pe care nu îl ai

Temperature împarte logits înainte de softmax, iar asta demontează ideea de „buton de creativitate”. Același apel greedy poate da două răspunsuri.

Pe această pagină

Iată aceeași cerere trimisă aceluiași model de cinci ori. Aceleași weights, același prompt, aceeași mașină, același random seed. Singurul lucru care se schimbă este un număr.

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 ('$ספטמבר..."

Nu s-a stricat nimic. Fiecare token din ultima linie a fost extras legitim din propria distribuție de probabilitate a modelului peste vocabularul său de 151.936 de intrări. Numărul care s-a schimbat se numește temperature, este descris în majoritatea documentațiilor ca un buton de creativitate, iar descrierea aceasta este greșită într-un fel pe care capitolul de față îl poate demonstra, nu doar afirma.

Acesta este și capitolul în care trei promisiuni anterioare ajung la scadență. Capitolul 4 a definit logit și nu l-a folosit cu adevărat. Caseta despre floating-point din Capitolul 2 s-a încheiat cu o instrucțiune — ține minte asta când Capitolul 17 întreabă de ce același prompt, model și seed pot produce tokens diferite. Iar caseta despre mixture-of-experts din Capitolul 9 a promis un catalog cu patru cauze ale non-determinismului. Toate trei apar mai jos.

Capitolul 4 a introdus logit ca scor real nenormalizat, câte unul pentru fiecare clasă. Capitolul 8 a făcut un language model să producă unul pentru fiecare intrare din vocabular. softmax transformă vectorul z\mathbf{z} în probabilități:

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

Temperature intră aici — numele este împrumutat din fizica statistică, unde același parametru controlează cât de puternic se concentrează o distribuție Boltzmann pe stările sale cu energie mică1 — și împarte logits înainte de exponențială:

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

Această poziționare este întregul mecanism și merită două rânduri de algebră ca să vezi de ce nu putea fi altundeva. Să presupunem că ai încerca să aplici temperature probabilităților — să le scalezi cu 1/T1/T și să renormalizezi. Ai obține

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

Constanta se anulează. Scalarea probabilităților nu face absolut nimic; distribuția revine neschimbată. Temperature are efect doar pentru că acționează asupra exponentului, unde împărțirea la TT înainte de exponentiere este același lucru cu ridicarea fiecărei probabilități la puterea 1/T1/T — o remodelare neliniară care schimbă rapoartele dintre intrări, nu scala lor comună.

Din această poziționare, ambele limite urmează fără efort suplimentar. Când T0T \to 0, cel mai mare logit se desprinde de restul, iar pp se prăbușește pe singurul token cu scorul cel mai mare: greedy decoding. Când TT crește, fiecare zi/Tz_i/T tinde spre zero, fiecare exponențială tinde spre 1, iar distribuția se aplatizează spre uniformă peste întregul vocabular. Exact la T=0T = 0 formula împarte la zero, așa că fiecare implementare îl tratează ca un caz special și folosește maximul aritmetic — inclusiv widgetul de mai jos, care trece la argmax la T0.001T \le 0.001.

Un avertisment, pentru că suprapunerea de nume creează confuzie reală. Există un al doilea lucru, fără legătură, numit temperature în machine learning: temperature scaling, o metodă de calibrare care potrivește o valoare pe un set de validare astfel încât încrederea unui clasificator să se potrivească cu acuratețea lui.2 Aceeași formulă, fără legătură cu generarea. Când lucrările spun „temperature”, adesea se referă la acela; capitolul acesta nu o face niciodată.

Iată distribuția, cu aritmetica în fața ta. logits sunt fixați și plauzibili, deci numerele din textul de mai jos pot fi verificate față de ce vezi:

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

10 din 10 tokenuri rămân după filtrare și împart probabilitatea.

Vezi datele ca tabel
TokenlogitDupă temperaturăDupă filtrare
␣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%
Eșantionare: temperatură, top-p și top-k

Zece continuări candidate pentru The capital of France is, la temperature 1 fără tăiere. ␣Paris deține 96,90 % din masă; ␣banana, jos de tot, cu un logit de 2.6-2.6, primește 0,00 %. Mută temperature la 0 și supraviețuiește un singur token, cu 100 %. Mută-l la 2 și ␣Paris coboară la 69,81 %, în timp ce ␣banana urcă la 0,17 % — tokenul respins de model, căruia un buton rotit de cititor îi acordă probabilitate reală.

Numărul ␣banana este tot argumentul în miniatură: creșterea temperature nu poate da unui model o idee pe care nu o avea. logits sunt deja calculați, ordinea este deja fixată, iar temperature o păstrează exact — nicio cantitate de căldură nu mută vreodată un token cu scor mai mic deasupra unuia cu scor mai mare. Tot ce face este să redistribuie masă în josul clasamentului produs chiar de model. Temperature mare nu face un model mai inventiv; îl face mai probabil să emită tokens pe care el însuși le-a cotat drept proaste.

Pe un vocabular real, asta încetează să mai fie o curiozitate și devine motivul pentru care ieșirea la temperature mare este inutilizabilă. Măsurat pe Qwen/Qwen2.5-0.5B-Instruct, un singur forward pass, promptul de mai sus, numărând câți tokens sunt necesari pentru a acumula o anumită parte din masa de probabilitate:

temperatureprobabilitate top-1entropietokens care dețin 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

Citește încet ultimul rând. La T=2T = 2, pe o întrebare cu exact un răspuns corect, 32.966 de tokens diferite împart primii 90 % din masa de probabilitate. Acesta nu este un spațiu creativ mai larg. Este un model căruia aritmetica i-a spus să trateze o particulă coreeană și un identificator C++ ca opțiuni active pentru cuvântul de după A:. Gunoiul din blocul de început este consecința directă și nu este un bug în model sau în bibliotecă — este ceea ce a cerut requestul.

Intervalul util este îngust și depinde de task, nu de gust. La o întrebare factuală, răspunsul este un singur token, iar orice căldură peste aproximativ 1,2 injectează eroare degeaba. La una deschisă există într-adevăr mai mult de o continuare bună, iar puțină căldură cumpără varietate care rămâne fluentă:

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..."

La 1,3 modelul a inventat un nume propriu și o propoziție care nu se parsează. Banda dintre „identic de fiecare dată” și „incoerent” este cam 0,6–1,1 pentru acest model pe acest task, iar sfatul onest este să o găsești măsurând pe taskul tău, nu copiind un număr dintr-un articol de blog.

Există o întrebare evidentă ascunsă sub toate acestea: dacă modelul are o distribuție de probabilitate și un token este cel mai probabil, de ce să nu îl alegi mereu? Greedy decoding este gratuit, reproductibil și nu are nevoie de parametri.

Pentru că rezultatul este acesta:

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 %

Opt propoziții, o singură propoziție. Aproape nouă din zece ferestre de patru tokens apăruseră deja mai devreme în aceeași ieșire. Aceasta este neural text degeneration, numită și explicată de Holtzman et al. în lucrarea care a introdus top-p.3 Modelul nu este stricat; maximizarea probabilității secvenței este pur și simplu obiectivul greșit pentru text deschis. Scrisul uman nu este cea mai probabilă secvență de cuvinte — poartă surpriză, probabilitatea sa per-token rătăcește, coboară și își revine — în timp ce traseul de probabilitate maximă este un punct fix care, odată intrat, nu are niciun motiv să iasă.

De aceea există sampling. Este și — iar aceasta este partea care rămâne adesea nespusă — nu o lege universală. Capitolul 12 a măsurat 24 din 24 corecte pe probleme de cuvinte în doi pași cu greedy decoding simplu, iar sampling la temperature 0,8 a coborât rezultatul la 81 %; self-consistency a cheltuit apoi de șase ori mai mulți tokens ca să urce înapoi unde greedy era deja. Ambele lucruri sunt adevărate simultan:

Generare deschisă. Nu există o singură continuare corectă, deci cea mai probabilă este o capcană — intră în buclă, iar 87,6 % din ea este copiată din ea însăși. Folosește sample.

Taskuri cu un singur răspuns corect. Există o continuare corectă, deci a extrage orice altceva înseamnă a extrage o eroare. Cei 100 % din Capitolul 12 au devenit 81 % exact din acest motiv. Nu folosi sample.

Majoritatea prompts de producție sunt de al doilea fel și sunt configurate ca primul, pentru că temperature a rămas la valoarea folosită de codul de exemplu.

Două feluri de a tăia, și doar unul se adaptează

Link către secțiunea: Două feluri de a tăia, și doar unul se adaptează

Sampling din distribuția completă nu este ceea ce face cineva în practică, pentru că coada este enormă și plină de nonsens. Ceva trebuie tăiat. Există două răspunsuri clasice și diferă într-un aspect care decide totul.

Top-k păstrează un număr fix de candidați. Sortează după probabilitate, păstrează primii kk, aruncă restul, renormalizează.4 Top-p, numit și nucleus sampling, păstrează o cantitate fixă de masă: ia tokens în ordine descrescătoare până când probabilitatea lor cumulată ajunge la pp, apoi se oprește.3 Formal, nucleul este cel mai mic set VpV_p cu

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

Diferența sună cosmetic, dar nu este, pentru că cele două prompts pe care le trimiți în același minut au forme de distribuție complet diferite. Ambele sunt același model la temperature 1:

Q: What is the capital of France?\nA:Once upon a time,
probabilitate top-196,01 %25,39 %
tokens care dețin 90 % din masă1467
top-k = 40 păstrează99,61 % din masă78,87 % din masă
masă în rangurile 2–403,61 %53,48 %
token la rangul 40␣Av, 0,0093 %␣Dr, 0,128 %

Un singur kk fix, două eșecuri în direcții opuse. Pe promptul factual, k=40k = 40 admite 39 de tokens care împreună valorează 3,6 % — lasă să treacă mizerie, inclusiv un candidat la nouă miimi de procent, pentru că regula numără sloturi, nu dovezi. Pe promptul de poveste, același k=40k = 40 aruncă 21 % din masa pe care modelul a atribuit-o cu adevărat, pentru că nucleul real acolo are 467 de tokens lățime.

Top-p face exact același număr să îndeplinească ambele roluri. Setează p=0.9p = 0.9 și păstrează 1 token pe primul prompt și 467 pe al doilea, pentru că pune o întrebare despre distribuție în loc să-i impună un număr. Urmărește direct adaptarea — aceeași tăiere, patru temperature:

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

3 din 10 tokenuri rămân după filtrare și împart probabilitatea.

Vezi datele ca tabel
TokenlogitDupă temperaturăDupă filtrare
␣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%
Eșantionare: temperatură, top-p și top-k

Top-p la 0,90 cu temperature la 1,5: trei dintre cei zece tokens supraviețuiesc și împart masa, ␣Paris renormalizat la 91,10 %. Acum mută doar temperature. La 0,7, același 0,90 lasă un singur supraviețuitor — un nucleu atât de îngust este greedy decoding sub alt nume. La 2,0 lasă cinci. Tăierea nu s-a mișcat; forma de dedesubt s-a schimbat.

Widgetul acesta clarifică și o concepție greșită care merită numită, fiindcă îi costă pe oameni bani reali. Pe o distribuție sigură, top_p = 0.9 nu este „puțină varietate”. Este greedy. La temperature 1, tokenul principal de aici deține 96,90 %, deja peste 0,9, deci nucleul are lățimea de un token și nimic altceva nu poate fi extras. Echipele setează top_p la 0,9 crezând că au relaxat ceva și apoi se întreabă de ce fiecare răspuns este identic.

Setează top-k în schimb, iar eșecul opus este la fel de vizibil:

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

5 din 10 tokenuri rămân după filtrare și împart probabilitatea.

Vezi datele ca tabel
TokenlogitDupă temperaturăDupă filtrare
␣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%
Eșantionare: temperatură, top-p și top-k

Top-k la 5, fără top-p. Cinci tokens supraviețuiesc la fiecare temperature, pentru că cinci este ce s-a cerut. La temperature 1, după cum se vede, cei patru candidați de sub ␣Paris valorează împreună 2,79 %. Coboară la 0,7 și aceiași patru valorează 0,38 % — tăierea este teatru, iar modelul este efectiv greedy. Ridică la 2,0 și valorează 22,54 %. Setare identică, număr identic de supraviețuitori, trei comportamente complet diferite, și nimic din request nu îți spune pe care îl primești.

Penalties, cu formulele, pentru că sunt confundate endemic

Link către secțiunea: Penalties, cu formulele, pentru că sunt confundate endemic

Trei mecanisme diferite circulă sub nume similare, fac lucruri diferite, iar diferența este măsurabilă. Fie cic_i numărul de ori de câte ori token ii a apărut deja.

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

Scade o constantă din orice token care a apărut măcar o dată. O apariție și patruzeci de apariții sunt penalizate identic. Este un comutator, nu un buton gradual.

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

Scade proporțional cu numărul. Un token folosit de patru ori este penalizat de patru ori mai tare decât unul folosit o dată, iar presiunea se compune pe măsură ce textul crește.

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}

Originalul, din lucrarea CTRL.7 Împarte în loc să scadă, cu cazul pentru semn necesar fiindcă împărțirea unui logit negativ l-ar face mai mare. Prin urmare, forța lui depinde de magnitudinea logitului, ceea ce înseamnă că același ρ\rho lovește diferit în puncte diferite ale aceleiași propoziții.

Aceeași continuare degenerată de mai devreme, cu fiecare dintre ele aplicată. „Pași alterați” numără câți dintre cei 120 de pași de generare au ales un token diferit de cel pe care l-ar fi ales modelul nepenalizat. Rularea are aici 120 de pași față de 140 în blocul de mai sus, de aceea baselineul nepenalizat arată 85,5 % în loc de 87,6 %:

setare4-grams repetatepași alterați
nimic85,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

Trei lucruri ies la iveală. Presence la 0,5 a schimbat trei decizii din 120 și a redus repetiția cu un sfert — bucla era ținută laolaltă de o mână de tokens. Frequency la 0,5 a schimbat de patru ori mai multe decizii pentru un efect mult mai mare, pentru că multiplicatorul de număr continuă să crească, în timp ce constanta presence nu. Iar CTRL penalty la valoarea de 1,2 copiată peste tot a rescris 35 din 120 de decizii, ceea ce nu este un mic imbold; este un alt model.

Ultimul număr pregătește eșecul despre care nu te avertizează nimeni.

Ce fac penalties textului care ar trebui să se repete

Link către secțiunea: Ce fac penalties textului care ar trebui să se repete

Codul se repetă. Tabelele se repetă. Listele se repetă. Ieșirea structurată se repetă prin definiție — asta este structura. Un penalty nu poate face diferența dintre un model blocat într-o buclă și un model care emite corect al patrulea rând al unui tabel, pentru că ambele arată ca un token care apare din nou.

Aceleași trei taskuri, generate în trei feluri:

tasknimicfrequency 0,5repetition 1,2
tabel markdown, 6 rânduri0 / 56 pași alterați0 / 562 / 62
funcție Python0 / 930 / 9310 / 110
listă cu bullets, 1–120 / 500 / 500 / 50

Frequency penalty la 0,5 s-a dovedit inofensiv pe toate trei, ceea ce este un rezultat util și ușor surprinzător, și spune ceva precis: fiindcă nicio decizie nu s-a schimbat, tokens structurali trebuie să fi câștigat pozițiile lor cu mai mult decât a scăzut penalty-ul, chiar și după ce au apărut de cinci și șase ori. CTRL penalty, care împarte în schimb, le dislocă, iar iată ce a produs:

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

Alinierea se destramă: cantitatea de spațiere din interiorul fiecărei celule se schimbă de la rând la rând, pentru că șirul de spații înainte de pipe-ul de închidere este exact genul de repetiție pe care penalty-ul există ca să o rupă. Cosmetic, și a costat șase tokens în plus. Cazul Python nu este cosmetic:

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,

Penalty-ul a împins modelul de pe total — deja folosit în docstring — pe total_sum, a umplut ieșirea cu comentarii inventate ca să își cheltuiască bugetul pe tokens nefolosite, apoi a intrat într-un range cu trei argumente și pas. Comentariul spune incrementing by 2 each time, ceea ce este greșit pentru o sumă de pătrate de la 1 la nn. Un repetition penalty a produs cod incorect dintr-un prompt la care se răspundea corect fără el.

Regula care urmează este scurtă: penalties sunt pentru proză deschisă și ar trebui să fie oprite pentru cod, ieșire structurată, date tabelare și orice are o schemă. Capitolul 18 este exact despre a doua categorie.

Fiecare implementare reală aplică acestea într-o secvență specifică:

penalties → temperature → top-k → top-p → sample

Nu este contabilitate arbitrară, iar inversarea a două etape produce distribuții cu adevărat diferite. Două măsurători, ambele pe promptul factual.

Tăiere înainte sau după temperature. Nucleul este calculat pe distribuția care îi este dată, iar temperature schimbă radical acea distribuție:

top-p 0,9 după temperaturetop-p 0,9 înainte de temperature
T=1.0T = 1.01 token1 token
T=1.5T = 1.5353 tokens1 token
T=2.0T = 2.032.966 tokens1 token

La T=2T = 2 aceeași setare nominală produce un set de candidați de 32.966 sau de 1, în funcție pur și simplu de etapa care rulează prima. Dacă te-ai întrebat vreodată de ce ridicarea temperature „nu face nimic” la un provider și distruge ieșirea la altul cu aceleași două numere, tabelul acesta este un răspuns plauzibil.

Penalizare înainte sau după temperature. A scădea un penalty α\alpha și apoi a împărți la TT dă un penalty efectiv de α/T\alpha/T; a împărți mai întâi și apoi a scădea dă α\alpha. Cu un presence penalty de 1,0 aplicat tokenului principal:

temperaturepenalizezi, apoi temperezitemperezi, apoi penalizezi
0,599,858 %99,948 %
1,089,839 %89,839 %
2,010,783 %6,830 %

Identice la T=1T = 1, așa cum trebuie să fie. La distanță de un factor de 1,58 la T=2T = 2. „Presence penalty 1,0” nu este o cantitate bine definită de penalty dacă nu știi și unde se aplică temperature, iar nicio API nu documentează asta.

Afișează detaliile

Opțional: întregul pipeline, în ordinea de mai sus.

Șaisprezece linii, și tot ce este în acest capitol se află în ele. Este același calcul pe care îl face widgetul, pe un vector real de logits în loc de zece numere fixe.

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)])

cumsum(0) - p din linia top-p este masa cumulată excluzând tokenul curent, ceea ce face ca nucleul să includă tokenul care trece pragul în loc să se oprească imediat înaintea lui. Greșește asta cu unu și top_p = 0.9 devine în tăcere o tăiere puțin mai strictă decât în orice altă implementare.

Acesta este unul dintre puținele locuri din a doua jumătate a cursului unde Python este limbajul potrivit, iar motivul este structural, nu stilistic: fiecare linie de mai sus are nevoie de întregul vector de logits în mâinile tale, iar printr-o HTTP API acel vector nu există. Poți trimite temperature și top_p unui provider; nu le poți implementa și nu poți vedea ce au făcut.

Fiecare provider acceptă un subset diferit al acestor controale, cu intervale diferite, și le ignoră pe restul în tăcere. Aceasta nu este o plângere abstractă. Orice aplicație care oferă alegere de model trebuie să noteze diferențele undeva, iar fișierul unde face asta este o hartă a incompatibilității. Iată ce declară un astfel de catalog pentru un singur parametru în cele nouă surse de text pe care le suportă:

interval temperature declaratsurse
0–1Anthropic, Google, Meta, Cerebras, PaLM
0–1,5Mistral
0–2OpenAI, DeepSeek, xAI

Cuvântul este același; scala nu este. O „temperature de 1” este distribuția nemodificată la unul și căldura maximă permisă la altul, iar jumătate din catalog nu poate exprima valoarea pe care cealaltă jumătate o tratează ca neutră-plus-un-pic. Restul butoanelor sunt la fel de inegale: intrările OpenAI, DeepSeek și xAI acceptă presence și frequency penalties și niciun topK; intrările Google, Meta, Cerebras și PaLM acceptă topK și niciun penalty; Anthropic acceptă topK, topP și stop sequences și niciun penalty; iar exact una din cele nouă — Mistral — acceptă seed. Trimiterea unui parametru pe care un provider nu îl implementează nu produce, de regulă, nicio eroare: requestul reușește, butonul nu face nimic, iar tu concluzionezi că setarea nu are efect.

Și observă ce este un astfel de fișier: o afirmație despre API-ul altcuiva, scrisă într-o anumită zi, pe care nimic nu o verifică ulterior. Un catalog care spune 0–1 pentru un provider care acceptă acum 0–2 va limita în tăcere fiecare request.

Încă două controale aparțin aceleiași familii. logprobs, acolo unde este oferit, returnează log-probabilitățile tokenului ales și adesea primele câteva alternative — singura fereastră pe care o ai către distribuția despre care vorbește acest capitol și baza fiecărei euristici de încredere construită pe un model închis. Iar maximum tokens plus stop sequences încheie generarea fără nicio referire la probabilitate: o limită dură și o potrivire de șir. Ambele apar ca finish_reason din Capitolul 14, unde length înseamnă că răspunsul tău a fost tăiat la mijlocul propoziției de un buget, nu încheiat de model.

Setează un seed și sampling devine reproductibil. Partea aceasta este reală și ușor de verificat:

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\"."

Identic byte cu byte în interiorul aceluiași seed, diferit între seeds, exact cum se promite. Deci ceea ce fixează seedul este extragerea aleatorie din ultima linie a acelei funcții sample — ce token este ales pentru o distribuție dată.

Ceea ce nu fixează este distribuția. Și acolo este problema, pentru că vectorul de logits pe care îl produce modelul tău nu este un obiect matematic; este rezultatul a miliarde de adunări floating-point, iar acestea au o ordine.

Capitolul 2 a lăsat pregătit acest experiment. Aceleași un milion de numere float32, însumate în grupări diferite:

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

Uită-te la ultima linie. Numărul de chunks schimbă răspunsul. Nu este o curiozitate despre numpy; este mecanismul, pentru că atunci când un server de inference împarte o reducere peste mai multe sau mai puține unități paralele, face exact asta. Iar un server împarte în funcție de câte requesturi servește.

Iată efectul asupra modelului însuși. Același prompt, același forward pass, singura diferență fiind câte alte requesturi s-au întâmplat să fie în 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

Rulat singur, modelul este perfect determinist — douăzeci de treceri, identice până la bit. Pune promptul identic într-un batch cu requesturi fără legătură și 97 % din logits se schimbă. Nimic din requestul tău nu s-a schimbat. A sosit requestul altcuiva.

Acum partea onestă, pentru că de obicei asta este povestită ca și cum ar fi sfârșitul discuției. O schimbare de 2.5×1052.5 \times 10^{-5} modifică ieșirea doar dacă doi tokens candidați erau la distanță mai mică decât atât unul de celălalt. Pe 717 pași de generare în douăsprezece prompts, cel mai mic gap dintre primii doi logits a fost 2.5×1032.5 \times 10^{-3} — de o sută de ori mai mare decât perturbația — și niciun pas nu a fost suficient de aproape ca să se inverseze. Deci pe acest model, în float32, pe un laptop, batching a mutat fiecare logit și nu a schimbat niciun token.

Aceasta este o descriere a unor condiții favorabile, nu o reasigurare, iar o singură schimbare a acelor condiții este suficientă:

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."

Șase din opt răspunsuri diverg, iar unul dintre ele se degradează grav. Tabelul din Capitolul 2 spune de ce: bfloat16 păstrează 7 biți de mantisă, deci lângă o magnitudine de logit de 16 valorile reprezentabile sunt la 0,125 distanță — 16,0, apoi 16,125, apoi 16,25 — iar rotunjirea poate muta un logit cu până la 0,0625. Între timp, 4,7 % dintre pașii de generare măsurați mai sus au avut un gap top-two sub 0,1. Aceasta este întreaga diferență dintre cele două experimente: în float32 perturbația era de o sută de ori mai mică decât cea mai apropiată decizie, iar în bfloat16 este de aceeași dimensiune. Inference de producție rulează în 16-bit, pe hardware cu kernels fuzionate și ordine de reducere pe care nimeni nu promite să le păstreze. Dacă „zgomotul numeric este neglijabil” este o întrebare despre precizie și hardware, nu despre model.

Așadar, cele patru cauze, catalogate așa cum a promis Capitolul 9:

Caseta din Capitolul 2. Ordinea unei sume îi schimbă valoarea, deci orice schimbare în felul în care este împărțită o reducere schimbă logits. Acesta este substratul; celelalte trei sunt moduri de a schimba ordinea.

Dynamic batching îți grupează requestul cu ale unor străini

Link către secțiunea: Dynamic batching îți grupează requestul cu ale unor străini

Continuous batching, din Capitolul 13, este motivul pentru care inference este accesibil — și înseamnă că forma matricelor prin care trec tokens tăi depinde de trafic. Măsurat mai sus: 147.321 logits s-au mișcat pentru că dimensiunea batchului s-a schimbat.

Caseta din Capitolul 9 a spus-o deja. Routerul face o alegere discretă per token per strat, supusă limitelor de capacitate per expert calculate peste batch. Un token care singur ar fi mers la expertul 7 merge la expertul 12 în companie. Nu este o diferență de rotunjire; este un set diferit de weights.

Un version string precum -latest este un pointer, iar pointerele sunt redirecționate. Providerii actualizează și serving stackul sub un identificator de versiune fix. Niciuna dintre acestea nu este anunțată la granularitatea care ți-ar permite să o corelezi cu schimbarea propriei ieșiri.

Parametrul seed de la OpenAI este onest despre asta în singurul fel în care poate fi: vine împreună cu un câmp system_fingerprint care identifică configurația backendului, iar documentația spune că determinismul este best-effort și că un fingerprint schimbat înseamnă că rezultatele pot diferi. Citește asta drept ceea ce este — un provider care îți spune că el controlează toate cele patru cauze de mai sus, că tu nu controlezi niciuna și că singurul lucru pe care îl poate oferi este să îți spună după fapt că ceva s-a mișcat.

Tot ce am discutat aici a fost despre un buton și consecințele lui. Fă un pas înapoi și apare problema mai grea: obiectul pe care l-am ajustat este o distribuție de probabilitate, iar distribuțiile de probabilitate nu au interfață.

Un function call are. Un rând de bază de date are. Un handler POST care așteaptă un corp JSON cu trei câmpuri obligatorii are una și va respinge orice altceva. Între model și fiecare altă componentă din sistemul tău stă un contract despre care una dintre părți nu poate face promisiuni: modelul va produce ceva, extras dintr-o distribuție pe care ai modelat-o, dar nu ai fixat-o, iar codul de cealaltă parte are nevoie de o valoare de un tip cunoscut, altfel aruncă eroare.

Puntea dintre aceste două lumi este construită din materialul acestui capitol, nu din parsing și retry. Dacă un token ar rupe structura cerută, nu îl samplezi și speri — îi setezi logit la -\infty înainte ca softmax să îl vadă. Constrained decoding este o mască peste același vector pe care l-am remodelat tot capitolul, și transformă „te rog răspunde în JSON” dintr-o cerere într-o garanție.

Capitolul 18 este acel contract: tool calling, JSON Schema, structured outputs și ce îți trebuie ca să construiești în siguranță un sistem determinist deasupra unuia probabilistic.


Toate măsurătorile din acest capitol vin din Qwen/Qwen2.5-0.5B-Instruct pe CPU, float32 dacă nu se precizează altfel, cu sampling implementat exact cum este scris în secțiunea opțională, nu delegat unei biblioteci. Este un model mic, iar valorile specifice sunt ale lui; mecanismele nu sunt. How to generate text with different decoding methods de Von Platen (Hugging Face, 2020) este articolul față de care este măsurat acesta și rămâne cea mai bună introducere scurtă la același material. Pentru secțiunea despre determinism: notele PyTorch despre reproductibilitate descriu ce fixează și ce nu fixează un seed pe o singură mașină, documentația OpenAI pentru seed și system_fingerprint descrie ce poate și ce nu poate promite un provider, iar discuția Thinking Machines din 2025 despre batch-invariant kernels este cea mai clară explicație publică despre de ce remedierea acestui lucru la nivelul serverului de inference este posibilă, dar nu gratuită.

  1. Ackley, D. H., Hinton, G. E. și Sejnowski, T. J. A Learning Algorithm for Boltzmann Machines. Cognitive Science 9(1), pp. 147–169 (1985), unde temperature dintr-un softmax vine din fizica statistică. Hinton, G., Vinyals, O. și Dean, J., Distilling the Knowledge in a Neural Network, arXiv:1503.02531 (2015), secțiunea 2, este locul unde același parametru reapare în deep learning modern — ca mod de a expune distribuția completă a unui profesor, adică soft labels din Capitolul 13, nu samplingul din acest capitol.

  2. Guo, C., Pleiss, G., Sun, Y. și Weinberger, K. Q. On Calibration of Modern Neural Networks. arXiv:1706.04599 (2017). Nu confunda asta cu temperature din acest capitol. Temperature scaling potrivește o singură valoare pe un set de validare astfel încât încrederea modelului să se potrivească cu acuratețea sa; este o metodă de calibrare post-hoc aplicată ieșirilor unui clasificator. Temperature sampling este un control runtime asupra felului în care un generator extrage tokens. Aceeași formulă, scop diferit și nicio valoare comună.

  3. Holtzman, A., Buys, J., Du, L., Forbes, M. și Choi, Y. The Curious Case of Neural Text Degeneration. arXiv:1904.09751 (2019). Introduce nucleus sampling și măsurarea faptului că decodingul bazat pe maximizare produce text al cărui profil de probabilitate nu seamănă deloc cu textul uman. 2

  4. Fan, A., Lewis, M. și Dauphin, Y. Hierarchical Neural Story Generation. arXiv:1805.04833 (2018). Lucrarea care a popularizat 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. și Cotterell, R. Locally Typical Sampling. arXiv:2202.00666 (2022).

  7. Keskar, N. S., McCann, B., Varshney, L. R., Xiong, C. și Socher, R. CTRL: A Conditional Transformer Language Model for Controllable Generation. arXiv:1909.05858 (2019). Secțiunea 4.1 este repetition penalty original — cel care împarte.

Gata să lași LIA să aleagă?

Construiește cu toate modelele AI într-un singur loc — începe gratuit azi.