Skip to content
mlmentorship

Implement beam search with EOS and length normalization

The implementation tests bounded hypothesis state, log-probability accounting, finished-sequence handling, and whether the decoder fits the task.

Published · 8 min read ·Core ·Mixed

ML implementation · active recall

Practice before you read

40 minutes. Clarify the contract, implement a correct baseline, test edge cases, then optimize.

ML implementation · closed-book attempt

Clarify the contract, implement a correct baseline, test edge cases, then optimize.

Problem

Context. A next-token function maps a token prefix to log probabilities over a fixed vocabulary. Inputs include beginning/end token IDs, positive beam width k, and a maximum number of newly generated tokens. Task. Return the highest-final-score completed sequence found; if none completes within the budget, return the best remaining live sequence. Specify the length-normalization rule, including whether beginning and end tokens count, before comparing results. Constraints. Keep at most k live hypotheses and never expand a completed sequence. Accumulate token log probabilities consistently, define tie behavior, and respect the generation budget. Beam search is approximate search over sequences, not a guarantee of finding the global highest-probability completion.

40:00recommended time

Closing or reloading clears the scratchpad. Only score, weak rubric dimensions, attempt count, and retry date can be stored locally.

Implement beam search around a next-token function. Keep at most live hypotheses, stop expanding finished sequences, and return the best completed result.

Implement a bounded sequence search that handles end-of-sequence tokens and a consistently declared final scoring rule.

Problem

Context. A next-token function maps a token prefix to log probabilities over a fixed vocabulary. Inputs include beginning/end token IDs, positive beam width k, and a maximum number of newly generated tokens.

Task. Return the highest-final-score completed sequence found; if none completes within the budget, return the best remaining live sequence. Specify the length-normalization rule, including whether beginning and end tokens count, before comparing results.

Constraints. Keep at most k live hypotheses and never expand a completed sequence. Accumulate token log probabilities consistently, define tie behavior, and respect the generation budget. Beam search is approximate search over sequences, not a guarantee of finding the global highest-probability completion.

The baseline algorithm

Represent each hypothesis as (tokens, cumulative_log_probability).

At each step:

  1. call the model for every live prefix;
  2. add each candidate token’s log probability to the prefix score;
  3. move EOS candidates to the finished pool;
  4. keep only the top unfinished candidates;
  5. stop when no live candidates remain, the token budget is exhausted, or a valid early-stop bound proves no live beam can beat the best finished one;
  6. rank finished hypotheses with one consistent final scoring rule.

Finished beams stop expanding even while they remain final candidates

Length normalization can reverse the raw-probability winnerRaw probability favors A EOS; log-probability divided by length favors A x EOS. Count EOS, exclude BOS, and stop expanding finished beams.Keep finished beam identities; changethe declared scoreRaw complete-sequence probabilityA EOS: 0.18A x EOS: 0.15Winner A EOSFinal score: log probability dividedby lengthA EOS: -0.8574A x EOS: -0.6324Winner A x EOS
Derivation
Route EOS out of live search, then compare completions with the declared length ruleA normalized toy starts A with probability 0.6 and B with 0.4. A has next-token probabilities 0.5, 0.3, 0.2 for x, EOS, z. B has 0.6, 0.25, 0.15 for y, x, EOS. Log totals are computed from those probabilities. EOS candidates enter a separate finished enclosure and never expand. Live pruning retains A x and B y. At a later step A x emits EOS with probability 0.5. Final comparison includes EOS and excludes BOS, using alpha one: A EOS has probability 0.18, length two and final score about -0.857; A x EOS has probability 0.15, length three and final score about -0.632. The longer completion wins only under this declared normalization.NORMALIZED TOY PROBABILITIES; BEAM WIDTH 2Child score = parent log score + log tokenprobabilityA: p = 0.6; log p = -0.511token plog totalA x0.5-1.204A EOS0.3-1.715A z0.2-2.120B: p = 0.4; log p = -0.916token plog totalB y0.6-1.427B x0.25-2.303B EOS0.15-2.813EOS LEAVES THE FRONTIER: NEVER EXPAND ITA EOS: -1.715; finishedB EOS: -2.813; finishedPRUNE ONLY LIVE CANDIDATES: KEEP TWOKEEP A x: -1.204KEEP B y: -1.427drop A z: -2.120drop B x: -2.303LATER: p(EOS | A x) = 0.5Final score = log p / length; alpha = 1Length includes EOS, excludes BOS.rawfinalA EOS; n=2-1.715-0.857A x EOS; n=3-1.897-0.632Raw prefers A EOS; this final rule prefers Ax EOS.Only if no completion exists, rank remaininglive beams.
Each parent distributes total token probability one. EOS candidates leave live search before pruning. The later final comparison divides log probability by the declared length. It can prefer a longer completion despite its lower raw probability.
Check: Which score ranks completed sequences at the end? Why does an EOS sequence never return to live expansion?
Read it this way: Raw probability favors A EOS; log-probability divided by length favors A x EOS. Count EOS, exclude BOS, and stop expanding finished beams.
Which finished beam wins after dividing log probability by its counted length?

