刚开始接手 DeepSeek V4 950 的推理优化时,我犯过一个典型错误——把注意力全放在权重矩阵的低精度转换上。FP8 权重、INT8 激活,一套组合拳打下去,显存确实降了,但端到端吞吐几乎没有变化,甚至在某些 batch 下延迟还涨了。真正的问题不在权重算得多快,而在推理链路里张量怎么流动:哪些数据在反复搬运、哪里在无意义地做格式转换、哪些算子因为低精度反而引入了额外的内存访问。这篇就完整复盘一下我们在 DeepSeek V4 950 上做整网统一低精度数据流优化的全过程,包括方案设计、实测数据、精度保卫战和踩过的一个重要性能坑,希望能给正在啃大模型推理优化的朋友一些参考。
DeepSeek V4 950 这个规模的模型(950B 级总参数,激活参数约 35B,MoE 架构),推理瓶颈早就不在 compute 上了,而在数据搬运。这不是理论推断,是压测后的直接感受:把 FP16 换成 FP8 之后,理论算力供给远过剩,可显存带宽、L2 缓存命中率、跨卡通信量却成了限制吞吐的硬约束。低精度优化的核心目标,必须从“减少 FLOPs”变成“减少字节流动”。
1. 为什么大模型推理的性能瓶颈出在“数据流”上
1.1 算力与搬运的失衡
任何算力系统都有一个计算强度(arithmetic intensity)的概念——每个字节从显存搬到计算单元后,能支撑多少次浮点运算。以 H800 SXM 为例,FP8 稠密算力大约是 1500-2000 TFLOPS 级别,而 HBM 带宽在 3.35TB/s 左右。算下来,哪怕要把算力全部喂饱,每个字节的数据至少也要支撑几百次 FLOPs。
大模型推理的尴尬在于:权重虽然足够大,但激活值、KV cache、中间张量才是真正频繁读写的对象。DeepSeek V4 950 在 decode 阶段每个 token 虽然只需要读一部分专家权重(MoE 激活参数约 35B),但 KV cache 和激活值却要反复经过桥接层、attention 层和多个专家网络。数据搬运量不会因为你把单个算子里的矩阵乘法算得快就减少,它取决于整条链路上有多少字节从 HBM 拉进拉出。
很多“权重量化方案”只看权重体积,声称能减少 X% 的显存和带宽,但真正压测时收益远低于预期,就是因为中间激活、KV cache 和残差流的搬运仍然以 FP16/FP32 为主。你把矩阵乘法的输入改成低精度,又把输出立即转回高精度,等于只在一个很窄的窗口里省了带宽,整体流动还是老样子。
1.2 传统逐算子低精度的三个致命伤
第一层问题,是层间量化-反量化切换。逐算子做低精度时,每个 Linear 之前需要做 FP32 scale 计算,矩阵乘完又需要 dequant 回高精度,给下一个算子用。一次在线推理链路里可能有几百个这样的切换点,每一步都产生额外的 HBM 读写。看起来单个算子省了 50% 的带宽,实际上省下的全被 quant/dequant kernel 的额外读写吃回去了。
第二层问题,是布局不统一。不同 kernel 对 FP8 等低精度数据的排布要求完全不一样:有的要求 per-channel scale,有的要求 per-block scale,有的数据需要先做 NC 到 NK 的 layout 转换。这些转换 kernel 在 profile 里经常会被归到“others”类别,容易被忽略,但它们的代价是实打实的显存读写开销。
第三层问题,是精度失配。不是所有计算都对低精度免疫。残差连接、注意力 logits、MoE router 的 logits 都是精度敏感点,一旦被无差别地压到低比特,模型输出质量立刻崩。传统方案只能靠“人工找敏感层、单独保高精度”来补救,但这种补丁式做法又会让数据流在高低精度之间来回切换,前面说的切换开销全回来了。
1.3 整网统一低精度数据流是什么
一句话解释:不是每个算子各自决定用什么精度,而是全链路共用一套低精度主格式、一套缩放因子体系和一套张量内存布局。算子的输入直接吃上一个算子的低精度输出,输出也保持低精度继续往下传,中间不落地到高精度,不重复计算 scale,不做布局变换。
这个理念和 CPU/GPU 上的“数据流处理器”设计有些类似——把注意力从“计算单元利用率”转移到“数据移动效率”。对 DeepSeek V4 950 这种 MoE 大模型来说,统一数据流能带来的收益是结构性的:每个 token 跨越多层、多个专家,张量要被重复读很多遍,只要每遍搬运的字节数都能降下来,端到端的吞吐提升就会非常可观。
2. 核心设计:统一张量布局、缩放因子流与算子融合
2.1 量化格式选型:为什么选块级缩放
低精度格式的选型直接影响精度和硬件效率的平衡。当前可选路线无非三种:E4M3 的 FP8、INT8 加 per-tensor scale、以及 MXFP8 风格的块级缩放。FP8 E4M3 动态范围和精度都能兼顾,但它的精度是“静态”的——整层一个 scale,一旦某层激活值的数值分布起伏较大,就容易出现溢出或者精度损失。INT8 更极端,动态范围完全依赖 scale,激活值里只要出现一个异常大的离群点,per-tensor scale 会被拉得很大,其他正常数值的有效位宽就会骤降。
我们在 DeepSeek V4 950 上最终采用的是“类 MXFP8 块级缩放”方案:每 32 个元素共享一个 8-bit scale,配合 E4M3 尾数。这样既保留了 FP8 的低比特优势,又不会因为某个离群值拉崩整个张量的精度。
选择块级缩放的直接原因是实测数据:per-tensor FP8 在做激活量化时,95 分位误差比块级缩放高出一个数量级,而且这种误差会在多层之间累积,最后表现为下游任务的精度不可控。块级缩放相当于给数据流加了一层“局部自适应的保险”。
2.2 统一缩放因子流
在传统做法里,每一个量化算子都会根据输入张量重新计算 scale,这样做的好处是每层都能自适应,坏处是层与层之间数值范围“各自为政”,数据流中反复出现大的动态范围变化。
我们的做法是设计了一条“主 scale 流”:从网络入口开始,通过校准集统计出一条全局激活 scale 曲线,沿主干网络传递。Attention 内部的 logits 和 softmax 路径单独使用一组 scale,因为这里的数值范围与注意力权重直接相关,容易波动;而 FFN/专家层的输入输出统一走主 scale。这样设计的目的很简单——避免每一层重新统计 scale 带来的计算开销,同时减少层间数值范围突变带来的精度损失。
这里要特别说明:所谓“统一”并不意味着全模型一个 scale。那太粗暴了。实际落地是“分段统一”:每个 Stage(比如前 N 层)内部共享同一套 scale,Stage 边界允许做一次尺度切换。这个粒度既能减少切换点,又不至于因为动态范围差距太大导致精度劣化。
2.3 算子融合:把量化-反量化节点彻底干掉
统一数据流要想真正减少字节流动,就必须在计算图层面把 quant/dequant 节点尽可能消除。我们重点做了三类融合:
RMSNorm 与量化融合。原本 RMSNorm 输出 FP32,然后量化成 FP8 送给下一个 Linear。融合后 RMSNorm 直接在寄存器里完成 normalization,并以 FP8 格式写回,避免了 FP32 中间张量在 HBM 上落盘再被读回的过程。
GELU/SwiGLU 与量化融合。FFN 里 Gate 分支的激活函数输出后立即进入另一个 Linear,这个“激活输出->量化->矩阵乘”的路径也是显存搬运大户。融合后激活函数计算结果直接量化并保持低精度,省掉一次整张激活的写回与重读。
FlashAttention 输出直接低精度化。Attention 输出不再先写回 FP16 再做后面的 Linear,而是在 kernel 内部完成 low-precision 转换,直接以 FP8 块格式输出。这一步对 decode 阶段尤其有效,因为单 token 的 attention 输出虽然不大,但乘以 batch 数和层数后,累计的搬运量非常可观。
融合后的数据流可以简化成这样的链路:输入 token -> RMSNorm(FP8) -> Linear(FP8) -> 低精度残差更新 -> Attention(FP8 输入,FP32 logits 内部计算) -> 输出 FP8 -> MoE 路由(高精度 logits) -> 专家 FFN(全 FP8)。整个主干上几乎没有 FP16 中间张量。
2.4 张量布局统一带来的额外收益
低精度数据流的另一个关键点是张量排布。不同硬件后端对 FP8 张量的内存对齐要求不同,我们统一使用 128 字节对齐的 NC 布局,并让所有相关 kernel 遵循同一约定,从而彻底消除了 kernel 间的 layout 转换。
省下的开销比想象中大。最初做 PP(pipeline profile)时发现,layout 转换 kernel 的耗时能占到总 kernel 时间的 5% 到 8%。这还不算它引发的 L2 cache miss——一旦数据排布不连续,后续算子读数据时的缓存命中率就会显著下降,这部分隐藏开销在端到端性能里很难定位,但体感非常明显。
3. DeepSeek V4 950 落地的实测数据与性能瓶颈排查
3.1 测试环境与基线设置
压测环境是 8 卡 H800 80GB 节点,模型配置为 DeepSeek V4 950(950B 总参,MoE 架构,256 个路由专家,8 个共享专家),使用 Tensor Parallel=8 + Expert Parallel=8(TP 和 EP 重叠挂在同一组卡上)。输入输出长度混合分布:平均输入 2048 token,平均输出 512 token,batch 大小动态调整,目标吞吐 4800 token/s。
基线是 FP16 全精度推理,使用 vLLM 框架,FlashAttention 开启,KV cache 使用 FP16。这一基线配置下的显存占用、prefill 吞吐、decode 吞吐和 TTFT 记录如下表(表中数据是我们环境的实际测量值,不同集群可能会有差异):
| 指标 | FP16 基线 | 统一低精度数据流 | 变化 |
|---|---|---|---|
| 显存占用(峰值) | 约 620GB | 约 415GB | 降低约 33% |
| Prefill 吞吐(token/s) | 约 5200 | 约 7800 | 提升约 50% |
| Decode 吞吐(token/s) | 约 3100 | 约 4700 | 提升约 52% |
| 端到端 TTFT(ms) | 约 320 | 约 260 | 降低约 19% |
| 单 token 平均延迟(ms/token) | 约 1.45 | 约 1.02 | 降低约 30% |
| Calibration 后精度偏差(下游平均) | — | < 0.8% | 可接受 |
表格里的数据不是一次跑出来的,前后调了两周才稳定。注意 decode 吞吐的提升比 prefill 更明显,原因在于 decode 阶段算子更小、更密集,瓶颈更加偏向带宽和搬运。低精度数据流正好打在这个痛点上。
3.2 一个“假阳性”性能坑:显存降了,延迟反而涨了
这是整个过程里最有价值的一个排查案例,值得单独写出来。
现象:第一版统一数据流实现跑通后,显存占用确实降了,但单 token decode 延迟从基线的 1.45ms/token 反而涨到了 1.6ms/token。预想中的性能提升完全没出现,当时的第一反应是量化精度有问题,模型在疯狂重试或者数值异常导致 kernel 效率降低。
排查过程是这样:
先用nsys profile抓 kernel 时间占比,发现耗时的前三名里出现了一个奇怪的算子——Gather。它的耗时占比高达 18%,而在基线版本里它几乎可以忽略不计。直觉告诉我,这不是真实的计算需求变大,而是低精度张量的内存布局出了问题。
进一步看数据,发现 MoE 路由之后,token 会被分派给不同的专家,这本身就需要Gather操作。但在 FP16 基线里,token 的 hidden state 是连续存储的,Gather 可以在 L2 缓存里完成大部分工作。而统一低精度后,为了满足矩阵乘对 128 字节对齐的要求,每个专家收到的 token 被单独存成一块连续内存,导致 Gather 从“大块连续搬”变成了“大量碎片搬”,L2 cache 命中率骤降,大量请求直接打到 HBM 上。
定位到根因后,修复方式是在 MoE 分发前先把 tokens 按专家 ID 排序,确保同一个专家的 token 在内存中是连续存放的。这个排序本身也会引入开销,但与省下的 HBM 访问相比非常划算。修复后 decode 延迟直接从 1.6ms/token 降到 1.02ms/token,Gather 的 kernel 时间占比也从 18% 降到了 3% 以内。
这个坑给我们的教训是:低精度优化的收益不是自动兑现的,数据排布必须跟着精度一起改,否则“省带宽”会变成“制造缓存 miss”。
3.3 长序列场景下的 KV Cache 低精度策略
DeepSeek V4 950 在长上下文场景下会面临很大的 KV cache 显存压力。32K 上下文、大 batch 时,KV cache 占用的显存甚至能超过模型权重本身。低精度化 KV cache 是绕不开的。
我们的做法是:KV cache 的 K 和 V 分别使用不同的量化策略。K 矩阵用 FP8 E4M3 加 per-head 缩放,V 矩阵用块级缩放 FP8。原因很简单——K 在 attention 计算中参与点积,对动态范围更敏感;V 只是被加权求和,对精度的容忍度略高。更激进的做法是 V 直接压到 INT4,但实测在 950 上会导致下游任务出现约 1.5% 的精度损失,最终没有采用。
KV cache 低精度化加上统一数据流之后,长上下文场景的 decode batch 可以扩大约 40%,这意味着同样的 GPU 资源能服务更多并发请求。但这里有个隐性风险:如果序列长度超过某个阈值,量化误差会在长距离 attention 中累积,模型可能出现重复生成或上下文遗忘。后文会讲到如何通过层粒度策略缓解。
4. 精度保卫战:哪些层必须留在高精度
4.1 敏感层定位:定量分析替代拍脑袋
低精度优化最怕“全模型量化后才知道哪里崩了”。我们的方法是先做敏感性分析,再决定每一层能不能量化。
具体做法分三步:第一步,用与线上分布一致的数据构建校准集,大概 2000 条样本;第二步,逐模块替换为低精度实现,其余部分保持高精度,跑完校准集记录困惑度(PPL)和一组下游任务指标;第三步,把替换后相比基线劣化超过阈值的模块标记为“敏感模块”。
在 950 上跑下来的敏感性排序(由高到低)大致是:Attention 的 QKV 投影输入路径、MoE Router 的 logits 计算、残差流主干上的部分全连接层、顶层分类头、中间层 FFN 的 Gate 分支。前两者是几乎不能碰的雷区,后几者可以按规模选择性地低精度化。
4.2 注意力路径的高精度旁路设计
Attention 路径是精度保卫战的重中之重。点积结果一旦被量化,softmax 后的分布很容易失真——本来接近 one-hot 的注意力分布变得平滑,模型就会“注意力涣散”。
我们的设计是:Attention 的 Q/K 点积计算内部保留 FP32 累加;softmax 的输入和输出保持 FP32;但 Attention 的最终输出会以 FP8 写回,供后续线性层使用。这里其实是通过“高精度内部计算 + 低精度对外输出”的方式,既保证了注意力分布的准确性,又不破坏统一数据流的连续性。
如果整个 attention 都用高精度,那数据流在这里就要经历一次“低->高->低”的切换,前面强调的统一性就破了。高精度旁路只承担 attention 内部的计算,不改变主干数据流的精度连续性。
4.3 MoE Router 高精度保留的必要性
很多人会低估 MoE Router 对精度的敏感度。Router 的 logits 虽然只有很小的计算量,但它决定了 token 发往哪些专家。一旦 logits 被量化误差污染,路由结果可能发生剧烈变化——某个 token 被错误地发到一个不擅长的专家,整个 decode 质量都会波动。
在统一数据流方案里,MoE Router 是我们刻意保留下来的少数 FP32 计算路径。它不是模型主干上的瓶颈(计算量小),所以保留 FP32 不会对性能产生明显负面影响。但它的收益很大:路由稳定性直接关系到多点采样、长对话一致性等场景的实际体验。我们的原则是,计算量小的组件不要为了“统一”而强行压低精度,该高就高,否则捡了芝麻丢了西瓜。
4.4 动态切换机制:敏感场景自动保精度
静态固定哪些层用高精度、哪些层用低精度,只能应对“平均情况”。实际使用中,代码生成、数学推理、长文档问答等场景对数值精度的敏感度完全不同。一个在通用对话上精度无损的量化方案,可能在数学推理上直接崩掉。
我们为关键层设计了一个动态切换开关。系统运行时,根据输入类型检测器判断当前请求的场景,如果命中高敏感场景,就把敏感模块动态切换回高精度实现。这个开关不是模型热加载,而是预编译了同一份计算图的高/低精度两个版本,运行时通过 kernel 选择器完成切换,切换代价小于 5 毫秒,对在线服务影响可以忽略。
这个方法也有代价——需要同时维护两套 kernel 实现,而且显存占用会略有上升。但对于一个需要同时服务多种业务的生产系统来说,精度可用性比那一点显存更重要。实际线上运行后,高敏感场景的精度回退率从 3.2% 降到了 1% 以内。
5. 可复现的执行清单与关键参数配置建议
5.1 从校准到上线的完整工程链路
如果要在自己的模型上复现这套方案,建议严格按下面的顺序走,不要跳步:
第一步,数据流分析。先用nsys或ncu跑一遍基线推理,输出 kernel 耗时占比、HBM 读写量、L2 命中率。这一步的目的不是找“哪个算子慢”,而是找出“哪些张量在 HBM 上反复读写”。只有数据流看得清楚,后面的低精度优化才不会走偏。
第二步,离线仿真。在实际改代码之前,先写一个量化仿真脚本,把权重和激活值在 Python 层模拟成 FP8 块级缩放格式,测一下端到端精度损失。这个仿真不一定完全准确,但能快速筛掉明显不可行的方案。
第三步,逐模块替换。按照敏感性分析的结果,从最不敏感的位置开始替换为低精度实现,每替换一个模块就做一次精度回测。如果某一步 PPL 出现不可逆上升,立即回退,改用更保守的量化策略。
第四步,整网统一。所有模块都完成低精度替换后,再统一 scale 体系、统一内存布局、消除切换点。这一步是性能提升的大头,也是工程上最容易反复的地方。
第五步,在线灰度。先切 5% 流量,观察下游指标,确认无异常后再逐步放大到全量。
5.2 FlashAttention 参数调优与低精度的配合
大模型推理基本绕不开 FlashAttention,我们在 DeepSeek V4 950 上对相关参数做了专门调优,这几个值和低精度数据流配合得好不好,直接影响最终性能。
block_size参数影响 attention 计算分块的大小。低精度数据流下,建议把 block_size 适当调大,比如从 64 提升到 128。原因是低精度数据每个块读入的数据量减半,同样大小的 block 只需要一半的 HBM 带宽,分块大一点可以摊薄块间同步的开销。但 block_size 不能无限大,否则 L2 cache 放不下,反而导致 cache miss 上升。我们实测在 128 到 256 之间达到平衡,最终取 128。
window_size要结合具体场景调。窗口注意力(sliding window attention)在长上下文场景下可以显著减少 KV cache 读取量,但如果窗口太小,模型会丢失远期信息,在低精度叠加下更加明显。我们在线上的经验是:32K 上下文场景 window_size 设在 4096 到 8192 之间比较稳;再小就会出现长程依赖下降的问题。
softmax_scale不要随意改。它本质上是 attention logits 的缩放系数,直接关系 softmax 的数值范围。低精度数据流里,如果softmax_scale设置不合适,logits 很容易在低精度下溢出或下溢出。我们的建议是让softmax_scale保持与高精度基线一致,不要为了让 logits 落在 FP8 最佳数值区间而强行调整它,因为这会改变注意力分布的语义。
如果后端支持local/global attention混合策略,建议把低精度 KV cache 主要用在 local 部分,global 部分保持相对高精度(FP8 E4M3 且不做进一步压缩),这样能兼顾长程依赖和显存占用。
5.3 常见问题对照表
这里整理了我们在落地和线上运行中遇到的几个典型问题,都附了根因和解决方向:
| 问题现象 | 可能根因 | 解决方向 |
|---|---|---|
| PPL 在量化后飙升 | 激活值有极端离群点,块级缩放的块大小不合适 | 缩小块大小(如从 32 降到 16),或对离群维度单独处理 |
| 长文本生成开始重复 | KV cache 低精度导致长程注意力信息丢失 | global 部分保留高精度 KV,或提高 V 的量化精度 |
| Decode 延迟不降反升 | MoE 路由后张量分散,Gather 成为瓶颈 | 先按 expert 排序 token,再让同一专家数据连续存放 |
| 吞吐提升但显存没下降多少 | 中间激活仍以高精度落盘 | 检查算子融合覆盖范围,重点看 Norm 和激活函数后的量化点 |
| 量化后模型在数学任务上崩 | 数学任务对某些 logits 精度敏感,敏感层需要保留高精度 | 用动态切换机制,在高敏感场景下切回高精度实现 |
5.4 几点个人体会
这个项目做下来,我最大的体会是:低精度优化不是“权重压成 FP8”这么简单,它是一个数据流工程。模型越大,数据搬运问题越突出,低精度统一数据流的收益就越明显。但收益不会自动落袋,需要配套的布局统一、算子融合和敏感性分析一起做,才能把理论带宽节省变成实打实的吞吐提升。
另一个心得是预热和校准集的质量非常关键。校准集如果和线上分布不一致,量化 scale 会在上线后“暴露真面目”。我们第一次上线就是因为校准集过度集中在对话样本,导致代码场景下表现不佳。后来从线上抽了多场景混合样本重新校准,问题才解决。如果环境允许,可以考虑做在线 scale 动态微调,但那就涉及更复杂的运行时系统和 kernel 设计,不是所有团队都有必要做到这一步。
最后想说的是,DeepSeek V4 950 这次实践的可复制性其实很高——统一的思路、敏感性分析方法、量化选型和闪存参数调优,都是可以直接迁移到其他 MoE 大模型上的。只要肯在数据流分析和敏感层定位上多花时间,大部分推理性能瓶颈都能被系统地拆解掉。