未経験者が世界最大級DeepSeek-R1(671B)でマルチノード推論とSFTを成功させた記録
この記事の対象読者
単一GPU(または単一ノード)でのLLMのファインチューニング経験があり、次にマルチノードでの分散学習・推論に挑戦したい方
DeepSeek-R1のような大規模MoEモデル特有の「落とし穴」と、その具体的な回避策に興味がある方
vLLMやRay、device_mapを使った大規模推論・学習の具体的なノウハウを知りたい方
松尾研LLM開発コンペ のレベル感やコミュニティの雰囲気を知りたい方
概要
本稿は、LLMモデル開発未経験の私が、天才ぞろいのチームoNo.1で世界最大規模のDeepSeek-R1 0528(671B)に挑戦し、マルチノード推論とSFTを成功させるまでの記録です。
背景と挑戦
- 未経験からの挑戦:分散学習・推論の実務経験どころかモデル開発未経験状態でコンペティションに参加
- 世界最大規模モデル:DeepSeek-R1 0528(671B、MoE構成)という巨大モデルでの実装
- 2段階アプローチ:①マルチノード推論の安定化 → ②限定層QLoRAによるSFT動作検証
成功の鍵:徹底的な理解
本プロジェクトで私が採用したのは、マイクロソフトの牛尾剛さんが実践されている「徹底的に理解してから進む」アプローチでした。
- 各コマンド・パラメータの意味を自分なりに理解するまで次に進まない(完全理解ではない)
- エラーが起きたら根本原因を突き止めてから対処
- ドキュメントやコードを読み込み、"なぜそうなるのか"を自分の頭の中で言語化
- チームメンバーへの報告も「わからない」ではなく「ここまでやってみて自分なりに理解したが、この部分が不明」という形で行う
結果
この「急がば回れ」の姿勢が功を奏し、①マルチノード推論の安定稼働、②その勢いでSFT動作検証の完了という2つのマイルストーンを達成できました。
以下、具体的な作業内容と得られた知見を共有します。
背景:松尾研LLM開発コンペとチーム配属
松尾研LLM開発プロジェクトとは
東京大学松尾・岩澤研究室が主催する「LLM開発コンペ2025」は、汎用データを活用したコンペティション形式のLLM開発を通じて、日本のAI技術力向上と次世代開発者育成を目指すプロジェクトです。評価指標として、世界トップレベルの研究者・専門家向けベンチマーク「Humanity's Last Exam(HLE)」でのSotA(State-of-the-Art:最高性能)達成を目標に掲げていました。
本プロジェクトは、国立研究開発法人新エネルギー・産業技術総合開発機構(NEDO)の「日本語版医療特化型LLMの社会実装に向けた安全性検証・実証」における基盤モデル開発プロジェクトの一環として実施されています。
私が予選で配属されたチーム
私が予選で配属されたチームは、リーダーシップに優れ全方位に稀有な才を持つリーダー、明確な戦略を描く戦略家、モデル評価分析・博識のスペシャリスト、世界最先端学習手法を理解する専門家など、各分野のエキスパートが揃うチームでした。未経験者の私から見れば「天才の集まり」のような環境で、正直なところ、貢献できるか不安も大きかったのが本音です。
チームの戦略:世界最大規模モデルでの挑戦
チームが掲げた戦略は明快でした。当時世界最大規模のオープンモデルである**DeepSeek-R1 0528(671B、MoE構成)を事後学習(Post-training)**し、HLEでのスコアを最大化してコンペ1位を目指す——。この大胆な戦略のもと、私はマルチノード推論とSFT(Supervised Fine-Tuning)の動作検証というミッションを任されました。
実践編:未経験者が取り組んだ2つのステップ
ここからは、未経験者の私が「徹底的に理解してから進む」アプローチで、どのようにマルチノード推論とSFTを成功させたのか、具体的な作業内容と得られた知見を共有します。
作業内容①:マルチノード推論を先に成功させて土台を作った
作業の背景
SFT(事後学習)を実施する前に、まずDeepSeek-R1(671B)の推論を安定稼働させる必要がありました。この段階で分散システムの基礎(クラスタ構築・通信・メモリ管理・並列度調整)を理解し、運用勘を掴むことが後続作業の成否を左右します。
環境構築の難易度:小規模モデルとの圧倒的な差
ここで強調したいのは、671Bという超大規模モデルの環境構築は、小規模モデル(7B~70B級)とは次元が異なるということです。
小規模モデル(例:7B~70B級)の場合:
- 単一GPU(または単一ノード)で動作可能
-
python run.pyのような単純なコマンドで起動 - 環境変数やライブラリのバージョン違いがあっても、エラーメッセージが明確で対処しやすい
- メモリ不足やクラッシュが起きても、数分で再起動して再試行できる
一方、671B級の超大規模モデルの場合:
- 複数ノード・複数GPUの協調動作が必須:今回は16GPU(2ノード×8GPU)を正確に連携させる必要がある
- 分散通信の設定が複雑:Ray/DeepSpeedなどの分散フレームワークで、ノード間IP、ポート、通信プロトコルを正確に設定
- 些細なミスが致命的:環境変数1つ、ライブラリバージョン0.1の違い、共有メモリの容量不足で、クラスタ全体がクラッシュ
- エラーの原因特定が困難:16GPUのどこで何が起きているのか、ログを読み解くだけで数時間かかることも
- 再起動コストが膨大:一度クラッシュすると、クラスタの再構築・モデルロード・検証まで30分~1時間以上かかる
つまり、環境構築の段階で徹底的に理解し、堅牢な土台を作ることが絶対条件でした。ここを曖昧にしたまま進めば、SFT段階で原因不明のエラーに何日も悩まされることになります。私は「推論を先に完璧に動かす」という方針により、この落とし穴を回避できました。
目的
DeepSeek-R1(671B、MoE)のマルチノード推論を安定稼働させ、以下を達成する。
- 16GPU(2ノード×8GPU)Rayクラスタの構築と動作検証
- vLLMサーバーでのHLE推論実行の成功
- 分散環境の運用勘(ログ監視・トラブルシュート・復旧手順)の獲得
これにより、SFT実施時の「環境起因のエラー」を事前に排除する。
実施内容
- 構成
- 2ノード × 8GPU(各80GB)想定、Rayクラスタで分散実行
- vLLM サーバーを TP=16 + Expert Parallel 有効で起動
- 実施手順
- ジョブ確保(2ノード16GPU)→ 各ノードでモジュール・Condaを統一
- Rayヘッド起動(
--head、固定IP)、ワーカー参加(明示IP)→ray statusでGPU=16/16を確認 - vLLM 起動(
--tensor-parallel-size 16 --enable-expert-parallel --distributed-executor-backend ray) - 主要パラメータは GPUメモリ利用率 0.90–0.93、
max-num-seqsとmax-num-batched-tokensを段階調整
- つまずきと対処
- Ray接続不安定: ヘッドIPの明示、
ray stop --force && rm -rf /tmp/ray*で再初期化 - 共有メモリ不足:
/dev/shmの掃除と容量確認をルーチン化 - ライブラリ競合: CUDA/cuDNN/FA2 を推奨バージョンを見つけ出して固定(PyTorch 2.7 + cu126)
- Ray接続不安定: ヘッドIPの明示、
- 結果
- クラスタが安定(GPU 16/16)、大規模KVキャッシュ下でも推論スループットが安定
- HLE系の推論実行まで通し、ダッシュボードでワーカー負荷とスループットの感覚を掴めた
わかったこと(推論編)
- まず"通信と並列の健全性"を固めると、その後のチューニングがすべて速い
- ノード間のIP固定とクリーンアップ手順は"テンプレ化"しておくべき
- vLLMの並列度(TP/EP)とメモリ設定は保守的に始め、安定を確認してから上げる
この成功体験により、分散システムへの不安が自信に変わり、次のSFTステップへの心理的ハードルが大きく下がりました。
作業内容②:限定層のみQLoRAでSFTを安定化
作業の背景
マルチノード推論の安定化に成功したことで、分散環境の基礎が固まりました。次のステップは、この環境を活用してDeepSeek-R1を事後学習(SFT)し、HLEベンチマーク向けの出力形式と正答率を向上させることです。ただし、671Bという巨大モデルを全パラメータ更新するのは現実的ではないため、限定層のみをQLoRAで効率的に学習するアプローチを採用しました。
目的
DeepSeek-R1(671B)を最小限の計算リソースで安全にSFTする。
- 4bit量子化(NF4 + double quant)によるメモリ削減
- 最終層Attentionのみへの限定的LoRA適用による学習の安定化
- まずは「動作検証」を優先し、少量データで完走させる
実施内容
1. DeepSeek-R1特有の制約と対策
DeepSeek-R1は通常のTransformerモデルとは異なる構造(MoE:Mixture of Experts)を持ち、汎用の学習フレームワーク(Hugging Face Trainer等)では以下の問題が発生します。
(1) MoEゲートの学習モード問題
-
問題:DeepSeek-R1のMoEゲート(Expert選択機構)には、学習モード(
self.training=True)で動作させるとassert not self.trainingエラーが発生する実装上の制約がある -
対策:MoEゲート部分を強制的に評価モード(
eval())に固定し、パラメータを凍結(requires_grad=False)
# MoE gate: eval固定+凍結(学習禁止assert回避)
for name, module in model.named_modules():
if name.endswith(".mlp.gate"):
module.eval()
for p in module.parameters():
p.requires_grad = False
- さらに、
model.train()呼び出し後も再度同じ処理を実行し、ゲートのevalモード維持を保証
(2) MLP(FFN)のshared_experts実装問題
-
問題:DeepSeek-R1のMLP層には、変数の初期化順序に起因する
UnboundLocalError: local variable 'y' referenced before assignmentが発生する既知の不具合がある -
対策:MLP層の
forwardメソッドを恒等写像(入力をそのまま返す)で上書きし、実質的に無効化+全パラメータ凍結
# helper: MLP forward identity
def _mlp_forward_identity(self, hidden_states, *args, **kwargs):
return hidden_states
# Disable MLP path (identity forward)
for name, module in model.named_modules():
if name.endswith(".mlp"):
module.forward = _mlp_forward_identity.__get__(module, type(module))
for p in module.parameters():
p.requires_grad = False
2. 限定層LoRAの戦略的設計
(1) 最終層Attentionのみを学習対象に
-
理由:
- MLPを無効化しているため、学習可能なのはAttention層のみ
- 全層を学習すると計算コスト・メモリ消費が膨大
- 最終層は出力に最も近く、形式・スタイルの調整に効果的
- 実装:
# 最終層のself_attnから線形層を自動検出
last_idx = num_layers - 1
for n, m in model.named_modules():
if f"model.layers.{last_idx}.self_attn." in n and "linear" in m.__class__.__name__.lower():
candidate.add(n.split(".")[-1])
# 優先順位付けでtarget_modules選定(q_a_proj, q_b_proj等)
targets = ["q_a_proj", "q_b_proj"] # 例
# LoRA注入後、最終層以外は凍結
model = get_peft_model(model, lora_config)
for n, m in model.named_modules():
if "lora_" in n and (f"model.layers.{last_idx}." not in n):
for p in getattr(m, "parameters", lambda: [])():
p.requires_grad = False
(2) LoRA設定
- LoRA階数:r=4(最小限の低ランク適応)
- alpha:α=8(r×2で適度なスケーリング)
- dropout:0.0(少量データなので過学習リスクは低い)
- 学習率:5e-5(保守的な値で安定性確保)
3. 汎用Trainerを使わない理由と手動ループの実装
(1) Trainerが使えない理由
-
Accelerateの自動デバイス配置との衝突:既に
device_mapで8GPU分散配置済みのモデルに対し、Accelerateが再配置を試みて衝突 - MoEゲート・MLP対策の複雑さ:Trainerのフック機構では、上記のeval固定・forward上書きを適切なタイミングで実行するのが困難
- 柔軟性の欠如:最終層のみLoRA学習という特殊な設定に対応しにくい
(2) 手動学習ループの実装
# optimizer(LoRAパラメータのみ)
trainable = [p for p in model.parameters() if p.requires_grad]
opt = PagedAdamW8bit(trainable, lr=5e-5)
model.train()
# MoE gate再固定(model.train()後に必須)
for name, module in model.named_modules():
if name.endswith(".mlp.gate"):
module.eval()
for p in module.parameters():
p.requires_grad = False
# 手動ループ
max_steps = 10
while step < max_steps:
for i in range(0, num_samples, batch_size):
batch = {k: v[i:i+batch_size].clone() for k, v in encoded.items()}
batch = collator([...]) # DataCollatorで整形
for k in list(batch.keys()):
batch[k] = batch[k].to(first_device, non_blocking=True)
opt.zero_grad(set_to_none=True)
out = model(**batch)
loss = out.loss
loss.backward()
opt.step()
step += 1
4. device_mapによる8GPU分散配置とOOM回避の鍵
OOM(Out of Memory)との戦い:なぜ手動配置が必要か
671Bモデルを8GPU(H100 80GB×8)で動かす場合、自動配置(device_map="auto")ではOOMで失敗します。理由は以下の通りです。
(1) モデルロード時のメモリ急増
- NVMe(safetensorsファイル)→ CPU → GPU への3段階転送
- 自動配置では、全層をCPUに一旦展開してからGPUに転送するため、CPUメモリが瞬間的に数百GB必要
- 671Bモデル(BF16で約1.3TB)を4bit量子化しても、中間バッファで数百GBは消費
(2) 不要な層のGPU配置によるメモリ圧迫
- 自動配置は「均等分散」を目指すが、実際には以下の層がGPUメモリを無駄に消費:
-
Embedding層(
model.embed_tokens):巨大な語彙埋め込みテーブル、推論時のみ使用 -
正規化層(
model.norm):小さいが、最終段にあるためGPU配置が強制される -
LM Head(
lm_head):出力語彙への投影層、推論時のみ必要
-
Embedding層(
(3) 手動配置によるOOM回避戦略
以下の手動device_mapにより、学習に不要な層はCPUに退避し、GPUメモリを最大限確保します。
# 基本構成:層を8GPU(H100 80GB×8)に均等分散
device_map = {"model.embed_tokens": "cuda:0"} # Embedding層はGPU0に配置(入力処理で必要)
for i in range(num_layers):
device_map[f"model.layers.{i}"] = f"cuda:{i % len(gpus)}" # Transformer層を8GPUに循環配置
device_map["model.norm"] = f"cuda:{gpus[-1]}" # 正規化層は最終GPU
device_map["lm_head"] = f"cuda:{gpus[-1]}" # LM Headは最終GPU
# さらに、max_memoryで各GPUの使用上限を明示的に制限
max_memory = {f"cuda:{i}": "78GiB" for i in range(8)} # GPU当たり78GiB上限(80GBの97.5%)
max_memory["cpu"] = "300GiB" # CPUは大容量確保(中間バッファ+オフロード用)
ポイント:なぜEmbed_tokens層をGPU0に配置するか
一見、Embedding層はCPUに逃がすべきと思われますが、実際には以下の理由でGPU配置が必要です。
- 入力処理の開始点:全バッチの入力トークンをEmbeddingに通すため、CPUにあるとGPU↔CPU転送コストが毎ステップ発生
- 4bit量子化の恩恵:量子化により、Embedding層のメモリフットプリントは1/4に削減(BF16時の巨大さが緩和)
- GPU0の余裕:Transformer層0~63の一部がGPU0に配置されるが、量子化により1GPU当たり約10層は余裕で収まる
逆に、学習に直接関与しない層(例:未使用のExpert、凍結された中間層)はCPUに退避する選択肢もあると思います(今回はシンプルさ優先でGPU配置)。
メモリ管理の全体像
# NVMeオフロード:ロード中の一時ファイルをNVMeに書き出し
offload_folder = os.path.join(nvme_dir, "offload")
os.makedirs(offload_folder, exist_ok=True)
# モデルロード時の設定
model = AutoModelForCausalLM.from_pretrained(
model_path,
quantization_config=bnb_config,
device_map=device_map, # 手動配置
max_memory=max_memory, # GPU/CPU上限明示
low_cpu_mem_usage=True, # CPU側も省メモリモード
offload_state_dict=True, # 辞書をNVMeに退避
offload_folder=offload_folder, # NVMeオフロード先
torch_dtype=torch.bfloat16,
)
OOM回避の検証
ロード完了後、以下で各GPUのメモリ使用状況を確認します。
nvidia-smi
# 期待:各GPU使用率 70~78GiB(80GBの88~97%)
この手動配置により、671Bモデルでも8GPU×80GB(合計640GB)に収まり、OOMを回避して学習を完走できます。
まとめ:手動device_mapがOOM回避の鍵
- 自動配置ではほぼ確実に失敗:CPUメモリ不足+GPU配置の非効率
- 手動配置で層単位の最適化:学習に必要な層のみGPUに配置、不要な層はCPU退避(今回は全層GPU配置で成功)
- max_memory明示:各デバイスの上限を設定し、暴走を防ぐ
- NVMeオフロード活用:ロード中の一時ファイルをNVMeに書き出し、CPUメモリを節約
この手法は、超大規模モデルを限られたリソースで動かすために必要なテクニックです。
5. 実行結果
-
成功指標:
- 10ステップの学習完走(loss値の出力確認)
- アダプタファイル(
adapter_model.safetensors)の生成 - 最終層LoRAテンソル(
lora_B.weight等)の非ゼロ確認(更新の証明)
-
出力先:
/nvme12/$USER/minqlora_out_loop
わかったこと(SFT編)
- DeepSeek-R1特有の地雷を回避:MoEゲートとMLP問題は、ソースコードレベルの理解と対策なしには突破不可能
- 最終層Attentionのみでも効果的:q/k/v層だけでも"思考の流れ"と"情報選択"のクセは十分に整えられる
- 手動ループの重要性:汎用Trainerに頼らず、モデル固有の制約に合わせたカスタム実装が安定性の鍵
-
手動device_map配置がOOM回避の生命線:自動配置(
device_map="auto")ではほぼ確実にOOMで失敗。層単位での手動GPU/CPU配置+max_memory明示+NVMeオフロードの3点セットが、671B級超大規模モデルを限られたリソースで動かす必要テクニック - まずは動かす:少量データ・短ステップでも「完走」させることで、次の本格学習への確信が得られる
振り返り:2つのステップが相互に強化し合った
推論の安定化がSFT成功の後押しに
マルチノード推論を先に成功させた経験が、SFTの成功確率を大きく高めました。
- 分散環境の不確実性を排除:先にマルチノード推論を成功させたことで、分散周りの不確実性を排除できた
- 異常検知能力の向上:ログの読み方(ボトルネック推定)が洗練し、SFTの異常検知が早くなった
- 復旧可能な運用設計:"戻れる運用"(チェックポイント・クリーンアップ・再起動テンプレ)が整備でき、SFTの試行回転が速まった
未経験者でも成果を出せた理由
振り返ってみると、以下の要素が成功の鍵でした。
- 段階的アプローチ:推論→SFTと段階を踏むことで、各ステップで確実に理解を深められた
- 徹底的な理解の姿勢:「なぜそうなるのか」を理解するまで進まない姿勢が、トラブル時の対処力につながった
- テンプレ化とドキュメント化:成功パターンを再利用可能な形で整理し、試行錯誤の時間を短縮できた
- チームの支援:専門家揃いのチームからの的確なアドバイスが、学習曲線を大きく加速させた
東京大学松尾・岩澤研究室LLMコミュニティの素晴らしさ
最後に、このプロジェクトを通じて強く感じた松尾・岩澤研究室のLLMコミュニティの素晴らしさについて触れたいと思います。
初心者に開かれた門戸
このコミュニティの最大の特徴は、初心者でも受け入れてくれる懐の深さです。LLMモデル開発未経験の私がプロジェクトに参加できたこと自体が、その証明です。「経験がないから無理」ではなく、「誠実な動機と挑戦する心があれば、誰にでも機会を与える」という姿勢が貫かれています。
理論と実践の両輪
多くのコミュニティでは理論的な議論に終始しがちですが、松尾・岩澤研究室では実務的な標準コードの提供まで行われています。
- マルチノード推論のための環境構築スクリプト
- SFT実行のためのテンプレートコード
- トラブルシュート用のチェックリスト
こうした「すぐに使える資産」が整備されているため、初心者でも理論を実践に落とし込むまでの時間を大幅に短縮できます。
挑戦を後押しする文化
コミュニティ全体に「挑戦を応援する文化」が根付いています。
- 質問に対して丁寧かつ的確なフィードバック
- 失敗を責めず、学びの機会として捉える雰囲気
- チームメンバー間での知見の積極的な共有
誠実な動機で取り組む限り、どんなバックグラウンドの人でも挑戦の機会が与えられ、成長できる環境が整っているように感じます。
日本最高峰のLLM開発学習環境
正直に言って、日本国内でLLM開発を学ぶ上で、これより良いコミュニティは見つからないのではないかと感じています。
- 世界最先端の研究知見へのアクセス
- H100×8を複数ノードという圧倒的な計算リソース
- トップレベルの専門家との協働機会
- 初心者にも開かれた参加の門戸
この環境で学べたことは、私にとって一生の財産です。もし、LLM開発に興味があり、挑戦してみたいという方がいれば、ぜひ松尾・岩澤研究室のLLMコミュニティへの参加を検討してみてください。誠実な動機と学ぶ意欲があれば、必ず成長の機会が得られるはずです。
本プロジェクトは、国立研究開発法人新エネルギー・産業技術総合開発機構(「NEDO」)の「日本語版医療特化型LLMの社会実装に向けた安全性検証・実証」における基盤モデルの開発プロジェクトの一環として行われました。