Back to the map
Language ModelsFoundation9h

Sampling & decoding

Temperature, top-p, and why zero is not deterministic.

A model never picks the next token outright. It produces a probability for every token it could say next, and something has to choose one from that spread. Sampling is that choice, and temperature and top-p are the dials on it: low settings make the model take the likeliest option almost every time, higher settings let less likely ones through, which reads as more variety and more invention.

Set it high on a task with one correct answer and the model drifts into plausible detail it made up. Set it to zero and assume you will get the same answer twice and you will be wrong — hosted APIs batch work across GPUs in ways that make exact repeats impossible, so tests that assert on an exact string fail now and then for reasons that look like nothing.

It is a setting on every messages API call, so it applies to everything above: prompting, tool calling, agents. Structured output works on the same machinery from the other end, by forbidding tokens that would break the format rather than reweighting them. Evaluation is how you find the right value for a given task instead of guessing.

Temperature only changes anything where the model was already unsure, and zero is not a promise of the same answer twice.

In more detailTemperature rescales the probability distribution before sampling; top-p and top-k truncate it. Temperature can only create variety where the model was genuinely uncertain, on a heavily peaked distribution, even a high temperature changes nothing. Temperature zero is also not reproducible on hosted APIs, because batched GPU inference, expert routing and differing hardware all introduce non-determinism.

Learn first

Section 1 of 3, 3h

How a model picks the next token

By the end: You can choose sampling settings on purpose and explain what each one does.

  • The probability distribution over the vocabulary
  • Temperature, top-p and top-k
  • Which to change and which to leave alone
  • Why temperature zero is not reproducible

A model does not decide on an answer. At each step it produces a probability for every token it could write next, and something has to pick one of them. That picking is sampling, and the settings you have seen copied into every code example, temperature, top-p and top-k, are all ways of reshaping those probabilities before the draw. This section is about what each one actually does, and why even the most deterministic setting on offer will not guarantee the same output twice.

Every step is a choice#

At each step the model produces a probability for every token in its vocabulary. Sampling is how one of them gets picked. All the settings you can change are ways of reshaping that distribution before the draw.

After "The capital of France is":

  " Paris"    0.94
  " located"  0.02
  " a"        0.01
  ...everything else shares the remaining 0.03

Temperature#

Temperature divides the raw scores, the logits, before they are turned into probabilities. Below 1 the peaks get sharper and the likely token becomes almost certain. Above 1 the distribution flattens and unlikely tokens get a real chance.

Original:    Paris 0.94   located 0.02   a 0.01

t = 0.2:     Paris 0.99+  everything else vanishes
t = 1.0:     unchanged
t = 1.8:     Paris 0.60   others rise substantially

It cannot invent uncertainty

If the model is confident, no temperature setting will change the answer much. Asking for one colour gives blue at temperature 0 and at temperature 2. High temperature only creates variety where the model was already unsure, which is why it is not a creativity dial.

top-p and top-k#

These truncate rather than rescale. top-k keeps the k most likely tokens and discards the rest. top-p, also called nucleus sampling, keeps the smallest set of tokens whose probabilities add up to p.

top_p = 0.9   keep tokens until their probabilities sum to 0.9,
              then sample among those

Adaptive: when the model is confident, that is one or two tokens.
When it is unsure, it is many.

In practice, change one of them. Tuning temperature and top-p together makes the effect of each hard to reason about. Most people leave top-p at its default and adjust temperature.

Choosing#

  • Extraction, classification, anything parsed, anything evaluated: 0 to 0.3.
  • General assistant output: around 0.7, which is usually the default.
  • Creative writing and brainstorming: 0.8 to 1.0.
  • Above roughly 1.5: degrades quickly and rarely useful.

Zero is not deterministic#

Temperature 0 means always take the most likely token — greedy decoding — which sounds reproducible. On a hosted API it is not. Requests are batched onto GPUs, floating point addition is not exactly associative so the order of a batched reduction changes the last bits, mixture-of-experts routing can depend on batch composition, and different requests land on different hardware.

