diffusers 中 Wan-Animate-2 角色动画 ModularPipeline 完整实战指南:分段参考提取与蒸馏采样
【免费下载链接】diffusers🤗 Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch.项目地址: https://gitcode.com/GitHub_Trending/di/diffusers
本篇技术指南围绕 Hugging Face diffusers 仓库中对阿里 Wan 团队 Wan-Animate-2 模型的官方支持展开,讲解如何用一张参考角色图像 + 一段驱动视频(driving video)生成角色跟随视频,并深入剖析其"定长分段 + 逐段参考 K/V 提取 + 循环内解码"的模块化流水线实现,以及基础版与蒸馏版两套预设的差异。读完本文,你将掌握从模型加载、显存优化到参数调优的完整实战方案,并能从源码层面理解每个分段内部究竟发生了什么。
Wan-Animate-2 在 diffusers 中的定位
Wan-Animate-2(由阿里 Wan 团队提出,仓库即 Wan-Video/Wan2.2)是一个角色动画(character animation)模型:它把一张参考角色图像的外观与一段驱动视频的运动"嫁接"在一起,输出角色按照驱动动作表演的新视频。在 diffusers 中,它被实现为基于ModularPipeline的模块化流水线,核心源码位于 src/diffusers/modular_pipelines/wan_animate_2/,测试位于 tests/modular_pipelines/wan_animate_2/test_modular_pipeline_wan_animate_2.py。
驱动视频不是一次整段处理的,而是被切分为定长分段(fixed-length segments),每个分段依次经历三个阶段:
- 参考提取(reference-extraction pass):把该分段送入 Transformer,在每一层提取并缓存该分段的 K/V(键值对);
- 去噪(denoise):在去噪的每一步,生成 token 通过注意力机制共同关注本分段缓存的参考 K/V,从而让生成结果对齐驱动运动;
- 循环内解码(decode inside the loop):解码必须在分段循环内部完成,因为下一个分段要以当前分段解码后的尾部帧作为条件,运动连续性是在像素空间而非隐空间上跨分段传递的。
官方文档(即本仓库中的 docs/source/en/api/pipelines/wan_animate_2.md)提供了完整的可直接运行示例,本文将在其基础上逐层深入。
快速上手:加载流水线并生成角色动画
以下示例完整继承自官方文档,可直接复制运行。它使用ModularPipeline通用加载入口,配合load_components以bfloat16精度加载组件:
import torch from diffusers import ModularPipeline from diffusers.utils import export_to_video, load_image, load_video pipe = ModularPipeline.from_pretrained("Wan-AI/Wan2.2-Animate-2-14B-Diffusers") pipe.load_components(dtype=torch.bfloat16) # The transformer weights and the per-segment reference KV cache do not co-reside on one 80 GB # card at the default resolution, so stream the transformer's blocks. Compiling the blocks is # required as the in-context attention runs on the flex backend pipe.transformer.enable_group_offload( onload_device=torch.device("cuda"), offload_device=torch.device("cpu"), offload_type="block_level", use_stream=True, ) pipe.text_encoder.to("cuda") # or "mps", "xpu", "cpu" pipe.image_encoder.to("cuda") pipe.vae.to("cuda") pipe.transformer.compile_repeated_blocks(fullgraph=False) # The first demo from the official repository: https://github.com/Wan-Video/Wan-Animate-2 demo = "https://raw.githubusercontent.com/Wan-Video/Wan-Animate-2/main/examples/demo1" image = load_image(f"{demo}/reference.png") driving_video, driving_video_fps = load_video(f"{demo}/template.mp4", return_fps=True) prompt = "人物外观描述:一只银灰色虎斑纹的小猫,拥有圆润的脸庞、竖立的耳朵和巨大的圆形眼睛。它身穿一套深蓝色的制服套装,包括一件带有金色纽扣的西装外套和一条百褶裙。外套里面搭配着白色衬衫,领口处系着一个红色的蝴蝶结,袖口露出白色的衬衫边缘。背景描述:背景为纯白色,光线均匀明亮,无其他杂物或装饰。" videos = pipe( image=image, driving_video=driving_video, driving_video_fps=driving_video_fps, prompt=prompt, output="videos", ) export_to_video(videos[0], "output.mp4", fps=24)代码中的关键点说明:
load_video(..., return_fps=True)返回驱动视频的原始帧率,流水线会据此将驱动视频重采样到模型生成帧率fps(默认 24);output="videos"是模块化流水线风格的输出命名参数,最终返回视频列表videos;export_to_video(videos[0], "output.mp4", fps=24)把首段视频以 24 fps 写盘;pipe.transformer.enable_group_offload(...)与compile_repeated_blocks(fullgraph=False)属于显存优化与性能必需项,详见下文"显存与性能"一节。
两套预设:基础版与蒸馏版
diffusers 为 Wan-Animate-2 提供了两套检查点预设,二者唯一的加载差异就是模型仓库 ID,其余调用方式完全一致:
| 预设 | 模型仓库 ID | 采样步数 | 引导方式 |
|---|---|---|---|
| 基础版 | Wan-AI/Wan2.2-Animate-2-14B-Diffusers | 40 步(默认) | 分类器自由引导(CFG),guidance_scale=3.0 |
| 蒸馏版 | Wan-AI/Wan2.2-Animate-2-14B-Distilled-Diffusers | 10 步(默认) | 无 CFG,guider 被固定在guidance_scale=1.0 |
关键差异可以从测试用例 tests/modular_pipelines/wan_animate_2/test_modular_pipeline_wan_animate_2.py 中直接印证:基础版配置{"num_inference_steps": 40}且 guider 配置{"guidance_scale": 3.0},蒸馏版则是{"num_inference_steps": 10}与{"guidance_scale": 1.0}。
需要特别注意的是,整个流水线不存在guidance_scale参数:引导完全由流水线的 guider 组件(ClassifierFreeGuidance)接管。基础版的 guider 需要同时跑条件分支与无条件分支(is_uncondtion分别传False/True),蒸馏版则只跑条件分支。这一定义位于 src/diffusers/modular_pipelines/wan_animate_2/denoise.py:
WanAnimate2SegmentDenoiseInner的expected_components中以FrozenDict({"guidance_scale": 3.0})创建 guider;WanAnimate2DistilledSegmentDenoiseInner继承前者,但 guider 配置改为FrozenDict({"guidance_scale": 1.0}),注释明确指出"蒸馏模型专为少步采样训练、不使用 CFG,因此只运行条件分支"。
两个预设分别对应WanAnimate2ModularPipeline与WanAnimate2DistilledModularPipeline(见 src/diffusers/modular_pipelines/wan_animate_2/modular_pipeline.py),后者的default_blocks_name指向WanAnimate2DistilledBlocks。
分段循环:源码级的执行流程拆解
从源码结构看,整个分段循环被组织为一个WanAnimate2SegmentLoopWrapper(继承LoopSequentialPipelineBlocks),基础版与蒸馏版分别组合出WanAnimate2DenoiseStep与WanAnimate2DistilledDenoiseStep。以基础版为例,每个分段内部依次执行 7 个子步骤(见 denoise.py):
vae_encoder -> prev_frames -> prepare -> scheduler_reset -> ref_extract -> denoise_inner -> decode各子步骤的职责如下:
| 子步骤 | 类 | 核心行为 |
|---|---|---|
vae_encoder | WanAnimate2SegmentVaeEncoderStep | 对本分段的驱动视频切片做 VAE 编码,并叠加 i2v 条件掩码,得到驱动分段隐变量driving_video_latents与条件张量driving_video_condition |
prev_frames | WanAnimate2SegmentPrevFramesStep | 把上一分段解码出的尾部帧(首分段为全零)VAE 编码、掩码后与参考图像隐变量堆叠,构成生成侧条件张量reference_latents;这是运动连续性跨分段的载体 |
prepare | WanAnimate2SegmentPrepareStep | 为本分段抽取初始噪声,并新建一个全新的参考 K/V 缓存WanAnimate2KVCache |
scheduler_reset | WanAnimate2SegmentSchedulerResetStep | 重置调度器——每个分段都是独立去噪轨迹,求解器状态与时间步需逐段重新准备 |
ref_extract | WanAnimate2RefExtractStep | 以kv_cache_mode="extract"运行 Transformer,一次性编码驱动分段并把每一层的参考 K/V 存入缓存 |
denoise_inner | WanAnimate2SegmentDenoiseInner | 内层时间步循环,以kv_cache_mode="cached"逐时间去噪,去噪前向关注缓存的参考 K/V |
decode | WanAnimate2SegmentDecodeStep | VAE 解码本分段,完成帧移到 CPU,释放本分段的 K/V 缓存与隐变量 |
值得强调的两个设计动机(源码注释明确写出):
- 为什么解码必须在循环内:下一个分段要以上一分段的解码后像素为条件,且"运动连续性在像素空间而非隐空间跨分段传递"——这正是
WanAnimate2SegmentPrevFramesStep在像素域取上一段尾部帧并重新 VAE 编码的原因(见 denoise.py); - 为什么解码后要立即释放资源:完成帧被搬到 CPU,并调用
block_state.kv_cache.clear()与torch.cuda.empty_cache()——"跨分段保留它们会把分配器碎片化到足以在高分辨率下 OOM"(见 denoise.py)。
另外,WanAnimate2SegmentVaeEncoderStep的说明还揭示了一个微妙的技术点:Wan VAE 在时间维度上是因果的,因此"整段视频一次性编码再切片"与"每个分段在其自身切片上重新开始时间卷积"并不等价——这正是每个分段都必须单独 VAE 编码的原因。
参考提取与 K/V 缓存的底层原理
参考提取的核心实现在 Transformer 模型 src/diffusers/models/transformers/transformer_wan_animate_2.py 中,WanAnimate2KVCache承载每一层的参考 K/V。kv_cache_mode只有两种取值:
"extract"(参考提取):对参考 token 做密集自注意力,并把每一层的 K/V 写入缓存;该模式下时间步被固定为 1(源码中timestep_input = timestep * 0 + 1),因为这一步不是去噪而是"记忆";"cached"(生成):生成 token 与缓存的参考 token联合参与注意力,通过 flex attention 后端完成 in-context 注意力计算。
正是"in-context attention 运行在 flex 后端"这一点,决定了官方示例必须调用transformer.compile_repeated_blocks(fullgraph=False)做编译:源码注释明确写道,未经编译时 PyTorch 的 flex attention 会回退到非编译实现,而编译是保证性能的前提。此外提取与生成两种模式还使用了不同的 RoPE 步长(rope_stride=self.refer_stride if kv_cache_mode == "extract" else 1)。
在流水线层面,参考提取前需要先算好与分段无关的几何量,这由WanAnimate2PrepareSegmentsStep(src/diffusers/modular_pipelines/wan_animate_2/before_denoise.py)完成:由于 zigzag 填充保证每个分段恰好segment_frame_length帧,因此隐变量网格、打包序列长度(max_seq_len、max_seq_len_ref)与噪声形状对所有分段一致,只需计算一次。它还校验驱动视频与参考图像的 letterbox 尺寸必须一致,否则抛出ValueError。
关键输入参数详解
结合 modular_blocks_wan_animate_2.py 中的自动文档字符串与测试用例中的默认值(test_modular_pipeline_wan_animate_2.py),完整参数清单如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
prompt | 必填 | 角色/背景外观描述提示词,必须是str类型(源码check_inputs强制校验) |
negative_prompt | None | 负向提示词;仅在 guider 需要无条件嵌入或显式传入时才会编码 |
prompt_ref | "人物动作的参考视频" | 驱动视频上下文的固定参考提示词,用于引导参考提取前向 |
max_sequence_length | 512 | 提示词编码的最大序列长度,超出部分截断、不足部分补零 |
image | 必填 | 承载待动画角色的参考图像 |
driving_video | 必填 | 提供运动的驱动视频(VideoProcessor.preprocess_video接受的任意格式) |
driving_video_fps | None | 驱动视频原始帧率;设置时按最近邻重采样到fps,为None时按原样使用 |
fps | 24 | 模型生成帧率 |
height/width | 800/640 | 生成视频的目标面积(不是硬性分辨率),详见下文 |
segment_frame_length | 81 | 每个推理分段的帧数 |
prev_segment_conditioning_frames | 1 | 从上一分段携带的条件帧数(分段间重叠量) |
num_inference_steps | 40(基础版)/10(蒸馏版) | 去噪步数 |
generator | None | 用于确定性生成的 torch 生成器 |
output_type | "np" | 解码视频的输出类型(np/pt/ PIL) |
height/width 的"面积"语义
height与width(默认 800 × 640)设定的是生成视频的目标面积(height * width),而非最终分辨率。实际帧尺寸会保持参考图像的宽高比,计算方式见 encoders.py 中的WanAnimate2ProcessImagesInputStep:
aspect_ratio = image_height / image_width max_area = block_state.height * block_state.width block_state.height = int(math.sqrt(max_area * aspect_ratio)) // mod_value * mod_value block_state.width = int(math.sqrt(max_area / aspect_ratio)) // mod_value * mod_value其中mod_value = vae_scale_factor_spatial * 2(即 16),保证分辨率满足 VAE 与 2×2 空间 patch 的可整除要求。驱动视频的每一帧随后被letterbox(保持宽高比缩放 + 黑色填充)到该分辨率;填充的"黑边"区域在最终输出时依据预处理阶段记录的crop_region裁剪掉(见 decoders.py 的WanAnimate2DecodeStep)。
一个实用提示(官方文档明确提到):输入已经处于目标 letterbox 尺寸时,预处理会原样通过,因此预处理也可以完全放到流水线外部完成,再直接传入已处理好的张量。
驱动视频的 fps 重采样与 zigzag 填充
在WanAnimate2ProcessVideosInputStep(encoders.py)中:
- 若传入了
driving_video_fps,先按最近邻重采样到fps(get_frame_indices实现); - 逐帧 letterbox 到目标帧尺寸;
- 计算分段数:由于分段间重叠
prev_segment_conditioning_frames帧,每段实际前进effective_segment = segment_frame_length - prev_segment_conditioning_frames帧; - 若尾部剩余帧不足以凑满一个完整分段,则用zigzag(镜像)填充补齐,例如
[0 1 2 3 4]补 3 帧变成[0 1 2 3 4 | 4 3 2]。填充帧对模型而言是"真实内容",但为填充帧生成的多余帧会在最终拼接时按real_frame_len裁掉。
WanAnimate2SegmentVaeEncoderStep的另一个细节:每个分段的驱动切片都要单独 VAE 编码,因为 Wan VAE 时间上因果,"整段编码后切片"并不等价。
letterbox 处理器的一像素差异
驱动视频与参考图像都使用自定义的 WanAnimate2VideoProcessor:resize_mode="fill"保持宽高比并把剩余区域填充为fill_color(默认黑色)。其文档字符串特别强调,内容的粘贴位置是((height - src_h) // 2, (width - src_w) // 2)——这是参考实现的放置约定,与VaeImageProcessor的(height // 2 - src_h // 2, ...)在"帧尺寸为偶数、内容尺寸为奇数"时相差一行或一列,而这一像素的位移偏差是模型真实看到的内容差异,因此不能简单复用通用处理器。
显存与性能:分组卸载与编译
官方示例的前几行不是可选项,而是让默认分辨率(约 800 × 640)的推理跑得起来的必要条件:
- 分组卸载(group offload):Transformer 权重与逐段参考 K/V 缓存无法在默认分辨率下共驻一块 80 GB 显存(源码与文档均如此说明),因此把 Transformer 的 block 以"block_level"粒度在 CUDA 与 CPU 之间流式传输(
use_stream=True启用流式重叠); - 重复块编译:
compile_repeated_blocks(fullgraph=False)编译 Transformer 的重复块,因为 in-context 注意力运行在 flex 后端,未经编译会显著拖慢。
其余组件(text_encoder、image_encoder、vae)显式搬到目标设备,支持"cuda"/"mps"/"xpu"/"cpu"。整体加载使用load_components(dtype=torch.bfloat16)指定精度,14B 参数量级的模型配合低精度与卸载策略才能落在消费级到单卡 A100/H100 的可行区间内。
模块化 API 一览
本文档对应的公开 API(见 src/diffusers/modular_pipelines/wan_animate_2/init.py):
WanAnimate2ModularPipeline:基础版角色动画流水线,default_blocks_name = "WanAnimate2Blocks";通过vae_scale_factor_spatial(8)、vae_scale_factor_temporal(4)、num_channels_latents(16)等属性为预处理与噪声张量提供尺度信息,requires_unconditional_embeds依据 guider 是否启用且条件数 >1 来决定是否需要无条件嵌入;WanAnimate2DistilledModularPipeline:蒸馏版,继承基础版,default_blocks_name = "WanAnimate2DistilledBlocks";WanAnimate2Blocks:基础版流水线块,按text_encoder -> image_encoder -> video_encoder -> vae_encoder -> denoise -> decode组合,各块声明了自己的ComponentSpec(组件依赖)与InputParam/OutputParam(输入输出契约);WanAnimate2DistilledBlocks:蒸馏版流水线块,仅将num_inference_steps默认值覆写为 10、guider 固定为 1.0,其余块复用。
测试方面,test_modular_pipeline_wan_animate_2.py 覆盖了快速推理、加载、工作流、显存与 guider 行为;其中明确标注 Wan-Animate-2不支持批处理(单次调用只接受一张角色图与一段驱动视频,num_videos_per_prompt亦不存在),相关批量测试被@pytest.mark.skip跳过。
小结
Wan-Animate-2 的 diffusers 实现把"长视频动画"拆解为可管理的定长分段流水线:每个分段依次完成驱动切片 VAE 编码、参考 K/V 提取、缓存引导的去噪与循环内解码,通过上一段解码尾帧完成像素级运动接力;基础版与蒸馏版共享同一套模块化骨架,仅在采样步数与 guider 配置上分道扬镳。理解这条vae_encoder -> prev_frames -> prepare -> scheduler_reset -> ref_extract -> denoise_inner -> decode的段内链路,以及height/width的"面积"语义与 zigzag 填充规则,就能在官方示例之上自由调整分辨率、分段长度与采样参数,跑出稳定且连贯的角色动画结果。
【免费下载链接】diffusers🤗 Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch.项目地址: https://gitcode.com/GitHub_Trending/di/diffusers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考