序言

        因手头没有GPU,只能在CPU上体验视频生成,聊以自慰。本文将分享我通过直接修改Wan2.1模型源码,在纯CPU环境下成功运行1.3B和14B两大文生视频模型的全过程。尽管生成速度无法与GPU相比,但在支持AVX-512指令的CPU加速下,数十分钟到数小时即可生成一段短视频,仍在可接受范围内。

        在GPU资源紧张或稀缺的情况下,这套经过验证的CPU部署方案,或许能为你打开一扇体验AIGC视频生成的大门。它不仅是一个可行的替代路径,更能让我们更深刻地理解模型运行的成本与底层逻辑。

本方案测试的基础环境:

CPU:Intel(R) Xeon(R) Gold 6248R CPU @ 3.00GHz

内存:376GB

操作系统:BigCloud Enterprise Linux 8.2

内核:5.10.0-200.el8_2.bclinux.x86_64

docker: 24.0.9

1 pypi依赖

conda create -n Wan2.1-py3.10 python=3.10

conda activate Wan2.1-py3.10

# 下载Wan2.1 源码

git clone -b main https://github.com/Wan-Video/Wan2.1

cd Wan2.1

git checkout 9737cba

# 【重要】由于Wan2.1依赖的组件较多,避免在安装过程中引入cuda版本的torch,应先安装CPU版本的torch

pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu

# 再安装其他组件

pip install --no-deps --force-reinstall -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu

# 【重要】以下是完整版本的requirements.txt

--extra-index-url https://download.pytorch.org/whl/cpu

accelerate==1.13.0

aiohappyeyeballs==2.6.1

aiohttp==3.13.3

aiosignal==1.4.0

async-timeout==5.0.1

attrs==25.4.0

certifi==2022.12.7

cffi==2.0.0

charset-normalizer==2.1.1

cryptography==46.0.5

dashscope==1.25.13

decorator==5.2.1

defusedxml==0.7.1

diffusers==0.27.0

easydict==1.13

einops==0.8.2

filelock==3.20.0

frozenlist==1.8.0

fsspec==2025.12.0

ftfy==6.3.1

hf-xet==1.3.2

huggingface-hub==0.25.2

idna==3.4

ImageIO==2.37.2

imageio-ffmpeg==0.6.0

importlib_metadata==8.7.1

Jinja2==3.1.6

MarkupSafe==3.0.2

moviepy==2.2.1

mpmath==1.3.0

multidict==6.7.1

networkx==3.1

numpy==1.26.4

opencv-python==4.11.0.86

openvino==2024.4.0

openvino-dev==2024.4.0

openvino-telemetry==2025.2.0

packaging==25.0

pillow==11.3.0

proglog==0.1.12

propcache==0.4.1

psutil==7.2.2

pycparser==3.0

python-dotenv==1.2.2

PyYAML==6.0.3

regex==2026.2.28

requests==2.28.1

safetensors==0.7.0

sympy==1.14.0

tokenizers==0.15.2

torch==2.10.0+cpu

torchaudio==2.10.0+cpu

torchvision==0.25.0+cpu

tqdm==4.67.3

transformers==4.39.0

typing_extensions==4.15.0

urllib3==1.26.13

wcwidth==0.6.0

websocket-client==1.9.0

yarl==1.23.0

zipp==3.23.0

2 源码修改

        通义万相原生不支持CPU,其源码中有大量CUDA的Hard Code,因此调整起来要有足够的耐心。

2.1 CUDA硬代码修改

        这部分代码修改遵循一定的规律,如:对于存在device='cuda'或device=torch.device("cuda") 的代码行尽量转换为:device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 以确保最大兼容性。

        修改列表如下:

脚本文件

修改内容

# 共享配置文件

Wan2.1/wan/configs/shared_config.py

# T5模型参数精度改为全精度

wan_shared_cfg.t5_dtype = torch.bfloat16torch.float32

# 主模型参数精度改为全精度

