HuggingFace镜像/InternVL3_5-8B
模型介绍文件和版本分析
下载使用量0

InternVL3_5-8B

[

📂GitHub📂 GitHub📂GitHub

](https://github.com/OpenGVLab/InternVL) [

📜InternVL1.0📜 InternVL 1.0📜InternVL1.0

](https://huggingface.co/papers/2312.14238) [

📜InternVL1.5📜 InternVL 1.5📜InternVL1.5

](https://huggingface.co/papers/2404.16821) [

📜InternVL2.5📜 InternVL 2.5📜InternVL2.5

](https://huggingface.co/papers/2412.05271) [

📜InternVL2.5−MPO📜 InternVL2.5-MPO📜InternVL2.5−MPO

](https://huggingface.co/papers/2411.10442) [

📜InternVL3📜 InternVL3📜InternVL3

](https://huggingface.co/papers/2504.10479) [

📜InternVL3.5📜 InternVL3.5📜InternVL3.5

](https://huggingface.co/papers/2508.18265)

[

🆕技术博客🆕 技术博客🆕技术博客

](https://internvl.github.io/blog/) [

🗨️对话演示🗨️ 对话演示🗨️对话演示

](https://chat.intern-ai.org.cn/) [

🚀快速开始🚀 快速开始🚀快速开始

](#quick-start) [

📖使用文档📖 使用文档📖使用文档

](https://internvl.readthedocs.io/en/latest/)

image

简介

我们推出了全新的开源多模态模型系列 InternVL3.5,该系列在 InternVL 现有基础上显著提升了模型的通用性、推理能力和推理效率。核心创新点包括 级联强化学习(Cascade RL) 框架,通过离线强化学习确保稳定收敛、在线强化学习优化对齐的两阶段流程增强推理能力。这种由粗到精的训练策略大幅提升了下游推理任务(如 MMMU 和 MathVista)的性能。为优化效率,我们提出 视觉分辨率路由(ViR) 机制,可动态调整视觉 tokens 的分辨率而不损失性能。结合 ViR,我们设计的解耦式 视觉-语言部署(DvD) 策略将视觉编码器与语言模型部署在不同 GPU 上,有效平衡计算负载。这些技术共同使 InternVL3.5 相比前代模型 InternVL3 在整体推理性能上提升高达 16.0%,推理速度提升 4.05 倍。此外,InternVL3.5 新增 GUI 交互和具身智能体等能力。值得关注的是,我们的最大模型 InternVL3.5-241B-A28B 在通用多模态、推理、文本及智能体任务上均达到开源多模态大模型的当前最佳水平,大幅缩小了与 GPT-5 等领先商业模型的性能差距。所有模型及代码已完全开源。

image/jpg

斜线填充柱形代表闭源商业模型。图表展示了模型在多模态通用、推理、文本及智能体任务基准集上的平均得分,包含:MMBench v1.1 (en)、MMStar、BLINK、HallusionBench、AI2D、OCRBench、MMVet、MME-RealWorld (en)、MVBench、VideoMME、MMMU、MathVista、MathVision、MathVerse、DynaMath、WeMath、LogicVista、MATH500、AIME24、AIME25、GPQA、MMLU-Pro、GAOKAO、IFEval、SGP-Bench、VSI-Bench、ERQA、SpaCE-10 和 OmniSpatial。

详见 快速开始 了解模型使用方法。

InternVL3.5 系列

以下表格为您概述 InternVL3.5 系列模型。 为保持与前代产品的一致性,我们提供两种模型格式:GitHub 格式(与先前版本一致)和 HF 格式(符合官方 Transformers 标准)。

若需在这两种格式间转换模型 checkpoint,请参考 custom2hf 和 hf2custom 相关脚本。

Github 格式

模型视觉参数数量语言参数数量总参数数量HF 链接ModelScope 链接
InternVL3.5-1B0.3B0.8B1.1B🤗 链接🤖 链接
InternVL3.5-2B0.3B2.0B2.3B🤗 链接🤖 链接
InternVL3.5-4B0.3B4.4B4.7B🤗 链接🤖 链接
InternVL3.5-8B0.3B8.2B8.5B🤗 链接🤖 链接
InternVL3.5-14B0.3B14.8B15.1B🤗 链接🤖 链接
InternVL3.5-38B5.5B32.8B38.4B🤗 链接🤖 链接
InternVL3.5-20B-A4B0.3B20.9B21.2B-A4B🤗 链接🤖 链接
InternVL3.5-30B-A3B0.3B30.5B30.8B-A3B🤗 链接🤖 链接
InternVL3.5-241B-A28B5.5B235.1B240.7B-A28B🤗 链接🤖 链接

HuggingFace 格式

模型#视觉参数#语言参数#总参数HF 链接ModelScope 链接
InternVL3.5-1B-HF0.3B0.8B1.1B🤗 链接🤖 链接
InternVL3.5-2B-HF0.3B2.0B2.3B🤗 链接🤖 链接
InternVL3.5-4B-HF0.3B4.4B4.7B🤗 链接🤖 链接
InternVL3.5-8B-HF0.3B8.2B8.5B🤗 链接🤖 链接
InternVL3.5-14B-HF0.3B14.8B15.1B🤗 链接🤖 链接
InternVL3.5-38B-HF5.5B32.8B38.4B🤗 链接🤖 链接
InternVL3.5-20B-A4B-HF0.3B20.9B21.2B-A4B🤗 链接🤖 链接
InternVL3.5-30B-A3B-HF0.3B30.5B30.8B-A3B🤗 链接🤖 链接
InternVL3.5-241B-A28B-HF5.5B235.1B240.7B-A28B🤗 链接🤖 链接

image/jpg

我们使用 VLMEvalkit 进行评估。若要启用模型的思考模式,请将系统提示设置为 R1_SYSTEM_PROMPT。 启用思考模式时,建议设置 do_sample=True 和 temperature=0.6 以减少不必要的重复。

我们的训练流程包括四个阶段:多模态持续预训练(CPT)、监督微调(SFT)和级联强化学习(CascadeRL)。在级联强化学习阶段,我们首先在离线强化学习设置下使用混合偏好优化(MPO)对模型进行微调,然后在在线强化学习设置下使用 GSPO 进行微调。 对于 InternVL3.5 的 Flash 版本,我们额外引入了一个轻量级训练阶段,称为视觉一致性学习(ViCO),它减少了表示图像 patch 所需的 token 成本。

image/jpg

在此,我们还开源了不同训练阶段后的模型权重,供潜在的研究使用。 如果不确定选择哪个版本,请选择不带任何后缀的版本,因为它已完成完整的训练流程。

模型训练流程HF 链接ModelScope 链接
InternVL3.5-1B-PretrainedCPT🤗 链接🤖 链接
InternVL3.5-1B-InstructCPT + SFT🤗 链接🤖 链接
InternVL3.5-1B-MPOCPT + SFT + MPO🤗 链接🤖 链接
InternVL3.5-1BCPT + SFT + CascadeRL🤗 链接🤖 链接
InternVL3.5-2B-PretrainedCPT🤗 链接🤖 链接
InternVL3.5-2B-InstructCPT + SFT🤗 链接🤖 链接
InternVL3.5-2B-MPOCPT + SFT + MPO🤗 链接🤖 链接
InternVL3.5-2BCPT + SFT + CascadeRL🤗 链接🤖 链接
InternVL3.5-4B-PretrainedCPT🤗 链接🤖 链接
InternVL3.5-4B-InstructCPT + SFT🤗 链接🤖 链接
InternVL3.5-4B-MPOCPT + SFT + MPO🤗 链接🤖 链接
InternVL3.5-4BCPT + SFT + CascadeRL🤗 链接🤖 链接
InternVL3.5-8B-PretrainedCPT🤗 链接🤖 链接
InternVL3.5-8B-InstructCPT + SFT🤗 链接🤖 链接
InternVL3.5-8B-MPOCPT + SFT + MPO🤗 链接🤖 链接
InternVL3.5-8BCPT + SFT + CascadeRL🤗 链接🤖 链接
InternVL3.5-14B-PretrainedCPT🤗 链接🤖 链接
InternVL3.5-14B-InstructCPT + SFT🤗 链接🤖 链接
InternVL3.5-14B-MPOCPT + SFT + MPO🤗 链接🤖 链接
InternVL3.5-14BCPT + SFT + CascadeRL🤗 链接🤖 链接
InternVL3.5-30B-A3B-PretrainedCPT🤗 链接🤖 链接
InternVL3.5-30B-A3B-InstructCPT + SFT🤗 链接🤖 链接
InternVL3.5-30B-A3B-MPOCPT + SFT + MPO🤗 链接🤖 链接
InternVL3.5-30B-A3BCPT + SFT + CascadeRL🤗 链接🤖 链接
InternVL3.5-38B-PretrainedCPT🤗 链接🤖 链接
InternVL3.5-38B-InstructCPT + SFT🤗 链接🤖 链接
InternVL3.5-38B-MPOCPT + SFT + MPO🤗 链接🤖 链接
InternVL3.5-38BCPT + SFT + CascadeRL🤗 链接🤖 链接
InternVL3.5-241B-A28B-PretrainedCPT🤗 链接🤖 链接
InternVL3.5-241B-A28B-InstructCPT + SFT🤗 链接🤖 链接
InternVL3.5-241B-A28B-MPOCPT + SFT + MPO🤗 链接🤖 链接
InternVL3.5-241B-A28BCPT + SFT + CascadeRL🤗 链接🤖 链接

模型的 Flash 版本将尽快发布。

模型架构

InternVL3.5: 该系列模型沿用了前几代InternVL采用的“ViT–MLP–LLM”范式。 我们使用Qwen3系列和GPT-OSS初始化语言模型,并使用InternViT-300M和InternViT-6B初始化视觉编码器。 设计中还保留了InternVL1.5中引入的动态高分辨率策略。

InternVL3.5-Flash: 与InternVL3.5相比,InternVL3.5-Flash进一步集成了视觉分辨率路由模块(ViR),从而衍生出一系列适用于资源受限场景的高效变体。 具体而言,在InternVL3.5中,每张图像的图像块最初被视觉编码器表示为1024个视觉令牌,然后通过像素重排模块压缩为256个令牌,再传递给大型语言模型(LLM)。 在InternVL3.5-Flash中,如下图所示,额外包含了一个压缩率更高的像素重排模块,能够将视觉令牌压缩至64个。 对于每个图像块,图像块路由模块通过评估其语义丰富度来确定合适的压缩率,并将其路由至相应的像素重排模块。 得益于这种感知图像块的压缩机制,InternVL3.5-Flash能够将视觉令牌数量减少50%,同时保持接近InternVL3.5的100%性能。

image/jpg

训练与部署策略

预训练

在预训练阶段,我们使用大规模文本和多模态语料库的组合,对所有模型参数进行联合更新。具体而言,给定一个由多模态令牌序列$\mathbf{x}=\left(x_1, x_2, \ldots, x_L\right)$组成的任意训练样本,对每个文本令牌计算下一个令牌预测(NTP)损失,如下所示:

Li=−log⁡pθ(xi∣x1,…,xi−1), \mathcal{L}_{i}=-\log p_\theta\left(x_i \mid x_1, \ldots, x_{i-1}\right),Li​=−logpθ​(xi​∣x1​,…,xi−1​),

其中$x_i$是预测令牌,${x_1, x_2, \ldots, x_{i-1}}$中的前缀令牌可以是文本令牌或图像令牌。值得注意的是,对于对话样本,损失计算仅包含响应令牌。 此外,为了减轻训练过程中对较长或较短响应的偏向,我们采用平方平均法对NTP损失进行重加权,如下所示:

Li′=wi∑jwj⋅Li,wi=1N0.5,\mathcal{L}_{i}^{'} = \frac{w_i}{\sum_j w_j} \cdot \mathcal{L}_i, \quad w_i = \frac{1}{N^{0.5}},Li′​=∑j​wj​wi​​⋅Li​,wi​=N0.51​,

其中$N$表示训练样本中需要计算损失的令牌数量。我们还引入了随机JPEG压缩,以提升模型的实际应用性能。

监督微调

在监督微调阶段,我们采用与预训练阶段相同的目标函数,并使用平方根平均策略计算最终损失。本阶段将上下文窗口设置为32K tokens,以适配长上下文信息。

与InternVL3相比,InternVL3.5的监督微调阶段包含更多高质量、多样化的训练数据,这些数据来源于三个渠道:

(1) 复用InternVL3的指令跟随数据,以保持对视觉-语言任务的广泛覆盖。

(2) “思维链”模式的多模态推理数据,旨在培养模型的深度思考能力。为构建此类数据,我们首先使用InternVL3-78B对图像进行描述,然后将描述输入DeepSeek-R1,以采样具有详细推理过程的展开序列。我们过滤掉最终答案不正确的展开序列。这些数据集的问题涵盖数学、科学等多个专业领域,从而增强模型在不同推理任务上的性能。

(3) 能力扩展数据集,赋予InternVL3.5新技能,包括基于GUI的交互、具身交互和可扩展向量

级联强化学习

级联强化学习旨在结合离线强化学习和在线强化学习的优势,以高效方式逐步推进多模态大语言模型的后训练过程。

具体而言,我们首先使用离线强化学习算法对模型进行微调,作为高效的热身阶段,以达到满意的结果,这能为后续阶段保证高质量的展开序列。

随后,我们采用在线强化学习算法,基于模型自身生成的展开序列进一步优化输出分布。与单一的离线或在线强化学习阶段相比,我们的级联强化学习在仅消耗少量GPU时间成本的情况下,实现了显著的性能提升。

在离线强化学习阶段,我们采用混合偏好优化(MPO)对模型进行微调。具体而言,MPO的训练目标是偏好损失$\mathcal{L}{p}$、质量损失$\mathcal{L}{q}$和生成损失$\mathcal{L}_{g}$的组合,可表示为:

LMPO=wpLp+wqLq+wgLg, \mathcal{L}_{\text{MPO}}= w_{p} \mathcal{L}_{p} + w_{q} \mathcal{L}_{q} + w_{g} \mathcal{L}_{g} ,LMPO​=wp​Lp​+wq​Lq​+wg​Lg​,

其中$w_{*}$表示分配给每个损失分量的权重。DPO损失、BCO损失和LM损失分别作为偏好损失、质量损失和生成损失。

在在线强化学习阶段,我们采用无参考模型约束的GSPO作为在线强化学习算法,发现其在训练密集型模型和混合专家(MoE)模型时均更为有效。与GRPO类似,优势定义为从同一查询采样的响应间的归一化奖励。 GSPO的训练目标如下:

LGSPO(θ)=Ex∼D,{yi}i=1G∼πθ old (⋅∣x)[1G∑i=1Gmin⁡(si(θ)A^i,clip⁡(si(θ),1−ε,1+ε)A^i)], \mathcal{L}_{\mathrm{GSPO}}(\theta)=\mathbb{E}_{x \sim \mathcal{D},\left\{y_i\right\}_{i=1}^G \sim \pi_{\theta \text { old }}(\cdot \mid x)}\left[\frac{1}{G} \sum_{i=1}^G \min \left(s_i(\theta) \widehat{A}_i, \operatorname{clip}\left(s_i(\theta), 1-\varepsilon, 1+\varepsilon\right) \widehat{A}_i\right)\right],LGSPO​(θ)=Ex∼D,{yi​}i=1G​∼πθ old ​(⋅∣x)​[G1​i=1∑G​min(si​(θ)Ai​,clip(si​(θ),1−ε,1+ε)Ai​)],

其中重要性采样比率定义为逐token比率的几何平均值。

更多技术细节和实验详情,请参见我们的论文。

视觉一致性学习

我们进一步将ViCO作为额外的训练阶段,以将视觉分辨率路由器(ViR) 集成到InternVL3.5中,从而降低InternVL3.5的推理成本。由此得到的高效版InternVL3.5被称为InternVL3.5-Flash。具体而言,ViCO包含两个阶段:

一致性训练: 在此阶段,训练整个模型以最小化不同压缩率下视觉令牌所生成的响应分布之间的差异。 在实践中,我们引入了一个额外的参考模型,该模型以InternVL3.5为初始值并保持冻结状态。 对于一个样本,每个图像块被表示为256个或64个令牌,训练目标定义如下:

LViCO=Eξ∼R[1N∑i=1NKL(πθref(yi∣y<i,I)  ∥  πθpolicy(yi∣y<i,Iξ))],\mathcal{L}_\text{ViCO} = \mathbb{E}_{\xi \sim \mathcal{R}} \Bigg[ \frac{1}{N} \sum_{i=1}^{N} \mathrm{KL} \Big( \pi_{\theta_{ref}}\left(y_i \mid y_{<i}, I\right) \;\Big\|\; \pi_{\theta_{policy}}\left(y_i \mid y_{<i}, I_\xi\right) \Big) \Bigg],LViCO​=Eξ∼R​[N1​i=1∑N​KL(πθref​​(yi​∣y<i​,I)​πθpolicy​​(yi​∣y<i​,Iξ​))],

其中$\mathrm{KL}$表示KL散度,ξ\xiξ表示压缩率,从{14,116}\{\frac{1}{4},\frac{1}{16}\}{41​,161​}中均匀采样。当ξ=14\xi=\frac{1}{4}ξ=41​时,图像IξI_\xiIξ​表示为256个令牌;当ξ=116\xi=\frac{1}{16}ξ=161​时,则表示为64个令牌。值得注意的是,参考模型始终以ξ=14\xi=\frac{1}{4}ξ=41​进行推理。

路由器训练: 此阶段旨在训练ViR,使其为不同的输入选择合适的分辨率权衡方案。 ViR被设计为一个二进制分类器,并使用标准的交叉熵损失进行训练。 为了构建路由目标,我们首先计算基于未压缩视觉令牌(即每个块256个令牌)和基于压缩视觉令牌(即每个块64个令牌)的模型输出之间的KL散度。 在此阶段,主MLLM(ViT、MLP和LLM)保持冻结状态,仅训练ViR。 具体而言,我们首先计算每个块的损失比:

ri=LViCO(yi∣I116)LViCO(yi∣I14),r_i = \frac{\mathcal{L}_\text{ViCO}\big(y_i \mid I_{\frac{1}{16}}\big)}{\mathcal{L}_\text{ViCO}\big(y_i \mid I_{\frac{1}{4}}\big)},ri​=LViCO​(yi​∣I41​​)LViCO​(yi​∣I161​​)​,

该比值量化了因压缩视觉令牌而导致的相对损失增加。基于此比值,块路由器的二进制真实标签定义为:

yirouter={0,ri<τ  (压缩影响可忽略)1,ri≥τ  (压缩影响显著),y_i^\text{router} = \begin{cases} 0, & r_i < \tau \; \text{(压缩影响可忽略)} \\ 1, & r_i \ge \tau \; \text{(压缩影响显著)}, \end{cases}yirouter​={0,1,​ri​<τ(压缩影响可忽略)ri​≥τ(压缩影响显著),​

其中yirouter=0y_i^{\text{router}}=0yirouter​=0和yirouter=1y_i^{\text{router}}=1yirouter​=1分别表示压缩率ξ\xiξ设置为116\tfrac{1}{16}161​和14\tfrac{1}{4}41​。

更多技术和实验细节请参见我们的论文。

测试时扩展(Test-Time Scaling)

测试时扩展(TTS)已被实证为提升LLM和MLLM推理能力的有效方法,尤其适用于需要多步推理的复杂任务。 在本研究中,我们实现了一种全面的测试时扩展方法,可同时提升推理深度(即深度思考)和广度(即平行思考)。

深度思考(Deep Thinking):通过激活思考模式,我们引导模型在生成最终答案前,进行刻意的逐步推理(即将复杂问题分解为逻辑步骤并验证中间结论)。这种方法能系统性地改善复杂问题解决方案的逻辑结构,尤其针对那些需要多步推理的问题,并提升推理深度。

平行思考(Parallel Thinking):借鉴InternVL3的经验,对于推理任务,我们采用Best-of-N(BoN)策略,使用VisualPRM-v1.1作为评判模型,从多个推理候选中选出最优响应。 这种方法能提升推理广度。

值得注意的是,除非另有说明,我们论文中报告的实验结果均未应用TTS。目前,我们仅将TTS应用于推理基准测试,因为我们发现模型已展现出强大的感知和理解能力,启用TTS并未带来显著提升。

解耦视觉-语言部署(Decoupled Vision-Language Deployment)

在多模态推理中,视觉编码器和语言模型具有截然不同的计算特性。将图像转换为语义特征的视觉编码器具有高度并行化能力,且不依赖长期历史状态。相比之下,语言模型采用自回归方式进行推理,需要前序状态来计算下一个状态。这种顺序特性使得语言部分对内存带宽和延迟更为敏感。 当MLLM大规模在线部署时,视觉和语言模型往往会相互阻塞,从而产生额外的推理成本。随着视觉模型规模增大或图像分辨率提高,这种影响会更加明显。

image/jpg

如上图所示,我们提出了解耦视觉-语言部署(DvD)方案来解决此问题。该方案通过分离视觉和语言处理,并特别关注优化预填充阶段。视觉子系统对图像进行批处理并生成紧凑的特征嵌入,然后将其传输至语言子系统,以便在解码前与文本上下文进行融合。这种分离减轻了阻塞问题,并使多模态预填充性能更接近纯语言模型。 在我们的系统实现中,ViT和MLP(以及InternVL3.5-Flash中的ViR)部署在视觉服务器上,而语言服务器仅执行LLM。通信是单向的,通过TCP传输BF16格式的视觉特征,也可选择采用RDMA以实现更高的传输速度。视觉处理、特征传输和语言处理被组织成一个异步三阶段流水线,能够实现重叠执行并最大限度减少流水线停顿。

DvD提高了视觉端的GPU利用率和处理效率,同时使语言服务器能够专注于LLM的预填充和解码,而不会被视觉计算阻塞。这种设计带来了更高的吞吐量和响应性。此外,该架构支持对视觉和语言模块进行独立的硬件成本优化,并便于无缝集成新模块,而无需修改语言服务器的部署。

多模态能力评估

多模态推理与数学能力

image/jpg

OCR、图表与文档理解

image/jpg

多图像理解与真实世界感知

image/jpg

综合多模态理解与多模态幻觉评估

image/jpg

视觉定位

image/jpg

多模态多语言理解

image/jpg

视频理解

image/jpg

GUI任务

image/jpg

具身任务

image/jpg

SVG任务

image/jpg

image/jpg

语言能力评估

image/jpg

消融实验

级联强化学习

image/jpg

image/jpg

解耦式视觉-语言部署

image/jpg

快速开始

我们提供了使用 transformers 运行 InternVL3.5-8B 的示例代码。请注意,参数规模 up to 30B 的模型可在单张 A100 GPU 上部署,38B 模型需两张 A100 GPU,235B 模型需八张 A100 GPU。

在大多数情况下,LMDeploy 和 vLLM 均可用于模型部署。但对于 InternVL3.5-20B-A4B,建议使用 vLLM,因为 lmdeploy 暂不支持 GPT-OSS。

请使用 transformers>=4.52.1 以确保模型正常运行。对于 20B 版本模型,需 transformers>=4.55.0。

模型加载

16 位(bf16 / fp16)

import torch
from transformers import AutoTokenizer, AutoModel
path = "OpenGVLab/InternVL3_5-8B"
model = AutoModel.from_pretrained(
    path,
    torch_dtype=torch.bfloat16,
    low_cpu_mem_usage=True,
    use_flash_attn=True,
    trust_remote_code=True).eval().cuda()

BNB 8位量化

import torch
from transformers import AutoTokenizer, AutoModel
path = "OpenGVLab/InternVL3_5-8B"
model = AutoModel.from_pretrained(
    path,
    torch_dtype=torch.bfloat16,
    load_in_8bit=True,
    low_cpu_mem_usage=True,
    use_flash_attn=True,
    trust_remote_code=True).eval()

多GPU

import math
import torch
from transformers import AutoTokenizer, AutoModel

path = "OpenGVLab/InternVL3_5-8B"
model = AutoModel.from_pretrained(
    path,
    torch_dtype=torch.bfloat16,
    low_cpu_mem_usage=True,
    use_flash_attn=True,
    trust_remote_code=True,
    device_map="auto").eval()

思考模式

若要启用思考模式,请将系统提示设置为我们的思考系统提示。启用思考模式时,建议将 do_sample 设置为 True 并将 temperature 设置为 0.6,以减少不必要的重复。

R1_SYSTEM_PROMPT = """
You are an AI assistant that rigorously follows this response protocol:

1. First, conduct a detailed analysis of the question. Consider different angles, potential solutions, and reason through the problem step-by-step. Enclose this entire thinking process within <think> and </think> tags.

2. After the thinking section, provide a clear, concise, and direct answer to the user's question. Separate the answer from the think section with a newline.

Ensure that the thinking process is thorough but remains focused on the query. The final answer should be standalone and not reference the thinking section.
""".strip()

model.system_message = R1_SYSTEMP_PROMPT

使用 Transformers 进行推理

import math
import numpy as np
import torch
import torchvision.transforms as T
from decord import VideoReader, cpu
from PIL import Image
from torchvision.transforms.functional import InterpolationMode
from transformers import AutoModel, AutoTokenizer

IMAGENET_MEAN = (0.485, 0.456, 0.406)
IMAGENET_STD = (0.229, 0.224, 0.225)

def build_transform(input_size):
    MEAN, STD = IMAGENET_MEAN, IMAGENET_STD
    transform = T.Compose([
        T.Lambda(lambda img: img.convert('RGB') if img.mode != 'RGB' else img),
        T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC),
        T.ToTensor(),
        T.Normalize(mean=MEAN, std=STD)
    ])
    return transform

def find_closest_aspect_ratio(aspect_ratio, target_ratios, width, height, image_size):
    best_ratio_diff = float('inf')
    best_ratio = (1, 1)
    area = width * height
    for ratio in target_ratios:
        target_aspect_ratio = ratio[0] / ratio[1]
        ratio_diff = abs(aspect_ratio - target_aspect_ratio)
        if ratio_diff < best_ratio_diff:
            best_ratio_diff = ratio_diff
            best_ratio = ratio
        elif ratio_diff == best_ratio_diff:
            if area > 0.5 * image_size * image_size * ratio[0] * ratio[1]:
                best_ratio = ratio
    return best_ratio

def dynamic_preprocess(image, min_num=1, max_num=12, image_size=448, use_thumbnail=False):
    orig_width, orig_height = image.size
    aspect_ratio = orig_width / orig_height

    # calculate the existing image aspect ratio
    target_ratios = set(
        (i, j) for n in range(min_num, max_num + 1) for i in range(1, n + 1) for j in range(1, n + 1) if
        i * j <= max_num and i * j >= min_num)
    target_ratios = sorted(target_ratios, key=lambda x: x[0] * x[1])

    # find the closest aspect ratio to the target
    target_aspect_ratio = find_closest_aspect_ratio(
        aspect_ratio, target_ratios, orig_width, orig_height, image_size)

    # calculate the target width and height
    target_width = image_size * target_aspect_ratio[0]
    target_height = image_size * target_aspect_ratio[1]
    blocks = target_aspect_ratio[0] * target_aspect_ratio[1]

    # resize the image
    resized_img = image.resize((target_width, target_height))
    processed_images = []
    for i in range(blocks):
        box = (
            (i % (target_width // image_size)) * image_size,
            (i // (target_width // image_size)) * image_size,
            ((i % (target_width // image_size)) + 1) * image_size,
            ((i // (target_width // image_size)) + 1) * image_size
        )
        # split the image
        split_img = resized_img.crop(box)
        processed_images.append(split_img)
    assert len(processed_images) == blocks
    if use_thumbnail and len(processed_images) != 1:
        thumbnail_img = image.resize((image_size, image_size))
        processed_images.append(thumbnail_img)
    return processed_images

def load_image(image_file, input_size=448, max_num=12):
    image = Image.open(image_file).convert('RGB')
    transform = build_transform(input_size=input_size)
    images = dynamic_preprocess(image, image_size=input_size, use_thumbnail=True, max_num=max_num)
    pixel_values = [transform(image) for image in images]
    pixel_values = torch.stack(pixel_values)
    return pixel_values

path = 'OpenGVLab/InternVL3_5-8B'
model = AutoModel.from_pretrained(
    path,
    torch_dtype=torch.bfloat16,
    load_in_8bit=False,
    low_cpu_mem_usage=True,
    use_flash_attn=True,
    trust_remote_code=True,
    device_map="auto").eval()
tokenizer = AutoTokenizer.from_pretrained(path, trust_remote_code=True, use_fast=False)

# set the max number of tiles in `max_num`
pixel_values = load_image('./examples/image1.jpg', max_num=12).to(torch.bfloat16).cuda()
generation_config = dict(max_new_tokens=1024, do_sample=True)

# pure-text conversation (纯文本对话)
question = 'Hello, who are you?'
response, history = model.chat(tokenizer, None, question, generation_config, history=None, return_history=True)
print(f'User: {question}\nAssistant: {response}')

question = 'Can you tell me a story?'
response, history = model.chat(tokenizer, None, question, generation_config, history=history, return_history=True)
print(f'User: {question}\nAssistant: {response}')

# single-image single-round conversation (单图单轮对话)
question = '<image>\nPlease describe the image shortly.'
response = model.chat(tokenizer, pixel_values, question, generation_config)
print(f'User: {question}\nAssistant: {response}')

# single-image multi-round conversation (单图多轮对话)
question = '<image>\nPlease describe the image in detail.'
response, history = model.chat(tokenizer, pixel_values, question, generation_config, history=None, return_history=True)
print(f'User: {question}\nAssistant: {response}')

question = 'Please write a poem according to the image.'
response, history = model.chat(tokenizer, pixel_values, question, generation_config, history=history, return_history=True)
print(f'User: {question}\nAssistant: {response}')

# multi-image multi-round conversation, combined images (多图多轮对话,拼接图像)
pixel_values1 = load_image('./examples/image1.jpg', max_num=12).to(torch.bfloat16).cuda()
pixel_values2 = load_image('./examples/image2.jpg', max_num=12).to(torch.bfloat16).cuda()
pixel_values = torch.cat((pixel_values1, pixel_values2), dim=0)

question = '<image>\nDescribe the two images in detail.'
response, history = model.chat(tokenizer, pixel_values, question, generation_config,
                               history=None, return_history=True)
print(f'User: {question}\nAssistant: {response}')

question = 'What are the similarities and differences between these two images.'
response, history = model.chat(tokenizer, pixel_values, question, generation_config,
                               history=history, return_history=True)
print(f'User: {question}\nAssistant: {response}')

# multi-image multi-round conversation, separate images (多图多轮对话,独立图像)
pixel_values1 = load_image('./examples/image1.jpg', max_num=12).to(torch.bfloat16).cuda()
pixel_values2 = load_image('./examples/image2.jpg', max_num=12).to(torch.bfloat16).cuda()
pixel_values = torch.cat((pixel_values1, pixel_values2), dim=0)
num_patches_list = [pixel_values1.size(0), pixel_values2.size(0)]

question = 'Image-1: <image>\nImage-2: <image>\nDescribe the two images in detail.'
response, history = model.chat(tokenizer, pixel_values, question, generation_config,
                               num_patches_list=num_patches_list,
                               history=None, return_history=True)
print(f'User: {question}\nAssistant: {response}')

question = 'What are the similarities and differences between these two images.'
response, history = model.chat(tokenizer, pixel_values, question, generation_config,
                               num_patches_list=num_patches_list,
                               history=history, return_history=True)
print(f'User: {question}\nAssistant: {response}')

# batch inference, single image per sample (单图批处理)
pixel_values1 = load_image('./examples/image1.jpg', max_num=12).to(torch.bfloat16).cuda()
pixel_values2 = load_image('./examples/image2.jpg', max_num=12).to(torch.bfloat16).cuda()
num_patches_list = [pixel_values1.size(0), pixel_values2.size(0)]
pixel_values = torch.cat((pixel_values1, pixel_values2), dim=0)

questions = ['<image>\nDescribe the image in detail.'] * len(num_patches_list)
responses = model.batch_chat(tokenizer, pixel_values,
                             num_patches_list=num_patches_list,
                             questions=questions,
                             generation_config=generation_config)
for question, response in zip(questions, responses):
    print(f'User: {question}\nAssistant: {response}')

# video multi-round conversation (视频多轮对话)
def get_index(bound, fps, max_frame, first_idx=0, num_segments=32):
    if bound:
        start, end = bound[0], bound[1]
    else:
        start, end = -100000, 100000
    start_idx = max(first_idx, round(start * fps))
    end_idx = min(round(end * fps), max_frame)
    seg_size = float(end_idx - start_idx) / num_segments
    frame_indices = np.array([
        int(start_idx + (seg_size / 2) + np.round(seg_size * idx))
        for idx in range(num_segments)
    ])
    return frame_indices

def load_video(video_path, bound=None, input_size=448, max_num=1, num_segments=32):
    vr = VideoReader(video_path, ctx=cpu(0), num_threads=1)
    max_frame = len(vr) - 1
    fps = float(vr.get_avg_fps())

    pixel_values_list, num_patches_list = [], []
    transform = build_transform(input_size=input_size)
    frame_indices = get_index(bound, fps, max_frame, first_idx=0, num_segments=num_segments)
    for frame_index in frame_indices:
        img = Image.fromarray(vr[frame_index].asnumpy()).convert('RGB')
        img = dynamic_preprocess(img, image_size=input_size, use_thumbnail=True, max_num=max_num)
        pixel_values = [transform(tile) for tile in img]
        pixel_values = torch.stack(pixel_values)
        num_patches_list.append(pixel_values.shape[0])
        pixel_values_list.append(pixel_values)
    pixel_values = torch.cat(pixel_values_list)
    return pixel_values, num_patches_list

video_path = './examples/red-panda.mp4'
pixel_values, num_patches_list = load_video(video_path, num_segments=8, max_num=1)
pixel_values = pixel_values.to(torch.bfloat16).cuda()
video_prefix = ''.join([f'Frame{i+1}: <image>\n' for i in range(len(num_patches_list))])
question = video_prefix + 'What is the red panda doing?'
# Frame1: <image>\nFrame2: <image>\n...\nFrame8: <image>\n{question}
response, history = model.chat(tokenizer, pixel_values, question, generation_config,
                               num_patches_list=num_patches_list, history=None, return_history=True)
print(f'User: {question}\nAssistant: {response}')

question = 'Describe this video in detail.'
response, history = model.chat(tokenizer, pixel_values, question, generation_config,
                               num_patches_list=num_patches_list, history=history, return_history=True)
print(f'User: {question}\nAssistant: {response}')

流式输出

除了此方法外,您还可以使用以下代码获取流式输出。

from transformers import TextIteratorStreamer
from threading import Thread

# Initialize the streamer
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True, timeout=10)
# Define the generation configuration
generation_config = dict(max_new_tokens=1024, do_sample=False, streamer=streamer)
# Start the model chat in a separate thread
thread = Thread(target=model.chat, kwargs=dict(
    tokenizer=tokenizer, pixel_values=pixel_values, question=question,
    history=None, return_history=False, generation_config=generation_config,
))
thread.start()

# Initialize an empty string to store the generated text
generated_text = ''
# Loop through the streamer to get the new text as it is generated
for new_text in streamer:
    if new_text == model.conv_template.sep:
        break
    generated_text += new_text
    print(new_text, end='', flush=True)  # Print each new chunk of generated text on the same line

微调

目前已有多个仓库支持InternVL系列模型的微调,包括InternVL、SWIFT、XTuner等。有关微调的更多详细信息,请参考它们的文档。

部署

LMDeploy

LMDeploy是一个用于压缩、部署和服务大语言模型与视觉语言模型的工具包。

pip install lmdeploy>=0.9.1

LMDeploy 将多模态视觉语言模型(VLM)复杂的推理过程抽象为易于使用的管道,类似于大语言模型(LLM)的推理管道。

一个“Hello, world”示例

from lmdeploy import pipeline, PytorchEngineConfig
from lmdeploy.vl import load_image

image = load_image('https://raw.githubusercontent.com/open-mmlab/mmdeploy/main/tests/data/tiger.jpeg')

# Please set tp=2 for the 38B version and tp=8 for the 241B-A28B version.
model = 'OpenGVLab/InternVL3_5-8B'
pipe = pipeline(model, backend_config=PytorchEngineConfig(session_len=32768, tp=1))

response = pipe(('describe this image', image))
print(response.text)

多图像推理

处理多张图像时,可将所有图像放入同一个列表中。请注意,多张图像会导致输入标记数量增加,因此通常需要增大上下文窗口的大小。

from lmdeploy import pipeline, PytorchEngineConfig
from lmdeploy.vl import load_image
from lmdeploy.vl.constants import IMAGE_TOKEN

# Please set tp=2 for the 38B version and tp=8 for the 241B-A28B version.
model = 'OpenGVLab/InternVL3_5-8B'
pipe = pipeline(model, backend_config=PytorchEngineConfig(session_len=32768, tp=1))

image_urls=[
    'https://raw.githubusercontent.com/open-mmlab/mmdeploy/main/demo/resources/human-pose.jpg',
    'https://raw.githubusercontent.com/open-mmlab/mmdeploy/main/demo/resources/det.jpg'
]

images = [load_image(img_url) for img_url in image_urls]
# Numbering images improves multi-image conversations
response = pipe((f'Image-1: {IMAGE_TOKEN}\nImage-2: {IMAGE_TOKEN}\ndescribe these two images', images))
print(response.text)

批量提示词推理

使用批量提示词进行推理非常简单,只需将它们放入列表结构中即可:

from lmdeploy import pipeline, PytorchEngineConfig
from lmdeploy.vl import load_image

# Please set tp=2 for the 38B version and tp=8 for the 241B-A28B version.
model = 'OpenGVLab/InternVL3_5-8B'
pipe = pipeline(model, backend_config=PytorchEngineConfig(session_len=32768, tp=1))

image_urls=[
    "https://raw.githubusercontent.com/open-mmlab/mmdeploy/main/demo/resources/human-pose.jpg",
    "https://raw.githubusercontent.com/open-mmlab/mmdeploy/main/demo/resources/det.jpg"
]
prompts = [('describe this image', load_image(img_url)) for img_url in image_urls]
response = pipe(prompts)
print(response)

多轮对话

使用该流水线进行多轮对话有两种方式。一种是按照 OpenAI 的格式构建消息,并使用上述介绍的方法;另一种是使用 pipeline.chat 接口。

from lmdeploy import pipeline, PytorchEngineConfig, GenerationConfig
from lmdeploy.vl import load_image

# Please set tp=2 for the 38B version and tp=8 for the 241B-A28B version.
model = 'OpenGVLab/InternVL3_5-8B'
pipe = pipeline(model, backend_config=PytorchEngineConfig(session_len=32768, tp=1))

image = load_image('https://raw.githubusercontent.com/open-mmlab/mmdeploy/main/demo/resources/human-pose.jpg')
gen_config = GenerationConfig(top_k=50, top_p=0.95, temperature=0.6, max_new_tokens=8192)
sess = pipe.chat(('describe this image', image), gen_config=gen_config)
print(sess.response.text)
sess = pipe.chat('What is the woman doing?', session=sess, gen_config=gen_config)
print(sess.response.text)

服务

LMDeploy 的 api_server 能让模型通过一条命令轻松打包成服务。其提供的 RESTful API 兼容 OpenAI 的接口。以下是服务启动示例:

lmdeploy serve api_server OpenGVLab/InternVL3_5-8B --server-port 23333 --tp 1 --backend pytorch

要使用 OpenAI 风格的接口,您需要安装 OpenAI:

pip install openai

然后,使用以下代码进行 API 调用:

from openai import OpenAI

client = OpenAI(api_key='YOUR_API_KEY', base_url='http://0.0.0.0:23333/v1')
model_name = client.models.list().data[0].id
response = client.chat.completions.create(
    model=model_name,
    messages=[{
        'role':
        'user',
        'content': [{
            'type': 'text',
            'text': 'describe this image',
        }, {
            'type': 'image_url',
            'image_url': {
                'url':
                'https://modelscope.oss-cn-beijing.aliyuncs.com/resource/tiger.jpeg',
            },
        }],
    }],
    temperature=0.8,
    top_p=0.8)
print(response)

许可证

本项目基于 apache-2.0 许可证发布。本项目使用预训练的 Qwen3 作为组件,该组件同样基于 apache-2.0 许可证授权。

引用

如果您发现本项目对您的研究有所帮助,请考虑引用:

@article{wang2025internvl3_5,
  title={InternVL3.5: Advancing Open-Source Multimodal Models in Versatility, Reasoning, and Efficiency},
  author={Wang, Weiyun and Gao, Zhangwei and Gu, Lixin and Pu, Hengjun and Cui, Long and Wei, Xingguang and Liu, Zhaoyang and Jing, Linglin and Ye, Shenglong and Shao, Jie and others},
  journal={arXiv preprint arXiv:2508.18265},
  year={2025}
}