多租户推理部署最常见的翻车现场,不是模型精度下降,而是同一批GPU上跑着实时对话、离线摘要、内部评测等多个业务时,某个租户突然把并发拉高,显存瞬间被KV cache占满,其他服务的请求从正常几十毫秒变成几秒超时。推理负载和训练不同,它的请求到达时间不可预测、序列长度差异大、延迟要求严格,因此不能只靠简单的先到先得或者手动分配几张卡来解决。本文从资源隔离和优先级队列调度两条主线展开,先拆解资源争抢发生在哪些环节,再给出不同层次的隔离手段和调度方案,最后讨论监控、超卖与降级这些落地时容易忽略的细节。

一、先看资源争抢发生在哪些环节
推理服务的资源争抢并不只是显存不够,通常会同时出现在计算单元、显存容量、显存带宽、PCIe带宽以及CPU内存拷贝这几条链路上。一个典型场景是:租户A发来一批超长序列请求,模型为每个token缓存KV状态,很快把剩余显存吃光;租户B此时提交的新请求即使优先级更高,也会因为显存分配失败而报OOM。与此同时,GPU的SM单元可能并没有打满,因为计算负载被显存压力卡住了。这说明如果只看GPU利用率一个指标,往往会错过真实瓶颈。
调度层面的争抢同样隐蔽。许多推理网关默认使用FIFO队列,所有请求按到达顺序排队。一旦离线批量任务排到了实时请求前面,实时请求的尾延迟就会急剧恶化。更棘手的是,长序列请求和短序列请求混在一个批次里,会导致短请求被长请求拖累,整个batch的延迟取决于最慢的那一个。因此,多租户部署必须在多个维度上同时设置边界,否则单点优化很难稳定。
理解争抢的层次之后,就可以针对性地设计策略:显存和计算单元适合用硬隔离或容器限制进行配额管理;请求排队和调度顺序适合用优先级队列配合抢占机制;而像KV cache这种与序列长度强相关的资源,则需要在推理框架层通过参数限制每个租户的最大并发和最大序列长度。
二、资源隔离:从GPU硬隔离到推理框架参数
严格的多租户隔离可以从NVIDIA MIG开始。MIG能把一张A100或H100切成多个独立的GPU实例,每个实例拥有独立的SM、显存和L2缓存,租户之间几乎不会互相影响。这种方式适合对安全性和稳定性要求极高的场景,比如不同客户的私有推理服务。但MIG的缺点是切分粒度固定,切分后单个实例算力会下降,而且不是所有推理框架都对MIG有良好支持,需要提前测试。
如果不需要那么强的隔离,MPS(Multi-Process Service)是另一个选择。MPS允许多个进程共享同一GPU,同时通过上下文隔离降低争抢,可以在一定程度上提升小负载下的GPU利用率。不过在显存保护和错误隔离方面,MPS不如MIG彻底,一个进程崩溃仍可能影响到其他进程。因此实际部署中,MPS通常和容器资源限制配合使用。
在Kubernetes环境中,容器级别的限制是基础。通过设置requests和limits可以控制CPU和内存,而GPU显存则需要依赖推理框架自身限制或nvidia-device-plugin的显存分配策略。以vLLM为例,下面这个Pod配置同时限制了CPU、内存和推理框架的显存使用比例,并限制了并发序列数和最大长度,能有效防止单个租户无限制地抢占显存。
apiVersion: v1
kind: Pod
metadata:
name: inference-tenant-a
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
resources:
requests:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: "1"
limits:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: "1"
args:
- --model
- /models/llama-3-8b
- --gpu-memory-utilization
- "0.6"
- --max-num-seqs
- "32"
- --max-model-len
- "4096"
这里的gpu-memory-utilization设为0.6,表示该推理实例最多使用单卡总显存的60%,剩余显存留给其他进程或系统缓冲。max-num-seqs限制同时处理的序列数量,防止单个租户通过大量短请求占满计算队列;max-model-len则限制最大序列长度,避免超长请求把KV cache撑爆。这三个参数组合起来,能够在推理框架层形成一个软隔离边界,比只依赖容器显存限制更贴近推理负载的实际行为。
三、优先级队列调度:让关键请求先获得资源
资源隔离解决了单个租户最多能用多少资源,但资源紧张时先给谁用,需要优先级队列来决策。优先级队列的核心不是简单的插队,而是把请求按业务重要性和延迟敏感度分成多层。例如实时对话请求进入高优先级队列,离线报表生成进入低优先级队列,模型评测和A/B测试可以放在中等优先级。当高优先级请求到达时,调度器可以暂停或驱逐低优先级任务,把资源腾出来。
实现优先级队列有两种常见路径。一种是在推理网关层做排队,比如在API网关或消息队列中根据请求头、API key或模型名称打标,再路由到不同优先级的队列。使用Redis Streams或Kafka搭建多级队列时,消费者可以优先拉取高优先级队列的消息,低优先级队列只在空闲时被消费。这种方案的优点是灵活,不依赖底层调度器,适合已有消息中间件的团队。
另一种是在Kubernetes调度层实现,使用Volcano或Kueue这类批调度组件。它们支持定义多个Queue,每个Queue有独立的资源配额和权重,并能在资源不足时触发抢占。下面是一个Volcano的Queue和PodGroup配置示例,高优先级队列的权重更高,且允许在资源紧张时回收低优先级队列的资源。
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: high-priority-queue
spec:
weight: 3
reclaimable: true
capability:
cpu: "20"
memory: "80Gi"
nvidia.com/gpu: "8"
---
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
name: inference-high-priority
spec:
minMember: 1
queue: high-priority-queue
priorityClassName: high-priority
这个配置中,weight为3表示高优先级队列在资源竞争时能获得更多份额,reclaimable为true允许它从低优先级队列回收资源。PodGroup通过priorityClassName关联到Kubernetes的PriorityClass,当集群资源不足时,高优先级Pod可以触发低优先级Pod的驱逐。实际的抢占行为还取决于调度器的抢占策略,需要与集群管理员确认是否开启。
如果不想引入额外的调度组件,也可以在推理服务内部实现一个简单的优先级队列。下面这段Python代码用堆结构维护请求优先级,优先级数值越小越先处理,可以作为自定义推理网关的排队逻辑。
import heapq
from dataclasses import dataclass, field
from typing import Any
@dataclass(order=True)
class PrioritizedRequest:
priority: int
seq_id: int = field(compare=False)
payload: Any = field(compare=False)
class PriorityQueue:
def __init__(self):
self._heap = []
self._counter = 0
def push(self, priority, payload):
heapq.heappush(self._heap, PrioritizedRequest(priority, self._counter, payload))
self._counter += 1
def pop(self):
if self._heap:
return heapq.heappop(self._heap).payload
return None
这种进程内队列适合单实例或网关层场景,优点是实现简单、延迟低,缺点是无法跨多个推理实例协调优先级。一旦推理服务扩容成多个副本,就需要把队列外置到Redis或Kafka,或者依赖Kubernetes调度器来做全局决策。实际项目中,往往采用网关层队列与Kubernetes队列配合的方式:网关负责请求级优先级排序,Kubernetes负责Pod级资源的抢占和重新分配。
四、工程落地:监控、超卖与降级策略
只做隔离和队列还不够,多租户系统必须可观测。关键指标包括请求排队时间、GPU显存使用率、KV cache命中率、单请求延迟分位数以及OOM事件次数。排队时间突然上升通常意味着某个租户的负载超过了配额,显存使用率长期接近限制值则提示隔离边界需要调整。把这些指标接入Prometheus和Grafana后,可以按租户、模型和队列维度拆分,快速定位是谁在抢占资源。
超卖是提升集群利用率的常见手段,但在推理场景中必须谨慎。可以设置较低的requests和较高的limits,让调度器按requests分配资源,同时允许Pod在空闲时突发使用更多资源。例如一个Pod的GPU requests设为0.4,limits设为1.0,在低峰期它能用满整卡,但高峰期多个Pod会按0.4的份额排队。这种策略必须配合优先级抢占,否则低优先级Pod可能一直占用空闲资源,导致高优先级Pod无法启动。Kubernetes的ResourceQuota和LimitRange可以用来约束每个命名空间的总资源量,避免单个租户请求超量。
降级策略是最后一道防线。当高优先级请求到达而资源不足时,可以驱逐低优先级Pod,或者让低优先级请求排队等待。推理网关层也可以返回503状态码并让客户端重试,或者在显存不足时自动切换到更小的模型或更短的上下文长度。下面是一个Kubernetes PriorityClass的示例,用于定义高优先级工作负载,配合驱逐策略可以在资源紧张时优先保证关键推理服务。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000
globalDefault: false
description: "High priority inference workload"
---
apiVersion: v1
kind: Pod
metadata:
name: inference-high
spec:
priorityClassName: high-priority
containers:
- name: vllm
image: vllm/vllm-openai:latest
需要强调的是,优先级和抢占必须和资源配额绑定,否则高优先级任务会无限制地抢占低优先级任务,导致低优先级任务长期饥饿。合理的做法是给每个优先级队列设置最小资源保证,比如高优先级队列至少保留30%的GPU资源,低优先级队列在空闲时可以借用,但高优先级请求到来时能够快速回收。这种“保证+借用+抢占”的模式,既兼顾了资源利用率,又能在关键时刻保护关键业务。
多租户推理部署的资源争抢问题,本质上是资源边界的确定和调度顺序的决策。硬隔离和软限制划定了每个租户的活动范围,优先级队列和抢占机制决定了资源紧张时的分配顺序。把这两个层面配合起来,再通过监控指标持续调整配额和权重,才能在共享GPU池上同时获得稳定的延迟和较高的利用率。