11. Sampling and stopping¶
So far we have always taken the top score (greedy). A real chat model samples from the distribution: the temperature decides how sharp the distribution is, top-k / top-p cut off the long tail, the repetition and presence penalties keep it from going round in circles, and it stops at a stop token. This chapter writes all of that as sample() and generate() — every later chapter generates with them.
What this chapter runs
ch11_sample.py, about 30 seconds.
qwen/ ├── weights.py ├── embedding.py ├── norm.py ├── mlp.py ├── rope.py ├── attention.py ├── deltanet.py ├── chunk.py ├── layer.py ├── model.py ├── cache.py └── sample.py + new in this chapter main.py … ch10_cache.py scripts of the earlier chapters ch11_sample.py + this chapter's script
+ new in this chapter · ~ changed in this chapter · the rest were written in earlier chapters
From scores to one token¶
"""采样:从 logits 里挑下一个 token。温度、top-k、top-p、重复惩罚,以及什么时候停。"""
import torch
from qwen.cache import State
def sample(logits, temperature=1.0, top_k=0, top_p=1.0, repetition_penalty=1.0, presence_penalty=0.0, generated=()):
"""logits: [词表] 的分数 -> 一个 token id。temperature=0 就是贪心(永远取最高分)。"""
logits = logits.float().clone()
if generated and (repetition_penalty != 1.0 or presence_penalty):
seen = torch.tensor(sorted(set(generated)), device=logits.device)
s = logits[seen]
s = torch.where(s > 0, s / repetition_penalty, s * repetition_penalty) # repetition penalty (multiplicative): seen tokens get positive scores lowered and negative scores pushed further down
logits[seen] = s - presence_penalty # presence penalty (subtractive): a flat deduction for anything seen; Qwen's model card recommends this one
if temperature <= 0:
return int(logits.argmax())
logits = logits / temperature # the lower the temperature, the sharper the distribution
if top_k > 0:
kth = logits.topk(top_k).values[-1]
logits[logits < kth] = float("-inf") # keep only the k highest scores
if top_p < 1.0:
sorted_logits, order = logits.sort(descending=True)
probs = sorted_logits.softmax(-1)
drop = probs.cumsum(-1) - probs > top_p # drop everything after the cumulative probability passes p (keep the one that crosses the line)
sorted_logits[drop] = float("-inf")
logits = torch.full_like(logits, float("-inf")).scatter(0, order, sorted_logits)
return int(torch.multinomial(logits.softmax(-1), 1))
def generate(model, ids, max_new_tokens=200, stop=(), on_token=None, **kw):
"""ids: [1, T] 的提示词 -> 生成的 token 列表。kw 原样传给 sample;每生成一个 token 调一次 on_token。"""
state = State(model)
logits = model(ids, state)[0, -1] # prefill: the whole prompt in one pass
out = []
for _ in range(max_new_tokens):
nxt = sample(logits, generated=out, **kw)
out.append(nxt)
if on_token:
on_token(nxt)
if nxt in stop:
break
logits = model(torch.tensor([[nxt]], device=ids.device), state)[0, -1] # decode: only one new token per step
return out
The order matters: penalties first (they change the scores), then divide by the temperature, then top-k, then top-p, and finally the softmax and the draw.
- Temperature \(T\): \(\mathrm{softmax}(z / T)\). \(T \to 0\) degenerates into greedy; \(T = 1\) is the model's own distribution; \(T > 1\) is more random.
- top-k: keep only the \(k\) highest scores (Qwen recommends 20).
- top-p (nucleus): accumulate probabilities from high to low and drop everything after the cumulative sum passes \(p\) — the number of candidates adapts to how sharp the distribution is.
- Repetition penalty (multiplicative) and presence penalty (subtractive): both act on tokens already generated. Qwen3.5's model card recommends the presence penalty (
presence_penalty = 1.5 ~ 2.0), because the multiplicative penalty behaves unintuitively on negative logits. - Stopping:
<|im_end|>(this turn is over) or<|endoftext|>. Note that sampling can draw another token at the moment the model wants to stop, so "it never stops" is sometimes a sampling-parameter problem.
generate() is chapter 10's loop plus sample(): prefill once, then one token per step; the on_token callback is for chapter 12's streaming output.
One prompt, several ways of sampling¶
"""第 11 章:看一个位置的分布,比较几种采样参数,演示惩罚"""
import os
import torch
from tokenizers import Tokenizer
from qwen.cache import State
from qwen.model import Model
from qwen.sample import generate, sample
from qwen.weights import find_model
torch.set_num_threads(max(1, os.cpu_count() // 2))
path = find_model()
model = Model.from_pretrained(path)
tk = Tokenizer.from_file(str(path / "tokenizer.json"))
STOP = (tk.token_to_id("<|im_end|>"), tk.token_to_id("<|endoftext|>"))
prompt = "<|im_start|>user\n写一句关于秋天的话。<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
ids = torch.tensor([tk.encode(prompt).ids])
logits = model(ids, State(model))[0, -1] # 1. what the first token's distribution looks like
for T in (0.5, 1.0, 2.0):
p = torch.softmax(logits / T, dim=-1)
top = p.topk(5)
print(f"温度 {T}:top-5 概率 {[f'{v:.2f}' for v in top.values.tolist()]},"
f"{[tk.decode([i]) for i in top.indices.tolist()]},累计到 0.9 要 {int((p.sort(descending=True).values.cumsum(0) < 0.9).sum()) + 1} 个候选")
torch.manual_seed(0) # 2. one sentence per sampling setting
for kw in (dict(temperature=0), dict(temperature=1.0, top_k=20, top_p=1.0, presence_penalty=2.0),
dict(temperature=0.7, top_p=0.8, top_k=20), dict(temperature=2.0)):
out = generate(model, ids, max_new_tokens=50, stop=STOP, **kw)
print(f"{str(kw):<75} {tk.decode(out)!r}")
prompt2 = "<|im_start|>user\n把“好”重复写 50 次。<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n" # 3. penalties: deliberately provoking repetition
ids2 = torch.tensor([tk.encode(prompt2).ids])
for kw in (dict(), dict(repetition_penalty=1.3), dict(presence_penalty=2.0)):
out = generate(model, ids2, max_new_tokens=30, stop=STOP, temperature=0, **kw)
print(f"贪心 + {str(kw):<32} {tk.decode(out)!r}")
温度 0.5:top-5 概率 ['0.48', '0.30', '0.18', '0.03', '0.01'],['秋风', '秋天的', '秋天', '秋', '金秋'],累计到 0.9 要 3 个候选
温度 1.0:top-5 概率 ['0.27', '0.21', '0.16', '0.06', '0.04'],['秋风', '秋天的', '秋天', '秋', '金秋'],累计到 0.9 要 36 个候选
温度 2.0:top-5 概率 ['0.02', '0.01', '0.01', '0.01', '0.01'],['秋风', '秋天的', '秋天', '秋', '金秋'],累计到 0.9 要 97435 个候选
{'temperature': 0} '秋风卷起落叶,铺就了一条金黄的铺路,把整个世界都染成了温暖的金色。'
{'temperature': 1.0, 'top_k': 20, 'top_p': 1.0, 'presence_penalty': 2.0} '秋风如刀,却已温柔地抚平了世间所有的寒凉与躁动。'
{'temperature': 0.7, 'top_p': 0.8, 'top_k': 20} '秋天的脚步总是带着一种温柔的迟暮,它像一位隐退的诗人,在枝头低吟着关于收获与寂寥的诗意。'
{'temperature': 2.0} '预约 farbaketindeki Tiempo how员维打造的宝物句.律呃⾌ար ..... пр1 materie ots nutlest dyn hvor!.-lnd教育管理 analytic fantast homeкультур Jennings服务平台**;内控edintele rendszer 성과를 granšie وبحಥ oci dateFormat薄弱_counts mentale te'
贪心 + {} '好,好,好,好,好,好,好,好,好,好,好,好,好,好,好,'
贪心 + {'repetition_penalty': 1.3} '好,好的。'
贪心 + {'presence_penalty': 2.0} '好,好,好,好,好。\n好,好,好,好,好。\n好,好,好,好,'
At temperature 0.5 three candidates already add up to 0.9; at 2.0 it takes nearly a hundred thousand, and what comes out is fragments of every language. In the penalty group, the model would faithfully repeat "好," (the user did ask for it); a repetition penalty of 1.3 makes it stop after two, and a presence penalty of 2.0 makes it keep repeating in a different pattern — penalties are a blunt knife, and whether and how far to open them depends on the scenario.
How to explain it
To explain sampling, get the order right: penalties → temperature → top-k → top-p → draw; say for each step whether it changes the scores or the candidate set. Then two frequently asked points: top-p beats top-k because the number of candidates adapts; the repetition and presence penalties differ in multiplying versus subtracting, and in how they treat negative logits. Finally mention that an inference engine samples on the GPU for a whole batch of requests in parallel, each request with its own parameters.
Summary¶
-
sample(): penalties → temperature → top-k → top-p → multinomial draw;temperature=0is greedy. -
generate(): prefill + token-by-token decode + stop tokens, with a callback. - Qwen3.5's recommendation: non-thinking mode
temperature=1.0, top_k=20, presence_penalty=2.0(text tasks).
File created in this chapter: qwen/sample.py.
Exercises for this chapter (1)
Write the code and run the tests in your browser, or judge locally with python practice/judge.py. The exercises are in Chinese for now.
- 207. 采样前的分数处理:惩罚、温度、top-k、top-p (easy, needs PyTorch)
Found a mistake or a better way to say it? Edit this page on GitHubReport an issue