vLLM 推出双向 KV 传输,多轮智能体负载 TTFT 最高降 3.3 倍
Improve TTFT for Multi-Turn Agentic Workloads with Bi-Directional KV Transfer in vLLM
vLLM 博客介绍双向 KV 传输机制,让 decode 节点在后续轮次把已计算的 KV 块回传给 prefill 节点,避免重复计算对话历史。在 Amazon EC2 P5en 加 AWS EFA 的 1P1D 部署上,最长提示下 Qwen3-32B 的 TTFT 降低约 1.6 倍,zai-org/GLM-5-FP8 降低约 3.3 倍。
vLLM 官方给出双向 KV 传输的架构、阈值参数与复现脚本,可据此评估多轮智能体场景下的部署改造。
多轮智能体工作负载建立在智能体推理、调用工具并整合先前结果时不断累积的上下文之上。每新增一轮,客户端都会重新发送对话历史,包括系统指令、先前的提示、工具结果和模型响应,导致不断增长的上下文被反复处理。在 Prefill-decode(P-D)分离中,每个请求被拆分到专用节点上:prefill 节点处理提示并构建其 KV 缓存,然后 decode 节点获取这些 KV 块并生成响应。
然而,传统的分离式服务只在一个方向上传输 KV 缓存:从 prefill 节点到 decode 节点。这种单向流动给多轮工作负载带来了两个低效之处。首先,生成响应后,decode 节点同时持有提示和新生成 token 的 KV 缓存条目。在处理下一轮时,prefill 节点无法获取这些条目,因此必须重新计算先前响应 token 的 KV 投影。其次,decode 节点维护会话状态,而 prefill 节点不维护,这增加了 prefill 节点上活跃会话的缓存被逐出的概率。在完全缓存未命中的情况下,prefill 节点必须重新计算整个对话历史的 KV 投影,包括系统指令、先前的用户请求、工具结果和模型生成的响应。随着对话增长,这种冗余工作会消耗更多加速器容量并增加首 token 时间(TTFT)。
对于构建对话助手和智能体应用的客户而言,更长的交互历史会因重复的 prefill 计算而增加延迟并消耗加速器容量。在本文中,我们展示双向 KV 缓存传输如何使 prefill 和 decode 节点能够跨轮次复用先前计算出的上下文,从而提高加速器利用率,并帮助应用在对话增长时保持响应迅速的 TTFT。
在我们使用通过 AWS Elastic Fabric Adapter(AWS EFA,AWS 中跨服务器使用的低延迟 RDMA 网络)连接的 Amazon EC2 P5en 实例、采用 1P1D 部署的多轮基准测试中,双向 KV 传输将 Qwen3-32B 的 TTFT 最多降低 1.6 倍,将 zai-org/GLM-5-FP8 的 TTFT 最多降低 3.3 倍(参见性能数据部分);结果取决于工作负载和配置。
我们最初在通过 EFA 连接的 AWS Trainium 实例集群上构建并验证了该功能,并将其作为与加速器无关的能力贡献给了 vLLM。此处展示的结果来自带有 EFA 的 Amazon EC2 P5en GPU 集群,表明这些优势同样适用于 GPU。本文介绍了双向传输架构、路由器元数据流、KV 块保留与重算控制、性能结果,以及如何复现该基准测试。
双向 KV 传输
双向 KV 传输是一种机制,允许 decode 节点在对话的后续轮次中将先前计算出的 KV 返回给 prefill 节点,从而避免在 prefill 节点上重新计算共享上下文,并由可配置的 KV 重算阈值控制。这缓解了前文描述的两个计算冗余场景,而实现这一点需要 KV 在其产生的轮次之后仍然存活,并以反方向传输,由具备 KV 缓存感知能力的路由器进行协调。
面向双向传输的 NIXL KV 连接器增强
我们扩展了 NVIDIA Inference Xfer Library (NIXL) KV connector,使 prefill 节点能够消费远程 KV 块,类似于 decode 节点已经消费 prefill 节点提供的 KV 块的方式。这不是一个简单的改动,因为这两种节点类型在架构上不同:prefill 节点为新提示 token 计算 KV 表示,并能提供完整的提示 KV 块。decode 节点缺乏生成完整 KV 表示的能力(或效率),只能提供先前缓存的 KV 块。因此,prefill 和 decode 节点仍然各自承担自己的角色和逻辑,用于计算远程和本地 KV 块的元数据。然而,实际的 KV 传输机制(NIXL READ)对于 P->D 和 D->P 两个方向都是通用的。
下图首先以蓝色展示了现有流程:路由器将请求发送到 prefill 节点,prefill 节点计算提示 KV 并返回其块元数据,然后路由器将请求和元数据转发到 decode 节点,以进行标准的 P→D NIXL READ。编号的红色路径展示了双向 KV 传输的新增部分:(1) decode 节点在生成后保留符合条件的 KV 块,(2) 在流式响应中返回其元数据,(3) 路由器缓存该元数据以供下一次 prefill 请求使用。然后 prefill 节点 (4) 在应用本地缓存匹配后检查可远程复用的 KV,(5) 当可复用 token 数量达到阈值时排队一个异步接收,以及 (6) 在计算剩余后缀之前执行 D→P NIXL READ。

