元鉴
返回中文阅读流

NVIDIA Developer Blog

在 Kubernetes 上部署解耦的大语言模型推理工作负载

随着大语言模型(LLM)推理工作负载复杂性增加,单一单体服务进程开始触及极限。预填充和解码阶段……

中文内容

已翻译official company source英文原文2026-05-26

随着大语言模型(LLM)推理工作负载的复杂性不断增长,单一的整体式服务进程开始触及其极限。预填充和解码阶段具有根本不同的计算特征,但传统部署却将它们强制放在同一硬件上,导致 GPU 利用不足且扩展缺乏灵活性。

解耦式服务通过将推理流水线拆分为不同阶段来解决这一问题,例如预填充、解码和路由,每个阶段都作为独立服务运行,并可按照自身需求分配资源和扩展。

本文将概述解耦式推理如何部署在 Kubernetes 上,探讨不同的生态系统解决方案及其在集群上的执行方式,并评估它们开箱即用提供的能力。

聚合式推理和解耦式推理有何不同?

在深入了解 Kubernetes manifests 之前,先理解 LLM 的两种推理部署模式会很有帮助:在聚合式服务中,单个进程(或紧密耦合的一组进程)处理从输入到输出的整个推理生命周期。解耦式服务则将流水线拆分为不同阶段,例如预填充、解码和路由,每个阶段都作为独立服务运行(见下方图 1)。

The diagram shows the difference between aggregated and disaggregated inference. The figure on the left shows aggregated inference: input into a multi-GPU box, which then filters through a context prefill and generation decode. On the rightThe diagram shows the difference between aggregated and disaggregated inference. The figure on the left shows aggregated inference: input into a multi-GPU box, which then filters through a context prefill and generation decode. On the right
图 1. 聚合式服务与解耦式服务对比

聚合式推理

在传统的聚合式设置中,单个模型服务器(或并行配置中协同工作的一组服务器)处理完整的请求生命周期。用户提示输入后,服务器对其进行分词,运行预填充以构建上下文,以自回归方式生成输出 token(解码),并返回响应。所有这些都发生在单个进程或紧密耦合的 pod 组中。

这在概念上很简单,并且适用于许多使用场景。但这意味着你的硬件会在两种根本不同的工作负载之间切换:预填充(Prefill)计算密集,受益于较高的浮点运算能力(FLOPS);而解码(decode)受内存带宽限制,受益于大容量、高速内存。

分离式推理

分离式架构将这些阶段拆分为不同的服务:

  • 预填充工作器处理输入提示词。这是计算密集型任务。你需要让 GPU 实现高吞吐,并且可以进行激进的并行化。
  • 解码工作节点一次生成一个输出 token。由于 LLM 的自回归特性,这一过程受内存带宽限制。你需要配备可快速访问高带宽内存(HBM)的 GPU。
  • 路由器/网关负责引导传入请求,管理预填充与解码阶段之间的键值(KV)缓存路由,并处理跨工作节点的请求负载均衡。

为什么要解耦?有三个突出的原因:

  1. 各阶段具有不同的资源与优化特征:通过解耦,你可以根据每个阶段的需求匹配 GPU 资源、模型分片技术和批大小,而不是在单一方案上妥协。
  2. 独立扩展:预填充和解码的流量模式不同。长上下文提示会产生较大的预填充突发,但解码流则保持稳定。独立扩展每个阶段,让你能够响应实际需求。
  3. 更高的 GPU 利用率:分离各阶段可让每个阶段充分利用其目标资源(预填充侧重计算,解码侧重内存带宽),而不是在两者之间交替。

NVIDIA Dynamo 和 llm-d 等框架实现了这种模式。问题变成了:如何在 Kubernetes 上编排它?

为什么调度是 Kubernetes 上多 Pod 推理性能的关键

部署多 Pod 推理工作负载(无论是模型并行聚合模型还是解耦模型)只是完成了一半。调度器如何在集群中放置 Pod 会直接影响性能;将 Tensor Parallel(TP)组的 Pod 放置在同一机架上,并使用高带宽 NVIDIA NVLink 互连,可能决定推理是快速完成还是遭遇网络瓶颈。这里最重要的是三项调度能力:

  • Gang scheduling 确保组内所有 Pod 以全有或全无的语义进行放置,防止出现浪费 GPU 的部分部署。
  • Hierarchical gang scheduling 将基础 gang scheduling 扩展到多级工作负载。在解耦推理中,你需要按组件或角色提供嵌套的最低保障:每个 Tensor Parallel 组(例如,组成一个解码实例的四个 Pod)必须以原子方式调度,完整系统(至少 n 个预填充实例 + 至少 m 个解码实例 + 路由器)也需要系统级协调。否则,一个角色可能会消耗所有可用 GPU,而另一个角色无限期等待——这是一种占用资源却无法处理请求的部分部署。
  • 拓扑感知放置会将紧密耦合的 Pod 共置在具有高带宽互连的节点上,从而最大限度降低节点间通信延迟。

