Sampling and Temperature

Why the same prompt can give different answers. See how a model picks each token from its chances, and what temperature and top-p really change.

14 minLesson 5 of 6
On this page
  1. Picking from the chances
  2. Temperature: sharpen or flatten
  3. The code
  4. Top-p: cut off the long tail
  5. Choosing settings
  6. Common mistakes
  7. Try it yourself
  8. Check yourself
  9. What to practise next

Ask an assistant to suggest a name for your new bakery. Ask again in a fresh chat with the exact same words. You get a different name. Nothing broke, and the model did not change its mind. It rolled a weighted die, and the die landed somewhere else.

In What Is an LLM, the toy generator always took the most likely word. Most assistants do not. Here is how they pick instead, and what the settings called temperature and top-p do to the pick.

Picking from the chances

Each model call gives every possible next token a raw score. Softmax turns the scores into chances that add up to 1, as in the attention lesson. Then one token is chosen.

Always taking the top token is called greedy decoding. It is predictable, but it tends to produce flat, repetitive text, and it can get stuck repeating the same phrase. So most systems sample: they pick at random, but weighted by the chances. A token with chance 0.46 is picked about 46% of the time.

The usual way to sample: draw a random number r between 0 and 1, then walk through the tokens adding up their chances. The first token whose running total passes r is the pick. Each token owns a slice of the line from 0 to 1, and the slice is as wide as its chance.

Temperature: sharpen or flatten

Temperature is a single number that changes the chances before the pick. Divide every score by the temperature, then apply softmax as usual.

  • Below 1, the gaps between scores get bigger, so the top token gets even more likely. The text is safer and more predictable.
  • Exactly 1 leaves the model's chances as they are.
  • Above 1, the gaps shrink, so unlikely tokens get a bigger share. The text is more varied, and more likely to go wrong.
  • 0 is treated as greedy: always the top token. (You cannot divide by zero, so code handles it as a special case.)

Here is a model finishing "My favourite fruit is", with five candidate words.

Temperature and sampling for one tokenUse the arrows or ← → keys
scaled = each score / temperaturechances = softmax(scaled)r = random number in [0, 1)running = 0for each token: running += chance  if r < running: pick this token
  1. word
    0apple
    1mango
    2banana
    3kiwi
    4durian
    score
    02
    11.5
    21
    30
    4-1
    chance
    00.46
    10.28
    20.17
    30.06
    40.02
    temperature
    1

    1The model's raw scores. At temperature 1 the scores are used as they are, and softmax gives the chances (rounded).

  2. word
    0apple
    1mango
    2banana
    3kiwi
    4durian
    running
    00.46
    10.74
    20.91
    30.98
    41
    r
    0.62
    running
    0.46

    2Draw a random number: say r = 0.62. Start walking. After apple the running total is 0.46. 0.62 is not below 0.46, so apple's slice does not contain it. Keep going.

  3. word
    0apple
    1mango
    2banana
    3kiwi
    4durian
    running
    00.46
    10.74
    20.91
    30.98
    41
    r
    0.62
    running
    0.74
    pick
    mango

    3After mango the running total is 0.74. 0.62 is below 0.74, so the pick is mango, even though apple was more likely. Greedy decoding would have said apple every time.

  4. word
    0apple
    1mango
    2banana
    3kiwi
    4durian
    scaled
    04
    13
    22
    30
    4-2
    chance
    00.66
    10.24
    20.09
    30.01
    40
    temperature
    0.5

    4Now temperature 0.5. Dividing by 0.5 doubles every score: 4, 3, 2, 0, −2. The gaps double too, so apple jumps from 0.46 to 0.66 and durian all but disappears.

  5. word
    0apple
    1mango
    2banana
    3kiwi
    4durian
    scaled
    01
    10.75
    20.5
    30
    4-0.5
    chance
    00.34
    10.26
    20.2
    30.12
    40.07
    temperature
    2

    5Temperature 2 halves every score: 1, 0.75, 0.5, 0, −0.5. The gaps shrink, the chances (rounded) even out, and durian is now picked about 1 time in 13.

  6. T = 0.5
    00.66
    10.24
    20.09
    30.01
    40
    T = 1
    00.46
    10.28
    20.17
    30.06
    40.02
    T = 2
    00.34
    10.26
    20.2
    30.12
    40.07

    6The order never changes: apple is always first. Temperature only changes how far ahead of the others it is. Same scores, three very different amounts of variety.

1 / 6

The code

The random number is passed in as r so you can test the function. In real use you would pass Math.random() or random.random().

function chancesAt(scores, temperature) {
  if (temperature === 0) { // greedy: all the chance on the top score
    const top = scores.indexOf(Math.max(...scores));
    return scores.map((_, i) => (i === top ? 1 : 0));
  }
  const scaled = scores.map((s) => s / temperature); // the only change
  const biggest = Math.max(...scaled);
  const exps = scaled.map((s) => Math.exp(s - biggest));
  const total = exps.reduce((a, b) => a + b, 0);
  return exps.map((e) => e / total);
}
 
