Skip to content

L5 扩散模型推理与加速 ​

目标 ​

  • 用 Diffusers 运行一个 DiT 结构的文生图模型,测出每一步去噪的耗时;
  • 做一个“动机实验”:测量相邻去噪步之间中间特征的相似度,理解为什么缓存能加速;
  • 实现一个最朴素的缓存,量化它带来的加速和质量损失;
  • 掌握生成质量的评测方法;
  • (选做)对照组内论文 BWCache,画出各模块特征随去噪步变化的热力图,并运行组内论文的官方代码。

做完这个实验,你就能读懂方向三的大部分论文。

环境 ​

  • 一张 24 GB 显存的 GPU(RTX 4090、A100 等)
  • Python 包:uv pip install torch diffusers transformers accelerate sentencepiece numpy pillow

本实验使用 PixArt-Σ:约 0.6B 参数的 DiT 文生图模型,显存需求小,也是不少缓存加速论文的评测模型。它的文本编码器是 T5,提示词用英文。

为什么不用 WebUI

Stable Diffusion WebUI、ComfyUI 适合出图,但做加速研究需要改模型内部的计算过程,用 Python 直接调用 Diffusers 才方便插入计时、缓存和统计代码。

第 1 步:生成一张图并计时 ​

python
# step1_timing.py
import time

import torch
from diffusers import PixArtSigmaPipeline

pipe = PixArtSigmaPipeline.from_pretrained(
    "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS", torch_dtype=torch.float16
).to("cuda")
prompt = "a shiba inu running in the snow, cinematic lighting, highly detailed"

# 用前后钩子给每次 transformer 调用(即每个去噪步)计时
step_times = []

def before(module, args, kwargs):
    torch.cuda.synchronize()
    module._t0 = time.perf_counter()

def after(module, args, kwargs, output):
    torch.cuda.synchronize()
    step_times.append(time.perf_counter() - module._t0)

pipe.transformer.register_forward_pre_hook(before, with_kwargs=True)
pipe.transformer.register_forward_hook(after, with_kwargs=True)

pipe(prompt, num_inference_steps=5)          # 预热:第一次运行包含初始化开销
step_times.clear()

torch.cuda.synchronize()
start = time.perf_counter()
image = pipe(prompt, num_inference_steps=20, generator=torch.Generator("cuda").manual_seed(0)).images[0]
torch.cuda.synchronize()
total = time.perf_counter() - start

image.save("baseline.png")
print(f"总耗时 {total:.2f} s,其中 transformer {sum(step_times):.2f} s,共 {len(step_times)} 步")
print(f"平均每步 {sum(step_times) / len(step_times) * 1000:.0f} ms")

预期结果:transformer 的耗时占总耗时的绝大部分,这就是加速研究的重点。剩下的时间花在文本编码和 VAE 解码上(视频模型的 VAE 解码占比会明显更高)。

第 2 步:动机实验——相邻步的特征有多像 ​

python
# step2_similarity.py(接着上面的 pipe 和 prompt)
import torch.nn.functional as F

features = []
block = pipe.transformer.transformer_blocks[14]   # 取中间一层
handle = block.register_forward_hook(
    lambda module, args, output: features.append(output.detach().float().flatten().cpu())
)
pipe(prompt, num_inference_steps=20, generator=torch.Generator("cuda").manual_seed(0))
handle.remove()

sims = [F.cosine_similarity(features[i], features[i - 1], dim=0).item() for i in range(1, len(features))]
for i, s in enumerate(sims, start=1):
    print(f"第 {i} 步 vs 第 {i - 1} 步:{s:.4f}")

把相似度画成折线图。你会看到大部分相邻步的相似度非常高,而开头几步和个别位置变化较大。相似度高说明可以复用上一步的结果,变化大的地方必须重新计算,几乎所有缓存方法都建立在这个观察上。换不同的层、不同的提示词再测一次,看规律是否一致。

第 3 步:最朴素的缓存 ​

每隔一步才真正计算一次 transformer,其余步直接复用上一次的输出:

python
# step3_naive_cache.py(接着上面的 pipe 和 prompt)
class NaiveCache:
    def __init__(self, transformer, interval=2):
        self.forward = transformer.forward
        self.interval = interval
        self.reset()

    def reset(self):
        self.step, self.cached = 0, None

    def __call__(self, *args, **kwargs):
        if self.cached is None or self.step % self.interval == 0:
            self.cached = self.forward(*args, **kwargs)
        self.step += 1
        return self.cached

cache = NaiveCache(pipe.transformer, interval=2)
pipe.transformer.forward = cache

cache.reset()
image = pipe(prompt, num_inference_steps=20, generator=torch.Generator("cuda").manual_seed(0)).images[0]
image.save("naive_cache.png")

# 做完后恢复原来的 forward(删掉实例上的属性,就会用回类上原来的方法),
# 否则后面的步骤都会在缓存开着的情况下运行
del pipe.transformer.forward

用第 1 步的方法计时,再和 baseline.png 对比画质。把 interval 改成 3、4,记录“加速比—质量”的变化。你会发现这种固定间隔的做法质量下降很快。研究中的方法都在回答:什么时候复用、复用哪一部分、复用时怎么修正误差:

思路代表工作
复用整步,按输入变化自适应决定是否跳过TeaCache
复用部分层或部分模块DeepCache、FORA、BWCache
按 token 区分,只重算重要的 tokenToCa
不直接复用,而是预测下一步的特征TaylorSeer、Predict to Skip

交互演示:特征缓存 ​

下面用示意数据演示缓存方法的基本逻辑:累计变化低于阈值时复用上一次的结果,否则重新计算。拖动阈值,看看加速比和误差怎么变化,再和第 3 步“每隔一步复用”的做法对比。

