Sampling: temperature, top-k, top-p
Code version: DeepInfer @ 9bd9cf2 · 2026-10-21
In one sentence
Sampling selects the next token from the model's probabilities. Temperature, top-k and top-p shape the probabilities first; then a random number makes the choice.
The idea#
From scores to a choice#
The model gives one raw score per vocabulary token, the logits. Softmax turns them into probabilities. Sampling then selects one token. There are two simple choices, and neither is good alone:
- Always take the most likely token (greedy decoding). The result is repeatable, but texts often become dull or repeat the same phrase.
- Draw from the full distribution. The result is varied, but sometimes the draw selects a very unlikely token, and the text goes wrong.
Three settings give control between these two ends. They are fields of SamplingParams, one set per request.
Temperature#
Temperature T divides every logit before softmax: p = softmax(logits / T).
- T = 1 keeps the model's distribution.
- T < 1 makes it sharper: likely tokens get more probability.
- T > 1 makes it flatter: unlikely tokens get more probability.
- T close to 0 gives (almost) greedy decoding.
Example with six candidates (made-up logits 3.0, 2.5, 2.0, 1.5, 0.5, −1.0). At T = 1 the best token has probability 0.435. At T = 0.5 it has 0.641. At T = 2 it has 0.305.
Top-k and top-p#
These two filters remove the unlikely tail of the distribution. Then the probabilities of the remaining tokens are scaled up so that they add up to 1 again.
- Top-k keeps the k most likely tokens.
top_k = -1keeps all. - Top-p (nucleus sampling) keeps the smallest group of most likely tokens whose probabilities add up to more than p.
top_p = 1.0keeps all. Top-p adapts: when the model is sure, the group is small; when it is unsure, the group is large.
The draw#
Put the kept probabilities side by side on a line from 0 to 1. Take a random number u between 0 and 1. The token whose part of the line contains u wins. A likely token has a long part, so it wins more often.
The random number comes from a seed. The same seed gives the same numbers, so the same request gives the same text. In DeepInfer, the seed is the field seq_seed. The deep dive shows how the number is made.
An everyday comparison#
Think of a raffle. Each token buys tickets in proportion to its probability. Temperature changes the ticket prices: a low temperature makes tickets cheap for the favourites and expensive for the rest. Top-k and top-p send the small buyers away before the draw. The seed decides which ticket the hand pulls out.
The comparison stops here: a real raffle is new each time. With a fixed seed, the "hand" always pulls the same ticket for the same request and position.
The picture#
Follow one token across panel 2. "sofa" survives top-p, because the mass before it (0.699) is still below 0.8. Top-k 2 then removes it. In panel 3, u = 0.70 is past the end of "mat" (0.622), so "floor" wins.
In DeepInfer#
This is a basics page, so this part is short. The page The sampler explains the full sampler, with draft tokens.
Step 1: the settings of one request#
def __init__(
self,
temperature: float = 1.0,
top_p: float = 1.0,
top_k: int = -1,
ignore_eos: bool = False,
max_tokens: int = 16,
logprobs: bool = False,
prompt_logprobs: bool = False,
top_logprobs: int = 0,
first_n_token_no_eos: int = 0,
eos_id: int = 0,
so_args: dict | None = None,
golden_token_ids: list[int] | None = None,
seq_seed: int | None = None,
) -> None:The C++ engine copies the values into each sequence once (csrc/engine/sequence.hpp:103-115). For each step, the sampler gets one temperature, top-p, top-k and seed per sequence (csrc/engine/prepare_sample.cpp:158-161).
Step 2: logits only for the sampled rows#
Sampler.forward first selects the rows to sample, then computes their logits in float32 (deepinfer/model_executor/layers/sampler.py:140-142). process_sampling then calls one fused kernel for temperature, top-p, top-k, log-softmax and the draw (deepinfer/model_executor/layers/sampler_utils.py:185-194).
Step 3: the order of the steps#
The PyTorch reference of the kernel shows the order:
seed_offset = 0 if tag == "default" else _MTP_SEED_OFFSET
logits.mul_((1.0 / transform_inputs.temperatures).unsqueeze(1))
apply_top_p_top_k_ref(logits, transform_inputs.top_ps, transform_inputs.top_ks)
logprobs = torch.log_softmax(logits, dim=-1)Deep dive#
Deep diveTemperature, with numbers
With logits zi and temperature T:
p_i(T) = exp(z_i / T) / Σ_j exp(z_j / T)
| Token | Logit | exp(z / 1) | p at T = 1 | p at T = 0.5 | p at T = 2 |
|---|---|---|---|---|---|
| mat | 3.0 | 20.086 | 0.435 | 0.641 | 0.305 |
| floor | 2.5 | 12.182 | 0.264 | 0.236 | 0.237 |
| sofa | 2.0 | 7.389 | 0.160 | 0.087 | 0.185 |
| bed | 1.5 | 4.482 | 0.097 | 0.032 | 0.144 |
| roof | 0.5 | 1.649 | 0.036 | 0.004 | 0.087 |
| moon | −1.0 | 0.368 | 0.008 | 0.000 | 0.041 |
| sum | 46.155 | 1.000 | 1.000 | 1.000 |
The ratio of two probabilities is exp((za − zb) / T). For "mat" and "floor" the logit gap is 0.5. The ratio is exp(0.5) = 1.65 at T = 1, and exp(1.0) = 2.72 at T = 0.5. A lower temperature makes every gap larger.
Temperature 0 in DeepInfer. Division by 0 is not possible. SamplingParams replaces any temperature below SAMPLING_EPS = 1e-5 with 1e-5, and also sets top_p = 1.0 and top_k = -1 (deepinfer/sampling_params.py:10, deepinfer/sampling_params.py:76-79). Then a logit gap of only 0.01 becomes 0.01 / 1e-5 = 1,000, and exp(−1,000) is 0 in float32. So the best token gets probability 1 in practice. Exactly equal logits still share the probability.
A negative temperature raises ValueError (deepinfer/sampling_params.py:94-95). The reference multiplies the logits by 1/T (deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:209).
Deep diveTop-p and top-k: the algorithm, step by step
The reference implementation of DeepInfer's kernel does this for each row (deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:121-150):
- Compute p = softmax(logits / T) over the full vocabulary. The division by T happens just before this function (
deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:209). - Sort the tokens by p, largest first. Ties keep token order (a stable sort).
- Top-p: for each token, compute the mass before it: the sum of the probabilities of all better tokens. Remove the token if that mass is greater than
top_p(line 139). So the first token always stays. - Top-k: rank the tokens that are left. Remove every token with rank ≥
top_k, unlesstop_kis −1 (line 149). - Set the logits of removed tokens to −∞ and take log-softmax. The kept probabilities now add up to 1.
Worked example (panel 2 of the figure, T = 1, top_p = 0.8, top_k = 2):
| Token | p | Mass before | Top-p 0.8 | Rank | Top-k 2 | Final p |
|---|---|---|---|---|---|---|
| mat | 0.435 | 0.000 | keep | 0 | keep | 0.435 / 0.699 = 0.622 |
| floor | 0.264 | 0.435 | keep | 1 | keep | 0.264 / 0.699 = 0.378 |
| sofa | 0.160 | 0.699 | keep | 2 | remove | 0 |
| bed | 0.097 | 0.859 | remove | — | — | 0 |
| roof | 0.036 | 0.956 | remove | — | — | 0 |
| moon | 0.008 | 0.992 | remove | — | — | 0 |
Top-p alone keeps 3 tokens with mass 0.859, the smallest group that reaches 0.8. With top-k 2, the final group is {mat, floor}.
The order matters. DeepInfer measures top-p on the full distribution, then cuts to k. Some other engines cut to k first and measure top-p on the renormalized top-k probabilities. Example: p = (0.5, 0.3, 0.2), top_k = 2, top_p = 0.55.
- DeepInfer order: mass before the second token is 0.5 ≤ 0.55, so it stays. Result: 2 tokens.
- Top-k first: renormalized (0.625, 0.375). Mass before the second token is 0.625 > 0.55, so it goes. Result: 1 token.
The kernel tests check the DeepInfer rule (deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:366-377). Without its smallest token, the kept prefix has a full-softmax mass of at most top_p. The prefix has at most k tokens.
Limits. top_p must be in (0, 1]; top_k must be −1 or between 1 and the int32 maximum, so top_k = 0 raises ValueError (deepinfer/sampling_params.py:96-99). A top_k larger than the vocabulary keeps all tokens (deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:149).
Deep diveThe seeded draw and determinism
The draw in DeepInfer does not use a random generator with state. It makes the random number from a hash, so the result depends only on the inputs. The docstring says that the result is "deterministic in (seq_seeds, sample_positions, is_nextn_predict)", and independent of batch size and row order (deepinfer/model_executor/layers/tilelang_ops/random_sampling.py:17-20). The PyTorch reference shows the steps:
def random_sampling_ref(
probs: torch.Tensor,
seq_seeds: torch.Tensor,
sample_positions: torch.Tensor,
is_nextn_predict: bool = False,
) -> torch.Tensor:
"""Pure-PyTorch sampler: hash (seq_seeds, 3 * sample_positions + case) to a
uniform and invert the fp64 CDF. For Ascend, this will be replaced later."""
sample_case = 1 if is_nextn_predict else 0
seq = (sample_positions.to(torch.int64) * 3 + sample_case) * i64(0x9E3779B97F4A7C15)
combined = seq_seeds.to(torch.int64) + (seq ^ (seq * (1 << 13)))
u = _hash_to_uniform(combined).unsqueeze(-1)
cdf = probs.double().cumsum(dim=-1)
target = u * cdf[:, -1:]
return (cdf > target).long().argmax(dim=-1).int()- Line 79: mix the token position with a case number:
position · 3 + case, times a large odd constant. The case is 0 for a normal sample. Case 1 is the new sample after the main model rejects a draft token, and case 2 is the accept test of a draft (deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:239,deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:245). - Line 80: mix the value once more with a shift and an XOR, and add the request seed
seq_seed. - Line 81: the SplitMix64 hash turns the 64-bit value into a uniform number u in [0, 1) (
deepinfer/model_executor/layers/tilelang_ops/random_sampling.py:58-67). - Lines 82-84: build the running sum (CDF) of the kept probabilities in float64. Return the first token whose running sum is greater than u × total.
Worked example (panel 3): kept probabilities (0.622, 0.378), CDF (0.622, 1.000). With u = 0.70: 0.622 is not greater than 0.70; 1.000 is. The sampler returns the second token, "floor". The value 0.70 is illustrative; the real u comes from the hash.
Which position? In a prefill step, the position is seqlen_k, the number of tokens in the sequence so far. That is the position of the token that the step makes (csrc/engine/prepare_sample.cpp:198). In a decode step, it is seqlen_k − seqlen_q + 1: the position of the token that the first new row predicts (csrc/engine/prepare_sample.cpp:206). The draft module samples with the tag "mtp", which adds the seed offset 1000000007, so its random numbers differ from those of the main sample (deepinfer/model_executor/models/mtp/dflash.py:329, deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:15, deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:93).
The default seed is random. If you do not set seq_seed, SamplingParams takes 64 random bits from Python's random module (deepinfer/sampling_params.py:85-91). So two runs differ. For repeatable output, the README asks for a fixed seq_seed together with DEEPINFER_BATCH_INVARIANT=1 and DEEPINFER_ENABLE_MTP_VARLEN=0, on NVIDIA only (README.md:28). The same seed alone is not enough: the logits themselves can differ slightly between batches. See Determinism and batch invariance.
Deep diveAll SamplingParams fields
| Field | Type, default | Effect | Checks (sampling_params.py) |
|---|---|---|---|
temperature | float, 1.0 | Divides the logits. Below 1e-5: set to 1e-5, top-p and top-k off. | ≥ 0 (deepinfer/sampling_params.py:94-95); clamp (deepinfer/sampling_params.py:76-79) |
top_p | float, 1.0 | Nucleus filter. | in (0, 1] (deepinfer/sampling_params.py:96-97) |
top_k | int, −1 | Keep the k best tokens; −1 keeps all. | −1 or 1 … int32 max (deepinfer/sampling_params.py:98-99) |
ignore_eos | bool, False | Do not stop at EOS. | — |
max_tokens | int, 16 | Maximum output tokens. 0 becomes 1. | 1 … int32 max (deepinfer/sampling_params.py:70-71, deepinfer/sampling_params.py:100-101) |
logprobs | bool, False | Ignored: the constructor deletes it. | (deepinfer/sampling_params.py:63) |
prompt_logprobs | bool, False | Also return a logprob for each prompt token. | — |
top_logprobs | int, 0 | Return the n best alternatives with their logprobs. | 0 … 1000 (deepinfer/sampling_params.py:102-103) |
first_n_token_no_eos | int, 0 | With 1: the first output token cannot be EOS. | — |
eos_id | int, 0 | The id that first_n_token_no_eos forbids. The engine overwrites it with the tokenizer's EOS id (deepinfer/engine/llm_engine.py:527). | — |
so_args | dict or None | A grammar for structured output. | — |
golden_token_ids | list or None | Force these output tokens (for tests). | — |
seq_seed | int or None | The seed of the draw; None gives a random 64-bit seed; an int is masked to 64 bits. | (deepinfer/sampling_params.py:85-91, deepinfer/sampling_params.py:104-105) |
Defaults are in the constructor signature (deepinfer/sampling_params.py:47-62). Note the order in the constructor: the checks run before the temperature clamp, so temperature=0, top_k=0 still raises ValueError for top_k (deepinfer/sampling_params.py:75-79).
Engine-wide settings that change sampling:
DEEPINFER_SAMPLER_FORCE_TOPK(default 0, off;deepinfer/environ.py:150): keep only the N largest logits of each row before all other steps (deepinfer/model_executor/layers/sampler_utils.py:166-167). This is faster, but tokens outside the N best can never be drawn.DEEPINFER_DECODE_TOKEN_BLACKLIST(default empty;deepinfer/environ.py:151): token ids whose logits the sampler sets to −∞ (deepinfer/model_executor/layers/sampler.py:168-169).
Deep diveWhat the returned logprobs mean
The fused kernel returns "processed target logprobs" (deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:51-52). These are the log-probabilities after temperature, top-p and top-k. So the logprob of an output token is the log of its final probability, not of the raw model probability.
In the worked example, "floor" was drawn. Its returned logprob is ln 0.378 = −0.973. The raw model probability was 0.264 (ln = −1.332). A removed token, for example "sofa", has logprob −∞.
top_logprobs = n selects the n best entries of the same processed row (deepinfer/model_executor/layers/sampler.py:636-648). With top_k = 2 and top_logprobs = 4, only two entries are finite.
What can go wrong#
- "Greedy" that is not exactly greedy. Temperature 0 becomes 1e-5. Tokens with exactly the same logit still share the probability, and the seed decides between them (
deepinfer/sampling_params.py:76-79). - Different output on each run. Without
seq_seed, each request gets a random seed (deepinfer/sampling_params.py:87-88). Set a seed, and read Determinism and batch invariance for the other conditions. - Invalid values.
top_k = 0,top_p = 0, a negative temperature ortop_logprobsabove 1000 raiseValueErrorwhen you make theSamplingParams(deepinfer/sampling_params.py:93-105). - No logprobs. The parameter
logprobsis ignored (deepinfer/sampling_params.py:63). Usetop_logprobsfor alternatives. Each output already carries the logprob of its token. - Logprobs that look too high. They are measured after the filters, not on the raw model distribution (deep dive "What the returned logprobs mean").
Why it matters. The same model can give careful or creative text, and repeatable or varied text, only through these few settings. The settings also change what the logprobs mean, and with speculative decoding they decide how many draft tokens the engine accepts.
Check yourself#
- 1What does a temperature below 1 do to the distribution?easy
Show answer
It makes it sharper: the likely tokens get more probability, the unlikely ones less. In the example, the best token goes from 0.435 at T = 1 to 0.641 at T = 0.5.
- 2Probabilities after temperature are (0.40, 0.25, 0.20, 0.10, 0.05). Which tokens stay withmedium
top_p = 0.6andtop_k = -1in DeepInfer, and what are their final probabilities?Show answer
The mass before each token is 0, 0.40, 0.65, 0.85, 0.95. A token goes when that mass is greater than 0.6, so the first two stay. Their final probabilities are 0.40 / 0.65 = 0.615 and 0.25 / 0.65 = 0.385. The rule is in
deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:139. - 3You createmedium
SamplingParams(temperature=0). What values dotemperature,top_pandtop_khave afterwards?Show answer
temperature = 1e-5,top_p = 1.0,top_k = -1(deepinfer/sampling_params.py:76-79). - 4Two runs send the same prompt withhard
seq_seed=7, but in the second run the request sits in another position of the batch. Why does the draw itself not change, and what can still change the output?Show answer
The random number is a hash of the seed and the token position only. It does not depend on the row order or the batch size (
deepinfer/model_executor/layers/tilelang_ops/random_sampling.py:17-20,deepinfer/model_executor/layers/tilelang_ops/random_sampling.py:79-81). But the logits can differ slightly between batches, because kernels can sum in another order. A small change can move a token across u. The README therefore also asks forDEEPINFER_BATCH_INVARIANT=1andDEEPINFER_ENABLE_MTP_VARLEN=0(README.md:28). - 5With p = (0.5, 0.3, 0.2),hard
top_k = 2andtop_p = 0.55, how many tokens does DeepInfer keep? How many would an engine keep that applies top-k first and then top-p on the renormalized probabilities?Show answer
DeepInfer keeps 2. It measures top-p on the full distribution: the mass before the second token is 0.5 ≤ 0.55. Then top-k 2 keeps both (
deepinfer/model_executor/layers/tilelang_ops/fused_sample.py:137-150). The other order renormalizes to (0.625, 0.375); the mass before the second token is 0.625 > 0.55, so it keeps only 1.