暇 ひまネタNEWS(^o^) | スキマ時間にちょっとしたネタを提供スキマ時間に、話題のまとめを一気読み 全サイト自動巡回中
HIMA NETA ORIGINAL READER
本文だけモード Zenn IT・ガジェット 約37分で読めます

Slurm は Kaggle で役に立つ

読みやすさ この端末に設定を保存します
書体
文字
背景

広告・ランキング・関連記事を除き、配信元の安全な文字装飾だけを残しています。

1. はじめに

Slurm は、スパコンや研究室の GPU クラスタでよく使われるジョブスケジューラです。投入されたジョブをキューに並べておいて、CPU・メモリ・GPU が空いたものから順に実行してくれます。

「大きなクラスタのための道具でしょ?」という印象がありますが、実は自宅の GPU 1 枚で Kaggle をするときにも、めちゃくちゃ役に立ちました!Kaggle の RSNA Knee Abnormality Detection(膝 MRI の異常検出)に RTX 3090 1 枚で参加したとき、学習ジョブの管理に Slurm を使っていたんです。41 日間で、RSNA の学習だけで 253 本のジョブを回して、GPU を延べ 469 時間も使いました。ここで紹介する方法は一般化してあるので、どんな GPU でも使えると思います!3090 は Kaggle で戦うにはつらいことは置いておいてくださいw

Claude Code や Codex に実験を任せるなら、なおさらおすすめです。学習を Slurm に投げておけば、エージェントのセッションが切れても学習はそのまま走り続けてくれます。

3 行でいうと、こんな感じです。

  • GPU 1 枚のジョブキューなら、Ray や Kubernetes より入れるものが少なくて済みます。導入も Codex に任せたら約 25 分で終わりました
  • 学習を Slurm に任せておけば、Claude Code や Codex のセッションが切れても学習は止まりません
  • 別のコンペのジョブも同じキューに入れれば、1 枚の GPU を順番に分け合えます。優先度も変えられます

複数のエージェントのセッションが sbatch で同じ Slurm のキューにジョブを入れ、RTX 3090 が 1 本ずつ実行する図

Slurm のコマンドの基本は、Turing の ymg_aq さんの記事「手元のGPUを遊ばせないためのジョブスケジューラ入門」がとても丁寧にまとめてくれています。なのでこの記事では、Kaggle の実験で実際に使った設定と、エージェントと組み合わせるときの工夫を中心に紹介していきますね。

2. GPU 1 枚あるある

まずは、自宅の GPU 1 枚で実験していて「うわ、困った…」となったことを並べてみます。

  • GPU がサボる。 学習が終わったことに気づかないと、次の実験を始めるまで GPU は止まったままです。「前のジョブが終わったら次を投入する」処理を tmux の sleep ループで書いたこともありますが、ループ自体がエラーも出さずに止まっていたことがありました。ひどい…
  • エージェントが力尽きると、学習も道連れになる。 Claude Code などのバックグラウンドタスクで学習を動かしていると、セッションが終わったときに学習も止まっちゃいます。うっかりセッションを閉じてしまうこともありますし、Codex や Claude の利用上限でエージェントのループが止まることもありますよね
  • 別のコンペと GPU を取り合う。 別のコンペの実験も同じ GPU で動かそうとすると、どっちが先に使うかを人間が調整することになります。メモリが足りなくなって、どちらかが強制終了されることもありました

どれも、Slurm に任せるとかなり楽になりますよ!

3. Slurm とは

Slurm は 2 つのデーモンで動いています。ジョブを受け付けて資源を割り当てる slurmctld と、実際にジョブを実行する slurmd です。munge は、この 2 つがお互いを認証するために使います。ジョブは、パーティションと呼ばれる投入先に sbatch コマンドで入れます。

もともとは多数のノードをまとめて管理するための道具ですが、1 台で使うなら、同じマシンで slurmctld と slurmd を動かすだけです。しかも、キューの状態は slurmctld が StateSaveLocation のディレクトリに保存してくれるので、デーモンを再起動してもキューは消えません!

ちなみに Slurm には、終わったジョブの記録をデータベースに残す accounting という機能もあります。今回は入れていません。その代わりの工夫は 5.4 で紹介しますね。

4. 導入は Codex 任せで 25 分