这三项能力决定了 AI 调度器(例如 KAI Scheduler)如何根据应用的调度约束来放置 Pod。此外,AI 编排层还需要确定哪些内容需要进行 gang-scheduled,以及何时进行。例如,当预填充(prefill)独立扩缩时,需要有机制判断新的 Pod 会组成一个具备最低可用性保证的 gang,同时不干扰现有的解码(decode)Pod。因此,编排层和调度器需要在整个应用生命周期中紧密协作,处理多级自动扩缩、滚动更新等,以确保 AI 工作负载获得最佳运行条件。

这正是更高层级工作负载抽象发挥作用的地方。LeaderWorkerSet(LWS)和 NVIDIA Grove 等 API 允许用户以声明方式表达其推理应用的结构:存在哪些角色、它们如何相互关联、应如何扩缩,以及哪些拓扑约束很重要。该 API 的 operator 会将这种应用级意图转换为具体的调度约束(包括 PodGroup、gang 要求、拓扑提示),从而决定要创建哪些 gang 以及何时创建。

KAI Scheduler 随后发挥关键作用,满足这些约束,解决“如何做”的问题:gang scheduling、hierarchical gang scheduling 以及 topology-aware placement。在本文中,我们使用 KAI 作为调度器,不过社区中也有其他调度器支持这些功能的部分子集。读者可以通过 Cloud Native Computing Foundation (CNCF) 生态系统了解更广泛的调度领域。

部署解耦式推理

解耦式架构包含多个角色,每个角色都有不同的资源画像和扩缩容需求。由于解耦式流水线中的每个角色都是一个独立的工作负载,因此在 LWS 中,一种自然的方法是为每个角色创建一个单独的资源。

Prefill 工作节点(四个副本,2 阶 Tensor Parallelism):

apiVersion: leaderworkerset.x-k8s.io/v1
kind: LeaderWorkerSet
metadata:
  name: prefill-workers
spec:
  replicas: 4
  leaderWorkerTemplate:
    size: 2
    restartPolicy: RecreateGroupOnPodRestart
    leaderTemplate:
      metadata:
        labels:
          role: prefill-leader
      spec:
        containers:
        - name: prefill
          image: <model-server-image>
          args: ["--role=prefill", "--tensor-parallel-size=2"]
          resources:
            limits:
              nvidia.com/gpu: "1"
    workerTemplate:
      spec:
        containers:
        - name: prefill
          image: <model-server-image>
          args: ["--role=prefill"]
          resources:
            limits:
              nvidia.com/gpu: "1"

解码工作节点(两个副本,4 路张量并行):

apiVersion: leaderworkerset.x-k8s.io/v1
kind: LeaderWorkerSet
metadata:
  name: decode-workers
spec:
  replicas: 2
  leaderWorkerTemplate:
    size: 4
    restartPolicy: RecreateGroupOnPodRestart
    leaderTemplate:
      metadata:
        labels:
          role: decode-leader
      spec:
        containers:
        - name: decode
          image: <model-server-image>
          args: ["--role=decode", "--tensor-parallel-size=4"]
          resources:
            limits:
              nvidia.com/gpu: "1"
    workerTemplate:
      spec:
        containers:
        - name: decode
          image: <model-server-image>
          args: ["--role=decode"]
          resources:
            limits:
              nvidia.com/gpu: "1"

路由器(标准部署——无需领导者-工作节点拓扑):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: router
spec:
  replicas: 2
  selector:
    matchLabels:
      app: router
  template:
    metadata:
      labels:
        app: router
    spec:
      containers:
      - name: router
        image: <router-image>
        env:
        - name: PREFILL_ENDPOINT
          value: "prefill-workers"
        - name: DECODE_ENDPOINT
          value: "decode-workers"

每个角色都作为其自身的资源进行管理。你可以独立地扩展、预填充和解码,并按不同的时间安排更新它们。

