它改变表示,不自动改变数学结构
量化位于模型参数/激活的数值表示与算子实现层。它不改变线性层的逻辑 shape,也不等同剪枝、蒸馏或 KV Cache 量化;后者把同一原则应用到跨 decode step 持久保存的 K/V,并有独立的生命周期与 metadata 成本。
对线性层:
常见仿射量化写成:
是 scale, 是 zero point。对称 signed INT8 常取 、;asymmetric 格式则保留非零 。 是能被浮点 reference 使用的反量化近似,不会与原始 普遍 bitwise 相同。
Granularity 决定 scale 的 shape
仍以 为例:
| 粒度 | scale / zero-point 逻辑 shape | 代价与适用边界 |
|---|---|---|
| per-tensor weight | [1,1] | metadata 最少;不同输出通道共享动态范围,outlier 影响最大 |
| per-output-channel weight | [1,N] | 每个输出通道独立 scale;常见于权重量化,metadata 随 增长 |
| group-wise weight,group size | [K/G,N] | 每个输出通道沿 分组;误差通常更小,但 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 代入
取 、:
- per-tensor 权重 scale 为
[1,1]; - per-output-channel scale 为
[1,6],沿 归约; - per-token activation scale 为
[4,1],沿 归约; - 输出与整数 accumulator 都为
[4,6]。
这个小权重若原本按 FP16 保存,占 bytes。INT8 payload 为 96 bytes,再加 6 个 FP32 scales 的 24 bytes,共 120 bytes,即 1.6×,而不是理想的 2×。大矩阵可以更好地摊薄 scale,但还要计入 zero points、packing 对齐、padding、workspace 和 framework object 开销。
Weight-only 与 W8A8 是两条不同数据路径
Weight-only
Weight-only quantization 让 以 INT8/INT4 等格式存储, 仍是 FP16/BF16/FP32。概念上的输出为:
生产 kernel 通常边加载 packed weights 边反量化,或使用支持相应格式的矩阵乘路径。主要机会是减少每步重复读取的权重 bytes,因此对小 batch decode 的低复用 GEMV/小 GEMM尤其相关;长 prefill 已可能受算力限制,反量化、packing 与 kernel 效率会决定是否受益。
W8A8
W8A8 同时量化 activation 和 weight。若 使用 per-token scale , 使用 per-output-channel scale ,对称 INT8 可分解为:
、 是 INT8;教学实现显式令 为 INT32。生产硬件的输入格式、累加位宽、scale 应用位置和 epilogue 由具体 kernel 决定,不能从 “W8A8” 这个标签单独推出。若使用 asymmetric zero points,展开式还包含行和、列和等校正项。
| 路径 | 低位宽持久对象 | 运行时额外工作 | 最常见风险 |
|---|---|---|---|
| Weight-only | weights + scales/zeros | 反量化或低比特 dot-product | activation 仍占原 dtype;kernel/fusion 不佳抵消带宽收益 |
| W8A8 | weights;activation 在运行时量化 | activation range、quantize、整数累加、rescale | activation outlier、校准漂移、量化边界破坏 fusion |
| Fake quant | 通常没有;立即变回 float | round/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 性能。
成本、失败场景与生产映射
量化后的权重容量应写成:
若 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 拥有 的 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 边界。
- PyTorch — Quantization in Practice
- PyTorch — torch.quantize_per_tensor
- Xiao et al., SmoothQuant — W8A8 activation outlier 与等价尺度变换背景。
- TensorRT-LLM — Quantization — 生产格式、kernel 支持与配置示例。