作业调度系统——Kubernetes 和 Slurm / Job Scheduling with Kubernetes and Slurm
📅 创建时间:2026-06-03 🏷️ 标签:#AI-Infra #集群侧 #调度 #Kubernetes #Slurm #作业管理 📚 前置知识:[[01-gpu-cluster-hardware]](GPU 集群硬件架构)[[03-gpu-virtualization]](GPU 虚拟化) 📚 相关知识:[[05-multi-tenant-management]](多作业与多租户)[[07-nccl-cluster-networking]](NCCL 集群组网)
场景:当 1000 个训练请求撞上一个 512 卡集群
┌─────────────────────────────────────────────────────────────┐
│ │
│ 集群状态:512 张 H100,当前运行 1 个作业(64 卡) │
│ 调度器队列(按提交时间排序): │
│ │
│ 作业 #1:微调 7B 模型,申请 8 卡,预计 2 小时 │
│ 作业 #2:预训练 70B 模型,申请 512 卡,预计 5 天 │
│ 作业 #3:跑推理服务,申请 4 卡,长期运行 │
│ 作业 #4:实验性训练,申请 128 卡,预计 3 天 │
│ 作业 #5:预训练 405B 模型,申请 512 卡,预计 15 天 │
│ │
│ 调度器面临的问题: │
│ → 448 张卡空闲,应该分配给谁? │
│ → 作业 #2 和 #5 都要 512 卡,但总共只有 512 张 │
│ → 作业 #3 长期运行,要一直占着 4 卡吗? │
│ → 作业 #1 只需要 8 卡,等 512 卡作业释放太慢 │
│ │
│ 调度器必须做决策:先来先服务,还是效率最优? │
│ 这个决策由作业调度系统(Slurm / Kubernetes)来完成。 │
│ │
└─────────────────────────────────────────────────────────────┘第1节:两种调度范式
Slurm vs Kubernetes 的哲学差异
┌─────────────────────────────────────────────────────────────┐
│ Slurm 与 Kubernetes 的核心定位 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Slurm(HPC 传统路线): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 定位:HPC(高性能计算)专用调度器 │ │
│ │ 设计理念:一个任务 = 一组计算资源,任务结束即释放 │ │
│ │ 典型用户:超算中心、科研机构、大模型训练 │ │
│ │ 核心优势:专为大规模 GPU 作业设计,支持 Gang Scheduling│ │
│ │ 劣势:API 相对简单,生态不如 K8s 丰富 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Kubernetes(云原生路线): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 定位:通用容器编排平台,GPU 调度是扩展能力 │ │
│ │ 设计理念:Pod 是最小调度单位,服务长期运行 │ │
│ │ 典型用户:互联网公司、推理服务、混合云环境 │ │
│ │ 核心优势:生态丰富(存储/网络/安全),支持滚动更新 │ │
│ │ 劣势:原生 GPU 调度能力弱,需要扩展插件 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 实际选择建议: │
│ → 专注大模型预训练 + 科研场景:Slurm │
│ → 推理服务 + 微服务 + 多租户:Kubernetes │
│ → 大型企业往往两者并存:Slurm 管训练,K8s 管推理服务 │
│ │
└─────────────────────────────────────────────────────────────┘第2节:Slurm 详解
Slurm 架构
┌─────────────────────────────────────────────────────────────┐
│ Slurm 架构图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ slurmctld │ ← 中央控制器(主节点) │
│ │ (控制平面) │ 调度决策、作业管理、资源分配 │
│ └──────┬──────┘ │
│ │ │
│ ┌──────┴──────────────────────────┐ │
│ │ slurmdbd(可选) │ 作业历史、计费 │
│ │ MySQL/PostgreSQL │ 账户管理 │
│ └─────────────────────────────────┘ │
│ │ │
│ │ (控制器下发指令) │
│ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ slurmd │ │ slurmd │ │ slurmd │ ... │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │ GPU×8 │ │ │ │ GPU×8 │ │ │ │ GPU×8 │ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ 用户交互: │
│ → squeue:查看作业状态 │
│ → srun / sbatch:提交作业 │
│ → salloc:分配资源(交互式) │
│ │
└─────────────────────────────────────────────────────────────┘Slurm 核心命令
bash
# 1. 交互式资源分配(调试用)
salloc --nodes=2 --gres=gpu:a100:4 --time=1:00:00 --partition=debug
# 2. 提交批处理作业(最常用)
sbatch << 'EOF'
#!/bin/bash
#SBATCH --job-name=llama-training
#SBATCH --nodes=16 # 使用 16 个节点
#SBATCH --gres=gpu:h100:8 # 每节点 8 张 H100
#SBATCH --time=7-00:00:00 # 最多运行 7 天
#SBATCH --partition=train # 提交到 train 队列
#SBATCH --output=train_%j.out
#SBATCH --error=train_%j.err
echo "Job started on $(hostname)"
srun python train.py --config=config_70b.yaml
EOF
# 3. 查看作业队列
squeue --user=$USER
# 4. 查看集群资源状态
sinfo -N -l
# 5. 取消作业
scancel <job_id>
# 6. 查看作业详情
scontrol show job <job_id>Slurm 作业调度策略
┌─────────────────────────────────────────────────────────────┐
│ Slurm 调度策略详解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 调度模式(FIFO / Fair Share / Backfill): │
│ │
│ 1. FIFO(默认): │
│ → 按提交顺序调度,资源够就运行,否则等待 │
│ → 问题:大作业会饿死一堆小作业 │
│ │
│ 2. Fair Share(多租户推荐): │
│ → 每个用户的"信用"由已用资源和配额决定 │
│ → 用得少的用户优先级更高 │
│ → 防止某个用户长期独占集群 │
│ │
│ 3. Backfill(提升利用率): │
│ → 当大作业在等待时,调度小的空隙作业填进去 │
│ → 不影响大作业启动时间的前提下提升利用率 │
│ │
│ Gang Scheduling(所有 GPU 同时分配): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 场景:作业 A 要 512 卡,必须同时分配 │ │
│ │ 问题:没有 512 卡连续空闲块 │ │
│ │ 解法:Gang Scheduling │ │
│ │ → 等待,直到有 512 卡同时可用时一次性分配 │ │
│ │ → 分配后,所有 512 卡要么全跑,要么全不跑 │ │
│ │ 代价:可能增加作业等待时间(等待资源凑齐) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Slurm 队列(Partition)配置
bash
# slurm.conf 中定义队列
NodeName=node[001-064] NodeAddr=10.0.0.[1-64] RealMemory=1024000 Gres=gpu:h100:8 State=UNKNOWN
PartitionName=train Nodes=node[001-064] Default=NO MaxTime=7-00:00:00 QOS=train Priority=10
PartitionName=debug Nodes=node[001-008] Default=YES MaxTime=01:00:00 QOS=debug Priority=100
PartitionName=inference Nodes=node[001-064] MaxTime=UNLIMITED QOS=inference Priority=5┌─────────────────────────────────────────────────────────────┐
│ 队列配置示例及调度行为 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 队列名称 │ 节点数 │ 最大时长 │ 优先级 │ 用途 │
│ ├─────────────┼─────────┼────────────┼─────────┼──────────┤
│ │ debug │ 8 节点 │ 1 小时 │ 100 │ 调试验证 │
│ │ train │ 64 节点│ 7 天 │ 10 │ 正式训练 │
│ │ inference │ 64 节点│ 无限 │ 5 │ 推理服务 │
│ │ long │ 64 节点│ 30 天 │ 1 │ 超长作业 │
│ │
│ 调度顺序:debug > train > inference > long │
│ debug 优先级最高(适合快速迭代),但节点少 │
│ │
└─────────────────────────────────────────────────────────────┘第3节:Kubernetes 详解
Kubernetes GPU 调度架构
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes GPU 调度架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ kube-apiserver │ API 入口,接收 Pod 创建请求 │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ kube-scheduler │ 调度器,决定 Pod 放在哪个节点 │
│ │ ┌─────────────────────────────┐ │ 调度决策: │
│ │ │ Device Plugin 机制 │ │ → K8s 原生调度器 │
│ │ │ 感知 GPU 资源 │ │ 不理解 GPU │
│ │ │ 发现 GPU、报告 GPU 健康 │ │ → 依赖 Device │
│ │ │ 管理 GPU 分配 │ │ Plugin 提供 │
│ │ └─────────────────────────────┘ │ 的 GPU 信息 │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ kubelet │ │ kubelet │ │ kubelet │ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │GPU×8 │ │ │ │GPU×8 │ │ │ │GPU×8 │ │ │
│ │ │+DCGM │ │ │ │+DCGM │ │ │ │+DCGM │ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DCGM(Data Center GPU Manager): │ │
│ │ → NVIDIA 提供的监控 DaemonSet │ │
│ │ → 导出 GPU 利用率、显存、温度等指标到 Prometheus │ │
│ │ → 支持 GPU 健康检查(ECC、NVLink 状态) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Kubernetes Device Plugin 注册机制
yaml
# GPU 节点上部署 NVIDIA Device Plugin(DaemonSet)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-device-plugin-daemonset
namespace: gpu-operator
spec:
selector:
matchLabels:
name: nvidia-device-plugin
template:
metadata:
labels:
name: nvidia-device-plugin
spec:
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- image: nvcr.io/nvidia/k8s-device-plugin:v0.14.0
name: nvidia-device-plugin
args:
- "--config-file=/etc/kubernetes/device-plugin-config/config.yaml"
resources:
limits:
nvidia.com/gpu: "8" # 向 K8s 声明每节点 8 GPU
env:
- name: PASS_DEVICE_PLUGIN_STRATEGY
value: "true"yaml
# 用户提交 GPU 作业 Pod
apiVersion: v1
kind: Pod
metadata:
name: training-pod
spec:
containers:
- name: trainer
image: pytorch/pytorch:2.1.0
command: ["python", "train.py"]
resources:
limits:
nvidia.com/gpu: "8" # 申请 8 张 GPU
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"Kubernetes 批量调度增强:Volcano
┌─────────────────────────────────────────────────────────────┐
│ Volcano:Kubernetes 上的批量作业调度器 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 原生 K8s 调度器的问题: │
│ → 一次只调度一个 Pod,没有全局最优概念 │
│ → 不支持 Gang Scheduling(所有 Pod 必须同时分配) │
│ → 不支持作业优先级抢占 │
│ │
│ Volcano 解决这些问题: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Gang Scheduling │ │
│ │ → 作业的所有 Pod 要么全部调度成功,要么都不跑 │ │
│ │ → 对应 Slurm 的 --share 行为 │ │
│ │ │ │
│ │ 2. 作业优先级与抢占 │ │
│ │ → 高优先级作业可以抢占低优先级 Pod │ │
│ │ │ │
│ │ 3. 作业亲和性调度 │ │
│ │ → 将关联作业调度到相近节点(减少网络通信) │ │
│ │ │ │
│ │ 4. 资源公平与队列 │ │
│ │ → 多租户资源配额,支持分层队列 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Volcano Job 示例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ apiVersion: batch.volcano.sh/v1alpha1 │ │
│ │ kind: Job │ │
│ │ spec: │ │
│ │ minAvailable: 64 # 至少 64 个 Pod 才能跑 │ │
│ │ tasks: │ │
│ │ - replicas: 64 │ │
│ │ name: worker │ │
│ │ template: │ │
│ │ spec: │ │
│ │ containers: │ │
│ │ - name: trainer │ │
│ │ image: pytorch/pytorch:2.1.0 │ │
│ │ resources: │ │
│ │ limits: │ │
│ │ nvidia.com/gpu: "1" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第4节:Slurm 与 Kubernetes 的深度对比
核心维度对比
┌─────────────────────────────────────────────────────────────┐
│ Slurm vs Kubernetes 深度对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 维度 │ Slurm │ Kubernetes │
│ ├───────────────────┼──────────────────┼──────────────────┤
│ │ 设计哲学 │ HPC 作业调度 │ 服务编排 │
│ │ 最小调度单位 │ 作业(Job) │ Pod │
│ │ 生命周期管理 │ 作业级(批处理)│ Pod 级(长期) │
│ │ GPU 感知能力 │ 原生(gres=gpu)│ 依赖 Device Plugin│
│ │ Gang Scheduling │ 原生支持 │ 需要 Volcano │
│ │ 资源预留 │ 支持 srun/salloc│ 不直接支持 │
│ │ Fair Share │ 原生支持 sacct │ 需要扩展 │
│ │ Checkpoint 集成 │ 原生(suspend) │ 无 │
│ │ 生态丰富度 │ 中等(HPC 专用)│ 极丰富 │
│ │ 学习曲线 │ 较陡(HPC 黑话)│ 较陡(概念多) │
│ │ 典型场景 │ 大模型预训练 │ 推理/微服务 │
│ │ 多租户 │ 账户/队列体系 │ Namespace+RBAC │
│ │ 作业抢占 │ 原生 │ 需要 Volcano │
│ │
└─────────────────────────────────────────────────────────────┘训练作业提交对比
┌─────────────────────────────────────────────────────────────┐
│ 同一训练作业的两种提交方式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Slurm 提交(batch 脚本): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ #!/bin/bash │ │
│ │ #SBATCH --job-name=llama70b │ │
│ │ #SBATCH --nodes=8 │ │
│ │ #SBATCH --gres=gpu:h100:8 │ │
│ │ #SBATCH --time=7-00:00:00 │ │
│ │ #SBATCH --partition=train │ │
│ │ │ │
│ │ echo "Started at $(date)" │ │
│ │ srun python /workspace/train.py \ │ │
│ │ --model_size=70B \ │ │
│ │ --num_gpus=$SLURM_NTASKS │ │
│ │ echo "Finished at $(date)" │ │
│ └─────────────────────────────────────────────────────┘ │
│ $ sbatch train_70b.sh │
│ → 返回作业 ID(job_id: 12345) │
│ │
│ Kubernetes 提交(YAML + kubectl): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ $ kubectl create -f training-job.yaml │ │
│ │ job.batch/llama70b created │ │
│ │ $ kubectl get pods -l job-name=llama70b │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 环境变量对比: │
│ Slurm: $SLURM_NTASKS, $SLURM_NODEID, $SLURM_JOBID │
│ K8s: $HOSTNAME(节点名,需要自己拼接) │
│ │
└─────────────────────────────────────────────────────────────┘第5节:调度系统的工程实践
调度决策的关键参数
┌─────────────────────────────────────────────────────────────┐
│ GPU 调度器必须考虑的关键参数 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 资源粒度(Resource Granularity): │
│ → 最小分配单位:单卡 / 单节点 / 自定义资源组? │
│ → Slurm: gres=gpu:h100:1(单卡级别) │
│ → K8s: nvidia.com/gpu=1(单卡级别) │
│ → 问题:申请 8 卡,实际用了 5 卡 → 3 卡浪费 │
│ │
│ 2. 拓扑感知(Topology Awareness): │
│ → TP=8 需要同节点 8 卡(NVSwitch) │
│ → 调度器必须知道哪些卡在同一节点 │
│ → Slurm: TRESBillingWeights=GPU:1.0:NVLINK │
│ → K8s: 需要 node.kubernetes.io/gpu-topology 注解 │
│ │
│ 3. 作业规模与等待时间: │
│ → 大作业(512+ 卡)等待时间可能很长 │
│ → Backfill 算法允许小作业先跑(不耽误大作业启动) │
│ → 作业预估时间不准 → 调度策略失效 │
│ │
│ 4. 抢占策略(Preemption): │
│ → 紧急大作业能否抢占正在跑的小作业? │
│ → Slurm: scontrol update job=xxx priority=9999 │
│ → K8s: kubectl cordon node + drain(需外部触发) │
│ → 抢占有代价:被抢作业需要 Checkpoint 后重启 │
│ │
└─────────────────────────────────────────────────────────────┘调度系统的高可用
┌─────────────────────────────────────────────────────────────┐
│ Slurm 高可用配置示例 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 双主控(Active/Passive): │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ slurmctld │◄───────►│ slurmctld │ │
│ │ (Primary) │ │ (Secondary) │ │
│ │ 10.0.0.1 │ │ 10.0.0.2 │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ └──────────┬─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ slurmd on every node │ 它们连接任意一个 │
│ └─────────────────────┘ slurmctld 即可 │
│ │
│ 故障切换:Primary 挂了 → Secondary 自动接管 │
│ 作业状态保存在 slurmstepd(每个作业一个进程) │
│ → slurmctld 挂了不影响正在运行的作业 │
│ │
└─────────────────────────────────────────────────────────────┘升华:调度是集群的"大脑"
┌─────────────────────────────────────────────────────────────┐
│ 调度系统的工程哲学 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 调度没有最优解,只有适合的 trade-off: │
│ → FIFO 最公平但利用率低 │
│ → Fair Share 公平但复杂 │
│ → Gang Scheduling 适合大作业但小作业饿死 │
│ → 每个策略都偏向某一类作业 │
│ │
│ 2. 调度器的"拓扑盲"是最常见的性能杀手: │
│ → 不理解 NVLink 的调度器会把 TP=8 分配到不同节点 │
│ → 结果:性能从 90% MFU 降到 40% │
│ │
│ 3. 调度策略必须和业务目标对齐: │
│ → 科研机构:公平优先(小实验室不能饿死) │
│ → 企业:效率优先(按时交付最重要) │
│ → 超算中心:吞吐量优先(完成更多作业) │
│ │
│ 一句话总结: │
│ 调度器决定了集群资源的分配方式, │
│ 它比任何单个作业更影响集群的整体效率。 │
│ 配置错误的调度器可以让 50% 的集群资源长期闲置。 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ Slurm 的 slurm.conf 完整配置参数
✅ Kubernetes Device Plugin 的具体实现方式
✅ Volcano 的全部调度策略参数
✅ scontrol / sinfo / squeue 的每个子命令
必须理解:
🔴 Slurm 和 Kubernetes 的定位差异(HPC 作业 vs 云原生服务)
🔴 Gang Scheduling 是什么、为什么大模型训练需要它
🔴 Slurm 的三种调度策略(FIFO / Fair Share / Backfill)
🔴 Kubernetes Device Plugin 机制:调度器如何"看见"GPU
🔴 为什么 TP=8 需要拓扑感知调度(不能随机分配节点)
🔴 调度策略和业务目标的对应关系(科研 vs 企业 vs 超算)学习状态:🟡 开始学习