需要注意的是,调度器会将预填充工作节点和解码工作节点视为相互独立的工作负载。调度器会成功放置它们,但并不知道它们构成了一条单一的推理流水线。在实践中,这意味着几点:

  • 预填充与解码之间的拓扑协调(将它们放置在同一机架上,以便快速传输 KV cache)需要手动添加 pod 亲和性规则,这些规则会引用两个 LWS 资源之间的标签。
  • 扩展一个角色并不会自动考虑另一个角色:如果一波长上下文请求突增需要更多预填充容量,你会扩展 prefill-workers,但除非你自己配置了亲和性,否则新的预填充 pod 不保证会落在现有解码 pod 附近。
  • 发布新的模型版本意味着要协调三个独立资源的更新——LWS 的分区更新机制支持按资源进行分阶段发布,但跨资源的同步由外部管理。

最后一点值得特别指出。推理框架发展很快,并不总是保证版本之间的向后兼容性,因此旧版本的预填充 pod 和新版本的解码 pod 可能无法通信。模型加载也需要时间,而且预填充和解码 worker 经常以不同速度进入就绪状态。在未同步的发布过程中,这可能会造成临时不平衡,即许多新的解码 pod 已就绪,但新的预填充 pod 很少就绪(反之亦然)。这可能会在一切追上之前,在你的推理流水线中造成瓶颈。

这些模式是可行的。只是这种协调发生在 Kubernetes 原语之外:在推理框架的路由层、自定义自动扩缩器、定制 operator,甚至是手动完成。另一种选择是使用 Grove 的 API,它采用了不同的方法,将这种协调移入 Kubernetes 资源本身。

它在单个 PodCliqueSet 中表达所有角色:

apiVersion: grove.io/v1alpha1
kind: PodCliqueSet
metadata:
  name: inference-disaggregated
spec:
  replicas: 1
  template:
    cliqueStartupType: CliqueStartupTypeExplicit
    terminationDelay: 30s

    cliques:
    - name: router
      spec:
        roleName: router
        replicas: 2
        podSpec:
          schedulerName: kai-scheduler
          containers:
          - name: router
            image: <router-image>
            resources:
              requests:
                cpu: 100m

    - name: prefill
      spec:
        roleName: prefill
        replicas: 4
        startsAfter: [router]
        podSpec:
          schedulerName: kai-scheduler
          containers:
          - name: prefill
            image: <model-server-image>
            args: ["--role=prefill", "--tensor-parallel-size=2"]
            resources:
              limits:
                nvidia.com/gpu: "1"
        autoScalingConfig:
          maxReplicas: 8
          metrics:
          - type: Resource
            resource:
              name: cpu
              target:
                type: Utilization
                averageUtilization: 70

    - name: decode
      spec:
        roleName: decode
        replicas: 2
        startsAfter: [router]
        podSpec:
          schedulerName: kai-scheduler
          containers:
          - name: decode
            image: <model-server-image>
            args: ["--role=decode", "--tensor-parallel-size=4"]
            resources:
              limits:
                nvidia.com/gpu: "1"
        autoScalingConfig:
          maxReplicas: 6
          metrics:
          - type: Resource
            resource:
              name: cpu
              target:
                type: Utilization
                averageUtilization: 80

    topologyConstraint:
      packDomain: rack

Grove operator 为每个角色管理 PodClique,并在所有这些 PodClique 之间协调调度、启动和生命周期。YAML 中有几点需要注意:

  • prefill 和 decode 上的 startsAfter: [router] 告诉 operator 在 router 就绪之前阻止它们启动。这是以声明式方式表达的,并通过 init containers 强制执行。首次部署时,router pods 会先启动并进入就绪状态,然后 prefill 和 decode pods 会并行启动(因为二者都依赖 router)。
  • 每个 clique 上的 autoScalingConfig 允许你定义按角色划分的扩缩容策略。operator 会为每个角色创建一个水平 Pod 自动扩缩器(HPA),因此 prefill 和 decode 会根据各自的指标独立扩缩容。
  • 带有 packDomain: rack 的 topologyConstraint 会指示 KAI Scheduler 将所有 cliques 打包到同一个机架内,从而通过高带宽互连优化 prefill 与 decode 阶段之间的 KV cache 传输。

应用此 manifest 后,你可以检查 Grove 创建的所有资源:

$ kubectl get pcs,pclq,pg,pod
NAME                                            AGE
podcliqueset.grove.io/inference-disaggregated   45s

