深入 vLLM EPD:Disaggregated Encoder / Encoder-Prefill/Decode 源码拆解

12828 字
29 min read
标签: Source Code Analysis

vLLM 主线最近引入了 EPD(Disaggregated Encoder / Encoder-Prefill/Decode)。它并不是把整条推理链都拆成内建分布式 pipeline,而是把多模态请求里的 encoder outputs 抽成可复用、可远端加载的中间态。

本文聚焦三个问题:主线已经实现了什么、它与常见 P/D 分离的区别、以及距离生产化还差哪些环节。

如果只记一条结论:当前主线已经具备 encoder 输出解耦与远端注入能力,但完整线上拓扑仍依赖外部 proxy 编排。

说明:本文基于 vLLM main 分支的 commit 4eefbf9609e5ddb996e3ac37e192e92466ec35cc(commit 时间:2026-04-02 11:52:18 +0000)进行分析,目标仓库为 https://github.com/vllm-project/vllm


1. 执行摘要

当前实现

Rendering diagram…

要点说明:

  1. 当前主线的 EPD,本质上是“把多模态 encoder 从 prefill/decode 所在实例中拆出去”,并通过 ECConnector 在进程间传递 encoder outputs,而不是把整个推理流程自动做成一个内建分布式 pipeline。
  2. 在线 E|PD / E|P|D 示例并不只靠引擎内部完成 orchestration,而是依赖外部代理 examples/online_serving/disaggregated_encoder/disagg_epd_proxy.py 做“三段式编排”。
  3. 当前主线源码里 EC 相关 mixin 与完整执行链仍集中在 V1 路径;in-tree connector 只有 ECExampleConnector,定位是示例实现。

架构分析

  1. 当前 vLLM 的 EPD 更像“内核能力 + 外部编排”,而不是单开关即可落地的完整产品。
  2. 它解决的是多模态 encoder 与文本 generation 的耦合;与 disaggregated prefill 是正交关系,前者传 encoder outputs,后者传 KV cache。

2. 背景与动机

当前实现

Rendering diagram…

要点说明:

  1. encoder、prefill、decode 的资源画像不同,长期共置会带来排队干扰与扩容耦合。
  2. 这种差异已经进入 runtime:scheduler 有独立的 encoder_compute_budgetencoder_cache_manager,纯 producer 不分配 KV cache,encoder-only 关闭 chunked prefill。
  3. 文本请求在代理层和引擎层都天然可以 bypass encoder。

架构分析

  1. vLLM 做 EPD,核心不是“切阶段”本身,而是把短时、重计算、强波动的视觉 encoder 从文本 generation 实例里拆出来,减少互相拖累。
  2. 纯 PD 分离主要治理 TTFT 与 ITL;EPD 更强调消除视觉 encoder 对文本生成的干扰,所以两者的收益形态并不相同。

3. 官方定义与能力边界

3.1 概念地图

概念当前仓库中的含义传递对象典型拓扑关键模块
aggregated serving单个 vLLM 实例同时承担该请求需要的全部阶段;对多模态请求来说通常是 E+P+D 共置,对纯文本请求则没有 E无跨进程中间态1 x vLLM常规 scheduler / worker
disaggregated encoder把多模态 encoder 从 PD 实例拆出,PD 侧加载远端 encoder outputsencoder outputs / embeddingsE + PDvllm/distributed/ec_transfer
disaggregated prefill把 prefill 与 decode 拆开,decode 侧加载远端 KVKV cache + 传输参数P + Dvllm/distributed/kv_transfer
E|PDEPD 的最小在线形态;E 单独实例,PD 为 combined 实例仅 EC1E + 1PD 或扩展到多实例disagg_epd_proxy.py + EC connector
E|P|D在 EPD 上再叠加 PD 分离先 EC,后 KV1E + 1P + 1Ddisagg_epd_proxy.py + EC + KV connector
ec_both单实例既是 EC producer 又是 EC consumer;仍可生成 token,不等于单独的 encoder-only 实例EC聚合节点上的混合角色ec_role="ec_both"

3.2 解决什么问题,不解决什么问题

当前实现

Rendering diagram…

要点说明:

  1. EPD 解决的是“多模态 encoder 结果如何拆出、复用、注回 generation 流水线”。
  2. EPD 不会自动把整条多模态请求链都变成可复用中间态,也不会自动补齐 transport 和 cache governance。

架构分析

  1. EPD 的“边界”非常清楚:它把“视觉 encoder 结果”外提成可复用中间态,但没有把“整个多模态请求处理”都提成可复用中间态。当前 proxy 仍会让 E、P、D 三端分别解析和预处理原始多模态输入。
  2. 对自研框架来说,不能把 EPD 误读为“只要把 vision tower 拆到另一台机器就行”。如果没有 hash、metadata、cache lifecycle、失败回退,这个设计会很快失效。

3.3 ec_role 与运行时行为矩阵

ec_rolescheduler 侧行为worker 侧行为是否走 encoder-only 快路径KV / sampler 形态典型部署
ec_producer不参与远端 EC 载入决策;scheduler 侧基本没有 consumer 式命中/加载语义save_caches(),不 start_load_caches()是。execute_model()has_ec_transfer() and not is_consumer 时会提前返回空输出get_kv_cache_spec() 直接返回空;Ray executor 也不会把它当成正常采样实例纯 encoder 节点
ec_consumer参与 has_cache_item() / build_connector_meta(),把“远端载入还是本地重算”前移到 schedulerstart_load_caches(),再走正常 prefill/decode正常持有 KV cache,可与 KV connector 组合成 P/D 分离PD 或 P 节点
ec_both具备与 consumer 相同的远端命中/metadata 构建语义同时具备 load 与 save 能力否。因为 ec_both 也是 consumer,不满足 not is_consumer仍是正常生成实例聚合节点或单实例复用场景

要点说明:

  1. ec_both 是一个很容易被误读的角色。它不是“带一点 producer 能力的 encoder-only”,而是“仍然按正常生成实例运行,但同时能读写 EC”。
  2. 这也解释了为什么 ec_both 可以用于单实例基准或重复图像命中测试,但它并不等价于真正的 E-only 节点。

4. 仓库与代码入口总览

4.1 文档 / 示例 / 测试入口

  1. 官方功能文档:
    • docs/features/disagg_encoder.md
    • docs/features/disagg_prefill.md
  2. 在线示例:
    • examples/online_serving/disaggregated_encoder/README.md
    • examples/online_serving/disaggregated_encoder/disagg_epd_proxy.py
    • examples/online_serving/disaggregated_encoder/disagg_1e1pd_example.sh
    • examples/online_serving/disaggregated_encoder/disagg_1e1p1d_example.sh
    • examples/online_serving/disaggregated_serving/README.md
    • examples/online_serving/ec_both_encoder/ec_both_encoder.sh
  3. EPD 直接相关测试:
    • tests/v1/ec_connector/unit/test_ec_example_connector.py
    • tests/v1/ec_connector/integration/test_epd_correctness.py
    • tests/v1/ec_connector/integration/run_epd_correctness_test.sh
    • tests/v1/core/test_scheduler.py
    • tests/v1/engine/test_engine_core.py

