GPU 资源调度:AI 集群算力管理的核心引擎
GPU 资源调度AI 集群算力管理的核心引擎一、GPU 算力碎片化与利用率困境AI 基础设施的核心痛点在 AI 集群中GPU 是最昂贵也最稀缺的资源。一张 A100-80G 的月租成本约 1.5 万元一个 8 卡节点的年成本超过 140 万元。然而生产环境中 GPU 的平均利用率往往只有 30%-50%。算力碎片化的根源在于不同任务对 GPU 资源的需求差异极大——训练任务需要多卡并行且长时间占用推理任务需要低延迟响应但单次占用时间短数据处理任务只需要少量 GPU 内存。传统的静态分配方式一台机器绑定一个任务导致大量 GPU 资源在任务空闲时被浪费。GPU 资源调度的核心目标是在满足各类任务 SLA 的前提下最大化 GPU 利用率降低单位算力成本。二、GPU 资源调度机制从时间片共享到多实例切分GPU 资源调度需要在空间共享多任务分时复用同一 GPU和时间共享任务排队等待 GPU 空闲两个维度上协同设计。下图展示了 GPU 资源调度的分层架构flowchart TB subgraph 任务提交层 Train[训练任务 高优/长时] Infer[推理服务 在线/低延迟] Batch[批处理任务 低优/弹性] end subgraph 调度引擎层 Queue[优先级队列] Scheduler[调度器 Bin-Packing/DRF] Planner[容量规划 GPU拓扑感知] end subgraph GPU 切分层 MIG[MIG 实例切分 A100] MPS[MPS 多进程共享] TimeSlice[时间片轮转] VGPU[vGPU 虚拟化] end subgraph 物理GPU层 GPU1[GPU 0] GPU2[GPU 1] GPU3[GPU 2] GPU4[GPU 3] end Train -- Queue Infer -- Queue Batch -- Queue Queue -- Scheduler Scheduler -- Planner Planner -- MIG Planner -- MPS Planner -- TimeSlice Planner -- VGPU MIG -- GPU1 MPS -- GPU2 TimeSlice -- GPU3 VGPU -- GPU4 style Scheduler fill:#ff9,stroke:#333 style MIG fill:#9ff,stroke:#333 style MPS fill:#9f9,stroke:#3332.1 MIGMulti-Instance GPUA100 的硬件级切分MIG 是 NVIDIA 从 A100 开始引入的硬件级 GPU 切分技术。一张 A100 可以被切分为最多 7 个独立实例每个实例拥有独立的 SM、L2 Cache 和显存带宽实例之间硬件级隔离互不影响。MIG 适合推理服务场景——每个推理实例分配一个 MIG 切片既保证隔离性又避免整卡浪费。2.2 MPSMulti-Process Service软件级空间共享MPS 允许多个 CUDA 进程共享同一 GPU 的计算单元通过协作式调度减少上下文切换开销。MPS 适合计算密集但显存占用不高的任务但缺乏隔离性——一个进程的异常可能影响其他进程。2.3 时间片轮转K8s 设备插件的默认策略K8s 的 NVIDIA Device Plugin 默认以整卡为调度单位。通过time-slicing配置可以将一张 GPU 虚拟为多个设备以时间片方式分配给不同 Pod。这种方式实现简单但存在上下文切换开销且无法保证隔离性。三、生产级 GPU 资源调度实现3.1 基于 K8s 的 GPU 调度器扩展# GPU 时间片配置——NVIDIA Device Plugin # 为什么用时间片而非 MIG # 因为 MIG 只支持 A100/H100 等少数架构 # 时间片方案兼容所有 GPU 型号适合异构集群 apiVersion: v1 kind: ConfigMap metadata: name: nvidia-device-plugin-config namespace: gpu-operator data: config.yaml: | version: v1 flags: migStrategy: none failOnInitError: true sharing: timeSlicing: renameByDefault: false resources: - name: nvidia.com/gpu replicas: 4 # 每张 GPU 虚拟为 4 个设备/** * GPU 感知调度器——扩展 K8s 调度框架 * 为什么需要自定义调度器而非用默认调度 * 因为默认调度器只看 GPU 数量不看 GPU 利用率和拓扑关系 * 可能将通信密集的训练任务调度到跨 NUMA 节点的 GPU 上 * 导致 NVLink 通信退化为 PCIe 通信性能下降 50% 以上 */ package scheduler import ( context fmt k8s.io/klog/v2 framework k8s.io/kubernetes/pkg/scheduler/framework ) type GPUScheduler struct { handle framework.Handle } func (g *GPUScheduler) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) (int64, *framework.Status) { gpuAllocatable : nodeInfo.Allocatable.ScalarResources[nvidia.com/gpu] gpuRequested : nodeInfo.Requested.ScalarResources[nvidia.com/gpu] if gpuAllocatable 0 { return 0, nil } // 计算当前节点 GPU 利用率 // 为什么用利用率而非剩余数量做打分 // 因为 Bin-Packing 策略优先填满已有节点的 GPU // 避免将任务分散到多个节点导致碎片化 utilization : float64(gpuRequested) / float64(gpuAllocatable) // 利用率越高的节点得分越高Bin-Packing 策略 score : int64(utilization * 100) // 拓扑感知加分如果 Pod 需要多卡且节点有 NVLink if requiresMultiGPU(pod) hasNVLink(nodeInfo.Node()) { score 20 klog.V(4).Infof(节点 %s 有 NVLink拓扑加分, nodeInfo.Node().Name) } // 推理服务优先调度到 MIG 实例 if isInferenceService(pod) hasMIGInstances(nodeInfo.Node()) { score 15 } return min(score, 100), nil } func (g *GPUScheduler) ScoreExtensions() framework.ScoreExtensions { return nil } // 判断 Pod 是否需要多卡并行 func requiresMultiGPU(pod *v1.Pod) bool { gpuCount : int64(0) for _, container : range pod.Spec.Containers { if limit, ok : container.Resources.Limits[nvidia.com/gpu]; ok { gpuCount limit.Value() } } return gpuCount 1 }3.2 GPU 利用率监控与弹性调度 GPU 利用率监控与弹性调度控制器 为什么需要弹性调度 因为推理服务的流量存在明显波峰波谷 固定分配 GPU 在低峰期浪费算力弹性调度可自动缩容释放资源 import subprocess import json from dataclasses import dataclass from typing import List dataclass class GPUMetrics: gpu_id: int utilization: float # 计算利用率 0-100 memory_used_mb: int memory_total_mb: int temperature: int running_processes: int class GPUMonitor: def __init__(self): self.metrics_history: List[List[GPUMetrics]] [] def collect(self) - List[GPUMetrics]: 采集 GPU 指标 try: result subprocess.run( [nvidia-smi, --query-gpuindex,utilization.gpu,memory.used, memory.total,temperature.gpu, --query-compute-appsgpu_uuid,pid, --formatcsv,noheader,nounits], capture_outputTrue, textTrue, timeout10 ) return self._parse_metrics(result.stdout) except Exception as e: # 采集失败时返回空列表调度器将使用历史数据 return [] def get_low_utilization_gpus(self, threshold: float 30.0, window_minutes: int 15 ) - List[int]: 识别持续低利用率的 GPU if len(self.metrics_history) window_minutes: return [] # 取最近 N 分钟的平均利用率 # 为什么用滑动窗口平均而非瞬时值 # 因为 GPU 利用率天然波动大瞬时值可能导致误判 # 滑动窗口平均能过滤短期波动识别真正的空闲 GPU recent self.metrics_history[-window_minutes:] avg_util {} for snapshot in recent: for m in snapshot: avg_util.setdefault(m.gpu_id, []) avg_util[m.gpu_id].append(m.utilization) low_util_gpus [] for gpu_id, utils in avg_util.items(): if sum(utils) / len(utils) threshold: low_util_gpus.append(gpu_id) return low_util_gpus class ElasticScheduler: 弹性调度控制器根据 GPU 利用率自动扩缩容 def __init__(self, monitor: GPUMonitor, min_replicas: int 1, max_replicas: int 8): self.monitor monitor self.min_replicas min_replicas self.max_replicas max_replicas self.current_replicas min_replicas def reconcile(self): 调度循环每分钟执行一次 self.monitor.metrics_history.append(self.monitor.collect()) low_gpus self.monitor.get_low_utilization_gpus( threshold30.0, window_minutes15) # 如果超过 50% 的 GPU 持续低利用率缩容 total_gpus self.current_replicas if len(low_gpus) total_gpus * 0.5: new_replicas max(self.min_replicas, self.current_replicas - 1) if new_replicas self.current_replicas: self._scale_down(new_replicas) return # 如果所有 GPU 利用率超过 80%扩容 all_high all( m.utilization 80 for snapshot in self.monitor.metrics_history[-5:] for m in snapshot ) if all_high: new_replicas min(self.max_replicas, self.current_replicas 1) if new_replicas self.current_replicas: self._scale_up(new_replicas) def _scale_down(self, target: int): 缩容优先释放低利用率 GPU 上的推理实例 # 为什么缩容而非直接回收 GPU # 因为推理服务需要优雅停机直接回收会导致进行中的请求失败 self.current_replicas target def _scale_up(self, target: int): 扩容从资源池中分配新 GPU self.current_replicas target3.3 训练与推理混部分时复用 GPU# K8s Pod 配置——训练与推理混部 # 为什么训练和推理可以混部 # 因为训练任务通常在凌晨跑推理服务在白天高峰 # 两者在时间上天然互补混部可将 GPU 利用率从 40% 提升至 80% apiVersion: apps/v1 kind: Deployment metadata: name: inference-service labels: app: inference priority: high spec: replicas: 4 template: spec: containers: - name: inference image: inference-server:v2 resources: limits: nvidia.com/gpu: 1 memory: 16Gi requests: nvidia.com/gpu: 1 memory: 8Gi # 低优先级训练任务到来时可被抢占 # 为什么推理服务设置低优先级 # 不是因为推理不重要而是因为推理可以快速迁移到其他节点 # 而训练任务的检查点保存耗时较长抢占代价更高 priorityClassName: preemptible-inference --- # 训练任务——使用 K8s Job apiVersion: batch/v1 kind: Job metadata: name: model-training spec: template: spec: containers: - name: trainer image: training-worker:v2 resources: limits: nvidia.com/gpu: 4 # 多卡训练 memory: 64Gi priorityClassName: high-priority-training restartPolicy: OnFailure四、架构权衡GPU 调度方案的代价与边界MIG 切分的代价MIG 切分后每个实例的计算能力和显存带宽被固定无法动态调整。A100 切分为 7 个实例后每个实例只有约 1/7 的算力对于需要高吞吐的推理场景可能不够。MIG 切分和重组需要重启 GPU 上的所有任务无法在线调整。时间片的代价时间片轮转存在上下文切换开销每次切换需要保存和恢复 GPU 状态约增加 5%-15% 的性能损耗。多个任务共享 GPU 时一个计算密集型任务可能抢占大部分时间片导致其他任务延迟不稳定。弹性调度的代价弹性缩容需要优雅迁移推理实例迁移过程中该实例无法服务可能导致短暂的服务降级。扩容需要预热模型加载权重到 GPU对于 70B 模型预热时间可能超过 2 分钟无法应对突发流量。混部的代价训练任务抢占推理实例时推理请求会短暂失败。需要配合负载均衡和重试机制确保请求被路由到其他可用实例。混部增加了集群调度的复杂度调度延迟可能增加 30%-50%。适用边界GPU 调度优化适用于拥有 10 张以上 GPU 的 AI 集群。对于小规模集群3-5 张 GPU静态分配 手动调度更简单可靠。调度优化的收益与集群规模正相关规模越大碎片化越严重优化收益越显著。五、总结GPU 资源调度的核心目标是最大化算力利用率、降低单位成本。三个关键策略MIG 硬件切分实现推理服务的精细隔离、弹性调度根据利用率自动扩缩容、训练与推理混部分时复用 GPU。落地路线上建议先建立 GPU 利用率监控体系识别碎片化严重的节点再根据 GPU 型号选择切分方案A100/H100 用 MIG其他用时间片最后引入弹性调度和混部策略进一步提升利用率。GPU 调度不是一次性配置而是需要持续监控和调优的动态过程。