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 推理逻辑可以无差别地处理,这考验了我们在模型前端处理上的灵活性和工程能力。

将如此强大的模型引入 MNN,适配上述这些先进特性,是我们这次工作的核心。下面,我们将详细介绍针对这三大挑战的具体解决方案。
适配动态位置编码
解决方案: 我们将 pos_embeds 的计算拆解,把复杂的索引和权重插值计算移到计算图外(由 MNN C++ Runtime 在运行时处理),仅将纯粹的张量运算保留在 ONNX 模型内。
Python 端修改:
VisionEncoder 的 forward 函数不再自己计算插值,而是直接接收预先计算好的 idx_tensor 和 weight_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_tensor 和 weight_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: