TL;DR:长智能体上下文会让 GLM-5.3 在 H200 上触及两个极限:KV 缓存内存和预填充速度。我们在 16 块 H200 上同时解决这两个问题,拆分为一个预填充节点和一个解码节点:

  • 预填充上下文并行(PCP)按 token 而非按 head 将每个提示拆分到多块 GPU 上,这比张量并行更适合 GLM-5.3 的稀疏注意力。
  • 解码上下文并行(DCP)按 token 对预填充引擎的 KV 缓存进行分片,使 1M 上下文能够放进预填充节点。
  • 混合 HiSparse让解码将 KV 保留在主机内存中,从而容纳 1M token 的请求。
  • 在 AgentX 上,PCP8 × DCP8 预填充配合 Hybrid HiSparse 解码,在相同吞吐量下,每用户速度是数据并行部署的 1.7–3.2 倍,并且它是我们测试过的唯一能服务 1M token 上下文的配置。

在内存受限的部署中服务长上下文

智能体工作负载具有长上下文,且每一轮都会增长,这给 GPU 内存和预填充都带来了压力。在 H200 上,GLM-5.3 的权重占据了每块 GPU 的大部分内存,留给这些长上下文 KV 缓存的空间很小。而且由于每一轮都会向预填充添加更多工具输出和文件内容,在负载下预填充引擎可能成为整个部署的瓶颈。

第 1 部分展示了 Hybrid HiSparse 如何让长上下文在单个 8× H200 节点上的聚合部署中容纳。预填充/解码分离需要的不止这些。在预填充节点上,我们使用 PCP 通过跨提示的 token 并行化来加速长提示。然而,使用 MLA 时,这会让每个 PCP rank 都持有 KV 缓存的完整副本,因此我们将其与 DCP 结合以消除这种复制。在解码节点上,Hybrid HiSparse 允许在长上下文下实现更高的并发。vLLM 新的模块化核心(Model Runner V2)让组合所有这些变得容易得多。

为什么不用 TP 或 DP?

在 8× H200 节点上运行 GLM-5.3 预填充的两种标准方式是 DP8 和 TP8:

  • DP8将每个提示分配给单块 GPU,因此其注意力无法使用其他七块。这限制了长提示的首 token 时间(TTFT),并且提示的 KV 缓存必须能放进那块 GPU 的内存中。
  • TP8将注意力头分散到各 GPU 上。由于 MLA 的潜在 KV 由全部 64 个头共享,每个 GPU 都存储完整缓存,并读取它来仅服务 8 个头。索引器会对每个更早的 token 打分,以选出稀疏 MLA 所关注的 2,048 个 token,而它完全没有被分区,因此其二次方成本会在每个 GPU 上重复。

这两种方案都无法在把一条长提示词的注意力工作分散到八块 GPU 上的同时,又不把它的完整 KV 保留在每一块为它服务的 GPU 上。接下来两节分别处理这两半。PCP 把一条提示词的查询分散到各 GPU 上,DCP 则拆分它的 KV。

PCP:拆分 token,而不是拆分头

在我们的 8× H200 预填充引擎中,PCP 按 token 把每条提示词分散到八块 GPU 上:每块 GPU 用全部 64 个头计算提示词八分之一的查询。朴素拆分会导致负载不均衡。在因果注意力下,一个 token 会关注它之前的每一个 token,因此持有提示词末尾的 GPU 所做的工作会远多于持有开头的 GPU。PCP 改为把提示词切成 16 个块并两两配对:GPU 从前部取一个块、从后部取一个块,这样每块 GPU 都得到相同混合的廉价与昂贵 token(图 3 将其显示为八块 GPU 上的 V 形模式)。这就是来自用于可扩展百万 token 推理的上下文并行的负载均衡分片。最后,每块 GPU 的新 KV 会被全收集,因此每个查询仍能关注完整上下文。

Figure 1. One prefill on 8 GPUs under TP8 and PCP8, shaded by the GPU that owns each piece. The bars show sparse MLA's arithmetic intensity; the line marks where the H200 becomes compute-bound in BF16.
图 1. 在 TP8 和 PCP8 下于 8 块 GPU 上进行的一次预填充,按拥有各部分的 GPU 着色。柱状图显示稀疏 MLA 的算术强度;折线标出 H200 在 BF16 下变为计算受限的位置。

图 1 比较了这两种拆分。PCP 解决了这两个问题。每块 GPU 只索引自己的 token,因此索引器不再在每块 GPU 上复制,每块 GPU 只做八分之一。并且现在从内存读取的每一行 KV 可服务全部 64 个头,而不是 8 个,把稀疏 MLA 从每字节移动约 26 FLOPs 提升到约 191 FLOPs,1接近 H200 在 BF16 下变为计算受限的位置。2

不复刻索引器对长预填充最为关键。索引器的成本随提示长度呈二次增长,而稀疏 MLA 仅呈线性增长,因为每个查询只关注固定的 2,048 个 token。从头计算预填充的 FLOPs,在 TP8 下,当提示达到约 32K token 时,索引器的开销就超过了稀疏 MLA;在 PCP8 下,由于没有 GPU 复刻它,交叉点推迟到约 256K。

