NVIDIA Dynamo 如何用会话级 ID 实现智能体推理
Session-Aware Agentic Inference with NVIDIA Dynamo
NVIDIA Dynamo 提出以统一的会话级 ID 把请求级服务基础设施改造成程序感知系统,支持会话感知路由、共享 KV cache 索引和跨 vLLM、SGLang 的程序化缓存迁移。
原文给出会话级 ID、追踪回放与调度策略的完整设计,可据此理解智能体推理服务栈的改造路径。
TL;DR
Agentic 工作负载改变了推理服务器所看到的流量形态。与单轮聊天不同,一个 agent 会话可能涉及大规模初始 prefill、重复的模型调用以及并行运行的子 agent,并且在轮次之间工具执行时 KV cache 仍保持驻留。
这篇技术博客详细介绍了 NVIDIA Dynamo 如何使用统一的会话级标识符,将请求级服务基础设施转变为程序感知系统,从而实现会话感知路由、共享 KV cache 索引,以及跨 vLLM 和 SGLang 的程序化 cache 移动。
引言
Agentic 工作负载改变了推理服务器所看到的流量形态。一次编码会话以大规模 prefill 开场,通常是数万 token 的系统提示、工具定义和用户特定指引,之后的每一轮都会追加到一个会话会完整重发的上下文中。一个任务会扇出为数十次这样的调用,同时还有短生命周期和长生命周期的子 agent 并行运行。一个会话的大部分墙钟时间都花在工具调用之间,其上下文驻留在 KV cache 中而没有任何生成发生。要服务好这些,关键在于系统能保持驻留并复用多少上下文,以及它在仍满足延迟目标的前提下能同时承载多少会话。Agentic 工作负载与标准聊天机器人之间的这种对比如图 1 所示。

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

