跳到主内容
快讯直播
AI智模界
教程

llama.cpp 查表起草:不挂草稿模型也能提速

这篇能做出什么

给本地跑 llama.cpp 的人一个"零成本"提速手段:prompt lookup(查表起草 / lookup decoding)。

做完之后你会得到:

  • 一个开了查表起草的 llama-server 或 llama-cli 命令,不需要加载任何额外的草稿模型 GGUF。
  • 一套可复现的对照测速脚本,能量化出"开 / 不开起草"的 tokens/s 差距。
  • 对"什么样的任务能提速、什么样的任务白开"的判断力,以及它和草稿模型、并行起草之间的取舍。

典型适用任务:把 CSV 转成 Markdown 表格、把一段配置改写成另一种格式、代码重命名与补全、让模型引用原文作答。这类任务的输出里有大量 token 是从输入里"抄"过来的,命中率高,提速明显。反过来,写故事、自由翻译这类每个词都要新造的任务,收益会掉到接近 0。

---

前置条件清单

1. 一台能跑动目标模型的机器(纯 CPU 也能测,GPU 上更容易看出效果)。

2. 一份已编译好的 llama.cpp,至少包含 llama-server 和 llama-cli。编译方式以官方文档当前版本为准。

3. 一个 GGUF 模型文件,以及你自己机器上的实际磁盘路径。

4. Python 3 与 urllib(标准库即可,不用装第三方包)。

5. 会看命令行帮助:llama-server --help。参数名在不同构建里会演进,凡是本文提到的开关,都以你自己那份构建的 --help 输出为准。

---

第 1 步:先确认你的构建里有这些开关

查表起草没有独立的一套"大命令",它是挂在投机采样(speculative decoding)参数体系上的。先确认可用性:

```bash

llama-server --help | grep -iE "draft|lookup"

```

你应该能看到类似下面这几类条目:

```text

--draft-max N 单次最多起草多少个 token

--draft-min N 少于这个长度的起草直接放弃

--draft-p-min P 起草 token 的最低概率门槛

--lookup-cache-static FNAME 从文件加载一份预置 n-gram 表

--lookup-cache-dynamic FNAME 把运行中形成的 n-gram 表保存到文件

```

如果 grep 出来的为空,说明这份构建里相关功能没编进去,或者参数名改了。用 llama-server --help | head -n 200 手动翻一遍,或者搜 draft。

怎么算"开启":不指定草稿模型(不传 -md / --model-draft)而只给出 --draft-max 时,llama.cpp 会走 n-gram 查表这条路径。如果你的构建里另有更明确的 lookup 开关(例如帮助文本里出现 lookup 字样且带默认值),优先按帮助文本写的来,本文后续命令在此基础上加一行即可。

---

第 2 步:理解查表起草是怎么"抄"的

投机采样的通用套路是:先廉价地猜一段后续 token(draft),再让主模型一次前向并行验证这段猜测,把匹配的前缀一次性接受。猜得越准、接受得越长,同样的墙钟时间里产出的 token 就越多。

普通做法要额外挂一个小模型来猜。查表起草不挂模型,改成"翻历史记录":

1. 维护一张 n-gram 表,键是最近若干个 token 组成的片段,值是这个片段在历史上后面跟着的 token 序列。

2. 生成到某一步时,拿当前上下文末尾的 n-gram 去查表。

3. 命中,就把历史上它后面跟着的那串 token 拿出来当 draft,交给主模型验证。

4. 长度上限由 --draft-max 控制;如果查到的后续长度不够、低于 --draft-min,这次起草直接作废,省掉一次无谓的验证开销。

5. 表里的候选可能有多个,具体挑哪一条由实现决定(出现频率、最近出现等策略都有)。

6. 表的来源有两种:动态——本次运行已经处理过的 prompt 加上已经生成的 token;静态——事先存好的一份文件,用 --lookup-cache-static 加载。

这里有个关键结论:命中条件是"字面重复",不是"语义相近"。模型不知道"总结一下"和"概括一下"是一个意思,但它知道刚才输入里出现过 user_007,dept_2 这串东西。所以查表起草吃的是模板化、引用型、格式转换型的活。

也正因为如此,它和上下文长度直接相关:-c 给得越大,能查到的历史越多,命中率越高。

---

第 3 步:准备一个高重复度的测试任务

写一个脚本,让同一个 prompt 分别打到两个端口的服务上。任务选"CSV 转 Markdown 表格",因为输出几乎逐字复用了输入。

