全部术语

GLOSSARY ENTRY

推理量化基础

  • Quantization
  • Weight-only Quantization
  • W8A8

用 scale、可选 zero point 与明确粒度把浮点权重或激活映射到低位宽表示;它可能降低存储和带宽,但只有真实低位宽 kernel 消费该表示时才可能兑现性能收益。

它改变表示,不自动改变数学结构

量化位于模型参数/激活的数值表示与算子实现层。它不改变线性层的逻辑 shape,也不等同剪枝、蒸馏或 KV Cache 量化;后者把同一原则应用到跨 decode step 持久保存的 K/V,并有独立的生命周期与 metadata 成本。

对线性层:

Y=XW,XRM×K,WRK×N,YRM×NY=XW,\qquad X\in\mathbb{R}^{M\times K},\quad W\in\mathbb{R}^{K\times N},\quad Y\in\mathbb{R}^{M\times N}

常见仿射量化写成:

q=clip(round(x/s)+z,qmin,qmax),x^=s(qz)q=\operatorname{clip}\left(\operatorname{round}(x/s)+z,q_{min},q_{max}\right), \qquad \hat{x}=s(q-z)

s>0s>0 是 scale,zz 是 zero point。对称 signed INT8 常取 z=0z=0q[127,127]q\in[-127,127];asymmetric 格式则保留非零 zzx^\hat{x} 是能被浮点 reference 使用的反量化近似,不会与原始 xx 普遍 bitwise 相同。

Granularity 决定 scale 的 shape

仍以 W:[K,N]W:[K,N] 为例:

粒度scale / zero-point 逻辑 shape代价与适用边界
per-tensor weight[1,1]metadata 最少;不同输出通道共享动态范围,outlier 影响最大
per-output-channel weight[1,N]每个输出通道独立 scale;常见于权重量化,metadata 随 NN 增长
group-wise weight,group size GG[K/G,N]每个输出通道沿 KK 分组;误差通常更小,但 scale、packing 与 kernel 更复杂
per-tensor activation[1,1]简单,但请求/token 间动态范围差异大
per-token activation[M,1]每个 token row 独立 scale;需要运行时求范围并把 scale 交给 kernel

“更细粒度一定更准”也不是无条件结论:它提供更多 scale 自由度,但最终误差还取决于 clipping、校准数据、outlier、位宽、是否量化 bias/残差,以及下游算子敏感性。更细粒度还会增加 metadata bytes 和地址计算。

一个具体 shape 代入

X:[4,16]X:[4,16]W:[16,6]W:[16,6]

  • per-tensor 权重 scale 为 [1,1]
  • per-output-channel scale 为 [1,6],沿 K=16K=16 归约;
  • per-token activation scale 为 [4,1],沿 K=16K=16 归约;
  • 输出与整数 accumulator 都为 [4,6]

这个小权重若原本按 FP16 保存,占 16×6×2=19216\times6\times2=192 bytes。INT8 payload 为 96 bytes,再加 6 个 FP32 scales 的 24 bytes,共 120 bytes,即 1.6×,而不是理想的 。大矩阵可以更好地摊薄 scale,但还要计入 zero points、packing 对齐、padding、workspace 和 framework object 开销。

Weight-only 与 W8A8 是两条不同数据路径

Weight-only

Weight-only quantization 让 WW 以 INT8/INT4 等格式存储,XX 仍是 FP16/BF16/FP32。概念上的输出为:

Y^=XW^=X(sWqW)\hat{Y}=X\hat{W}=X(s_W\odot q_W)

生产 kernel 通常边加载 packed weights 边反量化,或使用支持相应格式的矩阵乘路径。主要机会是减少每步重复读取的权重 bytes,因此对小 batch decode 的低复用 GEMV/小 GEMM尤其相关;长 prefill 已可能受算力限制,反量化、packing 与 kernel 效率会决定是否受益。

W8A8

W8A8 同时量化 activation 和 weight。若 XX 使用 per-token scale smXs^X_mWW 使用 per-output-channel scale snWs^W_n,对称 INT8 可分解为:

