量化:用更少的比特做更多的事

LLM的参数量从7B到671B不断增长,但GPU显存的增长速度远跟不上。量化是缓解这一矛盾最直接的技术——用更少的比特表示模型参数和激活值,在不修改模型架构的前提下减少显存占用和加速推理。

2026年,量化技术已经形成了从INT4到FP8的完整谱系,每种精度都有其适用场景和工程权衡。

量化的基本原理

对称量化

将浮点数映射到整数的固定范围。以INT8为例:

x_int8 = round(x_fp / scale)
scale = max(|x_fp|) / 127

反量化:x_fp ≈ x_int8 × scale

非对称量化

引入零点偏移,处理分布不对称的情况:

x_int8 = round((x_fp - zero_point) / scale)
scale = (max(x) - min(x)) / 255
zero_point = min(x)

量化粒度

  • 逐张量(per-tensor):整个张量共用一个scale,最简单但精度损失大
  • 逐通道(per-channel):每个输出通道一个scale,精度好但额外参数多
  • 逐组(per-group):将通道分组,组内共用scale,是精度和效率的折中
def per_group_quantize(x, group_size=128):
    """分组量化"""
    original_shape = x.shape
    # 重排为 [out_features // group_size, group_size, in_features]
    x_reshaped = x.view(-1, group_size, original_shape[-1])
    
    # 每组计算独立的scale
    scales = x_reshaped.abs().max(dim=1, keepdim=True).values / 127
    
    # 量化
    x_int8 = (x_reshaped / scales).round().clamp(-128, 127).to(torch.int8)
    
    return x_int8, scales

def per_group_dequantize(x_int8, scales, original_shape):
    """分组反量化"""
    x_fp = x_int8.float() * scales
    return x_reshaped.view(original_shape)

INT8量化

适用场景

INT8是最成熟的量化方案,几乎所有推理框架都原生支持。适用于:

  • 需要良好精度-速度平衡的场景
  • CPU和GPU部署
  • 对兼容性要求高的生产环境

精度表现

INT8量化在大部分LLM上的精度损失控制在1-3%以内。对于7B以上的模型,INT8量化几乎是"免费午餐"——精度损失可以忽略,但显存和速度都有显著改善。

W8A8(权重+激活均INT8)

将权重和激活都量化到INT8,可以获得最大的推理加速。但激活量化比权重量化更具挑战性——激活值包含异常值(outlier),这些异常值会压缩正常值的量化精度。

LLM.int8()

BitsAndBytes提出的方案解决了激活异常值问题。它将激活分解为"正常值"和"异常值"两部分:

  • 正常值(>99.5%的元素)使用INT8量化
  • 异常值(<0.5%的元素)保持FP16精度
def llm_int8_matmul(A, B, threshold=6.0):
    """LLM.int8() 矩阵乘法"""
    # 识别异常值
    outlier_mask = A.abs() > threshold
    normal_mask = ~outlier_mask
    
    # 正常部分:INT8量化计算
    A_normal = A[normal_mask]
    B_quantized, scales = quantize_int8(B)
    result_normal = dequantize(A_normal_int8 @ B_quantized, scales)
    
    # 异常部分:FP16精度计算
    A_outlier = A[outlier_mask]
    result_outlier = A_outlier @ B
    
    # 合并结果
    result = torch.zeros_like(A @ B)
    result[normal_mask] = result_normal
    result[outlier_mask] = result_outlier
    
    return result

SmoothQuant

SmoothQuant通过数学等价变换将激活的异常值"迁移"到权重上,使得激活的分布更均匀,便于量化。其核心公式:

Y = (X · diag(s)^{-1}) · (diag(s) · W)

选择 s_j = max(|X_j|)^α / max(|W_j|)^{1-α},通过调整α控制异常值迁移的程度。

INT4量化

适用场景

INT4将模型大小压缩到1/4,是当前显存优化的主流方案。适用于:

  • 显存极度受限的部署(如单卡部署70B模型)
  • 对推理速度要求不极致的场景
  • 对精度损失有一定容忍度的应用

精度挑战

INT4的精度损失比INT8显著——通常在3-8%之间。关键挑战在于4比特只能表示16个离散值,对于分布复杂的权重来说过于粗糙。

GPTQ

GPTQ是一种后训练量化方法,通过逐层最小化量化误差来恢复精度:

