AI 推理服务的资源超卖策略:在稳定与效率之间找平衡
一、你的 GPU 集群平均利用率 35%,但老板说"再买 GPU 预算批不下来"
GPU 推理服务的资源利用率悖论:为了保证稳定性,给每个推理服务留了 30-50% 的 GPU 显存 buffer——防止突发流量导致 OOM。结果是 8 张 A100 GPU,平均利用率不到 40%,但每次高峰期仍然有服务报 OOM。
问题在于"静态资源分配"——每个推理服务独占 GPU 或 GPU 的一部分,无论它当前在用不用。A 服务在下午 2 点高峰期跑满,B 服务同一时间几乎无负载——B 的 GPU 闲置,A 却不能借用。这就是资源超卖(Resource Oversubscription)需要解决的问题:允许服务申请超过物理资源的"逻辑资源",通过调度器动态分配,在整体利用率低时给需要资源的服务多分配。
但超卖有风险——当多个服务的"高峰期"重叠时,物理资源不够分,必须有所取舍。这正是资源超卖策略的核心:定义优先级、抢占规则、过载保护。
二、底层机制与原理剖析
资源超卖的三层机制:
第一层:超卖率设定。超卖率 = 逻辑资源总量 / 物理资源总量。例如:8 张 A100 共 640GB 显存,超卖率 1.5x 意味着允许申请总量 960GB 显存。超卖率的选择依赖于"所有服务的峰值是否同时发生"——如果服务的峰值时间错开(如在线服务白天高峰、批处理夜间执行),超卖率可以设高。如果同时间段服务同时跑来峰值,超卖率只能接近 1.0。
第二层:优先级抢占。当物理资源不足时,低优先级的服务可以被"抢占"(驱逐),把资源让给高优先级服务。抢占不是"杀掉进程"——GPU 推理服务加载模型权重需要 30-60 秒。更温和的做法是:降低低优先级服务的 batch size 和 GPU 配额,而不是直接驱逐。
第三层:过载保护。即便有超卖和抢占,极端情况下物理资源仍然可能不够。需要在入口做流量控制:拒绝超出物理容量上限的新请求,返回 429(Too Many Requests)或排队等待。
三、生产级代码实现
"""
GPU 资源超卖调度器
核心数据:资源池、服务优先级、抢占规则
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from enum import IntEnum
import logging
import time
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class Priority(IntEnum):
"""服务优先级(数字越小优先级越高)"""
P0_ONLINE = 0 # 在线推理——不可抢占
P1_BATCH = 1 # 批处理推理——可被 P0 抢占
P2_EXPERIMENT = 2 # 实验/开发——可被任何优先级抢占
@dataclass
class GPUResource:
"""GPU 资源配置"""
vram_gb: float # 显存(GB)
compute_percent: float # 计算占比(0-100)
@dataclass
class ServiceRequest:
"""服务资源请求"""
service_id: str
priority: Priority
requested: GPUResource
# 当前实际使用量(可能低于 requested)
actual_usage: GPUResource = field(default_factory=lambda: GPUResource(0, 0))
# 可抢占标记
preemptible: bool = True # False 表示不可抢占
created_at: float = field(default_factory=time.time)
class GPUResourcePool:
"""
GPU 资源池管理
三种状态:
– FREE: 空闲,可分配给任何请求
– ALLOCATED: 已分配,正在使用
– PREEMPTIBLE: 已分配但可被抢占(低优先级服务的资源)
"""
def __init__(self, total_vram_gb: float, oversell_ratio: float = 1.5):
self.total_vram = total_vram_gb
self.oversell_ratio = oversell_ratio
# 逻辑容量(允许超卖后的总量)
self.logical_capacity = total_vram_gb * oversell_ratio
# 当前使用量
self.allocated_vram = 0.0 # 已分配的显存总量
self.actual_usage_vram = 0.0 # 实际使用的显存
# 活跃服务
self.services: Dict[str, ServiceRequest] = {}
logger.info("GPU Pool: %.0fGB physical, %.0fGB logical (%.1fx oversell)",
total_vram_gb, self.logical_capacity, oversell_ratio)
def can_allocate(self, request: ServiceRequest) -> bool:
"""
判断是否可以分配资源
检查两个条件:
1. 逻辑容量是否足够(超卖角度的检查)
2. 物理容量 + 可抢占资源是否足够(安全角度的检查)
"""
# 条件 1:逻辑容量检查
if self.allocated_vram + request.requested.vram_gb > self.logical_capacity:
# 超过逻辑容量 → 如果该请求优先级高于某个已有服务,可以抢占
preemptible_vram = self._get_preemptible_vram()
if (self.allocated_vram – preemptible_vram + request.requested.vram_gb
<= self.logical_capacity):
# 通过抢占可以容纳
return True
return False
# 条件 2:物理容量检查
physical_available = self.total_vram – self.actual_usage_vram
if request.requested.vram_gb > physical_available:
# 物理容量不足 → 需要抢占
preemptible_usage = self._get_preemptible_actual_usage()
if request.requested.vram_gb > physical_available + preemptible_usage:
return False
return True
def allocate(self, request: ServiceRequest) -> List[str]:
"""
分配资源——可能需要抢占低优先级服务
返回:被抢占的服务 ID 列表
"""
preempted = []
# 如果需要抢占
physical_available = self.total_vram – self.actual_usage_vram
if request.requested.vram_gb > physical_available:
shortage = request.requested.vram_gb – physical_available
# 从最低优先级开始抢占
sorted_services = sorted(
self.services.values(),
key=lambda s: (s.priority, -s.created_at), # 低优先级 + 新来的优先被抢
)
for svc in sorted_services:
if shortage <= 0:
break
if svc.priority <= request.priority:
# 同优先级或更高优先级——不能抢占
continue
if not svc.preemptible:
continue
# 抢占这个服务
shortage -= svc.actual_usage.vram_gb
preempted.append(svc.service_id)
logger.warning("Preempting %s (P%d) for %s (P%d)",
svc.service_id, svc.priority,
request.service_id, request.priority)
# 注册服务
self.services[request.service_id] = request
self.allocated_vram += request.requested.vram_gb
self.actual_usage_vram += request.requested.vram_gb
# 移除被抢占的服务
for svc_id in preempted:
self._remove_service(svc_id)
logger.info("Allocated %.0fGB to %s (P%d) | Pool: %.0f/%.0fGB phys, %.0f/%.0fGB logical",
request.requested.vram_gb, request.service_id, request.priority,
self.actual_usage_vram, self.total_vram,
self.allocated_vram, self.logical_capacity)
return preempted
def release(self, service_id: str):
"""释放服务占用的资源"""
self._remove_service(service_id)
def update_actual_usage(self, service_id: str, actual: GPUResource):
"""更新服务的实际使用量"""
if service_id in self.services:
old_actual = self.services[service_id].actual_usage
self.actual_usage_vram -= old_actual.vram_gb
self.actual_usage_vram += actual.vram_gb
self.services[service_id].actual_usage = actual
def get_utilization(self) -> Tuple[float, float]:
"""获取利用率(物理 / 逻辑)"""
phys_util = self.actual_usage_vram / self.total_vram * 100
logical_util = self.allocated_vram / self.logical_capacity * 100
return phys_util, logical_util
def _get_preemptible_vram(self) -> float:
"""获取可抢占的显存总量(逻辑分配)"""
return sum(
s.requested.vram_gb
for s in self.services.values()
if s.preemptible
)
def _get_preemptible_actual_usage(self) -> float:
"""获取可抢占的显存实际使用量"""
return sum(
s.actual_usage.vram_gb
for s in self.services.values()
if s.preemptible
)
def _remove_service(self, service_id: str):
if service_id in self.services:
svc = self.services.pop(service_id)
self.allocated_vram -= svc.requested.vram_gb
self.actual_usage_vram -= svc.actual_usage.vram_gb
# —————————————————————————
# 模拟使用
# —————————————————————————
if __name__ == "__main__":
pool = GPUResourcePool(total_vram_gb=640, oversell_ratio=1.5)
# 部署服务
services = [
ServiceRequest("agent-online", Priority.P0_ONLINE,
GPUResource(160, 100), preemptible=False),
ServiceRequest("batch-analysis", Priority.P1_BATCH,
GPUResource(120, 80)),
ServiceRequest("model-eval", Priority.P2_EXPERIMENT,
GPUResource(100, 60)),
ServiceRequest("new-service", Priority.P1_BATCH,
GPUResource(80, 60)),
]
for svc in services:
can = pool.can_allocate(svc)
print(f" {svc.service_id} (P{int(svc.priority)}): "
f"可分配={can}, 需要={svc.requested.vram_gb:.0f}GB")
if can:
preempted = pool.allocate(svc)
if preempted:
print(f" → 抢占: {preempted}")
phys, log = pool.get_utilization()
print(f"\\n利用率: 物理 {phys:.1f}% / 逻辑 {log:.1f}%")
四、边界分析与架构权衡
超卖率的设定:
- 超卖率过高(3x+)→ 高峰期大量抢占 → 低优先级服务频繁被驱逐 → 批处理和实验任务无法完成
- 超卖率过低(<1.2x)→ 利用率提升有限 → 没有解决问题
- 推荐从 1.3x 起步,观察一个完整的业务周期(含周末)的抢占次数。如果一周内抢占 < 5 次,可以逐步提高到 1.5-1.8x
抢占的伤害控制:
- 被抢占的服务不是杀掉退——而是"降级"。降低被抢占服务的 batch size(继续运行,但慢一点),或者将其排队等待资源释放
- 抢占前给服务一个 grace period(如 30 秒)来保存 Checkpoint 再退出
- 对可抢占的批处理任务做 Checkpoint,被抢占后可以从断点恢复而不是从头开始
不适合超卖的负载类型:
- 延时敏感的在线推理——超卖导致资源竞争,延迟不可控
- 状态ful 的模型推理——被抢占后需要重新加载模型权重,冷启动成本太高
- 安全关键型负载——不能接受任何因抢占导致的不可用
五、总结
GPU 资源超卖本质是用稳定性(增加低优先级任务的抢占风险)换效率(提高整体 GPU 利用率)。核心三机制:超卖率(逻辑容量/物理容量)、优先级抢占(低优先级被高优先级驱逐)、过载保护(防止超卖过头导致全面崩溃)。关键是优先级体系设计——在线推理不可抢占(P0),批处理可被在线抢占(P1),实验任务随时被抢占(P2)。只要你把最关键的负载设置了"不可抢占 + 独占资源",其余负载的超卖就是安全的。



