13. 【Slurm】GPU集群调度:基础架构与GRES
当我们把多个 GPU 组成一个共享集群,应该如何进行资源的调度?
Part 1: 为什么 GPU 调度是独特的挑战?
1.1 GPU vs CPU:根本性差异
传统 CPU 调度的假设在 GPU 上完全失效:
CPU调度假设:
✓ 资源可细粒度分割 (1个核心给Job A,1个核心给Job B)
✓ 时间片轮转可行 (每个进程运行10ms然后切换)
✓ 资源同质化 (所有CPU核心基本一样)
✓ 延迟不敏感 (上下文切换微秒级)
GPU调度现实:
✗ 不可细粒度分割 (1块GPU = 24GB显存,不可分)
✗ 时间片切换代价巨大 (模型加载/卸载需要秒级)
✗ 高度异构 (A100 vs V100 vs H100性能差异巨大)
✗ 延迟极度敏感 (训练任务可能运行数天)
1.2 深度学习工作负载的特殊性
特征 1:不可预测的运行时间
# 同一个训练脚本,运行时间可能相差10倍
# 取决于:数据量、模型大小、超参数、收敛速度...
# 实际案例 (来自Venus GPU集群6个月的数据)
成功完成: 52.2%
被取消: 27.9%
失败: 19.9%
# 结果:传统的"预测作业时间"调度算法失效
特征 2:资源需求的两极分化
单GPU任务: 52.5% (大量小实验)
8 GPU任务: 22.6% (标准分布式训练)
>8 GPU任务: 10.3% (大模型训练)
问题:如何在满足大任务的同时,不饿死小任务?
特征 3:显存是硬约束
# CPU: 内存不足 → Swap到磁盘 (慢但能跑)
# GPU: 显存不足 → CUDA Out of Memory (直接崩溃)
model = ResNet50() # 需要 ~2GB
batch_size = 128 # 需要 ~18GB
total = 20GB # 只有24GB GPU无法再跑其他任务
# 显存成为不可压缩的硬性资源
特征 4:通信拓扑至关重要
场景:8-GPU分布式训练
拓扑1 (同节点,NVLink):
GPU0 ←─(600GB/s)─→ GPU1
训练速度: 1000 images/sec
拓扑2 (跨节点,PCIe + 网络):
Node1:GPU0 ←─(100Gb/s网络)─→ Node2:GPU1
训练速度: 200 images/sec
性能差异:5倍!
1.3 多租户环境的核心挑战
挑战矩阵:
┌─────────────┬──────────────────┬──────────────────┐
│ 维度 │ CPU集群 │ GPU集群 │
├─────────────┼──────────────────┼──────────────────┤
│ 资源价值 │ $1-5/核心/月 │ $500-3000/卡/月 │
│ 用户期望 │ 批处理,能等 │ 交互式,快速反馈 │
│ 公平性定义 │ CPU-Hours │ GPU-Hours? GPU-$? │
│ 资源竞争 │ 中等 │ 极度激烈 │
│ 队列等待 │ 可接受 │ 极度不满 │
└─────────────┴──────────────────┴──────────────────┘
真实场景:
场景:10个研究员共享8块A100 (每块价值$15,000)
用户A: "我的实验只需要1个GPU跑15分钟,为什么要等2小时?"
用户B: "我的大模型需要8个GPU训练3天,凭什么被小任务打断?"
用户C: "我上周用了很少GPU,这周应该有更高优先级吧?"
管理员: "如何让所有人都觉得公平?"
→ 这就是多租户调度的核心矛盾
Part 2: Slurm 架构深度剖析
2.1 为什么选择 Slurm 而不是 Kubernetes?
决策矩阵:
┌──────────────┬─────────────────┬──────────────────┐
│ 需求 │ Kubernetes │ Slurm │
├──────────────┼─────────────────┼──────────────────┤
│ 批处理作业 │ ⚠️ 需要额外组件 │ ✅ 原生支持 │
│ 公平性调度 │ ⚠️ 简单优先级 │ ✅ 复杂FairShare │
│ GPU拓扑感知 │ ❌ 不支持 │ ✅ 原生支持 │
│ 作业排队 │ ⚠️ 需要额外组件 │ ✅ 原生支持 │
│ 账户系统 │ ❌ 无 │ ✅ 完整账户树 │
│ 历史追溯 │ ❌ 弱 │ ✅ 详细记账 │
│ MPI深度集成 │ ⚠️ 部分支持 │ ✅ 完美集成 │
│ 微服务编排 │ ✅ 最佳选择 │ ❌ 不适合 │
└──────────────┴─────────────────┴──────────────────┘
结论:深度学习训练 = 批处理 + 长时运行 → Slurm更合适
模型推理服务 = 微服务 + 动态伸缩 → Kubernetes更合适
2.2 Slurm 核心架构详解
2.2.1 三层架构
┌────────────────────────────────────────────────────┐
│ Control Layer (slurmctld) │
│ ┌──────────────────────────────────────────────┐ │
│ │ Scheduling Engine │ │
│ │ - Priority Calculation │ │
│ │ - Backfill Algorithm │ │
│ │ - Resource Matching │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ State Manager │ │
│ │ - Node State (IDLE/ALLOCATED/DOWN/DRAIN) │ │
│ │ - Job State (PENDING/RUNNING/COMPLETED) │ │
│ │ - GRES State (GPU分配状态) │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Accounting Interface (slurmdbd) │ │
│ │ - 历史作业记录 │ │
│ │ - FairShare计算 │ │
│ │ - 资源使用统计 │ │
│ └──────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
↕️ RPC
┌────────────────────────────────────────────────────┐
│ Execution Layer (slurmd) │
│ │
│ Node 1 Node 2 Node 3 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ slurmd │ │ slurmd │ │ slurmd │ │
│ ├─────────┤ ├─────────┤ ├─────────┤ │
│ │ cgroups │ │ cgroups │ │ cgroups │ 资源隔离 │
│ │ CPU │ │ CPU │ │ CPU │ │
│ │ Memory │ │ Memory │ │ Memory │ │
│ │ GPU │ │ GPU │ │ GPU │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└────────────────────────────────────────────────────┘
↕️
┌────────────────────────────────────────────────────┐
│ Client Layer │
│ sbatch srun salloc scancel squeue sacct │
└────────────────────────────────────────────────────┘
2.2.2 关键机制:调度循环
# 伪代码:Slurm调度循环 (每个调度周期)
def scheduling_cycle():
# 第1步:计算所有Pending作业的优先级
for job in pending_jobs:
job.priority = calculate_priority(
fairshare_factor=get_fairshare(job.user),
age_factor=job.queue_time / MAX_AGE,
size_factor=job.requested_nodes / TOTAL_NODES,
qos_factor=job.qos.weight,
partition_factor=job.partition.priority
)
# 第2步:按优先级排序
pending_jobs.sort(key=lambda j: j.priority, reverse=True)
# 第3步:主调度 (按优先级顺序尝试)
for job in pending_jobs:
nodes = find_available_nodes(job.requirements)
if nodes:
allocate_job(job, nodes)
if job.partition.preempt_mode == "SUSPEND":
# 如果需要,可以抢占低优先级作业
preempt_low_priority_jobs(job)
# 第4步:回填调度 (Backfill)
# 如果高优先级作业在等待,小作业可以"插队"
# 只要不延迟高优先级作业的预期开始时间
for small_job in pending_jobs[high_priority_count:]:
if can_backfill(small_job, high_priority_jobs):
nodes = find_available_nodes(small_job.requirements)
if nodes:
allocate_job(small_job, nodes)
# 第5步:状态同步
sync_state_to_slurmdbd()
# 调度周期:默认每秒执行一次
关键设计点:
1. 双队列策略
- 主队列:严格优先级
- 回填队列:机会主义调度
2. 预计算
- 预先计算高优先级作业的"预期开始时间"
- 回填时不能超过这个时间
3. 状态分离
- slurmctld:内存中的快速状态
- slurmdbd:持久化的历史状态
Part 3: GRES - 深度剖析 GPU 资源调度
3.1 GRES 架构设计
GRES = Generic Resource Scheduling (通用资源调度)
为什么需要GRES?
CPU/Memory:Slurm原生支持
GPU:需要额外抽象层
设计目标:
1. 通用性:不仅支持GPU,还支持MIC、FPGA等
2. 细粒度:跟踪每块GPU的状态
3. 拓扑感知:知道GPU之间的连接关系
4. 类型化:区分A100、V100、H100
3.1.1 GRES 配置层次
# 层次1: slurm.conf (全局配置)
GresTypes=gpu,mps,mig
# 层次2: 节点级配置
NodeName=gpu-node[01-08] \
CPUs=128 \
Sockets=2 \
CoresPerSocket=64 \
Gres=gpu:a100:8 # 8块A100
# 层次3: gres.conf (详细配置)
# 每块GPU的详细信息
NodeName=gpu-node01
Name=gpu Type=a100 File=/dev/nvidia0 Cores=0-15
Name=gpu Type=a100 File=/dev/nvidia1 Cores=16-31
Name=gpu Type=a100 File=/dev/nvidia2 Cores=32-47
Name=gpu Type=a100 File=/dev/nvidia3 Cores=48-63
Name=gpu Type=a100 File=/dev/nvidia4 Cores=64-79
Name=gpu Type=a100 File=/dev/nvidia5 Cores=80-95
Name=gpu Type=a100 File=/dev/nvidia6 Cores=96-111
Name=gpu Type=a100 File=/dev/nvidia7 Cores=112-127
# Cores字段:该GPU亲和的CPU核心
# 为什么重要?NUMA架构下,CPU-GPU亲和性影响性能!
3.1.2 CPU-GPU 拓扑感知
典型双路服务器拓扑:
┌──────────────────────────────────────────────────┐
│ Node (服务器) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Socket 0 │ │ Socket 1 │ │
│ │ (CPU 0-63) │ │ (CPU 64-127) │ │
│ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│ │ │ NVLink Hub │ │ │ │ NVLink Hub │ │ │
│ │ └────────────┘ │ │ └────────────┘ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ GPU0-3 │ │ GPU4-7 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ └──────────PCIe Switch─────┘ │
└──────────────────────────────────────────────────┘
性能差异:
1. GPU0访问Socket0的内存:200GB/s (本地NUMA)
2. GPU0访问Socket1的内存:100GB/s (跨NUMA)
3. GPU0与GPU1通信 (同Socket):600GB/s (NVLink)
4. GPU0与GPU4通信 (跨Socket):100GB/s (PCIe)
→ 拓扑感知调度可提升性能2-6倍!
Slurm 如何实现拓扑感知?
# 配置选项
SelectType=select/cons_tres
SelectTypeParameters=CR_Core_Memory
# 启用拓扑感知
TopologyPlugin=topology/tree
# 定义拓扑
SwitchName=s0 Nodes=gpu-node[01-04] # 同一机架
SwitchName=s1 Nodes=gpu-node[05-08]
SwitchName=root Switches=s0,s1 # 根交换机
3.2 GPU 分配策略详解
3.2.1 分配选项对比
# 选项1:简单计数 (不推荐用于深度学习)
#SBATCH --gres=gpu:2
# Slurm只保证2个GPU,但可能是:
# - GPU0 + GPU7 (跨Socket,性能差)
# - GPU0 + GPU1 (同Socket,性能好)
# 选项2:指定类型
#SBATCH --gres=gpu:a100:2
# 保证是A100,但仍不能保证拓扑
# 选项3:精确控制 (推荐)
#SBATCH --gpus-per-node=8 # 需要8个GPU
#SBATCH --gpu-bind=closest # 绑定到最近的CPU
#SBATCH --ntasks-per-node=8 # 8个任务
#SBATCH --cpus-per-task=16 # 每任务16核
3.2.2 GPU Binding 详解
# --gpu-bind选项深度解析
# 1. closest: 每个任务绑定到最近的GPU
# 适用场景:数据并行训练
srun --gpu-bind=closest python train.py
# 结果:
# Task 0 → GPU 0 (亲和CPU 0-15)
# Task 1 → GPU 1 (亲和CPU 16-31)
# ...
# 2. single:N: 每个任务独占N个GPU
# 适用场景:模型并行
srun --gpu-bind=single:2 python train.py
# 结果:
# Task 0 → GPU 0,1
# Task 1 → GPU 2,3
# 3. mask_gpu: 手动指定每个任务的GPU掩码
# 适用场景:复杂的混合并行
srun --gpu-bind=mask_gpu:0x3,0xC python train.py
# 0x3 = 二进制 0011 = GPU 0,1
# 0xC = 二进制 1100 = GPU 2,3
# 4. none: 不绑定,让应用自己选择
# 适用场景:需要动态GPU选择
srun --gpu-bind=none python train.py
3.2.3 环境变量机制
# Slurm如何告诉应用程序使用哪些GPU?
# 关键环境变量:CUDA_VISIBLE_DEVICES
# 当作业分配到GPU后,Slurm自动设置:
JobID=12345, Node=gpu-node01
分配GPU: 0, 2, 5
# Slurm在执行脚本前设置:
export CUDA_VISIBLE_DEVICES=0,2,5
# PyTorch/TensorFlow会读取这个变量:
import torch
print(torch.cuda.device_count()) # 输出: 3
# 应用看到的是"3个GPU",映射到物理GPU 0,2,5
底层实现:cgroups
# Slurm通过Linux cgroups限制GPU访问
# /sys/fs/cgroup/devices/slurm/uid_1000/job_12345/
# devices.allow 文件内容:
c 195:0 rwm # GPU 0 (主设备号195,次设备号0)
c 195:2 rwm # GPU 2
c 195:5 rwm # GPU 5
# 如果进程尝试访问GPU 1:
# → Permission denied (被cgroups阻止)
3.3 高级特性:MPS 和 MIG
3.3.1 MPS (Multi-Process Service)
问题:单个GPU通常被一个任务独占,即使只用了20%
MPS解决方案:在GPU上运行多个CUDA进程,共享GPU
┌────────────────────────────────────┐
│ GPU (A100) │
│ ┌──────────┐ ┌──────────┐ │
│ │ Job 1 │ │ Job 2 │ │
│ │ (20% GPU)│ │ (30% GPU)│ │
│ └──────────┘ └──────────┘ │
│ MPS Daemon │
└────────────────────────────────────┘
Slurm MPS 配置:
# gres.conf
NodeName=gpu-node01
Name=gpu Type=a100 File=/dev/nvidia0
Name=mps Count=100 File=/dev/nvidia0 # 100%的MPS资源
# 用户请求MPS
#SBATCH --gres=mps:20 # 请求20%的GPU
# 限制:
# 1. 只能在同一GPU上共享
# 2. 显存仍然是硬限制
# 3. 调试困难(多个进程共享)
3.3.2 MIG (Multi-Instance GPU)
MIG:将单个物理GPU分割成多个完全隔离的GPU实例
A100支持的分割模式:
┌─────────────────────────────────────┐
│ A100 (40GB显存, 108个SM) │
├─────────────────────────────────────┤
│ 模式1: 7个实例 (7x 1/7, 5GB, 14 SM) │
│ 模式2: 3个实例 (3x 1/3, 10GB, 28 SM) │
│ 模式3: 1个实例 (1x 1/1, 40GB, 108 SM)│
└─────────────────────────────────────┘
优势:
✅ 硬件级隔离(不像MPS是软件隔离)
✅ 独立的显存空间
✅ 独立的错误隔离
劣势:
❌ 只有A100/H100支持
❌ 需要重启GPU才能改变分割
Slurm MIG 配置:
# 1. 在节点上创建MIG实例
nvidia-smi mig -cgi 1g.5gb,1g.5gb,1g.5gb -C
# 2. gres.conf配置
NodeName=gpu-node01
Name=gpu Type=a100 File=/dev/nvidia0
Name=mig Type=1g.5gb File=/dev/nvidia0 \
MultipleFiles=/dev/nvidia-caps/nvidia-cap1
Name=mig Type=1g.5gb File=/dev/nvidia0 \
MultipleFiles=/dev/nvidia-caps/nvidia-cap2
# 3. 用户请求MIG实例
#SBATCH --gres=mig:1g.5gb:1
Part 4: 多租户公平性 - FairShare 算法深度解析
4.1 公平性的定义
问题:什么叫"公平"?
场景1:简单平均
User A: 使用0个GPU
User B: 使用8个GPU
→ 平均每人4个?但User A根本没任务!
场景2:历史补偿
User A: 上周用了0个GPU,这周应该优先
User B: 上周用了100 GPU-Hours,这周应该靠后
→ 但如果User A一直不用,优先级无限高?
场景3:按需分配
User A: 提交了50个小任务(每个1 GPU)
User B: 提交了1个大任务(8 GPU)
→ 谁优先?按任务数还是按GPU数?
Slurm 的答案:Fair Tree FairShare 算法
核心理念:
1. 账户树结构 (Account Hierarchy)
2. 份额分配 (Share Distribution)
3. 历史衰减 (Usage Decay)
4. 相对公平 (Relative Fairness)
4.2 账户树结构详解
root (100 shares)
|
┌─────────────┼─────────────┐
│ │ │
team_A team_B team_C
(50 shares) (30 shares) (20 shares)
│ │ │
┌────┴───┐ ┌────┴───┐ │
alice bob carol dave eve
(30) (20) (15) (15) (20)
解释:
- team_A获得50%的资源 (50/100)
- team_B获得30%的资源
- team_C获得20%的资源
- alice在team_A内获得60%的资源 (30/50)
- 因此alice的全局份额 = 50% * 60% = 30%
配置示例:
# 创建账户树
sacctmgr add account team_A \
Description="Team A" \
Organization=nvidia \
Parent=root \
Fairshare=50
sacctmgr add user alice \
Account=team_A \
Fairshare=30
# 查看账户树
sacctmgr show assoc tree format=account,user,fairshare
4.3 FairShare 计算详解
4.3.1 Fair Tree 算法公式
def calculate_fairshare_factor(user):
"""
FairShare Factor ∈ [0, 1]
- 1.0: 用户使用资源远少于其份额
- 0.5: 用户使用资源等于其份额
- 0.0: 用户使用资源远多于其份额
"""
# 第1步:计算用户的目标份额 (Target Share)
# 基于账户树的层级关系
target_share = calculate_target_share_from_tree(user)
# 第2步:计算实际使用量 (Actual Usage)
# 从slurmdbd查询历史使用记录
actual_usage = query_usage_from_database(
user=user,
time_window=DECAY_HALF_LIFE # 默认14天
)
# 第3步:归一化实际使用量
# 使用量需要按时间衰减
normalized_usage = actual_usage * decay_function(age)
# 第4步:计算相对份额
if normalized_usage == 0:
return 1.0 # 未使用任何资源
fairshare_factor = target_share / normalized_usage
# 第5步:限制在[0, 1]范围
return min(1.0, max(0.0, fairshare_factor))
4.3.2 时间衰减机制
# 为什么需要时间衰减?
# 如果不衰减,一个用户3个月前的使用量仍然影响今天的优先级
# 半衰期 (Half-Life) 模型
PriorityDecayHalfLife = 14-0 # 14天
# 衰减函数
def decay_factor(age_in_seconds):
"""
age = 0天 → decay = 1.0 (100%权重)
age = 14天 → decay = 0.5 (50%权重)
age = 28天 → decay = 0.25 (25%权重)
age = 无限大 → decay = 0.0 (0%权重)
"""
half_life = 14 * 24 * 3600 # 14天转秒
return 2 ** (-age_in_seconds / half_life)
# 实际使用量计算
effective_usage = sum(
job.gpu_hours * decay_factor(job.age)
for job in user.completed_jobs
)
图示:
使用量历史 (倒序排列,最近的在前)
Today 14天前
│ │
▼ ▼
100 GPU-Hours 100 GPU-Hours
(权重 1.0) (权重 0.5)
有效使用 100 有效使用 50
28天前
│
▼
100 GPU-Hours
(权重 0.25)
有效使用 25
总有效使用量 = 100 + 50 + 25 = 175 GPU-Hours
4.4 优先级综合计算
完整优先级公式:
def calculate_job_priority(job):
"""
Job Priority = 多因子加权和
"""
# 各因子权重 (在slurm.conf中配置)
WEIGHT_AGE = 1000 # 等待时间权重
WEIGHT_FAIRSHARE = 10000 # 公平性权重
WEIGHT_JOBSIZE = 0 # 作业大小权重
WEIGHT_QOS = 10000 # 服务质量权重
WEIGHT_PARTITION = 0 # 分区权重
priority = (
WEIGHT_AGE * age_factor(job) +
WEIGHT_FAIRSHARE * fairshare_factor(job.user) +
WEIGHT_JOBSIZE * jobsize_factor(job) +
WEIGHT_QOS * qos_factor(job) +
WEIGHT_PARTITION * partition_factor(job)
)
return priority
def age_factor(job):
"""等待时间因子"""
max_age = 7 * 24 * 3600 # 7天
return min(1.0, job.queue_time / max_age)
def jobsize_factor(job):
"""作业大小因子 (可选)"""
# 小作业优先 OR 大作业优先?取决于策略
if FAVOR_SMALL_JOBS:
return 1.0 / job.requested_nodes
else:
return job.requested_nodes / TOTAL_NODES
配置示例:
# slurm.conf
PriorityType=priority/multifactor
# 各因子权重
PriorityWeightAge=1000
PriorityWeightFairshare=10000
PriorityWeightJobSize=0
PriorityWeightQOS=10000
# FairShare相关配置
PriorityDecayHalfLife=14-0 # 14天半衰期
PriorityMaxAge=7-0 # 最多7天的等待时间被计入
PriorityFavorSmall=NO # 不偏好小作业
# 使用FairTree算法 (Slurm 19.05+默认)
PriorityFlags=FAIR_TREE
4.5 实战案例:优先级计算
# 场景:3个用户竞争GPU资源
# 用户配置
users = {
'alice': {'fairshare': 0.33, 'target_share': 0.33},
'bob': {'fairshare': 0.33, 'target_share': 0.33},
'carol': {'fairshare': 0.34, 'target_share': 0.34}
}
# 历史使用 (过去14天的有效使用量)
usage = {
'alice': 100 GPU-Hours, # 重度使用
'bob': 50 GPU-Hours, # 中度使用
'carol': 10 GPU-Hours # 轻度使用
}
# 计算FairShare Factor
total_usage = 160
alice_fs = (0.33 * 160) / 100 = 0.53 # 低于目标
bob_fs = (0.33 * 160) / 50 = 1.06 # 超过目标
carol_fs = (0.34 * 160) / 10 = 5.44 # 远超目标 (限制为1.0)
# 当前队列中的作业
jobs = [
{'user': 'alice', 'queue_time': 2小时, 'qos': 'normal'},
{'user': 'bob', 'queue_time': 1小时, 'qos': 'normal'},
{'user': 'carol', 'queue_time': 30分钟, 'qos': 'high'}
]
# 计算优先级 (假设权重:Age=1000, FairShare=10000, QOS=10000)
alice_priority = 1000*(2/168) + 10000*0.53 + 10000*1 = 15312
bob_priority = 1000*(1/168) + 10000*1.0 + 10000*1 = 20006
carol_priority = 1000*(0.5/168)+ 10000*1.0 + 10000*2 = 30003
# 调度顺序:carol > bob > alice
# carol虽然等待时间最短,但因为历史使用少+高QOS,优先级最高
Part 5: Backfill 调度算法详解
5.1 为什么需要 Backfill?
问题场景:
时间轴:
t=0 t=10 t=20
│ │ │
├──────────────────┼───────────────────┤
│ 大作业等待 │ 大作业运行 │
│ (需要8 GPU) │ (8 GPU, 10小时) │
├──────────────────┼───────────────────┤
队列中的小作业:
- Job A: 需要1 GPU, 运行30分钟
- Job B: 需要2 GPU, 运行1小时
传统FIFO: Job A和Job B必须等到t=20才能运行
→ 浪费:在t=0到t=10期间,有大量空闲GPU
Backfill: 如果Job A和Job B能在t=10之前完成,
就可以立即运行
→ 利用率提升,用户满意度提升
5.2 Backfill 算法实现
def backfill_scheduling(pending_jobs, current_time):
"""
Backfill算法:允许低优先级作业"插队"
前提:不延迟高优先级作业的预期开始时间
"""
# 第1步:识别高优先级作业
high_priority_job = pending_jobs[0] # 优先级最高的作业
# 第2步:预测高优先级作业何时能开始
expected_start_time = predict_start_time(high_priority_job)
# 第3步:尝试backfill低优先级作业
for job in pending_jobs[1:]: # 跳过第一个高优先级作业
# 检查是否能在expected_start_time之前完成
if can_finish_before(job, expected_start_time):
# 尝试分配资源
nodes = find_available_nodes(job)
if nodes:
allocate_job(job, nodes)
# 更新资源状态
update_available_resources(nodes, job)
def predict_start_time(job):
"""
预测作业何时能获得足够资源
"""
# 模拟未来:假设当前运行的作业按walltime完成
future_timeline = []
# 记录当前运行作业的结束时间
for running_job in get_running_jobs():
end_time = running_job.start_time + running_job.walltime
future_timeline.append((end_time, running_job.resources))
# 按时间顺序排序
future_timeline.sort()
# 模拟资源释放过程
available_resources = get_current_available_resources()
for time, resources in future_timeline:
available_resources += resources
if available_resources >= job.required_resources:
return time
# 如果一直等不到,返回无穷大
return float('inf')
def can_finish_before(job, deadline):
"""
检查作业是否能在deadline之前完成
"""
if not job.walltime:
# 如果用户没有指定walltime,保守估计
return False
earliest_start = get_current_time()
return earliest_start + job.walltime <= deadline
5.3 Backfill 的关键参数
# slurm.conf
# 调度类型
SchedulerType=sched/backfill
# Backfill相关参数
bf_max_job_test=100 # 每个周期测试多少个作业
bf_max_job_user=2 # 每个用户最多backfill多少个作业
bf_max_time=300 # backfill调度最长运行时间(秒)
bf_interval=30 # backfill周期(秒)
bf_continue # 即使找到一个backfill也继续寻找
bf_yield_interval=2000000 # 让出CPU的时间(微秒)
# 为什么需要这些限制?
# - 大集群可能有10000+个pending作业
# - 如果对每个作业都尝试backfill,计算时间太长
# - 需要在"找到最优解"和"调度延迟"之间平衡
Backfill 效果:
场景:1000节点集群,平均利用率70%
不使用Backfill:
- 平均等待时间: 2.5小时
- 集群利用率: 70%
- 小作业等待时间: 4小时
使用Backfill:
- 平均等待时间: 1.8小时 (↓28%)
- 集群利用率: 85% (↑15%)
- 小作业等待时间: 45分钟 (↓81%)
→ 显著提升用户体验和资源利用率
Part 6: 深度学习特有的调度挑战
6.1 梯度累积与 GPU 共享
问题:显存限制
# 理想情况:大batch size训练
batch_size = 1024
model.train(data_loader)
# 但GPU显存只有24GB,模型本身20GB,batch只能:
batch_size = 32 # 太小!训练慢且不稳定
# 解决方案:梯度累积 (Gradient Accumulation)
accumulation_steps = 32
for i, batch in enumerate(data_loader):
loss = model(batch)
loss = loss / accumulation_steps # 归一化
loss.backward()
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
# 等价于batch_size = 32 * 32 = 1024
# 但显存占用不变!
调度含义:
传统思路:1个GPU = 1个作业
新思路: 1个GPU = 2-3个使用梯度累积的作业
挑战:
1. 如何跟踪每个作业的实际GPU使用率?
2. 如何避免总显存超限?
3. 如何处理作业间的干扰?
Slurm 解决方案:MPS
# 允许多个作业共享GPU,但需要用户指定MPS份额
#SBATCH --gres=mps:30 # 30%的GPU
# 限制:
# - 用户必须知道自己的显存需求
# - 调度器无法动态调整
# - 作业间仍可能互相干扰
6.2 分布式训练的通信模式
6.2.1 数据并行 (Data Parallel)
# 每个GPU一个模型副本,同步梯度
# Ring-AllReduce通信模式
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ GPU 0 │───→│ GPU 1 │───→│ GPU 2 │───→│ GPU 3 │
└───────┘ └───────┘ └───────┘ └───────┘
↑ │
└────────────────────────────────────────┘
# 通信量:O(模型参数量)
# 模型越大,通信越慢
# 调度含义:
# - 需要低延迟网络 (InfiniBand > Ethernet)
# - 需要拓扑感知调度 (同机架 > 跨机架)
6.2.2 模型并行 (Model Parallel)
# 模型太大,单GPU放不下,切分到多个GPU
# Pipeline并行示例 (GPT-3类大模型)
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ GPU 0 │───→│ GPU 1 │───→│ GPU 2 │───→│ GPU 3 │
│ Layer1-8│ │Layer9-16│ │Layer17-24│ │Layer25-32│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
# 通信量:O(激活值)
# 批量越大,通信开销占比越小
# 调度含义:
# - 必须使用NVLink连接的GPU
# - 不能跨节点(延迟太高)
6.2.3 调度策略
# Slurm配置:拓扑感知
def allocate_for_distributed_training(job):
if job.training_type == "data_parallel":
# 优先同机架,其次跨机架
strategy = "compact" # 紧凑分配
elif job.training_type == "model_parallel":
# 必须同节点,且使用NVLink
strategy = "pack" # 打包到同一节点
return find_nodes(job, strategy)
# slurm.conf配置
SelectTypeParameters=CR_Core_Memory,CR_ONE_TASK_PER_CORE
# 作业提交
#SBATCH --nodes=4 # 4个节点
#SBATCH --ntasks-per-node=8 # 每节点8个任务
#SBATCH --gres=gpu:8 # 每节点8个GPU
#SBATCH --switches=1 # 尽量在1个交换机下 (拓扑)
6.3 动态资源需求
挑战:训练过程中资源需求变化
# 阶段1:数据预处理 (CPU密集)
preprocess_data() # 需要:64 CPU核心,0 GPU
# 阶段2:训练 (GPU密集)
train_model() # 需要:16 CPU核心,8 GPU
# 阶段3:评估 (混合)
evaluate_model() # 需要:32 CPU核心,2 GPU
# 传统调度:按峰值分配 (64 CPU + 8 GPU)
# → 浪费:大部分时间资源闲置
解决方案:作业步骤 (Job Steps)
#!/bin/bash
#SBATCH --job-name=train_pipeline
# Step 1: 预处理 (只要CPU)
srun --nodes=1 --ntasks=64 --gres=gpu:0 \
python preprocess.py
# Step 2: 训练 (需要GPU)
srun --nodes=1 --ntasks=8 --gres=gpu:8 \
python train.py
# Step 3: 评估 (少量GPU)
srun --nodes=1 --ntasks=16 --gres=gpu:2 \
python evaluate.py
Part 7: 实战配置与调优
7.1 完整配置示例
slurm.conf (核心配置)
# === 集群定义 ===
ClusterName=nvidia_ai_cluster
SlurmctldHost=master-node-01
# === 调度配置 ===
# 使用cons_tres插件 (支持GPU细粒度调度)
SelectType=select/cons_tres
SelectTypeParameters=CR_Core_Memory
# 调度器类型
SchedulerType=sched/backfill
# 优先级插件
PriorityType=priority/multifactor
PriorityWeightAge=1000
PriorityWeightFairshare=10000
PriorityWeightJobSize=0
PriorityWeightQOS=10000
PriorityDecayHalfLife=14-0
PriorityMaxAge=7-0
PriorityFlags=FAIR_TREE
# === GRES配置 ===
GresTypes=gpu,mps,mig
# === 进程跟踪与资源控制 ===
ProctrackType=proctrack/cgroup
TaskPlugin=task/cgroup,task/affinity
# === 记账 ===
AccountingStorageType=accounting_storage/slurmdbd
AccountingStorageHost=master-node-01
AccountingStorageTRES=gres/gpu,gres/mps
JobAcctGatherType=jobacct_gather/cgroup
JobAcctGatherFrequency=30
# === 日志 ===
SlurmctldLogFile=/var/log/slurm/slurmctld.log
SlurmdLogFile=/var/log/slurm/slurmd.log
DebugFlags=CPU_Bind,gres
# === 拓扑 ===
TopologyPlugin=topology/tree
# === 节点定义 ===
NodeName=gpu-node[01-08] \
CPUs=128 \
Sockets=2 \
CoresPerSocket=64 \
ThreadsPerCore=1 \
RealMemory=512000 \
Gres=gpu:a100:8 \
State=UNKNOWN
# === 分区定义 ===
# 交互式开发分区
PartitionName=interactive \
Nodes=gpu-node[01-02] \
MaxTime=4:00:00 \
DefaultTime=1:00:00 \
MaxNodes=1 \
Priority=1000 \
State=UP
# 批处理训练分区
PartitionName=training \
Nodes=gpu-node[03-08] \
MaxTime=7-00:00:00 \
DefaultTime=24:00:00 \
Priority=100 \
State=UP \
Default=YES
# === 拓扑定义 ===
SwitchName=rack1 Nodes=gpu-node[01-04]
SwitchName=rack2 Nodes=gpu-node[05-08]
SwitchName=root Switches=rack1,rack2
gres.conf (GPU 详细配置)
# === 自动检测 ===
AutoDetect=nvml
# === gpu-node01配置 ===
NodeName=gpu-node01
# GPU 0-3 连接到 Socket 0
Name=gpu Type=a100 File=/dev/nvidia0 \
Cores=0-15 Links=1,2,3
Name=gpu Type=a100 File=/dev/nvidia1 \
Cores=16-31 Links=0,2,3
Name=gpu Type=a100 File=/dev/nvidia2 \
Cores=32-47 Links=0,1,3
Name=gpu Type=a100 File=/dev/nvidia3 \
Cores=48-63 Links=0,1,2
# GPU 4-7 连接到 Socket 1
Name=gpu Type=a100 File=/dev/nvidia4 \
Cores=64-79 Links=5,6,7
Name=gpu Type=a100 File=/dev/nvidia5 \
Cores=80-95 Links=4,6,7
Name=gpu Type=a100 File=/dev/nvidia6 \
Cores=96-111 Links=4,5,7
Name=gpu Type=a100 File=/dev/nvidia7 \
Cores=112-127 Links=4,5,6
# MPS配置 (可选)
Name=mps Count=800 File=/dev/nvidia[0-7]
# Links字段说明:
# GPU 0的Links=1,2,3 表示GPU 0通过NVLink连接到GPU 1,2,3
cgroup.conf (资源隔离)
# cgroup基础配置
CgroupAutomount=yes
CgroupReleaseAgentDir="/etc/slurm/cgroup"
# 约束配置
ConstrainCores=yes # 限制CPU核心
ConstrainRAMSpace=yes # 限制内存
ConstrainDevices=yes # 限制设备访问 (关键!)
ConstrainSwapSpace=yes # 限制Swap
# 内存策略
AllowedRAMSpace=100 # 允许使用100%请求的内存
AllowedSwapSpace=0 # 禁止使用Swap
# 设备约束
AllowedDevicesFile="/etc/slurm/cgroup_allowed_devices_file.conf"
7.2 性能调优策略
7.2.1 调度器调优
# === 提升调度吞吐量 ===
# 增加调度线程
SchedulerParameters=defer,max_switch_wait=300
# defer: 延迟分配,积攒作业后批量调度
# max_switch_wait: 等待最优拓扑的最长时间
# 调整backfill参数
bf_max_job_test=200 # 增加backfill尝试数
bf_max_job_user=4 # 每用户最多backfill 4个作业
bf_continue # 持续寻找backfill机会
# === 减少调度延迟 ===
SchedulerTimeSlice=30 # 30秒调度一次
batch_sched_delay=3 # 延迟3秒收集作业
# === 预留高优先级作业资源 ===
ReservationOverride=Prompt # 提示用户有预留冲突
7.2.2 网络调优
# 优化TCP参数 (对分布式训练重要)
# /etc/sysctl.conf
net.core.rmem_max = 134217728 # 128MB接收缓冲
net.core.wmem_max = 134217728 # 128MB发送缓冲
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_no_metrics_save = 1
net.ipv4.tcp_congestion_control = bbr
# 对于InfiniBand
echo 8192 > /sys/class/net/ib0/queues/rx-0/rps_cpus
7.2.3 监控与告警
# 关键监控指标
# 1. 调度器性能
sdiag # Slurm诊断工具
# 关注:
# - Main schedule cycle: 应该 < 1秒
# - Backfilling: 应该 > 50%的时间启用
# 2. 队列健康度
squeue -o "%.18i %.9P %.8j %.8u %.2t %.10M %.6D %R" | \
awk '{print $5}' | sort | uniq -c
# 输出每个状态的作业数
# PD (Pending): 如果 > 总作业的30%,可能需要扩容
# 3. 用户公平性
sshare -A # 查看所有账户的FairShare
# 检查是否有用户的RawUsage远超其RawShares
# 4. GPU利用率
srun --gres=gpu:1 nvidia-smi dmon -s u
# 如果长期 < 80%,考虑优化作业或允许共享
# 5. 节点健康
sinfo -Nl
# 检查DRAIN/DOWN节点比例
7.3 常见问题排查
问题 1:GPU 不可见
# 症状
sbatch: error: Batch job submission failed: Invalid generic resource (gres) specification
# 排查步骤
# 1. 检查slurmctld日志
grep -i gres /var/log/slurm/slurmctld.log
# 常见原因:
# - GresTypes未在slurm.conf中声明
# - gres.conf文件格式错误
# - slurmd未报告GPU
# 2. 验证节点GPU状态
scontrol show node gpu-node01 | grep Gres
# 应该显示:Gres=gpu:8
# 3. 手动触发节点重新注册
scontrol reconfigure
scontrol update NodeName=gpu-node01 State=RESUME
问题 2:作业一直 PENDING
# 查看具体原因
squeue -j <jobid> -o "%.18i %.9P %.8j %.8u %.2t %.10M %.9l %.6D %R"
# 常见原因及对策:
# Reason: Resources
# → 资源不足,等待或减少资源请求
# Reason: Priority
# → 优先级低,可提升QOS或减少历史使用量
# Reason: ReqNodeNotAvail
# → 请求的节点不可用,检查节点状态
# Reason: QOSMaxGresPerUser
# → 达到QOS限制,等待其他作业完成
# 详细诊断
scontrol show job <jobid>
# 检查:Reason, Dependency, Reservation字段
问题 3:作业间 GPU 干扰
# 症状:多个作业在同一GPU上运行但没有使用MPS
# 排查
scontrol show node gpu-node01 -d | grep AllocTRES
# 输出类似:
# AllocTRES=cpu=64,mem=256G,gres/gpu=8
# 检查cgroup配置
cat /sys/fs/cgroup/devices/slurm/uid_*/job_*/devices.list
# 应该看到每个job只能访问分配给它的GPU设备号
# 如果看到多个job访问同一GPU:
# 1. 检查cgroup.conf中ConstrainDevices=yes
# 2. 重启slurmd守护进程
systemctl restart slurmd
Part 8: 前沿技术与未来趋势
8.1 GPU 虚拟化技术
NVIDIA vGPU:
传统GPU分配:1个物理GPU = 1个VM/容器
vGPU技术:1个物理GPU = N个虚拟GPU
- 每个vGPU有独立的显存空间
- 支持迁移和动态调整
- 需要GPU支持虚拟化特性
挑战:
- 性能开销 (约10-15%)
- 需要企业级授权
- Slurm原生不支持,需要插件
8.2 AI 驱动的智能调度
# 未来调度器:基于机器学习
class AIScheduler:
def predict_job_runtime(self, job):
"""
使用历史数据预测作业运行时间
特征:代码hash、数据集大小、模型架构、GPU型号
"""
features = extract_features(job)
predicted_time = self.ml_model.predict(features)
return predicted_time
def optimize_placement(self, jobs):
"""
强化学习优化GPU分配
目标:最小化总完成时间 + 最大化公平性
"""
state = get_cluster_state()
action = self.rl_agent.select_action(state)
return action
# 已有研究:
# - Tiresias: 用Gittins Index预测最优调度
# - AntMan: 动态调整GPU资源分配
# - Pollux: 共同优化资源分配和超参数
8.3 云原生 HPC 融合
# Kubernetes + Slurm混合调度
# 方案1:Slurm on K8s
# 在Kubernetes上运行Slurm组件
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: slurmctld
spec:
serviceName: slurmctld
replicas: 1
template:
spec:
containers:
- name: slurmctld
image: slurm:latest
volumeMounts:
- name: config
mountPath: /etc/slurm
# 方案2:Volcano Scheduler
# K8s原生的批处理调度器,支持Gang Scheduling
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: pytorch-training
spec:
minAvailable: 8 # 至少8个Pod同时运行
schedulerName: volcano
tasks:
- replicas: 8
template:
spec:
containers:
- name: pytorch
resources:
limits:
nvidia.com/gpu: 1
进阶学习资源
- Slurm 官方文档: https://slurm.schedmd.com/
- NVIDIA HPC SDK: https://developer.nvidia.com/hpc-sdk
- 论文推荐:
- "Tiresias: A GPU Cluster Manager for Distributed Deep Learning"
- "Pollux: Co-adaptive Cluster Scheduling for Goodput-Optimized Deep Learning"
- "AntMan: Dynamic Scaling on GPU Clusters for Deep Learning"