Temperature, top-k and top-p: the three knobs on the API
Three parameters everyone has typed into a request body and nobody has explained together. They are two operations, in a fixed order.
Download sampling_from_scratch.py — everything below runs from it. NumPy only, no model needed.
I shipped an LLM feature with temperature=0.7, top_p=0.9 in the config for
about eight months before I could have told you what the second one did. I knew the
folklore — "lower temperature is more factual", "top_p is like temperature but
different" — and the folklore is wrong in a way that matters. My mental model was three
dials on the same box. It isn't. It's a contrast slider and a crop tool, applied in that
order, and once you see the order the inconsistent intuitions all resolve.
Temperature is contrast. Top-k and top-p are the crop.
At every step the model hands you one array: a score per vocabulary entry. Sixteen entries in my toy vocab, fifty thousand in a real one. That's it. That array is the only thing these three parameters touch.
logits = model(prompt) # one float per vocab entry, unbounded
probs = softmax(logits) # percentages, summing to 1
token = draw_one(probs) # weighted dice roll
Temperature goes in on line 1 — it divides the logits before the softmax. Top-k
and top-p go in on line 3 — they delete entries from probs and renormalise
what's left. Different stage, different kind of operation:
logits = model(prompt) / T # CONTRAST: gaps stretch/squash
probs = softmax(logits)
probs = keep_top_k(probs, k) # CROP: a fixed count survives
probs = keep_top_p(probs, p) # CROP: a fixed mass survives
probs = probs / probs.sum() # what's left must sum to 1
token = draw_one(probs)
| Knob | What it actually is | Programmer's analogy |
|---|---|---|
| temperature | Divide the logits. Ranking is preserved; only the gaps change. | The contrast slider. Same photo, more or less separation between tones. |
| top-k | Keep the k highest entries. A fixed count. | LIMIT 40. Blind to how good row 41 was. |
| top-p | Keep the smallest set whose probabilities add up to p. A fixed mass. | Pagination by bytes, not by rows. The row count falls out of the data. |
top_k and top_p do
happens below. That single boundary is the whole confusion.
Why Holtzman's paper is the one that matters
Before 2019 the received wisdom was that better decoding meant getting closer to the most likely sequence: beam search, wider beams, greedy at the limit. The Curious Case of Neural Text Degeneration is the paper that said the goal itself is wrong. Maximising likelihood produces text that repeats and then locks — the same clause over and over — and the authors' central observation is that human-written text does not sit at the top of the model's probability distribution at all. It wanders through a lower-probability band that greedy decoding never visits.
So the fix isn't a better search. It's to sample, but from a truncated distribution — and to truncate by mass rather than by count, because the right count changes at every step. That's nucleus sampling. Top-k, from Fan et al.'s story-generation paper the year before, is the thing it improves on.
The one formula
Read it as pipeline stages:
| Step | Shape | What it does |
|---|---|---|
logits / T | (V,) | Temperature. On the raw scores, before anything else. |
softmax | (V,) | Percentages summing to 1. The boundary. |
argsort(−p) | (V,) | Most likely first. Both crops need the ranking. |
cumsum | (V,) | Running total. Where it crosses p, you stop. |
keep, zero rest | (V,) | The nucleus. Everything outside it is unreachable. |
/ sum | (V,) | Renormalise. After truncating, never before. |
Temperature never reorders anything
Dividing every logit by the same positive number is monotonic. Whatever was the most likely token stays the most likely token. Temperature cannot promote a word into first place; it can only decide how big a lead first place has. That's why "raise the temperature to get a different answer" works only in combination with the dice roll — you widened the gap the dice have to cover, you didn't change the ranking.
Don't take my word for it. Here's the actual output:
T=0.2 H=0.0980 Monday=0.9811 tomorrow=0.0180 next=0.0009 the=0.0001
T=0.5 H=0.7347 Monday=0.7743 tomorrow=0.1563 next=0.0471 the=0.0173
T=0.8 H=1.2322 Monday=0.5862 tomorrow=0.2156 next=0.1019 the=0.0545
T=1.0 H=1.4843 Monday=0.4967 tomorrow=0.2232 next=0.1225 the=0.0743
T=1.5 H=1.9354 Monday=0.3525 tomorrow=0.2068 next=0.1386 the=0.0993
T=2.0 H=2.2101 Monday=0.2717 tomorrow=0.1821 next=0.1349 the=0.1051
ranking is IDENTICAL at every temperature:
all six orderings equal? True
only the GAPS changed. temperature cannot promote a token.
H is entropy in nats — how undecided the distribution is. At T=0.2 it's
0.098, which is "I have already decided". At T=2.0 it's 2.21, against a ceiling of ln 16
≈ 2.77 for a 16-token vocabulary, which is nearly "no idea". Same model, same prompt,
same ranking.
The trap at T = 0
temperature=0 is in every API doc and it is not a temperature. Dividing by
zero gives you infinities, then nan out of the softmax, then a crash in the
sampler. Every real implementation branches around it:
T = 0 exactly:
raises: T=0: real code branches to argmax instead
raw numpy would give: [6.0e+12 5.2e+12 4.6e+12] ... then inf/nan
what real code does instead: argmax -> 'Monday' ✓
So "temperature 0" in a request body means "skip this whole page and take the argmax". It is a special case wearing a number's clothing. The interview question here is not the division; it's whether you know T = 0 means greedy, and that greedy means the failure mode in the last section.
Top-k: a fixed count, blind to shape
Keep the k highest-probability tokens, zero the rest, renormalise. On my confident step
— "the meeting is scheduled for ___", where Monday takes half the mass —
small k works fine:
k=1 keeps 0.4967 of the mass | Monday=1.0000
k=3 keeps 0.8424 of the mass | Monday=0.5896 tomorrow=0.2649 next=0.1454
k=5 keeps 0.9468 of the mass | Monday=0.5246 tomorrow=0.2357 next=0.1294 ...
k=10 keeps 0.9952 of the mass | Monday=0.4991 tomorrow=0.2242 next=0.1231 ...
Note what renormalising does: at k=3, Monday goes from 0.4967 to 0.5896.
The survivors absorb the deleted mass in proportion to what they already had. Nothing is
"boosted"; the denominator shrank.
Now the same k on a second step, where the model genuinely doesn't know what comes next — a flat distribution with entropy 2.7374 nats against that 2.77 ceiling:
k=3 confident step keeps 0.8424 | flat step keeps 0.2616 (gap 0.5807)
k=5 confident step keeps 0.9468 | flat step keeps 0.4117 (gap 0.5351)
k=10 confident step keeps 0.9952 | flat step keeps 0.7275 (gap 0.2678)
One k=3 setting is two completely different behaviours. On the confident
step it keeps 84% of what the model believes — reasonable. On the flat step it keeps 26%
and throws away three quarters of a distribution the model was telling you was genuinely
open. Turn k up to fix the flat step and you have now let junk into the confident one.
There is no value of k that is right for both, and a real generation makes both kinds of
step in the same sentence.
Top-p: a fixed mass, so the set size adapts
Nucleus sampling asks a different question. Not "how many tokens?" but "how much of the model's belief?". Sort, take a running total, stop when it crosses p. The size of the surviving set is decided by the distribution, not by you. Same two steps, same script:
p confident step flat step
0.5 2 tokens, mass 0.7199 7 tokens, mass 0.5476
0.8 3 tokens, mass 0.8424 12 tokens, mass 0.8333
0.9 4 tokens, mass 0.9166 14 tokens, mass 0.9253
0.95 6 tokens, mass 0.9671 15 tokens, mass 0.9654
1.0 16 tokens, mass 1.0000 16 tokens, mass 1.0000
That p=0.9 row is the paper. One setting, four tokens where the model is confident and fourteen where it isn't. The nucleus grew because the distribution flattened — nobody adjusted anything.
at p=0.9 the nucleus is 4 tokens on the confident step and 14 on the flat one.
confident nucleus: Monday tomorrow next the
flat nucleus: Monday tomorrow next the March later noon sometime ...
k=3 cuts at a place it chose before seeing the
data. p=0.9 cuts one bar later here, and ten bars later on
the flat step. Both crops then renormalise, so all three panels end up
summing to 1.
asdf at 0.000167 — reachable roughly once every six thousand
tokens. On a 50k vocab that tail is where the genuinely bizarre outputs
come from. Holtzman's argument is precisely that this tail is
disproportionately reachable when you sample untruncated.
The order is part of the definition
Because temperature reshapes and truncation crops, running them in the other order gives you a different distribution — not a rounding difference, a different set of reachable tokens. At T=1.5 and p=0.9 on my fixed vector:
T=1.5, p=0.9
temp-then-p keeps 7 tokens: Monday tomorrow next the March later noon
p-then-temp keeps 4 tokens: Monday tomorrow next the
same surviving set? False
Monday temp-then-p 0.3809 p-then-temp 0.4422 diff -0.0613
tomorrow temp-then-p 0.2234 p-then-temp 0.2594 diff -0.0359
next temp-then-p 0.1498 p-then-temp 0.1739 diff -0.0241
the temp-then-p 0.1073 p-then-temp 0.1246 diff -0.0173
March temp-then-p 0.0589 p-then-temp 0.0000 diff +0.0589
max abs difference: 0.0613
March is reachable in one order and impossible in the other. Real
implementations — Hugging Face's logits processors, vLLM, the hosted APIs — do
temperature, then top-k, then top-p. Raising temperature flattens the
distribution first, which makes the nucleus grow, which is what you expected
when you turned the dial up. In the wrong order the nucleus is frozen at T=1 and
temperature only redistributes inside it. Same two numbers, quietly different product.
The other order mistake is renormalising before you truncate instead of after. It doesn't error — it just hands the sampler a vector that no longer sums to 1:
and renormalising BEFORE truncating instead of after:
sum after cutting an already-normalised vector: 0.9807
np.random.choice raises: probabilities do not sum to 1
Why greedy decoding actually fails
You don't need a language model to see degeneration. A first-order Markov chain over eight words is enough: take the argmax at every step and the moment you revisit a state you are in a cycle forever, because the argmax is deterministic. Nothing can break it. This is the same mechanism as the repetition loop in a real model, minus the excuses.
greedy from 'the': the model is a model is a model is a model is ...
loop detected, period 3: model is a
greedy avg surprisal: 1.0670 bits/token
sampled (T=1.0, p=0.9): the model is the the data good model learns from data ...
loop detected? no -- it revisits states without getting stuck
sampled avg surprisal over 200 runs: 2.0463 bits/token
greedy is 0.9792 bits/token LESS surprising
-- and unreadable. maximising likelihood is not the goal.
Read those two surprisal numbers again. Surprisal is −log₂ P(token) under
the model — the number the model is trained to make small. Greedy wins on it, by a full
bit per token, and produces "model is a model is a model is a". The sampled
text is twice as surprising to the model and is the one you'd ship.
And the parameter that reproduces this on purpose: top_k=1. Keeping exactly
one token means the renormalised distribution is 1.0 on the argmax, so the dice roll is
a formality.
top-k with k=1 is greedy wearing a hat: identical to the greedy walk
k=1 walk: the model is a model is a model is a model is ...
temperature=0.7, top_p=0.9 என்று config-ல் எழுதி ஒரு LLM feature-ஐ
எட்டு மாதங்கள் production-ல் ஓட விட்டேன் — இரண்டாவது parameter என்ன செய்கிறது
என்று சொல்லத் தெரியாமல். எனக்குத் தெரிந்ததெல்லாம் கதைகள்தான் — "temperature
குறைத்தால் உண்மையாக இருக்கும்", "top_p-ம் temperature மாதிரிதான், ஆனால்
வேறு" — அந்தக் கதைகள் தவறு, முக்கியமான ஒரு விதத்தில். என் மனதில் ஒரே பெட்டியில்
மூன்று dial-கள் இருந்தன. அப்படி இல்லை. அது ஒரு contrast slider-ம் ஒரு crop
tool-ம், அந்த வரிசையில். வரிசையைப் பார்த்த பிறகு குழப்பங்கள் எல்லாம் தீர்ந்தன.
Temperature என்பது contrast. Top-k, top-p என்பவை crop.
ஒவ்வொரு step-லும் model உங்களுக்குத் தருவது ஒரே ஒரு array: vocabulary-ல் உள்ள ஒவ்வொரு entry-க்கும் ஒரு score. என் விளையாட்டு vocab-ல் பதினாறு entry, நிஜமான ஒன்றில் ஐம்பதாயிரம். அவ்வளவுதான். இந்த மூன்று parameter-களும் தொடுவது அந்த array-ஐ மட்டும்தான்.
logits = model(prompt) # ஒரு vocab entry-க்கு ஒரு float
probs = softmax(logits) # சதவீதம், கூட்டினால் 1
token = draw_one(probs) # எடையிட்ட பகடை உருட்டல்
Temperature வரி 1-ல் நுழைகிறது — softmax-க்கு முன் logits-ஐ
வகுக்கிறது. Top-k-ம் top-p-ம் வரி 3-ல் நுழைகின்றன — probs-லிருந்து
entry-களை அழித்துவிட்டு மீதியை renormalise செய்கின்றன. வேறு stage, வேறு
வகையான operation:
logits = model(prompt) / T # CONTRAST: இடைவெளி விரியும்/சுருங்கும்
probs = softmax(logits)
probs = keep_top_k(probs, k) # CROP: நிலையான எண்ணிக்கை தப்பிக்கும்
probs = keep_top_p(probs, p) # CROP: நிலையான mass தப்பிக்கும்
probs = probs / probs.sum() # மீதி 1-ஆகக் கூட வேண்டும்
token = draw_one(probs)
| Knob | உண்மையில் என்ன | Programmer-க்கான ஒப்புமை |
|---|---|---|
| temperature | Logits-ஐ வகுக்கிறது. வரிசை மாறாது; இடைவெளி மட்டும் மாறும். | Contrast slider. அதே புகைப்படம், நிறங்களுக்கு இடையே அதிக அல்லது குறைந்த வேறுபாடு. |
| top-k | உயர்ந்த k entry-களை வைத்துக்கொள். நிலையான எண்ணிக்கை. | LIMIT 40. வரிசை 41 எவ்வளவு நல்லது என்பது தெரியாது. |
| top-p | Probability கூட்டுத்தொகை p-ஐ எட்டும் மிகச்சிறிய தொகுப்பை வைத்துக்கொள். நிலையான mass. | வரிசைகளால் அல்ல, bytes-ஆல் pagination. எத்தனை வரிசை என்பது data-விலிருந்து வரும். |
top_k, top_p செய்யும் அனைத்தும் கீழே. அந்த ஒரு
எல்லைதான் முழுக் குழப்பமும்.
Moonu knob illa — rendu operation dhaan. Temperature softmax-kku munnadi logits-a divide pannudhu, top-k-um top-p-um softmax-kku appuram probs-a crop pannudhu. Andha border dhaan ellaa confusion-oda moolam.
Holtzman-ன் paper ஏன் முக்கியம்
2019-க்கு முன் நம்பப்பட்டது: நல்ல decoding என்றால் மிக அதிக probability உள்ள sequence-ஐ நெருங்குவது — beam search, அகலமான beam, எல்லையில் greedy. The Curious Case of Neural Text Degenerationதான் இலக்கே தவறு என்று சொன்ன paper. Likelihood-ஐ அதிகப்படுத்தினால் text திரும்பத் திரும்ப வந்து பிறகு பூட்டிக்கொள்கிறது — அதே clause மீண்டும் மீண்டும். ஆசிரியர்களின் மையக் கவனிப்பு: மனிதர்கள் எழுதும் text, model-ன் distribution-ன் உச்சியில் இருப்பதே இல்லை. அது greedy decoding ஒருபோதும் செல்லாத, குறைந்த probability உள்ள ஒரு பட்டையில் அலைகிறது.
எனவே தீர்வு சிறந்த ஒரு search அல்ல. Sample செய்ய வேண்டும், ஆனால் வெட்டப்பட்ட ஒரு distribution-லிருந்து — எண்ணிக்கையால் அல்ல, mass-ஆல் வெட்ட வேண்டும், ஏனெனில் சரியான எண்ணிக்கை ஒவ்வொரு step-லும் மாறுகிறது. அதுதான் nucleus sampling. அதற்கு முந்தைய ஆண்டு Fan et al.-ன் story-generation paper-ல் வந்த top-k, இது மேம்படுத்தும் விஷயம்.
அந்த ஒரு சூத்திரம்
இதை pipeline stage-களாகப் படியுங்கள்:
| Step | Shape | என்ன செய்கிறது |
|---|---|---|
logits / T | (V,) | Temperature. Raw score மீது, எல்லாவற்றுக்கும் முன். |
softmax | (V,) | கூட்டினால் 1 ஆகும் சதவீதம். இதுதான் எல்லை. |
argsort(−p) | (V,) | அதிக probability முதலில். இரு crop-க்கும் வரிசை தேவை. |
cumsum | (V,) | ஓடும் கூட்டுத்தொகை. p-ஐத் தாண்டும் இடத்தில் நிறுத்து. |
keep, zero rest | (V,) | Nucleus. அதற்கு வெளியே உள்ள அனைத்தும் எட்ட முடியாதவை. |
/ sum | (V,) | Renormalise. வெட்டிய பிறகு, ஒருபோதும் முன் அல்ல. |
Temperature ஒருபோதும் வரிசையை மாற்றுவதில்லை
ஒவ்வொரு logit-ஐயும் ஒரே நேர்மறை எண்ணால் வகுப்பது monotonic. முன்பு எது அதிக probability கொண்ட token-ஆக இருந்ததோ அதுவே பிறகும் இருக்கும். Temperature ஒரு வார்த்தையை முதலிடத்துக்குத் தூக்க முடியாது; முதலிடத்தின் முன்னிலை எவ்வளவு பெரியது என்பதை மட்டுமே தீர்மானிக்க முடியும். அதனால்தான் "வேறு பதில் வேண்டுமா, temperature-ஐ ஏற்று" என்பது பகடை உருட்டலோடு சேர்ந்துதான் வேலை செய்கிறது — பகடை கடக்க வேண்டிய இடைவெளியை நீங்கள் அகலப்படுத்தினீர்கள், வரிசையை மாற்றவில்லை.
என் வார்த்தையை நம்ப வேண்டாம். இதோ நிஜமான output:
T=0.2 H=0.0980 Monday=0.9811 tomorrow=0.0180 next=0.0009 the=0.0001
T=0.5 H=0.7347 Monday=0.7743 tomorrow=0.1563 next=0.0471 the=0.0173
T=0.8 H=1.2322 Monday=0.5862 tomorrow=0.2156 next=0.1019 the=0.0545
T=1.0 H=1.4843 Monday=0.4967 tomorrow=0.2232 next=0.1225 the=0.0743
T=1.5 H=1.9354 Monday=0.3525 tomorrow=0.2068 next=0.1386 the=0.0993
T=2.0 H=2.2101 Monday=0.2717 tomorrow=0.1821 next=0.1349 the=0.1051
ranking is IDENTICAL at every temperature:
all six orderings equal? True
only the GAPS changed. temperature cannot promote a token.
H என்பது nats-ல் entropy — distribution எவ்வளவு
முடிவெடுக்காமல் இருக்கிறது என்பது. T=0.2-ல் அது 0.098, அதாவது "நான் ஏற்கனவே
முடிவு செய்துவிட்டேன்". T=2.0-ல் அது 2.21, 16-token vocabulary-க்கான உச்ச
வரம்பான ln 16 ≈ 2.77-க்கு எதிராக — கிட்டத்தட்ட "தெரியவே தெரியாது". அதே
model, அதே prompt, அதே வரிசை.
T = 0 என்ற பொறி
temperature=0 ஒவ்வொரு API doc-லும் இருக்கிறது, அது ஒரு
temperature அல்ல. பூஜ்ஜியத்தால் வகுத்தால் infinity, பிறகு softmax-லிருந்து
nan, பிறகு sampler-ல் crash. நிஜமான ஒவ்வொரு implementation-ம்
அதைச் சுற்றி வளைக்கிறது:
T = 0 exactly:
raises: T=0: real code branches to argmax instead
raw numpy would give: [6.0e+12 5.2e+12 4.6e+12] ... then inf/nan
what real code does instead: argmax -> 'Monday' ✓
எனவே request body-ல் "temperature 0" என்றால் "இந்தப் பக்கம் முழுவதையும் தவிர்த்துவிட்டு argmax எடு" என்று அர்த்தம். எண்ணின் உடையணிந்த ஒரு சிறப்பு வழக்கு. இங்கே interview கேள்வி வகுத்தல் அல்ல; T = 0 என்றால் greedy என்றும், greedy என்றால் கடைசிப் பிரிவில் உள்ள தோல்வி முறை என்றும் உங்களுக்குத் தெரியுமா என்பதுதான்.
Top-k: நிலையான எண்ணிக்கை, வடிவம் தெரியாதது
அதிக probability உள்ள k token-களை வைத்துக்கொள், மீதியைப் பூஜ்ஜியமாக்கு,
renormalise செய். என் நம்பிக்கையான step-ல் — "the meeting is scheduled for
___", அங்கு Monday பாதி mass-ஐ எடுத்துக்கொள்கிறது — சிறிய k
நன்றாக வேலை செய்கிறது:
k=1 keeps 0.4967 of the mass | Monday=1.0000
k=3 keeps 0.8424 of the mass | Monday=0.5896 tomorrow=0.2649 next=0.1454
k=5 keeps 0.9468 of the mass | Monday=0.5246 tomorrow=0.2357 next=0.1294 ...
k=10 keeps 0.9952 of the mass | Monday=0.4991 tomorrow=0.2242 next=0.1231 ...
Renormalise என்ன செய்கிறது என்று பாருங்கள்: k=3-ல் Monday
0.4967-லிருந்து 0.5896-க்குப் போகிறது. தப்பியவை, அழிக்கப்பட்ட mass-ஐ
ஏற்கனவே தங்களிடம் இருந்ததற்கு விகிதாசாரமாக உறிஞ்சிக்கொள்கின்றன. எதுவும்
"உயர்த்தப்படவில்லை"; வகுத்தி சுருங்கியது.
இப்போது இதே k-ஐ இரண்டாவது step-ல் — அங்கு அடுத்து என்ன வரும் என்று model-க்கு உண்மையிலேயே தெரியவில்லை — 2.77 உச்ச வரம்புக்கு எதிராக 2.7374 nats entropy கொண்ட ஒரு தட்டையான distribution:
k=3 confident step keeps 0.8424 | flat step keeps 0.2616 (gap 0.5807)
k=5 confident step keeps 0.9468 | flat step keeps 0.4117 (gap 0.5351)
k=10 confident step keeps 0.9952 | flat step keeps 0.7275 (gap 0.2678)
ஒரே k=3 அமைப்பு, முற்றிலும் வேறான இரு நடத்தைகள். நம்பிக்கையான
step-ல் model நம்புவதில் 84%-ஐ வைத்துக்கொள்கிறது — நியாயம். தட்டையான
step-ல் 26% மட்டும் வைத்துக்கொண்டு, model உண்மையிலேயே திறந்தது என்று
சொன்ன distribution-ன் முக்கால் பங்கைத் தூக்கி எறிகிறது. தட்டையான step-ஐச்
சரிசெய்ய k-ஐ ஏற்றினால், நம்பிக்கையான step-க்குள் குப்பையை அனுமதித்துவிட்டீர்கள்.
இரண்டுக்கும் சரியான k மதிப்பு எதுவும் இல்லை, ஒரு நிஜமான generation ஒரே
வாக்கியத்தில் இரு வகை step-களையும் செய்கிறது.
k oru fixed count dhaan, distribution-oda shape-a paakave paakadhu. Same k=3 confident step-la 84% mass keep pannudhu, flat step-la 26% dhaan. Rendukkum sariyaana k value-e kidaiyaadhu.
Top-p: நிலையான mass, எனவே தொகுப்பின் அளவு தானாக மாறும்
Nucleus sampling வேறு ஒரு கேள்வியைக் கேட்கிறது. "எத்தனை token?" அல்ல, "model-ன் நம்பிக்கையில் எவ்வளவு?" என்று. வரிசைப்படுத்து, ஓடும் கூட்டுத்தொகை எடு, p-ஐத் தாண்டும்போது நிறுத்து. தப்பிக்கும் தொகுப்பின் அளவை distribution தீர்மானிக்கிறது, நீங்கள் அல்ல. அதே இரு step, அதே script:
p confident step flat step
0.5 2 tokens, mass 0.7199 7 tokens, mass 0.5476
0.8 3 tokens, mass 0.8424 12 tokens, mass 0.8333
0.9 4 tokens, mass 0.9166 14 tokens, mass 0.9253
0.95 6 tokens, mass 0.9671 15 tokens, mass 0.9654
1.0 16 tokens, mass 1.0000 16 tokens, mass 1.0000
அந்த p=0.9 வரிசைதான் paper. ஒரே அமைப்பு: model நம்பிக்கையாக இருக்கும் இடத்தில் நான்கு token, இல்லாத இடத்தில் பதினான்கு. Distribution தட்டையானதால் nucleus வளர்ந்தது — யாரும் எதையும் மாற்றவில்லை.
at p=0.9 the nucleus is 4 tokens on the confident step and 14 on the flat one.
confident nucleus: Monday tomorrow next the
flat nucleus: Monday tomorrow next the March later noon sometime ...
k=3 data-ஐப் பார்ப்பதற்கு முன்பே தேர்ந்தெடுத்த
இடத்தில் வெட்டுகிறது. p=0.9 இங்கே ஒரு bar தாமதமாகவும்,
தட்டையான step-ல் பத்து bar தாமதமாகவும் வெட்டுகிறது. இரு crop-ம் பிறகு
renormalise செய்வதால், மூன்று பலகங்களும் கூட்டினால் 1 ஆகின்றன.
asdf போன்ற token-களில் 0.0111 mass-ஐ விட்டுவைக்கிறது —
ஏறக்குறைய ஆறாயிரம் token-க்கு ஒரு முறை எட்டக்கூடியது. 50k vocab-ல் அந்த
வால்தான் உண்மையிலேயே விநோதமான output-கள் வரும் இடம். வெட்டாமல் sample
செய்தால் இந்த வால் விகிதாசாரத்துக்கு மீறி எட்டக்கூடியதாகிறது என்பதுதான்
Holtzman-ன் வாதம்.
வரிசை என்பது வரையறையின் ஒரு பகுதி
Temperature வடிவத்தை மாற்றுகிறது, truncation வெட்டுகிறது — எனவே அவற்றை மறுவரிசையில் ஓட்டினால் வேறு distribution கிடைக்கிறது. இது ஒரு rounding வித்தியாசம் அல்ல, எட்டக்கூடிய token-களின் தொகுப்பே வேறு. என் நிலையான vector-ல் T=1.5, p=0.9-ல்:
T=1.5, p=0.9
temp-then-p keeps 7 tokens: Monday tomorrow next the March later noon
p-then-temp keeps 4 tokens: Monday tomorrow next the
same surviving set? False
Monday temp-then-p 0.3809 p-then-temp 0.4422 diff -0.0613
tomorrow temp-then-p 0.2234 p-then-temp 0.2594 diff -0.0359
next temp-then-p 0.1498 p-then-temp 0.1739 diff -0.0241
the temp-then-p 0.1073 p-then-temp 0.1246 diff -0.0173
March temp-then-p 0.0589 p-then-temp 0.0000 diff +0.0589
max abs difference: 0.0613
March ஒரு வரிசையில் எட்டக்கூடியது, மற்றொன்றில் சாத்தியமே
இல்லை. நிஜமான implementation-கள் — Hugging Face-ன் logits processors, vLLM,
hosted API-கள் — temperature, பிறகு top-k, பிறகு top-p
என்றே செய்கின்றன. Temperature-ஐ ஏற்றினால் distribution முதலில்
தட்டையாகிறது, அதனால் nucleus வளர்கிறது — dial-ஐ ஏற்றும்போது
நீங்கள் எதிர்பார்த்தது அதுதான். தவறான வரிசையில் nucleus T=1-ல்
உறைந்துவிடுகிறது, temperature அதற்குள் மட்டுமே மறுபங்கீடு செய்கிறது. அதே
இரு எண், அமைதியாக வேறான ஒரு product.
மற்றொரு வரிசைத் தவறு: வெட்டிய பிறகு அல்லாமல், வெட்டுவதற்கு முன் renormalise செய்வது. அது error தரவில்லை — sampler-க்கு கூட்டினால் 1 ஆகாத ஒரு vector-ஐக் கொடுக்கிறது, அவ்வளவுதான்:
and renormalising BEFORE truncating instead of after:
sum after cutting an already-normalised vector: 0.9807
np.random.choice raises: probabilities do not sum to 1
Order maathinaa vera distribution dhaan varum. Temp-then-p 7 token vidudhu, p-then-temp 4 dhaan — March oru order-la reachable, innoru order-la impossible. Adhaan ellaa inference server-um temp munnadi vekkudhu.
Greedy decoding உண்மையில் ஏன் தோல்வியடைகிறது
Degeneration-ஐப் பார்க்க உங்களுக்கு ஒரு language model தேவையில்லை. எட்டு வார்த்தைகள் மீதான ஒரு first-order Markov chain போதும்: ஒவ்வொரு step-லும் argmax எடுங்கள், ஏற்கனவே பார்த்த ஒரு state-க்குத் திரும்பிய நொடியில் நீங்கள் என்றென்றைக்குமான ஒரு சுழற்சியில் இருக்கிறீர்கள், ஏனெனில் argmax தீர்மானகரமானது. எதுவும் அதை உடைக்க முடியாது. இது ஒரு நிஜமான model-ல் வரும் repetition loop-ன் அதே இயங்குமுறை, சாக்குப்போக்குகள் இல்லாமல்.
greedy from 'the': the model is a model is a model is a model is ...
loop detected, period 3: model is a
greedy avg surprisal: 1.0670 bits/token
sampled (T=1.0, p=0.9): the model is the the data good model learns from data ...
loop detected? no -- it revisits states without getting stuck
sampled avg surprisal over 200 runs: 2.0463 bits/token
greedy is 0.9792 bits/token LESS surprising
-- and unreadable. maximising likelihood is not the goal.
அந்த இரண்டு surprisal எண்களை மீண்டும் படியுங்கள். Surprisal என்பது model-ன்
கீழ் −log₂ P(token) — model சிறியதாக்கப் பயிற்றுவிக்கப்படும்
எண். Greedy அதில் வெல்கிறது, token-க்கு ஒரு முழு bit-ஆல், மேலும் "model
is a model is a model is a" என்று தருகிறது. Sample செய்யப்பட்ட text
model-க்கு இரு மடங்கு ஆச்சரியமானது, ஆனால் நீங்கள் ship செய்வது அதுதான்.
இதை வேண்டுமென்றே மீண்டும் நிகழ்த்தும் parameter: top_k=1.
சரியாக ஒரு token-ஐ வைத்துக்கொள்வது என்றால், renormalise செய்யப்பட்ட
distribution argmax-ல் 1.0 — எனவே பகடை உருட்டல் ஒரு சம்பிரதாயம்.
top-k with k=1 is greedy wearing a hat: identical to the greedy walk
k=1 walk: the model is a model is a model is a model is ...
मैंने config में temperature=0.7, top_p=0.9 लिखकर एक LLM feature
लगभग आठ महीने production में चलाया — और तब तक मैं आपको बता नहीं सकता था कि
दूसरा parameter करता क्या है। मुझे बस लोककथाएँ पता थीं — "temperature कम रखो
तो जवाब ज़्यादा सही आता है", "top_p भी temperature जैसा ही है, बस अलग" — और वे
कथाएँ ऐसी जगह ग़लत हैं जहाँ फ़र्क़ पड़ता है। मेरे दिमाग़ में एक ही डिब्बे पर तीन
dial थे। ऐसा नहीं है। यह एक contrast slider और एक crop tool है, इसी क्रम में —
और क्रम दिख जाने के बाद सारी उलझनें अपने आप सुलझ जाती हैं।
Temperature contrast है। Top-k और top-p crop हैं।
हर step पर model आपको एक ही array देता है: vocabulary की हर entry के लिए एक score। मेरी खिलौना vocab में सोलह entry, असली में पचास हज़ार। बस इतना ही। ये तीनों parameter सिर्फ़ उसी array को छूते हैं।
logits = model(prompt) # हर vocab entry के लिए एक float
probs = softmax(logits) # प्रतिशत, जिनका जोड़ 1
token = draw_one(probs) # भारित पासा फेंक
Temperature line 1 पर घुसता है — यह softmax से पहले logits को भाग
देता है। Top-k और top-p line 3 पर आते हैं — वे probs से entry
मिटाते हैं और बची हुई को renormalise करते हैं। अलग stage, अलग तरह का operation:
logits = model(prompt) / T # CONTRAST: फ़ासले खिंचें/सिकुड़ें
probs = softmax(logits)
probs = keep_top_k(probs, k) # CROP: तय गिनती बचती है
probs = keep_top_p(probs, p) # CROP: तय mass बचता है
probs = probs / probs.sum() # बची हुई का जोड़ 1 होना चाहिए
token = draw_one(probs)
| Knob | असल में यह क्या है | Programmer वाली उपमा |
|---|---|---|
| temperature | Logits को भाग देता है। क्रम वही रहता है; सिर्फ़ फ़ासले बदलते हैं। | Contrast slider। वही तस्वीर, रंगों के बीच कम या ज़्यादा फ़र्क़। |
| top-k | सबसे ऊपर की k entry रखो। एक तय गिनती। | LIMIT 40। row 41 कितनी अच्छी थी, इससे बेख़बर। |
| top-p | सबसे छोटा वह समूह रखो जिसकी probability का जोड़ p तक पहुँचे। एक तय mass। | Rows से नहीं, bytes से pagination। गिनती data से तय होती है। |
top_k और top_p जो करते हैं वह सब नीचे। वही
एक सीमा पूरी उलझन की जड़ है।
Holtzman का paper ही क्यों मायने रखता है
2019 से पहले मान्यता यह थी कि बेहतर decoding का मतलब है सबसे ज़्यादा probability वाले sequence के क़रीब पहुँचना: beam search, चौड़े beam, और अंत में greedy। The Curious Case of Neural Text Degeneration वही paper है जिसने कहा कि लक्ष्य ही ग़लत है। Likelihood को अधिकतम करने पर text दोहराने लगता है और फिर अटक जाता है — वही clause बार-बार। लेखकों की मूल टिप्पणी यह है कि इंसान का लिखा text model के probability distribution की चोटी पर होता ही नहीं। वह एक कम-probability वाली पट्टी में घूमता है, जहाँ greedy decoding कभी जाता ही नहीं।
तो हल कोई बेहतर search नहीं है। हल है sample करना, पर एक काटे गए distribution से — और काटना गिनती से नहीं, mass से, क्योंकि सही गिनती हर step पर बदलती है। यही nucleus sampling है। Top-k, जो एक साल पहले Fan et al. के story-generation paper से आया, वही चीज़ है जिसे यह सुधारता है।
वह एक सूत्र
इसे pipeline के चरणों की तरह पढ़िए:
| Step | Shape | यह क्या करता है |
|---|---|---|
logits / T | (V,) | Temperature। कच्चे scores पर, सबसे पहले। |
softmax | (V,) | प्रतिशत जिनका जोड़ 1। यही सीमा है। |
argsort(−p) | (V,) | सबसे संभावित पहले। दोनों crop को क्रम चाहिए। |
cumsum | (V,) | चलता जोड़। जहाँ यह p पार करे, वहाँ रुको। |
keep, zero rest | (V,) | Nucleus। इसके बाहर सब कुछ अपहुँच है। |
/ sum | (V,) | Renormalise। काटने के बाद, कभी पहले नहीं। |
Temperature कभी क्रम नहीं बदलता
हर logit को एक ही धनात्मक संख्या से भाग देना monotonic है। जो token पहले सबसे संभावित था वही बाद में भी रहेगा। Temperature किसी शब्द को पहले स्थान पर नहीं चढ़ा सकता; वह सिर्फ़ यह तय करता है कि पहले स्थान की बढ़त कितनी बड़ी हो। इसीलिए "अलग जवाब चाहिए तो temperature बढ़ा दो" सिर्फ़ पासे के साथ मिलकर काम करता है — आपने पासे के लिए फ़ासला चौड़ा किया, क्रम नहीं बदला।
मेरी बात पर मत जाइए। यह रहा असली output:
T=0.2 H=0.0980 Monday=0.9811 tomorrow=0.0180 next=0.0009 the=0.0001
T=0.5 H=0.7347 Monday=0.7743 tomorrow=0.1563 next=0.0471 the=0.0173
T=0.8 H=1.2322 Monday=0.5862 tomorrow=0.2156 next=0.1019 the=0.0545
T=1.0 H=1.4843 Monday=0.4967 tomorrow=0.2232 next=0.1225 the=0.0743
T=1.5 H=1.9354 Monday=0.3525 tomorrow=0.2068 next=0.1386 the=0.0993
T=2.0 H=2.2101 Monday=0.2717 tomorrow=0.1821 next=0.1349 the=0.1051
ranking is IDENTICAL at every temperature:
all six orderings equal? True
only the GAPS changed. temperature cannot promote a token.
H nats में entropy है — distribution कितना अनिर्णीत है।
T=0.2 पर यह 0.098 है, यानी "मैं तय कर चुका हूँ"। T=2.0 पर यह 2.21 है, जबकि
16-token vocabulary की छत ln 16 ≈ 2.77 है — यानी लगभग "कुछ पता नहीं"। वही
model, वही prompt, वही क्रम।
T = 0 वाला जाल
temperature=0 हर API doc में है और वह temperature है ही नहीं।
शून्य से भाग देने पर infinity मिलती है, फिर softmax से nan, फिर
sampler में crash। हर असली implementation इसके इर्द-गिर्द branch करता है:
T = 0 exactly:
raises: T=0: real code branches to argmax instead
raw numpy would give: [6.0e+12 5.2e+12 4.6e+12] ... then inf/nan
what real code does instead: argmax -> 'Monday' ✓
तो request body में "temperature 0" का मतलब है "यह पूरा पन्ना छोड़ो और argmax ले लो"। यह संख्या के कपड़े पहने एक special case है। यहाँ interview सवाल भाग देना नहीं है; सवाल यह है कि आपको पता है या नहीं कि T = 0 का मतलब greedy है, और greedy का मतलब वह failure है जो आख़िरी हिस्से में है।
Top-k: तय गिनती, आकार से बेख़बर
सबसे ज़्यादा probability वाले k token रखो, बाक़ी को शून्य करो, renormalise
करो। मेरे आश्वस्त step पर — "the meeting is scheduled for ___", जहाँ
Monday आधा mass ले लेता है — छोटा k ठीक चलता है:
k=1 keeps 0.4967 of the mass | Monday=1.0000
k=3 keeps 0.8424 of the mass | Monday=0.5896 tomorrow=0.2649 next=0.1454
k=5 keeps 0.9468 of the mass | Monday=0.5246 tomorrow=0.2357 next=0.1294 ...
k=10 keeps 0.9952 of the mass | Monday=0.4991 tomorrow=0.2242 next=0.1231 ...
ध्यान दीजिए renormalise क्या करता है: k=3 पर Monday 0.4967 से
0.5896 हो जाता है। बचे हुए token मिटाए गए mass को उसी अनुपात में सोख लेते
हैं जो उनके पास पहले से था। किसी को "बढ़ावा" नहीं मिला; भाजक छोटा हो गया।
अब वही k एक दूसरे step पर, जहाँ model को सचमुच नहीं पता कि आगे क्या आएगा — उस 2.77 की छत के मुक़ाबले 2.7374 nats entropy वाला एक सपाट distribution:
k=3 confident step keeps 0.8424 | flat step keeps 0.2616 (gap 0.5807)
k=5 confident step keeps 0.9468 | flat step keeps 0.4117 (gap 0.5351)
k=10 confident step keeps 0.9952 | flat step keeps 0.7275 (gap 0.2678)
एक ही k=3 setting, दो बिल्कुल अलग बर्ताव। आश्वस्त step पर वह
model की 84% मान्यता रखता है — ठीक है। सपाट step पर वह 26% रखता है और उस
distribution का तीन-चौथाई फेंक देता है जिसे model सचमुच खुला बता रहा था।
सपाट step ठीक करने के लिए k बढ़ाइए, तो आपने आश्वस्त step में कचरा घुसा
दिया। ऐसा कोई k नहीं है जो दोनों के लिए सही हो, और असली generation एक ही
वाक्य में दोनों तरह के step करता है।
Top-p: तय mass, इसलिए समूह का आकार ख़ुद ढल जाता है
Nucleus sampling एक अलग सवाल पूछता है। "कितने token?" नहीं, बल्कि "model की मान्यता का कितना हिस्सा?"। क्रम में लगाओ, चलता जोड़ लो, p पार होते ही रुक जाओ। बचे समूह का आकार distribution तय करता है, आप नहीं। वही दो step, वही script:
p confident step flat step
0.5 2 tokens, mass 0.7199 7 tokens, mass 0.5476
0.8 3 tokens, mass 0.8424 12 tokens, mass 0.8333
0.9 4 tokens, mass 0.9166 14 tokens, mass 0.9253
0.95 6 tokens, mass 0.9671 15 tokens, mass 0.9654
1.0 16 tokens, mass 1.0000 16 tokens, mass 1.0000
वह p=0.9 वाली पंक्ति ही पूरा paper है। एक ही setting: जहाँ model आश्वस्त है वहाँ चार token, जहाँ नहीं है वहाँ चौदह। Nucleus इसलिए बढ़ा क्योंकि distribution सपाट हुआ — किसी ने कुछ बदला नहीं।
at p=0.9 the nucleus is 4 tokens on the confident step and 14 on the flat one.
confident nucleus: Monday tomorrow next the
flat nucleus: Monday tomorrow next the March later noon sometime ...
k=3
वहाँ काटता है जो जगह उसने data देखने से पहले ही चुन ली थी।
p=0.9 यहाँ एक bar बाद काटता है, और सपाट step पर दस bar बाद।
दोनों crop फिर renormalise करते हैं, इसलिए तीनों पैनलों का जोड़ 1 ही रहता है।
asdf जैसे token में 0.0111
mass छोड़ देता है — यानी लगभग हर छह हज़ार token में एक बार पहुँच में। 50k
vocab पर वही पूँछ है जहाँ से सचमुच अजीब output आते हैं। Holtzman का तर्क
ठीक यही है कि बिना काटे sample करने पर यह पूँछ अनुपात से कहीं ज़्यादा पहुँच
में आ जाती है।
क्रम परिभाषा का हिस्सा है
चूँकि temperature आकार बदलता है और truncation काटता है, इन्हें उल्टे क्रम में चलाने पर आपको एक अलग distribution मिलता है — rounding का फ़र्क़ नहीं, बल्कि पहुँच में आने वाले token का अलग समूह। मेरे तय vector पर T=1.5 और p=0.9 पर:
T=1.5, p=0.9
temp-then-p keeps 7 tokens: Monday tomorrow next the March later noon
p-then-temp keeps 4 tokens: Monday tomorrow next the
same surviving set? False
Monday temp-then-p 0.3809 p-then-temp 0.4422 diff -0.0613
tomorrow temp-then-p 0.2234 p-then-temp 0.2594 diff -0.0359
next temp-then-p 0.1498 p-then-temp 0.1739 diff -0.0241
the temp-then-p 0.1073 p-then-temp 0.1246 diff -0.0173
March temp-then-p 0.0589 p-then-temp 0.0000 diff +0.0589
max abs difference: 0.0613
March एक क्रम में पहुँच में है और दूसरे में असंभव। असली
implementation — Hugging Face के logits processors, vLLM, hosted APIs —
temperature, फिर top-k, फिर top-p करते हैं। Temperature
बढ़ाने से distribution पहले सपाट होता है, जिससे nucleus बढ़ता है —
dial ऊपर करते वक़्त आपने यही उम्मीद की थी। ग़लत क्रम में nucleus T=1 पर जम
जाता है और temperature सिर्फ़ उसके अंदर बाँटता रहता है। वही दो संख्याएँ,
चुपचाप अलग product।
दूसरी क्रम-ग़लती है काटने के बाद के बजाय काटने से पहले renormalise करना। यह error नहीं देता — बस sampler को एक ऐसा vector थमा देता है जिसका जोड़ 1 नहीं है:
and renormalising BEFORE truncating instead of after:
sum after cutting an already-normalised vector: 0.9807
np.random.choice raises: probabilities do not sum to 1
Greedy decoding असल में क्यों नाकाम होता है
Degeneration देखने के लिए आपको language model की ज़रूरत नहीं। आठ शब्दों पर एक first-order Markov chain काफ़ी है: हर step पर argmax लीजिए, और जिस पल आप किसी देखे हुए state पर लौटे, आप हमेशा के लिए एक चक्र में हैं, क्योंकि argmax निश्चयात्मक है। कोई चीज़ उसे तोड़ नहीं सकती। असली model की repetition loop का यही तंत्र है, बस बहाने कम हैं।
greedy from 'the': the model is a model is a model is a model is ...
loop detected, period 3: model is a
greedy avg surprisal: 1.0670 bits/token
sampled (T=1.0, p=0.9): the model is the the data good model learns from data ...
loop detected? no -- it revisits states without getting stuck
sampled avg surprisal over 200 runs: 2.0463 bits/token
greedy is 0.9792 bits/token LESS surprising
-- and unreadable. maximising likelihood is not the goal.
उन दो surprisal संख्याओं को फिर से पढ़िए। Surprisal model के अनुसार
−log₂ P(token) है — वही संख्या जिसे छोटा करने के लिए model
train होता है। Greedy उसमें जीतता है, प्रति token पूरे एक bit से, और देता है
"model is a model is a model is a"। Sample किया गया text model के
लिए दोगुना चौंकाने वाला है और वही है जिसे आप ship करेंगे।
और वह parameter जो इसे जानबूझकर दोहराता है: top_k=1। ठीक एक
token रखने का मतलब है कि renormalise किया गया distribution argmax पर 1.0 है,
इसलिए पासा फेंकना बस औपचारिकता है।
top-k with k=1 is greedy wearing a hat: identical to the greedy walk
k=1 walk: the model is a model is a model is a model is ...
The code
Pure NumPy. No torch, no model, no training — sampling is arithmetic over a probability vector, so the whole mechanism is six short functions and you can watch every number move. Every function under 20 lines.
Download the full script — 309 lines plus 6 runnable demos. Then:
python3 sampling_from_scratch.py
# ---------------------------------------------------------------------------
# STAGE 0 -- softmax. Turns a row of logits into percentages summing to 1.
#
# The `- max` is not maths, it's overflow protection: exp(1000) is inf,
# exp(1000-1000) is 1, and the ratio is unchanged.
# ---------------------------------------------------------------------------
def softmax(logits):
z = logits - np.max(logits)
e = np.exp(z)
return e / e.sum()
# ---------------------------------------------------------------------------
# STAGE 1 -- TEMPERATURE. It divides the LOGITS, before the softmax.
#
# T < 1 stretches the gaps apart -> sharper, closer to argmax.
# T > 1 squashes the gaps together -> flatter, closer to uniform.
# It is monotonic, so it can never reorder the vocabulary: the most likely
# token before is the most likely token after. It is a contrast slider.
#
# T = 0 is division by zero. Every real implementation special-cases it to
# mean "greedy" instead of computing it.
# ---------------------------------------------------------------------------
def apply_temperature(logits, T):
if T == 0:
raise ZeroDivisionError("T=0: real code branches to argmax instead")
return logits / T
# ---------------------------------------------------------------------------
# STAGE 2 -- TOP-K (Fan et al., 2018). Keep the k highest-probability tokens,
# throw the rest away, renormalise what is left back to sum 1.
#
# k is a fixed COUNT. It has no idea what shape the distribution is, so the
# probability mass it happens to capture swings wildly step to step.
# ---------------------------------------------------------------------------
def top_k(p, k):
keep = np.argsort(-p)[:k] # indices of the k largest
out = np.zeros_like(p)
out[keep] = p[keep]
return out / out.sum(), np.sort(keep) # RENORMALISE AFTER truncating
# ---------------------------------------------------------------------------
# STAGE 3 -- TOP-P / nucleus sampling (Holtzman et al., 2019). Keep the
# SMALLEST set whose cumulative probability reaches p, then renormalise.
#
# p is a fixed MASS, so the size of the set is decided by the distribution,
# not by you. Confident step -> a couple of tokens. Flat step -> dozens.
# That adaptivity is the whole paper.
# ---------------------------------------------------------------------------
def top_p(p, p_thresh):
order = np.argsort(-p) # most likely first
cum = np.cumsum(p[order])
n = int(np.searchsorted(cum, p_thresh) + 1) # include the crossing token
n = min(n, len(p))
keep = order[:n]
out = np.zeros_like(p)
out[keep] = p[keep]
return out / out.sum(), np.sort(keep)
# ---------------------------------------------------------------------------
# STAGE 4 -- the pipeline every inference server runs, in this order:
# logits -> temperature -> softmax -> top-k -> top-p -> renormalise -> draw
# Temperature acts on logits. Truncation acts on probabilities. Swap those
# two and you get a different distribution, not a rounding difference.
# ---------------------------------------------------------------------------
def sample_once(logits, T=1.0, k=None, p_thresh=None, rng=None):
rng = rng or np.random.default_rng(0)
probs = softmax(logits) if T == 0 else softmax(apply_temperature(logits, T))
if T == 0:
return int(np.argmax(logits)) # the special case, not a division
if k is not None:
probs, _ = top_k(probs, k)
if p_thresh is not None:
probs, _ = top_p(probs, p_thresh)
return int(rng.choice(len(probs), p=probs))
Now break it
This is the part that actually makes it stick. Reading won't do it. You debug for a living — reverse-engineering from broken behaviour is your native mode, and every one of these is a bug I have watched reach production.
| Break this | Watch for |
|---|---|
Delete the if T == 0 guard in apply_temperature and call it with 0.0 | logits/0 gives inf, softmax returns nan, rng.choice raises. What you wanted was np.argmax → Monday. |
Call walk(0, 60, "sample", k=1) | Byte-identical to the greedy walk: the model is a model is a…, loop of period 3. k=1 is greedy with extra steps. |
In demo_5, truncate first and apply temperature to the survivors | The nucleus shrinks from 7 tokens to 4. March goes from 0.0589 to 0.0000 — reachable in one order, impossible in the other. |
Set p_thresh=1.0 in top_p | Nothing is cut. 0.0111 of mass stays in the junk tail; asdf at 0.000167 is still drawable. |
In top_k, swap the last line to out / p.sum() — normalise by the pre-cut total | The returned vector sums to 0.9468 at k=5, not 1.0. NumPy raises "probabilities do not sum to 1"; a hand-rolled sampler would just under-sample the last token silently. |
In top_p, drop the + 1 in the searchsorted line | The token that crosses p is excluded, so p=0.5 on the confident step keeps 1 token, not 2 — silently greedy at every confident step. |
On the maths anxiety
There is almost none here, and that is worth saying plainly, because "nucleus sampling"
sounds like it needs measure theory. It needs: division, a sort, a running total, and a
comparison. If you can write sorted(rows, reverse=True) and stop a loop when
a counter crosses a threshold, you have implemented the paper.
Two symbols worth owning. Entropy is the average surprisal of a
distribution — one number for "how undecided is the model", in nats if you use
ln and bits if you use log₂. Its ceiling is ln V
for a V-token vocabulary, so on my 16 tokens 2.77 means uniform and 0 means certain.
Surprisal is the same idea for a single token, −log
P(token): the log turns a product of probabilities into a sum, which is the only
reason logs are in here at all.
What you cannot skip is the shape discipline: which array is logits and which is probabilities. Almost every decoding bug I've written was applying an operation on the wrong side of the softmax. That's not maths, that's types.
Where to go next
This page gave you one thing: the three parameters as two operations in a fixed order, in code you can break. It skipped beam search entirely, skipped every benchmark in the paper, and skipped the newer samplers. Here's where those live.
-
The Curious Case of Neural Text Degeneration
Read it for the diagnosis, not the algorithm — you already have the algorithm above. The plot of human text probability against beam-search text probability is the argument, and the section on why likelihood maximisation produces repetition is the part nobody paraphrases well.
-
Hierarchical Neural Story Generation
Top-k is a couple of paragraphs in a paper that is mostly about generating stories. Worth skimming for the context: it was introduced as a practical fix for sampling junk, a year before anyone framed the count-versus-mass problem this page is built around.
-
How to generate text
The same knobs on an actual GPT-2 instead of my toy vector, plus the beam search I skipped and the n-gram repetition penalty I didn't mention. Go here to see the outputs as English sentences rather than as numbers.
-
Decoding strategies in large language models
The comparison across greedy, beam, top-k and nucleus drawn as trees of branching token choices. This is the visual answer to "what is beam search actually doing", which is the one thing this page deliberately refused to explain.