返回 文章 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 多个 PR。Ramp 称其 30% 的已合并 PR 来自智能体。Spotify 报告每月有 650 多个智能体生成的 PR。Claude Code 和 Codex 这类工具在一次编码会话中会发起数百次 API 调用,每次调用都带着完整的对话历史。在这些工作流背后,是一套承受着巨大 KV 缓存压力的推理栈。

折线图:在智能体推理工作负载中,由于提示词和上下文在连续请求之间被反复复用,累计 KV 缓存读取量的增长速度明显快于写入量。
图 1. 在智能体推理中,由于提示词和上下文在连续请求之间被反复复用,累计 KV 缓存读取量超过写入量。

以 Claude Code 为例。第一次 API 调用把对话前缀写入 KV 缓存之后,之后每次发往同一个 worker 的调用都能命中 85-97% 的缓存。智能体团队(或称智能体集群)把这个数字推得更高:4 个 Opus 队友的整体缓存命中率达到 97.2%。11.7 倍的读写比意味着,系统每写入一个 token,就要从缓存中读取将近 12 次。这是一种一次写入、多次读取(write-once-read-many,WORM)的访问模式:系统提示词和不断增长的对话前缀只计算一次,之后每次调用都直接从缓存提供。要在所有 worker 上最大化缓存复用率,并让 KV 块保持热状态且可被路由到,这是智能体推理最核心的优化目标。

这些数字来自托管式 API 基础设施,前缀匹配、缓存放置和淘汰都由服务提供方掌控。对于在自有 GPU 上运行开源模型的团队,这些能力开箱即用是不存在的。我们一直在构建 Dynamo 来弥补这一差距。本文将介绍我们如何在三个层面上让 Dynamo 原生支持智能体:前端 API、路由器和 KV 缓存管理。

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

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

第一层:前端

多协议支持

智能体 harness 越来越多地采用 v1/responses 和 v1/messages,而不是 v1/chat/completions,以便干净地处理交错思考与工具调用等新模式。这些 API 的关键差别在于结构。在 v1/chat/completions 中,消息内容是一个扁平字符串,工具调用则作为单独的字段附加上去。例如,请注意 GLM 和 MiniMax 在把各自的模型部署于 v1/chat/completions 端点之后,对交错思考的处理方式并不相同。v1/responses 和 v1/messages API 使用带类型的内容块,因此一个助手回合里可以把思考、工具调用和文本作为不同的对象。这对推理很重要,因为 orchestrator 可以看到块的边界,进行提示词优化,并针对不同类型的块应用不同的缓存和调度策略。Dynamo 通过统一的内部表示同时提供这三个端点,因此同一套部署就可以作为任意 harness 的推理后端。我们团队内部一直在运行 GLM-5 和 MiniMax2.5 的 Dynamo 部署,为我们的 Codex 和 Claude Code harness 提供支撑。这让我们能够把自己的后端实现与闭源推理做基准测试,目标是在缓存复用性能上达到同等水平。未来几周,我们将发布完整的文章,以及部署这两个模型的一些优化方案。

▶ https://github.com/user-attachments/assets/5fa8a224-44e8-4acb-943b-9b8af84815e6

视频 1. 用 Dynamo 部署 Claude 服务

▶ https://github.com/user-attachments/assets/8694b544-3fd1-4931-9fd0-7d9a3a2fb78f

视频 2. 用 Dynamo 部署 Codex 服务

我们还投入精力,为多种开源模型提供首日(day-0)的工具调用和推理解析支持。如果你发现某个模型尚不受支持,请提交 issue,或使用 tool-call-parser-generator skill,借助你偏好的 harness 来生成解析器。

智能体提示(Agent hints):Harness 与 Orchestrator 之间的接口

如今,推理服务器看到的只是匿名的、已分词的请求。但智能体 harness 掌握着基础设施永远看不到的全局上下文:哪些智能体正阻塞在工具调用上,哪些刚刚被创建,一个会话还剩多少轮,当前这次调用是快速查询还是长篇综合。使用编码智能体时,用户等待的是最终结果,而不是单个 token 流,因此 orchestrator 可以在多个智能体之间重新排序、调整请求优先级,而不影响最终用户体验。会话可以持续几分钟乃至数天,期间还有很长的工具调用停顿。这足以让我们以传统服务方式做不到的手段来优化推理调度。

