Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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"