NVIDIA Dynamo 用会话级 ID 优化智能体推理的路由、准入控制与 KV 缓存
NVIDIA Dynamo 通过会话级 ID 优化智能体推理:路由、准入控制与 KV 缓存管理
NVIDIA Dynamo 团队介绍用统一的会话级标识符 session_id 把请求级推理服务升级为程序感知系统,覆盖会话感知路由、共享 KV 索引与程序化缓存迁移,并兼容 vLLM 和 SGLang。
PyTorch:Blog· Ishan Dhanani, Jamie Li, Karen Chung·2026-10-09 02:13· 46 分钟前AI 评分58
基于 NVIDIA Dynamo 的会话感知智能体推理
AI 导读
NVIDIA Dynamo 引入统一的会话级标识符 session_id,把请求级推理服务升级为程序感知系统,覆盖会话感知路由、共享 KV 索引与程序化缓存迁移,兼容 vLLM 和 SGLang。
正文
TL;DR
智能体工作负载改变了推理服务器所面对的流量形态。与单轮对话不同,一个智能体会话可能涉及一次大规模的初始预填充、反复的模型调用,以及并行运行的子智能体,同时 KV 缓存保持驻留,而工具在各轮之间执行。
这篇技术博客详细介绍了 NVIDIA Dynamo 如何使用统一的会话级标识符,将请求级服务基础设施转变为程序感知系统,从而在 vLLM 和 SGLang 上实现会话感知路由、共享 KV 缓存索引以及程序化缓存迁移。
引言
智能体工作负载已经改变了推理服务器所面对的流量形态。一个编码会话以一次大规模预填充开始,通常是数万 token 的系统提示、工具定义和用户特定指引,之后的每一轮都会追加到一个会话会完整重发的上下文中。一个任务会扩展为数十次这样的调用,同时还有短生命周期和长生命周期的子智能体并行运行。一个会话的大部分墙钟时间都花在工具调用之间,其上下文驻留在 KV 缓存中,而没有任何生成发生。要很好地服务这些负载,关键在于系统能保持驻留并复用多少上下文,以及它在仍满足延迟目标的前提下能同时承载多少个会话。智能体工作负载与标准聊天机器人之间的这种对比如图一所示。