def gptq_quantize_layer(layer, calibration_data):
    """GPTQ单层量化"""
    W = layer.weight.data  # [out, in]
    
    # 用校准数据计算Hessian矩阵
    H = compute_hessian(layer, calibration_data)  # [in, in]
    H += torch.eye(H.shape[0]) * 0.01  # 正则化
    
    # 逐列量化
    for i in range(W.shape[1]):
        # 量化第i列
        w = W[:, i]
        w_q = quantize(w)
        
        # 计算量化误差
        err = (w - w_q) / H[i, i]
        
        # 将误差分摊到未量化的列
        W[:, i+1:] -= err.unsqueeze(1) @ H[i, i+1:].unsqueeze(0)
        
        W[:, i] = w_q
    
    layer.weight.data = W

GPTQ的核心洞察是:量化一个权重后,其误差可以通过调整未量化的权重来补偿。这种"逐列量化+误差补偿"策略显著减少了累积量化误差。

AWQ

AWQ(Activation-aware Weight Quantization)发现:不是所有权重都同等重要——与大幅激活相对应的权重更为关键。AWQ通过激活分布感知的缩放来保护这些"重要权重":

def awq_quantize(W, X_calib, ratio=0.1):
    """AWQ量化"""
    # 找出重要权重通道(激活值大的通道)
    activation_magnitude = X_calib.abs().mean(dim=0)
    important_channels = activation_magnitude.topk(
        int(len(activation_magnitude) * ratio)
    ).indices
    
    # 对重要通道施加缩放
    scale = torch.ones(W.shape[1])
    scale[important_channels] = activation_magnitude[important_channels] / \
                                  activation_magnitude.mean()
    
    W_scaled = W * scale
    
    # 量化
    W_quantized = quantize_int4(W_scaled)
    
    # 反量化时除以scale
    W_dequantized = dequantize(W_quantized) / scale
    
    return W_dequantized

GGUF格式

llama.cpp使用的GGUF格式支持INT4量化,在CPU推理场景下非常流行。GGUF的INT4采用了一种特殊的"块量化"策略——每32个权重共享一个scale,实现了紧凑存储和高效反量化。

FP8量化

浮点格式的优势

与整数量化不同,FP8保持浮点数格式,有独立的指数和尾数位。这使得FP8天然支持更大的动态范围,不需要复杂的缩放策略。

FP8有两种格式:

  • E4M3:4位指数,3位尾数。精度高,动态范围小。适合前向传播
  • E5M2:5位指数,2位尾数。精度低,动态范围大。适合后向传播(梯度)

H100的原生支持

NVIDIA H100 GPU原生支持FP8张量核心计算,使得FP8量化不仅节省显存,还能真正加速计算。这是FP8相比INT8/INT4的重要优势——后者的反量化可能成为瓶颈。

量化感知训练(QAT)与FP8

FP8量化通常与量化感知训练配合使用:

class FP8QuantizationAwareTraining:
    def __init__(self, model, calibration_data):
        self.model = model
        self.calibration_data = calibration_data
    
    def quantize_weights(self):
        """将权重量化到FP8(STE直通估计器)"""
        for param in self.model.parameters():
            param_fp8 = param.data.to(torch.float8_e4m3fn)
            # STE:前向用FP8,反向用FP16梯度
            param.data = param_fp8.to(param.dtype)

FP8 vs INT8 对比

特性INT8FP8
精度中高
动态范围需scale调整天然支持
硬件加速通用H100+原生
实现复杂度
生态成熟度中(2026年快速成长)

混合精度策略

实践中,最优方案通常是对模型不同部分使用不同精度:

def apply_mixed_precision(model):
    """混合精度量化策略"""
    for name, module in model.named_modules():
        if 'embed' in name:
            # 嵌入层:保持FP16(对量化敏感)
            module.weight = module.weight.half()
        elif 'lm_head' in name:
            # 输出头:保持FP16
            module.weight = module.weight.half()
        elif 'attention' in name:
            # 注意力层:INT8(精度要求中等)
            quantize_module_int8(module)
        elif 'ffn' in name or 'mlp' in name:
            # FFN层:INT4(占参数主体,压缩收益最大)
            quantize_module_int4(module)
        else:
            # 其他层:INT8
            quantize_module_int8(module)

性能基准对比

以Llama-3-70B为例(单卡A100 80GB):

量化方案显存占用推理速度困惑度变化适用场景
FP16(基线)140GB1.0x0%多卡部署
INT870GB1.5x+1.2%单卡A100
INT4 (GPTQ)35GB2.0x+4.5%单卡消费级GPU
INT4 (AWQ)35GB2.1x+3.2%单卡消费级GPU
FP8 (H100)70GB2.5x+0.8%H100部署

结语

量化技术在2026年已经高度成熟。INT8是通用部署的默认选择,INT4适合显存受限场景,FP8在H100+硬件上提供了精度和速度的最佳平衡。混合精度策略进一步细化了不同层的量化方案。选择量化方案时,需要综合考虑硬件支持、精度要求和部署环境的约束。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。