量化粒度本质上问的是"多少个数共享一个scale":per-tensor用1个scale控制4500万权重,per-channel用4096个scale,per-group用86个scale。粒度越细精度越高,内存开销也越大。
Per-tensor、per-channel、per-token 和 per-group 是同一个问题的四种叫法:多少个数共享一个 scale?其他一切——它们的内存开销、它们保持的精度、内核是否能使用它们——都从这个问题的答案推导而来。
量化值以一个小整数存储。要还原它所代表的真实数值,需要一个 scale,还可以选一个 zero point:real ≈ scale × (code − zero)。Scale 必须来自某个地方,它总是从某组值的范围中导出。Granularity 就是这组值的大小。
Per-tensor。整个矩阵共用一个 scale。4096×11008 层中的每一个权重——共 4500 万个——都根据矩阵中任意位置的最大绝对值导出的单一数值来衡量。
Per-channel。每行一个 scale(对于权重,通常是每个输出通道),因此具有 4096 个输出通道的矩阵携带 4096 个 scale。每个输出神经元的权重根据各自的范围进行缩放。
Per-group。沿输入维度,每行内每 g 个连续权重共享一个 scale。当 g = 128 且输入特征为 11008 时,该行现在携带 86 个 scale 而非 1 个。
对于激活值,等效轴有不同名称,原因相同:per-token 表示激活矩阵每行一个 scale,即批处理中每个 token 一个 scale,它是 per-channel 在激活值端的对应概念。
以形状为 [4096, 11008] 的单个权重矩阵为例——共 4510 万个权重——使用 4 位量化,并为每个 scale 组存储 16 位 scale 和 4 位 zero point。权重有效载荷在表中每一行都相同,只有元数据在移动。
weights 45.1e6 * 4 bits = 22.55 MB payload
per-tensor 1 scale = 20 bits
overhead per weight ≈ 0.0000004 bits
per-channel (row) 4096 scales = 81,920 bits
overhead per weight = 20/11008 ≈ 0.0018 bits
per-group g=128 4096 * 86 = 352,256 = 7.05e6 bits
overhead per weight = 20/128 = 0.156 bits
per-group g=32 4096 * 344 = 1.41e6 = 2.82e7 bits
overhead per weight = 20/32 = 0.625 bits
这个结果的形态才是值得记住的东西。Per-tensor 和 per-channel 的开销都可以忽略不计——per-channel 的开销不足有效载荷的二十分之一,而第一个产生可衡量开销的粒度是 per-group。这就是为什么 per-channel 权重量化是通用且从无争议的,也是为什么人们唯一调优的粒度就是 group size。GPTQ 和 AWQ 中那个特定选择的算术取决于 group size。
同一张表的精度侧比成本侧要剧烈得多,而这才是重点。假设 4500 万权重矩阵中有一个权重大小为 0.9,而典型权重大小为 0.02。Per-tensor 4 位网格在 16 个刻度上跨越 [-0.9, 0.9],因此步长为 0.12——是典型权重的 6 倍。本质上矩阵中每个权重都舍入到零。Per-channel 将这场灾难限制在单个输出行;g = 128 的 per-group 将其限制在 4500 万中的 128 个权重。元数据成本购买了三个数量级的containment,这就是为什么没有人发布低于 8 位的 per-tensor 权重。
关于前面算术的一个注记:per-channel 行假设沿输入维度分组,因此一个输出通道的 11008 个权重共享 20 位。如果格式改为每个输入通道存储一个 scale,则除数是 4096,开销为每权重 0.0049 位——相同的数量级,不同的常数。这是"per-channel"这个短语中的真实歧义,在将文件大小与 GPTQ 和 AWQ 中的 group size 匹配之前,值得检查格式指的是哪个轴。
权重有一个特性,使细粒度在第二种意义上也变得廉价:它们在推理运行前就已知。从权重自己的组计算出的 scale 只需计算一次,离线进行,然后存储。它除了与它所属的权重一起被读取外,不需要任何运行时工作——而由于 4 位仅权重内核已经以最快速度读取内存,每权重额外的 0.156 位是它必须移动的字节数的 0.156/4.156 ≈ 3.8% 增长。
精度论证同样直接。对于 4500 万权重,单个 scale 由其中最大的单个权重决定,因此一个极端值为所有内容设置了网格步长。缩小共享 scale 的集合会缩小设置它的范围,而在相同级别数下更窄的范围意味着更细的步长。每种广泛使用的量化格式都在这条曲线的某个位置;细粒度端是所有为 4 位及以下设计的格式所在的地方。
激活值得到同一频谱的粗粒度端,原因不是内存——而是在需要数字的时刻它们必须在哪里。
在量化矩阵乘法中,累加运算遍历归约维度。沿该维度变化的 scale 无法从总和中分解出来:你必须分别在最内层循环中重新缩放每个部分积,这正是整数张量核心要避免的算术。沿输出维度变化的 scale——权重的每个输出通道、激活值的每个 token——可以被提取出来并一次性应用到完成的累加器上,成本几乎为零。
所以实际规则是结构性的而非经验性的:权重可以沿归约维度按组进行量化,因为去量化发生在权重解包时,在乘法之前。激活值通常按 per-tensor 或 per-token 进行量化,因为任何更细的粒度都会落入累加内部。这就是为什么激活值量化必须通过移动来处理异常值,而不是通过隔离它们——参见 SmoothQuant 和异常值特征。
KV cache 是第三种情况,它介于两者之间。缓存的键和值是激活值,所以它们的 scale 无法离线知道,但与 matmul 内部的激活值不同,它们是被存储而不是立即消费的——这意味着 scale 可以附加在每个 head、每个 token 或每个缓存块上,而不会进入累加。这就是为什么缓存量化方案 routinely 使用对馈送线性层的激活值来说不可能的粒度,以及为什么 8 位或 4 位缓存往往比相同位宽应用于一般激活值造成的质量损失更小。
配置很少使用"granularity"这个词。它们改用:
group_size=128 — per-group 权重。GPTQ 和 AWQ 配置中的 group_size=-1 表示 per-channel,即每输出行一个 scale。
W8A8 配合"dynamic per-token"激活值 — per-tensor 或 per-channel 权重,运行时计算的 per-token 激活值。
bitsandbytes 和 FP8 方案中的 Block-wise — per-group 的另一种称呼。NF4 使用 64 的块;Transformers 的 FineGrainedFP8Config 文档记录了默认 weight_block_size 为 (128, 128),这是二维块而不是沿一个轴的运行。
llama.cpp 的 K-quants 中的 Super-blocks — 一种层次结构,其中一个块有自己的量化 scale,超块有一个覆盖这些块的 scale。该结构在 K、M 和 S 在 GGUF 量化名称中的含义中解码。
当两种格式声称相同的位宽但产生不同的文件大小时,粒度几乎总是原因,第二节中的算术将解释差异。