```python

bench_lookup.py

import json

import sys

import urllib.request

CSV = "\n".join(

f"{i},user_{i:03d},dept_{i % 5},L{i % 4 + 1},active" for i in range(1, 61)

)

PROMPT = f"""把下面的 CSV 转成 Markdown 表格,表头保持原样,只输出表格本身,不要解释。

CSV:

id,name,dept,level,status

{CSV}

"""

BODY = {

"prompt": PROMPT,

"n_predict": 1024,

"temperature": 0,

"top_k": 1,

"cache_prompt": False, # 关掉 KV 复用,否则第二次请求会被 prompt cache 加速

"stream": False,

}

def run(port: int):

req = urllib.request.Request(

f"http://127.0.0.1:{port}/completion",

data=json.dumps(BODY).encode("utf-8"),

headers={"Content-Type": "application/json"},

)

with urllib.request.urlopen(req, timeout=900) as resp:

return json.load(resp)

if __name__ == "__main__":

port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080

data = run(port)

t = data["timings"]

print(f"generated_tokens = {t['predicted_n']}")

print(f"tok_per_second = {t['predicted_per_second']:.2f}")

```

两个要点:

  • cache_prompt: false 必须写。否则第二个服务收到的同一段 prompt 会命中 KV 缓存,你会把"缓存复用"误判成"起草加速"。
  • temperature: 0 + top_k: 1 用贪心采样。这既让结果可复现,也避开采样路径带来的差异。

---

第 4 步:跑基线

```bash

llama-server \

-m /path/to/your-model.gguf \

-c 8192 \

-ngl 99 \

--parallel 1 \

--host 127.0.0.1 --port 8080

```

-ngl 99 表示尽量把层放到 GPU;纯 CPU 测试就删掉它。--parallel 1 是让服务只开一个槽位,避免并发调度干扰测速。

等模型加载完,另开一个终端:

```bash

python3 bench_lookup.py 8080

```

记下 tok_per_second。建议连跑三次取中位数,第一次往往被显存预热和首轮分配拖慢。

---

第 5 步:开查表起草,再跑一次

保持模型、上下文、量化方式全部不变,只加起草参数,换一个端口:

```bash

llama-server \

-m /path/to/your-model.gguf \

-c 8192 \

-ngl 99 \

--parallel 1 \

--draft-max 16 \

--draft-min 4 \

--host 127.0.0.1 --port 8081

```

```bash

python3 bench_lookup.py 8081

```

把两次的 tok_per_second 相除就是加速比。下面的输出只是示意格式,数字必须在你自己机器上测,不同模型、量化、硬件差异很大:

```text

基线(不开起草)

generated_tokens = 1018

tok_per_second = 27.9

查表起草(--draft-max 16 --draft-min 4)

generated_tokens = 1018

tok_per_second = 49.6

```

在"CSV 转 Markdown 表格"这类高度模板化的任务上,观察到 1.5 倍以上、2 倍上下是常见量级;在写散文、自由翻译这类任务上,常见结果是接近 1.0 倍。请以你自己的实测为准。

同时看一眼服务端日志。llama.cpp 会打印每一轮起草与接受的统计(形如 n_draft / n_accept 的字段),接受率是判断"值不值得开"最直接的指标:接受率低说明表里查不到东西,加参数也救不回来。

---

第 6 步:调 --draft-max 和 --draft-min

这两个是核心旋钮,含义不同:

  • --draft-max:一次最多起草多长。调大能覆盖更长的重复片段(比如整行 CSV 一次性抄下来),但验证成本随长度线性上升。从 8 起,逐步试到 16、24、32,看加速比什么时候掉头。
  • --draft-min:短于这个长度的起草直接放弃。设成 0 意味着哪怕只查到 1 个 token 也要走一遍投机流程,这个开销可能比省下的还多。重复度中等时,把它设到 3~5 通常更划算。
  • --draft-p-min:主要影响带草稿模型的采样路径,纯查表场景一般保持默认即可。

一个实用做法:固定 --draft-max,只扫 --draft-min,因为后者对"低命中场景"的伤害更直接。

如果你的构建里有 --lookup-cache-static / --lookup-cache-dynamic,可以进一步把 n-gram 表持久化:

```bash

llama-server \

-m /path/to/your-model.gguf \

-c 8192 -ngl 99 \

--draft-max 16 --draft-min 4 \

--lookup-cache-static /tmp/lookup.bin \

--host 127.0.0.1 --port 8081

```

