27届大模型面试准备(十九):模型量化全攻略——INT8/INT4、GPTQ、AWQ、SmoothQuant 与 KV Cache 量化
27届大模型面试准备十九模型量化全攻略——INT8/INT4、GPTQ、AWQ、SmoothQuant 与 KV Cache 量化上一篇《LoRA 与 PEFT 全攻略》讲的是怎么用最少的可训练参数把模型调好属于训练侧省显存。这一篇转到部署侧的省显存主线——量化Quantization。它和第十五篇《高效推理全攻略》是一对那篇讲的是调度和算子怎么快这篇讲的是数字表示怎么小。量化几乎是所有推理面试的必问题因为它同时牵扯数值分析、硬件指令集和工程权衡三层。本文按为什么要量化 → 量化的数学 → PTQ 三大流派GPTQ / AWQ / SmoothQuant→ QAT → KV Cache 量化 → 落地选型与踩坑的顺序展开每节都给原理、公式骨架、代码片段结尾给面试速答和高频追问清单。一、先算一笔账为什么必须量化面试官问为什么要量化只回答为了省显存会显得很浅。真正的答案有三层而且要能当场算出来。1.1 显存账一个 70B 参数的模型权重按不同精度存储所需显存精度每参数字节70B 权重显存能否单卡 A100-80GFP324280 GB不能FP16 / BF162140 GB不能INT8170 GB勉强无余量给 KV CacheINT40.535 GB可以还剩 45G 给 KV Cache这张表要能脱口而出。结论INT4 是70B 单卡可跑的分水岭这就是 INT4 量化在社区如此流行的直接原因。1.2 带宽账更关键但常被忽略LLM 的 decode 阶段是访存密集memory-bound而非计算密集。每生成一个 token都要把全部权重从 HBM 读进片上算力其实是闲着的。Roofline 视角decode 阶段单 token 的算术强度 计算量 ≈ 2 × N_params FLOPs (每个参数一次乘一次加) 访存量 ≈ N_params × bytes_per_param 算术强度 2 / bytes_per_param FP16: 2/2 1 FLOP/Byte INT8: 2/1 2 FLOP/Byte INT4: 2/0.5 4 FLOP/Byte A100 的 ridge point ≈ 312 TFLOPS / 2 TB/s ≈ 156 FLOP/Byte → 1、2、4 都远低于 156说明 decode 彻底卡在带宽上所以有个重要推论decode 阶段的速度近似与权重字节数成反比。FP16 换 INT4权重体积减 4 倍理论 decode 吞吐能提升接近 4 倍——这个提升不是来自算得快而是来自读得少。这是面试的高分回答点。1.3 成本账显存小了同一张卡就能塞更大的 batch 和更长的 KV Cache单位 token 成本随之下降。量化本质上是在精度损失和服务成本之间做交易。量化带来的三重收益 ┌─────────────┬─────────────┬──────────────┐ │ 显存下降 │ 带宽下降 │ batch 变大 │ │ 4x (INT4) │ 4x │ 吞吐再上升 │ └──────┬──────┴──────┬──────┴──────┬───────┘ └─────────────┴─────────────┘ 单 token 成本 ↓↓ 代价精度损失可控二、量化的数学骨架2.1 均匀仿射量化主流量化都是均匀量化uniform quantization核心两个参数缩放因子 scales和零点 zero-pointz。量化: q clamp( round(x / s) z, q_min, q_max ) 反量化: x̂ s × (q - z)其中对 INT8 对称量化z 0q ∈ [-128, 127]INT8 非对称量化z ≠ 0q ∈ [0, 255]。scale 的取法min-max 校准对称: s max(|x|) / (2^(b-1) - 1) # b8 → 127 非对称: s (max(x) - min(x)) / (2^b - 1) z round(-min(x) / s)一段可直接跑的实现import torch def quantize_tensor(x: torch.Tensor, num_bits: int 8, symmetric: bool True): 返回 (q, scale, zero_point)。symmetricTrue 时 zero_point 恒为 0。 qmax 2 ** (num_bits - 1) - 1 if symmetric else 2 ** num_bits - 1 qmin -(2 ** (num_bits - 1)) if symmetric else 0 if symmetric: scale x.abs().max() / qmax zp torch.tensor(0, dtypetorch.int32) else: scale (x.max() - x.min()) / (qmax - qmin) zp torch.round(qmin - x.min() / scale).to(torch.int32) scale torch.clamp(scale, min1e-8) q torch.clamp(torch.round(x / scale) zp, qmin, qmax) return q.to(torch.int8), scale, zp def dequantize_tensor(q, scale, zp): return (q.to(torch.float32) - zp) * scale2.2 量化粒度per-tensor / per-channel / per-group这是面试高频追问点。粒度越细误差越小但元数据scale开销越大。权重矩阵 W [out_features, in_features] per-tensor : 整个 W 共用 1 个 scale —— 最省误差最大 per-channel : 每个 out_channel 一个 scale —— 常用于 INT8 权重 per-group(g128): 每 128 个输入维度一组 scale —— INT4 标配GPTQ/AWQ 默认 in_features 4096, group_size 128 → 每个输出通道有 4096/128 32 个 scale → 额外开销: 32 × 2 bytes(FP16) / (128 × 0.5 bytes) 每权重多 ~1 bit → 所以说INT4 group-128的等效位宽约 4.25 bit结论要背下来INT8 用 per-channel 就够INT4 必须用 per-group通常 g128否则精度崩。2.3 W8A8 / W4A16 命名法W指权重weightA指激活activation。方案含义典型代表适用场景W8A8权重和激活都 INT8SmoothQuant、LLM.int8()追求吞吐prefill 计算受益大W4A16权重 INT4激活仍 FP16GPTQ、AWQ追求显存和 decode 速度W4A8权重 INT4激活 INT8QoQ、部分定制方案极致压榨工程复杂W8A16权重 INT8激活 FP16bitsandbytes LLM.int8()保精度实现简单FP8 (E4M3/E5M2)浮点 8 位H100 原生支持精度最好的 8 位方案关键判断W4A16 只解决带宽问题不解决算力问题——因为算之前要把 INT4 反量化回 FP16 再做 GEMM。所以 W4A16 对 decode带宽瓶颈提升巨大对 prefill算力瓶颈几乎没提升甚至因为反量化开销略慢。这个反直觉的点是面试区分度极高的答案。三、量化难在哪激活值的离群点如果量化只是简单地缩放取整就不会有这么多论文。真正的难点是LLM 激活值中存在系统性离群点outlier。某层 hidden_states 的分布示意4096 维 绝大多数通道: |x| ∈ [0, 3] 少数几个通道: |x| ∈ [50, 100] ← outlier channel固定出现在特定维度 若 per-tensor 量化: scale 100 / 127 ≈ 0.787 → 值为 0.5 的正常激活量化后 round(0.5/0.787) 1 → 反量化回来 0.787误差 57%大量小信号被压成 0 或 1这就是 LLM.int8() 论文的核心发现当模型参数超过约 6.7B 时会稳定出现幅值极大的离群特征维度且这些维度对模型效果至关重要直接裁掉会导致性能崩溃。三种应对思路正好对应后面三个流派离群点问题 │ ├── 思路A把离群点单独拎出来算 → LLM.int8() 混合精度分解 ├── 思路B把难度从激活转移到权重 → SmoothQuant 数学等价缩放 └── 思路C干脆不量化激活只量化权重 但按激活重要性保护关键权重 → AWQ / GPTQ四、PTQ 三大流派详解PTQPost-Training Quantization训练后量化不需要重训是工业界绝对主流。4.1 LLM.int8()混合精度分解最朴素也最好懂的方案。做法把激活中超过阈值通常 6.0的离群通道挑出来用 FP16 算其余通道走 INT8。X [batch, 4096] × W [4096, 11008] 1) 找出 X 中 |x| 6.0 的列索引集合 O通常 |O| 10 2) 拆分: X_out X[:, O] (FP16) W_out W[O, :] (FP16) X_int X[:, ~O] (INT8) W_int W[~O, :] (INT8) 3) Y X_out W_out dequant(X_int W_int)优点几乎零精度损失实现简单bitsandbytes 一行load_in_8bitTrue。缺点慢。因为要动态检测离群点、拆分张量、跑两次 GEMM 再相加实际吞吐往往比 FP16 还低。所以它适合显存不够但不追求速度的场景比如本地跑大模型做实验不适合线上服务。这个能省显存但更慢的坑必须记住。4.2 SmoothQuant把量化难度从激活转移到权重核心洞察非常优雅激活难量化有离群点权重好量化分布平坦。那能不能在数学上等价地把一部分难度从激活挪给权重原式: Y X · W 引入对角缩放矩阵 diag(s): Y (X · diag(s)^-1) · (diag(s) · W) └──── X̂ ────┘ └──── Ŵ ────┘ X̂ 的离群点被 s 除小了 → 好量化了 Ŵ 被 s 乘大了一点 → 稍微难量化但权重本来余量足缩放因子的取法引入超参 αmigration strength通常 0.5s_j max(|X_j|)^α / max(|W_j|)^(1-α) α 0 → 全部难度留在激活等于不做 α 1 → 全部难度推给权重权重会崩 α 0.5 → 两边均分实践最优关键点diag(s)^-1 可以被融合进上一层的 LayerNorm 或前一个线性层的权重里推理时零额外开销。这是它比 LLM.int8() 快的根本原因。torch.no_grad() def smooth_ln_linear(ln, linears, act_scales, alpha0.5): 把 LayerNorm 后面的若干 Linear 一起做 SmoothQuant 平滑。 act_scales: 校准得到的每通道激活最大绝对值 [hidden] device, dtype linears[0].weight.device, linears[0].weight.dtype act_scales act_scales.to(devicedevice, dtypedtype) # 权重侧每通道最大值对所有共享输入的 linear 取并集 w_scales torch.cat( [fc.weight.abs().max(dim0, keepdimTrue)[0] for fc in linears], dim0 ).max(dim0)[0].clamp(min1e-5) s (act_scales.pow(alpha) / w_scales.pow(1 - alpha)).clamp(min1e-5) # 难度迁移LN 除以 s等价于激活除以 sLinear 权重乘以 s ln.weight.div_(s) if getattr(ln, bias, None) is not None: ln.bias.div_(s) for fc in linears: fc.weight.mul_(s.view(1, -1))SmoothQuant 是W8A8方案因此它对 prefill 阶段计算密集有真实的算力加速——INT8 Tensor Core 吞吐是 FP16 的 2 倍。这一点和 W4A16 形成鲜明对比。4.3 GPTQ基于二阶信息的逐层误差补偿GPTQ 走的是另一条路只量化权重W4A16但量化时不是简单取整而是逐列量化并把误差补偿到还没量化的列上。它的理论基础是 OBQOptimal Brain Quantization目标是最小化每层输出的重建误差目标: argmin_Ŵ || X·W - X·Ŵ ||²_F 用二阶泰勒展开海森矩阵 H 2·X^T·X 量化第 q 列后对剩余列的最优补偿量: δ_rest - ( w_q - quant(w_q) ) / [H^-1]_qq × [H^-1]_{q, rest}直观理解这一列我取整取多了那就让后面还没量化的列少一点把整体输出拉回来。这是一种贪心的误差重分配。算法骨架# GPTQ 单层量化的核心循环简化版省略 Cholesky 与分块细节 def gptq_quantize_layer(W, H_inv, group_size128, bits4): W: [out, in]H_inv: [in, in] 海森逆的 Cholesky 上三角 Q torch.zeros_like(W) for i in range(W.shape[1]): # 按输入维度逐列 w W[:, i].clone() d H_inv[i, i] if i % group_size 0: # 每组重新算 scale scale, zp find_params(W[:, i:i group_size], bits) q quantize_column(w, scale, zp, bits) # 取整 Q[:, i] q err (w - dequant(q, scale, zp)) / d # 归一化误差 # 关键把误差按海森逆的相关性补偿给后面所有未量化列 W[:, i 1:] - err.unsqueeze(1) H_inv[i, i 1:].unsqueeze(0) return Q工程要点- 需要少量校准数据通常 128 条、每条 2048 token来估计H X^T X。- 海森矩阵可能奇异实践中要加阻尼H λ·mean(diag(H))·Iλ 取 0.01。- 采用分块 惰性更新block size 128避免逐列更新的巨大访存开销这是 GPTQ 相对 OBQ 能跑得动 175B 的工程关键。- 单卡量化 70B 大约 1~4 小时。4.4 AWQ按激活幅值保护关键权重AWQActivation-aware Weight Quantization的核心观察比 GPTQ 更简洁并非所有权重同等重要只有约 1% 的显著权重决定了模型效果而判断显著性的依据不是权重自身大小而是对应的激活幅值大小。论文里的消融实验很有说服力保护策略INT3 下的 PPL不保护全量化显著劣化按权重幅值挑 1% 保 FP16几乎无改善按激活幅值挑 1% 保 FP16大幅改善接近 FP16但混合精度保留 1% FP16对硬件不友好不规则内存访问。AWQ 的巧思是用逐通道缩放来等价实现保护而不真的保留 FP16。对显著通道 j 的权重先放大 s 倍量化后再在激活侧除回来 Ŵ_j quant(W_j · s_j) Y (Ŵ_j / s_j) · X_j 放大后W_j 在量化格点上占的相对误差变小 → 等效精度更高 s 的搜索s (mean|X_j|)^α网格搜 α ∈ [0,1] 取重建误差最小GPTQ vs AWQ 对比面试必问维度GPTQAWQ核心思想二阶误差补偿激活感知的通道缩放是否改权重数值是补偿会改后续列是等价缩放校准数据依赖较强可能过拟合校准集较弱泛化更好量化耗时慢小时级快分钟级推理速度相当略快kernel 更规整精度相当个别任务 AWQ 更稳相当长尾/多语言场景更稳一句话总结选型追求快速量化和跨域泛化选 AWQ已有成熟 GPTQ 流水线且校准数据贴合业务分布GPTQ 也够用。目前社区vLLM、SGLang对两者支持都很完善。五、QAT量化感知训练PTQ 在 INT4 以下INT3/INT2会明显掉点这时需要 QATQuantization-Aware Training——训练时就模拟量化误差。核心是伪量化节点fake quant 直通估计器STE前向: x̂ dequant(quant(x)) # 引入真实的取整误差 反向: ∂L/∂x ∂L/∂x̂ · 1{qmin ≤ x/s ≤ qmax} └──── STEround 的梯度直接当作 1 ────┘class FakeQuant(torch.autograd.Function): staticmethod def forward(ctx, x, scale, qmin, qmax): ctx.save_for_backward(x, scale) ctx.qmin, ctx.qmax qmin, qmax q torch.clamp(torch.round(x / scale), qmin, qmax) return q * scale staticmethod def backward(ctx, g): x, scale ctx.saved_tensors # STE 截断区间外梯度置零 mask ((x / scale) ctx.qmin) ((x / scale) ctx.qmax) return g * mask, None, None, None现实中大模型很少做全量 QAT太贵主流是QLoRA 式的量化基座 LoRA 微调——基座冻结在 NF4只训 LoRA。这在第十八篇已详细讲过两篇可以串起来回答QLoRA 本质是 PTQ 之后用 PEFT 把损失补回来比真 QAT 便宜两个数量级。六、KV Cache 量化长上下文场景的胜负手权重量化解决的是模型多大但长上下文场景下KV Cache 才是显存大头。KV Cache 显存 2 × L × H_kv × d_head × S × B × bytes 以 Llama-3-70B (L80, H_kv8 GQA, d_head128) 为例 batch32, seq32768, FP16: 2 × 80 × 8 × 128 × 32768 × 32 × 2 B ≈ 343 GB ← 比 140G 权重还大 改 INT8: 171 GB 改 INT4: 86 GB所以在长上下文 大 batch 场景KV Cache 量化的收益比权重量化更大。实践要点1.K 和 V 要区别对待。K 有明显的通道级离群点因为要和 Q 做点积、且叠加了 RoPEV 的分布更平坦。因此常见做法是K 用 per-channel 量化V 用 per-token 量化。2.保留最近若干 token 为 FP16residual window如最近 128 个因为新 token 对注意力贡献最敏感。3. INT8 KV Cache 基本无损可放心上INT4 KV Cache 在长文本检索类任务大海捞针上会掉点要实测。4. vLLM 中开启方式--kv-cache-dtype fp8H100 上 FP8 KV 比 INT8 精度更好且有原生支持。# per-token 量化 V每个 token 一个 scale比 per-tensor 稳很多 def quant_kv_per_token(v: torch.Tensor, bits8): v: [B, H, S, D] → 沿最后一维求 scale每个 (b,h,s) 一个 scale qmax 2 ** (bits - 1) - 1 scale v.abs().amax(dim-1, keepdimTrue) / qmax # [B,H,S,1] scale scale.clamp(min1e-8) q torch.clamp(torch.round(v / scale), -qmax - 1, qmax).to(torch.int8) return q, scale七、落地选型与真实踩坑7.1 决策树要不要量化 │ 显存够 延迟达标 ──否── 直接 FP16/BF16别自找麻烦 │是显存/成本有压力 ▼ 主要瓶颈是 decode 还是 prefill │ │ decode(生成长) prefill(输入长/高并发) │ │ W4A16 (AWQ/GPTQ) W8A8 (SmoothQuant) 或 FP8 │ │ └──── 长上下文────────┘ │是 叠加 KV Cache 量化 (FP8/INT8) │ 精度不达标 │是 放宽到 INT8 权重或 QLoRA 补偿微调7.2 五个真实踩坑坑一只看 PPL 不看下游任务。PPL 掉 0.1 看起来无所谓但代码生成、数学推理、工具调用这类任务对量化极度敏感可能掉 5~10 个点。评测必须覆盖真实业务任务尤其是 JSON 结构化输出的合法率——量化后模型漏引号、括号不配对的概率会上升这在 Agent 场景是致命的。坑二校准集选错。GPTQ 用 wikitext 校准、却上线跑中文客服分布不匹配会明显掉点。校准集应该从线上真实流量采样 128~512 条这是成本极低、收益极大的一步。坑三以为 INT4 一定比 INT8 快。前面算过W4A16 在 prefill 阶段因为要反量化可能比 W8A8 更慢。高并发短输出场景W8A8 往往才是对的。坑四忽略 lm_head 和 embedding。词表 15 万、hidden 8192 的 lm_head 就有 12 亿参数占比不小但它对量化敏感直接决定输出分布。通用做法是 lm_head 保持 FP16 不量化其余全量化。坑五group_size 和硬件对不齐。group_size 必须能被 kernel 的 tile 尺寸整除随手改成 64 或 100 可能导致 kernel fallback 到极慢的通用实现。没有充分理由就用默认的 128。7.3 一段可直接用的 vLLM 部署示例# AWQ INT4 权重 FP8 KV Cache长上下文高吞吐配置 python -m vllm.entrypoints.openai.api_server \ --model /models/Llama-3-70B-Instruct-AWQ \ --quantization awq_marlin \ --kv-cache-dtype fp8 \ --max-model-len 32768 \ --gpu-memory-utilization 0.92 \ --tensor-parallel-size 2注意awq_marlin而不是awqMarlin 是专门优化的 W4A16 kernel在 Ampere/Hopper 上比原始 AWQ kernel 快约 1.5~2 倍能用就一定要用。八、面试速答Q量化为什么能加速推理ALLM 的 decode 阶段是访存密集型每生成一个 token 都要把全部权重从 HBM 读一遍算力是闲置的。量化把权重字节数减少 N 倍访存量就减少 N 倍decode 吞吐近似提升 N 倍。所以加速主要来自读得少而非算得快。但 prefill 是计算密集型W4A16 因为需要反量化反而可能变慢只有 W8A8/FP8 这类激活也量化的方案才能吃到 INT8 Tensor Core 的算力红利。QLLM 量化最大的技术难点是什么A激活值的系统性离群点。模型超过约 6.7B 后会稳定出现少数幅值极大50~100 倍于常规值的特征维度且这些维度对效果至关重要。用 per-tensor 量化时这些离群点会把 scale 撑大导致绝大多数正常激活被压缩到极少的量化格点上误差急剧放大。三种解法分别是 LLM.int8() 的混合精度分解、SmoothQuant 的难度迁移、AWQ/GPTQ 的只量化权重加重要性保护。QGPTQ 和 AWQ 的区别AGPTQ 基于二阶海森信息做逐列量化和误差补偿把当前列的取整误差按相关性分摊给尚未量化的列理论性强但量化慢小时级且对校准集分布较敏感。AWQ 基于激活幅值大的通道对应的权重更重要这一观察通过逐通道缩放等价地保护关键权重量化快分钟级、跨域泛化更好、kernel 更规整。精度上二者接近工程上我倾向优先试 AWQ。Q什么是 W4A16、W8A8分别适合什么场景AW 是权重位宽A 是激活位宽。W4A16 只压权重收益在显存和带宽适合长文本生成、显存吃紧、追求 decode 速度的场景代表是 GPTQ/AWQ。W8A8 权重激活都压到 INT8能真正调用 INT8 Tensor Core对 prefill 这种计算密集阶段有实际算力加速适合高并发短输出场景代表是 SmoothQuant。H100 上还可以用 FP8动态范围比 INT8 大精度更好且硬件原生支持。QSmoothQuant 的原理为什么它不增加推理开销A它利用Y (X·diag(s)^-1)·(diag(s)·W)的数学等价性把激活的量化难度按s_j max|X_j|^α / max|W_j|^(1-α)α 通常 0.5迁移一部分到权重上让两边都变得好量化。之所以零开销是因为diag(s)^-1这个缩放可以在离线阶段直接融合进前一层的 LayerNorm 权重或线性层权重里推理时没有任何额外算子。QINT4 量化为什么必须用 group-wiseAINT4 只有 16 个量化格点表达能力极其有限。如果整个张量或整个通道共用一个 scale一旦范围内有较大值小值就会被大量压成同一个格点信息损失严重。按 128 个输入维度分组各自算 scale能让每组的动态范围贴合局部分布。代价是每 128 个 4-bit 权重要多存一个 FP16 scale和 zero point等效位宽从 4 升到约 4.25这个开销完全值得。QKV Cache 要不要量化A长上下文加大 batch 时必须量化。70B 模型在 batch32、seq32K 下 KV Cache 能到 343GB比 140GB 的权重还大这时 KV 量化的收益远超权重量化。实践上 INT8/FP8 基本无损可以放心上INT4 在大海捞针类长程检索任务上会掉点需要实测。工程细节是 K 和 V 要分开处理——K 因为叠加 RoPE 且要做点积存在通道级离群点宜用 per-channelV 分布平坦per-token 即可另外保留最近 128 个 token 为高精度会更稳。Q量化后如何验证效果A三层验证。第一层是 PPLwikitext/c4做快速冒烟只能判断有没有崩。第二层是标准 benchmarkMMLU、GSM8K、HumanEval重点看推理和代码这类对量化敏感的任务。第三层也是最重要的一层是业务真实数据的端到端评测Agent 场景还要专门统计 JSON 结构化输出合法率和工具调用参数准确率——量化后这两个指标的劣化往往比 PPL 明显得多而它们直接决定线上能不能用。九、高频追问清单为什么模型规模超过 6.7B 后离群点问题突然变严重和涌现现象有关系吗GPTQ 的海森矩阵为什么可以用X^T X近似阻尼系数的作用是什么AWQ 论文中按权重幅值挑 1% 保 FP16 几乎无效按激活幅值挑就有效如何解释NF4NormalFloat4和普通 INT4 的区别它的信息论依据是什么为什么 lm_head 通常不量化如果一定要量化会有什么现象FP8 的 E4M3 和 E5M2 分别用在哪为什么前向用 E4M3、反向梯度用 E5M2Marlin kernel 为什么比朴素 W4A16 kernel 快这么多它做了哪些优化量化和 MoE 一起用有什么特殊问题专家权重量化的难点在哪如果线上发现量化模型在某类 badcase 上明显劣化你的排查路径是什么量化会不会放大模型的安全风险比如更容易被越狱为什么下一篇A20会讲大模型评测体系——量化后到底掉了多少点这个问题最终要靠一套可信的评测体系来回答。从标准 benchmark 的局限、数据污染的检测到 LLM-as-Judge 的偏差校正和 Arena 式对战评测把怎么科学地说模型好不好讲清楚。

相关新闻

最新新闻

日新闻

周新闻

月新闻