← chapter index
Part 10 — Sampling

Sampling

How an inference engine turns one vocabulary-wide score vector into one concrete next token, one stop condition, and one more step of generation.

A transformer does not output text directly. It outputs logits: one raw score for every token in the vocabulary. Sampling is the machinery that takes that score vector, applies policy, and turns it into one token ID. That token might be chosen deterministically, or it might be drawn from a filtered distribution. Either way, this is the stage where a huge set of possibilities collapses into one emitted token.

Why this chapter exists: if you only look at the model core, you miss the control surface that users actually feel. Temperature, top-k, top-p, repetition penalties, stop sequences, grammar constraints, seeds, and beam search all live at the boundary between the network and the user-visible answer. The model proposes. The sampler commits.

Plain English: what sampling does

For our running example, the prompt is “What is the capital of France?”. After the forward pass, the language model head produces a logit vector z ∈ ℝV, where V is the vocabulary size. One entry might correspond to “ Paris”, another to “ Lyon”, another to “ London”, and so on through tens of thousands of candidates. Sampling is not about inventing meaning. It is about translating that score vector into an action.

In a production engine, the sampling pipeline is usually short but deliberate: adjust logits for penalties, divide by temperature, apply any hard masks or grammar rules, optionally filter to a candidate subset, run a numerically stable softmax, then either take the maximum or draw one sample. If the chosen token completes a stop sequence, emit nothing further. If it does not, append it to the context and continue.

A concrete score vector for “What is the capital of France?”

The full vocabulary may be 32K, 50K, or 128K tokens, but the sampler usually reasons about the same vector-shaped object regardless of size. To keep the arithmetic readable, we will show a small visible slice of the distribution. Suppose the model has already projected the final hidden state and produced the following candidate logits for the first generated token:

TokenRaw logit zᵢShifted value zᵢ − max(z)exp(shifted)Softmax pᵢ at τ = 1
“ Paris”6.00.01.00000.749
“ Lyon”4.2-1.80.16530.124
“ London”3.6-2.40.09070.068
“ Berlin”2.8-3.20.04080.031
“ Marseille”2.4-3.60.02730.020
“ French”1.1-4.90.00740.006
“ The”0.4-5.60.00370.003

The numbers are illustrative, not magical. A difference of only a few logit units is enough to make “ Paris” dominate after exponentiation. That is why softmax matters: it converts relative score gaps into a probability distribution that can be interpreted and manipulated consistently.

The full sampling pipeline

logits[V]
  ↓
apply_presence_frequency_or_repetition_penalties()
  ↓
divide_by_temperature()
  ↓
apply_hard_masks(grammar, banned tokens, EOS rules)
  ↓
filter_candidates(top-k / top-p / min-p / typical / beam policy)
  ↓
stable_softmax()
  ↓
argmax_or_random_draw(seed, RNG state)
  ↓
chosen token ID
  ↓
check stop sequences / structured-output completion
  ↓
append to context and continue

This pipeline is operationally cheap compared with the transformer layers that produced the logits, but it is strategically important. Small policy changes here can completely change the style, determinism, safety, and usefulness of the output. A model with identical weights can look terse, repetitive, playful, brittle, or reliable depending on sampler settings.

Maths: logits, temperature, softmax, and numerical stability

Let zᵢ be the logit for token i. With temperature τ, the probability assigned to token i is:

pᵢ = exp(zᵢ / τ) / Σⱼ exp(zⱼ / τ)

Temperature acts before softmax by dividing logits. Lower τ exaggerates differences and makes the distribution sharper. Higher τ compresses differences and makes the distribution flatter. The order of token scores does not change when you divide by a positive scalar, but the gaps between them do.

In real implementations you never compute softmax from raw logits directly. You first subtract the maximum scaled logit. That leaves the probabilities unchanged because the same constant is removed from every numerator and denominator term:

m = maxⱼ (zⱼ / τ)
pᵢ = exp((zᵢ / τ) - m) / Σⱼ exp((zⱼ / τ) - m)

Why does this help? Because exp(80) is enormous and can overflow, while exp(0) is safely 1. By shifting the largest term to zero, every other exponent becomes ≤ 1. The distribution is identical; the implementation becomes safe.

// stable softmax over a visible candidate list
max_val = max(logits)
sum_exp = 0
for i in 0..V-1:
    tmp[i] = exp(logits[i] - max_val)
    sum_exp += tmp[i]
