GPU 分时复用与 MIG 切分:云原生 AI 平台资源利用率提升的工程实践

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)。两者的底层机制截然不同,适用场景也各有边界。

flowchart TB A[GPU 资源共享] --> B[空间切分: MIG] A --> C[时间分片: Time-Slicing] B --> B1[硬件级隔离] B --> B2[独立 SM/内存带宽] B --> B3[故障隔离: 互不影响] B --> B4[实例数上限: A100 最多 7 个] C --> C1[驱动级调度] C --> C2[时间片轮转执行] C --> C3[无故障隔离] C --> C4[实例数无硬性上限] B1 --> D[适用: 推理服务 + 训练任务混部] C1 --> E[适用: 纯推理场景, 低优先级任务]

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 调度器,实现推理高峰与训练低谷之间的资源弹性切换。关键原则是------先回收浪费,再追求隔离,最后实现自动化。

相关推荐
学Linux的语莫1 小时前
大模型量化知识总结
人工智能·模型·量化
怪我冷i1 小时前
人工智能的数学基础——学习笔记
人工智能·笔记·学习
顾北顾1 小时前
AI 中的 MCP、Skills、Rules 到底是什么?
人工智能
羊羊小栈1 小时前
基于混合检索RAG的食品生产质量问答系统(BGE_BM25_大语言模型)
前端·人工智能·语言模型·自然语言处理·毕业设计·大作业
财迅通Ai1 小时前
视觉中国向港交所递交H股上市申请
人工智能·视觉中国
syc78901231 小时前
同架构编码工具实测 口述开发场景体验对比
人工智能·架构
Master_oid1 小时前
机器学习46:逻辑回归--基础篇
人工智能·机器学习·逻辑回归
kaixuan_dashen1 小时前
崩铁 ChatBox 1.0.0版本发布!基于KMP的开源AI 对话客户端
人工智能
aneasystone本尊1 小时前
带小龙虾逛 ClawHub:自定义 Skill 实战
人工智能