AI 推理服务的资源超卖策略:在稳定与效率之间找平衡
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 配额而不是直接驱逐。第三层过载保护。即便有超卖和抢占极端情况下物理资源仍然可能不够。需要在入口做流量控制拒绝超出物理容量上限的新请求返回 429Too 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(levellogging.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_factorylambda: GPUResource(0, 0)) # 可抢占标记 preemptible: bool True # False 表示不可抢占 created_at: float field(default_factorytime.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(), keylambda 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_gb640, oversell_ratio1.5) # 部署服务 services [ ServiceRequest(agent-online, Priority.P0_ONLINE, GPUResource(160, 100), preemptibleFalse), 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。只要你把最关键的负载设置了不可抢占 独占资源其余负载的超卖就是安全的。