从 BF16 到 FP8:HunyuanVideo1.5推理优化复盘
一、 背景
做推理优化,目标很简单:让整个服务跑快。顶层的调度,算法,其实是ROI更大的方向,我们之前分享的Feature Cache,Step distillation等都是这个方向。分布式推理带来的加速比也很大。
这篇文章讲另外一条主线:HunyuanVideo1.5在单卡上,FP16已经做了相对比较极致的优化后,如何通过FP8来进一步增强性能。
本文的主要端到端口径如下:
| 项目 | 值 |
|---|---|
| 模型 | HunyuanVideo1.5 480p T2V |
| GPU | 单卡 NVIDIA L40S |
| 分辨率 | 832x480 |
| 帧数 | 81 |
| denoise steps | 50 |
| dtype | bfloat16,FP8 仅作用在 transformer linear hot path |
| 输出 | latent,不包含 VAE decode 和视频写出 |
| CFG | guidance_scale=6.0 |
| seed | 20260702 |
prompt和negative prompt都比较长,详见文末。
二、收益概述
如果BF16本身还停留在松散实现上,FP8很容易变成“靠 baseline太弱赢出来”的优化。所以我们先进行了相对比较深入的BF16优化,然后再做FP8量化。
下面是使用同样的模型,diffusers,vllm-omni和我们的BF16实现(原始实现,链路优化版本和算子优化版本)在时间消耗上的对比:

下面是它们对应的吞吐的对比图:

可以看到,尽管我们实现的初始版本比diffusers效率还差一点,但是经过我们优化之后,效率比主流的vllm-omni还要快不少。
BF16的优化告一个段落之后,一个方向是做分布式推理,测试CP,TP,CFG等并行方法能够带来的加速。另外一个就是做低比特量化的工作。
Low-Bit Quantization无论在LLM还是生成式模型里面都是一个很热的话题,早期的方法是SmoothQuant等,现在大家比较关注的是使用Hadamard matrix来做平滑,然后在平滑的结果上做量化。在生成式模型领域里面,最成熟的就是4-bit的SVDQuant,不但在图像生成领域可以使用,在视频生成领域也可以使用。
低比特量化有两个点需要关注:
- 视觉效果损伤度
- 运行效率提升
一般来说,越低比特视觉效果损伤越大。运行效率除了考虑本身代码实现之外,还需要考虑硬件对于对应格式的支持。
FP8相较于FP4来说,比特数更多,整体效果也更容易保持。恰好L40s的tensorcore也支持FP8,所以我们先选择使用FP8来做优化。
下面是我们FP8优化前后和FP16的耗时对比:

下面是吞吐对比:

可以看出,并不是使用了FP8效率就高。我们主要使用FP8 GEMM来替换FP16 GEMM。参数部分按照per-channel的方式事先量化好,但是FP16的activation需要运行期进行量化,这会极大影响FP8的运行效率。尽管FP8的tensorcore的吞吐是FP16的两倍,我们初始的版本效率反而远远慢于BF16的版本。经过优化后,效率比FP16高,但是也远远达不到翻倍的效率。
三、FP8 从能跑到能赢,主要改了什么
1. 先修正 weight layout
FP8 不是把 Linear 换个 dtype 就结束。真实路径至少包括:
BF16 activation
-> dynamic per-token FP8 quant
-> FP8 GEMM
-> scale / bias / output dtype
最早的 FP8 版本首先解决的是“能不能跑”:把 checkpoint linear 换成 FP8 weight,forward 时在线量化 activation,然后走 scaled-mm。
这个阶段最容易犯的错误,是把 FP8 weight 转置后又 .contiguous(),导致原本应该 column-major 的 B 矩阵 stride 被破坏。torch._scaled_mm、vLLM CUTLASS、cuBLASLt 这类 FP8 GEMM 对 B layout 很敏感,一旦 layout 不对,就会掉进慢路径或 fallback。
这一步如果没做好,FP8的速度会直接掉到 770s 量级,基本就不用讨论后续优化了。也就是说,FP8 initial 慢,不只是 activation quant 慢,也包括 GEMM 本身没有走到正确的高效路径。
2. 再优化 FP8 GEMM 算子本身
修正 weight layout 之后,FP8 至少能跑到一个正常的方向上了。但这时候还有一个问题:FP8 GEMM 不等于天然快。理论上 FP8 Tensor Core 吞吐更高,但是如果 tile 选得不好,或者当前 shape 不适合这个 backend,最后还是会比 BF16 慢。
所以这部分我们花了很长时间用 NCU 做实验。方法也比较直接:把外部实现里跑得最快的 kernel 拿出来,和我们的本地 kernel 放在同一类 shape 上对比,看 latency 背后的指标到底差在哪里。
通过这些对比,我们发现本地 kernel 主要有几个问题:
- 有些 shape 下 SM throughput 没有打起来。也就是说,FP8 Tensor Core 理论吞吐很高,但 kernel 没有把硬件喂饱。
- 有些 shape 下 memory throughput 已经很高,继续改计算部分收益不大,真正要减少的是额外读写和中间 tensor。
- 有些 kernel 的 eligible warps 太少,register/thread 和 occupancy 会互相卡住,导致看起来 tile 很大,但实际并发不够。
- shared memory 和 epilogue 部分也会影响最终收益。尤其是带 bias、scale、output dtype 的 full path,不能只看一个裸 GEMM。
- 最重要的是,不同 GEMM 形态适合的 backend 不一样。本地 rowwise、cuBLASLt、FlashInfer 风格路线都有可能在某些 shape 上胜出,没有一个 backend 是所有情况下完胜的。
这里比较典型的是 FFN up 和 FFN down。
FFN up 的 shape 大致是:
M=4096, N=8192, K=2048
这个 shape 的 N 很大。LightX2V Triton 在这个位置曾经选到 BLOCK_M=256, BLOCK_N=128, BLOCK_K=128, num_warps=8, num_stages=3。而我们本地早期 rowwise 路线主要还是 128x128x64。这说明差距不是 Python wrapper 这种小开销,而是主 GEMM kernel 的 tile 形态和实际吞吐没有打出来。
FFN down 又是另一个问题。它更像:
M=4096, N=2048, K=8192
这个 shape 下,rowwise CUTLASS 不一定是最好的。我们试过之后发现,有些情况下 cuBLASLt 或 FlashInfer 风格的路径更适合。因此 selector 不能写成“只要是本地 rowwise 就默认用”,而是要根据 shape 选择。
经过我们优化之后,和外部最好的实现对比,单算子都可以得到10%+的收益:
| 位置 | 本地最好结果 | 外部最好结果 | 本地收益 |
|---|---|---|---|
attention QKV M=4096 |
319.49us |
SGLang three-proj 365.57us |
12.61% |
| image attention QKV | 2551.81us |
FlashInfer three-proj 2849.63us |
10.45% |
| FFN up generic | 409.28us |
LightX2V Triton 458.75us |
10.78% |
| FFN down producer | 430.08us |
FlashInfer GELU+cuBLAS direct 543.74us |
20.90% |
3. 把通用判断前移到构造期
FP8 hot path 里最不应该反复出现的是这种问题:
- 这个输入是普通 Tensor 还是 Fp8 Tensor?
- native op 有没有加载?
- 这个 shape 能不能走 rowwise?
- 这个 shape 是不是更适合 cuBLASLt?
- 失败了要不要 fallback 到 torch?
通用库需要这些判断,但 HunyuanVideo1.5 的 FP8 linear 是我们自己替换的,输入 contract 也是我们自己定义的。热路径里继续每层都判断一遍,只会把 Python overhead 放进每个 denoise step。
所以后面的改法是:
- 构造期/refresh期:
根据 weight shape、device、bias contract 选 backend
绑定具体 native callable
- forward期:
- 普通 Tensor:fp8量化+gemm
- Fp8 Tensor:直接做fp8 gemm
这样,链路额外的消耗都被我们挡在构造期,运行期性能不受影响。
4. QKV 不应该重复量化三次
vllm-omni,sglang和lightx2v做法相对暴力,无论bf16还是fp8,都是把Q、K、V projection直接合并成一个projection。所以它们并不存在我们这里说的问题。
但是我自己测试的时候发现,Attention里面hidden_states依然保留分离的三个projection效率会更高。主要是对于我们的生成
- hidden_states: [1, 32760, 2048]
- encoder_hidden_states: [1, 26, 2048]
也就是hidden_states本来就很大,合并之后效率反而变低。那在这种情况下,q projection,k projection,v projection三个gemm都被替换成fp8之后,都会对hidden_states这个activation做quantization,这显然是有冗余的。
先量化好activation,再分别传入三个projection,会有差不多 9.05%的收益。
5. Producer fusion 要看位置,不要迷信
FFN down 的前面通常是 GELU/SiLU 这类 activation。朴素路径是:
GELU/SiLU 产 BF16
BF16 再 dynamic quant 成 FP8
FP8 再 GEMM
更合理的方式是让 activation producer 直接产出 FP8 tensor 和它对应的 per-token scale:
(fp8_tensor, scale)
这样后面的 FP8 linear 直接消费。如果输入已经是 torch.float8_e4m3fn,就认为它已经量化过,直接使用传入的 scale;如果输入还是 BF16/FP16/FP32,才在 FP8 linear 内部做 dynamic per-token quant。
这部分优化收益不会很大,在我们的测试里面,可以把推理时间从287.33s减小到282.44s,大致1.7%的收益。
四、这轮优化真正学到的事
这次 FP8 优化最重要的经验,不是“某个 kernel 比别人快多少”,而是边界要分清。
BF16 阶段,最大的收益来自attention模式和数据形态:compact context 让模型摆脱低效 masked/padded path。这个收益如果不先拿到,后面 FP8 的成绩会被baseline水分放大。
FP8 阶段,最关键的是三件事:
- layout 必须对:FP8 weight 的 column-major stride 不能被
.contiguous()破坏; - 算子要快:切菜的刀必须保证好使,而且砍骨头和切西瓜用的刀就是不一样;
- 热路径必须窄:backend 选择、symbol 检查、fallback 决策要尽量前移,不要每层 forward 都重新判断;
- 模型语义要用上:QKV 共享 activation quant,activation producer 直接产 FP8 contract,这些比单纯换一个 GEMM backend 更接近端到端收益。
附录:复现输入
prompt:
A fluffy orange kitten wearing a tiny straw hat sits quietly on a stone beside a beautiful lotus pond, fishing with a delicate bamboo fishing rod. The fishing line gently drops into the crystal-clear water. Pink and white lotus flowers bloom across the pond, surrounded by lush green lotus leaves swaying softly in the breeze. Dragonflies hover above the water while small fish occasionally create gentle ripples. Warm golden sunlight filters through the trees, casting soft light on the kitten and the pond. The kitten looks focused and adorable, its tail gently swaying as it patiently waits for a fish to bite. Peaceful summer atmosphere, cinematic composition, ultra detailed fur, realistic water reflections, volumetric lighting, shallow depth of field, highly detailed nature, photorealistic, smooth natural motion, masterpiece, 8K.
negative prompt:
blurry, low quality, low resolution, noisy, artifacts, flickering, jitter, shaky camera, distorted anatomy, malformed body, extra limbs, extra paws, missing limbs, duplicated cat, multiple tails, multiple heads, deformed face, bad eyes, cross-eyed, unnatural expression, floating objects, broken fishing rod, unrealistic water, oversaturated colors, watermark, logo, text, subtitle, frame, cropped, out of frame, motion blur, ghosting, inconsistent lighting, temporal inconsistency, unnatural animation
warmup prompt:
A small white puppy wearing a blue scarf rests beside a quiet mountain stream at sunrise, cinematic, photorealistic, soft natural motion, detailed fur, realistic reflections.
warmup negative prompt:
blurry, low quality, artifacts, jitter, distorted anatomy, watermark, text, temporal inconsistency
留言
0 条已审核留言