KV-cache-aware 路由器
vLLM 包含连接器端的双向 KV 传输支持,而跨对话轮次协调 KV 复用则需要一个 KV-cache-aware 路由器。vLLM GitHub 提供了一个示例代理,它实现了本文中使用的流程,可以根据你的环境进行调整。
KV-cache-aware 路由器是客户端、prefill 和 decode 节点之间的一个轻量级、有状态的控制平面组件。它维护一个从客户端提供的 conversation_id 到 decode 的 kv_transfer_params 的内存映射。路由器仅存储块位置元数据,而不存储 KV 张量,并且从响应流中获取该元数据,而不是单独查询 decode 节点。
下图展示了 router 如何在多轮对话中管理 KV 元数据。在第一轮时,缓存查找未命中,请求走标准的 P→D 流程;随后 router 从响应流中保存 decode 的元数据。在后续轮次中,router 使用 conversation_id 检索该元数据,使 prefill 能够复用来自 decode 的 KV,并在生成完成后刷新元数据以供下一轮使用。

块保留 TTL
通常,decode 实例会在生成完成后立即释放请求的块。在双向模式下,它会将这些块保留一段可配置的时长(decoder_kv_blocks_ttl,默认 480 秒),并发布它们的位置和过期时间,因此对话的 KV(包括提示前缀和生成的响应)在下一轮仍可用。对于在 TTL 到期前到达的轮次,prefill 节点会复用该 KV;对于更晚到达的轮次,则回退到常规 prefill。由于该 TTL 是固定的且不会续期,应将其调整为轮次之间的预期时间,以在 KV 复用与加速器内存使用之间取得平衡。一个自然的下一步改进是将心跳续期的租约扩展到 decode 侧,这样 decode 实例可以在对话空闲时立即回收块,而不是在固定计时器结束时才回收。
KV 重算阈值
KV 复用并不总是比本地重算更快。对于较短的可复用前缀,互连和协调开销可能超过重算这些 token 所需的时间。kv_recompute_threshold 设置控制这一决策,其默认值为 64 个 token。当需要计算的 token 数低于阈值时,prefill 节点进行重算;当达到或超过阈值时,则从 decode 节点拉取 KV。部署时可以根据模型计算时间、KV 大小和互连性能来调整该值。
性能数据
在评估中,我们在使用 Amazon EC2 P5en 实例并通过 EFA 连接的 1P1D 部署上使用了 Qwen3-32B 和 GLM-5-FP8 模型。为了模拟 prefill 节点已逐出其本地缓存,或 router 选择了另一个没有该对话 KV 块的 prefill 节点的场景,我们通过 --no-enable-prefix-caching 禁用了 prefill 节点上的前缀缓存。
在两个模型上,开启双向 KV 传输都减少了多轮对话中的 prefill 计算量,而且收益通常随提示长度增长。关闭该功能时,prefill 实例每轮都会重算整个增长的上下文,因此 TTFT 急剧上升:Qwen3-32B 从 2k 时的 158 ms 攀升到 24k 时的 778 ms,GLM-5-FP8 从 2k 时的 500 ms 攀升到 20k 时的 2,334 ms。开启该功能后,decode 实例保留的 KV 被复用而非重算,使 TTFT 平缓得多:Qwen3-32B 在同一范围内仅从 122 ms 上升到 460 ms(在最长提示下约减少 1.6 倍),而 GLM-5-FP8 从 358 ms 上升到 702 ms,在长提示下比重算路径低至多 3.3 倍。prefill 耗时分解解释了原因。
双向传输会增加少量 KV 传输时间(Qwen3-32B 约为 30 ms 至 130 ms;GLM-5-FP8 约为 29 ms 至 110 ms),但它所省去的 prefill 计算远超这段 KV 传输时间。因此,开启传输后的总 prefill 时长(Qwen3-32B:128 ms 至 414 ms;GLM-5-FP8:316 ms 至 541 ms)远低于仅重算路径(Qwen3-32B:153 ms 至 744 ms;GLM-5-FP8:500 ms 至 2,331 ms)。更大的 GLM-5-FP8 模型表现出更大的绝对和相对差距,因为其更高的每 token 重算时间使得 KV 复用随着模型和上下文规模扩大而收益更显著。
Qwen3-32B
下图绘制了 Qwen3-32B 的 TTFT 与提示长度的关系,对比了双向 KV 传输开启与关闭的情况。