第 4 步:评测生成质量 ​

加速方法要同时报告速度和质量。

和原始输出比(衡量加速带来的损失,加速研究最常用):

python
import numpy as np
from PIL import Image

def psnr(path_a, path_b):
    a = np.asarray(Image.open(path_a), dtype=np.float64)
    b = np.asarray(Image.open(path_b), dtype=np.float64)
    mse = np.mean((a - b) ** 2)
    return 10 * np.log10(255 ** 2 / mse)

print(f"PSNR: {psnr('baseline.png', 'naive_cache.png'):.2f} dB")

SSIM 和 LPIPS 可以用 torchmetrics 计算。PSNR、SSIM 越高越好,LPIPS 越低越好。

常用指标汇总

类别指标说明
与原始输出的差异PSNR、SSIM、LPIPS同一提示词和随机种子下,与不加速版本逐张对比
图文一致性CLIP Score图像与提示词的向量相似度
人类偏好PickScore、ImageReward、HPSv2用人类偏好数据训练的打分模型,可从多个候选中选最佳图像
分布质量FID、ISFID 衡量生成图像与真实图像分布的差异,越低越好;IS 衡量清晰度和多样性,越高越好,现在用得较少
视频VBench从画质、运动、时间一致性等多个维度评测视频生成

第 5 步(选做):对照组内论文 ​

画出每个模块的变化 ​

BWCache(ICLR 2026)的出发点是一张热力图:横轴是去噪步,纵轴是 DiT 的各个模块,颜色表示该模块的输出和上一步相比变化了多少(相对 L1 距离)。在 PixArt-Σ 上画一张同样的图:

先确认第 3 步之后已经恢复了原来的 forward(del pipe.transformer.forward)。如果缓存还开着,有一半的去噪步不会真正执行 DiT 模块,画出的热力图是错的。

python
# step5_block_heatmap.py(接着上面的 pipe 和 prompt)
import matplotlib.pyplot as plt
import numpy as np

blocks = pipe.transformer.transformer_blocks
prev, diff = {}, {i: [] for i in range(len(blocks))}


def make_hook(i):
    def hook(module, args, output):
        out = output.detach().float()
        if i in prev:   # 相对 L1 距离:|这一步 − 上一步| 的均值 ÷ |上一步| 的均值
            diff[i].append(((out - prev[i]).abs().mean() / prev[i].abs().mean()).item())
        prev[i] = out   # 只保留上一步的输出,避免占用太多内存
    return hook


handles = [b.register_forward_hook(make_hook(i)) for i, b in enumerate(blocks)]
pipe(prompt, num_inference_steps=20, generator=torch.Generator("cuda").manual_seed(0))
for h in handles:
    h.remove()

plt.imshow(np.array([diff[i] for i in range(len(blocks))]), aspect="auto", cmap="viridis")
plt.xlabel("Timestep")
plt.ylabel("Block")
plt.colorbar(label="Relative L1 distance")
plt.savefig("block_heatmap.png", dpi=150, bbox_inches="tight")

对照 BWCache 论文中的图:各模块的变化随去噪步怎样变化?是否也是开头和结尾变化大、中间变化小?不同模块之间有没有差别?换几个提示词再画一次。

运行官方代码 ​

做完前 4 步,就可以运行组内论文的官方代码了:

论文代码支持的模型
BWCache(ICLR 2026)hsc113/BWCacheOpen-Sora 等视频模型
PrediT(ACM MM 2026)hsc113/PrediTFLUX.1-dev、DiT-XL/2、HunyuanVideo
SCOPE(ACM MM 2026)hsc113/SCOPEMAGI-1、SkyReels-V2

按各仓库的 README 安装。PrediT 和 SCOPE 可以先在没有 GPU 的电脑上运行单元测试,确认安装正确后再上服务器跑模型。复现的顺序和注意事项见复现组内论文。

现成的加速工具 ​

  • Diffusers 内置了几种缓存方法(如 First Block Cache、Pyramid Attention Broadcast、FasterCache),用法见官方文档的缓存章节。
  • cache-dit:统一的 DiT 缓存加速库,支持较多模型。
  • SGLang Diffusion 集成了 TeaCache、TaylorSeer 等方法。
  • xDiT:多卡并行推理 DiT。

这些工具适合做对比实验的 baseline。

视频生成模型 ​

模型说明
Wan2.2阿里通义万相,Apache-2.0 许可。TI2V-5B 版本可在单张 4090 上生成 720p 视频
HunyuanVideo 1.5腾讯混元,约 8.3B 参数,可在消费级显卡上运行;许可证有使用限制
LTX-2 系列Lightricks 开源,支持音视频同步生成;注意许可证条款
CogVideoX智谱开源,较早但资料多
Open-Sora全流程开源(含训练),适合研究训练与推理全链路
Stable Video Diffusion2023 年的 UNet 结构图生视频模型,已较旧,作为了解历史

视频模型更新很快,选用前先查看各项目的最新版本、显存需求和许可证。

其他生成任务(按需了解):口型同步 LatentSync、Wav2Lip;图像驱动对口型 Sonic;人脸动画 SkyReels-A1(组内实测单张 4090 显存不够)。

常见问题 ​

  • 显存不足:使用 pipe.enable_model_cpu_offload() 代替 .to("cuda"),或换更小的模型;计时实验要在不 offload 的条件下做,否则数据没有意义。
  • 下载模型慢:设置 HF_ENDPOINT,见网络加速。
  • 结果每次不一样:固定 generator 的随机种子,对比实验必须用相同的种子和提示词。

作者:崔涵帅、姚智、王振胜、唐志清 · 边缘智能系统实验室