Prhub

#54994 [Bugfix][Multimodal] Handle prefix-covered items in SHM worker cache

原始 PR 作者 waizuichougou 合并时间 2026-09-03 14:39 文件变更 4 提交数 7 评论 15 代码增减 +115 / -4

执行摘要

修复前缀缓存与 SHM 多模态缓存同时启用时,第二个相同请求导致 EngineCore 崩溃的问题。

防止同时启用前缀缓存和 SHM 多模态缓存时,重复请求导致 EngineCore 崩溃。PR body 中明确描述了生产触发链:'second request with the same image and prompt -> prefix cache covers the image placeholder range -> NewRequestData.from_request() -> strip_covered_mm_data() replaces the feature payload with data=None -> worker_base._apply_mm_cache() -> ShmObjectStoreReceiverCache.get_and_update_features() -> touch_receiver_cache_item() asserts that data is not None -> EngineCore terminates'。此问题由 #52041 引入的优化引发,该优化旨在避免广播前缀缓存完全覆盖的多模态项的张量负载,但未考虑 SHM 缓存命中项的地址描述符同样需要到达工作端以完成引用计数。

该 PR 值得精读,因为它修复了一个会导致服务崩溃的关键 bug,并且其修改涉及缓存系统不同层次(调度器、工作进程 SHM 缓存)的精细交互。关注点在于:

1) strip_covered_mm_data() 如何区分对待普通张量数据和 SHM 地址描述符;
2) ShmObjectStoreReceiverCache.get_and_update_features() 的过滤逻辑如何确保引用计数正确。修复逻辑清晰,测试覆盖充分(单元测试 + 集成场景测试),是处理缓存一致性问题的一个优秀范例。

讨论亮点

维护者 njhill 推送了小改动并添加了测试(njhill: Thanks for the fix @waizuichougou! I pushed a couple of minor changes, including adding some tests.)。贡献者 waizuichougou 因多次重复 rebase 以解决 CI 问题而被提醒停止(njhill: @waizuichougou please stop rebasing, thanks!),随后贡献者就此道歉并澄清了误解。此交互表明,对于由上游问题导致的 CI 失败,等待上游修复或重试失败任务是更佳实践,而非频繁 rebase。

实现拆解

  1. 入口变更:在 vllm/multimodal/utils.py 的 strip_covered_mm_data() 函数中,增加对 MultiModalFeatureSpec.data 内是否包含 'address' 键的判断。若包含(即为 SHM 地址描述符),则保留该项,即使其占位符范围已被前缀缓存完全覆盖。此改动确保了 SHM 接收器所需的关键元数据不会被剥离。
  2. 核心逻辑适配:在 vllm/multimodal/cache.py 的 ShmObjectStoreReceiverCache 类中,重写 get_and_update_features() 方法。该方法现在先过滤掉 data is None 的特征项(这些是已被剥离的、非 SHM 缓存命中的普通张量负载),再调用父类方法处理剩余的、携带有效数据或 SHM 地址描述符的项。这避免了 SHM 接收器在后续 touch_receiver_cache_item() 中对 data=None 项执行断言失败。
  3. 回归测试配套:在 tests/multimodal/test_cache.py 中新增 test_shm_receiver_handles_prefix_covered_items 测试,模拟完整的“发送者缓存命中 -> 调度器剥离覆盖数据 -> 接收器处理”的流程,验证修复后地址描述符被正确解析、引用被确认,且对象最终可被回收。在 tests/v1/core/test_output.py 中新增 test_strip_covered_mm_data_shm_address_item 单元测试,直接验证 strip_covered_mm_data() 函数对包含 'address' 键的数据项予以保留。
文件 模块 状态 重要度
vllm/multimodal/utils.py 多模态处理 modified 6.57
vllm/multimodal/cache.py 多模态缓存 modified 6.65
tests/multimodal/test_cache.py 缓存测试 modified 6.35
tests/v1/core/test_output.py 输出处理测试 modified 5.55

关键符号

strip_covered_mm_data get_and_update_features

关键源码片段

vllm/multimodal/utils.py core-logic

核心修复逻辑所在:修改 `strip_covered_mm_data()` 函数,在剥离前缀覆盖数据时保留 SHM 地址描述符。

