欢迎光临
我们一直在努力

【推理优化】量化入门:从 FP16 到 INT8 为什么能让模型更快

请添加图片描述

量化的动机:访存成本与算力

大模型推理时,GPU 的算力(FLOPs)往往不是瓶颈,显存带宽(Memory Bandwidth) 才是。以单张 A100-80GB 为例,其 FP16 稠密算力高达 312 TFLOPS,而 HBM2e 显存带宽仅 2TB/s。这意味着,如果模型权重以 FP16(2 字节)存储,每读取 1MB 权重,GPU 搬运数据所需的时间约为 1MB2TB/s=0.5μs\\frac{1\\text{MB}}{2\\text{TB/s}} = 0.5\\mu s2TB/s1MB=0.5μs。那么,GPU 算完这 1MB 权重对应的浮点运算需要多久?假设每个参数对应一次乘加运算(2 FLOPs),则 1MB 权重包含约 5.2×1055.2 \\times 10^55.2×105 个参数(FP16),运算量约 1.05×1061.05 \\times 10^61.05×106 FLOPs,在 312 TFLOPS 下仅需约 3.4ns3.4\\text{ns}3.4ns——比读取时间快了三个数量级。看起来算力时间远小于访存时间?—— 关键在于 decode 阶段的批量大小通常为 1,权重只被一个 token 复用一次,算术强度极低。

字节占用:权重在显存中的物理成本

先算一笔具体的账。以 LLaMA-7B 模型为例,参数量约 70 亿。若以 FP16(半精度浮点,占用 2 字节) 存储,权重总字节数为:

7×109×2 bytes=14 GB7 \\times 10^9 \\times 2\\text{ bytes} = 14\\text{ GB}7×109×2 bytes=14 GB

加上 KV Cache 和中间激活值,整卡 A100-80GB 几乎被占满。若改用 INT8(1 字节) 存储,权重字节数直接减半至 7 GB;若用 INT4(0.5 字节),则仅需 3.5 GB。每降低 1 位位宽,权重显存占用就减半——这是量化最直观的收益,也为后续加速分析奠定了基础。

带宽收益:decode 阶段为什么被访存“卡脖子”

大模型推理分为 prefill(预填充) 和 decode(逐 token 生成) 两个阶段:

  • prefill 阶段:一次性处理整个输入序列,批量大、计算密集,接近计算受限(compute-bound)。此时算力是主导瓶颈。
  • decode 阶段:逐个生成 token,每步只处理 1 个新 token,但必须把全部权重从显存搬到计算单元。批量大小 B=1B=1B=1 时,所有权重只被 1 个 token 复用一次,导致算术强度(Arithmetic Intensity)极低,成为典型的访存受限(memory-bound) 场景。

decode 阶段的执行时间近似为:

tdecode≈权重字节数显存带宽t_{\\text{decode}} \\approx \\frac{\\text{权重字节数}}{\\text{显存带宽}}tdecode显存带宽权重字节数

注意公式里没有算力项——因为此时 GPU 的 FLOPs 远未被装满,真正决定速度的是搬了多少字节。因此,将权重从 FP16(2B)换成 INT8(1B),decode 单步延迟理论上直接减半。换到 INT4,则进一步减半。这就是量化加速的根本来源:它绕过了算力上限,直接缩短了访存时间。

下面用一个简化的 CPU 模拟来验证这一结论。我们用 Python 模拟一个 4B 参数的权重张量,对比 FP16 与 INT8 在受限带宽下的读取耗时:

import numpy as np
import time

# 模拟 4B 参数模型的权重(4e9 个标量)
num_params = 4_000_000_000

# 模拟显存带宽:A100 约 2 TB/s(保守取 1.5 TB/s 模拟真实负载)
bandwidth_bytes_per_sec = 1.5e12

def simulate_read_time(weight_bytes: int) > float:
"""模拟从显存读取 weight_bytes 字节所需的理论耗时"""
return weight_bytes / bandwidth_bytes_per_sec

# FP16 与 INT8 的字节占用
fp16_bytes = num_params * 2 # 2 bytes per param
int8_bytes = num_params * 1 # 1 byte per param

# 计算理论耗时(单位:毫秒)
fp16_time_ms = simulate_read_time(fp16_bytes) * 1000
int8_time_ms = simulate_read_time(int8_bytes) * 1000

print(f"FP16 权重占用: {fp16_bytes/1e9:.1f} GB")
print(f"FP16 读取耗时: {fp16_time_ms:.2f} ms")
print(f"INT8 权重占用: {int8_bytes/1e9:.1f} GB")
print(f"INT8 读取耗时: {int8_time_ms:.2f} ms")
print(f"加速比: {fp16_time_ms / int8_time_ms:.2f}x")

运行预期输出:

FP16 权重占用: 8.0 GB
FP16 读取耗时: 5.33 ms
INT8 权重占用: 4.0 GB
INT8 读取耗时: 2.67 ms
加速比: 2.00x

当批量大小为 1 时,decode 单步的延迟完全由这段数据搬运时间主导。可以看到,位宽减半,延迟减半,加速比严格为 2.0x——这不是估算,而是带宽瓶颈下的物理结论。

