GPU 调度器多租户 AI 工作负载:Kubernetes 下的架构设计与工程实践

当训练任务抢占推理服务的最后一兆显存,当 MIG 切分后的算力碎片无人问津——多租户 GPU 集群的资源调度,正在成为 AI 基础设施中最棘手的系统工程问题之一。

一、问题的本质:GPU 资源的双重碎片化

在 CPU 主导的传统世界里,cgroup 可以精确到 0.01 核心,超卖策略弹性十足。GPU 则呈现出截然不同的物理约束:

  • 显存不可压缩:80GB HBM2e 一旦被分配,无法像内存一样 swap 到磁盘(虽然 CUDA 22.x 引入了 Unified Memory oversubscription,但性能代价极其沉重)
  • 算力切换代价高:MPS 多进程共享 CUDA 上下文,上下文切换的微秒级开销在训练场景会被放大;MIG 分区后,各实例的 SM 和显存控制器属于硬隔离,无法动态调整
  • PCIe/NVLink 拓扑敏感:跨 NUMA 节点的 GPU 间通信带宽可能下降 60%,忽略拓扑的调度等同于性能自杀

这种物理特性导致了两类碎片化:

1.1 显存碎片化

一个典型的集群状态:8×H100 80GB 节点上,各 GPU 剩余显存分别为 [22, 45, 8, 61, 33, 70, 15, 42] GB。此时一个需要 50GB 显存的推理副本无法被调度到任何单卡上——尽管整体剩余 296GB。

1.2 算力碎片化

训练任务以 24 小时为周期运行,推理服务需要 7×24 运行。当训练任务结束时释放的 GPU 无法被推理服务立即回填(因为 Pod 调度、容器启动、模型加载需要数十秒),算力利用率出现波谷。

理解这两种碎片化是设计调度器的前提。

二、NVIDIA 共享策略的三层地基

在进入 Kubernetes 调度层之前,必须先理解 GPU 本身的共享机制——调度器的决策受限于这些底层能力。

2.1 MIG(Multi-Instance GPU)

Ampere 及更新架构支持将单张 GPU 切分为最多 7 个独立实例,每个实例拥有独立的 SM 阵列、显存、显存控制器和 L2 Cache。

# 将 H100 切分为 1 个 4g.40gb + 2 个 2g.20gb
nvidia-smi mig -i 0 -cgi 19,5,5 -C

# 查看 MIG 配置
nvidia-smi mig -lgi
# +---------------------------------------+
# | GPU instances:                        |
# | GPU   Name          Profile  Instance |
# |                       ID     Id       |
# |   0  MIG 4g.40gb       19       0     |
# |   0  MIG 2g.20gb        5       0     |
# |   0  MIG 2g.20gb        5       1     |
# +---------------------------------------+

工程陷阱:MIG 配置切换需要约 5-10 秒,且该 GPU 上所有进程必须退出。这意味着 MIG 切换是一种"离线"操作——无法在不中断业务的情况下动态调整分区。

2.2 MPS(Multi-Process Service)

MPS 允许多个进程(来自不同容器或同一容器的不同线程)共享同一个 CUDA Context,通过时间分片轮转执行各自的 kernel。

# 启动 MPS 控制 daemon
nvidia-cuda-mps-control -d

# 设置 MPS 计算资源配额(某租户最多使用 40% SM)
echo "set_default_active_thread_percentage 40" | nvidia-cuda-mps-control

MPS 的实现依赖于 GPU 的硬件时间片调度器。关键问题在于:当一个进程提交了一个超长 kernel(比如大矩阵乘法的 GEMM),MPS 无法在其执行中抢占——只能等待 kernel 完成后才交出 GPU。

2.3 时间分片(Time-Slicing)

这是最"朴素"的策略:每个 Context 轮流获得一个时间窗口(通常 1-100ms),由 GPU 调度器自动轮转。

在 Kubernetes Device Plugin 中配置:

# config.yaml for nvidia-device-plugin
version: v1
sharing:
  timeSlicing:
    renameByDefault: false
    resources:
      - name: nvidia.com/gpu
        replicas: 4  # 将 1 张物理 GPU 暴露为 4 个可调度单元

