简介:阿里Wan2.1视频加速方案是一份面向ComfyUI使用者与视频创作者的轻量级代码资源,借助TeaCache缓存技术,着力解决大模型生成视频时耗时较长的问题。实际部署时,只需通过ComfyUI-WanVideoWrapper插件在现有流程中增加一个节点,无需额外下载模型,即可快速接入加速能力。压缩包内共包含3个文件,包括inscode环境配置、html说明文档以及gitignore工程规范,整体体积仅6KB,下载与审阅都非常轻便。截至目前,已有117人学习下载,适合具备一定工作流基础的开发者深入试用。代码包提供rel_l1_thresh和加速起始步数两项可调参数,便于平衡生成速度与画面质量;根据评测,在720×480分辨率、81帧输出下,迭代10步可提速15%,迭代20步可提速近40%,且对成片质量影响极小。通过阅读源码与配置样例,可以理解TeaCache加速机制的核心思路,并快速迁移到自身Wan2.1项目中,显著提升视频创作效率。
1. Wan2.1 视频加速方案:先弄清楚慢在哪,再下手也不迟
Wan2.1 是当前开源视频生成模型里热度非常高的一款,但更多开发者第一次跑通后,第一反应往往是“这也太慢了”。生成十几秒的视频画面,在单卡上动辄要等几分钟,调一处 prompt 又要从头等一遍,反复改几轮之后基本失去耐心。标题里的“视频加速方案[代码]”,说的不是把播放器调流畅,而是把视频生成推理这条链路——从文本编码、DiT 迭代去噪到视频解码——逐段拆开,用算子替换、图编译、缓存跳过、步数裁剪这几板斧把单次生成的等待时间压下来。这篇内容适合手里有单卡、想本地复现或跑小规模份数的开发者:新手可以按步骤抄代码,熟手可以直接拿参数表和避坑清单做对照。下面所有方案都以我实际跑通过的经验为基础,代价是踩了不少坑。
2. 为什么 Wan2.1 这么慢:管线拆解与耗时定位
很多人一说到视频生成加速,第一反应就是“换更大的显卡”。但实际上如果不弄清时间花在哪个环节,换了卡也只是把等的时间从十分钟变成八分钟,治标不治本。Wan2.1 的推理链路拆开看并不复杂,真正拖后腿的是三个环节叠在一起:文本编码、去噪迭代、视频解码。下面把每个环节的耗时角色说清楚,才知道钱和优化精力应该花在哪。
2.1 生成链路里的三个耗时大户:文本编码器、DiT 主干和 3D VAE
Wan2.1 的推理流程可以理解成三段:先把 prompt 送进文本编码器得到条件向量;再把它和随机噪声一起送进 DiT 主干,做几十轮迭代去噪;最后把去噪后的潜变量交给 3D VAE 解码,还原成视频帧。三者的耗时特征完全不同。文本编码器虽然庞大,但整条推理里只跑一次,加载权重和编码 prompt 会花一点时间,可它不参与迭代循环,属于固定开销。DiT 主干是真正的重头戏,每一轮去噪都要把它完整跑一遍,因此它的单次延迟直接乘以迭代步数。很多人会忽略最后的 3D VAE 解码:长视频、高分辨率下解码阶段能占到总耗时的 20% 以上,显存也会在这里突然冲高。
如果只盯着“DiT 太慢”这一个点,很容易忽略 VAE 解码带来的隐性瓶颈。我常见到的情况是:DiT 阶段用各种手段优化得很好,结果最后 VAE 解码直接吃满显存,整体耗时依旧难看。所以做加速之前,最好先做一次端到端计时,搞清楚每一段各占多少秒。这一步花不了十分钟,但能让后面所有优化动作都有的放矢。
2.2 为什么 DiT 那样重:序列长度、掩码与去噪迭代的乘法效应
DiT 慢的根源可以从两个层面理解。第一,视频比图像多了时间维度,模型把连续帧切成大量 patch 后拼成一个超长序列,注意力机制的计算量随序列长度近似平方增长。第二,去噪不是跑一次,而是几十次迭代,每次迭代都要重复这个高成本计算。这两件事乘在一起,就形成了“单个 step 慢”和“step 数量多”的双重打击。
Wan2.1 使用的注意力结构还带了额外的掩码处理。视频形态下,注意力不是让所有帧无差别互相看的,而是存在局部窗口、前后帧关联等限制条件,掩码参与计算会导致一些通用加速内核无法直接套用。这也是为什么很多人换上普通 FlashAttention 后没有明显收益,反而出现画面错乱。因为掩码没传对,模型看到的“上下文关系”就变了。
理解这一点对加速很有用:优化 kernel 时要确保 mask 被原样传递;做步数裁剪时要意识到“少跑一步”等于“少跑一整轮所有层的计算”;做缓存跳过时要尽量挑那些对掩码不敏感的层下手。把这三个乘法因子拆开看,加速思路就清晰了。
2.3 四类加速手段的优先级:算子、图、算法与显存
针对 Wan2.1 的加速,主流做法可以分成四类,它们的见效幅度、实现成本和风险各不相同。我习惯按下表的顺序来考量,而不是一上来就上最强手段。
| 方向 | 代表手段 | 典型收益 | 实现成本 | 主要风险 |
|---|---|---|---|---|
| 算子级 | FlashAttention / SageAttention 替换原生注意力 | 10%~30% | 低,只需替换 forward | 数值精度、mask 传参 |
| 图级 | torch.compile 编译 DiT 主干 | 5%~20% | 中,需处理编译期 | 显存碎片、首次编译慢 |
| 算法级 | TeaCache 跳层、减少采样步数、蒸馏 LoRA | 30%~60% | 中,参数敏感 | 质量下降、画面闪烁 |
| 资源级 | VAE 分块解码、CPU offload、半精度加载 | 解决 OOM 或带宽瓶颈 | 较低 | 速度不一定提升 |
算子级和算法级的收益最直接,建议优先做。图级编译视你的 PyTorch 版本而定,有时候收益不明显,但通常不会让速度变慢。资源级更多是解决“能不能跑”的问题,当显存不够导致 OOM 时再引入它。常见的错误是一上来就把 CPU offload 打开,结果显存省了,但每轮迭代都在做数据传输,整体耗时反而翻倍。
3. 先立基线再动手:替换注意力算子与打开编译
做任何加速之前,第一件事是建立可复现的基线数据。没有基线,后面所有优化都无法判断是真实收益还是测量误差。这一章给出一个简单但严谨的测速方法,然后分别演示算子级加速和 torch.compile 的实操方法,代码可以直接复制修改。
3.1 用计时脚本建立基线的正确姿势:warm-up 与固定参数
很多人第一次测速,跑一个视频生成就记录耗时,然后拿这个数字当基线。这个数字通常偏慢很多,因为第一次运行要完成 CUDA kernel 加载、显存分配、模型权重初始化等一次性工作。正确做法是先跑一次“热身”,再连续测多次取平均值。测试时还要固定 prompt、分辨率、帧数和采样步数,否则结果没有可对比性。
import time import torch def warmup_and_benchmark(generate_fn, prompt: str, steps: int, repeat: int = 5): """ 先跑一次 warm-up,再测 repeat 次取平均。 generate_fn 是已经封装好的生成函数,参数固定。 """ # 预热:让 CUDA kernel、显存分配、cuDNN 搜索都稳定下来 generate_fn(prompt=prompt, steps=steps) torch.cuda.synchronize() times = [] for i in range(repeat): t0 = time.perf_counter() generate_fn(prompt=prompt, steps=steps) torch.cuda.synchronize() # 确保 GPU 计算真正结束再计时 times.append(time.perf_counter() - t0) avg = sum(times) / len(times) print(f"warm 后平均耗时: {avg:.2f}s, 各次耗时: {[f'{t:.2f}' for t in times]}") return avg这段脚本的逻辑很简单:先用一次完整生成让 GPU 进入稳定状态,再连续测五次取平均。torch.cuda.synchronize()是关键,没有它计时器可能在 GPU 还在排队时就已经读完了时间,测出虚假的“快”。我在实际项目里见过有人因此把一次 80ms 的 kernel 当成 1ms 在优化,方向完全跑偏。
测试参数要固定到和真实使用场景一致。Wan2.1 这类模型对分辨率、帧数非常敏感,不同尺寸下的耗时差距可能超过一倍。建议在脚本里把宽度、高度、帧数、步数全部显式写死,不要从配置文件里读默认值,否则后面改动配置时基线就失效了。
3.2 替换 attention 为 PyTorch 内置 SDPA:掩码必须原样传递
Wan2.1 的 DiT 主干内部有大量注意力模块,原生实现不一定调用了最高效的内核。PyTorch 自带的scaled_dot_product_attention已经能覆盖多数场景,并且不需要额外安装任何依赖。如果你的环境里有 SageAttention 这类加速库,替换逻辑完全相同,只是把内部函数换一下。下面这段代码演示如何扫描模型里的 attention 模块并替换其 forward 实现。
import torch.nn.functional as F def patch_attention_to_sdpa(model): """ 遍历模型,把名字里带 attention 的模块 forward 替换成 SDPA。 返回被替换的模块数量,便于确认是否生效。 """ patched = 0 for name, module in model.named_modules(): if "attention" not in name.lower(): continue # 保留原 forward,必要时回退用 orig_forward = module.forward def forward_with_sdpa(q, k, v, mask=None, *args, **kwargs): # 视频 DiT 的 mask 往往是 float 类型的局部注意力掩码, # 必须原样传给 SDPA,不能随手丢掉。 return F.scaled_dot_product_attention(q, k, v, attn_mask=mask) module.forward = forward_with_sdpa patched += 1 return patched这个 patch 的核心逻辑是:把原来手写 attention 计算的 forward 改成调用 SDPA,权重完全不改,只换执行路径。需要注意的是,不同版本的 Wan2.1 注意力模块的 forward 签名不完全一样,有些把 mask 放在位置参数里,有些放在关键字参数里。我在自己的环境里是先打印几个 attention 模块的__dict__确认参数名,再决定 wrapper 的写法,避免盲目替换导致调用报错。
替换后可以先用非常短的帧数跑一次,确认画面没有出现错位或黑块,再测速度。SDPA 的收益在长序列下更明显,Wan2.1 的视频 patch 序列往往很长,所以通常能看到 10% 以上的整体耗时下降。如果换了之后没效果,先检查是不是替换没有真正生效——打印一下patched的数量,如果为 0 说明模块命名规则和预期不一致。
3.3 torch.compile 的正确打开方式:只编译 DiT 主干,别编译整个 Pipeline
torch.compile 是另一条见效路径。它的原理是把模型的计算图做一次预先编译,融合部分 kernel 并减少 Python 调度开销。很多教程直接对整个 pipeline 调用 torch.compile,在我实际测试中这不是个好主意——文本编码器和 VAE 也被纳入编译范围,会导致编译时间大幅增加、显存碎片变多,而收益几乎没有增加。
更稳妥的做法是只对 DiT 主干做编译。Wan2.1 的 pipeline 对象通常会有一个存放 DiT 模型的空间,拿到它的引用,单独编译,再放回去。
import torch # dit_model 是从你的 pipeline 里取到的 DiT 主干 dit_model = pipeline.dit_model # 具体属性名以你的加载代码为准 dit_model = torch.compile( dit_model, mode="reduce-overhead", # 减少每次调用的调度开销,通常收益最均衡 fullgraph=True, # 要求整个模型编译成一个图,失败时会报错 ) # 再放回 pipeline pipeline.dit_model = dit_model这里mode="reduce-overhead"是我个人比较推荐的模式,它不会像max-autotune那样做长时间矩阵形状搜索,编译时间短很多,收益差距却不大。fullgraph=True会强制整个模型编译成单个图,如果模型里有不支持的算子,编译会直接报错;新手遇到报错可以先把它改成False再试,但速度收益也会打折。
有一个容易被低估的点:torch.compile 的第一次调用会非常慢,可能持续好几分钟,CPU 跑满、GPU 空闲,这常常被误认为“卡死了”。实际上它正在编译,等日志里出现编译完成信息后,后续调用才会快。所以测速时务必先做一次 warm-up,并且把编译产物目录设置为固定路径,让它能复用之前的编译缓存。显存方面,编译后的模型有时会比原来多占用几百 MB,如果显存本身很紧,可以考虑先做显存管理再编译。
4. 进一步压缩采样成本:步数裁剪、TeaCache 与显存腾挪
算子替换和编译只是优化了“每一步多快”,而真正能把总耗时砍半的手段,往往在“需要跑多少步”这一层。Wan2.1 默认去噪步数偏保守,实际使用时可以适当压缩;社区里的 TeaCache 方案更是可以直接跳过一部分层的计算。这一章的参数调整属于收益最高的一类优化,但也是翻车重灾区。
4.1 采样步数并非越少越好:Wan2.1 的 CFG 与调度器取舍
Wan2.1 这类扩散模型默认使用 50 步去噪,这是官方为保底质量给出的数字,不代表每一步都必不可少。实际测试中,将步数从 50 降到 30,画面质量损失通常可以接受,耗时直接下降 40%。但继续往下压,运动连贯性和细节会明显劣化,出现“PPT 感”或画面闪动。原因是视频模型对去噪轨迹更敏感,不像图像模型那样用 20 步依然能看。
CFG(无分类器引导)的处理也要讲究。很多加速教程建议降低 CFG 来省时间,因为 CFG 意味着每一步要跑两次模型。但对 Wan2.1 来说,CFG 降到 3.0 以下,画面会明显发灰、对比度下降,人物和背景之间甚至会出现“糊边”。我在对比中发现,保持 CFG 在 4.5~5.0 区间,只缩减步数,是画质和速度之间最稳的平衡点。
| 参数 | 保守默认值 | 加速建议值 | 说明 |
|---|---|---|---|
| num_inference_steps | 50 | 25~30 | 低于 20 步容易闪 |
| guidance_scale | 5.0 | 4.5~5.0 | 不要低于 3.5 |
| 分辨率 | 较高 | 视显存而定 | 降低分辨率对速度影响最直接 |
| 帧数 | 81 起 | 按场景裁剪 | 帧数线性影响耗时与显存 |
还有一个值得尝试的方向是走蒸馏路线。社区里有一些针对视频模型的蒸馏 LoRA,可以把有效步数降到 8~12 步还保持画面稳定,这比硬调调度器参数省心。但蒸馏 LoRA 和 Wan2.1 不同版本之间的兼容性参差不齐,加载后要跑几段不同 prompt 验证运动幅度和文字一致性,不能只看着一段视频正常就觉得成功。
4.2 TeaCache 缓存机制与 rel_l1_thresh 调参:跳层但不跳崩
TeaCache 是社区里常用的一种视频生成加速方案,核心直觉是:去噪过程相邻时间步的隐层状态变化很大,但某些层的输出其实很相似,没必要每步都完整计算。于是它把某一层的输出缓存下来,当检测到当前步与上一步差异小于阈值时,直接复用缓存结果,跳过该层的计算。听起来很理想,但跳得太多或跳得不是地方,画面就会崩。
# TeaCache 的常见配置项,接入方式以你使用的脚本为准 teacache_cfg = { "enable": True, "start_step": 3, # 前几步不跳,保证基础结构生成 "end_step": 20, # 最后几步不跳,避免收尾时崩坏 "cache_type": "random", # 缓存计算方式:random 或 avg "rel_l1_thresh": 0.15, # 相似度阈值:越小越保守 }我一开始直接用了rel_l1_thresh=0.25,生成速度确实快了很多,但视频里物体的边缘开始出现抖动和残影。后来把阈值降到 0.15,并把start_step从 1 改成 3,画面才稳定下来。这个参数的核心含义是“当前输出和缓存输出之间的相对差异小于阈值时,用缓存顶替”,所以阈值越大,跳过越激进,风险也越高。建议从 0.1 起步,每次加 0.05,看到画面出现断续或闪烁就回退。
另一个容易踩的点是,TeaCache 不要对整个模型的所有层无差别生效,至少要保留前若干层和最后一两层不参与缓存。前几层的特征质量决定了整体结构,最后几步关系到画面收敛,这两个位置偷懒最容易翻车。你可以在配置里额外加一层“跳过层名单”,把 cross-attention 相关的层排除掉。社区有人贪图省事把全部层都开缓存,结果是视频里人物转头动作变得非常机械。
4.3 VAE 分块解码与 CPU offload:当显存不够但还想跑长视频
显存不足是 Wan2.1 落地的经典难题。很多人以为 DiT 去噪阶段是显存大头,实际跑起来才发现,最后一步的 3D VAE 解码直接让显存冲顶。原因是视频解码有三维卷积和时间维缓存,长帧数、高分辨率叠加后,中间激活一路积攒到解码结束才释放。最直接的解法是分块解码,也就是把潜变量切成若干小块,分别解码再拼接,避免一次性把所有中间结果都放在显存里。
# 以 diffusers 类 pipeline 为例,启用 tiling 的示意配置 pipeline.enable_vae_tiling() # 启用 VAE 分块解码 pipeline.enable_sequential_cpu_offload() # 显存仍不够时的后备方案enable_vae_tiling会牺牲一点拼接痕迹,但只要切块重叠区设置合理,肉眼几乎看不出来。切块尺寸不宜太小,太小会让解码出来的画面在拼接处出现亮度不一致,建议保持默认或者在默认基础上只调整纵向切块数。enable_sequential_cpu_offload则是把非当前执行的模块放到 CPU 上,需要用时再搬回 GPU,代价是 PCIe 传输开销,速度可能下降 10%~30%。我的习惯是先上 tiling,解决了解码 OOM 就不要动 offload,因为它对测试节奏的干扰很明显。
如果你用的是自己写的推理脚本而不是现成 pipeline,VAE 分块需要手动实现。思路是把潜变量的时间维和空间维切成块,逐块 decode,再用权重插值处理好重叠区。这个实现有一定工作量,但能救回很多“差一点就能跑”的素材。对于只跑短片段、显存充裕的开发者,这两项都可以不开,保持默认速度。
5. Wan2.1 加速避坑:5 个翻车现场与排查清单
这一章把我自己实际遇到过的几个经典问题整理成清单,每一条都是“现象 → 原因 → 解决”的结构。如果你在操作过程中遇到类似情况,可以直接对照排查。
5.1 编译首跑像死机:预热与测速玄学
现象:执行 torch.compile 后的第一次生成,CPU 占用拉满,GPU 却几乎不动,持续好几分钟,看起来像程序卡死。有人甚至直接强制杀进程重来,结果每次都在同一位置卡很久。
原因:这是编译期在生成优化后的计算图,不是死机。torch.compile 会把模型算子做融合和调度重构,这一过程受模型规模影响,Wan2.1 这种大体量模型编译几分钟属于正常。另外,首次调用时 CUDA kernel 也在缓存加载中,测到的耗时会异常偏高。
解决:编译时把torch.compile的产物目录固定到本地磁盘,比如设置环境变量TORCHINDUCTOR_CACHE_DIR;编译完成后先小规模跑一次再正式测速。测速时至少跑一次 warm-up,否则第一次的编译开销会污染所有后续数据。
5.2 换成了加速注意力,画面出现黑块与条纹
现象:替换为 SDPA 或 SageAttention 后,生成视频中出现大面积黑块、彩色噪点条纹,运动场景尤其明显。
原因:注意力掩码没有正确传递。Wan2.1 的注意力带有视频局部关联掩码,替换 forward 时如果只传了 q、k、v,丢掉 mask,模型就失去了帧间的关联约束,输出去噪特征错位,最终呈现为画面损坏。
解决:检查替换代码是否把 mask 参数完整透传。建议替换后先用 9 帧、低分辨率的短片段验证,确认画面正常后再上长视频。另外,部分加速内核要求 mask 是 float 类型且用特定数值填充,如果你用的是第三方注意力库,先读它的接口文档,不要默认参数名一致。
5.3 TeaCache 跳步省了一半时间,画面却在“闪”
现象:开启 TeaCache 后生成速度确实变快,但视频里人物面部、背景边缘反复闪烁,物体运动轨迹不连贯。
原因:缓存阈值rel_l1_thresh设置过大,导致差异较大的步也错误复用了缓存。另一个常见原因是缓存跳过的层包含了 cross-attention 层,这部分负责文本与视觉信息的对齐,一旦跳过,语义一致性就会被破坏。
解决:阈值降到 0.15 以下,start_step至少为 2~3。排查时先关闭所有缓存,只开其中一层,逐步增加跳过的范围,直到找到闪动的边界。自动化脚本里我会把阈值作为命令行参数传入,方便快速扫描不同值的画面差异。
5.4 VAE 解码长视频爆显存:梯度检查点救不了推理
现象:生成 5 秒短视频没问题,换成 10 秒以上长视频时,DiT 阶段运行正常,但接近结尾直接报 CUDA OOM。
原因:DiT 阶段为了省钱可能开了梯度检查点来减少显存占用,但推理阶段根本不需要梯度。OOM 的真正来源是 VAE 解码时,时间维和空间维的中间激活同时驻留显存。长视频的潜变量更大,解码器的三维卷积在逐帧展开时会累积大量中间缓存。
解决:启用分块解码(tiling),把潜变量切成小块分别解码。如果显存还不够,就把输出分辨率降到模型允许的下限。不要为了省显存关闭 VAE 的部分卷积层,那会直接导致画质崩坏。
5.5 半精度加载后色彩发灰、对比度不对
现象:模型用 FP16 或 BF16 加载后,生成的视频明显发灰,颜色饱和度下降,部分场景出现过曝或暗部死黑。
原因:扩散模型对潜变量归一化范围和中间层的数值精度很敏感。半精度虽然能省一半显存,但某些中间层一旦超出半精度的表示范围,数值就会被截断,最终反映为色彩失真。不同版本和不同硬件上,这个现象的表现程度也不同。
解决:先别急着给所有模块开半精度。把文本编码器和 VAE 保留在更高精度,只让 DiT 主干使用半精度;如果问题依旧,检查潜变量是否按模型默认的均值和方差做了归一化。这个过程属于排错型调整,建议每次只改动一个精度开关,对比两段视频的色彩差异,而不是一次把所有模块都改成半精度。
6. 加速效果验证:从“感觉快了”到“可复现的快”
加速做完了,最怕的是拿不出一个能说服自己的数据。很多人只留下“好像快了”的印象,过两天改了一版代码后,很难判断是变快了还是变慢了。这一章给出一个简单但严谨的验证套路:把测量误差压住,再做质量对照。
6.1 用日志和锁频把测量误差压住
GPU 的 Boost 频率会让连续几次推理的耗时上下浮动,测速数据看起来很不稳定。要拿到可信的数字,可以先把 GPU 锁定到一个固定频率,再配合日志记录每次生成的关键节点耗时。
import os import time import torch # 锁频示例:具体频率范围按你的显卡型号调整 os.system("nvidia-smi -lgc 1500,1600") def timed_generate(generate_fn, prompt: str, steps: int): torch.cuda.synchronize() t0 = time.perf_counter() frames = generate_fn(prompt=prompt, steps=steps) torch.cuda.synchronize() total = time.perf_counter() - t0 print(f"[timed] steps={steps} total={total:.3f}s") return frames, total锁频需要 root 权限,某些云主机上会失败,失败时去掉这行即可。日志里除了总耗时,最好把每步平均耗时打出来,也就是total / steps。这个数字能帮你快速判断优化的效果到底出在“单步变快”还是“步数变少”。如果单步耗时没变化,说明算子替换或编译没生效;如果单步耗时下降很多,但总耗时没怎么变,就需要检查是不是开销都被调度和排队吃掉了。
6.2 质量对照:同一段 prompt 跑四组,肉眼判断可交付
速度有了数据,还要验证质量没有崩。我的习惯是固定一段包含运动、人脸、文字三个元素的 prompt,分别跑“原始配置、仅算子替换、仅步数压缩、全套加速”四组视频,时长保持 5 秒左右,分辨率一致。然后把四段视频放在同一画面里轮流播放,重点观察三个位置:人脸五官是否稳定、运动物体边缘有没有残影、画面色彩有没有明显偏移。
这个过程没有量化指标,但它是决定“能不能交付”的最后一道卡口。速度优化得再漂亮,如果画面闪烁,就没有任何意义。所以在完整跑一批素材之前,我会先花十几分钟做这个对照,把有问题的配置找出来关掉。
我现在养成的习惯是:每一次调整加速参数,都在日志里附上当时用的阈值、步数、编译模式的记录,否则三天后回来看,根本不知道哪组参数才是最终可用的版本。视频生成加速本身就是个反复“调参—验证—回退”的过程,把验证做扎实,比多试几种激进配置更重要。希望这些经验能帮你在 Wan2.1 上少走弯路,早点跑出满意的速度与画面。
本文还有配套的精品资源,点击获取