LLM 学習チェックポイントの設定をどう決めるか
はじめに
チェックポイントは、学習を中断した地点から再開するために、モデルとオプティマイザの状態をストレージに書き出したものです。1 回に書く量、保存の間隔、書き込みにかけてよい時間が、ストレージに必要な容量と書き込みスループットを決めます。この記事では、これらを決めるための数字を公開資料から整理し、予算内でできるだけ安い FSx for Lustre を見積もる流れを示します。例には、NVIDIA Nemotron 3.5 Super 120B-A12B(以下 Nemotron 3.5 Super)を 32 ノードで学習する場合を使います。
!
実際に大規模な学習を運用していて、チェックポイントの設定やストレージの選び方について現場の知見をお持ちの方がいたら、ぜひコメントで教えてください。とても喜びます。
!
この記事が扱うのは、チェックポイントの書き込みです。Physical AI のように動画を大量に読む学習では、ストレージへの要求は読み込みが中心になり、話が変わります。たとえば Turing の記事は、7 台の車載カメラの動画を PB 級の規模で S3 から直接読むために、Range GET で必要な部分だけを取り、先読みで S3 のレイテンシーを隠しています。デコードの置き場所も設計の対象で、NVIDIA DALI の動画リーダーは GPU のハードウェアデコーダーで動画をデコードします。読み込みの設計は、この記事とは別に検討が必要です。
保存時の書き込み
ZeRO-3 や FSDP、テンソル並列、パイプライン並列、エキスパート並列で学習する場合、モデルとオプティマイザの状態は全ノードに分割されて置かれています。これを PyTorch の Distributed Checkpoint や Megatron-LM の分散チェックポイントで共有ファイルシステムに保存すると、全ノードが同じタイミングで、それぞれが持つ分割をシャードとして書き込みます。データ並列で同じ値を持つ部分は、どれか 1 つのランクだけが書くように重複が除かれます。一方、DDP のように全ランクが同じ状態を持ち、1 つのランクだけがファイルに書く方式では、書き込みスループットの上限はそのノード 1 台で決まります。書き込み先には、EC2 のインスタンスストアとしてノードに付いたローカルの NVMe、全ノードが同じファイルシステムをマウントする FSx for Lustre、オブジェクトストレージの S3 があります。ただし、書き込み先によって必要な仕組みが違います。FSx for Lustre にはファイルシステム向けの書き込みでそのまま書けますが、S3 に直接書くには S3 向けの書き込みの実装が要り、ノードごとのローカル NVMe から再開するには、後の節で扱うローカルチェックポイントの仕組みが要ります。
ローカルの NVMe は、ノードごとに独立しているので、全体の書き込みスループットはノード数に比例して増えます。その代わり、そのノードが使えなくなると、書いたシャードも読めなくなります。FSx for Lustre は全ノードで共有され、ノードを失ってもデータは残ります。全体の書き込みスループットの基準値は、確保したストレージ容量と、作成時に選ぶ容量 1 TiB あたりのスループットの積で決まります。後者を、コンソールではストレージ単位あたりのスループット、API では PerUnitStorageThroughput と呼びます。S3 は容量を事前に確保する必要がなく、ノードを失ってもデータは残ります。
保存の方式には、同期と非同期があります。同期では、ストレージへの書き込みが終わるまで学習を止めます。非同期では、状態を GPU のメモリからホストのメモリへコピーする間だけ学習を止め、ストレージへの書き込みは学習と並行して進めます。どちらの方式でも、書き込みが完了するまでそのチェックポイントは再開に使えず、その間にジョブが中断すると、直近の書き込みが完了したチェックポイントから再開します。
非同期で短くなるのは学習が止まる時間 C です。中断したときに失う学習の時間は、保存の開始から書き込みの完了までの時間 t で決まり、同じ書き込みスループットなら同期と非同期で t はほぼ同じです。非同期で書き込みにかける時間を長くとるほど t は延び、失う学習の時間は長くなります。
設定を決める量
この記事で決める設定は、保存の間隔、同期か非同期か、ストレージの書き込みスループットと容量です。保存の回数を増やすほど学習が止まる時間の合計が増え、減らすほど中断したときに失う学習が増えるので、これらは次の量の関係で決まります。
記号
意味
S
全ノードを合計した、1 回のチェックポイントの大きさ
C
1 回の保存で学習が止まる時間。同期では書き込みの完了まで、非同期ではホストのメモリへのコピーの間
t_a
非同期で、学習を再開してから書き込みが完了するまでの時間。同期では 0
t
保存の開始から書き込みの完了までの時間で、t = C + t_a
t_w
ストレージへの書き込みにかける時間。同期では C、非同期では t_a
T
保存の開始から次の保存の開始までの間隔
M
ジョブの平均中断間隔。故障を含め、チェックポイントからの再開が必要になる中断の平均間隔
R
中断してから学習を再開するまでの時間
\eta
学習時間のうち、保存で止まる時間と中断で失う時間を合わせて無駄になる割合
T^{*}
\eta が最も小さくなる保存間隔
図の下段に並べた 2 つの式は、\eta の近似式と、そこから求めた T^{*} です。どちらも「保存間隔の決め方」の節で導きます。
S を t_w の間に書き終えるには、ストレージが 1 秒あたり S/t_w 以上を書ける必要があります。これが必要な書き込みスループット W の最小値です。t_w は測る値ではなく、同期なら学習を止めてよい時間、非同期なら次の保存までに書き終える時間として、設計する側が決める値です。実際には、書き込みの効率やほかの I/O との競合を見込んで、これより大きく確保します。
W = \frac{S}{t_w}
チェックポイントの大きさ
重みのバイト数
2026 年 11 月に Hugging Face の API で取得した Nemotron 3.5 Super の BF16 版の重みのファイルサイズの合計は約 248.9 GB で、総パラメータ数の約 124.5B で割ると 1 パラメータあたり 2.0 バイトです。
MoE(Mixture of Experts)は、expert と呼ぶ複数の部分ネットワークのうち、トークンごとに一部だけを使うモデルです。1 トークンの計算に使うパラメータを活性パラメータと呼び、モデル名の 120B-A12B は総パラメータ約 120B、活性パラメータ約 12B を表します。保存では使われなかった expert の重みも書き出すので、チェックポイントの大きさは活性パラメータ数ではなく総パラメータ数に比例します。
再開に必要な状態
故障による中断や手動での一時停止で学習を止めたあと、止めた地点から同じように学習を再開するには、重みに加えて、オプティマイザの状態も保存します。混合精度の学習では、計算用の bf16 の重みに加えて、更新の丸め誤差をためないための fp32 の重みを持ちます。この fp32 の重みを master weight と呼びます。オプティマイザに Adam を使う場合、1 パラメータあたりのバイト数は次のとおりです。
状態
バイト数
bf16 の重み
2
fp32 の master weight
4
fp32 の Adam の 1 次と 2 次のモーメント
8
合計
14
この内訳は、Megatron-LM の distributed optimizer のように、bf16 の重みと fp32 の master weight を別々に持つ構成の値です。ZeRO の論文は、fp16 の重みと勾配で同じバイト数の内訳を示していて、勾配の 2 バイトを含めると 16 バイトです。FSDP2 を使う TorchTitan のように、分割して持つ重み自体が fp32 で、bf16 の重みは計算の間だけ作る構成では、保存するのは fp32 の重みとモーメントの 4 + 8 = 12 バイトです。モーメントを bf16 で持つ構成では 2 + 4 + 4 = 10 バイトになります。DeepSeek-V3 の技術報告は、AdamW の 1 次と 2 次のモーメントを bf16 で持ち、master weight を fp32 で持つと書いています。bf16 の重みと bf16 のモーメントだけを持ち、fp32 の重みを持たない構成では 2 + 4 = 6 バイトです。どの値になるかはフレームワークと設定で決まるので、実際に 1 回保存して確かめます。再開には、学習率のスケジューラ、ステップ数、乱数生成器、データの読み取り位置などの状態も必要ですが、これらは重みとオプティマイザの状態に比べて小さいので、大きさの計算では数えていません。
Nemotron 3.5 Super の大きさ
試算に使う Nemotron 3.5 Super の総パラメータ数は約 124.5B で、この値には画像エンコーダーのパラメータも含まれます。1 パラメータあたりのバイト数ごとに、チェックポイントの大きさ S と、32 ノードに均等に分けたときの 1 ノードあたりの大きさは次のとおりです。
保存する状態
1 パラメータあたり
S
1 ノードあたり
bf16 の重みだけ
2 バイト
約 0.25 TB
約 7.8 GB
重みとオプティマイザの状態
14 バイト
約 1.74 TB
約 54 GB
モーメントを bf16 にした場合
10 バイト
約 1.24 TB
約 39 GB
bf16 の重みと bf16 のモーメントだけの場合
6 バイト
約 0.75 TB
約 23 GB
画像エンコーダーを凍結して学習するなら、そのパラメータはオプティマイザの状態を持たないので、bf16 の重みの 2 バイトだけを保存すれば足ります。公開されている例では、awsome-distributed-ai の verl の例が、20B のモデルを GPU 12 枚で学習すると 1 つのチェックポイントが約 117 GB になると書いています。以降の試算では 14 バイトの約 1.74 TB を使い、最後に 6 バイトの場合の結果も示します。
公開事例の保存間隔
保存の間隔についての公開情報は乏しく、モデルの技術報告にはほとんど書かれていません。後の節で見るとおり、フレームワークに既定値がある場合も、時間ではなくステップ数で決まっています。時間で間隔を書いている公開事例は、次の表のように少数です。
出典
間隔
補足
BLOOM 176B
3 時間ごと。学習ログによると、計算機の保守の停止に備えて 100 ステップごとに変更
Hugging Face のブログによると 1 回が 2.3 TB、学習ログによると保存は 40 秒
DeepSeek の Fire-Flyer AI-HPC
5 分
保存と読み込みはどちらも数秒
Meta の信頼性の論文
大規模ジョブの典型は 1 時間
1,024 GPU のジョブの平均故障間隔は 7.9 時間
Unicron
30 分。再計算の時間を比べるために論文が置いた値
128 GPU のクラスターでの評価
ジョブの平均中断間隔
保存の間隔は、1 回の保存で止まる時間と、ジョブがどのくらいの頻度で中断するかの両方で決まります。次の表は、公開されている故障や中断の記録を、1 ノードに GPU 8 枚を積んだ 32 ノードのジョブの平均中断間隔に換算した値です。中断がノード数に比例して起きると仮定し、元の平均間隔を M_0、元のノード数を N_0 として M = M_0 N_0 / 32 で計算しました。ノード数は、論文の GPU 数を 1 ノード 8 GPU として割った値です。元の論文が数えた範囲、たとえば対象のクラスターや期間の取り方で値は変わるので、表の値は目安です。数えている事象は出典ごとに違い、GPU の故障、ハードウェアが原因の再起動、原因を問わない中断が混ざっているので、比べるときは注意が必要です。
出典
規模と期間
32 ノードに換算した平均中断間隔
Meta の信頼性の論文
128 ノード、1,024 GPU のジョブで 7.9 時間
約 32 時間
OPT-175B
124 ノード、60 日とみなした 2 か月で、ハードウェアの障害による手動の再起動が少なくとも 35 回、自動の再起動が 70 回以上
105 回で約 53 時間、35 回で約 159 時間
BLOOM
48 ノードで、GPU の故障が週 1〜2 回
約 126〜252 時間
Llama 3
2,048 ノード、54 日で 466 回の中断。うち想定外が 419 回で、その約 78%、約 327 回がハードウェアの問題と確認されたか、そう疑われた中断
466 回で約 178 時間、419 回で約 198 時間、327 回で約 254 時間
MegaScale
10,000 GPU を超える本番の学習で、3 週間とみなした数週間に 100 回を超える再起動。1,250 ノードとして計算
約 197 時間。GPU 数も回数も「超える」なので目安
Acme
588 ノード、182.5 日とみなした 6 か月で、ハードウェアが原因の故障が 103 件
約 781 時間
ネットワークの障害や、複数のノードがまとめて故障する場合があるので、ノード数に比例させる換算は目安です。ジョブの中断には GPU の故障のほかにネットワークやソフトウェアの障害も含まれるので、GPU の故障率だけからは M を決められません。参考として、NCSA Delta の研究は、GPU 1 枚あたりのメモリエラーの平均間隔を H100 で約 88,768 時間、A100 で約 283,271 時間と報告しています。B200 と B300 の故障率は公開されていません。
保存間隔の決め方
Young の式
保存間隔 T は、学習が止まる時間と、中断で失う学習の時間の合計が最も小さくなるように決めます。学習時間のうち無駄になる割合を \eta とすると、次の式で近似できます。
\eta \approx \frac{C}{T} + \frac{T/2 + t + R}{M}
第 1 項は、保存で学習が止まる分です。間隔 T ごとに C だけ止まるので、止まる割合は C/T です。
第 2 項は、中断で失う分です。1 回の中断で失う時間の期待値を T/2 + t + R とし、中断が平均 M に 1 回起きるので M で割ります。ここで t は、保存を始めてから書き込みが終わり、そのチェックポイントが再開に使えるようになるまでの時間です。
!
ここからの導出は、確率変数と期待値を使います。数理統計になじみがない方は、導出を飛ばして「1 回の中断で失う時間の平均は T/2 + t になる」という結論だけを眺めても、この先を読み進められます。内容は統計検定 2 級くらいなので、学んでおくと性能の見積もりなどいろいろな場面で役に立ちます。
T/2 + t は、次のように求まります。直近の保存を始めた時刻から中断までの経過時間を連続型の確率変数 X とし、中断は間隔のどこでも同じように起きる、つまり X が区間 [0, T) の連続一様分布に従うと仮定します。また、保存は予定どおり t < T で書き終わり、C は T より十分短いとします。
X \sim \mathrm{U}(0, T), \qquad E[X] = \frac{T}{2}, \qquad P(X < t) = \frac{t}{T}
X \ge t なら直近の保存は書き終わっているので、やり直すのは X だけです。X < t なら直近の保存はまだ使えないので、1 つ前の保存まで戻り、さらに間隔 1 つ分の T をやり直します。やり直す時間を L とすると、指示関数 \mathbf{1}{X < t} を使って次のように書けます。
L = X + T \cdot \mathbf{1}{X < t}
期待値をとると、次のようになります。
E[L] = E[X] + T \cdot P(X < t) = \frac{T}{2} + T \cdot \frac{t}{T} = \frac{T}{2} + t
最悪の場合は X が t の直前のときで、L は T + t に近づきます。
これに再開までの時間 R を足したものが、1 回の中断で失う時間です。たとえば T が 45 分、t が 5 分なら、学習のやり直しは平均 22.5 分 + 5 分 = 27.5 分、最悪で 45 分 + 5 分 = 50 分です。
t と R が T によらないとすると、右辺を T で微分して 0 と置いた -C/T^2 + 1/(2M) = 0 から、最適な間隔は次のようになります。
T^{*} \approx \sqrt{2\,C\,M}
この式は Young が 1974 年に示したもので、C \ll T \ll M のときに成り立ちます。C が M に比べて無視できない場合の補正は、Daly が 2006 年に加えています。
!
Young の式と Daly の補正は、HPC のチェックポイント研究で古くから使われてきた式です。もともとは、スーパーコンピューターで長時間の計算を故障に備えてどの間隔で保存するかを決めるためのものでした。大規模な LLM の学習でも同じ考え方が使われていて、Meta の信頼性の論文もこの形の式で保存の間隔を見積もっています。
M は、Llama 3 の換算値の約 178〜254 時間の範囲にある 200 時間とし、R は 15 分とします。
方式
M
C
t
T^{*}
\eta
同期
200 時間
5 秒
5 秒
約 45 分
約 0.50%
同期
200 時間
60 秒
60 秒
約 2.6 時間
約 1.42%
同期
200 時間
300 秒
300 秒
約 5.8 時間
約 3.05%
非同期
200 時間
5 秒
5 分 5 秒
約 45 分
約 0.54%
同期
約 32 時間
5 秒
5 秒
約 18 分
約 1.72%
同期
約 781 時間
5 秒
5 秒
約 88 分
約 0.22%
最後の 2 行は、M を前の節の表で最も短い Meta の換算値と、最も長い Acme の換算値にした場合で、T^{*} は M によって大きく変わります。
式の前提
この式は、故障が互いに独立で、故障の起きやすさが時間によらないこと、つまり故障間隔が指数分布に従うことを前提にしています。Aupy らの論文が引く Heien らの測定では、HPC システムの故障間隔は形状パラメータ 0.58〜0.71 の Weibull 分布に当てはまりました。Weibull 分布は、形状パラメータが 1 のとき指数分布と同じになり、1 より小さいほど故障の直後に次の故障が集中します。同じ論文は、形状パラメータ 0.5、2^{19} プロセスのシミュレーションで、Daly の式の間隔を使うと、論文が提案する間隔の場合より実行時間が約 61%、Young の式では約 50% 長くなったと報告しています。
平均中断間隔も一定ではないので、運用中に記録した中断から M を更新して、間隔を見直します。評価の節目、損失が急に悪化したときの巻き戻し、予約した計算資源の期限などでもチェックポイントから再開するので、これらの時刻に合わせて保存する場合もあります。
フレームワークの既定値
Megatron-LM、それを継承する Megatron Bridge、TorchTitan では、非同期の保存は既定で無効です。有効にしなければ同期で保存され、書き込みが終わるまで学習は止まります。次の表は、2026 年 10 月に取得したコミットのソースコードで確かめた既定値で、リンク先はそのコミットの該当行です。
フレームワーク
保存間隔の既定
非同期保存の既定
Megatron-LM
既定値はなしで、保存先を指定すると間隔の指定が必須
無効。分散チェックポイント形式で --use-persistent-ckpt-worker を指定しないと、有効にしても無効に戻される
Megatron Bridge
Megatron-LM の設定を継承
Megatron-LM の設定を継承するので無効。async_save で有効にする
TorchTitan
500 ステップ
無効
Hugging Face Trainer
500 ステップ
非同期保存の機能はなく、save_steps ごとに同期で保存
ローカル NVMe と複製
前の節の非同期保存を一歩進めると、チェックポイントを短い間隔で各ノードのローカル NVMe に書き、共有ストレージへは長い間隔で書く方法があります。ローカル NVMe は書き込みが速いので、学習が止まる時間と、共有ストレージに必要な書き込みスループットを小さくできます。弱点は、そのノードを失うとローカルに書いた分も失うことです。NVIDIA Resiliency Extension は、このローカルへの保存を提供するライブラリで、ほかのノードに複製を持って冗長性を高める仕組みも備えています。ローカルに書いたチェックポイントを共有ストレージへ移す処理は含まず、共有ストレージへの保存は、通常のチェックポイントとして別に行います。この節では、この方法で何を守れて何を守れないかを整理します。次の図は、保存先ごとに、プロセスの再起動、1 ノードの喪失、クラスター全体の停止のあとでデータが残るかを示しています。
NVIDIA B300 を 8 枚積んだ p6-b300.48xlarge には、3,800 GB の NVMe SSD が 8 本、合計 30,400 GB のインスタンスストアがあり、1 ノードあたり約 54 GB のシャードを何世代分も置けます。共有ストレージへの転送が終わるまでは、そのノードが使えなくなるとシャードを失います。
NVIDIA Resiliency Extension のローカルチェックポイントは、Megatron-LM などの学習ループに組み込んで使い、各ノードの SSD か RAM ディスクに保存し、ほかのノードにも複製を持つことで、1 ノードを失っても複製からシャードを復元します。複製の組み方は設定で決め、保存と再開で同じ設定にする必要があります。
NVIDIA Resiliency Extension は、GPU 1 枚を受け持つプロセスであるランクを組にして、組の中で複製を持ち合います。組の作り方は、組の大きさ F と、組の中のランクの間隔 J の 2 つの設定で決まり、それぞれ replication_factor と replication_jump です。J の既定値は 1 ノードの GPU 数なので、組のランクは別々のノードに置かれ、組の中の F ノードが互いの分の複製をすべて持ちます。複製は既定では無効で、Megatron-LM では --replication で有効にします。F の既定値は NVIDIA Resiliency Extension でも Megatron-LM でも 2 です。ランクの総数が J \times F で割り切れないと、組を作る処理が止まります。p6-b300.48xlarge 32 台ではランクの総数が 256、J が 8 なので、F に使えるのは 1、2、4、8、16、32 です。
組を持っていても、故障したノードの分の複製を作り直す前に、同じ組のほかのノードもすべて失われると、そのシャードは失われます。この確率を、組に固定する NVIDIA Resiliency Extension の置き方と、比較のために複製先をランダムに選ぶ置き方で比べます。前提は次のとおりです。
前提
内容
ノード数
32
組の大きさ F
既定値の 2。原本を含めて 2 か所に置く
最初の故障
ノード A が故障する
\Delta
A の分の複製を作り直すまでの時間。例では 1 時間
M_n
1 ノードが失われてインスタンスストアのデータが読めなくなる平均間隔。例では 6,400 時間で、ジョブの中断がすべてノードの喪失によるとした多めの見積もり
p
ほかの各ノードが \Delta の間に失われる確率で、p \approx \Delta / M_n。例では約 0.016%
独立性
ほかのノードは互いに独立に失われる
シャードの数
A が持つシャードは十分に多い
組に固定する場合
F = 2 では、A と組になるノード B が、A のシャードの複製をすべて持ちます。A のシャードを失うのは、B が \Delta の間に失われるときだけなので、確率は次のとおりです。
P_{\text{組}} = p
複製先をランダムに選ぶ場合
A の各シャードの複製を、ほかの 31 ノードからランダムに選んだ 1 台に置くとします。シャードが十分に多いと、31 ノードのどれにも A のどれかのシャードの複製があります。そのため、31 ノードのどれか 1 台が \Delta の間に失われれば、A のシャードのどれかを失います。31 台のどれも失われない確率は、各ノードが独立に確率 1 - p で残るので (1 - p)^{31} です。A のシャードを失う確率はその余事象なので、次のようになり、p が小さければ 31p で近似できます。
P_{\text{ランダム}} = 1 - (1 - p)^{31} \approx 31\, p
近似は、(1 - p)^{31} を二項定理で展開し、p が小さいときに p^2 以降の項を捨てたものです。
(1 - p)^{31} = 1 - 31p + \binom{31}{2}p^2 - \binom{31}{3}p^3 + \cdots \approx 1 - 31p
31p は、31 台それぞれが失われる確率 p を足した値です。2 台以上が同時に失われる場合を重ねて数えていますが、p が小さければその分は無視できます。前提の表の例の値では、正確な値が約 0.4832%、近似が約 0.4844% です。
前提の表の例の値を入れると、組に固定する場合は約 0.016%、ランダムに置く場合は約 0.48% で、ランダムに置くと約 31 倍になります。例の M_n は、ジョブの平均中断間隔 200 時間を 32 倍した値です。F を 4 にすると、組に固定する場合は組のほかの 3 台がすべて失われるときだけなので p^3、ランダムでは、シャードが十分に多く 3 台の組み合わせのどれにも複製があるとして、p が小さいときの近似で \binom{31}{3} p^3 = 4{,}495\, p^3 になり、どちらも桁違いに小さくなります。その代わり、各ノードが持つ複製の量は F に比例して増えます。
チェックポイントは、シャードが 1 つでも欠けると再開に使えません。そのため、ある世代のシャードを 1 つ失っても全部失っても、その世代は同じように使えなくなります。複数の世代を同じ組に置いている場合は、組を失えばそれらの世代をまとめて失い、共有ストレージにある世代まで戻ります。どちらの置き方でも失う世代の数は同じなので、確率が低い、組に固定する置き方のほうが有利です。置き方によってデータを失う組み合わせの数が変わることは、Copysets の論文が分散ストレージについて示しています。
この計算は故障が独立だという前提に立っているので、ラックや電源の障害で複数のノードがまとめて止まる場合や、クラスター全体が止まる場合には当てはまりません。その場合に備えて、FSx for Lustre か S3 にもコピーを置きます。
データが通る経路
チェックポイントのデータは、GPU のメモリからホストのメモリを経て、ローカル NVMe か、ネットワークインターフェースを通って共有ストレージへ書かれます。経路の途中にはそれぞれ上限があり、いちばん低い上限が書き込みスループットを決めます。
ローカル NVMe は PCIe でつながったノード内のディスクなので、ネットワークの帯域は効かず、ディスクの性能で決まります。S3、S3 Files、EFA を使わない FSx for Lustre は、ENA というネットワークインターフェースで TCP を使って通信します。EFA は、OS を通らずに SRD というプロトコルで通信するネットワークインターフェースで、EFA を有効にした FSx for Lustre との通信や、ノード間の集団通信に使います。
ネットワークインターフェースの帯域は、インスタンスの種類で大きく違います。
インスタンス
ネットワークインターフェースの帯域
c6in.2xlarge
最大 40 Gbps、基準値 12.5 Gbps
g6e.12xlarge
100 Gbps
p6-b300.48xlarge
1 枚目のネットワークカードが 350 Gbps、ほかに 400 Gbps のネットワークカードが 16 枚あり、16 枚の合計は 6,400 Gbps
TCP には、インスタンスの帯域とは別の上限もあります。EC2 の文書によると、同じクラスタープレイスメントグループにない相手との 1 本の TCP 接続は 5 Gbps までです。同じグループの中では 10 Gbps、同じ AZ で ENA Express を使えば 25 Gbps まで上がりますが、いずれにしても 1 本の接続には上限があります。S3 の性能のガイドラインも、多数の接続に分けて並列に送ることを勧めています。FSx for Lustre の文書は、クライアント 1 台あたりの最大スループットを、EFA を使わない場合は 100 Gbps、EFA では 700 Gbps、EFA と GPUDirect Storage では 1,200 Gbps としています。ENA で TCP を使う場合のもう 1 つの上限は、次の節で説明します。
FSx for Lustre の容量と書き込みスループット
FSx for Lustre の SSD を使う Persistent 2 では、ディスクのスループットの基準値は確保したストレージ容量とストレージ単位あたりのスループットの積で決まり、ストレージ単位あたりのスループットは 125、250、500、1,000 MB/s/TiB から選びます。料金は使用量ではなく確保した容量にかかり、ストレージ容量の文書にあるとおり、容量は増やせますが減らせません。
容量は GiB で指定し、EFA を使わない場合は 1,200 GiB も選べます。それより大きい容量の単位はストレージサーバー 1 台あたりの容量で決まります。EFA を使わない場合は 2,400 GiB、EFA を使う場合は 125 MB/s/TiB で 38,400 GiB、250 MB/s/TiB で 19,200 GiB、500 MB/s/TiB で 9,600 GiB、1,000 MB/s/TiB で 4,800 GiB です。スループットの計算では、AWS の文書の例に合わせて 1,000 GiB を 1 TiB として扱います。EFA を使うファイルシステムは、スループットが 10 GB/s を超える場合に推奨されていて、作成後に EFA の有無もストレージ単位あたりのスループットも変えられません。EFA を使わない SSD のファイルシステムは、作成後にストレージ単位あたりのスループットを変更できます。
ストレージ単位あたりのスループットは、ディスクのスループットの基準値です。性能の表によると、125 MB/s/TiB と 250 MB/s/TiB には、ディスクのスループットを一時的に 500 MB/s/TiB まで上げるバーストがあり、500 MB/s/TiB と 1,000 MB/s/TiB にはありません。また、ENA で TCP を使って通信する場合、クライアント 1 台とストレージサーバー 1 台の間のトラフィックは 5 Gbps が上限なので、1 台のクライアントが書けるのは、ストレージサーバーの台数に 5 Gbps を掛けた値までです。
Nemotron 3.5 Super の S を 1.74 TB とすると、ストレージへの書き込みにかける時間 t_w ごとに必要な書き込みスループット W = S / t_w は次のとおりです。
t_w
W
300 秒
約 5.8 GB/s
60 秒
約 29 GB/s
20 秒
約 87 GB/s
同期で保存するなら t_w がそのまま学習の止まる時間 C になるので、C を短くするほど確保する容量と料金が増えます。非同期なら C と t_w を別々に決められます。前回の書き込みが次の保存の開始までに終わっていない場合の動きは、フレームワークで違います。TorchTitan は前回の書き込みが終わるまで待つので、その分だけ学習が止まります。Megatron-LM と Megatron Bridge は警告を出して次の保存を始めるので、学習は止まりませんが、書き終わっていない書き込みが積み重なります。どちらでも重なりを避けるには W \ge S/(T - C) が必要で、T が約 45 分なら約 0.65 GB/s が下限です。t_w を長くすると W は小さくなりますが、t が延びるので中断で失う学習の時間 T + t と \eta は大きくなります。ローカルの NVMe を経由して共有ストレージへ転送する場合も、次の保存までに転送を終える必要があるので、平均で S/T 以上の書き込みスループットが要ります。
!
コラム: 保存で止まる GPU 時間と FSx for Lustre の料金のトレードオフ
同期で保存する場合、C を短くすると FSx for Lustre の料金は増えますが、保存で止まる GPU の時間は減ります。1 年間の学習で両者を比べると次のとおりです。前提は、S が 1.74 TB、p6-b300.48xlarge 32 台、M が 200 時間、T を Young の式で \sqrt{2CM} に置くことです。GPU の時間は、1 年の 8,760 時間に止まる割合 C/T を掛け、2026 年 11 月時点の p6-b300.48xlarge のオレゴンのオンデマンド料金、1 時間 142.416 ドルで金額にしました。FSx for Lustre は、各 C で最も安い構成の月額の 12 か月分で、必要な書き込みスループットが 10 GB/s を超える場合は EFA を使う構成にしています。
C
止まる GPU 時間/年
その金額/年
FSx for Lustre/年
合計/年
30 秒
約 10,200 GPU 時間
約 18.2 万ドル
約 44.9 万ドル
約 63.1 万ドル
60 秒
約 14,500 GPU 時間
約 25.8 万ドル
約 24.2 万ドル
約 50.0 万ドル
120 秒
約 20,500 GPU 時間
約 36.4 万ドル
約 13.8 万ドル
約 50.3 万ドル
300 秒
約 32,400 GPU 時間
約 57.6 万ドル
約 4.9 万ドル
約 62.5 万ドル
実際には GPU は年単位で確保していることが多く、保存で止まっても GPU の支払いは変わりません。その場合、止まる GPU 時間の金額は、その時間で行えたはずの追加の学習を金額に換算した機会費用として読みます。この考え方では、合計は C が 90 秒前後で最も小さく、年に約 46 万ドルです。この比較は保存で止まる時間だけを数えています。中断で失う学習も GPU 時間なので、それも足すと合計は年に約 76 万ドルになり、最も小さくなる C は 50 秒前後に移ります。C を短くしすぎると FSx for Lustre の料金が、長くしすぎると止まる GPU の時間が、合計を押し上げます。GPU の単価が予約や個別の価格で下がれば、最も小さくなる C は長い側へ動きます。非同期で保存できるなら、止まる時間はホストのメモリへのコピーの間だけになります。書き込みにかける時間は学習を止めずに長くとれるので、FSx for Lustre の料金と、中断で失う学習の時間の釣り合いだけで決められ、止まる GPU 時間の費用を小さくできます。Megatron-LM では分散チェックポイント形式と常駐の保存ワーカーが条件になるので、使っているフレームワークで有効にできるなら、非同期で保存するのがよいと考えます。
確保する容量は、残す世代数を k、ストレージ単位あたりのスループットを \tau、容量の単位を u として、次の式で決まります。容量は GiB で比べ、kS はバイト数を 2^{30} で割って GiB にし、W/\tau は MB/s を MB/s/TiB で割った値を 1,000 倍して GiB とします。
\text{確保する容量} = u \left\lceil \frac{\max\left(k S,\ W/\tau\right)}{u} \right\rceil
Nemotron 3.5 Super で FSx for Lustre を見積もる
見積もりは、チェックポイントの設定を決め、そこから FSx for Lustre に必要な書き込みスループットと容量を逆算し、最も安い構成の月額を求める順に進めます。式はこれまでの節で説明したとおりで、計算の過程はこの節の最後の折りたたみにまとめました。
見積もる人が決める値
次の値を決めれば、見積もりができます。既存の環境で測れる値は測り、測れない値は右の概算から始めます。
値
決め方
測れないときの概算
この例の値
総パラメータ数
モデルの定義から読む
なし
約 124.5B
1 パラメータあたりのバイト数
小さい構成で 1 回保存し、チェックポイントの大きさを総パラメータ数で割る
bf16 の重み、fp32 の master weight、fp32 のモーメントなら 14、fp32 の重みとモーメントなら 12、モーメントが bf16 なら 10
14
ノード数
学習の計画から決める
なし
32
同期か非同期か
使うフレームワークで非同期を有効にできるかで決める
なし
非同期
保存で止まる時間 C
1 回保存し、学習のステップが止まった時間を測る
非同期は 1 ノード分を GPU からホストへ写す時間で、MegaScale と Fire-Flyer はどちらも数秒と報告している。同期は t_w と同じ
5 秒程度
書き込みにかける時間 t_w
予算と、中断で失ってよい学習の時間から決める
なし
5 分または 20 分
平均中断間隔 M
既存のクラスターのジョブの中断を記録する
ジョブの平均中断間隔の節の表から選ぶ
200 時間
再開までの時間 R
中断から再開までを 1 回試して測る
10 分にチェックポイントの読み込み時間を足す
約 15 分。FSx for Lustre の構成で変わる
残す世代数 k
何世代前まで戻れればよいかで決める
完了した 3 世代と書き込み中の 1 世代
4
EFA を使うか
必要な書き込みスループットが 10 GB/s を超えるなら、AWS の推奨に合わせて使う
なし
使わない
料金
使うリージョンの料金ページで確かめる
なし
オレゴン
見積もりの結果
左のグラフは、書き込みにかける時間 t_w ごとに、FSx for Lustre の最も安い構成の月額を示します。右のグラフは、同じ t_w で、同期と非同期それぞれの無駄になる割合 \eta を示します。
t_w を延ばすほど必要な書き込みスループットが下がり、FSx for Lustre は安くなります。容量が残す世代を置く量で決まるところまで下がると、それ以上は安くなりません。
EFA を使う構成は容量の単位が大きいので、同じ書き込みスループットなら、EFA を使わない構成と同じか高くなります。10 GB/s を超える範囲では、AWS の推奨に合わせて EFA を使う構成で計算しています。
同期では t_w がそのまま学習の止まる時間になるので、\eta は t_w とともに急に増えます。非同期では止まる時間が数秒のままなので、t_w を延ばしても \eta はゆっくりしか増えません。
主な組み合わせの結果は次のとおりです。「最も安い構成」の列は、FSx for Lustre を作るときに選ぶ 2 つの値で、たとえば「250 MB/s/TiB、7,200 GiB」は、ストレージ単位あたりのスループットを 250 MB/s/TiB に、ストレージ容量を 7,200 GiB にすることを意味します。この構成の書き込みスループットの基準値は、7,200 GiB を 7.2 TiB として 250 MB/s/TiB を掛けた 1.8 GB/s です。250 MB/s/TiB 以下ではバーストで一時的にこれを超えられますが、見積もりでは基準値を使います。容量は、必要な書き込みスループットから出る容量と、残す世代を置く容量の大きいほうで決まるので、世代を置く容量で決まった場合は、基準値が必要な書き込みスループットより大きくなります。
チェックポイントの設定
必要な書き込みスループット
最も安い構成
書き込みスループットの基準値
容量を決めたもの
月額
\eta
14 バイト、非同期、20 分で書き終える
約 1.45 GB/s
250 MB/s/TiB、7,200 GiB
1.8 GB/s
世代を置く容量
約 1,500 ドル
約 0.76%
14 バイト、非同期、5 分で書き終える
約 5.8 GB/s
500 MB/s/TiB、12,000 GiB
6.0 GB/s
書き込みスループット
約 4,100 ドル
約 0.54%
6 バイト、非同期、5 分で書き終える
約 2.5 GB/s
500 MB/s/TiB、7,200 GiB
3.6 GB/s
書き込みスループット
約 2,400 ドル
約 0.53%
14 バイト、同期、60 秒で書き終える
約 29 GB/s
EFA あり、1,000 MB/s/TiB、33,600 GiB
33.6 GB/s
書き込みスループット
約 20,200 ドル
約 1.39%
この例では、非同期で 45 分ごとに保存し、20 分で書き終えればよいとすると、250 MB/s/TiB の 7,200 GiB、月額約 1,500 ドルで予算を見積もれます。中断で失う学習を減らしたい場合は、書き終える時間を 5 分に縮めて月額約 4,100 ドルで見積もります。
計算の過程: 14 バイト、非同期、5 分で書き終える場合
ノード数を N、総パラメータ数を P、1 パラメータあたりのバイト数を b とします。
チェックポイントの大きさと 1 ノードあたりの大きさ
S = P \times b = 124.45 \times 10^{9} \times 14 \approx 1.74\ \text{TB}, \qquad \frac{S}{N} = \frac{1.74\ \text{TB}}{32} \approx 54\ \text{GB}
保存で止まる時間 C を 5 秒にするのに必要な、1 ノードあたりの GPU からホストへの転送
\frac{S/N}{C} = \frac{54\ \text{GB}}{5\ \text{s}} \approx 11\ \text{GB/s}
保存間隔
T^{*} = \sqrt{2CM} = \sqrt{2 \times 5\ \text{s} \times 720{,}000\ \text{s}} \approx 2{,}683\ \text{s} \approx 45\ \text{min}
必要な書き込みスループット
W = \frac{S}{t_w} = \frac{1.74\ \text{TB}}{300\ \text{s}} \approx 5.8\ \text{GB/s}
確保する容量。kS を GiB にし、W/\tau は MB/s を MB/s/TiB で割った値を 1,000 倍して GiB にします。\tau = 500 MB/s/TiB、u = 2{,}400 GiB では次のとおりです。
kS = \frac{4 \times 1.742\ \text{TB}}{2^{30}} \approx 6{,}491\ \text{GiB}, \qquad \frac{W}{\tau} \times 1{,}000 = \frac{5{,}808}{500} \times 1{,}000 \approx 11{,}616\ \text{GiB}
u \left\lceil \frac{\max(6{,}491,\ 11{,}616)}{2{,}400} \right\rceil = 2{,}400 \times 5 = 12{,}000\ \text{GiB}
月額。オレゴンの 500 MB/s/TiB の料金は GiB 月あたり 0.340 ドルです。
12{,}000\ \text{GiB} \times 0.340\ \text{USD} = 4{,}080\ \text{USD/month}
再開までの時間と無駄になる割合。この構成のスループットの基準値は 12{,}000 / 1{,}000 \times 500 = 6{,}000 MB/s で、読み込みも同じ程度出ると仮定すると、約 290 秒です。
R = 600 + \frac{1.74\ \text{TB}}{6.0\ \text{GB/s}} \approx 890\ \text{s}, \qquad t = C + t_w = 305\ \text{s}
\eta = \frac{C}{T} + \frac{T/2 + t + R}{M} = \frac{5}{2{,}683} + \frac{1{,}341 + 305 + 890}{720{,}000} \approx 0.54\%
中断で失う学習の時間の上限は、T + t \approx 2{,}683 + 305 \approx 2{,}988 秒、約 50 分です。
ほかのストレージ単位あたりのスループットと EFA の有無で同じ計算をすると、次のようになります。
ストレージ単位あたりのスループット
EFA なし
EFA あり
125 MB/s/TiB
48,000 GiB、約 7,000 ドル/月
76,800 GiB、約 11,100 ドル/月
250 MB/s/TiB
24,000 GiB、約 5,000 ドル/月
38,400 GiB、約 8,100 ドル/月
500 MB/s/TiB
12,000 GiB、約 4,100 ドル/月
19,200 GiB、約 6,500 ドル/月
1,000 MB/s/TiB
7,200 GiB、約 4,300 ドル/月
9,600 GiB、約 5,800 ドル/月
まとめ
FSx for Lustre をできるだけ安く見積もるには、先にチェックポイントの大きさ、保存間隔、書き込みにかける時間を決め、そこから必要な書き込みスループットと容量を逆算します。Nemotron 3.5 Super を 32 ノードで学習する例では、非同期で 45 分ごとに保存し、20 分で書き終えればよいとすると、250 MB/s/TiB の 7,200 GiB、月額約 1,500 ドルで足ります。書き込みにかける時間を延ばすほど FSx for Lustre は安くなりますが、中断で失う学習と再開の時間は延びるので、その釣り合いで決めます。ここでの値はどれも試算なので、実際の構成で保存と再開の時間、中断の頻度、書き込みスループットを測って置き換えてください。
Compiling and running my project (Rust, wgpu & winit)
Preample
Installed NixOS last week, been liking it! I have an issue with OBS, more on that in another post, and then this issue, but everything else has been great.
The issue
When I compile and run my project, I crash with the following output. The program compiles fine, but crashes immediately upon startup when creating the window.
thread 'main' (66200) panicked at src/window.rs:147:55:
called `Result::unwrap()` on an `Err` value: CreateSurfaceError { inner: Hal(FailedToCreateSurfaceForAnyBackend({})) }
[details="Full backtrace (RUST_BACKTRACE=full)"]
thread 'main' (66276) panicked at src/window.rs:147:55:
called `Result::unwrap()` on an `Err` value: CreateSurfaceError { inner: Hal(FailedToCreateSurfaceForAnyBackend({})) }
stack backtrace:
0: 0x5c8010839b71 - std[68c81ae915296b05]::backtrace_rs::backtrace::libunwind::trace
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
1: 0x5c8010839b71 - std[68c81ae915296b05]::backtrace_rs::backtrace::trace_unsynchronized::<std[68c81ae915296b05]::sys::backtrace::_print_fmt::{closure#1}>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
2: 0x5c8010839b71 - std[68c81ae915296b05]::sys::backtrace::_print_fmt
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:74:9
3: 0x5c8010839b71 - <<std[68c81ae915296b05]::sys::backtrace::BacktraceLock>::print::DisplayBacktrace as core[efd5e57aef94d71e]::fmt::Display>::fmt
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:44:26
4: 0x5c801085404a - <core[efd5e57aef94d71e]::fmt::rt::Argument>::fmt
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/fmt/rt.rs:152:76
5: 0x5c801085404a - core[efd5e57aef94d71e]::fmt::write
6: 0x5c8010840dec - core[efd5e57aef94d71e]::io::write::default_write_fmt::<std[68c81ae915296b05]::sys::stdio::unix::Stderr>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/io/write.rs:402:11
7: 0x5c8010840dec - <std[68c81ae915296b05]::sys::stdio::unix::Stderr as core[efd5e57aef94d71e]::io::write::Write>::write_fmt
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/io/write.rs:335:13
8: 0x5c8010815bc6 - <std[68c81ae915296b05]::sys::backtrace::BacktraceLock>::print
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:47:9
9: 0x5c8010815bc6 - std[68c81ae915296b05]::panicking::default_hook::{closure#0}
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:292:27
10: 0x5c8010831a51 - std[68c81ae915296b05]::panicking::default_hook
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:319:9
11: 0x5c8010831c0b - std[68c81ae915296b05]::panicking::panic_with_hook
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:815:13
12: 0x5c8010815c72 - std[68c81ae915296b05]::panicking::panic_handler::{closure#0}
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:688:13
13: 0x5c801080da59 - std[68c81ae915296b05]::sys::backtrace::__rust_end_short_backtrace::<std[68c81ae915296b05]::panicking::panic_handler::{closure#0}, !>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:182:18
14: 0x5c8010816aed - __rustc[3d2f79520d6fa809]::rust_begin_unwind
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:679:5
15: 0x5c801085491c - core[efd5e57aef94d71e]::panicking::panic_fmt
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/panicking.rs:80:14
16: 0x5c8010854642 - core[efd5e57aef94d71e]::result::unwrap_failed
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/result.rs:1871:5
17: 0x5c800f5f836d - <core[efd5e57aef94d71e]::result::Result<wgpu[72de333dbba5b64b]::api::surface::Surface, wgpu[72de333dbba5b64b]::api::surface::CreateSurfaceError>>::unwrap
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/result.rs:1232:23
18: 0x5c800f5f836d - <Peanuts[5daf6c7432f9e7d0]::window::State>::new::{closure#0}
at /home/neropatti/Projects/GIT/Peanuts/src/window.rs:147:55
19: 0x5c800f611a31 - pollster[1817c31098131509]::block_on::<<Peanuts[5daf6c7432f9e7d0]::window::State>::new::{closure#0}>
at /home/neropatti/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pollster-0.4.0/src/lib.rs:126:28
20: 0x5c800f5dfdd3 - Peanuts[5daf6c7432f9e7d0]::main
at /home/neropatti/Projects/GIT/Peanuts/src/main.rs:32:21
21: 0x5c800f5a8c2b - <fn() as core[efd5e57aef94d71e]::ops::function::FnOnce<()>>::call_once
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/ops/function.rs:250:5
22: 0x5c800f59837e - std[68c81ae915296b05]::sys::backtrace::__rust_begin_short_backtrace::<fn(), ()>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:166:18
23: 0x5c800f5adf41 - std[68c81ae915296b05]::rt::lang_start::<()>::{closure#0}
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/rt.rs:206:18
24: 0x5c8010830c69 - <&dyn core[efd5e57aef94d71e]::ops::function::Fn<(), Output = i32> + core[efd5e57aef94d71e]::panic::unwind_safe::RefUnwindSafe + core[efd5e57aef94d71e]::marker::Sync as core[efd5e57aef94d71e]::ops::function::FnOnce<()>>::call_once
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/ops/function.rs:287:21
25: 0x5c8010830c69 - std[68c81ae915296b05]::panicking::catch_unwind::do_call::<&dyn core[efd5e57aef94d71e]::ops::function::Fn<(), Output = i32> + core[efd5e57aef94d71e]::panic::unwind_safe::RefUnwindSafe + core[efd5e57aef94d71e]::marker::Sync, i32>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:576:43
26: 0x5c8010830c69 - std[68c81ae915296b05]::panicking::catch_unwind::<i32, &dyn core[efd5e57aef94d71e]::ops::function::Fn<(), Output = i32> + core[efd5e57aef94d71e]::panic::unwind_safe::RefUnwindSafe + core[efd5e57aef94d71e]::marker::Sync>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:544:19
27: 0x5c8010830c69 - std[68c81ae915296b05]::panic::catch_unwind::<&dyn core[efd5e57aef94d71e]::ops::function::Fn<(), Output = i32> + core[efd5e57aef94d71e]::panic::unwind_safe::RefUnwindSafe + core[efd5e57aef94d71e]::marker::Sync, i32>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panic.rs:359:14
28: 0x5c8010830c69 - std[68c81ae915296b05]::rt::lang_start_internal::{closure#0}
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/rt.rs:175:24
29: 0x5c8010830c69 - std[68c81ae915296b05]::panicking::catch_unwind::do_call::<std[68c81ae915296b05]::rt::lang_start_internal::{closure#0}, isize>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:576:43
30: 0x5c8010830c69 - std[68c81ae915296b05]::panicking::catch_unwind::<isize, std[68c81ae915296b05]::rt::lang_start_internal::{closure#0}>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:544:19
31: 0x5c8010830c69 - std[68c81ae915296b05]::panic::catch_unwind::<std[68c81ae915296b05]::rt::lang_start_internal::{closure#0}, isize>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panic.rs:359:14
32: 0x5c8010830c69 - std[68c81ae915296b05]::rt::lang_start_internal
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/rt.rs:171:5
33: 0x5c800f5adf27 - std[68c81ae915296b05]::rt::lang_start::<()>
at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/rt.rs:205:5
34: 0x5c800f5dfece - main
35: 0x759f8822b285 - __libc_start_call_main
36: 0x759f8822b338 - __libc_start_main@GLIBC_2.2.5
37: 0x5c800f583be5 - _start
38: 0x0 - <unknown>
[/details]
You can find all of the code on the repository, but here are some things of interest:
[details="shell.nix"]
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
# nativeBuildInputs is usually what you want -- tools you need to run
nativeBuildInputs = with pkgs.buildPackages; [
gcc
#python3
];
LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath [
libGL # Didn't need this to get past the previous error, but this might be needed! Idk!
libxkbcommon
wayland
];
}
[/details]
[details="Cargo.toml"]
[package]
name = "Peanuts"
version = "0.1.0"
edition = "2024"
[dependencies]
env_logger = "0.11.9"
log = "0.4.29"
pollster = "0.4.0"
wgpu = "28.0.0"
winit = "0.30.13"
num-traits = "0.2.19"
[dependencies.image]
version = "0.25.2"
default-features = false
features = ["png"]
[/details]
[details="The line that crashes"]
impl<'a> State<'a> {
// Creating some of the wgpu types requires async code
pub async fn new(window: &'a Window) -> State<'a> {
let size = window.inner_size();
// The instance is a handle to our GPU
// Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
// # Safety
//
// The surface needs to live as long as the window that created it.
// State owns the window, so this should be safe.
let surface = instance.create_surface(window).unwrap(); // <--- Crash happens here
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
[/details]
Additional details
- The repository I linked is an earlier version of my actual project, with unnecessary things cut out
- Some of the code originates from this awesome wgpu tutorial
[details="My system's configuration.nix"]
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ config, pkgs, ... }:
{
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
./nvidia.nix
];
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "nixos"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Enable networking
networking.networkmanager.enable = true;
# Set your time zone.
time.timeZone = "Europe/Helsinki";
# Select internationalisation properties.
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "fi_FI.UTF-8";
LC_IDENTIFICATION = "fi_FI.UTF-8";
LC_MEASUREMENT = "fi_FI.UTF-8";
LC_MONETARY = "fi_FI.UTF-8";
LC_NAME = "fi_FI.UTF-8";
LC_NUMERIC = "fi_FI.UTF-8";
LC_PAPER = "fi_FI.UTF-8";
LC_TELEPHONE = "fi_FI.UTF-8";
LC_TIME = "fi_FI.UTF-8";
};
# Enable the X11 windowing system.
# You can disable this if you're only using the Wayland session.
services.xserver.enable = true;
# Enable plasma
services.displayManager.sddm.enable = true;
services.desktopManager.plasma6.enable = true;
# Configure keymap in X11
services.xserver.xkb = {
layout = "fi";
variant = "nodeadkeys";
};
# Configure console keymap
console.keyMap = "fi";
# Enable CUPS to print documents.
services.printing.enable = true;
# Enable sound with pipewire.
services.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
# jack.enable = true;
};
# Enable touchpad support (enabled default in most desktopManager).
# services.libinput.enable = true;
# Define a user account. Don't forget to set a password with ‘passwd’.
users.users."neropatti" = {
isNormalUser = true;
description = "neropatti";
extraGroups = [ "networkmanager" "wheel" ];
packages = with pkgs; [
kdePackages.kate
obs-studio
obsidian
rustup
prismlauncher
mpv
gimp
pciutils
ffmpeg
];
};
programs.obs-studio.package = (pkgs.obs-studio.override {
cudaSupport = true;
});
programs.ssh.startAgent = true;
#boot.initrd.availableKernelModules = [
# "nvidia_drm" "nvidia_modeset" "nvidia" "nvidia_uvm"
#];
programs.thunderbird.enable = true;
programs.steam.enable = true;
# Install firefox.
programs.firefox.enable = true;
# Allow unfree packages
nixpkgs.config.allowUnfree = true;
# List packages installed in system profile.
# You can use https://search.nixos.org/ to find more packages (and options).
environment.systemPackages = with pkgs; [
home-manager
wget
git
qdirstat
];
services.displayManager.autoLogin.enable = true;
services.displayManager.autoLogin.user = "neropatti";
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
# programs.gnupg.agent = {
# enable = true;
# enableSSHSupport = true;
# };
# List services that you want to enable:
# Enable the OpenSSH daemon.
# services.openssh.enable = true;
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "26.05"; # Did you read the comment?
}
[/details]
[details="nvidia.nix"]
{ pkgs, config, libs, ... }:
{
# Enable OpenGL
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
nvidia-vaapi-driver
];
};
# Load nvidia driver for Xorg and Wayland
services.xserver.videoDrivers = ["nvidia"];
hardware.nvidia = {
# Modesetting is required.
modesetting.enable = true;
# Nvidia power management. Experimental, and can cause sleep/suspend to fail.
powerManagement.enable = false;
# Fine-grained power management. Turns off GPU when not in use.
# Experimental and only works on modern Nvidia GPUs (Turing or newer).
powerManagement.finegrained = false;
# Use the NVidia open source kernel module (not to be confused with the
# independent third-party "nouveau" open source driver).
# Support is limited to the Turing and later architectures. Full list of
# supported GPUs is at:
# https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus
# Only available from driver 515.43.04+
# Currently alpha-quality/buggy, so false is currently the recommended setting.
open = false;
# Enable the Nvidia settings menu,
# accessible via `nvidia-settings`.
nvidiaSettings = true;
# Optionally, you may need to select the appropriate driver version for your specific GPU.
package = config.boot.kernelPackages.nvidiaPackages.legacy_580;
# 0a:00.0 VGA compatible controller: NVIDIA Corporation GP106 [GeForce GTX 1060 6GB] (rev a1)
# 0a:00.1 Audio device: NVIDIA Corporation GP106 High Definition Audio Controller (rev a1)
prime = {
# Make sure to use the correct Bus ID values for your system!
#intelBusId = "PCI:00:02.0";
nvidiaBusId = "PCI:0a:00.0";
};
};
}
[/details]
If there's any info I forgot to provide, or anything else I could have done better, let me know! Thank you for taking the time to read this.
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 を順番に分け合えます。優先度も変えられます
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 じゃだめなの?
!
Ray と Kubernetes は今回試していません。この章は、ドキュメントと issue を読んで比べた内容です。
だめではないんです。ただ、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 の指定が抜けます。投入をラッパースクリプトにまとめたのは、これが理由でした。
!
ジョブが GPU を持っているかは、ReqTRES ではなく、scontrol show job の TresPerNode=gres/gpu:1 か、scontrol show node -d の GresUsed=gpu:1 で確かめてください。ReqTRES に GPU がなくても、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 を見ても、デスクトップ画面の分しか出ていません。
!
再起動やフリーズのあとは、squeue の表示を、GPU が働いている証拠にしないでください。ログが進んでいるか、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 を入れて」と頼むところから試してみてください!
参考文献
ymg_aq「手元のGPUを遊ばせないためのジョブスケジューラ入門」Tech Blog - Turing(Zenn)
Slurm: slurm.conf、sbatch、scontrol、Quality of Service (QOS)
Ray: Starting Ray、GCS Fault Tolerance、KubeRay の RocksDB による GCS の耐障害性
ray-project/ray issue #41437
Kubernetes: Creating a cluster with kubeadm
NVIDIA/k8s-device-plugin
Kueue Overview
sometimes i scroll thru cutlass issues to read wild bug reports cutlass#3797
45 likes · 1 repost · 1,779 views
Interactive World Simulation with Cosmos for Physical AI
We present a shared framework that adapts Cosmos world foundation models for real-time interactive world simulation in Physical AI. By streaming visual observations in response to actions and scene controls, the framework enables users and embodied agents to interact with evolving virtual environments. It supports both predicting the visual consequences of actions and generating observations from physical state maintained by an external simulator. We demonstrate this approach across camera navigation and robot manipulation, controllable video transfer, and multi-view autonomous vehicle (AV) simulation. These examples provide a shared foundation for developing interactive virtual environments for Physical AI training and evaluation. Our code and model checkpoints are available at nvidia/cosmos and huggingface.co. The project website is available at research.nvidia.com.
Comments: Project website: research.nvidia.com
NVIDIAのOSS「OpenShell」:自律AIエージェントを権限で閉じ込める実行環境
!海外の記事から
!🚀 上級者ワークフロー
📰 海外事例
元記事:OpenShell – A safe, private runtime for fleets of autonomous AI agents(github.com)
NVIDIA/OpenShell
---
!ざっくり概要
NVIDIAがGitHubで公開している OpenShell(Apache License 2.0)の紹介です。ファイルを読み、パッケージを入れ、APIを呼び、認証情報も使う自律型のAIエージェントを、データ・秘密情報・ネットワークへの無制限なアクセスなしに動かすための実行環境です。各エージェントが触れる範囲を「ポリシー」に宣言しておくと、OpenShellがそれを強制します。ここで紹介するのはREADMEに書かれた範囲で、実際には動かしていません。
<br>
!記事の要点
二つの守り方
一つ目はカーネルレベルの強制です。エージェントは隔離されたサンドボックスの中で動き、アクセスできるファイルと使えるシステムコール(OSへの命令)がカーネルの機能で絞られます。ネットワーク接続もすべて、外へ出る前にポリシーの検査を通ります。エージェントは本物の認証情報を見ることがなく、承認済みの宛先へ向かう通信にだけ、OpenShellが認証情報を付け足します。
二つ目は、ポリシー変更の形式検証です。ポリシーを変えて新しいホストへ認証情報つきで接続できるようになる、新しいAPIメソッドを呼べるようになる、といった危うい権限拡大を、承認の前に数学的な検証(形式検証)で見つけます。該当する変更は人の確認待ちになります。
使い始め
Linux、Apple SiliconのMac、WSL 2上のWindows(実験的)で動き、Docker・Podman・仮想化のいずれかが要ります。READMEのクイックスタートは、インストーラーを実行して openshell sandbox create --name demo を打つだけです。初期のサンドボックスはエージェントなしの最小のUbuntuで、実際のエージェントはチュートリアルで入れます。そこでは OpenCode を無料のOpenRouterモデルで動かし、エージェントが新しい権限を必要とするたびに承認していく流れが示されています。
周辺の仕組み
制御役のゲートウェイをKubernetes(コンテナを大量に管理する基盤)へ置く構成や、各言語の開発キット(SDK)も用意されています。
気をつけたい点
バージョンは0.1系で、公式が安定版の運用や新しい隔離機能を整えている途中です。また、匿名の利用統計を集める設定が標準で入っています。集めるのは操作の種別と回数だけで、サンドボックス名・ファイルパス・プロンプト・認証情報・モデル名は含まないと書かれています。止めたいときは環境変数 OPENSHELL_TELEMETRY_ENABLED=false を設定します。
<br>
!編集者のひとこと
自律エージェントで一番気をつけるのは、どこまで読ませて、どこまで動かさせるかだと思います。その封じ込めの環境をNVIDIAが用意しているのが面白い。
特に引かれたのは、OpenCodeを無料のOpenRouterモデルで動かす例です。無料モデルは入力が学習に使われるのが基本だと理解しているので、読ませてはいけない範囲は最初から閉じておきたい。OpenShellで封じ込めた状態から始めて、必要な権限だけ足していけば、初手で読み込まれすぎる事故は避けられる。そこがポイントかなと。
1 like
I got so much fucking swag even NVIDIA copying my shit
helixdb/helix-foundry
19 likes · 1 repost · 1,982 views
I had to go digging into the source to realize what it was, e.g.
github.com/NVIDIA/auto-ontology/blob/main/auto_on…
finally got to try nvidia openshell to sandbox my intern after having plugged off my dgx spark out of security scares
NVIDIA/OpenShell
will transfer to my dgx up next t.co
32 likes · 1 repost · 2,045 views
Nvidia – open-source auto-ontology | Discussion
2 likes
Mentions
LLM 学習チェックポイントの設定をどう決めるか はじめに チェックポイントは、学習を中断した地点から再開するために、モデルとオプティマイザの状態をストレージに書き出したものです。1 回に書く量、保存の間隔、書き込みにかけてよい時間が、ストレージに必要な容量と書き込みスループットを決めます。この記事では、これらを決めるための数字を公開資料から整理し、予算内でできるだけ安い FSx for Lustre を見積もる流れを示します。例には、NVIDIA Nemotron 3.5 Super 120B-A12B(以下 Nemotron 3.5 Super)を 32 ノードで学習する場合を使います。 ! 実際に大規模な学習を運用していて、チェックポイントの設定やストレージの選び方について現場の知見をお持ちの方がいたら、ぜひコメントで教えてください。とても喜びます。 ! この記事が扱うのは、チェックポイントの書き込みです。Physical AI のように動画を大量に読む学習では、ストレージへの要求は読み込みが中心になり、話が変わります。たとえば Turing の記事は、7 台の車載カメラの動画を PB 級の規模で S3 から直接読むために、Range GET で必要な部分だけを取り、先読みで S3 のレイテンシーを隠しています。デコードの置き場所も設計の対象で、NVIDIA DALI の動画リーダーは GPU のハードウェアデコーダーで動画をデコードします。読み込みの設計は、この記事とは別に検討が必要です。 保存時の書き込み ZeRO-3 や FSDP、テンソル並列、パイプライン並列、エキスパート並列で学習する場合、モデルとオプティマイザの状態は全ノードに分割されて置かれています。これを PyTorch の Distributed Checkpoint や Megatron-LM の分散チェックポイントで共有ファイルシステムに保存すると、全ノードが同じタイミングで、それぞれが持つ分割をシャードとして書き込みます。データ並列で同じ値を持つ部分は、どれか 1 つのランクだけが書くように重複が除かれます。一方、DDP のように全ランクが同じ状態を持ち、1 つのランクだけがファイルに書く方式では、書き込みスループットの上限はそのノード 1 台で決まります。書き込み先には、EC2 のインスタンスストアとしてノードに付いたローカルの NVMe、全ノードが同じファイルシステムをマウントする FSx for Lustre、オブジェクトストレージの S3 があります。ただし、書き込み先によって必要な仕組みが違います。FSx for Lustre にはファイルシステム向けの書き込みでそのまま書けますが、S3 に直接書くには S3 向けの書き込みの実装が要り、ノードごとのローカル NVMe から再開するには、後の節で扱うローカルチェックポイントの仕組みが要ります。 ローカルの NVMe は、ノードごとに独立しているので、全体の書き込みスループットはノード数に比例して増えます。その代わり、そのノードが使えなくなると、書いたシャードも読めなくなります。FSx for Lustre は全ノードで共有され、ノードを失ってもデータは残ります。全体の書き込みスループットの基準値は、確保したストレージ容量と、作成時に選ぶ容量 1 TiB あたりのスループットの積で決まります。後者を、コンソールではストレージ単位あたりのスループット、API では PerUnitStorageThroughput と呼びます。S3 は容量を事前に確保する必要がなく、ノードを失ってもデータは残ります。 保存の方式には、同期と非同期があります。同期では、ストレージへの書き込みが終わるまで学習を止めます。非同期では、状態を GPU のメモリからホストのメモリへコピーする間だけ学習を止め、ストレージへの書き込みは学習と並行して進めます。どちらの方式でも、書き込みが完了するまでそのチェックポイントは再開に使えず、その間にジョブが中断すると、直近の書き込みが完了したチェックポイントから再開します。 非同期で短くなるのは学習が止まる時間 C です。中断したときに失う学習の時間は、保存の開始から書き込みの完了までの時間 t で決まり、同じ書き込みスループットなら同期と非同期で t はほぼ同じです。非同期で書き込みにかける時間を長くとるほど t は延び、失う学習の時間は長くなります。 設定を決める量 この記事で決める設定は、保存の間隔、同期か非同期か、ストレージの書き込みスループットと容量です。保存の回数を増やすほど学習が止まる時間の合計が増え、減らすほど中断したときに失う学習が増えるので、これらは次の量の関係で決まります。 記号 意味 S 全ノードを合計した、1 回のチェックポイントの大きさ C 1 回の保存で学習が止まる時間。同期では書き込みの完了まで、非同期ではホストのメモリへのコピーの間 t_a 非同期で、学習を再開してから書き込みが完了するまでの時間。同期では 0 t 保存の開始から書き込みの完了までの時間で、t = C + t_a t_w ストレージへの書き込みにかける時間。同期では C、非同期では t_a T 保存の開始から次の保存の開始までの間隔 M ジョブの平均中断間隔。故障を含め、チェックポイントからの再開が必要になる中断の平均間隔 R 中断してから学習を再開するまでの時間 \eta 学習時間のうち、保存で止まる時間と中断で失う時間を合わせて無駄になる割合 T^{*} \eta が最も小さくなる保存間隔 図の下段に並べた 2 つの式は、\eta の近似式と、そこから求めた T^{*} です。どちらも「保存間隔の決め方」の節で導きます。 S を t_w の間に書き終えるには、ストレージが 1 秒あたり S/t_w 以上を書ける必要があります。これが必要な書き込みスループット W の最小値です。t_w は測る値ではなく、同期なら学習を止めてよい時間、非同期なら次の保存までに書き終える時間として、設計する側が決める値です。実際には、書き込みの効率やほかの I/O との競合を見込んで、これより大きく確保します。 W = \frac{S}{t_w} チェックポイントの大きさ 重みのバイト数 2026 年 11 月に Hugging Face の API で取得した Nemotron 3.5 Super の BF16 版の重みのファイルサイズの合計は約 248.9 GB で、総パラメータ数の約 124.5B で割ると 1 パラメータあたり 2.0 バイトです。 MoE(Mixture of Experts)は、expert と呼ぶ複数の部分ネットワークのうち、トークンごとに一部だけを使うモデルです。1 トークンの計算に使うパラメータを活性パラメータと呼び、モデル名の 120B-A12B は総パラメータ約 120B、活性パラメータ約 12B を表します。保存では使われなかった expert の重みも書き出すので、チェックポイントの大きさは活性パラメータ数ではなく総パラメータ数に比例します。 再開に必要な状態 故障による中断や手動での一時停止で学習を止めたあと、止めた地点から同じように学習を再開するには、重みに加えて、オプティマイザの状態も保存します。混合精度の学習では、計算用の bf16 の重みに加えて、更新の丸め誤差をためないための fp32 の重みを持ちます。この fp32 の重みを master weight と呼びます。オプティマイザに Adam を使う場合、1 パラメータあたりのバイト数は次のとおりです。 状態 バイト数 bf16 の重み 2 fp32 の master weight 4 fp32 の Adam の 1 次と 2 次のモーメント 8 合計 14 この内訳は、Megatron-LM の distributed optimizer のように、bf16 の重みと fp32 の master weight を別々に持つ構成の値です。ZeRO の論文は、fp16 の重みと勾配で同じバイト数の内訳を示していて、勾配の 2 バイトを含めると 16 バイトです。FSDP2 を使う TorchTitan のように、分割して持つ重み自体が fp32 で、bf16 の重みは計算の間だけ作る構成では、保存するのは fp32 の重みとモーメントの 4 + 8 = 12 バイトです。モーメントを bf16 で持つ構成では 2 + 4 + 4 = 10 バイトになります。DeepSeek-V3 の技術報告は、AdamW の 1 次と 2 次のモーメントを bf16 で持ち、master weight を fp32 で持つと書いています。bf16 の重みと bf16 のモーメントだけを持ち、fp32 の重みを持たない構成では 2 + 4 = 6 バイトです。どの値になるかはフレームワークと設定で決まるので、実際に 1 回保存して確かめます。再開には、学習率のスケジューラ、ステップ数、乱数生成器、データの読み取り位置などの状態も必要ですが、これらは重みとオプティマイザの状態に比べて小さいので、大きさの計算では数えていません。 Nemotron 3.5 Super の大きさ 試算に使う Nemotron 3.5 Super の総パラメータ数は約 124.5B で、この値には画像エンコーダーのパラメータも含まれます。1 パラメータあたりのバイト数ごとに、チェックポイントの大きさ S と、32 ノードに均等に分けたときの 1 ノードあたりの大きさは次のとおりです。 保存する状態 1 パラメータあたり S 1 ノードあたり bf16 の重みだけ 2 バイト 約 0.25 TB 約 7.8 GB 重みとオプティマイザの状態 14 バイト 約 1.74 TB 約 54 GB モーメントを bf16 にした場合 10 バイト 約 1.24 TB 約 39 GB bf16 の重みと bf16 のモーメントだけの場合 6 バイト 約 0.75 TB 約 23 GB 画像エンコーダーを凍結して学習するなら、そのパラメータはオプティマイザの状態を持たないので、bf16 の重みの 2 バイトだけを保存すれば足ります。公開されている例では、awsome-distributed-ai の verl の例が、20B のモデルを GPU 12 枚で学習すると 1 つのチェックポイントが約 117 GB になると書いています。以降の試算では 14 バイトの約 1.74 TB を使い、最後に 6 バイトの場合の結果も示します。 公開事例の保存間隔 保存の間隔についての公開情報は乏しく、モデルの技術報告にはほとんど書かれていません。後の節で見るとおり、フレームワークに既定値がある場合も、時間ではなくステップ数で決まっています。時間で間隔を書いている公開事例は、次の表のように少数です。 出典 間隔 補足 BLOOM 176B 3 時間ごと。学習ログによると、計算機の保守の停止に備えて 100 ステップごとに変更 Hugging Face のブログによると 1 回が 2.3 TB、学習ログによると保存は 40 秒 DeepSeek の Fire-Flyer AI-HPC 5 分 保存と読み込みはどちらも数秒 Meta の信頼性の論文 大規模ジョブの典型は 1 時間 1,024 GPU のジョブの平均故障間隔は 7.9 時間 Unicron 30 分。再計算の時間を比べるために論文が置いた値 128 GPU のクラスターでの評価 ジョブの平均中断間隔 保存の間隔は、1 回の保存で止まる時間と、ジョブがどのくらいの頻度で中断するかの両方で決まります。次の表は、公開されている故障や中断の記録を、1 ノードに GPU 8 枚を積んだ 32 ノードのジョブの平均中断間隔に換算した値です。中断がノード数に比例して起きると仮定し、元の平均間隔を M_0、元のノード数を N_0 として M = M_0 N_0 / 32 で計算しました。ノード数は、論文の GPU 数を 1 ノード 8 GPU として割った値です。元の論文が数えた範囲、たとえば対象のクラスターや期間の取り方で値は変わるので、表の値は目安です。数えている事象は出典ごとに違い、GPU の故障、ハードウェアが原因の再起動、原因を問わない中断が混ざっているので、比べるときは注意が必要です。 出典 規模と期間 32 ノードに換算した平均中断間隔 Meta の信頼性の論文 128 ノード、1,024 GPU のジョブで 7.9 時間 約 32 時間 OPT-175B 124 ノード、60 日とみなした 2 か月で、ハードウェアの障害による手動の再起動が少なくとも 35 回、自動の再起動が 70 回以上 105 回で約 53 時間、35 回で約 159 時間 BLOOM 48 ノードで、GPU の故障が週 1〜2 回 約 126〜252 時間 Llama 3 2,048 ノード、54 日で 466 回の中断。うち想定外が 419 回で、その約 78%、約 327 回がハードウェアの問題と確認されたか、そう疑われた中断 466 回で約 178 時間、419 回で約 198 時間、327 回で約 254 時間 MegaScale 10,000 GPU を超える本番の学習で、3 週間とみなした数週間に 100 回を超える再起動。1,250 ノードとして計算 約 197 時間。GPU 数も回数も「超える」なので目安 Acme 588 ノード、182.5 日とみなした 6 か月で、ハードウェアが原因の故障が 103 件 約 781 時間 ネットワークの障害や、複数のノードがまとめて故障する場合があるので、ノード数に比例させる換算は目安です。ジョブの中断には GPU の故障のほかにネットワークやソフトウェアの障害も含まれるので、GPU の故障率だけからは M を決められません。参考として、NCSA Delta の研究は、GPU 1 枚あたりのメモリエラーの平均間隔を H100 で約 88,768 時間、A100 で約 283,271 時間と報告しています。B200 と B300 の故障率は公開されていません。 保存間隔の決め方 Young の式 保存間隔 T は、学習が止まる時間と、中断で失う学習の時間の合計が最も小さくなるように決めます。学習時間のうち無駄になる割合を \eta とすると、次の式で近似できます。 \eta \approx \frac{C}{T} + \frac{T/2 + t + R}{M} 第 1 項は、保存で学習が止まる分です。間隔 T ごとに C だけ止まるので、止まる割合は C/T です。 第 2 項は、中断で失う分です。1 回の中断で失う時間の期待値を T/2 + t + R とし、中断が平均 M に 1 回起きるので M で割ります。ここで t は、保存を始めてから書き込みが終わり、そのチェックポイントが再開に使えるようになるまでの時間です。 ! ここからの導出は、確率変数と期待値を使います。数理統計になじみがない方は、導出を飛ばして「1 回の中断で失う時間の平均は T/2 + t になる」という結論だけを眺めても、この先を読み進められます。内容は統計検定 2 級くらいなので、学んでおくと性能の見積もりなどいろいろな場面で役に立ちます。 T/2 + t は、次のように求まります。直近の保存を始めた時刻から中断までの経過時間を連続型の確率変数 X とし、中断は間隔のどこでも同じように起きる、つまり X が区間 [0, T) の連続一様分布に従うと仮定します。また、保存は予定どおり t < T で書き終わり、C は T より十分短いとします。 X \sim \mathrm{U}(0, T), \qquad E[X] = \frac{T}{2}, \qquad P(X < t) = \frac{t}{T} X \ge t なら直近の保存は書き終わっているので、やり直すのは X だけです。X < t なら直近の保存はまだ使えないので、1 つ前の保存まで戻り、さらに間隔 1 つ分の T をやり直します。やり直す時間を L とすると、指示関数 \mathbf{1}{X < t} を使って次のように書けます。 L = X + T \cdot \mathbf{1}{X < t} 期待値をとると、次のようになります。 E[L] = E[X] + T \cdot P(X < t) = \frac{T}{2} + T \cdot \frac{t}{T} = \frac{T}{2} + t 最悪の場合は X が t の直前のときで、L は T + t に近づきます。 これに再開までの時間 R を足したものが、1 回の中断で失う時間です。たとえば T が 45 分、t が 5 分なら、学習のやり直しは平均 22.5 分 + 5 分 = 27.5 分、最悪で 45 分 + 5 分 = 50 分です。 t と R が T によらないとすると、右辺を T で微分して 0 と置いた -C/T^2 + 1/(2M) = 0 から、最適な間隔は次のようになります。 T^{*} \approx \sqrt{2\,C\,M} この式は Young が 1974 年に示したもので、C \ll T \ll M のときに成り立ちます。C が M に比べて無視できない場合の補正は、Daly が 2006 年に加えています。 ! Young の式と Daly の補正は、HPC のチェックポイント研究で古くから使われてきた式です。もともとは、スーパーコンピューターで長時間の計算を故障に備えてどの間隔で保存するかを決めるためのものでした。大規模な LLM の学習でも同じ考え方が使われていて、Meta の信頼性の論文もこの形の式で保存の間隔を見積もっています。 M は、Llama 3 の換算値の約 178〜254 時間の範囲にある 200 時間とし、R は 15 分とします。 方式 M C t T^{*} \eta 同期 200 時間 5 秒 5 秒 約 45 分 約 0.50% 同期 200 時間 60 秒 60 秒 約 2.6 時間 約 1.42% 同期 200 時間 300 秒 300 秒 約 5.8 時間 約 3.05% 非同期 200 時間 5 秒 5 分 5 秒 約 45 分 約 0.54% 同期 約 32 時間 5 秒 5 秒 約 18 分 約 1.72% 同期 約 781 時間 5 秒 5 秒 約 88 分 約 0.22% 最後の 2 行は、M を前の節の表で最も短い Meta の換算値と、最も長い Acme の換算値にした場合で、T^{*} は M によって大きく変わります。 式の前提 この式は、故障が互いに独立で、故障の起きやすさが時間によらないこと、つまり故障間隔が指数分布に従うことを前提にしています。Aupy らの論文が引く Heien らの測定では、HPC システムの故障間隔は形状パラメータ 0.58〜0.71 の Weibull 分布に当てはまりました。Weibull 分布は、形状パラメータが 1 のとき指数分布と同じになり、1 より小さいほど故障の直後に次の故障が集中します。同じ論文は、形状パラメータ 0.5、2^{19} プロセスのシミュレーションで、Daly の式の間隔を使うと、論文が提案する間隔の場合より実行時間が約 61%、Young の式では約 50% 長くなったと報告しています。 平均中断間隔も一定ではないので、運用中に記録した中断から M を更新して、間隔を見直します。評価の節目、損失が急に悪化したときの巻き戻し、予約した計算資源の期限などでもチェックポイントから再開するので、これらの時刻に合わせて保存する場合もあります。 フレームワークの既定値 Megatron-LM、それを継承する Megatron Bridge、TorchTitan では、非同期の保存は既定で無効です。有効にしなければ同期で保存され、書き込みが終わるまで学習は止まります。次の表は、2026 年 10 月に取得したコミットのソースコードで確かめた既定値で、リンク先はそのコミットの該当行です。 フレームワーク 保存間隔の既定 非同期保存の既定 Megatron-LM 既定値はなしで、保存先を指定すると間隔の指定が必須 無効。分散チェックポイント形式で --use-persistent-ckpt-worker を指定しないと、有効にしても無効に戻される Megatron Bridge Megatron-LM の設定を継承 Megatron-LM の設定を継承するので無効。async_save で有効にする TorchTitan 500 ステップ 無効 Hugging Face Trainer 500 ステップ 非同期保存の機能はなく、save_steps ごとに同期で保存 ローカル NVMe と複製 前の節の非同期保存を一歩進めると、チェックポイントを短い間隔で各ノードのローカル NVMe に書き、共有ストレージへは長い間隔で書く方法があります。ローカル NVMe は書き込みが速いので、学習が止まる時間と、共有ストレージに必要な書き込みスループットを小さくできます。弱点は、そのノードを失うとローカルに書いた分も失うことです。NVIDIA Resiliency Extension は、このローカルへの保存を提供するライブラリで、ほかのノードに複製を持って冗長性を高める仕組みも備えています。ローカルに書いたチェックポイントを共有ストレージへ移す処理は含まず、共有ストレージへの保存は、通常のチェックポイントとして別に行います。この節では、この方法で何を守れて何を守れないかを整理します。次の図は、保存先ごとに、プロセスの再起動、1 ノードの喪失、クラスター全体の停止のあとでデータが残るかを示しています。 NVIDIA B300 を 8 枚積んだ p6-b300.48xlarge には、3,800 GB の NVMe SSD が 8 本、合計 30,400 GB のインスタンスストアがあり、1 ノードあたり約 54 GB のシャードを何世代分も置けます。共有ストレージへの転送が終わるまでは、そのノードが使えなくなるとシャードを失います。 NVIDIA Resiliency Extension のローカルチェックポイントは、Megatron-LM などの学習ループに組み込んで使い、各ノードの SSD か RAM ディスクに保存し、ほかのノードにも複製を持つことで、1 ノードを失っても複製からシャードを復元します。複製の組み方は設定で決め、保存と再開で同じ設定にする必要があります。 NVIDIA Resiliency Extension は、GPU 1 枚を受け持つプロセスであるランクを組にして、組の中で複製を持ち合います。組の作り方は、組の大きさ F と、組の中のランクの間隔 J の 2 つの設定で決まり、それぞれ replication_factor と replication_jump です。J の既定値は 1 ノードの GPU 数なので、組のランクは別々のノードに置かれ、組の中の F ノードが互いの分の複製をすべて持ちます。複製は既定では無効で、Megatron-LM では --replication で有効にします。F の既定値は NVIDIA Resiliency Extension でも Megatron-LM でも 2 です。ランクの総数が J \times F で割り切れないと、組を作る処理が止まります。p6-b300.48xlarge 32 台ではランクの総数が 256、J が 8 なので、F に使えるのは 1、2、4、8、16、32 です。 組を持っていても、故障したノードの分の複製を作り直す前に、同じ組のほかのノードもすべて失われると、そのシャードは失われます。この確率を、組に固定する NVIDIA Resiliency Extension の置き方と、比較のために複製先をランダムに選ぶ置き方で比べます。前提は次のとおりです。 前提 内容 ノード数 32 組の大きさ F 既定値の 2。原本を含めて 2 か所に置く 最初の故障 ノード A が故障する \Delta A の分の複製を作り直すまでの時間。例では 1 時間 M_n 1 ノードが失われてインスタンスストアのデータが読めなくなる平均間隔。例では 6,400 時間で、ジョブの中断がすべてノードの喪失によるとした多めの見積もり p ほかの各ノードが \Delta の間に失われる確率で、p \approx \Delta / M_n。例では約 0.016% 独立性 ほかのノードは互いに独立に失われる シャードの数 A が持つシャードは十分に多い 組に固定する場合 F = 2 では、A と組になるノード B が、A のシャードの複製をすべて持ちます。A のシャードを失うのは、B が \Delta の間に失われるときだけなので、確率は次のとおりです。 P_{\text{組}} = p 複製先をランダムに選ぶ場合 A の各シャードの複製を、ほかの 31 ノードからランダムに選んだ 1 台に置くとします。シャードが十分に多いと、31 ノードのどれにも A のどれかのシャードの複製があります。そのため、31 ノードのどれか 1 台が \Delta の間に失われれば、A のシャードのどれかを失います。31 台のどれも失われない確率は、各ノードが独立に確率 1 - p で残るので (1 - p)^{31} です。A のシャードを失う確率はその余事象なので、次のようになり、p が小さければ 31p で近似できます。 P_{\text{ランダム}} = 1 - (1 - p)^{31} \approx 31\, p 近似は、(1 - p)^{31} を二項定理で展開し、p が小さいときに p^2 以降の項を捨てたものです。 (1 - p)^{31} = 1 - 31p + \binom{31}{2}p^2 - \binom{31}{3}p^3 + \cdots \approx 1 - 31p 31p は、31 台それぞれが失われる確率 p を足した値です。2 台以上が同時に失われる場合を重ねて数えていますが、p が小さければその分は無視できます。前提の表の例の値では、正確な値が約 0.4832%、近似が約 0.4844% です。 前提の表の例の値を入れると、組に固定する場合は約 0.016%、ランダムに置く場合は約 0.48% で、ランダムに置くと約 31 倍になります。例の M_n は、ジョブの平均中断間隔 200 時間を 32 倍した値です。F を 4 にすると、組に固定する場合は組のほかの 3 台がすべて失われるときだけなので p^3、ランダムでは、シャードが十分に多く 3 台の組み合わせのどれにも複製があるとして、p が小さいときの近似で \binom{31}{3} p^3 = 4{,}495\, p^3 になり、どちらも桁違いに小さくなります。その代わり、各ノードが持つ複製の量は F に比例して増えます。 チェックポイントは、シャードが 1 つでも欠けると再開に使えません。そのため、ある世代のシャードを 1 つ失っても全部失っても、その世代は同じように使えなくなります。複数の世代を同じ組に置いている場合は、組を失えばそれらの世代をまとめて失い、共有ストレージにある世代まで戻ります。どちらの置き方でも失う世代の数は同じなので、確率が低い、組に固定する置き方のほうが有利です。置き方によってデータを失う組み合わせの数が変わることは、Copysets の論文が分散ストレージについて示しています。 この計算は故障が独立だという前提に立っているので、ラックや電源の障害で複数のノードがまとめて止まる場合や、クラスター全体が止まる場合には当てはまりません。その場合に備えて、FSx for Lustre か S3 にもコピーを置きます。 データが通る経路 チェックポイントのデータは、GPU のメモリからホストのメモリを経て、ローカル NVMe か、ネットワークインターフェースを通って共有ストレージへ書かれます。経路の途中にはそれぞれ上限があり、いちばん低い上限が書き込みスループットを決めます。 ローカル NVMe は PCIe でつながったノード内のディスクなので、ネットワークの帯域は効かず、ディスクの性能で決まります。S3、S3 Files、EFA を使わない FSx for Lustre は、ENA というネットワークインターフェースで TCP を使って通信します。EFA は、OS を通らずに SRD というプロトコルで通信するネットワークインターフェースで、EFA を有効にした FSx for Lustre との通信や、ノード間の集団通信に使います。 ネットワークインターフェースの帯域は、インスタンスの種類で大きく違います。 インスタンス ネットワークインターフェースの帯域 c6in.2xlarge 最大 40 Gbps、基準値 12.5 Gbps g6e.12xlarge 100 Gbps p6-b300.48xlarge 1 枚目のネットワークカードが 350 Gbps、ほかに 400 Gbps のネットワークカードが 16 枚あり、16 枚の合計は 6,400 Gbps TCP には、インスタンスの帯域とは別の上限もあります。EC2 の文書によると、同じクラスタープレイスメントグループにない相手との 1 本の TCP 接続は 5 Gbps までです。同じグループの中では 10 Gbps、同じ AZ で ENA Express を使えば 25 Gbps まで上がりますが、いずれにしても 1 本の接続には上限があります。S3 の性能のガイドラインも、多数の接続に分けて並列に送ることを勧めています。FSx for Lustre の文書は、クライアント 1 台あたりの最大スループットを、EFA を使わない場合は 100 Gbps、EFA では 700 Gbps、EFA と GPUDirect Storage では 1,200 Gbps としています。ENA で TCP を使う場合のもう 1 つの上限は、次の節で説明します。 FSx for Lustre の容量と書き込みスループット FSx for Lustre の SSD を使う Persistent 2 では、ディスクのスループットの基準値は確保したストレージ容量とストレージ単位あたりのスループットの積で決まり、ストレージ単位あたりのスループットは 125、250、500、1,000 MB/s/TiB から選びます。料金は使用量ではなく確保した容量にかかり、ストレージ容量の文書にあるとおり、容量は増やせますが減らせません。 容量は GiB で指定し、EFA を使わない場合は 1,200 GiB も選べます。それより大きい容量の単位はストレージサーバー 1 台あたりの容量で決まります。EFA を使わない場合は 2,400 GiB、EFA を使う場合は 125 MB/s/TiB で 38,400 GiB、250 MB/s/TiB で 19,200 GiB、500 MB/s/TiB で 9,600 GiB、1,000 MB/s/TiB で 4,800 GiB です。スループットの計算では、AWS の文書の例に合わせて 1,000 GiB を 1 TiB として扱います。EFA を使うファイルシステムは、スループットが 10 GB/s を超える場合に推奨されていて、作成後に EFA の有無もストレージ単位あたりのスループットも変えられません。EFA を使わない SSD のファイルシステムは、作成後にストレージ単位あたりのスループットを変更できます。 ストレージ単位あたりのスループットは、ディスクのスループットの基準値です。性能の表によると、125 MB/s/TiB と 250 MB/s/TiB には、ディスクのスループットを一時的に 500 MB/s/TiB まで上げるバーストがあり、500 MB/s/TiB と 1,000 MB/s/TiB にはありません。また、ENA で TCP を使って通信する場合、クライアント 1 台とストレージサーバー 1 台の間のトラフィックは 5 Gbps が上限なので、1 台のクライアントが書けるのは、ストレージサーバーの台数に 5 Gbps を掛けた値までです。 Nemotron 3.5 Super の S を 1.74 TB とすると、ストレージへの書き込みにかける時間 t_w ごとに必要な書き込みスループット W = S / t_w は次のとおりです。 t_w W 300 秒 約 5.8 GB/s 60 秒 約 29 GB/s 20 秒 約 87 GB/s 同期で保存するなら t_w がそのまま学習の止まる時間 C になるので、C を短くするほど確保する容量と料金が増えます。非同期なら C と t_w を別々に決められます。前回の書き込みが次の保存の開始までに終わっていない場合の動きは、フレームワークで違います。TorchTitan は前回の書き込みが終わるまで待つので、その分だけ学習が止まります。Megatron-LM と Megatron Bridge は警告を出して次の保存を始めるので、学習は止まりませんが、書き終わっていない書き込みが積み重なります。どちらでも重なりを避けるには W \ge S/(T - C) が必要で、T が約 45 分なら約 0.65 GB/s が下限です。t_w を長くすると W は小さくなりますが、t が延びるので中断で失う学習の時間 T + t と \eta は大きくなります。ローカルの NVMe を経由して共有ストレージへ転送する場合も、次の保存までに転送を終える必要があるので、平均で S/T 以上の書き込みスループットが要ります。 ! コラム: 保存で止まる GPU 時間と FSx for Lustre の料金のトレードオフ 同期で保存する場合、C を短くすると FSx for Lustre の料金は増えますが、保存で止まる GPU の時間は減ります。1 年間の学習で両者を比べると次のとおりです。前提は、S が 1.74 TB、p6-b300.48xlarge 32 台、M が 200 時間、T を Young の式で \sqrt{2CM} に置くことです。GPU の時間は、1 年の 8,760 時間に止まる割合 C/T を掛け、2026 年 11 月時点の p6-b300.48xlarge のオレゴンのオンデマンド料金、1 時間 142.416 ドルで金額にしました。FSx for Lustre は、各 C で最も安い構成の月額の 12 か月分で、必要な書き込みスループットが 10 GB/s を超える場合は EFA を使う構成にしています。 C 止まる GPU 時間/年 その金額/年 FSx for Lustre/年 合計/年 30 秒 約 10,200 GPU 時間 約 18.2 万ドル 約 44.9 万ドル 約 63.1 万ドル 60 秒 約 14,500 GPU 時間 約 25.8 万ドル 約 24.2 万ドル 約 50.0 万ドル 120 秒 約 20,500 GPU 時間 約 36.4 万ドル 約 13.8 万ドル 約 50.3 万ドル 300 秒 約 32,400 GPU 時間 約 57.6 万ドル 約 4.9 万ドル 約 62.5 万ドル 実際には GPU は年単位で確保していることが多く、保存で止まっても GPU の支払いは変わりません。その場合、止まる GPU 時間の金額は、その時間で行えたはずの追加の学習を金額に換算した機会費用として読みます。この考え方では、合計は C が 90 秒前後で最も小さく、年に約 46 万ドルです。この比較は保存で止まる時間だけを数えています。中断で失う学習も GPU 時間なので、それも足すと合計は年に約 76 万ドルになり、最も小さくなる C は 50 秒前後に移ります。C を短くしすぎると FSx for Lustre の料金が、長くしすぎると止まる GPU の時間が、合計を押し上げます。GPU の単価が予約や個別の価格で下がれば、最も小さくなる C は長い側へ動きます。非同期で保存できるなら、止まる時間はホストのメモリへのコピーの間だけになります。書き込みにかける時間は学習を止めずに長くとれるので、FSx for Lustre の料金と、中断で失う学習の時間の釣り合いだけで決められ、止まる GPU 時間の費用を小さくできます。Megatron-LM では分散チェックポイント形式と常駐の保存ワーカーが条件になるので、使っているフレームワークで有効にできるなら、非同期で保存するのがよいと考えます。 確保する容量は、残す世代数を k、ストレージ単位あたりのスループットを \tau、容量の単位を u として、次の式で決まります。容量は GiB で比べ、kS はバイト数を 2^{30} で割って GiB にし、W/\tau は MB/s を MB/s/TiB で割った値を 1,000 倍して GiB とします。 \text{確保する容量} = u \left\lceil \frac{\max\left(k S,\ W/\tau\right)}{u} \right\rceil Nemotron 3.5 Super で FSx for Lustre を見積もる 見積もりは、チェックポイントの設定を決め、そこから FSx for Lustre に必要な書き込みスループットと容量を逆算し、最も安い構成の月額を求める順に進めます。式はこれまでの節で説明したとおりで、計算の過程はこの節の最後の折りたたみにまとめました。 見積もる人が決める値 次の値を決めれば、見積もりができます。既存の環境で測れる値は測り、測れない値は右の概算から始めます。 値 決め方 測れないときの概算 この例の値 総パラメータ数 モデルの定義から読む なし 約 124.5B 1 パラメータあたりのバイト数 小さい構成で 1 回保存し、チェックポイントの大きさを総パラメータ数で割る bf16 の重み、fp32 の master weight、fp32 のモーメントなら 14、fp32 の重みとモーメントなら 12、モーメントが bf16 なら 10 14 ノード数 学習の計画から決める なし 32 同期か非同期か 使うフレームワークで非同期を有効にできるかで決める なし 非同期 保存で止まる時間 C 1 回保存し、学習のステップが止まった時間を測る 非同期は 1 ノード分を GPU からホストへ写す時間で、MegaScale と Fire-Flyer はどちらも数秒と報告している。同期は t_w と同じ 5 秒程度 書き込みにかける時間 t_w 予算と、中断で失ってよい学習の時間から決める なし 5 分または 20 分 平均中断間隔 M 既存のクラスターのジョブの中断を記録する ジョブの平均中断間隔の節の表から選ぶ 200 時間 再開までの時間 R 中断から再開までを 1 回試して測る 10 分にチェックポイントの読み込み時間を足す 約 15 分。FSx for Lustre の構成で変わる 残す世代数 k 何世代前まで戻れればよいかで決める 完了した 3 世代と書き込み中の 1 世代 4 EFA を使うか 必要な書き込みスループットが 10 GB/s を超えるなら、AWS の推奨に合わせて使う なし 使わない 料金 使うリージョンの料金ページで確かめる なし オレゴン 見積もりの結果 左のグラフは、書き込みにかける時間 t_w ごとに、FSx for Lustre の最も安い構成の月額を示します。右のグラフは、同じ t_w で、同期と非同期それぞれの無駄になる割合 \eta を示します。 t_w を延ばすほど必要な書き込みスループットが下がり、FSx for Lustre は安くなります。容量が残す世代を置く量で決まるところまで下がると、それ以上は安くなりません。 EFA を使う構成は容量の単位が大きいので、同じ書き込みスループットなら、EFA を使わない構成と同じか高くなります。10 GB/s を超える範囲では、AWS の推奨に合わせて EFA を使う構成で計算しています。 同期では t_w がそのまま学習の止まる時間になるので、\eta は t_w とともに急に増えます。非同期では止まる時間が数秒のままなので、t_w を延ばしても \eta はゆっくりしか増えません。 主な組み合わせの結果は次のとおりです。「最も安い構成」の列は、FSx for Lustre を作るときに選ぶ 2 つの値で、たとえば「250 MB/s/TiB、7,200 GiB」は、ストレージ単位あたりのスループットを 250 MB/s/TiB に、ストレージ容量を 7,200 GiB にすることを意味します。この構成の書き込みスループットの基準値は、7,200 GiB を 7.2 TiB として 250 MB/s/TiB を掛けた 1.8 GB/s です。250 MB/s/TiB 以下ではバーストで一時的にこれを超えられますが、見積もりでは基準値を使います。容量は、必要な書き込みスループットから出る容量と、残す世代を置く容量の大きいほうで決まるので、世代を置く容量で決まった場合は、基準値が必要な書き込みスループットより大きくなります。 チェックポイントの設定 必要な書き込みスループット 最も安い構成 書き込みスループットの基準値 容量を決めたもの 月額 \eta 14 バイト、非同期、20 分で書き終える 約 1.45 GB/s 250 MB/s/TiB、7,200 GiB 1.8 GB/s 世代を置く容量 約 1,500 ドル 約 0.76% 14 バイト、非同期、5 分で書き終える 約 5.8 GB/s 500 MB/s/TiB、12,000 GiB 6.0 GB/s 書き込みスループット 約 4,100 ドル 約 0.54% 6 バイト、非同期、5 分で書き終える 約 2.5 GB/s 500 MB/s/TiB、7,200 GiB 3.6 GB/s 書き込みスループット 約 2,400 ドル 約 0.53% 14 バイト、同期、60 秒で書き終える 約 29 GB/s EFA あり、1,000 MB/s/TiB、33,600 GiB 33.6 GB/s 書き込みスループット 約 20,200 ドル 約 1.39% この例では、非同期で 45 分ごとに保存し、20 分で書き終えればよいとすると、250 MB/s/TiB の 7,200 GiB、月額約 1,500 ドルで予算を見積もれます。中断で失う学習を減らしたい場合は、書き終える時間を 5 分に縮めて月額約 4,100 ドルで見積もります。 計算の過程: 14 バイト、非同期、5 分で書き終える場合 ノード数を N、総パラメータ数を P、1 パラメータあたりのバイト数を b とします。 チェックポイントの大きさと 1 ノードあたりの大きさ S = P \times b = 124.45 \times 10^{9} \times 14 \approx 1.74\ \text{TB}, \qquad \frac{S}{N} = \frac{1.74\ \text{TB}}{32} \approx 54\ \text{GB} 保存で止まる時間 C を 5 秒にするのに必要な、1 ノードあたりの GPU からホストへの転送 \frac{S/N}{C} = \frac{54\ \text{GB}}{5\ \text{s}} \approx 11\ \text{GB/s} 保存間隔 T^{*} = \sqrt{2CM} = \sqrt{2 \times 5\ \text{s} \times 720{,}000\ \text{s}} \approx 2{,}683\ \text{s} \approx 45\ \text{min} 必要な書き込みスループット W = \frac{S}{t_w} = \frac{1.74\ \text{TB}}{300\ \text{s}} \approx 5.8\ \text{GB/s} 確保する容量。kS を GiB にし、W/\tau は MB/s を MB/s/TiB で割った値を 1,000 倍して GiB にします。\tau = 500 MB/s/TiB、u = 2{,}400 GiB では次のとおりです。 kS = \frac{4 \times 1.742\ \text{TB}}{2^{30}} \approx 6{,}491\ \text{GiB}, \qquad \frac{W}{\tau} \times 1{,}000 = \frac{5{,}808}{500} \times 1{,}000 \approx 11{,}616\ \text{GiB} u \left\lceil \frac{\max(6{,}491,\ 11{,}616)}{2{,}400} \right\rceil = 2{,}400 \times 5 = 12{,}000\ \text{GiB} 月額。オレゴンの 500 MB/s/TiB の料金は GiB 月あたり 0.340 ドルです。 12{,}000\ \text{GiB} \times 0.340\ \text{USD} = 4{,}080\ \text{USD/month} 再開までの時間と無駄になる割合。この構成のスループットの基準値は 12{,}000 / 1{,}000 \times 500 = 6{,}000 MB/s で、読み込みも同じ程度出ると仮定すると、約 290 秒です。 R = 600 + \frac{1.74\ \text{TB}}{6.0\ \text{GB/s}} \approx 890\ \text{s}, \qquad t = C + t_w = 305\ \text{s} \eta = \frac{C}{T} + \frac{T/2 + t + R}{M} = \frac{5}{2{,}683} + \frac{1{,}341 + 305 + 890}{720{,}000} \approx 0.54\% 中断で失う学習の時間の上限は、T + t \approx 2{,}683 + 305 \approx 2{,}988 秒、約 50 分です。 ほかのストレージ単位あたりのスループットと EFA の有無で同じ計算をすると、次のようになります。 ストレージ単位あたりのスループット EFA なし EFA あり 125 MB/s/TiB 48,000 GiB、約 7,000 ドル/月 76,800 GiB、約 11,100 ドル/月 250 MB/s/TiB 24,000 GiB、約 5,000 ドル/月 38,400 GiB、約 8,100 ドル/月 500 MB/s/TiB 12,000 GiB、約 4,100 ドル/月 19,200 GiB、約 6,500 ドル/月 1,000 MB/s/TiB 7,200 GiB、約 4,300 ドル/月 9,600 GiB、約 5,800 ドル/月 まとめ FSx for Lustre をできるだけ安く見積もるには、先にチェックポイントの大きさ、保存間隔、書き込みにかける時間を決め、そこから必要な書き込みスループットと容量を逆算します。Nemotron 3.5 Super を 32 ノードで学習する例では、非同期で 45 分ごとに保存し、20 分で書き終えればよいとすると、250 MB/s/TiB の 7,200 GiB、月額約 1,500 ドルで足ります。書き込みにかける時間を延ばすほど FSx for Lustre は安くなりますが、中断で失う学習と再開の時間は延びるので、その釣り合いで決めます。ここでの値はどれも試算なので、実際の構成で保存と再開の時間、中断の頻度、書き込みスループットを測って置き換えてください。
Compiling and running my project (Rust, wgpu & winit) Preample Installed NixOS last week, been liking it! I have an issue with OBS, more on that in another post, and then this issue, but everything else has been great. The issue When I compile and run my project, I crash with the following output. The program compiles fine, but crashes immediately upon startup when creating the window. thread 'main' (66200) panicked at src/window.rs:147:55: called `Result::unwrap()` on an `Err` value: CreateSurfaceError { inner: Hal(FailedToCreateSurfaceForAnyBackend({})) } [details="Full backtrace (RUST_BACKTRACE=full)"] thread 'main' (66276) panicked at src/window.rs:147:55: called `Result::unwrap()` on an `Err` value: CreateSurfaceError { inner: Hal(FailedToCreateSurfaceForAnyBackend({})) } stack backtrace: 0: 0x5c8010839b71 - std[68c81ae915296b05]::backtrace_rs::backtrace::libunwind::trace at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9 1: 0x5c8010839b71 - std[68c81ae915296b05]::backtrace_rs::backtrace::trace_unsynchronized::<std[68c81ae915296b05]::sys::backtrace::_print_fmt::{closure#1}> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14 2: 0x5c8010839b71 - std[68c81ae915296b05]::sys::backtrace::_print_fmt at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:74:9 3: 0x5c8010839b71 - <<std[68c81ae915296b05]::sys::backtrace::BacktraceLock>::print::DisplayBacktrace as core[efd5e57aef94d71e]::fmt::Display>::fmt at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:44:26 4: 0x5c801085404a - <core[efd5e57aef94d71e]::fmt::rt::Argument>::fmt at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/fmt/rt.rs:152:76 5: 0x5c801085404a - core[efd5e57aef94d71e]::fmt::write 6: 0x5c8010840dec - core[efd5e57aef94d71e]::io::write::default_write_fmt::<std[68c81ae915296b05]::sys::stdio::unix::Stderr> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/io/write.rs:402:11 7: 0x5c8010840dec - <std[68c81ae915296b05]::sys::stdio::unix::Stderr as core[efd5e57aef94d71e]::io::write::Write>::write_fmt at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/io/write.rs:335:13 8: 0x5c8010815bc6 - <std[68c81ae915296b05]::sys::backtrace::BacktraceLock>::print at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:47:9 9: 0x5c8010815bc6 - std[68c81ae915296b05]::panicking::default_hook::{closure#0} at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:292:27 10: 0x5c8010831a51 - std[68c81ae915296b05]::panicking::default_hook at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:319:9 11: 0x5c8010831c0b - std[68c81ae915296b05]::panicking::panic_with_hook at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:815:13 12: 0x5c8010815c72 - std[68c81ae915296b05]::panicking::panic_handler::{closure#0} at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:688:13 13: 0x5c801080da59 - std[68c81ae915296b05]::sys::backtrace::__rust_end_short_backtrace::<std[68c81ae915296b05]::panicking::panic_handler::{closure#0}, !> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:182:18 14: 0x5c8010816aed - __rustc[3d2f79520d6fa809]::rust_begin_unwind at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:679:5 15: 0x5c801085491c - core[efd5e57aef94d71e]::panicking::panic_fmt at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/panicking.rs:80:14 16: 0x5c8010854642 - core[efd5e57aef94d71e]::result::unwrap_failed at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/result.rs:1871:5 17: 0x5c800f5f836d - <core[efd5e57aef94d71e]::result::Result<wgpu[72de333dbba5b64b]::api::surface::Surface, wgpu[72de333dbba5b64b]::api::surface::CreateSurfaceError>>::unwrap at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/result.rs:1232:23 18: 0x5c800f5f836d - <Peanuts[5daf6c7432f9e7d0]::window::State>::new::{closure#0} at /home/neropatti/Projects/GIT/Peanuts/src/window.rs:147:55 19: 0x5c800f611a31 - pollster[1817c31098131509]::block_on::<<Peanuts[5daf6c7432f9e7d0]::window::State>::new::{closure#0}> at /home/neropatti/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pollster-0.4.0/src/lib.rs:126:28 20: 0x5c800f5dfdd3 - Peanuts[5daf6c7432f9e7d0]::main at /home/neropatti/Projects/GIT/Peanuts/src/main.rs:32:21 21: 0x5c800f5a8c2b - <fn() as core[efd5e57aef94d71e]::ops::function::FnOnce<()>>::call_once at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/ops/function.rs:250:5 22: 0x5c800f59837e - std[68c81ae915296b05]::sys::backtrace::__rust_begin_short_backtrace::<fn(), ()> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/sys/backtrace.rs:166:18 23: 0x5c800f5adf41 - std[68c81ae915296b05]::rt::lang_start::<()>::{closure#0} at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/rt.rs:206:18 24: 0x5c8010830c69 - <&dyn core[efd5e57aef94d71e]::ops::function::Fn<(), Output = i32> + core[efd5e57aef94d71e]::panic::unwind_safe::RefUnwindSafe + core[efd5e57aef94d71e]::marker::Sync as core[efd5e57aef94d71e]::ops::function::FnOnce<()>>::call_once at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/ops/function.rs:287:21 25: 0x5c8010830c69 - std[68c81ae915296b05]::panicking::catch_unwind::do_call::<&dyn core[efd5e57aef94d71e]::ops::function::Fn<(), Output = i32> + core[efd5e57aef94d71e]::panic::unwind_safe::RefUnwindSafe + core[efd5e57aef94d71e]::marker::Sync, i32> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:576:43 26: 0x5c8010830c69 - std[68c81ae915296b05]::panicking::catch_unwind::<i32, &dyn core[efd5e57aef94d71e]::ops::function::Fn<(), Output = i32> + core[efd5e57aef94d71e]::panic::unwind_safe::RefUnwindSafe + core[efd5e57aef94d71e]::marker::Sync> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:544:19 27: 0x5c8010830c69 - std[68c81ae915296b05]::panic::catch_unwind::<&dyn core[efd5e57aef94d71e]::ops::function::Fn<(), Output = i32> + core[efd5e57aef94d71e]::panic::unwind_safe::RefUnwindSafe + core[efd5e57aef94d71e]::marker::Sync, i32> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panic.rs:359:14 28: 0x5c8010830c69 - std[68c81ae915296b05]::rt::lang_start_internal::{closure#0} at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/rt.rs:175:24 29: 0x5c8010830c69 - std[68c81ae915296b05]::panicking::catch_unwind::do_call::<std[68c81ae915296b05]::rt::lang_start_internal::{closure#0}, isize> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:576:43 30: 0x5c8010830c69 - std[68c81ae915296b05]::panicking::catch_unwind::<isize, std[68c81ae915296b05]::rt::lang_start_internal::{closure#0}> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panicking.rs:544:19 31: 0x5c8010830c69 - std[68c81ae915296b05]::panic::catch_unwind::<std[68c81ae915296b05]::rt::lang_start_internal::{closure#0}, isize> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/panic.rs:359:14 32: 0x5c8010830c69 - std[68c81ae915296b05]::rt::lang_start_internal at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/rt.rs:171:5 33: 0x5c800f5adf27 - std[68c81ae915296b05]::rt::lang_start::<()> at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/std/src/rt.rs:205:5 34: 0x5c800f5dfece - main 35: 0x759f8822b285 - __libc_start_call_main 36: 0x759f8822b338 - __libc_start_main@GLIBC_2.2.5 37: 0x5c800f583be5 - _start 38: 0x0 - <unknown> [/details] You can find all of the code on the repository, but here are some things of interest: [details="shell.nix"] { pkgs ? import <nixpkgs> {} }: pkgs.mkShell { # nativeBuildInputs is usually what you want -- tools you need to run nativeBuildInputs = with pkgs.buildPackages; [ gcc #python3 ]; LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath [ libGL # Didn't need this to get past the previous error, but this might be needed! Idk! libxkbcommon wayland ]; } [/details] [details="Cargo.toml"] [package] name = "Peanuts" version = "0.1.0" edition = "2024" [dependencies] env_logger = "0.11.9" log = "0.4.29" pollster = "0.4.0" wgpu = "28.0.0" winit = "0.30.13" num-traits = "0.2.19" [dependencies.image] version = "0.25.2" default-features = false features = ["png"] [/details] [details="The line that crashes"] impl<'a> State<'a> { // Creating some of the wgpu types requires async code pub async fn new(window: &'a Window) -> State<'a> { let size = window.inner_size(); // The instance is a handle to our GPU // Backends::all => Vulkan + Metal + DX12 + Browser WebGPU let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::PRIMARY, ..Default::default() }); // # Safety // // The surface needs to live as long as the window that created it. // State owns the window, so this should be safe. let surface = instance.create_surface(window).unwrap(); // <--- Crash happens here let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::default(), compatible_surface: Some(&surface), force_fallback_adapter: false, }) .await .unwrap(); [/details] Additional details - The repository I linked is an earlier version of my actual project, with unnecessary things cut out - Some of the code originates from this awesome wgpu tutorial [details="My system's configuration.nix"] # Edit this configuration file to define what should be installed on # your system. Help is available in the configuration.nix(5) man page, on # https://search.nixos.org/options and in the NixOS manual (`nixos-help`). { config, pkgs, ... }: { imports = [ # Include the results of the hardware scan. ./hardware-configuration.nix ./nvidia.nix ]; # Use the systemd-boot EFI boot loader. boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; networking.hostName = "nixos"; # Define your hostname. # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant. # Configure network proxy if necessary # networking.proxy.default = "http://user:password@proxy:port/"; # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain"; # Enable networking networking.networkmanager.enable = true; # Set your time zone. time.timeZone = "Europe/Helsinki"; # Select internationalisation properties. i18n.defaultLocale = "en_US.UTF-8"; i18n.extraLocaleSettings = { LC_ADDRESS = "fi_FI.UTF-8"; LC_IDENTIFICATION = "fi_FI.UTF-8"; LC_MEASUREMENT = "fi_FI.UTF-8"; LC_MONETARY = "fi_FI.UTF-8"; LC_NAME = "fi_FI.UTF-8"; LC_NUMERIC = "fi_FI.UTF-8"; LC_PAPER = "fi_FI.UTF-8"; LC_TELEPHONE = "fi_FI.UTF-8"; LC_TIME = "fi_FI.UTF-8"; }; # Enable the X11 windowing system. # You can disable this if you're only using the Wayland session. services.xserver.enable = true; # Enable plasma services.displayManager.sddm.enable = true; services.desktopManager.plasma6.enable = true; # Configure keymap in X11 services.xserver.xkb = { layout = "fi"; variant = "nodeadkeys"; }; # Configure console keymap console.keyMap = "fi"; # Enable CUPS to print documents. services.printing.enable = true; # Enable sound with pipewire. services.pulseaudio.enable = false; security.rtkit.enable = true; services.pipewire = { enable = true; alsa.enable = true; alsa.support32Bit = true; pulse.enable = true; # If you want to use JACK applications, uncomment this # jack.enable = true; }; # Enable touchpad support (enabled default in most desktopManager). # services.libinput.enable = true; # Define a user account. Don't forget to set a password with ‘passwd’. users.users."neropatti" = { isNormalUser = true; description = "neropatti"; extraGroups = [ "networkmanager" "wheel" ]; packages = with pkgs; [ kdePackages.kate obs-studio obsidian rustup prismlauncher mpv gimp pciutils ffmpeg ]; }; programs.obs-studio.package = (pkgs.obs-studio.override { cudaSupport = true; }); programs.ssh.startAgent = true; #boot.initrd.availableKernelModules = [ # "nvidia_drm" "nvidia_modeset" "nvidia" "nvidia_uvm" #]; programs.thunderbird.enable = true; programs.steam.enable = true; # Install firefox. programs.firefox.enable = true; # Allow unfree packages nixpkgs.config.allowUnfree = true; # List packages installed in system profile. # You can use https://search.nixos.org/ to find more packages (and options). environment.systemPackages = with pkgs; [ home-manager wget git qdirstat ]; services.displayManager.autoLogin.enable = true; services.displayManager.autoLogin.user = "neropatti"; # Some programs need SUID wrappers, can be configured further or are # started in user sessions. # programs.mtr.enable = true; # programs.gnupg.agent = { # enable = true; # enableSSHSupport = true; # }; # List services that you want to enable: # Enable the OpenSSH daemon. # services.openssh.enable = true; # Open ports in the firewall. # networking.firewall.allowedTCPPorts = [ ... ]; # networking.firewall.allowedUDPPorts = [ ... ]; # Or disable the firewall altogether. # networking.firewall.enable = false; # Copy the NixOS configuration file and link it from the resulting system # (/run/current-system/configuration.nix). This is useful in case you # accidentally delete configuration.nix. # system.copySystemConfiguration = true; # This option defines the first version of NixOS you have installed on this particular machine, # and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions. # # Most users should NEVER change this value after the initial install, for any reason, # even if you've upgraded your system to a new NixOS release. # # This value does NOT affect the Nixpkgs version your packages and OS are pulled from, # so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how # to actually do that. # # This value being lower than the current NixOS release does NOT mean your system is # out of date, out of support, or vulnerable. # # Do NOT change this value unless you have manually inspected all the changes it would make to your configuration, # and migrated your data accordingly. # # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion . system.stateVersion = "26.05"; # Did you read the comment? } [/details] [details="nvidia.nix"] { pkgs, config, libs, ... }: { # Enable OpenGL hardware.graphics = { enable = true; extraPackages = with pkgs; [ nvidia-vaapi-driver ]; }; # Load nvidia driver for Xorg and Wayland services.xserver.videoDrivers = ["nvidia"]; hardware.nvidia = { # Modesetting is required. modesetting.enable = true; # Nvidia power management. Experimental, and can cause sleep/suspend to fail. powerManagement.enable = false; # Fine-grained power management. Turns off GPU when not in use. # Experimental and only works on modern Nvidia GPUs (Turing or newer). powerManagement.finegrained = false; # Use the NVidia open source kernel module (not to be confused with the # independent third-party "nouveau" open source driver). # Support is limited to the Turing and later architectures. Full list of # supported GPUs is at: # https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus # Only available from driver 515.43.04+ # Currently alpha-quality/buggy, so false is currently the recommended setting. open = false; # Enable the Nvidia settings menu, # accessible via `nvidia-settings`. nvidiaSettings = true; # Optionally, you may need to select the appropriate driver version for your specific GPU. package = config.boot.kernelPackages.nvidiaPackages.legacy_580; # 0a:00.0 VGA compatible controller: NVIDIA Corporation GP106 [GeForce GTX 1060 6GB] (rev a1) # 0a:00.1 Audio device: NVIDIA Corporation GP106 High Definition Audio Controller (rev a1) prime = { # Make sure to use the correct Bus ID values for your system! #intelBusId = "PCI:00:02.0"; nvidiaBusId = "PCI:0a:00.0"; }; }; } [/details] If there's any info I forgot to provide, or anything else I could have done better, let me know! Thank you for taking the time to read this.
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 を順番に分け合えます。優先度も変えられます 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 じゃだめなの? ! Ray と Kubernetes は今回試していません。この章は、ドキュメントと issue を読んで比べた内容です。 だめではないんです。ただ、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 の指定が抜けます。投入をラッパースクリプトにまとめたのは、これが理由でした。 ! ジョブが GPU を持っているかは、ReqTRES ではなく、scontrol show job の TresPerNode=gres/gpu:1 か、scontrol show node -d の GresUsed=gpu:1 で確かめてください。ReqTRES に GPU がなくても、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 を見ても、デスクトップ画面の分しか出ていません。 ! 再起動やフリーズのあとは、squeue の表示を、GPU が働いている証拠にしないでください。ログが進んでいるか、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 を入れて」と頼むところから試してみてください! 参考文献 ymg_aq「手元のGPUを遊ばせないためのジョブスケジューラ入門」Tech Blog - Turing(Zenn) Slurm: slurm.conf、sbatch、scontrol、Quality of Service (QOS) Ray: Starting Ray、GCS Fault Tolerance、KubeRay の RocksDB による GCS の耐障害性 ray-project/ray issue #41437 Kubernetes: Creating a cluster with kubeadm NVIDIA/k8s-device-plugin Kueue Overview
sometimes i scroll thru cutlass issues to read wild bug reports cutlass#3797
Interactive World Simulation with Cosmos for Physical AI We present a shared framework that adapts Cosmos world foundation models for real-time interactive world simulation in Physical AI. By streaming visual observations in response to actions and scene controls, the framework enables users and embodied agents to interact with evolving virtual environments. It supports both predicting the visual consequences of actions and generating observations from physical state maintained by an external simulator. We demonstrate this approach across camera navigation and robot manipulation, controllable video transfer, and multi-view autonomous vehicle (AV) simulation. These examples provide a shared foundation for developing interactive virtual environments for Physical AI training and evaluation. Our code and model checkpoints are available at nvidia/cosmos and huggingface.co. The project website is available at research.nvidia.com. Comments: Project website: research.nvidia.com
NVIDIAのOSS「OpenShell」:自律AIエージェントを権限で閉じ込める実行環境 !海外の記事から !🚀 上級者ワークフロー 📰 海外事例 元記事:OpenShell – A safe, private runtime for fleets of autonomous AI agents(github.com) NVIDIA/OpenShell --- !ざっくり概要 NVIDIAがGitHubで公開している OpenShell(Apache License 2.0)の紹介です。ファイルを読み、パッケージを入れ、APIを呼び、認証情報も使う自律型のAIエージェントを、データ・秘密情報・ネットワークへの無制限なアクセスなしに動かすための実行環境です。各エージェントが触れる範囲を「ポリシー」に宣言しておくと、OpenShellがそれを強制します。ここで紹介するのはREADMEに書かれた範囲で、実際には動かしていません。 <br> !記事の要点 二つの守り方 一つ目はカーネルレベルの強制です。エージェントは隔離されたサンドボックスの中で動き、アクセスできるファイルと使えるシステムコール(OSへの命令)がカーネルの機能で絞られます。ネットワーク接続もすべて、外へ出る前にポリシーの検査を通ります。エージェントは本物の認証情報を見ることがなく、承認済みの宛先へ向かう通信にだけ、OpenShellが認証情報を付け足します。 二つ目は、ポリシー変更の形式検証です。ポリシーを変えて新しいホストへ認証情報つきで接続できるようになる、新しいAPIメソッドを呼べるようになる、といった危うい権限拡大を、承認の前に数学的な検証(形式検証)で見つけます。該当する変更は人の確認待ちになります。 使い始め Linux、Apple SiliconのMac、WSL 2上のWindows(実験的)で動き、Docker・Podman・仮想化のいずれかが要ります。READMEのクイックスタートは、インストーラーを実行して openshell sandbox create --name demo を打つだけです。初期のサンドボックスはエージェントなしの最小のUbuntuで、実際のエージェントはチュートリアルで入れます。そこでは OpenCode を無料のOpenRouterモデルで動かし、エージェントが新しい権限を必要とするたびに承認していく流れが示されています。 周辺の仕組み 制御役のゲートウェイをKubernetes(コンテナを大量に管理する基盤)へ置く構成や、各言語の開発キット(SDK)も用意されています。 気をつけたい点 バージョンは0.1系で、公式が安定版の運用や新しい隔離機能を整えている途中です。また、匿名の利用統計を集める設定が標準で入っています。集めるのは操作の種別と回数だけで、サンドボックス名・ファイルパス・プロンプト・認証情報・モデル名は含まないと書かれています。止めたいときは環境変数 OPENSHELL_TELEMETRY_ENABLED=false を設定します。 <br> !編集者のひとこと 自律エージェントで一番気をつけるのは、どこまで読ませて、どこまで動かさせるかだと思います。その封じ込めの環境をNVIDIAが用意しているのが面白い。 特に引かれたのは、OpenCodeを無料のOpenRouterモデルで動かす例です。無料モデルは入力が学習に使われるのが基本だと理解しているので、読ませてはいけない範囲は最初から閉じておきたい。OpenShellで封じ込めた状態から始めて、必要な権限だけ足していけば、初手で読み込まれすぎる事故は避けられる。そこがポイントかなと。
I got so much fucking swag even NVIDIA copying my shit helixdb/helix-foundry
I had to go digging into the source to realize what it was, e.g. github.com/NVIDIA/auto-ontology/blob/main/auto_on…
finally got to try nvidia openshell to sandbox my intern after having plugged off my dgx spark out of security scares NVIDIA/OpenShell will transfer to my dgx up next t.co
Nvidia – open-source auto-ontology | Discussion