1. 从权重到激活:FP8 量化到底动了哪些东西
很多人第一次接触 FP8 量化,脑子里冒出来的第一个念头就是“把权重从 16 位压到 8 位,显存直接砍半”。这个理解对了一半,但漏掉了真正决定成败的另一半——激活值。权重是静态的,训练完就固定在那里,量化起来相对可控;激活值是动态的,每来一条输入就变一次,分布随上下文长度、batch 大小、甚至 prompt 的语言风格而漂移。你只压权重不压激活,推理时的显存大头和带宽瓶颈依然卡在那里。
FP8 的两套格式 E4M3 和 E5M2,本质上是在“动态范围”和“精度”之间做取舍。E4M3 用 4 位指数、3 位尾数,能表示的数值范围窄一些,但尾数多一位意味着同样范围内的精度更高;E5M2 反过来,指数多一位能覆盖更大的动态范围,代价是尾数只剩 2 位,精度掉得厉害。业界常见的做法是:权重和激活的前向传播用 E4M3,反向传播里的梯度用 E5M2。原因不复杂——前向的数值分布相对集中,E4M3 的精度优势能发挥出来;梯度的数值跨度大,动不动就出现极小值或极大值,必须靠 E5M2 的宽范围兜住,否则直接溢出成 NaN。
这里有一个容易被忽略的细节:FP8 量化不是简单地把每个数四舍五入到最近的 8 位表示。真正在工程里落地时,普遍采用“分块缩放”策略。具体来说,把权重矩阵按行或按列切成若干块,每块单独算一个缩放因子,块内的数值先除以这个因子再转成 FP8,推理时再乘回来。这样做的好处是,不同块之间的数值分布差异被局部消化掉了,不会因为某一行的极端值把整层的量化精度拉垮。你可以把它理解成给每个小区域单独配一副眼镜,而不是整张图共用一副度数不对的镜片。
注意:分块粒度不是越细越好。块太小,缩放因子的存储开销和计算开销会吃掉量化带来的收益;块太大,局部极值的影响又压不住。实践中 128 或 256 个元素一块是比较常见的折中。
2. 长上下文实测:显存、速度与精度的三角博弈
长上下文是这次实测的重头戏。我把上下文从 4K 一路拉到 128K,每隔一档记录显存占用、首 token 延迟和生成吞吐。测试环境是一张 80G 显存的加速卡,模型用的是 70B 级别的稠密架构,FP8 量化覆盖权重和激活。为了控制变量,所有测试都用同一段技术文档作为输入,输出长度固定 512 token,温度设为 0。
先看显存。4K 上下文时,FP8 量化后的显存占用比 FP16 基线低了大约 42%,这个数字和理论预期基本吻合。但到了 32K,差距开始收窄,FP8 只比 FP16 省了 31% 左右。原因在于 KV Cache 的膨胀——上下文越长,KV Cache 占用的显存比例越高,而 KV Cache 是否量化、量化到什么程度,直接决定了长上下文场景下的显存天花板。我这次测试对 KV Cache 也做了 FP8 处理,如果不做,32K 以上基本就跑不动了。
首 token 延迟的变化更有意思。4K 时 FP8 比 FP16 快了约 18%,但 64K 时这个优势缩小到 7%,128K 时几乎持平。这说明长上下文场景下,计算瓶颈从矩阵乘法的算力转移到了注意力机制的内存访问上。FP8 在算力密集的部分收益明显,但注意力那块的内存带宽压力,它帮不上太多忙。生成吞吐倒是全程保持优势,128K 时仍有约 22% 的提升,因为解码阶段是逐 token 生成的,算力密集特征更突出。
精度方面,我用了一套包含事实问答、代码补全和长文摘要的评测集。4K 到 16K 区间,FP8 和 FP16 的输出质量差异在统计上不显著,人工评估也几乎分辨不出来。但到了 64K 以上,FP8 在需要精确回忆远处细节的任务上开始出现可察觉的退化,比如问它“文档第 3 节提到的那个参数默认值是多少”,FP16 能准确答出,FP8 偶尔会给出一个相近但错误的数值。这个退化不是均匀发生的,它和注意力权重的量化误差在长距离上的累积有关。
| 上下文长度 | 显存节省(vs FP16) | 首 token 延迟变化 | 生成吞吐变化 | 精度退化风险 |
|---|---|---|---|---|
| 4K | 约 42% | 快 18% | 快 25% | 极低 |
| 16K | 约 38% | 快 14% | 快 24% | 低 |
| 32K | 约 31% | 快 11% | 快 23% | 中低 |
| 64K | 约 26% | 快 7% | 快 22% | 中 |
| 128K | 约 21% | 快 3% | 快 22% | 中高 |
这张表里的数字是多次运行取中位数的结果,不同硬件和软件栈会有出入,但趋势是一致的:上下文越长,FP8 的显存优势越被 KV Cache 稀释,延迟优势越被内存带宽吃掉,只有生成吞吐始终坚挺。
3. 量化配置的实操细节:从校准到部署
校准集的选择直接决定量化误差的分布。我试过三种方案:用通用语料校准、用领域语料校准、用随机噪声校准。结果很明确——领域语料校准的效果最好,通用语料次之,随机噪声最差。原因在于缩放因子是根据校准数据里的数值分布算出来的,如果校准集和实际推理时的输入分布不匹配,缩放因子就会偏,量化误差自然大。比如你拿一堆英文新闻去校准一个主要处理中文技术文档的模型,效果肯定打折扣。
校准的样本数量也有讲究。太少,统计出来的分布不稳定,缩放因子波动大;太多,校准本身耗时太长,而且边际收益递减。我的经验是 512 到 1024 条样本比较合适,每条样本的长度覆盖你目标上下文长度的典型值。如果你主要跑 32K 上下文,校准样本就别只用 2K 的短文本,否则长距离依赖部分的数值分布根本没被校准到。
具体到配置层面,有几个参数值得单独拎出来说。第一个是activation_scheme,通常选dynamic,让激活的缩放因子在推理时动态计算,而不是固定用校准时的值。动态方案对输入分布的变化更鲁棒,代价是每次前向都要算一次缩放,有一点点额外开销。第二个是weight_scale_rounding,控制权重缩放因子的取整方式,一般保持默认就好,除非你发现量化后某些层的输出异常,可以试着调成更保守的取整策略。第三个是kv_cache_quant,长上下文场景下建议开启,但要注意它和activation_scheme的联动——如果激活用动态缩放而 KV Cache 用静态缩放,两者之间的数值一致性需要额外验证。
实操心得:量化配置改完之后,别急着跑全量评测。先拿一小段输入做一次前向,把 FP8 和 FP16 的中间层输出逐层对比,看哪一层的相对误差突然跳高。通常问题出在少数几个“敏感层”上,把这些层排除在量化之外,整体精度能回来不少,而显存节省只损失一点点。
4. 踩过的坑与排查手册
第一个坑是“量化后模型输出重复”。现象是生成结果里频繁出现相同的短语或句子,像是卡住了。排查下来发现是某些层的激活值在 FP8 表示下出现了饱和,大量数值被截断到同一个最大值,导致注意力分布变得极端尖锐。解决办法是检查这些层的激活缩放因子,如果发现缩放因子偏小,说明校准集里这些层的激活值范围被低估了,换一批更有代表性的校准数据重新校准即可。
第二个坑是“长上下文下显存不降反升”。这个听起来反直觉,但确实会发生。原因是某些推理框架在开启 KV Cache 量化后,会额外分配一块缓冲区来做量化/反量化的中转,如果上下文长度超过某个阈值,这块缓冲区的开销反而超过了 KV Cache 本身节省的显存。排查方法是看框架的显存分配日志,找到那块异常增长的缓冲区,调整它的分配策略或直接关掉 KV Cache 量化,改用分页注意力来省显存。
第三个坑是“首 token 延迟忽高忽低”。同一段输入,连续跑五次,延迟能差出 30%。后来定位到是动态激活缩放的计算被放在了关键路径上,而且没有做批处理优化。解决办法是把动态缩放的计算提前到模型加载时做一次预热,或者改用静态缩放加周期性校准的方案。这个坑在短上下文下不明显,因为计算量小,波动被掩盖了;长上下文下计算量放大,波动就暴露出来了。
| 问题现象 | 可能原因 | 排查动作 | 解决方向 |
|---|---|---|---|
| 输出重复、卡顿 | 激活饱和导致注意力尖锐 | 检查各层激活缩放因子 | 更换校准集,排除敏感层 |
| 长上下文显存不降反升 | 量化中转缓冲区膨胀 | 查看显存分配日志 | 关闭 KV Cache 量化或调整缓冲策略 |
| 首 token 延迟波动大 | 动态缩放计算在关键路径 | 逐层计时,定位耗时层 | 预热计算或改用静态缩放 |
| 远处细节回忆错误 | 长距离注意力量化误差累积 | 对比 FP16 的注意力权重分布 | 对注意力层采用更高精度或混合方案 |
还有一个不太容易发现的问题是“量化后的模型对 prompt 格式变得敏感”。同样的语义,换个标点符号或者调整一下换行,输出质量就有明显差异。这是因为 FP8 的精度损失让模型对输入的微小扰动更敏感了。应对办法是在部署时对输入做一层标准化,把多余的空白、不一致的标点统一掉,减少不必要的分布偏移。
5. 混合精度策略:哪里该省,哪里不能省
全量 FP8 量化在多数场景下够用,但如果你对精度有硬要求,混合精度是更稳妥的选择。我的做法是把模型拆成三档:第一档是嵌入层和最后的输出层,这两层直接和词表打交道,量化误差会直接反映到 token 概率上,建议保持 FP16;第二档是中间的注意力层和前馈层,这是量化的主战场,FP8 的收益主要从这里来;第三档是 LayerNorm 和残差连接相关的操作,这些操作的数值范围窄但精度要求高,保持 FP16 更安全。
混合精度的配置不是拍脑袋定的,得看实测数据。我的流程是:先全量 FP8 跑一遍评测,记录每个任务上的精度损失;然后把损失最大的那几个任务对应的层找出来,逐层恢复成 FP16,看精度回来多少、显存多花多少;重复这个过程,直到精度和显存的性价比达到你的接受阈值。这个过程听起来繁琐,但一旦调好,后续同架构的模型可以直接复用这套配置,边际成本很低。
提示:混合精度下,不同精度层之间的衔接处容易出现数值不一致。比如 FP8 的输出直接喂给 FP16 的 LayerNorm,如果缩放因子没对齐,LayerNorm 的输入分布会偏移。建议在精度切换的边界加一个显式的反量化操作,把数值先还原到 FP16 再往下传。
从影响范围来看,FP8 量化加长上下文的组合,最直接的受益场景是大规模文档理解和多轮长对话。前者需要模型在几万 token 的输入里精准定位信息,后者需要模型在几十轮交互后依然记得早期约定。这两个场景对显存和吞吐的要求都很苛刻,FP8 带来的显存节省让单卡能塞下更长的上下文,吞吐提升让单位时间能服务更多请求。但代价是精度上的细微退化,在需要精确回忆的任务上需要额外校验。
6. 部署侧的工程考量:不只是模型的事
模型量化完了,部署侧还有一堆事要处理。首先是推理框架的版本匹配,FP8 的支持在不同框架、不同版本之间差异很大,有的版本只支持权重 FP8,有的支持权重加激活,有的连 KV Cache 量化都覆盖了。选版本的时候别只看版本号,要看它的 release note 里明确写了支持哪些量化路径。我遇到过版本号更高但 FP8 支持反而回退的情况,原因是那个版本重构了量化模块,新模块还没覆盖所有路径。
其次是 batch 策略的调整。FP8 量化后,单次前向的显存占用降低了,理论上可以塞更大的 batch。但 batch 大了之后,激活值的分布会变宽,动态缩放的负担加重,首 token 延迟可能不降反升。我的经验是先把 batch 设成 1 跑基准,然后逐步加倍,观察吞吐和延迟的拐点。通常拐点出现在 batch 等于 4 到 8 之间,超过这个范围,吞吐增长放缓而延迟明显上升。
还有一个容易被忽视的点是量化模型的保存和加载。FP8 量化后的权重需要连同缩放因子一起保存,缩放因子的精度和存储格式会影响加载后的推理一致性。有的框架默认用 FP32 存缩放因子,有的用 FP16,加载时如果格式不匹配,量化误差会悄悄变大。建议在保存时显式指定缩放因子的精度,并在加载后做一次一致性校验,拿一小段输入对比保存前后的输出差异。
从更宏观的视角看,FP8 量化不是孤立的优化手段,它和分页注意力、连续批处理、投机解码这些技术是协同关系。分页注意力解决了 KV Cache 的碎片化问题,让长上下文下的显存利用更高效;连续批处理让不同长度的请求能混在一起跑,提升整体吞吐;投机解码用一个小模型来加速大模型的生成。FP8 量化降低了单次前向的成本,让这些技术的收益进一步放大。但协同也意味着复杂度上升,任何一个环节配置不当,整体效果都可能打折扣。
7. 一些实测中的个人体会
跑完这一轮实测,我最深的体会是:FP8 量化的收益不是线性的,它和上下文长度、batch 大小、任务类型都有复杂的交互。短上下文、大 batch、生成类任务,FP8 的收益最明显;长上下文、小 batch、精确回忆类任务,收益被稀释,甚至需要混合精度来兜底。所以别指望一套配置打天下,得根据实际负载来调。
另一个体会是校准的重要性怎么强调都不为过。我见过太多人随便拿几百条通用语料校准一下就上线,结果在特定领域上精度崩得厉害。校准集应该尽可能贴近真实推理时的输入分布,如果线上流量有多种分布,可以考虑做多套校准配置,按请求特征动态切换。这个做法的工程复杂度不低,但在精度敏感的场景下值得投入。
最后分享一个小技巧:量化后的模型在首次推理时往往有一个“预热”过程,前几次的输出可能不稳定。我的做法是在服务启动后先跑几十条预热请求,把动态缩放的计算缓存和显存分配都稳定下来,再开始接收真实流量。这个预热过程在长上下文场景下尤其重要,因为长上下文的计算图更大,预热需要的时间也更长。预热期间可以把请求路由到备用实例,避免影响线上服务质量。