执行摘要
- 一句话:修复 EPD 编码实例因多余 gather 崩溃
- 推荐动作:值得精读。该 PR 展示了如何在 worker 侧而非调度器侧消除错误报告,以及如何通过拆分 encode/gather 半路径来避免无意义计算与崩溃;对 Model Runner V2 多模态路径的维护者和 EPD 部署使用者有参考价值。测试代码对 mock 契约的描述也值得借鉴。
功能与动机
PR body 指出:EPD encoder 实例一旦被调度器交给一个 EC connector 已持有的多模态项,就会在 gpu/model_runner.py:1392 → model_state.get_mm_embeddings(...) 内触发 RuntimeError: Encoder cache miss,整个实例随之崩溃,后续所有请求都得到连接错误。根因是调度器在 ec_connector.has_cache_item() 为 true 时正确跳过了该项的编码调度,但 V2 runner 在 is_encoder_only 早退之前就调用了 get_mm_embeddings(),其内部 gather 对本地 encoder cache 缺失的项抛异常,而 encoder-only 实例根本不需要这次 gather 的产物。V1 没有此 bug,因为其 producer 路径在 gather 之前就返回了。
实现拆解
- 在 vllm/v1/worker/gpu/model_states/interface.py 的 ModelState 抽象类新增 execute_mm_encoder(scheduled_encoder_inputs) 方法:内部调用 encoder_runner.prepare_mm_inputs() 得到 mm_hashes 与 mm_kwargs,执行 encoder_runner.execute_mm_encoder() 后将输出按 mm_hash 写入 encoder_cache.encoder_outputs,全程不调用 gather_mm_embeddings()。
- 在 vllm/v1/worker/gpu/model_runner.py 的 execute_model() 中,将 maybe_get_output 上下文内的 get_mm_embeddings 调用按 self.is_encoder_only 分流:encoder-only 实例只调用 execute_mm_encoder() 完成编码与发布,其余实例保持原逻辑。
- 重构 vllm/v1/worker/gpu/model_states/default.py 的 DefaultModelState.get_mm_embeddings():将原先内联的 prepare/execute/update 三行逻辑替换为一次 self.execute_mm_encoder() 调用,避免与接口新方法重复实现,语言模型实例行为不变。
- 新增回归测试:tests/v1/worker/test_encoder_runner.py 的 test_execute_mm_encoder_caches_outputs_without_gathering 与 test_execute_mm_encoder_is_a_noop_without_scheduled_items;tests/v1/core/test_scheduler.py 的参数化测试 test_encoder_input_skipped_when_connector_already_has_the_item(覆盖 ec_producer / ec_consumer),固定连接器已有缓存时调度器跳过重编码的优化。
- 附带修复 docs/governance/committers.md 列表前缺空行(markdownlint MD032),解决 #51300 引入的 pre-commit 全量失败。
关键文件:
vllm/v1/worker/gpu/model_states/interface.py(模块 模型状态;类别 source;类型 data-contract;符号 execute_mm_encoder): 定义新的 execute_mm_encoder() 接口方法,是本次修复的核心数据契约变更,提供只编码不 gather 的半路径。
vllm/v1/worker/gpu/model_runner.py(模块 模型执行;类别 source;类型 core-logic): execute_model() 中按 is_encoder_only 分流是本次修复的入口,避免 encoder-only 实例进入 gather 路径导致崩溃。
vllm/v1/worker/gpu/model_states/default.py(模块 模型状态;类别 source;类型 refactor;符号 get_mm_embeddings): 将 get_mm_embeddings() 中内联的编码逻辑重构为调用新方法,避免重复实现,保证语言模型实例行为不变。
tests/v1/worker/test_encoder_runner.py(模块 编码器测试;类别 test;类型 test-coverage;符号 test_execute_mm_encoder_caches_outputs_without_gathering, test_execute_mm_encoder_is_a_noop_without_scheduled_items): 新增 encode-only 行为回归测试,验证 execute_mm_encoder 写入缓存且不调用 gather,并覆盖无调度项时的 no-op 行为。
tests/v1/core/test_scheduler.py(模块 调度器测试;类别 test;类型 test-coverage;符号 test_encoder_input_skipped_when_connector_already_has_the_item): 参数化回归测试固定调度器在连接器已有缓存时跳过重编码的优化,防止未来破坏该行为。
docs/governance/committers.md(模块 文档;类别 docs;类型 documentation): 附带修复 #51300 引入的 markdownlint MD032 问题,保证 pre-commit 通过。
关键符号:execute_mm_encoder, get_mm_embeddings, execute_model, test_execute_mm_encoder_caches_outputs_without_gathering, test_execute_mm_encoder_is_a_noop_without_scheduled_items, test_encoder_input_skipped_when_connector_already_has_the_item
关键源码片段
vllm/v1/worker/gpu/model_states/interface.py
定义新的 execute_mm_encoder() 接口方法,是本次修复的核心数据契约变更,提供只编码不 gather 的半路径。
# vllm/v1/worker/gpu/model_states/interface.py
@abstractmethod
def get_mm_embeddings(
self,
scheduled_encoder_inputs: dict[str, list[int]],
input_batch: InputBatch,
req_states: RequestState,
) -> torch.Tensor | None:
raise NotImplementedError
def execute_mm_encoder(
self, scheduled_encoder_inputs: dict[str, list[int]]
) -> None:
"""Run the multi-modal encoder and cache its outputs by `mm_hash`.
The encode half of `get_mm_embeddings`, without the gather, for callers
that run no language model.
"""
# 与 get_mm_embeddings 不同的是,这里不调用 gather_mm_embeddings,
# 因此即使本地 encoder_cache 缺少某些项,也不会抛 `Encoder cache miss`。
# 编码输出按 mm_hash 写入缓存,供 EC connector 或下游 gather 使用。
mm_hashes, mm_kwargs = self.encoder_runner.prepare_mm_inputs(
scheduled_encoder_inputs
)
if mm_kwargs:
encoder_outputs = self.encoder_runner.execute_mm_encoder(mm_kwargs)
self.encoder_cache.encoder_outputs.update(zip(mm_hashes, encoder_outputs))
vllm/v1/worker/gpu/model_runner.py
execute_model() 中按 is_encoder_only 分流是本次修复的入口,避免 encoder-only 实例进入 gather 路径导致崩溃。
# vllm/v1/worker/gpu/model_runner.py(execute_model 内)
else:
scheduled_encoder_inputs = scheduler_output.scheduled_encoder_inputs
if self.lora_config is not None:
set_active_mm_loras(
model=self.model,
lora_manager=self.lora_manager,
encoder_cache=self.encoder_cache,
req_id_to_index=self.req_states.req_id_to_index,
lora_state=self.lora_state,
scheduled_encoder_inputs=scheduled_encoder_inputs,
)
with self.ec_connector.maybe_get_output(
scheduler_output
) as ec_connector_output:
if self.is_encoder_only:
# 编码器实例不运行语言模型,只负责编码并发布到连接器:
# 若走 get_mm_embeddings 的 gather 路径,会对连接器已持有、
# 但本地缓存缺失的项抛 `Encoder cache miss`,拖垮整个引擎。
self.model_state.execute_mm_encoder(scheduled_encoder_inputs)
else:
inputs_embeds = self.model_state.get_mm_embeddings(
scheduled_encoder_inputs, input_batch, self.req_states
)
评论区精华
Review 中有两个核心讨论点:一是 Isotr0py 建议将新方法从 encode_mm_inputs 改名为 execute_mm_encoder,因为该方法内部要调用 prepare_mm_inputs 而非直接接收 mm_inputs,作者已采纳并重命名;二是 Copilot 指出测试中 prepare_mm_inputs 的 mock 返回值形状应与真实 EncoderRunner.prepare_mm_inputs 契约一致(返回 mm_hashes 与 (modality, item) 元组列表),作者相应调整了测试 mock,使其对接口变化更敏感。
- 新方法命名 encode_mm_inputs vs execute_mm_encoder (design): 采纳建议,重命名为 execute_mm_encoder。
- 测试 mock 返回值需匹配真实契约 (testing): 作者按建议修正测试 mock 形状,最终使用 [("image", MagicMock())]。
风险与影响
- 风险:
- 核心路径变更:execute_model() 是多模态执行主路径,新增 is_encoder_only 分支可能影响未来其他 encoder-only 使用场景,但目前该实例不运行语言模型,风险受控。
- 缓存一致性依赖:修复依赖调度器跳过调度的项一定已在 EC connector 中;若连接器与本地 cache 状态不一致(如 warm storage 重启),encoder-only 实例不再崩溃,但消费者侧仍可能遇到本地 cache miss。
- 测试覆盖局限:test_scheduler.py 通过 monkeypatch has_cache_item 模拟持有状态,未覆盖真实连接器交互,建议补充 EPD 集成测试观察。
- 文档改动仅补空行,无功能风险。
- 影响:影响范围集中在 EPD(disagg_epd_proxy 多 encoder 实例)部署:修复后单个 encoder 实例的崩溃不再拖垮整个引擎,重复图像请求与 warm storage 重启场景从 500/进程退出恢复为 200。对普通 decoder/语言模型实例无行为变化。团队侧,通过新增接口与回归测试把"连接器已有缓存时跳过重编码"的调度优化固化为契约,防止后续简单修复破坏该性能优化。
- 风险标记:核心路径变更, 依赖 EC 缓存一致性, 跨组件行为耦合, 测试未覆盖真实连接器
关联脉络
- PR #51300 docs(governance): refresh committers list, add TSC note, update project leads: 本 PR 附带 commit 修复了 #51300 引入的 committers.md 列表前缺空行问题,属于跨 PR 的 pre-commit 修复联动。
- PR #50939 [Model Runner V2] Fix -1 placeholder draft token ids in rejection sam…: 同属 Model Runner V2 执行路径的 bugfix,且同涉及对 encoder/sampler 路径的边界处理,与本 PR 属于同一演进线。
- PR #51210 [ModelRunner V2] Minor indexing optimizations: 同改 vllm/v1/worker/gpu/model_runner.py,是 MRV2 执行路径性能与正确性演进的相邻 PR。
参与讨论