NAME                                                  AGE
podclique.grove.io/inference-disaggregated-0-router   44s
podclique.grove.io/inference-disaggregated-0-prefill  44s
podclique.grove.io/inference-disaggregated-0-decode   44s

NAME                                                AGE
podgang.scheduler.grove.io/inference-disaggregated-0  44s

NAME                                              READY   STATUS    AGE
pod/inference-disaggregated-0-router-k8x2m        1/1     Running   44s
pod/inference-disaggregated-0-router-w9f4n        1/1     Running   44s
pod/inference-disaggregated-0-prefill-abc12       1/1     Running   44s
pod/inference-disaggregated-0-prefill-def34       1/1     Running   44s
pod/inference-disaggregated-0-prefill-ghi56       1/1     Running   44s
pod/inference-disaggregated-0-prefill-jkl78       1/1     Running   44s
pod/inference-disaggregated-0-decode-mn90p        1/1     Running   44s
pod/inference-disaggregated-0-decode-qr12s        1/1     Running   44s

一个 PodCliqueSet、三个 PodClique(每个角色一个)、一个用于协调调度的 PodGang,以及与每个角色副本数匹配的 Pod。startsAfter 依赖关系通过 init containers 强制执行:Prefill 和 decode Pod 会等待 router 就绪,然后其主容器才会启动。

扩展解耦工作负载

一旦解耦工作负载开始运行,扩展就成为核心的运维挑战。预填充和解码具有不同的瓶颈;团队可能希望根据首个 token 生成时间(TTFT)自动扩展预填充工作器,并根据 token 间延迟(ITL)独立自动扩展解码工作器,以在满足服务级别协议(SLA)的同时尽量降低 GPU 成本。

在实践中,解耦式扩展在三个层级上运作:

  1. 按角色扩展:在单个角色内添加或移除 pod(例如,将预填充从 4 个副本扩展到 6 个副本)
  2. 按 TP 组扩缩:将完整的张量并行组作为原子单元进行扩缩,因为不能只添加半个 TP 组。
  3. 跨角色协调:当你增加预填充容量时,可能还需要扩展路由器以处理增加的吞吐量,或扩展解码以消耗额外的预填充输出。

不同的工具面向不同的层级。

推理框架如何协调扩缩

推理框架通过自定义自动伸缩器在应用层面解决扩缩容问题,这些自动伸缩器能够查看特定于推理的指标。llm-d 的工作负载变体自动伸缩器(WVA)通过 Prometheus 监控每个 Pod 的 KV 缓存利用率和队列深度,并使用备用容量模型来确定何时应增加或移除副本。WVA 并不直接扩缩容部署,而是将目标副本数作为 Prometheus 指标发出,由标准的 HPA/基于 Kubernetes 的事件驱动自动伸缩(KEDA)执行——从而将扩缩容执行保持在 Kubernetes 原生原语内。

NVIDIA Dynamo 规划器采用了不同的方法:它原生理解解耦式服务,运行独立的预填充和解码扩缩容循环,分别以 TTFT 和 ITL SLA 为目标。它使用时间序列模型预测即将到来的需求,根据经过性能分析的每 GPU 吞吐量曲线计算副本需求,并在两个角色之间执行全局 GPU 预算。

这种全局可见性很重要,因为在实践中,预填充与解码之间存在一个会随请求模式变化而变化的最佳比例。将预填充扩容 3 倍而不扩容解码,额外的输出就无处可去——解码会成为瓶颈,KV 缓存传输会排队。应用层面的自动伸缩器能够处理这一点,因为它们可以看到完整的流水线;而针对单个资源的 Kubernetes 原生 HPA 本身并不会维持跨资源比例。

使用单独的 LWS 资源进行扩缩容

每个角色对应一个 LWS 时,你可以分别独立扩缩容:

kubectl scale lws prefill-workers --replicas=6
kubectl scale lws decode-workers --replicas=3

标准 HPA 可以分别以每个 LWS 为目标,或者由外部自动扩缩容器(如 Dynamo planner 或 llm-d 的自动扩缩容器)进行协调决策并同时更新两者。协调逻辑位于自动扩缩容器中,而不是 Kubernetes 资源本身。

使用 Grove 进行扩缩容

Grove 将按角色扩缩容整合到单一资源中。每个 PodClique 都有自己的副本数和可选的 autoScalingConfig,因此 HPA 可以根据按角色划分的指标独立管理各个角色:

kubectl scale pclq inference-disaggregated-0-prefill --replicas=6