4.2 核心代码地图

  1. 配置与初始化:
    • vllm/engine/arg_utils.py: 暴露 --ec-transfer-config--mm-encoder-only
    • vllm/config/ec_transfer.py: ECTransferConfig
    • vllm/distributed/ec_transfer/ec_transfer_state.py: worker 侧全局 EC connector 初始化
    • vllm/v1/worker/gpu_worker.py: 创建 model runner 前调用 ensure_ec_transfer_initialized()
  2. 请求解析与多模态输入建模:
    • vllm/entrypoints/openai/chat_completion/serving.py
    • vllm/entrypoints/serve/render/serving.py
    • vllm/entrypoints/chat_utils.py
    • vllm/inputs/preprocess.py
    • vllm/v1/engine/input_processor.py
    • vllm/multimodal/inputs.py
    • vllm/v1/request.py
  3. 调度与缓存:
    • vllm/v1/core/encoder_cache_manager.py
    • vllm/v1/core/sched/scheduler.py
    • vllm/v1/core/sched/output.py
  4. 执行与注入:
    • vllm/v1/worker/ec_connector_model_runner_mixin.py
    • vllm/v1/worker/gpu_model_runner.py
  5. Connector 抽象与实现:
    • vllm/distributed/ec_transfer/ec_connector/base.py
    • vllm/distributed/ec_transfer/ec_connector/factory.py
    • vllm/distributed/ec_transfer/ec_connector/example_connector.py
  6. 与 PD 组合时的 KV 参数传递:
    • vllm/entrypoints/openai/chat_completion/protocol.py
    • vllm/v1/request.py
    • vllm/v1/core/sched/scheduler.py
    • vllm/v1/engine/output_processor.py

4.3 初始化路径、请求路径、传输路径、失败路径

Rendering diagram…
  1. 初始化路径:
    • CLI --ec-transfer-config / --mm-encoder-only
    • ECTransferConfig
    • GPUWorkerensure_ec_transfer_initialized()
    • ECConnectorFactory.create_connector()
  2. 请求路径:
    • OpenAI Chat 请求
    • renderer / parser / multimodal preprocess
    • InputProcessor.process_inputs()
    • Request
    • Scheduler.schedule()
    • GPUModelRunner.execute_model()
  3. 传输路径:
    • EC: _execute_mm_encoder() -> self.encoder_cache -> save_caches() -> 远端/共享介质 -> start_load_caches() -> self.encoder_cache -> _gather_mm_embeddings()
    • KV: prefill 响应把 kv_transfer_params 带回上层,再由 decode 请求继续使用
  4. 失败路径:
    • 远端 EC 不存在: scheduler 直接回退到本地 encode
    • 远端 EC 在调度后消失: start_load_caches()FileNotFoundError
    • 载入后仍缺项: _gather_mm_embeddings() 断言 Encoder cache miss
    • kv_transfer_params 但没有 KV connector: engine warning 后禁用

5. EPD 端到端请求生命周期

以下以“图像 + 文本”的在线 E|P|D 请求为例说明。E|PD 只是把其中的 prefill 与 decode 合并为一个 PD 实例。

全链路 Mermaid 时序图

Rendering diagram…

5.1 请求进入系统

  1. 客户端向 disagg_epd_proxy.py 发送 OpenAI chat completion 请求。
  2. 代理的 extract_mm_items() 只从 messages[*].content 的 list 形态里提取 image_url / audio_url / input_audio 三类 item,而不是泛化地抓取一切非文本内容。这意味着当前 demo proxy 的模态覆盖范围,其实比引擎内部的多模态抽象更窄。
  3. fanout_encoder_primer() 会为每个 multimodal item 构造一个“只保留该 item、完全删除文本”的子请求,并把 max_tokens 固定为 1,以确保服务端真正走到 encoder 执行路径。所有 primer 子请求必须全部成功,后续 P/PD/D 阶段才会继续。

5.2 多模态输入解析

  1. 每个 encoder primer 子请求与原始请求,在各自 vLLM 实例里都会走同一条标准入口:
    • OpenAIServingChat.create_chat_completion()
    • OpenAIServingRender.render_chat()
    • parse_chat_messages()
    • InputPreprocessor._process_multimodal()
    • InputProcessor.process_inputs()
  2. InputProcessor 把多模态 item 统一折叠成 MultiModalFeatureSpec,其中最关键的字段有:
    • data: 真正给 vision tower 的输入
    • identifier: 用于 cache / connector 命中的 key;LoRA 场景下可变成 lora_name:mm_hash
    • mm_position: 该多模态 item 在 decoder 输入序列里的 placeholder 位置
    • mm_hash: 原始多模态 processor 产出的基础 hash
  3. InputProcessor 会先按 mm_position 对多模态项排序,再生成 mm_features。因此后续 scheduler / worker 实际看到的顺序,是“在 decoder 序列中的占位顺序”,不一定等同于原始 messages 遍历顺序。
  4. MultiModalFeatureSpec.data 允许为 None。源码注释写得很明确:这是为了在 API server 与 engine core 之间跳过不必要的 IPC。所以 EPD 的调度与命中语义,真正依赖的是 identifier / mm_position / mm_hash,而不是始终依赖原始多模态 payload 常驻内存。
  5. PlaceholderRange 还支持 is_embed mask,这意味着“placeholder token 数”和“当前 step 真正需要切出的 encoder embedding 数”并不总是一一对应;后续 _gather_mm_embeddings() 会根据这个 mask 做稀疏切片。

5.3 encoder 如何被触发 / 分流

  1. 代理层不是把“原始完整请求”直接发给 encoder;而是 fanout_encoder_primer() 为每个多模态 item 构造一个只包含该 item、没有文本内容的子请求,并发打到 encoder 集群。
  2. 在 encoder 实例内,GPUModelRunner.execute_model() 会检测到“当前有 EC transfer,且本实例不是 consumer”,于是走 encoder-only 分支:
    • 进入 EC connector 生命周期上下文
    • 执行 _execute_mm_encoder()
    • 立即返回 make_empty_encoder_model_runner_output()
  3. 这个分支不会继续走 LM prefill/decode,因此 encoder-only 实例不需要 KV cache。

