2106 字
11 分钟
24GB 统一内存跑不动 Wan2.2-5B:VAE 解码的 MPS 内存黑洞,和一次干脆的放弃

想在本地摸一下 Wan2.2-TI2V-5B 的内存占用——这是阿里 Wan2.2 系列里最小的 dense 模型,5B DiT,官方号称单卡 4090 能在 9 分钟内出一段 5 秒 720P。手头的机器是 M4 Pro、24GB 统一内存。结论先说:权重装得下、去噪跑得动,但 VAE 解码在 MPS 后端上是个内存黑洞,这条路走不通。最后模型缓存和所有中间产物全删了,正式认输。

失败过程里挖出来的东西比跑通更有意思:PyTorch MPS OOM 报错里 “other allocations” 到底是什么、“max allowed: 30.19 GiB” 这个比物理内存还大的上限怎么算出来的、以及为什么这个失败和内存大小根本没关系——128GB 的 M3 Max 跑同一个 workflow 也炸。

账面先算一遍:24GB 装不装得下#

diffusers 版仓库 Wan-AI/Wan2.2-TI2V-5B-Diffusers 总共 34.2GB:

组件磁盘加载后
DiT transformer20.1GB(fp32 存储,5B × 4 bytes)bf16 ~10GB
umt5-xxl 文本编码器11.4GB(bf16)bf16 ~11.4GB
VAE2.8GB(fp32,官方推荐 fp32 跑)fp32 ~2.8GB

三件全常驻就是 ~24GB,加上激活和系统本身,24GB 统一内存一次性装不下。但文本编码和去噪在时间上是分离的——prompt 编码完,text encoder 就没用了。所以方案是分阶段、分进程

  1. encode:只加载 umt5-xxl,编码 prompt,embeddings 存盘,进程退出;
  2. denoise:只加载 DiT + VAE,读 embeddings 去噪,latent 存盘;
  3. decode:只加载 VAE,解码 latent 出视频。

每个阶段独立进程,RSS 和 Metal 内存都是干净的。测量用一个后台线程每 200ms 采样 psutil RSS 和 torch.mps.driver_allocated_memory(),记录峰值。

下载时顺手踩了个 hf CLI 的坑:hf download <repo> "text_encoder/*" "transformer/*" ... 这种写法里,位置参数会被当成显式文件名,glob 静默失效——警告写着 Ignoring --include since filenames have being explicitly set,结果 text_encoder 整个目录没下下来。要用 --include "text_encoder/*" 这种带 flag 的形式才是 pattern 匹配。

前两个阶段:一切正常#

测试配置故意缩小:480×832、21 帧、4 步去噪(只测内存,不管画质)。环境 torch 2.13.0 + diffusers 0.39.0,MPS 后端。

文本编码:umt5-xxl bf16 上 MPS,加载 6.2 秒,编码 1.9 秒,输出 (1, 226, 4096) 的 embeddings。峰值 11.0GB

去噪:DiT bf16 + VAE fp32 加载后 Metal 占 12.15GB;有个值得注意的瞬时峰——加载过程 RSS 冲到 15.8GB,因为 safetensors 里是 fp32,读进来还要转 bf16,读取和转换的两份短暂共存。去噪 4 步共 32.8 秒(8.2s/步,CFG 开着所以每步实际算两个 batch),全程峰值 13.1GB。产出 latent (1, 48, 6, 30, 52)

到这里一切按剧本走。分阶段策略是成立的:单阶段峰值 11~13GB,24GB 机器绰绰有余。

然后就撞墙了。

VAE 解码:三次 OOM,一次误诊#

第一次直接在 pipeline 里连着解码,炸:

RuntimeError: MPS backend out of memory (MPS allocated: 20.52 GiB,
other allocations: 8.13 GiB, max allowed: 30.19 GiB)

合理,去噪的 20GB 还占着。于是去噪后先 pipe.transformer = None + empty_cache(),再开 vae.enable_tiling() 分块解码。又炸,但数字变了:

MPS allocated: 12.05 GiB, other allocations: 18.13 GiB

自己的池子缩了,“other allocations” 反而涨到 18GB。当时的第一反应是被进程外的东西挤占了——别的 app 在吃 Metal 内存?重跑一次,“other allocations” 涨到 24.13GB,而且连续两次独立运行,数字精确到小数点后两位都一样(6.05 + 24.13)。系统当时 87% 内存空闲。

外部挤占的假设站不住了:外部压力不可能两次给出 bit 级相同的数字。这是进程内的、确定性的行为。

去翻 PyTorch 源码(aten/src/ATen/mps/MPSAllocator.mm),报错字符串是这么拼的:

"MPS backend out of memory (MPS allocated: ",
format_size(m_total_allocated_memory.current),
", other allocations: ",
format_size(current_allocated_size() - m_total_allocated_memory.current),

current_allocated_size()MPSAllocator.h 里的定义带着一句自白:

// there are implicit allocations from MPS backend, so we need to query the
// 'device' for total allocated size instead of manually tracking in MPSAllocator
size_t current_allocated_size() const {
return [m_device currentAllocatedSize];
}

也就是说,“other allocations” = Metal driver 报告的本进程总分配量,减去 PyTorch 自己池子里记账的部分。这个差值就是 MPS/MPSGraph 框架的”隐式分配”——PyTorch 的 allocator 根本不经手、只能事后问 MTLDevice 要总数的那部分。它不是别的进程,是你自己进程里 PyTorch 管不到的暗账。

