🦙 llama.cpp 图解教程llama.cpp Visual Guide 第四部分 · llama 推理内部Part 4 · Inside llama inference 21 / 40
第四部分 · llama 推理内部Part 4 · Inside llama inference

采样Sampling

上一课词表把文字变成 token id 喂进模型,模型一路计算(M4a),最后在输出层吐出一排 logits——词表里每个 token 各对应一个原始分数,分数越高代表模型越"看好"它当下一个词。可下一个词只能有一个,怎么从几万个分数里挑出它?这一课讲的采样(sampling),就是"从一排分数到一个 token"的最后一步。

采样远不止"选最大的"那么简单。每次都选最高分,模型会死板、重复、毫无新意;可纯随机又会胡言乱语。真正的采样是一套裁剪 + 塑形 + 抽选的组合拳:先划掉没希望的候选、再调分布的软硬、压一压老重复的词,最后才按概率抽一个。llama.cpp 把这些手段做成一个个可插拔的采样器,串成一条采样链

🔌 生活类比
采样像摇号抽奖logits 是每个号码的原始权重,温度调节"凭实力还是凭运气",top-k/top-p 先划掉没希望的号,惩罚项压低最近老出现的号,最后 dist 按权重摇一个出来——或者 greedy 干脆选权重最大的那个。同一堆号码,配不同的规则,摇出来的"性格"就完全不同。

从 logits 到 token

logits
每个 token 一个分数
->
penalties
压低重复
->
top_k / top_p
裁剪候选
->
temp
塑形分布
->
dist
选一个 token

先看清采样的输入和输出。输入是一个候选数组:词表里每个 token 一条记录,含 token id、它的 logit(原始分数)、以及待会儿算出来的概率 p。输出是其中一个被选中的 id。采样要做的,就是在这个数组上一通操作,最后挑出一条。

这个候选数组在 llama.cpp 里叫 llama_token_data_array,每条记录是 llama_token_data。整条采样管线,本质上就是不断改写这个数组:有的采样器把某些候选的 logit 砸成负无穷(等于划掉),有的重新算概率、重新排序,最后一步从中选定一个、把它的下标记在数组的 selected 字段上。

🌍 宏观理解
为什么要分这么多步、而不是一步选完?因为"选下一个词"需求多样:写代码要严谨(偏确定),写诗要发散(偏随机),还要避免老车轱辘话来回说。把这些需求拆成一个个独立的小操作、按需组合,远比写一个巨大的"全能采样函数"灵活。

于是管线大致长这样:先用惩罚压低重复,再用 top-k/top-p 砍掉长尾候选,接着用温度调节剩下分布的软硬,最后用 dist 按概率抽一个(或 greedy 直接取最大)。每一步只做一件小事,叠起来就是一套完整的采样策略。

要强调的是,这套管线只动 logits/概率、不碰模型本身。模型每步老老实实算出同一排 logits,至于怎么从中选词,全由采样这层说了算。所以"换个生成风格"根本不用动模型,调调采样参数即可——这也是同一个模型能时而严谨、时而天马行空的原因。

不妨把这排 logits 想象成一座高低起伏的山脉:模型越看好的词,峰就越高。采样要做的,就是按这座山的形状来取舍——只在高峰附近选(保守),还是连山脚的小丘也给点机会(发散)。后面的每个采样器,其实都在重塑这座山的轮廓,再决定从哪儿落子。这个画面记住了,后面的 top-k、温度就都好理解了。把这条链路用一个具体例子走一遍就清楚了:

追踪一次采样:5 个候选词,看一排 logits 怎么一步步变成最终选中的一个 token(数字为示意)。
① logits
3.22.11.00.5-0.3
cat / dog / sky / run / blue
÷T
T=0.7
② 温度缩放
4.63.01.40.7-.4
T<1 放大差距
top-k
k=3
③ 截断候选
4.63.01.40.7-.4
只留分数最高的 3 个
softmax
top-p .9
④ 概率 → 采样
.78.18.04
按概率抽一个 → cat

采样器接口

// 简化自 include/llama.h
struct llama_sampler_i {
    const char * (*name)  (...);                          // 名字(可空)
    void (*accept)(llama_sampler *, llama_token);          // 喂回选中 token(可空)
    void (*apply) (llama_sampler *, llama_token_data_array * cur_p); // 改/排候选(必需)
    void (*reset)(llama_sampler *);                        // 清状态(可空)
};
struct llama_sampler { const llama_sampler_i * iface; llama_sampler_context_t ctx; };
必需apply(cur_p)
改写候选数组:划掉 / 重排 / 重算概率——每个采样器的核心
可空accept · reset · name · clone · free
accept 把选中 token 喂回有状态采样器;其余按需实现
状态llama_sampler_context_t ctx
每个采样器私有的账本:penalties 记历史、mirostat 记反馈