"Name any animal", temperature 0, five runs:

  Elephant, Lion, Lion, Lion, Elephant

Same prompt, same setting, different answers.
Measured, not theoretical. This is why exact-match eval assertions flake.

Where people go wrong

Assuming a higher temperature makes the model more creative. It makes sampling less predictable. On a prompt with one obvious answer it changes nothing, and on an open one it mostly adds noise.

Build this: Run one prompt ten times at temperature 0 and ten at 1.5, and count distinct outputs. Then do it with a prompt that has one overwhelmingly likely answer and note that temperature barely moves it.

Open this section on its own page
Section 2 of 3, 3h

Sampling settings for real tasks

By the end: You can pick settings for a task, and use multiple samples on purpose rather than by accident.

  • Defaults by kind of task
  • max_tokens is a safety limit, not a length control
  • Stop sequences
  • Sampling several answers and voting
  • Best of n with a checker
  • Log probabilities, where available

Temperature and top-p have sensible defaults for each kind of task, and this section starts there. The larger part is what people get wrong around them. max_tokens does not make a model brief; it cuts it off, often mid-sentence or mid-JSON, which is why you check the stop reason on every response. Stop sequences end generation at a marker and save you paying for text you would throw away. And sampling several answers deliberately — voting between them, or generating a few and picking with a checker — is a real quality lever rather than an accident of a high temperature.

Where beam search went

Beam search keeps several candidate continuations alive at once and returns the highest-scoring whole sequence. It is still standard in translation and speech recognition, where there is one correct output to find. It is not used for chat, because optimising for the most probable sequence produces bland, repetitive text: the safest next word at every step is rarely the interesting one. If you are asked why sampling replaced it, that is the answer.

A starting point by task#

Task                              Start with
Extraction, classification        low temperature, one sample
Code, SQL                         low temperature, check by running it
Drafting, brainstorming           default or higher temperature
Hard reasoning with one answer    several samples, then vote
Starting points to measure against, not rules. Leave top-p alone unless you have a reason.

max_tokens cuts, it does not shorten#

The model does not know about your max_tokens. It writes as it normally would and is cut off when the limit is reached, often mid-sentence or mid-JSON. Use it as a cost and safety ceiling, check the stop reason on every response, and ask for brevity in the prompt if you want short answers.

Stop sequences#

A stop sequence ends generation as soon as the model produces a given string. It is useful when output has a natural end marker, such as a closing tag, and saves paying for text you would throw away.

Several samples, then vote#

For a question with one right answer, such as a number or a category, you can sample several answers at a normal temperature and take the most common one. Independent mistakes rarely agree, so the majority is more often right than a single sample. The technique is known as self-consistency. It multiplies cost by the number of samples, so use it where accuracy is worth that.

from collections import Counter

answers = [ask(question, temperature=0.8) for _ in range(5)]
best, votes = Counter(answers).most_common(1)[0]
if votes < 3:
    flag_for_review(question, answers)   # disagreement is itself a signal

Best of n, with a checker#

When you can check an answer, generate several and keep one that passes. Code that must compile and pass tests, SQL that must run and return the expected columns. The checker does the hard work and the samples just give it more to choose from.

Log probabilities#

Some APIs can return the probability the model assigned to each token. For classification with short labels, that gives a rough confidence you can threshold. Treat it with care: these probabilities are often overconfident, so calibrate against labelled data before relying on them.

About seeds

Some providers accept a seed to make sampling repeatable and describe it as best effort. It reduces variation, but do not build anything that breaks if two calls differ.

Where people go wrong

Lowering max_tokens to get shorter answers. You get the same answers, cut off.

Build this: Pick twenty questions with numeric answers. Compare one sample at low temperature with a five-sample majority vote on accuracy and cost, and note how often a split vote marked a wrong answer.

Open this section on its own page
Section 3 of 3, 3h

Sampling from scratch in NumPy