5.4 encoder outputs 如何生成

  1. _execute_mm_encoder() 根据 scheduler_output.scheduled_encoder_inputs 把需要编码的多模态 item 批量收集出来。
  2. 然后调用模型的 embed_multimodal(**mm_kwargs_batch) 真正运行 vision encoder。
  3. 每个 item 的输出以 self.encoder_cache[key] = output 形式落到 worker 本地 encoder_cache;这里的 key 实际沿用 MultiModalFeatureSpec.identifier,无 LoRA 时通常等于基础 mm_hash
  4. 实际 batching 比报告开头看起来更精细:runner 会先按 modality 分组批处理;如果一个 batch 中 modality 混杂,或者为了保持 item 顺序必须拆分,就会变成多个 micro-batch。
  5. 某些视频 / EVS / dynamic-resolution video 场景下,源码会主动退化成“逐视频顺序编码”,以降低峰值显存,而不是强行把视频样本堆到同一个 encoder batch 里。
  6. LoRA 场景下,EPD 不只是把 identifier 改成 lora_name:mm_hash。如果 tower connector LoRA 打开,runner 还会额外构造 TOWER / CONNECTOR 两套 LoRA mapping,再执行 embed_multimodal()。也就是说,LoRA 既影响 cache key,也影响 encoder 执行上下文。

5.5 metadata 如何组织

  1. scheduler 不直接把 tensor 发给 worker;它只在 SchedulerOutput.ec_connector_metadata 里携带一个“这一轮需要从外部加载哪些 mm item”的 opaque metadata。
  2. ECExampleConnector 而言,这个 metadata 是 ECExampleConnectorMetadata,里面只是一组 MMMeta(mm_hash, num_token);字段名虽然叫 mm_hash,但实际承载的是用于载入的字符串 key,无 LoRA 时通常等于基础 mm_hash
  3. 这说明当前元数据设计是极简的:worker 知道“要加载哪些 hash”,至于 tensor 的 schema / 版本 / dtype / 设备兼容性,不在 connector metadata 里显式表达。

5.6 outputs 如何缓存 / 传输

  1. encoder 侧本地缓存:
    • self.encoder_cache 是 worker 内的物理缓存。
    • EncoderCacheManager 是 scheduler 内的逻辑缓存。
  2. 远端传输:
    • maybe_save_ec_to_connector()ECConnector.save_caches()
    • ECExampleConnector.save_caches() 把 tensor 转 CPU 后写入 <shared_storage_path>/<key>/encoder_cache.safetensors
  3. 这个设计里,“外部 EC”与“本地 encoder_cache”是两套缓存层次,不是同一份对象。
  4. ExampleConnector 的 save/load 路径是一条非常明确的“GPU -> CPU -> safetensors 文件 -> 目标设备”链路:save_caches() 会对张量做 detach().cpu()start_load_caches() 再用 safetensors.torch.load_file(..., device=current_platform.device_type) 直接读回目标设备。
  5. ECExampleConnector._generate_filename_debug() 在读写两侧都会自动创建 <shared_storage_path>/<key>/ 目录,因此“目录存在”不等价于“缓存文件已经有效写完”;真正命中依赖的还是 encoder_cache.safetensors 是否存在、是否可读。

5.7 PD 侧如何加载 / 注入

  1. Prefill 或 PD 实例收到原始完整请求后,会再次独立解析出同样的 mm_hash / identifier / mm_position
  2. scheduler 的 _try_schedule_encoder_inputs() 对每个多模态 item 做三选一:
    • 本地 encoder cache 命中: EncoderCacheManager.check_and_update_cache()
    • 远端 EC 命中: ec_connector.has_cache_item(identifier),随后放进 external_load_encoder_input
    • 否则本地算 encoder,放进 scheduled_encoder_inputs
  3. 进入 worker 后:
    • ECConnectorModelRunnerMixinbind_connector_metadata()
    • consumer 角色执行 start_load_caches()
    • ECExampleConnector.start_load_caches() 从共享目录把 safetensors 读回 self.encoder_cache
    • _gather_mm_embeddings()self.encoder_cache[key] 切出当前 step 所需区间
    • _preprocess() 把这些 mm_embeds 注入 model.embed_input_ids(...)
  4. 当前 EC load 是 step 内同步完成的,并没有像 KV transfer 那样把请求推进到某个 WAITING_FOR_REMOTE_* 状态;这也是为什么现有 ExampleConnector 更像“同步 cache load”,而不是完整的异步远端传输 pipeline。
  5. _gather_mm_embeddings() 不只是简单切张量。它既支持 PlaceholderRange.is_embed 的稀疏切片,也支持多个 multimodal feature 在同一段 prompt 上重叠时通过 OR mask 合并 is_mm_embed,例如 use_audio_in_video 这类复合输入场景。

5.8 prefill / decode 如何继续执行

  1. 如果代理配置了独立 prefill 实例,process_prefill_stage() 会给请求塞入:
    • kv_transfer_params = {"do_remote_decode": true, "do_remote_prefill": false, ...}
    • max_tokens = 1
    • stream = false
  2. OpenAI 协议层 ChatCompletionRequest 会把 kv_transfer_params 塞进 SamplingParams.extra_args
  3. Request 初始化时会从 sampling_params.extra_args 里抽出 kv_transfer_params
  4. prefill 结束时,scheduler 把 kv_transfer_params 放进 EngineCoreOutputOutputProcessor 再把它带到上层响应。
  5. 代理把返回的 kv_transfer_params 注回原始请求,然后转发给 decode 实例。
  6. decode 实例此时不关心 EC;它主要消费的是 KV connector 路径。

5.9 最终如何返回结果

  1. E|PD:
    • 代理先完成 encoder primer
    • 然后把原始请求直接发给 PD 实例
    • PD 完成 prefill + decode 并把结果返回给客户端
  2. E|P|D:
    • 代理先完成 encoder primer
    • 再调用 prefill 实例拿 kv_transfer_params
    • 最后把原始请求 + kv_transfer_params 发给 decode 实例,decode 负责最终流式/非流式输出

5.10 失败路径与 fallback

  1. 远端 EC 根本不存在时,fallback 发生在 scheduler 阶段:has_cache_item() 返回 False,该 item 会回到“本地计算 encoder”分支。
  2. 远端 EC 在 scheduler 认为“存在”之后被删除时,当前 ExampleConnector 不做恢复,它会在 start_load_caches() 中直接触发 FileNotFoundError
  3. 如果某个 item 理应已经装入 self.encoder_cache,但到 _gather_mm_embeddings() 时仍未拿到,会触发 assert encoder_output is not None
  4. 文本请求不会走上述路径:没有 mm_features,也没有 encoder primer 子请求。

6. 核心工程实现拆解

6.0 总体架构图

Rendering diagram…

调度与缓存图

Rendering diagram…