for i in 0..V-1:
    probs[i] = tmp[i] / sum_exp

This is still conceptually the same algorithm on GPU, CPU, or TPU. The differences are in vectorization, memory layout, and whether you fuse surrounding steps to avoid extra reads and writes.

Tensor shapes and sampler state

ObjectMeaningTypical shapeNotes
logitsRaw vocabulary scores from the LM head[V]Usually float16, bfloat16, or float32 before final selection
penalized_logitsScores after repetition/presence/frequency edits[V]Often done in-place
maskAllowed/disallowed token bitmap or additive mask[V]Grammar constraints and banned tokens live here
probsNormalised probabilities[V] or [K]After filtering, the active set may be smaller than V
history_countsPer-token repetition countssparse map or [V]Sparse is common because only a tiny fraction of V appears
rng_stateRandom generator stateimplementation-specificRequired for stochastic decoding
selected_idChosen token IDscalarAppended to the sequence if not stopped

Notice how small these tensors are compared with the model weights and the KV cache. Sampling is not where most memory is spent. But sampling is still part of the serving hot path, so unnecessary allocations, full sorts, or host-device synchronizations can hurt latency.

Temperature tabs: one scalar, large behavioral effect

Conceptually, temperature rescales confidence. It does not change which token is ranked first, second, or third, but it changes how much probability mass separates them. That matters only when your policy uses probabilities rather than pure argmax.

With τ = 0.7, the same visible logits become a much sharper distribution. Using our running example, the probabilities are approximately Paris 0.888, Lyon 0.068, London 0.029, Berlin 0.009, and the remainder are nearly gone. Lower temperature is useful when you want correctness, formatting discipline, or deterministic-feeling behavior, but if you push it too low the model becomes brittle and repetitive.

At τ = 1, nothing is rescaled. We recover the baseline probabilities from the table above: Paris 0.749, Lyon 0.124, London 0.068, Berlin 0.031, and so on. This is the clean reference setting for understanding the raw distribution the model produced.

With τ = 1.3, the distribution flattens: approximately Paris 0.628, Lyon 0.157, London 0.099, Berlin 0.054, Marseille 0.039. The top candidate still leads, but alternatives become materially more likely. This can improve variety in storytelling or brainstorming, but it also increases the chance of wrong or malformed tokens.

As τ → 0, the distribution collapses onto the largest logit. In the limit, this is equivalent to greedy decoding: always choose the argmax token. Many APIs expose “temperature 0” as a special case rather than literally dividing by zero. The engineering rule is simple: treat it as deterministic argmax, not as a numeric softmax call.

One subtle point that matters in practice: temperature should be applied to logits, not probabilities. Scaling probabilities after softmax is not equivalent and usually produces the wrong behavior.

Penalty tabs: discouraging reuse without rewriting the model

Penalties edit the score vector using sequence history. They are runtime heuristics, not learned parameters. Their job is not to make the model smarter; their job is to make the output less repetitive and more useful under finite context and finite patience.

Frequency penalty subtracts an amount proportional to how many times a token has already appeared. A common form is zᵢ′ = zᵢ − λf · count(i). If the model has already emitted “ Paris” twice and λf = 0.4, the adjusted logit drops by 0.8. Typical values are small, often around 0.1 to 0.8. This targets loops like “Paris Paris Paris” by pushing repeated tokens down more each time they recur.

Presence penalty cares only whether a token has appeared at least once, not how many times. A common form is zᵢ′ = zᵢ − λp · 1[count(i) > 0]. If a token has been seen, it pays a one-time tax. This is useful when you want the model to move to new topics or new vocabulary rather than reusing the same concepts even once.

Repetition penalty is often implemented multiplicatively rather than additively. One common rule is: if token i has appeared before, then zᵢ′ = zᵢ / r when zᵢ > 0, else zᵢ′ = zᵢ · r, with r > 1. Typical values are around 1.05 to 1.2. This preserves sign behavior better than a flat subtraction and is popular in local LLM runtimes.

Penalty order matters. In most engines, you apply penalties before top-k or top-p filtering, because the whole point is to let those penalties influence which tokens survive the filter. If you filter first and penalize later, some candidates never get a fair chance to replace the repeated token you were trying to suppress.

Decoding strategy tabs: the policy layer

Once you have a valid probability distribution, you still need a rule for selecting a token. Greedy decoding ignores randomness. Stochastic strategies keep controlled variability. Filtering strategies do not change the model weights; they decide which parts of the model’s uncertainty you are willing to honor.

