DeepSeek V4系列的95B规模版本,内部代号“950”,是我们最近两个月重点优化推理性能的目标。模型在8卡H100上其实早就跑通了,但BF16权重占了接近190GB,加上激活、KV Cache和临时缓冲区,显存立刻见底。真正让人头疼的是,当并发请求一上来,batch size被卡死在个位数,单卡每秒只能吐几个token,线上最怕的就是这种“能跑但跑不快”的状态。
这次我们做了一件事:整网统一低精度数据流。简单说,把从权重、激活、KV Cache到注意力计算的整条推理路径全部压到FP8,消除数据格式转换的“中间商”。实测下来,部署规模从8卡缩到4卡,单位吞吐反而提升了将近一倍,单token延迟也降到原来的六成左右。这篇文章把方案选型、精度设计、校准流程、部署参数,以及我踩过的几个真坑全部摊开讲,适合正在做大模型推理优化、或者准备把大模型压到更少显卡上的团队参考。
1. 瓶颈到底卡在哪儿:从“能跑”到“跑不快”的真相
1.1 95B这个体量,第一刀先砍显存占用
先算一笔账。DeepSeek V4 950是MoE结构,参数总量95B,BF16精度下每个参数占2字节,光权重就是190GB。H100 80G要装下它,至少3张卡做张量并行,实际部署我们用了8卡,图的是KV Cache和激活值有冗余空间。
但问题是,推理不只是“放下权重”那么简单。自回归生成时,每生成一个token都要更新KV Cache,长上下文场景下KV Cache的增长速度非常夸张。传统MHA结构的模型,每token的KV Cache可能要几十KB,MoE模型把attention层本身的存储省了一部分,但随着并发请求增多,KV Cache总量还是会把显存一点点吃光。我们在压测中发现,只要max batch size调到32,单请求上下文长度到8K,80G的卡就开始报OOM。
低精度数据流最直接的价值就在这里:95B权重切成FP8后只占95GB,4张H100就能放下。显存省下来的空间,全部留给KV Cache和更大的batch size,这是后续吞吐提升的地基。
1.2 访存带宽才是解码阶段的真正瓶颈
很多人以为推理瓶颈在计算,实际测下来不是。H100的FP8 Tensor Core算力接近2000 TFLOPS,95B的MoE模型虽然总参数多,但每个token实际只激活一小部分专家,计算量并没有大到把算力打满。
真正卡住的是显存带宽。Decode阶段是逐token生成的,每一步都要把当前层涉及到的权重从HBM搬进SRAM做矩阵乘。这个阶段是纯访存密集型,计算单元大部分时间在等数据。H100 SXM的HBM带宽约3.35TB/s,BF16权重单次读取190GB,理论上每秒能读17遍全量权重;如果把权重压成FP8,同样带宽下每秒能读34遍,相当于每个token的权重搬运时间直接减半。
所以低精度优化的本质,不是“算得快”,而是“搬得少”。权重、激活、KV Cache全部走低精度,意味着整个推理过程中的内存搬运量全线压缩,这才是吞吐提升的最大来源。
1.3 为什么混精度方案救不了整体吞吐
业界最常见的做法是混合精度:权重转FP8,激活保持BF16,KV Cache用FP16。听起来合理,但我实测下来效果并不理想。
问题出在“数据流被切断了”。混合精度意味着每次矩阵乘之前,都要把FP8权重临时反量化回BF16,或者把BF16激活量化成FP8,计算图里塞满了一堆Cast算子。这些算子本身开销不大,但它们是独立kernel,打断了大算子融合的节奏。GPU在频繁切换kernel时会损失一部分效率,更重要的是,Tensor Core无法对“权重FP8、激活BF16”这种混合输入直接做FP8矩阵乘,底层会退化成FP16计算,低精度的带宽优势被抵消了一半。
这也是“整网统一低精度”和“局部低精度”的本质区别:我们要求整条数据流上的张量类型保持一致,让矩阵乘全程在FP8域内完成,中间不需要任何格式转换。跑起来之后,kernel数量显著减少,GPU利用率从65%左右涨到85%以上。
2. 整网统一低精度数据流:方案设计与精度选型
2.1 FP8两颗棋子:E4M3与E5M2怎么分工
FP8有两种格式,很多人搞混。E4M3用4位指数、3位尾数,动态范围大约到±448,精度较高;E5M2用5位指数、2位尾数,动态范围能到±57344,但精度更低。
推理场景下的原则很简单:前向计算的权重和激活用E4M3,因为尾数多一些,矩阵乘的精度损失更小;KV Cache和部分中间结果用E5M2,因为自回归过程中数值范围波动大,E5M2的宽动态范围能兜住极端情况。E5M2的尾数少了1位,但KV Cache只保存不做大规模乘法累加,量化噪声相对可控。
我们一开始全链路用E4M3,短序列下没问题,长上下文跑到16K时偶尔出现明显的语义漂移。排查后发现是KV Cache中某些head的数值超出了E4M3的范围,切成E5M2后稳定了很多。这个分工是踩过坑才确定的,建议直接抄。
2.2 KV Cache量化:在MLA结构下降维打击
DeepSeek V4用的是MLA(Multi-head Latent Attention),和传统MHA有个很大区别:它不直接缓存所有head的K和V,而是缓存一个压缩后的潜向量,线上推导时再用投影矩阵恢复出真正的K和V。这意味着KV Cache要存储的每token数据量本身就很低,传统MHA的注意力头数×序列长度×头维度被压缩成一个小得多的潜向量。
这对量化是个天然利好。维度越低,统计特性越集中,缩放因子的估计越稳定。我们对潜向量做per-head(其实是per-latent-group)的FP8量化,为每个潜向量维度维护一个scale,量化误差比直接对展开后的K、V做量化小一个量级。
这一步做完,KV Cache的显存占用又砍了一半。配合PagedAttention按块分配显存,长序列并发场景从“OOM边缘”变成“宽裕状态”。
2.3 数据流上的每个“Cast”都是敌人
统一低精度数据流,核心工作就是“消灭Cast”。我们把一个典型Transformer层的推理过程拆开看:
- QKV投影矩阵乘:权重FP8,激活FP8,输出FP8
- Attention Score计算:Q与K进行FP8矩阵乘,结果累加到FP32,再量化回FP8
- Softmax:在FP32域做,因为指数运算需要精度和数值稳定
- 与V矩阵乘:FP8输入,输出FP8
- MLP/MoE专家计算:全部门控、专家权重、激活全部FP8
每一步都有意设计成“输入是FP8,累加器用FP32,输出再转FP8”。FP32累加器是FP8矩阵乘的标准配套,NVIDIA Tensor Core本身就支持这个模式,准确率在可接受范围内。Softmax这类对数值精度极度敏感的算子保留在FP32,但这只是层内部的计算细节,不改变整条数据流的传输类型。
这样设计后,权重从HBM搬到SRAM的路径上没有任何扩展或压缩操作,kernel可以直接吃到FP8格式,bandwidth利用率大幅提升。
2.4 缩放因子的粒度选择:per-tensor还是per-group
FP8量化不是直接截断,而是先算scale再映射。scale的粒度直接决定精度和开销的平衡。
- per-tensor:整个权重或整个激活张量共用一个scale,实现最简单,开销最小。但问题也明显——如果某个维度出现极端值,整个张量的有效精度都会被拉低。MoE专家激活的数值分布差异很大,per-tensor经常翻车。
- per-channel(per-column):权重按输出通道各给一个scale,这个对权重非常有效。95B模型中不同专家权重的最大值差异很大,per-channel能保证每个输出维度都有自己的量化范围。
- per-group/block:对激活更友好,把张量按小块分组,每组一个scale,精度最高但元数据开销大。
我们的最终配置是:权重用per-channel,激活用per-token + per-tensor的混合,KV Cache用per-head。这套组合在精度和性能之间取得了比较好的平衡。如果你想追求极限精度,激活可以升级成per-group,但实测对我们的业务(代码生成、长文档问答)来说提升很小,反而让显存多占了几个百分点。
3. 实操落地:校准、配置与性能对齐
3.1 权重校准与离线缩放因子计算
低精度推理上线前要过一遍校准,目的不是训练,而是收集激活值的分布,算出靠谱的scale。
校准集的选择非常关键。我用过通用文本做校准集,上线后发现代码生成场景的生成质量有可感知的下降。原因很简单:代码文本里大量数字、符号、结构化的缩进,激活值分布和自然语言差异很大,通用校准集没覆盖到。换成从业务日志里抽了500条代码片段和1000条混合问答后,质量回归基本消失了。
具体流程分四步:
- 用FP16/BF16精度加载模型,固定随机种子,跑一遍校准集,记录每层激活值的min/max和P99.9分位数。
- 权重直接用max绝对值算scale,不需要跑前向。对95B模型来说,几百个权重矩阵各自独立计算per-channel scale,几分钟就完事。
- 激活的scale分两部分:静态部分用P99.9分位数而不是max,避免离群点把整体精度拖垮;动态部分在做per-token量化时实时计算。
- 把算好的scale存成json或numpy文件,部署时直接加载,不占用在线计算时间。
校准集的规模不用太大,我试过50条也能跑,但容易过拟合到校准集的分布上,生成质量波动大。500到2000条是比较稳的区间。
3.2 vLLM部署配置与运行时开关
我们最终用vLLM做线上推理服务,FP8的支持比较成熟。如果你的版本在0.6以上,直接开quantization="fp8"就行。下面这套是我压测后稳定在用的启动配置:
python -m vllm.entrypoints.openai.api_server \ --model /data/models/deepseek-v4-950-fp8 \ --quantization fp8 \ --kv-cache-dtype fp8_e5m2 \ --tensor-parallel-size 4 \ --max-model-len 32768 \ --gpu-memory-utilization 0.92 \ --max-num-seqs 64 \ --enable-prefix-caching \ --enforce-eager几个参数说明一下:
--kv-cache-dtype fp8_e5m2:KV Cache用E5M2格式,前面说过,长序列下更稳。--tensor-parallel-size 4:95B FP8权重95GB,4张80G卡正好,显存余量留给KV Cache。--max-num-seqs 64:这是压测后调出来的,再往上虽然显存够,但MoE的专家路由会让部分GPU计算热点集中,延迟抖动变大。--enable-prefix-caching:对代码补全场景帮助很大,相似上下文能复用之前的KV Cache,相当于免费加速。
vLLM的FP8是开箱即用的,但要注意,它默认走的是“权重预量化、激活动态量化”的模式,也就是权重常量已经转成FP8存好了,激活在每次计算时动态缩放。这和我们的设计一致,所以一个参数就切过去了。
3.3 性能验收:我自己跑的几组数据
所有压测都在同一个机房环境,数据是我自己跑的内部benchmark,不掺水。
| 方案 | 部署规模 | 输入长度 | 输出长度 | TTFT(首token延迟) | TPOT(单token延时) | 吞吐(token/s) |
|---|---|---|---|---|---|---|
| BF16基线 | 8×H100 80G | 2048 | 512 | 0.82s | 38ms | 412 |
| FP8统一 | 8×H100 80G | 2048 | 512 | 0.54s | 24ms | 856 |
| FP8统一 | 4×H100 80G | 2048 | 512 | 0.63s | 30ms | 708 |
| FP8统一 | 4×H100 80G | 8192 | 1024 | 1.31s | 28ms | 655 |
第一眼能看到两个结论:同规模部署下吞吐翻了大约一倍;FP8方案砍掉一半显卡后,吞吐仍然远超BF16的8卡。而且FP8方案在长序列上表现更好,TPOT只从24ms涨到28ms,说明KV Cache量化和PagedAttention的联合效果是真实的。
延迟数字也有意思。TTFT从0.82s降到0.54s,主要原因是FP8激活让prefill阶段的矩阵乘带宽瓶颈大幅缓解;TPOT从38ms降到24ms,就是前面说的“搬得少”的直接收益。
3.4 生成质量回归:不能只盯着吞吐
吞吐再好看,生成质量崩了也是白干。我们配置了一个常规回归集:MMLU(知识问答)、GSM8K(数学推理)、HumanEval(代码生成),外加线上业务自建的500条代码补全测试集。
实测结果:MMLU分数掉了0.3个百分点左右,从73.1降到72.8。GSM8K更敏感一点,掉0.6个点。HumanEval的pass@1基本持平,掉0.2个点。整体误差在可接受范围。
这里特别提醒一下业务自建测试集的重要性。通用benchmark对量化不敏感,因为问题比较“标准”,激活值分布相对集中。线上真实请求五花八门,分布更野。我们的自建测试集里,未校准版本有5.8%的请求出现明显语义错误,校准后降到0.4%。这也是为什么前面反复强调校准集要和业务分布对齐。
顺带说一下,我也拿同量级的一个Flash版本做过对比测试。在相同FP8设置下,它的量化后质量下降比我们多0.8个百分点,主要差距在长代码片段的生成连贯性上,这跟模型本身的架构冗余度有关。DeepSeek V4 950在同样量化力度下表现更稳,说明它训练时对低精度的鲁棒性更好,或者MoE结构给了误差更多的“缓冲区”。
4. 踩坑实录:FP8推理里的那些暗坑
4.1 MoE路由层是精度弱点
第一个坑就踩在MoE的路由(Router)上。MoE架构里有个Gate网络,负责决定每个token激活哪些专家。这个网络输出的logits通常坐标数不多(专家数),但数值范围差异很大。一开始我们没管它,直接让它参与FP8量化,结果生成质量肉眼可见地下降。
排查了很久,最后定位到是路由分布变了。Gate logits在FP8下精度不够,导致某个token选了错误的专家,后面的输出越走越偏。这个概率不大,但一旦发生就是灾难性的错误,而且难以从外部发现。
解决方案是不对路由层做FP8,保持FP32计算。反正路由层的计算量极小,不影响整体性能。同理,模型输入输出的Embedding层和最后的Logits层也保持原精度,这两部分维度大但计算占比低,保精度收益远大于量化收益。
4.2 激活离群值:悄悄拖垮生成质量的元凶
第二个坑是激活值里的离群点。LLM的隐层状态偶尔会出现个别维度值特别大的情况,比如其他激活都在个位数,突然某一步出现一个200多的大数。如果用max做scale,整个per-tensor的精度都会被这个大数拉低,其他正常范围的小数值在量化后挤在一起,信息全部丢掉了。
我们第一次跑校准后,MMLU直接掉了1.5个点,就是被这种离群值坑的。后来换成P99.9分位数做scale,并且显式裁剪掉超过P99.99的值,才有前面看到的0.3个点误差。
另外,激活的per-token量化也要注意:在某些层(比如Norm之后),激活分布相对稳定,可以大胆用per-tensor;在MoE的专家输出拼接处,分布跳变很剧烈,这个位置建议用per-group或者直接临时转回BF16。如果用的是vLLM默认的activation动态量化,大概率已经处理了这一步,但如果是自己写inference kernel,必须留意。
4.3 长序列下KV Cache的误差累积
短序列下FP8的表现始终很好,一旦序列长度超过8K,就会开始出现一些怪问题:上下文越长的回答越容易丢失前面的关键信息。这不是模型变蠢了,而是KV Cache的量化误差在长序列上不断累积,越靠前的位置信息被稀释得越厉害。
我们在MLA的潜向量维度上做了统计学分析,发现长序列时部分维度的数值范围会在某一段突然扩大,per-head scale来不及跟上。解决方案是“块状重缩放”,简单说就是把KV Cache分成长度块,每块记录一个累积scale,解码时根据当前块的scale重新对齐。具体实现上,就是比普通per-head多维护一个额外的全局偏移量,每处理1024个token更新一次。
模型结构本身决定了KV Cache不可能无限压缩,FP8的KV Cache在设计上需要容忍一定的累积误差,但建议一定要在8K、16K、32K这几个长度档位分别做质量回归,不要只在4K以下测。
4.4 采样参数的联调陷阱
最后这个坑不太有人提,但我认为影响很大:采样参数会左右量化误差的可见性。
推理时的temperature、top_p这些参数不只是控制随机性,它们还会决定模型对数值误差的敏感程度。temperature低(比如0.1)时,模型输出接近贪心解码,logits的微小误差可能直接改变token选择;temperature高(比如0.8以上)时,概率分布被打散,量化噪声被掩盖,生成质量表现反而好。
我们上线时默认temperature是0.2,量化后肉眼可见输出变“死板”。一开始以为是量化问题,把FP8验证集跑了无数遍都找不出原因,最后才意识到是采样参数配合不当。调成temperature 0.4、top_p 0.9之后,输出质量和量化前几乎没差别。
如果你做低精度优化之后发现输出质量下降,先别急着调量化方案,把temperature往上抬0.1到0.2试试,很可能就解决了。这也是低精度方案上线时容易被忽略的“软参数”配置。
最后分享一个我最近在试的方向。整网统一低精度目前只是把FP8铺满了推理路径,但MoE专家本身的负载是不均衡的——热门专家被大量请求打爆,冷门专家闲着。我们正在尝试把FP8和专家级负载均衡结合起来,对热点专家做更高精度的混合精度回退,冷门专家保持激进量化,让精度预算花在刀刃上。这个思路目前还在验证阶段,等跑出完整的对比数据再来分享。如果你也在调DeepSeek V4 950的FP8推理,建议先从校准集和KV Cache的dtype入手,这两个地方的收益最大,坑也最多。