量化基础与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_F3232标准浮点
GGML_TYPE_F1616半精度浮点
GGML_TYPE_Q8_08对称量化,block size 32
GGML_TYPE_Q4_04对称量化,block size 32
GGML_TYPE_Q4_14非对称量化,带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和缓存优化的。理解了这个,你才能写出高效的量化算子。

知识体系结构图

下面这张图,是我梳理的量化知识体系。从数据类型到内存布局,再到实际算子,一层层往下走。

量化知识体系结构图 量化动机:内存 & 速度 数据类型 FP16 | INT8 (Q8_0) | INT4 (Q4_0, Q4_1) 量化方法 对称量化 | 非对称量化 | Per-channel | Per-block GGML内存布局 Block结构 | 参数打包 | 缓存对齐 算子实现:反量化 → 计算 → 再量化

这张图我画了好几次才满意。每一层都依赖下一层,但实际开发时,我建议从底层往上走——先搞懂内存布局,再写算子,最后选量化类型。

避坑指南

最后分享几个我踩过的坑:

  • 量化参数精度:scale和zero point一定要用FP16存。我用FP32存过,内存占用多了,精度没提升。
  • 边界处理:block size不一定能整除张量维度。最后一块不满怎么办?GGML的做法是补零。我一开始没处理,结果推理结果全是NaN。
  • 多线程量化:量化过程可以并行。每个block独立计算scale和量化值。我用OpenMP一跑,速度提升了4倍。

好了,这一章就到这里。量化这块内容多,但搞懂了,后面写算子就轻松了。


专注资料整理