1. GPU调度概述

大家好,我是你们的讲师。今天咱们聊聊GPU调度。说实话,这玩意儿刚接触时我也觉得头大——GPU不像CPU,它是个“黑箱”,你扔进去任务,它怎么跑、跑多快,很多时候你根本看不见。但搞运维的,必须得懂。

我个人习惯把GPU调度理解成“分蛋糕”。蛋糕就是GPU的计算资源,谁吃、吃多少、什么时候吃,得有个规矩。这个规矩,就是调度器。

1.1 GPU工作原理:它到底在忙什么?

GPU最初设计是为了图形渲染。你想想看,一张图片上几百万个像素点,每个点都要做矩阵运算。CPU是“一个大脑处理复杂问题”,GPU是“几千个小脑同时算简单问题”。

现代GPU用于通用计算(GPGPU),核心还是那套:大规模并行。一个典型的深度学习训练任务,比如ResNet-50,里面全是卷积、池化、全连接。这些操作可以拆成无数个小矩阵乘法,扔给GPU的几千个CUDA核心同时算。

关键点:GPU不是“快”,而是“多”。它用数量换时间。一个任务如果没法并行化,扔给GPU反而更慢——因为数据搬运的开销太大了。

我记得有一次,一个团队抱怨说“GPU利用率只有30%”。我上去一看,他们写了个循环,每次只处理一条数据。说白了,GPU在等CPU喂数据。这就是典型的“数据饥饿”。

GPU内部结构大致如下:

  • 流多处理器(SM):GPU的基本计算单元。每个SM包含多个CUDA核心。
  • 显存(VRAM):GPU自己的内存,带宽极高。训练大模型时,显存往往是瓶颈。
  • NVLink/NVSwitch:多卡通信的“高速公路”。我见过有人用PCIe跑多卡训练,结果通信开销占了40%——换NVLink后直接翻倍。

我的经验:排查GPU性能问题,先看显存带宽利用率,再看SM活跃度。很多“GPU跑不满”的问题,根源在数据搬运。

1.2 调度器角色:谁来决定GPU给谁用?

调度器,说白了就是个“管家”。它要回答三个问题:

  1. 谁有资格用?(资源配额、优先级)
  2. 用哪块卡?(拓扑亲和性、显存大小)
  3. 用多久?(时间片、抢占策略)

我曾经遇到过一个事故:两个团队同时提交了训练任务,结果互相抢卡,导致双双崩溃。原因就是调度器没有做资源隔离。从那以后,我坚持所有GPU集群必须配调度器,哪怕只有两块卡。

调度器的核心职责包括:

职责 说明 我的踩坑记录
资源抽象 把物理GPU抽象成可分配的资源单位 有人把一块V100切成4份,结果显存碎片化严重
任务排队 按优先级/FIFO/公平策略排队 优先级队列没设上限,高优任务把低优饿死了
拓扑感知 尽量把任务调度到同一PCIe交换机下的卡 跨NUMA节点通信,延迟高了3倍
故障处理 GPU报错时自动迁移任务 Xid错误没处理,任务卡死了一周

警告:调度器不是万能的。它只能管理它“知道”的资源。如果你手动在节点上跑了nvidia-smi占用了显存,调度器是看不见的——这就是为什么我强调“所有GPU必须通过调度器分配”。

1.3 常见调度框架:Kubernetes、Slurm、YARN

目前主流就这三个。我三个都用过,各有各的脾气。

1.3.1 Kubernetes + GPU

K8s是目前最火的。它通过device plugin暴露GPU资源。你只需要在Pod里声明nvidia.com/gpu: 1,调度器就会自动绑卡。

apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  containers:
  - name: cuda-container
    image: nvidia/cuda:11.0-base
    resources:
      limits:
        nvidia.com/gpu: 1  # 申请1块GPU

