MNN模型支持:Qwen3-VL

Qwen3-VL 架构解析

在我们深入技术细节之前,首先从推理引擎的视角,解析一下 Qwen3-VL 的架构创新。Qwen3-VL 作为 Qwen 系列迄今最强大的视觉语言模型,其卓越性能背后是模型结构的深度优化,而这些优化也为 MNN 这样的推理引擎带来了新的挑战。

1. DeepStack:从“单向连接”到“多层融合” 传统的视觉语言模型通常采用 Vision Encoder -> LLM 的串行结构,视觉特征在输入语言模型后便不再与视觉模块交互。而 Qwen3-VL 引入的 DeepStack 机制(如下图所示)打破了这一常规。它将 Vision Transformer (ViT) 不同层级的特征图(feature maps)提取出来,直接注入到语言模型解码器(LLM Decoder)的对应层。

  • 对推理引擎的挑战: 这种结构意味着计算图不再是简单的线性序列。MNN 需要处理 Vision Encoder 的多个输出张量,并在语言模型的推理过程中,于指定的网络层精确地将这些 deepstack 张量作为额外输入进行融合。这要求引擎具备灵活的图执行能力和高效的内存管理,以处理这种“旁路”输入,避免不必要的延迟。

2. 位置编码革新:pos_embeds 与 Interleaved MRoPE Qwen3-VL 为了增强对长视频和高分辨率图像的空间与时间理解能力,对位置编码进行了革新,例如采用了 Interleaved MRoPE。同时,其 Vision Encoder 引入了一个新的动态位置嵌入 pos_embeds。这个 pos_embeds 通过双线性插值动态生成,以适应任意分辨率的输入图像。

  • 对推理引擎的挑战: 动态插值的计算过程包含大量循环和条件判断,这些操作难以直接转换为高效的静态计算图(ONNX)。若强行在图内实现,会引入大量低效算子,严重影响推理性能。因此,如何将这部分动态计算逻辑剥离出主计算图,在运行时(Runtime)高效实现,同时保证模型精度,成为了适配的关键。

3. 架构多样性:Dense 与 MoE 的统一 Qwen3-VL 同时提供了常规的 Dense 模型和更高效的 MoE (Mixture-of-Experts) 变体。然而,Qwen3-VL-MoE 的专家层(Experts)在官方实现上与标准的 Qwen3-MoE 结构存在差异,这给模型的统一转换和部署带来了障碍。

  • 对推理引擎的挑战: 为了避免为两种 MoE 模型开发两套独立的推理后端,我们需要在模型转换阶段进行“适配”。目标是生成一种标准化的 ONNX 结构,让 MNN 的 MoE 推理逻辑可以无差别地处理,这考验了我们在模型前端处理上的灵活性和工程能力。

Qwen3-VL 模型架构图

将如此强大的模型引入 MNN,适配上述这些先进特性,是我们这次工作的核心。下面,我们将详细介绍针对这三大挑战的具体解决方案。

适配动态位置编码

解决方案: 我们将 pos_embeds 的计算拆解,把复杂的索引和权重插值计算移到计算图外(由 MNN C++ Runtime 在运行时处理),仅将纯粹的张量运算保留在 ONNX 模型内。

Python 端修改:

VisionEncoder 的 forward 函数不再自己计算插值,而是直接接收预先计算好的 idx_tensorweight_tensor

# VisionEncoder 的 forward 函数修改
def forward(self, flatten_patches, position_ids, attention_mask, idx_tensor, weight_tensor):
    # ...
    hidden_states = self.patch_embed(flatten_patches)

    # 使用预先计算的索引和权重来获取 pos_embeds
    pos_embeds = self.pos_embed(idx_tensor) * weight_tensor.unsqueeze(2)
    pos_embeds = torch.sum(pos_embeds, 0, False)
    hidden_states = hidden_states + pos_embeds
    # ... 后续 transformer block 计算
    return image_embeds, deepstack_feature

C++ 端实现:

在 C++ 运行时,我们复现了插值逻辑,根据输入的图像尺寸动态生成 idx_tensorweight_tensor,并作为新的输入传给 MNN 模型。

if (isQwen3VL) {
    // 根据图像尺寸 grid_h, grid_w 计算插值所需的索引和权重
    const int num_patches = grid_h * grid_w;
    auto idx_tensor = Express::_Input({4, num_patches}, ...);
    auto weight_tensor = Express::_Input({4, num_patches}, ...);
    // ... 循环计算每个 patch 的4个插值点索引和权重 ...
    // ... Reshape 和 Permute 操作以匹配模型输入维度 ...

    // 将计算好的张量作为额外输入
    moduleInputs.push_back(idx_tensor);
    moduleInputs.push_back(weight_tensor);
}

适配 DeepStack 特征

解决方案: 我们为语言模型的 forward 函数增加了 deepstack_embeds 输入,并在 C++ 端实现了特征的正确收集与传递。

Python 端修改:

# LLM 的 forward 函数修改
def forward(self, ..., deepstack_embeds: torch.Tensor = None):
    ...
    for i in range(len(self.blocks)):
        # ... transformer block 计算 ...
        hidden_states, kv = self.blocks[i](...)

        # 在指定层注入 deepstack 特征
        if deepstack_embeds is not None and i < deepstack_embeds.shape[0]:
            hidden_states += deepstack_embeds[i]
    ...
    return logits, ...

C++ 端实现:

修改 embedding 函数,在处理输入序列时,正确地收集 deepstack 特征,并将其作为语言模型的一个额外输入。

// 在 embedding 函数中处理多模态输入
VARP Omni::embedding(const std::vector<int>& input_ids) {
    std::vector<VARP> deepstacks;
    bool hasDeepStack = !mDeepStackEmbeddings.empty();

    for (int id : input_ids) {
        if (id == mVisionPad) { // 遇到图像占位符
            if (hasDeepStack) {
                deepstacks.push_back(mDeepStackEmbeddings[vision_idx]);
            }
        }
    }
    // 将收集到的 deepstack 特征拼接成一个 tensor
    if (hasDeepStack) {
        mExtraArgs[0] = Express::_Concat(deepstacks, 1);
    }
    return ...;
}

MoE 模型导出

解决方案: 在导出前动态重构专家层使其与之前的Qwen3-MoE架构保持一致。我们在模型加载阶段实现了一个适配器,它能检测到 Qwen3-VL-MoE 的特殊结构,并动态地将其重构为标准的 ModuleList 形式。

适配逻辑精简如下:

# 在 Mlp 模块的初始化函数中进行适配
class Mlp(torch.nn.Module):
    def __init__(self, ...):
        # ...
        is_qwen3_vl_moe = not isinstance(self.experts, torch.nn.ModuleList)
        if is_qwen3_vl_moe:
            original_experts = self.experts
            new_experts_list = torch.nn.ModuleList()
            for i in range(self.num_experts):
                # 1. 实例化一个标准的 Expert 模块
                expert_mlp = Qwen3Expert(...)
                # 2. 从原始打包的权重中切片并赋值
                expert_mlp.gate_up_proj_linear.weight.data = ...
                expert_mlp.down_proj_linear.weight.data = ...
                new_experts_list.append(expert_mlp)
            # 3. 用重构后的标准 ModuleList 替换原有 experts
            self.experts = new_experts_list

通过在 Python 端进行这次“预处理”,导出的 ONNX 模型拥有了完全一致的 MoE 结构,极大地简化了 MNN 在 C++ 端的推理实现。

模型下载

我们已经将转换好的 MNN 模型上传至社区,欢迎下载体验:




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • 一图读懂Qwen
  • 端侧LLM硬件系列(二):内存容量
  • Qwen3-Next:下一代MoE模型架构解析
  • MNN模型支持:面壁小钢炮MiniCPM-V-4
  • 端侧LLM硬件系列(一):内存带宽