Bridges: DSA to LLMs
Priority queues and Dijkstra to beam search
Exploring the best partial sequences
The decoder's beam is a priority queue of half-finished sentences - the exact frontier Dijkstra expands, only scored by log-probability instead of distance.
In Shortest paths: Dijkstra you watched Dijkstra grow a shortest-path tree by repeatedly pulling the closest unsettled node off a priority queue and expanding it - best-first search. Beam search, the workhorse decoder behind translation and captioning, is the very same machine pointed at a different graph: its nodes are half-written sentences, its edge weights are per-token log-probabilities, and its heap keeps the highest-scoring partial sequences alive at every step. Learn one and you have learned the other; the only real differences are what a node is and the fact that the beam is bounded to width instead of holding the whole frontier.
The one-line idea
The engine you already own: a heap-backed frontier
Every one of these algorithms is a loop over the same two moves: pop the most promising item from a priority queue, then push its expansions back in. Dijkstra pops the min-cost node and pushes relaxed neighbours; beam search pops (surfaces) the top- sequences and pushes each one's next-token children. The heap is what makes “which candidate is best?” cheap enough to ask thousands of times per sentence. Here is the translation, term for term - nothing on the right is a metaphor.
| Graph search - Dijkstra / A* | Beam-search decoding |
|---|---|
| node = a partial path from the source | a partial sequence (the prefix so far) |
| edge weight | |
| cumulative path cost | - the negative log-probability |
| priority queue / binary heap | the beam - a top- frontier |
| pop the single min-cost node | surface the highest-scoring beams |
| goal node reached | the end-of-sequence token is emitted |
| A* heuristic steers the search | length / reward terms steer decoding |
The one honest difference: Dijkstra keeps every reachable node on the heap and is therefore exact. Beam search deliberately throws away all but the top at each step to stay affordable - the sequence graph has paths ( = vocabulary, = length), far too many to settle exactly. That truncation is what makes beam search a fast heuristic rather than a guaranteed optimum.
Scoring a path: add up the log-probabilities
A model gives you - the probability of each next token given the prefix. The probability of a whole sequence is the product of these, but a decoder scores it as the sum of log-probabilities:
Three reasons for the log. Multiplying hundreds of probabilities underflows to zero in floating point; is monotonic, so it never changes the ranking; and it turns a product into a sum, converting “probability of a path” into an additive cost - exactly the additive edge weights Dijkstra accumulates along a route. Negate the log-probs and “more probable” becomes “less costly,” and beam search is Dijkstra with a fixed-width frontier.
One subtlety follows immediately. Because every , longer paths keep adding negative terms, so a raw score always favours shorter sequences. Beam search corrects this with length normalization, dividing by the length raised to a power :
Within a single step every candidate on the beam has the same length, so this rescaling changes nothing there - it only matters when you finally compare finished sentences of different lengths (once an end-of-sequence token can fire).
Beam width : from greedy to (almost) exhaustive
The demo below decodes a toy language model, three candidate tokens per step, three steps deep. Drag the beam width and step through the levels. Each step pushes every surviving beam's children into the priority queue on the right; the heap sorts them by score and surfaces the top ; everything below the cut is pruned and its branch fades in the tree. Watch how is plain greedy decoding and larger widens toward exhaustive search.
Tree: each node is a partial sentence, showing its last token and its running score (sum of log-probs). Solid nodes are the beams the heap kept; faded nodes were pruned this step. Queue: the same candidates, sorted by score - the top-k above the dashed cut survive.
Start: the queue holds one beam - the empty sequence - with cumulative score log P = 0.
Priority queue
The aha: greedy's trap, and why width saves it
import heapq, math
def beam_search(next_dist, k, steps):
# each beam is (cumulative log-prob score, list of tokens)
beams = [(0.0, ["<s>"])]
for _ in range(steps):
frontier = [] # the priority queue for this level
for score, seq in beams:
for token, p in next_dist(seq): # PUSH every child
heapq.heappush(frontier, (score + math.log(p), seq + [token]))
beams = heapq.nlargest(k, frontier) # SURFACE top-k, prune the rest
return max(beams) # best-scoring complete sequence
# Same loop as Dijkstra: swap `next_dist` for graph edges and `log(p)`
# for -weight, drop the top-k cap, and you are back to shortest paths.Check yourself
With beam width k = 1, beam search reduces to exactly:
The heap in beam search orders candidate sequences by their:
How is beam-search decoding the same algorithm as Dijkstra's shortest path?
Try to state it, then check.
Lock it in
- Beam search is best-first frontier expansion: a priority queue of partial sequences, the same pop-then-push loop Dijkstra runs on a graph.
- The mapping is exact - nodes are partial sequences, edge weights are per-token negative log-probs, cumulative cost is , and the beam is the frontier.
- Logs turn the product of probabilities into an additive cost (and dodge underflow); length normalization by stops raw scores from favouring shorter sequences.
- Beam width spans greedy () to near-exhaustive; capping to top- makes it a fast heuristic, not the exact optimum Dijkstra guarantees.
Primary source
Ask your teacher