6.1 模式开关与配置入口

当前实现

  1. CLI 参数入口在 vllm/engine/arg_utils.py
    • --mm-encoder-only
    • --ec-transfer-config
  2. ECTransferConfig 的核心字段在 vllm/config/ec_transfer.py
    • ec_connector
    • ec_role,支持 ec_producer / ec_consumer / ec_both
    • engine_id
    • ec_connector_extra_config
    • ec_connector_module_path
  3. compute_hash() 当前没有纳入任何 factor,返回的是空 factors 的 hash。这意味着 EC transfer 配置本身并不参与计算图哈希。
  4. 官方示例 README 指明 encoder 实例推荐组合:
    • --enforce-eager
    • --no-enable-prefix-caching
    • 很大的 --max-num-batched-tokens
    • 可选 --mm-encoder-only
  5. engine_id 在未显式传入时会自动生成 UUID,但当前并不参与 cache key 或 compute_hash();它更像 connector 实例身份而不是一致性边界。
  6. ec_connector_module_path 的动态加载能力,源码注释明确写的是 “Only supported in V1”,这进一步说明当前 EPD 扩展点仍主要依附在 V1 runner / connector 栈上。

架构分析

  1. 从配置设计看,vLLM 把 EPD 视为“额外的 transport / scheduling capability”,而不是一种全新 engine type。
  2. compute_hash() 目前忽略 ECTransferConfig,说明当前实现默认认为“EC transfer 不改变模型计算图”,但这也意味着 connector 层并不会帮助你做任何模型版本隔离。

6.2 encoder-only / PD 实例初始化差异

当前实现

  1. GPUWorker 在分布式环境初始化后调用 ensure_ec_transfer_initialized(),由此在 worker 进程内创建全局 EC connector。
  2. GPUModelRunner.execute_model() 对 “has_ec_transfer()not get_ec_transfer().is_consumer” 有专门分支:这就是 encoder-only 实例的主执行路径。
  3. GPUModelRunner.get_kv_cache_spec() 在纯 producer 路径上直接返回空字典,因此不会创建真正的 KV cache。
  4. tests/v1/engine/test_engine_core.py 明确验证了:
    • ec_producer 只有一个 null KV block
    • 没有 kv_cache_groups
    • 没有 kv_cache_tensors
    • chunked prefill 被禁用
  5. model_executor/models/interfaces.pyStageMissingLayer("language_model", mod) 支持 --mm-encoder-only,让 encoder-only 实例跳过语言模型组件初始化。
  6. vllm/v1/executor/ray_executor.py 还根据 is_ec_consumer 来决定是否启用 sampler,这意味着纯 producer 实例不会走正常生成采样语义。
  7. 一个很关键但不那么显眼的细节是:ec_both 不会走第 2 条里的“encoder-only 提前返回”分支,因为它同时也是 consumer。也就是说,ec_both 本质上仍是正常生成实例,只是额外具备读写 EC 的能力。

架构分析

  1. 当前的 encoder-only 实例不是“裁掉一部分算子后仍能正常生成”的普通 vLLM;它更接近一个只保留 vision tower 与必要调度骨架的专门化执行体。
  2. ec_both 的引入说明 vLLM 已经开始支持“聚合节点也参与 EC 读写”的混合部署,但这不改变 EPD 主设计仍以“encoder-only producer + consumer PD/P”组合为主。

6.3 connector 抽象:接口、职责、扩展点、调用链

