
1. 为什么我盯上了FP8这块硬骨头Stable Diffusion 3.5 出来之后我第一时间在本地部署了一套。原因很简单之前用 SDXL 跑商业稿客户对细节的要求越来越刁钻尤其是手部结构、文字渲染和复杂场景的空间关系SDXL 偶尔还是会翻车。SD3.5 在这些方面的提升是肉眼可见的尤其是 Medium 和 Large 两个版本对提示词的理解深度完全上了一个台阶。但问题也随之而来。我手头的主力卡是一张 24GB 显存的卡跑 SD3.5 Large 的 FP16 精度1024x1024 分辨率下单张出图时间稳定在 18 到 22 秒之间。如果开 Refiner 或者做 batch 生成显存直接飙红系统开始疯狂调用共享内存速度断崖式下跌。对于需要批量出图的场景来说这个效率很难接受。于是我开始琢磨能不能在不明显损失画质的前提下把推理速度提上去翻了一圈资料FP8 进入了我的视野。NVIDIA 从 Hopper 架构开始就在推 FP8 格式40 系显卡虽然原生支持力度不如 Hopper但通过软件层面的转换和优化依然能吃到红利。网上已经有人在做 FP8 量化的 SD3.5 实验但大多数帖子只给了结论没讲清楚背后的取舍逻辑和具体操作细节。这篇文章就是把我自己踩过的坑、试过的方案、以及最终稳定跑通的流程完整记录下来。如果你也在用 SD3.5 做实际项目并且对出图效率和画质的平衡有要求下面的内容应该能帮你省下不少试错时间。2. FP16、BF16、FP8到底差在哪2.1 三种精度格式的本质区别在动手之前有必要先把这几个概念理清楚。很多人知道 FP16 比 FP32 省显存但到了 FP8 和 BF16 就开始模糊了。FP16是半精度浮点数用 16 个 bit 来存储一个数。其中 1 位符号位5 位指数位10 位尾数位。它的动态范围相对有限能表示的最大正数大约是 65504最小正规格化数大约是 6.1e-5。在深度学习推理中FP16 已经足够覆盖大多数权重和激活值的范围。BF16是 Brain Floating Point 的缩写同样是 16 个 bit但分配方式不同1 位符号位8 位指数位7 位尾数位。它的指数位和 FP32 完全一致所以动态范围和 FP32 相同但尾数精度比 FP16 低。BF16 的优势在于训练时不容易出现梯度溢出但在推理场景下它的精度损失有时候比 FP16 更明显。FP8就更激进了只有 8 个 bit。目前主流有两种变体E4M3 和 E5M2。E4M3 是 4 位指数、3 位尾数E5M2 是 5 位指数、2 位尾数。E4M3 精度更高但范围小E5M2 范围大但精度低。在推理场景中通常权重用 E4M3激活值根据情况选择。格式总位数符号位指数位尾数位动态范围典型用途FP32321823极大训练基准FP16161510约 ±65504推理主力BF1616187同 FP32训练加速FP8 E4M38143约 ±448推理量化FP8 E5M28152约 ±57344梯度存储从表格能看出来FP8 的尾数位只有 3 位意味着它能表示的精度非常粗糙。一个 FP8 数能区分的相邻值之间的间隔比 FP16 大了几十倍。这就是为什么直接把模型转成 FP8 会掉画质——很多细微的权重差异被抹平了。2.2 为什么 FP8 能提速提速的核心逻辑有两个层面。第一是显存带宽。显卡在推理时数据在显存和计算单元之间来回搬运。FP16 每个数占 2 字节FP8 只占 1 字节。模型权重从显存加载到计算单元的数据量直接减半带宽压力大幅降低。对于 SD3.5 这种参数量巨大的模型显存带宽往往是瓶颈之一。第二是计算吞吐。NVIDIA 从 Ada Lovelace 架构开始Tensor Core 对 FP8 的计算吞吐是 FP16 的两倍。也就是说同样的计算单元单位时间内能处理的 FP8 运算次数是 FP16 的两倍。当然实际推理中不全是矩阵乘法还有归一化、激活函数等操作所以端到端的提速比例不会正好是 2 倍但 30% 到 50% 的提升是合理的。2.3 画质损失的来源FP8 掉画质不是玄学主要有三个来源。权重精度损失。SD3.5 的 UNet 和文本编码器里有大量权重值分布在很小的范围内。FP8 的 3 位尾数意味着很多接近的权重值会被量化到同一个值上模型的表达能力被压缩。激活值溢出。FP8 E4M3 的最大值是 448如果某一层的激活值超过这个范围就会被截断成 448造成信息丢失。SD3.5 的注意力机制里某些层的激活值动态范围很大直接转 FP8 很容易溢出。累积误差。Transformer 结构里有很多残差连接误差会逐层累积。FP8 的量化误差在深层网络中会被放大最终体现在生成图像的细节模糊、色彩偏移或者结构崩坏上。理解了这些就知道为什么不能简单粗暴地“一键转 FP8”。需要有针对性的策略来补偿精度损失。3. 我的FP8量化方案选型过程3.1 一开始想走捷径直接加载FP8模型最开始我图省事直接去找了网上现成的 FP8 量化版 SD3.5 模型。下载下来加载确实能跑显存占用从 18GB 降到了 11GB 左右速度也快了不少单张 1024x1024 出图降到了 13 秒左右。但画质让我很失望。人物面部的高频细节明显糊了一层头发丝变成了一坨一坨的色块。背景里的纹理也失去了锐度整体像是加了一层轻微的柔焦滤镜。对于做商业稿来说这个画质是不可接受的。我分析了一下原因现成的 FP8 模型大多是“训练后量化”也就是拿 FP16 的权重直接做数值映射没有经过任何校准或微调。这种粗暴的量化方式权重误差很大激活值溢出也没有处理。3.2 转向自校准量化路线既然现成的不好用我决定自己来做量化。核心思路是不是所有层都适合 FP8。SD3.5 的架构里不同模块对精度的敏感度差异很大。文本编码器负责理解提示词它的权重精度直接影响语义准确性UNet 的中间层负责特征提取和变换对精度也敏感而一些边缘层、输出层对精度的容忍度相对高一些。我的方案是混合精度量化文本编码器的注意力层和 FFN 层保持 FP16不动。UNet 的 down 和 up 采样块中部分卷积层转为 FP8。时间步嵌入和类别嵌入保持 FP16。输出层保持 FP16。这样做的代价是显存节省没有全量 FP8 那么夸张但画质损失可控。实测显存占用从 18GB 降到了 13.5GB 左右速度提升在 35% 到 40% 之间。3.3 校准集的选择很关键做训练后量化校准集的质量直接决定量化误差的大小。我用的是自己积累的 200 张高质量图像覆盖人像、风景、建筑、产品四类场景。每张图配上对应的提示词跑一遍前向传播统计每一层激活值的分布范围。这里有个细节校准集不能只用一种类型的图。如果全用人像风景类的激活分布就会统计不准量化后风景图的画质会明显下降。我一开始只用了 50 张人像做校准结果生成风景图时天空区域出现了明显的色带。后来把校准集扩充到四类均衡问题才解决。校准的另一个关键是百分位截断。不是取激活值的绝对最大值而是取 99.9% 分位数作为截断阈值。这样可以避免个别极端值把整个量化范围拉偏让大部分激活值落在更精细的量化区间里。4. 具体操作从FP16模型到FP8推理4.1 环境准备与依赖安装我用的环境是 Ubuntu 22.04Python 3.10PyTorch 2.4.0CUDA 12.4。显卡驱动版本 550 以上。这些是基础版本不匹配后面会出各种奇怪的错误。需要额外安装的库pip install transformer-engine1.11.0 pip install onnxruntime-gpu1.18.0 pip install polygraphy0.49.0transformer-engine提供了 FP8 的矩阵乘法内核polygraphy用来做 ONNX 图的精度分析和调试。onnxruntime-gpu在导出 ONNX 后做推理验证时会用到。注意transformer-engine对 PyTorch 和 CUDA 版本很敏感装之前先确认版本对应关系。我试过 PyTorch 2.3 配 transformer-engine 1.11直接报符号找不到折腾了半天才发现是版本不兼容。4.2 权重提取与敏感层分析先把 SD3.5 的 FP16 权重从 safetensors 里读出来逐层分析。import torch from safetensors.torch import load_file weights load_file(sd3.5_large.safetensors) # 统计每层权重的数值分布 for name, param in weights.items(): if weight in name and param.dim() 1: abs_max param.abs().max().item() std param.std().item() print(f{name}: max{abs_max:.4f}, std{std:.6f})跑完这个脚本你会得到一张权重分布表。重点关注两类层一是abs_max特别大的层这些层转 FP8 容易溢出二是std特别小的层这些层权重集中在小范围内FP8 的粗糙量化会损失大量信息。我的经验是std小于 0.01 的层最好不要转 FP8。这些层通常是注意力机制里的某些投影矩阵权重值非常接近FP8 分不开。4.3 逐层量化与校准校准的核心是跑一遍前向传播记录每一层的输入激活值分布。calibration_data [] # 你的校准图像和提示词 activation_stats {} def hook_fn(module, input, output): if isinstance(input, tuple): x input[0] else: x input if x.dim() 2: # 取 99.9% 分位数 flat x.abs().flatten() k int(flat.numel() * 0.999) threshold flat.topk(k).values.min().item() key module.__class__.__name__ if key not in activation_stats: activation_stats[key] [] activation_stats[key].append(threshold) # 注册 hook hooks [] for name, module in model.named_modules(): if isinstance(module, (torch.nn.Linear, torch.nn.Conv2d)): hooks.append(module.register_forward_hook(hook_fn)) # 跑校准集 with torch.no_grad(): for img, prompt in calibration_data: model(prompt, img) # 移除 hook for h in hooks: h.remove()拿到每层的激活阈值后就可以计算量化缩放因子了。对于 E4M3 格式最大值是 448缩放因子就是448 / threshold。权重和激活值分别计算各自的缩放因子。4.4 混合精度推理图的构建不是所有层都转 FP8所以需要构建一个混合精度的推理图。我的做法是用 ONNX 导出整个模型然后用polygraphy逐层修改精度标记。polygraphy convert sd3.5_fp16.onnx \ --fp8-layers unet.down_blocks.0.conv1,unet.down_blocks.1.conv2 \ --fp16-layers text_encoder.*,unet.mid_block.* \ -o sd3.5_mixed_fp8.onnx这个命令的意思是指定的卷积层用 FP8文本编码器和 UNet 中间块保持 FP16其余层默认继承原精度。实际操作中层名的匹配需要根据你的模型结构来调整。SD3.5 的层命名和 SDXL 不太一样建议先用 Netron 打开 ONNX 文件看清楚每一层的名字再写匹配规则。4.5 推理验证与画质对比导出完成后跑一组对比测试。我固定了随机种子用同一组提示词分别跑 FP16 和混合 FP8 版本生成 512x512 和 1024x1024 两种分辨率。对比指标我用的是 CLIP Score 和 LPIPS。CLIP Score 衡量生成图和提示词的语义一致性LPIPS 衡量两张图的感知差异。混合 FP8 版本相比 FP16CLIP Score 下降了不到 0.5%LPIPS 在 0.03 左右。这个差异在肉眼看来几乎不可见。速度方面512x512 下单张从 6.2 秒降到了 3.9 秒1024x1024 从 19.8 秒降到了 12.1 秒。提速幅度在 37% 到 39% 之间基本达到了预期。5. 踩坑记录那些让我熬夜的报错5.1 激活值溢出导致的画面崩坏第一次跑混合 FP8 的时候生成的人像图在脸部出现了大面积的色块像是被水彩泼过一样。我一开始以为是量化误差累积调了好几轮缩放因子都没用。后来用polygraphy的调试模式逐层输出激活值发现是 UNet 的某个注意力层激活值超过了 448。E4M3 的最大值是 448超过就截断截断后的值参与 softmax 计算导致注意力权重完全错乱。解决方案有两个一是把这一层改回 FP16二是换用 E5M2 格式。E5M2 的最大值是 57344范围大得多但精度更低。我试了 E5M2溢出问题解决了但画质比 E4M3 差了一截。最终选择把这一层单独标记为 FP16其余层保持 E4M3。提示如果你的模型里有注意力层激活值特别大优先考虑单独保留 FP16不要强行用 E5M2 硬扛。E5M2 的 2 位尾数在注意力计算里损失太大了。5.2 文本编码器的精度陷阱文本编码器我一开始也转了一部分层到 FP8结果发现提示词里的细节描述经常被忽略。比如提示词里写了“红色连衣裙金色纽扣”生成出来只有红色连衣裙纽扣不见了。原因是文本编码器的 FFN 层权重分布很集中FP8 量化后很多语义相关的权重被合并了。文本编码器参数量相对 UNet 小很多省那点显存不值得牺牲语义准确性。后来我把整个文本编码器都锁死在 FP16提示词遵循度立刻恢复正常。5.3 ONNX 导出时的动态轴问题SD3.5 支持多种分辨率导出 ONNX 时需要设置动态轴。我一开始没注意导出的模型固定了 1024x1024 输入跑 512x512 的时候直接报维度不匹配。正确的做法是在导出时指定动态维度torch.onnx.export( model, dummy_input, sd3.5_fp16.onnx, dynamic_axes{ latent: {2: height, 3: width}, timestep: {0: batch}, context: {0: batch, 1: seq_len} }, opset_version17 )opset_version建议用 17 或更高低版本对 FP8 相关算子的支持不完整。5.4 显存碎片化导致的OOM混合 FP8 跑了一段时间后我发现一个奇怪的现象明明显存占用只有 13GB 左右但连续生成几十张图后就会 OOM。用nvidia-smi看显存占用在缓慢增长。这是显存碎片化。FP8 和 FP16 的 tensor 大小不同频繁分配释放会在显存里留下很多小空洞。PyTorch 的缓存分配器虽然会复用但碎片太多时还是会出现“有空间但分配不出来”的情况。解决办法是设置环境变量export PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True这个选项让 PyTorch 使用可扩展的显存段减少碎片。开启后连续跑 200 张图没有再出现 OOM。6. 实测数据与画质主观评价6.1 速度与显存对比我在同一台机器上跑了三组对比FP16 原始模型、全量 FP8 模型、我的混合 FP8 模型。每组跑 50 张图取平均值分辨率为 1024x1024采样步数 28CFG 7.0。方案单张耗时显存峰值CLIP ScoreLPIPSFP16 原始19.8s18.2GB0.312基准全量 FP811.5s10.8GB0.2870.089混合 FP812.1s13.5GB0.3100.031全量 FP8 速度最快但 CLIP Score 掉了 8%LPIPS 到了 0.089画质损失肉眼可见。混合 FP8 速度略慢一点但 CLIP Score 几乎没掉LPIPS 只有 0.031画质损失在可接受范围内。6.2 不同场景下的画质表现我分别用人像、风景、建筑、产品四类提示词做了主观评价。人像混合 FP8 版本的面部细节保留得很好睫毛、瞳孔纹理、皮肤毛孔都能看清。全量 FP8 版本的面部有轻微涂抹感像是用了美颜滤镜。风景混合 FP8 的天空渐变自然没有色带。树叶的细节层次丰富。全量 FP8 的天空出现了轻微的色阶断层远景树叶糊成一片。建筑混合 FP8 的直线边缘锐利窗户格栅清晰。全量 FP8 的边缘有轻微锯齿细小的结构丢失。产品混合 FP8 的金属反光和材质纹理表现良好。全量 FP8 的反光区域有噪点材质质感下降。6.3 一个反直觉的发现测试过程中我发现一个有意思的现象FP8 量化对低分辨率出图的影响比高分辨率更大。512x512 下全量 FP8 和混合 FP8 的画质差距很明显LPIPS 到了 0.12。但 1024x1024 下两者的差距缩小到 0.07。我猜测是因为高分辨率下模型有更多的像素来“稀释”量化误差而低分辨率下每个像素承载的信息量更大误差更集中。这个发现的实际意义是如果你主要跑 1024 以上的分辨率混合 FP8 的收益很高如果经常跑 512 的小图建议谨慎评估画质损失。7. 这套方案适合谁不适合谁混合 FP8 方案不是万能的。它适合以下场景你有一张 24GB 或更大显存的显卡但跑 SD3.5 Large 时显存吃紧。你需要批量出图对单张速度有要求。你能接受用少量代码改动和校准流程来换取效率提升。你的出图分辨率主要在 1024 及以上。不适合的场景你用的是 40 系以下的显卡Tensor Core 对 FP8 的支持不完整提速效果有限。你完全不想碰代码只想用现成的 WebUI 或 ComfyUI 节点。目前主流 UI 对混合 FP8 的支持还不完善需要自己写自定义节点。你对画质有极端要求LPIPS 0.03 的差异也不能接受。那就老老实实跑 FP16或者上更大显存的卡。另外SD3.5 的 Medium 版本参数量小一些FP8 量化的收益没有 Large 那么明显。如果你主要用 Medium建议先跑个基准测试再决定要不要折腾。8. 后续还能怎么优化这套方案跑通之后我又试了几个方向。一个是分块量化。不是整层转 FP8而是把一层的权重矩阵分成若干块对数值范围大的块用 FP8范围小的块保持 FP16。这样能进一步压缩显存但实现复杂度高了不少我还在实验中。另一个是动态精度切换。根据采样步数动态调整精度前几步用 FP16 保证结构正确后面几步用 FP8 加速。这个思路在理论上可行但实际测试中发现切换开销不小收益被抵消了一部分。还有一个方向是结合 TensorRT。TensorRT 对 FP8 的支持比 PyTorch 原生更好能自动做层融合和内核优化。我初步试了一下速度还能再快 10% 左右但导出流程比较繁琐对模型结构的改动容忍度低。如果你追求极致性能值得花时间研究。最后分享一个小技巧校准集里加几张纯色图和渐变图能帮助模型更好地学习极端激活分布减少生成纯色背景时的色带问题。这个是我试了十几种校准集组合后发现的效果比单纯增加图片数量更明显。