综合来看,这些特性使 PCP 非常适合预填充长提示。但 PCP 仍有两项成本。每块 GPU 都持有全部注意力权重,而 MoE 专家通过 EP8 分散在八块 GPU 上。并且由于新的 KV 在每一层都会被 all-gather,每块 GPU 最终仍要存储完整的 KV 缓存。限制上下文长度的是这份复制的缓存,而非计算,这正是 DCP 要解决的问题。

DCP:把 KV 缓存也分片

一个 100 万 token 的 GLM-5.3 请求需要 53.4 GiB 的 FP8 KV,3但每块 GPU 只有约 15 GiB 的空间。解码上下文并行(DCP)通过拆分缓存本身来解决这个问题:token 以轮询方式分配到八块 GPU 上,因此每块 GPU 存储每第八个 token,同一请求每块 GPU 只需 6.7 GiB(=53.4/8)。图 2 将其与 DP8 和 TP8 做了对比。

Figure 2. TP replicates MLA's latent KV, so eight GPUs buy no room. DCP divides it by eight.
图 2. TP 会复制 MLA 的潜在 KV,因此八块 GPU 也换不来额外空间。DCP 将其除以八。

注意: PCP 拆分查询;DCP 拆分 KV。这些名称是历史遗留的。DCP 是为解码构建的,解码时每个请求只有一个查询 token,因此 KV 缓存是唯一值得拆分的东西。PCP 是为预填充构建的,预填充时提示中的众多查询 token 才是工作量所在。我们的配置在预填充引擎中同时运行两者。

由于这种分片,每块 GPU 的查询所需的大部分 KV 现在都存放在其他 GPU 上。在解码中,DCP 把查询发送到 KV:每块 GPU 在自己的分片上做注意力计算,部分结果通过 log-sum-exp 合并。在这个预填充引擎中则相反:每一层,每块 GPU 将各分片 all-gather 成一份临时的全上下文副本,为自己的查询做注意力计算,然后释放该副本。只有持久缓存被除以八。

图 3 把这一切整合在一起。逐步查看一层 PCP8 × DCP8,了解每个 GPU 存储哪些 KV、计算哪些查询,以及什么数据跨互连传输。切换到 DP8、TP8 或 PCP8 进行对比。注意这两种布局并不对齐:GPU 存储每第八个 token,但计算块 和 的查询,这就是为什么每一层都需要 all-gather。

这个 all-gather 并非没有代价:每一层都会重新发送整个已缓存的上下文,因此其开销随上下文长度增长。图 4 通过在一个 PCP8 节点上运行相同的 2K token 预填充块,在相同缓存上下文下对比有无 DCP8 来单独衡量它。在上下文达到 64K token 之前,DCP8 每步增加约 6%,在 248K 时升至 15%。这就是 8 倍大 KV 池的代价。这一开销可以通过将每层的 all-gather 与上一层的计算重叠来降低,这是我们目前正在进行的优化。

Figure 4. DCP8's per-step overhead for a 2K-token chunk on one 8× H200 PCP8 engine. The band spans 1K, 4K, and 8K chunks.
图 4. DCP8 在一个 8× H200 PCP8 引擎上处理 2K token 块的每步开销。色带覆盖 1K、4K 和 8K 块。

超过约 250K token 后,仅靠 PCP8 无法容纳 KV 缓存;此时 DCP 不再是开销,而是使长提示成为可能的关键。PCP8 × DCP8 结合后,为预填充引擎同时提供了速度和空间。

部署

我们将其作为预填充/解码(P/D)分离式部署在两个 8× H200 节点上提供服务。两个引擎都使用 FP8 KV 缓存和带三个草稿 token 的 MTP 推测解码(MTP3):

  • 预填充:一个 PCP8 × DCP8 × EP8 引擎,配备 512 GiB 的 CPU KV 卸载,以便后续轮次复用其前缀。
  • 解码:八个 DP rank,采用 EP8 和 Hybrid HiSparse,来自第 1 部分(每个 rank 48 GiB 主机池 + 16 GiB 卸载),因此解码并发受限于主机内存而非 HBM。

一个前缀感知的llm-d路由器将每个请求发送到预填充引擎,NIXL 连接器将其 KV 缓存移动到解码 rank,解码 rank 将 token 流式返回。

我们将 PCP8 × DCP8 预填充与 DP8 预填充进行对比(八个 DP rank,每个都带 EP8 和 64 GiB 的 CPU KV 卸载,总计同样是 512 GiB),并在解码节点上将 Hybrid HiSparse 与纯 CPU KV 卸载进行对比,两者都使用相同的 512 GiB 主机内存(HiSparse 为 8 × 48 GiB 的 HiSparse 池 + 8 × 16 GiB 的卸载)。每个配置都使用相同的 16 块 GPU、主机内存和 vLLM 构建。

结果