看看一个采样器到底是什么。llama_sampler_i 就是一组函数指针:apply(核心,改写候选数组)、accept(把选中的 token 喂回来给有状态采样器记账)、reset(清状态),还有 name/clone/free。配上一块状态 ctx,就构成一个 llama_sampler

这里 apply 是唯一必需的——它拿到候选数组,按自己的规则改一改(划掉一些、重排一下、重算概率)。accept 可空,只有"有记忆"的采样器才用得上:惩罚项要记住前面出过哪些 token、mirostat 要根据反馈调参,它们都靠 accept 把"刚选中的 token"收进自己的状态。

🌍 宏观理解
这种"一组函数指针 + 一块状态"的设计,你应该眼熟——它就是 L10 后端、L19 记忆接口那套接口 + 实现的又一次运用。每个采样器只要实现这几个函数,就能被统一调度;引擎不在乎你内部是 top-k 还是 mirostat,只管按顺序调 apply。

把采样器抽象成统一接口,最大的好处是可组合。既然它们长一个样,就能像积木一样排成一队,挨个作用在同一个候选数组上。下一节的"采样链",就是这种可组合性的直接产物。

顺带说状态 ctx:它是每个采样器私有的小账本。无状态的采样器(如 top_k)ctx 几乎是空的;有状态的(penalties/mirostat/grammar)则把历史、参数、反馈都存在这里。采样器之间互不干扰,各记各的账。

为什么接口里好几个函数都标着"可空"?因为不是每个采样器都用得上每件事。像 top_k 这种纯粹"裁一刀"的,根本不需要记忆,也就不必实现 accept;而 reset 只在复用同一个采样器跑多段生成时才有意义。把这些做成可选,让最简单的采样器可以只写一个 apply,既省事又清晰——接口只要求"必需的那件事",其余按需。

采样链

1

chain_init

建一条空的采样链(llama_sampler_chain)。

2

chain_add 若干采样器

按顺序加入 penalties、top_k、top_p、temp、dist……每个都是独立采样器。

3

sample

对候选数组按加入顺序逐个 apply,最后一个(dist/greedy)选出 token。

4

accept

把选中 token 喂回链,让有状态采样器(penalties 等)记住它。

采样链 llama_sampler_chain 本身也是一个采样器——它内部装着一串子采样器,它的 apply 就是按加入顺序把每个子采样器的 apply 挨个跑一遍。这是典型的"组合模式":一条链对外看也是一个采样器,对内是许多采样器的队列。

# 伪代码: 组一条采样链
chain = llama_sampler_chain_init(params)
chain.add(llama_sampler_init_penalties(...))    # 压低重复
chain.add(llama_sampler_init_top_k(40))         # 留前 40
chain.add(llama_sampler_init_top_p(0.95, 1))     # 核采样
chain.add(llama_sampler_init_temp(0.8))          # 温度
chain.add(llama_sampler_init_dist(seed))         # 按概率随机选
id = llama_sampler_sample(chain, ctx, -1)         # 跑全链 -> 返回 token id

用起来很直观:先 chain_init 建空链,再 chain_add 按想要的顺序把采样器一个个塞进去,最后 llama_sampler_sample 一把梭——它读出某位置的 logits、组成候选数组、跑完整条链、返回选中的 token id,并顺手把这个 token accept 回去。

顺序很重要。同样几个采样器,排列不同,结果可能不同:一般先做惩罚和裁剪(缩小候选集),再做温度(调软硬),最后才是 dist/greedy(真正选定)。把"选定"放最后,是因为前面每一步都在为这"临门一脚"准备一个更合理的候选分布。

🔬 细节 / 源码对应
注意链会接管加进来的采样器的所有权——一旦 add 进去,释放链时会一并释放它们,你不用单独操心。这种"加进去就交给链管"的约定,让组装采样策略很省心:拼好一条链,用完整体释放即可。

这套"链"的设计,本质上是把"采样策略"变成了数据(一串采样器配置),而不是写死的代码。于是用户在命令行/配置里调几个参数,就能拼出千变万化的采样行为,引擎主干一行都不用改——又是一次"会变的部分集中起来"的体现。

