L1.10 Slurm 与 Ray 作业提交基础
三维坐标
layer: L1(算力与编排)|level: Senior|pillar: 硬件架构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:8 | HPC 中心、预训练团队 |
| Ray | Task / Actor | @ray.remote(num_gpus=1) | RL、AutoML、RLHF |
| K8s+Volcano | Pod / PodGroup | resources.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-a100、gpu-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" 的差别可用多节点集群对照。
踩坑预警
- Slurm
CUDA_VISIBLE_DEVICES:Slurm 自动设置;勿在脚本里硬编码cuda:0以外设备而不读环境变量。 - Ray GPU fractional:
num_gpus=0.5允许多 task 共享卡——训练慎用,推理/debug 可用。 - Ray 与 Slurm 混部:需 Slurm prolog 里
ray start或 KubeRay on Slurm——勿让两套调度器 double-book GPU(L1.4 思考题)。 - partition 时间限制:
--time超限作业被 kill,长训练需换 partition 或 checkpoint 续跑。 - 被抢占 ≠ 会续训:
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 卡集群):
- 小作业占用分布:5、6、6、7 张 → 空闲 3+2+2+1 = 8 张,集群空闲率 8/32 = 25%。
- Gang 作业需要 2 台「8 卡全空」的节点:当前 每台最多空 3 张,满足条件的节点数为 0——25% 的空闲算力对它而言是 0。
- 若无干预:只要小作业到达率足够高,退出释放的卡会立刻被新小作业填上,整机窗口永远不出现——这就是「碎片饥饿(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 间隔把单次损失上限锁死。
用具体数字算一遍:
- 纯计算时间 480 min(8 h);每 30 min 落一次 checkpoint、每次 2 min → 16 次 × 2 = 32 min 开销。
- 被抢占 3 次:每次平均丢掉半个 checkpoint 间隔的进度(~15 min 需重算)+ 重新排队等 40 min → 3 × (15+40) = 165 min。
- 墙钟总时长 = 480 + 32 + 165 = 677 min ≈ 11.3 h,膨胀 677/480 ≈ 1.41 倍。
- 有效算力比 = 纯计算 / 实际烧卡时间 = 480 / (480+32+45) = 86.2%(排队不烧卡,不计入分母)——即约 14% 的 GPU 时被 checkpoint 开销和重算浪费。
- 对照组:若没有 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 的工作量):
- 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%。
- Ray 弹性复用:rollout 时 16 卡全部给 vLLM Actor → 160/16 = 10 min;train 时 16 卡全部给训练 → 80/16 = 5 min → 迭代 15 min,吞吐翻倍,理想利用率趋近 100%(现实中权重同步、Actor 调度会吃掉 5%~15%)。
- 换算成钱: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 做编排层,正是这套账算下来的结果。
延伸阅读
- L1.4 资源调度与编排 §2.4、§3 混合调度思考题
- Slurm:GRES 文档、Preemption 文档、Scheduling Configuration(backfill)
- Ray:Cluster Setup、Placement Groups
- verl / OpenRLHF 源码中的 Ray 编排层——看真实 RLHF 框架如何组合 Actor + Placement Group