By the end: You can implement greedy, temperature, top-k and nucleus sampling over a logits vector and explain, with numbers, why top-p adapts to the model's confidence where top-k cannot.

  • Greedy decoding as argmax
  • Temperature as a division before the softmax
  • Top-k truncation with argpartition
  • Nucleus sampling with a sorted cumulative sum
  • Why a fixed k is wrong on both a confident and an uncertain step
  • The order the dials are applied in

You already know what temperature and top-p do to the output of an API. This section writes them, because the from-scratch version is a common warm-up question and because implementing nucleus sampling is the shortest way to understand the argument for it. Everything here operates on one vector of logits — the raw scores the model produces for every token in the vocabulary, before anything turns them into probabilities.

The softmax and greedy decoding#

Logits are unnormalised and can be any real number. The softmax turns them into a distribution; greedy decoding skips it entirely, because the largest logit is the largest probability and argmax does not care about the monotonic transformation in between.

import numpy as np


def softmax(logits):
    z = logits - logits.max()        # stability: the largest exponent becomes exp(0)
    e = np.exp(z)
    return e / e.sum()


def greedy(logits):
    return int(np.argmax(logits))    # no softmax needed: argmax is unchanged by it

Temperature#

Temperature divides the logits before the softmax. Below one, the gaps between logits widen and the distribution sharpens; above one they narrow and it flattens. In the limit as temperature goes to zero the distribution becomes one-hot on the argmax, which is why temperature zero and greedy are the same operation — and why a real implementation special-cases it rather than dividing by zero.

def sample_temperature(logits, temperature, rng):
    if temperature == 0.0:
        return greedy(logits)        # the limit, not a division
    p = softmax(logits / temperature)
    return int(rng.choice(len(p), p=p))

Note what temperature cannot do. It rescales the gaps between logits, so where one token is already far ahead of the rest, even a high temperature leaves it far ahead. Variety appears only where the model was genuinely undecided.

Top-k#

Top-k keeps the k highest-scoring tokens, renormalises over them and samples. The point is to cut off the long tail of tokens with tiny but nonzero probability, which collectively hold enough mass to be chosen more often than you would like over a few hundred steps. Use argpartition rather than a full sort: it finds the k largest in linear time without ordering them, and order does not matter here.

def sample_top_k(logits, k, temperature, rng):
    logits = logits / temperature
    # the k largest, unordered - cheaper than argsort over a 100k vocabulary
    keep = np.argpartition(logits, -k)[-k:]
    p = softmax(logits[keep])        # renormalise over the survivors only
    return int(rng.choice(keep, p=p))

Nucleus sampling#

Top-p keeps the smallest set of tokens whose probabilities add up to at least p, then renormalises over that set. The size of the set is not fixed: it is whatever the model's confidence makes it. Sort descending, take the cumulative sum, and cut at the first index where the total crosses the threshold — including that index, so the set always has at least one member even when the top token alone exceeds p.

def sample_top_p(logits, top_p, temperature, rng):
    logits = logits / temperature
    order = np.argsort(logits)[::-1]          # descending by probability
    p = softmax(logits[order])
    cum = np.cumsum(p)
    # first index where the running total reaches the threshold, kept inclusively
    cut = int(np.searchsorted(cum, top_p)) + 1
    keep, p = order[:cut], p[:cut]
    return int(rng.choice(keep, p=p / p.sum()))
The +1 is the whole correctness of the function. Without it, a step where one token holds more than p of the mass keeps nothing.

Why top-p adapts and top-k does not#

The argument is easier to run than to read. Take two logit vectors over a vocabulary of fifty: one peaked, where a single token holds 95% of the mass, and one flat, where the top token holds under 4%.

def nucleus_size(logits, top_p, temperature=1.0):
    p = np.sort(softmax(logits / temperature))[::-1]
    return int(np.searchsorted(np.cumsum(p), top_p)) + 1


rng = np.random.default_rng(0)

peaked = np.full(50, -6.0)
peaked[7], peaked[3] = 6.0, 3.0           # one clear winner, one runner-up
flat = rng.normal(size=50) * 0.4          # no token much ahead of any other

