编码器缓存#

编码器缓存 (EC) 存储多模态模型编码器阶段的输出,按 vLLM 的每个输入 mm_hash 进行索引。当两个请求共享一个多模态输入——相同的图像、视频或音频片段——第二个请求将从缓存中加载编码器输出,而编码器不会运行。

这适用于 vLLM 通过其编码器缓存扩展点暴露的任何模态:视觉编码器(用于图像和采样视频帧的 CLIP / ViT 风格塔)、音频编码器(用于原始波形的 Whisper 风格塔)以及诸如 Qwen2.5-Omni 的组合模态编码器。连接器是模态无关的——它缓存一个形状为 [num_tokens, hidden_dim] 的张量,键为 mm_hash,而不知道是哪个编码器生成的。

vLLM 通过 ECConnectorBase``(仅限 vLLM v1)暴露了编码器缓存扩展点。LMCache vLLM 端提供了一个 ``LMCacheECConnector 适配器,在 LMCache 端提供了一个 ECCacheEngine;它们共同支持使用 LMCache 的任何存储后端(本地 CPU、本地磁盘、远程、NIXL)来备份编码器缓存。

启用它#

--ec-transfer-config 传递给 vllm serve

vllm serve <model> \
    --ec-transfer-config '{
      "ec_connector": "LMCacheECConnector",
      "ec_role": "ec_both",
      "ec_connector_module_path": "vllm.distributed.ec_transfer.ec_connector.lmcache_connector"
    }'

ec_role 选项: ``ec_producer``(仅保存), ``ec_consumer``(仅读取), ``ec_both``(单实例默认)。

LMCACHE_CONFIG_FILE 设置为指向一个配置了至少一个 EC 存储后端的 YAML 文件:

chunk_size: 256
local_cpu: true
max_local_cpu_size: 2          # GiB
local_disk: "file:///var/lmcache/ec"
max_local_disk_size: 16        # GiB

要独立于(单独的)KV Cache 调整 EC 存储大小,请在 YAML 中使用 ec_ 前缀覆盖,或在环境中使用 LMCACHE_EC_ 前缀(例如 ec_max_local_disk_size: 64LMCACHE_EC_MAX_LOCAL_DISK_SIZE=64)。EC 和 KV 始终在 独立StorageManager 实例中运行,因此一个不能逐出另一个。

如果您不设置 local_disk``(或其 EC 覆盖),引擎仍然会启动,但 EC 条目仅存在于 CPU 内存中,并且在进程重启时不会保留。如果您希望缓存持久化,请将 ``local_disk``(或 ``ec_local_disk)设置为一个真实路径——没有隐式的磁盘默认位置。

验证它是否正常工作#

三个独立信号:

  1. vLLM 指标。 loggers.py 在预热请求后报告 MM 缓存命中率:X%

  2. LMCache 日志行。 冷(首次)请求会发出 LMCache INFO: EC put: stored N bytes for mm_hash=H。热请求不会发出 EC put

  3. 磁盘文件。local_disk 下,形式为 <model>@1@0@<chunk_hash>@<dtype>.pt 的条目在第一次请求后出现,并在此后被重用。@1@0@ 前缀反映了 EC 缓存键中的哨兵 world_size=1, worker_id=0,因此所有张量并行的 rank 共享一个条目。

设计说明(用户可见)#

  • 缓存键使用哨兵 TP 形状。 编码器输出在 TP 排中复制,因此 EC 键使用 world_size=1, worker_id=0,而不管部署的实际 TP。来自 N 个排名的并发放置会落在相同的键上,内容相同。

  • 数据类型与 KV 量化解耦。 EC 缓存键的 dtype 字段是编码器输出的数据类型(vllm_config.model_config.dtype),而不是 metadata.kv_dtype。更改 KV 量化不会使 EC 条目失效。

  • 将 StorageManager 与 KV 分离。 KV 和 EC 的访问模式非常不同(KV 是分块/逐层/高吞吐量;EC 是单张张量/请求范围)。共享一个分配器池会导致一个缓存以不可预测的方式逐出另一个缓存。因此,针对每个缓存的大小调整参数(ec_max_local_*)是明确的。

  • 连接器角色固定为“worker”。 vLLM 的 ECConnectorBase 是双角色(调度器和工作者)。LMCache 连接器无论如何都会调用 create_lmcache_metadata(..., role=\"worker\"),因为调度器端的 has_cache_item 需要一个完全构造的 StorageManager,而 LMCache 当前在 metadata.role == \"scheduler\" 时会中止磁盘后端的设置。

完整的内部设计(类层次、代码路径、后续工作)位于源代码树中的 docs/design/v1/encoder-cache.md

基准测试#

在一台单个 H100 80GB 上进行实时测量,使用 ``Qwen/Qwen2.5-VL-7B-Instruct``(bf16)和《大雄兔的故事》(10:34,720p,≈ 60 MB MP4)。对同一 vLLM 服务器发送相同的聊天完成请求,1 次冷启动 + N 次热启动。

两个配置,仅改变 ``num_frames``(vLLM 从视频中采样的帧数):

num_frames

EC 条目

冷 TTFT (秒)

温暖 TTFT 平均值 (秒)

已保存

加速

32 (vLLM 默认)

34.3 MB

3.923

3.125

798 毫秒

1.26×

128

130.8 MB

5.895

3.375

2.52 秒

1.75×

随着 num_frames 的增加,速度提升也会增加,因为编码器的工作负载与帧数呈线性扩展,而其余的 Prefill(对生成的多模态令牌和短文本提示的 LM 前向推理)则呈亚线性扩展。相同的原理适用于其他模态:当编码器占据 Prefill 的主导份额时(高帧数的长视频、长音频片段、高分辨率的大图像),收益最大;而当文本 Prefill 占主导时,收益最小。

再现#

服务器(重型编码器配置):

vllm serve <Qwen2.5-VL-7B-Instruct path> \
    --port 8000 \
    --gpu-memory-utilization 0.85 \
    --max-model-len 32768 \
    --max-num-seqs 8 \
    --limit-mm-per-prompt '{"video": 1}' \
    --media-io-kwargs '{"video": {"num_frames": 128}}' \
    --enforce-eager \
    --ec-transfer-config '{"ec_connector": "LMCacheECConnector",
                           "ec_role": "ec_both",
                           "ec_connector_module_path": "vllm.distributed.ec_transfer.ec_connector.lmcache_connector"}'

客户端:任何重新发送相同多模态负载的流式 OpenAI 兼容客户端。基准测试测量 TTFT(首次令牌时间),因为编码器在 Prefill 期间运行——任何编码器节省都在此处显现。解码每秒令牌数不受 EC 影响。