Qwen3-VL 深度源码分析:视觉语言多模态模型的完整解剖

0. 引言

视觉语言模型(Vision Language Model,简称VLM)代表了人工智能领域的重要进展,它能够同时理解和处理文本与图像信息。Qwen3-VL作为通义千问系列的最新多模态模型,在架构设计上进行了多项创新性改进。本文将从源码层面深入剖析Qwen3-VL的技术实现,帮助读者全面理解这一先进模型的工作原理。

Qwen3-VL的核心改进包括:将目标检测的坐标表示从归一化坐标改回绝对坐标,增强了模型在空间定位任务上的表现;引入DeepStack架构,通过多层视觉特征融合提升视觉理解能力;采用MRoPE-Interleave位置编码方案,更好地建模时空关系;支持3D检测和空间关系推理等高级视觉任务。这些技术创新使Qwen3-VL在多个视觉语言基准测试中取得了显著的性能提升。

在这里插入图片描述

1. 多模态大模型的基础架构

在深入Qwen3-VL的具体实现之前,我们需要理解多模态大模型的基本组成部分。任何一个成熟的视觉语言模型都包含四个核心组件,它们各司其职,共同完成从原始输入到最终输出的完整处理流程

首先是**Chat Template(对话模板)**组件,它负责将用户的多模态输入转换为模型可以理解的标准格式。这一组件需要处理文本、图像、视频等不同模态的输入,并将它们统一编码为符合模型要求的序列格式。对于Qwen系列模型,采用的是ChatML格式,通过特殊标记符来区分不同的角色和内容类型

其次是Image Processor(图像处理器),专门负责对输入图像进行预处理。这一步骤包括图像的缩放、裁剪、归一化等操作,将原始图像转换为模型视觉编码器所需的张量格式。在LLaVA等模型中,还会在这一阶段完成图像的patch切分工作,为后续的视觉编码做好准备。

第三个组件是Processor(处理器),它是一个协调者的角色。Processor整合了Image Processor处理图像和Tokenizer处理文本的功能,确保多模态输入能够正确对齐。在某些模型架构中,如MiniCPM-V,Processor还会为图像预留占位符,确保视觉特征能够正确插入到文本序列中的指定位置。

最后是Model本身,包含了视觉编码器(Vision Model)、特征融合模块和语言模型(LLM Encoder)。视觉编码器负责提取图像特征,特征融合模块将视觉特征与文本特征结合,语言模型则在融合后的多模态表示上进行自回归生成。这种模块化的设计使得不同组件可以独立优化和升级。

基本流程代码示例

# 多模态模型的基本处理流程
from transformers import AutoConfig, AutoProcessor, AutoModelForCausalLM
from PIL import Image

# 1. 加载配置和模型组件
config = AutoConfig.from_pretrained("Qwen/Qwen3-VL-7B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen3-VL-7B-Instruct")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-VL-7B-Instruct")

# 2. 处理输入
image = Image.open("image.jpg")
messages = [
    {
        "role": "user",
        "content": [
            {"type": "image", "image": image},
            {"type": "text", "text": "请描述这张图片"}
        ]
    }
]

# 3. 通过processor统一处理
inputs = processor.apply_chat_template(
    messages,
    tokenize=True,
    return_dict=True,
    return_tensors="pt"
)

# 4. 模型推理
outputs = model.generate(
    **inputs,
    max_new_tokens=128,
    do_sample=True,
    temperature=0.7
)

这种清晰的模块划分不仅使代码结构更加清晰,也为模型的扩展和优化提供了便利。

在这里插入图片描述

2. Chat Template:多模态输入的标准化处理

Chat Template是连接用户输入和模型内部表示的重要桥梁。Qwen3-VL采用了基于Jinja2模板引擎的灵活设计,能够处理文本、图像、视频等多种模态的混合输入。与Qwen2-VL相比,Qwen3-VL的Chat Template进行了重要的功能扩展,特别是增加了对工具调用(Tool Call)的完整支持。

Qwen3-VL提供了两个版本的Chat Template:Instruct模型和Thinking模型。Instruct模型的模板相对简洁,专注于标准的指令遵循任务;而Thinking模型则在模板中集成了思维链(Chain of Thought)的处理逻辑,允许模型在生成最终答案前展示推理过程。这种设计使得同一个模型架构可以支持不同的应用场景。