当前实现

  1. ECConnectorBase 把接口严格分成两类:
    • scheduler 侧: has_cache_item()update_state_after_alloc()build_connector_meta()
    • worker 侧: bind_connector_metadata()clear_connector_metadata()start_load_caches()save_caches()get_finished()
  2. ECConnectorFactory 当前 registry 里内建的只有 ECExampleConnector;如果要扩展,只能走 ec_connector_module_path 动态加载。
  3. ECExampleConnector 的行为非常朴素:
    • has_cache_item() 只检查文件是否存在
    • save_caches() 直接把张量存到磁盘
    • start_load_caches() 直接从磁盘读回 GPU 设备
    • build_connector_meta() 只打包一组 key + num_token(数据结构字段名仍叫 mm_hash
  4. connector 在 scheduler 侧和 worker 侧不是同一个对象:scheduler 会自己创建一个 role=SCHEDULER 的 connector,而 worker 侧则通过 ec_transfer_state.py 把 connector 放进进程全局单例 _EC_CONNECTOR_AGENT。这意味着二者共享的是协议与配置,不共享内存状态。
  5. ECConnectorFactory 用的是 lazy registry:只有真正命中某个 connector 名称时,才会 import 对应模块。这让 EPD 扩展点保持轻量,但也意味着 connector 的初始化副作用和错误要等到 runtime 才暴露。

架构分析

  1. 这套抽象刻意把 connector 变成“调度器能问 cache 是否存在、worker 能把 tensor 拉进/写出 local encoder_cache”的最小接口,避免把 transport 细节写死在 scheduler/worker 里。
  2. 但当前抽象仍偏早期:
    • scheduler 侧没有显式的 async 状态机
    • worker 侧没有强制性的 save completion / retry / rollback 约束
    • metadata 太薄,难以支撑复杂的高性能 transport

6.4 metadata 设计:跨阶段传递了什么,为什么这样设计

当前实现

  1. MultiModalFeatureSpec 是跨阶段的核心实体,字段含义分别是:
    • identifier: cache / connector 命中 key
    • mm_hash: 基础 hash
    • mm_position: placeholder 范围
    • data: 真正的多模态输入
  2. SchedulerOutput 里有三类和 EPD 强相关的字段:
    • scheduled_encoder_inputs
    • free_encoder_mm_hashes
    • ec_connector_metadata
  3. ECExampleConnectorMetadata 只包含 MMMeta(mm_hash, num_token);其中字段名 mm_hash 实际承载的是 load 所需的字符串 key。
  4. 当前 ExampleConnector 在 worker load 路径里实际上只消费这个 key;num_token 没有参与真正的 IO / shape 校验。
  5. data=None 本身就是正式支持的状态,而不是异常值。它的意义是“这个 multimodal item 的大 payload 可以不在 API server 和 engine core 之间重复搬运”,但其定位、命中与注入语义仍然可通过 identifier / mm_position 保持完整。
  6. PlaceholderRange 里的 is_embed 使得同一个 placeholder 区间内部可以只有部分 token 需要被 encoder embedding 覆盖;因此 num_tokenget_num_embeds()、实际切片范围是三个不能简单混为一谈的量。

架构分析

  1. vLLM 的 metadata 设计倾向于“最小必要信息”:
    • placeholder 位置仍由请求状态决定
    • connector metadata 只回答“这一轮要加载哪些 EC”
  2. 这种设计的优点是 connector 与模型实现解耦;缺点是当前 metadata 不足以承担版本校验、shape 校验、跨硬件协商等职责。

6.5 encoder outputs 的保存、加载、注入路径

当前实现

  1. 保存路径:
    • GPUModelRunner._execute_mm_encoder()
    • self.encoder_cache[key] = output
    • maybe_save_ec_to_connector()
    • ECExampleConnector.save_caches()
  2. 加载路径:
    • scheduler 构造 ec_connector_metadata
    • ECConnectorModelRunnerMixin._get_ec_connector_output()
    • start_load_caches()
    • ECExampleConnector.start_load_caches()
    • self.encoder_cache[key] = ec_cache
  3. 注入路径:
    • _gather_mm_embeddings()self.encoder_cache 按 placeholder 区间切片
    • _preprocess()mm_embedsis_mm_embed 交给 model.embed_input_ids(...)

架构分析

  1. self.encoder_cache 是整个 EPD runtime 的“合流点”。只要某个张量最终进入这个 dict,后续 prefill/decode 就不在乎它是本地算出来的还是远端载入的。
  2. 这比在模型内部硬编码“远端 embedding 输入”要干净,因为它把远端传输问题隔离在 worker 外围。

6.6 scheduler / orchestration 如何推进状态

当前实现

  1. _try_schedule_encoder_inputs() 是 EPD 的关键调度点。对每个 multimodal item,它依次检查:
    • 当前 step 是否需要该 item
    • 是否已在 decoder KV 中消费完成
    • 本地 encoder cache 是否命中
    • cache / budget 是否允许分配
    • 远端 EC 是否存在
    • 最终决定本地算还是远端载入
  2. schedule() 会对需要的 item 先做逻辑 allocate(),再调用 ec_connector.update_state_after_alloc()
  3. build_connector_meta()SchedulerOutput 组装阶段执行。
  4. _update_after_schedule() 会推进 num_computed_tokens,随后调用 _free_encoder_inputs()
  5. _free_encoder_inputs() 的释放条件是“该 item 对应的 placeholder 已经被消费进入 decoder KV”;这意味着物理释放不是立刻发生。
  6. scheduler 虽然按 request 调度,但在 _try_schedule_encoder_inputs() 内部会额外维护 mm_hashes_to_schedulenum_embeds_to_schedule 这两个“按 item 粒度”的临时状态,用来避免同一步里对同一个 identifier 重复调度、也避免容量计算失真。
  7. disable_chunked_mm_input=True 且当前 token window 只覆盖到某个 multimodal item 的一部分时,scheduler 会把 num_new_tokens 回滚到该 item 开始之前,而不是让一个 MM item 被截断地进入当前 step。
  8. can_allocate() 失败时,如果 num_computed_tokens 已经因为 prefix caching 等原因越过了该 item 的 start_pos,scheduler 会把本轮 num_new_tokens 直接置成 0,而不是勉强调度一部分。这是一个很强的正确性优先决策。
  9. “远端命中”在 _try_schedule_encoder_inputs() 里会被放进 external_load_encoder_input,并同样累加到 num_embeds_to_schedule。也就是说,它虽然不扣 encoder compute budget,但会占 encoder cache 的本地落点预算。

架构分析

  1. scheduler 里的 EPD 逻辑本质上是一个三路选择器:
    • reuse local
    • load remote
    • recompute local
  2. 这也是为什么 EPD 真正依赖 scheduler,而不是只靠 worker:你必须在 token budget、encoder budget、cache capacity、placeholder overlap 这几个约束下做选择。

6.7 cache 机制:命中判断、key、生命周期、一致性

当前实现

  1. 逻辑 cache manager 是 EncoderCacheManager
    • key 是 request.mm_features[input_id].identifier
    • 粒度是“单个 multimodal item”
    • 容量按 encoder embeddings 数量而不是字节计
  2. check_and_update_cache() 负责命中与引用计数更新。
  3. can_allocate() 负责容量判断,并可能触发逻辑层 eviction;被逐出的 mm_hash 会放到 freed,稍后通过 SchedulerOutput.free_encoder_mm_hashes 通知 worker。
  4. worker 物理释放发生在 GPUModelRunner 处理 scheduler_output.free_encoder_mm_hashes 时,从 self.encoder_cachepop 掉对应 key。
  5. reset_encoder_cache() 只清本地逻辑/物理缓存;engine 注释明确说它主要用于调试,且“不尝试 re-sync internal caches”。
  6. ECExampleConnector 的外部 key 最终落到单个字符串路径上:无 LoRA 时通常是基础 mm_hash,有 LoRA 时可能带 lora_name: 前缀;无论哪种情况都不带模型版本。
  7. 除了 EPD 的 encoder cache 外,vLLM 还存在独立的 multimodal processor cache:MultiModalConfig.mm_processor_cache_gb / mm_processor_cache_type 控制的是“预处理与 processor 输出缓存”,而且它会在每个 API 进程和 engine core 进程各自复制一份,总内存开销近似为 mm_processor_cache_gb * (api_server_count + data_parallel_size),并不是 EPD 的跨进程共享缓存。

架构分析

  1. 当前 EPD 实际上有三层 cache:
    • scheduler 逻辑引用与容量管理
    • worker 本地张量缓存
    • connector 外部持久化 / 传输层
  2. 一致性是当前实现最脆弱的部分:
    • 外部 ExampleConnector 没有模型版本 key
    • 没有外部缓存删除 API
    • reset_encoder_cache() 不会清外部磁盘文件
  3. 因而当前实现更像“operator 需要自己管理 EC 目录生命周期”,而不是“框架自动保证跨实例一致性”。
  4. 这也解释了为什么“重复 preprocessing”至今还是问题。即使开启了 mm processor cache,它也主要是进程内 / 本地复用,并没有自动变成 E|P|D 三段共享的规范化中间态。

6.8 与 disaggregated prefill 的关系和组合方式

当前实现

  1. EPD 与 PD 分离用的是两套独立系统:
    • EPD: vllm/distributed/ec_transfer
    • PD: vllm/distributed/kv_transfer
  2. 官方文档明确把 E|P|D 描述为:Prefill 先按 EPD 的方式拿到 encoder outputs,然后只执行一步 prefill,再把 KV 交给 decode。
  3. disagg_epd_proxy.py 里这两个阶段在编排上是串行的:
    • 先 encoder primer
    • 再可选 prefill
    • 最后 decode
  4. kv_transfer_params 通过 OpenAI 协议对象进入 SamplingParams.extra_args,再由 Request 和 scheduler / output processor 传下去与传回来。

架构分析

  1. EPD 与 PD 分离的本质差异是“中间态不同”:
    • EPD 传的是模态 encoder outputs
    • PD 传的是自回归 attention 的 KV states
  2. E|P|D 不是一个全新特性,而是“两条中间态通道首尾串接”的组合模式。

6.8.1 示例 proxy 的真实编排语义

当前实现

  1. disagg_epd_proxy.py 对 encode 集群和 decode / prefill 集群的负载均衡策略并不相同:
    • encoder primer 对同一个请求内的 multimodal items 做 round-robin
    • prefill / decode 则是对实例列表做 random.choice()
  2. proxy 在控制面上是“全 barrier”语义:
    • 所有 primer 子请求都成功,后续阶段才继续
    • 任何一个 primer 异常或非 200,整个原始请求直接失败
  3. prefill 阶段会强制覆盖若干请求参数:
    • 注入 kv_transfer_params
    • stream=false
    • max_tokens=1
    • 删除 stream_options
  4. 这套示例编排没有任何“sticky session”或“跨阶段亲和性”概念。encode 子请求打到哪台 E、后续原始请求打到哪台 P/PD/D,都是独立决定的。

架构分析

  1. 这意味着示例 EPD 正确性真正依赖的不是“同一个请求一直路由到同一台机器”,而是:
    • 多端能算出稳定一致的 identifier
    • 共享 EC / KV 存储可被不同实例无状态消费
  2. 这种设计很适合先验证数据面抽象是否成立,但它也暴露出当前 control plane 仍偏 demo:
    • 没有跨阶段亲和路由
    • 没有 primer 与后续阶段的流水线重叠
    • 没有更细粒度的失败补偿与重试

6.9 异常 / 边界情况

当前实现

  1. cache miss:
    • 正常 miss 在 scheduler 阶段回退到本地计算
    • 非正常 miss 在 worker 阶段可能报 FileNotFoundErrorEncoder cache miss
  2. 形状不符 / 版本不符:
    • ECExampleConnector 不携带 tensor schema 或模型版本元数据
    • start_load_caches() 直接 load_file(...)[\"ec_cache\"]
    • _gather_mm_embeddings() 只断言“取得到条目”
  3. oversize 输入:
    • InputProcessor._validate_model_input() 会在单个多模态 item 超过预分配 encoder cache 大小时抛错
  4. chunking 边界:
    • compute_mm_encoder_budget()_try_schedule_encoder_inputs() 都有针对 chunked mm input 的边界处理
  5. encoder-decoder 模型:
    • 当前有 EncoderDecoderCacheManager
    • 但其注释直接说明 encoder-decoder 还没有真正复用 encoder cache,只是在调度层做兼容

架构分析

  1. 当前代码对“找不到 cache”处理得比“cache 找到了但内容不兼容”更充分。
  2. 如果要把 EPD 做到生产可用,versioning、schema 校验、跨实例失效广播、外部 cache deallocation 会比“再写一个更快的 transport”更先变成刚需。

6.10 测试覆盖了什么,没覆盖什么

当前实现

  1. tests/v1/ec_connector/unit/test_ec_example_connector.py 已覆盖:
    • 初始化
    • has_cache_item()
    • update_state_after_alloc()
    • metadata 构建
    • save / load
    • 已存在缓存时跳过 load
    • 空 metadata
    • 不存在文件时抛 FileNotFoundError
  2. tests/v1/core/test_scheduler.py 已覆盖:
    • preemption/resumption 下,本地命中 / 远端命中 / 无命中 三种优先级
    • “远端载入也占用 encoder cache 容量,但不消耗 encoder compute budget”
  3. tests/v1/engine/test_engine_core.py 已覆盖:
    • producer 与 consumer 初始化差异
  4. tests/v1/ec_connector/integration/test_epd_correctness.pyrun_epd_correctness_test.sh 已覆盖:
    • baseline vs 1E+1PD
    • baseline(1P+1D) vs 1E+1P+1D
    • multimodal prompts 与 text-only prompts

架构分析

  1. 当前测试足以证明“主路径正确性”和“部分 fallback 行为”,但还不够证明“生产稳定性”。
  2. 还明显缺少:
    • shape / dtype / model-version mismatch 测试
    • connector 并发写读竞争测试
    • EC 外部缓存失效 / deallocation 测试
    • Model Runner V2 (MRv2) 路径覆盖
    • 大规模 E|P|D 组合压测

6.11 源码里的隐含不变量

当前实现

  1. 只要启用了 EC connector,scheduler 每一步都会构造 ec_connector_metadata 对象,哪怕里面是空的。原因很直接:worker 侧 mixin 在进入 connector 生命周期时会 assert scheduler_output.ec_connector_metadata is not None
  2. 当前实现没有 WAITING_FOR_REMOTE_EC 之类的请求状态;与 KV transfer 不同,EC load 在 ExampleConnector 里是同步、step 内完成的。
  3. 尽管源码里的局部变量名经常写作 mm_hash,worker 侧 self.encoder_cache 实际沿用的是 identifier 这一逻辑 key;因此 LoRA-aware tower connector cache 在逻辑上天然与 base tower cache 隔离。
  4. update_state_after_alloc() 会在“本地重算”和“远端载入”两条路径上都被调用;只是 ExampleConnector 内部会再次检查 is_consumerhas_cache_item(),因此 miss 场景下最终是 no-op。

架构分析

  1. 这些不变量说明,当前 EPD 的设计重心是“把远端载入统一收束到一次 step 内的同步生命周期”,而不是像 KV transfer 那样显式建一套异步状态机。
  2. 这也是为什么现在的 connector metadata 可以做得很薄。一旦未来要支持真正异步 EC transfer,这些不变量大概率都要被打破,metadata、状态流转和失败回退协议都会明显变厚。

7. 关键调用链与 Mermaid 时序图

7.1 关键调用链

请求解析链

Rendering diagram…

EPD 调度链

Rendering diagram…

encoder producer 执行链

Rendering diagram…

consumer 注入链

Rendering diagram…

E|P|D 组合链

Rendering diagram…

7.2 全链路时序图

Rendering diagram…

7.3 失败路径时序图

Rendering diagram…
Rendering diagram…

8. 设计思想与工程取舍

当前实现

Rendering diagram…

要点说明:

  1. vLLM 没有把“远端 encoder outputs”直接暴露成模型 API,而是统一回收到 self.encoder_cache 后再注入模型。
  2. connector 被设计成 scheduler / worker 双角色对象,而不是单侧 transport 适配层。
  3. identifier / mm_hash 分离,以及 ec_both 的存在,都说明它在为更复杂的部署组合预留空间。

架构分析

  1. EPD 的核心目标,是把短时、重计算的 encoder 从长尾 generation 实例里拆出来,减少 queueing、资源配比和扩缩容上的互相牵制。
  2. connector 的价值不只是搬运张量,而是让 scheduler 能在“本地命中 / 远端命中 / 本地重算”之间做一致决策。
  3. 所以 EPD 的关键不只在 transport,也在 stable key、metadata 和 cache lifecycle;没有这些,远端复用就无法成立。
  4. 它和纯 PD 分离的区别也很明确:PD 解决的是 KV 在 prefill 与 decode 之间怎么搬,EPD 解决的是多模态 encoder 输出如何拆出、复用并注回 generation 流水线。

更深一层的架构思考

  1. 把分离边界放在 encoder outputs 是合理的:语义稳定、对 LM 主干侵入小、复用价值也高。
  2. 当前最大的遗留问题不是 encoder 没拆开,而是 preprocessing 仍未共享,系统还停留在“算子解耦”,还没有真正做到“请求级解耦”。
  3. 职责拆分也比较干净:MultiModalFeatureSpec 负责表示,Scheduler / EncoderCacheManager 负责决策,ECConnector / GPUModelRunner 负责搬运与注入。

典型实现难点与对应解法

  1. 难点一:跨实例如何稳定地认定“这是同一个 multimodal item”。

    • EPD 能否成立,前提就是 E 端和 P/PD/D 端必须算出同一个定位 key。
    • vLLM 用 InputProcessor.process_inputs() -> MultiModalFeatureSpec -> _get_mm_identifier() 统一 key 语义,避免代理层和引擎层各算一套;代价是 E、P、D 仍会重复 parser / preprocess / hash。
  2. 难点二:远端命中不等于“免费”,调度器必须把它算进本地容量。

    • 远端已有 encoder outputs,不代表 consumer 侧可以把它当成零成本命中,因为张量最终仍要落到本地 self.encoder_cache
    • _try_schedule_encoder_inputs() 会先判断本地 cache 容量,再决定远端载入还是本地重算;两条路径都会 allocate(),差别只是远端命中不扣 encoder_compute_budget
  3. 难点三:encoder outputs 的生命周期必须跟 decoder 消费进度对齐,而不是跟 encoder 完成时刻对齐。

    • 真正决定能不能删的,不是 encoder 是否跑完,而是 embeddings 是否已被对应请求消费进 decoder KV。
    • vLLM 把释放做成两阶段:scheduler 先释放引用,真正 eviction 只在容量压力下发生,从而避免过早回收。
  4. 难点四:connector 不能只是一个搬运 tensor 的库,它必须同时服务 scheduler 决策和 worker 执行。

    • 如果 connector 只暴露 worker 侧 load/save,scheduler 就无法在本地命中、远端载入和本地重算之间做前置选择。
    • 当前接口把 scheduler 决策和 worker 搬运分开了,但 metadata 还偏薄,暂时不足以承载版本、shape、dtype、部分写入等更重的生产语义。
  5. 难点五:远端加载失败该怎么回退,决定了你要不要引入异步状态机。

    • 正常 cache miss 不难处理,直接回到本地重算即可;更难的是“调度时命中、执行时失效”。
    • 当前 EC load 更像 step 内同步 cache load,主路径简单,但“调度后远端对象失效如何优雅降级”还没有真正解决。
  6. 难点六:在线编排到底放在引擎里还是放在代理里,本质上是在决定控制面边界。

    • EPD 不只是 worker 内多一个 load/save,还涉及什么时候先打 encoder primer、什么时候再发原始请求、E|PD 和 E|P|D 怎么串起来。
    • 现在由 disagg_epd_proxy.py 先承担编排,优点是主引擎侵入小,缺点是 barrier 语义重,跨阶段 overlap 和补偿机制还不够。

9. 性能与收益分析

当前实现

Rendering diagram…

要点说明:

  1. 从当前实现可以直接推出,EPD 主要面向三类收益:资源池解耦、混合负载排队改善、重复 encoder 计算消除。
  2. 纯 PD 分离和 EPD 都在“拆阶段”,但收益来源不同:前者偏 TTFT / ITL 治理,后者偏 encoder 干扰消除与远端复用。

架构分析

  1. 收益高度依赖 workload。图像多、encoder 占比高、文本与多模态混跑时最容易体现价值;超长 decode 主导时,收益更多是去干扰和独立扩容。
  2. 纯 PD 与 EPD 都在拆阶段,但优化对象不同:前者搬 KV、治理 TTFT / ITL,后者搬 encoder outputs、治理多模态对 generation 的干扰。
  3. 当前仍有重复多模态预处理,所以端到端 TTFT 改善会被吃掉一部分。

我对收益上限的判断

  1. EPD 更像系统级优化,收益通常先出现在 goodput 和尾延迟,再体现在单请求平均值。
  2. 最容易吃到红利的场景,是图像多、encoder 重、文本与多模态混跑;如果 workload 已被超长 decode 主导,收益会更偏“更稳、更好扩容”。
  3. 当前上限仍受三点限制:重复 preprocessing、朴素 transport、barrier 式编排。

从源码能直接看见的性能地板与天花板

当前实现

  1. 当前 in-tree ECExampleConnector 的数据路径是:
    • producer 侧 detach().cpu() 后写 safetensors 文件
    • consumer 侧再从文件读回目标设备
    • 因而天然包含 CPU round-trip 与文件系统开销
  2. 示例 proxy 的控制面是 barrier 式的:
    • 先 fanout 完所有 primer
    • 等所有 primer 成功
    • 再进入 P/PD/D
    • 没有跨阶段 overlap
  3. _execute_mm_encoder() 虽然会按 modality 做 batching,但混合 modality 会被拆 batch,某些视频场景甚至会主动退化成顺序编码。
  4. 远端 EC 命中不会消耗 encoder compute budget,但仍要占 consumer 侧 encoder cache 容量;纯 producer 实例也确实不会初始化 LM / KV。

架构分析

  1. 当前性能上限主要受三方面限制:朴素 transport、未流水化的 control plane、保守的 mixed-modality batching。
  2. 但性能地板并不差:producer 不建 KV,text-only 请求天然 bypass encoder,remote hit 至少能稳定省掉 encoder compute budget。
  3. 未来更强 connector 和 control plane 带来的第一波收益,未必来自单次搬运更快,而更可能来自去掉 barrier、减少重复 preprocessing 和 CPU round-trip。

10. 限制、未完成点与演进方向

当前实现

Rendering diagram…

要点说明:

  1. 当前实现最明显的不足,不是功能不存在,而是生产级闭环还没有补齐。
  2. 缺口主要集中在 connector、control-plane、重复 preprocessing、外部 cache 生命周期以及 runner 收敛。

架构分析

  1. 眼下的短板不在“能不能跑”,而在“能不能稳定、可控、可观测地跑”:外部 cache 生命周期、版本 / schema 校验、高性能 transport、重复 preprocessing、MRv2 对齐都还没补齐。
  2. ECExampleConnector 基本只靠字符串 key 命中,connector 级版本防护不足;模型或权重变化后如果共享目录未清,理论上存在陈旧 EC 误命中的空间。
  3. 后续演进大概率会围绕三条线展开:更强的 connector、更轻的请求表示与 preprocessing 复用、以及与统一 runner / 调度栈的整合。

我认为最难补齐的几个点

  1. 最难补的不是“再写一个更快的 connector”,而是外部 cache 一致性,因为它会同时碰到模型升级、脏数据、部分写入、过期回收和跨节点清理。
  2. 第二难的是失败回退:scheduler 看到命中,不代表 worker 一定能成功载入,所以需要端到端降级协议,而不是局部 try/except。
  3. 第三难的是消除重复 preprocessing。这一步决定 EPD 能不能从“算子解耦”走到“请求级解耦”,但它会同时牵动请求协议、缓存 key 和多模态处理器。
  4. 第四难的是新旧 runner 的收敛。只要 EPD 长期只在 V1 路径成熟,后续 connector、测试和控制逻辑就会越来越依赖旧路径。

更现实的演进路线

  1. 第一阶段先把 connector 做强,补齐版本戳、shape 校验、写完成标记、失败回退、批量回收和监控。
  2. 第二阶段把“规范化后的多模态 metadata”做成正式中间产物,让 P/D 侧少做甚至不再做重复 preprocessing。
  3. 第三阶段再把外部代理里的关键编排逐步内聚回正式 control plane,让 E|P|D 从 demo 变成平台能力。
  4. 第四阶段完成 MRv2 收敛,让 EPD 真正进入统一 runner 时代。

11. 附录

11.1 关键文件 / 类 / 函数索引

类别文件关键类 / 函数作用
文档docs/features/disagg_encoder.md全文官方 EPD 定义、动机、开发说明
文档docs/features/disagg_prefill.md全文官方 PD 分离定义与能力边界
示例examples/online_serving/disaggregated_encoder/disagg_epd_proxy.pyextract_mm_items / fanout_encoder_primer / maybe_prefill / process_prefill_stage / forward_non_stream / forward_stream在线 E|PD / E|P|D 编排
示例examples/online_serving/ec_both_encoder/ec_both_encoder.sh全文ec_both 单实例基准与重复图像命中示例
配置vllm/config/ec_transfer.pyECTransferConfigEC transfer 配置与角色定义
配置vllm/config/multimodal.pyMultiModalConfigmm_encoder_only、processor cache、MM IPC 等多模态运行时开关
抽象vllm/distributed/ec_transfer/ec_connector/base.pyECConnectorBase / ECConnectorRole / ECConnectorMetadataEC connector 最小抽象
工厂vllm/distributed/ec_transfer/ec_connector/factory.pyECConnectorFactory.create_connectorconnector 创建与动态加载
示例实现vllm/distributed/ec_transfer/ec_connector/example_connector.pyECExampleConnector / ECExampleConnectorMetadata / MMMeta磁盘版 EC connector
初始化vllm/distributed/ec_transfer/ec_transfer_state.pyensure_ec_transfer_initializedworker 侧 connector 全局初始化
多模态表示vllm/multimodal/inputs.pyPlaceholderRange / MultiModalFeatureSpec多模态占位与 cache key 表示
入口解析vllm/entrypoints/chat_utils.py_parse_chat_message_content_mm_part / parse_chat_messagesOpenAI chat 多模态内容解析
预处理vllm/inputs/preprocess.py_process_multimodal多模态预处理
输入转换vllm/v1/engine/input_processor.py_get_mm_identifier / process_inputs / _validate_model_input生成 EngineCoreRequestmm_features
请求对象vllm/v1/request.pyRequest / RequestStatus运行态请求与 kv_transfer_params
引擎核心vllm/v1/engine/core.pyadd_request / _initialize_kv_cachesKV connector 校验、无 KV cache 场景下禁用 chunked prefill
调度vllm/v1/core/sched/scheduler.pyschedule / _try_schedule_encoder_inputs / _update_after_schedule / _free_encoder_inputs / reset_encoder_cacheEPD 调度与状态推进
调度输出vllm/v1/core/sched/output.pySchedulerOutput / NewRequestData跨 scheduler -> worker 的 step 输出
逻辑缓存vllm/v1/core/encoder_cache_manager.pyEncoderCacheManager / EncoderDecoderCacheManager / compute_mm_encoder_budgetencoder cache 容量、引用与回收
worker mixinvllm/v1/worker/ec_connector_model_runner_mixin.pymaybe_save_ec_to_connector / _get_ec_connector_outputworker 内 connector 生命周期
worker 执行vllm/v1/worker/gpu_model_runner.py_batch_mm_inputs_from_scheduler / _execute_mm_encoder / _gather_mm_embeddings / _preprocess / execute_model / get_kv_cache_spec / reset_encoder_cache真正的 encoder 运行、EC 保存/加载、注入
worker 初始化vllm/v1/worker/gpu_worker.pyuse_v2_model_runner / ensure_ec_transfer_initialized 调用点决定 V1/V2 runner 与 EC 初始化
执行器vllm/v1/executor/ray_executor.pyuses_sampler区分 producer 与 consumer 的采样语义
LM 跳过vllm/model_executor/models/interfaces.py_mark_language_model--mm-encoder-only 下跳过语言模型
KV 参数桥接vllm/entrypoints/openai/chat_completion/protocol.pyto_sampling_params 中对 kv_transfer_params 的处理P 阶段 -> D 阶段参数桥接
输出桥接vllm/v1/engine/output_processor.py处理 engine_core_output.kv_transfer_params把 KV 传输参数带回上层响应
调试 APIvllm/entrypoints/serve/cache/api_router.py/reset_encoder_cache调试用 encoder cache reset

11.2 术语表

术语含义
ECEncoder Cache,通常指可复用的 encoder outputs
EC connector在不同 vLLM 实例之间传递 / 保存 / 载入 EC 的抽象
mm_hash多模态 processor 产出的基础 hash
identifier实际参与 cache / connector 命中的 key;可能带 LoRA 前缀
mm_position多模态 placeholder 在 decoder 输入中的位置区间
E|PDEncoder 与 combined Prefill/Decode 分离
E|P|DEncoder、Prefill、Decode 三段分离
aggregated serving不做阶段分离的普通单实例 serving
disaggregated prefill只拆 prefill 与 decode,通过 KV connector 连接
encoder-only instance只跑多模态 encoder 的 producer 实例
ec_both既是 EC producer 又是 EC consumer 的混合角色
100%

分享文章

Markdown 链接

      

标题 + 链接