在 🤗 Transformers 中集成 Ovis2:多模态视觉-语言大模型的架构解析与图像文本生成实践
Ovis2 是阿里巴巴国际数字商业集团 AIDC-AI 团队在其 Ovis 系列基础上推出的多模态大语言模型(MLLM),延续了"对齐视觉与文本嵌入"的核心设计思路。本文以 docs/source/en/model_doc/ovis2.md 为主线,结合当前仓库中 src/transformers/models/ovis2/ 的源码实现,系统讲解 Ovis2 在 Transformers 中的模块组成、关键配置项、Processor 的图像分块策略,并给出可直接运行的图像描述(Image Captioning)推理代码。读完本文,你将掌握如何加载官方转换后的 Ovis2 权重做端到端多模态生成,并理解底层 placeholder 替换、视觉 indicator token 等机制。
Ovis2 概览:从 Ovis 到 Ovis2
根据官方文档,Ovis2 是 AIDC-AI 团队在阿里巴巴国际数字商业集团开发的 Ovis 模型的更新版本,作为 Ovis1.6 的继任者,Ovis2 是多模态大语言模型(MLLM)领域的最新进展之一。它继承了 Ovis 系列"聚焦于对齐视觉与文本嵌入"的架构设计,同时在数据整理(data curation)与训练方法上引入了重大改进。
从模型历史看,Ovis 系列的研究成果于 2024-05-31 发表在 Hugging Face 论文平台,而 Ovis2 的模型实现由社区成员 thisisiron 于 2025-08-18 贡献并合入 Transformers。当前仓库中已包含完整的 Ovis2 实现文件,集中在:
- 模型实现:modeling_ovis2.py
- 配置类:configuration_ovis2.py
- 图像预处理(Torchvision 后端):image_processing_ovis2.py
- 图像预处理(PIL 后端):image_processing_pil_ovis2.py
- 多模态 Processor:processing_ovis2.py
- 权重转换脚本:convert_ovis2_weights_to_hf.py
代码中的模型与配置均已注册到 Auto 体系(见 auto_mappings.py 等自动映射文件),因此既可以直接使用 Ovis2ForConditionalGeneration 这样的显式类,也可以通过 AutoModelForImageTextToText / AutoProcessor 这类通用入口加载。
Ovis2 的架构设计:从源码理解三大组成模块
打开 modeling_ovis2.py 可以看到,Ovis2Model(第 467 行起)由三部分拼接而成:
class Ovis2Model(Ovis2PreTrainedModel):
def __init__(self, config: Ovis2Config):
super().__init__(config)
self.vision_tower = Ovis2VisionModel(config.vision_config)
self.language_model = AutoModel.from_config(config.text_config)
self.visual_embeddings_table = Ovis2VisualEmbeddingTable(config.vision_config.vocab_size, config.hidden_size)
- 视觉塔(vision_tower):
Ovis2VisionModel,一个自带"视觉分词头"的 ViT,负责把图片编码成视觉 token 的概率分布; - 语言模型(language_model):由
text_config指定,默认回落到 Qwen2 架构(详见配置小节),承担全部文本理解与自回归生成; - 视觉嵌入表(visual_embeddings_table):充当"视觉词汇表 → LLM 嵌入空间"的投影桥,其独特之处在于继承了
nn.Embedding却重写了前向逻辑——当输入是整数索引时走标准查表;当输入是连续的视觉概率分布向量时,则退化为一次矩阵乘法torch.matmul(visual_tokens, self.weight)(见Ovis2VisualEmbeddingTable第 359 行起)。这一设计正是 Ovis 系列"把视觉信号软化、与文本 embedding 空间对齐"思想的具体实现。
视觉编码器的"软分词":visual indicator tokens
与多数将图像 patch 直接映射为 embedding 的 MLLM 不同,Ovis2 的视觉塔做了一层显式的 token 化。Ovis2VisionTransformer 由 patch 嵌入(nn.Conv2d,kernel/stride 等于 patch_size)、若干 Ovis2VisionEncoderLayer 以及尾部 RMSNorm 组成;每层编码器都采用 pre-norm 的 RMSNorm + 多头注意力 + 带 gate/up/down 三个线性层与 SiLU 激活的 FFN(结构与 Ovis2VisionMLP 相同)。
真正关键的是 Ovis2VisionModel 顶部的两个线性层(modeling_ovis2.py 第 410-416 行):
self.head_linear = nn.Linear(
config.hidden_size * config.hidden_stride * config.hidden_stride,
self.vocab_size - self.num_visual_indicator_tokens,
bias=False,
)
self.head_norm = nn.LayerNorm(self.vocab_size - self.num_visual_indicator_tokens)
它把视觉塔最后一层隐藏状态映射到一个"视觉词汇表"上(默认 vocab_size=16384,其中末尾 5 个类别被保留给 visual indicator tokens),随后按 tokenize_function 归一化:
softmax(默认):直接对 logits 做 softmax,产生连续的概率分布作为软视觉 token;st_argmax:straight-through 式的硬 argmax,训练时梯度可穿透;gumbel_argmax:Gumbel-Softmax 的硬采样变体。
从该模块的实现(见 configuration_ovis2.py 第 25-53 行)可以看到 num_visual_indicator_tokens 与 vocab_size 共同定义了软词表的边界。此外当 hidden_stride > 1 时,视觉塔会把空间相邻的 hidden_stride × hidden_stride 个 patch 合并拼接后再送进 head_linear,用于在分辨率与 token 数之间做取舍(代码中对非整除情况会先做 zero-padding 使 token 序列长度保持完全平方)。
前向融合:placeholder 的精确替换
Ovis2Model.forward(第 537 行起)是理解图像"如何进入文本流"的最佳入口,其逻辑可以概括为三步:
- 图像特征提取:调用
get_image_features(pixel_values=...),视觉塔输出每个视觉 token 的软概率,随后拼接宽度为num_visual_indicator_tokens的零向量并经过visual_embeddings_table投影,得到真正可被 LLM 消费的连续向量image_features(见第 483-509 行); - 图像占位符替换:通过
get_placeholder_mask(第 511 行起)在input_ids/inputs_embeds中定位所有等于config.image_token_id(默认151665)的占位 token,并用inputs_embeds.masked_scatter(...)将图像特征"铺"进这些位置,同时校验图像特征数量与占位符数量严格一致,否则直接报错; - indicator token 注入:遍历
visual_indicator_token_ids(默认[151666, 151667, 151668, 151669, 151670]),凡是文本中出现这些 id 的位置,都会被替换成相应的visual_indicator_features(第 569-579 行),从而把"图像提示符"的语义也带入 LLM embedding 空间。
完成上述融合后,inputs_embeds 连同 attention_mask、position_ids 等一起交给 language_model。Ovis2ForConditionalGeneration(第 601 行起)在此之上套了不带 bias 的 lm_head 输出 logits,并借助 logits_to_keep 只计算必要位置的 logits 以降低生成开销。整条链路最终返回 Ovis2CausalLMOutputWithPast,其中额外的 image_hidden_states 携带了视觉编码器产生的中间图像表征,方便研究者做二次分析与调试。
从 modeling_ovis2.py 第 366-379 行的类属性可以看出,Ovis2 原生支持 gradient checkpointing、KV Cache(_supports_cache_class)、Flash Attention、Flex Attention 与 SDPA 等注意力后端(_supports_flash_attn / _supports_flex_attn / _supports_sdpa 均为 True),并声明了多模态输入 input_modalities = ("image", "text")。
配置项详解:Ovis2Config 与 Ovis2VisionConfig
Ovis2 的配置由顶层 Ovis2Config 与嵌套的 Ovis2VisionConfig(同时实现了 AutoConfig 子配置映射,见 configuration_ovis2.py 第 35 行 base_config_key = "vision_config")构成。
Ovis2VisionConfig
控制视觉塔(ViT 风格编码器 + 软分词头),关键字段及其默认值如下表所示(取自源码类定义):
| 字段 | 默认值 | 说明 |
|---|---|---|
hidden_size |
1024 |
视觉塔隐藏维度 |
intermediate_size |
2816 |
FFN 中间层维度 |
num_hidden_layers |
24 |
编码器层数 |
num_attention_heads |
8 |
注意力头数 |
num_channels |
3 |
输入图像通道数 |
image_size |
224 |
输入图像边长(也支持 list / (h, w)) |
patch_size |
14 |
patch 边长(支持 list / (h, w)) |
rms_norm_eps |
1e-5 |
RMSNorm 的 epsilon |
attention_dropout |
0.0 |
注意力 dropout |
qkv_bias / mlp_bias |
False |
是否使用 bias |
hidden_act |
"silu" |
FFN 激活函数 |
vocab_size |
16384 |
视觉"软词表"大小 |
hidden_stride |
1 |
相邻 patch 合并步长(>1 时压缩视觉 token) |
num_visual_indicator_tokens |
5 |
视觉 indicator token 数量 |
initializer_range |
0.02 |
参数初始化范围 |
tokenize_function |
"softmax" |
软分词方式:softmax / st_argmax / gumbel_argmax |
Ovis2Config
顶层配置控制整体模型,关键字段如下(见 configuration_ovis2.py 第 58-99 行):
| 字段 | 默认值 | 说明 |
|---|---|---|
model_type |
"ovis2" |
模型类型标识,用于 Auto 注册 |
vision_config |
None |
视觉子配置(dict 或 Ovis2VisionConfig,缺省自动生成且 num_visual_indicator_tokens 取 indicator 数量) |
text_config |
None |
文本子配置(dict 或 PreTrainedConfig,缺省回落为 qwen2,即语言模型默认基于 Qwen2 架构,见第 94-97 行) |
image_token_id |
151665 |
图像占位符 token id |
visual_indicator_token_ids |
(151666, 151667, 151668, 151669, 151670) |
编码图像提示的 indicator token id 序列 |
vocab_size |
151643 |
文本词表大小 |
hidden_size |
1536 |
LLM 隐藏维度 |
tie_word_embeddings |
True |
是否共享输入/输出词嵌入 |
值得注意的是 __post_init__ 的处理方式(第 88-99 行):若传入 dict 形式的 vision_config/text_config 会自动实例化对应配置类;text_config 为空时直接构造 CONFIG_MAPPING["qwen2"](),vision_config 为空时会用 len(visual_indicator_token_ids) 自动对齐 indicator token 个数。这意味着最小配置只需 Ovis2Config(),即可得到一个"Ovis2 骨架 + Qwen2 语言模型"的完整模型。
配置文件中还给出了从零初始化模型的示例(configuration_ovis2.py 第 63-74 行):
>>> from transformers import Ovis2ForConditionalGeneration, Ovis2Config
>>> # Initializing a Ovis2 style configuration
>>> configuration = Ovis2Config()
>>> # Initializing a model from the Ovis2-2B style configuration
>>> model = Ovis2ForConditionalGeneration(configuration)
>>> # Accessing the model configuration
>>> configuration = model.config
Processor 与动态分块:多分辨率图像如何被切分
Ovis2Processor:模板对话与占位符展开
Ovis2Processor(processing_ovis2.py)把图像处理器与分词器粘合在一起,支持基于 Chat Template 的对话式输入。其 __init__ 接受 image_token="<image>" 与 image_seq_length=256 两个关键参数:前者是对话内容中标记图像位置的 token,后者是每张图(默认单一 tile 时)展开的图像 token 数量。
Processor 通过 replace_image_token 方法(第 53-66 行)依据图像预处理产出的 grids 信息动态构造占位文本。伪结构为:
<IMG_START> <IMG_ATOM>×256 <IMG_GRID>
<(若网格大于 1×1)每行每列重复 <IMG_ATOM>×256,并用 <IMG_COL> / <IMG_ROW> 分隔>
<IMG_END>
也就是说,当图像被切成多个 tile 时,每个 tile 都会获得独立的图像 token 块,token 数量由 image_seq_length × 网格数 决定,网格内部用 <IMG_COL>(列分隔)与 <IMG_ROW>(行分隔)标记空间布局。unused_input_names = ["grids"] 表明 grids 只是处理时的中间信息,最终不会传给模型。
Ovis2ImageProcessor:自适应 tile 画布
image_processing_ovis2.py 实现了对任意长宽比图像的自适应切块。其内部的核心函数包括:
get_all_supported_aspect_ratios(min_image_tiles, max_image_tiles):枚举所有允许的(宽 × 高)tile 网格组合;get_optimal_tiled_canvas(...):从中挑出与原图长宽比最接近的画布;get_min_tile_covering_grid(...):以covering_threshold = 0.9为界,寻找"patch 覆盖面积占比"达标前提下 tile 数最少的网格;split_image_into_grid(...)与compute_patch_covering_area(...):负责把图像切格并计算有效覆盖面积。
这些函数共同支撑两类可配置行为(定义在 Ovis2ImageProcessorKwargs 中):
| 参数 | 默认值 | 说明 |
|---|---|---|
crop_to_patches |
False |
是否启用"按 patch 裁剪/分块"流程 |
min_patches |
1 |
crop_to_patches=True 时的最小 patch(tile)数 |
max_patches |
12 |
crop_to_patches=True 时的最大 patch(tile)数 |
use_covering_area_grid |
True |
是否用"覆盖面积"启发式决定 tile 网格 |
预处理遵循 CLIP 的统计口径(源码导入了 OPENAI_CLIP_MEAN 与 OPENAI_CLIP_STD),并基于 Torchvision 后端执行缩放与归一化;对应的 Ovis2ImageProcessorPil 则是面向 PIL 输入、逐图处理的封装版本(见 image_processing_pil_ovis2.py)。默认 max_image_tiles=12 使模型可以在保持底层 patch 分辨率的前提下兼容从竖图、方图到超宽图的多种输入。
开箱即用:图像描述推理实战
下面以文档中的官方用法示例为基础(见 docs/source/en/model_doc/ovis2.md),做少量加固(显式指定 torch_dtype 与 device_map),并改用仓库测试图片目录中的真实样例图作为输入。你完全可以把它替换为任意本地图片:
import torch
from PIL import Image
from transformers import AutoModelForImageTextToText, AutoProcessor
# 加载已转换为 HF 格式的官方 checkpoint(约 2B 参数)
model = AutoModelForImageTextToText.from_pretrained(
"thisisiron/Ovis2-2B-hf",
torch_dtype=torch.bfloat16,
device_map="auto",
).eval()
processor = AutoProcessor.from_pretrained("thisisiron/Ovis2-2B-hf")
# 组装对话式消息:注意 content 中使用 {"type": "image"} 声明图像位置
messages = [
{
"role": "user",
"content": [
{"type": "image"},
{"type": "text", "text": "Describe the image."},
],
},
]
# 读取本地测试图片(也可以用你自己的图片路径替代)
image = Image.open("tests/fixtures/tests_samples/COCO/000000039769.png")
# 先通过 chat template 展开为纯文本 prompt,便于检查
messages = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
print(messages)
# 统一做多模态编码:text -> input_ids,image -> pixel_values
inputs = processor(images=[image], text=messages, return_tensors="pt")
inputs = inputs.to(model.device)
# 视觉塔通常在 float32 下精度更好/内存更大,官方示例将其显式转成 bf16 以省显存
inputs["pixel_values"] = inputs["pixel_values"].to(torch.bfloat16)
with torch.inference_mode():
output_ids = model.generate(**inputs, max_new_tokens=128, do_sample=False)
# 仅截取新生成的 token(去除输入提示部分)
generated_ids = [output_ids[len(input_ids):] for input_ids, output_ids in zip(inputs.input_ids, output_ids)]
output_text = processor.batch_decode(generated_ids, skip_special_tokens=True)
print(output_text)
该流程的几个要点:
- 模型与处理器配套使用:
AutoModelForImageTextToText与AutoProcessor都会读取远端仓库的config.json与preprocessor_config.json,自动实例化Ovis2ForConditionalGeneration、对应的图像处理器与分词器; apply_chat_template:把 messages 结构转成带<|im_start|>/<|im_end|>角色标记的指令串(processor 内部再替换成<IMG_START>等图像占位结构);- 精度选择:示例将
pixel_values显式转成bfloat16。若 GPU 显存充足也可保持更高精度,但需与torch_dtype的选择保持一致,避免视觉塔输入输出 dtype 不匹配; - 截取生成段:
output_ids[len(input_ids):]剔除了输入提示部分,只保留真正生成的图像描述文本。
如果希望绕开 Chat Template、直接用原始指令字符串,模型 docstring 也给出了等价写法(modeling_ovis2.py 第 640-662 行),例如把 prompt 构造为:
<|im_start|>user\n<image>\nDescribe the image.<|im_end|>\n<|im_start|>assistant\n
然后同样走 processor(images=image, text=prompt, return_tensors="pt") → model.generate(...) 即可。
需要说明的是:图像与文本分支均较大,2B 级 checkpoint 建议在具备 CUDA 且显存充足的 GPU 上以 bfloat16 运行;CPU 推理耗时较长,仅适合功能验证。文档与测试中出现的 HF 转换后 checkpoint 名称包括 thisisiron/Ovis2-1B-hf 与 thisisiron/Ovis2-2B-hf(如配置类 docstring 与 test_processing_ovis2.py 均引用了 Ovis2-1B-hf,模型测试则以 Ovis2-2B-hf 为例),加载时请按实际可用模型名替换。
API 参考速览
除上面剖析的机制外,官方文档还登记了以下公开 API,均可在 model_doc/ovis2.md 中通过 autodoc 索引到完整 docstring:
| 类/方法 | 作用 |
|---|---|
Ovis2Config |
顶层配置类(文本 + 视觉双子配置) |
Ovis2VisionConfig |
视觉塔配置类 |
Ovis2Model |
无语言模型头的整体骨干:视觉塔 + 语言模型 + 视觉嵌入表 |
Ovis2ForConditionalGeneration(forward、get_image_features) |
带 lm_head 的条件生成模型;get_image_features 可单独抽取投影后的图像特征 |
Ovis2ImageProcessor(preprocess) |
基于 Torchvision 的图像预处理,含动态分块 |
Ovis2ImageProcessorPil(preprocess) |
面向 PIL 输入的图像预处理 |
Ovis2Processor(__call__) |
图像处理器 + 分词器 + Chat Template 的统一入口 |
质量保障与扩展阅读
- 测试覆盖:当前仓库在 tests/models/ovis2/ 下提供了
test_modeling_ovis2.py(模型前向/生成/图像特征)、test_processing_ovis2.py(Processor 与 chat template 行为)以及test_image_processing_ovis2.py(分块与预处理),可作为理解 Ovis2 预期行为的参考用例; - 自动注册:Ovis2 的模型类、配置类已写入
Auto映射(见 src/transformers/models/auto/auto_mappings.py 与 modeling_auto.py),因此前述AutoModelForImageTextToText/AutoConfig/AutoModel均可直接解析ovis2类型; - 权重转换:convert_ovis2_weights_to_hf.py 提供了将 Ovis2 原始权重整理为 Transformers 兼容格式的转换入口,对从源头复现或二次训练很有价值;
- 代码生成说明:模型主体文件头部注明它由 modular_ovis2.py 自动生成,若希望研究该架构的模块化拆分方式,可对比阅读
modular源文件与生成文件。
综上,Ovis2 在 Transformers 中的落地融合了三条关键设计:以"软视觉词表 + visual indicator tokens"承载图像语义、以可查表亦可矩阵乘的视觉嵌入表完成跨模态投影、以"占位符 masked_scatter + 动态 tile 预处理"实现灵活的任意分辨率输入。理解这三条主线后,无论是直接部署推理还是基于该实现做二次开发,都能有的放矢。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00