性能实测:在 4-way 时间分片下,FP16 GEMM 的端到端吞吐约为独占模式的 72-85%(取决于 kernel 长度和显存带宽竞争)。对于低延迟推理(P99 < 100ms),这个开销通常是不可接受的。

三、Kubernetes GPU 调度器架构

3.1 标准调度链

K8s 默认调度器对 GPU 的感知极其朴素——它只认 nvidia.com/gpu 这个 extended resource,不区分 GPU 型号、显存大小、MIG 配置。

完整的多租户调度需要以下组件协同:

Pod → kube-scheduler → GPU Operator → device-plugin → GPU 分配
         ↑                    ↑
    scheduler-extender   mig-manager
    (自定义优选策略)    (MIG 分区管理)
         ↑
    GPU 监控 (DCGM → Prometheus → 自定义评分器)

3.2 Scheduler Extender 模式

自定义调度逻辑最直接的入口是 K8s 的 scheduler extender 机制:

// extender.go - 极简化的 GPU 优选逻辑
func prioritize(args schedulerapi.ExtenderArgs) schedulerapi.HostPriorityList {
    var results schedulerapi.HostPriorityList
    pods := args.Pods.Items

    for _, node := range args.Nodes.Items {
        score := 0

        // 1. 显存连续性评分:碎片越少得分越高
        fragScore := evaluateFragmentation(node)
        score += fragScore * 40  // 权重 40%

        // 2. NVLink 拓扑评分:推理 Pod 优先分到 NVLink 域内
        if needsTensorParallelism(pods) {
            nvlinkScore := evaluateNvidiaLinkDomain(node)
            score += nvlinkScore * 35
        }

        // 3. 能效评分:H100 推理能效约为 H800 的 1.3 倍
        efficiencyScore := evaluatePowerEfficiency(node)
        score += efficiencyScore * 25

        results = append(results, schedulerapi.HostPriority{
            Host:  node.Name,
            Score: score,
        })
    }
    return results
}

3.3 Dynamic Resource Allocation (DRA)

Kubernetes 1.26+ 引入了 DRA 机制,将 GPU 从简单的整数资源变为结构化资源:

# GPUClaimTemplate.yaml
apiVersion: resource.k8s.io/v1alpha3
kind: ResourceClaimTemplate
metadata:
  name: gpu-inference-h100-80g
spec:
  spec:
    parametersRef:
      kind: GPUClaimParameters
      group: gpu.resource.admin
---
apiVersion: gpu.resource.admin/v1alpha1
kind: GPUClaimParameters
memoryGB: 80
gpuType: H100
nvLinkPeers: 8        # 要求 NVLink 全互联
migProfile: "3g.40gb" # 可选 MIG 配置
shPolicy: exclusive   # exclusive / mps / timeSlice

DRA 的优势在于将 GPU 的物理属性编码到 ResourceClaim 中,调度器不再只看 "1 张 GPU",而是看到 "80GB 显存 + NVLink 全互联 + 独占"。

四、Bin Packing 与 Defragmentation 算法

4.1 经典算法在生产中的局限

First-Fit Decreasing (FFD) 是调度器中最常见的 GPU 分配策略——按显存需求降序排列 Pod,依次放入第一个满足条件的节点。其实现简单,但在多约束场景下表现糟糕:

def ffd_schedule(pods: List[GPUNode], nodes: List[GPUNode]) -> Dict[str, str]:
    """First-Fit Decreasing - 基线算法"""
    allocation = {}
    sorted_pods = sorted(pods, key=lambda p: p.gpu_memory_req, reverse=True)

    for pod in sorted_pods:
        for node in nodes:
            if (node.available_gpu_memory >= pod.gpu_memory_req and
                node.available_gpus >= pod.gpu_count):
                # 分配
                node.available_gpu_memory -= pod.gpu_memory_req
                node.available_gpus -= pod.gpu_count
                allocation[pod.name] = node.name
                break
        else:
            # 无法调度,进入 pending
            allocation[pod.name] = None

    return allocation

