随着大语言模型(LLM)推理工作负载日益复杂,单一的整体服务进程开始触及自身极限。预填充和解码阶段有着根本不同的计算特征,而传统部署方式将它们强制放在同一硬件上,导致GPU利用率不足且扩缩容缺乏灵活性。
分离式服务通过将推理管道拆分为不同的阶段(如预填充、解码和路由)来解决这一问题,每个阶段都作为一个独立的服务运行,可以按自身需求进行资源配置和扩缩容。
本文将概述如何在Kubernetes上部署分离式推理,探讨不同的生态系统方案及其在集群上的执行方式,并评估它们开箱即用的能力。
在深入了解Kubernetes配置文件之前,先理解LLM的两种推理部署模式会有所帮助:在聚合式服务中,单个进程(或紧密耦合的进程组)处理从输入到输出的整个推理生命周期。分离式服务则将管道拆分为不同阶段,如预填充、解码和路由,每个阶段作为独立服务运行(见下文图1)。
图1. 聚合式与分离式服务对比
在传统的聚合式部署中,单个模型服务器(或并行配置中的协调服务器组)处理完整的请求生命周期。用户提示输入后,服务器对其进行分词,运行预填充以构建上下文,自回归地生成输出令牌(解码),并返回响应。整个过程发生在一个进程或紧密耦合的Pod组中。
这在概念上很简单,适用于许多用例。但这意味着硬件需要在两种根本不同的工作负载之间交替:预填充是计算密集型的,受益于高浮点运算;而解码受内存带宽限制,受益于大容量、快速的内存。
分离式架构将这些阶段拆分为不同的服务:
像NVIDIA Dynamo和llm-d这样的框架实现了这种模式。问题变成了:如何在Kubernetes上编排它?
部署多Pod推理工作负载(无论是模型并行的聚合模型还是分离模型)只是成功的一半。调度器如何在集群中放置Pod直接影响性能:将张量并行组的Pod放置在具有高带宽NVIDIA NVLink互连的同一机架上,可能意味着快速推理和网络瓶颈之间的差异。这里有三个最重要的调度能力:
这三种能力决定了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度张量并行):
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度张量并行):
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"每个角色作为自己的资源进行管理。可以独立地扩缩容、预填充和解码,并按不同时间表更新它们。
需要注意的是,调度器将预填充工作节点和解码工作节点视为独立的工作负载。调度器会成功放置它们,但不知道它们构成单个推理管道。实际上,这意味着几件事:
最后一点值得指出。推理框架发展迅速,并不总是保证版本间的向后兼容性,因此旧版本的预填充Pod和新版本的解码Pod可能无法通信。模型加载也需要时间,预填充和解码工作节点经常以不同的速度准备就绪。在未同步的推出过程中,这可能会造成暂时的失衡,许多新解码Pod准备就绪而很少新预填充Pod准备就绪(或相反)。这会在推理管道中造成瓶颈,直到一切赶上。
这些模式是可行的。协调只是发生在Kubernetes原语之外:在推理框架的路由层、在自定义自动扩缩器、专用操作器甚至手动操作中。另一种选择是使用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: rackGrove操作器管理每个角色的PodClique,并协调所有角色之间的调度、启动和生命周期。YAML中的几个注意点:
startsAfter: [router] 告诉操作器在路由器就绪之前门控它们的启动。这是声明式表达的,并通过初始化容器强制执行。当首次部署时,路由器Pod首先启动并准备就绪,然后预填充和解码Pod并行启动(因为两者都依赖于路由器)。autoScalingConfig 允许定义每个角色的扩缩容策略。操作器为每个角色创建一个水平Pod自动扩缩器,因此预填充和解码基于各自的指标独立扩缩容。topologyConstraint 与 packDomain: rack 告诉KAI调度器将所有clique打包在同一机架内,优化预填充和解码阶段之间通过高带宽互连的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,三个PodClique(每个角色一个),一个用于协调调度的PodGang,以及与每个角色副本数匹配的Pod。startsAfter 依赖通过初始化容器强制执行:预填充和解码Pod等待路由器准备就绪后才启动其主容器。
一旦分离式工作负载运行起来,扩缩容就成为核心操作挑战。预填充和解码有不同的瓶颈,团队可能希望根据首令牌时间和令牌间延迟独立地自动扩缩容预填充工作节点和解码工作节点,以满足服务级别协议,同时最小化GPU成本。
在实践中,分离式扩缩容在三个层面运作:
不同的工具处理不同的层面。
推理框架通过具有推理特定指标可见性的自定义自动扩缩器在应用程序层面解决扩缩容问题。llm-d的工作负载变体自动扩缩器通过Prometheus监控每个Pod的KV缓存利用率和队列深度,使用备用容量模型来确定何时应添加或移除副本。WVA不直接扩缩容部署,而是将目标副本数作为Prometheus指标发出,由标准HPA/KEDA执行扩缩容——将扩缩容执行保持在Kubernetes原生原语内。
NVIDIA Dynamo规划器采用不同的方法:它原生理解分离式服务,分别运行以TTFT和ITL SLA为目标的预填充和解码扩缩容循环。它使用时序模型预测即将到来的需求,从预先分析的每GPU吞吐量曲线计算副本需求,并在两个角色之间强制执行全局GPU预算。
这种全局可见性很重要,因为在实践中,预填充和解码之间存在一个最优比率,该比率随请求模式而变化。将预填充扩展3倍而不扩展解码,额外的输出无处可去——解码成为瓶颈,KV缓存传输队列堆积。应用程序级自动扩缩器可以处理这个问题,因为它们可以看到整个管道;而针对单个资源的Kubernetes原生HPA本身不维护跨资源比率。
每个角色一个LWS,独立扩缩容每个角色:
kubectl scale lws prefill-workers --replicas=6
kubectl scale lws decode-workers --replicas=3标准HPA可以分别针对每个LWS,或者外部自动扩缩器(如Dynamo规划器或llm-d的自动扩缩器)做出协调决策并更新两者。协调逻辑存在于自动扩缩器中,而不是Kubernetes资源本身。
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 # 新增
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组成的配置中:
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的领导者与工作节点比率:
$ kubectl scale pcsg inference-disaggregated-0-prefill --replicas=3领导者和工作节点clique作为一个单元一起扩缩容,新副本(prefill-2)有一个pleader Pod和四个pworker Pod,匹配该比率。为第三个副本创建了一个新的PodGang,以确保它被gang调度。
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 # 新增无论是在集群上运行单个分离式管道还是操作数十个管道,这些构建块正在出现,社区正在公开地构建它们。本文中的每种方法都代表了简单性和集成协调之间光谱上的不同点。
正确的选择取决于工作负载、团队的操作模型,以及希望平台处理多少生命周期管理而不是应用程序层。
查看更多信息的资源:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。