背景
松尾研究室が運営するLLMコンペ2025に参加させていただきました。私は、SFTのコード作成を担当しました。
1チームあたり3ノード使用でき、Qwen3-235B-A22Bなどの大きなモデルを学習する必要があったのですが、英語、中国語などを調べてもマルチノードのLLMファインチューニングの情報がなく、とても苦労しました。
特に、trlを用いてQwen3-235B-A22Bの学習に成功した公開されている情報は一切なく、この記事が初めての例だと思います。
最終的にSFTをすることが出来たので情報を公開します。
ライブラリについて
多くのチームがaxolotlを用いていたようでした。しかし私達のチームでは trl
(OpenR1)を使用しました。
trlはhugging face公式のリポジトリであり、LLMファインチューニングにおけるデファクトスタンダードと言えます。日々拡張されつづれているため、様々な技術を簡単に使用することが出来てとても便利です。
また、Open-R1はtrlを用いて作られたLLMのファインチューニング用のリポジトリです。
私達のチームではより手早くPDCAを回すために実装を早くしたいという理由からtrl (Open-R1)を用いることにしました。
マルチノードについて
マルチノードのプログラムは、シングルノードに比べて難易度が高くなります。私達もシングルノードの学習はすぐに出来たのですが、マルチノードの学習が上手くいかずとても時間がかかりました。
マルチノードにおいて、各ノードは完全に別のPCです。したがって、マルチノードでプログラムを動かすイメージとしては、以下になります。
- 各ノードでノード固有のIDを渡して、同じプログラムを動作させる。
- 各ノードは特定のポートを通して通信をして、互いを認識する。
- プログラムの中で、他のノードの情報が必要な場合はそのポートで送受信する。
以下の実装コードを見てもらった方がいいかもしれません。
実装コード
以下のコードは実際にQwen3-235B-A22BのSFTを行った時のコードです。コメントアウトなどは消して見やすくしてます。ただし、私達が使用していた環境で動いたということなので、他の環境で動く保証はないので注意してください。
#!/bin/bash
#SBATCH --partition P02 # 利用するパーティション(キュー)
#SBATCH --ntasks-per-node=1 # 1ノードあたりのタスク数
#SBATCH --nodes=3 # 利用するノード数
#SBATCH --gpus-per-node=8 # 1ノードあたりのGPU数
#SBATCH --nodelist osk-gpu[54,56,91] # 利用するノードのリスト
#SBATCH --job-name sft-235b # ジョブの名前
#SBATCH --time 72:00:00 # ジョブの最大実行時間
#SBATCH --output sft-235b.out # 標準出力ファイル
#SBATCH --error sft-235b.err # 標準エラーファイル
#SBATCH --mem=0 # 各ノードのメモリサイズ
#SBATCH --cpus-per-task=160 # number of cores per tasks
# Slurmで確保したノードリストの先頭をマスターノードのアドレスとして設定
export MASTER_ADDR=$(scontrol show hostnames $SLURM_JOB_NODELIST | head -n 1)
# 使用されていない適当なポート番号を設定 (例: 29500)
export MASTER_PORT=29500
# これらは必須ではない可能性が高い
export NCCL_DEBUG=WARN
export NCCL_DEBUG_SUBSYS=ALL
export NCCL_P2P_DISABLE=1
export NCCL_P2P_LEVEL=NVL
export NCCL_IB_GID_INDEX=3
module load cuda/12.8 # nvccを使うためにCUDAをロード
source openr1/bin/activate # venvを有効化
ulimit -v unlimited
ulimit -m unlimited
cd llm2025compet/training/open-r1/src || exit 1
srun --jobid $SLURM_JOB_ID --mem=0 bash -c \
"accelerate launch \
--config_file ../recipes/accelerate_configs/zero3.yaml \
--num_machines 3 \
--num_processes 24 \
--main_process_ip \"$MASTER_ADDR\" \
--main_process_port \"$MASTER_PORT\" \
--rdzv_backend c10d \
open_r1/sft.py \
--config ../../configs/Qwen3-235b/sft/config_main.yaml \
--dataconfig ../../configs/data_configs/sft_ver3_0.yaml"
# 実行コマンド
# sbatch ./llm2025compet/training/commands/sft-qwen-235b.sh
以下注意点などを解説していきます。
Slurm
運営からの指示があり、Slurmを用いました。Slurmはタスク管理が出来るほか、複数のノードで同じプログラムを実行することが出来ます。以下のSlurmの設定はどのノードをどれだけ使用するかの設定です。
#SBATCH --partition P02 # 利用するパーティション(キュー)
#SBATCH --ntasks-per-node=1 # 1ノードあたりのタスク数
#SBATCH --nodes=3 # 利用するノード数
#SBATCH --gpus-per-node=8 # 1ノードあたりのGPU数
#SBATCH --nodelist osk-gpu[54,56,91] # 利用するノードのリスト
#SBATCH --job-name sft-235b # ジョブの名前
#SBATCH --time 72:00:00 # ジョブの最大実行時間
#SBATCH --output sft-235b.out # 標準出力ファイル
#SBATCH --error sft-235b.err # 標準エラーファイル
#SBATCH --mem=0 # 各ノードのメモリサイズ
#SBATCH --cpus-per-task=160 # number of cores per tasks
注意点としては、以下になります。
- ノード数とノードのリストの数は合わせる必要があります。
- --mem=0がないとメモリが足りないと怒られるときがあります。
- --cpus-per-task=160がないと、複数コアを使用できず遅くなるときがあります。
nvcc
これがないと、nvccが無いとエラーが出ました。環境によると思います。
module load cuda/12.8
ulimit
CPUメモリが全然空いているはずなのに、メモリが足りないと言われました。その時にこれを入れると治りました。メモリの使用制限量を撤廃します。
ulimit -v unlimited
ulimit -m unlimited
accelerate
1ノードには8つのGPU(H100)があるため、accelerateが必要です。(torchrunでもいいですが、accelerateの方が簡単にできます) GPUのテンソルの分配などは自動でやってくれます。
注意点は以下になります。
- deepspeed zero3じゃないと、GPUメモリ不足で3ノードでもQwen3-235B-A22BのSFTは出来ません。zero2ではダメです。
- ipアドレスと、ポートを指定する必要があります。これを用いて他のノードと通信します。通信が上手くいかないと、30分ぐらい待機した後訳の分からないエラーを吐きます。
- --rdzv_backend c10dは無くても良かった気がします。入れておくのが吉です。
"accelerate launch \
--config_file ../recipes/accelerate_configs/zero3.yaml \
--num_machines 3 \
--num_processes 24 \
--main_process_ip \"$MASTER_ADDR\" \
--main_process_port \"$MASTER_PORT\" \
--rdzv_backend c10d \
open_r1/sft.py \
--config ../../configs/Qwen3-235b/sft/config_main.yaml \
--dataconfig ../../configs/data_configs/sft_ver3_0.yaml"
実行コマンド
実行コマンドはこれです。bashでこれを実行しましょう
sbatch ./llm2025compet/training/commands/sft-qwen-235b.sh
MoE+zero3+マルチノードの問題
以下が最も大事です。これに何週間も詰まりました。
小さいモデルのマルチノードSFTは出来ていたのですが、モデルをQwen3-235B-A22Bにすると出来なくなっていました。原因は、MoEブロックのパラメータをノードを跨いで置いてはいけないという事でした。
モデルをロードした後、以下のプログラムを入れると、上手く学習出来ました。
# MoE × ZeRO-3 安定化(leaf module 指定)
if getattr(model.config, "model_type", "") == "qwen3_moe":
print("[MoE] Detected model_type='qwen3_moe'. Applying ZeRO-3 leaf module setting...")
# (任意)ルータのロジットを有効化したい場合はコメントアウトを外す
# if hasattr(model.config, "output_router_logits"):
# model.config.output_router_logits = True
# print("[MoE] Enabled output_router_logits=True")
if _QwenSparseMoeBlock is not None:
deepspeed.utils.set_z3_leaf_modules(model, [_QwenSparseMoeBlock])
print("[MoE] Set ZeRO-3 leaf module: Qwen3MoeSparseMoeBlock (direct import)")
else:
print("[MoE] Direct import of Qwen3MoeSparseMoeBlock failed; falling back to name scan...")
set_flag = False
for m in model.modules():
if "SparseMoeBlock" in m.__class__.__name__:
deepspeed.utils.set_z3_leaf_modules(model, [m.__class__])
print(f"[MoE] Set ZeRO-3 leaf module by name: {m.__class__.__name__}")
set_flag = True
break
if not set_flag:
print("[MoE][WARN] Could not find a SparseMoeBlock; ZeRO-3 leaf NOT set (collectives may hang).")
これは以下のGithubのissueに基づくもので、チームメンバーがそれを見つけてくれたおかげで何とか学習出来ました。感謝です。
まとめ
私が、コンペで得た経験を全てこの記事にまとめました。コンペで勝つことは出来ませんでしたが、Qwen3-235B-A22Bという大きなモデルのSFTを実行できたことに満足しております。また、このような機会をくれた松尾研究室に改めて感謝を申し上げたいです。
本記事に載せたコードは全てリポジトリから引用したものです。リポジトリは以下で公開しているのでもっと詳しく知りたい人はそちらをご覧ください。
備考
時間不足でDeepSeek-R1の学習は出来ませんでしたが、出来る可能性は十分にあると思います。
コンペでは、Qwen3-235B-A22BのORPO (DPOのようなもの) の学習にも成功しています。GRPO、DPOはメモリ不足出来ませんでした。
本プロジェクトは、国立研究開発法人新エネルギー・産業技術総合開発機構(以下「NEDO」)の「日本語版医療特化型LLMの社会実装に向けた安全性検証・実証」における基盤モデルの開発プロジェクトの一環として行われます。