Prhub

#49975 [Bugfix][Multimodal] Include media IO config in MM cache hash

原始 PR 作者 guan404ming 合并时间 2026-07-29 18:48 文件变更 5 提交数 2 评论 4 代码增减 +54 / -3

执行摘要

修复多模态缓存哈希忽略 media IO 配置导致的碰撞

PR body 指出:Image hash ignores decode config, so same bytes + different image_mode / rgba_background_color collide, returning another request's cached features。需要将影响解码结果的 IO 配置纳入缓存哈希计算,避免碰撞。

此 PR 修复了多模态缓存的关键一致性问题,设计上采用“仅在改变时附加”的轻量方案,避免对未使用 IO 配置的路径引入开销。建议合并,并关注后续 io_config 在其他模态(如视频)的扩展。

讨论亮点

review 中仅有一个实质性讨论:DarkLight1337 要求将测试文件中的内联 import(from io import BytesIO)移到文件顶部,提交者 guan404ming 在下一个 commit 中修复。此外 mergify 提示存在合并冲突(后已解决)。

实现拆解

  1. 数据模型扩展:在 MediaWithBytesvllm/multimodal/media/base.py)新增可选的 io_config: dict[str, Any] | None 字段,用于记录解码过程中改变媒体内容的配置项。
  2. IO 路径改造:在 ImageMediaIO.load_bytesvllm/multimodal/media/image.py)中,先调用 _convert_image_mode 得到转换后的图片,然后判断转换结果是否与原图不同;若不同,则将 image_modergba_background_color 作为 io_config 附加到返回的 MediaWithBytes 中;若未发生转换则 io_configNone
  3. 哈希逻辑适配:在 MultiModalHasher.serialize_itemvllm/multimodal/hasher.py)中,处理 MediaWithBytes 且内容为 Image.Image 时,先检查是否存在 io_config;若存在,则将 io_config 与原始字节一同纳入哈希值计算,否则保持原有行为。
  4. 调用路径统一:在 inputs.pyget_mm_hashes 中,将 data_items 的遍历从直接 for item in data_items 改为 for item in data_items.get_all_items_for_hash(),确保所有项目都经过统一的哈希条目提取。
  5. 测试覆盖:新增两个测试函数 test_hash_collision_media_io_configtest_hash_media_io_noop_config_preserves_hash,分别验证不同背景颜色产生不同哈希、以及未改变图片的 IO 配置不改变哈希值。
文件 模块 状态 重要度
vllm/multimodal/media/base.py 多模态 modified 4.99
vllm/multimodal/media/image.py 多模态 modified 5.94
vllm/multimodal/hasher.py 多模态 modified 5.53
tests/multimodal/test_hasher.py 哈希器 modified 5.85
vllm/multimodal/processing/inputs.py 多模态 modified 3.92

关键符号

ImageMediaIO.load_bytes MultiModalHasher.serialize_item MediaWithBytes.__init__ _rgba_png_bytes test_hash_collision_media_io_config test_hash_media_io_noop_config_preserves_hash

关键源码片段

vllm/multimodal/media/base.py data-contract

核心数据模型 `MediaWithBytes` 新增 `io_config` 字段,是哈希键扩展的基础。

from dataclasses import dataclass, field
from typing import Any, Generic, TypeVar_T = TypeVar("_T")@dataclass
class MediaWithBytes(Generic[_T]):
    """
    Wrapper that couples a media object with its original encoded bytes.
    The `io_config` field records decode settings that altered the media,
    enabling the hasher to differentiate between otherwise identical bytes
    that were decoded with different parameters.
    """
    media: _T
    original_bytes: bytes = field(repr=False)
    # `io_config` is None if decode settings did NOT change the media;
    # otherwise it carries the config dict (e.g. {"image_mode": ..., "rgba_background_color": ...})
    # so that the hash key reflects the effective media content.
    io_config: dict[str, Any] | None = None
vllm/multimodal/media/image.py core-logic

核心业务逻辑 `load_bytes` 在图片被转换时构造 `io_config`,是哈希区分的关键。

def load_bytes(self, data: bytes) -> MediaWithBytes[Image.Image]:
    try:
        image = Image.open(BytesIO(data))
        # ... pixel limit check and normalization ...
        image = normalize_image(image)
        image.load()
        # Apply configured mode conversion (e.g. RGBA → RGB with background)
        converted = self._convert_image_mode(image)
    except (OSError, Image.UnidentifiedImageError) as e:
        raise ValueError(f"Failed to load image: {e}") from e
​
    # If conversion changed the image, record the IO config that caused it
    io_config = None
    if converted is not image:
        io_config = {
            "image_mode": self.image_mode,
            "rgba_background_color": self.rgba_background_color,
        }
    # Return the converted media with the config attached
    return MediaWithBytes(converted, data, io_config)
vllm/multimodal/hasher.py core-logic

哈希序列化逻辑新增分支,将 `io_config` 纳入哈希计算。

@classmethod
def serialize_item(cls, obj: object) -> Iterable[bytes | memoryview]:
    # ... existing cases ...
​
    if isinstance(obj, MediaWithBytes) and isinstance(obj.media, Image.Image):
        exif = obj.media.getexif()
        if Image.ExifTags.Base.ImageID in exif and isinstance(
            exif[Image.ExifTags.Base.ImageID], uuid.UUID
        ):
            return (exif[Image.ExifTags.Base.ImageID].bytes,)
​
        # NEW: incorporate IO config into hash if present
        if obj.io_config:
            return cls.iter_item_to_bytes(
                "image",
                {"io_config": obj.io_config, "data": obj.original_bytes},
            )
        # Fallback: only original bytes
        return cls.iter_item_to_bytes("image", obj.original_bytes)
​
    # ... other modalities ...

评论区精华

测试文件 import 放置位置 style

DarkLight1337 在 review 评论中要求将 `from io import BytesIO` 等 import 移到文件顶部,而不是在函数内部导入。

结论:提交者 guan404ming 在下一个 commit 中将 import 移到文件顶部,符合 PEP8 规范。 · 已解决

风险与影响

变更核心逻辑涉及哈希键构成,但仅在 IO 配置实际改变图像时附加字段,且与原有路径保持兼容(io_config 默认为 None,哈希行为不变)。风险较低。注意 get_all_items_for_hash() 路径替换需确认所有调用点行为一致,避免遗漏某些媒体项。

影响所有使用多模态缓存(特别是图片)的场景。修复前,不同 IO 配置会导致缓存误命中;修复后,哈希正确区分配置,缓存命中更准确,但同一配置下的缓存分享不受影响。用户无需修改代码即可受益。

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论