用于可扩展多模态模型服务的编码器解耦

9 分钟阅读
多模态工作流 @ vLLM

动机:为什么要将编码器从 LMM 服务中解耦?

现代大型多模态模型 (LMMs) 引入了独特的推理瓶颈:在任何文本生成开始之前,所有图像都必须经过视觉编码器(例如 ViT)处理。该编码器阶段的计算特性与文本的预填充(Prefill)和解码(Decode)阶段截然不同。将编码器、预填充和解码运行在同一个 GPU 实例上——这是目前常见的方法——会产生根本性的效率低下。

将编码器与文本生成部署在一起的问题

1. 编码器–预填充–解码干扰

当前流水线(E+PD 在同一 GPU 上)

  • [E PD] -> [E PD] -> [E PD]
  • 所有请求必须在下一个请求开始前完成这两个阶段。
  • 编码器的工作无法与其他请求的预填充/解码重叠。

影响

  • 编码器速度慢且波动大(取决于分辨率、图像数量和复杂性)。
  • 当混合纯文本请求时,单个 LMM 输入可能会阻塞整个批次。
  • 预填充和流式解码的延迟变得抖动且不可预测。
  • 计算密集型的编码器和内存密集型的解码必须共享相同的硬件和并行策略,这对两者来说都是次优的。

2. 耦合且低效的资源分配

这三个阶段具有不同的最优配置

  • 编码器:一次性计算,计算密集型,高并行度。
  • 预填充:高内存带宽,大矩阵乘法。
  • 解码:严重受限于内存带宽,长生命周期,顺序执行。

共同部署强行在所有阶段使用统一的并行计划和资源比例,这意味着

  • 无法在不超额配置文本生成 GPU 的情况下提高编码器吞吐量。
  • 偶尔出现的多模态请求会导致巨大的成本浪费和效率降低。

解决方案:编码器解耦

将视觉编码器分离为独立的可扩展服务可带来显著收益。

1. 流水线执行与消除干扰

采用解耦后

E → P D   (Request 1)
......E → P D   (Request 2)
..........E → P D   (Request 3)
  • 请求 N 的编码器可以在请求 N–1 处于预填充或解码阶段时运行。
  • 纯文本请求完全绕过编码器,绝不会排在图像任务后面等待。
  • 消除了编码器引起的排队延迟。
  • 系统变成流水线并行,提高了吞吐量并平滑了延迟。

2. 独立的细粒度扩展

每个阶段最终都可以根据自身的需求曲线进行扩展

  • 根据多模态图像的数量扩展 编码器 GPU
  • 根据总请求率和输出长度扩展 预填充/解码 GPU

这防止了浪费

  • 无需为了应对罕见的图像峰值而购买庞大的解码集群。
  • 每个资源池都使用合适的硬件和并行策略。

3. 编码器输出缓存与复用

中心化的编码器服务天然支持跨请求缓存

  • 高频使用的图像(如徽标、图表、产品照片)的嵌入向量只需计算一次,并在不同用户/请求间复用。
  • 缓存的请求拥有 零编码器成本,直接缩短了首字延迟(TTFT)。
  • 随着缓存命中率的提高,编码器负载会显著降低。

设计

组件

代理与路由器

  • 编排请求流。
  • 将多模态 (MM) 输入发送到编码器实例。
  • 在转发原始请求(此时图像嵌入已存入远程存储)到预填充/解码 (PD) 实例之前,等待编码器完成。

数据传输层

  • 用于存储编码器产生的多模态嵌入(编码器缓存,或称 EC 嵌入)的远程存储。
  • 作为编码器工作节点与 PD 工作节点之间的共享传输媒介。

EC 连接器

  • 工作节点/调度器与数据传输层之间的桥梁。
  • 处理编码器缓存的存储与检索。

角色

  • 调度侧连接器:
    • 确定在当前调度迭代中应加载或保存哪些多媒体嵌入。
    • 生成描述下游工作节点所需缓存操作的元数据。
  • 工作节点侧连接器:
    • 执行实际的加载/保存操作(读/写远程存储)。
    • 管理每个工作节点的运行时嵌入传输。

工作流

数据流图

