揭秘云原生混布资源调度器Koordinator (十六)GPU 资源隔离与 RuntimeHooks

4 阅读16分钟

一、核心使命与设计理念

1.1 GPU 资源隔离的使命

在 GPU 共享场景下,多个容器共享同一张 GPU 会面临严重的资源竞争和隔离问题:

┌────────────────────────────────────────────────────────┐
│        GPU 共享场景的三大核心挑战                       │
├────────────────────────────────────────────────────────┤
│  1. 设备可见性问题                                      │
│     └─ 容器默认能看到节点上的所有 GPU                   │
│     └─ 容器 A 可能误用容器 B 分配的 GPU                 │
│     └─ 需要限制容器只能访问被分配的 GPU                 │
│                                                         │
│  2. 显存隔离问题                                        │
│     └─ CUDA 应用默认会尽可能多地申请显存                │
│     └─ 一个容器可能耗尽整张 GPU 的显存                  │
│     └─ 需要限制每个容器的显存使用量                     │
│                                                         │
│  3. 计算资源抢占                                        │
│     └─ GPU 调度器 (SM) 不支持 CPU 那样的时间片轮转     │
│     └─ 一个容器的大任务可能长时间占用 GPU               │
│     └─ 需要实现 GPU QoS 保障机制                        │
└────────────────────────────────────────────────────────┘

生产案例 - 某AI平台的 GPU 隔离问题:

未隔离场景:

节点: 1NVIDIA V100 (16GB 显存)
容器 A: 推理服务,分配 50% GPU,实际只需 6GB 显存
容器 B: 推理服务,分配 50% GPU,实际需要 8GB 显存

问题:
1. 容器 A 看到 GPU 0GPU 1,误配置使用了 GPU 1
2. 容器 ACUDA 程序申请了 12GB 显存,导致容器 B OOM
3. 容器 A 的大 batch 推理任务占用 GPU 2 秒,容器 B 延迟飙升

结果:
- P99 延迟从 200ms 增加到 2500ms
- 容器 B 每天 OOM 重启 5-10- GPU 利用率虽然高,但服务 SLA 严重不达标

RuntimeHooks 的三大隔离能力:

  1. 设备可见性隔离: 通过 CUDA_VISIBLE_DEVICES 限制容器只能看到分配的 GPU
  2. 显存隔离: 通过 cgroup devices 子系统限制显存访问
  3. 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                             
       - 创建容器并应用隔离策略                          
  └─────────────────────────────────────────────┘        
                                                           
└──────────────────────────────────────────────────────────┘

核心设计原则:

  1. 透明性: 容器无需修改代码,隔离策略自动注入
  2. 兼容性: 兼容 containerd、CRI-O 等主流容器运行时
  3. 灵活性: 支持多种隔离模式(独占、共享、MPS)
  4. 可观测性: 记录所有隔离操作,便于故障排查

二、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 编号⭐⭐
UUIDGPU-aaa,GPU-bbb使用 GPU UUID⭐⭐⭐⭐⭐
PCI Bus ID0000: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 采用 应用层引导 + 内核层保护 的混合方案:

  1. 应用层: 通过环境变量 CUDA_MEM_LIMIT 建议应用控制显存
  2. 内核层: 通过 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 必须等待容器 Akernel 完成                 │
│    → 容器 B 的延迟从 10ms 增加到 2010ms                 │
│                                                         │
└────────────────────────────────────────────────────────┘

4.2 NVIDIA MPS (Multi-Process Service)

NVIDIA MPS 是 GPU 共享的官方解决方案,但有一定局限性:

MPS 工作原理:

┌────────────────────────────────────────────────────────┐
│          NVIDIA MPS 架构                                │
├────────────────────────────────────────────────────────┤
│                                                         │
│  无 MPS:                                                │
│    容器 ACUDA Context AGPU                       │
│    容器 BCUDA Context BGPU                       │
│    问题: 多个 Context 切换开销大,并发差                 │
│                                                         │
│  有 MPS:                                                │
│    容器 A ┐                                             │
│    容器 B ├→ MPS ServerShared CUDA ContextGPU    │
│    容器 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 延迟500ms80ms84% ↓
离线训练吞吐量100 samples/s85 samples/s15% ↓ (可接受)
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次数过多"