这两天搭通了一套文生视频服务,项目标题很直白:vLLM-Omni + Wan2.1-T2V-1.3B 部署。选这个组合的原因很现实,我要做的多模态内容生成 demo 需要输入一段自然语言描述,输出短视频文件,而且不能只在离线脚本里验证,得能通过 HTTP 接口给上层应用调用。vLLM-Omni 负责多模态模型的推理调度,Wan2.1-T2V-1.3B 负责把文本变成视频,两者接在一起,“文本进、视频出”的服务就算立住了。
这篇不打算复读官方 readme,更像一份手术记录:环境怎么搭、显存怎么算、命令怎么写、调用怎么验、哪些地方最容易翻车。适合正在做开源文生视频服务化的团队,尤其是想在有限显卡上把模型真正跑起来的人。无论你之前有没有碰过 vLLM,只要照着这个流程走,大概率能把服务拉起来。
1. 项目思路拆解:这套组合到底解决什么问题
1.1 为什么推理底座选 vLLM-Omni
先说结论:如果你只是想“拿一张卡生成一条视频”,那用最原始的加载权重脚本也能跑。但要做成服务,一次接受十几个甚至几十个请求,还得稳定输出,那就要挂一个推理框架来兜底。
vLLM-Omni 的核心能力有两条:PagedAttention 和连续批处理。PagedAttention 可以理解成把显存划分成一个个小格子,KV Cache 按需申请、按需释放,需要多少就占多少,而不是一次性把所有内容都塞进显存;连续批处理则是把不同用户的请求在同一个 GPU 计算周期内切碎并穿插执行,一个请求在等显存的时候,另一个请求的矩阵运算可以插进来。这两件事对视频生成特别关键,因为视频 token 序列动辄上千,任何一个 token 用完就不再占内存,调度器会动态管理。没有这套机制,多用户并发时只能一条一条排队,体验会非常差。
vLLM-Omni 这里还补上了多模态输入输出支持。视频任务不只是自回归生成文本 token,它前面有文本编码器,中间有 DiT 主干,后面还有 VQ 解码器。框架把这些模块统一放进一个服务进程里,推理时按需调用,返回的结构也和 OpenAI 兼容,前端接起来非常省事。
1.2 为什么视频模型选 Wan2.1-T2V-1.3B
Wan2.1-T2V-1.3B 是一个参数量只有 1.3B 的文生视频模型。整体结构大致分三段:文本编码器负责把输入的自然语言转成语义向量,DiT 主干根据语义向量逐步预测视频 token 序列,最后 VQ 解码器把 token 还原成可见的画面帧。
选它最直接的原因是部署门槛低。当前能用的开源视频生成模型,不少是几十 B 的参数量,单卡基本没戏,要上多卡集群或者很贵的显存。Wan2.1-T2V-1.3B 默认输出分辨率在 480P 级别,默认帧数不算多,出片时长大概几秒,做 demo 和技术验证完全够用。如果你想验证“一条文本进去,一段视频出来”的整条链路,又不想一开始就被硬件卡死,这个模型是很合适的中介点。
另一个原因是这个模型在开源社区已经积累了不少推理适配经验,权重格式稳定,文本编码器、DiT、解码器之间的调用关系清晰。相比一些只放出推理代码、不放出权重细节的模型,它更适合被 vLLM 这类框架集成。
1.3 目标场景和适用边界
这套组合适合以下几类人:
- 想快速搭出“文生视频 API”的开发者,需求是给一句 prompt 返回一段 mp4;
- 做多模态 Agent 的团队,需要让模型具备视觉内容生成能力;
- 做批量短视频素材的工具类产品,需要低成本的视频生成管道;
- 想在 vLLM 框架上扩展视频能力的框架使用者。
同时也要明确边界:1.3B 模型出片质量有限,光影细节、复杂逻辑推理都做不到特别精致。如果目标是商业级高清视频渲染,那需要更大模型和更贵的硬件,这套组合只解决“有没有”的问题,不负责“好不好到能上线”的问题。
2. 部署前的环境准备
2.1 硬件资源估算与显存规划
很多人第一句就问“我这个显卡能不能跑”。与其直接回答,不如把显存账算明白。按 FP16 精度估算:
| 组件 | 大概显存占用 | 说明 |
|---|---|---|
| DiT 主模型权重 | 2.6 GB 左右 | 1.3B 参数,FP16 重量 |
| 文本编码器 | 4 ~ 5 GB | 负责 prompt 语义编码 |
| VQ 解码器 | 1 GB 左右 | 负责视频 token 解码成帧 |
| KV Cache 和激活值 | 3 ~ 6 GB | 随并发和序列长度变化 |
| 视频张量临时缓存 | 2 GB 左右 | 后处理和返回前的中转区 |
加起来,一个请求跑通大约需要 10 到 12 GB 显存。所谓 16G 能跑,指的是比较紧的 16G,基本只能低并发跑单条;如果要开并发,推荐 24G 显存起步,或者两张 16G 显卡用张量并行。32G 单卡就非常舒适了。
注意:显存估算永远要留余量。驱动、系统桌面、其他进程都会占显存,不能把卡上所有显存都分配给推理服务。我习惯预留 15% 给这类开销,否则跑一会就会出现莫名的 OOM。
2.2 软件依赖与版本对齐
软件栈的基线建议:
- 操作系统:Ubuntu 20.04 或 22.04
- Python:3.10
- CUDA:12.1 及以上
- PyTorch:2.1 及以上
- NVIDIA 驱动:535 或更新版本
vLLM-Omni 建议直接拉对应分支源码编译,不要只依赖 pypi 上的旧包。源码安装的好处是能拿到当前 commit 的完整能力,尤其是视频模型支持往往更新很快,老版本的 vLLM 很可能不认识 Wan2.1-T2V-1.3B 的网络结构。
安装命令大致这样:
conda create -n vllm-video python=3.10 -y conda activate vllm-video pip install torch==2.1.2 --index-url https://download.pytorch.org/whl/cu121 git clone <vLLM-Omni 源码仓库地址> cd vllm-omni pip install -e .flash-attn 是另一个变量。它会直接编译 CUDA kernel,非常容易在环境不对时失败。如果编译卡住,先确认本机 CUDA 编译器版本和 PyTorch 的 CUDA 版本一致,比如 PyTorch 是 cu121,那 nvcc 也要是 12.1 系列。装完跑一个检查:
python -c "import torch; print(torch.version.cuda); import vllm; print(vllm.__version__)"能正常打印,说明基础环境没问题,再往下走。
2.3 模型权重获取与目录规划
Wan2.1-T2V-1.3B 的权重可以从国内可直接访问的模型社区下载,搜索 Wan-AI/Wan2.1-T2V-1.3B 仓库。下载时优先拿 safetensors 格式,避免后面格式转换的折腾。
下载完先别急着启动服务,确认目录结构完整。一个可用的模型目录大致长这样:
Wan2.1-T2V-1.3B/ ├── config.json ├── model.safetensors ├── tokenizer.model ├── tokenizer_config.json ├── vq_decoder.safetensors └── text_encoder/如果拿到的是旧式 pt 权重,需要转成 safetensors 再让 vLLM 加载。转换原则很简单:加载原模型的 state_dict,然后用 safetensors 格式原样保存,key 一个都不要改。不要图省事直接改文件后缀,那样加载必挂。
权重校验这块也值得做。几十个 GB 的文件在传输过程中很容易出现丢字节的问题,启动时报“文件损坏”或“加载到中间突然失败”很多时候不是代码问题,而是权重文件本身坏了。下载完先算一次 sha256,和仓库提供的哈希值比对,一致再进下一步。
3. 核心部署实操过程
3.1 启动文生视频 HTTP 服务
整个部署最核心的就一条命令:
vllm serve /data/models/Wan-AI/Wan2.1-T2V-1.3B \ --task generate \ --max-model-len 8192 \ --gpu-memory-utilization 0.86 \ --tensor-parallel-size 1 \ --host 0.0.0.0 \ --port 8000 \ --trust-remote-code启动后观察日志。看到类似这样的输出,说明服务已经起来了:
INFO: Started server process [12345] INFO: Waiting for model to be loaded... INFO: Model loaded in 42.3s INFO: Uvicorn running on http://0.0.0.0:8000这里有一个容易踩的坑:如果不加--task generate,vLLM 会默认把模型当纯文本 LLM 加载,加载阶段可能不报错,但真正请求时会提示模型不支持文本补全,或者直接报结构错误。视频和多模态模型必须显式声明生成任务类型,这一步不能省。
3.2 关键启动参数解读
| 参数 | 作用 | 建议值 |
|---|---|---|
--max-model-len | 控制最大序列长度,直接决定 KV Cache 占用量 | 8192 是相对保守值 |
--gpu-memory-utilization | 指定推理服务可用显存比例 | 0.85 ~ 0.92,留出余量 |
--tensor-parallel-size | 张量并行卡数 | 单卡填 1,双卡填 2 |
--max-num-seqs | 同时处理的序列数量上限 | 默认即可,需要并发再调 |
--trust-remote-code | 允许加载模型仓库里的自定义代码 | 必须有,很多模型依赖它 |
--enforce-eager | 关闭 CUDA Graph,降低启动显存占用 | 显存不够时使用,性能略降 |
--gpu-memory-utilization很多人不敢调高,怕 OOM。实际上这个参数控制的是服务启动时预分配的显存上限,0.86 表示最多用 86%,剩下的给驱动和系统。如果你对显存规划有把握,可以调到 0.9,减少 KV Cache 被挤占的可能。
--max-model-len要特别注意。视频模型出的 token 序列可能很长,如果设得太小,视频会明显变短甚至直接截断;设得太大,KV Cache 占满显存,启动阶段就会 OOM。我这边先用 8192 起步,跑通后再根据实际单条请求的 token 消耗量做调整。
3.3 用 OpenAI 兼容接口验证文生视频
服务起来之后,先用 curl 做一次冒烟测试:
curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "wan2.1-t2v", "prompt": "A red fox running through snowy forest at night, cinematic lighting", "max_tokens": 4096 }'返回的 JSON 里,choices[0].message.content会包含视频数据,通常是 base64 编码的字符串,有时候是一个对象数组。用 Python 解析并落地成文件更靠谱:
import requests import base64 import json resp = requests.post( "http://127.0.0.1:8000/v1/chat/completions", json={ "model": "wan2.1-t2v", "prompt": "A red fox running through snowy forest at night, cinematic lighting", "max_tokens": 4096, }, timeout=120, ) data = resp.json() content = data["choices"][0]["message"]["content"] # 兼容字符串和对象数组两种返回结构 if isinstance(content, str): video_b64 = content.split(",", 1)[-1] else: video_b64 = content[0].get("video", content[0].get("data", "")) with open("/tmp/output.mp4", "wb") as f: f.write(base64.b64decode(video_b64))判断成功的标准有两个:文件能正常打开,且播放出来的画面和 prompt 描述明显相关。如果文件打不开,优先怀疑返回结构解析有问题,把原始 JSON 完整打印出来看,别急着怪模型。
提示:
max_tokens设置太小时,视频会在中途截断,表现是文件存在但很短,或者画面后面全是黑帧。先给一个较大的值(4096)跑通,再根据实际 token 消耗量回退调整。
4. 常见问题与排查技巧实录
4.1 启动阶段的典型报错速查
| 现象 | 常见原因 | 处理思路 |
|---|---|---|
| 启动时提示 CUDA driver 版本过旧 | 驱动和 CUDA 运行时不对齐 | 升级 NVIDIA 驱动,容器内用nvidia-smi确认驱动可见 |
| flash-attn 编译失败 | nvcc 和 PyTorch CUDA 版本不一致 | 统一 CUDA 版本,或换预编译 wheel |
| 加载模型到一半报文件损坏 | 权重下载不完整 | 重新下载,sha256 校验后再试 |
| 报 max seq len 超限 | KV Cache 把显存占满 | 调小--max-model-len,或调高--gpu-memory-utilization |
| 提示模型不支持 generate task | 漏了--task generate | 补上参数重启 |
启动阶段的问题绝大多数和版本对齐有关。先记住一个经验:不要混装不同 CUDA 版本的 PyTorch 和编译工具链。任何“编译到一半莫名出错”“kernel 加载不了”的问题,先查版本,再换包,这个顺序不能反。
4.2 推理阶段的效果问题
服务能启动只是第一步,真正让人头疼的是“视频出来了但不正常”。
最常见的是生成视频黑屏。这个我踩过几次,原因不一定是模型坏了,而是 prompt 太抽象。比如中文输入“一只漂亮的猫”,文本编码器对齐效果不稳定;换成英文描述“a cute cat walking in a bright room, soft lighting, high quality”,出片率会高很多。文生视频模型本质上还是在做语义到视觉 token 的映射,prompt 越具体,越贴近训练数据,生成结果越稳定。
还有一种情况是视频只有开头几秒有画面,后面全黑。优先查max_tokens是否够长。视频 token 数量比文本多得多,一个 16 帧的短片可能需要上千 token,4096 只是保守值,小模型设 2048 也很容易截断。
另外,首次请求特别慢是正常现象。模型刚加载完,CUDA kernel 需要热身,缓存也没建立起来。我这边第一条请求花了接近 20 秒,后续同长度请求掉到 5 到 8 秒。如果你第一条请求正常、后续更快,说明框架调度进入稳态,不用紧张。
4.3 问题定位的三板斧
遇到稀奇古怪的问题,我一般按三个顺序排查:
第一,先把显存情况摸清楚。启动服务前用nvidia-smi -l 1实时观察显存变化,如果请求时显存直接顶满然后告警,先降低并发或者调低--max-model-len。
第二,用离线脚本排除网络和 API 层干扰。直接把请求打到 vLLM 的 Python API 上,跳过 HTTP 层,可以定位问题是在模型推理还是服务封装。
第三,回归最小用例。每次改完环境或参数,用同一条固定 prompt 生成视频,对比输出文件的长度和内容。如果改动之后表现完全不一致,说明参数配置出了问题,而不是模型本身波动。
5. 优化方向与实践建议
5.1 吞吐与并发调优
服务跑通之后,如果要做多用户接入,有几个参数值得调。
--max-num-seqs控制同时处理的序列数量,默认值比较保守,想要更高吞吐可以调大一些。但这不是越大越好,序列太多会挤压显存,KV Cache 总量又有限,最终会出现排队和换入换出,反而变慢。我这边实测,8 到 16 是比较合理的区间。
--enable-chunked-prefill值得开启。它的思路是把 prompt 编码阶段和自回归生成阶段交错执行,避免一个超长 prompt 独占 GPU 太久。对视频生成这种“一段 prompt 进来,大量 token 出去”的任务,这个开关带来的提升是肉眼可见的。
如果你的显卡支持 FP8,--kv-cache-dtype fp8也能压低 KV Cache 占用,间接提升并发上限。前提是模型本身对精度损失不敏感,视频生成任务一般来说可以接受,但建议开 A/B 对比。
5.2 从文本到多模态:扩展交互能力
既然底座选了 vLLM-Omni,就别只停留在文本进视频出。这套框架的价值在于多模态统一调度,可以在旁边再接一个语音识别模块,把语音输入转成文本,再送进 Wan2.1-T2V,形成“你说话,它出视频”的完整链路。
另一种扩展思路是前面再接一个 LLM 作为提示词生成器。用户只给一个粗糙意图,比如“给我做一个关于春天的视频”,LLM 先把它扩写成一段高质量英文视频描述,再传给视频模型。这一步能把出片质量提升不少,因为 LLM 更擅长生成符合模型偏好的详细 prompt。
这些都是可落地的方向,而且不影响原有服务。只要把文本到视频的调用封装成一个内部函数,上层怎么扩展都行。
5.3 最后分享两个值得记住的经验
第一,权重下载之后一定先校验,再谈部署。几十 GB 的文件,传输过程中坏几个字节概率不低,很多“启动到一半异常退出”的坑,换了权重文件马上就好了。
第二,每次改动环境或依赖后,不要只跑一次通过就算完。用同一条 prompt、同一个参数跑三条视频,对比输出文件的 md5。如果依赖升级后输出 md5 稳定不变,说明推理结果没被环境改动破坏。这个习惯帮我避过很多次“日志全绿,实际渲染结果已经崩了”的暗坑。
整套流程走下来,vLLM-Omni + Wan2.1-T2V-1.3B 这套组合是可以稳定供服务的。它不追求生成效果的上限,但把“文本到视频”这件事的工程链路做得足够顺。硬件的坑、环境的坑、参数的坑,提前有数之后,一个下午基本能完成从零到能用的部署。