「でも導入が大変なんでしょ?」と思ったあなた、安心してください。導入の手間は、ほとんど Claude や Codex が引き受けてくれます。

この章に載せている slurm.conf・gres.conf・cgroup.conf とセットアップスクリプトは、ぜんぶ Codex が書いたものです。私がやったのは sudo bash scripts/setup_slurm.sh を実行したことだけ!sudo が要る操作だけは自分で打ちます。入れると決めてから動くまで、約 25 分でした。slurm.conf の書き方を一から覚える必要もありません。

入れたあとも同じです。今の時代、Claude に「GPU のジョブは Slurm 経由で動かして」と頼めば、難しいジョブ定義を自分で書かなくても、いい感じに Slurm 経由にしてくれます!

使った環境はこんな感じです。

項目 内容
マシン 自宅のデスクトップ 1 台(Ubuntu 26.04.1 LTS)
CPU / メモリ 10 コア 20 スレッド / 61GB。Slurm には 54,000MB として登録
GPU RTX 3090(24GB)1 枚。画面表示にも使用
Slurm Ubuntu のパッケージの slurm-wlm 25.11.2。accounting はなし

セットアップスクリプトは、root 以外で実行すると止まるようにしてあります。それと、/etc/slurm/ に違う内容の設定ファイルがあるときは、上書きせずに止まります。

scripts/setup_slurm.sh
#!/usr/bin/env bash
# Explicit host-admin setup. Never run the experiment controller as root.
set -euo pipefail
if [[ $EUID != 0 ]]; then
  echo 'Run on the host: sudo bash scripts/setup_slurm.sh' >&2
  exit 1
fi
if [[ $(hostname -s) != home-ubuntu ]]; then
  echo 'These configs target home-ubuntu; review the node configuration first.' >&2
  exit 1
fi
for name in slurm.conf gres.conf cgroup.conf; do
  if [[ -e /etc/slurm/$name ]] && ! cmp -s "ops/slurm/$name" "/etc/slurm/$name"; then
    echo "Existing /etc/slurm/$name differs; refusing to overwrite." >&2
    exit 1
  fi
done
apt-get update
apt-get install -y slurm-wlm munge
install -d -o slurm -g slurm /var/lib/slurm/slurmctld /var/lib/slurm/slurmd /var/log/slurm
install -d -m 755 /etc/slurm
for name in slurm.conf gres.conf cgroup.conf; do
  install -m 644 "ops/slurm/$name" "/etc/slurm/$name"
done
if [[ ! -s /etc/munge/munge.key ]]; then
  /usr/sbin/mungekey --create
fi
systemctl enable --now munge
systemctl enable --now slurmctld slurmd
slurmd -C
slurmd -G
scontrol ping
sinfo -Nel

設定ファイルは次の 3 つです。リポジトリの ops/slurm/ に置いて、Git で管理しています。

ops/slurm/slurm.conf
# Single-node desktop configuration. CPU topology is checked against slurmd -C.
ClusterName=rsna-local
SlurmctldHost=home-ubuntu
SlurmUser=slurm
AuthType=auth/munge
StateSaveLocation=/var/lib/slurm/slurmctld
SlurmdSpoolDir=/var/lib/slurm/slurmd
SlurmctldPidFile=/run/slurmctld.pid
SlurmdPidFile=/run/slurmd.pid
SlurmctldLogFile=/var/log/slurm/slurmctld.log
SlurmdLogFile=/var/log/slurm/slurmd.log
SelectType=select/cons_tres
SelectTypeParameters=CR_Core_Memory
SchedulerType=sched/backfill
ProctrackType=proctrack/cgroup
TaskPlugin=task/cgroup,task/affinity
GresTypes=gpu
ReturnToService=2
MinJobAge=86400
NodeName=home-ubuntu CPUs=20 Boards=1 SocketsPerBoard=1 CoresPerSocket=10 ThreadsPerCore=2 RealMemory=54000 Gres=gpu:1 State=UNKNOWN
PartitionName=gpu Nodes=home-ubuntu Default=YES MaxTime=08:00:00 State=UP OverSubscribe=NO
ops/slurm/gres.conf
# Manual GPU registration avoids a dependency on the NVML autodetection plugin.
NodeName=home-ubuntu Name=gpu File=/dev/nvidia0
ops/slurm/cgroup.conf
CgroupPlugin=autodetect
ConstrainCores=yes
ConstrainRAMSpace=yes
ConstrainDevices=yes