举个顺序影响结果的例子。假如你把温度放在 top-p 之前,温度会先把分布整体烫平、再让 top-p 去圈范围,圈出来的核就偏大、偏发散;反过来先 top-p 圈定再升温,则是在一个已经收紧的小集合里调随机性,结果更可控。同样的零件、不同的次序,最终"性格"就有微妙差别——这正是把顺序交给用户配置的价值。

实际项目里,这条链常有个约定俗成的默认顺序。llama.cpp 的上层(common)大致按"惩罚 -> 裁剪(top-k/top-p/min-p 等)-> 温度 -> dist"来排。你不必死记,但记住那个大原则就够了:先缩小候选、再调软硬、最后才抽签。绝大多数采样策略,都是在这条主轴上加加减减。

常见采样器与 greedy vs dist

采样器作用
greedy选 logit 最大的(argmax,确定)
dist按概率随机选(靠 seed)
top_k只留前 k 个候选
top_p核采样:留累积概率达 p 的最小集
min_p留概率不低于"最大值 × p"的候选
temp缩放 logits,调随机性
penalties压低重复/高频/已出现的 token
mirostat动态调温,稳住困惑度

来认认常用的几个采样器。它们各管一段:有的负责"裁"(缩小候选集),有的负责"塑"(改分布形状),有的负责"选"(最终拍板)。

🔬 细节 / 源码对应
先说最终拍板的两个:greedy 永远选 logit 最大的那个——确定性,同样输入永远同样输出,适合要复现、要严谨的场景;dist 则把 logits 经 softmax 变成概率,再按概率随机抽一个,带来多样性,靠随机种子 seed 控制。一条链最后接 greedy 还是 dist,决定这次生成是"确定"还是"随机"。

再说"裁"的两位主力:top_k 留分数最高的固定 k 个、其余划掉;top_p(核采样)按概率从高到低累加、留到累计达 p 为止——候选数随分布自适应(分布尖时留得少、平时留得多)。两者常配合:先 top_k 砍掉长尾,再 top_p 自适应收口。

"塑"的代表是温度 temp:把 logits 除以一个温度值 T 再 softmax。T 小则分布更尖(更确定),T 大则更平(更随机)。它不改候选集,只改"软硬"。还有 penalties 压低重复、mirostat 动态调温稳住困惑度等,各有专长。

⚠ 注意
2023 年那套全局采样函数(llama_sample_top_k/llama_sample_top_p/llama_sample_temperature 等)已经全部移除,统一换成了"采样器对象 + 链"这套模型。看老教程别再找那些函数了。

单独说说 penalties 这一类,因为它最贴近日常体验。它盯着最近生成过的 token,对老重复的词施加惩罚(调低 logit),于是模型不容易陷进"复读机"式的循环。常见的有重复惩罚、频率惩罚、存在惩罚几种口味,分别对应"出现过就罚""出现越多越罚""只要出现就一视同仁地罚"。调它们,能在"连贯"和"啰嗦"之间找平衡。

接回主回路与衔接

把采样接回主回路:每生成一个 token,llama_decode(L17)算出 logits,采样链从中选一个 id,这个 id 一边经词表(L20)变回文字显示、一边被包成新 batch 喂回 llama_decode 进入下一步。采样就是自回归循环里"挑下一个词"那一环。

还有一个和 grammar(L23)的衔接要先打招呼:语法约束本质上也是一个采样器(它的 apply 把不合语法的 token 划掉)。但它通常塞进主链,而是作为独立对象、按 grammar_first 决定在链前还是链后单独施加——这一课熟悉了采样器接口,L23 再看 grammar 就水到渠成。

🌍 宏观理解
所以这一课真正要带走的,是一个心智模型:采样 = 在候选数组上排一队小变换,最后选一个。模型决定"每个词多大概率合适",采样决定"这次到底挑谁"。理解了它,你就理解了为什么同一个模型、同一句提示,调调参数就能从"一本正经"变到"天马行空"。

顺带点一个实用细节:要让随机生成可复现,关键在 dist 的那个随机种子 seed。固定 seed、固定采样参数,同一段提示就能跑出完全一样的结果——这在调试、对比实验时极有用。反过来,想要每次都不一样,让 seed 随时间变即可。确定性到底掌握在你手里。

最后澄清一个常见误解:采样调不出模型本来没有的能力。它只能在模型给出的那排 logits 上做文章——好的采样能让一个模型扬长避短(少出昏招、保持多样),但变不出模型压根学不会的知识。所以效果不好时,先分清是"模型不行"还是"采样没调好":前者要换模型/微调(L24),后者调调参数即可。