我们运行了 SemiAnalysisAgentX基准测试:回放 Claude Code 的 trace 并启用前缀复用,每个并发度使用全新服务器,运行一小时,MTP3 使用固定为 2.99 的合成接受长度,因此每个配置都获得相同的投机加速。吞吐量是每 GPU 的逻辑 tok/s(包含缓存):输入加输出 token,计入前缀缓存的输入,覆盖全部 16 块 GPU。交互性是 AgentX 的 P90 E2E 归一化每用户输出 tokens/s,即图 5 的 x 轴。

Figure 5. One point per concurrency, each on a fresh deployment, at two context limits: c8–c64 at 142K and c8–c32 at 1M. Labels give the concurrency for PCP8 × DCP8 prefill with HiSparse decode; the other configurations run the same concurrencies, right to left. Hollow points are off their own configuration's Pareto frontier.
图 5。每个并发度一个点,各自在全新部署上运行,两个上下文上限:c8–c64 为 142K,c8–c32 为 1M。标签给出的是 PCP8 × DCP8 预填充配合 HiSparse 解码时的并发度;其他配置运行相同的并发度,从右到左。空心点表示不在其自身配置的帕累托前沿上。

在 c32 之前,部署主要受预填充限制,因此预填充器的影响更大。两种设置都使用 HiSparse 解码时,PCP8 × DCP8 预填充在相同吞吐量下提供的交互性是 DP8 的 1.7–3.2 倍,并将中位 TTFT 从 1.5–5.2 s 降至 0.8–0.9 s。在此负载下解码器影响不大:在 c8 和 c16 时,两个 PCP8 × DCP8 配置彼此相差在几个百分点以内,而仅卸载解码在 c8 时略占优势。

从 c32 开始,部署变得更受解码限制,HiSparse 解码开始领先。在 c64 时,它几乎将吞吐量翻倍,达到 8,340 逻辑 tok/s/GPU、29.6 tok/s/用户,而其他所有配置都降至 1–2 tok/s/用户。

在 1M 上下文下,部署需要两者兼备。一个 DP8 预填充 rank 无法容纳一个 1M token 的请求,仅卸载的解码 rank 也无法做到。

让这一切组合起来

vLLM 中许多有挑战性的工作就是让这类特性能够组合。每一个都会改变 KV 缓存的布局、调度或移动方式,而每一种组合都必须保持正确且高效。

对于 P/D,KV 需要在两个引擎之间改变布局。prefill 引擎将每个请求的 KV 按 token 分片到 8 个 GPU 上。每个 decode rank 都需要完整的 KV(包括 MTP 层的 KV),并以自己的分页布局存放。NIXL connector 在两个节点之间完成这种重组,同时来自 CPU offload 的前缀复用也必须继续正常工作。

这里的每种配置都运行在Model Runner V2上。它把模型特定的状态排除在公共执行路径之外,并在不进行 CPU–GPU 同步的情况下运行投机解码。Hybrid HiSparse 就构建在它之上。

试用

一切都在 vLLM v0.32.0 或更高版本上运行(即vllm/vllm-openai:v0.32.0镜像)。上述结果是在 vLLM main 上于9e38b4111a测得的。复现 gist固定了2537629eb2。prefill 引擎的关键参数:

--prefill-context-parallel-size8\
--decode-context-parallel-size8\
--dcp-comm-backendag_rs\
--enable-expert-parallel\
--attention-config'{"sparse_mla_force_mqa":true}'

如果没有sparse_mla_force_mqa,提示词长度在 2,048 token top-k 以内的 prefill 批次会走 dense MHA 路径。该参数让它们也保持在 sparse-MLA(MQA)内核上,这样每一行 KV 都能服务所有 head。

致谢

这项工作由 Roberto L. Castro(Red Hat)、Simon Veitner(Red Hat)、Matthew Bonanni(Red Hat)、Lucas Wilkinson(Red Hat)和 Nicolò Lucchesi(Mistral)完成。我们感谢 Maroon Ayoub(Red Hat)和 Robert Shaw(Red Hat)对部署进行压力测试、报告错误并修复 bug。

备注

脚注

  1. 每次查询时,sparse MLA 读取 2,048 行选中的 KV,每行 660 字节(其中 656 字节为 FlashMLA 的fp8_ds_mla格式中的 FP8 KV,外加 4 字节索引),约 1.35 MB。每个 head 为其 BF16 查询和输出额外增加 2,176 字节,并且每行执行 2,176 次 FLOP(QK 覆盖 576 维,PV 覆盖 512 维)。因此 8 个 head 在 1.37 MB 上执行 35.7 MFLOP,约合每字节 26 FLOP;64 个 head 在 1.49 MB 上执行 285 MFLOP,约合每字节 191 FLOP。↩

  2. 对于 H200 SXM:989 dense BF16 TFLOP/s(数据手册中的 1,979 假设了 2:4 稀疏性) 4.8 TB/s 每字节 206 FLOP(NVIDIA H200 数据手册)。↩

  3. 每个 token:78 层 × 656 B 的 MLA KV,21 个索引器层 × 132 B 的索引器键(128 个 FP8 值加 4 字节的 scale),以及 MTP 层的 788 B,合计 54,728 B。对于 = 1,048,576 个 token,即 54,728 MiB 53.4 GiB。↩