設定のポイントはこのあたりです。

  • SelectType=select/cons_tres と CR_Core_Memory で、CPU コア・メモリ・GPU をジョブごとに別々に割り当てます
  • cgroup.conf の 3 つの Constrain で、ジョブが要求した分を超える CPU・メモリ・GPU を使えないようにします
  • gres.conf は、GPU を Slurm に登録するファイルです。NVIDIA の管理ライブラリ NVML による自動検出には頼らず、/dev/nvidia0 を手で書きました。ただ、これはカーネルを更新したときに裏目に出ます(10.2 で書きます)
  • MinJobAge=86400 で、終わったジョブも 24 時間は scontrol で見られるようにしています。accounting の代わりですね
  • ReturnToService=2 は、再起動したノードを自動で使える状態に戻す設定です
  • パーティションは gpu の 1 つだけにして、1 ジョブの上限は 8 時間にしました

5. 基本的な使い方

ここからは、実験で実際に使ったコマンドを順に見ていきましょう!

5.1 sbatch でジョブを投入する

sbatch でジョブを投げると、ジョブはキューに入ります。GPU が使われていれば、空くまで何時間でも待って、空いたらすぐ始まります。寝ている間も GPU に働いてもらえるのは、これのおかげです!

GPU を使う学習の投入は、次のラッパースクリプトだけを使うことにしました。資源の要求(--gres=gpu:1、CPU 8 コア、メモリ 28G)をこの 1 か所に固定しておけば、GPU の指定を付け忘れる心配がありません。2 つ目の引数にジョブ ID を渡すと、そのジョブの後ろにつなげます(5.2 で説明します)。それと、再投入のたびにログのファイル名を attempt1.out、attempt2.out と変えるので、前の試行のログが上書きされることもありません。

scripts/gpu_submit.sh(コメントを省略)
#   scripts/gpu_submit.sh <TAG> [afterany-jobid] [mem]
set -euo pipefail
cd "$(dirname "$0")/.."
TAG="${1:?usage: gpu_submit.sh <TAG> [afterany-jobid]}"
AFTER="${2:-}"
MEM="${3:-28G}"
JOB="runs/dino/$TAG/job.sh"
[ -x "$JOB" ] || { echo "no executable $JOB" >&2; exit 1; }
mkdir -p "runs/dino/$TAG/slurm"
N=1; while [ -e "runs/dino/$TAG/slurm/attempt$N.out" ]; do N=$((N+1)); done
DEP=()
[ -n "$AFTER" ] && DEP=(--dependency="afterany:$AFTER")
sbatch --parsable -p gpu --gres=gpu:1 -c 8 --mem="$MEM" -J "$TAG" \
  -o "runs/dino/$TAG/slurm/attempt$N.out" "${DEP[@]}" "$JOB"

5.2 --dependency でジョブをつなぐ

ジョブどうしの順番は、依存関係で決められます。sbatch に --dependency=afterany:<ジョブ ID> を付けて投げると、そのジョブは、指定したジョブが終わるまでキューの中で待ちます。afterany は、前のジョブが成功でも失敗でも、終わったら始まるタイプです。前のジョブが成功したときだけ始めたいなら、afterok を使います。

JOB1=$(sbatch --parsable train_a.sh)          # --parsable を付けると、ジョブ ID だけが出力される
sbatch --dependency=afterany:$JOB1 train_b.sh  # JOB1 が終わったら、成否にかかわらず始める
sbatch --dependency=afterok:$JOB1 eval_a.sh    # JOB1 が成功したときだけ始める

--parsable で受け取ったジョブ ID を次の投入に渡していけば、ジョブを何本でも数珠つなぎにできます。afterany なら、途中の 1 本が失敗しても後ろのジョブは止まりません。しかも依存関係は slurmctld が持っているので、tmux やエージェントのセッションが終わっても消えません。黙って止まる sleep ループとは、これでお別れです!

