先说一个我印象很深的部署场景。前阵子帮团队优化一个13B模型的推理服务FP16加载后显存剩得不多并发一上来就时不时爆OOM。当时第一反应是换一张更大显存的卡后来冷静下来分析把权重切成FP8、KV Cache改成低精度存储显存占用肉眼可见地降下来延迟还顺带得到了改善。从那以后数值精度就成了我做部署优化时第一个会去盘点的变量。这篇文章不聊花活就围绕“模型部署与推理优化”里最基础也最容易糊弄过去的一环数值精度与混合精度。我会把 FP32、FP16、BF16、FP8 这几档精度到底差在哪讲清楚也会说明部署阶段说的“混合精度”到底在混什么最后给一条从 FP32 一路压到 FP8 的实操路径以及我在真实项目里踩过的坑。适合正在做大模型推理部署、被显存或延迟卡脖子、想搞明白 vLLM 或 TensorRT-LLM 里各种 dtype 参数的人。1. 数值精度的基础认知FP32、FP16、BF16 到底差在哪1.1 浮点数格式一眼看懂符号位、指数位、尾数位想搞懂精度问题首先得知道一个浮点数在计算机里是怎么存的。不管是 FP32、FP16 还是 BF16、FP8本质上都是三部分符号位、指数位、尾数位。符号位决定正负指数位决定这个数的量级范围尾数位决定数的精细程度。用一个生活化的类比来讲指数位是“尺子的量程”尾数位是“尺子上的刻度”。量程越大能表示的数越大或越小刻度越多两个相邻数之间能区分得越细。FP16 之所以在训练里容易出问题就是因为它的量程不够大很小的梯度会直接变成 0。常见精度的位结构如下类型总位数指数位尾数位单参数占用特点FP32328234 字节量程和精度都很好但太占资源FP16165102 字节精度尚可量程小容易上下溢出BF1616872 字节量程接近 FP32但尾数精度低FP8 E4M38431 字节高精度、小量程适合前向计算FP8 E5M28521 字节大量程、低精度适合梯度与误差只看表格可能没什么感觉落到实际项目里就是一个明确的显存账。一个 7B 参数模型FP32 权重需要 28GBFP16 或 BF16 是 14GBFP8 只需要 7GB。就这一项就决定了你的模型能不能塞进一张 24GB 的显卡以及跑长上下文时还剩多少空间给 KV Cache。1.2 精度不只是“省显存”还有算力与带宽两本账很多人以为降低精度只是为了省显存实际上在推理优化里它同时撬动了三件事显存容量、显存带宽、计算吞吐。先看带宽。GPU 推理时每一层都要把权重从显存搬到计算单元里。权重精度减半搬运的数据量就减半带宽压力立刻下降。这就是为什么模型量化之后哪怕计算逻辑没变延迟也会降——很多时候瓶颈根本不在算力而在显存带宽。对于那种“每秒生成 token 数上不去”的模型把权重从 FP16 切成 FP8往往比调什么 batch size 都管用。再看算力。现代显卡都有专门的低精度计算单元也就是 Tensor Core。NVIDIA 从 Volta 架构开始支持 FP16到 Ampere 支持 BF16再到 Hopper 和 Ada Lovelace 架构支持 FP8。以 H100 为例FP8 的矩阵乘吞吐大约是 FP16 的两倍是 FP32 的好几倍。也就是说只要数值稳定性扛得住用低精度计算不仅是“省着用”还是“跑得更快”。1.3 部署场景里的真实矛盾模型变大预算不会跟着变大做推理优化的人都会遇到同一个矛盾模型参数在涨上下文长度在涨但单卡显存和采购预算没怎么涨。7B 模型可能还能用 FP16 跑70B 模型就非常难受了。再加上现在大家都在卷长上下文KV Cache 的显存开销随着序列长度线性增长往往序列一长显存就被 KV Cache 吃掉了大半。这个时候如果还是坚持所有东西都用高精度就只有换卡这一条路。但换卡的成本和周期在大多数团队里都是不可接受的。所以你会看到现在开源推理框架里FP8、INT8、INT4 这些低精度方案成了标配功能不是大家喜欢折腾而是被显存和成本逼出来的。理解这一点你就知道精度选择不是一个“学术爱好”而是部署环节里的核心工程决策。2. 混合精度它不是“更高精度”而是“哪里用多少位”2.1 混合精度的真正战场训练阶段的数值稳定性“混合精度”这个词最早火起来是因为训练。如果你把整个网络从 FP32 直接降到 FP16训练大概率会失败。原因很简单FP16 的范围最大到 65504但训练过程中梯度经常远小于这个值小到一定程度就直接下溢成 0梯度一断模型就彻底不更新了。那混合精度是怎么解决的呢典型方案是主权重保持 FP32前向和反向计算用 FP16梯度在计算后再缩放回来。其中最关键的是 loss scaling——把损失值放大若干倍让梯度在 FP16 范围内落在一个比较安全的区间等梯度计算完再缩小回去更新 FP32 权重。这样既享受了 FP16 的加速又保住了训练稳定性。后来 BF16 出现训练才省心了很多。BF16 的指数位跟 FP32 一样多所以动态范围基本不存在下溢问题哪怕尾数精度低一点大多数场景也能接受。这就是为什么现在大模型预训练基本都用 BF16 混精度而不是 FP16。理解这段历史对做部署很有帮助因为部署阶段的精度选择本质上沿用了同一套逻辑不是全盘降精度而是关键地方保精度其他地方放开一点。2.2 部署时我们实际在“混”什么权重、激活、KV Cache部署阶段不涉及训练里的梯度但同样有“混合精度”的需求而且混的东西更具体。你可以把部署时的精度分为三块来盘权重精度这是最容易动手的一项。把模型参数从 FP16 转成 INT8 或 FP8显存立刻降下来而且对精度影响相对可控。激活精度也就是前向计算过程中产生的中间结果。激活的数值分布在不同层差异很大直接降精度容易出问题需要更谨慎。KV Cache 精度Transformer 推理时缓存的 Key 和 Value 张量。这部分会随序列长度线性增长长上下文场景下把它降精度收益极明显但它对精度也很敏感量化不好会让生成质量明显退化。实际部署里最常见的组合是什么我项目里用得最多的是权重 FP8 或 INT8KV Cache INT8 或 FP8激活保持 BF16。计算时高精度输入和低精度输入做混合矩阵乘在 Tensor Core 上执行但算子层面的归一化、Softmax 这类还是用高精度做。这就是部署意义上的“混合精度”。2.3 AMP 的取舍逻辑部署时同样适用训练里常用的自动混合精度AMP本质上就是“自动决定哪些层用低精度算、哪些层必须留在高精度”。Ampere 时代的经验是卷积、矩阵乘这些计算密集的层对低精度容忍度高可以切LayerNorm、Softmax、损失计算这些操作数值范围变化大必须留在高精度。这个经验放到部署时依然成立。举个例子Transformer 里的 RMSNorm 和注意力 Softmax我不是不会在 FP8 下实现而是实测下来很容易因为数值分布太广导致精度崩塌收益又不大所以目前主流框架里这些层也都是保持高精度计算的。真正吃掉大量资源的是线性层和矩阵乘它们对低精度的容忍度最高所以首先应该被降到 FP8。做决策时你可以把模型里的算子分成三类必须高精度、可以低精度、受益于低精度。第一类放着不动第三类优先降第二类看精度余量。这就是“混合”两个字的核心不是所有东西一起降而是精准地降该降的地方。3. FP8 的到来格式、硬件与缩放因子三大门槛3.1 一个字节里的两种精度E4M3 与 E5M2FP8 是最近两年最热门的推理精度原因很简单它能把权重压缩到每参数 1 字节同时又有比 INT8 更好的数值表现。FP8 不是一个统一格式而是两种子格式这一点很多人第一次接触时会忽略。E4M3 是 4 位指数、3 位尾数动态范围上限只有 448但因为尾数多一位表示精度明显更好。E5M2 是 5 位指数、2 位尾数动态范围可以到 57344量程更大但精度更粗。在推理场景里权重和激活的数值范围通常比较集中对精度要求更高所以 E4M3 是首选。E5M2 一般用于梯度累积或者误差补偿这种动态范围特别大的地方。两者怎么选有一个简单经验如果主要做前向推理无脑用 E4M3如果要做前向误差补偿或者梯度回传才考虑 E5M2。NVIDIA 的常见方案里也是把 E4M3 作为主格式TensorRT-LLM、vLLM 里提到的 FP8 量化默认基本都指 E4M3。3.2 什么样的硬件支持 FP8你的卡到底能不能用FP8 不是软件层面随便模拟一下就行的它需要硬件 Tensor Core 原生支持才能获得吞吐收益。目前 NVIDIA 这边H100、H200、L40S、L40、RTX 40 系Ada Lovelace 架构都原生支持 FP8。换句话说如果你手头是 A100、A10、RTX 30 系折腾 FP8 只能靠跑分模拟实际收益很有限还不如老实用 INT8 量化。这个限制在实际部署里非常关键。我见过有人拿着 A100 配置了半天 FP8结果框架直接报“不支持”或者跑出来精度是准的但速度没提升就是因为卡本身没有 FP8 单元。所以动手之前先确认你的 GPU compute capability 版本。Hopper 和 Ada 架构能跑 FP8Ampere 及更早的架构就别在 FP8 上花时间了。开源工具这边vLLM 从较新版本开始支持 FP8 权重加载TensorRT-LLM 也有成熟的 FP8 方案PyTorch 原生支持了torch.float8_e4m3fn这种 dtype配合 torchao 可以做量化。TGI 也在推进相关支持。生态已经比较完整不太需要从零写量化核。3.3 缩放因子FP8 量化里最容易出错的部分FP8 本身只表示有限范围内的数要把一个 FP16 范围内的张量塞进 FP8就必须做缩放。缩放因子的逻辑和 INT8 量化类似先统计目标张量的数值分布确定一个比例系数 scale量化过程是quant round(x / scale)反量化是x_approx quant * scale。scale 选择得准不准直接决定量化误差大小。scale 的粒度有三种per-tensor 对整个张量用一个 scale简单但对异常值敏感per-channel 每个通道一个 scale更精准但存储和计算开销更大per-block 按小块来比如每 128 个元素一组精度和开销的平衡最好。KV Cache 量化通常用 per-block 或 per-channel权重量化常用 per-channel激活量化由于数值动态变化常用动态计算 scale 的方式。一个我在实践中反复遇到的坑是激活值偶尔会出现极端大值这些离群值会拉高 scale导致大多数正常值被量化到很小的档位精度白白损失。处理方式通常是对激活做 clipping把 99.99% 分位以外的值截断掉用剩余范围来算 scale。这在量化领域叫校准时的离群点处理不做这一步FP8 激活量化的效果会非常不稳定。3.4 为什么 KV Cache 走 FP8 是大趋势KV Cache 是一个很容易被忽略的显存黑洞。模型参数显存是固定的但 KV Cache 会随着并发请求数和上下文长度不断增长。一个 7B 模型的 FP16 KV Cache在长上下文和较大并发下吃掉的内存甚至可能超过权重本身。把 KV Cache 从 FP16 压到 FP8理论上直接省一半显存这对提升长文本吞吐和批量大小都有立竿见影的效果。但 KV Cache 的精度敏感性比很多人想象的更高。它被反复读取误差会随着序列长度累积。我在实测中见过量化后单请求输出质量正常但上下文一长就出现重复、逻辑断裂的情况。更稳妥的做法是分步走先用 INT8 做 KV Cache 量化稳定后再尝试 FP8并且配合 per-block 缩放来稀释误差累积。不要为了省那一点显存把生成质量这个根本底线给丢了。4. 实操把模型从 FP32 一路压到 FP8 的完整流程4.1 先摸清基线显存、延迟、PPL 一个都不能少任何量化优化第一步都不是直接压精度而是把基线数据先量出来。我一般会记录五个数据模型权重占用的显存、KV Cache 占用的显存、单请求的首 token 延迟TTFT、生成每 token 的延迟TPOT、同一批评测集上的困惑度PPL或任务指标。PPL 是一个特别重要的量化健康指标。它不需要你跑完整下游任务只需要拉一段有代表性的文本让模型计算在给定精度下输出的困惑度。PPL 变化越小说明量化对模型分布的破坏越小。一般我要求 PPL 上升控制在 1%~3% 以内超过 5% 就要认真排查。记录基线的意义在于没有对比你根本不知道量化后到底损失了什么也不知道该回退哪一层。我见过太多人量化完只看“能不能跑”不看“跟原来的输出差多少”结果上线后出现明显质量下滑又不清楚是哪一步引入的。4.2 能走框架现成能力就别手写量化核不少人一提到 FP8 就以为要写 CUDA 核、手动 schedule 缩放矩阵其实大部分场景完全不需要。现在的开源框架已经把这些脏活封装好了我的建议是先把框架能力用起来再决定要不要深入定制。vLLM 里启动服务时可以直接指定--quantization fp8并加载权重为torch.float8_e4m3fn框架会自动处理缩放和反量化逻辑。TensorRT-LLM 的 FP8 方案更成熟支持从 FP16 权重通过校准生成 FP8 引擎。PyTorch 生态里可以配合 torchao 做量化后继续跑torch.compile。哪怕你是用 ollama 或者 docker 起 vllm 容器做本地部署底层也是同一套精度处理逻辑理解了 FP8 的缩放原理排查起来就不会觉得黑盒。值得注意的一点是加载 FP8 权重和用 FP8 做矩阵乘算力是两码事。只把权重存成 FP8 但计算时反量化回 FP16显存省了但算力收益有限真正让 Tensor Core 跑 FP8 矩阵乘才吃得满算力红利。看框架日志里是否走低精度 Tensor Core或者直接看实际吞吐变化就能判断出属于哪种情况。4.3 校准与评估从量化噪音里挑出敏感层如果要做静态量化尤其是激活量化一定要准备校准数据集。校准集的选取原则是跟线上真实数据分布尽量一致不要拿训练集里的通用文本硬套。我习惯的做法是从验证集中切出一段长度覆盖几百到几千 token保证模型能看到不同上下文长度下的激活分布。校准后要做逐层敏感度分析。方法很简单先把所有目标层全部量化然后逐个把某一层回退到 FP16对比 PPL 变化。哪一层回退后 PPL 降幅明显就说明这一层是敏感层最终方案里让它留在高精度。这个过程叫 layer-wise 回退在 TensorRT-LLM 这类工具里已经支持自动做正则化不够时人工介入也很有必要。评估阶段不要只盯汇总指标。我会额外固定几十个提示词让原始 FP16 模型和量化模型分别生成同样长度的输出肉眼对比一遍。很多量化问题PPL 看着还行但具体输出里会出现事实错乱、重复循环这些只有看生成文本才能发现。4.4 线上部署时怎么确认收益量化做完不代表结束真正的检验是上线环境。上线前我在压测环境跑三件事固定 batch 大小的并发压测看 TTFT 和 TPOT 的分布变化监控显存峰值看量化后到底省了多少随机抽检一批线上请求日志跟量化前的老版本做结果对比。这里要特别提醒一个容易误判的点显存省了不代表延迟一定降。如果某个算子在当前框架里没有 FP8 实现运行时会被悄悄回退到高精度那么计算时间可能没变甚至变慢。所以压测时不要只看平均延迟要留意是不是有异常的长尾请求它们往往是算子回退或显存换页的信号。另外上线部署时我会故意把量化版本和原始版本同时保留一段时间灰度切流量。因为量化误差在小样本评测里很难暴露真实用户的长尾 prompt 才会暴露出各种边界情况。灰度至少跑一天观察报错率、重复率、上下文召回质量这些偏“体验向”的指标再决定全量切换。5. 量化踩坑实录那些文档不会写的问题5.1 常见问题速查表这一路做下来遇到的典型问题我整理成了一张速查表方便你出了问题直接对着查。症状可能原因处理方式输出乱码或开始胡说八道激活量化时 scale 选错离群值污染了分布改用动态激活量化或对激活做 clippingPPL 上升超过 5%权重量化引入误差过大个别敏感层被压太低逐层回退敏感层保留 FP16显存降了但延迟没变算子没有 FP8/Tensor Core 实现被回退到高精度检查算子支持列表换成 CUDA Graph 或换框架上下文一长生成质量就崩KV Cache 量化误差随序列长度累积换 per-block 缩放或 KV Cache 暂回 INT8量化后部分 prompt 明显变差校准集与线上数据分布不一致重新选校准集覆盖长尾场景FP8 权重加载报错显卡架构不支持 FP8或框架版本过旧确认 compute capability 8.9/9.0升级框架多卡显存不均衡低精度权重下 KV Cache 占比升高负载调度不均调整 KV Cache 分页策略或显存比例5.2 我自己的实操心得量化这件事我的最大体会是不要追求一次性把所有东西都降到最低精度。稳妥的路径是先量化权重保持激活和 KV Cache 高精度跑通评估再尝试 KV Cache 量化最后才碰激活量化。每一步都留一个可回退的开关出问题就知道是哪一层引入的。另一个容易被忽略的细节是量化之后一定要保留一组原始 FP16 模型的输出作为“基准答案”。后面所有版本的对比都以这一组输出为准去看 diff。长期迭代下来你会发现很多看起来像是量化引出的问题其实可能是模型本身在长文本下的不稳定没有基准答案很难定位。还有一个操作习惯监控量化后的数值饱和度。做一个简单的统计看量化张量里有百分之多少的数值顶到了缩放范围的上限。饱和度长期偏高说明 scale 给太小了数值正在被截断饱和度偏低说明 scale 给太大有效数值停留在低档位精度在浪费。这个指标比单纯看 PPL 更能提前预警问题。5.3 一点后续扩展的方向FP8 只是当前比较顺手的方案不代表它是终点。INT4 和更低比特的量化在显存压力更大的场景下越来越流行而像 AWQ、GPTQ 这类量化感知方法在精度和性能之间还在不断找更优平衡点。我的建议是先把 FP8 这条路走通把缩放因子、校准、评估这一整套方法论内化下来再碰到新的精度格式时你只需要换一套映射规则思路是通用的。我目前做推理部署的默认策略是权重优先量化KV Cache 看上下文压力决定激活保持高精度留好回退开关。这套组合在多个模型上跑下来质量和性能的平衡都比较稳。如果你也在折腾类似的东西建议从一个小模型开始验证流程别一上来就挑战 70B流程顺手了再放大也不迟。