Greedy decoding selects argmaxᵢ pᵢ, or equivalently argmaxᵢ zᵢ. For our running example, greedy chooses “ Paris” every time. It is deterministic, cheap, and easy to reason about. It is also prone to blandness and repetition because it never explores near-tie alternatives. When people say “temperature zero,” this is usually what they mean operationally.

Top-k keeps only the K highest-probability tokens, renormalizes them, and samples from that truncated set. If K = 3 on our τ = 1 example, the surviving tokens are Paris, Lyon, and London. Their renormalized probabilities become roughly 0.796, 0.132, and 0.072. Top-k is easy to implement, but the same K may be too loose on confident steps and too tight on uncertain steps.

Top-p, also called nucleus sampling, sorts tokens by descending probability and keeps the smallest prefix whose cumulative mass is at least P. With P = 0.90, our example keeps Paris, Lyon, and London because their cumulative mass reaches 0.941. If the model were more certain, top-p might keep only one or two tokens; if it were uncertain, it might keep dozens. That adaptivity is why top-p became popular for chat systems.

Min-p keeps any token whose probability satisfies p(token) ≥ min_p × p(max). Suppose min_p = 0.1. The threshold is then 0.1 × 0.749 = 0.0749. Only Paris and Lyon survive, because London at 0.068 falls just below the relative cutoff. Min-p acts like a dynamic confidence floor tied to the best token, which makes it behave differently from fixed-K truncation.

Typical sampling prefers tokens whose information content is close to the expected information content of the distribution. Define surprise sᵢ = -log pᵢ and entropy H = Σᵢ pᵢ(-log pᵢ). Keep tokens with small |sᵢ − H|, then sample among them. In plain English, it avoids tokens that are too obvious and tokens that are too weird. On this example, a typical sampler may downweight an overwhelmingly dominant token if it is much less surprising than the distribution’s entropy would suggest.

Typical sampling is harder to explain at first glance, but the intuition is practical: a token can be high probability and still be “too certain” relative to the overall shape of the distribution, just as a token can be low probability and “too surprising.” Typical decoding tries to stay near the expected surprise level.

Entropy, surprise, and why these policies feel different

Two distributions can have the same top token and still feel very different to decode from. In one case the top token may hold 0.95 probability and everything else is dust. In another case the top token may hold 0.30 and ten alternatives are plausible. Top-k does not know that difference unless K changes. Top-p partially adapts because cumulative mass rises faster in low-entropy distributions and slower in high-entropy ones. Typical sampling goes one step further by explicitly using the information content of each token.

surprise(token i) = -log pᵢ
entropy H(p)      = Σᵢ pᵢ (-log pᵢ)

That distinction matters when you want text that is neither robotic nor chaotic. If the distribution is already sharp, typical decoding may reject extremely unsurprising tokens that would make generation collapse into dull loops. If the distribution is already broad, it may reject bizarre tail tokens that are technically allowed by top-p but feel too off-pattern for the current uncertainty level.

There is no universally best policy. Greedy excels when correctness is tightly coupled to the single highest-probability token. Top-k is simple and predictable. Top-p adapts well to variable confidence. Min-p is a relative confidence gate. Typical sampling is often aesthetically appealing because it respects the shape of uncertainty rather than just its rank ordering. The practical lesson is to think in distribution shape, not in brand names.

A worked numerical walkthrough

Let us walk one step all the way through. Assume there are no structural bans yet, no stop condition yet, and we choose τ = 1, top-k = 3, and stochastic selection with a fixed seed. Start from the visible slice above:

Raw logits:
Paris      6.0
Lyon       4.2
London     3.6
Berlin     2.8
Marseille  2.4
French     1.1
The        0.4

Softmax at τ = 1:
Paris      0.749
Lyon       0.124
London     0.068
Berlin     0.031
Marseille  0.020
French     0.006
The        0.003

Top-k = 3 keeps:
Paris      0.796
Lyon       0.132
London     0.072

If the random draw is u = 0.81, cumulative sampling over the renormalized top-3 distribution yields: Paris occupies [0.000, 0.796), Lyon occupies [0.796, 0.928), and London occupies [0.928, 1.000]. A draw of 0.81 therefore picks “ Lyon”, even though “ Paris” was the single most likely token. This is the entire point of stochastic decoding: allow high-probability alternatives to occasionally win.