2.1 Instruct模型的模板机制

对于Instruct模型,Chat Template的核心功能是将多模态输入转换为统一的文本序列,并通过特殊标记来标识不同模态的内容。当处理包含图像的输入时,模板会将图像位置标记为<|vision_start|><|image_pad|><|vision_end|>,这个标记序列在后续处理中会被实际的图像特征向量替换。

# Instruct模型处理示例
messages = [
    {"role": "system", "content": "你是一个助手,可以调用工具。"},
    {
        "role": "user",
        "content": [
            {"type": "text", "text": "今天图片这里显示的天气如何?"},
            {"type": "image_url", "image_url": "http://example.com/weather.jpg"}
        ]
    }
]

# 模板处理后的输出
"""
<|im_start|>system
你是一个助手,可以调用工具。
<|im_end|>
<|im_start|>user
今天图片这里显示的天气如何?<|vision_start|><|image_pad|><|vision_end|>
<|im_end|>
<|im_start|>assistant
"""

模板处理的关键在于维护正确的序列顺序和角色标识。每个对话轮次都由<|im_start|><|im_end|>标记包围,明确标识了system、user和assistant的发言边界。这种结构化的表示使得模型能够清楚地理解对话上下文和当前应该执行的任务。

2.2 Tool Call支持

Qwen3-VL的一个重要特性是对工具调用的原生支持。当system消息中包含工具定义时,模板会在开头插入详细的工具使用说明,告诉模型如何调用可用的函数。工具调用的结果会以<function_response>标签包裹,并作为user角色的消息返回给模型。

# 工具调用的完整流程
messages = [
    {"role": "system", "content": "你是一个助手,可以调用工具。"},
    {
        "role": "user",
        "content": [{"type": "text", "text": "北京今天天气如何?"}]
    },
    {
        "role": "assistant",
        "tool_calls": [{
            "function": {
                "name": "get_weather",
                "arguments": {"location": "北京"}
            }
        }]
    },
    {
        "role": "tool",
        "content": [{"type": "text", "text": '{"temperature": 25, "condition": "晴"}'}]
    }
]

# 模板会将tool response转换为特殊格式
"""
<|im_start|>user
<function_response>
{"temperature": 25, "condition": "晴"}
</function_response>
<|im_end|>
"""

值得注意的是,工具调用的返回也可以包含图像。这使得Qwen3-VL能够支持需要返回视觉内容的工具,例如绘图工具、图表生成工具或图像检索工具。

2.3 Thinking模型的思维链处理

Thinking模型的模板在Instruct模型基础上增加了对推理过程的显式支持。模板会识别内容中的<think></think>标签,将其中的推理内容单独提取出来,并在适当的时候展示给用户或用于模型训练。

# Thinking模型的推理过程处理
assistant_message = {
    "role": "assistant",
    "content": "<think>首先分析图片中的天气特征...</think>根据图片显示,今天天气晴朗"
}

# 模板会自动分离推理内容和最终回答
"""
<|im_start|>assistant
<think>
首先分析图片中的天气特征...
</think>

根据图片显示,今天天气晴朗
<|im_end|>
"""

Thinking模板的一个关键设计是判断何时输出思维链。只有在最后一轮用户提问之后的assistant回复中,才会包含<think>标签。这样设计的原因是,在多轮工具调用的中间步骤,不需要展示推理过程,只有在生成最终答案时才需要完整的思维链。

在这里插入图片描述

3. 视觉处理流水线:从像素到特征向量

Qwen3-VL在视觉处理方面继承了Qwen2-VL的核心架构,但在patch size和处理策略上进行了优化。视觉处理的目标是将原始的图像或视频数据转换为模型可以理解的特征向量序列,这个过程涉及多个精心设计的步骤

3.1 动态分辨率处理

与传统的固定分辨率方法不同,Qwen3-VL采用动态分辨率处理策略。这意味着输入图像不会被强制缩放到固定尺寸,而是根据内容自适应地调整。这种设计的优势在于既能保留图像细节,又能控制计算成本。

