10. Top-K Sampling Filter for Next-Token Generation
Problem
A lab's inference server needs to sample the next token from a language model instead of always taking the argmax (greedy decoding), because greedy decoding produces repetitive, low-diversity text. Top-k sampling restricts the candidate pool to only the k highest-scoring tokens before converting scores into a probability distribution to sample from, which keeps generation focused while still allowing variety among plausible tokens.
Given the raw logits for every token in the vocabulary, keep only the k highest-scoring ones and renormalize them into a probability distribution.
Source: src/10_top_k_sampling_filter.py
def top_k_filter(logits: list[float], k: int) -> list[float]:
>>> top_k_filter([2.0, 1.0, 0.1], 2)
[0.731059, 0.268941, 0.0]
>>> top_k_filter([1.0, 1.0, 1.0], 1)
[1.0, 0.0, 0.0]
Step-by-Step Approach
- Handle the trivial cases first: an empty
logitslist ork <= 0both mean "keep nothing" — return all zeros (or an empty list). - Clamp
ktomin(k, len(logits))so a caller-supplied k larger than the vocabulary just means "keep everything." - Select the indices of the k highest-scoring logits. Sorting by
(-logits[i], i)picks the top-k by value and breaks ties by lower index, in one pass. - Subtract the maximum kept logit from every kept logit before exponentiating (the standard "max-subtraction" trick) so
math.expnever overflows on large logits. - Sum the exponentiated scores and divide each by that sum to get a softmax probability, restricted to just the kept indices.
- Round each probability to 6 decimal places and place it back at its original index in a zero-initialized output list; every non-kept index stays 0.0.
The key insight is that top-k sampling is really two separate steps glued together: a selection step (which tokens survive) and a renormalization step (turning surviving scores into a valid probability distribution) — keeping them conceptually separate makes both the max-subtraction stability trick and the zero-fill for dropped tokens obvious.
Reference solution
import math
def top_k_filter(logits: list[float], k: int) -> list[float]:
n = len(logits)
if n == 0 or k <= 0:
return [0.0] * n
k = min(k, n)
# pick the k highest-scoring indices, O(n log k) via a bounded heap-style
# selection (sorted() here for clarity; heapq.nsmallest would be the
# O(n log k) production choice) -- ties broken by lower index first
top_indices = sorted(range(n), key=lambda i: (-logits[i], i))[:k]
# subtract the max kept logit before exponentiating for numerical stability
m = max(logits[i] for i in top_indices)
exp_scores = {i: math.exp(logits[i] - m) for i in top_indices}
total = sum(exp_scores.values())
result = [0.0] * n
for i in top_indices:
result[i] = round(exp_scores[i] / total, 6)
return result
Key Functions & Tricks
sorted(range(n), key=...)— selects the top-k indices and their tie-break order in one call instead of a separate selection then sort.key=lambda i: (-logits[i], i)— negating the score sorts descending by value while the index acts as a deterministic tie-breaker.max-subtraction before exp()— prevents floating-point overflow when logits are large, a standard numerical-stability trick for softmax.math.exp— converts log-scale scores back to a linear scale so they can be summed and normalized into probabilities.round(..., 6)— keeps expected test outputs as clean, reproducible literals instead of raw floating-point noise.zero-initialized output list— makes "not in the top-k" an explicit, easy-to-check 0.0 rather than a missing key.
How to Recognize This Pattern
Reach for "select-then-normalize" whenever a problem talks about restricting a distribution to a subset before turning it into probabilities — the phrases "top-k," "nucleus/top-p," or "restrict the candidate pool" are the signal. Common variations swap the selection rule (top-p keeps the smallest set of tokens whose cumulative probability exceeds a threshold, instead of a fixed count) while reusing the same softmax-and-zero-fill second half. A common pitfall is exponentiating raw logits without subtracting the max first, which silently overflows on realistic logit magnitudes; another is forgetting that ties in the selection step need an explicit, documented tie-break rule, since "top-k" is ambiguous when the k-th and (k+1)-th scores are equal.