Llama 3.1 发布之后,量化方案又吵成了一锅粥:有人跟风跑 MMLU,发现量化模型分数比 FP16 参考模型还高,于是宣布“量化还能提精度”;另一拨人换几个任务,结论又反过来。Fireworks 在一篇技术文章里把这个现象归结为基于任务的指标噪声太大,并给出一个更细的做法:用 KL 散度观察量化前后词元概率分布的变化。我顺着这个思路想复现一次,却卡在第一步:手头模型的 logprobs 拿不到,通道也不支持自选档位。后来我把 Codex 接到 TaoToken,从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建 API Key,再把 Base URL 填成 https://taotoken.net/api,终于把这套散度对照流程跑通。这篇文章记录的是这套流程,重点放在怎么让 Codex 写脚本、怎么读结果,而不是替任何一家量化方案背书。
1. MMLU 的分差为什么不能信:量化评估要从“对错”换到“分布”
1.1 全有或全无的评分把模型的“犹豫”丢掉了
Fireworks 在文章里举过一个很典型的例子:一个位置上,参考模型认为正确答案概率是 0.51,错误答案是 0.49;量化模型略为扰动,变成正确答案 0.49、错误答案 0.51。两个模型对这个 token 的真实看法只差了 2%,但落到 MMLU 的评分上,参考模型这题得 1 分,量化模型直接 0 分。
这种“全有或全无”的评分方式,就是任务指标噪声大的根源。MMLU 本质上是把一道题的正确性当成阶跃函数来打分,模型内部那 0.51 和 0.49 的分布差异全部丢掉了。连续几十道题都这样,量化模型和参考模型之间那 1% 的分差,绝大部分不是能力差距,而是对“不确定答案”的不同舍入。Fireworks 在自家 Llama 3.1 8B 的量化测试里就看到过这种现象:某个更激进的量化级别,MMLU 分数反而比 FP16 参考模型高几个百分点。这不是量化让模型变聪明了,而是基准测试里的噪声恰好偏向了它。
所以,当你只想回答“这个量化版本能不能上线”时,MMLU 并不是合适的尺子。它适合分析基础模型的一般能力,却不适合比较两个行为已经很接近的模型。比较两个高度相似的模型,需要看它们在同一段文本上预测下一个词元时,概率分布到底偏离了多远。
1.2 散度才是能量化分布的尺子:KLD 和词元拒绝率
Fireworks 推荐的两个指标都直接建立在词元概率分布上。
第一个是 KL 散度(KLD),衡量量化前后每个位置词元概率分布的变化程度。就算两个模型每个位置选出的最高概率词元完全相同,只要这个词元背后的概率大小变了,KL 散度也会反映出来。
第二个是词元拒绝率,统计“量化模型最高概率词元 ≠ 参考模型最高概率词元”的位置比例。换个角度看,它就像把量化模型当作草稿模型时,参考模型每次校验后的拒绝比例。拒绝率越高,量化模型在投机解码场景下的收益就越低。
这两个指标不是算一个总数就结束,还要按推理阶段拆分。Fireworks 把散度和拒绝率都分成预填充阶段、生成阶段两组,因为推理的不同部分可能采用不同的量化技术:预填充阶段大量并行计算,对注意力部分的量化更敏感;生成阶段逐步自回归,QKV 投影和 KV 缓存的量化误差会不断累积。如果不拆开看,两个阶段的退化会被平均掉,最后得到一个“看起来还行”的假象。
2. 先配一条能拿 logprobs 的通道:从 TaoToken 官网创建 Key 到 Codex 的 config.toml
2.1 打开官网创建 Key,再把 Base URL 填进 Codex
复现这套评估的前提,是模型接口愿意把 logprobs 还给你。Fireworks 在原文里也专门提了一句“我们鼓励其他提供商公开对数概率值”。TaoToken 的定位是统一 API 兼容通道,把不同模型的调用方式收敛成同一套 OpenAI 风格接口;在做散度评估时,它最有用的一点就是能按标准格式返回 logprobs。
先去 TaoToken 注册,在控制台创建一个 API Key。创建后拿到的字符串就是下面所有配置里的 YOUR_API_KEY。需要注意:这个网址是给人注册、建 Key、看用量、查模型广场用的。真正填进 Codex 或 Python SDK 的接口地址是 https://taotoken.net/api,末尾不要加 /v1,也不要带 ?utm_source=... 那一串,那串参数是网页统计用的,加进接口请求会让地址直接失效。
模型 ID 也不要靠猜。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的模型广场,找到你想对比的参考模型和量化模型,复制它们完整的 ID。每个模型的 logprobs 支持情况可能不同,优先选模型卡片里明确写了支持 logprobs 的档位。
2.2 ~/.codex/config.toml 里新增一个名为 taotoken 的 provider
Codex 的配置放在 ~/.codex/config.toml。打开这个文件,在 [model_providers] 下新增一段:
model = "MODEL_ID_FROM_TAOTOKEN" # 以模型广场为准
model_provider = "taotoken"
[model_providers.taotoken]
name = "TaoToken"
base_url = "https://taotoken.net/api"
env_key = "TAOTOKEN_API_KEY"
wire_api = "chat"
保存后先设置环境变量,再启动 Codex:
export TAOTOKEN_API_KEY=YOUR_API_KEY
codex
env_key 的意思是 Codex 会从同名环境变量读取 API Key。这样 Key 不会写死在配置里,也方便后面在 Python 脚本里复用同一个环境变量。配置好后随便问 Codex 一句话,确认它已经能通过 TaoToken 通道拿到模型响应。这一步不做散度计算,只验证链路通不通。
3. 复现 Fireworks 的散度方法:让 Codex 写出“强制跟读”脚本
3.1 参考模型先自由生成:收集 top-16 logprob 分布
Fireworks 的方法第一步,是让参考模型根据各种提示自由生成,直到完整结束。这里参考模型指的是原始精度版本,通常也叫 FP16、BF16 或带 -hf 后缀的档位。生成时在每个位置记录前 N 个词元的对数概率。原文给的经验值是 N=16,理由是前 16 个词元大约能覆盖分布 0.99 的质量。
在 Codex 会话里描述这个需求,它会生成类似下面的脚本骨架:
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://taotoken.net/api",
)
REFERENCE_MODEL = "MODEL_ID_FROM_TAOTOKEN" # 模型广场里的原始精度模型
QUANT_MODEL = "MODEL_ID_FROM_TAOTOKEN" # 模型广场里的量化模型
TOP_N = 16
def free_generate(prompt, model=REFERENCE_MODEL):
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0,
logprobs=True,
top_logprobs=TOP_N,
)
content = resp.choices[0].logprobs.content
return content # 每个元素包含 token、logprob、top_logprobs
提示集不要只放一两道 MMLU 题。原文特别指出,量化对代码生成的影响和对函数调用的影响可能完全不同。所以至少准备四类提示:代码补全、函数调用、JSON 结构输出、多步数学。每类 5 到 10 条,总共 20 到 40 条即可。数量不需要太大,散度指标对提示集的选择比任务指标稳定得多。
3.2 量化模型不许自由发挥:用参考输出前缀强制跟读
直接让量化模型重新生成一遍,然后计算最终文本的差异,是不行的。原因在原文里写得很清楚:量化模型可能生成一条完全不同但困惑度很低的路径,比如不断输出重复词元,最后整体概率看起来还不错,实际上已经偏离了任务。
正确做法是“强制”量化模型跟读参考模型。所谓跟读,就是在每个位置上,把参考模型已经生成的前缀作为上下文,让量化模型只预测下一个词元的概率分布;无论量化模型更想选哪个词元,脚本都继续使用参考模型的前缀,不让它把话题带偏。
def forced_next_distribution(prompt, reference_prefix, model=QUANT_MODEL):
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "user", "content": prompt},
{"role": "assistant", "content": reference_prefix},
],
max_tokens=1,
temperature=0,
logprobs=True,
top_logprobs=TOP_N,
)
content = resp.choices[0].logprobs.content
return content[0].top_logprobs # 词元 + logprob 列表
这里的 reference_prefix 是参考模型已经生成的那段文本。由于 logprobs 返回的是 token 文本,直接拼接可能引入一点边界误差。想更严格,可以让服务端返回 token ID 或偏移量来做精确前缀,但这取决于 TaoToken 上游端点是否提供这些字段。对量化对比来说,两个模型吃的是同一套前缀,边界误差的方向是一致的,不会颠覆结论。
3.3 预填充 / 生成分桶:KL 散度与拒绝率的计算入口
拿到两个模型在每个位置上的 top-16 分布后,接下来的计算就简单了。
KL 散度需要对齐两个分布的词元集合。参考模型的 top-16 里有某些词元,量化模型的 top-16 里不一定有。脚本里把缺失概率设为一个很小的值,比如 1e-12,然后只取参考分布支撑集求和。拒绝率更直接:比较两个模型 top-1 词元是否相同,统计不同位置的比例。
import math
def kl_from_logprobs(ref_top, quant_top, eps=1e-12):
qmap = {item.token: math.exp(item.logprob) for item in quant_top}
total = 0.0
for item in ref_top:
p = math.exp(item.logprob)
q = qmap.get(item.token, eps)
total += p * math.log(p / q)
return total
最后把结果拆成两组:参考模型输出序列里前 20 个新词元归入预填充阶段,其余归入生成阶段。如果上游接口支持 prompt logprobs,那就更精确;拿不到时用这个近似也足够,因为两个模型面对的是同样的位置偏移。
脚本跑完后会输出一张表,结构类似这样,数字以你这次实际跑出的结果为准:
| 模型档位 | 预填充 KL | 生成 KL | 预填充拒绝率 | 生成拒绝率 |
|---|---|---|---|---|
| 量化档位 A | 以脚本输出为准 | 以脚本输出为准 | 以脚本输出为准 | 以脚本输出为准 |
| 量化档位 B | 以脚本输出为准 | 以脚本输出为准 | 以脚本输出为准 | 以脚本输出为准 |
把 stdout 贴回 Codex 会话,让它帮你解释哪一档散度增长异常,再决定要不要继续对比。
4. 结果怎么读:叠量化等级跑,看散度是否单调,别被 MMLU 的 1% 带走
4.1 从模型广场挑几个量化档位,由低到高排成序列
原文用 Llama 3.1 8B Instruct 做了四档量化,从只量化 MLP 矩阵乘法,到逐步叠加 QKV、KV 缓存、预填充注意力。TaoToken 模型广场不一定提供一模一样的分层档位,但你可以选两到四个粒度不同的版本。原则是让它们构成一条“激进程度递增”的序列,例如建议:
- 档位 1:原始精度版本,作为参考基线
- 档位 2:常见 8bit 量化版本
- 档位 3:更激进的 4bit 或 AWQ 版本
- 档位 4:如果广场有 KV 缓存也被量化的版本,再往上叠一档
排好序后,对每个档位跑一遍第 3 节的脚本,得到各自的预填充 KL、生成 KL、预填充拒绝率、生成拒绝率。这一步的关键动作是“同一套提示集、同一个参考前缀”,量化档位之间只允许模型 ID 不同,其他参数全部固定。
4.2 散度单调递增而 MMLU 忽上忽下,才是 Fireworks 说的噪声
正常情况下,你会看到 KL 散度和词元拒绝率都随量化激进程度单调上升。这是 Fireworks 原文明确提到的规律:散度指标彼此之间、以及与量化级别之间保持单调关系。如果某个更激进的档位反而 KL 散度大幅下降,先怀疑是不是模型 ID 填反了,或者服务端对那个模型做了不同处理。
相比之下,MMLU 这类任务指标不会给你这么干净的曲线。它会在两个档位之间突然上升 1%,又在下一档跌回去。原文里那个“量化级别 2 的 MMLU 高于 FP16”的现象,就是为了提醒你:任何基于任务的基准,在比较两个接近的模型时,1% 之内的分差都落在噪声范围内。你复现时如果也看到量化模型某个任务分数比参考模型高,不要急着高兴,先看同档位的 KL 散度是不是也环比上升了。散度上升而 MMLU 上升,说明基准噪声大于量化影响;散度几乎不变而 MMLU 大起大落,说明基准本身不可复现。
4.3 分段归因:MLP 影响小、QKV 影响大、预填充和生成要分开判断
把散度拆成预填充和生成两个桶之后,还能回答“量化到底伤在哪”的问题。Fireworks 的测试结论是:量化 MLP 层对模型整体质量影响相对较小;量化 QKV 投影的影响更明显;量化 KV 缓存对长生成场景的影响比对短输入更显著;而量化预填充里的注意力操作,对长预填充模型的延迟收益最大,对质量的影响也要单独盯。
这些规律不能直接套用到所有模型上,但可以指导你下一步怎么选。比如你的业务是长文档总结,预填充占比很大,那就优先关注预填充 KL 散度;如果你的业务是代码补全,生成阶段很长,那就把生成 KL 散度当作上线阈值。Fireworks 给过一个经验值:KL 散度小于 0.007 时,量化质量通常可以接受。但这个阈值不是金标准,最终还是要拿你自己的业务提示集跑一轮。
5. 排障:Codex 接到 TaoToken 后最常见的三类错
5.1 401 Unauthorized:Key 没创建或带到了 URL 上
Codex 报 401,第一件事是确认 TAOTOKEN_API_KEY 环境变量是否真的设置成了你在官网创建的那一串完整 Key。很多复制操作会漏掉末尾字符,或者把终端里的引号也带进去。第二件事是检查请求目标:接口地址必须是 https://taotoken.net/api,不要在后面拼 /v1,也不要带上网页的 UTM 参数。UTM 那串是给人点链接做归因用的,放进 API 请求只会得到无效地址,甚至被网关直接拒绝。
5.2 404 Model Not Found:模型 ID 要从模型广场复制
如果 Codex 或 Python 脚本返回 404,大概率是 config.toml 里的 model 字段和 TaoToken 模型广场列出的 ID 不一致。不要凭记忆写模型名,也不要在标准模型名后面自己加日期后缀或量化标记。回到模型广场,打开目标模型的详情页,复制完整 ID 再填进去。
5.3 logprobs 返回 null:端点不支持,换模型而不是改参数
脚本里 logprobs=True、top_logprobs=16 都写了,响应里却拿不到 logprobs.content,这通常是模型端点本身不返回 logprobs,不是参数格式的问题。Fireworks 在原文里也提过,希望更多提供商公开对数概率值;现实是并非所有模型服务都会开启这个能力。遇到这种情况,换一个模型广场里标注支持 logprobs 的模型,而不是反复调参数。
另外,散度脚本跑得慢是正常的。每个位置都要把参考前缀重新发给量化模型,位置一多调用次数就上去了。可以抽样位置,比如每隔 5 个 token 计算一次,这样能把总调用量降下来,同时保留趋势判断。
6. 跑通之后:用模型对话先验证,再去 Coding Plan 和 API Keys 对账
6.1 在模型对话里用同一把 Key 发一条测试消息
散度脚本得到第一份结果后,先用 TaoToken 模型对话 做一次人工验证:从你的提示集里挑一条,用参考模型和量化模型各跑一次,直接看输出质量的直观差异。这一步把“散度数字”和“真实体感”对上。
尤其是当你发现某个档位 KL 散度很低,但对话里量化模型已经明显出现重复或漏字时,说明你的提示集没有覆盖到那个退化方向。回到第 3 节,往提示集里补这一类业务样本,再跑第二轮。这个闭环才是 Fireworks 原文说的“开发者是质量最佳评判者”的落地方式。
6.2 长期写代码再看 Coding Plan 与用量核对
散度评估不是一次性任务。模型换了、量化档位换了、业务提示集变了,都需要重新跑。确认这套流程稳定后,可以回到 控制台 API Keys 看这次跑批一共消耗了多少调用量;如果准备把 Codex 长期用作日常写码工具,再打开 Coding Plan 估一下套餐够不够覆盖后续的散度跑批与日常开发。量化的结论不该由别人替你拍板,你自己跑的散度曲线,比任何博客里的 MMLU 榜单都更能说明问题。




被折叠的 条评论
为什么被折叠?



