跳到主要内容

L1.10 Slurm 与 Ray 作业提交基础

三维坐标 layer: L1(算力与编排)level: Seniorpillar: 硬件架构

L1.4 对比了 K8s / Slurm / Ray 三种调度心智模型;L1.4 动手实验聚焦 kind+Volcano。本章补齐 Slurm 与 Ray 的最小提交路径——让你在超算或 Ray 集群上独立申请 GPU 并跑通一个训练/任务脚本,并理解两套调度器在 Gang 语义、抢占策略、异构任务编排上的深层差异。

学习目标

  • 前置知识:读过 L1.4;会写 Python / bash。无需自有 Slurm 集群(可用 Docker 模拟 Slurm 概念或用文档对照)。
  • 学完产出:① 能写一份带 #SBATCH --gres=gpu:N 的 Slurm 脚本并用 sbatch/squeue/scancel 管理生命周期;② 能解释 partition、QOS、fairshare 对排队的影响;③ 能 ray start --head 起本地集群并用 @ray.remote(num_gpus=1) 提交任务;④ 能用 ray job submit 打包远程作业;⑤ 能说明 Slurm 适合「整集群批训练」、Ray 适合「细粒度 RLHF/超参搜索」的选型依据,并用碎片率/利用率算例支撑选型结论。
  • 阅读姿势:三种调度器不是互斥——成熟平台常是 Slurm 管大卡训练池 + K8s 管推理 + Ray 管交互式流水线(见 L1.4 §2.4)。盯住一条主线:调度单位的粒度(整 job → Pod → 函数调用)决定了它擅长什么形态的负载

背景与现状

系统提交单位GPU 申请典型用户
Slurm批作业(Job)#SBATCH --gres=gpu:8HPC 中心、预训练团队
RayTask / Actor@ray.remote(num_gpus=1)RL、AutoML、RLHF
K8s+VolcanoPod / PodGroupresources.limits训推混合平台

Slurm 的 Gang 语义是天生的srun 一次拉起 N 个 task,要么全起要么全失败。Ray 则把集群抽象成资源池,调度粒度细到函数调用;要在 Ray 里获得 Gang 语义,需要显式声明 Placement Group(放置组)

📅 时效性提示(截至 2026 年中):Slurm 处于 25.11 稳定线(SchedMD 按「年.月」发版,每半年一个大版本),本文所有 sbatch/gres 语法在 23.x 之后均稳定;Ray 处于 2.5x 版本线活跃迭代中,ray job submit / Placement Group API 自 2.x 起已稳定。值得注意的行业趋势:Ray Train / Ray Data 在 RLHF 与多模态数据流水线中的地位持续上升——主流 RLHF 框架(如 verl、OpenRLHF)底层编排均基于 Ray。具体版本号请以官方 Release Notes 为准。

原理与架构

2.1 Slurm 核心概念

  • partition:节点逻辑分组(如 gpu-a100gpu-debug),各自可挂不同的时间上限与准入策略。
  • GRES:Generic Resource,gres.conf 声明每节点 GPU 型号与数量;--gres=gpu:a100:4 可以指定型号。
  • sbatch / srun / salloc:批提交 / 步内并行 / 交互分配。多机训练的标准形态是 sbatch 包一层 srun,由 srun 完成 Gang 式拉起。
  • 优先级机制multifactor 插件把 fairshare(历史用量越多优先级越低)、age(排队越久越高)、QOS 权重加权成一个优先级分数;backfill 调度器允许小短作业「见缝插针」地越过队首大作业先跑——前提是不推迟队首作业的预计开始时间。

2.2 Ray 核心概念

  • Task:无状态远程函数;Actor:有状态 worker(如常驻显存的 reward model)。
  • Placement Group:以 bundle 为单位原子性预留一组资源(类似 Gang),支持 PACK(聚拢,利于通信)与 SPREAD(打散,利于容错)策略。
  • Ray Job:把 working_dir + pip 依赖打包提交到集群,生命周期独立于提交端。

