参考
不同硬件和推理引擎模型输出的精度差异
在不同硬件和推理引擎上,同一个深度学习模型输出存在精度差异是一个常见且正常的现象。这种差异源于多个层面因素的叠加,从底层的硬件算术逻辑到上层的软件优化策略,每一个环节都可能引入微小的数值变化。
CPU vs. GPU 的不同算术逻辑
例如torch.cuda 和 cpu 相同运算,结果不一样
-
一些CPU(如x86架构)在执行浮点计算时,会使用80位的扩展精度寄存器进行中间计算,最后再将结果截断为32位(FP32)或64位(FP64),而GPU则严格按照32位或16位进行计算,这种不同的中间精度会导致最终结果出现微小偏差。
-
并行计算顺序: GPU是高度并行的处理器。浮点数的加法和乘法不满足严格的结合律(即 \((a+b)+c\) 不完全等于\(a+(b+c)\))。在GPU上,一个大规模的求和运算会被拆分成数千个并行任务,其求和顺序是不确定的,每次运行都可能不同,从而导致最终结果的微小波动
1
2
3
4
5(0.1 + 1e20) - 1e20 >>> 0 0.1 + (1e20 - 1e20) >>> 0.1 -
专用计算单元: 现代GPU(如NVIDIA GPU)包含专门用于AI计算的Tensor Cores,而CPU则使用AVX等指令集。这些专用单元在执行矩阵乘法等操作时,为了效率可能会采用与通用计算单元不同的算法或数值表示,从而引入差异
算子(Kernel)实现的差异
即使是实现同一个数学公式,不同硬件和框架的底层算子(Kernel)实现也大相径庭。
- 硬件层面:
- NVIDIA GPU 依赖于其 cuBLAS 和 cuDNN 库。
- 华为 Ascend NPU 依赖于其 CANN (Compute Architecture for Neural Networks) 库。
- 这些库是厂商针对自家硬件深度优化的“黑盒”,其内部的算法选择、分块策略、内存使用方式和精度处理都有差异,导致计算路径不同。
- 框架层面:
- 像 vLLM 和 MindIE 这样的高性能推理框架,为了极致的优化(例如处理 PagedAttention),会实现高度定制化的算子。这些自定义算子的算法逻辑和数值稳定性可能与 PyTorch 的原生实现不同,从而引入差异。
- “批次不变性”的缺失 (Lack of Batch Invariance):
- 这是一个更深层次、但至关重要的原因。理想情况下,对一个样本进行推理的结果,不应该受到同时处理的其他样本(即批次大小)的影响。
- 然而,许多高性能算子(如矩阵乘法)为了在不同批次大小下都达到最优性能,会选择不同的内部算法或分块策略。例如,处理小批量时可能不使用 Tensor Cores,而处理大批量时则会使用。
- 这导致了 模型(输入[0]) 的结果不等于 模型(输入)[0]。
- 在一个推理服务中,服务器的负载是动态变化的,这意味着你的请求被处理时的批次大小是“随机”的。不确定的批次大小 + 缺乏批次不变性的算子 = 用户层面不确定的输出结果。这完美解释了为什么即使在同一台机器上,连续两次相同的请求也可能得到不同的结果。
解码策略的放大效应(Amplification via Decoding)
计算层面产生的微小数值差异,会在语言模型的生成(解码)过程中被急剧放大。
- 贪心解码 (Greedy Decoding, temperature = 0):
- 即使采用确定性的贪心解码,当两个候选 Token 的 logits 值非常接近时(例如 Token_A: 10.001 vs Token_B: 10.002),硬件或算子引入的微小计算误差足以改变它们的排序,导致 argmax 选择了另一个 Token。
- 采样解码 (Sampling, temperature > 0):
- 在采样解码中,微小的 logits 差异会改变整个词表的概率分布。即使排序不变,概率的微小变化也可能导致在随机采样时选出完全不同的 Token。
- 蝴蝶效应: 一旦在某个生成步骤中选择了不同的 Token,这个 Token 就会成为下一步生成的输入,导致后续的整个文本序列走向完全不同的方向,从而产生巨大的差异。
如何系统性地诊断和定位问题
当遇到不同环境导致模型效果差异巨大时,可以按照以下步骤进行排查。
建立一个可控的比较基准
首先要排除掉所有已知变量,确保是在对等的比较。
- 统一模型和精度: 确保使用完全相同的模型权重,并且使用相同的数值精度(例如,统一使用 BF16,避免 FP16 和 BF16 混用)。不使用或使用相同的量化策略。
- 固定解码策略: 采用贪心解码 (temperature = 0, top_p = 1) 来消除解码过程中的随机性。
- 固定随机种子: 为所有相关库(PyTorch, NumPy, etc.)设置固定的随机种子。
- 关闭动态优化: 禁用 Prefix Cache、NTP 等可能影响精度的动态优化选项。
- (可选,用于调试)强制确定性算法: 在 PyTorch 中,可以设置 torch.use_deterministic_algorithms(True)。这会强制 PyTorch 使用确定性的(但通常更慢的)算法实现,有助于定位问题是否源于 cuDNN 的非确定性选择。
- 验证输入: 确保经过 Chat Template 渲染后的输入 Tokens ID 序列是完全一致的。
定位差异来源
-
检查 Logits 分布: 开启 logprobs 选项,获取每个生成 Token 的 logits 或 log probabilities。逐个 Token 对比两个平台的输出,找到第一个出现差异的 Token,并检查其 logits 值的差异程度。
-
进行算子级差异检查: 这是最深入的调试方法。
-
修改模型的前向传播函数,逐层(或逐个算子)保存其输出的隐藏状态(hidden states)。
- 在两个平台上分别运行并保存这些张量。
-
计算每层输出张量的相对误差 $$( A - B ₂ / A ₂)$$ 或余弦相似度。 - 定位误差突然增大的层。例如,如果前几层的余弦相似度都是 0.99999,但某一层突然降到了 0.99,那么问题很可能出在这一层或其紧邻的上一层。
-
进行统计学意义的评估
在生产环境中,我们更关心模型的宏观表现而非微观的数值一致性。
- 使用评测框架: 利用 OpenCompass、lm-eval-harness 等框架,在标准的学术评测集(如 GSM8k, MMLU, C-Eval)上运行模型。
- 分析结果: 比较两个平台上的评测分数。一般来说,如果核心指标(如准确率)的差异在 3% - 5% 以内,通常可以认为是可接受的范围。如果差异过大,则说明存在显著的性能衰退,需要深入排查。
解决方案与延伸思考
- 调试时: 严格遵循上述诊断步骤,特别是使用贪心解码和确定性算法来定位问题。
- 生产中:
- 接受并评估: 认识到一定程度的差异是正常的性能权衡。重点是通过评测框架来确保模型的宏观能力没有显著下降。
- 保持环境一致: 尽量在开发、测试和生产环境中使用相同的硬件、驱动和软件栈。
- 重新进行提示词工程: 当切换到新的硬件或推理框架后,模型的“偏好”可能发生微小变化。重新进行一轮提示词(Prompt)调优可能是必要的。
延伸问题
- 量化后的模型一定比量化前差吗?
- 不一定。量化引入的噪声有时可以起到类似 Dropout 的正则化作用,可能打破模型对某些特征的“过拟合”,反而提升其泛化能力,在某些场景下效果会更好。
- 大模型天然就是不稳定的吗?
- 是的。从根本上说,大语言模型是概率模型。在实际应用中,为了生成内容的多样性和创造性,几乎都会使用 temperature > 0 的采样解码,这使其输出天然具有随机性。因此,所有的评测结果都只有统计学意义。
- 效果不一致一定是硬件或算子差异吗?
- 不一定。很多时候,这可能是由推理框架本身的 Bug 引起的。在定位问题时,不能排除软件缺陷的可能性。
flux 量化
背景
flowchart TD
A[线上真实 Flux 任务] --> B{任务参数解析}
B --> C[选择 Base Model]
B --> D[选择 LoRA / 多 LoRA]
B --> E[分辨率 / Steps / Guidance]
B --> F[Prompt / Seed / Scheduler]
C --> G[模型族画像]
D --> G
E --> G
F --> G
G --> H[离线质量研究]
G --> I[线上推理 Runtime]
H --> H1[量化敏感度研究]
H --> H2[BF16 / FP8 质量验证]
H --> H3[LoRA 泛化验证]
H --> H4[性能验证]
I --> I1[加载 Base]
I --> I2[动态加载 LoRA]
I --> I3[选择对应优化路径]
H4 --> J[生成经过验证的优化策略]
J --> I3
建立F16baseline
flowchart TD
A[选择一个 Base Model] --> B[BF16 / FP16 原始权重]
B --> C[固定推理环境]
C --> C1[固定 Seed]
C --> C2[固定 Prompt]
C --> C3[固定分辨率]
C --> C4[固定 Steps]
C --> C5[固定 Guidance]
C --> C6[固定 Scheduler]
C1 --> D[运行 Diffusers Eager 基线]
C2 --> D
C3 --> D
C4 --> D
C5 --> D
C6 --> D
D --> E[保存 Transformer 输出]
D --> F[保存每一步 Latent]
D --> G[保存最终 Image]
D --> H[保存性能数据]
H --> H1[端到端耗时 E2E]
H --> H2[Transformer 耗时]
H --> H3[显存峰值]
H --> H4[GPU 利用率]
数据采集和性能数据要区分两次测试
1 | |
一个 safetensors 文件可以放所有 step,例子
1 | |
保存每一步
1 | |
具体
1 | |
对比complie
使用
1 | |
对比 BF16 compile, Transformer, Latent 结果
- 正常的话,误差小 (怎样才算小, 可以baselineA vs baseline B baseline vs compile 不同参数 这样就知道大概范围 )
- 某个 timestep 开始发生异常数值放大,那就可疑
1 | |
最好画成曲线
1 | |
图片就对比PSNR,SSIM,LPIPS
ModelOpt 做基础 FP8 Calibration
flowchart TD
A["线上真实任务日志"] --> B["按 Base Model 分组"]
B --> C["分层抽样"]
C --> C1["Prompt 类型"]
C --> C2["Resolution"]
C --> C3["Steps"]
C --> C4["Guidance"]
C --> C5["Prompt 长度"]
C1 --> D["Calibration JSONL"]
C2 --> D
C3 --> D
C4 --> D
C5 --> D
D --> E["ModelOpt mtq.quantize"]
E --> F["自动收集 Activation"]
F --> G["自动求 amax / Scale"]
G --> H["Base FP8 Candidate"]
H --> I["独立 Validation Set"]
I --> I1["无 LoRA"]
I --> I2["代表性 LoRA"]
I --> I3["多 LoRA"]
I1 --> J["BF16 vs FP8"]
I2 --> J
I3 --> J
J --> K["Transformer / Latent / Image 质量 Gate"]
K -->|通过| L["进入真实 Runtime 性能测试"]
K -->|失败| M["敏感层 / Mixed Precision"]
- 准备线上数据Calibration、Validation数据
- 修改好ModelOpt fp8 脚本,获得fp8模型
- 对比 baseline / fp8 和 lora 配列组合的结果
数据不同的base model,用户自己的数据不要混淆,不同尺寸/steps的数据,按比例凑够128
| 线上字段 | Calibration 要不要 | 原因 |
|---|---|---|
base_model |
必须 | 不同 Base 必须分开 calibration |
prompt |
必须 | 影响 text condition / activation |
height |
必须 | 改变 latent/token shape |
width |
必须 | 同上 |
steps |
必须覆盖分布 | 决定 timestep trajectory |
guidance_scale |
建议 | FLUX-dev 会进入 Transformer guidance |
seed |
建议 | 改变初始 latent |
max_sequence_length |
建议 | 长 Prompt workload |
| LoRA | 特殊处理 | Dataset 最好同时保留 LoRA 字段,但 calibration 暂时不使用 |
| output | 不要 |
参考:https://github.com/NVIDIA/Model-Optimizer/tree/main/examples/diffusers
根据线上任务参数,搞成jsonl文件,自己改造py脚本, 放到 Model-Optimizer/examples/diffusers/quantization/
复用FP8_DEFAULT_CONFIG 和 filter_func_flux_dev
1 | |
敏感层测试
1 | |
伪代码
1 | |
如果 BF16 eager VS BF16 compile 的 Transformer Rel-L2 P95 大概 3e-4
1 | |
大概规则
1 | |
最后还要再验证一次:单 Block 都安全,不代表多个 FP8 Block 叠加以后一定安全。这一步才是最终 Mixed Precision Recipe 的验收。
1 | |
得到敏感层名单 , 使用TorchAO (ModelOpt 的 FP8 主要还是 Q/DQ fake-quant 数值模拟,不应该默认认为它已经在 L20 上用真正的 FP8 Tensor Core 加速)
1 | |
图片标准
| 指标 | 在问什么 | 趋势 | 主要缺点 |
|---|---|---|---|
| PSNR | 像素差多少? | ↑ 好 | 对微小位移很敏感 |
| SSIM | 局部结构像不像? | ↑ 好 | 不能很好理解高级语义 |
| LPIPS | 人眼感知像不像? | ↓ 好 | 依赖神经网络和实现 |
PSNR
Peak Signal-to-Noise Ratio ,两张图片逐像素到底差多少。

