首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >在K8s上部署分离式LLM推理

在K8s上部署分离式LLM推理

原创
作者头像
用户11764306
发布2026-04-16 10:30:09
发布2026-04-16 10:30:09
140
举报

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

分离式服务通过将推理管道拆分为不同的阶段(如预填充、解码和路由)来解决这一问题,每个阶段都作为一个独立的服务运行,可以按自身需求进行资源配置和扩缩容。

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

聚合式与分离式推理有何不同?

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

图1. 聚合式与分离式服务对比

聚合式推理

在传统的聚合式部署中,单个模型服务器(或并行配置中的协调服务器组)处理完整的请求生命周期。用户提示输入后,服务器对其进行分词,运行预填充以构建上下文,自回归地生成输出令牌(解码),并返回响应。整个过程发生在一个进程或紧密耦合的Pod组中。

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

分离式推理

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

  • 预填充工作节点处理输入提示。这是计算密集型的。需要最大化GPU以实现高吞吐量,并可以进行积极的并行化。
  • 解码工作节点逐个生成输出令牌。由于LLM的自回归特性,这受内存带宽限制。需要具有快速高带宽内存访问的GPU。
  • 路由器/网关负责引导传入请求,管理预填充和解码阶段之间的键值缓存路由,并处理工作节点间的请求负载均衡。

为什么要分离?三个突出原因:

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

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

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

部署多Pod推理工作负载(无论是模型并行的聚合模型还是分离模型)只是成功的一半。调度器如何在集群中放置Pod直接影响性能:将张量并行组的Pod放置在具有高带宽NVIDIA NVLink互连的同一机架上,可能意味着快速推理和网络瓶颈之间的差异。这里有三个最重要的调度能力:

  • Gang调度确保组中的所有Pod以“全有或全无”的语义被放置,避免部分部署浪费GPU。
  • 分层Gang调度将基本Gang调度扩展到多级工作负载。在分离式推理中,需要每个组件或角色的嵌套最小保证:每个张量并行组(例如,组成一个解码实例的四个Pod)必须原子地调度,而整个系统(至少n个预填充实例 + 至少m个解码实例 + 路由器)也需要系统级协调。没有这一点,一个角色可能消耗所有可用GPU,而另一个角色无限期等待——这是一种部分部署,占用了资源却无法服务请求。
  • 拓扑感知放置将紧密耦合的Pod放置在高带宽互连的节点上,最小化节点间通信延迟。

这三种能力决定了AI调度器(如KAI Scheduler)如何根据应用程序的调度约束来放置Pod。此外,AI编排层确定需要gang调度什么以及何时调度也同样重要。例如,当预填充独立扩缩容时,需要有人决定新Pod形成一个具有最小可用性保证的gang,而不干扰现有的解码Pod。因此,编排层和调度器需要在整个应用程序生命周期中紧密协作,处理多级自动扩缩容、滚动更新等,以确保AI工作负载的最佳运行时条件。

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

然后,KAI Scheduler扮演关键角色,满足这些约束,解决“如何”做的问题:gang调度、分层gang调度和拓扑感知放置。在本文中,我们使用KAI作为调度器,尽管社区中还有其他支持这些功能子集的调度器。读者可以通过云原生计算基金会生态系统探索更广泛的调度格局。

部署分离式推理

分离式架构具有多个角色,每个角色具有不同的资源特征和扩缩容需求。由于分离式管道中的每个角色都是一个不同的工作负载,使用LWS的一种自然方法是为每个角色创建一个单独的资源。

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

代码语言:yaml
复制
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"

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

代码语言:yaml
复制
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"

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

代码语言:yaml
复制
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缓存传输)需要手动添加Pod亲和性规则,这些规则引用两个LWS资源中的标签。
  • 扩缩容一个角色不会自动考虑另一个角色:如果长上下文请求的突发需要更多预填充容量,可以扩展预填充工作节点,但除非自己配置了亲和性,否则新的预填充Pod不能保证放置在现有解码Pod附近。
  • 推出新模型版本意味着协调跨三个独立资源的更新——LWS的分区更新机制支持每个资源的分阶段推出,但跨资源的同步是外部管理的。

最后一点值得指出。推理框架发展迅速,并不总是保证版本间的向后兼容性,因此旧版本的预填充Pod和新版本的解码Pod可能无法通信。模型加载也需要时间,预填充和解码工作节点经常以不同的速度准备就绪。在未同步的推出过程中,这可能会造成暂时的失衡,许多新解码Pod准备就绪而很少新预填充Pod准备就绪(或相反)。这会在推理管道中造成瓶颈,直到一切赶上。

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

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

