# PR #39888 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[Model] Use mm_features to compute mrope positions for PaddleOCR-VL
- 合并时间：2026-04-16 21:14
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/39888

---

# 执行摘要
本 PR 重构了 PaddleOCR-VL 模型的多模态旋转位置编码（M-RoPE）输入位置计算，从原有的基于 token 搜索改为使用 mm_features 驱动，提高了效率和代码清晰度。作为系统性重构的一部分，它新增了迭代器辅助方法并添加了全面测试，对模型性能有正向影响，且与 Qwen2.5-VL 等实现对齐。

# 功能与动机
**为什么做**：根据 issue #32656，原有 M-RoPE 计算通过搜索 input_tokens 来定位图像 / 视频 token，这种方法效率低下且可能与 mm_features 中的预期位置不对齐。目标是在所有支持 SupportsMRoPE 接口的模型中统一使用 mm_features.mm_position，以提升计算性能和一致性。PR body 提到这是跟进任务，并展示了重构后性能改善（CPU 时间减少）。

# 实现拆解
## 1. 新增迭代器方法 `iter_mm_grid_thw`
在 `vllm/model_executor/models/paddleocr_vl.py` 中，新增此方法用于从 mm_features 提取网格信息：
```python
def iter_mm_grid_thw(self, mm_features: list[MultiModalFeatureSpec]) -> Iterator[tuple[int, int, int, int, float]]:
    """
    迭代多模态特征，生成偏移、网格维度（t, h, w）和时间因子。
    排序确保处理顺序，并处理图像和视频模态，图像帧数断言为1，视频计算时间缩放。
    """
    spatial_merge_size = self.config.vision_config.spatial_merge_size
    tokens_per_second = getattr(self.config.vision_config, "tokens_per_second", 1.0)
    for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):
        offset = mm_feature.mm_position.offset
        if mm_feature.modality == "image":
            t, h, w = mm_feature.data["image_grid_thw"].data.tolist()
            assert t == 1, f"Image must have 1 frame, got {t}"
            yield offset, 1, h // spatial_merge_size, w // spatial_merge_size, 1.0
        elif mm_feature.modality == "video":
            t, h, w = mm_feature.data["video_grid_thw"].data.tolist()
            second_per_grid_ts = 1.0
            if mm_feature.data.get("second_per_grid_ts", None):
                second_per_grid_ts = mm_feature.data["second_per_grid_ts"].data.item()
            t_factor = second_per_grid_ts * tokens_per_second
            yield offset, t, h // spatial_merge_size, w // spatial_merge_size, t_factor
        else:
            raise ValueError(f"Unsupported modality: {mm_feature.modality}")
```

## 2. 重构核心位置计算 `get_mrope_input_positions`
在同一文件中，利用 `iter_mm_grid_thw` 简化逻辑：
- 移除原有复杂搜索和统计代码（约 94 行删除）。
- 直接迭代 mm_features，计算文本和网格位置，使用 numpy 广播生成位置张量。
- 返回位置张量和 delta 值，与原有接口兼容。

## 3. 测试配套
新增文件 `tests/model_executor/test_paddleocr_vl_mrope.py`，包含：
- **fixture**：`_force_cpu_default_device` 确保测试在 CPU 上运行。
- **虚拟配置**：`DummyConfig` 和 `DummyVisionConfig` 模拟模型配置。
- **辅助函数**：`make_model` 和 `make_mm_feature` 构建测试对象。
- **三个测试用例**：
 1. `test_get_mrope_input_positions_text_only`：验证纯文本输入。
 2. `test_get_mrope_input_positions_single_image`：验证单图像场景。
 3. `test_get_mrope_input_positions_multiple_images`：验证多图像场景。
测试覆盖了偏移计算、网格生成和 delta 校验，确保重构后输出与预期一致。

## 4. 性能与对齐
- 通过 profiler 截图显示 CPU 时间减少，验证性能提升。
- 实现与 Qwen2.5-VL 类似，使用 `torch.from_numpy` 等调整，确保跨模型一致性。



## 关键源码片段

### `vllm/model_executor/models/paddleocr_vl.py`

源码主文件，包含 M-RoPE 位置计算的核心重构，新增 iter_mm_grid_thw 方法并简化 get_mrope_input_positions。

```python
def iter_mm_grid_thw(
    self, mm_features: list[MultiModalFeatureSpec]
) -> Iterator[tuple[int, int, int, int, float]]:
    """
    迭代多模态特征并生成网格信息。
    
    参数:
        mm_features: 多模态特征规范列表
    
    生成:
        每个帧/图像的 (偏移, 网格_t, 网格_h, 网格_w, t_factor) 元组，其中偏移来自mm_position，网格维度根据spatial_merge_size调整，t_factor用于视频时间缩放。
    """
    spatial_merge_size = self.config.vision_config.spatial_merge_size
    tokens_per_second = getattr(self.config.vision_config, "tokens_per_second", 1.0)
    for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):  # 按偏移排序确保顺序
        offset = mm_feature.mm_position.offset
        if mm_feature.modality == "image":
            t, h, w = mm_feature.data["image_grid_thw"].data.tolist()  # 提取图像网格维度
            assert t == 1, f"Image must have 1 frame, got {t}"  # 图像帧数必须为1
            yield offset, 1, h // spatial_merge_size, w // spatial_merge_size, 1.0  # 返回处理后的网格信息
        elif mm_feature.modality == "video":
            t, h, w = mm_feature.data["video_grid_thw"].data.tolist()  # 提取视频网格维度
            second_per_grid_ts = 1.0
            if mm_feature.data.get("second_per_grid_ts", None):  # 检查是否有时间缩放因子
                second_per_grid_ts = mm_feature.data["second_per_grid_ts"].data.item()
            t_factor = second_per_grid_ts * tokens_per_second  # 计算时间因子
            yield (
                offset,
                t,
                h // spatial_merge_size,
                w // spatial_merge_size,
                t_factor,
            )
        else:
            raise ValueError(f"Unsupported modality: {mm_feature.modality}")  # 处理未知模态
```

# 评论区精华
review 中 gemini-code-assist[bot] 指出两个关键问题：
> “The `mm_feature.data` attribute can be `None` if the multimodal item is retrieved from the cache... Accessing `mm_feature.data["image_grid_thw"]` without a null check will cause a `TypeError`.”
> “If `text_len` is 0... `torch.arange(0)` will produce an empty tensor... `max()` is not defined for empty tensors.”

作者根据 DarkLight1337 建议调整实现更类似 Qwen2.5-VL，最终 DarkLight1337 批准并确认精度无误，表明问题已解决或风险可控。

# 风险与影响
**技术风险**：
- 缓存场景下 `mm_feature.data` 可能为 None，未处理会导致运行时 TypeError（review 指出，但提交历史未明确修复）。
- 文本长度为零时空张量操作崩溃（review 建议添加保护）。
**影响分析**：
- 用户端：PaddleOCR-VL 模型推理更高效，M-RoPE 计算准确。
- 系统端：作为统一重构的一部分，提升多模态模型接口一致性，便于后续维护和扩展。
- 团队端：为其他模型类似重构提供参考模式。

# 关联脉络
本 PR 是 issue #32656 系统性重构的组成部分，与历史 PR 如 #39869（Keye-VL）和 #39753 共享相同动机和设计模式。这表明 vLLM 项目正推进多模态模型接口标准化，使用 mm_features 替代低效搜索，以提升整体性能和代码可维护性。