返回 文章 build CMS 文章

NVIDIA Dynamo 如何为智能体推理做全栈优化:前端、路由器与 KV 缓存管理

Dynamo 通过前端 agent hints、KV 感知路由和共享缓存管理,让自托管开源模型也能获得托管 API 级别的智能体推理缓存复用效率。

NVIDIA Dynamo智能体推理KV 缓存推理优化
成长分 / 100 77 综合收获、行动、留存与影响

NVIDIA Dynamo 如何为智能体推理做全栈优化:前端、路由器与 KV 缓存管理
为什么值得读了解智能体推理中 KV 缓存读/写比高达 11.7 倍的 WORM 访问模式,以及为什么缓存复用是核心优化目标。

掌握 Dynamo 在三个层面(前端 API、路由器、KV 缓存管理)的具体优化机制与设计思路。

关键洞察
  1. 智能体推理中 KV 缓存读取量远超写入量,Claude Code 单工作进程缓存命中率 85-97%,多智能体团队可达 97.2%,读/写比 11.7 倍。
  2. Dynamo 前端通过通用内部表示同时服务 v1/chat/completions、v1/responses 和 v1/messages,并引入 nvext.agent_hints 让 harness 传递优先级、输出长度估计、推测性预填充和缓存控制信号。
  3. 路由器通过 Flash Indexer 维护全局 KV 块索引,实现 KV 感知放置,避免对话后续轮次因路由到不同 worker 而重新计算前缀。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:22965

编码智能体正开始大规模编写生产代码。Stripe 的智能体每周生成 1,300 多个 PRRamp 将 30% 的已合并 PR 归功于智能体Spotify 报告每月有 650 多个智能体生成的 PR。像 Claude Code 和 Codex 这样的工具在每次编码会话中会进行数百次 API 调用,每次调用都携带完整的对话历史。在这些工作流的背后,是一个承受着巨大 KV 缓存压力的推理栈。

折线图显示,在智能体推理工作负载中,由于跨顺序请求反复重用提示词和上下文,累计 KV 缓存读取量的增长速度显著快于写入量。

图 1. 在智能体推理中,由于跨顺序请求反复重用提示词和上下文,累计 KV 缓存读取量超过了写入量。

让我们以 Claude Code 为例。在第一次 API 调用将对话前缀写入 KV 缓存后,对同一工作进程的每次后续调用都能命中 85-97% 的缓存。智能体团队(或集群)进一步将这一比例推高,在 4 个 Opus 队友之间实现了 97.2% 的总体缓存命中率。11.7 倍的读/写比意味着,系统每写入一个 token,就要从缓存中读取近 12 次。这是一种一次写入多次读取(WORM)的访问模式:系统提示词和不断增长的对话前缀只计算一次,之后每次调用都从缓存中提供。最大化所有工作进程的缓存重用率,并保持 KV 块处于热状态且可路由,是智能体推理的核心优化目标。

这些数字来自托管 API 基础设施,其中提供商控制着前缀匹配、缓存放置和逐出。对于在自己的 GPU 上运行开源模型的团队来说,这些机制开箱即用并不存在。我们一直在构建 Dynamo 来弥合这一差距。本文将介绍我们如何在三个层面让 Dynamo 原生支持智能体:前端 API、路由器和 KV 缓存管理。

在本文中,我们统一使用三个术语:

Harness(框架):驱动工作流的智能体框架(Claude Code、Codex、OpenClaw、OpenCode 等)Orchestrator(编排器):Dynamo 的路由、调度和缓存管理层Runtime(运行时):执行模型并拥有 KV 缓存管理器的推理引擎(SGLang、vLLM、TRT-LLM)

第 1 层:前端

多协议支持

智能体框架正越来越多地采用 v1/responses

v1/messages

,而非 v1/chat/completions

,以清晰地处理包括交错思考和工具调用在内的新模式。这些 API 的关键区别在于结构。在 v1/chat/completions

中,消息内容是扁平字符串,工具调用作为单独字段附加。举例来说,请注意 GLMMiniMax API 在通过 v1/chat/completions

端点托管其模型时,处理交错思考的方式有何不同。v1/responses

v1/messages

