☰
LLM推理分阶段量化:prefill与decode差异化策略实战
2026/10/3 18:19:27 网站建设 项目流程

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-bit,KV 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 FP16:

python 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 参数计算:显存占用和速度收益怎么估

在动手之前,最好先估算一下显存占用和预期收益,避免白忙一场。

显存占用估算公式(简化版):

  • 权重大小 ≈ 参数量 × 位宽 / 8
  • KV Cache 大小 ≈ 2 × 层数 × 序列长度 × 隐藏维度 × 位宽 / 8

举个例子,一个 7B 模型,32 层,隐藏维度 4096,序列长度 4096:

  • FP16 权重:7B × 2 bytes ≈ 14 GB
  • Q4 权重:7B × 0.5 bytes ≈ 3.5 GB
  • FP16 KV Cache:2 × 32 × 4096 × 4096 × 2 bytes ≈ 2 GB
  • INT8 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 显存,序列长度 2048,batch 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 Cache,decode 速度从 60 提到 71,提升了约 18%,精度只多掉了 0.2%。这个组合的性价比是最高的。

提示:任务准确率是我自己构造的一个小测试集,包含 200 道中文指令跟随和推理题,仅供参考。你的场景不同,数字会有差异。

5. 常见问题与排查技巧实录

5.1 量化后模型“变傻”了怎么办

这是最常见的问题。表现是:输出变得重复、逻辑断裂、指令跟随变差、代码里出现不存在的函数名。

排查思路按这个顺序来:

  1. 先确认是不是量化的问题。用同样的 prompt 跑 FP16 版本,如果 FP16 正常、量化版不正常,那基本可以确定是量化导致的。
  2. 检查输出层和 embedding 的量化类型。这两个地方最容易出问题。把它们提到 Q8_0 或者 FP16,看是否改善。
  3. 检查 KV Cache 量化。如果用了 INT4 KV Cache,试试换成 INT8 或者 FP16。长上下文任务对 KV Cache 精度更敏感。
  4. 换校准集。如果校准集和实际场景差异太大,量化 scale 会偏。用领域数据重新校准。
  5. 降低量化激进程度。从 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,别用 INT4
  • key 的精度比 value 更重要,如果只能保一个,保 key
  • 可以考虑对靠近当前位置的 KV 用高精度,远处的用低精度,但这需要改推理代码
  • 如果实在不行,就接受长上下文下精度下降,或者用更大的显存跑 FP16 KV Cache

5.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 token):decode 占主导,重点优化 KV Cache
  • 长请求(>1024 token):prefill 占主导,重点优化权重矩阵乘
  • 批量请求: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。这样量化配置一改,立刻就能看到影响。

分阶段量化这件事,说到底是在精度和效率之间找更细的平衡点。一刀切的时代已经过去了,模型越来越大、场景越来越多样,粗放式的量化策略迟早会遇到瓶颈。把推理拆开看,针对不同阶段和模块用不同策略,虽然麻烦一点,但收益是实打实的。我自己的体会是,花在量化调优上的时间,最后都会以显存节省和速度提升的形式还回来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询