VLLM 处理多模态请求的核心流程在 BaseMultiModalProcessor.apply() 方法中(第 1810-1870 行):
python展开代码def apply(
    self,
    prompt: Union[str, list[int]],
    mm_data: MultiModalDataDict,
    hf_processor_mm_kwargs: Mapping[str, object],
    tokenization_kwargs: Optional[Mapping[str, object]] = None,
    return_mm_hashes: bool = False,
) -> MultiModalInputs:
    """
    处理多模态输入的主要步骤:
    1. 对提示文本和多模态数据一起应用 HF Processor
    2. 在 token IDs 中查找并更新序列,用占位符 token 替换
    3. 从处理后的 token IDs 中提取占位符 token 的信息
    """
在 Qwen2VLMultiModalProcessor._get_prompt_updates() 方法中(第 1040-1075 行):
python展开代码def _get_prompt_updates(
    self,
    mm_items: MultiModalDataItems,
    hf_processor_mm_kwargs: Mapping[str, Any],
    out_mm_kwargs: MultiModalKwargs,
) -> Sequence[PromptUpdate]:
    hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
    image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
    tokenizer = self.info.get_tokenizer()
    vocab = tokenizer.get_vocab()
    placeholder = {
        "image": vocab[hf_processor.image_token],  # <|image_pad|> 的 token ID
        "video": vocab[hf_processor.video_token],  # <|video_pad|> 的 token ID
    }
    merge_length = image_processor.merge_size**2
    def get_replacement_qwen2vl(item_idx: int, modality: str):
        grid_thw = out_mm_kwargs[f"{modality}_grid_thw"][item_idx]
        assert isinstance(grid_thw, torch.Tensor)
        # 计算每个图片/视频需要的 token 数量
        num_tokens = int(grid_thw.prod()) // merge_length
        return [placeholder[modality]] * num_tokens
    return [
        PromptReplacement(
            modality=modality,
            target=[placeholder[modality]],  # 要替换的目标 token
            replacement=partial(get_replacement_qwen2vl, modality=modality),
        ) for modality in ("image", "video")
    ]
VLLM 使用 PromptReplacement 策略来处理图片 token 的插入:
<|image_pad|> token 替换为多个相同的 tokengrid_thw(网格尺寸)和 merge_size 计算需要的 token 数量num_tokens = (grid_t * grid_h * grid_w) // merge_length在 _apply_prompt_updates() 方法中(第 1654-1725 行):
python展开代码def _apply_prompt_updates(
    self,
    token_ids: list[int],
    mm_prompt_updates: Mapping[str, Sequence[BoundPromptUpdate]],
    mm_item_counts: Mapping[str, int],
) -> tuple[list[int], str, Mapping[str, list[PlaceholderFeaturesInfo]]]:
    tokenizer = self.info.get_tokenizer()
    # 1. 查找 token 匹配
    mm_token_matches = {
        modality: find_token_matches(token_ids, updates)
        for modality, updates in mm_prompt_updates.items()
    }
    
    # 2. 应用 token 匹配
    if all(mm_match_counts.get(modality, 0) >= item_count
           for modality, item_count in mm_item_counts.items()):
        token_ids = self._apply_token_matches(
            token_ids,
            mm_token_matches,
            mm_item_counts,
        )
在 find_token_matches() 函数中(第 669-694 行):
python展开代码def find_token_matches(
    prompt: list[int],
    prompt_updates: Sequence[BoundPromptUpdate],
) -> Sequence[PromptTargetMatch]:
    """在 prompt 中查找所有匹配的 token 序列"""
    matches = []
    for update in prompt_updates:
        target = update.target
        if isinstance(target, _BoundPromptSequence):
            target_ids = target.token_ids
            for match in iter_token_matches(prompt, target_ids):
                matches.append(_PromptTargetTokenMatch(
                    modality=update.modality,
                    start_idx=match.start_idx,
                    end_idx=match.end_idx,
                ))
    return matches
在 apply_token_matches() 函数中(第 794-803 行):
python展开代码def apply_token_matches(
    prompt: list[int],
    mm_matches: Mapping[str, Sequence[PromptTargetMatch]],
    mm_item_counts: Mapping[str, int],
) -> list[int]:
    """应用 mm_matches 中的更新到 prompt"""
    if not mm_matches:
        return prompt
    token_id_seqs = _apply_matches(prompt, mm_matches, mm_item_counts)
    return flatten_2d_lists(token_id_seqs)
客户端发送的 OpenAI 兼容格式:
json展开代码{
  "messages": [
    {
      "role": "user",
      "content": [
        {"type": "text", "text": "请描述这张图片:"},
        {"type": "image", "image": "base64_encoded_image"},
        {"type": "text", "text": "请详细说明。"}
      ]
    }
  ]
}
content 数组中提取图片数据<|image_pad|> token<|image_pad|> token 替换为多个相同的 token<|image_pad|> token 在文本中的位置进行替换num_tokens = (grid_t * grid_h * grid_w) // merge_length<|image_pad|> token 的出现顺序一致VLLM 在处理 Qwen2VL 的多模态请求时:
<|image_pad|> token 替换为相应数量的相同 token这种设计使得 VLLM 能够灵活处理各种复杂的多模态输入场景,同时保持与训练时的一致性。


本文作者:Dong
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!