GPU 分时复用与 MIG 切分:云原生 AI 平台资源利用率提升的工程实践
一、GPU 空闲率居高不下:云原生 AI 平台的资源困局
在云原生 AI 平台中,GPU 资源的成本往往占整体基础设施投入的 60% 以上。然而,生产环境的监控数据却反复揭示一个尴尬的事实:集群平均 GPU 利用率长期徘徊在 30%--40% 之间。造成这一现象的核心原因并非业务负载不足,而是调度粒度过粗------Kubernetes 原生仅支持以整卡为单位分配 GPU,导致大量推理服务在低负载时段独占整张 A100/H100,剩余算力被白白浪费。
更具体的场景是:一个 BERT 推理服务峰值 QPS 仅需 20% 算力,却占用了整张 GPU;而另一个视觉模型服务因无法获得空闲 GPU 而排队等待。这种"占而不满"与"等而无卡"并存的矛盾,正是 GPU 分时复用与 MIG(Multi-Instance GPU)技术试图解决的核心痛点。
二、从硬件切分到时间片轮转:GPU 资源共享的底层机制
GPU 资源共享存在两条技术路线:空间切分(MIG) 与 时间分片(Time-Slicing)。两者的底层机制截然不同,适用场景也各有边界。
2.1 MIG 的硬件级隔离
MIG 是 NVIDIA 从 Ampere 架构(A100/A30)开始引入的硬件特性。它将一张 GPU 的 SM(Streaming Multiprocessor)和内存带宽在物理层面切分为多个独立实例,每个实例拥有专属的计算单元和显存通道,彼此之间实现真正的硬件隔离------一个实例的内存错误不会波及另一个实例。
以 A100 40GB 为例,MIG 支持的切分配置如下:
| 配置 Profile | SM 数量 | 显存 | 最大实例数 |
|---|---|---|---|
| 1g.5gb | 14 | 5 GB | 7 |
| 2g.10gb | 28 | 10 GB | 3 |
| 3g.20gb | 42 | 20 GB | 2 |
| 4g.20gb | 56 | 20 GB | 1 |
| 7g.40gb | 98 | 40 GB | 1 |
2.2 Time-Slicing 的驱动级轮转
Time-Slicing 则完全在驱动层实现,不依赖特定硬件架构。其原理是让多个 CUDA Context 共享同一 GPU,由驱动按时间片轮流调度执行。这种方式兼容性更广(支持所有 NVIDIA GPU),但缺乏硬件隔离------一个进程的异常可能导致其他进程的延迟抖动。
三、Kubernetes 集成 MIG 与 Time-Slicing 的生产级实现
3.1 MIG 设备插件配置
以下是在 Kubernetes 中配置 NVIDIA MIG 设备插件的完整方案:
yaml
# nvidia-mig-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nvidia-mig-config
namespace: gpu-operator
data:
config.yaml: |
version: v1
flags:
migStrategy: "mixed"
sharing:
timeSlicing:
renameByDefault: false
resources:
- name: nvidia.com/A100
rename: nvidia.com/A100-2g.10gb
devices: { }
mig:
1g.5gb: 7 # 将 A100 切分为 7 个 1g.5gb 实例
yaml
# Pod 使用 MIG 实例
apiVersion: v1
kind: Pod
metadata:
name: bert-inference
spec:
containers:
- name: inference
image: bert-server:latest
resources:
limits:
nvidia.com/mig-1g.5gb: 1 # 申请 1 个 MIG 1g.5gb 实例
env:
- name: CUDA_VISIBLE_DEVICES
valueFrom:
fieldRef:
fieldPath: ""
nodeSelector:
nvidia.com/gpu.product: "A100-SXM4-40GB-MIG"
3.2 Time-Slicing 设备插件配置
yaml
# nvidia-timeslicing-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nvidia-timeslicing-config
namespace: gpu-operator
data:
config.yaml: |
version: v1
sharing:
timeSlicing:
renameByDefault: true
resources:
- name: nvidia.com/A100
rename: nvidia.com/A100-SHARE
replicas: 4 # 将 1 张 A100 暴露为 4 个虚拟 GPU
3.3 动态 MIG 切换的 Operator 实现
生产环境中,不同时段的负载特征差异显著------白天推理流量高峰需要更多小粒度 MIG 实例,夜间训练任务则需要大粒度甚至整卡。以下 Operator 实现了基于时间窗口的 MIG 配置动态切换:
go
package controller
import (
"context"
"fmt"
"time"
nvidiav1 "github.com/NVIDIA/gpu-operator/api/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
)
// MIGScheduler 根据 Pod 调度需求动态调整 MIG 配置
type MIGScheduler struct {
client kubernetes.Interface
nodeCache map[string]string // node -> current MIG profile
}
// MIGProfile 定义节点级别的 MIG 切分策略
type MIGProfile struct {
Name string
Instances int
SMCount int
MemoryGB int
}
// 预定义的切分策略
var profiles = map[string]MIGProfile{
"inference-peak": {Name: "1g.5gb", Instances: 7, SMCount: 14, MemoryGB: 5},
"balanced": {Name: "2g.10gb", Instances: 3, SMCount: 28, MemoryGB: 10},
"training": {Name: "7g.40gb", Instances: 1, SMCount: 98, MemoryGB: 40},
}
// Reconcile 根据当前集群负载自动选择最优 MIG 配置
func (s *MIGScheduler) Reconcile(ctx context.Context, nodeName string) error {
// 统计该节点上推理 Pod 与训练 Pod 的数量
inferencePods, trainPods, err := s.countPodsByType(ctx, nodeName)
if err != nil {
return fmt.Errorf("统计 Pod 类型失败: %w", err)
}
// 根据负载比例选择策略,避免频繁切换引入抖动
var targetProfile string
if trainPods > 0 && inferencePods == 0 {
targetProfile = "training"
} else if inferencePods >= 4 {
targetProfile = "inference-peak"
} else {
targetProfile = "balanced"
}
currentProfile, exists := s.nodeCache[nodeName]
if exists && currentProfile == targetProfile {
klog.V(4).Infof("节点 %s MIG 配置无需变更: %s", nodeName, targetProfile)
return nil
}
// 执行 MIG 配置切换前,需确保节点上无运行中的 GPU Pod
if err := s.ensureNodeDrained(ctx, nodeName); err != nil {
return fmt.Errorf("节点排空失败,无法切换 MIG 配置: %w", err)
}
// 调用 NVIDIA 管理工具执行 MIG 切分
profile := profiles[targetProfile]
if err := s.applyMIGConfig(nodeName, profile); err != nil {
return fmt.Errorf("MIG 配置应用失败: %w", err)
}
s.nodeCache[nodeName] = targetProfile
klog.Infof("节点 %s MIG 配置已切换为 %s", nodeName, targetProfile)
return nil
}
// countPodsByType 统计节点上不同类型的 GPU Pod
func (s *MIGScheduler) countPodsByType(ctx context.Context, nodeName string) (int, int, error) {
pods, err := s.client.CoreV1().Pods("").List(ctx, metav1.ListOptions{
FieldSelector: fmt.Sprintf("spec.nodeName=%s,status.phase=Running", nodeName),
})
if err != nil {
return 0, 0, err
}
var inference, training int
for _, pod := range pods.Items {
for _, container := range pod.Spec.Containers {
if _, ok := container.Resources.Limits["nvidia.com/mig-1g.5gb"]; ok {
inference++
}
if _, ok := container.Resources.Limits["nvidia.com/A100"]; ok {
// 整卡分配通常用于训练
if val, exists := pod.Labels["workload-type"]; exists && val == "training" {
training++
}
}
}
}
return inference, training, nil
}
// ensureNodeDrained 确保节点上无运行中的 GPU 工作负载
func (s *MIGScheduler) ensureNodeDrained(ctx context.Context, nodeName string) error {
return wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, true,
func(ctx context.Context) (bool, error) {
pods, err := s.client.CoreV1().Pods("").List(ctx, metav1.ListOptions{
FieldSelector: fmt.Sprintf("spec.nodeName=%s,status.phase=Running", nodeName),
})
if err != nil {
return false, err
}
for _, pod := range pods.Items {
if hasGPUResource(&pod) {
return false, nil
}
}
return true, nil
},
)
}
// applyMIGConfig 调用 nvidia-smi 执行 MIG 配置
func (s *MIGScheduler) applyMIGConfig(nodeName string, profile MIGProfile) error {
// 通过 DaemonSet 执行节点上的 nvidia-smi 命令
// 生产环境中应通过 GPU Operator 的 ClusterPolicy CRD 触发
klog.Infof("在节点 %s 上应用 MIG 配置: %s (%d 实例)", nodeName, profile.Name, profile.Instances)
return nil
}
func hasGPUResource(pod *corev1.Pod) bool {
for _, container := range pod.Spec.Containers {
for name := range container.Resources.Limits {
if name == "nvidia.com/A100" || name == "nvidia.com/mig-1g.5gb" ||
name == "nvidia.com/mig-2g.10gb" {
return true
}
}
}
return false
}
四、MIG 与 Time-Slicing 的架构权衡
| 维度 | MIG | Time-Slicing |
|---|---|---|
| 隔离性 | 硬件级,SM 与内存带宽物理隔离 | 驱动级,进程间无隔离 |
| 性能稳定性 | 实例间互不干扰,延迟可预测 | 高负载时延迟抖动显著 |
| 灵活性 | 实例粒度受 Profile 约束,切换需重启 | 可任意倍数复制,无需重启 |
| 硬件要求 | 仅 Ampere+(A100/A30/H100) | 所有 NVIDIA GPU |
| 故障影响 | 单实例故障不影响其他实例 | 单进程异常可能拖慢整卡 |
| 适用场景 | 推理+训练混部、SLA 敏感型推理 | 开发测试、低优先级批处理 |
MIG 的关键局限:切分后无法在运行时动态调整,必须先清空节点上的 GPU 工作负载才能重新配置。这意味着在推理流量突增时,无法即时将 2g.10gb 切换为 1g.5gb 来承载更多实例。
Time-Slicing 的致命缺陷:当多个推理服务共享同一 GPU 时,一个服务的突发请求会导致其他服务的 P99 延迟从 50ms 飙升至 200ms 以上。对于 SLA 严格的在线推理场景,这种抖动是不可接受的。
五、总结
GPU 分时复用与 MIG 切分是云原生 AI 平台提升资源利用率的两种互补手段。MIG 提供硬件级隔离,适合推理与训练混部以及对延迟敏感的生产服务;Time-Slicing 提供灵活的共享能力,适合开发测试和低优先级批处理场景。
落地路线建议:第一步,在集群中识别 GPU 利用率低于 40% 的节点,优先部署 Time-Slicing 方案快速回收闲置算力;第二步,对 SLA 敏感的推理服务逐步迁移至 MIG 实例,确保延迟可预测;第三步,引入基于负载感知的动态 MIG 调度器,实现推理高峰与训练低谷之间的资源弹性切换。关键原则是------先回收浪费,再追求隔离,最后实现自动化。