量化基础与GGML类型系统
大家好,欢迎来到第三章。
这一章,我们聊聊量化。说实话,这是整个llama.cpp最核心的部分之一。没有量化,大模型推理的成本高得吓人。我刚开始接触GGML时,第一反应就是:这量化方式怎么跟以前见过的都不一样?
为什么需要量化?
先想一个问题:一个70B的模型,用FP32存,需要多少显存?280GB。就算用FP16,也要140GB。消费级显卡根本扛不住。
量化的本质,就是用更少的比特数来表示权重。代价是精度损失。但好消息是,大模型对权重的精度其实没那么敏感。我在项目中试过,INT4量化后的模型,推理速度能快3-4倍,而困惑度只涨了不到1%。
核心思想:用精度换速度,用精度换内存。
FP16量化
FP16其实不算严格意义上的量化。它只是把32位浮点数砍成16位。但GGML里把它当作量化的第一步。
FP16的格式很简单:1位符号位,5位指数位,10位尾数位。相比FP32,指数范围窄了,精度也低了。但推理任务中,这通常够用。
我个人的习惯是:如果显存够,优先用FP16。它没有额外的量化反量化开销,速度最快。
INT8量化
INT8就有点意思了。它把每个权重映射到[-128, 127]的整数范围。怎么做?
// 伪代码:对称量化
scale = max(abs(weights)) / 127
quantized = round(weights / scale)
dequantized = quantized * scale
这里有个坑:scale是每个张量单独算的。不同层的权重分布不一样,共用scale会损失精度。我在优化一个7B模型时,就踩过这个坑——所有层用同一个scale,结果模型直接崩了。
小技巧:GGML支持per-channel量化,每个输出通道单独算scale。精度能提升不少。
INT4量化
INT4更激进。每个权重只占4个比特。但4比特能表示的值只有16个。怎么保证精度?
GGML的做法是分组量化。比如每32个权重一组,共享一个scale和zero point。这样每组都能自适应地调整量化范围。
你想想看,如果权重分布是[-2.0, 1.5],量化到[0, 15]范围,scale就是(1.5 - (-2.0)) / 15 ≈ 0.233。每个权重减去最小值再除以scale,取整就完事了。
注意:INT4的反量化开销比INT8大。因为要解包、乘scale、加zero point。我实测过,INT4的推理速度比INT8快约30%,但内存占用少一半。怎么选?看你的瓶颈是显存还是计算。
GGML张量类型
GGML定义了一套自己的类型系统。每个张量都有一个type字段,告诉系统怎么存、怎么算。
| 类型 | 比特数 | 说明 |
|---|---|---|
| GGML_TYPE_F32 | 32 | 标准浮点 |
| GGML_TYPE_F16 | 16 | 半精度浮点 |
| GGML_TYPE_Q8_0 | 8 | 对称量化,block size 32 |
| GGML_TYPE_Q4_0 | 4 | 对称量化,block size 32 |
| GGML_TYPE_Q4_1 | 4 | 非对称量化,带zero point |
这些类型不是随便定义的。每个类型都对应一套优化的kernel。比如Q4_0,我专门写过AVX2的版本,一次处理16个权重,速度比标量快5倍。
内存布局
这块容易让人迷糊。GGML的张量内存布局跟PyTorch不一样。它不是简单的行优先或列优先。
GGML把张量分成多个block。每个block包含一组权重和对应的量化参数。比如Q4_0,block size是32。每个block里,前16字节存量化后的权重(32个4比特值),后2字节存scale(FP16)。
为什么要这么设计?
为了缓存友好。计算时,一次加载一个block,反量化、计算、累加,一气呵成。数据局部性极好。
// Q4_0 block结构示意
typedef struct {
float16_t d; // scale
uint8_t qs[16]; // 32个4-bit权重
} block_q4_0;
我曾经犯过一个错:把block size设成64,想着能减少参数存储。结果缓存命中率暴跌,速度反而慢了。嗯,这里要注意:block size不是越大越好,得跟缓存行大小对齐。
关键点:GGML的内存布局是面向SIMD和缓存优化的。理解了这个,你才能写出高效的量化算子。
知识体系结构图
下面这张图,是我梳理的量化知识体系。从数据类型到内存布局,再到实际算子,一层层往下走。
这张图我画了好几次才满意。每一层都依赖下一层,但实际开发时,我建议从底层往上走——先搞懂内存布局,再写算子,最后选量化类型。
避坑指南
最后分享几个我踩过的坑:
- 量化参数精度:scale和zero point一定要用FP16存。我用FP32存过,内存占用多了,精度没提升。
- 边界处理:block size不一定能整除张量维度。最后一块不满怎么办?GGML的做法是补零。我一开始没处理,结果推理结果全是NaN。
- 多线程量化:量化过程可以并行。每个block独立计算scale和量化值。我用OpenMP一跑,速度提升了4倍。
好了,这一章就到这里。量化这块内容多,但搞懂了,后面写算子就轻松了。