Important subtlety: sampling does not mean “choose randomly from anywhere.” Good samplers make randomness conditional on model confidence. Randomness lives inside a carefully shaped and filtered distribution.

Now imagine the next step of generation already contains the token “ Paris” twice because the model wrote a clumsy sentence. With a frequency penalty λf = 0.4, the logit for “ Paris” loses 0.8. It falls from 6.0 to 5.2. That does not guarantee it loses, but it narrows the gap enough that alternatives can compete. Penalties do not force diversity; they create breathing room for it.

Seeds and determinism

For stochastic decoding, the seed initializes the random number generator. Same prompt + same model weights + same sampler policy + same seed should produce the same random draws and therefore the same sampled tokens. But real systems add caveats: floating-point nondeterminism, fused kernels, vendor library versions, and batch interactions can all perturb results. Greedy decoding bypasses RNG entirely, but it can still differ across implementations if logits are numerically close and hardware behavior changes the tie.

A good implementation treats determinism as a contract with levels. Level 1: same process, same seed, same batch, same code path. Level 2: same hardware and libraries. Level 3: bitwise reproducibility across environments, which is much harder and often not worth the cost for user-facing chat. The practical promise most systems make is “reproducible enough,” not “identical on every machine forever.”

Stop sequences and generation termination

Sampling is not just about choosing a token; it is also where generation stops. There are several common stop conditions:

Stop conditionHow it worksOperational consequence
EOS tokenModel emits a learned end-of-sequence tokenNatural termination if training aligned well
Max tokensServer stops after a configured number of decode stepsHard cap for latency and cost
Stop stringEmitted text matches a configured byte/token patternUseful for chat separators and templates
Grammar completionParser state reaches an accepting final stateCommon in JSON or tool-call generation

Implementers often keep a rolling token window or a byte buffer to detect stop sequences without rescanning the full generated output each step. This is a tiny but real form of caching inside the sampler.

What sampler state persists across steps?

Sampling is stateless only in toy examples. A real engine carries small but important state from token to token. The most obvious piece is the random number generator state used for reproducible stochastic draws. Then come repetition counters for presence, frequency, or repetition penalties. Add stop-sequence matching state, grammar-parser state for structured output, and possibly speculative-decoding acceptance bookkeeping when a draft model is involved.

None of that state is large compared with the KV cache, but it still deserves design care. If you stream tokens to a client, pause, resume, or migrate a request between worker threads, you must move the sampler state with the request. If you batch many requests together, each request needs its own independent sampler state even while weights and kernels are shared. This is one of the reasons inference engines usually separate model state from request state: the weights are global, the sampler and cache metadata are per sequence.

Put differently, sampling is not a pure function of current logits once you enable real policies. The same logit vector can produce different legal actions depending on the request’s history. That is exactly what repetition penalties, grammar masks, and stop rules are supposed to do.

Structured output and grammar-constrained decoding

Structured output is best understood as dynamic masking. At each decode step, an external grammar engine or parser state machine tells the sampler which tokens are legal next moves. Illegal tokens receive -∞ or an equivalent hard ban before softmax. The sampler then runs normally over the remaining legal subset.

This means grammar constraints sit naturally in the sampling pipeline. They do not require retraining the model. They do require careful tokenization-aware engineering: a JSON quote might be one token in one tokenizer and several token fragments in another, so the grammar engine must operate on the actual token vocabulary, not on wishful character abstractions.

Beam search: useful, but less common for chat LLMs

Beam search keeps multiple partial continuations alive at once instead of committing to one token path immediately. At each step, it expands the best B sequences according to cumulative log-probability, then prunes back to the best B. This often improves sequence-level likelihood in tasks like translation or speech recognition, where the best overall sentence may require a locally non-greedy choice early on.

Why is it less common for chat LLMs? Because beam search is more expensive, less diverse than sampling, and often produces overly safe text. User-facing assistants usually benefit more from controlled stochastic decoding than from maximizing sequence likelihood. Still, beam search belongs in the design space because it is the classic reminder that “best next token” is not always “best full sentence.”

Speculative decoding: faster sampling by drafting ahead

Speculative decoding uses a smaller draft model to propose several next tokens, then asks the larger target model to verify them in batches. If the large model agrees, you accept multiple tokens at once and reduce latency. If it disagrees, you fall back to the target model’s choice at the first disagreement point.