wan_shared_cfg.param_dtype = torch.bfloat16torch.float32

# 根据实际情况调整负面提示词

wan_shared_cfg.sample_neg_prompt

Wan2.1/wan/modules/t5.py

# T5EncoderModel类的初始化默认参数值修改

dtype=torch.bfloat16float32,

device=torch.cuda.current_device()device('cpu'),

Wan2.1/wan/modules/vae.py

# WanVAE类的初始化默认参数值修改

device="cuda" 修改为

device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')

Wan2.1/wan/text2video.py

# 释放GPU上被标记为“空闲”的缓存内存,从而优化显存的使用效率,而CPU无需清理缓存,应注释掉

# torch.cuda.empty_cache()

# 同步GPU计算流,而CPU无需同步,应注释掉

# torch.cuda.synchronize()

# 其他6处对device的设置,均修改为CPU

torch.device('cpu')

Wan2.1/generate.py

device = local_rankcpu

2.2 注意力机制修改

# 修改后的Wan2.1/wan/modules/attention.py如下:

# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.

import torch

import torch.nn.functional as F

import warnings

# 导出两个接口函数,保持与原版相同的API接口,确保其他代码无需修改

__all__ = [

    'flash_attention',

    'attention',

]

def flash_attention(

    q,

    k,

    v,

    q_lens=None,

    k_lens=None,

    dropout_p=0.,

    softmax_scale=None,

    q_scale=None,

    causal=False,

    window_size=(-1, -1),

    deterministic=False,

    dtype=torch.float32,  # 默认精度改为float32

    version=None,

):

    """

    CPU 版本的注意力实现

    替代原来的 flash_attn

    """

    # 参数检查

    half_dtypes = (torch.float16, torch.float32)  # 移除 bfloat16

    assert dtype in half_dtypes

    

    # 移除CUDA断言,同时原版的q.size(-1) <= 256是Flash Attention的硬件限制,CPU无此限制

    # assert q.device.type == 'cuda' and q.size(-1) <= 256

    

    b, lq, lk, out_dtype = q.size(0), q.size(1), k.size(1), q.dtype

    

    # 处理变长序列,CPU版本不支持原生的变长序列优化,发出警告但继续执行,牺牲性能保功能

    if q_lens is not None or k_lens is not None:

        warnings.warn('变长序列支持有限,使用简化实现')

    

    # 确保数据类型一致,避免精度损失,CPU对混合精度计算不如GPU友好

    q = q.to(dtype)

    k = k.to(dtype)

    v = v.to(dtype)

    

    if q_scale is not None:

        q = q * q_scale

    

    # 计算注意力分数

    # 形状: q: [B, Lq, H, C], k: [B, Lk, H, C]

    # 我们需要在最后两个维度计算点积

    

    # 转置以匹配维度

    q = q.transpose(1, 2)  # [B, H, Lq, C]

    k = k.transpose(1, 2)  # [B, H, Lk, C]

    v = v.transpose(1, 2)  # [B, H, Lk, C]

    

    # 缩放计算,注意力分数的标准缩放,防止点积值过大导致softmax梯度消失

    scale = softmax_scale or (q.size(-1) ** -0.5)

    q = q * scale

    

    # 计算注意力分数

    attn = torch.matmul(q, k.transpose(-2, -1))

    

# 应用因果掩码,生成上三角矩阵,用于“自回归生成”,防止看到未来信息

# [[False,  True,  True],

# [False, False,  True],