两幅并排的示意图,分别标注为“Today”和“With Dynamo”,对比智能体系统的技术栈。两者都包含智能体逻辑、harness 以及协议组件层,其中 MCP 用于工具和数据,A2A 用于其他智能体。在“Today”栈中,推理连接到未指明的外部系统。在“With Dynamo”栈中,新增了一层 nvext 推理基础设施层,直接连接 SGLang、vLLM 和 TRT-LLM 等基于 GPU 的 runtime,表示一个标准化的推理接口。
图 2. 智能体系统技术栈对比:使用 Dynamo 后,新增的 nvext 推理基础设施层可以在各类智能体工作流中标准化对 GPU 支撑的 runtime 的访问。

Dynamo 新增的智能体提示扩展正是为弥合这一差距而设计的。它允许任何 harness 在全部三个 API 端点上为请求附加结构化提示,让路由器和 runtime 获得做出智能体感知的调度与缓存决策所需的上下文。这是一个 v1 版 API,我们正与社区积极共同设计,也希望构建智能体 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 字段:

  • priority 同时控制路由器和引擎两处的调度。在 Dynamo API 层面,数值越高表示“越重要”;Dynamo 会把它转换为路由器的队列顺序以及各后端特定的引擎优先级。
  • osl(输出序列长度)是 harness 对该请求将生成多少 token 的估计。路由器据此判断某个 worker 会被占用多久,从而改进负载均衡。harness 可以通过跟踪每种工具调用类型的平均输出长度,逐步学到这个值。
  • speculative_prefill 通知 orchestrator,在完整请求就绪之前,就开始在可能的 worker 上缓存该请求的前缀。当 harness 知道某个工具调用即将返回,并希望提前预热缓存时,这很有用。

用过 Anthropic 提示词缓存 API 的人对 cache_control 字段会很熟悉。它告诉 orchestrator 在 worker 上把已计算的前缀固定(pin)住,保留指定的 TTL,使其在工具调用的间隙中不被淘汰。目前 ephemeral 是唯一支持的类型(以与 Anthropic 的 API 保持一致)。我们将在下文的缓存保留一节中讨论其工作原理。关于智能体提示的完整文档,请见此处。

第二层:路由器

编码智能体遵循一种顺序模式:长预填充、工具调用、扩展前缀,如此往复。多智能体 harness 则把工作分发给并行的子智能体,各自上下文较短且相互独立。默认的轮询(round-robin)路由对这两种模式都视而不见,它无法考虑缓存局部性、请求优先级或会话结构。Dynamo 的路由器通过三种机制弥补这一差距:KV 感知放置、优先级调度和可扩展的路由策略。

KV 感知放置

如果没有缓存感知路由,一次对话的第 2 轮落在与第 1 轮相同 worker 上的概率只有约 1/N。每一次未命中都意味着完整的前缀重新计算,这是一个显著的性能瓶颈,对终端用户来说成本极高。Dynamo 的路由器维护着一份全局索引,记录哪些 KV 缓存块存在于哪些 worker 上。Flash Indexer 一文介绍了把这个索引器做到 170M ops/s(行星级规模的 KV 路由)所经历的六次迭代。对每个请求,路由器都会查询该索引,得到各 worker 的重叠得分,并选择能使缓存未命中与当前解码负载的综合成本最小的 worker。这个成本函数是可调的,下文将展示团队如何在其之上构建自定义的智能体感知路由策略。

优先级调度

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

  • 在路由器处,启用 --router-queue-threshold 后,优先级较高的请求会被提前到队列更靠前的位置。
  • 在引擎处,Dynamo 会对各后端特定的优先级极性进行归一化,并转发该请求,用于队列排序、抢占和 KV 缓存淘汰。

在路由器处,进入的请求会进入一个按有效到达时间排序的 BinaryHeap<QueueEntry>。更高的 priority 会让请求看起来像是更早到达,从而排在低优先级任务之前。只有当所有 worker 的负载都超过某个可配置的阈值时,请求才会进入队列;低于该阈值时,请求完全绕过队列,直接进入 worker 选择。当有容量释放出来(预填充完成或某个请求结束)时,队列会优先放出优先级最高的条目。

请求一旦被分发,SGLang、vLLM 和 TRT-LLM 对引擎优先级的解释可能各不相同,因此 Dynamo 会针对每种后端对面向引擎的数值做归一化。SGLang 等引擎还可以使用基于优先级的 radix 缓存淘汰,在内存压力下优先淘汰低优先级的块。

示意图:智能体推理中的请求路由与优先级划分,对延迟敏感的任务先被分发,并获得更高的优先级和更强的缓存保留。
图 3. 该图展示了智能体推理中的请求路由与优先级划分:对延迟敏感的任务被更早分发,并在引擎处获得更高的优先级和缓存保留。