print(nucleus_size(peaked, 0.9))          # 1
print(nucleus_size(flat, 0.9))            # 42
Same threshold, same vocabulary. The nucleus is one token wide on the confident step and forty-two on the uncertain one.

Now set k to ten and ask what it does on each. On the peaked step it admits nine tokens the model had all but ruled out, one of which will eventually be drawn; that is the mechanism behind a model producing a fluent sentence with one word that does not belong. On the flat step it discards thirty-two tokens the model considered roughly as plausible as the ones it kept, which flattens variety precisely where variety was warranted.

with k = 10

peaked step:  top-k keeps 10  |  top-p(0.9) keeps  1
flat   step:  top-k keeps 10  |  top-p(0.9) keeps 42
Top-k is a constant where the right answer varies; top-p reads the answer off the distribution.

Temperature is applied before truncation, and it changes the nucleus

The usual order is temperature, then top-k, then top-p. Temperature cannot change which tokens are in a top-k set, because dividing by a positive number preserves order, but it does change the size of a nucleus: flattening the distribution spreads the mass, so more tokens are needed to reach p. On the flat row above, the nucleus at p=0.9 is 36 tokens at temperature 0.5, 42 at 1.0 and 44 at 2.0. Raising both dials compounds rather than adds.

Two checks worth running#

Sampling code is easy to get subtly wrong and hard to notice, because wrong output still looks like text. Both of these are cheap.

# temperature approaching zero must agree with greedy
assert all(sample_temperature(flat, 1e-6, rng) == greedy(flat) for _ in range(20))

# top-k with k = vocabulary size must reproduce the full distribution
draws = [sample_top_k(flat, 50, 1.0, rng) for _ in range(5000)]
empirical = np.bincount(draws, minlength=50) / 5000
assert np.abs(empirical - softmax(flat)).max() < 0.02
The second catches a renormalisation bug, which is otherwise invisible: the output is still plausible text, drawn from the wrong distribution.

Where people go wrong

Cutting the nucleus exclusively, at the first index where the cumulative sum reaches p rather than after it. On any step where the top token alone holds more than p of the probability mass — which is most steps in the middle of a common word — the kept set is empty, and depending on the implementation you get a crash, a uniform draw over nothing, or a silent fallback. The +1 is the function.

Build this: Implement all four samplers over a logits vector and check them. Confirm temperature 1e-6 agrees with greedy, and that top-k with k set to the vocabulary size reproduces the softmax distribution to within sampling error over five thousand draws. Then take a real model's logits at two positions — one mid-word, one at the start of a sentence — and print the nucleus size at p=0.9 for each.

Open this section on its own page

Where you use this

A topic is easier to care about when you can see what it is for. These projects lean on it at a named stage.

Build your own GPT
  • Stage 7, Make it write: What temperature and top-k are doing to the distribution you just produced.

Now actually do it

Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.

1Map the temperature curve for your task

3h

Run your actual task at temperature 0, 0.3, 0.7, 1.0 and 1.5, scoring each on your eval set. Plot quality against temperature.

Done when: You picked a temperature from data rather than from a tutorial.

3 hours of practice

Words used here

6 terms this topic teaches, each with a one-line definition if one of them stopped you.

Logits
The raw scores a model produces before they become probabilities — one per vocabulary entry, unbounded, and not meaningful on their own.
Temperature
A divisor applied to the logits before the softmax, setting how much probability mass concentrates on the top candidates. Below 1 sharpens towards the likeliest token; above 1 flattens and lets unlikely ones through.
Top-k sampling
Keeping only the k highest-probability tokens, renormalising across them, and sampling from what is left. A fixed-size cutoff, regardless of how confident the model was.
Top-p sampling
Keeping the smallest set of tokens whose probabilities add up to p, then sampling from that set. The cutoff moves with the model's confidence: a near-certain continuation leaves one candidate, an open-ended one leaves hundreds.
Greedy decoding
Always taking the highest-probability token. Repeatable, and prone to loops and flat prose on anything open-ended.
Beam search
Keeping several candidate continuations alive at once and expanding the most promising, instead of committing to one token at a time. It helps where there is a single right answer, such as translation, and produces bland text on open-ended writing.