This belongs in the sampling chapter because the acceptance test is effectively a decoding policy wrapped around two models. The large model still determines correctness. The small model exists to offer plausible candidates so the expensive model can amortize work over multiple positions.

Cost, hardware, and operation-classification

Compared with attention and FFN matmuls, sampling is small. But small does not mean irrelevant. Decode latency is serial, so even “cheap” steps executed every token deserve engineering attention.

Sampler stageOperation typeTypical boundWhy
Penalty applicationElementwise vector editMemory-boundReads/writes logits with little arithmetic per element
Temperature scalingScalar divide over vectorMemory-boundOne arithmetic op per logit
Top-k / top-p filteringSelection + partial sort + prefix sumBranchy / memory-boundLow arithmetic intensity, irregular control flow
SoftmaxReduction + exp + normalizationMostly memory-boundTwo passes plus transcendental ops across V
Categorical drawPrefix scan or sampled lookupTinyNegligible compared with the model forward pass
Stop / grammar checkFinite-state updateTinyUsually a few bytes or parser-state transitions

On GPU, the temptation is to leave logits on device and fuse as much as possible so you do not bounce the vocabulary vector back to the CPU just to sample. On CPU, branch-friendly top-k and efficient sparse history structures matter more. On either target, a full O(V log V) sort every token is usually wasteful when a partial selection or threshold-based scan will do.

Optimisations and caching

Although the sampler has no KV cache, it still benefits from small cached state: token occurrence counts for penalties, rolling buffers for stop sequences, parser states for grammar constraints, and RNG state for reproducibility. Engines that implement speculative decoding may also cache draft tokens and acceptance statistics. All of this state is small, but it avoids repeated recomputation on the critical decode path.

Common optimisations include: keep logits resident on the same device as the LM head; fuse temperature, mask, and softmax when possible; use partial selection instead of full sort; maintain sparse repetition maps instead of dense [V] arrays when the vocabulary is huge; and short-circuit directly to argmax when the user requested greedy decoding or temperature zero.

Analogies that help without lying too much

Three analogies are worth keeping. First, roulette wheel: after softmax and filtering, the distribution is a weighted wheel; greedy just always takes the largest slice. Second, thermostat: temperature does not invent new options, it changes how dominant the current favorite feels. Third, bouncer at the door: top-k, top-p, min-p, and grammar constraints decide which candidates even get into the club before the final choice happens.

The analogy to avoid is “the model writes a full answer and then sampling chooses one version.” That is not what happens. The model produces one distribution per position, one token at a time. The future does not exist yet beyond those probabilities.

Common implementation mistakes

MistakeWhy it is wrongCorrect move
Applying softmax before penaltiesPenalties belong in logit spaceEdit logits first, then normalize
Scaling probabilities instead of logits for temperatureNot equivalent to temperature scalingDivide logits by τ before softmax
Using full sort for every top-kWastes work for large vocabulariesUse partial selection or heap-based top-k
Ignoring tokenizer boundaries in stop stringsText-level rules may not match token-level generation cleanlyTrack bytes/tokens carefully and test edge cases
Assuming seed guarantees cross-hardware identityFloating-point and kernel differences still matterDocument the scope of determinism you actually support

If you were implementing this yourself...

The most reliable path is incremental. Implement stable softmax and greedy selection first. Then add seeded categorical sampling. Then add top-k. Then top-p. Then penalties. Then stop sequences. Then grammar masks if you need structured output. Every stage should have tiny deterministic tests using small visible logit vectors like the one in this chapter.

token sample_next(
    float logits[V],
    History history,
    SamplerConfig cfg,
    RNG* rng)
{
    apply_penalties(logits, history, cfg);
    apply_temperature(logits, cfg.temperature);
    apply_masks(logits, cfg.grammar_state, cfg.banned_tokens);

    CandidateSet c = filter_candidates(logits, cfg);   // greedy, top-k, top-p, min-p, typical
    softmax_in_place(c.logits_or_probs);

    token t = cfg.greedy
        ? argmax(c)
        : categorical_sample(c, rng);

    update_stop_state(history, t, cfg.stop_rules);
    update_penalty_state(history, t);
    return t;
}

The engineering mindset is straightforward: the model forward pass produces scores; the sampler is a compact policy engine over those scores. Keep the math honest, keep the data movement small, and keep the semantics explicit. Once you do that, every “creative” or “deterministic” behavior becomes inspectable rather than mystical.