Amn=k=0K1qmkXqknW(整数累加)A_{mn}=\sum_{k=0}^{K-1}q^X_{mk}q^W_{kn}\quad\text{(整数累加)} Y^mn=smXsnWAmn\hat{Y}_{mn}=s^X_m s^W_n A_{mn}

qX:[M,K]q^X:[M,K]qW:[K,N]q^W:[K,N] 是 INT8;教学实现显式令 A:[M,N]A:[M,N] 为 INT32。生产硬件的输入格式、累加位宽、scale 应用位置和 epilogue 由具体 kernel 决定,不能从 “W8A8” 这个标签单独推出。若使用 asymmetric zero points,展开式还包含行和、列和等校正项。

路径低位宽持久对象运行时额外工作最常见风险
Weight-onlyweights + scales/zeros反量化或低比特 dot-productactivation 仍占原 dtype;kernel/fusion 不佳抵消带宽收益
W8A8weights;activation 在运行时量化activation range、quantize、整数累加、rescaleactivation outlier、校准漂移、量化边界破坏 fusion
Fake quant通常没有;立即变回 floatround/clip/dequant + 浮点算子把误差模拟误报成容量或速度收益

可运行的语义实验

实验问题:构造动态范围不同的权重列时,per-channel 是否比 per-tensor 更好地拟合本例权重?W8A8 的整数累加分解是否等价于先反量化输入再做浮点 matmul?

预期结果:per-channel 权重平均绝对误差低于 per-tensor;W8A8 分解断言通过,但相对原始浮点输出仍存在量化误差。代码不进行性能计时,因为显式 broadcast 乘法不是生产整数 GEMM。

import platform
import torch

torch.manual_seed(149)
DEVICE=torch.device("cuda" if torch.cuda.is_available() else "cpu")
DTYPE=torch.float32

def symmetric_quantize(x,scale,qmax=127):
    scale=scale.clamp_min(torch.finfo(x.dtype).eps)
    return torch.round(x/scale).clamp(-qmax,qmax).to(torch.int8)

def quantize_weight_per_tensor(weight):
    scale=weight.abs().amax().reshape(1,1)/127
    return symmetric_quantize(weight,scale),scale

def quantize_weight_per_output_channel(weight):
    scale=weight.abs().amax(dim=0,keepdim=True)/127
    return symmetric_quantize(weight,scale),scale

def quantize_activation_per_token(x):
    scale=x.abs().amax(dim=1,keepdim=True)/127
    return symmetric_quantize(x,scale),scale

def int32_accumulate(qx,qw):
    products=qx.to(torch.int32).unsqueeze(2)*qw.to(torch.int32).unsqueeze(0)
    return products.sum(dim=1,dtype=torch.int32)