API 使用类型化内容块,因此单个助手回合可以包含思考、工具调用和文本,作为彼此独立的对象。这对推理很重要,因为编排器可以看到块边界、执行提示优化,并针对每种块类型应用不同的缓存和调度策略。Dynamo 通过一个通用的内部表示来服务全部三个端点,因此单个部署可以作为任何 harness 的推理后端。我们团队一直在内部运行 GLM-5 和 MiniMax2.5 的 Dynamo 部署,为我们的 Codex 和 Claude Code harness 提供支持。这让我们能够将后端实现与闭源推理进行基准对比,目标是在缓存复用性能上达到持平。我们将在接下来几周内分享完整的文章以及一些用于部署这两个模型的优化配方。

视频 1. 使用 Dynamo 服务 Claude

视频 2. 使用 Dynamo 服务 Codex

我们还投入了对各种开源模型的 day-0 工具调用和推理解析支持。如果你发现某个模型不受支持,请提交 issue,或使用 tool-call-parser-generator 技能,用你选择的 harness 生成它。

Agent hints:Harness 编排器接口

如今,推理服务器看到的是匿名的 token 化请求。但 agent harness 拥有基础设施永远看不到的全局上下文:哪些 agent 被工具调用阻塞、哪些刚刚生成、一个会话中还剩多少回合,以及当前调用是快速查找还是长时间综合。使用编码 agent 时,用户等待的是最终结果,而不是单个 token 流,因此编排器可以跨 agent 重新排序和优先处理请求,而不会影响最终用户体验。会话会持续数分钟到甚至数天,其间有长时间的工具调用暂停。这足以以传统服务无法做到的方式优化推理调度。

图 2. Agentic 系统栈对比,显示使用 Dynamo 后,增加了一个 nvext 推理基础设施层,以标准化跨 agent 工作流对 GPU 支持运行时的访问。

Dynamo 新的 agent hints 扩展旨在弥合这一差距。它允许任何 harness 在全部三个 API 端点上为请求附加结构化提示,为路由器和运行时提供所需的上下文,以做出 agent 感知的调度和缓存决策。这是一个 v1 API,我们正在与社区积极共同设计,并非常希望从构建 agent harness 的团队那里获得反馈,了解哪些信号最有用。如果你有任何想法或反馈,请联系我们。

{
"model": "MiniMaxAI/MiniMax-M2.5",
"messages": [...],
"tools": [...],
"nvext": {
"agent_hints": {
"osl": 256,
"speculative_prefill": true,
"priority": 10
},
"cache_control": {
"type": "ephemeral",
"ttl": "1h"
}
}
}

agent_hints

字段:

控制路由器和引擎两端的调度。数值越高,在 Dynamo API 层面意味着“越重要”;Dynamo 会将其转化为路由器队列排序以及后端特定的引擎优先级。priority

(输出序列长度)是 harness 对该请求将生成多少 token 的估计。路由器用它来估算某个 worker 会被占用多久,从而改善负载均衡。harness 可以通过跟踪每种工具调用类型的平均输出长度,随时间推移学习这一数值。osl

指示编排器在完整请求就绪之前,就开始在某个可能的 worker 上缓存该请求的前缀。当 harness 知道某个工具调用即将返回、并希望提前预热缓存时,这很有用。speculative_prefill

cache_control

字段对使用过 Anthropic 提示缓存 API 的人来说会很熟悉。它告诉编排器将计算出的前缀固定在 worker 上,持续指定的 TTL,从而在工具调用间隙保护其不被驱逐。目前 ephemeral

是唯一支持的类型(以匹配 Anthropic 的 API)。我们将在下文的缓存保留部分讨论其工作原理。你可以在此处找到关于 agent hints 的完整文档。

第 2 层:路由器

编码 agent 遵循一种顺序模式:长预填充、工具调用、扩展前缀、重复。多 agent harness 会将工作分散到具有短小、独立上下文的并行子 agent 上。默认的轮询路由对这两种模式都视而不见——它无法考虑缓存局部性、请求优先级或会话结构。Dynamo 的路由器通过三种机制弥补了这一差距:KV 感知放置、优先级调度和可扩展路由策略。

KV 感知放置

