原文发表在知乎,可以移步:《https://zhuanlan.zhihu.com/p/2079733196787279646

最近在研究具身领域的世界模型,读到一个蚂蚁灵波的lingbot va系列的模型,想来复现一下,其中lingbot va1.0就是基于wan2.2基础之上进行研发的,所以在尝试lingbot va1.0之前想先把wan2.2的一些细节弄清楚。

通义万相Wan是阿里巴巴通义实验室推出的AI绘画与视频生成大模型​,核心能力覆盖从文生图、图生视频到动作生成、角色扮演等多个领域,是当前国内开源与商用并重的头部多模态模型之一。目前已经到Wan3.0版本了,本文研究的Wan2.2于2025.9月份开源发布,Wan2.2与Wan2.1在技术上基本是相同的,但在参数量,训练数据量上做了提升。

wan2.2的高层次的设计原理,思想等内容本文不会过多讲解,大家可以参考网上有很多篇文章了,例如《两万字长文解读wan2.1(通义万相)技术报告》。本文的关注和讲述的重点是input->output之间的一些计算细节,有了这些细节知识的填充,个人感觉才能对其整体的设计有更深入的理解。所以阅读本文之前建议首先对wan2.2有一个整体的理解。

1,getting started
先安装环境尝试跑一个case看看,安装环境不再赘述,按readme安装即可,我的环境因为flash-attn没有现成的版本,所以是下载源码编译安装,当然这些过程都交给了cursor的agent来完成。

模型下载:我下载了一个Wan2.2-I2V-A14B图片生成视频的模型,大小118G。

运行命令:

其中输入图像test.jpeg与输出的5秒视频我贴在下面了。可以看到这个case的效果还是不错的。不过后续又拿我个人照片生成了一个视频,就会发现人脸的变形与变化,效果就一般了。

python generate.py
–task i2v-A14B
–size 480*832
–ckpt_dir /home/vla/Wan2.2-I2V-A14B
–offload_model False
–convert_model_dtype
–frame_num 81
–sample_steps 20
–image ./test.jpeg
–save_file ./test_i2v.mp4
–prompt “海边浅滩,父母牵着中间小孩的手面向大海,轻轻把孩子抬起又放下,孩子双腿微微晃动,细小波浪拍岸,微风吹动头发和衣摆,镜头固定,自然日光。”
在这里插入图片描述

输入图片test.jpeg
用以上参数生成视频,大约用了 5 分钟。运行过程中显存占用约56G。

2,整体架构
wan2.2模型包括以下几个部分:

T5 / UMT5-XXL:文本编码器
Wan2.1 VAE:3D causal VAE,压缩比 时间 4 × 空间 8×8
两套 DiT(MoE)
high_noise_model:高噪声段
low_noise_model:低噪声段
其中VAE就是生成式模型中常用的图像或视频压缩模型,上文case中输入和输出的分辨率都不小,一般都会用vae的encoder将其压缩在一个隐空间中,在这里就是时间 4 × 空间 8×8=256倍的压缩。在隐空间中生成式模型进行视频的生成,生成后将其通过vae的decoder转换成原始分辨率和时长大小。关于vae以前看过一篇文章可以参考《古月居 - ROS机器人知识分享社区》。

DiT部分使用flow matching算法去噪,训练时去噪步数 1000,所以 t >= 900 用高噪专家,之后用低噪专家。高噪专家用于快速去除噪声,低噪专家用于细节的渲染。

计算过程如下流程图:

prompt + JPEG

├─ T5 → 文本 embedding(正/负提示)
└─ 图像 resize + 首帧填入 81 帧视频

VAE encode → 条件 latent y(带 mask)

随机噪声 latent

20 步 Flow Matching(MoE:高噪/低噪两个 DiT)

VAE decode → RGB 视频 [-1, 1]

save_video → test_i2v.mp4
以上各部分在下文有较为详细的讲述。

3,wan-vae

3.1,encoder
上文讲过vae encoder负责将一段视频压缩到latent空间以减少后续大模型要处理的数据量。

注意在推理和训练场景中,输入有些不同,在训练场景中,因为要为模型训练提供label,所以需要将视频压缩到latent空间。而在推理场景中,因为输入是一张图片,需要对图像进行压缩。但在技术实现中其实是一致的,大家可以将一终图片看成一段视频,不过此段视频只有第一帧是有效的,后续的视频片段全是零。

在上文的case中,整体输入[1, 3, 81, 464, 832],bs=1,3代表rgb三通道,81代表帧数(稍后会详述),464*832是分辨率。最终输出[16, 21, 58, 104](其实应该是两个[16, 21, 58, 104],代表均值与方差,技术原理可以查询vae)。

帧数:

frame_num 必须是 4n+1(1、5、9、…、81)。encoder不会把 81 帧一次喂进去,而是切成 1 + 4 + 4 + …。其中1代表第1帧,后面每4帧组成一个chunk进行处理。因为视频具备连续性,所以将第1帧单独处理,后面每4帧会在时间上也进行压缩成一个latent。

81 帧时 iter_ = 21,于是 同一套 encoder函数 被调用 21 次:

i=0 encoder(x[:,:, 0:1]) # 第 0 帧, [B,3,1,H,W]
i=1 encoder(x[:,:, 1:5]) # 第 1–4 帧 [B,3,4,H,W]
i=2 encoder(x[:,:, 5:9]) # 第 5–8 帧

i=20 encoder(x[:,:,77:81]) # 第 77–80 帧
每chunk都调用self.encoder。块(chunk)与块之间不靠返回值衔接,靠共享的 feat_cache。也就是每块不是隔裂的进行处理,而是下一块会使用到上一块提供的缓存信息,依次往后,这样的话最后的chunk中提取到的latent信息其实也一定程度上包含了前面所有chunk的信息。

def encode(self, x, scale):
self.clear_cache()
t = x.shape[2]
iter_ = 1 + (t - 1) // 4
## 对encode输入的x,按时间拆分为1、4、4、4…
for i in range(iter_):
self._enc_conv_idx = [0]
if i == 0:
out = self.encoder(
x[:, :, :1, :, :],
feat_cache=self._enc_feat_map,
feat_idx=self.enc_conv_idx)
else:
out
= self.encoder(
x[:, :, 1 + 4 * (i - 1):1 + 4 * i, :, :],
feat_cache=self._enc_feat_map,
feat_idx=self.enc_conv_idx)
out = torch.cat([out, out
], 2)
encoder内部结构:

[ 0] ResidualBlock 96 -> 96
[ 1] ResidualBlock 96 -> 96
[ 2] Resample downsample2d ← 空间 /2
[ 3] ResidualBlock 96 -> 192
[ 4] ResidualBlock 192 -> 192
[ 5] Resample downsample3d ← 空间 /2 + 时间 /2
[ 6] ResidualBlock 192 -> 384
[ 7] ResidualBlock 384 -> 384
[ 8] Resample downsample3d ← 空间 /2 + 时间 /2
[ 9] ResidualBlock 384 -> 384
[10] ResidualBlock 384 -> 384
上面流程中的三次下采样分别对应上面架构图中的三个矩形框。

ResidualBlock 内部:

其中的CausalConv3d是一个核心,causal代表因果关系,常规3d巻积的时候,可能会将未来的信息也一起处理。但causal卷积只会使用历史信息,不会使用未来信息,这样不会在当前信息的处理中泄露未来信息。

residual.0 RMS_norm
residual.1 SiLU
residual.2 CausalConv3d k=(3,3,3) s=(1,1,1) ← 占 feat_cache 一个槽
residual.3 RMS_norm
residual.4 SiLU
residual.5 Dropout
residual.6 CausalConv3d k=(3,3,3) s=(1,1,1) ← 占 feat_cache 一个槽
shortcut Identity 或 CausalConv3d k=(1,1,1)
Resample 内部(两种模式):

代表架构图中的三次下采样,第1次只空间下采样,对应模式downsample2d,后面2次空间和时间都下采样,对应模式downsample2d

[2] downsample2d:
resample.0 ZeroPad2d padding=(0,1,0,1)
resample.1 Conv2d k=(3,3) s=(2,2) p=(0,0) 96->96
[5] downsample3d:
resample.0 ZeroPad2d padding=(0,1,0,1)
resample.1 Conv2d k=(3,3) s=(2,2) p=(0,0) 192->192
time_conv CausalConv3d k=(3,1,1) s=(2,1,1) 192->192
缓存:

上面的结构中会有大量的CausalConv3d,因为CausalConv3d的输入包括了时间维度上的信息,所以在算子CausalConv3d中加入了历史cache的输入。这样就可以实现每个chunk提取出的latent信息可以包含前面所有chunk的信息,也就是实现了时序信息的融合。