Qwen3-VL使用16×16的patch size,这比Qwen2-VL的14×14更大。更大的patch size意味着每张图像会被切分成更少的patch,从而减少序列长度和计算量。但同时,通过支持更高的输入分辨率,模型仍然能够捕捉足够的细节信息。

# 图像预处理的核心参数
class ImageProcessConfig:
    patch_size = 16                              # Qwen3-VL使用16x16的patch
    spatial_merge_size = 2                       # 空间合并尺寸
    temporal_patch_size = 2                      # 时间维度的patch尺寸
    min_pixels = 56 * 56                         # 最小像素数
    max_pixels = 14 * 14 * 4 * 1280             # 最大像素数,支持超高分辨率

def smart_resize(height, width, factor=28, min_pixels=3136, max_pixels=12845056):
    """
    根据图像尺寸智能调整分辨率
    确保高宽都是factor的整数倍,同时控制总像素数在合理范围内
    """
    import math

    # 计算目标尺寸
    total_pixels = height * width
    if total_pixels > max_pixels:
        scale = math.sqrt(max_pixels / total_pixels)
        height = int(height * scale)
        width = int(width * scale)
    elif total_pixels < min_pixels:
        scale = math.sqrt(min_pixels / total_pixels)
        height = int(height * scale)
        width = int(width * scale)

    # 调整为factor的整数倍
    height = round(height / factor) * factor
    width = round(width / factor) * factor

    return height, width

# 使用示例
# 输入:1200×673的图像
# smart_resize(1200, 673, factor=28)
# 输出:1216×672 (都是28的整数倍)

这个智能缩放算法确保了无论输入图像的原始尺寸如何,处理后的图像都满足模型的要求。factor参数设为28而不是16,是因为后续还会有2×2的spatial merge操作,所以需要确保尺寸是28的整数倍。

3.2 Temporal维度的处理

Qwen3-VL的一个重要特性是同时支持图像和视频输入。为了统一处理这两种输入,模型引入了temporal(时间)维度。对于静态图像,会在时间维度上复制一份,形成一个包含两帧相同内容的"伪视频"。这种设计使得视觉编码器可以使用统一的架构处理图像和视频。

# Patch构建过程
def build_patches(image_tensor, patch_size=16, temporal_patch_size=2):
    """
    将图像转换为patch序列
    对于图像,会在时间维度复制以支持统一的3D patch格式

    Args:
        image_tensor: [B, C, H, W] 或 [B, T, C, H, W]
    """
    import torch

    # 图像在时间维度复制
    if image_tensor.ndim == 4:  # 如果是图像,增加时间维度
        image_tensor = image_tensor.unsqueeze(1)  # [B, 1, C, H, W]

    if image_tensor.shape[1] == 1:
        # 复制成两帧,形成伪视频
        image_tensor = image_tensor.repeat(1, temporal_patch_size, 1, 1, 1)  # [B, 2, C, H, W]

    batch_size, frames, channels, height, width = image_tensor.shape

    # 计算grid尺寸
    grid_t = frames // temporal_patch_size
    grid_h = height // patch_size
    grid_w = width // patch_size

    # 重组为patch
    # 最终形状: [B, grid_t, grid_h, grid_w, channels, temporal_patch_size, patch_size, patch_size]
    patches = image_tensor.reshape(
        batch_size,
        grid_t, temporal_patch_size,
        channels,
        grid_h, patch_size,
        grid_w, patch_size
    )

    # 一个patch的维度: channel * temporal_patch_size * patch_size * patch_size
    # 即: 3 * 2 * 16 * 16 = 1536
    return patches, (grid_t, grid_h, grid_w)

每个patch的完整维度是3×2×16×16=1536,这比传统的2D patch包含了更多的信息。时间维度的引入不仅统一了图像和视频的处理流程,还为模型提供了潜在的时序建模能力,即使对于静态图像也能利用这种结构化的表示。

3.3 Patch重排序与空间合并

Qwen3-VL采用了一种特殊的patch重排序策略,这与后续的DeepStack架构密切相关。在将2D图像展平为1D序列时,模型不是简单地按行或按列顺序排列,而是将每2×2个相邻的patch作为一组进行排列

# Patch重排序示例
def reorder_patches(patches, merge_size=2):
    """
    按照空间邻近性重排序patch

    输入形状: [batch, grid_t, grid_h, grid_w, channel, t_patch, h_patch, w_patch]
    输出形状: [batch, total_patches, patch_feat_dim]
    """
    import torch

    batch, grid_t, grid_h, grid_w, channel, t_patch, h_patch, w_patch = patches.shape

    # 重组维度以实现分组
    # 将grid_h分成grid_h//merge_size和merge_size两个维度
    patches = patches.reshape(
        batch,
        grid_t,
        grid_h // merge_size, merge_size,
        grid_w // merge_size, merge_size,
        channel, t_patch, h_patch, w_patch
    )

    # 重排序:先遍历大grid,再遍历内部的2×2 block
    # 原始: [batch, grid_t, gh//2, 2, gw//2, 2, c, t, h, w]
    # 目标: [batch, grid_t, gh//2, gw//2, 2, 2, c, t, h, w]
    patches = patches.permute(0, 1, 2, 4, 3, 5, 6, 7, 8, 9)

    # 展平为序列
    flatten_patches = patches.reshape(
        batch, grid_t * grid_h * grid_w,
        channel * t_patch * h_patch * w_patch
    )

    return flatten_patches

# 可视化重排序效果
# 假设有4×4=16个patch,标记为1-16
# 原始布局:
# [ 1,  2,  3,  4]
# [ 5,  6,  7,  8]
# [ 9, 10, 11, 12]
# [13, 14, 15, 16]
#
# 重排序后的序列(merge_size=2):
# [1, 2, 5, 6, 3, 4, 7, 8, 9, 10, 13, 14, 11, 12, 15, 16]

这种重排序的好处在于,后续的Patch Merger模块可以很方便地将每4个连续的patch合并成一个特征,既保持了空间邻近性,又减少了序列长度。这是Qwen3-VL在效率和性能之间取得平衡的重要设计。

3.4 Image Grid THW表示

处理完图像后,除了得到patch序列,还会生成一个image_grid_thw张量,记录每张图像的时间、高度、宽度信息。这个信息在后续的位置编码和特征重组中都会用到。

# Image Grid THW的生成
def get_image_grid_thw(images, patch_size=16, temporal_patch_size=2):
    """
    计算每张图像在patch化后的grid尺寸
    返回(T, H, W),其中T是时间帧数,H和W是空间维度的grid数量
    """
    import torch

    grid_thw_list = []

    for image in images:
        if image.ndim == 3:  # 静态图像 [C, H, W]
            t_frames = 1
            h, w = image.shape[-2:]
        else:  # 视频 [T, C, H, W]
            t_frames = image.shape[0]
            h, w = image.shape[-2:]

        # 计算grid尺寸(after resize)
        grid_t = (t_frames + temporal_patch_size - 1) // temporal_patch_size
        grid_h = h // patch_size
        grid_w = w // patch_size

        grid_thw_list.append([grid_t, grid_h, grid_w])

    return torch.tensor(grid_thw_list)

# 使用示例
# 对于一张1200×673的图像
# 经过smart_resize后变成1216×672
# grid_h = 672 // 16 = 42
# grid_w = 1216 // 16 = 76
# grid_t = 1 (图像会被复制成2帧,然后2帧合并成1个temporal patch)
# 因此 image_grid_thw = [[1, 42, 76]]
# 总patch数 = 1 * 42 * 76 = 3192
# 经过spatial merge(2×2合并)后,序列长度 = 3192 // 4 = 798

这个grid_thw信息贯穿整个模型的forward过程,是连接视觉编码器和语言模型的重要桥梁。
在这里插入图片描述

4. DeepStack架构:多层视觉特征融合

DeepStack是Qwen3-VL相比前代模型的一个重要创新。传统的视觉语言模型通常只使用视觉编码器最后一层的输出特征,而DeepStack架构则从视觉编码器的多个中间层提取特征,并将这些多尺度特征融合到语言模型的不同层中。
---

4.1 中间层特征提取