1 温度(temperature)到底在做什么? 点击展开

温度 T 的作用是缩放 logits:把每个分数除以 T,再 softmax 变概率。T=1 是原样;T 小于 1,大分数被进一步放大、小分数被压扁,分布更尖锐,模型更倾向选最可能的词(更确定、更保守);T 大于 1 则把差距拉平,分布更平坦,冷门词也有机会(更随机、更有创意)。

两个极端很有意思:T 趋近 0,分布尖到只剩最大那个,温度采样就退化成 greedy;T 很大时分布趋于均匀,几乎是瞎猜。所以温度是一个连续的"确定 <-> 随机"旋钮,greedy 不过是它的一个极端特例。

要记住温度只改软硬、不改候选集——它不删任何 token,只重新分配大家的概率。删候选是 top-k/top-p 的活。两类操作正交,组合才好用:先用 top-p 圈定合理候选集,再用温度调这个集合内部的随机程度。

2 top-k 和 top-p 有何不同? 点击展开

top_k固定个数:把候选按分数排序,留前 k 个、其余划掉。简单直接,但有个毛病——分布尖时 k 个里混进很多没希望的,分布平时又可能把好候选挡在门外。它不看分布形状,只数个数。

top_p(核采样 nucleus)留累积概率达 p 的最小集合:按概率从高到低累加,加到超过 p 就停。它的候选数是自适应的——分布尖时可能只留两三个,分布平时可能留几十个。这种"按概率密度收口"往往比固定 k 更合理。

实践中常两者叠用:先 top_k(比如 40)砍掉绝大多数长尾、控制开销,再 top_p(比如 0.95)在剩下的里自适应收口。一个管"最多留多少",一个管"按质量留多少",配合起来既快又稳。

3 为什么做成"链"而不是一个大函数? 点击展开

因为可组合 + 可配置 + 状态隔离。每个采样器是独立小部件、自带状态(penalties 记历史、mirostat 记反馈、dist 记随机数),顺序可调、增删自由。用户在配置里写一串采样器名字和参数,引擎照单拼出一条链——想要什么策略就拼什么,不必改一行引擎代码。

对比"一个写死的大采样函数":那样每加一种新手段都得改主函数、各种 if 越堆越多,参数也纠缠不清。拆成链之后,新增一种采样器只是多写一个独立实现,对已有的零影响。这正是 L11 算子、L16 建图积木一脉相承的"小部件组合出复杂行为"。

还有个细节:grammar(L23)这种采样器通常不进主链,而是按 grammar_first 在链前或链后单独施加。这说明"链"也不死板——它给特殊约束留了在合适位置插入的余地,足够灵活。

✅ 关键要点
  • 采样 = 在候选数组 llama_token_data_array 上裁剪塑形、最后选一个 token。
  • 采样器 llama_sampler_iapply(必需,改候选)+ accept(喂回选中 token)+ reset;配状态 ctxllama_sampler
  • 采样链:chain_init -> chain_add 若干采样器 -> sample,按顺序逐个 apply。
  • greedy=选最大(确定)、dist=按概率随机;top_k/top_p 裁候选、temp 调软硬、penalties/mirostat 各有专长。
  • 旧全局 llama_sample_* 已移除,统一为采样器对象 + 链;grammar(L23)是链外的特殊采样器。
💡 设计洞察
把采样从"一个写死的大函数"拆成"一串可插拔的小变换",是典型的责任链 / 管道设计——和你在 ggml 算子链(L09)、建图积木(L16)里见过的是同一种味道:用小而独立的部件,组合出复杂多变的行为。于是"换一种生成风格"只是换链里几个环、调几个数,模型和引擎主干纹丝不动。读懂采样,你就握住了把模型从"一本正经"调到"天马行空"的那几个旋钮。

🧪 自测 · 想一想为什么这么设计

1. greedy 采样选哪个 token?
  1. 最后一个
  2. logit 最大的那个(argmax,确定性)
  3. logit 最小的那个
  4. 随机一个
看答案与解析 点击展开
答案:B。greedy 永远取 logit 最大的候选(argmax),同样输入永远同样输出;dist 才是按概率随机抽。要复现/严谨用 greedy,要多样性用 dist。
2. top_p(核采样)保留哪些候选?
  1. 按概率从高到低累加、达到阈值 p 的最小候选集合
  2. 全部候选
  3. 固定的前 50 个
  4. 概率大于 p 的全部