下一张图将相同运行的 prefill 时长拆分为重算和 KV 传输两部分。

两张图共同表明,开启该功能时 TTFT 几乎保持平稳,而关闭路径随着上下文增长急剧攀升,因为少量 KV 传输时间远小于它所省去的重算时间。
zai-org/GLM-5-FP8
GLM-5-FP8 也呈现相同的两个视图。第一张图绘制了该功能开启与关闭时的 TTFT 与提示长度关系。

第二张图将 prefill 时长拆分为重算和 KV 传输两部分。

GLM-5-FP8 表现出比 Qwen3-32B 大得多的差距,在长提示下 TTFT 最多降低约 3.3 倍,因为其更高的每 token 重算时间使得 KV 复用随着上下文长度增长而收益更显著。
如何运行基准测试
要使用配备 EFA 的 Amazon EC2 P5en 实例复现我们的 1P1D 基准测试配置(一个 prefill 节点和一个 decode 节点),请按照下方 复现我们的结果 指南中的分步说明操作。该指南涵盖环境设置、启动 vLLM prefill 和 decode 服务器、运行基准测试以及收集结果。该功能可在其他受支持的配置上运行,但性能结果会因硬件、模型、工作负载和网络配置而异。
结论
在本文中,我们展示了双向 KV 缓存传输如何在智能体的对话历史增长时降低 TTFT。在 P-D 分离部署中,该功能允许 prefill 节点跨轮次复用 decode 节点保留的 KV 块,从而减少重复计算。可配置的块保留和重算阈值决定何时发生复用。在使用通过 EFA 连接的 Amazon EC2 P5en 实例的 1P1D 部署中,在测试的最长提示长度下,我们观察到 Qwen3-32B 的 TTFT 降低了 1.6 倍,zai-org/GLM-5-FP8 降低了 3.3 倍;结果取决于工作负载和部署配置。要在您的环境中实现这一点,请使用 vLLM v0.21.0 或更高版本并配合 NixlConnector,在 prefill 和 decode 服务器上均启用 bidirectional_kv_xfer,使用 KV 缓存感知路由器,并遵循 复现指南。
致谢
我们感谢 Nicolò Lucchesi(Mistral)、Mark McLoughlin(Red Hat)以及 vLLM 社区在维护此功能方面给予的支持。
附录:复现我们的结果
拓扑结构为两台 AWS p5en 实例(prefill 在 :8100,decode 在 :8200)并启用 EFA,另有一台小型节点在 :8000 上运行代理和基准测试客户端。
本文附带三个脚本:serve_pd_multiturn.sh 在开启或关闭该功能的情况下启动任一侧,run_multiturn_sweep.sh 驱动提示长度扫描,cleanup.sh 拆除部署。
测试环境
两台 GPU 节点的配置完全相同:
| 实例类型 | p5en.48xlarge(8 块 NVIDIA H200),一台用于 prefill,一台用于 decode |
| 区域 / 可用区 | ap-south-1(孟买),ap-south-1b(可用区 ID aps1-az3),两个节点位于同一可用区 |
| 放置 | 无集群放置组;两台实例均启动到定向的按需容量预留中,默认租户 |
| AMI | 适用于 Ubuntu 24.04 x86_64 的公共 AWS ParallelCluster 3.14.2 镜像,aws-parallelcluster-3.14.2-ubuntu-2404-lts-hvm-x86_64-202602121713(ami-0133f195033a4baae 位于 ap-south-1;AMI ID 因区域而异)。它附带内核 6.14.0-1018-aws、NVIDIA 驱动 570 和 CUDA 12.8,因此请将驱动、CUDA 工具包和 EFA 安装程序升级到以下版本。 |
| 操作系统 / 内核 | Ubuntu 24.04.4 LTS,6.14.0-1018-aws |
| NVIDIA 驱动 / CUDA | 驱动 580.126.16(CUDA 13.0 驱动 API);CUDA 工具包 13.0 |
| EFA 接口 | 每实例 16 个,每张网卡一个(网卡 0-15,所有接口类型均为 efa),每个 200 Gb/s,总计 3.2 Tb/s。每个 NUMA 节点上有 8 个接口,每块 GPU 与其中 2 个共享一个 PCIe 交换机。 |
| EFA 软件 | EFA 安装程序 1.50.0,EFA 内核模块 3.3.0g,libfabric 2.6.0amzn1.0,rdma-core 64.0amzn0 |
NIXL 的 EFA 设备选择。 LIBFABRIC 后端使用 efa provider 将所有 16 个 EFA 设备作为 rail 打开。然后它仅在该 GPU 共享 PCIe 交换机的那 2 个 rail 上注册每块 GPU 的 KV cache,因此每次传输都保持在 PCIe 本地。未设置任何 NIXL 或 libfabric 设备覆盖;这是默认选择。要在您自己的节点上确认这一点,请使用 NIXL_LOG_LEVEL=DEBUG 启动服务器并检查注册行:
grep -E "Created rail [0-9]+ \(device=|mapped to rail" server.log1. 配置节点
在两台 GPU 节点上安装 EFA 用户态栈并确认 provider 存在。我们使用了安装程序 1.50.0:
curl -O https://efa-installer.amazonaws.com/aws-efa-installer-1.50.0.tar.gz
tar -xf aws-efa-installer-1.50.0.tar.gz && cd aws-efa-installer
sudo ./efa_installer.sh -y
fi_info --version # libfabric 2.6.0amzn1.0
fi_info -p efa -t FI_EP_RDM # verify the EFA provider is present on both GPU nodes2. 在两台 GPU 节点上安装 vLLM 和 NIXL
python3 -m venv ~/venv-pd && source ~/venv-pd/bin/activate
pip install --upgrade pip && pip install vllm==0.29.0 nixl==1.4.1在两台节点上各应用一次 vLLM PR #40186。它将 kv_transfer_params 附加到 streaming 响应上,这正是多轮代理关联各轮所需:代理始终使用 stream=True 查询 decode 节点,因此如果没有此补丁,decode 节点永远不会返回块元数据,每一轮都会变成缓存未命中。
SITE=$(python -c 'import site; print(site.getsitepackages()[0])')
curl -sL https://github.com/vllm-project/vllm/pull/40186.diff -o /tmp/40186.diff
patch -p1 -d "$SITE" --fuzz=3 < /tmp/40186.diff3. 下载模型
export HF_HOME=/fsx/huggingface # a large shared volume
hf download Qwen/Qwen3-32B4. 启动 P/D 对
在每个 GPU 节点上运行一次 serve_pd_multiturn.sh。BIDIR 是 A/B 两侧之间唯一变化的东西,且必须在两个节点上保持一致:
# on the prefill node
P_IP=<P_IP> D_IP=<D_IP> ROLE=prefill BIDIR=false ./serve_pd_multiturn.sh
# on the decode node
P_IP=<P_IP> D_IP=<D_IP> ROLE=decode BIDIR=false ./serve_pd_multiturn.sh在 prefill 侧展开为:
export FI_PROVIDER=efa
export VLLM_NIXL_SIDE_CHANNEL_HOST=<P_IP> VLLM_NIXL_SIDE_CHANNEL_PORT=5600
vllm serve Qwen/Qwen3-32B --served-model-name Qwen --dtype bfloat16 \
--tensor-parallel-size 8 --max-model-len 32768 --max-num-seqs 8 \
--no-enable-prefix-caching --port 8100 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_buffer_device":"cuda",
"kv_role":"kv_producer","engine_id":"prefill-engine-001","kv_rank":0,
"kv_parallel_size":2,"kv_connector_extra_config":{"backends":["LIBFABRIC"],
"kv_recompute_threshold":64,"decoder_kv_blocks_ttl":480,
"bidirectional_kv_xfer":false}}'--no-enable-prefix-caching 仅在 prefill 节点上设置——这就是基准测试模拟的缓存被逐出的场景。在 decode 节点上保持前缀缓存启用。等待两个 /health 端点返回 200。
5. 启动多轮代理
python3 -m venv ~/venv-proxy && source ~/venv-proxy/bin/activate
pip install fastapi uvicorn httpx pandas numpy aiohttp transformers tqdm
sudo dnf install -y jq || sudo apt-get install -y jq
git clone --depth 1 --branch v0.29.0 https://github.com/vllm-project/vllm.git ~/vllm_src
export VLLM_SRC=~/vllm_src
python3 $VLLM_SRC/examples/disaggregated/disaggregated_serving/disagg_proxy_multiturn.py \
--port 8000 --prefiller-host <P_IP> --prefiller-ports 8100 \
--decoder-hosts <D_IP> --decoder-ports 8200 \
2>&1 | tee ~/proxy_$(date +%Y%m%d_%H%M%S).log代理接收主机和端口,而不是 URL。复用由客户端发送 conversation_id 驱动,因此这里没有可启用的标志。
6. 运行 OFF 侧,然后运行 ON 侧
VLLM_SRC=~/vllm_src LEG=off ./run_multiturn_sweep.sh对于 ON 侧,停止两个服务器和代理,在每个节点上使用 BIDIR=true 重新启动步骤 4,重启代理(其对话缓存是进程内的),然后:
VLLM_SRC=~/vllm_src LEG=on ./run_multiturn_sweep.sh对 GLM-5-FP8 使用 MODEL=zai-org/GLM-5-FP8 SERVED_NAME=GLM 重复两侧。
7. 收集数据
三个量,每个来源一个:
- TTFT,来自基准测试由
--stats-json-output写入的每请求统计 JSON。每条记录都带有ttft_ms以及input_num_tokens和conversation_id。这是 TTFT 图表的 y 轴。 - Prefill 时长,来自代理日志,该日志将每个请求的 prefill 阶段报告为
Prefill done in <N>ms。这是 prefill 时长分解中每个柱状条的总高度。 - KV 传输时间和传输量,来自 prefill 节点的日志,该日志报告
KV Transfer metrics:行,包含平均和 P90 传输时间、每次传输的字节数以及吞吐量。这是每个 ON 柱状条中的 KV-xfer 组件。
NIXL 遥测由发起传输的一方记录,因此 D->P 回读出现在 prefill 节点的日志中,而普通的 P->D 拉取出现在 decode 节点的日志中。
按提示长度对每个量求平均,然后绘图:将 TTFT 对提示长度绘制为两条线(ON 和 OFF),并将 prefill 时长绘制为按提示长度成对的堆叠柱状条,其中 ON 柱状条拆分为 prefill 计算和 KV 传输。两种视图之间的差异正是关键所在——柱状条所揭示的传输开销,正是 TTFT 曲线所转化出的节省。
故障排查
- 代理在 ON 运行中始终记录
cache MISS——缺少--send-conversation-id、PR #40186 未在两个节点上应用,或BIDIR=false遗留在 decode 节点上,此时它完全不返回kv_transfer_params。 - prefill 节点上出现
Declining expired remote read——decode 节点的块在下一轮到达之前已失效。提高decoder_kv_blocks_ttl(默认 480 秒),并保持代理的缓存 TTL 低于该值。 fi_info -p efa为空或回退到套接字——缺少 EFA provider。重新安装aws-efa-installer并确认实例启动时启用了 EFA 接口;否则 NIXL 要么初始化失败,要么静默地通过 TCP 运行,这会使传输数据失去意义。
清理
./cleanup.sh proxy # on the proxy / benchmark driver node
./cleanup.sh server # on each GPU node然后终止实例并确认没有实例仍在运行。
来源:vLLM Blog · vllm.ai