请求生命周期

  1. 代理接收请求

    • 从原始请求中提取多模态输入。
    • 创建 N 个编码器作业(每个 MM 输入一个),并将其分发给编码器实例。
  2. 编码器调度

    • 编码器调度器运行任务,计算嵌入。
    • 通过 EC 连接器将计算出的嵌入存储到远程存储中。
  3. 编码器完成

    • 当所有嵌入存储完毕后,编码器工作节点通知代理。
  4. 代理将请求转发给 PD 实例

    • 原始请求(带有图像哈希但不含原始像素数据)被发送到预填充/解码节点。
  5. PD 执行

    • PD 实例使用 EC 连接器从远程存储加载 MM 嵌入。
    • 正常执行预填充和解码,并将嵌入直接注入模型运行器的缓存中。

实现

核心组件

1. ECConnectorRole

定义连接器实例的运行位置

class ECConnectorRole(enum.Enum):
    SCHEDULER = 0   # in scheduler process
    WORKER = 1      # in worker process

2. ECConnectorMetadata

在调度侧和工作节点侧连接器之间共享的抽象同步/状态对象

class ECConnectorMetadata(ABC):
    pass

3. ECConnectorBase

所有连接器的抽象接口。

关键字段

  • role: 调度器或工作节点
  • config: 连接器特定配置
  • metadata: ECConnectorMetadata

关键方法

  • has_caches(request): 检查远程嵌入是否已存在
  • build_connector_meta(sched_output): 确定工作节点必须加载哪些缓存
  • update_state_after_alloc(request, item): 根据缓存命中/未命中更新缓存分配
  • save_caches(encoder_cache): 将编码器输出推送到远程存储
  • start_load_caches(metadata): 在预填充/解码执行前,在 PD 侧加载缓存

调度侧行为

1. 连接器初始化

调度器(Scheduler)

if self.vllm_config.ec_transfer_config is not None:
    self.ec_connector = ECConnectorFactory.create_connector(
        config=self.vllm_config,
        role=ECConnectorRole.SCHEDULER,
    )

工作节点

def ensure_ec_transfer_initialized(vllm_config):
    global _EC_CONNECTOR_AGENT
    if vllm_config.ec_transfer_config is None:
        return
    if vllm_config.ec_transfer_config.is_ec_transfer_instance and _EC_CONNECTOR_AGENT is None:
        _EC_CONNECTOR_AGENT = ECConnectorFactory.create_connector(
            config=vllm_config,
            role=ECConnectorRole.WORKER,
        )

2. 远程缓存检查

调度媒体项目时

remote_cache_has_item = self.ec_connector.has_caches(request)

3. 缓存状态更新

调度后

for i in external_load_encoder_input:
    self.encoder_cache_manager.allocate(request, i)
    if self.ec_connector:
        self.ec_connector.update_state_after_alloc(request, i)

4. 元数据构建

调度器迭代结束时

ec_meta = self.ec_connector.build_connector_meta(scheduler_output)
scheduler_output.ec_connector_metadata = ec_meta

工作节点侧行为

工作节点使用 ECConnectorModelRunnerMixin 将连接器操作集成到 GPU 模型运行器中。


执行集成

编码器侧(保存至远程存储)

计算嵌入后

for (mm_hash, pos_info), output in zip(mm_hashes_pos, encoder_outputs):
    self.encoder_cache[mm_hash] = scatter_mm_placeholders(...)
    self.maybe_save_ec_to_connector(self.encoder_cache, mm_hash)

预填充/解码侧(加载远程嵌入)

媒体编码器路径被包装在一个加载器中,在运行本地编码器之前注入缓存的嵌入

with self.maybe_get_ec_connector_output(
        scheduler_output,
        encoder_cache=self.encoder_cache,
    ) as ec_connector_output:

    self._execute_mm_encoder(scheduler_output)
    mm_embeds, is_mm_embed = self._gather_mm_embeddings(scheduler_output)

性能结果

环境: 4×A100 80G
数据集: 随机多模态数据集 (vllm bench serve --dataset-name random-mm)
输入: 400 / 2000 文本 token;每个请求 1–4 张图像(640×640 → 每张约 400 个视觉 token)
输出: 150 tokens
QPS 范围: 4–24
模型: Qwen3‑VL‑4B‑Instruct
基准: 1 个编码器 + 3 个 PD 实例 (1E3PD) 对比数据并行 (--data-parallel-size 4)

生产级 LMM 服务系统需要严格的尾部延迟保障——通常是 P99 TTFTP99 TPOT——以确保极端情况下的可靠性。我们定义 goodput(有效吞吐量) 为同时满足两个 SLO(我们的评估中为 20000 ms TTFT,100 ms TPOT)的最大可持续请求速率。


短文本工作负载(~400 tokens)

