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 这些技术的发展,正在一步步缩小这个差距。

发表评论 取消回复