图 2:展示了 dynamo trace 的屏幕截图,其中为 agent 及其工具调用分别设置了独立的轨道。Prefill、decode 和工具执行出现在同一时间线上,展示了整个会话中工作的排序方式和重叠情况。
由于每条记录都带有时间戳和结束原因,我们可以查看工具调用的分布,并将其与 KV cache 驱逐行为关联起来。在内部,我们已使用这些技术端到端地优化模型部署,将捕获的 trace 用作回测数据。这些数据以及这些重放工具中的大部分也为我们的内部自动研究框架提供支持,使 AI 能够生成新颖的路由策略,并研究主动式 KV cache 卸载策略,我们将在下文介绍。
重放 agentic trace
捕获的 trace 是一个可复用的基准。由于它记录的是 agent 发送的工作负载,而非 agent 的决策,因此一次捕获可以按需多次重放相同的请求调度,而无需重新运行模型、工具或任何外部 API。重放与 trace 一样保持无内容:序列哈希会变成合成块 ID,在保留跨轮次和跨会话的 prompt 前缀共享的同时,不暴露原始 token。
一次捕获驱动两条路径。离线时,AISimulate 会针对 Dynamo 的模拟调度器、路由器和 KV cache 运行请求图,让你无需消耗 GPU 时间即可比较 worker 数量、服务拓扑、路由策略和缓存容量。在线时,AIPerf 会在真实 GPU 上针对真实的 Dynamo 端点重放同一请求图,以获得最终的延迟、吞吐量和缓存数据。
会话层的路由与准入控制
KV cache 对于高效的 LLM 推理一直很重要,但 agentic 推理增加了真正的复杂性:缓存位于何处、在每个内存层级上可以复用多少,以及系统是否主动进行优先级排序、驱逐和共享。吞吐量最终更多地取决于技术栈回答这些问题的能力,而非原始算力。
由于 Dynamo 路由器足够灵活,能够对不止下一个请求做出反应,我们得以针对这些特性迭代会话感知的路由和准入控制策略,从而提升 agentic 形态工作负载(包括 RL 后训练)的服务性能。目标是尽可能多地复用 KV cache 并避免抖动:即因为 HBM 乃至最终的 DRAM 无法同时容纳每个 agent 的工作集,而对每个请求都进行驱逐和重新 prefill。
请求级路由的问题
请求级路由器,包括 Dynamo 自带的 KvRouter,一次只解决一个请求的放置问题:给定一个 prompt,哪个 worker 的缓存重叠最多。对于单个轮次来说,这是正确的单位,但对于 agent 来说却是错误的单位,并且在高并发下,它会忽略请求背后 agent 的两个问题:
- 缓存占用爆炸。在轮次之间,agent 的缓存不会被释放。当 agent 在 GPU 之外运行工具调用时,它会一直占用 HBM 中的块。当 N 个 agent 并发处于第 k 步时,总工作集为
N × context_k,而请求级路由器对此总量毫无概念。它一次只能看到一个轮次的上下文。一旦工作集超过 HBM,进而超过 DRAM,引擎就会开始驱逐活跃会话的缓存,而每个下一轮次都要付出完整的重新 prefill 代价,而不是缓存命中。 - 没有工具边界的背压。当 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 rollout 上也带来了类似的大幅提升。在 8xH20-3e 上使用 Qwen3-Coder-30B-A3B-Instruct 运行 Uni-Agent SWE-Bench,对比 VERL 默认的 Global LB,我们的策略在低并发(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 会发出存储事件。只有当 worker 并行布局的每个物理对象都存在时,页面才计为命中,并且信用适用于其设备本地前缀之外。
在请求时,Dynamo 将提示哈希为 SGLang KV 页面,并将每个逻辑页面扩展为 worker 的 TP/PP 布局、K/V 张量和可选后端标签所隐含的物理 Mooncake 对象。Mooncake 发布有序的 Store 和 Remove 事件,Dynamo 将其消费到全局共享池索引中,仅当每个所需物理对象都存在时,才保守地将页面视为可重用。该索引将共享命中返回为连续页面范围,而不是在路由热路径上探测 Mooncake。FlashIndexer 独立计算每个候选 worker 的原生设备、CPU 和磁盘前缀重叠。对于设备驻留前缀深度为 D 的 worker,路由器仅对位置 >= D:shared_beyond_device = |shared_hit_ranges ∩ [D, request_pages)| 处的共享命中计入信用。这防止 GPU 本地命中被重复计算,同时允许本地前缀较短的 worker 获得 Mooncake 中已可用后缀的信用。最终放置分数结合了加权原生层级重叠、可配置共享池信用和实时 worker 负载。
迈向可编程 KV 缓存
最终,在智能体工作负载上实现高性能推理需要会话感知的 KV 块管理。为了预测此类工作负载中给定 KV 缓存块的优先级值,我们需要来自路由器和引擎的互补信息。路由器了解会话生命周期和跨 worker 的重用模式;引擎单独了解其持有的 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 传输将缓存前缀从一个 worker 移动到另一个 worker | 新的引擎 worker 从现有 worker 的缓存中预热其缓存。 |
| Prefetch | 在需要之前,从更高层级(例如 DRAM)获取 KV 到更低层级 | 当子代理接近完成时,将主代理的上下文重新加载到 HBM。 |
| Demote | 将 KV 从更低层级推送到更高层级。 | 长时间工具调用,或暂停的轨迹。 |
| Pin | 将高价值 KV 缓存固定在其当前层级,持续有限的 TTL | 固定所有请求共享的系统缓存。 |
| Retain | 使 KV 块在缓存压力下更(或更不)可能被保留。 | 在工具调用期间保留子代理的 KV 块。 |
每个提示都携带会话 ID,因此接收 KV 的层级知道它属于哪个会话,并可以将其作为一个单元进行分组、放置和回收,而不是作为匿名块。这在较冷的层级中最为重要,因为持有主机内存或磁盘的存储后端否则只能看到不透明的哈希,无法区分活跃轨迹的前缀和已完成的轨迹。引擎可以裁剪、推迟或忽略任何提示,而不发送提示的工作负载的行为与今天完全相同。
Share 已经有一个可工作的实现,以最小的调度器钩子分层到 HiCache 中,而 Prefetch 和 Demote 的设计是在 Share 落地后构建在该机制之上。
使用会话前缀索引器启用 KV 提示策略计算
为了让 Dynamo 在会话级别组合和执行这些 KvHint 策略,它必须拥有一个持续更新的结构,该结构在 KV 块、它们的会话身份以及它们在会话内的位置谱系之间进行双向映射。为此,我们实现了会话前缀索引器,这是一种基数树结构,其中每个分支代表一个唯一会话(或从另一个会话分支出的子代理会话)。
Session-Prefix Indexer 会持续从推理引擎的 KV 事件(Stored / Removed)以及 Dynamo 路由器主索引器(“FlashIndexer”)的前缀缓存命中中获取数据并保持更新。因此,通过 FlashIndexer 和 Session-Prefix Indexer,Dynamo 路由器能够累积掌握以下信息:(1) 推理引擎 worker 的 block 归属,以及 (2) 这些相同 block 的 session 成员关系。这种信息丰富的视图可以被各种 KV hint 策略使用,这些策略需要关于 KV block 的 worker 位置、活跃 session 生命周期和/或引擎工作负载的信号。
借助这些索引器,我们正在 agentic 工作负载数据集上,使用 Dynamo vLLM/SGLang 积极试验 KvHint 策略。

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