Prhub

#26101 [VLM] accept precomputed multimodal metadata

原始 PR 作者 mickqian 合并时间 2026-05-24 15:43 文件变更 2 提交数 4 评论 3 代码增减 +69 / -33

执行摘要

支持接收预计算的 VLM 元数据以减少重复计算

优化 VLM 请求的处理性能,通过允许下游传递已经计算好的哈希、偏移和 MRoPE 位置,避免在 serving 路径中重复进行昂贵的 tokenization 和位置编码计算。该需求来自多模态性能优化系列工作,与 PR #26116、#26117 等一脉相承。

值得精读以了解多模态处理器如何扩展支持预计算元数据,以及如何统一处理器输出获取方式。但建议在合并后立即修复 pad_value 嵌套问题,并补充对预计算路径的测试覆盖。

讨论亮点

仅有一条来自 gemini-code-assist[bot] 的 review:指出在 base_processor.pypad_value 的提取被嵌套在 hash_value 的检查分支内部,导致如果未提供 hash,则 pad_value 会被忽略。建议将 pad_value 提到与 hash 同级的判断。作者未对此回复或修改,PR 已合并,该问题仍未解决。

实现拆解

  1. 基础处理器:在 collect_mm_items_from_processor_output 方法中,增加对预计算元数据字段(hashoffsetspad_valuemodality)的识别和提取。新增统一的 get_data_value 获取器,支持 dict 和 object 两种输入。当生成的 MultimodalDataItem 数量为 1 时,将元数据从 tensor 转为 python 类型并赋值到 item 上。
  2. Qwen-VL 处理器:简化 _get_processor_output_value 为一行,统一使用该函数获取处理器输出,替代直接属性访问。增强 _get_precomputed_mrope_from_output 对 mrope_position_delta shape 的兼容性(支持 ndim<=1 后 reshape)。在 process_mm_data_async 中用 _get_processor_output_value 替代 hasattr 模式,使 image_grid_thw 和 video_grid_thw 的获取逻辑一致,并优化 mrope_positions squeeze 的条件判断。
  3. 整体:未新增测试文件,但修改的函数均属于核心流程,依赖现有 VLM 测试覆盖。
文件 模块 状态 重要度
python/sglang/srt/multimodal/processors/base_processor.py 多模态处理器 modified 7.04
python/sglang/srt/multimodal/processors/qwen_vl.py 多模态处理器 modified 6.23

关键符号

collect_mm_items_from_processor_output _get_processor_output_value _get_precomputed_mrope_from_output process_mm_data_async

关键源码片段

python/sglang/srt/multimodal/processors/base_processor.py core-logic

核心改动:扩展 `collect_mm_items_from_processor_output` 以接受预计算 metadata,并增加统一的 `get_data_value` 获取器和元数据字段提取逻辑。

def collect_mm_items_from_processor_output(self, data_dict, modality=None):
    # 统一获取器:兼容 dict 和 object
    get_data_value = (
        data_dict.get
        if hasattr(data_dict, 'get')
        else lambda name, default=None: getattr(data_dict, name, default)
    )
    # 显式 modality 处理
    explicit_modality = modality or (
        modality_value
        if isinstance(modality_value := get_data_value('modality'), Modality)
        else Modality.from_str(str(modality_value))
        if modality_value is not None else None
    )
    items = {}
    for attr_name, value in data_dict.items():
        # 跳过元数据字段,后续独立处理
        if attr_name in ('input_ids', 'format', 'modality', 'hash', 'pad_value', 'offsets'):
            continue
        current_modality = explicit_modality or self.ATTR_NAME_TO_MODALITY.get(attr_name)
        if attr_name == 'precomputed_embeddings':
            current_modality = current_modality or Modality.IMAGE
        if current_modality:
            item = items.setdefault(current_modality, MultimodalDataItem(modality=current_modality))
            item.set(self.FEATURE_NAMES.get(attr_name, attr_name), value)