# [False, False, False]]

    if causal:

        mask = torch.triu(torch.ones(lq, lk, dtype=torch.bool, device=q.device), diagonal=1)

        mask = mask.unsqueeze(0).unsqueeze(0)  # [1, 1, Lq, Lk]

        attn = attn.masked_fill(mask, float('-inf'))

    

    # 应用滑动窗口掩码,采用局部注意力机制,适合长序列处理

    if window_size != (-1, -1):

        left, right = window_size

        if left > 0 or right > 0:

            mask = torch.ones(lq, lk, dtype=torch.bool, device=q.device)

            for i in range(lq):

                start = max(0, i - left)

                end = min(lk, i + right + 1)

                mask[i, start:end] = False

            mask = mask.unsqueeze(0).unsqueeze(0)  # [1, 1, Lq, Lk]

            attn = attn.masked_fill(mask, float('-inf'))

    

    # 最后一个维度做softmax,确保注意力权重和为1

    attn = F.softmax(attn, dim=-1)

    

    # dropout,提供正则化,防止过拟合

    if dropout_p > 0. and not deterministic:

        attn = F.dropout(attn, p=dropout_p)

    

    # 加权求和,即注意力权重 × 值向量

    output = torch.matmul(attn, v)  # [B, H, Lq, C]

    

    # 转置回原始形状

    output = output.transpose(1, 2)  # [B, Lq, H, C]

    

    # 恢复原始数据类型

    return output.type(out_dtype)

def attention(

    q,

    k,

    v,

    q_lens=None,

    k_lens=None,

    dropout_p=0.,

    softmax_scale=None,

    q_scale=None,

    causal=False,

    window_size=(-1, -1),

    deterministic=False,

    dtype=torch.float32,  # 改为 float32

    fa_version=None,

):

    """

    统一注意力接口

    总是使用我们的 CPU 版本

    """

    return flash_attention(

        q=q,

        k=k,

        v=v,

        q_lens=q_lens,

        k_lens=k_lens,

        dropout_p=dropout_p,

        softmax_scale=softmax_scale,

        q_scale=q_scale,

        causal=causal,

        window_size=window_size,

        deterministic=deterministic,

        dtype=dtype,

        version=fa_version,

    )

【注】支持CPU的代价显而易见,首先是失去了Flash Attention的IO优化,内存占用增加,计算速度也大幅下降。这也是功能优先于性能的直接体现。

3 模型精度转换

一、为什么要执行模型转换?

答:大多数消费级CPU(如Intel/AMD的x86架构)没有原生支持BF16计算的硬件单元(如Tensor Core)。直接加载BF16模型,CPU需要将其转换为FP32进行计算,这反而会增加额外的开销。因此,为了在CPU上获得最佳的兼容性和稳定性,通常需要将权重统一转换为FP32格式。

二、为什么模型精度要区分BF16和FLOAT32?

答:这本质上是精度、显存(内存)和速度之间的权衡。

BF16 (bfloat16):被称为“大脑浮点数”,是专为深度学习设计的16位格式。它保留了与32位浮点数(FP32)相同的数值范围(能表示很大和很小的数),但牺牲了小数精度(尾数位较少)。

优点:显存占用仅为FP32的一半,计算速度更快,且能有效避免训练和推理中的数值溢出(NaN)问题。缺点:精度较低,在某些对数值敏感的计算中可能累积误差。

FLOAT32 (FP32):标准的单精度浮点数。

优点:精度最高,计算最稳定,是CPU和通用计算的通用标准。

缺点:内存占用大,计算速度慢。

三、哪些模型权重文件需要转换?

答:models_t5_umt5-xxl-enc-bf16.pth 是Wan2.1模型的文本编码器(基于UMT5-XXL架构)。它负责将你输入的文字提示(Prompt)理解并编码成模型能够处理的数学向量(语义特征)。它是连接“语言”和“视频画面”的桥梁,对生成内容是否符合描述至关重要。T5-XXL模型参数量较大(通常超过10GB),如果使用FP32,显存占用会翻倍,很多显卡(甚至CPU内存)会直接爆掉。文本编码对绝对数值精度的要求相对较低,但对数值范围(不要溢出)要求高,BF16完美契合这一需求。官方发布BF16版本是为了让更多显存有限的用户能够运行这个庞然大物。

.safetensors文件是Hugging Face推广的一种更安全、加载速度更快的格式。它不像传统的PyTorch .pth文件使用不安全的pickle序列化,因此更受社区推荐,通常内部张量精度为FP32,已经符合CPU推理的要求,无需转换。

VAE(变分自编码器)负责将视频像素压缩到潜在空间(编码)以及将生成的数据还原成视频画面(解码)。它是图像/视频重建的核心。VAE模型通常较小,且对数值精度非常敏感。为了保持最高的重建质量(避免画面模糊或伪影),官方通常直接提供FP32精度的权重。

四、使用下面的脚本可对模型权重进行转换:

# convert-weight-bfload16-2-float32.py

#!/usr/bin/env python3

"""

转换模型权重为 float32

"""

import torch

import os

import glob

#model_dir = "/opt/models/Wan2.1-T2V-1.3B"

model_dir = "/opt/models/Wan2.1-T2V-14B"

# 查找所有模型文件

model_files = []

for ext in ['*.pth', '*.pt', '*.safetensors', '*.bin']:

    model_files.extend(glob.glob(os.path.join(model_dir, '**', ext), recursive=True))

    model_files.extend(glob.glob(os.path.join(model_dir, ext)))

print(f"找到 {len(model_files)} 个模型文件")

for filepath in model_files:

    print(f"处理: {filepath}")

    try:

        # 加载

        if filepath.endswith('.safetensors'):

            from safetensors.torch import load_file, save_file

            # 使用safetensors库的安全、高效方式加载模型权重到一个Python字典(state_dict)中

            state_dict = load_file(filepath)

        else:

            # 强制将所有张量加载到CPU内存中

            # 加载.pth文件有潜在的安全风险(如果文件来源不可信),因为其使用pickle。

            state_dict = torch.load(filepath, map_location='cpu')

        # 转换

        converted = False

        if isinstance(state_dict, dict):

            for key in list(state_dict.keys()):

                if isinstance(state_dict[key], torch.Tensor):

                    # 核心判断:检查这个张量的数据类型(dtype)是否是torch.bfloat16(BF16) 或 torch.float16(FP16/Half)。

                    if state_dict[key].dtype in (torch.bfloat16, torch.float16):

                        # 执行转换:使用.to(torch.float32)方法,将张量原地转换(in-place)为torch.float32(FP32) 精度。

                        state_dict[key] = state_dict[key].to(torch.float32)

                        converted = True

        # 保存

        if converted:

            backup = filepath + '.bf16_backup'

            if not os.path.exists(backup):

                os.rename(filepath, backup)

            if filepath.endswith('.safetensors'):

                save_file(state_dict, filepath)

            else:

                torch.save(state_dict, filepath)

            print(f" ✓已转换并备份到 {backup}")

        else:

            print(f"无需转换")

    except Exception as e:

        print(f" ✗错误: {e}")

print("转换完成!")

4 测试效果

4.1 推理命令和输出效果

# 推理命令:

CUDA_VISIBLE_DEVICES=-1 python generate.py --task t2v-14B --size 832*480 --ckpt_dir /opt/models/Wan2.1-T2V-14B --offload_model True --t5_cpu --prompt "两只可爱的鹧鸪,一雌一雄,雌鸟体羽灰褐,雄鸟拥有醒目的红眼圈和鲜艳的面颊。它们在林间覆着薄雪的苔石上相依偎,低头啄食着散落的草籽。背景是冬日的阔叶林,有模糊的光秃枝干和远处墨绿的松柏。柔和的晨光穿过稀疏的树冠,在林间投下长长的光柱。动态镜头缓缓推进,焦点集中在它们灵动的眼神、细腻的羽毛纹理以及啄食时轻微的颤动上。风格写实,4K高清,自然纪录片质感,宁静而充满生机。" --save_file wan2.1-cpu-鹧鸪-1.mp4 --sample_steps 15 --frame_num 40

# 负面提示词:

wan_shared_cfg.sample_neg_prompt = '过曝,静态画面,细节模糊,字幕,风格化,画作感,画面静止,整体发灰,低质量,JPEG压缩伪影,丑陋,残缺,多余的手指,畸形的手,畸形的脸,毁容,肢体变形,手指融合,静止,杂乱的背景,背景人群,逻辑错误,动作不连贯,帧率低,色彩失真,负片效果,色调异常,色差,褪色,色彩暗淡'

# 输出效果:

wan2.1-cpu-鹧鸪-1

# 使用ffplay观看

apt install ffmpeg

ffplay wan2.1-cpu-鹧鸪-1.mp4

4.2 命令详细解释

传参

解释

CUDA_VISIBLE_DEVICES=-1

禁用所有可用的CUDA GPU,强制程序在CPU上进行所有张量计算。

python generate.py

Wan2.1模型框架的入口脚本,负责解析参数、加载模型、执行推理流程。

--task t2v-14B

指定要执行的任务类型和模型规模,告诉程序需要加载对应的模型架构和处理逻辑。

--size 832*480

指定生成视频的分辨率。这是一个标准的480p(SD)分辨率,纵横比接近16:9。较低的分辨率(如480p而非1080p)可以大幅减少内存消耗和计算时间。

--ckpt_dir /opt/models/Wan2.1-T2V-14B

指定模型权重文件(checkpoint)所在的目录路径。

--offload_model True

启用模型“分载”或“逐层加载”功能。当模型过大无法一次性全部载入内存时,此参数会控制程序在需要计算某一层时才将其加载到内存,计算完成后释放,从而允许在内存有限的CPU上运行超大模型。

--t5_cpu

将T5文本编码器固定在CPU内存中,不加载到GPU显存。

--prompt

提示词的质量和细节丰富度,直接影响生成视频的质量和相关性。

--save_file wan2.1-cpu-鹧鸪-1.mp4

.mp4是通用的视频容器格式。

--sample_steps 15

在扩散模型中,这决定了从随机噪声“去噪”出清晰图像的迭代次数。15是一个非常低的值(通常在20-50之间)。在CPU上设置为15是为了大幅缩短推理时间,是速度与质量之间的折衷。步数越少,生成越快,但画面细节和稳定性可能越差。

--frame_num 40

决定了视频的时长,模型通常默认为8fps或10fps

【问题】为什么推理时只支持几个固定的分辨率?

        Wan2.1采用Transformer架构,它需要一种机制来理解图像中每个像素块(Patch)的“位置”。这通过位置编码实现。在训练时,模型被固定喂入特定尺寸(如256x256, 480x480, 720x720)的图像。模型学会了“第1行第1列的Patch”对应某个编码向量,“第16行第16列的Patch”对应另一个编码向量。如果输入一个832x480的分辨率,模型会试图将832/16=52个Patch和480/16=30个Patch的位置信息映射到它学过的编码表上。但它的编码表里只有训练时见过的最大位置(如720/16=45)。对于第46到第52个位置,模型没有对应的编码信息,要么报错,要么生成毫无意义的重复图案或黑块。

        此外,Wan2.1使用VAE将图像压缩到潜在空间(Latent Space)进行生成,解码时再还原。VAE的编码器和解码器在训练时也是针对特定输入尺寸(如256x256)进行优化的。强行输入其他尺寸,会导致潜在特征分布偏移,解码出来的图像严重失真或出现伪影。

5 常见问题

        一、性能和效率:并非所有的CPU都适合进行视频生成,具有AVX-512指令的CPU推理速度会更快,专为 AI 计算优化,能大幅提升矩阵运算效率,经测试Intel(R) Xeon(R) Gold 6248R CPU @ 3.00GHz型号CPU上,Wan2.1-1.3B-t2v推理 15帧经过15次迭代大约需要10分钟,Wan2.1-14B-t2v 40帧经过15次迭代大约需要2小时。

        二、过大的内存消耗:帧数越多,视频时间越长,同时所需的内存会以指数级增长,经测试:分辨率在832*480的视频,若80帧,内存需求会冲高到320GB左右,这是普通CPU的笔记本所望尘莫及的门槛。