问题场景:训练 Pod 请求 4 GPU × 80GB,推理 Pod 请求 1 GPU × 20GB。FFD 先把训练 Pod 塞进节点,留下 4×60GB 碎片。后续多个推理 Pod 无法使用这些碎片(单节点剩余 4×60GB 但无 NVLink),导致整体浪费。

4.2 拓扑感知的 Best-Fit 策略

实践中更有价值的思路是拓扑感知 Best-Fit:

def topo_aware_best_fit(pod: GPUPod, nodes: List[GPUNode]) -> Optional[str]:
    """考虑 NVLink 拓扑的最优适配"""
    candidates = []

    for node in nodes:
        # 1. 基本资源满足
        if not basic_fit(pod, node):
            continue

        # 2. 评分:剩余资源越少越好(减少碎片)
        #    但 NVLink 域内的 GPU 必须同时分配
        topology = node.nvlink_topology
        gpus_in_domain = topology.get_interconnected_gpus(pod.gpu_count)

        if gpus_in_domain is None:
            # 无法满足 NVLink 全互联,降低优先级
            fragmentation_penalty = 0.3
        else:
            fragmentation_penalty = 1.0

        # 3. 综合评分
        remaining_after = node.remaining_memory - pod.gpu_memory_req
        # Best-fit: 分配后剩余最少的节点(紧凑 packing)
        # Worst-fit: 分配后剩余最多的节点(分散负载)
        fit_score = 1.0 / (remaining_after + 1)

        topology_score = get_bandwidth_score(gpus_in_domain, pod)

        score = (fit_score * 0.4 + topology_score * 0.4 + 
                 fragmentation_penalty * 0.2)

        candidates.append((node.name, score))

    if not candidates:
        return None

    return max(candidates, key=lambda x: x[1])[0]

4.3 运行时 Defragmentation

调度完成后并非万事大吉——随着 Pod 创建和销毁,集群会逐渐碎片化。运行时重整策略:

  • 优雅迁移:对 StatefulSet 推理副本执行 draining → migrate → reschedule,利用 Checkpoint/Restore In Userspace (CRIU) 或模型级别的保存/恢复实现热迁
  • 抢占式回收:低优先级训练任务在高优先级推理到达时主动 checkpoint 并退出,留出 GPU 空间
  • 预测性回填:基于历史数据预判训练 Pod 完成时间,提前调度推理副本准备接管
# 基于 Prometheus 监控数据的碎片化检测
def detect_fragmentation(cluster_state, threshold=0.3) -> List[str]:
    """碎片化超过 30% 的节点"""
    fragmented_nodes = []

    for node in cluster_state.nodes:
        # 计算每张 GPU 的可用显存
        gpu_avail_mem = node.gpu_free_memory  # list of per-GPU values

        total_free = sum(gpu_avail_mem)
        if total_free == 0:
            continue

        # 最大连续块 vs 总空闲的比率
        max_contiguous = max(gpu_avail_mem)
        fragmentation_ratio = 1.0 - (max_contiguous / total_free)

        if fragmentation_ratio > threshold:
            fragmented_nodes.append(node.name)

    return fragmented_nodes

五、QoS 保障与公平性

5.1 GPU 显存的硬隔离

NVIDIA MIG 提供了物理层隔离。非 MIG 场景下,CUDA 的内存分配器不保证隔离——一个进程因 Bug 耗尽同卡其他进程的显存时,OOM Killer 可能误杀无辜进程。

解决方案:

# 通过 MPS 显存配额限制(soft limit)
echo "set_default_device_pinned_mem_limit 0 16384" | nvidia-cuda-mps-control
# 限制 MPS client 0 的 pinned memory 为 16GB

更彻底的隔离需要 CUDA MPS 的 per-client 配额或 MIG 硬分区。

5.2 算力调度的公平性

时间分片的公平性取决于时间片轮转的精度。短期公平可以通过 MPS 的 CUDA_MPS_ACTIVE_THREAD_PERCENTAGE 实现;长期公平需要一个中心化的"记账器"追踪各租户的实际 GPU 时间,作为调度偏好输入。

