Beam search, and why chat models dropped it
Beam search explores several candidate sentences at once and keeps the most probable one, which is exactly right for translation and exactly wrong for open-ended chat.
- 14 min read
- 3 reading levels
- Updated
Read these first
On this page 7
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
Beam search keeps several possible sentences alive at the same time. It picks the most likely one at the end, rather than committing to the best word at every step.
Think about driving to an unfamiliar address. At the first junction you take the road that looks widest. Two turns later it narrows into a lane and you are stuck.
A more careful driver keeps three routes in mind. They commit only once it is clear which route actually arrives. Slower to plan, better at arriving.
Greedy decoding is the first driver. Beam search is the second.
What greedy decoding gets wrong
Greedy decoding takes the single best word at every step and never reconsiders.
That works until a word that looks good right now leads somewhere bad. The model cannot undo it. Everything afterwards is built on the wrong turn.
Beam search fixes this by carrying several partial sentences forward together. The number it carries is called the beam width. At each step it extends all of them, scores every result, and keeps the best few.
How it works
start
|
+-- "I" (55 out of 100) <- looks best right now
| +-- "am" -> "fine" ends up at 17 out of 100
|
+-- "The" (45 out of 100) <- looks worse right now
+-- "train" -> "is" ends up at 23 out of 100
greedy takes "I", because 55 beats 45, and lands on the worse sentence.
beam width 2 keeps both, and finds the better one at the end.The best first word did not lead to the best sentence. That gap is the entire reason beam search exists.
So why did chat models stop using it?
Because it works. That sounds like a joke and it is not.
Beam search finds high-probability text. High-probability text is safe, common, predictable text. It is the language equivalent of ordering plain rice at every restaurant because rice is never wrong.
Researchers measured this. Human writing is not the most probable thing a model could say. People take small risks constantly, and that unpredictability is what makes writing feel alive.
So beam search reliably produces answers that are correct, fluent, dull and repetitive. For a translation that is fine. For a conversation it is a downgrade.
There is a second reason, and it is practical. Carrying four candidate sentences means keeping four sets of the model's notes in memory. Streaming also becomes impossible. You cannot show a word until you know which candidate survived.
Where beam search is still the right answer
- Translation apps, where there is a correct output and dullness is not a flaw.
- Speech recognition, turning audio into the most likely words.
- Image captioning.
- Any task with one right answer and a scoring rule you trust.
Where you have already seen the trade
- Google Translate giving the same sensible translation every time.
- A voice typing tool that corrects an earlier word after you keep speaking.
- ChatGPT giving you a different answer to the same question, on purpose.
Remember this
- Beam search carries several candidate sentences and picks the most likely at the end.
- Greedy decoding can be trapped by a word that looks best right now.
- Chat models abandoned beam search because most-likely text is bland, and because it cannot stream.
What to learn next
- Constrained decoding and grammars — forcing a shape on the output without searching for it.
- Speculative decoding — running several candidate tokens at once, for speed rather than quality.
- Temperature and sampling — what chat models use instead.
Developer — Code and libraries.
Setup
# no libraries needed
python3 --versionA toy language model written as a dictionary, so every probability is visible and every result is checkable by hand.
Greedy, beam search, and the full search space
import math
# A hand-written toy language model: prefix -> next word -> probability.
LM = {
(): {"I": 0.55, "The": 0.45},
("I",): {"am": 0.34, "will": 0.33, "have": 0.33},
("I", "am"): {"fine": 0.90, "late": 0.10},
("I", "will"): {"go": 0.60, "wait": 0.40},
("I", "have"): {"time": 0.70, "doubts": 0.30},
("The",): {"train": 0.95, "rain": 0.05},
("The", "train"): {"is": 0.97, "left": 0.03},
("The", "rain"): {"stopped": 0.50, "started": 0.50},
("The", "train", "is"): {"</s>": 0.55, "late": 0.45},
("The", "train", "is", "late"): {"today": 0.95, "</s>": 0.05},
("The", "train", "is", "late", "today"): {"</s>": 1.0},
("I", "am", "fine"): {"</s>": 1.0},
("I", "am", "late"): {"</s>": 1.0},
("I", "will", "go"): {"</s>": 1.0},
("I", "will", "wait"): {"</s>": 1.0},
("I", "have", "time"): {"</s>": 1.0},
("I", "have", "doubts"): {"</s>": 1.0},
("The", "train", "left"): {"</s>": 1.0},
("The", "rain", "stopped"): {"</s>": 1.0},
("The", "rain", "started"): {"</s>": 1.0},
}
def greedy():
seq, score = (), 0.0
while True:
word, p = max(LM[seq].items(), key=lambda kv: kv[1])
score += math.log(p)
if word == "</s>":
return seq, score
seq += (word,)
def beam_search(width):
beams = [((), 0.0)] # (sequence so far, cumulative log-probability)
finished = []
while beams:
candidates = []
for seq, score in beams:
for word, p in LM[seq].items():
if word == "</s>":
finished.append((seq, score + math.log(p))) # stopping has a probability too
else:
candidates.append((seq + (word,), score + math.log(p)))
candidates.sort(key=lambda sc: -sc[1])
beams = candidates[:width] # keep only the best `width` live paths
finished.sort(key=lambda sc: -sc[1])
return finished
g_seq, g_score = greedy()
print(f"greedy : {' '.join(g_seq):<26} logprob {g_score:>7.3f} prob {math.exp(g_score):.4f}")
for width in (1, 2, 4):
seq, score = beam_search(width)[0]
print(f"beam k={width} : {' '.join(seq):<26} logprob {score:>7.3f} prob {math.exp(score):.4f}")
print("\nevery complete sentence this toy model can produce, best first:")
for seq, score in beam_search(20):
print(f" {' '.join(seq):<28}{score:>8.3f} {math.exp(score):.4f} {len(seq)} words")
print("\nlength penalty: divide the score by len**alpha, or short sentences always win")
for alpha in (0.0, 0.7, 1.0):
ranked = sorted(beam_search(20), key=lambda sc: -sc[1] / (len(sc[0]) ** alpha))
print(f" alpha {alpha}: '{' '.join(ranked[0][0])}'")greedy : I am fine logprob -1.782 prob 0.1683 beam k=1 : I am fine logprob -1.782 prob 0.1683 beam k=2 : The train is logprob -1.478 prob 0.2281 beam k=4 : The train is logprob -1.478 prob 0.2281 every complete sentence this toy model can produce, best first: The train is -1.478 0.2281 3 words The train is late today -1.730 0.1773 5 words I am fine -1.782 0.1683 3 words I have time -2.063 0.1270 3 words I will go -2.217 0.1089 3 words I will wait -2.623 0.0726 3 words I have doubts -2.910 0.0544 3 words I am late -3.979 0.0187 3 words The train left -4.356 0.0128 3 words The rain stopped -4.487 0.0112 3 words The rain started -4.487 0.0112 3 words The train is late -4.675 0.0093 4 words length penalty: divide the score by len**alpha, or short sentences always win alpha 0.0: 'The train is' alpha 0.7: 'The train is late today' alpha 1.0: 'The train is late today'
Reading the output
Beam width 1 is greedy. Identical sequence, identical score. That is the definition, and it is a useful sanity check on any beam implementation you write.
Width 2 already finds the better sentence. Greedy scored 0.1683, beam scored 0.2281 — thirty-five percent more probable. Greedy was seduced by "I" at 0.55 over "The" at 0.45. But "The" led to a near-certain continuation, while "I" led to a fork with three roughly equal options.
Width 4 found nothing extra. Returns diminish fast. In real systems the curve flattens between 4 and 10, and beyond that quality often gets worse, which is discussed below.
Log-probabilities, never probabilities. Multiplying forty numbers below one underflows to zero in float32. Adding their logarithms does not. Every real implementation sums logs, and any beam code you see multiplying probabilities is broken at realistic lengths.
</s> carries a probability, and it is easy to forget. The line finished.append((seq, score + math.log(p))) includes the probability of stopping there. Drop that term and short sequences get an unearned advantage, which is a common and subtle bug.
The length penalty flips the winner. Every extra token adds a negative number, so a longer sequence can never have a higher raw score than a prefix of itself. Dividing by length raised to alpha corrects for this. At alpha=0.0 the short sentence wins; at 0.7 the longer, more informative one does. This parameter is length_penalty in HuggingFace and it changes outputs a great deal.
The costs nobody mentions
Memory. Width k means k KV caches. On a long context that is k times the dominant memory cost of serving.
Compute. Width k means k sequences in every forward pass. Roughly k times the arithmetic per generated token.
No streaming. You cannot emit a token until you know which beam survives, and a beam can be dropped at any step. Users see nothing until the whole answer exists. For a chat product this alone is disqualifying.
Interaction with sampling. Beam search and top-p are answering different questions. Beam sample exists in transformers, and it is a rarely-useful hybrid: sampling inside a search for the mode.
When it is still the right call
out = model.generate(**inputs, num_beams=4, length_penalty=1.0,
early_stopping=True, do_sample=False)Use it for translation, speech recognition, image captioning, structured extraction with a scoring rule you trust — tasks with a right answer where dullness costs nothing. early_stopping=True halts when num_beams finished candidates exist, which is the usual and correct setting.
Do not use it for chat, creative writing, brainstorming, or anything where you would be unhappy to receive the same answer every time.
Common mistakes
Believing more beams is better. In machine translation, quality peaks around width 4 to 10 and then declines. The declining part is not a bug in the search; it is the search working correctly on a model whose global optimum is bad.
Ignoring length_penalty. Default 1.0 in transformers, meaning full length normalisation. Set it to 0.0 and outputs get shorter; above 1.0 and they get longer. Many "the model truncates my summaries" reports are this parameter.
Beam searching with a repetition penalty. They fight. Beam search is looking for the highest-scoring sequence and the penalty keeps moving the scores. Use no_repeat_ngram_size if you must, and be aware it makes some correct outputs unreachable.
Reporting a beam-search win without matching compute. Width 4 costs about four times as much. Compare it against four sampled candidates with a reranker before concluding search was what helped.
Try it yourself
Add ("The", "train", "is"): {"</s>": 0.20, "late": 0.80} in place of the existing entry and re-run. Predict first: does the beam-2 winner change, and does the length penalty still flip it? Working out why is worth more than the answer.
What to learn next
- Constrained decoding and grammars — forcing a shape on the output without searching for it.
- Speculative decoding — running several candidate tokens at once, for speed rather than quality.
- Temperature and sampling — what chat models use instead.
Researcher — Mathematics and papers.
The objective
Beam search approximates
$$ \hat{y} = \arg\max_{y} \ \log P(y \mid x) = \arg\max_{y} \sum_{t=1}^{|y|} \log P(y_t \mid y_{<t}, x) $$
Exact search is intractable — $|V|^{T}$ sequences — so the beam keeps the top $k$ prefixes by partial score at each step. It offers no optimality guarantee: a prefix that ranks $k+1$ at step $t$ is discarded permanently, however good its continuations would have been.
Length normalisation
Since $\log P \le 0$ per token, raw scores decrease monotonically with length and the search is biased toward short outputs. Two standard corrections.
Simple normalisation, and what transformers implements:
$$ s(y) = \frac{\log P(y \mid x)}{|y|^{\alpha}} $$
The GNMT form (Wu et al., 2016, arxiv.org/abs/1609.08144):
$$ s(y) = \frac{\log P(y \mid x)}{\mathrm{lp}(y)}, \qquad \mathrm{lp}(y) = \frac{(5 + |y|)^{\alpha}}{(5 + 1)^{\alpha}} $$
with $\alpha \approx 0.6$ to $0.7$ in their experiments. The additive constant softens the correction on very short outputs.
The beam search curse
Koehn and Knowles, Six Challenges for Neural Machine Translation, 2017 (arxiv.org/abs/1706.03872) report BLEU peaking at small beam widths. It degrades as the beam grows. Better search, worse output.
Stahlberg and Byrne, On NMT Search Errors and Model Errors: Cat Got Your Tongue?, EMNLP 2019 (arxiv.org/abs/1908.10090) resolved this with exact search over a Transformer base model on the WMT15 English-German test set. For more than half the sentences, the model's global best score belongs to the empty translation.
Read that again. The mode of the model's distribution is often the empty string. Beam search produces usable output partly because it fails to find the optimum, and small beams fail in a direction that happens to be helpful. Any argument of the form "our decoder finds higher-probability sequences, therefore it is better" is refuted by this result.
Degeneration in open-ended generation
Holtzman et al., ICLR 2020 (arxiv.org/abs/1904.09751) show the same phenomenon outside translation. Beam-search continuations have per-token probabilities far higher and far less variable than human text, and they repeat.
The framing that stuck: human language is not maximum-probability language. Speakers distribute information roughly evenly rather than always choosing the most predictable word, which is the same observation that motivates typical sampling.
Minimum Bayes risk decoding
If the mode is a bad estimate, target expected utility instead:
$$ \hat{y} = \arg\max_{y \in \mathcal{Y}} \ \mathbb{E}{y' \sim P(\cdot \mid x)} \big[ u(y, y') \big] \approx \arg\max{y \in \mathcal{Y}} \ \frac{1}{N} \sum_{n=1}^{N} u(y, y^{(n)}) $$
Sample $N$ candidates, score every candidate against every other with a utility $u$ (BLEU, COMET, a learned metric), and return the most "central" one. Eikema and Aziz, Is MAP Decoding All You Need?, 2020 (arxiv.org/abs/2005.10283) show that MBR avoids the pathologies of mode-seeking search. With a strong neural utility it now leads most machine-translation quality tables.
Cost is $O(N^2)$ utility evaluations. The idea reappears in LLM work under other names: self-consistency for chain-of-thought is MBR with exact-match utility over sampled reasoning paths.
Diverse variants
- Diverse beam search (Vijayakumar et al., 2016, arxiv.org/abs/1610.02424) partitions beams into groups. Each group penalises tokens already chosen by earlier groups, giving genuinely different candidates rather than $k$ near-duplicates.
num_beam_groupsanddiversity_penaltyintransformers. - Constrained beam search forces or forbids specified sequences, used for terminology constraints in translation. Related to but weaker than grammar-constrained decoding.
- Contrastive search (Su et al., 2022, arxiv.org/abs/2202.06417) picks among top-$k$ candidates by trading model confidence against similarity to already-generated hidden states.
penalty_alphaintransformers. A deterministic decoder that avoids degeneration without sampling — genuinely useful and under-used.
Why LLM serving stacks deprioritised it
Four reasons, in descending order of importance.
- Open-ended generation does not want the mode, per Holtzman and Stahlberg.
- Streaming is incompatible with a search that can retract a prefix.
- $k$ concurrent KV caches per request, against a scheduler already memory-bound.
- RLHF and instruction tuning sharpen the distribution considerably, which shrinks the gap between greedy and beam and removes most of the remaining benefit.
vLLM implements beam search outside the main sampling path (LLM.beam_search) precisely because it does not fit the continuous-batching model. Its presence is for translation-style workloads, not chat.
Papers
- Wu et al., Google's Neural Machine Translation System, 2016 — arxiv.org/abs/1609.08144
- Vijayakumar et al., Diverse Beam Search, 2016 — arxiv.org/abs/1610.02424
- Koehn and Knowles, Six Challenges for Neural Machine Translation, 2017 — arxiv.org/abs/1706.03872
- Stahlberg and Byrne, On NMT Search Errors and Model Errors, EMNLP 2019 — arxiv.org/abs/1908.10090
- Eikema and Aziz, Is MAP Decoding All You Need?, 2020 — arxiv.org/abs/2005.10283
- Su et al., A Contrastive Framework for Neural Text Generation, 2022 — arxiv.org/abs/2202.06417
What to learn next
- Constrained decoding and grammars — forcing a shape on the output without searching for it.
- Speculative decoding — running several candidate tokens at once, for speed rather than quality.
- Temperature and sampling — what chat models use instead.