图 1:对比标准聊天机器人交替的用户与模型轮次,与包含工具调用和工具响应的智能体工作流。在智能体工作流中,单个用户请求可以触发多次推理调用,后续步骤依赖于中间结果。
大多数开源服务栈仍然是为图中上方的模型而构建的。它们各自独立地路由、准入和缓存每个请求,没有将请求与之前轮次关联起来的 harness 或会话的概念。
以最优方式服务这种形态,正是我们一直在努力构建的方向。在之前的文章 Full-Stack Optimizations for Agentic Inference 中,我们分享了 NVIDIA Dynamo 技术栈三个层面的工作:路由器、推理引擎和 KV 缓存管理器。在过去几个月里,我们围绕一个单一原语统一了这些工作:一个通用的会话级标识符。会话级 ID(在论文和学术界常被称为 program 或 trajectory)将请求感知基础设施转变为程序感知基础设施,并作为额外会话感知优化的基础。
本文分四个部分介绍这一进展:
- Session_ID:Dynamo 如何从现有的 harness 请求头中识别会话和子智能体,以及自定义 harness 如何提供相同的信息。
- 追踪与回放:与会话关联的追踪如何将 harness 活动与推理性能联系起来,并支持离线模拟和实时基准测试。
- 会话感知调度:路由和准入控制如何考虑会话工作集,并在工具边界施加背压以减少缓存抖动。
- 共享 KV 缓存感知:实验性的共享池索引器如何让路由器在放置请求时考虑外部存储中可复用的 KV。
- 可编程 KV 缓存:Session-Prefix Indexer 和拟议的 KvHint 接口如何支持跨内存层的会话感知缓存策略,由 Dynamo 提供策略意图,推理引擎控制执行。
使用 Session ID 识别 Agent 会话
Dynamo session ID 是单个 agentic 推理与工具调用链的唯一稳定标识符。给定轨迹中的每个 LLM 请求都共享相同的 session_id。通常,子会话携带一个 parent_session_id,因此追踪和重放工具可以建立 agent 与其生成的子 agent 或子任务之间的联系。本文中的每一项优化都是对 session ID 和 parent session ID 中所含会话信息的可选消费者。
根据调用方的不同,有几种不同的方式与 Dynamo 共享稳定的会话标识符。
开箱即用地支持编码 agent
Dynamo 识别当今流行编码 agent 已经发出的身份标头,并将每个标头映射到相同的内部 agent_context:session 标头变为 session_id,parent 标头(如果存在)变为 parent_session_id。Claude Code、Codex 和 OpenCode 无需配置即可使用。
| 编码 agent | Session 标头 | Parent 标头 |
|---|---|---|
| Claude Code | x-claude-code-session-id(根),x-claude-code-agent-id(子 agent) |
x-claude-code-parent-agent-id |
| Codex | thread-id |
x-codex-parent-thread-id |
| OpenCode | x-opencode-session-id |
x-opencode-parent-session-id |
我们正在与整个生态系统的 harness 提供商合作,使其在所有地方都成为默认设置,从而无需用户做任何更改即可扩展支持。Dynamo 的 CI 会跟踪每个一等 harness 的变化。你可以在这里找到受支持的一等 ID 的完整列表。
为不原生发送会话身份的 harness 提供的插件
对于自身不发出兼容标头的 harness,agent-plugins 仓库提供了小型集成,将 harness 的原生会话身份转换为 x-dynamo-session-id。目前有三个可用:用于 Dynamo 的 OpenAI 兼容端点的 Pi provider、映射 Hermes 的 session_id 的 Hermes 中间件,以及映射 OpenClaw 的 sessionId 的 OpenClaw provider。每个插件都在仓库中各自的文件夹内附带自己的安装、配置和验证步骤。
自定义 harness:用一个标头选择加入
自定义 harness 通过一个规范标头 X-Dynamo-Session-ID 选择加入。Dynamo 将其规范化为内部 agent_context 结构,供堆栈的其余部分读取,因此下游无需知道请求来自哪个 harness。如果 harness 支持子 agent,你还可以提供 X-Dynamo-Parent-Session-ID。以下代码块展示了任何自定义 agent 如何在不采用专有 API 的情况下获得 Dynamo 的优化。
curl http://localhost:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-dummy' \
-H 'x-dynamo-session-id: research-run-42:researcher' \
-d '{"model":"my-model","messages":[{"role":"user","content":"..."}]}'
Harness
推理协同设计
Agent 可观测性工具已经可以捕获 harness 层面发生的事情,但它们尚未展示每个步骤之下推理堆栈表现的统一图景。我们将这一差距视为 harness
推理协同设计:为双方提供同一请求的共享、可度量的追踪,可用于性能可视化、重放和离线模拟,全部以常量 session ID 为键。此追踪旨在轻量。它可以作为额外的可选元数据,与其他追踪收集格式(如 Harbor 的 ATIF 或 Letta 的新 Trajectory 格式)一起使用。
捕获请求追踪
要启用跟踪收集,请在部署时使用 DYN_REQUEST_TRACE=1 运行。Dynamo 会自动记录每个请求的信息,默认情况下不会存储提示、响应或工具调用内容(这些内容可以选择启用)。相反,Dynamo 会在每个响应流结束后发出一个 request_end 记录,其中包含会话字段、输出 token 数量、结束原因、KV 缓存指标、工具调用名称,以及一个包含请求输入长度和逐块序列哈希的重放块,类似于 Mooncake 跟踪格式。序列哈希可用于使用内置的 dynamo.replay 或我们的负载生成工具 AIPerf 等工具进行重放。