6 经验总结

        这次因为手头没有GPU,硬是把Wan2.1这个视频生成模型在CPU上跑起来了。整个过程挺折腾,但也学到了不少实在的东西,记录如下。

1. 核心就一条:想用CPU跑,得把模型“改造”成CPU能听懂的样子。

        官方模型默认是给GPU(CUDA)用的,代码里到处是device='cuda'这种硬编码。我们的核心工作就是把这些地方都改成 device='cuda' if torch.cuda.is_available() else 'cpu',或者直接强制指定为CPU。说白了,就是告诉程序:“别找显卡了,就用CPU算。”

2. 模型精度必须转,不然大概率出错或奇慢无比。

        很多大模型(特别是里面的文本编码器部分)为了省显存,用的是bfloat16(BF16)这种格式。但消费级CPU大多不直接支持BF16计算,硬要算的话,CPU会在后台悄悄把它转成float32(FP32)再算,反而多了一道手续,更慢。

所以,必须提前用脚本把模型文件里BF16的权重,全部转换成FP32格式。这是CPU上能稳定运行的前提,虽然模型文件会变大,但值得。

3. 注意力机制是性能瓶颈,但没办法。

        模型里最核心的Flash Attention优化,是专门为GPU显存的高带宽设计的,在CPU上完全用不了。我们只能用最基础的PyTorch矩阵乘法来实现注意力计算。

        后果就是:计算速度慢很多,内存占用会猛增。这是CPU方案最吃亏的地方,但为了功能能跑通,只能接受。

4. 参数设置要“抠门”,一切为了能跑起来。

        在CPU上跑,就不能想着和GPU比质量、比速度了。设置参数时要非常“小气”:

        分辨率调低:比如用832x480,别想1080p。

        帧数调少:先试试20、40帧,别上来就80帧。

        推理步数调低:比如降到15步,虽然画面会糙点,但生成时间能从几小时缩短到一两小时。

        用好内存卸载:--offload_model True和 --t5_cpu这两个参数是内存救命稻草。它们让模型不是一次性全部加载,而是用哪层加载哪层,能让你在有限的内存里塞下更大的模型。

5. 对硬件有要求,不是随便找个电脑就能玩。

        CPU:最好是有AVX-512指令集的Intel服务器级CPU(比如至强系列),计算会快不少。不过经过调研,不少5000元左右的笔记本电脑使用的AMD处理器也支持AVX-512。

        内存:越大越好!生成视频时内存消耗是爆炸式增长的。生成一个832x480分辨率的40帧短视频,可能就需要几十GB内存。想跑更长的视频(比如80帧),没有128G甚至256G内存,基本不用想。普通笔记本(通常16G-32G内存)很难胜任。

6. 这事的价值:没GPU时的“权宜之计”和“学习工具”。

        说白了,在CPU上跑Wan2.1,不是为了“好用”,而是为了“能用”。给没有显卡的研究者/学生一个体验和学习的途径:你可以通过它理解文生视频的整个流程,调试提示词,观察效果,而不必拥有昂贵的GPU。

        验证想法和流程:可以在CPU上把代码、环境、流程全部跑通调试好,以后再移植到有GPU的机器上,就能专注于提升质量。深刻理解模型推理的成本:亲手在CPU上等上两小时生成一个短视频后,你会对“算力”和“模型规模”有最直观的感受,明白那些高质量、长视频的生成背后需要多么巨大的资源。

总结一下:

        在CPU上跑通Wan2.1,是一次“条件受限下的技术妥协”。它过程繁琐(改代码、转模型、调参数),要求不低(大内存、好CPU),结果也不完美(速度慢、画质需妥协)。但它实实在在地打通了一条路,让没有GPU的人也能摸一摸、用一下这个前沿技术。如果你是AI爱好者但苦于没有显卡,不介意折腾和等待,那么这套方案值得一试。如果你追求效率和质量,那还是得想办法搞块好显卡。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