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:#333
2.1 MIG(Multi-Instance GPU):A100 的硬件级切分
MIG 是 NVIDIA 从 A100 开始引入的硬件级 GPU 切分技术。一张 A100 可以被切分为最多 7 个独立实例,每个实例拥有独立的 SM、L2 Cache 和显存带宽,实例之间硬件级隔离,互不影响。MIG 适合推理服务场景——每个推理实例分配一个 MIG 切片,既保证隔离性,又避免整卡浪费。
2.2 MPS(Multi-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-gpu=index,utilization.gpu,memory.used,"
"memory.total,temperature.gpu",
"--query-compute-apps=gpu_uuid,pid",
"--format=csv,noheader,nounits"],
capture_output=True, text=True, timeout=10
)
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(
threshold=30.0, window_minutes=15)
# 如果超过 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 = target
3.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 调度不是一次性配置,而是需要持续监控和调优的动态过程。
更多推荐
所有评论(0)