2.3 两个真实业务症状:调度语义差异如何咬人

症状一:多机作业卡在 CF/PD 状态两小时,squeue 显示 REASON=Resources,但监控面板明明有 8 张空闲卡。 根因是碎片化 + Gang 语义:这 8 张空闲卡分散在 4 台节点上(每台 1~3 张),而作业要求 --nodes=2 --gres=gpu:8(两台整机)。Slurm 的 Gang 分配要么凑齐要么不跑,散落的碎片卡永远凑不成两台整机——空闲率 25% 与「一卡难求」并存。解法见思考题 1。

症状二:低优先级微调作业夜里被抢占(preempted)三次,早上发现从 step 0 重跑,8 小时白烧。 根因是集群开了 PreemptMode=REQUEUE(高优作业可抢占低优作业并令其重新排队),而作业脚本没有实现 checkpoint 续训——每次 requeue 都从头开始。抢占是集群提高利用率的合理机制,能否被抢占后无损恢复是用户侧的责任。代价与收益的定量权衡见思考题 2。

2.4 选型速查:什么负载投给谁

把「背景与现状」的对比表往深处推一层,选型的判断轴其实只有三条:任务粒度、Gang 需求、状态形态

判断轴投给 Slurm投给 Ray投给 K8s(+Volcano)
任务粒度小时~天级整机批作业秒~分钟级函数/Actor分钟~常驻级 Pod
Gang 需求原生(srun 全起全停)显式 Placement Group需 Volcano PodGroup 插件
状态形态无状态批处理 + checkpoint常驻有状态 Actor(reward model、KV cache)长期服务(推理 API)
失败语义job 失败 → requeue 重跑task 失败 → 血统(lineage)重建Pod 失败 → 控制器重建
典型反例用 Slurm 跑秒级超参试探 → 排队开销远超计算用 Ray 跑 3 天 2048 卡预训练 → 容错/隔离不如 Slurm 成熟裸 K8s 跑多机训练 → 无 Gang 会死锁(L1.4)

两条经验法则:① 单任务时长如果短于平均排队时长,就不该走批调度器——这正是 RLHF rollout(秒级)不适合逐个 sbatch 的原因;② 需要「要么全起要么全不起」的多卡通信组,必须有 Gang 语义兜底——Slurm 天生带、Ray 靠 Placement Group、K8s 靠 Volcano,三者殊途同归(详见思考题 1、3 的定量展开)。

动手实践:Slurm 与 Ray 双路提交

实验目标:分别走通 Slurm 批提交与 Ray 资源池两条路径,产出「同一个 GPU 检测脚本在两种调度器下的完整生命周期」对照体验。

A. Slurm:最小 GPU 作业脚本

#!/bin/bash
#SBATCH --job-name=hello-gpu
#SBATCH --partition=gpu
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --gres=gpu:1
#SBATCH --cpus-per-task=8
#SBATCH --mem=32G
#SBATCH --time=00:30:00
#SBATCH --output=logs/%x-%j.out

module load cuda/12.6 2>/dev/null || true
source ~/venvs/train/bin/activate

python -c "
import torch
print('cuda_available', torch.cuda.is_available())
if torch.cuda.is_available():
print('device', torch.cuda.get_device_name(0))
"
mkdir -p logs
sbatch train.slurm
squeue -u $USER # 看排队/运行
scontrol show job $JOBID # 看详情
scancel $JOBID # 取消

常用排查

squeue -p gpu -o "%.10i %.9P %.8j %.8u %.2t %.10M %.6D %R"
# ST=PD 且 REASON=Resources → 等资源;Priority → 排队
sacct -j $JOBID --format=JobID,State,ExitCode,Elapsed

B. Ray:本地单机构集群

pip install "ray[default]"
ray start --head --port=6379 --num-gpus=0 # 无 GPU 时用 num-gpus=0 练流程
# ray_hello.py
import ray
ray.init(address="auto")