如果没有缓存感知路由,对话的第 2 轮只有约 1/N 的概率落在与第 1 轮相同的 worker 上。每一次未命中都是一次完整的前缀重新计算,这是显著的性能瓶颈,对最终用户而言成本极高。Dynamo 的路由器维护一个全局索引,记录哪些 KV 缓存块存在于哪些 worker 上。Flash Indexer 文章介绍了让该索引器达到 1.7 亿次操作/秒(行星级 KV 路由)的六次迭代。在每个请求上,路由器都会查询该索引以获取每个 worker 的重叠分数,并选择使缓存未命中与当前解码负载的综合成本最小化的 worker。这个成本函数是可调的,我们将在下文展示团队如何在其之上构建自定义的 agent 感知路由策略。

优先级调度

priority

是唯一面向用户的调度旋钮。数值越高,在 Dynamo API 层面意味着“越重要”。Dynamo 在两层都使用这一个提示:

  • 路由器处,当启用 --router-queue-threshold

时,更高优先级的请求会在队列中被提前。- 在

引擎处,Dynamo 会规范化后端特定的极性,并转发请求以进行队列排序、抢占和 KV 缓存驱逐。

在路由器处,传入请求进入一个 BinaryHeap<QueueEntry>

,按有效到达时间排序。更高的 priority

使请求看起来像是更早到达,从而将其排在低优先级工作之前。只有当所有 worker 都超过可配置的负载阈值时,请求才会进入队列。低于该阈值时,它们会完全绕过队列,直接进入 worker 选择。当容量释放时(prefill 完成或某个请求结束),队列会优先清空优先级最高的条目。

一旦被调度,SGLang、vLLM 和 TRT-LLM 可能会以不同方式解释引擎优先级,因此 Dynamo 会按后端对面向引擎的值进行归一化。像 SGLang 这样的引擎还可以使用基于优先级的 radix cache 淘汰机制,在内存压力下优先淘汰较低优先级的块。

图 3。此图展示了智能体推理中的请求路由与优先级排序,其中对延迟敏感的任务会更早被调度,并在引擎处被分配更高的优先级和缓存保留。

智能体工作负载路由策略

一个具有 200K 上下文窗口的研究智能体需要拥有足够空闲 KV 容量的 worker 来保存其完整状态。路由器的默认成本函数(重叠分数 + 解码负载)可处理常见情况,但具有特定领域工作负载的团队可以使用路由器的 Python 绑定来实现自定义路由策略。核心的 KvRouter

类提供 best_worker()

用于查询路由决策,get_potential_loads()

用于检查每个 worker 的负载,以及 generate()

用于在一次调用中完成路由 + 调度。自定义路由器注册在与默认组件相同的服务网格上,并且可以按请求覆盖路由配置:

# 为自定义路由逻辑查询每个 worker 的负载和重叠情况
loads = await router.get_potential_loads(token_ids)
# 根据请求属性覆盖路由配置
# 长上下文受益于更重的重叠权重
config = {"overlap_score_weight": 2.0} if len(token_ids) > 8192 else {}
worker_id, dp_rank, overlap = await router.best_worker(
token_ids,
request_id="req-123",
update_indexer=True,
router_config_override=config
)
# 或者当测试框架有自己的 worker 选择逻辑时
# (例如会话亲和性),完全绕过默认选择器
stream = await router.generate(
token_ids, model=model, worker_id=chosen_worker
)

NeMo Agent Toolkit (NAT) 团队使用这些 API 构建了一个自定义的在线学习智能体路由器。他们的路由器从 nvext

注解中提取会话元数据,并将其输入到一个 Thompson Sampling 老虎机风格的代价函数中,该函数学习在负载下哪些 worker 对哪些前缀模式表现最佳。与 Dynamo 的默认路由相比,他们测得 p50 TTFT 降低了 4 倍,p50 每秒 token 数提升了 1.5 倍。对延迟敏感请求进行优先级标记,在中等内存压力下实现了高达 63% 的 p50 TTFT 降低。有关实现细节,请参阅 NAT Dynamo 集成示例。我们很快将在 Dynamo 中将其作为路由策略提供。

第 3 层:KV 缓存管理

智能体工作负载产生的块具有截然不同的复用价值——系统提示词每一轮都会复用,推理 token 则永远不会再次复用——但默认的 LRU 淘汰将所有块一视同仁。一次 2-30 秒的工具调用暂停可能会使智能体的整个前缀老化淘汰,迫使其恢复时进行完整重计算。缓存需要理解块的价值、支持跨 worker 共享,并尊重智能体生命周期边界。

统一淘汰的问题

块类型 复用模式 价值
系统提示词 + 工具定义 每一轮 最高
对话历史 后续轮次,单调增长
思考/推理 token 推理循环关闭后通常零复用(占输出的很大一部分) 接近零
子智能体 KV 多轮后智能体消亡。无需保留 接近零

LRU 只关注最近使用时间。在高流量环境中,等待被调用工具完成(智能体等待外部 API 的 2-30 秒)可能导致智能体的块老化淘汰,当智能体恢复时,整个前缀必须重新计算。为了解决这个问题,我们需要提供编排器 API 来控制哪些块应该保留、它们应该存放在哪里以及保留多长时间。

KV 缓存作为共享资源

如今,KV 缓存被视为每个 worker 上的本地、临时资源。智能体约 32K token 的系统提示词和工具定义会在为其提供服务的每个 worker 上独立计算。当一个主智能体生成 4 个子智能体,每个子智能体都有重叠的工具定义时,如果子智能体落在不同的 worker 上,该共享前缀会被重新计算 4 次。在我们对 Claude Code 团队会话的分析中,我们直接测量了这一点:队友的平均缓存命中率为 79.4%,而主智能体的探索子智能体为 91.3%(读写比分别为 5.0 倍和 11.7 倍),这一差距几乎完全由每个队友首次调用时的冷启动写入造成。目标是使高价值 KV 缓存块可供集群中的所有 worker 使用。本质上,它们在冷启动时写入一次,然后随时可被任何 worker 读取。

诸如 SGLang 的 HiCache 和 Dynamo 的 KV Block Manager (KVBM) 等解决方案正在朝着 4 层内存层次结构发展:

图 4. 展示四层 KV 缓存内存层次结构的示意图,涵盖 GPU、CPU、本地 NVMe 和远程存储,以实现跨 worker 的可扩展共享缓存复用。

块的流转遵循写穿路径:当工作节点为某个前缀计算 KV 时,块会自动从 GPU 流向 CPU 再流向磁盘。每个块在全局注册表中通过序列哈希进行去重。一旦某个块被注册,它就是不可变的,并且任何能够访问该存储层级的工作节点都可以通过地址访问它。

这直接解决了子代理冷启动问题。当主代理计算工具定义和系统提示时,这些块会写穿到共享存储。当子代理 1 在不同的工作节点上启动时,路由器查询 Flash Indexer,在共享存储中找到这些块,工作节点通过 NIXL(RDMA 读取)加载它们,而不是从头重新计算。子代理 2 也是如此。四次冗余的预填充计算变成一次计算加三次加载。同样的机制也解决了分离式预填充-解码服务中的缓存一致性问题。在分离模式下,预填充工作节点计算 KV 并通过 NIXL 将其传输到解码工作节点。解码工作节点生成 token,产生新的 KV 状态。在下一轮中,预填充工作节点需要原始前缀和第一轮生成的 token,但这些只存在于解码工作节点上。借助共享存储,解码工作节点将其新的块写入公共层级,任何预填充工作节点都可以在下一轮获取它们。

多层级存储解决了共享和持久化问题,但块仍然只有在请求到达工作节点之后才会到达 GPU。代理系统缺失的一环是预取:harness 可以利用历史时序数据来预测代理的工具调用可能何时返回,这意味着它知道将需要哪些块以及何时需要。我们正在构建预取钩子,以便 harness 可以发出信号“在下一个请求之前将这些块从存储带到 GPU”。结合保留 API(见下文),这为 harness 提供了完整的生命周期控制:固定块以防止驱逐,设置优先级以控制驱逐顺序,并在需要之前主动预取块。

图 5。代理推理中 KV 缓存预取的示意图,其中块在初始调用后被卸载,并在后续请求之前主动从存储预取到 GPU。

选择性缓存保留

使块全局可用解决了共享问题,但没有解决驱逐问题。SGLang 和 vLLM 都通过优先级堆支持基于优先级的驱逐,其中 harness 为每个请求分配一个数字优先级,较低优先级的块先被驱逐。TensorRT-LLM 通过 TokenRangeRetentionConfig 更进一步

(由 Dynamo 团队成员@jthomson04设计和实现),它允许在单个请求内进行按区域控制。

