KEN’S CAT LOG
▣

将自制的 3 亿参数 LLM 量化,比较 CPU 上的速度与准确性

我在上一篇文章中制作了一个用于克罗地亚语 RAG 的 3.2 亿参数模型。既然目标是在 CPU 上运行,这次就对它进行了量化,测试它能缩小和加速到什么程度,以及准确性会下降多少。

先说结论。

  • Q6_K 正合适。 体积缩至 1/5(254MB),生成速度约提升 4 倍,准确性与原模型相同
  • Q2_K 会失效。 虽然很快,但即使问题有答案,它也会回答“没有答案”
  • 出乎意料的是,Q3_K_M 比 Q4_K_M 更准确

做了什么

我将用 PyTorch 制作的模型转换为可在 CPU 推理中常用的 llama.cpp 上运行的格式(GGUF),并以 8 种不同精度保存。

  1. 将模型导出为与 Qwen3 相同的格式,再转换为 GGUF
  2. 由于分词器是自制的,llama.cpp 将其识别为“未知分词器”而拒绝转换。对比分词规则后发现它与 Qwen2 完全一致,于是将其注册为 Qwen2 后通过了转换
  3. 确认转换是否正确。 160 篇文本的分词结果与 PyTorch 版全部一致。在 20 道题中,每次都选择概率最高的词进行生成,20 道题的输出也完全一致
  4. 使用 llama-quantize 制作 F16~Q2_K 版本

因此,表中的 F32 与 PyTorch 版的内容相同,是本次比较的基准。

测量方法

  • CPU 为 i7-13700K,未使用 GPU
  • llama.cpp 设置为:生成使用 8 线程,文本读取使用 16 线程(测试后这是最快的组合)
  • 对评估用的 100 道题 × 4 种上下文,让每个模型各回答 400 次
    • 段落:包含答案、约 100 个 token 的段落
    • 长文:包含答案、约 3,000 个 token 的文本(也混入了无关文章)
    • 无答案(段落/长文):不含答案的文本;能够拒答即为正确
  • 上一篇文章用 300 道题测试,因此数字会略有不同

结果:准确性

格式 大小 段落正确 长文正确 长文拒答 编造(无答案 段落/长文) 核验后的准确率
F32(基准) 1,233MB 77% 53% 30% 1% / 9% 80.9%
F16 617MB 77% 53% 30% 1% / 9% 80.9%
Q8_0 329MB 76% 53% 30% 1% / 8% 81.9%
Q6_K 254MB 79% 54% 30% 2% / 9% 81.3%
Q5_K_M 223MB 72% 45% 41% 1% / 6% 84.4%
Q4_K_M 193MB 68% 37% 46% 1% / 7% 75.9%
Q3_K_M 160MB 72% 48% 38% 1% / 8% 80.4%
Q2_K 129MB 39% 11% 78% 4% / 7% 72.2%

“核验后的准确率”是指:用程序确认模型引用的内容是否确实存在于文本中;若不存在,就替换为“无答案”后,在实际作答中回答正确的比例。

  • F16、Q8_0、Q6_K 与基准几乎相同。 差距仅有 1~2 题
  • 从 Q5_K_M 往下,与其说错误变多,不如说“拒答”变多。 看起来是此前训练出的“没把握就拒答”的特性原样保留了下来,编造并没有增加
  • Q4_K_M 比 Q3_K_M 更差。 长文分别为 37% 和 48%。虽然只有 100 道题,存在一定波动,但 11 个百分点的差距很大,原因尚未调查
  • Q2_K 会拒答 78% 的长文。 它已经像是另一个模型了

结果:速度

格式 生成速度 单次平均时间
PyTorch fp32(参考) 段落 14.6・长文 6.7 tok/s —
F32 18.7 tok/s 2.76 秒
F16 35.9 tok/s 2.08 秒
Q8_0 59.0 tok/s 1.93 秒
Q6_K 73.1 tok/s 1.33 秒
Q5_K_M 81.2 tok/s 1.26 秒
Q4_K_M 89.1 tok/s 1.10 秒
Q3_K_M 103.4 tok/s 1.12 秒
Q2_K 119.9 tok/s 1.07 秒