@ray.remote(num_cpus=1)
def hello():
return "ray ok"

print(ray.get(hello.remote()))
python ray_hello.py
ray stop

C. Ray Job Submit(远程集群)

# 在 head 节点
ray job submit --address=http://127.0.0.1:8265 \
--working-dir . \
-- python train.py --epochs 1

ray job status $JOB_ID
ray job logs $JOB_ID

D. Ray Placement Group:Gang 式预留(进阶,可选)

对应 2.2 节与思考题 1/3——在 Ray 里显式获得「要么全给要么不给」的 Gang 语义:

# ray_pg_demo.py —— 无 GPU 环境用 CPU bundle 演示同一语义
import ray
from ray.util.placement_group import placement_group
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy

ray.init(address="auto")

# 原子性预留 2 个 bundle(真实训练场景换成 {"GPU": 8})
pg = placement_group([{"CPU": 1}, {"CPU": 1}], strategy="PACK")
ray.get(pg.ready()) # 阻塞直到全部 bundle 就绪——Gang 语义所在
print("placement group ready:", pg.bundle_specs)

@ray.remote(num_cpus=1)
def worker(rank):
return f"worker {rank} in pg"

refs = [worker.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg)).remote(i) for i in range(2)]
print(ray.get(refs))
python ray_pg_demo.py # 需先 ray start --head(见 B 步骤)

观察点:若把 bundle 需求改到超过集群资源(如 {"CPU": 999}),pg.ready()一直挂起而不是部分分配——这就是思考题 1 里「PG 凑不齐也会整体 pending」的可复现实验;strategy="PACK""SPREAD" 的差别可用多节点集群对照。

踩坑预警

  1. Slurm CUDA_VISIBLE_DEVICES:Slurm 自动设置;勿在脚本里硬编码 cuda:0 以外设备而不读环境变量。
  2. Ray GPU fractionalnum_gpus=0.5 允许多 task 共享卡——训练慎用,推理/debug 可用。
  3. Ray 与 Slurm 混部:需 Slurm prolog 里 ray start 或 KubeRay on Slurm——勿让两套调度器 double-book GPU(L1.4 思考题)。
  4. partition 时间限制--time 超限作业被 kill,长训练需换 partition 或 checkpoint 续跑。
  5. 被抢占 ≠ 会续训PreemptMode=REQUEUE 环境下,脚本必须自查最新 checkpoint 并从之恢复(见 2.3 症状二),否则每次抢占都从头跑。

配套代码ai-infra-labs ray_hello.py — 本地 Ray 集群 smoke;Slurm 脚本见同目录 train.slurm 模板。

深入思考

下面三题每题先给题干,再用 <details> 折叠一份图文并茂的参考答案。建议先合上答案自己想 3 分钟,再展开对照。

思考题 1:Gang 调度与碎片饥饿——为什么「有 8 张空闲卡」还是起不来

2.3 节症状一里,4 台 8 卡节点各被小作业占走 5/6/6/7 张卡,剩下 3+2+2+1 = 8 张空闲卡,而一个 --nodes=2 --gres=gpu:8 的整机 Gang 作业排了两小时也起不来。结合 2.1 节的 partition/backfill 机制2.2 节 Ray Placement Group,分析:① 这种「碎片饥饿」的形成机理;② Slurm 侧有哪些手段能缓解;③ Ray 的 Placement Group 在同样场景下会表现更好还是一样卡?

展开参考答案(含碎片饥饿形成图 + 算一遍)

结论:Gang 语义要求资源原子性凑齐,而细粒度小作业会把整机切成「每台差几张」的碎片——空闲总量足够但拓扑上凑不成套,大作业就会无限期饥饿;Slurm 靠分区隔离 + backfill 预留缓解,Ray 的 Placement Group 同样受碎片制约,但它的原子预留 + PACK 策略能在碎片形成前主动聚拢,两者是「事后等」与「事前留」的差别。