Qwen3-VL的视觉编码器共有27层Vision Transformer Block。DeepStack架构选择了第8层、第16层和第24层的输出作为额外的特征。这些中间层捕捉了不同抽象级别的视觉信息:浅层更关注低级视觉特征如边缘和纹理,深层则编码更高级的语义信息。

# DeepStack特征提取示例
class Qwen3VLVisionModel(nn.Module):
    def __init__(self, config):
        super().__init__()

        # 27层Vision Transformer Block
        self.blocks = nn.ModuleList([
            Qwen3VLVisionBlock(config)
            for _ in range(27)
        ])

        # DeepStack配置:提取第8、16、24层的特征
        self.deepstack_visual_indexes = [8, 16, 24]

        # 为最终输出准备的patch merger
        self.merger = Qwen3VLVisionPatchMerger(config, use_postshuffle_norm=False)

        # 为每个中间层准备独立的patch merger
        # 用于在fusion时进行维度转换
        self.deepstack_merger_list = nn.ModuleList([
            Qwen3VLVisionPatchMerger(config, use_postshuffle_norm=True)
            for _ in range(len(self.deepstack_visual_indexes))
        ])

    def forward(self, pixel_values, grid_thw):
        """
        前向传播过程中提取多层特征
        """
        # Patch embedding
        hidden_states = self.patch_embed(pixel_values)

        # 添加位置编码
        position_embeddings = self.get_position_embeddings(grid_thw)
        hidden_states = hidden_states + position_embeddings

        # 存储中间层特征
        deepstack_features = []

        # 逐层前向传播
        for layer_idx, block in enumerate(self.blocks):
            hidden_states = block(hidden_states, position_embeddings)

            # 如果当前层是DeepStack指定的层,提取并处理特征
            if layer_idx in self.deepstack_visual_indexes:
                # 找到对应的merger
                merger_idx = self.deepstack_visual_indexes.index(layer_idx)
                merger = self.deepstack_merger_list[merger_idx]

                # 将当前层特征通过merger处理
                # merger会进行4→1的空间合并,并将维度转换到LLM的hidden size
                deepstack_feature = merger(hidden_states)
                deepstack_features.append(deepstack_feature)

        # 处理最终层的输出
        final_hidden_states = self.merger(hidden_states)

        return final_hidden_states, deepstack_features

每个中间层的特征都经过独立的Patch Merger处理。Patch Merger是一个关键组件,它完成两个重要任务:将4个相邻patch的特征合并为1个,减少序列长度;将特征维度从视觉编码器的hidden size转换到语言模型的hidden size
在这里插入图片描述

4.2 Patch Merger的实现细节

Patch Merger采用MLP架构,通过可学习的线性变换实现特征合并和维度转换。与简单的平均池化或最大池化不同,MLP能够学习更复杂的特征融合策略。

class Qwen3VLVisionPatchMerger(nn.Module):
    def __init__(self, config, use_postshuffle_norm=False):
        super().__init__()
        self.vision_hidden_size = config.hidden_size        # 视觉编码器的hidden size (1664)
        self.llm_hidden_size = config.text_config.hidden_size  # LLM的hidden size (3584)
        self.merge_size = config.spatial_merge_size         # 通常是2,表示2×2合并

        # MLP结构:先升维再降维
        merged_dim = self.vision_hidden_size * (self.merge_size ** 2)  # 1664 * 4

        self.mlp = nn.Sequential(
            nn.Linear(merged_dim, self.llm_hidden_size),
            nn.GELU(),
            nn.Linear(self.llm_hidden_size, self.llm_hidden_size)
        )

        # 可选的后处理LayerNorm
        self.use_postshuffle_norm = use_postshuffle_norm
        if use_postshuffle_norm:
            self.ln = nn.LayerNorm(self.llm_hidden_size)

    def forward(self, hidden_states):
        """
        输入: [seq_len, vision_hidden_size]
        输出: [seq_len // 4, llm_hidden_size]
        """
        seq_len, hidden_size = hidden_states.shape
        merge_size_sq = self.merge_size ** 2

        # 每4个patch为一组
        num_groups = seq_len // merge_size_sq
        grouped = hidden_states.reshape(num_groups, merge_size_sq, hidden_size)

        # 将每组的4个patch拼接
        concatenated = grouped.reshape(num_groups, merge_size_sq * hidden_size)

        # 通过MLP转换
        merged = self.mlp(concatenated)

        # 可选的LayerNorm
        if self.use_postshuffle_norm:
            merged = self.ln(merged)

        return merged