一个请求携带零个或多个指令。没有指令的块遵循默认的 LRU 路径,零开销。驱逐器变成一个双结构系统:用于未优先化块的 LRU 空闲列表(O(1),不变)和用于已标注块的优先级队列。harness 可以表达“系统提示块最后被驱逐(优先级:100);对话上下文在 30 秒工具调用中存活(持续时间:45 秒);解码 token 最先被驱逐(优先级:1)”,而引擎无需理解原因。

Anthropic 的提示缓存允许你在其基础设施上将前缀标记为可缓存。Dynamo 的 cache_control

API 将相同的语义带到自托管推理中。当请求包含 cache_control: { type: "ephemeral", ttl: "1h" }

,路由器会在该 TTL 内将匹配的前缀节点固定在工作节点的基数树中,保护它们在工作节点 L2 存储中不被逐出。

下一步是将保留策略与分布式缓存连接起来。目前,保留指令仅适用于单个工作节点的本地缓存。当一个块在工作节点 A 上被固定,但下一个请求路由到工作节点 B 时,固定不会跟随。将保留语义扩展到 HiCache/KVBM 的共享存储层意味着 harness 可以一次固定一个块,并使其跨工作节点存活:优先级和 TTL 元数据通过写穿路径随块一起传递,任何从共享存储加载该块的工作节点都会继承保留策略。结合上述预取钩子,这为 harness 提供了跨整个内存层次的端到端生命周期控制。

代理生命周期感知

考虑一个典型的 Claude Code 会话。主代理运行 20 多轮,累积不断增长的对话前缀。在此过程中,它会生成探索子代理,每个子代理运行 1-3 轮后终止。它可能生成一个由 4 名专家组成的团队,并行处理不同的子任务,然后终止。中途,代理达到上下文限制并总结其历史,将约 175K 个 token 压缩到约 40K。这些事件中的每一个都会产生短暂的 KV:永远不会再被引用的块。子代理终止、上下文总结和封闭推理循环都会产生短暂的 KV,这些 KV 与系统提示等高价值块占用相同的内存。推理模型会放大这一点:<think>...</think>

块占生成 token 的约 40%,但在推理循环关闭的那一刻就变得短暂。如果没有生命周期感知,缓存会一视同仁地对待所有这些块。

代理工作流展示跨生命周期阶段的 KV 缓存行为。序列从系统提示、工具和对话历史开始,随后是推理步骤和一个工具调用,该调用生成在共享上下文上操作的子代理。子代理生成中间结果并终止,同时创建额外的推理块并随后丢弃。总结步骤压缩对话并在主代理恢复之前逐出未使用的 KV 块。持久上下文被保留,而来自子代理、推理和中间步骤的短暂 KV 块随时间被移除。

图 6. 代理工作流展示跨生命周期阶段的 KV 缓存使用情况,将持久上下文与在子代理、工具和推理步骤中创建并丢弃的短暂块分开。

上述保留原语(优先级、TTL、token 范围)为我们提供了构建块。缺少的是将它们与会话关联的能力。如果 harness 能够将子代理的请求标记为属于某个会话,并将该会话的 KV 标记为短暂,那么逐出器可以首先针对这些块,并完全跳过将它们写入共享存储。当子代理终止时,其会话的块首先被回收。同样的机制适用于思考 token:引擎可以检测 <think>

在生成过程中划定边界,并在插入时将这些块标记为临时块,这样它们就会跳过 L2 回写,并在没有任何外部信号的情况下先于普通块被逐出。这里的设计空间很广:由 harness 驱动的会话标记、引擎原生的语义检测、以及将两者结合的混合方法。我们正在积极探索多个方向,并预计正确的答案会因工作负载和框架而异。

弥合差距

智能体推理中最大的优化面,在于 harness 所知与基础设施所能看到之间的差距。哪些智能体被阻塞、哪些即将恢复、哪些 KV 值得保留、哪些可以丢弃——所有这些上下文都存在于 harness 层,却从未跨越 API 边界。nvext.agent_hints

是我们为弥合这一差距所做的首次尝试:一小组结构化信号,让编排器能够做出有依据的路由、调度和缓存管理决策,而不是把每个请求都当作匿名 token 来处理。这是一个 v1 API,我们正在积极演进它。如果你正在构建智能体 harness、为智能体工作负载运行开源模型,或者正在思考缓存感知推理,我们想听听哪些信号对你的用例最重要。请通过 GitHub 联系我们,或在 X 上 @ 我们:@0xishand@KranenKyle@flowpow123