用具体数字算一遍(4 节点 × 8 卡 = 32 卡集群):

  1. 小作业占用分布:5、6、6、7 张 → 空闲 3+2+2+1 = 8 张,集群空闲率 8/32 = 25%
  2. Gang 作业需要 2 台「8 卡全空」的节点:当前每台最多空 3 张,满足条件的节点数为 0——25% 的空闲算力对它而言是 0。
  3. 若无干预:只要小作业到达率足够高,退出释放的卡会立刻被新小作业填上,整机窗口永远不出现——这就是「碎片饥饿(starvation)」,不是 bug,是调度策略后果。

Slurm 侧缓解:① 分区隔离——大作业专用 gpu-batch 分区、小作业赶去 gpu-debug,物理上不让碎片作业污染整机池;② --exclusive 让关键作业独占整机,防止自己成为碎片制造者;③ backfill 预留——backfill 调度器为队首大作业计算「预计可开始时间」并预留资源,此后只允许能在窗口前结束的短作业插队(这要求所有作业诚实填 --time,时限填得越准,backfill 越高效);④ 极端手段是 scontrol 手动 drain 两台节点排空。

Ray 侧对照:Placement Group 同样解不了「已经碎了」的局——PACK 策略的 PG 若凑不齐 bundle,也会整体 pending。但 Ray 的优势在事前:PG 是原子预留,一旦创建成功,后续小 task 无法侵占这批资源;且 RLHF 这类负载可以先建 PG 再按需往里放 Actor,相当于「先圈地后开工」。而 Slurm 用户通常是「用时才申请」,碎片已成事实。本质上两者都证明了 2.1/2.2 节的同一条定律:Gang 语义与细粒度共享天然冲突,必须靠分区/预留把两类负载隔开

思考题 2:抢占与优先级——集群利用率和个体公平的定量权衡

你的集群开启了 PreemptMode=REQUEUE:高优 QOS 作业可以抢占低优作业。运营方宣称这让集群利用率从 70% 提到 90%,但低优用户抱怨作业反复重跑。结合 2.1 节的 QOS/fairshare 机制2.3 节症状二,定量分析:一个 8 小时的低优作业在「被抢占 3 次 + 每 30 分钟 checkpoint」条件下,实际完成时间和有效算力比是多少?这笔账应该怎么算才公平?

展开参考答案(含抢占时间线图 + 算一遍)

结论:抢占把「集群闲置浪费」转移成「低优作业的重跑税」——有 checkpoint 时这笔税可控(本例墙钟膨胀 1.41 倍、有效算力比 86%),没有 checkpoint 时税率趋于无穷(永远从头跑);公平的算法是同时核算集群侧收益与个体侧代价,并用 checkpoint 间隔把单次损失上限锁死。

用具体数字算一遍

  1. 纯计算时间 480 min(8 h);每 30 min 落一次 checkpoint、每次 2 min → 16 次 × 2 = 32 min 开销。
  2. 被抢占 3 次:每次平均丢掉半个 checkpoint 间隔的进度(~15 min 需重算)+ 重新排队等 40 min → 3 × (15+40) = 165 min
  3. 墙钟总时长 = 480 + 32 + 165 = 677 min ≈ 11.3 h,膨胀 677/480 ≈ 1.41 倍
  4. 有效算力比 = 纯计算 / 实际烧卡时间 = 480 / (480+32+45) = 86.2%(排队不烧卡,不计入分母)——即约 14% 的 GPU 时被 checkpoint 开销和重算浪费。
  5. 对照组:若没有 checkpoint,每次抢占平均丢掉已跑进度的一半;只要抢占间隔短于作业时长,作业可能永远跑不完——症状二的「8 小时白烧」正是这个极端。