​
    # 当仅有一个 modality 时,将元数据附加到该 item
    if len(items) == 1:
        item = next(iter(items.values()))
        offsets = get_data_value('offsets')
        if offsets is not None:
            if isinstance(offsets, torch.Tensor):
                offsets = offsets.detach().cpu().tolist()
            # 转换为 (int, int) 列表
            item.offsets = [(int(s), int(e)) for s, e in offsets]
        hash_value = get_data_value('hash')
        if hash_value is not None:
            if isinstance(hash_value, torch.Tensor):
                hash_value = hash_value.item()
            item.hash = int(hash_value)
            # 注意:pad_value 提取位于 hash 分支内,若未提供 hash 则 pad_value 被忽略(潜在 bug)
            pad_value = get_data_value('pad_value')
            if pad_value is not None:
                if isinstance(pad_value, torch.Tensor):
                    pad_value = pad_value.item()
                item.pad_value = int(pad_value)
    return list(items.values())
python/sglang/srt/multimodal/processors/qwen_vl.py core-logic

Qwen-VL 特定改动:统一处理器输出获取方式,增强预计算 MRoPE 解析兼容性,精简冗余代码。

@staticmethod
def _get_processor_output_value(ret, key):
    # 统一获取处理器输出,支持 dict 和 object
    return ret.get(key) if hasattr(ret, 'get') else getattr(ret, key, None)def _get_precomputed_mrope_from_output(self, ret):
    # 从预计算输出中提取 MRoPE 位置,兼容多种 shape
    mrope_positions = self._get_processor_output_value(ret, 'mrope_positions')
    mrope_position_delta = self._get_processor_output_value(ret, 'mrope_position_delta')
    if mrope_positions is None or mrope_position_delta is None:
        return None
    mrope_positions = torch.as_tensor(mrope_positions)
    if mrope_positions.ndim == 3:
        if mrope_positions.shape[1] != 1:
            return None
        mrope_positions = mrope_positions.squeeze(1)
    if mrope_positions.ndim != 2 or mrope_positions.shape[0] != 3:
        return None
    mrope_position_delta = torch.as_tensor(mrope_position_delta)
    # 原用 if ndim==0 then reshape(1,1) elif ndim==1 then reshape(-1,1)
    # 简化:对 ndim<=1 统一 reshape
    if mrope_position_delta.ndim <= 1:
        mrope_position_delta = mrope_position_delta.reshape(-1, 1)
    return mrope_positions, mrope_position_delta

评论区精华

pad_value 提取嵌套在 hash_value 内可能被忽略 正确性

gemini-code-assist[bot] 指出 `pad_value` 检查位于 `hash_value` 条件分支内部,若 `data_dict` 未提供 `hash`,则 `pad_value` 不会被处理,可能导致功能异常。建议将 `pad_value` 提升到与 `hash` 同级。

结论:作者未修改代码,PR 已合并,该问题仍存在于代码中。 · 待处理

风险与影响

  1. pad_value 丢失风险pad_value 提取仍在 hash_value 条件内,若用户只传 pad_value 不传 hash,则 pad_value 不会生效,可能导致缓存或注意力掩码计算异常。
  2. 预计算格式兼容性:依赖外部提供的元数据格式(如 offsets 必须为 [(int,int)] 形式),若格式不匹配会静默失败或异常。
  3. 缺少单元测试:新增的预计算路径和元数据提取逻辑没有对应的新增测试,回归风险依赖已有用例。

影响范围:所有使用 VLM 模型(特别是 Qwen-VL 系列)的请求处理路径,尤其是启用了预计算元数据的场景(如多轮对话或图像批量处理)。影响程度:正向优化性能,但引入的 pad_value 嵌套问题可能影响部分依赖该字段的功能。团队内需关注该潜在 bug 并尽快修复。

pad_value 提取条件不独立 预计算元数据格式兼容性未验证 缺少单元测试覆盖

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论