ちなみに、優先度が同じなら、ジョブはだいたい投入順に始まります。ただ、必ずそうなるとは限りません。Slurm は、先頭のジョブの開始を遅らせない範囲で、後ろの小さなジョブを先に始めることがあるんです(backfill と呼びます)。順番が大事なジョブは、afterany でつないで明示しておくと安心ですよ。

5.3 squeue で予定表を見る

squeue は、キューに入っているジョブの一覧を出すコマンドです。ジョブ名(-J)を実験のタグにしておくと、squeue の出力がそのまま実験の予定表になって便利です。

ただひとつ注意があります。Slurm は仕事はきっちりやってくれますが、報告はしてくれません。キューが空になれば GPU は止まったままですし、ジョブが終わったことも誰にも教えてくれないんです。私も、ジョブの完了に気づかずに GPU を約 10 時間空けてしまったことがあります…。

その対策として、次のジョブを常に 1 本、後ろに並べておくようにしました。さらに、squeue を監視する小さな常駐プログラムを作って、ジョブがキューから抜けたとき、残りが 1 本になったとき、キューが空になったときにだけ通知が来るようにしています。

5.4 scontrol show job で成否を確かめる

accounting を入れていないので、終わったジョブの記録を調べる sacct は使えません。代わりに活躍するのが、ジョブやノードの状態を見たり変えたりできる scontrol です。slurm.conf に MinJobAge=86400 を書いて、終わったジョブも 24 時間は scontrol で見えるようにしています。

実験ループでは、squeue -h -j <ジョブ ID> が空になるまで待ってから、scontrol show job <ジョブ ID> -o を読みます。JobState=COMPLETED、ExitCode=0:0、結果ファイル cv.json の 3 つがそろったときだけ成功、というルールです。24 時間を過ぎると scontrol からも消えちゃうので、結果はループの状態ファイルに書いておきます。

5.5 scontrol update と --test-only で、待っているジョブを調整する

投入済みのジョブの資源は、投げ直さなくても変えられます。たとえばメモリの要求なら、scontrol update job=<ID> MinMemoryNode=28672 といった感じです。単位は MB なので、28G と書くと "Invalid MinMemoryNode value" で怒られます。ジョブ ID も依存関係もそのまま残るので、地味に便利です!

ジョブがいつ始まりそうかは、sbatch --test-only で調べられます。同じ条件で --gres=gpu:1 の有無だけを変えて試すと、待っている原因がメモリなのか GPU なのかが分かりますよ。

5.6 scancel で取り消して、投入し直す

Slurm は、投入されたバッチスクリプトの写しを、投入した時点で保存します。なので、投げたあとに job.sh を直しても、待っているジョブには反映されません。直したいときは、scancel で取り消して投げ直しましょう。bash -n の構文チェックでは、exec の行が消えたようなミスは見つからないので、書き込む前に中身を確かめておくのがおすすめです。

一方、Python のコードは、ジョブが始まったときに読まれます。予約したあとで train.py を直せば反映されますが、壊すと待っているジョブが全部失敗します。引数の検証も開始時にしか動かないので、投入する前に、その引数で検証だけ先に実行しておくと安心です。

6. Slurm × Claude Code / Codex

ここからは、エージェントに実験を任せるときの使い方です。

6.1 セッションが力尽きても、学習は止まらない

Slurm に投げた学習は、Slurm の管理下で動きます。投入したエージェントのプロセスとは関係がないので、Claude Code や Codex のセッションが切れても、利用上限でループが止まっても、学習は動き続けます!

実験ループの説明書にも、こう書いておきました。

チャットを閉じてもtmuxとSlurmの処理は続きます。マシンの停止・再起動は別です。

6.2 覚えておくのは、エージェントではなくリポジトリ

学習をエージェントから切り離すなら、「何を試して、結果がどうだったか」もエージェントの外に置いておく必要があります。Slurm が知っているのは、どのジョブが待っていて、どれが動いているかだけですからね。そこで、実験の状態はリポジトリのファイルに書いて、エージェントはそのファイルと squeue を読んで作業を続けるようにしました。

  • 実験 1 本ごとに、省略なしのコマンドを runs/dino/<タグ>/job.sh に書きます。新しい実験は、実績のある job.sh の変数を 1 つだけ変えて作ります
  • 学習ログの書式は、AGENTS.md(Claude Code や Codex が読む作業ルール)で決めています。先頭の行に START と設定と使うデバイスを書いて、30〜60 秒ごとに loss・速度・VRAM・残り時間を 1 行ずつ出します。最終行は必ず FINISHED か FAILED です。tqdm はキャリッジリターンのせいで tail -f で読めなくなるので、使いません
  • 判断の経緯は ops/decisions.md に追記します
  • AGENTS.md には「sbatch を手で書かない。投入は gpu_submit.sh だけ」と書いておきます