看答案与解析 点击展开
答案:A。top_p 按概率累加到达 p 为止,候选数随分布自适应(尖时少、平时多);top_k 才是固定个数。两者常配合:先 top_k 砍长尾,再 top_p 收口。
3. 把采样做成"链"(chain)的主要好处是什么?
  1. 可组合——按顺序施加多个独立、可配置的采样器
  2. 省内存
  3. 只能用一个采样器
  4. 跑得更快
看答案与解析 点击展开
答案:A。链把采样策略变成数据:每个采样器是独立小部件、自带状态,顺序可调、增删自由,用户调参就能拼出任意策略,引擎主干不动。快/省内存不是它的设计目的。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 结合 L20 和 L23,说说采样器为什么是在"词表的 token 空间"里工作,以及 grammar 如何作为一种"掩码"来约束这一步。

Last lesson the vocab turned text into token ids fed to the model; the model computes all the way through (M4a) and finally, at the output layer, emits a row of logits - one raw score per token in the vocab, a higher score meaning the model "favors" it more as the next word. But the next word can be only one, so how do you pick it from tens of thousands of scores? This lesson's topic, sampling, is that last step "from a row of scores to one token".

Sampling is far more than "pick the max". Always picking the top score makes the model rigid, repetitive, dull; but pure randomness babbles. Real sampling is a combo of prune + shape + draw: first cut hopeless candidates, then tune the distribution's softness, push down words that keep repeating, and only then draw one by probability. llama.cpp makes these means into pluggable samplers, strung into a sampler chain.

🔌 Analogy
Sampling is like a lottery draw: logits are each ticket's raw weight, temperature tunes "by skill or by luck", top-k/top-p first strike out hopeless tickets, the penalty pushes down tickets that keep showing up lately, and finally dist draws one by weight - or greedy simply takes the heaviest. The same pile of tickets, with different rules, draws an entirely different "personality".

From logits to a token

logits
one score per token
->
penalties
damp repeats
->
top_k / top_p
prune candidates
->
temp
shape distribution
->
dist
pick one token

First, see sampling's input and output clearly. The input is a candidate array: one record per token in the vocab, holding the token id, its logit (raw score), and a probability p computed later. The output is one selected id among them. What sampling does is work over this array and finally pick one record.

This candidate array is called llama_token_data_array in llama.cpp, each record a llama_token_data. The whole pipeline is essentially repeatedly rewriting this array: some samplers slam certain candidates' logit to negative infinity (a strike-out), some recompute probabilities and re-sort, and the last step selects one, recording its index in the array's selected field.

🌍 Big picture
Why so many steps instead of picking in one shot? Because "pick the next word" has varied needs: code wants rigor (lean deterministic), poetry wants divergence (lean random), and you must avoid rehashing the same phrases. Splitting these needs into independent small operations to combine on demand is far more flexible than one giant "do-it-all sampling function".

So the pipeline looks roughly like this: penalties damp repeats first, top-k/top-p chop the long tail, then temperature tunes the softness of what remains, and finally dist draws one by probability (or greedy takes the max). Each step does one small thing; stacked together they are a complete sampling strategy.

Worth stressing: this pipeline touches only logits/probabilities, never the model itself. The model dutifully computes the same row of logits each step; how a word is chosen from them is entirely up to the sampling layer. So "change the generation style" needs no change to the model, just sampling parameters - which is why one model can be rigorous one moment and wildly imaginative the next.

Picture this row of logits as a mountain range of peaks and valleys: the more the model favors a word, the higher its peak. Sampling chooses by the shape of this range - pick only near the high peaks (conservative), or give the foothills a chance too (divergent). Every later sampler is really reshaping this range's outline before deciding where to land. Hold this picture, and top-k and temperature later all become easy. Walking one concrete example through this pipeline makes it click:

Tracing one sampling step: 5 candidate words - watch a row of logits become the single chosen token (numbers illustrative).
(1) logits
3.22.11.00.5-0.3
cat / dog / sky / run / blue
/T
T=0.7
(2) temperature
4.63.01.40.7-.4
T<1 widens gaps
top-k
k=3
(3) truncate
4.63.01.40.7-.4
keep the top 3 only
softmax
top-p .9
(4) probs -> sample
.78.18.04
draw one by probability -> cat

The sampler interface