对于短文本请求,EPD 的优势随着每个请求图像数量的增加而显著提升。

  • 单图像: 有效吞吐量略有提高(23 → 24 QPS)。
  • 四图像: 有效吞吐量 翻倍(6 → 12 QPS)。

尾部延迟显著改善

  • P99 TTFT/TPOT 通常比非 EPD 基准 低 20–50%

吞吐量与速率曲线显示

  • 如果没有 EPD,多图像工作负载在 12–14 QPS 左右开始变得不稳定,P99 TPOT 激增 30–50%,违反 SLO。
  • EPD 将该不稳定性阈值大幅提高,并保持更平滑、增长更慢的延迟曲线,这要归功于消除了编码器-解码干扰,以及纯文本请求能够完全绕过视觉处理任务的能力。

长文本工作负载(~2000 tokens)

在更长的输入情况下,图像编码成本在总工作量中仅占一小部分,使系统进入解码主导阶段。即使在此情况下,EPD 也取得了巨大的收益。

违反 P99 前的基准可持续 QPS

  • 1 张图像: 8 QPS
  • 3–4 张图像: 4 QPS

EPD 保持

  • 分别为 18 / 11 / 9 / 8 QPS — 有效吞吐量提升 2 倍至 2.5 倍

额外改进

  • 在所有多模态设置下,有效解码吞吐量增加 10–30%
  • P99 TTFT 降低 30–50%
  • 在稳定运行区域内,P99 TPOT 降低 20–40%

解耦的编码/文本流水线消除了模态争用,实现了更高的并发性、改善的吞吐量,即使在高多模态负载下也能实现更严格的 SLO 合规性。


硬件可移植性:昇腾 NPU

我们在昇腾 NPU 上以极小的改动重复了这些实验

  • 环境: 4×昇腾 910B 32G
  • 模型: Qwen2.5‑VL‑7B‑Instruct
  • QPS: 1–10

在所有的昇腾实验中,EPD 表现出 相同的硬件无关收益

  • 持续更高的吞吐量(在稳定区域内提高 5–20%)。
  • P99 TTFT 和 P99 TPOT 的显著降低。
  • 延迟的拥塞点和更紧凑的尾部延迟分布。

这证实了 EPD 的收益源于架构解耦——而非硬件特性,使其在 GPU 和 NPU 平台上均具有可移植性。


总结

通过对 LMM 推理行为和生产工作负载需求的仔细分析,我们开发了一种 解耦的、流水线并行的多模态服务架构,该架构

  • 降低了 TTFT 和 TPOT,
  • 提高了吞吐量和稳定性,
  • 消除了跨模态干扰,并且
  • 实现了高效、可扩展的多模态服务。

该架构为下一代高性能 LMM 服务系统提供了实用的蓝图。展望未来,我们将继续通过优化编码器实例的参数加载扩展 EC 连接器支持来增强 vLLM。


ViT 数据并行 + LLM 张量并行

在探索编码器解耦之前,vLLM 首先在单节点多模态模型上引入了一种混合并行策略:ViT 数据并行 + LLM 张量并行方法,其中视觉编码器跨 GPU 进行数据并行,而语言模型使用张量并行。这种混合策略显著降低了 TTFT 并提高了总体吞吐量。该方法已被证明有效,并被其他服务框架采用,例如 SGLang

现有技术与行业采用

NVIDIA Dynamo 团队首次支持了基于 vLLM 的 EPD 风格的解耦,尽管文档有限。vLLM 原生 EPD 实现 (PR #25233) 于 2025 年 11 月初合并,并自 0.11.1 版本起可用,为开源生态系统带来了顶级的编码器解耦支持。


参考

  • Qiu, Haoran, et al. ModServe: Modality‑ and Stage‑Aware Resource Disaggregation for Scalable Multimodal Model Serving. 2025.
  • Singh, G., et al. Efficiently Serving Large Multimodal Models Using Encoder-Decoder Disaggregation. 2025.

致谢

我们要感谢主要贡献者——ZHENG Chenguang, Nguyen Kha Nhat Long, Tai Ho Chiu Hero, Le Manh Khuong, Wu Hang 和 Wu Haiyan——感谢他们在整个项目开发过程中所做出的重大贡献和技术专长。还要特别感谢社区维护者 Roger Wang, Nicolò Lucchesi 和 Cyrus Leung,感谢他们宝贵的反馈、深刻的审查以及在代码集成过程中的悉心指导,这些极大地提高了代码库的质量和稳定性。