はじめに
RTX 3090(VRAM 24GB)1枚、WSL2上のUbuntuという環境で、7Bパラメータ規模のTransformer系モデルを事前学習させるプロジェクトに取り組んでいます。
クラウドGPUではなく手元のハードウェアだけで完結させたい。その動機は単純で、研究の全工程を自分の手の届く範囲に置いておきたかったからです。
結論から言えば、最終的にはVRAM 22.6GBで安定動作し、約1770 tok/sのスループットに到達しました。しかし、そこに至るまでに踏んだ落とし穴は8つ。optimizer選定だけで6方式を試し、すべて失敗しています。
この記事では、各段階で何が起き、なぜ失敗し、どう判断したかを記録します。7Bモデルを「作った話」ではなく、7Bモデルを個人GPU環境で「安定して学習させるまでの工学的失敗記録」です。
同じ環境で大規模モデルの学習を考えている方の参考になれば幸いです。
追記(2026/8/11): 本記事のモデルでは現在、大規模日本語コーパスを用いた1Bトークン規模の事前学習を進めています。また、通常のTransformerを比較対象とした並列評価も実施しています。
2026/8/11更新: 実験計画の変更 — 並列比較パイプラインへの移行
これまでの経緯
本記事の初回公開後、実験を継続する中で、評価方法を見直しました。
Wikipediaデータによる初期実験(〜7月末):
約100Mトークン規模の日本語Wikipediaデータを用いて学習を行いました。複数の学習量で検証したところ、データの繰り返し利用に伴う性能変化が確認されました。
この実験を通じて、限られたGPU環境でも大規模モデルの学習・評価を継続的に行えるインフラ構成を構築できました。
大規模コーパスへの移行と追加検証(7月末〜8月初旬):
これらの検証を通じて、独自モデルにおける学習時の制御が学習の安定性や評価指標に影響することを確認しました。
この結果を踏まえ、独自モデル単独での長期学習を継続するだけでなく、学習制御を含めたモデルの特性を、通常のTransformerと同一データ系列上で比較検証することが必要と判断しました。
そのため、現在は学習を継続しながら、モデル間の比較をより厳密に行う新しい実験パイプラインへ移行しています。
なお、学習制御の具体的な方式、制御条件および内部的な実装については、現在進めている研究および知的財産保護の都合上、本記事では公開していません。
計画変更: なぜ並列比較が必要になったか
初期実験では独自モデルとTransformerを異なる時期・条件で評価していたため、単純な結果比較だけでは、観測された差がモデル構造によるものなのか、データや学習条件によるものなのかを十分に切り分けられません。
そこで、現在は可能な限り条件を揃えた並列比較へ実験計画を変更しています。
新しい実験パイプライン
旧計画:
独自モデルを単独で1Bトークン規模まで学習し、必要に応じて既存のTransformer結果と比較。
新計画:
同一の学習データ系列を用いて、独自モデル(7B)と比較対象のTransformer(3.3B)を並行して評価します。
現在の計画では、200Mトークンを1単位として、以下のサイクルを繰り返します。
- 次の200Mトークン分の学習・評価用データを準備
- 独自モデル(7B)を学習し、チェックポイントと評価結果を保存
- 同じデータ系列を用いてTransformer(3.3B)を学習し、チェックポイントと評価結果を保存
- 両モデルの評価結果を比較・記録
- 使用済みデータを整理してストレージを回収
- 次の200Mトークンについて同じ処理を実行
5サイクルを実施し、200M / 400M / 600M / 800M / 1Bトークン付近の複数地点で比較する計画です。
この方式の利点
比較条件の統一:
同じデータ系列を使用することで、モデル以外の条件による影響をできるだけ小さくできます。
ストレージ効率:
RTX 3090環境ではGPUだけでなくSSD容量も実験上の制約になります。そこで、学習データを一定量ずつ準備・使用・整理する方式を採用しています。
学習曲線の追跡:
単一の最終結果だけではなく、複数の学習段階で評価することで、学習量に対する性能変化を追跡できます。
これにより、最終的な性能だけでなく、どの程度の学習量でどのような変化が生じるかについても比較できるようになります。
200Mトークン時点の参考比較(初期実験)
以下は初期のWikipedia学習時に取得した結果です。
重要: この結果は現在進めている並列比較実験とは実験条件が異なるため、参考値として掲載しています。新しい実験による比較結果が得られた場合は、そちらを優先して更新する予定です。
CE(Cross Entropy)およびPerplexityは低いほど高性能を示します。
| 指標 | 独自モデル (n=12) | Transformer |
|---|---|---|
| Cross Entropy | 8.44 | 12.01 |
| Perplexity | 4,612 | 164,689 |
| Top-1 Accuracy | 2.4% | 0.3% |
| Top-5 Accuracy | 11.8% | 2.4% |
| 推論速度 | 4,439 tok/s | 10,234 tok/s |
また、計算量を近づけた別条件での比較でも、独自モデル側で良好な評価値が得られました。
ただし、これらはいずれも初期実験の結果であり、現在進めている同一データ系列による比較実験の結果をもって最終的な評価とする予定です。
詳細な研究結果については、別途公開しているプレプリント(https://doi.org/10.51094/jxiv.5809)も参照してください。
※本記事では、研究開発の背景、問題設定、実験環境、比較方法および公開可能な結果の概要を共有しています。学習アルゴリズムの具体的構成、制御則、内部パラメータ、具体的な実装方式、未公開の改良内容については、知的財産および研究上の理由から記載していません。(2026年8月更新)
2026/8/31更新
学習制御の改良版による200Mトークン学習が完走しました。改良版では、学習率制御と再帰深度に関わる複数の制御方式を組み合わせ、事前に閾値試験を実施した上で本番学習に投入しています。200Mトークンの学習を通じて異常終了なく安定した学習が確認されました。
完走後、学習制御の各要素が意図した通りに機能しているかを検証するための閾値試験を実施し、主要な検証項目をすべて通過しました。
現在は、200Mから400Mトークンへの継続学習に向けた学習データの追加生成を進めています。400Mトークン時点での比較結果が得られた段階で本セクションを更新する予定です。
条件:
- 学習データ: 大規模日本語コーパス(CulturaX)、200Mトークン、同一データ系列
- 教師モデル: llm-jp-4-32B(知識蒸留)
- seed=42、単一パス(データ繰り返しなし)
結果(200Mトークン時点の検証損失):
| モデル | パラメータ数 | 検証CE (n=12) |
|---|---|---|
| 独自モデル(7B) | 3.3B(実効) | 9.44 |
| Transformer | 3.3B | 9.44 |
200Mトークン時点では、独自モデルとTransformerの検証CEはほぼ同等(差 < 0.01)でした。
独自モデルはTransformerと比較して、推論時の再帰深度(n)を変えることで計算量を調整できるという構造的な特徴があります。浅い深度(n=3やn=6)でも一定の性能が得られるため、推論時の計算量と性能のトレードオフを柔軟に選択できます。ただし、この特性の定量的な評価は今後の課題です。
環境
- GPU: NVIDIA RTX 3090 (VRAM 24GB)
- OS: WSL2 Ubuntu 24 on Windows
- フレームワーク: PyTorch 2.6 + CUDA 12.4
- 精度: BF16
- モデル: 独自設計の7B Transformer系モデル(詳細非公開)
- データ: 事前処理済み学習データを使用
落とし穴 1: 8bit Optimizerは「ギリギリ収まる」計算が裏切る
最初に試したのは8bit AdamWです。
通常のAdamW (fp32) では、モデルのパラメータ数 × 2(一次・二次モーメント)× 4バイトのoptimizer stateが必要になります。7Bパラメータだと、これだけで数十GB。当然24GBには収まりません。
8bit量子化すればstateは1/4になるので、理論上は収まるはず——と思って実行したところ、学習開始直後に CUDA driver error: device not ready で停止しました。
原因は、optimizer stepの瞬間に一時的なfp32バッファが生成され、それがモデル本体+activationsと合わせてVRAMの物理限界を超えたことです。
「理論上の見積もり」と「実行時のピークVRAM」は別物です。PyTorchのmemory allocatorは断片化を起こしますし、CUDA内部のワークスペースも見えないメモリを消費します。
教訓: 「理論上収まる」は信用しない。実行時のピークVRAMで判断する。
落とし穴 2: CPU Offloadは転送速度が壁になる
次に試したのがCPU Offload方式です。optimizer stateをCPU RAM上に置き、毎step GPU↔CPU間で転送する方法です。
動きました。しかし、スループットは71 tok/s。PCIe 4.0 x16の帯域(理論32GB/s)があっても、毎stepで数GBのパラメータを往復させるオーバーヘッドは無視できません。
しかも、WSL2環境ではCPU側のメモリ管理にも癖があり、長時間実行するとRAM側でOOMが発生しました。
教訓: CPU Offloadは「動く」と「使える」の間に大きな溝がある。RTX 3090クラスの計算速度に対して、PCIeの帯域は圧倒的に不足する。
落とし穴 3: DeepSpeed ZeROはWSL2と相性が悪い
分散学習フレームワークDeepSpeedのZeRO-2/3を単一GPU環境で試しました。
ZeRO-3はVM(仮想メモリ)使用量が296GBまで膨張してOOM。WSL2はWindowsのページファイルを仮想メモリとして使うため、この膨張が致命的です。
ZeRO-2でもoptimizer stateの生成時にRAMが95%に達し、やはりOOM。
単一GPU + WSL2という構成では、ZeRO系は「分散しないのに分散フレームワークのオーバーヘッドだけ払う」結果になりました。
教訓: DeepSpeedはマルチGPU前提の設計。単一GPU + WSL2では、VMとRAMの管理が想定外の挙動を示す。
落とし穴 4: NVMe直接読み書きは桁違いに遅い
optimizer stateをmmapでNVMeに直接配置するStreaming方式も試しました。
スループットは14 tok/s。RTX 3090の計算能力を1/100も使えていない状態です。NVMe SSDはシーケンシャルリードで数GB/s出ますが、optimizerの更新パターンはランダムアクセスに近く、実効帯域は劇的に低下します。
教訓: NVMe I/Oはシーケンシャルでは速いが、optimizer stateのようなランダムアクセスパターンでは実用的な速度が出ない。
落とし穴 5: 既存のAdafactorもfp32コピーで詰まる
ここで発想を転換しました。「巨大なoptimizer stateをどう運ぶか」ではなく、「そもそもstateが小さいoptimizerを使えばよい」。
Adafactorは二次モーメントを行ベクトルと列ベクトルに因子分解して保持します。大規模な重み行列では、この因子分解によるstate削減効果は非常に大きく、理論上はほとんどメモリを消費しません。
これは正しい方向でした。しかし、PyTorchのAdafactor実装をそのまま使ったところ、内部で p.data.float() によるfp32コピーが発生し、内部処理で大きな一時メモリ確保が発生し、VRAM制約により device not ready になりました。
教訓: 「optimizer stateを減らす」という方向性は正しい。ただし、既存実装の内部挙動まで確認しないと、別の場所でメモリが爆発する。
落とし穴 6: 解決策はoptimizer stateを「運ぶ」のではなく「無くす」こと
最終的に、fp32一時コピーを回避するようoptimizer実装を環境に合わせて調整しました。BF16のまま直接更新し、メモリのピーク使用量を最小限に抑える構成です。
結果、optimizer stateをGB単位からMB単位まで削減できました。モデル本体と合わせてVRAM 22.6GBで安定動作しています。
6方式の失敗を振り返ると、問題の構造が見えます。
| 方式 | 失敗の本質 |
|---|---|
| 8bit AdamW | stateは小さくなったが、まだ大きすぎた |
| CPU Offload | stateを移動 → 転送がボトルネック |
| DeepSpeed ZeRO | stateを分散 → 分散先がない |
| NVMe Streaming | stateを退避 → I/Oが遅すぎる |
| 標準Adafactor | stateは小さいが、実装が別のメモリを消費 |
| 環境適合版 | stateそのものを削減 + 実装レベルでメモリ制御 |
すべての失敗に共通するのは、「巨大なstateが存在する前提で、それをどう扱うか」を最適化していたことです。正解は前提そのものを変えることでした。
教訓: 制約が厳しい環境では、「既存の仕組みを工夫して使う」より「制約に合った仕組みを選ぶ(または作る)」方が効果的。
落とし穴 7: DataLoaderの「見えないボトルネック」
optimizer問題を解決してGPU計算が回り始めると、次のボトルネックが現れました。
スループットは約80 tok/s。GPUベンチマークではforward単体で1800 tok/s以上出ているのに、20倍以上遅い。
時間内訳を計測したところ、学習ループの88%がDataLoader待ちでした。
原因は torch.tensor() の呼び出しです。学習データをNumPy配列から読み込む際に、サンプルごとに torch.tensor() を呼んでいました。この関数はPython→C++のブリッジコストが高く、小さな配列に対して毎回呼ぶと積み重なります。
修正は、shard(数千サンプルの塊)単位で torch.from_numpy() を1回だけ呼び、あとはインデックスで取り出す方式に変更。DataLoaderの占有率は88% → 2%に低下し、スループットは約1770 tok/sに到達しました。
教訓: PyTorchのtensor生成は意外に高コスト。大量のサンプルを扱う場合は、まとめて変換してからインデックスアクセスする。GPUがボトルネックだと思い込まず、必ず計測する。
落とし穴 8: torch.compileと動的モデルの構造的非互換
最後にtorch.compileを試しました。mode="reduce-overhead" はCUDA Graphを活用してカーネル起動のオーバーヘッドを削減するモードで、5〜15%の高速化が期待できます。
起動直後は正常に動作。しかし、約20分後に CUDA driver error: device not ready で停止しました。
原因は、CUDA Graphが「固定された実行グラフ」を前提とするのに対し、動的な実行経路を持つモデルでは相性問題が発生したことです。実行時の形状や処理経路が変化するモデルでは、CUDA Graphのキャッシュ効率が低下し、最終的にGPUドライバがリセットされる場合があります。
さらに、gradient checkpointingとの組み合わせも問題を悪化させます。checkpointingはbackward時にforwardを再実行しますが、この再実行パターンもCUDA Graphとは相性が悪い構成です。
torch.compileを外した状態で約1770 tok/sが出ているため、compileによる追加の高速化は不要と判断しました。
なお、torch.compileを有効にした状態で保存されたチェックポイントには、state_dictのキーに _orig_mod. というプレフィックスが付加されます。compile無効版でロードする際はこのプレフィックスを除去する処理が必要です。
教訓: torch.compileの reduce-overhead モードは、実行グラフが動的に変わるモデルには使えない。これはバグではなく、CUDA Graphの設計上の制約との構造的な非互換。
プロファイルに関する補足: sync無しの計測は嘘をつく
落とし穴ではありませんが、デバッグ中に遭遇した重要な知見を記録しておきます。
学習ループの時間内訳を time.perf_counter() で計測したところ、以下のような結果が出ました。
Forward=24% Loss=55% Backward=9% Optimizer=12%
Loss計算が55%?Backward(逆伝播)が9%?これは直感に反します。
原因は torch.cuda.synchronize() を挟んでいなかったことです。PyTorchのGPU演算は非同期で実行されるため、time.perf_counter() で区間を測っても、前の区間のGPUカーネルがまだ実行中の場合があります。結果として、Backwardの計算時間がLossの計測区間に「漏れ出す」現象が起きます。
synchronize() を挟んで正確に計測すると、実際の内訳は以下でした。
Forward=25% Loss=1% Backward=75%
Backward=75%でForward=25%、つまりBWD/FWD比=3.0x。これはgradient checkpointingの理論値と完全に一致します(backward中にforwardが再実行されるため、約3倍)。
ただし、synchronize() 自体にオーバーヘッドがあるため、常時有効にするとスループットが低下します。最初の数十iterationだけ有効にして計測し、以降は無効にする実装が実用的です。
教訓: GPUプロファイルでは torch.cuda.synchronize() を挟まないと、非同期実行により計測区間がずれる。ただし常時syncは性能を落とすので、計測時のみ有効にする。
最終構成
| 項目 | 選択 |
|---|---|
| Optimizer | Adafactor系(環境に合わせた低メモリ実装) |
| Gradient Checkpointing | ON(VRAM節約のため必須) |
| torch.compile | OFF(動的モデルとの非互換) |
| DataLoader | shard単位tensor変換 + streaming |
| バッチサイズ | 固定値(adaptive制御はCUDAコンテキスト破壊の原因) |
| プロファイル | 最初の数十iterのみsync付き計測 |
VRAM 22.6GB安定、約1770 tok/s。
まとめ: 8つの教訓
- 理論上のVRAM見積もりは信用しない。 ピークVRAMは実行してみないと分からない。
- 「stateを運ぶ」より「stateを無くす」。 CPU offload、分散、退避はすべて帯域制約に衝突する。
- 既存実装の内部挙動まで確認する。 Adafactorでもfp32コピーが隠れていた。
- DeepSpeedはマルチGPU前提。 単一GPU + WSL2では期待通りに動かない。
- DataLoaderのtensor生成は意外に重い。 まとめて変換する。
- GPUがボトルネックだと思い込まない。 計測すれば本当の原因が見える。
- torch.compileは動的実行グラフに使えない。 CUDA Graphの構造的制約。
- adaptive制御(バッチサイズ、メモリ管理等)は避ける。 CUDAコンテキストの安定性を優先する。
ひとつひとつは「言われてみれば当然」かもしれません。しかし、実際にコードを動かしてエラーログと向き合うまでは、どれも「自分には関係ない」と思っていたものばかりです。
同じ道を歩む方が、同じ穴に落ちずに済むことを願っています。