数据中心/云端

在 Kubernetes 上部署解 LLM 推理工作负载

随着大语言模型 (LLM) 推理工作负载的复杂性不断增加,单个单一的服务进程开始达到其极限。预填充和解码阶段具有截然不同的计算配置文件,但传统部署迫使它们使用相同的硬件,导致 GPU 未得到充分利用,扩展缺乏灵活性。

解服务通过将推理工作流分为预填充、解码和路由等不同阶段来解决这一问题,每个阶段都作为独立服务运行,并且可以根据自己的条件进行资源和扩展。

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

聚合推理和分解推理有何区别?

在深入研究 Kubernetes manifests 之前,它有助于了解 LLM 的两种推理部署模式:在聚合服务中,单个进程 (或紧密合的进程组) 处理从输入到输出的整个推理生命周期。解服务将工作流划分为不同的阶段,例如预填充、解码和路由,每个阶段都作为独立的服务运行 (请参见下面的图 1) 。

聚合推理

在传统的聚合设置中,单个模型服务器 (或并行配置中经过协调的服务器组) 会处理完整的请求生命周期。用户提示输入,服务器将其标记化,运行预填充以构建上下文,以自回归方式 (解码) 生成输出 tokens,并返回响应。一切都发生在单个进程或紧密合的 Pod 组中。

这在概念上很简单,适用于许多用例。但这意味着您的硬件可以在两种截然不同的工作负载之间交替使用:预填充是计算密集型工作负载,可受益于高浮点运算 (FLOPS) ,而解码则受内存带宽限制,可受益于大容量、快速的内存。

分类推理

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

  • 预填充工作者处理输入提示。这是计算密集型问题。您希望充分利用 GPU 以实现高吞吐量,并且可以积极地进行并行化。
  • 解码工作线程每次生成一个输出 tokens。由于 LLM 的自回归性质,这是受内存带宽限制的。您需要能够快速访问高带宽内存 (HBM) 的 GPU。
  • 路由器/ 网关引导传入请求,管理预填充和解码阶段之间的键值 (KV) 缓存路由,并处理工作节点中请求的负载均衡。

为何要解?脱颖而出的原因有三个:

  1. 每个阶段不同的资源和优化配置文件:借助解聚,您可以将 GPU 资源、模型分片技术和批量大小与每个阶段的需求相匹配,而不是在单一方法上做出妥协。
  2. 独立扩展:预填充和解码流量模式各不相同。长上下文提示词会产生大量的预填充突发,但会产生稳定的解码流。通过独立扩展每个阶段,您可以对实际需求做出响应。
  3. 更高的 GPU 利用率:分离阶段可让每个阶段使其目标资源达到饱和 (计算预填充、解码内存带宽) ,而不是交替使用。

NVIDIA Dynamo 和 llm-d 等框架都采用了这种模式。问题是:如何在 Kubernetes 上进行编排?

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

部署多 pod 推理工作负载 (模型并行聚合模型或分解模型) 只是其中的一半。调度程序在集群中放置 pod 的方式直接影响性能;将 Tensor Parallel (TP) 组的 pod 放置在具有高带宽 NVIDIA NVLink 互连的同一机架上可能意味着快速推理与网络瓶颈之间的区别。三种调度功能在此最为重要:

  • 分组调度可确保以全部或全部语义放置组中的所有 POD,从而防止浪费 GPU 的部分部署。
  • 分层群组调度将基本的群组调度扩展到多级工作负载。在解推理中,您需要为每个组件或角色提供最低保证:每个 Tensor Parallel 组 (例如,四个 POD 组成一个解码实例) 必须以原子方式调度,整个系统 (至少 n 个预填充实例+ 至少 m 个解码实例+ 路由器) 也需要系统级协调。如果不这样做,一个角色可以使用所有可用的 GPU,而另一个角色则会无限地等待,即部分部署可容纳资源,但无法服务请求。
  • 拓扑感知放置可在具有高带宽互连的节点上对紧密合的POD进行共置,从而更大限度地降低节点间通信延迟。

这三种功能决定了 AI 调度程序 (例如 KAI Scheduler) 如何根据应用的调度限制来放置 Pod。此外,对于 AI 编排层而言,确定需要进行分组调度的内容以及时间也很重要。例如,当预填充独立扩展时,需要在不中断现有解码 POD 的情况下,确定新 POD 组成具有最低可用性保证的小组。因此,编排层和调度程序需要在整个应用生命周期中紧密协作,处理多级自动扩展、滚动更新等,以确保 AI 工作负载的最佳运行时条件。

这正是更高级别的工作负载抽象派上用场的地方。APIs 如 LeaderWorkerSet (LWS) 和 NVIDIA Grove 允许用户以声明方式表达其推理应用的结构:存在哪些角色、它们之间的关系、它们应如何扩展以及哪些拓扑约束条件重要。API 的 Operator 会将该应用级意图转换为具体的调度约束条件 (包括 PodGroups、gang requirements、拓扑提示) ,以确定要创建和何时创建的群组。

KAI Scheduler 在满足这些约束条件方面发挥着关键作用,解决以下问题:帮调度、分层帮调度和拓扑感知放置。在本文中,我们使用 KAI 作为调度程序,不过社区中还有其他调度程序支持这些功能的子集。读者可以通过云原生计算基金会 (CNCF) 生态系统探索更广泛的调度环境。

部署解推理

分解架构具有多个角色,每个角色都有不同的资源配置文件和扩展需求。由于分解工作流中的每个角色都是不同的工作负载,因此 LWS 的一种自然方法是为每个角色创建单独的资源。

预填充工作节点 (四个副本,2 度张量并行) :

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"

