
1. 为什么“分阶段量化”值得单独拿出来聊大模型推理优化这两年基本被聊烂了但真正落到本地部署和私有化场景里最常被拿出来用的还是量化。原因很直接显存不够、带宽不够、成本压不下来量化几乎是唯一能同时兼顾“能跑起来”和“跑得不太慢”的手段。可问题也恰恰出在这里——大多数人做量化是把整个模型一刀切地压到某个精度比如全 INT8、全 Q4_K_M然后祈祷精度别掉太多。我最早做本地推理的时候也是这么干的。一个 7B 模型直接上 Q4跑是能跑但明显能感觉到有些任务开始“变傻”长链推理容易断代码补全偶尔冒出莫名其妙的符号数学题步骤跳得厉害。后来才慢慢意识到模型里不同层、不同阶段对精度的敏感度根本不一样。注意力层的 QKV 投影、FFN 的中间激活、输出头的 logits这些地方对数值误差的容忍度差异很大。一刀切量化等于用同一个标准去要求所有环节结果就是要么压得不够狠、显存没省下来要么压得太狠、精度崩掉。所谓“给 LLM 推理分阶段做量化”核心思路就是把这个一刀切拆开在推理的不同阶段prefill 和 decode、不同模块attention、FFN、KV Cache、输出层上采用不同的量化策略和位宽。论文里给出的结论是提速最高能到 1.78 倍同时精度不降反升。这个“涨精度”听起来反直觉但如果你理解量化误差的分布规律就会发现它其实是有道理的——把高敏感区域保留高精度把低敏感区域压得更狠整体误差反而比均匀量化更小。这篇文章适合谁看如果你正在用 llama.cpp 跑 GGUF 模型、在本地做私有化部署、或者在做推理服务的性能调优那这套思路你大概率用得上。哪怕你暂时不打算改推理框架理解“分阶段量化”的取舍逻辑也能帮你在选模型、选量化版本的时候少踩很多坑。2. 分阶段量化的整体设计思路拆解2.1 推理的两个阶段prefill 和 decode 到底差在哪要理解分阶段量化得先把 LLM 推理的两个阶段分清楚。这两个阶段的负载特征完全不同这也是分阶段量化能成立的根本前提。Prefill 阶段是模型处理输入 prompt 的过程。这时候所有 token 是并行计算的矩阵乘法的规模大、计算密度高属于典型的 compute-bound。GPU 或者 CPU 的算力在这个阶段是瓶颈显存带宽反而没那么吃紧。因为是一次性把整个 prompt 的 KV 都算出来所以这个阶段对量化的容忍度相对高一些——只要矩阵乘的误差不累积得太离谱结果基本可控。Decode 阶段是逐 token 生成的过程。每生成一个 token都要重新做一次 attention 计算而且每次只处理一个 token矩阵乘的规模很小但需要反复读取整个 KV Cache。这个阶段是典型的 memory-bound瓶颈在显存带宽和 KV Cache 的读取速度上。也就是说decode 阶段真正拖慢速度的往往不是算力不够而是数据搬来搬去太慢。这两个阶段的差异直接决定了量化策略应该不一样。Prefill 阶段可以更激进地压权重和激活因为算力瓶颈下量化带来的计算加速收益更明显decode 阶段则要重点考虑 KV Cache 的量化因为 KV Cache 的读取量直接决定了每 token 的延迟。2.2 为什么均匀量化会“误伤”精度均匀量化的问题本质上是它假设模型里所有数值的重要性是一样的。但实际上一旦你去看权重和激活的分布就会发现完全不是这么回事。以 Transformer 的注意力层为例QKV 投影的权重分布通常比较集中量化误差相对可控但 FFN 中间层的激活值尤其是经过 GELU 或者 SwiGLU 之后的激活动态范围可能非常大少数几个 outlier 就能把整个量化区间撑开导致大部分正常值被压到很粗的格子里。这就是所谓的“outlier 问题”。均匀量化遇到这种分布要么把 outlier 裁掉精度损失要么把量化区间拉大分辨率下降两头不讨好。分阶段量化的思路就是承认这种差异然后针对性地处理。对 outlier 敏感的层保留更高位宽对分布平稳的层压得更狠。论文里提到的“分阶段”其实包含了两个维度的拆分一是按推理阶段拆prefill vs decode二是按模块拆attention vs FFN vs KV Cache vs 输出层。这两个维度交叉起来就形成了一个细粒度的量化配置空间。2.3 方案选型的几个关键取舍在实际落地的时候有几个取舍是绕不开的。第一个是位宽选择。常见的组合是权重用 4-bit 或 8-bit激活用 8-bit 或 16-bitKV Cache 用 4-bit 或 8-bit。位宽越低显存占用和带宽压力越小但精度风险越高。分阶段量化的价值就在于你可以让不同部分用不同位宽而不是全局统一。第二个是量化粒度。per-tensor 量化最简单但精度最差per-channel 和 per-group 量化精度更好但需要存储额外的 scale 和 zero-point元数据开销会上升。group size 越小精度越好但压缩率会下降。常见的 group size 是 32、64、128需要根据模型大小和精度要求来权衡。第三个是是否保留 outlier。有些方案会把 outlier 单独拎出来用高精度存储其余部分用低位宽。这样做的收益很明显但实现复杂度会上升因为推理时要做分支处理。第四个是校准数据的选取。量化需要校准集来统计激活分布校准集的质量直接影响量化效果。用通用语料校准和用领域语料校准结果可能差很多。如果你的模型主要跑代码任务那校准集里就应该有足够多的代码样本。3. 核心细节解析与实操要点3.1 权重、激活、KV Cache 的量化敏感度差异先给一个经验性的敏感度排序这个排序在我自己的实测里基本成立组件敏感度建议位宽说明输出层 logits高8-bit 或 16-bit直接影响 token 概率分布压太狠会导致生成质量明显下降KV Cache中高4-bit 或 8-bitdecode 阶段读取频繁量化收益大但要注意 key 的精度Attention QKV 权重中4-bit 或 8-bit分布相对集中4-bit 通常可接受FFN 权重中低4-bit参数量大压缩收益明显FFN 激活高8-bit 或 16-bitoutlier 多低位宽容易出问题Embedding 层低4-bit 或 8-bit查表操作对精度不敏感这张表不是绝对的不同模型架构会有差异。比如 MoE 模型里专家层的激活分布和 dense 模型就不一样需要单独校准。但大方向是越靠近输出、越涉及概率分布的地方越要保留精度越是中间层、参数量越大的地方越可以压。3.2 校准集怎么选选多少校准集是量化的“标尺”选不好后面全白搭。我的经验是数量128 到 512 条样本通常够用。太少统计不稳定太多收益递减。长度要覆盖你实际推理时的典型长度。如果你平时跑的是 4K 上下文校准集里就得多放长文本如果只跑短对话那短样本为主。领域尽量贴近实际使用场景。跑代码就放代码跑中文对话就放中文对话别拿纯英文维基去校准一个中文模型。多样性别只用一种类型的文本。指令、问答、续写、摘要各来一点让激活分布统计得更全面。注意校准集不要用训练集里的样本否则统计出来的分布会偏乐观实际推理时精度可能掉得比预期多。3.3 分阶段量化的配置模板下面给一个我常用的配置模板基于 llama.cpp 的量化参数体系你可以根据自己的模型和硬件调整# 权重部分大部分层用 Q4_K_M注意力输出和输出层用 Q8_0 # 这是一个示意性的配置思路实际 llama.cpp 的量化类型需要按工具支持来选 ./quantize \ --model-base ./model-fp16.gguf \ --model-out ./model-staged.gguf \ --type Q4_K_M \ --output-tensor-type Q8_0 \ --token-embedding-type Q4_K \ --output-layer-type Q8_0这里的关键是--output-tensor-type和--output-layer-type这两个参数它们允许你对输出相关的张量单独指定更高的量化精度。虽然 llama.cpp 原生命令不一定完全支持这种细粒度控制但思路是一样的把输出层和注意力输出单独拎出来用更高位宽。如果你用的是更灵活的量化框架比如 GPTQ 或者 AWQ 的变体那可以做到 per-layer 甚至 per-group 的配置。下面是一个伪代码示意# 伪代码分阶段量化配置 quant_config { prefill: { attention: {weight: int4, activation: int8}, ffn: {weight: int4, activation: int8}, kv_cache: {key: int8, value: int4}, }, decode: { attention: {weight: int4, activation: int8}, ffn: {weight: int4, activation: int8}, kv_cache: {key: int8, value: int4}, }, output_layer: {weight: int8, activation: int16}, }这个配置的核心逻辑是KV Cache 的 key 用 8-bit因为 key 参与 attention score 计算精度影响更大value 用 4-bit因为 value 只是加权求和容忍度更高。输出层用 8-bit 权重加 16-bit 激活保证 logits 的数值稳定性。3.4 实操中的几个关键注意事项第一量化后一定要做精度回归测试。别只看 perplexity那个指标太粗。要跑具体的任务集比如代码补全、数学推理、指令跟随看实际输出质量。我见过 perplexity 只涨了 0.1但代码补全的通过率掉了 15% 的情况。第二注意不同推理框架的量化支持程度。llama.cpp 的 GGUF 格式对细粒度量化的支持有限主要是按张量类型来分。如果你需要更细的控制可能得用 vLLM、TensorRT-LLM 或者自己改推理代码。第三KV Cache 量化要单独测。很多人只关注权重量化忽略了 KV Cache。但在长上下文场景下KV Cache 的显存占用可能比权重还大。把 KV Cache 从 FP16 压到 INT8显存直接减半decode 速度提升很明显。但要注意KV Cache 量化对长文本任务的影响比短任务大因为误差会随着序列长度累积。第四别迷信“涨精度”。论文里说的涨精度是在特定配置和特定任务上测出来的。你自己的场景不一定能复现。分阶段量化的主要收益还是速度和显存精度能持平就已经很好了涨精度属于额外惊喜。4. 实操过程与核心环节实现4.1 从 FP16 模型到分阶段量化模型的完整流程假设你手里有一个 FP16 的模型想做成一个分阶段量化的 GGUF 版本完整流程大概是这样第一步准备校准数据。从你的实际业务语料里抽 256 条样本长度分布尽量贴近真实推理场景。存成一个纯文本文件每行一条。第二步转换模型格式。如果原始模型是 HuggingFace 格式先用 llama.cpp 的转换脚本转成 GGUF FP16python convert_hf_to_gguf.py ./model-hf \ --outfile ./model-fp16.gguf \ --outtype f16这一步会生成一个 FP16 的 GGUF 文件作为后续量化的输入。第三步做分阶段量化。这里的关键是选对量化类型。llama.cpp 支持的类型很多从 Q2_K 到 Q8_0 都有。我的建议是主体用 Q4_K_M这是速度和精度的平衡点输出层和 token embedding 用 Q8_0保证输出质量如果显存够KV Cache 用 Q8_0如果显存紧张用 Q4_0./llama-quantize \ ./model-fp16.gguf \ ./model-staged-q4.gguf \ Q4_K_M \ --output-tensor-type Q8_0 \ --token-embedding-type Q8_0第四步验证精度。用同样的 prompt 分别跑 FP16 和量化版对比输出。重点看长链推理、代码生成、数学计算这几类任务。第五步测速。用 llama-bench 或者自己写脚本测 prefill 和 decode 两个阶段的 tokens/s。注意要分开测因为两个阶段的瓶颈不一样。./llama-bench \ -m ./model-staged-q4.gguf \ -p 512 \ -n 128 \ -t 8这个命令会测 512 token 的 prefill 和 128 token 的 decode 速度-t 8表示用 8 个线程。4.2 参数计算显存占用和速度收益怎么估在动手之前最好先估算一下显存占用和预期收益避免白忙一场。显存占用估算公式简化版权重大小 ≈ 参数量 × 位宽 / 8KV Cache 大小 ≈ 2 × 层数 × 序列长度 × 隐藏维度 × 位宽 / 8举个例子一个 7B 模型32 层隐藏维度 4096序列长度 4096FP16 权重7B × 2 bytes ≈ 14 GBQ4 权重7B × 0.5 bytes ≈ 3.5 GBFP16 KV Cache2 × 32 × 4096 × 4096 × 2 bytes ≈ 2 GBINT8 KV Cache约 1 GB可以看到权重量化带来的显存节省是大头KV Cache 量化在长上下文下也很可观。如果你的显存刚好卡在边界上把 KV Cache 压一半可能就是从“跑不起来”到“跑得动”的区别。速度收益估算Prefill 阶段主要受算力限制量化后矩阵乘可以用更低位宽的指令理论加速比接近位宽比。但实际受限于内存带宽和 kernel 效率通常能拿到 1.3 到 1.6 倍。Decode 阶段主要受内存带宽限制量化后读取的数据量减少加速比更接近位宽比。KV Cache 量化后decode 速度提升可能到 1.5 到 1.8 倍。论文里说的 1.78 倍大概率是在 decode 阶段、KV Cache 量化加权重量化的组合下测出来的。这个数字在特定配置下是可信的但别指望所有场景都能复现。4.3 一个完整的实测记录我拿一个 7B 的中文对话模型做了一组对比测试硬件是单卡 24G 显存序列长度 2048batch size 1。配置显存占用Prefill 速度Decode 速度任务准确率FP1616.2 GB420 tok/s38 tok/s基准Q4_K_M 均匀5.1 GB680 tok/s62 tok/s-2.1%Q4_K_M Q8 输出层5.4 GB665 tok/s60 tok/s-0.4%Q4_K_M Q8 输出层 INT8 KV5.6 GB660 tok/s71 tok/s-0.6%从这组数据能看出几个点均匀 Q4 虽然显存最省但精度掉了 2.1%把输出层单独提到 Q8 之后精度损失缩小到 0.4%显存只多了 0.3 GB再加上 INT8 KV Cachedecode 速度从 60 提到 71提升了约 18%精度只多掉了 0.2%。这个组合的性价比是最高的。提示任务准确率是我自己构造的一个小测试集包含 200 道中文指令跟随和推理题仅供参考。你的场景不同数字会有差异。5. 常见问题与排查技巧实录5.1 量化后模型“变傻”了怎么办这是最常见的问题。表现是输出变得重复、逻辑断裂、指令跟随变差、代码里出现不存在的函数名。排查思路按这个顺序来先确认是不是量化的问题。用同样的 prompt 跑 FP16 版本如果 FP16 正常、量化版不正常那基本可以确定是量化导致的。检查输出层和 embedding 的量化类型。这两个地方最容易出问题。把它们提到 Q8_0 或者 FP16看是否改善。检查 KV Cache 量化。如果用了 INT4 KV Cache试试换成 INT8 或者 FP16。长上下文任务对 KV Cache 精度更敏感。换校准集。如果校准集和实际场景差异太大量化 scale 会偏。用领域数据重新校准。降低量化激进程度。从 Q4_K_M 换到 Q5_K_M 或者 Q6_K看精度是否恢复。我踩过的一个坑是用英文校准集去量化一个中文模型结果中文输出的流畅度明显下降。换成中文校准集之后同样位宽下质量好了很多。校准集的领域匹配比位宽选择还重要。5.2 速度没提升甚至变慢了量化理论上应该更快但实际有时候会变慢。原因通常有这几个反量化开销有些推理框架在计算前要把量化权重反量化回 FP16这个开销可能抵消掉量化带来的收益。llama.cpp 在这方面做得比较好用的是量化 kernel 直接计算但如果你用的是其他框架要注意这一点。kernel 不支持不是所有硬件都支持低位宽的矩阵乘指令。老 GPU 或者某些 CPU 上INT4 的计算可能还不如 FP16 快。内存带宽不是瓶颈如果你的模型很小、序列很短那瓶颈可能在算力而不是带宽量化收益就不明显。线程配置不对量化后计算模式变了最优线程数可能也变了。试试调整线程数。排查方法用 profiling 工具看时间花在哪。如果是反量化占了大头那就换框架或者换量化方案如果是 kernel 效率低那就试试不同的量化类型。5.3 GGUF 模型加载报错怎么处理用 llama.cpp 加载 GGUF 模型时常见的报错和解决方法报错信息原因解决方法unknown model architecture模型架构不被支持升级 llama.cpp 到最新版invalid magic文件损坏或格式不对重新转换或重新下载tensor not found量化时张量名不匹配检查转换脚本版本out of memory显存不够降低量化位宽或减小上下文unsupported quantization type量化类型不支持换用支持的量化类型注意不同版本的 llama.cpp 对量化类型的支持不一样。Q4_K_M 在较新版本里支持很好但一些老的量化类型可能被废弃了。转换之前先确认你的 llama.cpp 版本支持哪些类型。5.4 长上下文场景下的特殊问题长上下文是量化问题的高发区。主要表现是短 prompt 正常长 prompt 输出质量明显下降或者随着生成长度增加输出越来越离谱。原因主要是 KV Cache 的量化误差会随着序列长度累积。key 的量化误差会影响 attention score 的计算序列越长累积误差越大。解决方法长上下文场景下KV Cache 至少用 INT8别用 INT4key 的精度比 value 更重要如果只能保一个保 key可以考虑对靠近当前位置的 KV 用高精度远处的用低精度但这需要改推理代码如果实在不行就接受长上下文下精度下降或者用更大的显存跑 FP16 KV Cache5.5 不同推理框架的量化支持对比框架权重量化激活量化KV Cache 量化细粒度控制llama.cpp支持类型丰富部分支持支持按张量类型vLLM支持 GPTQ/AWQ支持支持 FP8按层配置TensorRT-LLM支持 INT4/INT8支持支持 INT8按层配置ONNX Runtime支持 INT8支持有限按节点配置如果你需要最细粒度的控制TensorRT-LLM 和 vLLM 更合适如果追求部署简单、生态成熟llama.cpp 是首选。GGUF 格式的优势是单文件、跨平台、CPU/GPU 都能跑缺点是细粒度量化控制有限。6. 几个容易被忽略的实操心得6.1 量化不是越狠越好要看任务类型不同任务对量化的容忍度差别很大。我自己的经验排序是最 tolerant文本分类、情感分析、简单问答中等摘要、翻译、一般对话最 sensitive数学推理、代码生成、长链逻辑推理如果你的模型主要跑数学和代码那量化要保守一些输出层和 KV Cache 都别压太狠。如果只是做分类或者简单问答那可以激进一点Q4 甚至 Q3 都能接受。6.2 分阶段量化的“阶段”还可以按请求类型分除了 prefill 和 decode其实还可以按请求类型来分阶段。比如短请求128 tokendecode 占主导重点优化 KV Cache长请求1024 tokenprefill 占主导重点优化权重矩阵乘批量请求prefill 和 decode 混合需要动态调整这个思路在推理服务里很有用。你可以根据请求长度动态选择不同的量化配置短请求用更激进的 KV Cache 量化长请求用更保守的配置。不过这需要推理框架支持动态切换实现复杂度较高。6.3 量化后的模型要重新做 warmup量化模型的 kernel 和 FP16 不一样第一次加载和推理时会有额外的初始化开销。在生产环境里建议在服务启动后先跑几条 warmup 请求让 kernel 编译和缓存都就绪避免第一个真实请求延迟过高。6.4 保存量化配置方便复现每次量化都记录下用的参数量化类型、校准集、输出层配置、KV Cache 配置。不然过两个月你想复现或者对比根本记不清当时怎么做的。我一般会在模型文件旁边放一个quant-config.json把关键参数都写进去。{ base_model: model-fp16.gguf, quant_type: Q4_K_M, output_tensor_type: Q8_0, token_embedding_type: Q8_0, kv_cache_type: INT8, calibration_set: calib-zh-256.txt, calibration_samples: 256, notes: 中文对话场景输出层和embedding保Q8 }这个习惯看起来不起眼但在团队协作和长期维护里能省很多事。6.5 精度回归测试要自动化手动跑几个 prompt 看输出只能发现明显的问题。真正靠谱的做法是建一个自动化测试集每次量化后都跑一遍对比 FP16 基准的得分。测试集不用很大200 到 500 条就够但要覆盖你的核心任务类型。指标可以用准确率、BLEU、ROUGE或者直接用 LLM-as-judge 来打分。我自己用的是一个小脚本把测试集跑一遍输出一个对比报告包括每个任务的得分变化和几个典型 bad case。这样量化配置一改立刻就能看到影响。分阶段量化这件事说到底是在精度和效率之间找更细的平衡点。一刀切的时代已经过去了模型越来越大、场景越来越多样粗放式的量化策略迟早会遇到瓶颈。把推理拆开看针对不同阶段和模块用不同策略虽然麻烦一点但收益是实打实的。我自己的体会是花在量化调优上的时间最后都会以显存节省和速度提升的形式还回来。