// r is a random number in [0, 1).
function sample(chances, r) {
  let running = 0;
  for (let i = 0; i < chances.length; i++) {
    running += chances[i];
    if (r < running) return i;
  }
  return chances.length - 1; // guard against rounding
}
 
const words = ["apple", "mango", "banana", "kiwi", "durian"];
const chances = chancesAt([2, 1.5, 1, 0, -1], 1);
words[sample(chances, 0.62)]; // "mango"
import math
 
def chances_at(scores, temperature):
    if temperature == 0:  # greedy: all the chance on the top score
        top = scores.index(max(scores))
        return [1.0 if i == top else 0.0 for i in range(len(scores))]
    scaled = [s / temperature for s in scores]  # the only change
    biggest = max(scaled)
    exps = [math.exp(s - biggest) for s in scaled]
    total = sum(exps)
    return [e / total for e in exps]
 
# r is a random number in [0, 1).
def sample(chances, r):
    running = 0.0
    for i, c in enumerate(chances):
        running += c
        if r < running:
            return i
    return len(chances) - 1  # guard against rounding
 
words = ["apple", "mango", "banana", "kiwi", "durian"]
chances = chances_at([2, 1.5, 1, 0, -1], 1)
words[sample(chances, 0.62)]  # "mango"

The cost is tiny next to running the model: one pass over the vocabulary to scale and normalise, and one pass to sample, so O(V) for a vocabulary of V tokens. The top-p filter below adds a sort, O(V log V), which is still tiny.

Top-p: cut off the long tail

A real vocabulary has tens of thousands of tokens or more. Most have tiny chances, but together that "long tail" adds up, and any one of them can be picked now and then, often derailing a sentence. Top-p sampling (also called nucleus sampling) cuts the tail off first.

Sort the tokens by chance and keep the most likely ones until their chances add up to at least p. Drop the rest, and rescale the kept ones so they sum to 1 again. With the fruit example at temperature 1 and p = 0.9: apple 0.46, plus mango 0.74, plus banana 0.91. That passes 0.9, so kiwi and durian are dropped, and the three survivors become about 0.51, 0.31 and 0.19.

Top-p filter
function topP(chances, p) {
  const order = chances.map((_, i) => i).sort((a, b) => chances[b] - chances[a]);
  const kept = new Set();
  let running = 0;
  for (const i of order) { // keep the likeliest until they cover p
    kept.add(i);
    running += chances[i];
    if (running >= p) break;
  }
  return chances.map((c, i) => (kept.has(i) ? c / running : 0));
}
def top_p(chances, p):
    order = sorted(range(len(chances)), key=lambda i: -chances[i])
    kept, running = set(), 0.0
    for i in order:  # keep the likeliest until they cover p
        kept.add(i)
        running += chances[i]
        if running >= p:
            break
    return [c / running if i in kept else 0.0 for i, c in enumerate(chances)]

A simpler cousin, top-k, keeps a fixed number of tokens, say the top 40. Top-p adapts: when the model is sure, it may keep only one or two tokens; when many continuations are reasonable, it keeps more. Systems often combine temperature with one of these filters.

Choosing settings

Low temperature suits tasks with one right answer: extracting fields from a form, classifying a ticket, writing code to a spec. Moderate temperature suits open-ended writing and brainstorming, where several different good answers are worth seeing. Very high temperatures rarely help: past a point, the extra variety is mostly mistakes. Many apps pick these values for you and do not expose them at all. Where you can set them, check the documentation: the allowed range and the default differ between providers, so the same number does not mean exactly the same thing everywhere.

Common mistakes

Try it yourself

Try it: Pick the settings

For each task, choose a low temperature (near 0) or a moderate one (around 0.7 to 1), and say why in one line.

Show a sample answer
  1. Low. There is one right answer per invoice. Any randomness can only add errors.
  2. Moderate. You want variety. At near 0, many names would be close variations of the single most likely one.
  3. Low. The output must follow a strict format; an unlikely token can break the JSON.
  4. Moderate. "Different" is the whole point. Also run it several times: sampling gives you new options for free.

Rule of thumb: if there is one correct output, go low. If you want options to choose from, go moderate and generate several.

Check yourself

Quick check

Chances are [0.5, 0.3, 0.2] and the random number is r = 0.85. Which token is picked?

What happens to the chances when temperature goes from 1 to 2?

With top-p = 0.8, chances [0.6, 0.25, 0.1, 0.05]. Which tokens are kept?

What to practise next

Write the temperature step yourself in Storyteller Word Odds. Then continue with Why Models Hallucinate, which uses everything in this module to explain why a model can be fluent and wrong at the same time.