エージェントが作業を再開するときの手順も、最初の項目を「squeue -h が空なら、既存の job.sh をコピーして 1 変数だけ変えた次の実験をすぐ投入する」にしておきました。

7. 複数のコンペで GPU を分け合う

7.1 同じパーティションに投入する

別のコンペのジョブも同じパーティションに入れておけば、Slurm が順番に GPU を割り当ててくれます。私の環境では、RSNA のほかに ARC Prize 2026 と、もう 1 つ別のプロジェクトのジョブも、同じ gpu パーティションに投げていました。優先度が同じなら投入順に始まるので、どっちが先に GPU を使うかを人間が調整する必要はありません!

自分のジョブを、別プロジェクトが最後に投げたジョブの後ろに afterany でつなぐ、という使い方もできます。相手が早く終わっても途中で失敗しても、すぐにこちらのジョブが始まるので、GPU が遊びません。

7.2 評価から集計まで、依存関係でまとめて予約する

依存関係を組み合わせると、次のようなジョブを 1 回のスクリプト実行でまとめて予約できます。

  • ベースモデルの評価
  • 2 つの条件での学習
  • 条件ごとの評価
  • 集計と Discord への投稿

次のスクリプトは、ARC Prize 側で使った投入スクリプトから、ジョブの名前と中身を省いて、組み方だけを残したものです。

submit.sh(名前と中身を省略)
common() {   # ジョブ名, GPU を使うか(0/1), 制限時間。#SBATCH の行はすべて最初のコマンドより前に置く
  echo "#!/bin/bash"
  echo "#SBATCH -J $1 -p gpu -c 4 --mem=16G -t $3 -o $WORK/logs/%j-$1.out"
  if [ "$2" = 1 ]; then echo "#SBATCH --gres=gpu:1"; fi
  case "$1" in train-*) echo "#SBATCH --signal=B:TERM@300";; esac   # 学習は SIGTERM を受けると resume.pt を保存する
  echo "set -euo pipefail; cd $REPO"
}

j1=$( { common eval-base 1 03:00:00; echo "<ベースモデルの評価>"; } | sbatch --parsable )
j2=$( { common train-a 1 08:00:00; echo "<条件 A の学習>"; } | sbatch --parsable )
j3=$( { common train-b 1 08:00:00; echo "<条件 B の学習>"; } \
      | sbatch --parsable --dependency=afterok:$j1 --kill-on-invalid-dep=yes )
j4=$( { common eval-a 1 03:00:00; echo "<条件 A の評価>"; } \
      | sbatch --parsable --dependency=afterok:$j2 --kill-on-invalid-dep=yes )
j5=$( { common eval-b 1 03:00:00; echo "<条件 B の評価>"; } \
      | sbatch --parsable --dependency=afterok:$j3 --kill-on-invalid-dep=yes )
j6=$( { common report 0 00:10:00; echo "<集計と Discord への投稿>"; } \
      | sbatch --parsable --dependency=afterany:$j4:$j5:$j1:$j2:$j3 --kill-on-invalid-dep=no )

ポイントはこのあたりです。

  • 条件 B の学習はベースモデルの評価で作るファイルを使うので、afterok で評価の成功を待ちます。評価は学習が成功したときだけ実行して、集計は afterany で何があっても最後に実行します
  • --kill-on-invalid-dep=yes を付けておくと、前のジョブが失敗して依存関係を満たせなくなったジョブを、Slurm がキューから消してくれます
  • --signal=B:TERM@300 は、制限時間の 300 秒前に、バッチスクリプトのシェルへ SIGTERM を送る指定です。学習側は SIGTERM を受けると resume.pt を保存します
  • 集計ジョブは GPU を使わないので、--gres を付けずに CPU だけのジョブにしています
  • #SBATCH の行を含むスクリプトを関数の出力として組み立てて、標準入力から sbatch に渡しています。ちょっとトリッキーですが、意外と快適です

