跳到正文
vLLM Blog· vLLM Team·· 23 小时前精选AI 评分62

vLLM 详解 GLM-5.3 优化第二部分:16 张 H200 上的 PCP、DCP 与 Hybrid HiSparse

GLM-5.3 Optimizations, Part 2: PCP, DCP, and Hybrid HiSparse on 16 H200s

AI 导读

vLLM 团队在 16 张 H200 上以 1 个 prefill 节点加 1 个 decode 节点的分离部署运行 GLM-5.3,用 PCP 按 token 切分 prompt、用 DCP 按 token 分片 KV cache,使 1M 上下文可以放进 prefill 节点。

推荐理由

原文给出 PCP、DCP 与 Hybrid HiSparse 在 16 张 H200 上的组合方式与实测对比,可作为长上下文部署的参考。

正文 · AI 翻译

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

  • Prefill 上下文并行(PCP) 按 token 而非按 head 将每个 prompt 拆分到多块 GPU 上,这比张量并行更适合 GLM-5.3 的稀疏注意力。
  • Decode 上下文并行(DCP) 按 token 对 prefill 引擎的 KV 缓存进行分片,因此 100 万上下文可以放在 prefill 节点上。
  • 混合 HiSparse 让 decode 将 KV 保留在主机内存中,因此 100 万 token 的请求可以放得下。
  • 在 AgentX 上,PCP8 × DCP8 prefill 配合混合 HiSparse decode,在相同吞吐量下,每用户速度是数据并行部署的 1.7–3.2 倍,并且它是我们测试过的唯一能服务 100 万 token 上下文的配置。

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

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

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

为什么不用 TP 或 DP?

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

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

这两种方案都没有将一个长 prompt 的注意力工作分散到八块 GPU 上,同时又不让每块服务它的 GPU 保留完整 KV。接下来两节分别解决这两半。PCP 将 prompt 的 query 拆分到多块 GPU 上,DCP 则拆分其 KV。

PCP:拆分 token,而非 head

在我们的 8× H200 prefill 引擎中,PCP 按 token 将每个 prompt 拆分到八块 GPU 上:每块 GPU 计算 prompt 八分之一对应的 query,且包含全部 64 个 head。朴素的拆分方式会导致负载不均衡。在因果注意力下,一个 token 会关注它之前的所有 token,因此持有 prompt 末尾的 GPU 所做的工作会远多于持有开头的 GPU。PCP 则把 prompt 切成 16 个块并两两配对:GPU 从前端取一块、从后端取一块,这样每块 GPU 都得到相同比例的廉价与昂贵 token(图 3 将其显示为八块 GPU 上的 V 形模式)。这就是 Context Parallelism for Scalable Million-Token Inference 中的负载均衡分片。最后,每块 GPU 的新 KV 会被 all-gather,因此每个 query 仍能关注完整上下文。

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 上执行一次 prefill,按拥有各部分的 GPU 着色。柱状图显示稀疏 MLA 的算术强度;折线标记 H200 在 BF16 下变为计算受限的位置。

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

不复制 indexer 对长 prefill 最为关键。indexer 的开销随 prompt 长度呈二次增长,而稀疏 MLA 仅呈线性增长,因为每个 query 只关注固定的 2,048 个 token。从头计算一次 prefill 的 FLOPs,在 TP8 下当 prompt 达到约 32K token 时 indexer 就会超过稀疏 MLA;在 PCP8 下,由于没有 GPU 复制它,交叉点推迟到约 256K。

综合来看,这些使 PCP 非常适合 prefill 长 prompt。但 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 进行了比较。

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 拆分 query;DCP 拆分 KV。这些名称是历史遗留。DCP 是为 decode 构建的,其中每个请求只有一个 query token,因此 KV 缓存是唯一值得拆分的东西。PCP 是为 prefill 构建的,其中 prompt 的大量 query token 才是工作所在。我们的设置会在 prefill 引擎中同时运行两者。

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

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

这个 all-gather 并非没有代价:每一层它都会重新发送整个缓存上下文,因此其开销随上下文长度增长。图 4 通过在单个 PCP8 节点上以相同缓存上下文运行相同的 2K token prefill 块,分别在有和没有 DCP8 的情况下隔离了这项开销。DCP8 在上下文最多 64K token 时每步增加约 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 结合后,为 prefill 引擎同时提供了速度和空间。

部署

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

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

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

我们将 PCP8 × DCP8 prefill 与 DP8 prefill(八个 DP rank,采用 EP8,每个 rank 64 GiB CPU KV 卸载,总计同样 512 GiB)进行比较,并将 Hybrid HiSparse 与 decode 节点上的普通 CPU KV 卸载进行比较,两者均使用相同的 512 GiB 主机内存(HiSparse 为 8 × 48 GiB 池 + 8 × 16 GiB 卸载)。每种配置都使用相同的 16 个 GPU、主机内存和 vLLM 构建。

结果

我们运行了 SemiAnalysis AgentX 基准测试:使用前缀复用回放 Claude Code 轨迹,每个并发级别使用全新服务器,运行一小时,MTP3 的合成接受长度固定为 2.99,因此每种配置都获得相同的推测加速。吞吐量是每 GPU 的逻辑 tok/s(包括缓存):输入加输出 token,计入前缀缓存输入,覆盖全部 16 个 GPU。交互性是 AgentX 的 P90 E2E 归一化每用户输出 token/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。每个并发度一个点,均在全新部署上测得,覆盖两个上下文长度上限:142K 下为 c8–c64,1M 下为 c8–c32。标签给出的是 PCP8 × DCP8 prefill 搭配 HiSparse decode 时的并发度;其他配置运行相同的并发度,从右到左排列。空心点表示该点不在其自身配置的帕累托前沿上。

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

从 c32 开始,部署变得更受 decode 限制,HiSparse decode 开始领先。在 c64 时,它几乎将吞吐翻倍,达到 8,340 logical tok/s/GPU(29.6 tok/s/user),而其他所有配置都降至 1–2 tok/s/user。

在 1M 上下文下,部署两者都需要。一个 DP8 prefill rank 无法容纳一个 1M token 的请求,仅 offload 的 decode rank 也无法容纳。

让一切组合起来

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

对于 P/D,KV 必须在两个引擎之间改变布局。prefill 引擎将每个请求的 KV 按 token 分片到八块 GPU 上。每个 decode rank 都需要完整的 KV(包括 MTP 层的 KV),并采用自己的分页布局。NIXL 连接器在两个节点之间完成这种重组,而来自 CPU offload 的前缀复用必须与之并行继续工作。

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

试用

一切都在 vLLM v0.32.0 或更高版本(vllm/vllm-openai:v0.32.0 镜像)上运行。上述结果是在 9e38b4111a 的 vLLM main 上测得的。复现 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,提示词能放入 2,048 token top-k 的 prefill 批次会走 dense 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 的 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 数据手册)。↩

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

来源:vLLM Blog · vllm.ai