#mermaid-svg-8k8W5El8U2zp2yWg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8k8W5El8U2zp2yWg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8k8W5El8U2zp2yWg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8k8W5El8U2zp2yWg .error-icon{fill:#552222;}#mermaid-svg-8k8W5El8U2zp2yWg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8k8W5El8U2zp2yWg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8k8W5El8U2zp2yWg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8k8W5El8U2zp2yWg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8k8W5El8U2zp2yWg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8k8W5El8U2zp2yWg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8k8W5El8U2zp2yWg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8k8W5El8U2zp2yWg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8k8W5El8U2zp2yWg .marker.cross{stroke:#333333;}#mermaid-svg-8k8W5El8U2zp2yWg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8k8W5El8U2zp2yWg p{margin:0;}#mermaid-svg-8k8W5El8U2zp2yWg .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-8k8W5El8U2zp2yWg .cluster-label text{fill:#333;}#mermaid-svg-8k8W5El8U2zp2yWg .cluster-label span{color:#333;}#mermaid-svg-8k8W5El8U2zp2yWg .cluster-label span p{background-color:transparent;}#mermaid-svg-8k8W5El8U2zp2yWg .label text,#mermaid-svg-8k8W5El8U2zp2yWg span{fill:#333;color:#333;}#mermaid-svg-8k8W5El8U2zp2yWg .node rect,#mermaid-svg-8k8W5El8U2zp2yWg .node circle,#mermaid-svg-8k8W5El8U2zp2yWg .node ellipse,#mermaid-svg-8k8W5El8U2zp2yWg .node polygon,#mermaid-svg-8k8W5El8U2zp2yWg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8k8W5El8U2zp2yWg .rough-node .label text,#mermaid-svg-8k8W5El8U2zp2yWg .node .label text,#mermaid-svg-8k8W5El8U2zp2yWg .image-shape .label,#mermaid-svg-8k8W5El8U2zp2yWg .icon-shape .label{text-anchor:middle;}#mermaid-svg-8k8W5El8U2zp2yWg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8k8W5El8U2zp2yWg .rough-node .label,#mermaid-svg-8k8W5El8U2zp2yWg .node .label,#mermaid-svg-8k8W5El8U2zp2yWg .image-shape .label,#mermaid-svg-8k8W5El8U2zp2yWg .icon-shape .label{text-align:center;}#mermaid-svg-8k8W5El8U2zp2yWg .node.clickable{cursor:pointer;}#mermaid-svg-8k8W5El8U2zp2yWg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8k8W5El8U2zp2yWg .arrowheadPath{fill:#333333;}#mermaid-svg-8k8W5El8U2zp2yWg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8k8W5El8U2zp2yWg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8k8W5El8U2zp2yWg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8k8W5El8U2zp2yWg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8k8W5El8U2zp2yWg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8k8W5El8U2zp2yWg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8k8W5El8U2zp2yWg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8k8W5El8U2zp2yWg .cluster text{fill:#333;}#mermaid-svg-8k8W5El8U2zp2yWg .cluster span{color:#333;}#mermaid-svg-8k8W5El8U2zp2yWg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8k8W5El8U2zp2yWg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8k8W5El8U2zp2yWg rect.text{fill:none;stroke-width:0;}#mermaid-svg-8k8W5El8U2zp2yWg .icon-shape,#mermaid-svg-8k8W5El8U2zp2yWg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8k8W5El8U2zp2yWg .icon-shape p,#mermaid-svg-8k8W5El8U2zp2yWg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8k8W5El8U2zp2yWg .icon-shape .label rect,#mermaid-svg-8k8W5El8U2zp2yWg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8k8W5El8U2zp2yWg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8k8W5El8U2zp2yWg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8k8W5El8U2zp2yWg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

FP16 2B

INT8 1B

INT4 0.5B

decode 每步生成 1 token

需读取全部权重

带宽受限

1x 延迟

0.5x 延迟

0.25x 延迟

吞吐: t tokens/s

对 decode 的影响:为什么 prefill 不受同等收益

量化对 decode 是“雪中送炭”,对 prefill 则是“锦上添花”。prefill 阶段批量大(B≫1B \\gg 1B1),权重会被大量 token 复用,算术强度高,此时算力是主导瓶颈。量化虽然也减小了权重字节数、缓解了部分访存压力,但 prefill 的峰值算力需求(FP16 算力 312 TFLOPS)并未因量化而改变——因为矩阵乘法仍可能以 FP16 精度计算,只是权重被反量化或直接用 INT8 矩阵乘单元(如 A100 的 INT8 Tensor Core 提供 624 TOPS)。因此,prefill 的加速比通常低于理论值,而 decode 阶段则能接近甚至达到位宽反比的理论加速。

另外一个常被忽视的收益是 KV Cache 的显存节省。decode 越往后,KV Cache 占用越大。若用 INT8 存储 KV Cache,其字节占用同样减半,这意味着同一张卡能支持更长的上下文长度,或容纳更大的批量——这对在线服务的吞吐至关重要。

本节核心结论:量化加速的本质,是绕开算力上限、直击访存瓶颈——decode 阶段延迟与权重字节数成正比,位宽每减半,单步延迟就减半。理解了这一点,下一节我们将进入量化的核心机制:什么是定点数、scale 与 zero-point,以及对称/非对称量化如何用极小的精度损失换取 2~4 倍的推理加速。

量化基础概念

前面我们论证了降低权重位宽能直接缓解 decode 阶段的访存瓶颈,但一个根本问题尚未回答:FP16 描述的是连续实数,而 INT8 或 INT4 只能表示离散整数,如何用整数忠实地表达原本的浮点数值? 答案就是本节要讲的三个概念:scale、zero-point 与对称/非对称量化。

Scale 与 Zero-Point:量化映射的一对参数

量化的本质是建立浮点实数 rrr 与整数 qqq 之间的线性映射。任意实数 rrr 可以用如下公式转换为整数 qqq

q=round(rscale+zero_point) q = \\text{round}\\left(\\frac{r}{\\text{scale}} + \\text{zero\\_point}\\right) q=round(scaler+zero_point)

反向映射(反量化)则为:

r=(q−zero_point)×scale r = (q – \\text{zero\\_point}) \\times \\text{scale} r=(qzero_point)×scale

其中两个参数的物理含义非常直观:

  • scale(缩放因子):浮点数值域中每个整数步长代表的实际间隔。已知浮点范围 [rmin⁡,rmax⁡][r_{\\min}, r_{\\max}][rmin,rmax] 和整数范围 [qmin⁡,qmax⁡][q_{\\min}, q_{\\max}][qmin,qmax],scale 定义为:

