模型缩到三分之一,量化到底丢了什么
文字稿约 2800 字 · 按画面章节整理展开收起
同一个模型,为什么有好几个大小?
前几期,我们一直在笔记本上跑这个模型:Qwen3 1.7B。名字里写着,它有 17 亿个参数,可下载下来,只有 1.4 GB。其实,Ollama 的模型库里,同一个模型还有好几个版本:FP16,4.1 GB;Q8_0,2.2 GB;还有 Q4_K_M,1.4 GB。这几个名字是什么意思,后面拆开讲。先看编号:我们一直在用的那个,和 Q4_K_M 一模一样,就是同一个文件。最大的版本,差不多是最小的三倍。缩小的时候,丢了什么?
今天,我们把这三个版本放在一起,比一比:大小、速度,还有答题。
模型文件里,装的主要是参数
先打开最大的那个文件。里面绝大部分,是这样一长串小数,叫参数。这个文件里,一共有 20.3 亿个参数。可名字里写的是 1.7B,也就是 17 亿,怎么多出来这么多?因为有一张词向量表,每个 token 对应一行 2048 个数。读输入的时候用它,写输出的时候,又用它。名字里只算一份,文件里却存了两份:17.2 亿,加上第二份的 3.1 亿,正好 20.3 亿。
FP16:每个参数,用 16 位来记
FP16 版本里,每个参数,用 16 个 0 和 1 来记。FP16,全称是 16-bit Floating Point,16 位浮点数。官方发布的版本,也是每个参数 16 位,只是格式稍有不同;这个 FP16 版本,是从它转换来的。20.3 亿个参数,乘以 16 位,再除以 8 换成字节,是 4.06 GB;再加上 6 MB 的词表文字和配置,正好是文件的 4.07 GB。
量化:换一把刻度更少的尺子
怎么让文件变小?最直接的办法:每个数,少用几位来记。这就叫量化。16 位,能分出六万多个不同的值;8 位,只有 256 个;4 位,只剩 16 个。打个比方:原来,我们用一把刻度很密的尺子量东西;现在,换成一把只有 16 个刻度的。这是文件里真实的 32 个参数。量化,就是把每个数,挪到离它最近的那个刻度上。挪动的这一点距离,就是误差。这一组里,最多差了 0.0056。尺子也不是整个文件共用一把:每 32 个数一组,按这一组的范围,单独配一把。尺子从哪开始、一格多长,也要记下来;摊到每个数头上,正好多出 0.5 位。
版本名,现在看得懂了
现在,版本名就看得懂了。Q 是 Quantized,量化。Q8_0,每个数 8 位;后面的 0,是最简单的那种尺子:以 0 为中心,只记一格多长。Q4_K_M,每个数 4 位;K 是 K-quants,开源项目 llama.cpp 里的一套量化方法,4 位的这种,尺子既记一格多长,也记从哪开始。M 是 Medium,中档;跟 S,也就是 Small 档比,它给一部分参数多留了几位。
名义上 4 位,实际平均几位?
Q4_K_M 每个数 4 位,是 FP16 的四分之一,那文件应该是 1.02 GB 才对。可实际是 1.36 GB。多出来的,有两笔。第一笔,就是刚才的尺子:4 位,加上 0.5 位,是 4.5 位。第二笔:并不是所有参数,都压到了 4 位。73.1% 的参数,是 4.5 位;24.0% 多留了一些,是 6.56 位,写输出用的那份词向量表,就占了其中的 15.3%;还有 2.9%,没压,还是 16 位。平均下来,这个文件每个参数 5.33 位。这个比例,跟模型、跟文件怎么做出来的都有关:llama.cpp 说明里,一个 8B 模型的 Q4_K_M,平均是 4.89 位。
三个文件的大小,都算得出来
用平均位数,把三个文件的大小再算一遍。FP16 刚才算过了。Q4_K_M:20.3 亿,乘以 5.33 位,除以 8,是 1.35 GB;加上 6 MB,正好 1.36 GB。Q8_0 也一样:8 位加 0.5 位,是 8.5 位,再算上没压的那 2.9%,平均 8.72 位,算出来,正好 2.22 GB。FP16 的文件,是 Q4_K_M 的 3.0 倍,因为平均每个参数的位数,也是 3.0 倍。
文件小了,生成快了多少?
文件小了,跑起来快了多少?同一个问题,三个版本,按真实速度回放,各写 256 个 token。看生成间隔,也就是每生成一个 token 要多久:Q4_K_M,12.2 毫秒;Q8_0,15.9 毫秒;FP16,24.2 毫秒。每个版本,都测了 8 次,取中位数。
文件缩到三分之一,生成间隔为什么只缩到一半?
为什么文件小,就快?因为每写一个 token,模型的各层参数,都要从内存里读一遍。文件越小,要读的就越少。可是,文件缩到了三分之一,生成间隔只缩到一半。把三个版本画在一起:横着是文件大小,竖着是生成间隔。把 FP16 和 Q4_K_M 连起来:文件每少 1 GB,生成间隔少 4.43 毫秒。顺着这条线,延长到文件为零,还剩 6.2 毫秒:照这条线算,这一段跟文件大小无关。中间的 Q8_0,离这条线只差 0.1 毫秒,对得上。这 6.2 毫秒里有什么?比如每一层计算的调度,还有把字送出来。这是推测,我们没有拆开测。
读输入:三个版本差不多
再看读输入。同一段 1709 个 token 的输入,看每秒能读多少个。FP16,每秒 1731 个;Q8_0,1711 个;Q4_K_M,1648 个,还略慢一点。一般的解释是:读输入的时候,很多 token 是一起算的,卡住速度的主要是计算,不是读内存;压过的参数,还要先换算回来,所以占不到便宜。llama.cpp 的说明里,一个 8B 模型的实测,也是这个规律。所以,量化主要缩短的是生成间隔;读输入的速度,基本没变。
答题变差了吗?三个版本,做同一套题
最后,也是最要紧的:答题变差了吗?我们用一套公开的中文选择题,叫 CMMLU,中文多任务语言理解评估。67 个科目,每科随机抽 15 道,一共 1005 道。每道题,看模型觉得下一个字,最可能是 A、B、C、D 里的哪个,就算它选了哪个。三个版本,题目、提示词、打分规则,完全一样。
1005 道题,各答对多少
FP16 答对 595 道;Q8_0 答对 590 道;Q4_K_M 答对 555 道。Q8_0 少对 5 道,Q4_K_M 少对 40 道。可是,模型每压一次,总会有些题换了答案。哪个差距是真的,哪个只是碰巧?
那就一题一题地比。一格就是一道题;跟 FP16 选得一样的,是浅色。先看 Q8_0:976 道选得一模一样,只有 29 道换了答案。其中,13 道从对变错,8 道从错变对,还有 8 道,是从一个错的,换成了另一个错的。13 比 8,就像抛 21 次硬币,13 次正面,这很常见。所以,少的这 5 道,分不清是真变差,还是碰巧。再看 Q4_K_M:换了答案的,一下多到 185 道。从对变错的 87 道,从错变对的,只有 47 道。如果两个版本一样好,差这么多的可能,不到千分之一。所以,Q4_K_M 确实变差了,约少对 4 个百分点。不过,这是一个 1.7B 的小模型、一套选择题,关掉思考,只看第一个字母;写长回答会怎样,我们没有测。换一个模型、换一种任务,要自己再测。
三个版本,放在一起看
总结一下。量化,就是每个参数少用几位来记,文件就按位数变小。文件小了,生成间隔就短,但读输入基本没变。答题上,Q8_0 跟 FP16 版本分不出差别;Q4_K_M 在这套选择题上,少对了 40 道。
下次挑一个量化版本,先问三件事
下次挑一个量化版本,先问三件事:第一,放得下吗?文件大小,约等于参数个数,乘以平均位数,再除以 8;运行的时候,还要再多占一些内存。第二,慢在哪?量化主要缩短生成间隔,读长输入,不会快多少。第三,答得还对吗?拿你自己的题,几个版本各跑一遍,逐题比。
今天,我们把量化拆开看了一遍:它省下了位数,换来了速度;在这套选择题上,Q4_K_M 也付出了一点准确率。下一期,我们就在自己的电脑上,用 Ollama,把模型跑起来。