代码语言:yaml
复制
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操作器管理每个角色的PodClique,并协调所有角色之间的调度、启动和生命周期。YAML中的几个注意点:

  • 预填充和解码上的 startsAfter: [router] 告诉操作器在路由器就绪之前门控它们的启动。这是声明式表达的,并通过初始化容器强制执行。当首次部署时,路由器Pod首先启动并准备就绪,然后预填充和解码Pod并行启动(因为两者都依赖于路由器)。
  • 每个clique上的 autoScalingConfig 允许定义每个角色的扩缩容策略。操作器为每个角色创建一个水平Pod自动扩缩器,因此预填充和解码基于各自的指标独立扩缩容。
  • topologyConstraintpackDomain: rack 告诉KAI调度器将所有clique打包在同一机架内,优化预填充和解码阶段之间通过高带宽互连的KV缓存传输。

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

代码语言:bash
复制
$ 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 依赖通过初始化容器强制执行:预填充和解码Pod等待路由器准备就绪后才启动其主容器。

扩缩容分离式工作负载

一旦分离式工作负载运行起来,扩缩容就成为核心操作挑战。预填充和解码有不同的瓶颈,团队可能希望根据首令牌时间和令牌间延迟独立地自动扩缩容预填充工作节点和解码工作节点,以满足服务级别协议,同时最小化GPU成本。

在实践中,分离式扩缩容在三个层面运作:

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

不同的工具处理不同的层面。

推理框架如何协调扩缩容

推理框架通过具有推理特定指标可见性的自定义自动扩缩器在应用程序层面解决扩缩容问题。llm-d的工作负载变体自动扩缩器通过Prometheus监控每个Pod的KV缓存利用率和队列深度,使用备用容量模型来确定何时应添加或移除副本。WVA不直接扩缩容部署,而是将目标副本数作为Prometheus指标发出,由标准HPA/KEDA执行扩缩容——将扩缩容执行保持在Kubernetes原生原语内。

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

这种全局可见性很重要,因为在实践中,预填充和解码之间存在一个最优比率,该比率随请求模式而变化。将预填充扩展3倍而不扩展解码,额外的输出无处可去——解码成为瓶颈,KV缓存传输队列堆积。应用程序级自动扩缩器可以处理这个问题,因为它们可以看到整个管道;而针对单个资源的Kubernetes原生HPA本身不维护跨资源比率。

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

每个角色一个LWS,独立扩缩容每个角色:

代码语言:bash
复制
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可以基于每个角色的指标独立管理角色:

代码语言:bash
复制
kubectl scale pclq inference-disaggregated-0-prefill --replicas=6

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

代码语言:bash
复制
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  # 新增
pod/inference-disaggregated-0-prefill-wx56y         1/1     Running   12s  # 新增
pod/inference-disaggregated-0-decode-mn90p          1/1     Running   5m
pod/inference-disaggregated-0-decode-qr12s          1/1     Running   5m

六个预填充Pod,两个路由器Pod,两个解码Pod——只有预填充发生了变化。

对于内部使用多节点张量并行的角色,PodCliqueScalingGroup确保多个PodClique作为一个单元一起扩缩容,同时保持它们之间的副本比率。例如,在每个预填充实例由一个领导者Pod和四个工作节点Pod组成的配置中:

代码语言:yaml
复制
podCliqueScalingGroups:
  - name: prefill
    cliqueNames: [pleader, pworker]
    replicas: 2
    minAvailable: 1
    scaleConfig:
      maxReplicas: 4

使用 replicas: 2,这将创建两个完整的预填充实例:2 x (1个领导者 + 4个工作节点) = 总共10个Pod。minAvailable: 1 保证意味着系统不会缩减到少于一个完整的张量并行组。

将组从2个副本扩展到3个副本会添加第三个完整实例,同时保持1:4的领导者与工作节点比率:

代码语言:bash
复制
$ kubectl scale pcsg inference-disaggregated-0-prefill --replicas=3

领导者和工作节点clique作为一个单元一起扩缩容,新副本(prefill-2)有一个pleader Pod和四个pworker Pod,匹配该比率。为第三个副本创建了一个新的PodGang,以确保它被gang调度。

代码语言:bash
复制
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   # 新增
podclique.grove.io/inference-disaggregated-0-prefill-2-pworker    8s   # 新增

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   # 新增

入门指南

无论是在集群上运行单个分离式管道还是操作数十个管道,这些构建块正在出现,社区正在公开地构建它们。本文中的每种方法都代表了简单性和集成协调之间光谱上的不同点。

正确的选择取决于工作负载、团队的操作模型,以及希望平台处理多少生命周期管理而不是应用程序层。

查看更多信息的资源:

  • NVIDIA Grove
  • KAI Scheduler
  • NVIDIA DynamoFINISHED

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 聚合式与分离式推理有何不同?
    • 聚合式推理
    • 分离式推理
    • 为什么要分离?三个突出原因:
  • 为什么调度是Kubernetes上多Pod推理性能的关键
  • 部署分离式推理
  • 扩缩容分离式工作负载
    • 推理框架如何协调扩缩容
    • 使用单独的LWS资源进行扩缩容
    • 使用Grove进行扩缩容
  • 入门指南
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档