7.3 優先度を変える(未検証)

コンペごとに優先度を変えたいときの手段は、次のとおりです。今回は使っていないので、Slurm のドキュメントで調べた内容になります。

  • sbatch --nice=100 や scontrol update job=<ID> nice=100 で、ジョブの優先度を下げられます。負の値で優先度を上げられるのは、特権ユーザーだけです(sbatch、scontrol)
  • scontrol top <ID> で、同じユーザー・パーティション・アカウント・QOS のジョブの中で、指定したジョブを先頭に移せます。QOS は、ジョブの種類ごとに優先度や上限を決める設定のことです。一般ユーザーには既定で無効なので、slurm.conf に SchedulerParameters=enable_user_top を書く必要があります
  • root なら、scontrol update job=<ID> priority=<値> で優先度を直接決められます。決めた値は固定されて、優先度プラグインは更新しなくなります。プラグインに戻すには、ジョブを hold してから release します
  • コンペごとにパーティションを分けて PriorityTier を変える方法や、QOS を分ける方法もあります

空いた時間にだけ別の実験を入れたい場合は、優先度の高いジョブのために実行中のジョブを止める preemption という機能もあります。こちらも試していないので、私は投入の前後に条件を確かめる仕組みを自作しました。投げる前に、他のジョブがないこと、nvidia-smi に計算プロセスがないこと、メモリとディスクの空きが足りていることをチェックします。メインの実験が GPU を必要としたら、学習側が自分から終了してくれる作りです。

8. メモリをジョブごとに区切る

cgroup.conf で設定しておくと、Slurm は、ジョブが要求した分を超える CPU・メモリ・GPU を、cgroup(Linux の資源制限の仕組み)で使えなくしてくれます。GPU を要求しなかったジョブからは、GPU が見えません。

これが特に効いてくるのが、メモリが足りなくなったときです。以前、tmux で動かしていた前処理(8 workers、22GB)が、systemd-oomd にユーザーのスコープごと強制終了されたことがありました。ところが、同時に Slurm で動いていた学習は無事だったんです!Slurm のジョブは、systemd-oomd が監視しているユーザーのスコープの外で動いているからですね。それからは、重い前処理にも --mem を付けて Slurm に投げるようにしています。

ただし、Slurm が数えるのは自分のジョブのメモリだけです。Slurm の外で動かしたプロセスの分は数えてくれないので、ジョブの横で大きな処理を動かすと、OOM killer に止められることがあります。そこは気をつけてください。

8.1 GPU ジョブどうしは、メモリを減らしても同時には動かない

GPU が 1 枚なら、GPU を要求するジョブどうしは、メモリ要求をいくら減らしても同時には動きません。メモリの合計が問題になるのは、GPU ジョブの横で、--gres を付けない CPU だけのジョブ(前処理や集計)を動かすときです。

ノードに登録したメモリは 54,000MB です。たとえば学習ジョブが --mem=40G だと、別のコンペの --mem=16G のジョブは横で動けません。40 + 16 が 54 を超えちゃうからです。

8.2 メモリ要求は、memory.peak ではなく anon と shmem で見積もる

では、学習ジョブに本当に必要なメモリはどれくらいなんでしょうか。実行中のジョブの cgroup のファイルを直接読むと分かります。

CG=/sys/fs/cgroup/system.slice/home-ubuntu_slurmstepd.scope/job_<ジョブ ID>
cat $CG/memory.peak                        # 33.9GB
grep -E '^(anon|shmem) ' $CG/memory.stat   # anon 4.5G、shmem 6.2G
cat $CG/memory.events                      # high 0 max 0 oom 0 oom_kill 0

コメントの値は、ある学習ジョブで測ったものです。memory.peak は 33.9GB で、数字だけ見るとかなりの大食いですよね。でも、このうち解放できないのは、プロセスが確保したメモリ(anon)と共有メモリ(shmem)で、合わせて約 11GB だけでした!残りは、mmap で読んでいる画像キャッシュがページキャッシュとして載っていた分で、必要になればいつでも解放できます。memory.events を見ても、制限には一度も近づいていません。