// simplified from include/llama.h
struct llama_sampler_i {
    const char * (*name)  (...);                          // name (nullable)
    void (*accept)(llama_sampler *, llama_token);          // feed back chosen token (nullable)
    void (*apply) (llama_sampler *, llama_token_data_array * cur_p); // edit/rank candidates (required)
    void (*reset)(llama_sampler *);                        // clear state (nullable)
};
struct llama_sampler { const llama_sampler_i * iface; llama_sampler_context_t ctx; };
requiredapply(cur_p)
rewrite the candidate array: strike out / re-rank / recompute probs - each sampler's core
nullableaccept / reset / name / clone / free
accept feeds the chosen token back to stateful samplers; the rest as needed
statellama_sampler_context_t ctx
each sampler's private ledger: penalties keeps history, mirostat feedback

See what a sampler actually is. llama_sampler_i is just a set of function pointers: apply (the core, rewrites the candidate array), accept (feeds the chosen token back so stateful samplers can keep tally), reset (clear state), plus name/clone/free. With a state blob ctx, it forms a llama_sampler.

Here apply is the only required one - it takes the candidate array and edits it by its own rule (strike some out, re-rank, recompute probabilities). accept is nullable, needed only by samplers with memory: the penalty must remember which tokens appeared before, mirostat must adjust by feedback; both use accept to take "the just-chosen token" into their state.

🌍 Big picture
This "a set of function pointers + a state blob" design should look familiar - it is another use of that interface + implementation pattern from L10's backends and L19's memory interface. Each sampler need only implement these functions to be scheduled uniformly; the engine does not care whether you are top-k or mirostat inside, it just calls apply in order.

Abstracting samplers into a unified interface buys composability above all. Since they look alike, they can line up like blocks, each acting on the same candidate array. Next section's "sampler chain" is the direct product of this composability.

A word on the state ctx: it is each sampler's private ledger. A stateless sampler (like top_k) has an almost-empty ctx; a stateful one (penalties/mirostat/grammar) keeps history, parameters, feedback here. Samplers do not interfere with one another, each keeping its own books.

Why are several interface functions marked "nullable"? Because not every sampler needs everything. A pure "one cut" sampler like top_k needs no memory and thus need not implement accept; reset matters only when reusing one sampler across several generations. Making these optional lets the simplest sampler write just an apply - tidy and clear; the interface demands only "the required thing", the rest on demand.

The sampler chain

1

chain_init

Build an empty sampler chain (llama_sampler_chain).

2

chain_add several samplers

Add penalties, top_k, top_p, temp, dist... in order; each is an independent sampler.

3

sample

apply over the candidate array in add-order; the last (dist/greedy) picks the token.

4

accept

Feed the chosen token back into the chain so stateful samplers (penalties etc.) remember it.

The sampler chain llama_sampler_chain is itself a sampler - it holds a list of child samplers, and its apply simply runs each child's apply in add-order. This is the classic "composite" pattern: a chain looks like one sampler outside, while inside it is a queue of many.

# pseudocode: build a sampler chain
chain = llama_sampler_chain_init(params)
chain.add(llama_sampler_init_penalties(...))    # damp repeats
chain.add(llama_sampler_init_top_k(40))         # keep top 40
chain.add(llama_sampler_init_top_p(0.95, 1))     # nucleus
chain.add(llama_sampler_init_temp(0.8))          # temperature
chain.add(llama_sampler_init_dist(seed))         # draw by probability
id = llama_sampler_sample(chain, ctx, -1)         # run the whole chain -> return a token id

It is straightforward to use: chain_init an empty chain, chain_add samplers in the order you want, and finally llama_sampler_sample does it all - it reads a position's logits, forms the candidate array, runs the whole chain, returns the chosen token id, and conveniently accepts that token back.

Order matters. The same few samplers in a different arrangement can give different results: generally do penalties and pruning first (shrink the candidate set), then temperature (tune softness), and dist/greedy last (the actual selection). Putting "selection" last is because every earlier step is preparing a more reasonable candidate distribution for that "final kick".

🔬 Details / source
Note the chain takes ownership of the samplers added to it - once added, freeing the chain frees them too, so you need not track them separately. This "add it and the chain manages it" convention makes assembling a strategy carefree: build a chain, free it whole when done.

This "chain" design essentially turns "the sampling strategy" into data (a list of sampler configs) rather than hardcoded code. So a user tuning a few parameters on the command line / config can assemble endlessly varied sampling behavior with not a line of the engine trunk changed - once more "gather the parts that vary".

An example of order affecting the result. If you put temperature before top-p, temperature first flattens the whole distribution and then top-p draws the range, so the nucleus comes out larger and more divergent; conversely, top-p fencing first then heating tunes randomness within an already-tightened small set, more controllable. Same parts, different order, subtly different "personality" - exactly the value of leaving order to user config.