嗯,这里要注意:K8s默认不支持GPU的“时间片”共享。你申请1块卡,它就独占整块。如果你想让多任务共享一块卡,得装额外的插件,比如GPU Share或MIG(A100以上支持)。

我个人习惯用K8s做在线推理服务。因为它的弹性伸缩做得很好,流量大了自动加Pod,流量小了缩回来。但训练任务嘛...我建议还是用Slurm。

1.3.2 Slurm + GPU

Slurm是HPC领域的“老大哥”。它设计之初就是管大规模并行计算的。GPU支持也很成熟,通过--gres=gpu:2指定卡数。

# 申请2块GPU,跑24小时
srun --gres=gpu:2 --time=24:00:00 train.py

Slurm的优势在于:

  • 细粒度控制:可以指定具体哪块卡(--gres-flags=enforce-binding)
  • 抢占式调度:高优任务可以抢低优任务的卡
  • 作业依赖:A任务跑完自动启动B任务

我曾经用Slurm管理过一个256卡集群。最头疼的是“碎片化”——任务有大有小,大的要8卡,小的要1卡,调度器经常把卡拆得七零八落,导致大任务等半天。后来我启用了--partition分区策略,把卡按规格分组,才解决。

避坑指南:Slurm默认不检查GPU健康状态。我曾经遇到过一块卡已经报Xid错误了,Slurm还继续往上面调度任务。解决方案是写个Proctrack插件,每次分配前跑一次nvidia-smi。

1.3.3 YARN + GPU

YARN是Hadoop生态的调度器。它本来是为MapReduce设计的,后来扩展支持了GPU。但说实话,YARN的GPU调度比较“粗糙”。

YARN通过yarn.nodemanager.resource.gpu配置GPU数量。但它不感知GPU的拓扑结构,也不支持MIG。你申请2块卡,它可能给你分配两块跨NUMA的卡,通信效率很低。

# 在container中申请GPU
<property>
  <name>yarn.nodemanager.resource.gpu</name>
  <value>4</value>
</property>

我个人觉得,YARN适合“顺便用用GPU”的场景。比如Spark MLlib里有些算法可以跑在GPU上,但又不是强依赖。如果你主要做深度学习,还是别用YARN了。

1.4 三种框架对比

特性 Kubernetes Slurm YARN
设计目标 微服务、容器化 HPC、批处理 大数据、MapReduce
GPU共享 需插件(GPU Share/MIG) 原生支持(--gres) 不支持
拓扑感知 弱(需手动配置) 强(支持NUMA绑定) 无
抢占式调度 支持(通过PriorityClass) 原生支持 支持(通过FairScheduler)
我的推荐场景 在线推理、小规模训练 大规模训练、HPC 大数据+GPU混合负载

总结一下:选哪个框架,取决于你的业务。我见过有人用K8s跑千卡训练,结果调度延迟高得离谱。也见过有人用Slurm跑推理服务,结果弹性伸缩跟不上。没有银弹,只有最适合的。

1.5 本章知识体系图

下面这张图是我自己画的,把GPU调度的核心逻辑串起来了。你仔细看看,应该能理解调度器在整个系统中的位置。

GPU调度核心知识体系 用户提交任务 调度器(Scheduler) 资源抽象 | 任务排队 | 拓扑感知 | 故障处理 Kubernetes Slurm YARN GPU物理资源 SM(流多处理器) | VRAM(显存) | NVLink/NVSwitch MIG(多实例GPU) | 时间片共享 | 显存隔离 图:GPU调度从用户任务到物理资源的完整链路

这张图从左到右看:用户提交任务 → 调度器决策 → 选择框架 → 分配GPU资源。每个环节都有坑,后面章节我会一个个拆解。

最后说一句:GPU调度不是“装个调度器就完事”的。你得理解GPU的工作原理,知道调度器在做什么,才能把集群用好。我见过太多人,装了个K8s就以为万事大吉,结果性能一塌糊涂。嗯,慢慢来,后面几章我会带你手把手排查。