そこで、既定を 28G に下げました。28 + 16 = 44 なので、別のコンペの CPU ジョブも横で動けます。ただし、注意点が 3 つあります。

  • メモリの上限が小さい cgroup ほどページキャッシュが強く回収されるので、下げたあとは 1 step あたりの時間が遅くなっていないかを確かめましょう
  • 学習中より評価中のほうがメモリを使う構成もあります。28G では、学習のあとの推論の途中で OOM killer に強制終了されたモデルがあって、そのモデルは 36G にしました。測るなら、評価まで含めて測るのがおすすめです
  • cgroup はジョブが終わると消えちゃうので、実行中に一定の間隔で値を記録しておきましょう

OOM で強制終了されたときは、学習ログには traceback も FINISHED も出ません。slurmd のログにだけ、次のような行が残ります。

error: Detected 4 oom_kill events in StepId=280.batch.

9. Ray や Kubernetes じゃだめなの?

だめではないんです。ただ、GPU 1 枚のジョブキューとして使うには、どちらも Slurm より手間がかかります。しかも、手間のかかり方が 2 つでかなり違うんですよね。

9.1 Kubernetes は、まずクラスタを組むところから

いちばん大きな差は、Kubernetes だとクラスタの構築から始めることになる点です。Kubernetes は、複数のノードでクラスタを組む前提で作られています。kubeadm で組む場合、既定ではコントロールプレーン(クラスタを管理するノード)に Pod を載せません。1 台で動かすには、この制限(taint)を外す手順が要ります(kubeadm のドキュメント)。Pod どうしが通信するためのネットワークアドオン(CNI)も入れる必要があって、入れるまではクラスタ内の DNS(CoreDNS)も起動しません。

GPU を使うには、さらに NVIDIA の device plugin が要ります。その前提として、nvidia-container-toolkit を入れて、nvidia-container-runtime をコンテナの既定のランタイムに設定します。学習コードも、コンテナイメージにまとめることになります。ジョブを待たせて順に実行したければ、そのうえで Kueue のような、ジョブの受け入れを管理する仕組みも入れます。

k3s や minikube のような 1 台向けの配布版を使えば構築は楽になりますが、device plugin やコンテナイメージ、Kueue が要るのは変わりません。Kaggle の学習を 1 台で順番に回したいだけなのに、やることが多すぎます…。

9.2 Ray は 1 台でもすぐ動くが、キューとしては物足りない

Ray は事情がちょっと違います。Ray のドキュメントには、ノートパソコンでも 1 台のサーバーでも動かせると書いてあって、ray start --head で 1 ノードの Ray が立ち上がります。pip で入るし、root 権限も要りません。インストールの手軽さなら、Ray のほうが上です。Slurm は apt で入れるものの sudo が必要で、munge の鍵や cgroup の設定もありますからね。

でも、Ray との差が出るのは、夜のうちにジョブを何本も並べておく、キューとしての使い方なんです。

1 つ目は、キューが残るかどうか。Slurm は、キューの状態を StateSaveLocation のディレクトリに保存します(slurm.conf)。Ray では、クラスタの状態を持つ GCS(Global Control Service)が、既定ではすべてをメモリに置きます。Ray のドキュメントによると、GCS が止まるとクラスタ全体が止まります。状態を残したければ、外部の Redis か、alpha 版の RocksDB バックエンドが必要です。

2 つ目は、開始をどれだけ待てるか。Ray Jobs でも、エントリーポイントに GPU を予約して投げれば、GPU が空くまで開始を待ってくれます。ただ、この開始待ちにはタイムアウトがあります。issue #41437 では、ジョブが「Job supervisor actor failed to start within 900.0 seconds.」というエラーで失敗しています。上限は RAY_JOB_START_TIMEOUT_SECONDS で変えられますが、大きくしておかないと、前のジョブが何時間も GPU を使っている間に、後ろのジョブが開始待ちのまま失敗しちゃいます。Slurm のジョブは、資源が空くまで何時間でも待ってくれます。

3 つ目は、ジョブどうしの依存関係。Slurm なら --dependency=afterany:<ジョブ ID> と書くだけです。

9.3 1 台なら、Slurm がいちばん入れるものが少ない

