vLLM Blog来源发布时间

GLM-5.3 优化,第 2 部分:16 块 H200 上的 PCP、DCP 和 Hybrid HiSparse

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

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. TP8 和 PCP8 下 8 块 GPU 上的一次预填充,按拥有各部分的 GPU 着色。柱状图显示稀疏 MLA 的算术强度;线标记 H200 在 BF16 下变为计算受限的位置。

图 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 进行了比较。

图 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 节点上以相同的缓存上下文、分别开启和关闭 DCP8 运行相同的 2K-token 预填充块来隔离这一开销。在最多 64K token 的上下文下,DCP8 每步增加约 6%,在 248K 时上升到 15%。这就是获得 8× 更大 KV 池的代价。这一开销可以通过将每层的 all-gather 与上一层的计算重叠来降低,我们目前正在研究这一优化。

图 4. DCP8 在单个 8× H200 PCP8 引擎上处理 2K-token 块时每步的开销。色带涵盖 1K、4K 和 8K 块。

在大约 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 轴。

图 5. 每个并发一个点,每个点都在全新部署上,采用两个上下文上限:142K 下的 c8–c64 和 1M 下的 c8–c32。标签给出 PCP8 × DCP8 预填充加 HiSparse 解码的并发数;其他配置运行相同的并发,从右到左。空心点位于其自身配置的 Pareto 前沿之外。

在 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。

注释

脚注

  1. 每次查询,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。 ↩

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

  3. 每个 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。 ↩

原始出处

vLLM Blog

内容说明

原始发布及相关权利归来源方。

机器翻译 · 请以原文为准