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 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!