面向智能体工作负载的路由策略

一个拥有 200K 上下文窗口的研究型智能体,需要有足够空闲 KV 容量的 worker 来容纳它的完整状态。路由器默认的成本函数(重叠得分 + 解码负载)可以处理常见情形,但有特定领域工作负载的团队可以通过路由器的 Python 绑定来实现自定义路由策略。核心的 KvRouter 类提供用于查询路由决策的 best_worker()、用于检查各 worker 负载的 get_potential_loads(),以及一次调用完成路由加分发的 generate()。自定义路由器与默认组件注册在同一个服务网格上,并且可以按请求覆盖路由配置:

# Query per-worker load and overlap for custom routing logic
loads = await router.get_potential_loads(token_ids)
# Override routing config based on request properties
# Long contexts benefit from heavier overlap weighting
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
)
# Or bypass the default selector entirely when the harness
# has its own worker selection logic (e.g., session affinity)
stream = await router.generate(
    token_ids, model=model, worker_id=chosen_worker
)

NeMo Agent Toolkit(NAT)团队使用这些 API 构建了一个自定义的在线学习型智能体路由器。他们的路由器从 nvext 注解中提取会话元数据,并将其输入到一个 Thompson Sampling 多臂老虎机式的成本函数中,学习在负载下哪些 worker 对哪些前缀模式表现最好。与 Dynamo 的默认路由相比,他们测得 p50 TTFT 降低到原来的 1/4,p50 每秒 token 数提升 1.5 倍。在中等内存压力下,为对延迟敏感的请求打上优先级标签,最多使 p50 TTFT 降低 63%。实现细节请参见 NAT Dynamo 集成示例。我们很快会将其作为 Dynamo 中的一种路由策略提供。

第三层:KV 缓存管理

智能体工作负载产生的块,其复用价值天差地别:系统提示词每一轮都会被复用,推理 token 则再也不会被用到;但默认的 LRU 淘汰对所有块一视同仁。一次 2 到 30 秒的工具调用停顿,就可能让智能体的整个前缀过期,在其恢复时被迫完整重算。缓存需要理解块的价值,支持跨 worker 共享,并遵循智能体的生命周期边界。

统一淘汰的问题

块类型 复用模式 价值
系统提示词 + 工具定义 每一轮 最高
对话历史 后续各轮,单调增长 高
思考/推理 token 推理循环结束后通常不再被复用(占输出的相当大一部分) 接近零
子智能体 KV 用上几轮后智能体即终止,无需保留 接近零

LRU 只看最近使用情况。在高流量环境下,等待被调用工具完成(智能体等待外部 API 时的 2 到 30 秒)可能会让该智能体的块过期,等智能体恢复时,整个前缀都必须重新计算。要解决这个问题,我们需要给 orchestrator 提供相应的 API,来控制哪些块应当保留、保存在哪里,以及保留多久。

把 KV 缓存当作共享资源

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

SGLang 的 HiCache 和 Dynamo 的 KV Block Manager(KVBM)等方案正在朝着四层内存层级迈进:

示意图:四层 KV 缓存内存层级。各层从 GPU 高带宽内存(最快、最小)开始,依次是 CPU 锁页 DRAM、本地 NVMe 存储,最后是远端的集群级存储。箭头表示各层之间的数据移动,包括从 GPU 卸载到 CPU、写穿到本地存储,以及复制到远端存储,说明了缓存容量随延迟增加而扩大。
图 4. 四层 KV 缓存内存层级示意图,覆盖 GPU、CPU、本地 NVMe 和远端存储,以实现可扩展的、跨 worker 的共享缓存复用。

块遵循写穿(write-through)路径:当某个 worker 计算出一段前缀的 KV 时,这些块会自动从 GPU 流向 CPU,再流向磁盘。每个块都会按序列哈希在全局注册表中去重。块一旦注册,就是不可变的,任何能访问该存储层的 worker 都可以寻址到它。

这直接解决了子智能体的冷启动问题。当主智能体计算出工具定义和系统提示词时,这些块会写穿到共享存储。当子智能体 1 在另一个 worker 上被创建时,路由器查询 Flash Indexer,在共享存储中找到这些块,worker 便通过 NIXL(RDMA 读取)加载它们,而不是从头重算。子智能体 2 同理。四次冗余的预填充计算变成了一次计算加三次加载。同样的机制也解决了解耦式预填充-解码(disaggregated prefill-decode)服务中的缓存一致性问题。在 disagg 模式下,预填充 worker 计算 KV,并通过 NIXL 把它传给解码 worker。解码 worker 生成 token,产生新的 KV 状态。到了下一轮,预填充 worker 既需要原始前缀,也需要第 1 轮生成的 token,但这些只存在于解码 worker 上。有了共享存储,解码 worker 就可以把新块写入公共层,任何预填充 worker 在下一轮都能取到它们。

