AI 推理服务的 GPU 资源调度——从静态分配到动态共享的架构演进实践
AI 推理服务的 GPU 资源调度——从静态分配到动态共享的架构演进实践一、背景与问题GPU 是 AI 推理服务中最昂贵的资源也是调度最复杂的资源。不同于 CPUGPU 的资源特性更非标一张 A100 80GB 显卡可能承载 1 个大模型70B的完整推理也可能同时服务 10 个小模型7B的并发请求。在早期部署中我们采用了最简单的一卡一模型静态分配策略——每个模型独占一张或多张 GPU。这种方式简单可靠但资源利用率严重不足夜间低谷时 GPU 利用率仅 12%而白天峰值时需要排队等待。引入动态 GPU 调度后整体 GPU 利用率从 38% 提升到 78%同时 P99 延迟反而降低了 22%。本文将记录这一演进过程。二、方案设计2.1 三种调度模式的演进flowchart TB subgraph 阶段一: 静态分配 S1[模型A - GPU 0,1br/模型B - GPU 2,3br/模型C - GPU 4,5,6,7] -- S2[问题GPU利用率12%-65%br/模型间无法共享GPU] end subgraph 阶段二: 模型级动态调度 S2 -- D1[模型注册到调度器br/GPU池化管理] D1 -- D2[按请求量弹性扩缩br/热门模型多副本br/冷门模型回收资源] D2 -- D3[问题扩缩延迟5分钟br/模型加载消耗GPU显存] end subgraph 阶段三: 请求级动态共享 D3 -- P1[GPU 细粒度切分br/MIG/vGPU] P1 -- P2[多模型共享单GPUbr/vLLM请求调度] P2 -- P3[GPU利用率稳定78%br/P99延迟降低22%] end2.2 核心技术栈技术作用成熟度NVIDIA MIGGPU 硬件分区A100/H100成熟vLLM 动态批处理请求级 GPU 共享成熟Triton Inference Server模型编排 动态加载成熟Kubernetes GPU OperatorGPU 池化管理成熟自研 GPU Scheduler全局 GPU 资源调度自研三、实战演示3.1 GPU 资源管控服务 GPU 资源调度服务。 管理 GPU 池的分配、回收与监控。 import asyncio import logging from dataclasses import dataclass, field from enum import Enum from typing import Dict, List, Optional, Tuple import pynvml # NVIDIA Management Library logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class GPUStatus(Enum): GPU 状态枚举 IDLE idle ALLOCATED allocated RESERVED reserved # 预留中加载模型中 MAINTENANCE maintenance dataclass class GPUResource: GPU 资源描述 gpu_id: int uuid: str memory_total_mb: int memory_used_mb: int utilization_pct: float status: GPUStatus GPUStatus.IDLE allocated_model: Optional[str] None allocated_vram_mb: int 0 dataclass class ModelDeploymentRequest: 模型部署请求 model_name: str required_vram_mb: int min_gpus: int 1 max_gpus: int 8 priority: int 0 # 优先级越大越优先 replicas: int 1 class GPUResourceScheduler: GPU 资源调度器。 核心职责 1. GPU 资源池的发现与健康检查 2. 模型的 GPU 分配与回收 3. 基于优先级的动态调度 def __init__(self): try: pynvml.nvmlInit() self.driver_version pynvml.nvmlSystemGetDriverVersion() logger.info(GPU 调度器初始化完成: 驱动版本%s, self.driver_version.decode()) # GPU 资源池UUID - GPUResource self.gpu_pool: Dict[str, GPUResource] {} # 模型部署表模型名 - 分配的 GPU UUID 列表 self.model_allocations: Dict[str, List[str]] {} except pynvml.NVMLError as e: logger.error(NVIDIA 驱动初始化失败: %s, e) raise RuntimeError(fGPU 驱动异常: {e}) from e def discover_gpus(self) - List[GPUResource]: 发现所有可用 GPU。 Returns: 可用 GPU 列表 Raises: RuntimeError: 如果 GPU 发现失败 try: gpus [] device_count pynvml.nvmlDeviceGetCount() for i in range(device_count): handle pynvml.nvmlDeviceGetHandleByIndex(i) uuid pynvml.nvmlDeviceGetUUID(handle).decode() mem_info pynvml.nvmlDeviceGetMemoryInfo(handle) util pynvml.nvmlDeviceGetUtilizationRates(handle) gpu GPUResource( gpu_idi, uuiduuid, memory_total_mbmem_info.total // (1024 * 1024), memory_used_mbmem_info.used // (1024 * 1024), utilization_pctutil.gpu ) self.gpu_pool[uuid] gpu gpus.append(gpu) logger.info( GPU 发现完成: 总数%d, 总显存%dGB, len(gpus), sum(g.memory_total_mb for g in gpus) // 1024 ) return gpus except pynvml.NVMLError as e: logger.error(GPU 发现失败: %s, e) raise RuntimeError(fGPU 发现异常: {e}) from e def allocate_gpus(self, request: ModelDeploymentRequest) - List[str]: 为模型分配 GPU 资源。 采用 Best-Fit 策略寻找满足显存需求的最少 GPU 数 同时优先选择当前利用率最低的 GPU。 Args: request: 模型部署请求 Returns: 分配的 GPU UUID 列表 Raises: ResourceExhaustedError: 如果没有足够的 GPU 资源 try: # 筛选可用 GPUIDLE 或 当前分配模型的 GPU candidates: List[GPUResource] [ g for g in self.gpu_pool.values() if g.status in (GPUStatus.IDLE, GPUStatus.ALLOCATED) ] # 按利用率升序排列优先使用低负载 GPU candidates.sort(keylambda g: g.utilization_pct) allocated: List[str] [] remaining_vram request.required_vram_mb for gpu in candidates: if remaining_vram 0: break available gpu.memory_total_mb - gpu.memory_used_mb # 预留 500MB 显存作为系统缓冲 if available 500 and remaining_vram available - 500: allocated.append(gpu.uuid) remaining_vram - min(remaining_vram, available - 500) if len(allocated) request.max_gpus: break if remaining_vram 0: raise ResourceExhaustedError( fGPU 资源不足: 需要{request.required_vram_mb}MB, f可用{sum(g.memory_total_mb - g.memory_used_mb for g in candidates)}MB ) # 更新 GPU 状态 for uuid in allocated: self.gpu_pool[uuid].status GPUStatus.RESERVED self.gpu_pool[uuid].allocated_model request.model_name self.model_allocations[request.model_name] allocated logger.info( GPU 分配完成: model%s, gpus%s, vram_per_gpu%dMB, request.model_name, allocated, request.required_vram_mb // len(allocated) ) return allocated except ResourceExhaustedError: raise except Exception as e: logger.error(GPU 分配异常: model%s, request.model_name, e) raise RuntimeError(fGPU 分配失败: {e}) from e def release_gpus(self, model_name: str) - None: 释放模型占用的 GPU 资源。 Args: model_name: 要释放的模型名称 allocated self.model_allocations.pop(model_name, []) for uuid in allocated: gpu self.gpu_pool.get(uuid) if gpu: gpu.status GPUStatus.IDLE gpu.allocated_model None gpu.allocated_vram_mb 0 logger.info(GPU 资源释放: model%s, gpus%s, model_name, allocated) def get_utilization_report(self) - Dict: 生成 GPU 利用率报告用于 Grafana 监控面板。 Returns: 包含全部 GPU 利用率统计的字典 gpus list(self.gpu_pool.values()) if not gpus: return {avg_utilization: 0, gpu_count: 0} return { avg_utilization: sum(g.utilization_pct for g in gpus) / len(gpus), gpu_count: len(gpus), idle_count: sum(1 for g in gpus if g.status GPUStatus.IDLE), allocated_count: sum(1 for g in gpus if g.status ! GPUStatus.IDLE), gpus: [ { uuid: g.uuid, utilization: g.utilization_pct, memory_used: g.memory_used_mb, model: g.allocated_model } for g in gpus ] }3.2 模型加载与 Kubernetes 集成# Kubernetes GPU Pod 配置使用 NVIDIA GPU Operator apiVersion: v1 kind: Pod metadata: name: llm-inference-worker labels: app: llm-inference gpu-scheduler: dynamic spec: containers: - name: inference-engine image: vllm/vllm-openai:latest resources: limits: nvidia.com/gpu: 1 # 请求 1 张 GPU memory: 64Gi cpu: 8 requests: nvidia.com/gpu: 1 memory: 48Gi cpu: 4 env: - name: MODEL_NAME value: meta-llama/Llama-3.1-8B-Instruct - name: MAX_MODEL_LEN value: 8192 - name: GPU_MEMORY_UTILIZATION value: 0.85 # 使用 85% 显存 - name: MAX_NUM_SEQS value: 64 # 最大并发序列数 - name: ENABLE_PREFIX_CACHING value: true volumeMounts: - name: model-cache mountPath: /models volumes: - name: model-cache persistentVolumeClaim: claimName: model-storage-pvc nodeSelector: accelerator: nvidia-a100 # 指定 GPU 类型/** * GPU 调度策略配置——Java 侧控制面。 * * 根据请求量和模型热度动态调整每个模型的 GPU 副本数。 */ Service Slf4j public class DynamicGPUSchedulingService { Autowired private GPUResourceSchedulerClient schedulerClient; /** * 基于 QPS 的弹性扩缩策略。 * * 扩缩阈值可配置 * - QPS 50/副本 → 扩容最多 8 副本 * - QPS 10/副本 → 缩容最少 1 副本 * - 扩容冷却3分钟内不重复扩容 * - 缩容冷却10分钟内不重复缩容 * * param modelName 模型名称 * param currentQps 当前 QPS */ Scheduled(fixedDelay 60000) // 每分钟执行一次 public void autoScaleByQps(String modelName, double currentQps) { try { // 获取当前副本数 int currentReplicas schedulerClient.getReplicaCount(modelName); // 计算建议副本数 int suggestedReplicas (int) Math.ceil(currentQps / 50.0); // 限制在 1-8 之间 suggestedReplicas Math.max(1, Math.min(8, suggestedReplicas)); if (suggestedReplicas currentReplicas) { return; // 无需调整 } // 执行扩缩 schedulerClient.scaleModel(modelName, suggestedReplicas); log.info(模型弹性扩缩: model{}, replicas: {} - {}, qps{}, modelName, currentReplicas, suggestedReplicas, currentQps); } catch (Exception e) { log.error(弹性扩缩失败: model{}, modelName, e); } } }四、深度解析4.1 MIG vs 软件分时共享NVIDIA MIGMulti-Instance GPU是 A100/A100 支持的硬件级 GPU 分区技术可将一张物理 GPU 划分为最多 7 个独立实例每个实例有隔离的显存、缓存和计算单元。特性MIG硬件分区vLLM 软件共享隔离性硬件级故障隔离软件级隔离可能相互影响灵活性分区固定需重启动态分配实时调整碎片化可能有碎片如 20GB 切片未使用无碎片所有显存池化适用场景多租户严格隔离单租户多请求在我们的实践中MIG 适合需要强隔离的企业多租户场景如不同客户的专属模型而 vLLM 软件共享更适合内部多模型共享同一集群的场景。大多数情况下软件共享的灵活性和资源利用率更高。4.2 GPU 利用率 vs 用户体验的平衡GPU 利用率并非越高越好。当 GPU 利用率超过 90% 时请求排队概率急剧增加。我们设定 GPU 利用率告警阈值为 85%当达到此阈值时自动触发扩容流程。同时引入请求优先级机制高优先级请求如在线客服对话可抢占低优先级请求如批量文档处理的 GPU 时间片。通过max_num_seqs和请求优先级队列在保证 SLA 的前提下最大化 GPU 利用率。4.3 冷模型加载的预热策略动态调度的最大挑战之一是冷模型加载延迟。当一个长期未使用的模型被调度到 GPU 时从对象存储拉取模型权重到 GPU 显存可能需要 30 秒到 2 分钟。我们的优化策略Keep-Warm 池为热门模型保留至少 1 个常驻副本永不回收。分层缓存SSD 本地缓存 → NVMe → 对象存储。80% 的模型加载命中 SSD 缓存加载时间降至 5-10 秒。懒迁移扩容时新副本不立即接管流量而是先完成模型加载和预热请求Sentinel Request确认可用后再加入负载均衡池。五、总结GPU 资源调度从静态分配到动态共享的演进本质上是一次资源利用率与调度复杂度的权衡。三条关键经验从粗到细逐步演进静态分配 → 模型级动态调度 → 请求级共享。不要一步跳到最复杂的方案每个阶段的复杂度收益需要明确量化。GPU 利用率 85% 是红线超过此值服务质量的下降会抵消资源节省带来的收益。设置自动扩容告警是这个红线的保障。冷加载是动态调度的 Achilles 之踵Keep-Warm 池 分层缓存的组合是性价比最高的解决方案不要指望纯动态能零延迟唤起模型。在 AI 推理成本GPU 租用费用持续高企的背景下GPU 资源调度已经从优化项变成了必选项。如果你的集群还停留在一卡一模型的静态分配是时候开始规划动态化的演进路径了。作者程序员鸭梨李然Java 架构师专注 AI 推理基础设施与 GPU 资源调度实践。欢迎留言交流你的 GPU 调度经验。