一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

AI 推理服务的资源超卖策略:在稳定与效率之间找平衡

时间:2026-08-26 10:57:50 编辑:袖梨 来源:一聚教程网

AI 推理服务的资源超卖策略:在稳定与效率之间找平衡并不只看表面做法,关键还要理解相关条件、限制和后续影响。

AI 推理服务的资源超卖策略:在稳定与效率之间找平衡

一、你的 GPU 集群平均利用率 35%,但老板说"再买 GPU 预算批不下来"

GPU 推理服务的资源利用率悖论:为了保证稳定性,给每个推理服务留了 30-50% 的 GPU 显存 buffer——防止突发流量导致 OOM。结果是 8 张 A100 GPU,平均利用率不到 40%,但每次高峰期仍然有服务报 OOM。

AI 推理服务的资源超卖策略:在稳定与效率之间找平衡

问题在于"静态资源分配"——每个推理服务独占 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, fieldfrom typing import Dict, List, Optional, Tuplefrom enum import IntEnumimport loggingimport timelogging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__)class Priority(IntEnum):"""服务优先级(数字越小优先级越高)"""P0_ONLINE = 0 # 在线推理——不可抢占P1_BATCH = 1 # 批处理推理——可被 P0 抢占P2_EXPERIMENT = 2# 实验/开发——可被任何优先级抢占@dataclassclass GPUResource:"""GPU 资源配置"""vram_gb: float # 显存(GB)compute_percent: float # 计算占比(0-100)@dataclassclass ServiceRequest:"""服务资源请求"""service_id: strpriority: Priorityrequested: 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_gbself.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 Truereturn False# 条件 2:物理容量检查physical_available = self.total_vram - self.actual_usage_vramif request.requested.vram_gb > physical_available:# 物理容量不足 → 需要抢占preemptible_usage = self._get_preemptible_actual_usage()if request.requested.vram_gb > physical_available + preemptible_usage:return Falsereturn Truedef allocate(self, request: ServiceRequest) -> List[str]:"""分配资源——可能需要抢占低优先级服务返回:被抢占的服务 ID 列表"""preempted = []# 如果需要抢占physical_available = self.total_vram - self.actual_usage_vramif 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:breakif svc.priority <= request.priority:# 同优先级或更高优先级——不能抢占continueif not svc.preemptible:continue# 抢占这个服务shortage -= svc.actual_usage.vram_gbpreempted.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] = requestself.allocated_vram += request.requested.vram_gbself.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 preempteddef 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_usageself.actual_usage_vram -= old_actual.vram_gbself.actual_usage_vram += actual.vram_gbself.services[service_id].actual_usage = actualdef get_utilization(self) -> Tuple[float, float]:"""获取利用率(物理 / 逻辑)"""phys_util = self.actual_usage_vram / self.total_vram * 100logical_util = self.allocated_vram / self.logical_capacity * 100return phys_util, logical_utildef _get_preemptible_vram(self) -> float:"""获取可抢占的显存总量(逻辑分配)"""return sum(s.requested.vram_gbfor s in self.services.values()if s.preemptible)def _get_preemptible_actual_usage(self) -> float:"""获取可抢占的显存实际使用量"""return sum(s.actual_usage.vram_gbfor 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_gbself.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)。只要你把最关键的负载设置了"不可抢占 + 独占资源",其余负载的超卖就是安全的。

热门栏目