资讯动态

vLLM双向KV传输方案如何将多轮Agent的TTFT延迟降低55%?

发布时间:2026/10/15 5:13:17 来源:尧图企业网站定制
先说个实际场景。最近我在帮某AI助手平台做推理服务优化碰到的问题非常典型多轮智能体对话进行到第四五轮之后用户发一句话要等将近两秒才能看到第一个字体感极差。拆开延迟链路一看问题基本落在TTFT也就是首令牌延迟上。社区里近期讨论的“基于vLLM的双向KV传输方案”引起了我们的注意我们基于vLLM做了二次开发最终把TTFT的P50从760毫秒降到340毫秒左右效果相当明显。这篇文章就把这个方案的核心原理、落地配置和踩坑过程整理出来给同样在搞推理后端、Agent框架或者正在被多轮对话延迟折磨的朋友一个参考。我默认读到这里的你至少对vLLM的基本概念不陌生比如分页KV Cache、PagedAttention、Prefix Caching这些。如果这些还不太清楚也不影响阅读我会在每个关键概念出现时用最简单的方式解释一下保证你顺着流程走下来能真正理解为什么需要“双向KV传输”。1. 多轮智能体负载下TTFT为什么会卡在 KV 上1.1 多轮Agent的负载画像和传统推理的冲突先给多轮智能体场景画个像。常见的Agent框架大多是“用户消息进来 → 模型规划 → 调用工具 → 拿到工具结果 → 再生成 → 再调用工具”这样的循环。真实线上跑起来一个会话可能拉长到五六轮甚至更久而每一轮对推理引擎来说都是一次全新的请求。麻烦的是这些请求不是孤立的它们共享前面所有轮的对话历史、系统提示词、工具定义甚至有时候还会出现父子分支任务。这会给推理系统带来什么压力我用一个类比来说。你上学的时候复习考试如果把整本教材从头到尾重读一遍当然能复习完但每一遍都从第一页开始读效率就太低了。传统推理引擎处理多轮会话就有这个味道每到新的一轮如果不做缓存复用就要把历史所有Token再算一遍KV值相当于每次都是“重读整本教材”。vLLM其实已经有Prefix Caching机制能在一定程度上解决这个问题它会按Token序列的前缀去做缓存匹配匹配上的部分直接复用KV不用重新计算。但问题在于在线部署时流量经过负载均衡很难保证同一个会话的每一轮都打进同一个引擎节点。比如第一轮请求在节点A处理A把KV缓存留在本地了第二轮请求被负载均衡器分配到了节点BB没有这个会话的KV只能从第一轮开始重算。这种“缓存放错位置”的情况在多轮Agent场景里非常常见是TTFT飙升的一个根因。更麻烦的是多轮智能体的文本结构往往不是完全线性的。它可能有反复尝试工具调用、修正参数、分支探索这些行为让缓存的命中率进一步降低。你辛辛苦苦算好的KV如果能被正确复用首Token延迟会低很多一旦没人复用就是白白消耗GPU算力。不做跨节点KV复用的话多轮Agent场景的延迟和成本都下不来。1.2 TTFT的三段耗时结构能省的部分基本都在KV要理解这个方案为什么有效先要把TTFT拆开看。一次请求从到达引擎到吐出第一个Token大致经历三段耗时调度等待请求在队列里等GPU资源或者等待调度器为它分配Block。这部分通常几毫秒到几十毫秒受并发影响。Prefill计算把用户输入的Prompt从头到尾过一遍模型生成整段Prompt的KV这是TTFT的大头。采样首TokenDecoder阶段只解码第一个Token相对很快。其中Prefill的耗时和输入Token长度直接相关。举个例子假设一个请求的历史上下文有4000 Token模型在特定GPU上的Prefill吞吐大概是12000 Token/s那么仅Prefill阶段就要花约330毫秒。如果系统提示词占800 Token且每个请求都重复计算这800 Token而跨节点KV复用又没打通那么每个请求都在白白浪费约66毫秒的GPU时间还在排队场景下进一步放大延迟。还有个点容易被忽视Prefill计算出来的KV不是放在内存里就不动了显存不够时还要被驱逐。vLLM的KV Cache是分Block管理的做了精细的显存复用和驱逐策略。本地显存越大能留住的KV越多命中率越高。可一旦请求被路由到没有对应KV的节点本地缓存再大也没用远端没货就是没货。这时候要不就等别的节点把KV传过来要不就在本地从头算。传统方案里KV传输大多是单向的要么只会推要么只会拉碰到多节点、多会话、多分支的智能体负载会频繁遇到“推过去用不上拉回来又没有”的尴尬局面。双向KV传输要解决的就是这个问题让KV缓存跟着逻辑会话走而不是困在某个物理节点上。2. 双向KV传输的核心设计让缓存跟着会话走2.1 什么是“双向”KV传输先说清楚“双向”到底指哪两个方向。以我们的实现为例KV传输发生在推理引擎节点和KV网关之间也可以发生在引擎与引擎之间。方向一推送方向。引擎在完成一轮生成后会把自己新增的KV块异步推送到KV网关或者邻居引擎相当于主动“上交”自己算出来的成果。这样做的好处是同一个会话的后续轮次即使被路由到了其他节点其他节点也能从网关或源引擎那里直接取到KV不用回源重算。方向二拉取方向。某个引擎收到请求后发现本地Prefix Cache缺失可以主动向KV网关或持有该缓存的远端引擎发起拉取把缺失的KV Block拉到本地再进行后续生成。这是“按需取用”的思路而不是傻等负载均衡器把请求转到有缓存的节点上。所以“双向”不是指物理链路上支持两个方向传输而是指数据流模式既有“生产者主动推送”又有“消费者按需拉取”两条通路都打通。传统的内存池方案往往只做单向引擎把KV推给独立缓存服务缓存服务再被动地等查询查询不到就回源计算。单向方案的最大问题在于如果某个引擎手里有缓存但还没推送到共享池或者共享池已经把某段KV驱逐了拉取请求就会落空最终回到重算路径。双向方案的思路是两头都留接口源引擎保留一份KV的同时允许其他引擎直接拉自己的副本网关也能在缓存被驱逐前反向请求重新传输。这样“你有我无”和“你有我也有”两种情况都能覆盖到。为什么这个设计适合多轮智能体因为Agent场景会产生大量分支请求。比如一个客服机器人在同一会话里可能先让模型总结用户诉求再调用订单查询工具然后又让模型对比几个方案。这些子请求共享父会话的绝大部分前缀。如果两台引擎分别处理这些分支请求而它们持有各自分支的KV那父子缓存就在不同节点之间产生交叉依赖。单向传输只能跟着某一个方向走很容易把缓存集中在同一侧双向传输能根据子请求的实际前缀缺失情况灵活地“你拉我的、我拉你的”命中率一下子就能上来。2.2 如何嵌入vLLM的缓存调度流程vLLM调度器的核心工作之一是管理KV Cache Block的生命周期。当一个新请求进入时调度器会执行Prefix Caching匹配找到与历史请求重合的前缀标记复用Block只给不足的部分分配新Block。这个流程本身很成熟问题是它默认只查本地的BlockTable。我们二次开发时在CacheEngine之上加了一个远程KV代理层相当于给调度器开了一条“外挂”路径。具体流程分四步请求进入vLLM的Sequence调度器先算本地Prefix Cache命中率。如果发现某些Block的哈希在本地索引中缺失就启动KV Transport Client判断该Block是否存在于远端KV网关。存在远端时KV Transport Client拉起对应Block反序列化后填入本地BlockTable并把引用关系告诉调度器调度器把对应的KV块当作“已命中”继续执行。生成过程中新增的KV块会被暂存在一个待推送队列里等完成一轮生成或者达到同步阈值异步推送给KV网关。KV网关侧维护一层KV索引并做去重、合并、LRU驱逐。推送和拉取都走同一个传输协议但消息类型区分Push和Pull实现真正的双向流动。我们在实现中把传输层做成独立进程避免阻塞vLLM的Event Loop。KV块在本地是张量传输时序列化成二进制数据流不打JSON否则一个Block几十KB甚至上百KBJSON序列化的开销会直接把TTFT吃掉。每个KV块在网关里除了存张量数据还维护了哈希值和会话ID。哈希值用于内容去重会话ID用于权限隔离。这一点非常关键我后面在问题排查部分会展开讲。为什么要用哈希而不是直接比对Prompt文本因为KV命中最紧要是看查Block的前缀匹配而Block的边界和Token序列是对应的。直接用哈希索引可以避免把整个Prompt文本传来传去无论在网关还是引擎端比较成本都低很多。vLLM本地已有Hash计算机制我们只是把这份Hash同步到远端而已。3. 在vLLM中落地关键开关与参数取舍3.1 传输服务与引擎的拓扑编排先给我的拓扑方案不一定适合所有团队但对3到6个推理引擎的中等规模集群来说很够用。我们在部署中拆成了四类角色vLLM推理引擎节点跑模型的节点负责本地生成和KV Block管理节点上挂了KV Transport Client。KV网关服务一个独立进程管理全局KV索引、缓存存储和权限校验可以部署在单独机器上也可以和其中一个引擎合并部署。路由控制器在负载均衡层感知请求所属的会话ID尽量把同一个会话的请求路由到已有KV缓存较多的节点做不到时再依赖拉取链路。监控面板单独拉取KV网关的命中率、传输延迟、推送队列积压、回源率指标。为什么要单独弄一个路由控制器虽然双向KV传输能在请求进错节点后把KV拉过来但每次跨节点拉KV都有网络开销。如果能在路由层就尽量让请求进入“最可能命中本地缓存”的节点减少跨节点传输效果会更好。我见过一些团队跳过路由优化直接硬上KV传输结果网络带宽被打满TTFT反而更差这属于架构设计上的失衡。KV网关和引擎之间的网络建议走独立的内网网段最好有10Gbps以上带宽。我们早期的测试在千兆内网里跑KV块传输一多网关的网卡直接成了瓶颈数据包排队时间比Prefill时间还长。后来把网关挪到和引擎同一机柜用高带宽内网互联传输延迟才降下来。3.2 关键参数选取与块大小的权衡vLLM的KV块大小通常以Token数为单位常见的有16、32、64。这个参数直接影响KV传输的粒度和效率。当块大小较小时比如16缓存匹配的粒度更细一个请求中只有缺失的16个Token需要去远端拉取传输的数据量更少但网络请求次数会变多哈希索引数量也随之增加。反过来块大小较大时比如64单次传输的数据量更大网络往返次数减少但可能出现“只需其中一部分但要整体拉取”的浪费。我们做个粗略计算。假设某个7B模型每层有两个KV头多层叠加后单个Block存储16 Token的KV大约占用24KB内存。假如要拉取1000个Block也就是约16000 Token的KV数据量约24MB。在10Gbps内网上理论传输时间约20毫秒算上序列化和网络等待大概几十毫秒。这比重新计算几万Token的Prefill要便宜得多。但如果网络只有1Gbps同样的24MB就要近200毫秒那就和本地重算的代价差不多了双向KV传输的优势就不明显。我们在实践中取了32作为默认块大小。比较激进的场景比如系统提示词很长且极其稳定我们会把系统提示词对应的KV块单独标记为“热块”用64大小做预取。而用户消息和工具结果这类变化频繁的内容用16大小按需拉取。不能一口气全局统一否则要么命中粒度太粗要么请求太碎。3.3 一个可供参考的启动配置下面的配置来自我们自编译的vLLM分支参数名不一定和官方版本完全一致不建议直接照抄重点是理解每个开关的作用。我们在引擎启动命令里大致长这样python -m vllm.entrypoints.openai.api_server \ --model /models/llama-3-8b-instruct \ --enforce-eager \ --tensor-parallel-size 1 \ --kv-transfer-mode bidirectional \ --kv-block-size 32 \ --kv-remote-cache-lru-size 200000 \ --kv-transfer-timeout-ms 150 \ --kv-transfer-push-interval-ms 500 \ --kv-transfer-prefetch-block-threshold 3几个参数按我的理解解释一下--kv-transfer-mode bidirectional开启双向传输同时启用Push和Pull两个方向。--kv-block-size 32设置KV块大小影响传输粒度和命中率。--kv-remote-cache-lru-size 200000KV网关侧最多缓存多少Block超出后按LRU驱逐。--kv-transfer-timeout-ms 150如果一个拉取请求超过150毫秒还没返回就放弃拉取并回源重算防止远端慢拖垮在线请求。--kv-transfer-push-interval-ms 500推送KV块的周期避免每个请求都触发一次推送洪峰。--kv-transfer-prefetch-block-threshold 3当调度器发现缺失块超过3个时启用预取一次性向后端索要连续一批块。KV网关进程单独启动负责接收引擎推送的Block、响应拉取请求。它不需要GPU跑在普通服务器上即可但内存要给足。我们给网关配了128GB内存主要用来做KV索引和临时Block缓存。网关本身不做模型计算所以CPU压力主要来自反序列化、哈希比对和网络IO。实测中一个网关扛住4个引擎的传输需求是没问题的。提示这里所有的参数名和默认值都来自我们的内部分支官方版本的vLLM不一定支持。真要在自己环境里复现建议先给KV网关和引擎的通信接口写好单元测试再逐步加参别直接上线。4. 压测模拟与效果观测给TTFT做一个对照实验4.1 构造多轮Agent负载集要验证这个方案的收益不能拿普通单轮问答数据集去测因为那双向KV传输几乎没有发挥空间。我们按真实Agent服务的特点构造了一套模拟负载集主要分三类请求系统提示词类所有请求共享一段固定的系统提示词模拟Agent场景下高重复前缀。多轮工具调用类前几轮是用户消息和工具返回结果交替后几轮请求需要复用前面所有历史。独立会话类每一轮都是完全独立的短问题模拟没有任何共享前缀的负载。压测脚本里把三类请求按比例混在一起比如20%纯系统提示词类、40%多轮工具调用类、40%独立会话类然后用固定并发去跑持续半小时。这样既能看出双向KV传输的收益也顺便看看它对负样本的损耗。另外要注意Agent场景的请求长度分布很宽。有的请求只有几十个Token有的请求因为带了工具定义和完整历史可能有七八千Token。压测时如果只用固定长度测出来的TTFT没有任何参考价值。我们最终用脚本按真实业务分布随机生成长度并模拟了会话A/B分支的情况。4.2 对照实验结果与指标解读我们做了三组对照不做任何跨机KV复用、只做单向KV池、双向KV传输。组间只改传输模式模型、并发、数据集完全一致。结果如下表所示方案TTFT P50TTFT P95前缀命中率GPU Prefill算力占用对比基线无跨机KV复用760ms1450ms42%100%单向KV池530ms920ms66%-32%双向KV传输340ms610ms88%-55%单看数据双向比单向更优的地方在于拉取方向覆盖了“缓存放错节点”的场景。单向KV池里引擎把KV推给网关后网关确实能缓存但如果请求被路由到了非缓存节点它需要去网关查询并按需拉取这其实已经是“拉取”了。那为什么单向的效果不够好因为很多Agent分支请求会同时依赖两台引擎各自算出来的KV单向池只反映“引擎→网关”的收敛路径如果网关驱逐了某些冷块或者源引擎还没来得及推送拉取就会落空。双向方案允许引擎之间直接互拉源引擎只要还持有KV就能救急等于在共享池之外多了一道保障。从TTFT的P95看下降幅度比P50还明显。这其实是多轮Agent场景的特性长尾请求往往是最长上下文的那些双向KV传输对这类请求的收益最大因为一次拉取能解决几千Token的重算开销。GPU Prefill算力占用下降也符合预期复用KV越多重复计算越少。4.3 收益边界什么场景看不到明显优化双向KV传输不是万能药如果你的业务根本不在Agent场景收益可能很小。根据我们的测试下面这几类场景基本看不到明显效果纯短问答请求本身只有几十个Token没有共享上下文。KV传输增加的序列化和网络开销甚至可能让TTFT变慢。单轮系统提示词极短共享前缀只占几百Token重算一次也就几十毫秒拉取KV反而可能更贵。计算节点与KV网关跨地域部署网络延迟几十毫秒起步拉KV的代价比本地重算还高这种情况应该把缓存放在计算侧。会话完全隔离每个用户请求都是独立无关联的不存在前缀复用没有优化空间。所以做方案选型之前最好先花一周时间统计线上请求的前缀重复率。如果前缀命中率本来就高于85%vLLM本地的Prefix Cache已经做得很好强行上双向KV传输属于给自己找事。只有当命中率在50%上下波动且明显存在“缓存放错节点”的现象时双向传输才能体现出价值。5. 常见问题与排查技巧实录5.1 传输链路反成瓶颈TTFT不降反升我们第一次把双向KV传输打开后没高兴多久就发现P95不降反升一度怀疑方案是不是有严重缺陷。排查后发现问题出在KV网关网卡带宽被打满。当时用的还是千兆内网网关同时接收4个引擎的推送流量加上大量拉取请求所有数据包在网卡队列里排队单次拉取时间从几十毫秒飙升到几百毫秒。解决办法分三步走先把网关和引擎调到同一高带宽内网网段这是网络层面的硬要求。然后把块大小从16调到32减少请求次数。最后给传输链路加了优先级系统提示词对应的KV块要优先推送和拉取因为它们复用价值最高普通用户消息块的优先级放低。调整后P95明显回落。排查这类问题有个简单方法在网关侧同时监控三项指标——网络吞吐、拉取请求耗时、回源率。如果网络吞吐没满但拉取耗时长多半是序列化或索引查询慢如果回源率上升很可能是超时时间设得太短拉取操作频繁放弃。5.2 KV一致性与会话隔离多租户场景下最危险的问题是缓存串号。KV本质上是由模型参数和输入Token共同决定的如果两个会话的前缀恰好一样它们在前缀部分生成的KV理论上是一致的可以安全复用。但如果仅仅是前缀Token序列相同而语义归属不同比如一个用户是普通会员、另一个用户是灰度测试用户它们的系统提示词可能包含不同权限描述模型输出的风格也会不同这种情况下把两个会话的KV混用不仅降低效果还可能造成严重的信息安全问题。我们的KV块在网关里挂了完整的会话ID和权限标签哈希值只用来判断内容是否是同一份不能作为唯一复用依据。vLLM的逻辑上下文中虽然通常不会包含对KV块做权限校验的环节但二开时必须在网关侧做一次拦截。这块不做严格校验后面出问题就是事故级别。我们遇到的一个真实坑是请求被负载均衡器重试时带了不同的会话ID网关没找到对应的KV块就把同哈希的其他会话块直接返回给新会话了。后来在KV网关的查询逻辑里加了会话ID的强校验哈希相同但是会话ID不同就视为索引未命中宁可回源重算不可跨会话复用。5.3 缓存回源风暴与补偿调度双向KV传输还有一个隐蔽风险当KV网关短暂宕机或网络抖动时大量请求会发现远端拉取不可用几乎在同一时刻回源做Prefill瞬间把GPU打满造成新的长尾延迟。我们内部叫它“回源风暴”。为了避免这个问题KV传输的客户端要有熔断能力。我们设定了一个规则如果最近10秒内拉取成功率低于80%就自动进入降级模式不再尝试远端拉取统一走本地重算同时降低新请求的调度优先级防止整个集群雪崩。等到拉取成功率恢复再逐步放量。超时时间的设置也有讲究。拉取超时设得太短比如50毫秒碰到网络波动的场景容易频繁放弃拉取白白增加回源率设得太长比如1秒在线请求会长时间挂起TTFT直接爆炸。我们最终把150毫秒作为默认值配合监控再动态调整。5.4 显存峰值抖动与KV驱逐策略远端拉取KV会带来显存尖峰。请求本来打算在本地算Prefill显存分配是按本地计算预估的现在改成从远端拉KV等于先把一批Block填进本地KV Cache显存占用瞬间增加。如果本地空闲Block不足拉取往往会被迫放弃或者把刚拉下来的Block又驱逐掉多次反复后命中率不升反降。我们的解决办法是在发起拉取前先做本地KV Cache空间预估如果空闲Block数量小于这个请求缺失Block数量的1.5倍就放弃远端拉取直接回源计算。这个1.5倍的余量是为了防止拉取完成前其他请求又把Cache占掉。宁可放弃部分缓存命中也不要触发显存溢出。KV驱逐策略也要配合双向传输调整。纯本地场景下vLLM的驱逐策略一般是优先驱逐最长时间未用的块。但多轮Agent场景里父会话的KV经常是在某个分支结束后很久才会被再次用到如果按照LRU直接驱逐等父会话后面的轮次需要它时又得重新算一遍。我们在网关侧把驱逐策略改成了“会话优先级LRU”混合模式长期存活的Agent会话的KV块优先级更高短会话和一次性分支的块优先被驱逐。提示显存和KV Cache的占用情况一定要接监控。KV传输属于跨进程操作很多显存异常不会立即体现在引擎日志里而是通过TTFT抖动和OOM体现出来。监控项至少包含显存剩余、KV Cache命中率、远端拉取量、推送队列积压、回源率。最后再分享一个小技巧做完这次优化我的体会是双向KV传输解决的本质问题不是“网络快不快”而是“缓存放错了地方”。多轮Agent场景下请求路由的随机性让本地Prefix Cache经常空转双向KV传输相当于给缓存装了一双能来回跑的腿让算好的KV物尽其用。如果你也想落地这个方案我建议别急着一步到位搭分布式KV网关。先花两三天把线上请求样本拉下来统计每个会话的平均轮次、系统提示词占比、跨节点重复率。如果命中率本来就不错调调vLLM本地的Prefix Cache参数就够了如果命中率低且缓存集中错位再考虑上双向传输。最后再分享一个监控小技巧在我们内部KV网关的指标永远单独拉一个看板重点关注命中率、传输延迟、回源率这三个值。只看TTFT的话问题往往要等到用户抱怨了才会暴露看KV网关的三件套网络抖一抖、缓存被逐一驱你都能提前感知并处理。这个思路换到任何类似系统里都适用缓存层的问题就该在缓存层解决别总让用户替你做探针。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