llama.cpp 的数据为 400 次的平均值(汇总了段落、长文和无答案情形)。PyTorch 的数据来自上一篇文章的测量。

  • 生成速度与体积几乎成反比。在 CPU 上,决定速度的是从内存读取权重的速度,而不是计算速度,因此越小越快
  • 即使同为 F32,llama.cpp 也比 PyTorch 快
  • 不过从 Q6_K 往下,单次耗时缩短得不多。在这个长度下,初始读取文本的时间占比更大

视频:让它在 CPU 上回答同一个长文问题

我给了模型一篇约 3,200 个 token 的文本,并问:“Need for Speed Heat 是何时发售的?”正确答案是 2019 年 11 月 8 日。

由于录制时还要在浏览器中渲染画面,视频中的速度比上表更慢。请将其视为便于并排比较的参考。

格式 视频中的生成速度 首字符出现前 总计 结果
PyTorch fp32 5.5 tok/s 4.8 秒 18.5 秒 正确
F32 14.2 tok/s 2.8 秒 8.1 秒 正确
F16 25.9 tok/s 3.5 秒 6.4 秒 正确
Q8_0 38.2 tok/s 4.0 秒 5.9 秒 正确
Q6_K 41.6 tok/s 2.6 秒 4.4 秒 正确
Q5_K_M 46.3 tok/s 2.6 秒 4.3 秒 正确
Q4_K_M 47.2 tok/s 2.3 秒 4.7 秒 正确(答案较长)
Q3_K_M 56.7 tok/s 2.5 秒 4.5 秒 正确(答案较长)
Q2_K 61.3 tok/s 2.7 秒 2.8 秒 拒答

从 F32 到 Q5_K_M,答案和引用都与 PyTorch 版一字不差。Q4_K_M 和 Q3_K_M 的回答变长了,但引用是正确的。Q2_K 明明有答案,却拒答称“正文中没有答案”。

PyTorch fp32(原始模型)

PyTorch fp32 在 CPU 上运行并回答长上下文问题的情形再生

llama.cpp F32(1,233MB)

llama.cpp F32 在 CPU 上运行并回答长上下文问题的情形再生

llama.cpp F16(617MB)

llama.cpp F16 在 CPU 上运行并回答长上下文问题的情形再生

llama.cpp Q8_0(329MB)

llama.cpp Q8_0 在 CPU 上运行并回答长上下文问题的情形再生

llama.cpp Q6_K(254MB)

llama.cpp Q6_K 在 CPU 上运行并回答长上下文问题的情形再生

llama.cpp Q5_K_M(223MB)

llama.cpp Q5_K_M 在 CPU 上运行并回答长上下文问题的情形再生

llama.cpp Q4_K_M(193MB)

llama.cpp Q4_K_M 在 CPU 上运行并回答长上下文问题的情形再生

llama.cpp Q3_K_M(160MB)

llama.cpp Q3_K_M 在 CPU 上运行并回答长上下文问题的情形再生

llama.cpp Q2_K(129MB)

llama.cpp Q2_K 在 CPU 上运行并回答长上下文问题的情形再生

思考

  • 如果要在 CPU 上运行这个模型,就选 Q6_K。 准确性保持原样,体积降至 1/5,生成速度约为 PyTorch 的 5 倍,没有犹豫的理由
  • 若想更稳妥,就选 Q8_0。 与原版几乎相同,只有 329MB,即使如此也足够小
  • 3 亿参数的小模型不耐量化。 据说大模型即使使用 Q4 也几乎不会退化,但这次从 Q5 开始就出现了明显下降。模型本身余量不大,因此被压缩后影响明显
  • 以“拒答”的方式失效是件好事。 对 RAG 而言,拒答比编造更安全。上次通过强化学习训练的拒答方式,在这里也发挥了作用
  • 在小模型上,速度差距会缩小。 从 Q6_K 往下,即使生成更快,读取文本的时间仍然存在,体感变化不大。视频中 Q6_K~Q3_K_M 也都在 4 秒多
  • Q4_K_M 为什么输给 Q3_K_M 仍然没有答案。希望通过增加题目数量,或查看各层如何被量化来进一步调查