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 会被全收集,因此每个查询仍能关注完整上下文。
图 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 做了对比。
注意: 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 与上一层的计算重叠来降低,这是我们目前正在进行的优化。
超过约 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 轴。
在 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。
备注
脚注
-
每次查询时,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。↩ -
对于 H200 SXM:989 dense BF16 TFLOP/s(数据手册中的 1,979 假设了 2:4 稀疏性) 4.8 TB/s 每字节 206 FLOP(NVIDIA H200 数据手册)。↩
-
每个 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。↩