权重格式转换与精度损失的定位
把一份 FP8 原始权重转成 BF16,供不支持 FP8 的 **** 平台加载(**** 的 ****,8 卡,单卡 64GB HBM,卡间互联 392 GB/s)。转换脚本跑完,目标目录里生成了新的权重文件,模型能加载,请求能返回。但输出与参考版本不一致:同一批问题下出现重复片段或提前终止,而同一模型在支持原生格式的另一条链路上结果正常。改动只落在权重这一项,问题要么在转换链路里,要么在加载后的推理路径上。定位要把两个来源分开:先固定比对条件,再逐层比对中间结果,最后用路径证据判断偏差是转换引入还是推理路径引入。
转换链路:格式、dtype、切分重排
原始权重到目标格式通常经过三步,每步都可能引入损伤:
| 步骤 | 做什么 | 常见损伤来源 |
|---|---|---|
| 格式转换 | 容器与数值格式互转,如 FP8 → BF16、safetensors 与 checkpoint 目录互转、导出 .onnx 一类中间格式 |
数值截断、旁路文件遗漏 |
| dtype 转换 | 逐张量改变数值精度,如转换时把输入节点指定为 FP16、输出指定为 FP32 | 截断误差、逐层累积 |
| 切分与重排 | 按 tensor_parallel_size 切分权重、重排张量布局与头序 |
切分边界错位、头序错位 |
FP8 → BF16 属于格式与 dtype 同时变化的转换。源格式与目标格式的位分配不同,转换本身有精度损失,虽有损程度低于反向(高位转低位),误差仍会随层数累积。格式转换还有一处比数值更常见的遗漏:转换脚本只处理权重张量,不会自动复制 tokenizer.json、tokenizer_config.json 一类旁路文件。目标目录缺这些文件时模型仍能加载,分词结果却与源不一致,输出随之偏离,且过程不报错。部分模型还要求改写配置文件字段,例如把 config.json 里的 model_type 改成目标代码分支期望的取值,字段没对齐时模型按另一套代码路径加载,行为偏离同样不报错。
按参数 × 字节数估算转换后的体积(非实测):FP8 每参数 1 字节,BF16 每参数 2 字节,同一份权重转完体积约为原来的两倍。体积翻倍是转换链路的直接代价,与精度无关。
精度损伤的可能来源
- dtype 截断:每层在转换时把数值截到目标精度能表示的集合,误差本层可见。
- 逐层转换误差累积:单层误差很小,但沿层数累加,越深的模型偏差越大。
- 转换工具版本差异:同一份权重经不同版本的工具转换,产物可能不同;工具版本落在兼容窗口外时,产物与参考版本不一致,而转换过程不报错。
- 旁路文件与配置字段遗漏:
tokenizer、config.json字段未同步,加载路径按默认值处理。 - 输入 dtype 不匹配:转换或加载时输入张量 dtype 与模型期望不符,推理照常运行却产出错误结果。
触发条件
| 转换场景 | 高风险点 |
|---|---|
| FP8 → BF16(位宽升位、格式变化) | 数值截断、体积翻倍 |
| 低精度 → 高精度(如 INT8 → BF16) | 截断误差已固化在源产物里,转高精度不能恢复 |
跨框架导出(导出 .onnx 一类中间格式) |
算子映射差异、dtype 默认值不同 |
| 转换工具版本与目标运行时不一致 | 转换不报错,推理阶段才异常 |
| 切分重排改变张量布局 | 头序或边界错位,能加载但输出偏离 |
现象
- 转换完成、模型能加载、能出结果,输出与参考版本不一致。
- 转换过程不报错:脚本正常退出,日志里也没有失配提示,问题在推理阶段才以质量偏差的形式出现。
- 属于「加载成功但接口异常」这一类:服务能起、模型能加载,返回内容却是重复、错乱或提前终止。
- 偏差不与输入规模成比例:短输入正常、长输入才异常,或反之。
定位步骤
- 固定采样配置与随机种子。把
temperature设为 0(greedy),固定seed与max_tokens,先排除采样随机性带来的差异。转换前后的比对必须在同一采样配置下做,greedy 两组输出相等是可比性的前提。 - 逐层比对中间结果。取若干层,用同一输入分别喂给源格式与目标格式,比对层输出与
logits,定位偏差从哪一层开始积累。 - 区分「转换引入」与「推理路径引入」。两个判据:其一,把目标格式在另一条已知正常的链路上加载,若仍偏差,损伤来自转换;其二,比对
target_backend与resolved_backend,若实际路径退回通用实现,损伤来自推理路径而非转换。 - 核对旁路文件与配置字段。列出源目录与目标目录的文件清单(
tokenizer.json、tokenizer_config.json、config.json),核对model_type一类字段是否与目标框架的代码分支匹配。 - 固定工具版本。记录转换工具版本,用兼容窗口内的版本重跑一次,确认产物是否一致。版本不一致时,转换过程不报错而推理阶段异常,是这类问题的典型形态。
# 示意:一次格式转换(脚本名与参数名取自转换工具,取值按本机规格填)
python <convert-tool>.py \
--input-fp8-hf-path /path/to/source \
--output-bf16-hf-path /path/to/target
# 转换脚本通常不自动复制旁路文件,需手动同步
cp /path/to/source/tokenizer.json /path/to/target/
cp /path/to/source/tokenizer_config.json /path/to/target/
比对转换产物本身可用模型查看工具(如 netron)核对图结构,改动前后两张图不一致时,先确认差异来自转换参数而非工具版本。
处置
| 方向 | 动作 | 代价 / 前提 |
|---|---|---|
| dtype 截断偏大 | 提高目标精度(如 INT8 → BF16) | 体积上升 |
| 工具版本不在窗口内 | 退回被验证过的工具版本重转 | 需重新验证产物 |
| 旁路文件或字段遗漏 | 同步 tokenizer、config.json 字段 |
无 |
| 推理路径回退 | 修 guard、换后端,路径确认后再判转换 | 与转换无关 |
| 精度损失阈值可配 | 用精度阈值(如 accuracy_loss_threshold)触发失败,而非人工比对 |
阈值需按任务设定 |
记录字段
# 一条格式转换记录(字段名示意)
source_format: # 源格式,如 FP8 / INT8 / safetensors
target_format: # 目标格式,如 BF16
convert_tool: # 转换工具名
convert_tool_version: # 转换工具版本,写 <pinned-version>
tensor_parallel_size: # 切分目标并行度
calibration_method: # 若为量化转换,记校准方法(min_max / kl_divergence)
bypass_files: # 是否同步 tokenizer / config 等旁路文件
compare_method: # 比对方法:greedy + seed + 逐层中间结果
seed: # 比对用随机种子
temperature: # 比对用采样参数
accuracy_before: # 源格式参考输出
accuracy_after: # 目标格式输出
diff_layer_first: # 偏差首次出现的层
path:
target_backend: # 目标算子路径
resolved_backend: # 实际命中路径
status: # 转换无损 / 转换引入偏差 / 路径引入偏差
小结
权重格式转换的损伤来自三处:数值截断、旁路文件与配置字段遗漏、转换工具版本差异。三者都表现为转换过程不报错、推理阶段才出偏差,因此定位要先固定采样配置与种子,再逐层比对中间结果,最后用另一条正常链路与 resolved_backend 两项判据把转换引入与推理路径引入分开。记录时写清源格式、目标格式、转换工具版本与比对方法,才能在下一次复现同一份结论。