PSNR 最大缺点:太关心像素位置,适合 same prompt + same seed,验证 compile 是否让输出轨迹发生漂移
| PSNR | 大概含义 |
|---|---|
∞ |
像素完全一致 |
> 60 dB |
几乎只有极小数值误差 |
50–60 dB |
极其接近,通常肉眼完全不可辨 |
40–50 dB |
很接近,存在少量像素差 |
30–40 dB |
可测差异,肉眼可能仍很像 |
20–30 dB |
差异比较明显 |
< 20 dB |
差异很大 |
SSIM
Structural Similarity Index ,结构看起来一样吗?通常越接近 1 越相似

| SSIM | 大概含义 |
|---|---|
1.000 |
完全一致 |
> 0.999 |
极其接近 |
0.99–0.999 |
非常相似 |
0.95–0.99 |
有轻微到可见差异 |
0.90–0.95 |
已有明显结构差异 |
0.80–0.90 |
差异比较明显 |
< 0.80 |
通常已经不能叫近似同一输出 |
LPIPS
人眼感知上,两张图片到底有多不像?通过模型
| LPIPS | 大概含义 |
|---|---|
0 |
完全一致 |
< 0.001 |
感知上几乎完全一致 |
0.001–0.01 |
极其相似 |
0.01–0.03 |
很接近,但存在一些感知差异 |
0.03–0.10 |
差异开始比较明显 |
0.10–0.20 |
明显不同 |
> 0.20 |
感知差异很大 |
latent 级别
1 | |
其中最值得看的排序是:
1 | |
| Relative L2 | 粗略解释 |
|---|---|
0 |
完全一致 |
< 1e-6 |
极其接近,接近纯浮点噪声 |
1e-6 ~ 1e-5 |
非常小的数值漂移 |
1e-5 ~ 1e-4 |
通常仍可视作很强的 numerical equivalence |
1e-4 ~ 1e-3 |
有可测漂移,需要继续看最终图 |
1e-3 ~ 1e-2 |
明显漂移 |
> 1e-2 |
通常已经不能叫“compile 级微小差异” |
最好再加一个 Percentile Error:绝大部分都很准 只有极少数 outlier or 整个 latent 都在漂
1 | |