路由器 (标准部署,无需 leader-worker 拓扑) :

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 缓存传输) 之间的拓扑协调需要手动添加在两个 LWS 资源中引用标签的 POD 亲和性规则。
  • 扩展一个角色并不会自动将另一个角色考虑在内:如果突发的长上下文请求需要更多的预填充能力,您可以扩展预填充工作者,但新的预填充 Pod 不能保证靠近现有的解码 Pod,除非您自己配置了 Affinity。
  • 推出新的模型版本意味着在三个独立资源之间协调更新 – LWS 的分区更新机制支持对每个资源进行分阶段部署,但跨资源的同步由外部管理。

最后一点值得注意。推理框架动作迅速,并不总是保证版本之间的向后兼容性,因此旧版本上的预填充 Pod 和新版本上的解码 Pod 可能无法通信。模型的加载也需要时间,预填充和解码工作节点通常会以不同的速度准备就绪。在不同步的部署过程中,这可能会造成暂时的不平衡,许多新的解码 POD 已准备就绪,但很少有新的预填充 POD (反之亦然) 。这可能会在推理工作流中造成瓶颈,直到一切顺利为止。

这些模式是有效的。协调只发生在 Kubernetes 基元之外:在推理框架的路由层、自定义自动缩放器、定制运算符中,甚至是手动进行。另一种选择是使用 Grove 的 API,该 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 为每个角色管理 PodCliques,并协调所有角色的调度、启动和生命周期。YAML 中需要注意的几点:

  • startsAfter:【router】在预填充和解码时,系统会告知操作员在路由器准备就绪之前为其启动打开大门。这一点以声明方式表示,并通过 init 容器执行。首次部署时,路由器 POD 会先启动并准备就绪,然后预填充和解码 POD 会并行启动 (因为两者都依赖于路由器) 。
  • autoScalingConfig您可以在每个集群中定义每个角色的扩展策略。操作员为每个模型创建水平 Pod 自动缩放器 (HPA) ,因此可以根据自己的指标独立地预填充和解码缩放。
  • topology 使用 packDomain 约束:rack 告知 KAI Scheduler 将所有 Clie 打包到同一机架中,从而通过高带宽互连优化预填充和解码阶段之间的 KV 缓存传输。

应用此清单后,您可以检查 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、三个 PodCliques (每个角色一个) 、一个用于协调调度的 PodGang,以及匹配每个角色副本数量的 Pod。这个startsAfter通过 init 容器强制执行依赖关系:预填充和解码 pod 在路由器准备就绪后才启动主容器。

扩展分解的工作负载

在运行解工作负载后,扩展成为核心运营挑战。预填充和解码存在不同的瓶颈;团队可能希望根据到第一个 token (TTFT) 的时间自动扩展预填充工作流,并独立基于 inter-token 延迟 (ITL) 对工作流进行解码,以满足服务水平协议 (SLA) 的要求,同时更大限度地降低 GPU 成本。

在实践中,分解扩展在三个级别上运行:

  1. 按角色扩展:在单个角色中添加或删除 Pod (例如。将预填充副本从 4 个扩展到 6 个)
  2. 按 TP 组扩展: 将完整的 Tensor Parallel 组扩展为原子单元,因为您无法添加半个 TP 组。
  3. 跨角色协调: 当您添加预填充容量时,您可能还需要扩展路由器以处理更高的吞吐量,或者扩展解码以消耗额外的预填充输出。

不同的工具适用于不同的级别。

推理框架如何协调扩展

推理框架可通过自定义自动缩放器解决应用级别的扩展问题,这些自动缩放器可监控特定于推理的指标。llm-d 的工作负载变体自动缩放器 (WVA) 可通过 Prometheus 监控每个 Pod 的 KV 缓存利用率和队列深度,并使用闲置容量模型来确定何时应添加或删除副本。WVA 不会直接扩展部署,而是会将目标副本数量作为基于 HPA/ Kubernetes 的标准事件驱动自动缩放 (KEDA) 所遵循的 Prometheus 指标,从而将缩放驱动保留在 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 规划器或 llm-d 的自动缩放器) 可以协调决策和更新。协调逻辑位于自动缩放器中,而非 Kubernetes 资源本身。

使用 Grove 进行扩展

Grove 将每个角色的扩展功能整合到单个资源中。每个 PodClique 都有自己的副本数量和可选项autoScalingConfig因此,HPA 可以根据每个角色的指标独立管理角色:

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

操作员创建额外的预填充 Pod,同时保持路由器和解码不变:

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、两个路由器 POD、两个解码 POD,仅对预填充进行了更改。

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

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

对于副本:二,这将创建两个完整的预填充实例:两个 x (一个领导者+ 四个工作者) = 总共 10 个 POD。这个最小可用:1保证意味着系统不会扩展到一个完整的 Tensor Parallel 组以下。

将群组从两个副本扩展到三个副本可添加第三个完整实例,同时保持 1:4 的领导者与员工比率:领导者与员工比率:

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

领导者和工人派系共同组成一个整体,新的复制品 ( prefill-2) 有一个请求者POD 和 4工作者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

开始使用

无论您是运行单个解管道,还是在集群中运行数十个,这方面的基础模组都在不断涌现,社区正在以开放的方式构建它们。本博客中的每种方法都代表了简单性和集成协调性之间的不同点。

正确的选择取决于您的工作负载、团队的运营模型,以及您希望平台处理的生命周期管理与应用层之间的关系。

有关更多信息,请查看这些资源。

与我们一起参加 Kubecon EU

如果您要参加在阿姆斯特丹举办的 KubeCon EU 2026,请亲临展位号 241 并 参加会议,我们将介绍一个端到端开源 AI 推理堆栈。探索 Grove 部署指南,并在 GitHubDiscord 上提问。我们很想听听您对 Kubernetes 上的分解推理有何看法。

标签