图 2:展示了 dynamo 跟踪的屏幕截图,其中为代理及其工具调用设置了单独的轨道。预填充、解码和工具执行出现在同一时间线上,显示了工作如何排序以及在会话中如何重叠。
由于每条记录都带有时间戳和结束原因,我们可以看到工具调用分布,并将其与 KV 缓存逐出行为联系起来。在内部,我们使用这些技术端到端地优化模型部署,将捕获的跟踪用作回测数据。这些数据和这些重放工具中的大部分也为我们的内部自动研究框架提供支持,这些框架让 AI 生成新颖的路由策略并研究主动 KV 缓存卸载策略,我们将在下面介绍。
重放代理跟踪
捕获的跟踪是可重复使用的基准。由于它记录的是代理发送的工作负载,而不是代理的决策,因此一次捕获可以任意多次重放相同的请求计划,而无需重新运行模型、工具或任何外部 API。重放与跟踪一样保持无内容:序列哈希变为合成块 ID,在保留跨轮次和会话的提示前缀共享的同时,不暴露原始 token。
一次捕获驱动两条路径。离线时,AISimulate 针对 Dynamo 的模拟调度器、路由器和 KV 缓存运行请求图,让你无需花费 GPU 时间即可比较工作节点数量、服务拓扑、路由策略和缓存容量。在线时,AIPerf 在真实 GPU 上针对真实的 Dynamo 端点重放同一图,以获得最终的延迟、吞吐量和缓存数据。
会话层的路由和准入控制
KV 缓存对于高效的 LLM 推理一直很重要,但代理推理增加了真正的复杂性:缓存位于何处、在每个内存层级中可以重用多少,以及系统是否主动进行优先级排序、逐出和共享。吞吐量最终更多地取决于堆栈如何回答这些问题,而不是原始计算能力。
由于 Dynamo 路由器足够灵活,可以作用于不仅仅是下一个请求,我们能够迭代针对这些特性的会话感知路由和准入控制策略,从而提高服务代理形态工作负载(包括 RL 后训练)的性能。目标是尽可能多地重用 KV 缓存并避免抖动:因为 HBM 以及最终的 DRAM 无法同时容纳每个代理的工作集,所以对每个请求进行逐出和重新预填充。
请求级路由的问题
请求级路由器(包括 Dynamo 的默认 KvRouter)一次解决一个请求的放置问题:给定一个提示,哪个工作节点具有最多的缓存重叠。对于单个轮次来说,这是正确的单位,但对于代理来说,这是错误的单位,并且在高并发下,它会忽略请求背后代理的两件事:
- 缓存占用爆炸。在轮次之间,agent 的缓存不会被释放。它驻留在 HBM 中占用块,而 agent 在 GPU 之外运行工具调用。当 N 个 agent 在第 k 步并发时,聚合工作集为
N × context_k,而请求级路由器对此聚合毫无概念。它一次只能看到一个轮次的上下文。一旦工作集超过 HBM,进而超过 DRAM,引擎就开始驱逐活跃会话的缓存,之后每个轮次都要付出完整的重新预填充代价,而不是缓存命中。 - 无工具边界背压。当 worker 超容量时,请求级路由器唯一的手段是取消或排队在途请求。两者都比替代方案更糟:在会话即将进入下一轮之前、即将空闲的暂停点推迟该会话。
会话感知准入控制
我们的首个 agent 感知路由策略将 ThunderAgent 论文(Kang 等,2026)中的调度器移植到 Dynamo 的 KV 感知路由器之上。它用 Rust 编写,直接拥有一个 KvRouter,而不是作为额外代理跳数位于其前面,并且它从每个响应中跟踪真实的 prompt_tokens + completion_tokens,而不是从原始字节估算 token 数。由于这是作为原生 Dynamo 插件编写的,你可以获取其源代码并根据你的工作负载形态进行调整,而无需更改 Dynamo 的底层路由器代码。
调度器按 program_id(会话 ID)对请求分组,并让每个程序经过两个独立状态:(REASONING | ACTING) × (ACTIVE | PAUSED)。程序在工具边界进入 ACTING。在内存压力下,调度器在逻辑上暂停 ACTING 个程序。没有解码抢占;在途轮次被允许完成,这使引擎可以驱逐它们的 KV 块。如果 worker 没有剩余的 ACTING 程序可暂停,它改为将最小的 REASONING 程序标记为在其下一个工具边界暂停。当 worker 再次有余量时,调度器优先恢复 token 数最小的已暂停程序,将它们重新装箱到阈值以下。
控制循环
暂停和恢复都由每个 worker 的一个量驱动:利用率,即 worker 的 KV 池中被其分配程序的工作集占用的比例:
U_worker = (sum of token weights for ACTING programs on worker) / (worker's KV pool capacity)
三个阈值(显示默认值)将该数字转化为控制循环:
- U ≥ 0.95(暂停阈值):worker 超订。该 tick 首先暂停最小的 ACTING 程序,直到 U 回落到 0.80(暂停目标)。
- 0.80 ≤ U < 0.95(软降级带):程序尚未暂停,但会承受 -2.0 的优先级惩罚,作为需要硬暂停之前的早期背压。
- 仅当 U ≤ 0.85 时才触发恢复:即暂停阈值减去 0.10 的恢复迟滞,这是暂停线以下的缓冲区,可防止循环在阈值处反复暂停和恢复。
恢复本身是一次装箱过程:已暂停程序按 token 数升序排序,调度器从小到大接纳它们,直到下一个会再次将利用率推高至阈值以上,同时考虑固定的每程序缓冲区。恢复的请求获得一秒的优先级提升,以免卡在新准入之后,而 30 分钟的强制恢复上限保证没有程序会无限期饥饿。
结果
在中高并发下,我们观察到吞吐量大幅提升。这一差异直接体现在吞吐量上。在 SWE-bench 上,于单台 8xH100 节点上运行两个 TP4 MiniMax-M2 副本时,程序感知调度相比仅使用 KV 路由将吞吐量提升了约 12-16%,差距来自请求级路由无法避免的重新预填充工作。
同样的策略在智能体 RL 推演中也带来了类似的大幅提升。在 8xH20-3e 上使用 Qwen3-Coder-30B-A3B-Instruct 针对 VERL 默认的 Global LB 运行 Uni-Agent SWE-Bench 时,我们的策略在低并发(CC32-128)下大致持平,此时无需任何暂停。在中等并发(CC192-256)下它开始领先,模型 token 吞吐量比 Global LB 高出 11.0-14.6%;而在高并发(CC384-512)下,Global LB 吞吐量急剧下降,而我们的策略则持续扩展。在整个扫描范围内,它将前缀缓存命中率保持在 94.5% 以上。

图 3:比较了 VERL Global LB、Dynamo Python TA 和 Dynamo 原生会话感知调度器在不同智能体准入并发级别下的模型 token 吞吐量。在高并发下,原生调度器在三者中保持最高吞吐量,而 Global LB 吞吐量急剧下降。
路由器中的共享 KV 缓存感知
一旦我们的工作集超出 HBM,KV 缓存放置就变得更加困难。缓存并不会消失,它会移入 CPU 内存,然后进入像 Mooncake 或 NIXL 支持的存储这样的共享存储中。到目前为止,Dynamo 路由器只能感知 GPU 和原生框架 CPU 卸载内存。当 KV 移入外部存储时,Dynamo 无法将其纳入成本函数本身。为了解决这个问题,我们与 Mooncake 团队合作,从 Mooncake Store 原生启用 KV 事件发射,并实现了一个新的实验性共享池索引器,与 Dynamo 极快的 FlashIndexer 协同工作。当 KV 从 HiCache(SGLang 的原生 CPU 卸载机制)移入 Mooncake 时,SGLang 会发出移除事件,而 Mooncake 会发出存储事件。只有当工作器并行布局的每个物理对象都存在时,页面才计为命中,并且信用适用于其设备本地前缀之外。
在请求时,Dynamo 将提示哈希为 SGLang KV 页面,并将每个逻辑页面扩展为工作器 TP/PP 布局、K/V 张量和可选后端标签所隐含的物理 Mooncake 对象。Mooncake 发布有序的 Store 和 Remove 事件,Dynamo 将它们消费到全局共享池索引中,仅当每个所需物理对象都存在时才保守地将页面视为可重用。该索引将共享命中返回为连续页面范围,而不是在路由热路径上探测 Mooncake。FlashIndexer 独立计算每个候选工作器的原生设备、CPU 和磁盘前缀重叠。对于设备驻留前缀深度为 D 的工作器,路由器仅对位置 >= D:shared_beyond_device = |shared_hit_ranges ∩ [D, request_pages)| 处的共享命中给予信用。这防止了 GPU 本地命中被重复计算,同时允许本地前缀较短的工作器获得 Mooncake 中已可用后缀的信用。最终放置分数结合了加权原生层重叠、可配置的共享池信用和实时工作器负载。
迈向程序化 KV 缓存
最终,要在智能体工作负载上实现高性能推理,需要具备会话感知的 KV 块管理。要预测此类工作负载中给定 KV 缓存块的优先级值,我们需要来自路由器和引擎的互补信息。路由器了解会话生命周期以及跨工作进程的复用模式;引擎则单独了解其持有的 KV 缓存状态。因此,为了让引擎(vLLM、SGLang)执行最大程度知情下的缓存策略,(1) Dynamo 应表达策略意图,(2) 引擎应基于确切的缓存状态解析该意图并执行它。
这就是可编程 KV 缓存的核心思想。本质上,Dynamo 路由器提供“大脑”,负责计算智能的、会话/负载/缓存感知的 KV 移动策略,而引擎则“执行”Dynamo 所要求的移动。因此,我们一直在为 Dynamo 和 vLLM/SGLang 开发功能:Dynamo 计算并发出 KV 策略的机制,以及引擎中所需的一流抽象,以将 Dynamo 基于会话的策略降为具体的、引擎内的 KV 移动。
路由器驱动的 KV 提示策略
Dynamo 的可编程 KV 缓存移动由一个狭窄的、由路由器发起的提示接口(“KvHint”)驱动,该接口使 Dynamo 能够将 KV 缓存意图传递给 vLLM/SGLang。这避免了对引擎原生调度器和缓存管理器进行侵入式修改。(SGLang 设计,vLLM 设计)。
编排器掌握着请求本地策略无法看到的信息。哪些会话仍然活跃,哪些 token 范围是共享前缀还是唯一尾部,工具调用间隙会持续毫秒还是分钟,子智能体何时打开和关闭。这些知识属于路由器层,因为 harness 契约和会话生命周期已经存在于那里,并且应该留在那里。引擎的职责是暴露机制,让路由器提供意图。
该提案是一组狭窄的软提示,路由器可以将其发送给引擎。以下是潜在 KvHint 及其功能的部分分类:
| KvHint 名称 | 功能 | 示例用途 |
|---|---|---|
| Share | 通过 p2p 传输将缓存前缀从一个工作进程移动到另一个工作进程 | 新的引擎工作进程从现有工作进程的缓存中预热其缓存。 |
| Prefetch | 在需要之前将 KV 从更高层级(例如 DRAM)获取到更低层级 | 当子智能体接近完成时,将主智能体的上下文重新加载到 HBM 中。 |
| Demote | 将 KV 从更低层级推送到更高层级。 | 长时间工具调用,或暂停的轨迹。 |
| Pin | 将高价值 KV 缓存固定在其当前层级,持续有限的 TTL | 固定所有请求共享的系统缓存。 |
| Retain | 使 KV 块在缓存压力下更(或更不)可能被保留。 | 在工具调用期间保留子智能体的 KV 块。 |
每个提示都携带会话 ID,因此接收 KV 的层级知道它属于哪个会话,并可以将其作为一个单元进行分组、放置和回收,而不是作为匿名块。这在较冷的层级中最为重要,因为持有主机内存或磁盘的存储后端否则只能看到不透明的哈希,无法区分活跃轨迹的前缀和已完成轨迹的前缀。引擎可以裁剪、推迟或忽略任何提示,而不发送提示的工作负载的行为与今天完全相同。
Share 已经有一个可工作的实现,以最小的调度器钩子分层到 HiCache 中,而 Prefetch 和 Demote 的设计是在 Share 落地后构建在该机制之上。
使用会话前缀索引器启用 KV 提示策略计算
为了让 Dynamo 在会话级别组合并执行这些 KvHint 策略,它必须拥有一个持续更新的结构,能够在 KV 块、其会话身份以及会话内位置谱系之间进行双向映射。为此,我们实现了 Session-Prefix Indexer,这是一种基数树结构,其中每个分支代表一个唯一的会话(或从另一个会话分支出的子代理会话)。
Session-Prefix Indexer 持续从推理引擎的 KV 事件(Stored / Removed)以及 Dynamo 路由器主索引器(“FlashIndexer”)的前缀缓存命中中获取数据。因此,通过 FlashIndexer 和 Session-Prefix Indexer,Dynamo 路由器能够累积地了解(1)推理引擎工作进程的块所有权,以及(2)这些相同块的会话归属。这种信息丰富的视图可以被各种 KV 提示策略所使用,这些策略需要关于 KV 块的工作进程位置、活动会话生命周期和/或引擎工作负载的信号。
借助这些索引器,我们正在使用 Dynamo vLLM/SGLang 在代理工作负载数据集上积极实验 KvHint 策略。

图 4:Session-Prefix Indexer 在 Dynamo 路由器内跟踪共享前缀、会话谱系和 KV 块位置。来自推理工作进程的 KV 事件更新此视图,支持路由决策以及向 vLLM 或 SGLang 发送可选 KV 提示的策略。
展望未来
高效的代理推理取决于在整个会话生命周期中管理上下文。Dynamo 的会话 ID 将 harness 行为与路由、准入控制和缓存管理连接起来,为服务栈提供代理不断变化的工作集的共享视图。
会话关联的跟踪和重放使策略可度量,而会话感知的准入控制通过在工具边界施加背压来减少缓存抖动。在此测试的 SWE-bench 配置中,程序感知调度相比仅 KV 感知路由将吞吐量提高了约 12–16%。实验性的共享池索引器将路由可见性扩展到外部 KV 存储。
Session-Prefix Indexer 和提出的 KvHint 接口朝着主动缓存移动的方向构建:Dynamo 提供策略意图,引擎保留对执行的控制。关键要点是缓存重用和会话并发必须一起优化。在轮次之间保持有用的上下文可用,并随着并发增长避免重复预填充,是高效服务更多代理会话的核心。
文章 Session-Aware Agentic Inference with NVIDIA Dynamo 首次出现在 PyTorch。
来源:PyTorch:Blog · pytorch.org
来源:RadarAI · aihot.news