前者用于从文件加载一份预置表(适合有一批固定语料,比如你自己的代码仓库),后者用于把运行中形成的表写出去。具体语义以你那份 --help 的说明为准。

---

第 7 步:做一次正确性对照

投机采样被接受的部分是逐 token 验证过的,所以在贪心采样下,开与不开起草的输出应当完全一致。把两次的返回文本 diff 一下:

```bash

curl -s http://127.0.0.1:8080/completion -H 'Content-Type: application/json' \

-d @body.json | python3 -c "import sys,json;print(json.load(sys.stdin)['content'])" > /tmp/a.txt

curl -s http://127.0.0.1:8081/completion -H 'Content-Type: application/json' \

-d @body.json | python3 -c "import sys,json;print(json.load(sys.stdin)['content'])" > /tmp/b.txt

diff /tmp/a.txt /tmp/b.txt && echo "一致"

```

如果出现差异,先确认两边采样参数完全一致;在非贪心采样下,实现可能会走近似路径,这时以贪心结果做基准对比更可靠。

---

查表起草 / 草稿模型 / 并行起草,怎么选

方案额外开销擅长的任务什么时候不划算
查表起草(prompt lookup)无额外模型,只多一张 n-gram 表的内存输出大量复用输入或历史的任务重复度低时收益趋近于 0
草稿模型(-md 挂小模型)额外 GGUF、额外显存,且 tokenizer 需兼容自由生成、翻译等通用场景显存紧张、小模型和主模型风格差异大时接受率低
并行起草通常建立在草稿模型路线之上,需要额外的后端或线程资源想压掉"先猜再验"这条串行链的开销没有草稿模型时无从谈起

选择顺序可以简单点:

1. 先开查表起草。它零额外模型、零额外显存,试错成本最低。

2. 如果实测加速比接近 1.0,看接受率。低接受率说明任务本身重复度不够,这时候才考虑上草稿模型。

3. 已经挂了草稿模型、但发现起草本身占了不少时间,再考虑并行起草把起草和验证重叠起来。

4. 两者不冲突:有草稿模型的构建里,查表起草和草稿模型可以分别测试,用同一个脚本对比。

---

常见坑与排错

开了没变快。 九成是任务重复度不够。先看服务端日志的接受统计,低接受率就别在参数上折腾了,换任务或者换方案。

起草全被拒,甚至直接报错。 先确认采样是不是贪心(temperature: 0、top_k: 1)。早期实现对此有硬性要求,新版本对采样的支持在放宽,但用贪心做基准最稳。

第二次请求突然变快。 那是 prompt cache 命中,不是起草生效。请求体里带上 cache_prompt: false。

--draft-max 调得越大越慢。 正常现象。draft 太长时,主模型验证的批次变大,而接受的 token 数没有同比增加。降回 16 附近再试。

--draft-min 设成 0 反而更慢。 短 draft 的固定开销(准备验证批次、调度)可能超过收益。设到 3 以上。

服务器开了多槽位时结果飘。 多并发槽位下投机解码的调度会变化。测速统一用 --parallel 1。

上下文开太小。 n-gram 表能覆盖的历史有限,-c 给到 4096 以下时,长文档任务的命中率会明显下降。

中文任务担心 token 粒度。 查表是在 token 层级做的,不是字符层级。只要输入输出在 token 层面高度重合(比如引用原文作答),照样命中。

输出质量担忧。 查表起草不改模型权重,只影响"一次前向处理多少个 token",被接受的猜测都经过验证。用上面的 diff 方法做一次对照即可放心。

---

下一步建议

  • 把测速脚本留下。 换模型、换量化、改上下文长度时都重跑一遍,形成自己的加速比基线,而不是记住别人嘴里的数字。
  • 按 workload 决定开关。 如果你的服务同时有"格式转换"和"开放式问答"两类请求,可以起两个端口、两套参数,由上游路由分发。
  • 试静态缓存。 有固定语料(内部文档、代码库)的团队,把 n-gram 表预置成静态文件,首次请求的命中率会更好。缓存构建方式以官方文档当前版本为准。
  • 再评估草稿模型。 查表起草跑完一轮后,你手上已经有了接受率的实测数据,这时再决定要不要为一个通用小模型多花显存,判断会理性得多。
  • 关注上游变化。 投机采样这块在 llama.cpp 里迭代较快,参数名和默认值会调整。升级构建后重新跑一次 --help | grep -iE "draft|lookup",再重跑一遍脚本。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。