def main():
    m,k,n=4,16,6
    row_range=torch.tensor([.05,.25,1.,2.],device=DEVICE)[:,None]
    col_range=torch.tensor([.04,.1,.3,.8,1.5,3.],device=DEVICE)[None,:]
    x=torch.randn(m,k,device=DEVICE,dtype=DTYPE)*row_range
    weight=torch.randn(k,n,device=DEVICE,dtype=DTYPE)*col_range
    reference=x@weight

    q_tensor,s_tensor=quantize_weight_per_tensor(weight)
    q_channel,s_channel=quantize_weight_per_output_channel(weight)
    weight_tensor=q_tensor.float()*s_tensor
    weight_channel=q_channel.float()*s_channel
    weight_only=x@weight_channel
    tensor_mae=(weight_tensor-weight).abs().mean()
    channel_mae=(weight_channel-weight).abs().mean()
    assert channel_mae<tensor_mae

    qx,sx=quantize_activation_per_token(x)
    accumulator=int32_accumulate(qx,q_channel)
    w8a8=accumulator.float()*sx*s_channel
    explicit=(qx.float()*sx)@(q_channel.float()*s_channel)
    torch.testing.assert_close(w8a8,explicit,rtol=1e-6,atol=1e-6)
    assert accumulator.shape==reference.shape==(m,n)
    assert accumulator.dtype==torch.int32

    fp16_bytes=weight.numel()*2
    int8_bytes=q_channel.numel(); scale_bytes=s_channel.numel()*s_channel.element_size()
    print(f"python={platform.python_version()} torch={torch.__version__}")
    print(f"device={DEVICE} dtype={DTYPE} X={tuple(x.shape)} W={tuple(weight.shape)}")
    print(f"per_tensor_scale={tuple(s_tensor.shape)} per_channel_scale={tuple(s_channel.shape)}")
    print(f"weight_mae per_tensor={tensor_mae:.6f} per_channel={channel_mae:.6f}")
    print(f"weight_only_output_max_abs_error={(weight_only-reference).abs().max():.6f}")
    print(f"W8A8 accumulator={tuple(accumulator.shape)} dtype={accumulator.dtype}")
    print(f"W8A8_output_max_abs_error={(w8a8-reference).abs().max():.6f}")
    print(f"W8A8_factorization_max_abs_diff={(w8a8-explicit).abs().max():.3e}")
    print(f"weight_storage fp16={fp16_bytes} int8_payload={int8_bytes} scales={scale_bytes} bytes")

if __name__=="__main__":
    with torch.inference_mode(): main()

完整文件位于 examples/quantization_basics.py。2026-08-13 在 Python 3.11.12、PyTorch 2.13.0+cu130、RTX 3080 上,per-tensor / per-channel 权重 MAE 分别为 0.007932 / 0.002662;weight-only 与原始输出最大差 0.026886,W8A8 为 0.055309,W8A8 两种等价分解最大差 1.907e-06

这些误差只属于固定 seed 的微型张量,不能代替 perplexity、下游任务、长上下文和真实请求分布评估。脚本用 INT32 broadcast products 模拟累加;它甚至比浮点 matmul 分配更多临时内存,所以没有资格用来衡量低位宽 kernel 性能。

成本、失败场景与生产映射

量化后的权重容量应写成:

MW=KNbW8+Nscalesscale+Nzeroszero+Mpacking/paddingM_W=KN\frac{b_W}{8}+N_{scale}s_{scale}+N_{zero}s_{zero}+M_{packing/padding}

若 activation 也量化,还要计入 runtime range reduction、quantize buffer、scale 传递和可能的 layout conversion。收益成立至少需要:低位宽 payload 不被过早展开、shape/alignment 落在高效 kernel 支持范围、反量化与 epilogue 能融合,以及精度门槛允许所选粒度。

常见失败包括:

  • checkpoint 标为 INT4,但模型加载时展开成 FP16 权重,容量与带宽收益消失;
  • backend 不支持该 group size、dtype、transpose 或 GPU capability,静默/显式 fallback 到较慢路径;
  • decode 因权重带宽下降受益,prefill 却因额外反量化或低效大 GEMM 变慢;
  • calibration 数据没有覆盖服务中的 activation outlier;
  • TP 切分后 scale/zero-point 没有按同一轴切分,rank 本地反量化语义错误;
  • 只比较模型文件大小,忽略加载后的 allocated/reserved memory、workspace 与未量化层。

张量并行 中,哪个 rank 拥有 WW 的 shard,通常也应拥有对应的 payload、scale 与 zero point;scale 是按全局轴还是本地 shard 计算会影响数值语义。实际 kernel 由 Backend Dispatch/Fallback 决定,仅凭配置名不能证明执行格式。

Kernel Backend、Triton Blocking 与 Persistent Kernel继续说明 packing、alignment、tile 和 fused epilogue 怎样决定低位宽 payload 是否被原生消费;fake quant 或先反量化再调用浮点 GEMM 都不是低位宽 fast path 证据。

参考资料

GPTQ、AWQ、SmoothQuant 与 FP8在本页共同公式之上比较方法级优化对象、校准信息与 checkpoint/kernel 边界。