Interview questions

8 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.

How a model picks the next token

What is the difference between temperature and top-p?

Temperature rescales the whole probability distribution, making peaks sharper or flatter. top-p truncates it, keeping the smallest set of tokens whose probabilities sum to p and sampling among those. Change one of them, not both, or the interaction becomes hard to reason about.

Is temperature 0 deterministic?

Not on a hosted API. Batched GPU execution, non-associative floating point addition, expert routing and varying hardware all introduce small differences. Locally, with a fixed seed and a single request, it can be.

You need consistent JSON output. What settings do you use?

Low temperature, around 0 to 0.2, and schema-constrained decoding if available, which makes structure a guarantee rather than a preference. Then check the stop reason, because truncation produces invalid JSON regardless of temperature.

Sampling settings for real tasks

What is self-consistency?

Sampling several answers to the same question and taking the most common one. Independent errors rarely agree, so the majority answer is more accurate than one sample. It costs one call per sample, so it suits high value questions with a single correct answer.

Then they push further

What does it mean when the samples disagree?

That the model is uncertain on this input. That is useful: route those cases to a person or a stronger model instead of trusting the majority of a split vote.

Why does it not help for open-ended writing?

There is no single answer to vote on. For writing you would need a judge or a person to choose between samples, which is a different technique with its own costs.

What gets marked up. Marked up for naming the cost and for using disagreement between samples as a confidence signal.

Your answers are sometimes cut off mid-sentence. Why?

The response hit max_tokens. The model does not plan around that limit, it is simply stopped. Check the stop reason, raise the limit or ask for shorter output in the prompt.

Sampling from scratch in NumPy

Implement top-p sampling given a vector of logits.

Divide the logits by the temperature, sort descending, softmax, take the cumulative sum, and find the first index where it reaches p. Keep everything up to and including that index, renormalise those probabilities so they sum to one, and draw from them. The inclusive cut matters: without it a step where the top token alone exceeds p keeps an empty set.

Then they push further

Why not just use a fixed k?

Because the right number of candidates depends on how confident the model is at that step, and that varies enormously. On a step where one token holds 95% of the mass, a nucleus at 0.9 is one token and k=10 admits nine the model had ruled out. On a genuinely open step the nucleus might be forty tokens and k=10 cuts off thirty the model thought equally plausible. Top-p reads the cut off the distribution instead of fixing it in advance.

Does temperature change which tokens survive top-k?

No, because dividing by a positive number is monotonic, so the ranking is unchanged and the same k tokens survive. It does change the probabilities they are then sampled with. It changes a top-p set, though: flattening the distribution spreads the mass, so more tokens are needed to reach the threshold.

What gets marked up. The inclusive cut and the renormalisation are what get checked. Sorting and cumulative-summing is the easy part; candidates who return an empty set on a confident step, or who forget to renormalise after truncating, are sampling from something that is not a distribution.

Why is greedy decoding the same as temperature zero?

Dividing the logits by a temperature approaching zero sends every gap between logits to infinity, so the softmax converges to one on the largest logit and zero everywhere else. Sampling from that distribution always returns the argmax. In code you special-case it rather than dividing by zero, but it is a limit and not an exception.

Why use argpartition rather than argsort for top-k?

Top-k needs the identity of the k largest, not their order, and argpartition finds them in time linear in the vocabulary rather than the n log n a full sort costs. On a vocabulary of a hundred thousand, executed for every token generated, that is a real difference. Nucleus sampling does need the sort, because it depends on the running total in descending order.

These questions on their own page, one link per question

Resources

Free to watch and read, picked for this topic.

Full courses: the whole topic, start to finish

Stop 10 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

Next on this routeStructured outputConstrain the shape; the values are still your problem.10h

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.