值得注意的是,最终层输出使用的merger不包含后处理LayerNorm(use_postshuffle_norm=False),而中间层使用的merger包含LayerNorm(use_postshuffle_norm=True)。这是因为中间层特征会被注入到语言模型的前几层,需要更好的归一化来保持训练稳定性。

4.3 特征融合到语言模型

提取的DeepStack特征会被注入到语言模型的前3层。具体来说,第8层的视觉特征被加到语言模型第0层的输出上,第16层的特征加到第1层,第24层的特征加到第2层。这种设计使得语言模型的浅层就能获得丰富的视觉信息。

class Qwen3VLTextModel(nn.Module):
    def forward(
        self,
        inputs_embeds,
        visual_pos_masks=None,       # 标记哪些位置是视觉token
        deepstack_visual_embeds=None, # 三层DeepStack特征
        **kwargs
    ):
        """
        多模态特征融合的关键步骤
        """
        hidden_states = inputs_embeds

        # 遍历所有语言模型层
        for layer_idx, decoder_layer in enumerate(self.layers):
            # 标准的transformer layer前向传播
            residual = hidden_states
            hidden_states = decoder_layer(
                hidden_states,
                attention_mask=kwargs.get('attention_mask'),
                position_ids=kwargs.get('position_ids'),
                **kwargs
            )

            # 如果当前层需要DeepStack特征注入
            if deepstack_visual_embeds is not None and layer_idx < len(deepstack_visual_embeds):
                # 获取对应层的DeepStack特征
                visual_embeds = deepstack_visual_embeds[layer_idx]

                # 只在视觉token的位置进行特征注入
                # visual_pos_masks标记了哪些位置是视觉token
                # 使用residual connection: 当前hidden state + 视觉特征
                if visual_pos_masks is not None:
                    hidden_states[visual_pos_masks] = (
                        hidden_states[visual_pos_masks] + visual_embeds
                    )

        return hidden_states

这种特征注入机制有几个关键点:

  1. 选择性注入:只在视觉token对应的位置注入特征,文本token不受影响
  2. 残差连接:使用加法而非替换,保留了语言模型自身学习的表示
  3. 层级注入:注入发生在每层的self-attention和FFN之后,不干扰transformer的标准计算流程
  4. 渐进式融合:浅层融合低级特征,帮助模型早期学习到基本的视觉信息

4.4 DeepStack的优势分析

DeepStack架构带来了多方面的性能提升。在目标检测和图像分割等需要精确定位的任务中,来自浅层的低级特征能够提供更准确的空间信息。在图像描述和视觉问答等高级理解任务中,来自深层的语义特征则提供了更丰富的上下文。通过在语言模型的不同层次融合这些多尺度特征,模型能够根据任务需求灵活利用不同抽象级别的视觉信息。

实验表明,DeepStack使Qwen3-VL在保持推理速度的同时,在多个视觉语言基准测试上取得了显著提升。相比只使用最终层特征的baseline,DeepStack版本在需要精确空间理解的任务上性能提升尤为明显。


5. MRoPE-Interleave:改进的多模态位置编码

位置编码是Transformer架构的核心组件之一,它赋予模型理解序列顺序的能力。对于多模态模型,位置编码需要同时建模文本的线性顺序、图像的2D空间结构、以及视频的时间动态。Qwen3-VL采用的MRoPE-Interleave机制是对传统RoPE的重要改进。

5.1 传统RoPE回顾

旋转位置编码(Rotary Position Embedding, RoPE)通过旋转变换将位置信息注入到attention机制中。对于序列中的位置m,RoPE为每对特征维度构造一个旋转角度,使得不同位置的query和key向量之间的点积自然包含了它们的相对位置信息。

# 传统RoPE的核心思想
import torch
import math