def strip_covered_mm_data(
    mm_features: list[MultiModalFeatureSpec],
    num_computed_tokens: int,
    uses_mrope: bool = False,
    uses_xdrope: bool = False,
) -> list[MultiModalFeatureSpec]:
    """Drop the tensor data of mm items whose placeholder span is fully inside
    a prefix-cache-covered region: no encoder run can be scheduled for them,
    so the workers never consume the payload fields. M-RoPE and XD-RoPE models
    keep CPU-side metadata fields used to compute positions. SHM address items
    are also kept so workers can balance the sender's reference count. The
    scheduler-side ``Request`` keeps the full features."""
    if not mm_features or num_computed_tokens == 0:
        return mm_features
​
    def maybe_strip(f: MultiModalFeatureSpec) -> MultiModalFeatureSpec:
        if (
            f.data is None
            # SHM address items must reach the worker so it can acknowledge
            # the sender's reference count before the item is evicted. This
            # mirrors how the SHM receiver identifies them ("address" in item).
            or "address" in f.data
            or (f.mm_position.offset + f.mm_position.length > num_computed_tokens)
        ):
            return f
​
        data = None
        if uses_mrope or uses_xdrope:
            data = MultiModalKwargsItem(
                {k: elem for k, elem in f.data.items() if elem.field.keep_on_cpu}
            )
        return replace(f, data=data)
​
    return [maybe_strip(f) for f in mm_features]
vllm/multimodal/cache.py core-logic

适配 SHM 接收器缓存:重写 `get_and_update_features()` 方法,过滤掉已被剥离(data=None)的项,避免对它们执行引用确认。

@override
def get_and_update_features(
    self, mm_features: list["MultiModalFeatureSpec"]
) -> list["MultiModalFeatureSpec"]:
    # strip_covered_mm_data preserves address items, so None here represents
    # a stripped uncached payload and has no SHM reference to acknowledge.
    features_with_data = [
        feature for feature in mm_features if feature.data is not None
    ]
    super().get_and_update_features(features_with_data)
    return mm_features

评论区精华

修复前缀缓存与 SHM 多模态缓存冲突导致的崩溃 正确性

核心问题是两个缓存机制(前缀缓存优化与 SHM 引用计数协议)在交互时产生了未处理的情况。优化侧剥离了数据,但协议侧需要数据(地址)来完成确认。修复在剥离侧增加了感知,在处理侧增加了过滤。

结论:通过 `strip_covered_mm_data()` 保留地址项,并让 SHM 接收器跳过 `data=None` 项,修复了崩溃,同时保持了引用计数平衡。 · 已解决

贡献者 CI 操作规范 other

贡献者为解决 CI 失败多次 rebase 分支,被维护者 njhill 提醒停止。贡献者随后道歉并澄清,误解为需 rebase 获取上游修复,而正确做法是等待上游修复或重试失败任务。

结论:达成了共识:对于上游问题导致的 CI 失败,不应频繁 rebase,应等待或重试。这体现了对 CI 流程和协作规范的理解。 · 已解决

风险与影响

回归风险:本次修改核心路径(strip_covered_mm_data 和 SHM 接收器缓存)是多模态处理与缓存子系统的交互点。尽管测试覆盖了主要场景(完全覆盖、部分覆盖、SHM 地址项),但对更复杂的混合请求模式(如并发流、不同模态交错)的测试依赖端到端验证,理论上存在引入细微回归的可能。
缓存引用计数失衡风险:修复旨在维持 SHM 缓存的引用计数平衡。核心逻辑是确保每个缓存命中(生成地址描述符)都能通过工作端的 touch 或 get 操作得到确认,从而使对象最终可被回收。如果过滤逻辑(features_with_data)或保留逻辑('address' in f.data)存在疏漏,可能导致引用泄漏(内存无法回收)或过早释放(缓存失效)。修复后的测试已验证引用可被正常回收。

用户影响:直接解决了在启用 --enable-prefix-caching 和 --mm-processor-cache-type shm 时,重复提交相同图像和提示导致服务中断(HTTP 500)的可用性问题,修复了生产环境中的一个关键故障。
系统影响:影响 vllm/multimodal 模块下的缓存管理(cache.py, utils.py)。确保了 SHM 缓存与前缀缓存优化(#52041)的兼容性,维持了缓存性能优势(避免不必要的数据传输)同时保证了系统稳定性。修复被纳入 v0.29.0 cherry picks,表明其影响级别。
团队影响:提供了一个关于缓存子系统交互的典型案例,强调了在优化路径(如数据剥离)时必须考虑所有下游消费方的约束(如 SHM 引用计数协议)。

核心路径变更 缓存引用计数平衡 跨组件交互修复

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论