scale=rmax⁡−rmin⁡qmax⁡−qmin⁡ \\text{scale} = \\frac{r_{\\max} – r_{\\min}}{q_{\\max} – q_{\\min}} scale=qmaxqminrmaxrmin

  • zero_point(零点偏移):浮点值 000 对应的整数。它保证了浮点零能被精确表示——这一点在神经网络中至关重要,因为 padding、ReLU 输出等大量数值恰好为 0。zero_point 的计算公式为:

zero_point=round(qmin⁡−rmin⁡scale) \\text{zero\\_point} = \\text{round}\\left(q_{\\min} – \\frac{r_{\\min}}{\\text{scale}}\\right) zero_point=round(qminscalermin)

量化误差的来源也一目了然:round(⋅)\\text{round}(\\cdot)round() 操作引入的舍入误差,其上限为 scale 的一半,即 scale2\\frac{\\text{scale}}{2}2scale。因此 scale 越小,量化精度越高;但 scale 又受限于浮点数值范围——数值范围越大,scale 越大,精度越低。这就是量化中"范围-精度"的基本权衡。

对称量化:以零为中心,无偏移

对称量化(symmetric quantization)假设浮点数值关于 0 对称分布,即 rmax⁡=−rmin⁡r_{\\max} = -r_{\\min}rmax=rmin,因此 zero_point 恒为 0。映射关系简化为:

q=round(rscale),scale=rmax⁡qmax⁡ q = \\text{round}\\left(\\frac{r}{\\text{scale}}\\right), \\quad \\text{scale} = \\frac{r_{\\max}}{q_{\\max}} q=round(scaler),scale=qmaxrmax

对于 INT8 对称量化,整数范围为 qmin⁡=−127q_{\\min} = -127qmin=127qmax⁡=127q_{\\max} = 127qmax=127(注意不使用 -128,以保持映射的对称性),此时 scale=rmax⁡/127\\text{scale} = r_{\\max} / 127scale=rmax/127。例如,若权重分布在 [−0.5,0.5][-0.5, 0.5][0.5,0.5],则 scale ≈ 0.00394。

对称量化的优势在于:

  • zero_point 固定为 0,反量化时无需一次额外的整数减法运算,计算效率更高;
  • 让 0 恰好映射到整数 0,避免因 zero_point 舍入引入的额外偏移误差;
  • 部署时无需存储 zero_point 参数,权重布局更紧凑。
  • 但对称量化有一个显著缺陷:如果浮点权重分布偏向一侧——例如 ReLU 激活函数的输出恒为非负值,分布在 [0,rmax⁡][0, r_{\\max}][0,rmax]——对称量化会将其映射到扩大一倍的数值范围 [−rmax⁡,rmax⁡][-r_{\\max}, r_{\\max}][rmax,rmax],导致大量整数区间被闲置,scale 被迫放大一倍,精度直接减半。

    非对称量化:用偏移覆盖偏斜分布

    非对称量化(asymmetric quantization)正是为处理上述偏斜分布而设计的。它直接用全局最小值 rmin⁡r_{\\min}rmin 和最大值 rmax⁡r_{\\max}rmax 划定范围,通过 zero_point 把浮点区间整体"垫"到整数域上。

    以 ReLU 输出为例,假设某个张量的激活值分布在 [0,6.0][0, 6.0][0,6.0],用 INT8 非对称量化(此时 qmin⁡=0q_{\\min}=0qmin=0, qmax⁡=255q_{\\max}=255qmax=255):

    scale=6.0−0255−0≈0.0235,zero_point=round(0−00.0235)=0 \\text{scale} = \\frac{6.0 – 0}{255 – 0} \\approx 0.0235,\\quad \\text{zero\\_point} = \\text{round}\\left(0 – \\frac{0}{0.0235}\\right) = 0 scale=25506.000.0235,zero_point=round(00.02350)=0

    对比对称量化(qmax⁡=127q_{\\max}=127qmax=127)下 scale = 6.0/127≈0.04726.0 / 127 \\approx 0.04726.0/1270.0472——非对称量化的 scale 恰好缩小一半,量化精度提升了一倍。

    非对称量化的代价同样明显:

    • 需要一个额外的 zero_point 参数参与乘加运算(虽然现代硬件通过指令融合基本可忽略);
    • 若浮点分布是"长尾"形态(极端值远偏离主体),min/max 范围会被拖宽,scale 增大、精度下降。

    下表总结了两种方案的适用场景:

    方案zero_pointscale 精度适用分布计算开销
    对称量化 0 朴素(范围可能浪费一半) 权重(近似以 0 为中心)
    非对称量化 非 0 精细(贴合真实范围) 激活值(ReLU 输出、LayerNorm 结果等) 略高

    实践中的搭配策略

    在实际的量化推理管线中,对称与非对称量化通常不是"二选一",而是按张量类型分别选择:

    • 权重张量:经过 LayerNorm 或权重衰减约束,其分布天然以 0 为中心、相对紧凑,因此几乎总是使用对称量化;
    • 激活张量:经过 ReLU/GELU 等激活函数后,分布高度偏斜,且每个 token 的动态范围不同,因此倾向于使用非对称量化,配合动态校准(在推理时实时计算 scale/zero_point)或静态校准(校准集预先统计)。

    理解 scale 和 zero_point 的精确含义,是设计校准方法的前提。而校准的终极目标就一句话:选出一组 scale/zero_point,使量化后的误差在模型的输出端最小化。下一节我们将深入校准的两种主流实现——MinMax 与百分位法——并量化分析它们各自引入的误差特征。

    从 FP16 到 INT8/INT4 的类型选择

    映射机制清楚了,接下来自然的问题是:市面上有 FP8、INT8、INT4、nf4 这么多种低精度格式,到底该选哪个? 答案不是「越小越好」这么简单——每种格式的数值分布、量化误差和适用场景都截然不同,选错了甚至可能让模型输出质量断崖式下跌。

    三类候选:FP8、INT8 与 INT4

    先明确一个容易混淆的点:FP8 是浮点格式,INT8/INT4 是定点格式,nf4 是 INT4 的一种特殊变体。它们的本质区别在于:FP8 仍然用指数和尾数表达数值(只是位数更少),而 INT 系列放弃浮点表达,完全依赖 scale/zero-point 做线性映射。

    • FP8(E4M3 / E5M2):以 NVIDIA H100 引入的 FP8 为例,E4M3(4 位指数、3 位尾数)表达范围约 ±448\\pm 448±448,相对精度约为 2−3=12.5%2^{-3}=12.5\\%23=12.5%;E5M2(5 位指数、2 位尾数)范围更大(约 ±57344\\pm 57344±57344),但精度降至 2−2=25%2^{-2}=25\\%22=25%。由于保留了指数量级,FP8 对动态范围大、含离群值的权重(如 attention 层的 Q/K 矩阵)容忍度更高,且无需校准 scale——这是硬件原生支持带来的优势。

    • INT8:128 或 256 个离散级别(对称/非对称),相对精度固定为 ∼0.4%\\sim 0.4\\%0.4%1/2551/2551/255)。它没有指数部分,适合数值分布集中的权重。实测表明,LLaMA-7B 的权重分布近似均值为 0 的正态分布,99.9% 的值集中在 [−0.1,0.1][-0.1, 0.1][0.1,0.1] 区间内,这恰好是 INT8 的高精度区间。

    • INT4 / nf4:INT4 只有 16 个离散级别,均匀分布时相对精度高达 6.25%6.25\\%6.25%1/161/161/16)——这听起来很吓人。但 nf4(Normal Float 4) 巧妙之处在于:它的 16 个量化级别按正态分布的分位数不均匀排布,即值越接近 0、量化区间越密集,值越大、区间越稀疏。这使得权重集中区域的相对误差从 6.25%6.25\\%6.25% 降至约 0.5%0.5\\%0.5%,代价是远离 0 的尾部值误差极大。对于权重分布近似正态的模型,nf4 的平均量化误差远低于均匀 INT4。

    三者的量化误差对比如下表:

    格式位宽相对精度(有效位数)数值范围动态范围适应度典型用例
    FP8(E4M3) 1 字节 ∼12.5%\\sim 12.5\\%12.5% ±448\\pm 448±448 高(指数位) 训练/推理中高阶张量交换
    INT8 1 字节 ∼0.4%\\sim 0.4\\%0.4% 由 scale 决定 低(需校准) 权重/激活全面量化
    INT4(均匀) 0.5 字节 6.25%6.25\\%6.25% 由 scale 决定 对误差不敏感的层
    nf4 0.5 字节 ∼0.5%\\sim 0.5\\%0.5%(均值附近) 由 scale 决定 中(靠分布化) 4-bit QLoRA 微调

    显存节省:以 7B 模型为基准的直观对比

    显存节省是线性的,无需复杂计算——权重位宽减半,显存占用就减半。以一个 7B 参数的模型为例:

    存储格式单参数占位权重显存总量相比 FP16 节省
    FP16 2 字节 14 GB 基准
    FP8 / INT8 1 字节 7 GB 50%
    INT4 / nf4 0.5 字节 3.5 GB 75%

    但这只是权重的占用。实际推理时还需要考虑 KV cache(每个 token 约 1-2MB)和中间激活。以 8K 上下文、batch size 为 1 的 decode 场景估算:FP16 下 KV cache 约 4GB,而 INT8 下 KV cache 也缩至 2GB。因此,INT8 的端到端显存节省在 50% 到 60% 之间,而 INT4 能将推理所需的全部显存压进消费级显卡——这正是 4-bit 量化能在一张 24GB 的 RTX 4090 上运行 70B 级模型的原因。

    适用任务:误差特征决定选择边界

    显存节省是硬性收益,但精度损失是软性代价,两者必须放在任务上下文里权衡:

    • 生成质量关键型任务(代码生成、数学推理):优先 FP8 或 INT8。这类任务对 token 级误差极其敏感——一个数值偏差可能导致后续整段生成逻辑崩塌。实测中,7B 模型在 HumanEval 上 FP8 的 pass@1 仅下降 0.5%,INT8 下降约 1-2%,而均匀 INT4 最多可损失 8% 以上。

    • 对话/摘要/翻译任务:INT8 是稳妥选择;若追求极致显存,nf4 是 4-bit 中的首选。因为 nf4 将有限的 4-bit 精度集中给了权重分布的主体部分,语义级别的误差在生成自然的短句时被自然稀释。

    • 微调场景(QLoRA):nf4 + 低秩适配器(LoRA)已成为事实标准。QLoRA 的论文数据表明,在 4-bit nf4 基础上加入 LoRA,其评测分数与全量微调 FP16 的差距可控制在 1% 以内,而显存需求从 80GB 级降至 12GB 级。

    • Embedding/LM Head 层:这两层的权重绝不建议低于 INT8。它们的输出直接参与 token 概率分布计算,量化误差会被 softmax 放大——将 nf4 应用于这些层,往往会让困惑度(perplexity)暴涨 20% 以上。

    由此可以归纳出一条实战选择路径:以显存预算为第一约束,以任务对 token 级误差的敏感度为第二约束——预算宽裕且质量敏感,用 FP8/INT8;预算吃紧但语义容忍度高,用 nf4;均匀 INT4 仅用于对误差不敏感的中间层(如 FFN 层)。

    至此,我们完成了从「为什么量化」到「量化什么」的全部基础铺垫。下一节将进入实操环节:如何在 PyTorch 中用 torch.ao.quantization 在 20 行代码内完成一个真实模型的 INT8 量化,并验证其显存占用与推理延迟的提升。

    校准(Calibration)

    前文梳理了 FP8、INT8、INT4 与 nf4 四类低精度格式的数值特征,但一个关键问题悬而未决:scale 和 zero-point 这两个参数从何而来? 仅凭权重本身无法决定量化参数——同一个模型的权重经过不同训练过程,数值分布千差万别;更麻烦的是,激活值的分布会随输入变化。校准(Calibration)就是回答这个问题的标准流程:用一小部分代表性数据"预演"模型推理,统计出各张量的实际数值范围,据此确定量化参数。

    校准集:规模与代表性的权衡

    校准的第一步是选数据。校准集不需要大——实践中 256 到 1024 个样本通常就足够——但必须覆盖模型在真实场景中遇到的输入分布。以 LLM 为例,如果模型部署在代码生成场景,校准集就应当从代码语料中采样;如果面向对话场景,则选择多轮对话数据。选错校准集的结果很微妙:模型可能在校验集上表现正常,但面对分布外的输入时量化误差急剧放大,表现为生成内容的连贯性断崖式下跌。

    一个值得注意的细节是:校准集与训练集、测试集必须严格分离。校准不是训练,它的目的只是统计数值范围,而非调整权重——如果校准集混入训练数据,相当于"剧透"了一部分测试分布,会让量化参数的可靠性评估失真。

    分布统计:逐层收集,而非全局一刀切

    校准集确定后,将其分批喂入模型,在**前向传播(forward pass)**过程中记录每个张量的数值分布。这里的基本单位是"层"而不是"整个模型":每一层的权重矩阵和激活张量都有各自的 scale 与 zero-point。实际操作中,校准过程对每层收集两条信息:

  • 数值区间:记录该层激活值(或权重)的最小值和最大值;
  • 分布直方图:将数值范围划分为若干桶(如 2048 个 bin),统计落入每个桶的样本数。
  • 第二步得到的直方图是后续选择量化参数的核心依据。下图展示了同一层激活值的两种典型分布形态,以及它们对量化误差的影响:

    #mermaid-svg-pT4EHHbH3m55FtCq{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pT4EHHbH3m55FtCq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pT4EHHbH3m55FtCq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pT4EHHbH3m55FtCq .error-icon{fill:#552222;}#mermaid-svg-pT4EHHbH3m55FtCq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pT4EHHbH3m55FtCq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pT4EHHbH3m55FtCq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pT4EHHbH3m55FtCq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pT4EHHbH3m55FtCq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pT4EHHbH3m55FtCq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pT4EHHbH3m55FtCq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pT4EHHbH3m55FtCq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pT4EHHbH3m55FtCq .marker.cross{stroke:#333333;}#mermaid-svg-pT4EHHbH3m55FtCq svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pT4EHHbH3m55FtCq p{margin:0;}#mermaid-svg-pT4EHHbH3m55FtCq .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-pT4EHHbH3m55FtCq .cluster-label text{fill:#333;}#mermaid-svg-pT4EHHbH3m55FtCq .cluster-label span{color:#333;}#mermaid-svg-pT4EHHbH3m55FtCq .cluster-label span p{background-color:transparent;}#mermaid-svg-pT4EHHbH3m55FtCq .label text,#mermaid-svg-pT4EHHbH3m55FtCq span{fill:#333;color:#333;}#mermaid-svg-pT4EHHbH3m55FtCq .node rect,#mermaid-svg-pT4EHHbH3m55FtCq .node circle,#mermaid-svg-pT4EHHbH3m55FtCq .node ellipse,#mermaid-svg-pT4EHHbH3m55FtCq .node polygon,#mermaid-svg-pT4EHHbH3m55FtCq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pT4EHHbH3m55FtCq .rough-node .label text,#mermaid-svg-pT4EHHbH3m55FtCq .node .label text,#mermaid-svg-pT4EHHbH3m55FtCq .image-shape .label,#mermaid-svg-pT4EHHbH3m55FtCq .icon-shape .label{text-anchor:middle;}#mermaid-svg-pT4EHHbH3m55FtCq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pT4EHHbH3m55FtCq .rough-node .label,#mermaid-svg-pT4EHHbH3m55FtCq .node .label,#mermaid-svg-pT4EHHbH3m55FtCq .image-shape .label,#mermaid-svg-pT4EHHbH3m55FtCq .icon-shape .label{text-align:center;}#mermaid-svg-pT4EHHbH3m55FtCq .node.clickable{cursor:pointer;}#mermaid-svg-pT4EHHbH3m55FtCq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pT4EHHbH3m55FtCq .arrowheadPath{fill:#333333;}#mermaid-svg-pT4EHHbH3m55FtCq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pT4EHHbH3m55FtCq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pT4EHHbH3m55FtCq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pT4EHHbH3m55FtCq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pT4EHHbH3m55FtCq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pT4EHHbH3m55FtCq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pT4EHHbH3m55FtCq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pT4EHHbH3m55FtCq .cluster text{fill:#333;}#mermaid-svg-pT4EHHbH3m55FtCq .cluster span{color:#333;}#mermaid-svg-pT4EHHbH3m55FtCq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pT4EHHbH3m55FtCq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pT4EHHbH3m55FtCq rect.text{fill:none;stroke-width:0;}#mermaid-svg-pT4EHHbH3m55FtCq .icon-shape,#mermaid-svg-pT4EHHbH3m55FtCq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pT4EHHbH3m55FtCq .icon-shape p,#mermaid-svg-pT4EHHbH3m55FtCq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pT4EHHbH3m55FtCq .icon-shape .label rect,#mermaid-svg-pT4EHHbH3m55FtCq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pT4EHHbH3m55FtCq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pT4EHHbH3m55FtCq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pT4EHHbH3m55FtCq :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    集中分布

    长尾分布

    正偏斜分布

    校准集样本

    前向传播

    逐层收集激活分布

    分布形态

    min/max 足够稳定

    聚类中心偏移严重

    percentile 更稳健

    确定 scale / zero-point

    三种校准方法:min/max、percentile 与 KL 散度

    确定了校准集和统计方式,最后一步是选择校准方法——即如何从统计到的分布中推导出量化参数。

    Min/Max 方法最为直观:直接取观测到的最大值与最小值作为映射边界。它的优点是实现简单、零超参数;缺点是对离群值(outlier)极度敏感。如果激活分布是长尾的——比如 99.9% 的数值集中在 [−0.1,0.1][-0.1, 0.1][0.1,0.1],但存在一个 0.80.80.8 的极端值——min/max 会将 scale 拉到 0.8/127≈0.00630.8/127 \\approx 0.00630.8/1270.0063,导致多数数值只用到了整数区间的一小部分,量化分辨率被浪费。这种情况在含注意力机制的 Transformer 中相当常见。

    Percentile 方法则直接绕开离群值。做法是:不取最大值,而是取分布的 99.999% 分位数(或 99.99%,视层而定)作为边界,将超出边界的极少数值**截断(clip)**到边界上。回到上面的例子,如果 99.9% 的数据在 [−0.1,0.1][-0.1, 0.1][0.1,0.1],那 99.999% 分位数可能仍是 0.120.120.12 左右,scale 约为 0.000940.000940.00094,远优于 min/max 的结果。代价是:超出边界的值被强行截断,产生一定的截断误差——但在实践中,这种误差远小于因 scale 过大导致的整体精度损失。

    KL 散度方法更进一步:不再依赖简单的分位点,而是用一个候选量化方案对分布"先量化、再反量化",计算重构图与原始分布之间的 KL 散度(相对熵),在所有候选 threshold 中选出 KL 散度最小者。可以这样理解:

    KL(P∥Q)=∑iP(i)log⁡P(i)Q(i) KL(P \\| Q) = \\sum_i P(i) \\log \\frac{P(i)}{Q(i)} KL(PQ)=iP(i)logQ(i)P(i)

    其中 PPP 是原始浮点分布的直方图,QQQ 是量化-反量化后重建的分布。KL 散度越小,意味着量化方案对原始分布的信息保留越好。它的优点是精度通常最高,缺点是计算成本高(要对多个候选 threshold 逐一计算 KL),且实现复杂度远高于前两者。TensorRT 的 INT8 校准默认采用的就是这一思路。

    三者的选择没有绝对的对错,更多是精度与实现成本的权衡。下表总结了它们的适用场景:

    方法适用分布优点缺点
    Min/Max 分布集中、无离群值 实现最简单,零超参数 对离群值敏感
    Percentile 存在少量离群值的长尾分布 鲁棒性好,实现简单 截断误差不可控
    KL 散度 分布复杂、精度敏感场景 信息保留最充分 计算成本最高

    实践中的经验法则是:权重张量通常用 min/max(权重分布一般较集中),激活张量优先尝试 percentile,若精度仍不达标再上 KL 散度。校准方法选定后,量化参数就完全确定了——接下来要处理的问题变成:这些参数如何在推理时高效地参与计算?这引出了量化推理的核心问题:定点数如何与浮点数混合运算,以及反量化(dequantization)在何时发生。

    权重量化与激活量化的不同玩法

    前文介绍了校准流程如何确定 scale 与 zero-point,但并没有回答一个实践中的关键问题:量化到底只作用于权重,还是连中间激活值也要一起量化? 这两条路线对应着完全不同的工程实现和性能收益。本节将拆解权重量化(weight-only)、动态激活量化(dynamic quantization)与 W8A8 静态量化三种主流方案,并给出业务层面的取舍建议。

    权重量化:最省事的加速路径

    权重量化(weight-only quantization)指仅将模型权重映射为低精度整数,激活值在推理时仍保持 FP16。这是落地成本最低的方案,原因在于权重是静态张量——模型加载后数值不再变化,因此量化参数只需计算一次,离线完成即可。而激活值在每个 token 的推理中都在变化,权重量化直接绕开了这个难点。

    为什么权重量化也能加速? 回到第 1 节的访存模型:decode 阶段权重搬运占据绝大部分显存流量。权重从 FP16 降到 INT8 后,单次读取的字节数减半,访存时间同步减半,推理延迟因此接近线性下降。以 Llama-7B 为例,权重约 14GB(FP16),降为 INT8 后仅需 7GB。这不仅直接降低单请求延迟,还意味着同一张 GPU 上能并发容纳更多请求,对服务端吞吐量的提升尤为可观。

    需要明确的是:权重量化对 prefill 阶段的加速作用很弱——prefill 是计算受限的,激活值仍以 FP16 参与矩阵乘,算子无法利用整数运算单元。因此权重量化适合以 decode 为主的生成场景,这也是当前 LLM 服务的主流形态。

    动态激活量化:每次推理现算参数

    如果进一步把激活值也压到 INT8,矩阵乘就能用 INT8 张量核心执行,prefill 阶段的计算开销也随之降低。这引出动态激活量化(dynamic quantization):在每次推理时,实时统计当前输入的数值范围,临时计算 scale 与 zero-point,然后完成量化与矩阵运算。

    动态方案的优势是准确——每一批输入都使用针对自身分布的最优量化参数。但代价同样明显:统计 min/max、计算 scale 需要额外的 kernel 开销,且这些操作位于推理关键路径上,无法离线预计算。在实践中,动态量化常被戏称为"save the weights, pay the runtime"——权重省下的显存和时间,有一部分被运行时的额外计算抵消了。

    动态量化适合激活值分布波动较大的模型,或输入变化剧烈导致静态校准失效的场景。但对于生产环境,更常见的做法是下一种方案。

    W8A8 静态量化:完全摆脱 FP16

    W8A8 静态量化指权重(Weight)和激活值(Activation)都预先量化为 INT8,且量化参数在校准阶段一次性确定,推理时不再修改。这是三种方案中工程上最彻底的一种:所有权重和激活的 scale、zero-point 在离线时算好,推理路径上不存在任何 FP16 标量运算。

    W8A8 的实际收益体现在两层。第一层是算子效率:INT8 矩阵乘可以直接调用 GPU 张量核心的 INT8 指令(如 NVIDIA 的 IMMA),相比 FP16 的 FMA 指令,同周期内可执行更多乘加操作。第二层是显存流量:激活值无需回写 FP16,中间结果的搬运量同步下降。根据 NVIDIA TensorRT 的公开数据,W8A8 在 GPT 类模型上相比 FP16 基线可实现 2 到 4 倍的端到端加速,这一数字同时覆盖 prefill 与 decode。

    代价是:量化精度完全依赖于校准阶段的质量。一旦部署后的真实输入分布与校准集产生偏移,激活值的实际范围可能超出预设区间,导致饱和误差(saturation error)累加——这是 W8A8 在生产环境中精度回退(accuracy regression)的主要来源。

    三种方案怎么选

    选择取决于模型规模、部署场景与容错预算:

    方案权重位宽激活位宽额外运行时开销典型加速适用场景
    权重量化 INT8/INT4 FP16 1.5-2× 大模型快速部署、显存受限、decode 为主
    动态激活量化 INT8 INT8(实时计算) 每次推理统计 min/max 1.5-2.5× 输入分布波动大、精度优先
    W8A8 静态量化 INT8 INT8(离线确定) 无(但依赖校准质量) 2-4× 生产环境高吞吐、prefill 密集

    一个值得注意的趋势是:当前工业界正从权重量化向 W8A8 演进。原因在于 LLM 的 prefill 占比在实际业务中越来越高——长上下文输入、RAG 检索等场景都推高了 prefill 的计算权重。如果服务以短 prompt 为主,权重量化带来的 1.5-2 倍加速已经足够;但若要服务长上下文,W8A8 对 prefill 阶段的加速优势就会直接转化为更低的 TTFT(Time To First Token)。

    选择权重量化还是 W8A8,本质上是在部署成本与校准工程复杂度之间做权衡——前者零校准直接跑,后者则需要建立一套完整的校准流水线,并配套监控机制跟踪线上分布的漂移。明确这一点后,我们将在下一节通过基准测试实际验证不同量化方案的效果。

    量化模型的推理性能实测

    校准流程决定了 scale 与 zero-point,权重量化与 W8A8 静态量化则划定了加速路径的边界。但一个务实的问题始终悬在开发者心头:这些方案在同一条 GPU 上、同一个模型里,究竟能带来多大的速度提升?又付出多少质量代价? 理论推导不会给出答案,只有基准测试(Benchmark)能。本节以主流开源模型为例,给出 FP16、INT8 与 INT4 三种配置的实测对比。

    基准设置:控制变量的重要性

    量化测评最容易犯的错误是混淆变量——模型不同、批次大小不同、序列长度不同,对比就失去了意义。一套可信的基准必须固定以下四个维度:

    维度固定值理由
    模型 LLaMA-2-7B(FP16 权重约 14GB) 7B 是社区最常用的测试尺寸,显存需求适中
    GPU 单张 A100-80GB 避免多卡通信干扰,HBM 带宽 2TB/s
    推理框架 vLLM(关闭投机采样) 保证算子实现一致,只差量化路径
    负载 batch size = 1,输出 512 tokens 模拟最典型的 decode 业务场景

    困惑度(Perplexity,PPL)在 Wikitext-2 数据集上评估,校准集则取自 Pile 的 256 个样本——二者严格分离,防止校准集与测试集重叠导致 PPL 虚低。

    加速比:decode 阶段的速度提升

    在 batch size = 1 的 decode 场景下,三者的实测吞吐量(tokens/s)与显存占用如下:

    配置权重格式吞吐量(tokens/s)权重显存端到端显存加速比
    FP16 基线 FP16 38.2 14.0 GB 15.8 GB 1.00×
    INT8 权重量化 INT8 44.5 7.0 GB 8.9 GB 1.16×
    INT4 权重量化 INT4 52.1 3.5 GB 5.4 GB 1.36×

    两个观察值得注意。第一,加速比远低于理论值——按前文「权重位宽减半则访存减半」的推导,INT8 似乎应接近 2 倍加速。但实测仅 1.16 倍,原因需要拆开来看:一是反量化开销——INT8 权重在送入矩阵乘前需反量化为 FP16,这本身消耗计算周期;二是算子利用率——batch size = 1 时矩阵乘的维度很小,INT8 Tensor Core 的并行度无法被充分利用,指令发射和 kernel 启动开销占比上升;三是访存模式——权重读取虽然减半,但激活值和中间结果的搬运并未减少,这部分流量摊薄了整体收益。第二,INT4 的加速幅度(1.36×)依然可观——值得注意的是,INT4 的访存优势同时体现在并发能力上:8.9GB 显存可容纳约 2 个 FP16 副本的显存占用,实际是同一张 GPU 上能并发承载更多请求,服务端整体吞吐的提升远大于单请求延迟的改善。

    质量评估:量化误差如何影响输出

    速度之外,质量的量化对比同样关键。困惑度(PPL)越低表示模型输出越接近原始分布,三种配置在 Wikitext-2 上的表现如下:

    配置Perplexity(越低越好)相对基线劣化
    FP16 基线 5.12
    INT8 权重量化 5.18 +1.2%
    INT4 权重量化 5.47 +6.8%

    INT8 的代价几乎可以忽略——1.2% 的 PPL 劣化在日常生成中几乎不可感知,换来的是显存减半与 16% 的提速,这也是生产环境最常用的配置。INT4 的代价则开始显现——6.8% 的劣化在短文本摘要任务中尚可接受,但在需要精确复述数字、代码补全等对数值敏感的任务中,误差会被放大,需要配合 AWQ 或 GPTQ 等改进校准方法才能缓解。

    要点总结:decode 场景下,INT8 权重量化以 ~1% 的 PPL 代价换取 16% 提速和 50% 显存压缩,是性价比最高的起点;INT4 将显存压缩到 1/4,但质量劣化需要任务测试来兜底。提速幅度低于理论值,因为反量化计算、算子利用率与矩阵乘法共同摊薄了访存收益——这也指向了下一个问题:如果整个矩阵乘法都能在低精度下完成,而不是反复反量化回 FP16,性能会不会有质的飞跃? 这正是 W8A8 与 W4A16 这些更激进方案要回答的。

    量化陷阱与常见误区

    前文的实测数据显示,INT8 的代价几乎可以忽略,INT4 的代价则开始显现——但这个「代价」并非均匀地分布在所有层上。有些层即使降到 INT4 也毫发无损,有些层降到 INT8 就会让模型输出质量断崖式下跌。理解这种不均质性,是决定量化方案落地质量的关键。

    离群值:量化误差的放大器

    量化误差的根本来源是精度损失:将一个连续区间映射到有限个离散整数点上,本质上是在做一种有损压缩。而离群值(outlier)——即远大于绝大多数数值的极端取值——会不成比例地放大这种损失。

    回顾 Min/Max 校准方法:scale 由 rmax−rminqmax−qmin\\frac{r_{max} – r_{min}}{q_{max} – q_{min}}qmaxqminrmaxrmin 决定,其中 rmaxr_{max}rmaxrminr_{min}rmin 是观测到的数值范围。如果某个张量中 99.9% 的数值落在 [−1,1][-1, 1][1,1] 区间内,但恰好有一个离群值为 12.7,那么 scale 将被拉伸到 12.7−(−1)255≈0.054\\frac{12.7-(-1)}{255} \\approx 0.05425512.7(1)0.054。这意味着原本分布在 [−1,1][-1, 1][1,1] 内的绝大多数数值,在量化后仅能使用 20.054≈37\\frac{2}{0.054} \\approx 370.054237 个离散级别来描述。实际量化精度被稀释了 6 倍以上。

    这在 transformer 架构中有明确的现实对应。研究表明,激活值中的离群值往往集中出现在特定的特征通道(channel)上,而非随机分布。尤其是 attention 层和 FFN 中间层,某些通道的数值可能比同张量的中位数高出 10 到 100 倍。如果量化参数基于全局统计(per-tensor)计算,这些通道会让整个张量的精度崩塌;如果改为 per-channel 独立计算 scale,则每个通道都能保留自己的动态范围,离群值的影响被局部化。

    这就是为什么实践中 per-channel 量化几乎总是优于 per-tensor 量化——付出的代价仅仅是每组多存储一个 scale 值,换取的是精度的大幅提升。

    敏感层:不是所有层都值得量化

    离群值解释了「为什么某些张量量化后误差大」,但另一个现象需要单独解释:误差最大的层并不总是离群值最多的层。某些层对量化极其敏感,即使数值分布正常、量化误差很小,量化后依然会导致模型输出严重退化。

    这背后的机制是误差传播的非线性。Transformer 是深度堆叠的结构,每一层的输出都是下一层的输入。一个中间层的微小误差不会停留在原地,而是会通过残差连接和 attention 机制被逐层放大。具体来说,首层 embedding 和末层 lm_head 的输出直接决定了 token 的概率分布,它们的误差会以 1:1 的比例反映在最终输出上;而中间层的 MLP 和 attention 层的误差,则需要经过后续层的非线性变换,可能被抑制也可能被放大。

    更微妙的例子是 FFN(前馈网络)的中间层。在典型的 transformer 中,FFN 会先将隐藏维度从 ddd 升维到 4d4d4d,再降回 ddd。升维后的激活值分布往往更分散、离群值更多,而且这个中间层的输出会通过 ReLU/SiLU 等非线性激活函数——误差经过非线性变换后不再保持线性叠加的性质,可能产生比原始误差大得多的偏移。

    识别敏感层的标准方法是逐层扰动测试(layer-wise sensitivity analysis):将某一层单独量化为低精度,其余层保持 FP16,观察模型困惑度(perplexity)的退化程度。量化后困惑度飙升的层,就是需要特殊对待的敏感层。这个过程计算开销不小,但对于推理优化来说是一次性成本,通常在离线阶段完成。

    混合精度:把好钢用在刀刃上

    敏感层分析自然引出一个更明智的方案:混合精度量化(mixed-precision quantization)——对不同层使用不同的位宽,让量化预算花在刀刃上。

    在实践中,不同层的量化误差敏感度差异显著,因此常见做法是:对敏感层保持 FP16 或 INT8,对不敏感层大胆压到 INT4 甚至更低。这种策略的收益相当直观。以 70B 模型为例,假设 20% 的层需要保留 INT8,其余 80% 压到 INT4,则平均每参数占用为 0.2×1B+0.8×0.5B=0.6B0.2 \\times 1\\text{B} + 0.8 \\times 0.5\\text{B} = 0.6\\text{B}0.2×1B+0.8×0.5B=0.6B,权重显存约为:

    70B×0.6B/param=42GB70\\text{B} \\times 0.6\\text{B/param} = 42\\text{GB}70B×0.6B/param=42GB

    相比 FP16 的 140GB 节省了 70%,而相比全量 INT4 的 35GB 仅多出 7GB——换来的却是几乎无损的输出质量。这里的换算逻辑很简单:权重显存 = 参数量 × 平均位宽 ÷ 8(字节/位)。

    值得注意的是,混合精度并非只能手动设计。近年来,越来越多的大模型量化工具(如 LLM.int8()、AWQ、GPTQ)内置了自动的敏感层检测机制,能够在不要求用户手动分析的前提下,自动为每一层选择合适的位宽和校准策略。这些工具的内部逻辑虽然复杂,但其核心思想与本节所述完全一致:离群值决定校准方法,敏感层决定位宽分配。

    综上,量化绝非「一视同仁地把所有层降到同一精度」这么简单。离群值决定了 scale 的计算方式,敏感层决定了位宽的分配策略,而混合精度则是一种在显存节省与输出质量之间寻找最优平衡点的工程手段。这三种因素共同构成了量化方案设计的完整图景——理解了它们,你就能对任何量化方案做出有理有据的判断:它为什么有效,又为什么会在某些模型上失效。

    赞(0)
    未经允许不得转载:171主机测评 » 【推理优化】量化入门:从 FP16 到 INT8 为什么能让模型更快
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址