The raw sequence score is:

Because log probabilities are non-positive, raw score prefers shorter sequences. A simple length penalty is:

State whether length includes BOS or EOS. Consistency matters more than one universal convention.

Reference outline

live = [((bos,), 0.0)]
finished = []

for _ in range(max_new_tokens):
    candidates = []
    for prefix, score in live:
        for token, token_logp in enumerate(step(prefix)):
            next_prefix = prefix + (token,)
            next_score = score + token_logp
            if token == eos:
                finished.append((next_prefix, next_score))
            else:
                candidates.append((next_prefix, next_score))
    live = top_k(candidates, beam_size)
    if not live:
        break

pool = finished or live
return max(pool, key=normalized_score)[0]

A production implementation batches beams and keeps tensor state. The scalar outline makes semantics easier to verify first.

What an L4 answer sounds like

The candidate keeps top tokens independently at each step, which is not beam search over sequences. Or they expand finished beams, compare normalized and unnormalized scores mid-loop, and return the best live beam even when a better completed sequence exists.

What an L5 answer adds

An L5 candidate uses cumulative log probability, maintains separate live and finished pools, defines length normalization, handles no-EOS fallback, and tests a case where the locally best first token does not produce the globally best sequence.

They test:

  • beam size one equals greedy decoding;
  • immediate EOS;
  • no EOS before the budget;
  • ties with deterministic ordering;
  • very small probabilities without underflow;
  • a finished beam is never expanded;
  • length penalty changes the selected sequence in a controlled example.

What an L6 answer adds

An L6 candidate connects sequence state to model state. Each beam carries or indexes a KV cache. Expanding and pruning hypotheses requires cache reordering without copying full prefixes unnecessarily. Finished beams release or freeze their state.

They discuss when beam search is the wrong decoder. Maximizing model likelihood often produces bland text for open-ended chat. Beam search is better suited to tasks with a constrained or reference-like answer, such as some translation, speech, or structured generation settings. For code or reasoning, sampling multiple candidates plus verification may outperform a narrow likelihood beam.

They also know early stopping is subtle under length normalization. A live beam with a worse current normalized score can improve relative ranking as length changes, so the stopping bound must match the final scoring rule.

Tells that get you a strong-hire vote

  • Scores accumulate in log space.
  • Live and finished hypotheses are separate.
  • EOS stops expansion.
  • Length conventions are explicit and consistent.
  • Tests include a globally non-greedy optimum.
  • Beam state is connected to KV-cache reorder semantics.
  • You state where beam search fits and where it does not.

Tells that get you down-leveled

  • Top- token sampling described as beam search.
  • Multiplying raw probabilities until they underflow.
  • Continuing after EOS.
  • Comparing different score conventions in one pool.
  • Returning only live hypotheses when completed ones exist.
  • Claiming larger beam always improves user-visible quality.

Common follow-up

“How would you add constrained decoding?”

Track the constraint state with each hypothesis and mask tokens that would make completion invalid. For a finite-state grammar, each beam carries a grammar state; token transitions update it. The pruning score remains probabilistic, but validity is a hard gate.

Use the beam-search starter before relying on the outline.

Related: decoding strategies, KV cache, and speculative decoding.