というわけで、GPU 1 枚でジョブキューが欲しいなら、Ray や Kubernetes より Slurm のほうが入れるものが少なく済む、というのが私の結論です。Slurm もクラスタ向けの道具ですが、1 台なら、デーモン 2 つと munge、設定ファイル 3 つで動きます。ただし、保守の手間はかかります。10.2 のように、カーネルを更新したあとに sudo で直す場面もありました。

10. ハマりどころ

最後に、運用中にハマったところをまとめておきます。同じ轍を踏まないでくださいね…!

10.1 --gres=gpu:1 を忘れると、GPU を使えない GPU ジョブになる

--gres=gpu:1 を付けずに投げたジョブにも、Slurm は CPU とメモリだけを割り当てて、普通に開始します。cgroup の ConstrainDevices=yes で GPU が見えなくなるので、学習は数秒で "CUDA unavailable" と言って止まります。私の環境では、afterany でつないだ後ろのジョブも同じように止まって、GPU が 2.4 時間空いてしまいました。

紛らわしいのは、scontrol show job の ReqTRES=cpu=8,mem=40G,node=1,billing=8 の欄に、GPU の要求(gres)が出ないことです。この欄を写して sbatch を手で書くと、GPU の指定が抜けます。投入をラッパースクリプトにまとめたのは、これが理由でした。

再投入したあとは、新しいログに device=NVIDIA GeForce RTX 3090 free=21.47GiB のような行が出たのを確かめるまで、直ったとは見なさないようにしています。

10.2 カーネルを更新したら、slurmd が起きてこない

カーネルが上がったのに、NVIDIA のカーネルモジュールが古いカーネル向けしか入っていないと、/dev/nvidia0 ができません。gres.conf に /dev/nvidia0 を手で書いていると、slurmd は gres.conf を読むところでエラーになって、起動しなくなります。

slurmd[4420]: fatal: can't stat gres.conf file /dev/nvidia0: No such file or directory

こうなると、sinfo はノードを down*(停止して応答しない状態)と表示しますし、学習中だったジョブは失われます。復旧には sudo が要ります。私の環境では、次のコマンドで戻りました。

sudo apt install -y linux-modules-nvidia-595-open-7.0.0-38-generic \
  && sudo modprobe nvidia && nvidia-smi \
  && sudo systemctl restart slurmd \
  && sudo scontrol update nodename=home-ubuntu state=resume

カーネルの自動更新を止めるか、再起動のあとに nvidia-smi と sinfo を確かめる習慣をつけておくといいと思います。

10.3 止まったジョブが、まだ RUNNING だと言い張る

マシンがフリーズして戻ってきたあと、学習のプロセスはもうないのに、Slurm が JobState=RUNNING を返し続けたことがありました。nvidia-smi を見ても、デスクトップ画面の分しか出ていません。

10.4 小さな落とし穴あれこれ

  • パーティションの MaxTime を超える --time を付けると、ジョブはエラーにならず、PartitionTimeLimit を理由に PENDING のまま待ち続けます。静かに詰まるのでやっかいです
  • --time は上限であって、所要時間ではありません。8 時間の上限を付けたジョブが 2 時間で終わることもあるので、開始時刻の見積もりに使うとずれます
  • --output のパスは、スクリプトの中の cd より前に、sbatch を実行したディレクトリを基準に開かれます。絶対パスで書いておくと安心です
  • Slurm は VRAM を資源として扱わず、GPU が何枚空いているかしか見ません。画面表示に使っている GPU だと、ブラウザや VS Code も VRAM を使うので要注意です

11. おわりに

この記事では、まず GPU 1 枚で Kaggle の実験をするときに困ることを整理して、Slurm の導入と基本的な使い方を紹介しました。そのうえで、Claude Code や Codex と組み合わせるときの工夫、複数のコンペで 1 枚の GPU を分け合う方法、メモリの見積もり方、ハマりどころをまとめました。

キューに並べておけば GPU は夜のあいだも働いてくれますし、エージェントのセッションが切れても学習は続きます。導入も Claude や Codex に任せられるので、手元の GPU で Kaggle をしている方は、まず「Slurm を入れて」と頼むところから試してみてください!

参考文献

Discussion

元のページを確認する

必要な場合のみ、広告などを含む配信元ページをフレームで表示できます。