输出:

整体输入[1, 3, 81, 464, 832],最终输出[16, 21, 58, 104],16是通道数。经过上面的分析,81帧数据处理成了21个latent,第1帧单独处理,后面每4帧压缩成一个latent。在空间上,原始分辨率464832经过了88的空间压缩形成58*104。

4,DIT
将数据压缩进latent空间后,下面就进入了生成的阶段,生成所使用的技术是flow matching技术,类似于diffusion原理。生成过程计算图如下:

x_noise [16, 21, 58, 104] 本步要更新的 latent
y_cond [20, 21, 58, 104] 固定

cat 通道

输入 [36, 21, 58, 104]

patch embed

视频 token [L, 5120] 每格已含 noise+mask+图

40 × (self-attn → 文本 cross-attn → FFN)

Head,out_dim=16
unpatchify

预测 v [16, 21, 58, 104] 只预测噪声通道的速度

CFG 混合
UniPC: x_noise ← step(v, t, x_noise)
y_cond 不动
x_noise:

我们知道,生成过程中的去噪的过程是将一个纯噪声逐步的去噪,最终形成有效数据。所以最开始的输入是一个纯纯的噪声,shape是[16, 21, 58, 104]。因为我们的目标是根据图片生成一个5秒的视频片段,共81帧(默认是16fps,所以5秒就是80帖,额外加上首帧就是81)。因为是在latent空间进行生成,所以noise跟vae encoder输出的shape是一样的,然后再经过vae decoder逆过程恢复成原始的视频。

y_cond:

去噪过程不能凭空进行,需要约束的条件输入,条件包括下面两个部分:msk+vae encoder的输出。

msk: [ 4, 21, 58, 104] 「哪一格是已知首帧」
VAE(encode): [16, 21, 58, 104] 首帧图像的 latent(首帧是图像,后面80帧是0)
concat 通道: [20, 21, 58, 104] ← 这就是 y
维 大小 含义
通道 20 4 维 mask + 16 维图像 latent
时间 21 81 像素帧压成 21 个时间格
高 58 464 / 8(VAE 空间下采样 8)
宽 104 832 / 8
用 16 维告诉模型「第一帧长什么样」,用 4 维告诉模型「只有第一格是给定的,后面请自己生成」。

msk:

像素: [1] [0 0 0 0] [0 0 0 0] … [0 0 0 0] 共 81
补齐: [1 1 1 1] [0 0 0 0] … [0 0 0 0] 共 84
折成: 格0=1111 格1=0000 … 格20=0000 共 21 格 × 4 通道
token化:

我们知道在transformer中是token与token之间进行attention,所以需要将latent空间中的tensor转换成token。

将x_noise与y_cond拼接后,再一次下采样,以减少token数,最终形成31668个token。

[36, 21, 58, 104]
Conv3d:每个 (1×2×2) 小块看全部 36 个通道
→ [5120, 21, 29, 52]
flatten
→ [B, L=31668, 5120] ← 只有这一套视频 token
denoise:

Classifier-Free Guidance:

推理时同一张噪声 latent 跑两遍 DiT,再按比例混合。

prompt就是本文开头命令中的Prompt。负向prompt没有指定,用的是程序中的默认值:

色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,
低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,
形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走
名字 输入 模型在学什么
cond 你的 prompt + 图像条件 y 「按这句话去生成」
uncond 负向 prompt + 同一个 y 「按默认/反面去生成」(过曝、静止、畸形手等)

s = 1:只用 prompt,不外推
s = 3.5:沿着「cond 比 uncond 多出来的方向」再走 3.5 倍,更贴 prompt
s 太大:饱和、伪影、动作僵
去噪总数选择:

t: 1000 → 850 → 700 → … → 50 → 0 ← 大约 20 个数
前几步 t 很大(≥900)→ 高噪专家
后面 t 变小(<900) → 低噪专家
上面的20来源于命令中指定的去噪步数:–sample_steps 20。这个值与生成时间整体上成正比。

5,decoder
vae decode 是 encode 的逆:

[16, 21, 58, 104]
反归一化
因果解码 21 次(缓存上一格)
clamp 到 [-1, 1]
→ [3, 81, 464, 832]
时间:1 + 20×4 = 81 帧,16 fps ≈ 5.06 秒save_video 再把 [-1,1] 映到 [0,255],按帧写成 H.264。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