TL;DR: 在 H200 上,长智能体上下文使 GLM-5.3 面临两个限制:KV 缓存内存和预填充速度。我们在 16 块 H200 上同时解决这两个问题,将其分为一个预填充节点和一个解码节点:
- 预填充上下文并行(PCP) 按 token 而非按 head 将每个提示拆分到多个 GPU 上,这比张量并行更适合 GLM-5.3 的稀疏注意力。
- 解码上下文并行(DCP) 按 token 对预填充引擎的 KV 缓存进行分片,因此 1M 上下文可以容纳在预填充节点上。
- Hybrid 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,因此其注意力无法使用其他七块 GPU。这限制了长提示的首 token 时间(TTFT),并且提示的 KV 缓存必须容纳在该 GPU 的内存中。
- TP8 将注意力 head 分区到多个 GPU 上。由于 MLA 的潜在 KV 由全部 64 个 head 共享,每块 GPU 都存储完整缓存并读取它以仅服务 8 个 head。索引器(它对每个较早的 token 打分以选出稀疏 MLA 关注的 2,048 个 token)完全没有分区,因此它的二次方开销在每块 GPU 上都重复。
这两种方案都无法在不将完整 KV 保留在每块服务它的 GPU 上的情况下,将一个长提示的注意力工作分散到八块 GPU 上。接下来两节分别解决这两个问题。PCP 将一个提示的查询拆分到各 GPU,DCP 拆分其 KV。
PCP:拆分 token,而不是 head
在我们的 8× H200 预填充引擎中,PCP 按 token 将每个提示拆分到八块 GPU 上:每块 GPU 使用全部 64 个 head 计算提示八分之一的查询。简单的拆分会不均衡。在因果注意力下,一个 token 会关注它之前的每个 token,因此持有提示末尾的 GPU 所做的工作远多于持有提示开头的 GPU。PCP 改为将提示切成 16 个块并两两配对:GPU 从前端取一块,从后端取一块,因此每块 GPU 获得相同比例的“便宜”和“昂贵” token(图 3 在八块 GPU 上显示为 V 形模式)。这就是 Context Parallelism for Scalable Million-Token Inference中的负载均衡分片。最后,每块 GPU 的新 KV 会被 all-gather,因此每个查询仍然可以关注完整上下文。
图 1 比较了两种拆分方式。PCP 解决了这两个问题。每块 GPU 只对自己的 token 做索引,因此索引器不再在每块 GPU 上复制,每块 GPU 只做八分之一的工作。而且现在从内存读取的每一行 KV 服务于全部 64 个 head 而不是 8 个,将稀疏 MLA 从每字节移动约 26 FLOPs 提高到约 191 FLOPs,1 接近 H200 在 BF16 下变为计算受限的位置。2
不复制索引器对长预填充最为关键。索引器的成本随提示长度呈二次方增长,而稀疏 MLA 的成本仅呈线性增长,因为每个查询只关注固定的 2,048 个 token。从头计算一次预填充的 FLOPs,一旦提示长度达到约 32K token,索引器在 TP8 下的开销就会超过稀疏 MLA;而在 PCP8 下由于没有 GPU 进行复制,交叉点被推迟到约 256K。
这些特性使得 PCP 非常适合长提示的预填充。但 PCP 仍有两项成本。每块 GPU 都保存全部注意力权重,而 MoE 专家则通过 EP8 分散在八块 GPU 上。并且由于新的 KV 在每一层都会被 all-gather,每块 GPU 最终仍要存储完整的 KV 缓存。限制上下文长度的正是这个被复制的缓存,而非计算,这也正是 DCP 所解决的问题。
DCP:对 KV 缓存也进行分片
一个 1M-token 的 GLM-5.3 请求需要 53.4 GiB 的 FP8 KV,3 但每块 GPU 只有大约 15 GiB 的空间。 Decode Context Parallelism(解码上下文并行) (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 节点上以相同的缓存上下文、分别开启和关闭 DCP8 运行相同的 2K-token 预填充块来隔离这一开销。在最多 64K token 的上下文下,DCP8 每步增加约 6%,在 248K 时上升到 15%。这就是获得 8× 更大 KV 池的代价。这一开销可以通过将每层的 all-gather 与上一层的计算重叠来降低,我们目前正在研究这一优化。
在大约 250K token 以上,仅靠 PCP8 已无法容纳 KV 缓存;在那里,DCP 不是开销,而是让长提示成为可能的关键。PCP8 × DCP8 相结合,为预填充引擎同时提供了速度和容量。
部署方式
我们将其作为预填充/解码(P/D)分离部署,运行在两个 8× H200 节点上。两个引擎都使用 FP8 KV 缓存和带三个草稿 token(MTP3)的 MTP 投机解码:
- 预填充: 一个 PCP8 × DCP8 × EP8 引擎,配备 512 GiB 的 CPU KV 卸载,以便后续轮次复用其前缀。
- 解码: 八个 DP 秩,带 EP8 和来自 Part 1 的 Hybrid HiSparse(每秩 48 GiB 主机池 + 16 GiB 卸载),因此解码并发受主机内存而非 HBM 限制。
一个具备前缀感知能力的 llm-d 路由器将每个请求发送到预填充引擎,NIXL 连接器将其 KV 缓存移动到一个解码秩,解码秩再将 token 流式传回。
我们将 PCP8 × DCP8 预填充与 DP8 预填充(八个 DP 秩,各带 EP8 和 64 GiB 的 CPU KV 卸载,总计同样为 512 GiB)进行比较,并在解码节点上将 Hybrid HiSparse 与普通 CPU KV 卸载进行比较,两种方式使用相同的 512 GiB 主机内存(HiSparse 为 8 × 48 GiB 的 HiSparse 池 + 8 × 16 GiB 的卸载)。每种配置都使用相同的 16 块 GPU、主机内存和 vLLM 构建。
结果
我们运行了 SemiAnalysis 的 AgentX 基准测试:重放带前缀复用的 Claude Code 轨迹,每个并发使用全新服务器,运行一小时,并使用合成接受长度固定为 2.99 的 MTP3,因此每种配置获得相同的投机加速。吞吐量为每块 GPU 的逻辑 tok/s(含缓存命中):输入加输出 token(计入前缀缓存的输入),除以全部 16 块 GPU。交互性是 AgentX 的每用户 P90 E2E 归一化输出 token/s,即图 5 的 x 轴。
在 c32 之前,部署主要受预填充限制,因此预填充器的影响更大。在两种设置中都使用 HiSparse 解码的情况下,PCP8 × DCP8 预填充在相同吞吐量下提供 DP8 的 1.7–3.2 倍交互性,并将中位 TTFT 从 1.5–5.2 秒降至 0.8–0.9 秒。在此负载下解码器几乎没有影响:在 c8 和 c16 时,两个 PCP8 × DCP8 配置彼此相差在几个百分点以内,而在 c8 时仅用卸载的解码略占优势。
从 c32 开始,部署变得更受解码限制,HiSparse 解码开始领先。在 c64 时,它将吞吐量几乎翻倍,达到 8,340 逻辑 tok/s/GPU、29.6 tok/s/用户,而其他所有配置都下降到 1–2 tok/s/用户。
在1M上下文下,该部署需要两者兼备。一个 DP8 prefill rank 无法容纳一个 1M-token 请求,而一个仅用 offload 的 decode rank 也同样无法做到。
让这一切协同运作
vLLM 中许多具有挑战性的工作,就是让这些特性能够协同运作。每一项特性都会改变 KV cache 的布局、调度或移动方式,而每一种组合都必须保持正确且快速。
对于 P/D,KV 需要在两个引擎之间改变布局。prefill 引擎将每个请求的 KV 按 token 分片到八块 GPU 上。每个 decode rank 需要完整的 KV,包括 MTP 层的 KV,并以自己的分页布局存放。NIXL 连接器在两个节点之间完成这种重组,同时 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-size 8 \
--decode-context-parallel-size 8 \
--dcp-comm-backend ag_rs \
--enable-expert-parallel \
--attention-config '{"sparse_mla_force_mqa":true}'如果不使用 sparse_mla_force_mqa,当 prefill 批次的提示词长度在 2,048-token top-k 范围内时,会走稠密 MHA 路径。该标志使它们同样保持在 sparse-MLA (MQA) 内核上,因此每行 KV 服务所有头。
致谢
这项工作由 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 字节(其中 FlashMLA 的 656 为 FP8 KV 的
fp8_ds_mla格式中 656 字节的 FP8 KV,加上一个 4 字节索引),约 1.35 MB。每个头为其 BF16 查询和输出增加 2,176 字节,并对每行执行 2,176 次 FLOPs(QK 覆盖 576 维,PV 覆盖 512 维)。因此 8 个头在 1.37 MB 上执行 35.7 MFLOPs,约为每字节 26 FLOPs;64 个头在 1.49 MB 上执行 285 MFLOPs,约为每字节 191 FLOPs。 ↩ -
对于 H200 SXM:989 dense BF16 TFLOP/s(数据手册中的 1,979 假设 2:4 稀疏性)和 4.8 TB/s,即每字节 206 FLOPs(NVIDIA H200 datasheet). ↩
-
每个 token:78 层 × 656 B 的 MLA KV,21 个索引器层 × 132 B 的索引器键(128 个 FP8 值加一个 4 字节缩放因子),以及 MTP 层的 788 B,总计 54,728 B。对于 = 1,048,576 个 token,即 54,728 MiB ≈ 53.4 GiB。 ↩