一、核心使命与设计理念
1.1 GPU 资源隔离的使命
在 GPU 共享场景下,多个容器共享同一张 GPU 会面临严重的资源竞争和隔离问题:
┌────────────────────────────────────────────────────────┐
│ GPU 共享场景的三大核心挑战 │
├────────────────────────────────────────────────────────┤
│ 1. 设备可见性问题 │
│ └─ 容器默认能看到节点上的所有 GPU │
│ └─ 容器 A 可能误用容器 B 分配的 GPU │
│ └─ 需要限制容器只能访问被分配的 GPU │
│ │
│ 2. 显存隔离问题 │
│ └─ CUDA 应用默认会尽可能多地申请显存 │
│ └─ 一个容器可能耗尽整张 GPU 的显存 │
│ └─ 需要限制每个容器的显存使用量 │
│ │
│ 3. 计算资源抢占 │
│ └─ GPU 调度器 (SM) 不支持 CPU 那样的时间片轮转 │
│ └─ 一个容器的大任务可能长时间占用 GPU │
│ └─ 需要实现 GPU QoS 保障机制 │
└────────────────────────────────────────────────────────┘
生产案例 - 某AI平台的 GPU 隔离问题:
未隔离场景:
节点: 1 张 NVIDIA V100 (16GB 显存)
容器 A: 推理服务,分配 50% GPU,实际只需 6GB 显存
容器 B: 推理服务,分配 50% GPU,实际需要 8GB 显存
问题:
1. 容器 A 看到 GPU 0 和 GPU 1,误配置使用了 GPU 1
2. 容器 A 的 CUDA 程序申请了 12GB 显存,导致容器 B OOM
3. 容器 A 的大 batch 推理任务占用 GPU 2 秒,容器 B 延迟飙升
结果:
- P99 延迟从 200ms 增加到 2500ms
- 容器 B 每天 OOM 重启 5-10 次
- GPU 利用率虽然高,但服务 SLA 严重不达标
RuntimeHooks 的三大隔离能力:
- 设备可见性隔离: 通过 CUDA_VISIBLE_DEVICES 限制容器只能看到分配的 GPU
- 显存隔离: 通过 cgroup devices 子系统限制显存访问
- QoS 保障: 通过优先级和调度策略保证服务质量
1.2 RuntimeHooks 架构设计(How)
RuntimeHooks 是 Koordlet 的关键组件,在容器生命周期的关键节点注入资源隔离策略:
┌──────────────────────────────────────────────────────────┐
│ RuntimeHooks 架构设计 │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. Hook 触发点 │ │
│ │ - PreRunPodSandbox: Pod 启动前 │ │
│ │ - PreCreateContainer: 容器创建前 │ │
│ │ - PreStartContainer: 容器启动前 │ │
│ │ - PostStartContainer: 容器启动后 │ │
│ │ - PreUpdateContainerResources: 更新前 │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 2. Hook 处理器 (GPU 相关) │ │
│ │ - GPUDeviceAllocate: 解析分配信息 │ │
│ │ - GPUEnvInject: 注入环境变量 │ │
│ │ - GPUCgroupSet: 设置 cgroup 限制 │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 3. 隔离策略执行 │ │
│ │ - 设置 CUDA_VISIBLE_DEVICES │ │
│ │ - 配置 CUDA_MPS_PIPE_DIRECTORY │ │
│ │ - 设置 devices cgroup 白名单 │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 4. 容器运行时 (CRI) │ │
│ │ - containerd / CRI-O │ │
│ │ - 创建容器并应用隔离策略 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
核心设计原则:
- 透明性: 容器无需修改代码,隔离策略自动注入
- 兼容性: 兼容 containerd、CRI-O 等主流容器运行时
- 灵活性: 支持多种隔离模式(独占、共享、MPS)
- 可观测性: 记录所有隔离操作,便于故障排查
二、CUDA_VISIBLE_DEVICES 机制详解
2.1 CUDA_VISIBLE_DEVICES 原理
CUDA_VISIBLE_DEVICES 是 NVIDIA CUDA 提供的环境变量,用于控制应用程序可见的 GPU 设备。
工作原理:
┌────────────────────────────────────────────────────────┐
│ CUDA_VISIBLE_DEVICES 工作原理 │
├────────────────────────────────────────────────────────┤
│ │
│ 节点物理 GPU: │
│ /dev/nvidia0 (UUID: GPU-aaa) │
│ /dev/nvidia1 (UUID: GPU-bbb) │
│ /dev/nvidia2 (UUID: GPU-ccc) │
│ /dev/nvidia3 (UUID: GPU-ddd) │
│ │
│ 场景 1: 不设置 CUDA_VISIBLE_DEVICES │
│ 应用程序调用 cudaGetDeviceCount() │
│ → 返回 4 (看到所有 GPU) │
│ 应用程序可以使用 GPU 0,1,2,3 │
│ │
│ 场景 2: CUDA_VISIBLE_DEVICES=1,3 │
│ CUDA Runtime 重新映射设备编号: │
│ 应用中的 GPU 0 → 物理 GPU 1 (/dev/nvidia1) │
│ 应用中的 GPU 1 → 物理 GPU 3 (/dev/nvidia3) │
│ 应用程序调用 cudaGetDeviceCount() │
│ → 返回 2 (只看到 2 张 GPU) │
│ 应用程序只能使用 GPU 0,1 (实际是物理 GPU 1,3) │
│ │
│ 场景 3: CUDA_VISIBLE_DEVICES=GPU-bbb,GPU-ddd (UUID) │
│ 与场景 2 效果相同,但更稳定 │
│ 即使设备顺序变化,UUID 仍然正确 │
│ │
└────────────────────────────────────────────────────────┘
支持的值格式:
| 格式 | 示例 | 说明 | 推荐度 |
|---|---|---|---|
| Minor 编号 | 0,1 | 使用设备 Minor 编号 | ⭐⭐ |
| UUID | GPU-aaa,GPU-bbb | 使用 GPU UUID | ⭐⭐⭐⭐⭐ |
| PCI Bus ID | 0000:03:00.0 | 使用 PCIe 总线地址 | ⭐⭐⭐ |
| 空值 | `` | 禁用所有 GPU | ⭐⭐⭐⭐ |
| -1 | -1 | 禁用所有 GPU (deprecated) | ⭐ |
Koordinator 选择 UUID 的原因:
优势:
1. 唯一性: UUID 全局唯一,不会冲突
2. 稳定性: 即使 GPU 热插拔,UUID 不变
3. 兼容性: 所有 CUDA 版本都支持 UUID
4. 可追溯: UUID 可以和 Device CRD 关联
劣势:
1. 长度较长: GPU-12345678-1234-1234-1234-123456789abc
2. 需要额外查询: 需要通过 NVML 获取 UUID
对比 Minor 编号:
Minor 编号: 简单,但不稳定 (热插拔后可能变化)
UUID: 稳定,但稍长
结论: Koordinator 使用 UUID 以确保稳定性和准确性
2.2 RuntimeHooks GPU 环境变量注入
Koordlet 通过 RuntimeHooks 在容器启动前注入 CUDA_VISIBLE_DEVICES:
// pkg/koordlet/runtimehooks/hooks/gpu/gpu.go
type gpuPlugin struct {
rule *runtimehooksRule
}
func (p *gpuPlugin) Register(op hooks.Options) {
klog.V(5).Infof("register hook %v", "gpu resource allocate")
// 注册 PreCreateContainer Hook
hooks.RegisterPodHook(hooks.PreCreateContainer, "gpu resource allocate",
"allocate gpu resources and inject env",
p.InjectContainerGPUEnv)
}
func (p *gpuPlugin) InjectContainerGPUEnv(
proto hooks.HooksProtocol,
) error {
containerCtx := proto.(*hooks.ContainerContext)
if containerCtx == nil {
return fmt.Errorf("container protocol is nil")
}
containerReq := containerCtx.Request
podAnnotations := containerCtx.Request.PodAnnotations
// 1. 解析 Pod Annotation 中的设备分配信息
deviceAllocations, err := apiext.GetDeviceAllocations(podAnnotations)
if err != nil {
return fmt.Errorf("failed to get device allocations: %v", err)
}
// 2. 提取 GPU 分配信息
gpuAllocations := deviceAllocations[schedulingv1alpha1.GPU]
if len(gpuAllocations) == 0 {
// 没有分配 GPU,跳过
return nil
}
// 3. 构建 CUDA_VISIBLE_DEVICES 值
var visibleDevices []string
for _, alloc := range gpuAllocations {
// 优先使用 UUID
if alloc.UUID != "" {
visibleDevices = append(visibleDevices, alloc.UUID)
} else {
// 回退到 Minor 编号
visibleDevices = append(visibleDevices, fmt.Sprintf("%d", alloc.Minor))
}
}
// 4. 注入环境变量
if containerReq.ContainerEnvs == nil {
containerReq.ContainerEnvs = make(map[string]string)
}
containerReq.ContainerEnvs["CUDA_VISIBLE_DEVICES"] = strings.Join(visibleDevices, ",")
klog.V(4).Infof("inject CUDA_VISIBLE_DEVICES=%s for container %s/%s/%s",
containerReq.ContainerEnvs["CUDA_VISIBLE_DEVICES"],
containerCtx.Request.PodMeta.Namespace,
containerCtx.Request.PodMeta.Name,
containerReq.ContainerMeta.Name)
return nil
}
注入流程时序图:
┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌─────────┐ ┌──────────┐
│ Pod │ │Scheduler │ │ RuntimeHooks │ │Container│ │ CUDA │
│ │ │ │ │ │ │ Runtime │ │ Runtime │
└────┬────┘ └─────┬────┘ └──────┬────────┘ └────┬────┘ └─────┬────┘
│ │ │ │ │
│ 1. Create │ │ │ │
│ Pod │ │ │ │
├─────────────> │ │ │
│ │ │ │ │
│ │ 2. Schedule │ │ │
│ │ Pod to Node │ │ │
│ ├───────────────> │ │
│ │ │ │ │
│ │ 3. PreBind │ │ │
│ │ 注入 Device │ │ │
│ │ Annotation │ │ │
│ │ │ │ │
│ │ │ 4. PreCreateContainer │
│ │ │ Hook 触发 │ │
│ │ <────────────────┤ │
│ │ │ │ │
│ │ │ 5. 解析 Annotation │
│ │ │ device-allocated: │
│ │ │ {"gpu":[{"minor":0, │
│ │ │ "uuid":"GPU-aaa"}]} │
│ │ │ │ │
│ │ │ 6. 注入环境变量│ │
│ │ │ CUDA_VISIBLE_DEVICES= │
│ │ │ GPU-aaa │ │
│ │ ├────────────────> │
│ │ │ │ │
│ │ │ │ 7. 创建容器 │
│ │ │ │ (带环境变量)
│ │ │ │ │
│ │ │ │ 8. 容器启动 │
│ │ │ ├──────────────>
│ │ │ │ │
│ │ │ │ 9. CUDA 初始化
│ │ │ │ 读取 CUDA_ │
│ │ │ │ VISIBLE_ │
│ │ │ │ DEVICES │
│ │ │ │ │
│ │ │ │ 10. 只看到 │
│ │ │ │ GPU-aaa │
│ │ │ │ │
◀──────────────────────────── 容器成功启动 ──────────────────────┘
2.3 生产案例 - 环境变量验证
验证容器中的 GPU 可见性:
# 1. 查看 Pod 的设备分配 Annotation
kubectl get pod gpu-pod -o jsonpath='{.metadata.annotations.scheduling\.koordinator\.sh/device-allocated}'
# 输出:
# {"gpu":[{"minor":1,"uuid":"GPU-12345678-1234-1234-1234-123456789001"}]}
# 2. 进入容器查看环境变量
kubectl exec -it gpu-pod -- env | grep CUDA
# 输出:
# CUDA_VISIBLE_DEVICES=GPU-12345678-1234-1234-1234-123456789001
# 3. 在容器内检查可见的 GPU 数量
kubectl exec -it gpu-pod -- python3 -c "import torch; print(f'GPU count: {torch.cuda.device_count()}')"
# 输出:
# GPU count: 1
# 4. 在容器内查看 GPU UUID
kubectl exec -it gpu-pod -- nvidia-smi -L
# 输出:
# GPU 0: Tesla V100-SXM2-16GB (UUID: GPU-12345678-1234-1234-1234-123456789001)
# 5. 对比节点上的实际 GPU
ssh node-1 nvidia-smi -L
# 输出:
# GPU 0: Tesla V100-SXM2-16GB (UUID: GPU-00000000-0000-0000-0000-000000000000)
# GPU 1: Tesla V100-SXM2-16GB (UUID: GPU-12345678-1234-1234-1234-123456789001)
# GPU 2: Tesla V100-SXM2-16GB (UUID: GPU-87654321-4321-4321-4321-210987654321)
# 结论: 容器只能看到分配给它的 GPU 1,在容器内被重新编号为 GPU 0
三、GPU 显存隔离机制
3.1 显存隔离的必要性
CUDA 应用默认会尽可能多地申请显存,导致共享场景下的显存竞争:
┌────────────────────────────────────────────────────────┐
│ 无显存限制的问题场景 │
├────────────────────────────────────────────────────────┤
│ │
│ 节点: 1 张 V100 (16GB 显存) │
│ │
│ 容器 A (分配 50% GPU): │
│ 实际需要: 6GB 显存 │
│ CUDA 实际申请: 14GB 显存 (尽可能多) │
│ │
│ 容器 B (分配 50% GPU): │
│ 实际需要: 8GB 显存 │
│ CUDA 尝试申请: 12GB 显存 │
│ 结果: cudaMalloc() 失败,返回 out of memory │
│ │
│ 问题根源: │
│ CUDA 应用通常使用 cudaMalloc() 申请显存 │
│ 没有操作系统级别的强制限制 │
│ 依赖应用程序自觉控制显存使用量 │
│ │
└────────────────────────────────────────────────────────┘
显存隔离的两种方案:
| 方案 | 原理 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 应用层限制 | 设置环境变量CUDA_MEM_LIMIT | 简单,无需内核支持 | 应用可以绕过 | 可信任的应用 |
| 内核层限制 | 通过 cgroup devices 限制设备访问 | 强制隔离,无法绕过 | 实现复杂 | 多租户环境 |
Koordinator 的显存隔离策略:
Koordinator 采用 应用层引导 + 内核层保护 的混合方案:
- 应用层: 通过环境变量
CUDA_MEM_LIMIT建议应用控制显存 - 内核层: 通过 cgroup devices 子系统限制 GPU 设备访问(未来扩展)
3.2 应用层显存限制
通过环境变量引导应用程序控制显存使用:
// pkg/koordlet/runtimehooks/hooks/gpu/gpu.go
func (p *gpuPlugin) InjectContainerGPUEnv(
proto hooks.HooksProtocol,
) error {
containerCtx := proto.(*hooks.ContainerContext)
containerReq := containerCtx.Request
podAnnotations := containerCtx.Request.PodAnnotations
// 解析设备分配信息
deviceAllocations, err := apiext.GetDeviceAllocations(podAnnotations)
if err != nil {
return err
}
gpuAllocations := deviceAllocations[schedulingv1alpha1.GPU]
if len(gpuAllocations) == 0 {
return nil
}
// 注入 CUDA_VISIBLE_DEVICES
var visibleDevices []string
var totalMemory int64
for _, alloc := range gpuAllocations {
if alloc.UUID != "" {
visibleDevices = append(visibleDevices, alloc.UUID)
} else {
visibleDevices = append(visibleDevices, fmt.Sprintf("%d", alloc.Minor))
}
// 累加分配的显存
if memLimit, ok := alloc.Resources[apiext.ResourceGPUMemory]; ok {
totalMemory += memLimit.Value()
}
}
if containerReq.ContainerEnvs == nil {
containerReq.ContainerEnvs = make(map[string]string)
}
// 注入 CUDA_VISIBLE_DEVICES
containerReq.ContainerEnvs["CUDA_VISIBLE_DEVICES"] = strings.Join(visibleDevices, ",")
// 注入显存限制 (以 MB 为单位)
if totalMemory > 0 {
memoryMB := totalMemory / (1024 * 1024)
containerReq.ContainerEnvs["CUDA_MEM_LIMIT"] = fmt.Sprintf("%dM", memoryMB)
klog.V(4).Infof("inject CUDA_MEM_LIMIT=%dM for container %s/%s/%s",
memoryMB,
containerCtx.Request.PodMeta.Namespace,
containerCtx.Request.PodMeta.Name,
containerReq.ContainerMeta.Name)
}
return nil
}
应用程序中使用显存限制:
# Python / PyTorch 示例
import os
import torch
# 1. 读取 CUDA_MEM_LIMIT 环境变量
cuda_mem_limit = os.getenv('CUDA_MEM_LIMIT', None)
if cuda_mem_limit:
# 解析显存限制 (例如 "8192M")
mem_limit_mb = int(cuda_mem_limit.rstrip('M'))
mem_limit_bytes = mem_limit_mb * 1024 * 1024
# 设置 PyTorch 显存分配策略
torch.cuda.set_per_process_memory_fraction(
mem_limit_bytes / torch.cuda.get_device_properties(0).total_memory
)
print(f"GPU memory limit set to {mem_limit_mb}MB")
# 2. 或者使用 TensorFlow
import tensorflow as tf
if cuda_mem_limit:
mem_limit_mb = int(cuda_mem_limit.rstrip('M'))
# 配置 TensorFlow GPU 显存增长策略
gpus = tf.config.list_physical_devices('GPU')
if gpus:
tf.config.set_logical_device_configuration(
gpus[0],
[tf.config.LogicalDeviceConfiguration(memory_limit=mem_limit_mb)]
)
3.3 内核层显存保护 (未来扩展)
通过 cgroup devices 子系统实现强制隔离:
// pkg/koordlet/runtimehooks/hooks/gpu/gpu_cgroup.go
// 未来扩展功能
func (p *gpuPlugin) SetGPUCgroup(
proto hooks.HooksProtocol,
) error {
containerCtx := proto.(*hooks.ContainerContext)
// 1. 获取容器的 cgroup 路径
cgroupPath := containerCtx.Request.CgroupParent
// 2. 解析分配的 GPU Minor 列表
gpuAllocations := ... // 从 Annotation 解析
// 3. 构建 devices.allow 白名单
// 只允许访问分配的 GPU 设备
for _, alloc := range gpuAllocations {
minor := alloc.Minor
// GPU 设备: c 195:minor (字符设备,主设备号 195)
deviceRule := fmt.Sprintf("c 195:%d rwm", minor)
// 写入 cgroup devices.allow
deviceAllowPath := filepath.Join(cgroupPath, "devices.allow")
if err := ioutil.WriteFile(deviceAllowPath, []byte(deviceRule), 0644); err != nil {
return fmt.Errorf("failed to set devices.allow: %v", err)
}
klog.V(4).Infof("allow GPU device %d for container %s", minor, containerCtx.Request.ContainerMeta.Name)
}
return nil
}
cgroup devices 子系统原理:
┌────────────────────────────────────────────────────────┐
│ cgroup devices 子系统工作原理 │
├────────────────────────────────────────────────────────┤
│ │
│ 1. 设备分类 │
│ 块设备 (b): 磁盘、SSD │
│ 字符设备 (c): GPU、声卡 │
│ │
│ 2. 设备标识 │
│ 主设备号 (major): 设备类型,GPU 为 195 │
│ 次设备号 (minor): 具体设备,GPU 0/1/2... │
│ │
│ 3. 权限控制 │
│ r: 读权限 │
│ w: 写权限 │
│ m: mknod 权限 (创建设备文件) │
│ │
│ 4. 白名单模式 │
│ devices.deny = "a" (默认拒绝所有设备) │
│ devices.allow = "c 195:0 rwm" (允许 GPU 0) │
│ devices.allow = "c 195:1 rwm" (允许 GPU 1) │
│ │
│ 5. 访问控制流程 │
│ 应用调用 open("/dev/nvidia0") │
│ ↓ │
│ 内核检查 cgroup devices 规则 │
│ ↓ │
│ 如果设备在白名单中,允许访问 │
│ 否则返回 Permission denied │
│ │
└────────────────────────────────────────────────────────┘
四、GPU QoS 保障机制
4.1 GPU QoS 的挑战
GPU 的调度器 (SM Scheduler) 不同于 CPU,不支持传统的时间片轮转:
┌────────────────────────────────────────────────────────┐
│ GPU vs CPU 调度器对比 │
├────────────────────────────────────────────────────────┤
│ │
│ CPU 调度器: │
│ - CFS (Completely Fair Scheduler) │
│ - 支持时间片轮转 (time slice) │
│ - 支持优先级 (nice value) │
│ - 支持 cgroup cpu.shares / cpu.cfs_quota_us │
│ - 可以精确控制每个进程的 CPU 使用比例 │
│ │
│ GPU 调度器: │
│ - SM (Streaming Multiprocessor) Scheduler │
│ - 不支持时间片轮转 │
│ - 不支持优先级 (CUDA 11.0+ 支持有限优先级) │
│ - 缺乏操作系统级别的 cgroup 控制 │
│ - 先到先得 (FCFS),大任务可能长时间占用 GPU │
│ │
│ 结果: │
│ 容器 A 提交一个大 kernel (耗时 2 秒) │
│ 容器 B 提交一个小 kernel (耗时 10ms) │
│ 容器 B 必须等待容器 A 的 kernel 完成 │
│ → 容器 B 的延迟从 10ms 增加到 2010ms │
│ │
└────────────────────────────────────────────────────────┘
4.2 NVIDIA MPS (Multi-Process Service)
NVIDIA MPS 是 GPU 共享的官方解决方案,但有一定局限性:
MPS 工作原理:
┌────────────────────────────────────────────────────────┐
│ NVIDIA MPS 架构 │
├────────────────────────────────────────────────────────┤
│ │
│ 无 MPS: │
│ 容器 A → CUDA Context A → GPU │
│ 容器 B → CUDA Context B → GPU │
│ 问题: 多个 Context 切换开销大,并发差 │
│ │
│ 有 MPS: │
│ 容器 A ┐ │
│ 容器 B ├→ MPS Server → Shared CUDA Context → GPU │
│ 容器 C ┘ │
│ 优势: 单个 Context,并发性能更好 │
│ │
│ MPS 配置: │
│ CUDA_MPS_PIPE_DIRECTORY=/tmp/mps-pipe │
│ CUDA_MPS_LOG_DIRECTORY=/var/log/mps │
│ │
└────────────────────────────────────────────────────────┘
MPS 的优势和局限:
| 方面 | 说明 |
|---|---|
| 优势 | - 减少 Context 切换开销 - 提高小 kernel 的并发性能 - 单个 GPU 支持更多进程 |
| 局限 | - 不支持显存隔离 - 不支持进程优先级 - 故障隔离差 (一个进程崩溃可能影响其他) |
| 适用场景 | - 推理服务 (小 kernel,高并发) - 可信任的应用 |
| 不适用场景 | - 训练任务 (大 kernel,需要独占) - 多租户环境 (需要强隔离) |
RuntimeHooks 注入 MPS 配置:
// pkg/koordlet/runtimehooks/hooks/gpu/gpu_mps.go
func (p *gpuPlugin) InjectMPSEnv(
proto hooks.HooksProtocol,
) error {
containerCtx := proto.(*hooks.ContainerContext)
containerReq := containerCtx.Request
// 检查是否启用 MPS
// 通过 Pod Annotation 控制: gpu.koordinator.sh/mps-enabled=true
if !p.isMPSEnabled(containerCtx.Request.PodAnnotations) {
return nil
}
if containerReq.ContainerEnvs == nil {
containerReq.ContainerEnvs = make(map[string]string)
}
// 注入 MPS 相关环境变量
containerReq.ContainerEnvs["CUDA_MPS_PIPE_DIRECTORY"] = "/tmp/nvidia-mps"
containerReq.ContainerEnvs["CUDA_MPS_LOG_DIRECTORY"] = "/var/log/nvidia-mps"
klog.V(4).Infof("inject MPS env for container %s/%s/%s",
containerCtx.Request.PodMeta.Namespace,
containerCtx.Request.PodMeta.Name,
containerReq.ContainerMeta.Name)
return nil
}
4.3 GPU Stream 优先级 (CUDA 11.0+)
CUDA 11.0 引入了 Stream 优先级,可以部分缓解 QoS 问题:
// CUDA Stream 优先级示例
#include <cuda_runtime.h>
int main() {
cudaStream_t highPriorityStream, lowPriorityStream;
int leastPriority, greatestPriority;
// 1. 获取优先级范围
cudaDeviceGetStreamPriorityRange(&leastPriority, &greatestPriority);
// 通常: leastPriority = 0, greatestPriority = -1
// 2. 创建高优先级 Stream
cudaStreamCreateWithPriority(&highPriorityStream, cudaStreamNonBlocking, greatestPriority);
// 3. 创建低优先级 Stream
cudaStreamCreateWithPriority(&lowPriorityStream, cudaStreamNonBlocking, leastPriority);
// 4. 在高优先级 Stream 上提交任务
// 延迟敏感的推理任务
myInferenceKernel<<<grid, block, 0, highPriorityStream>>>(...);
// 5. 在低优先级 Stream 上提交任务
// 批处理任务
myBatchProcessingKernel<<<grid, block, 0, lowPriorityStream>>>(...);
return 0;
}
局限性:
- 优先级只在同一个 GPU 的不同 Stream 之间有效
- 不同进程之间的优先级无法保证
- 需要应用程序主动使用
4.4 Koordinator 的 GPU QoS 策略
Koordinator 采用 调度层隔离 + 应用层优化 的策略:
┌────────────────────────────────────────────────────────┐
│ Koordinator GPU QoS 保障策略 │
├────────────────────────────────────────────────────────┤
│ │
│ 1. 调度层隔离 (DeviceShare 插件) │
│ - 延迟敏感 (LS) Pod: 独占 GPU 或大比例分配 │
│ - 吞吐优先 (BE) Pod: 小比例分配,可抢占 │
│ - 避免 LS 和 BE Pod 共享同一张 GPU │
│ │
│ 2. 应用层优化 │
│ - 推理服务: 使用 MPS + Stream 优先级 │
│ - 训练任务: 独占 GPU,避免共享 │
│ - 批处理任务: 使用低优先级 Stream │
│ │
│ 3. 监控和告警 │
│ - 监控 GPU kernel 执行时间 │
│ - P99 延迟超过阈值时告警 │
│ - 自动调整 GPU 分配比例 │
│ │
└────────────────────────────────────────────────────────┘
生产案例 - QoS 分级策略:
某AI平台的 GPU QoS 配置:
| Pod 类型 | QoS 等级 | GPU 分配策略 | 监控指标 |
|---|---|---|---|
| 在线推理(LS) | 高 | 独占 50-100% GPU,禁止共享 | P99 < 100ms |
| 离线训练(BE) | 低 | 10-25% GPU,可抢占 | 吞吐量最大化 |
| 模型调优(LS) | 中 | 独占整卡,时间限制 | 完成时间 < 1h |
效果统计:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 在线推理 P99 延迟 | 500ms | 80ms | 84% ↓ |
| 离线训练吞吐量 | 100 samples/s | 85 samples/s | 15% ↓ (可接受) |
| GPU 利用率 | 45% | 72% | 60% ↑ |
五、生产环境实践
5.1 完整的 GPU 隔离配置
Pod YAML 示例:
apiVersion: v1
kind: Pod
metadata:
name: gpu-inference-pod
namespace: ai-platform
labels:
app: inference
koordinator.sh/qosClass: LS # 延迟敏感
annotations:
# GPU 分配信息 (由 Scheduler 自动注入)
scheduling.koordinator.sh/device-allocated: |
{
"gpu": [
{
"minor": 1,
"uuid": "GPU-12345678-1234-1234-1234-123456789001",
"resources": {
"koordinator.sh/gpu-core": "50",
"koordinator.sh/gpu-memory": "8Gi",
"koordinator.sh/gpu-memory-ratio": "50"
}
}
]
}
# 可选: 启用 MPS
gpu.koordinator.sh/mps-enabled: "true"
spec:
containers:
- name: inference
image: inference-server:v1.0
resources:
requests:
koordinator.sh/gpu-core: "50"
koordinator.sh/gpu-memory: "8Gi"
limits:
koordinator.sh/gpu-core: "50"
koordinator.sh/gpu-memory: "8Gi"
# 环境变量 (由 RuntimeHooks 自动注入)
env:
- name: CUDA_VISIBLE_DEVICES
value: "GPU-12345678-1234-1234-1234-123456789001"
- name: CUDA_MEM_LIMIT
value: "8192M"
- name: CUDA_MPS_PIPE_DIRECTORY
value: "/tmp/nvidia-mps"
5.2 验证 GPU 隔离效果
步骤 1: 检查设备分配
# 查看 Pod 的 GPU 分配
kubectl get pod gpu-inference-pod -o jsonpath='{.metadata.annotations.scheduling\.koordinator\.sh/device-allocated}' | jq .
# 输出:
{
"gpu": [
{
"minor": 1,
"uuid": "GPU-12345678-1234-1234-1234-123456789001",
"resources": {
"koordinator.sh/gpu-core": "50",
"koordinator.sh/gpu-memory": "8Gi"
}
}
]
}
步骤 2: 验证环境变量
# 进入容器
kubectl exec -it gpu-inference-pod -- bash
# 检查 CUDA_VISIBLE_DEVICES
echo $CUDA_VISIBLE_DEVICES
# 输出: GPU-12345678-1234-1234-1234-123456789001
# 检查显存限制
echo $CUDA_MEM_LIMIT
# 输出: 8192M
# 使用 nvidia-smi 验证
nvidia-smi -L
# 输出: GPU 0: Tesla V100-SXM2-16GB (UUID: GPU-12345678-1234-1234-1234-123456789001)
# 验证可见的 GPU 数量
python3 -c "import torch; print(f'GPU count: {torch.cuda.device_count()}')"
# 输出: GPU count: 1
步骤 3: 测试显存隔离
# test_memory_limit.py
import torch
import os
cuda_mem_limit = os.getenv('CUDA_MEM_LIMIT', None)
if cuda_mem_limit:
mem_limit_mb = int(cuda_mem_limit.rstrip('M'))
print(f"CUDA_MEM_LIMIT: {mem_limit_mb}MB")
# 尝试申请超过限制的显存
try:
# 尝试申请 12GB 显存 (超过 8GB 限制)
tensor = torch.zeros(12 * 1024 * 256 * 1024, dtype=torch.uint8, device='cuda')
print("WARNING: Allocated more memory than limit!")
except RuntimeError as e:
print(f"Expected error: {e}")
print("Memory limit is working correctly")
# 申请在限制内的显存
try:
# 申请 6GB 显存 (在 8GB 限制内)
tensor = torch.zeros(6 * 1024 * 256 * 1024, dtype=torch.uint8, device='cuda')
print(f"Successfully allocated 6GB memory")
except RuntimeError as e:
print(f"Unexpected error: {e}")
5.3 故障排查
问题 1: 容器看到所有 GPU
现象:
kubectl exec -it gpu-pod -- nvidia-smi -L
# 输出: 看到节点上的所有 8 张 GPU
排查步骤:
# 1. 检查 Pod Annotation 是否存在
kubectl get pod gpu-pod -o jsonpath='{.metadata.annotations}' | grep device-allocated
# 2. 检查环境变量是否注入
kubectl exec -it gpu-pod -- env | grep CUDA_VISIBLE_DEVICES
# 3. 如果环境变量不存在,检查 RuntimeHooks 日志
kubectl logs -n koordinator-system koordlet-xxx | grep "inject CUDA_VISIBLE_DEVICES"
# 4. 检查 RuntimeHooks 是否启用
kubectl get cm -n koordinator-system koordlet-config -o yaml | grep RuntimeHooks
可能原因:
- Pod 没有请求 GPU 资源
- RuntimeHooks 未启用
- Koordlet 版本过低
问题 2: CUDA 程序报错 "no CUDA-capable device is detected"
现象:
kubectl logs gpu-pod
# 输出: RuntimeError: no CUDA-capable device is detected
排查步骤:
# 1. 检查 CUDA_VISIBLE_DEVICES 的值
kubectl exec -it gpu-pod -- echo $CUDA_VISIBLE_DEVICES
# 如果输出为空或 "-1",说明没有分配 GPU
# 2. 检查 UUID 是否正确
kubectl exec -it gpu-pod -- nvidia-smi -L
# 如果报错 "No devices were found",说明 UUID 无效
# 3. 对比 Device CRD 中的 UUID
kubectl get device <node-name> -o yaml | grep -A5 "uuid"
# 4. 检查节点的实际 GPU UUID
ssh <node> nvidia-smi -L
可能原因:
- GPU UUID 不匹配 (GPU 热插拔后变化)
- Device CRD 未及时更新
- GPU 硬件故障
问题 3: 显存溢出 (OOM)
现象:
kubectl logs gpu-pod
# 输出: RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB
排查步骤:
# 1. 检查分配的显存
kubectl get pod gpu-pod -o jsonpath='{.metadata.annotations.scheduling\.koordinator\.sh/device-allocated}' | jq '.gpu[0].resources."koordinator.sh/gpu-memory"'
# 2. 检查 CUDA_MEM_LIMIT 环境变量
kubectl exec -it gpu-pod -- echo $CUDA_MEM_LIMIT
# 3. 检查实际显存使用
kubectl exec -it gpu-pod -- nvidia-smi --query-gpu=memory.used --format=csv
# 4. 增加分配的显存
# 修改 Pod 的 GPU 资源请求
resources:
requests:
koordinator.sh/gpu-memory: "12Gi" # 从 8Gi 增加到 12Gi
5.4 监控指标
Prometheus 监控规则:
groups:
- name: gpu_isolation
rules:
# 1. GPU 设备可见性错误
- record: gpu:visibility_error_rate
expr: |
rate(koordlet_runtimehooks_gpu_visibility_errors_total[5m])
/
rate(koordlet_runtimehooks_gpu_inject_total[5m])
# 2. GPU 显存 OOM 次数
- record: gpu:oom_count
expr: |
increase(container_gpu_memory_oom_total[1h])
# 3. CUDA_VISIBLE_DEVICES 注入成功率
- record: gpu:env_inject_success_rate
expr: |
rate(koordlet_runtimehooks_gpu_env_inject_success_total[5m])
/
rate(koordlet_runtimehooks_gpu_env_inject_total[5m])
# 告警规则
- alert: GPUVisibilityErrorHigh
expr: gpu:visibility_error_rate > 0.1
for: 5m
annotations:
summary: "GPU可见性错误率超过10%"
- alert: GPUOOMFrequent
expr: gpu:oom_count > 5
for: 1h
annotations:
summary: "GPU OOM次数过多"