In real projects this chain often has a conventional default order. llama.cpp's upper layer (common) roughly arranges "penalties -> pruning (top-k/top-p/min-p etc.) -> temperature -> dist". You need not memorize it, but the big principle suffices: shrink candidates first, tune softness next, draw last. The vast majority of sampling strategies are just additions and subtractions along this main axis.

Common samplers and greedy vs dist

SamplerWhat it does
greedypick the max logit (argmax, deterministic)
distdraw randomly by probability (via seed)
top_kkeep only the top k candidates
top_pnucleus: keep the smallest set with cumulative prob p
min_pkeep candidates with prob no less than "max x p"
tempscale logits, tune randomness
penaltiesdamp repeated/frequent/seen tokens
mirostatdynamically tune temperature to hold perplexity

Meet the common samplers. Each owns a stage: some "prune" (shrink the candidate set), some "shape" (change the distribution's form), some "select" (the final call).

🔬 Details / source
First the two that make the final call: greedy always picks the max logit - deterministic, same input always same output, good for reproducible, rigorous scenarios; dist softmaxes logits into probabilities and then draws one randomly by probability, bringing diversity, controlled by a random seed. Whether a chain ends in greedy or dist decides if this generation is "deterministic" or "random".

Then the two pruning mainstays: top_k keeps the fixed top k by score and strikes out the rest; top_p (nucleus) accumulates by probability from high to low, keeping until the cumulative reaches p - the candidate count is adaptive (few when the distribution is peaked, many when flat). The two often pair: top_k chops the long tail, then top_p closes adaptively.

The "shape" representative is temperature temp: divide logits by a temperature T then softmax. Small T makes the distribution peakier (more deterministic), large T flatter (more random). It does not change the candidate set, only the softness. There are also penalties to damp repeats, mirostat to dynamically tune temperature and hold perplexity, each with a specialty.

⚠ Heads-up
The 2023-era global sampling functions (llama_sample_top_k/llama_sample_top_p/llama_sample_temperature etc.) are all removed, unified into this "sampler object + chain" model. Do not go looking for those functions in old tutorials.

A word on the penalties family, since it is closest to everyday experience. It watches recently generated tokens and penalizes oft-repeated words (lowering their logit), so the model is less likely to fall into a "broken record" loop. Common flavors are repeat, frequency, and presence penalties - "penalize if seen", "penalize more the more it appears", "penalize once seen, flatly". Tuning them balances "coherent" against "verbose".

Back to the main loop and the hand-off

Connecting sampling back to the main loop: per generated token, llama_decode (L17) computes logits, the chain picks one id from them, and this id is both turned back into text via the vocab (L20) for display and wrapped into a new batch fed back to llama_decode for the next step. Sampling is the "pick the next word" link in the autoregressive loop.

One hand-off with grammar (L23) to flag early: a grammar constraint is essentially a sampler too (its apply strikes out tokens that break the grammar). But it usually does not go into the main chain; it is a separate object applied before or after the chain per grammar_first - having learned the sampler interface here, grammar in L23 will come naturally.

🌍 Big picture
So what to truly take from this lesson is a mental model: sampling = line up a queue of small transforms over the candidate array, then pick one. The model decides "how likely each word is appropriate", sampling decides "who exactly gets picked this time". Understand it and you see why one model, one prompt, can go from "buttoned-up" to "wildly free" just by tuning parameters.

A practical detail in passing: to make random generation reproducible, the key is dist's random seed. Fix the seed and the sampling parameters, and the same prompt runs to identical results - invaluable for debugging and comparison experiments. Conversely, to vary each run, let the seed change with time. Determinism is firmly in your hands.

Finally, clear a common misconception: sampling cannot conjure abilities the model lacks. It can only work on the row of logits the model gives - good sampling lets a model play to its strengths (fewer blunders, kept diversity), but cannot invent knowledge the model never learned. So when results are poor, first tell "the model is weak" from "the sampling is mistuned": the former needs a different model / fine-tuning (L24), the latter just parameter tweaks.

1 What does temperature actually do? Click to expand

Temperature T scales logits: divide each score by T, then softmax into probabilities. T=1 is as-is; T below 1 amplifies big scores further and squashes small ones, making the distribution peakier, the model leaning toward the most likely word (more deterministic, more conservative); T above 1 flattens the gaps, making it flatter so longshots get a chance (more random, more creative).

The two extremes are interesting: as T approaches 0, the distribution peaks down to just the max, and temperature sampling degenerates into greedy; at very large T the distribution nears uniform, almost blind guessing. So temperature is a continuous "deterministic <-> random" knob, and greedy is merely one extreme special case of it.

Remember temperature only changes softness, not the candidate set - it deletes no token, only redistributes everyone's probability. Deleting candidates is top-k/top-p's job. The two operations are orthogonal and combine well: use top-p to fence a reasonable candidate set, then temperature to tune the randomness within that set.

2 How do top-k and top-p differ? Click to expand

top_k keeps a fixed count: sort candidates by score, keep the top k, strike out the rest. Simple and direct, but with a flaw - when the distribution is peaked, many hopeless ones sneak into the k, and when it is flat, good candidates may be shut out. It does not look at the distribution's shape, only counts.

top_p (nucleus) keeps the smallest set whose cumulative probability reaches p: accumulate by probability from high to low, stopping once it passes p. Its candidate count is adaptive - maybe just two or three when peaked, dozens when flat. This "closing by probability density" is often more reasonable than a fixed k.

In practice the two are often stacked: top_k (say 40) chops the vast long tail and bounds cost, then top_p (say 0.95) closes adaptively among the rest. One governs "how many at most", the other "how many by quality"; together they are both fast and steady.

3 Why a "chain" rather than one big function? Click to expand

Because of composability + configurability + state isolation. Each sampler is an independent small part with its own state (penalties keeps history, mirostat keeps feedback, dist keeps the RNG), with adjustable order and free add/remove. A user writes a list of sampler names and parameters in config, and the engine assembles a chain to order - whatever strategy you want, without changing a line of engine code.

Compare "one hardcoded big sampling function": there every new sampling means edits to the main function, ever more ifs, tangled parameters. Split into a chain, adding a new sampler is just one more independent implementation, with zero impact on the existing ones. This is the same "small parts compose complex behavior" as L11's operators and L16's graph blocks.

One more detail: a sampler like grammar (L23) usually does not enter the main chain but is applied before or after per grammar_first. This shows the "chain" is not rigid either - it leaves room to insert special constraints at the right spot, flexible enough.

✅ Key points
  • Sampling = prune and shape the candidate array llama_token_data_array, then pick one token.
  • Sampler llama_sampler_i: apply (required, edits candidates) + accept (feed back chosen token) + reset; with state ctx it forms a llama_sampler.
  • Sampler chain: chain_init -> chain_add several samplers -> sample, applying each in order.
  • greedy=pick the max (deterministic), dist=random by probability; top_k/top_p prune, temp tunes softness, penalties/mirostat have specialties.
  • Old global llama_sample_* are removed, unified into sampler object + chain; grammar (L23) is a special sampler outside the chain.
💡 Design insight
Splitting sampling from "one hardcoded big function" into "a string of pluggable small transforms" is the classic chain-of-responsibility / pipeline design - the same flavor you saw in ggml's operator chain (L09) and graph blocks (L16): small independent parts composing complex, varied behavior. So "change the generation style" is just swapping a few links and tuning a few numbers, with the model and engine trunk untouched. Understand sampling, and you hold the very knobs that turn a model from "buttoned-up" to "wildly free".

🧪 Self-test - think about the design

1. Which token does greedy sampling pick?
  1. the last one
  2. the one with the max logit (argmax, deterministic)
  3. the one with the min logit
  4. a random one
Show answer & explanation click to expand
Answer: B. greedy always takes the max-logit candidate (argmax); same input always same output. dist is the one that draws randomly by probability. Use greedy for reproducibility/rigor, dist for diversity.
2. Which candidates does top_p (nucleus) keep?
  1. the smallest set whose cumulative probability (high to low) reaches threshold p
  2. all candidates
  3. a fixed top 50
  4. all with probability greater than p
Show answer & explanation click to expand
Answer: A. top_p accumulates probability until reaching p, so the candidate count adapts to the distribution (few when peaked, many when flat); top_k is the fixed-count one. They often pair: top_k chops the tail, top_p closes adaptively.
3. What is the main benefit of making sampling a 'chain'?
  1. composability - apply several independent, configurable samplers in order
  2. it saves memory
  3. it allows only one sampler
  4. it runs faster
Show answer & explanation click to expand
Answer: A. The chain turns the strategy into data: each sampler is an independent part with its own state, order is adjustable, add/remove is free; users tune parameters to assemble any strategy without touching the engine. Speed/memory are not its design goal.
💭 Open questions (no single right answer - just think or try)
  • Drawing on L20 and L23, explain why a sampler works in 'the vocabulary's token space', and how grammar acts as a 'mask' to constrain this step.