vllm-omni 运行 Lance 3B 全模态离线推理:t2i / t2v / 图像视频编辑与理解实战
【免费下载链接】vllm-omniA framework for efficient model inference with omni-modality models项目地址: https://gitcode.com/GitHub_Trending/vl/vllm-omni
Lance(bytedance-research/Lance)是一个 3B 规模的统一自回归(autoregressive)+ 扩散(diffusion)多模态模型,基于 Qwen2.5-VL 骨干,采用与 BAGEL 同源的 ByteDance Mixture-of-Transformers(MoT)架构。本指南以examples/offline_inference/lance/README.md为骨架,结合仓库源码,完整讲解如何在 vllm-omni 中通过单卡 16 GB 以上显存运行 Lance 的全部六类模态任务(文生图、文生视频、图像编辑、视频编辑、图像理解、视频理解),并深入剖析底层LancePipeline的实现细节,使你不仅能跑通示例,还能理解 checkpoint 布局、mRoPE 位置编码、Wan2.2 VAE 等关键原理。
Lance 与 vllm-omni:BAGEL 血统的三种特化
Lance 的独特之处在于它是一个单模型覆盖生成与理解的统一多模态模型:同一个 Qwen2-MoT 主干既能做自回归文本生成,也能做扩散式图像 / 视频生成。官方发布的Lance_3Bcheckpoint 使用与 BAGEL 完全一致的*_moe_genMoT 权重布局,因此 vllm-omni 的实现思路是直接复用 BAGEL 的 transformer 核心与整条前向/生成机制,只特化三个地方(见 pipeline_lance.py):
- Checkpoint 布局:HF 仓库
bytedance-research/Lance在一个仓库内打包了Lance_3B/(图像)与Lance_3B_Video/(视频)两个 LLM checkpoint、Qwen2.5-VL-ViT/(理解 ViT)与Wan2.2_VAE.pth(VAE)。它没有 BAGEL 风格的顶层config.json携带vae_config/vit_config/latent_patch_size,这些是取自上config/config_factory.py与inference_lance.sh的常量,被硬编码在LANCE_DEFAULTS中。 - 理解 ViT:采用打包的 Qwen2.5-VL 视觉塔(
Qwen2.5-VL-ViT/vit.safetensors),取代 BAGEL 的 SigLIP。 - VAE:采用 Wan2.2(
Wan2.2_VAE.pth),取代 BAGEL 自带的自编码器。
在 lance_transformer.py 中,Lance 的 LLM 部分就是 BAGEL 的 Qwen2-MoT transformer 原样复用——Bagel/Qwen2MoTForCausalLM/Qwen2MoTConfig/NaiveCache全部直接 re-export,仅新增了面向 Qwen2.5-VL packedpixel_values+image_grid_thw布局的LanceBagel子类、3D 潜变量位置编码LancePositionEmbedding3D与 Qwen2.5-VL 视觉包装器。
六个模态任务对应上游 HF model card:
t2i(文生图)、t2v(文生视频)、image_edit(图像编辑)、video_edit(视频编辑)、x2t_image(图像理解)、x2t_video(视频理解)。示例脚本还额外提供了image2video(首帧图生视频)路径。
硬件要求
- 单张 NVIDIA GPU,16 GB 以上显存(BF16 精度,仓库在 B300 / A100 上验证);
- CUDA ≥ 12.4;
- 推荐安装
flash-attn,原因见下文“注意力后端”小节。
快速开始:文生图与文生视频
示例入口脚本为 end2end.py,它是单阶段(single-stage)离线推理脚本,不需要任何 deploy YAML。先看 README 中的两条核心命令:
# 文生图(Text-to-image) python examples/offline_inference/lance/end2end.py \ --model bytedance-research/Lance \ --prompts "a corgi astronaut on the moon, cinematic" \ --steps 30 --cfg-text-scale 4.0 --timestep-shift 3.5 \ --height 1024 --width 1024 \ --output ./out # 文生视频(Text-to-video,使用 Lance_3B_Video 子目录) python examples/offline_inference/lance/end2end.py \ --model bytedance-research/Lance/Lance_3B_Video --modality text2video \ --num-frames 25 --video-height 480 --video-width 768 \ --prompts "a cat playing piano, cinematic" \ --steps 30 --fps 8 --output ./out几点关键事实:
- checkpoint 自动解析:除
video_edit必须显式指定--model bytedance-research/Lance/Lance_3B_Video(以加载 3Dlatent_pos_embed表)外,其余路径都可以把--model指到顶层bytedance-research/Lance仓库,脚本会自动解析正确的子 checkpoint。 - 无需额外下载:HF 仓库已捆绑全部组件(
Lance_3B/、Lance_3B_Video/、Qwen2.5-VL-ViT/、Wan2.2_VAE.pth),一条命令即可开始。 - 视频帧数上限为121(
--num-frames参数注释中注明 max 121),对应潜变量空间的 31 帧上限,详见下文源码解析。
全部模态选项
--modality的合法取值(end2end.py 第 34-38 行):
| modality | 任务 | 必填输入 | 输出 |
|---|---|---|---|
text2img(默认) | 文生图 | --prompts | PNG |
text2video | 文生视频 | --prompts | MP4 |
image2video | 首帧图生视频 | --prompts+--image-path | MP4 |
img2img | 图像编辑 | --prompts+--image-path | PNG |
video2video | 视频编辑 | --prompts+--video-path | MP4 |
img2text | 图像理解(描述 / VQA) | --image-path(提示词可选,默认 "Describe this image in detail.") | 文本 |
video2text | 视频理解 | --video-path | 文本 |
参数详解
parse_args 中定义的完整参数如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
--model | bytedance-research/Lance | HF 仓库名或本地路径 |
--prompts | None | 文本提示词(nargs="+"可传多个) |
--txt-prompts | None | 提示词文件路径,每行一条,读取时自动去空行 |
--modality | text2img | 上述七种模态之一 |
--image-path | None | img2text / img2img / image2video 的输入图像 |
--video-path | None | video2text / video2video 的输入视频 |
--num-frames | 25 | text2video 的 RGB 帧数(最大 121) |
--video-height/--video-width | 480 / 768 | 视频帧分辨率 |
--height/--width | None(=1024) | t2i 图像分辨率 |
--fps | 12 | 保存 MP4 的帧率(对齐上游 Lance 的save_fps=12) |
--output | . | 输出目录(自动创建) |
--steps | 30 | 去噪步数(Lance 默认 30) |
--cfg-text-scale | 4.0 | 文本 CFG 强度(Lance 默认 4.0) |
--timestep-shift | 3.5 | Flow-match 时间步偏移(Lance 默认 3.5) |
--negative-prompt | None | 负向提示词(仅在cfg_text_scale > 1.0时生效) |
--seed | 42 | 随机种子 |
--max-text-tokens | 512 | x2t 理解路径的最大生成 token 数 |
--do-sample/--no-sample | 采样开启 | x2t 生成是否采样 |
--text-temperature | 0.8 | x2t 生成采样温度 |
--system-prompt | None | 覆盖任务系统提示词(x2t 专用) |
理解路径(x2t)的两个"坑"
Lance 的贪心解码器对很多提示词会立即输出 EOS,因此理解路径默认开启采样:
- 默认
--text-temperature 0.8——注释明确说明"低于 ~0.7 时 Lance 频繁立即 EOS,0.8 是良好默认值"; - 可通过
--no-sample切换回贪心解码; --max-text-tokens控制理解回答的长度上限(默认 512)。
--system-prompt的作用同样值得注意:x2t_image / x2t_video 任务使用 per-example 的 QA 风格指令(如 "Look at the image carefully and answer the question.")。如果不提供 system prompt,模型会回落到 caption 风格默认提示词,输出变成"描述图像"而非"回答问题"。在 prompts.py 的render_lance_prompt中,system_prompt参数直接覆盖SYSTEM_PROMPTS表中的默认任务提示词。
视频模态的 checkpoint 约束
视频模态(text2video/video2video/video2text/image2video)要求Lance_3B_Video子 checkpoint。脚本会做一次智能重写(end2end.py 第 231-236 行):
if args.modality in ("text2video", "video2video", "video2text", "image2video"): if not str(resolved_model).rstrip("/").endswith("Lance_3B_Video"): candidate = os.path.join(resolved_model, "Lance_3B_Video") if os.path.isdir(candidate): resolved_model = candidate如果不这样做,vllm-omni 会静默加载图像版Lance_3B的(4096, 2048)位置编码表,而视频潜变量的时间维t_lat >= 1会立即越界崩溃。
采样参数如何流入引擎
end2end.py将--steps、--seed、--height、--width直接写到default_sampling_params_list[0]上,把cfg_text_scale、timestep_shift等放入diffusion_params.extra_args:
extra["cfg_text_scale"] = args.cfg_text_scale extra["timestep_shift"] = args.timestep_shift if args.modality in ("img2text", "video2text"): extra["max_think_tokens"] = args.max_text_tokens extra["do_sample"] = args.do_sample extra["text_temperature"] = args.text_temperature if args.negative_prompt is not None: extra["negative_prompt"] = args.negative_prompt diffusion_params.extra_args = extra在流水线侧(pipeline_lance.py 的_forward_t2v/_forward_image_edit),这些extra_args会与 prompt 字典里的extra_args合并,并影响以下默认行为:
cfg_interval=(0.4, 1.0):上游 Lance 默认在 t<0.4 时关闭 CFG;vllm-omni 保持该区间,否则最后几步去噪会与上游输出产生分歧;cfg_renorm_type="global"、cfg_renorm_min=0.0:CFG 重归一化参数,两侧默认一致;cfg_img_scale=1.0(BAGEL 术语中的cfg_vit_scale=1.0):图像条件 CFG 默认关闭,与上游一致;- 负向提示词默认空字符串,仅在
cfg_text_scale > 1.0时构建无条件 KV 缓存分支。
引擎级参数:单阶段 Lance 如何被拉起
Lance 是单阶段扩散流水线,脚本把所有引擎旋钮以扁平 kwargs 传给Omni构造器(create_default_diffusion会据此物化 stage 配置),这与vllm_omni/deploy/lance.yaml中的默认值完全一致:
omni_kwargs.setdefault("pipeline", "lance") omni_kwargs.setdefault("max_num_batched_tokens", 32768) omni_kwargs.setdefault("max_num_seqs", 1) omni_kwargs.setdefault("enforce_eager", True) omni_kwargs.setdefault("trust_remote_code", True) omni_kwargs.setdefault("enable_prefix_caching", False) omni_kwargs.setdefault("async_chunk", False) omni = Omni(**omni_kwargs)对照 lance.yaml:pipeline: lance、async_chunk: false,stage 0 的max_num_batched_tokens: 32768、max_num_seqs: 1、enforce_eager: true、trust_remote_code: true、enable_prefix_caching: false、devices: "0"、默认seed: 42。注意 YAML 注释中说明:Lance 的 HFconfig.json只是描述性元数据(没有model_type字段),加载器无法仅凭模型目录自动检测,必须显式指定pipeline: lance。
在 model_executor/models/lance/pipeline.py 中,LANCE_PIPELINE定义了单阶段拓扑:model_type="lance"、default_deploy_config_name="lance.yaml"、model_arch="LancePipeline",stage 0 为DIFFUSION执行类型、final_output=True、final_output_type="image"。同时在 registry.py 中注册了"lance" → "pipeline_lance"的映射。
注意力后端提示:脚本默认设置DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN(os.environ.setdefault)。原因写在第 210-214 行注释中——上游使用flash_attn_varlen_func,而 SDPA 在 36 层 Qwen2 栈上会积累约 5 倍的数值漂移(B300 实测)。若未显式指定后端,默认优先 flash-attn。
源码级实现:LancePipeline 的三个特化点
LancePipeline(BagelPipeline)故意不调用BagelPipeline.__init__,因为父类关于config.json/vit_config.json/ SigLIP / BAGEL AE 的假设对 Lance 不成立;它复制 BAGEL 的构造序列,但使用 Lance 专用组件构建器(见 pipeline_lance.py)。
LANCE_DEFAULTS:硬编码常量
来自上游config_factory.py/inference_lance.sh的常量(针对官方Lance_3B/model.safetensors验证):
| 常量 | 值 | 依据 |
|---|---|---|
latent_patch_size_spatial/temporal | 1 / 1 | vae2llm.weight = (2048, 48)⇒ patch_latent_dim = 1×48,Lance 不做 BAGEL 式 2×2 patch 展开(Wan2.2 内部已 patchify) |
max_latent_size | 64 | latent_pos_embed.pos_embed = (4096, 2048)⇒ max_latent_size=64 |
max_num_video_latent_frames | 31 | Lance_3B_Video的 3D 表(31*64*64, 2048)=(126976, 2048) |
vit_max_num_patch_per_side | 70 | Qwen2.5-VL patch 数上限 |
connector_act | gelu_pytorch_tanh | 连接器激活函数 |
timestep_shift/num_timesteps/cfg_text_scale | 3.5 / 30 / 4.0 | 与 README 默认一致 |
vae_z_channels/vae_downsample_spatial/vae_downsample_temporal | 48 / 16 / 4 | Wan2.2 VAE 结构 |
模型装配顺序
- LLM(Qwen2-MoT):从
Lance_3B/llm_config.json读取Qwen2MoTConfig,强制qk_norm=True、tie_word_embeddings=False(官方 checkpoint 单独携带language_model.lm_head.weight,保持 head 不绑定才能零 missing/unexpected key 加载)。 - mRoPE 配置:Lance 是 Qwen2.5-VL-MoT,官方
rope_scaling = {"type": "mrope", "mrope_section": [16, 24, 24]}。vllm-omni 保留该配置——BagelRotaryEmbedding按rope_type自动分派,LanceBagel对纯文本块把标量位置广播为(3, S),对视频潜变量块输出真实的逐 token(t, h, w)三维位置。这是 Qwen2.5-VL 骨干产出连贯 x2t / t2v 输出的必要条件。 - 理解 ViT:用 transformers 的
Qwen2_5_VLVisionConfig/Qwen2_5_VisionTransformerPretrainedModel实例化,强制_attn_implementation="sdpa"(官方配置写的是flash_attention_2,在无 flash-attn 的硬件如 Blackwell 上会构造失败;sdpa 推理数值等价),再用LanceQwen2_5_VLNaViTWrapper包装。若 ViT 构造失败,理解路径不可用但生成路径(t2i)不受影响。 - VAE:Wan2.2(
Wan2.2_VAE.pth),_build_wan22_vae实现见 wan_vae.py,支持多帧视频解码decode_video。 - 视觉占位:Lance checkpoint 没有独立
connector/vit_pos_embed权重,LanceBagel保留visual_und=True以复用 BAGEL 的forward_cache_update_vit,随后把connector换成恒等映射LanceIdentityConnector、vit_pos_embed换成零操作LanceZeroVitPosEmbed,使严格加载检查不要求幻影权重、前向加法数值上为 no-op。 - 视频 3D 位置表:
Lance_3B_Video用 3D 潜变量位置表替换 BAGEL 的 2DPositionEmbedding,LancePositionEmbedding3D(max_num_frames=31, max_num_patch_per_side=64, hidden_size=2048)。 - 权重来源:
Lance_3B/model.safetensors以bagel.前缀加载(含language_model.* / vae2llm.* / llm2vae.* / time_embedder.* / latent_pos_embed.*);Qwen2.5-VL-ViT/vit.safetensors以bagel.vit_model.vision_model.前缀最后加载并覆盖——镜像上游inference_lance.py的加载顺序(vit.safetensors 最后胜出),以恢复与上游逐字节一致的 ViT 输出(视频 checkpoint 内置的vit_model.*会产生 rel_l2≈9 的偏差)。
forward 分派逻辑
forward(req)依据 prompt 字典的modalities与multi_modal_data字段分派(见 pipeline_lance.py):
| 条件 | 路由 |
|---|---|
modalities=["video"]+first_frame | _forward_i2v(首帧图生视频,VAE+ViT 预填充后全新生成多帧) |
modalities=["video"]+video | _forward_video_edit |
modalities=["video"] | _forward_t2v |
modalities=["text"]+video | _forward_x2t_video |
modalities=["text"]+image | _forward_x2t_image |
modalities=["image"]+img2img/image | _forward_image_edit |
| 其他 | 回落到BagelPipeline.forward(t2i) |
视频形状校验
t2v 前向会做两重校验(超出直接抛ValueError):
- 空间:
H/W ≤ max_latent_size * latent_downsample(64 × 16 = 1024 像素上限); - 时间:
(T-1) // 4 + 1 ≤ 31(Wan2.2 时间下采样 4),即 T ≤ 121 RGB 帧。
image_edit 的六段式预填充
上游 image_edit 的 KV 缓存结构被精确复刻(_forward_image_edit注释详述了Lance.validation_gen_KVcache的布局):
seg1 因果段 <|im_start|>system\n{sys}<|im_end|>\n<|im_start|>user\n seg2 全可见段 ViT(ref) — 参考图过 Qwen2.5-VL ViT seg3 全噪声段 VAE(ref) — 参考图过 Wan2.2 VAE, time_embed(t=0) seg4 因果段 用户编辑指令 seg5 因果段 <|im_end|>\n<|im_start|>assistant\n seg6 噪声 QUERY 待去噪的生成潜变量两个关键细节:CFG 无条件分支只跳过 seg4(用户指令),其余段包括系统头、ViT/VAE 预填充全部共享;cfg_text_context的 rope 计数器仍按 seg4 长度推进,保证 seg5 的 rope 位置两分支一致。此外,生成潜变量与参考 VAE 块共享相同的 mRoPE 位置网格——上游get_rope_index对 VAE 条件块和噪声块都输出锚定在同一 base 的[base_t, base_h+hi, base_w+wi],这正是模型能把噪声 token 映射回参考图 token、实现"编辑"的归纳偏置。参考图 VAE 编码结果通过单槽latent_cache在 gen 分支与 cfg 分支间共享,避免 Wan2.2 的mu + std * randn_like(std)每次调用重新采样导致两分支 KV 漂移。
输出落盘:图片与视频
- 图像任务:结果以
lance_{i}_{j}.png保存(i为请求序号,j为同一请求内的多图序号); - 视频任务:
lance_{i}.mp4,使用 imageio 以--fps(默认 12)编码(codec="libx264", quality=8);若 MP4 编码失败(如缺编码器),自动降级为逐帧 PNG 目录lance_{i}_frames/0000.png ...; - 理解任务:文本直接打印到 stdout(
[Output i] {text})。
进阶:统一 Gradio 演示(7 任务)
仓库还提供 gradio_demo.py,一个覆盖全部 7 个任务(Video Generation / Image to Video / Video Edit / Video Understanding / Image Generation / Image Edit / Image Understanding)的 Web UI,风格镜像上游 Lance 的lance_gradio.py:任务单选、宽高比 + 分辨率下拉(非自由 H/W 滑条)、左输入右输出的双栏布局、底部高级参数折叠面板。
双 Omni 实例与设备布局
后端按任务路由到两个独立的Omni实例:
Lance_3B:t2i、image_edit、x2t_image;Lance_3B_Video:t2v、i2v、video_edit、x2t_video。
设备布局(依据CUDA_VISIBLE_DEVICES可见卡数自动降级):
--ulysses-degree | 布局 | GPU 数 |
|---|---|---|
| 1(P0) | 图像 Omni → 逻辑 0,视频 Omni → 逻辑 1,图像 + 视频真正并行 | 2 |
| 2(P1) | 图像 → 0,1;视频 → 2,3;每个 Omni 内部用 Ulysses SP 切分 DiT 去噪序列,单请求 t2v/i2v 约快 1.5-1.8 倍且图像视频仍并发 | 4 |
| N | 图像 → 0..N-1;视频 → N..2N-1 | 2N |
--replicas-per-omni则在每块卡上各加载一份完整模型做吞吐扩展(2*N张卡),适合替代实验性的 SP(注释明确提示:Lance 目前缺少_sp_plan注册表且 i2v 首帧 pin 不感知 SP,SP>1 时 i2v 会崩溃,优先用 replica 做吞吐扩展)。GPU 数不足时会先降 replica 再降 SP 并打印 WARN。
分辨率桶:与上游画布严格对齐
上游把宽高比限制为6 个固定桶:21:9 / 16:9 / 4:3 / 1:1 / 3:4 / 9:16(没有 3:2 / 2:3,会被上游桶选择器吸附到 4:3 / 3:4)。每个桶的规范 (W, H) 由以下公式计算:
max_area = resolution_vae ** 2 w1 = round(sqrt(max_area * ar) / 16) * 16 h1 = round(w1 / ar / 16) * 16 # 再选实际比值更接近 ar 的候选其中resolution_vae = 480(video_360p)、640(video_480p)、768(image_768res)。Gradio 脚本按此精确计算并内置三张表,保证与上游在同一输入图像下落到相同画布、无宽高比漂移:
VIDEO_360P_AR_TO_SIZE:21:9→(752,320)、16:9→(624,352)、4:3→(560,416)、1:1→(480,480)、3:4→(416,560)、9:16→(352,624);VIDEO_480P_AR_TO_SIZE:21:9→(976,416)、16:9→(848,480)、4:3→(752,560)、1:1→(640,640)、3:4→(560,752)、9:16→(480,848);IMAGE_AR_TO_SIZE:21:9→(1152,496)、16:9→(1024,576)、4:3→(896,672)、1:1→(768,768)、3:4→(672,896)、9:16→(576,1024)。
上传图片时还会用 log 空间距离把任意 W×H 吸附到最近的预设桶(线性距离会不公平地偏向 4:3)。
质量参数与演示时长
质量参数锁定为上游值:steps=30、timestep_shift=3.5、cfg_text_scale=4.0(注释特别强调"NOT 8.0")、seed=42、fps=12。演示时长则刻意短于上游脚本:t2v/i2v 默认 5 秒(约 61 帧,上游为 121 帧 ≈ B300 上约 2 分钟/请求),video_edit 默认 4 秒(约 49 帧,对齐上游 50)。在保证逐帧保真度的前提下把交互延迟压到约 60 秒;需要逐字节复现上游官方输出时,可用滑块把时长延长到最多 10 秒。
官方示例注入
若传入--examples-root(指向上游 Lance 仓库检出目录),demo 会解析config/examples/*.json为每个任务注入可点击的官方示例:
- t2i / t2v:
{filename: prompt}映射只取 prompt 作为输入预设; - i2v / video_edit / x2t:
interleave_array = [prompt, media]或[media, [system, question, answer]],媒体路径按仓库相对路径解析,缺失则静默跳过; - image_edit:上游只给一张源图配 5 个不同 prompt(5 张相同缩略图体验差),demo 改用 i2v 帧池中 5 张视觉差异明显的源图并重写编辑指令;
- video_edit:自动把上游 8 个"左右分屏(源 | 编辑)"演示视频裁出左半边源视频作为可运行输入(上下 RGB 差分判断会被自然天空/地面方差误导,因此采用目视确认的垂直中线裁切),多轮编辑 filmstrip 取第一块面板为源。
调试建议与已知边界
- 立即 EOS / 空回答:理解路径务必保持
--text-temperature 0.8采样默认值,或用--system-prompt "Look at the image carefully and answer the question."这类 QA 指令替代 caption 默认; - 视频越界崩溃:确认
--model指向Lance_3B_Video(或顶层仓库目录且脚本能自动找到该子目录),且num_frames ≤ 121、video_height/width ≤ 1024; - 与上游数值差异:优先保证
DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN(SDPA 会引入约 5 倍数值漂移); - ViT 输出差异:必须让
Qwen2.5-VL-ViT/vit.safetensors最后加载(仓库已如此实现),否则 image_edit / video_edit 的 ViT 输出与上游逐字节不一致。
整体来看,vllm-omni 对 Lance 的支持呈现清晰的"继承 + 特化"路径:transformer 核心完整复用 BAGEL 的 MoT 实现,仅在 checkpoint 布局、Qwen2.5-VL 理解 ViT、Wan2.2 VAE 与 3D 位置编码四处做局部替换,从而以单阶段扩散流水线同时覆盖六大生成/理解模态。无论是快速跑通end2end.py,还是基于gradio_demo.py搭建交互式演示,本文涉及的参数、默认值与源码路径都可作为直接参考。
【免费下载链接】vllm-omniA framework for efficient model inference with omni-modality models项目地址: https://gitcode.com/GitHub_Trending/vl/vllm-omni
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考