这笔账怎么算才公平:集群侧收益是真实的——利用率 70% → 90% 意味着同样的硬件多产出约 28% 的算力,高优作业等待从小时级降到分钟级。个体侧代价也是真实的——低优用户付出 1.4 倍墙钟。工程上的公平做法是:① checkpoint 间隔按「单次可接受损失」倒推(想最多丢 15 min 就每 30 min 存一次,前提是存一次的开销远小于间隔);② 运营方用 fairshare 补偿——被抢占的作业量不计入(或折扣计入)用户历史用量,让被抢占者后续优先级回升;③ 提供 PreemptMode=SUSPEND 或最低运行时保护(如运行 <1 h 的作业不抢),压住最恶性的「刚起步就被杀」。回看 2.1 节:QOS/fairshare/抢占是一套连动系统,只调其中一个旋钮必然产生投诉。

思考题 3:RLHF 流水线为何常选 Ray 而非纯 Slurm?

RLHF 一次迭代里交织着训练(多卡 All-Reduce)、推理 rollout(vLLM 批生成)、奖励模型打分、经验回放搬运——任务粒度与资源画像差异巨大。结合 2.2 节的 Actor/Placement Group 与「背景与现状」中的提交单位对比,定量说明:在一个 16 卡池上,「Slurm 静态切两个 job」与「Ray 弹性复用同一池」的利用率与迭代时长差多少?为什么说这是调度单位粒度决定的?

展开参考答案(含静态切分 vs 弹性复用对比图 + 算一遍)

结论:RLHF 的推理与训练两阶段交替出现且资源需求此起彼伏,Slurm 的调度单位是整 job、资源在作业生命周期内静态锁死,两阶段互相干等,利用率理论上限 50%;Ray 的调度单位是函数/Actor,可在阶段间把 16 张卡整体倒手,同样的硬件迭代时长缩短一半——粒度差异直接折算成钱。

用具体数字算一遍(16 卡池,一次 RLHF 迭代 = rollout 160 GPU·min + train 80 GPU·min 的工作量):

  1. Slurm 静态切分:8 卡常驻推理 job + 8 卡常驻训练 job。rollout 需 160/8 = 20 min(此时训练 8 卡干等),train 需 80/8 = 10 min(此时推理 8 卡干等)→ 迭代 30 min,忙时 GPU·min = 240,总供给 16×30 = 480 → 利用率 50%
  2. Ray 弹性复用:rollout 时 16 卡全部给 vLLM Actor → 160/16 = 10 min;train 时 16 卡全部给训练 → 80/16 = 5 min → 迭代 15 min,吞吐翻倍,理想利用率趋近 100%(现实中权重同步、Actor 调度会吃掉 5%~15%)。
  3. 换算成钱:16 张卡若按小时计费,同样的实验预算下 Ray 方案能多跑一倍迭代——对需要几百次迭代的 RLHF 来说是「周 vs 半月」的差距。

为什么是「粒度」决定的:Slurm 的最小承诺单位是 job——--gres=gpu:8 在作业存活期内静态锁死,想换分配只能杀掉重排(重新排队 + 冷启动)。Ray 的最小承诺单位是一次函数调用/一个 Actor@ray.remote(num_gpus=1) 的 rollout worker 用完即还,Placement Group 只在需要 Gang(如训练 All-Reduce 组)时临时圈地。此外 RLHF 还有 Slurm 难以表达的形态:常驻有状态服务(reward model Actor 驻显存反复打分)、动态 DAG(rollout 结果实时喂给打分、打分结果攒批喂训练——Ray Data 流水线的用武之地)。这不是「一个固定 8 卡 job 跑 3 天」的 Slurm sweet spot。故成熟形态是二者组合:Ray 编排阶段内的细粒度任务,Slurm/K8s 提供池子本身——呼应「背景与现状」表中三系统各归其位的结论;2026 年主流 RLHF 框架(verl、OpenRLHF)无一例外选 Ray 做编排层,正是这套账算下来的结果。

延伸阅读