多层存储解决了共享和持久化问题,但块仍然要等请求到达 worker 之后才会进入 GPU。智能体系统缺失的一环是预取:harness 可以利用历史时序数据预测智能体的某次工具调用何时可能返回,也就意味着它知道哪些块将在何时被需要。我们正在构建预取钩子,让 harness 能够发出“在下一个请求到来之前,把这些块从存储带到 GPU”的信号。结合(下文的)保留 API,harness 就获得了完整的生命周期控制:固定块以防被淘汰,设置优先级以控制淘汰顺序,并在块被需要之前主动预取。

示意图:智能体推理中的一系列操作,依次为预填充、第一次 LLM 调用、工具调用和第二次 LLM 调用。第一次 LLM 调用之后,KV 缓存块被卸载到主机内存并存入持久化存储。第二次 LLM 调用之前,系统已主动把所需的 KV 缓存块从存储预取回 GPU。该图说明了预取如何利用对工作流的了解提前移动数据,从而降低后续请求的延迟。
图 5. 智能体推理中 KV 缓存预取的示意图:块在首次调用之后被卸载,并在后续请求之前从存储主动预取到 GPU。

选择性缓存保留

让块在全局可用解决了共享问题,但没有解决淘汰问题。SGLang 和 vLLM 都通过优先级堆支持基于优先级的淘汰:harness 为每个请求分配一个数值优先级,低优先级的块先被淘汰。TensorRT-LLM 则更进一步,提供了 TokenRangeRetentionConfig(由 Dynamo 团队成员@jthomson04 设计并实现),可以在单个请求内按区域进行控制。

一个请求可以携带零条或多条指令。没有指令的块沿用默认的 LRU 路径,开销为零。淘汰器变成一个双结构系统:一个用于未设优先级块的 LRU 空闲链表(O(1),保持不变),以及一个用于带注解块的优先级队列。harness 可以表达这样的意图:“系统提示词块最后淘汰(priority:100);对话上下文要撑过一次 30 秒的工具调用(duration:45s);解码 token 最先淘汰(priority:1)”,而引擎无需理解背后的原因。

Anthropic 的提示词缓存允许你在其基础设施上把前缀标记为可缓存。Dynamo 的 cache_control API 把同样的语义带到了自托管推理中。当请求包含 cache_control: { type: "ephemeral", ttl: "1h" } 时,路由器会在该 TTL 内把匹配的前缀节点固定在 worker 的 radix 树中,保护它们不被从 worker 的 L2 存储中淘汰。

下一步是把保留机制与分布式缓存连接起来。目前,保留指令只作用于单个 worker 的本地缓存。当某个块在 worker A 上被固定,而下一个请求被路由到 worker B 时,这个固定并不会随之跟过去。把保留语义扩展到 HiCache/KVBM 的共享存储层,意味着 harness 只需固定一次块,就能让它跨 worker 存活:优先级和 TTL 元数据会沿写穿路径随块一起传递,任何从共享存储加载该块的 worker 都会继承其保留策略。结合前文所述的预取钩子,harness 就能对整个内存层级实现端到端的生命周期控制。

智能体生命周期感知

设想一次典型的 Claude Code 会话。主智能体运行 20 多轮,积累起不断增长的对话前缀。途中它会派生出若干 explore 子智能体,每个运行 1 到 3 轮后即终止。它也可能派生出由 4 名专家组成的团队,并行处理不同的子任务,随后终止。到了中途,智能体触及上下文上限并对历史进行摘要,把约 175K token 压缩到约 40K。这些事件每一个都会产生临时性的 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 是我们弥合这一差距的第一步:一小组结构化信号,让 orchestrator 能够做出有依据的路由、调度和缓存管理决策,而不是把每个请求都当作匿名 token。这是一个 v1 版 API,我们仍在积极演进它。如果你正在构建智能体 harness、为智能体工作负载运行开源模型,或在思考缓存感知的推理,我们希望听到哪些信号对你的用例最重要。欢迎在 GitHub 上联系我们,或在 X 上 @ 我们:@0xishand、@KranenKyle、@flowpow123。