视频生成
两个家族:MiniMax-H3 在生成视频的同时生成原生立体声音轨,Wan 2.1 / 2.2 只生成视频。本页包含下载、运行示例,以及决定端到端耗时的设置。
这些命令遵循与文本模型示例相同的约定:Hugging Face CLI、从仓库根目录源码构建,并把 --backend 换成与你硬件匹配的值。参见按家族下载与运行。
MiniMax-H3(提示词 → 视频带音频)
H3 在同一个打包潜变量里对视频和 32 kHz 立体声音频一起去噪,所以音轨是模型输出的一部分,而不是事后配上去的。它是 CFG 蒸馏模型,必须用 --cfg 1.0,4–8 步是常用工作点(默认 20 步)。在 M5 Pro 上用 ggml_metal、22 帧、8 步、同一随机种子实测:256×256 比 stable-diffusion.cpp 快 2.4×(49.3 s → 20.9 s),640×384 快 1.7×(108.5 s → 63.1 s)。那是 Apple 芯片;在显存吃紧的 16 GB RTX 3080 Laptop 上,同一组对比的结论正好相反,其中的原因值得先看一眼——见两台机器上的速度。需要四个文件,来自两个仓库:
# 去噪器 + 文本编码器
hf download unsloth/MiniMax-H3-GGUF minimax_h3_fl2va_pruned-Q4_K.gguf --local-dir models
hf download unsloth/MiniMax-H3-GGUF qwen3vl_32b_minimax_h3-Q4_K_M.gguf --local-dir models
# 两个 VAE
hf download Comfy-Org/MiniMax-H3 vae/minimax_h3_video_vae_fp16.safetensors --local-dir models
hf download Comfy-Org/MiniMax-H3 vae/minimax_h3_audio_vae_fp32.safetensors --local-dir models
# 文本编码器不带分词器——从上游仓库取
hf download MiniMaxAI/MiniMax-H3 processor/vocab.json processor/merges.txt --local-dir models
# 文生视频(带音频)。输出 fox.mp4 和 fox.wav
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/minimax_h3_fl2va_pruned-Q4_K.gguf \
--backend ggml_metal --prompt "a red fox trotting through falling snow, cinematic" \
--width 640 --height 384 --video-frames 22 --diffusion-steps 8 --cfg 1.0 --output fox.mp4
也可以把四个网络都写进配置文件,让第一次运行自动下载——config/minimax-h3-fl2va.json 用于关键帧,config/minimax-h3-ref2va.json 用于参考。两者只有 model 一项不同,因此文本编码器与两个 VAE 只下载一次并共用(总计约 33.5 GB)。上面那对分词器文件是自动下载唯一补不上的东西,因为它不是一个命令行选项:
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --config config/minimax-h3-fl2va.json \
--prompt "a red fox trotting through falling snow, cinematic" --output fox.mp4
dotnet TensorSharp.Server.Host/bin/TensorSharp.Server.Host.dll --config config/minimax-h3-ref2va.json
一张图代表什么
一张图对 H3 有两种完全不同的含义,而且用不同的检查点。不指定时会自动推断,--video-mode 用于显式声明。
| 你想要什么 | --video-mode | 检查点 |
|---|---|---|
| "让这张照片动起来"——它成为第一帧 | i2v | minimax_h3_fl2va_pruned-* |
| "从照片 A 变到照片 B" | fl2v | minimax_h3_fl2va_pruned-* |
| "用这个人物,生成全新场景" | ref | minimax_h3_ref2va_pruned-* |
| "参考这个产品/人物,但换机位、背景和构图" | ref | minimax_h3_ref2va_pruned-* |
| 只有文本 | t2v | 都可以 |
两个去噪器是不同的文件而不是一个开关,而且各自会指名拒绝对方的输入:在 FL2VA 上要求 ref,报错会告诉你去加载 minimax_h3_ref2va_pruned-*.gguf;在 Ref2VA 上把 --image/--end-image 当关键帧传,报错会告诉你去加载 FL2VA 那个文件,或者改用 --ref-image 传这张图。关键帧与显式参考同时出现时会被直接拒绝,而不是悄悄丢掉其中一个——一段看起来"照做了"的视频才是更糟的失败。用的是哪个分区由文件名决定:名字里含 ref2va(不区分大小写)即选择参考条件路径,重命名或重新量化时请把它保留下来。
# 让照片动起来:它成为第一帧
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/minimax_h3_fl2va_pruned-Q4_K.gguf \
--backend ggml_metal --image portrait.jpg \
--prompt "the person turns toward the camera and smiles, subtle handheld motion" \
--width 640 --height 384 --video-frames 22 --diffusion-steps 8 --cfg 1.0 --output animated.mp4
# 首尾帧:两端钉住,模型补出中间运动
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/minimax_h3_fl2va_pruned-Q4_K.gguf \
--backend ggml_metal --image start.png --end-image end.png \
--prompt "a slow cinematic push-in" \
--width 640 --height 384 --video-frames 22 --diffusion-steps 8 --cfg 1.0 --output morph.mp4
# 参考:保留主体,机位、背景和构图全部重来
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/minimax_h3_ref2va_pruned-Q4_K.gguf \
--backend ggml_metal --ref-image person.jpg --ref-image bottle.png \
--prompt "she holds the bottle up to the light on a rooftop at golden hour, slow orbit" \
--width 640 --height 384 --video-frames 22 --diffusion-steps 20 --cfg 1.0 --output rooftop.mp4
参考图只会被缩小并保持自身宽高比,输出画布仍由 --width/--height 决定。在 Ref2VA 检查点上,普通的 --image 会被当作单张参考,因此只会"上传一张图"的客户端(包括 Web UI)无需改动即可使用。
参考:静图、片段与音轨
最多 九个参考,可以任意混搭。--ref-image 是静图,提示词里按位置称作 <Picture 1>、<Picture 2>……;--ref-video 是片段——视频文件或帧目录——会被重采样到 H3 自己的 24 fps 与自己的画布,称作 <Video 1>、<Video 2>……;--ref-audio 是音轨,重采样到音频 VAE 的 32 kHz 立体声并截断到所生成片段的时长。片段自带的声音要用 --ref-video-audio 单独给出,按位置与 --ref-video 配对,因为容器里的音轨无法通过帧解码器读取。
# 一个参考片段,外加它自己的音轨
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/minimax_h3_ref2va_pruned-Q4_K.gguf \
--backend ggml_metal --ref-video walk.mp4 --ref-video-audio walk.wav \
--prompt "the same woman walks along a beach at sunset, wide shot, waves behind her" \
--width 640 --height 384 --video-frames 22 --diffusion-steps 20 --cfg 1.0 --output beach.mp4
# 一张静图,加一段独立音轨
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/minimax_h3_ref2va_pruned-Q4_K.gguf \
--backend ggml_metal --ref-image singer.jpg --ref-audio song.wav \
--prompt "she performs on a small club stage under a single spotlight" \
--width 640 --height 384 --video-frames 22 --diffusion-steps 20 --cfg 1.0 --output stage.mp4
每个参考都是同一条打包序列里的额外 token,因此去噪器这边的开销是线性的、也容易预估:640×384 的参考是 240 个 token,576×448 的是 252 个;在 RTX 3080 Laptop 上对 640×384、22 帧的片段实测,单个去噪步从没有参考时的 4.37 s 变成八个参考时的 9.38 s——大约每个参考每步 626 ms,从一个到八个基本恒定。超过大约四个参考之后,主导的就变成 Qwen3-VL 那一趟,而不是去噪器:每个参考会给提示词加上约 250 个视觉占位 token,而这条提示词要过完整 50 层——两个参考是 548 token 的提示词,八个就是 2086,文本条件从约 65 s 涨到约 447 s,而整个 8 步去噪只要约 75 s。与其减少步数,不如先减少参考数量、提高参考质量。
参考片段是 H3 最昂贵的输入:22 帧 448×320 的参考会在输出本身所需的 1680 个 token 之外再加 980 个条件 token,而且 VAE 还得先把这 22 帧全部编码——在 M5 Pro 上实测,编码 14 s,之后每个去噪步约 9 s,而不带参考时只要 5 s。语言模型以 2 fps、每次两帧的方式看这段片段;去噪器则以潜变量的形式看到全部内容。
长度、尺寸,以及长片段的上限
宽高向上取整到 32 的倍数。帧数向上对齐到 17k+5 网格——5、22、39、56、73、90……——这来自视频 VAE 的时间分块,而不是随意选定的;fps 无论请求写什么都固定为 24。任意网格长度的片段都能正确解码:VAE 每次跑 5 个潜变量帧、带 2 帧前瞻,并对接缝做交叉淡化。反过来,一次性解码长片段会让细节被逐渐冲淡——对着条件照片测量,第 0 帧的相关度从 22 帧时的 0.97 掉到 90 帧时的 0.86——所以分块是正确性要求,而不是优化。只想要画面时用 --no-audio 跳过音频解码,省下音频 VAE 的时间与内存。
长片段过去会整片全黑。H3 在一条不加掩码的打包序列上做双向注意力,因此它的 key 数量就是整段片段——22 帧是 2364 个 token,107 帧是 8646——而所有内置的 ggml flash attention 内核都把 softmax 分子保持在 FP16,只留三个比特的余量。107 帧的片段在第一个去噪步就溢出成 NaN,回来时每个像素全黑、每个音频采样被削平,而同一条请求在 73 帧下却完全正常。现在 h3_attend 会按 key 数量取一个 2 的幂预先缩放 V,并在输出上还原——由于注意力对 V 是线性的,这一步是精确的;采样器也不再保存已经发散的片段,而是让请求失败并指出是哪一步,不再写出一个全黑文件。设 TS_H3_TRACE=1 可打印每一步的潜变量与速度幅值。
音频写成 MP4 旁边的 .wav 独立文件,而不是混流进去,因为混流需要一个未必装了的编码器;用 ffmpeg -i fox.mp4 -i fox.wav -c:v copy -c:a aac fox_with_audio.mp4 合并。完整说明见 MiniMax-H3 模型卡。
两台机器上的速度
与 stable-diffusion.cpp 的这组对比——22 帧、8 步、同一随机种子、两个引擎加载同一份权重——在跑过的两台机器上结论不同,而两组数字都值得保留:
| 22 帧、8 步、同一随机种子 | 256×256 | 640×384 |
|---|---|---|
M5 Pro,ggml_metal | sd.cpp 49.3 s → TensorSharp 20.9 s(快 2.4×) | sd.cpp 108.5 s → TensorSharp 63.1 s(快 1.7×) |
RTX 3080 Laptop 16 GB,ggml_cuda,三次取最好 | sd.cpp 37.8 s,TensorSharp 43.6 s(sd.cpp 快 1.15×) | sd.cpp 59.8 s,TensorSharp 63.7 s(sd.cpp 快 1.07×) |
按单个去噪步算,在这台 CUDA 机器上 TensorSharp 依然更快——3.325 s 对 3.338 s,由 8 步与 16 步的斜率求得——所以 640×384 上落后的那 3.9 s 全在固定的启动开销上,而其中约 3 s 根本不是推理:TensorSharp 编码 H.264,而 stable-diffusion.cpp 把 MJPEG+PCM 写进 AVI;此外还有 .NET 进程启动对原生二进制的差距。两台机器结论相反,是因为 3080 这台刻意很紧张——16 384 MiB 显存、31.7 GB 内存,对着一套 33.5 GB 的模型文件,权重装不下、页缓存也装不下,于是在一分钟量级的运行里启动开销占了主导。那台机器上的显存峰值,TensorSharp 是 15 780 MiB,stable-diffusion.cpp 是 12 035 MiB,后者用的是 --auto-fit --stream-layers --diffusion-fa --rng cpu;它默认的 --offload-to-cpu 路径在这台机器上根本跑不了这个模型,因为它要把 17.7 GB 锁进只有 12.3 GB 空闲的内存里。
在装不下整个模型的显卡上,现在有两件事会自动发生。其一,当去噪器与视频 VAE 放不下时,去噪器的设备驻留会在视频 VAE 加载之前先交还:已经跑完的 10.6 GB 去噪器加上 5.2 GB 的 VAE,在 16 GB 卡上是 15.8 GB,而 WDDM 不会让这次分配失败——它会悄悄用主机内存兜住溢出部分,于是整个解码都以 PCIe 的速度跑。解码期间的显存峰值从 16 041 MiB 降到约 5 600 MiB,在 640×384 上值 22 s。其二,去噪器文件现在会被顺序读一遍,并与它自己的上传流水化:权重是以指针形式绑定到 mmap 的 GGUF 上的,所以第一次上传原本会在主机到设备的拷贝内部把每一页从磁盘缺页读进来——0.91 GB/s,正是驱动最糟的情形,而页面驻留后是 5.97 GB/s。第一个去噪步 14.87 s → 约 10.2 s。两者合起来,让这台 RTX 3080 Laptop 在 640×384 上从 89.0 s 降到 63.7 s,在 256×256 上从 67.2 s 降到 43.6 s;prefault 开与关的输出逐字节相同。两者的判据并不相同——释放常驻看的是实测空闲显存,预读看的是空闲主存——各自在对应资源充裕时自动让路,因此资源够用的机器行为完全不变。
| 环境变量 | 默认值 | 作用 |
|---|---|---|
TS_H3_PHASE | 关 | 1 打印分阶段耗时——编码器打开 / 主干 / 拆卸、prefault、每一个去噪步、VAE 打开 / 解码。想看时间在自己硬件上究竟花在哪里,就用它。 |
TS_H3_PREFAULT | 3 | 去噪器 prefault:0 关,1 串行,2 与文本条件重叠,3 与上传流水化。 |
TS_H3_PREFAULT_THREADS | 1 | prefault 使用的读取流数量。 |
TS_H3_TE_GROUP | 关 | 取 n 时,把 50 层的文本编码器主干按每 n 层分组运行,每组跑完就释放其设备副本。 |
试过并被否掉的做法,同样在这台 RTX 3080 Laptop 上实测,免得有人再走一遍。文本编码器分层分组确实消掉了编码器自己的溢出(峰值 16 041 → 12 981 MiB),而且逐位相同,但它慢 3 s:这段主干是对十个 token 的提示词做一次性 prefill,每个权重只读一次,溢出的约 1.3 GB 只需过一次 PCIe,而分组仍然要搬完全部 17 GB,还额外增加分配/失效的开销——因此默认关闭,放在 TS_H3_TE_GROUP 后面。把 prefault 与文本条件重叠(TS_H3_PREFAULT=2)比与上传流水化更差,因为编码器自己的 17 GB 也要过同一个页缓存,会把刚放进去的页挤掉。多流 prefault 在这里同样是负收益——640×384 三次取最好:1 个流 63.9 s、4 个流 64.9 s、16 个流 66.6 s,仅编码器拆卸一项就从 2.2 s 涨到 4.0 s——因为它不像加载器自己的预热那样独占机器,而是与它正在预热的工作并发。给视频 VAE 做 prefault 是纯亏:多花 1.9 s 读取,解码却没有变化(9.4 s 对 9.1 s),因为那个 VAE 是算力瓶颈而不是缺页瓶颈。关掉 CUDA graph 没有区别(65.6 s 对 65.8 s),TS_GGML_ASYNC_COMPUTE=1 也没有(67.4 s 对 66.2 s),它管的是图提交而不是权重上传。用 ffmpeg 替换系统的 H.264 编码器则更慢,98.2 s。
纯 C# 的 CPU 路径
--backend cpu 让 H3 完全不用 ggml 跑起来,走的是 MiniMaxH3Direct{DiT, TextEncoder, VideoVae, AudioVae, VisionEncoder, VideoVaeEncoder3D, AudioVaeEncoder},底层是与 Wan 网络共用的 Direct{Context, Linear, Ops} 原语——H3 是唯一一个还没有纯托管 CPU 路径的生成模型。所有条件模式都可用:t2v、i2v、fl2v,以及参考图像、参考片段与参考音轨。一切都在主机上以 F32 运行,因此它是为了正确性、可移植性和没有加速卡的机器,而不是为了速度。
在完全相同的输入下与 GGML 路径对比——256×160、5 帧、单步、固定 --diffusion-seed。表中的对照组是 GGML 跟它自己比:它自己的 flash 内核对上显式 softmax 回退(TS_H3_NO_FLASH=1)——正是它让其余数字变得可读。
| 路径 | 托管 vs GGML(余弦) | 对照组(余弦) | 成片 PSNR |
|---|---|---|---|
| 单独的文本编码器(64 层,32B Q4_K_M) | 0.99999899 | — | — |
| t2v | 0.998740 | 0.997032 | 31.95 dB(对照组 28.17 dB) |
| i2v(3-D VAE 编码) | 0.999897 | — | 34.87 dB |
| 参考音频(音频 VAE 编码) | 0.999410 | — | 35.27 dB |
| 参考图像(视觉塔) | 0.998554 | 0.999275 | 28.80 dB(对照组 30.80 dB) |
| 单独的视觉塔输出 | 0.999919 | 0.999952 | — |
读懂这张表:t2v、i2v 与参考音频跟 GGML 的一致程度,比 GGML 自己两个注意力内核之间的一致程度还高。视觉塔是例外——它的残差大约是对照组的 1.4 倍,而不是低于对照组。在 737k 个元素上余弦达到 0.9999,说明这座塔在结构上是对的,但残差目前没有解释;关掉 flash 反而让一致性更差,所以 F16 的 K/V 转换并不是原因。这不能算“对齐”;在把对精度敏感的参考图像条件交给托管路径之前,值得先知道这一点。
DiT 与 GGML 并非逐位相同,也不可能相同,因为去噪器会放大极小的差异。TS_H3_DIT_LAYERS 会在两条路径上同时截断主干,把对比变成一条曲线:前 25 层两者的差异约为 1e-5,之后开始放大,而且是非单调的——深度 40 处为 1.15e-3,44 处为 1.5e-4,50 处为 1.26e-3。正是这个形状排除了 bug:如果某一层引入了错误,那么它之后的每个深度上曲线都不会下降。
同一段片子上,它对 ggml_cpu 的速度是互有胜负而非一律更慢:t2v 69 s 对 14 s,i2v 70 s 对 176 s,参考音频 63 s 对 71 s,参考图像 112 s 对 20 s。视觉塔是托管路径上最贵的一段。TS_H3_DUMP_TE、TS_H3_DUMP_VEL_V、TS_H3_DUMP_VEL_A 与 TS_H3_DUMP_VIS 会把这些张量写到磁盘,以便在一次前向上对比两条路径——不要改用成片对比,因为采样器会把第一步的差异放大。另外注意:--seed 是采样种子,视频生成的噪声取自 --diffusion-seed,不固定它就对比两次运行,实际上是在对比不同的噪声。
部署到服务端
在服务端,尺寸是启动参数,因为浏览器不会自己发尺寸:--video-width 640 --video-height 384 --video-steps 20 是文档给出的起点(--width / --height 是别名;只给其中一个时,H3 会从条件图像的宽高比推出另一个)。--video-mode 用于给只提供一种模式的部署固定条件模式;不写它,则每条请求各自推断,通常这才是你想要的。--video-frames、--fps、--video-vae、--video-text-encoder 与 --audio-vae 补齐其余部分。服务端完全没有 --cfg,这也是随仓库提供的配置既不设步数也不设引导的原因。
dotnet TensorSharp.Server.Host/bin/TensorSharp.Server.Host.dll --model models/minimax_h3_fl2va_pruned-Q4_K.gguf \
--backend ggml_metal --video-width 640 --video-height 384 --video-steps 20 --video-frames 22
curl -s http://localhost:5000/api/video-generate -H 'content-type: application/json' -d '{
"prompt": "a red fox trotting through falling snow, cinematic",
"width": 640, "height": 384, "frames": 22, "steps": 8, "cfg": 1.0,
"imagePath": "card.jpeg", "videoMode": "i2v"
}'
有三条路由接收这个请求体:POST /api/video-generate、POST /api/video-generate/stream(同样的东西,外加 SSE 进度事件)以及 OpenAI 形状的 POST /v1/videos/generations。三者共用同一个解析器,因此 videoMode、generateAudio、endImage、referenceImages、referenceVideos、referenceAudios 与 referenceVideoAudios 在三条路由上都可用,并且各自还接受下划线写法;referenceVideoAudios 按下标与 referenceVideos 配对。响应里带 audioUrl(OpenAI 路由上是 audio_url),模型没有产生音轨时为 null。所有文件字段都必须指向此前通过 /api/upload 上传的文件,并被限制在上传目录之内。模型层面的拒绝(加载了错误的检查点、关键帧与参考混用)会以 400 加上模型自己的报错信息返回,而不是笼统的 500。
GET /api/models 会返回一个 video 对象——非视频模型一律为 null——其中包含 family(minimax-h3)、supportsAudio、supportsImageConditioning、supportsEndImageConditioning、supportsReferenceConditioning 与 maxReferenceImages。Web UI 正是靠它判断该不该提供首帧、尾帧,或最多九个参考:它根据 supportsReferenceConditioning 启用参考附件控件,并在 maxReferenceImages 处停止添加,而不是去匹配架构字符串。
Wan 2.1 / 2.2(提示词 → 视频、图像 → 视频)
请下载 Turbo 版 DiT,而不是基础版。基础的 Wan2.2-TI2V-5B 走官方的 50 步 × CFG 配方,也就是每段视频 100 次 DiT 前向;同一模型的步数蒸馏 Turbo 版本经过训练可在无引导的 4 步内完成,TensorSharp 会从文件名识别出来,于是同一条请求从数小时变成数分钟。Turbo 仓库只提供 DiT,VAE 与文本编码器仍从基础仓库获取:
# 快车道:DiT 前向 4 次而不是 100 次。注意 “Wan2_2” 用的是下划线。
hf download hum-ma/Wan2.2-TI2V-5B-Turbo-GGUF Wan2_2-TI2V-5B-Turbo-Q8_0.gguf --local-dir models
hf download QuantStack/Wan2.2-TI2V-5B-GGUF VAE/Wan2.2_VAE.safetensors --local-dir models
hf download city96/umt5-xxl-encoder-gguf umt5-xxl-encoder-Q8_0.gguf --local-dir models
# 文本 → 视频
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/Wan2_2-TI2V-5B-Turbo-Q8_0.gguf \
--video-text-encoder models/umt5-xxl-encoder-Q8_0.gguf --video-vae models/VAE/Wan2.2_VAE.safetensors \
--prompt "A red fox trotting through falling snow, cinematic" \
--video-frames 81 --fps 24 --output out.mp4 --backend ggml_cuda
# 图像 → 视频(上传的图像作为首帧;仅 Wan 2.2 模型支持)
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/Wan2_2-TI2V-5B-Turbo-Q8_0.gguf \
--video-text-encoder models/umt5-xxl-encoder-Q8_0.gguf --video-vae models/VAE/Wan2.2_VAE.safetensors \
--image first_frame.png --prompt "the camera pushes in as the waves rise" \
--output out.mp4 --backend ggml_cuda
dotnet TensorSharp.Server.Host/bin/TensorSharp.Server.Host.dll --model models/Wan2_2-TI2V-5B-Turbo-Q8_0.gguf \
--video-text-encoder models/umt5-xxl-encoder-Q8_0.gguf --video-vae models/VAE/Wan2.2_VAE.safetensors \
--backend ggml_cuda --video-frames 121 --fps 24
加载时控制台会打印 step-distilled checkpoint detected -> 4 steps, guidance off (--diffusion-steps / --cfg override) 确认识别成功。在 M5 Pro 上用 ggml_metal 实测:一条 1088×832、121 帧(5 秒,720p 级)的图生视频请求,基础检查点需要 100 次 DiT 前向、约 3 小时 30 分,Turbo 检查点只需 4 次、17 分 30 秒——参数相同、分辨率相同,只有 --model 路径不同。若改到 480p(736×544、121 帧),Turbo 检查点 6 分 19 秒即可完成。
若要复现基础配方——例如生成参考样本,或使用没有蒸馏版本的 Wan 2.1 检查点——只需替换第一条下载命令与 --model 路径:
hf download QuantStack/Wan2.2-TI2V-5B-GGUF Wan2.2-TI2V-5B-Q8_0.gguf --local-dir models
# 50 步 x 2 次 CFG 前向;--cfg-cache-stride 以少量精度换取 1.30x / 1.43x
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model models/Wan2.2-TI2V-5B-Q8_0.gguf \
--video-text-encoder models/umt5-xxl-encoder-Q8_0.gguf --video-vae models/VAE/Wan2.2_VAE.safetensors \
--prompt "A red fox trotting through falling snow, cinematic" \
--video-frames 81 --fps 24 --cfg-cache-stride 2 --output out.mp4 --backend ggml_cuda
Wan 2.2 A14B 图生视频请把两个蒸馏专家下载到同一个 --local-dir——加载器会依据高噪专家的文件名找到对应的低噪专家,因此只需传一个 --model 路径。A14B 使用 Wan 2.1 的 VAE:
hf download jayn7/WAN2.2-I2V_A14B-DISTILL-LIGHTX2V-4STEP-GGUF \
high_noise/wan2.2_i2v_A14b_high_noise_lightx2v_4step-Q4_K_M.gguf --local-dir models
hf download jayn7/WAN2.2-I2V_A14B-DISTILL-LIGHTX2V-4STEP-GGUF \
low_noise/wan2.2_i2v_A14b_low_noise_lightx2v_4step-Q4_K_M.gguf --local-dir models
hf download QuantStack/Wan2.2-I2V-A14B-GGUF VAE/Wan2.1_VAE.safetensors --local-dir models
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll \
--model models/high_noise/wan2.2_i2v_A14b_high_noise_lightx2v_4step-Q4_K_M.gguf \
--video-text-encoder models/umt5-xxl-encoder-Q8_0.gguf --video-vae models/VAE/Wan2.1_VAE.safetensors \
--image first_frame.png --prompt "the camera pushes in as the waves rise" \
--output out.mp4 --backend ggml_cuda
请在 Wan 训练过的分辨率上生成。Wan 的训练分辨率是 480p(832×480)与 720p(1280×704),TI2V-5B 原生就是 720p 模型。低于约 0.3 MP 时 DiT 已经超出分布,无论花多少步,画面都会发糊、不稳定并出现偏色——低于该阈值时流水线会给出警告。请先在受支持的分辨率上生成,再缩小,而不是直接生成小尺寸。
Wan 的伴随文件、成本与分辨率
Wan 把提示词——在 Wan 2.2 检查点上还可以加一张上传的首帧图像——变成 H.264 MP4。--model 指定的 GGUF 只是 DiT,TensorSharp 会在其旁边解析两个伴随文件(放在 VAE/、HighNoise/ 或 LowNoise/ 子目录中同样可以):
- UMT5-XXL 文本编码器 —— 提示词 → 条件向量(
--video-text-encoder/TS_WAN_TE)。去噪开始前即从显存释放。 - 因果 3D 视频 VAE —— 潜变量 ↔ 帧(
--video-vae/TS_WAN_VAE)。需要哪一个由 DiT 自身读出,不用你指定:Wan 2.1 与 A14B 使用wan_2.1_vae,TI2V-5B 使用Wan2.2_VAE。 - A14B 的第二个专家 —— 两个 14B 专家必须同时存在。把它们放在同一目录,或放进
HighNoise/与LowNoise/子目录,加载器会依据文件名配对(TS_WAN_DIT2可覆盖;没有对应的 CLI 参数)。
| 系列 | 潜空间 | 模式 | 说明 |
|---|---|---|---|
| Wan 2.1 T2V(1.3B / 14B) | 16 通道,8×8×4 | 文本 → 视频 | 单个 DiT |
| Wan 2.2 TI2V-5B | 48 通道,16×16×4 | 文本 → 视频、图像 → 视频 | 稠密 5B,24 fps,可出 720p |
| Wan 2.2 A14B(T2V / I2V) | 16 通道(I2V 输入 36 通道) | 文本 → 视频、图像 → 视频 | 两个 14B 专家在时间步边界切换 |
每个去噪步是一张常驻权重的 ggml 图(CUDA 图捕获、flash attention;TI2V 图生视频还带 per-token 时间步调制),视频 VAE 的编码与解码各是一张图。各阶段按顺序交接显存——先文本编码器,再 DiT,最后 VAE——因此峰值约为 max(TE, DiT + attention, VAE):TI2V-5B 在 16 GB GPU 上 8 分钟内生成 81 帧 480p 图生视频,A14B 的两个专家也能在同一张卡上顺序执行。数值上与 diffusers 对齐(DiT 余弦相似度 > 0.995,VAE 编码器 > 0.999,解码 59.9 dB PSNR),同等负载下 Wan 2.1 端到端比 stable-diffusion.cpp 快 6.0×。
检查点决定端到端耗时
Wan 的 DiT token 数为 latent_frames × (h/2) × (w/2),自注意力开销为 O(tokens²),因此 5 秒 720p 视频确实是个大活:1088×832、121 帧就是 27 404 个 token,而 TI2V-5B 的官方配方要在其上跑 50 步 × 2 次无分类器引导前向 = 100 次 DiT 前向。步数蒸馏检查点经过训练可在无引导的 4 步内完成,因此同一段视频只需 4 次前向——去噪工作量只有 1/25。TensorSharp 会从 DiT 文件名识别(turbo、distill、lightning、lightx2v、fastwan、-dmd,或显式的 …-4steps-…,步数取 1–16),在加载时打印 step-distilled checkpoint detected -> 4 steps, guidance off 并自动套用该配方;--diffusion-steps 与 --cfg 仍可覆盖。
M5 Pro、ggml_metal、TI2V-5B Q8_0、1088×832×121 帧(27 404 token,图 → 视频) | 基础版,优化前 | 基础版,现在 | Turbo,现在 |
|---|---|---|---|
| DiT 前向次数 | 100(50 步 × CFG) | 100 | 4(无引导) |
| 单次前向 | 206.2 s | 120.2 s | 120.2 s |
| 去噪总耗时 | 20 615 s | 12 020 s | 481 s |
| VAE 解码 121 帧 | 863 s | 563 s | 563 s |
| 端到端 | 约 5 小时 58 分 | 约 3 小时 30 分 | 17 分 30 秒 |
两条加速路径互相独立:单次前向约 1.7× 来自把 DiT flash 注意力的 K/V 保持为 F16(单次 27k token 自注意力实测 2.02×,与 diffusers 参考实现的余弦相似度 0.999964——TS_WAN_DIT_KV_F16=0 可恢复 F32),以及在 Metal 上把 Wan VAE 卷积改走 MPSGraph 而非 ggml 的 im2col+GEMM 降级路径(736×544×81 帧下 VAE 解码 159 s → 80 s,数值不变,93.9 dB PSNR——TS_WAN_VAE_MPS_CONV=0 可回退);另一条是蒸馏检查点带来的前向次数减少 25×。一旦用上蒸馏检查点,瓶颈就变成 VAE 解码(约占整轮的 55%),而不再是 DiT。
帧数与分辨率决定其余部分
| 输出(M5 Pro,同一 Turbo 检查点与图像) | Token | 去噪 | VAE 解码 | 总计 |
|---|---|---|---|---|
| 736×544 × 81 帧(3.4 秒,480p 级) | 8 211 | 84 s | 159 s | 4 分 09 秒 |
| 736×544 × 121 帧(5 秒,480p 级) | 12 121 | 137 s | 237 s | 6 分 19 秒 |
| 1088×832 × 121 帧(5 秒,720p 级) | 27 404 | 481 s | 563 s | 17 分 30 秒 |
480p(约 0.4 MP)本身就是 Wan 训练过的分辨率,因此前两行属于分布内的正常档位,而不是降级模式——想在几分钟内出片就该选它。质量真正开始下滑是在约 0.3 MP 以下:那里 DiT 已超出分布,无论花多少步,画面都会发糊、不稳定并偏色,流水线也会给出警告。请在受支持的分辨率上生成后再缩小(用 --width 480 --height 704 而不是 320×480)。
要让一次大请求更便宜,按效果排序:(1) 换成步数蒸馏检查点——100 次前向变 4 次,压倒其他一切;(2) 减少帧数——121 → 61 大致把注意力工作量降到四分之一,VAE 解码减半;(3) 减小画面面积,但不要低于约 0.3 MP;(4) 减少步数,仅限基础检查点——30 步与 50 步观感接近,成本低 1.7×;(5) --cfg-cache-stride 2 或 3,即每 N 步才跑一次无条件前向、其余步复用缓存的引导方向——50 步下 100 次前向只跑 77 次(1.30×)或 70 次(1.43×)。它是近似方法,需要对齐参考样本时请关掉;在本就无引导的蒸馏检查点上也没有作用。
在 NVIDIA 上 ggml_cuda 是最快的 Wan 后端:RTX 2000 Ada 16 GB 跑官方 480p Wan2.1-1.3B 配方时为 12.0 s/步,ggml_vulkan 为 17.2,直连 cuda 后端为 19.3。Wan 完全不支持 mlx;cpu / ggml_cpu 后端只用于功能性验证。
可通过 CLI(--prompt、可选 --image、--video-frames、--fps、--flow-shift、--sampler、--negative-prompt)、HTTP API(/v1/videos/generations)或 Web UI 聊天上传图像来驱动。在服务端,--video-frames 与 --fps 设定的是默认值而非上限——请求自带 frames 或 fps 时各自覆盖;两者都不给时套用模型自身的配方(Wan2.2-TI2V 为 49 帧 24 fps,其余为 33 帧 16 fps)。帧数会对齐到 VAE 的时间网格(4k+1)。完整说明见仓库中的 docs/models/wan_zh-cn.md 卡片。