顺带把 “max allowed: 30.19 GiB” 也解了。上限公式是 PYTORCH_MPS_HIGH_WATERMARK_RATIO(默认 1.7)× recommendedMaxWorkingSetSize(PyTorch 源码注释说通常是物理内存的 ~75%)。验算:30.19 ÷ 1.7 = 17.76GiB,17.76 ÷ 24 = 0.74,严丝合缝。所以 24GB 的机器 PyTorch 允许 Metal 分到 30GB——反正统一内存能 swap,代价是性能。

定量:解 5 帧要 29.3GB#

搞清语义之后做了个对照实验:latent 只切前 2 帧(对应 5 个视频帧),关 tiling 直接解。这次成功了,数字触目惊心:

[decode_done] rss_gb: 0.03, mps_driver_gb: 28.55, mps_alloc_gb: 2.67
peak_driver_gb: 29.31

张量实际占用 2.67GB,Metal driver 峰值 29.31GB。差出来的 ~26GB 全是池外的隐式分配——发生在 Wan VAE 解码路径的 conv3d / F.interpolate 调用栈里(三次 OOM 的 traceback 分别死在 F.padF.conv3d_upsample_nearest_exact2d 上)。480×832 的 5 帧就要 29GB,21 帧非 tiled 直接超 30GB 上限;tiled 版本理论上单 tile 内存很小,但实测 “other allocations” 随 tile 迭代 8→18→24GB 单调爬升,攒着不放,最后一样撞墙。

这笔暗账具体是哪个 op 的 scratch、为什么不释放,我没有在上游找到实锤——PyTorch 仓库里有一簇至今 open 的同类 issue(#164299 “MPS Memory Leaks across core MPS files”、#105839#87090#154329),全是”Metal 侧内存在 PyTorch 池外增长/不释放”的形状,但没有一个定位到 conv3d 或 tiled decode,也没有一个已修复。所以”conv3d scratch 泄漏”只能算我的推测(n=1 机器、3 次复现),“MPS 后端存在池外内存失控的未解决问题”是可以引 issue 号的事实。

这不是 24GB 的锅,是关键证据:ComfyUI 上有人用 128GB 的 M3 Max 跑同一个 Wan2.2 5B TI2V workflow,同样姿势炸掉——other allocations: 139.74 GiB, max allowed: 163.20 GiBComfyUI#9308);M1 Ultra 128GB 也有同签名的报告(WanVideoWrapper#1856)。隐式分配跟着分辨率×帧数走,内存翻五倍,它也翻五倍。堆 RAM 解决不了。

最后用 CPU 解码兜了个底:fp32 非 tiled 解完整 21 帧,RSS 峰值 18.0GB,耗时 513 秒。能出片,证明 latent 是好的、链路是通的,但 8.5 分钟解一段 0.9 秒的视频,没有实用价值。

认输,清理#

至此判定成立:在当前 torch 2.13 的 MPS 后端上,Wan2.2-TI2V-5B 的端到端生成在这台机器上不可行。不是”配置没调好”的那种不可行——去噪部分完全健康,是 VAE 解码路径撞上了后端本身未解决的内存行为,且社区里 128GB 的机器同样倒在这里。继续调参是在残余自由变量里找退路,该停了。

34GB 模型缓存、venv、脚本、latent、测试视频,全部删除。实测数字和诊断留档,因为下一步是换战场:一张 RTX 4070 Ti SUPER(16GB VRAM)。调研下来那边的地形完全不同——CUDA 上 tiled VAE 没有 MPS 这个池外暗账问题(待实测确认);DiT 有现成 GGUF 量化(QuantStack,Q8_0 只有 5.4GB);还有 3 步蒸馏版 FastWan2.2-5B 训练分辨率就是原生 121×704×1280。不过要清醒的是:16GB 卡跑原生 720P 全网没有一手实测,3090 24GB 用官方脚本都在 VAE 解码这一步 OOM(Wan2.2#90)——解码是所有平台共同的窄门,只是 CUDA 那边有真正能用的 tiling。

经验#

  1. 视频模型的内存瓶颈不在权重,在 VAE 解码。5B 模型权重 bf16 才 10GB,解码激活轻松几倍于它。评估”XX 显存能不能跑”时,别只算参数量。
  2. MPS OOM 报错里 “other allocations” 是本进程的池外 Metal 分配(MPSGraph 隐式内存),不是别的 app。torch.mps.current_allocated_memory()driver_allocated_memory() 差距巨大时,问题就出在 PyTorch 管不到的那部分,empty_cache() 救不了。
  3. 完全相同的报错数字 = 确定性行为 = 进程内问题。“被外部挤占”的假设被这一个观察直接证伪,少走了很多弯路。
  4. 分阶段 + 分进程是统一内存机器跑大 pipeline 的正确姿势,顺便让每个阶段的内存数字都干净可信。
  5. 该认输时干脆点。“当前配置失败”和”这条路不可行”是两回事,但当失败点位于依赖的底层后端、上游 issue 挂着没人修、128GB 机器同样复现时,就不是配置问题了。留下诊断和数据,换个战场。
24GB 统一内存跑不动 Wan2.2-5B:VAE 解码的 MPS 内存黑洞,和一次干脆的放弃
https://blog.lishuyu.app/posts/wan22-5b统一内存跑视频模型失败记/
作者
猫猫魔女
发布于
2026-07-22
许可协议
CC BY-NC-SA 4.0