# 公平性记账器(伪代码)
class GPUBillingTracker:
    def __init__(self):
        self.tenant_usage = defaultdict(lambda: {
            'gpu_seconds': 0,
            'gpu_memory_gb_seconds': 0,
            'last_update': 0
        })

    def update(self, timeseries_data):
        """从 DCGM Exporter 的 Prometheus 指标更新"""
        for sample in timeseries_data:
            tenant = sample.labels['namespace']  # 以 namespace 为租户边界
            gpu_idx = sample.labels['gpu']
            utilization = sample.value

            self.tenant_usage[tenant]['gpu_seconds'] += (
                utilization * SCRAPE_INTERVAL
            )

    def get_fairness_score(self, tenant: str) -> float:
        """使用率越低的分数越高,鼓励多用"""
        total_usage = sum(t['gpu_seconds'] for t in self.tenant_usage.values())
        if total_usage == 0:
            return 1.0
        my_usage = self.tenant_usage[tenant]['gpu_seconds']
        return 1.0 - (my_usage / total_usage)

六、生产环境落地:一个实战配置

以 8×H100 80GB 节点为例,混合部署训练和推理的参考配置:

# node-pool-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: gpu-scheduler-config
data:
  config.yaml: |
    # 策略1:推理 Pod 优先使用 MIG 3g.40GB 分区(大显存,适中的算力)
    # 策略2:训练 Pod 使用整卡独占
    # 策略3:低优先级探索任务使用 MPS 共享未被分配的 GPU

    nodePools:
      - name: inference-pool
        gpuType: H100-80GB
        exclusive: false
        migProfile: "3g.40gb"
        maxReplicasPerNode: 2
        priorityClass: high

      - name: training-pool
        gpuType: H100-80GB
        exclusive: true
        migProfile: null
        maxReplicasPerNode: 1  # 1 Pod = 整卡
        priorityClass: medium
        preemptible: true

      - name: exploration-pool
        gpuType: H100-80GB
        exclusive: false
        sharing: timeSlicing
        replicas: 4  # 1 GPU 虚拟为 4 个逻辑单元
        priorityClass: low
        preemptible: true

部署后的资源分配效果:

Node: h100-node-01
├── GPU 0-3: [training-pool] ResNet-152 分布式训练 (整卡独占)
├── GPU 4-5: [inference-pool] MIG 3g.40GB × 4 实例 (LLM 推理)
├── GPU 6:   [exploration-pool] timeSlicing × 4 (Jupyter/实验)
└── GPU 7:   [reserved] 预热备用 (应对训练抢占)

七、GPU Operator 与 CNCF 生态

生产级部署强烈推荐 NVIDIA GPU Operator——它自动化了整个 GPU 基础设施栈的部署和管理:

# 安装 GPU Operator
helm install gpu-operator nvidia/gpu-operator \
  --namespace gpu-operator \
  --set devicePlugin.config.name=nvidia-device-plugin-config \
  --set mig.strategy=mixed \
  --set dcgmExporter.enabled=true \
  --set gfd.enabled=true  # GPU Feature Discovery

GPU Operator 自动处理:

  • NVIDIA Driver 容器化部署
  • Device Plugin(暴露 GPU 资源给 K8s)
  • DCGM Exporter(GPU 监控数据输出)
  • GPU Feature Discovery(自动标记节点 GPU 型号/驱动版本)
  • MIG Manager(MIG 分区配置自动化)

八、总结:从调度到治理

多租户 GPU 调度不是一个纯算法问题,而是从硬件能力到编排策略到治理体系的系统工程:

层级 核心问题 关键技术
硬件层 显存/算力/带宽隔离 MIG, MPS, NVLINK
编排层 Pod→GPU 的最优映射 Scheduler Extender, DRA
调度层 装箱效率与拓扑感知 Bin-Packing, Topology-Aware
运行时 碎片整理与 QoS 保障 Migration, Preemption, Fairness
治理层 成本分摊与容量规划 DCGM Metrics, Chargeback

终极目标是让 GPU 集群像 CPU 集群一样"透明"——租户不需要感知底层物理拓扑,调度系统自动将正确的工作负载放到正确的位置。距离这个目标,我们还有很长的路要走,但 DRA、MIG、eBPF GPU tracing 这些技术的发展,正在一步步缩小这个差距。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部