operator 会创建额外的预填充 pod,同时保持 router 和 decode 不变:

NAME                                                  AGE
podclique.grove.io/inference-disaggregated-0-router   5m
podclique.grove.io/inference-disaggregated-0-prefill  5m
podclique.grove.io/inference-disaggregated-0-decode   5m

NAME                                              READY   STATUS    AGE
pod/inference-disaggregated-0-router-k8x2m        1/1     Running   5m
pod/inference-disaggregated-0-router-w9f4n        1/1     Running   5m
pod/inference-disaggregated-0-prefill-abc12       1/1     Running   5m
pod/inference-disaggregated-0-prefill-def34       1/1     Running   5m
pod/inference-disaggregated-0-prefill-ghi56       1/1     Running   5m
pod/inference-disaggregated-0-prefill-jkl78       1/1     Running   5m
pod/inference-disaggregated-0-prefill-tu34v       1/1     Running   12s  # new
pod/inference-disaggregated-0-prefill-wx56y       1/1     Running   12s  # new
pod/inference-disaggregated-0-decode-mn90p        1/1     Running   5m
pod/inference-disaggregated-0-decode-qr12s        1/1     Running   5m

六个预填充 pod、两个 router pod、两个 decode pod——只有预填充发生了变化。

对于在内部使用多节点 Tensor Parallelism 的角色,PodCliqueScalingGroup 可确保多个 PodClique 作为一个单元一起扩缩容,同时保持它们之间的副本比例。例如,在每个预填充实例由一个 leader pod 和四个 worker pod 组成的配置中:

 podCliqueScalingGroups:
    - name: prefill
      cliqueNames: [pleader, pworker]
      replicas: 2
      minAvailable: 1
      scaleConfig:
        maxReplicas: 4

使用 replicas: Two 时,这会创建两个完整的预填充实例:2 x(1 个 leader + 4 个 worker)= 总共 10 个 pod。minAvailable: One 保证意味着系统不会缩容到少于一个完整的张量并行组。

将该组从两个副本扩展到三个副本,会在保持 1:4 的 leader-to-worker 比例的同时增加第三个完整实例:leader-to-worker 比例:

$ kubectl scale pcsg inference-disaggregated-0-prefill --replicas=3

leader 和 worker clique 都作为一个单元一起扩展,新的副本(prefill-2)有一个 pleader pod 和四个 pworker pod,与该比例相匹配。为第三个副本创建了一个新的 PodGang,以确保它被成组调度。

NAME                                                              AGE
podcliquescalinggroup.grove.io/inference-disaggregated-0-prefill  10m

NAME                                                              AGE
podclique.grove.io/inference-disaggregated-0-prefill-0-pleader    10m
podclique.grove.io/inference-disaggregated-0-prefill-0-pworker    10m
podclique.grove.io/inference-disaggregated-0-prefill-1-pleader    10m
podclique.grove.io/inference-disaggregated-0-prefill-1-pworker    10m
podclique.grove.io/inference-disaggregated-0-prefill-2-pleader    8s  # new
podclique.grove.io/inference-disaggregated-0-prefill-2-pworker    8s  # new

NAME                                                              AGE
podgang.scheduler.grove.io/inference-disaggregated-0              10m
podgang.scheduler.grove.io/inference-disaggregated-0-prefill-0    10m
podgang.scheduler.grove.io/inference-disaggregated-0-prefill-1    8s  # new

入门

无论你是在运行单个解耦式流水线,还是在整个集群中运行数十个这样的流水线,相关的构建模块都正在涌现,社区也在以开放的方式构建它们。本文中的每种方法都代表了从简单性到集成式协调这一谱系上的不同位置。

正确的选择取决于你的工作负载、团队的运维模式,以及你希望由平台处理多少生命周期管理、由应用层处理多少生命周期管理。

查看以下资源以获取更多信息。

  • 正文:NVIDIA Grove
  • 正文:KAI Scheduler
  • 正文:NVIDIA Dynamo

在 KubeCon EU 与我们相聚

如果你将参加在阿姆斯特丹举办的 KubeCon EU 2026,欢迎莅临 241 号展位,并参加我们的会议,我们将在会上介绍一个端到端的开源 AI 推理栈。请查阅 Grove Deployment Guide,并在 GitHub 或 Discord 上提问。我们期待了解你对 Kubernetes 上分离式推理的思考。

Like

标签

原文标题

Deploying Disaggregated LLM Inference Workloads on Kubernetes