def apply_rotary_pos_emb(q, k, position_ids, rope_theta=10000):
    """
    将RoPE应用到query和key向量

    Args:
        q, k: shape [batch, seq_len, num_heads, head_dim]
        position_ids: shape [batch, seq_len]
        rope_theta: RoPE的base frequency
    """
    head_dim = q.shape[-1]
    device = q.device

    # 计算每个维度对的频率
    # dim_t = [0, 2, 4, ..., head_dim-2]
    dim_t = torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)

    # freqs = [1/θ^(0/d), 1/θ^(2/d), ..., 1/θ^((d-2)/d)]
    freqs = 1.0 / (rope_theta ** (dim_t / head_dim))

    # 对每个位置计算角度
    # position_ids: [batch, seq_len] -> [batch, seq_len, 1]
    # freqs: [head_dim/2] -> [1, 1, head_dim/2]
    # angles: [batch, seq_len, head_dim/2]
    position_ids_expanded = position_ids.unsqueeze(-1).float()
    angles = position_ids_expanded @ freqs.unsqueeze(0)

    # 计算cos和sin
    cos = angles.cos()  # [batch, seq_len, head_dim/2]
    sin = angles.sin()

    # 复制以匹配head_dim
    cos = torch.cat([cos, cos], dim=-1)  # [batch, seq_len, head_dim]
    sin = torch.cat([sin, sin], dim=-1)

    # 应用旋转
    # q和k的每一对维度(x, y)会被旋转
    def rotate_half(x):
        """辅助函数:将向量的后半部分移到前面并取反"""
        x1, x2 = x.chunk(2, dim=-1)
        return torch.cat([-x2, x1], dim=-1)

    q_embed = (q * cos) + (rotate_half(q) * sin)
    k_embed = (k * cos) + (rotate_half(k) * sin)

    return q_embed, k_embed

这种方法在纯文本模型中效果很好,但对于多模态输入存在局限:它只能建模一维的序列顺序,无法表达图像patch之间的2D空间关系。

5.2 Multi-dimensional RoPE (MRoPE)

为了解决这个问题,Qwen2-VL引入了MRoPE,为每个位置分配三个position ID:temporal、height和width。这样,每个图像patch都有明确的时空坐标,模型可以区分在不同维度上的相对位置。

# 为图像patch生成3D position IDs
def get_3d_position_ids(image_grid_thw):
    """
    为图像生成时间、高度、宽度三个维度的position IDs

    Args:
        image_grid_thw: [num_images, 3], 每张图的(T, H, W)

    Returns:
        position_ids: [3, total_tokens], 三个维度的position IDs
    """
    import torch

    position_ids_list = []

    for t, h, w in image_grid_thw:
        # 生成每个维度的坐标
        # temporal: [0,0,0,...,0, 1,1,1,...,1, ..., t-1,t-1,t-1,...,t-1]
        t_ids = torch.arange(t, dtype=torch.long).repeat_interleave(h * w)

        # height: [0,0,...,0, 1,1,...,1, ..., h-1,h-1,...,h-1] (重复t次)
        h_ids = torch.arange(h, dtype=torch.long).repeat_interleave(w).repeat(t)

        # width: [0,1,2,...,w-1, 0,1,2,...,w-1, ...] (重复t*h次)
        w_ids = torch.arange(w, dtype=torch.long).repeat(t * h)

        # 堆叠三个维度
        position_ids = torch.stack([t_ids, h_ids, w_ids], dim=0)
        position_ids_list.append(position_ids)

    return torch.cat(position_ids_list, dim=1)

# 示例:一张2×3的图像(简化)
# patches布局:
# [p0, p1, p2]  # t=0
# [p3, p4, p5]  # t=1
#
# position_ids:
# temporal: [0, 0, 0, 1, 1, 1]
# height:   [0, 0, 0, 0, 0, 0]  (只有一行)
# width:    [0, 1, 2, 0, 1, 2]

有了三维position ID后,需要为每个维度分别计算RoPE,然后组合起来。原始的MRoPE实现是将head_dim按维度分块:前1/3用于temporal,中间1/3用于height,后1/3用于width。

5.3 MRoPE-Interleave的改进

…详情请参照古月居

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