単一GPUではもう回らない:メモリ・演算・帯域の壁
2019年までは、モデルを1枚のカードに押し込むというのは batch size を調整するだけで片付くエンジニアリングの問題でした。大規模モデル時代になると、この命題はまるごと書き換わります。パラメータ数は億単位から千億、进而兆単位へ跳び、単一アクセラレータの数十GBのメモリからフレームワークのオーバーヘッド・演算子のワークスペース・通信用バッファを差し引くと、実際にモデルを入れられるのは四分の一にも満たないことがよくあります。残りはどこへ消えたのか。オプティマイザの状態、勾配、活性化値、そして断片化した一時テンソルがそれを食べています。
分解して考えると、1回の訓練で同時に保持すべきものは4つあります。前向伝播で生じる活性化値(逆伝播で勾配を計算するために使う)、各パラメータの勾配、同規模のオプティマイザ状態(モーメンタンや分散など)、そしてパラメータ自体です。前二者は長いシーケンスの訓練ではシーケンス長の二乗で増え、後二者はパラメータ数に線形して増えます。結果として直感に反する現象が起きます。シーケンス長を半分に、batch size を小さくすると、パラメータが多いときよりむしろメモリ圧迫が缓解されやすいのです。
2枚目の壁は演算量です。大規模モデル訓練の浮動小数点演算量は、おおよそパラメータ数・シーケンス長・データ量の積に比例します。単一カードの演算能力が最新フラッグシップカードであっても、千億級モデルのフルプリトレーニングには月単位の時間が必要です。この時間尺度はイテレーション型の研究作業には許容できません。複数カードは「動くようにするため」ではなく、「反復ループを閉じるほど速く走るため」のものです。
3枚目の壁が最も隠れていて、しかも最も過小評価されているのが、帯域の壁です。分散型訓練は計算を多くのカードへ分割しますが、各ステップの間ではテンソルを交換する必要があります。通信量がインターメージの帯域が支えうるスループットを超えた瞬間、計算ユニットは大量に空転し、ハードウェア利用率は崖のように落ちます。このとき複数カードは加速するどころか、少数のカードより遅くなります。本稿で扱う4つの並列戦略は、本質的にすべて同じ問いに答えています:与えられたクラスタ形状と帯域の条件下で、どうやって通信量を演算量以下に押し下げるのか。
並列戦略は「大きいほど良い」ではなく、帯域とメモリの釣り合いを解く問題です。選択を誤ったときの代償は「少し遅い」程度ではなく、追加したカードが完全に無駄になりかねません。
4つの並列は、それぞれ別の次元を切る
初心者が最も取り違えやすいのは、この4つの並列を同じ階層にある4つの選択肢として考えてしまうことです。実際には、これらはモデルの中のまったく異なる次元を削っており、通信コストの桁数そのものが何桁も違っています。まずこの表で全体の直感をつくっておき、残りの各節で一つずつ掘り下げます。
| 並列戦略 | 分割対象 | 通信プリミティブ | 通信頻度 | 通信量の桁 | 硬い制約 |
|---|---|---|---|---|---|
| データ並列(DP) | バッチ次元 | 勾配の all-reduce | ステップごとに1回 | モデルパラメータ数に比例 | 参加カードは完全なモデルを保持できる必要がある |
| テンソル並列(TP) | 重み行列の行/列 | all-reduce または all-gather | Transformer 層ごとに最低2回 | シーケンス長・隠れ次元と比例 | 高帯域ドメイン内に固定されている必要がある |
| パイプライン並列(PP) | 層 | ポイントツーポイントの send/recv | マイクロバッチごとに1回 | マイクロバッチの活性化値サイズに比例 | パイプラインバブル(pipeline bubble)が存在する |
| エキスパート並列(EP) | MoE のエキスパート | all-to-all によるトークン分散 | MoE 層ごとに | トークン数・隠れ次元と比例 | キャパシティ係数とロードバランスが必要 |
この表を上から下へ読んでいくと、明確な段階関係が見えてきます。データ並列は通信量が最も「重いが」頻度は最も低く、テンソル並列は頻度が最も高い一方で1回あたりは最も「軽く」、パイプライン並列は勾配をほとんど送らず活性化値だけを送るうえ、エキスパート並列は高い頻度と大きなデータ量を同時に備えており、最も調整が難しい種類です。この性質が、後述のあらゆる技術的トレードオフを直接規定します。
さらに重要な組織原則があります。通信コストが高い並列次元ほど「小さく閉じ込める」べきで、つまりその並列度をインターメージが支えうる範囲内に収めておく必要がある。一方、通信コストが並列度に緩やかにしか増えない次元(典型的にはパイプライン)は、モデルを大量の比較的低速なノードへ広げ配置するのに向いています。この原則は後の3D並列の節で定量的に示します。
データ並列:通信量は小さいが、同期はコストが高い
データ並列の考え方は、わずかに自明なほど単純です。各カードがモデルの完全コピーを持ち、このバッチのデータを何個かに分けて各カードへ振り分け、それぞれが自分の勾配を前向・逆伝播で計算し、逆伝播が終わった時点で全カードの勾配を一度だけ同期して、モデルパラメータを一致させます。そうすれば、モデル容量は単一カードのメモリに制約されず、計算量は全カードへ線形に薄められます。
ただし、その「単純さ」は概念レベルの話に留まります。データ並列の実際の難関は2つ。ひとつは勾配同期の1回あたりの通信量が大きすぎることで、もうひとつはこの同期が硬いバリアであり、計算で覆い隠せないことです。
勾配蓄積:実効バッチと通信回数の分離
まず勾配蓄積の話から入ります。データ並列のオーバーヘッドに直接結びついているからです。ロジックはこうです。大きなバッチを複数のマイクロバッチに分割し、前向・逆伝播を何度も回す。毎ステップ同期するのではなく勾配をバッファに積み上げ、設定回数だけ溜まったら all-reduce を一度だけ実行します。数学的には、これはより大きな実効バッチと同じものになります。
重要な判断材料はここです。勾配蓄積は all-reduce の回数を減らしますが、1回あたりの all-reduce の通信量は減らしません。勾配バッファに fp32 の全量勾配が入っているなら、32回蓄積してから一度だけ同期しても、通信量は全量勾配分のままです。32回送る必要がなくなっただけのこと。ここの勘定を誤る人は多く、蓄積すれば通信コストが 1/32 まで落ちると思ってしまいます。実際には、32回の送信が1回の大きな送信にまとめられただけです。
では勾配蓄積はいったい何を節約するのか。節約されるのは通信の起動オーバーヘッド、バッファのピークメモリ、そして帯域が制約されているときのネットワーク占有時間です。より重要なのは、これが1つの制御手段になることです。全体の実効バッチを変えずに同期頻度を調整し、「同期回数を少なくする」と「1回の通信が帯域をまとめて占有する」の間でトレードオフできる。
| 戦略 | 実効バッチ | all-reduce 回数 | 1回あたりの通信量 | 向いているケース |
|---|---|---|---|---|
| 蓄積せず、マイクロバッチごとに同期 | 小さい | 多い | 小さい | 帯域が極端に細い、モデルが小さい |
| 高い蓄積倍率 | 大きい | 少ない | 大きい | 帯域は許容範囲、通信の起動コストを節約したい |
| パラメータ凍結 / 勾配の疎化 | 変わらない | 少ない | 小さい | ファインチューニングでは自然に適している |
最後の行は個別に語っておく価値があります。ファインチューニングに入り、学習対象がパラメータのごく一部だけになったなら、同期すべき勾配はそれ自体で小さくなります。データ並列のコストパフォーマンスが質的に変わるからです。低ランクファインチューニング系の手法が大規模なマルチGPU構成でも依然として効く理由の一つもここにあります。それらが削っているのが、まさにデータ並列で最も痛い部分費用だからです。
all-reduce のコストは一体どこに出るのか
勾配同期の古典的な実装はリング型の all-reduce です。各 rank が勾配のシャードを持ち、リングに沿って自分のシャードを次のノードへ送り、同時に前のノードから届くシャードを受け取ります。1周すると各 rank が完全な勾配を揃えます。コストは単一カード分のデータ量に対して 2(N-1)/N 倍です。もう1つは2段構成の reduce-scatter + all-gather で、総量は同じでも遅延特性が異なります。リング型は大メッセージに対して帯域効率が高く、reduce-scatter + all-gather の2段構造は逆に、超大メッセージのほうが確実にリンクの帯域を使い切れます。
実際の性能損失は3か所から来ており、それぞれを profile で個別に見る価値があります。
- 帯域の上限。実際の通信スループットは名目上のリンク帯域に決して届きません。メッセージの分割、プロトコルスタックのオーバーヘッド、テイル効果、すべてが割り引かれます。ノード間がイーサネットや infiniband の場合、実効帯域とピークの間には明らかな差が生じますが、NVLink ドメイン内ではその差はずっと小さくなります。
- 小メッセージの遅延。モデルが小さい場合や、勾配をシャードして1片が小さくなった場合、通信時間は帯域ではなく遅延が支配します。リングの周回数が増えるほど遅延の蓄積が顕著になります。このときは「帯域アルゴリズム」を「遅延アルゴリズム」に差し替えることに意味があります。
- ハードバリアのセマンティクス。all-reduce は全 rank が到着するまで完了できません。1枚でも遅いカードがあれば、クラスタ全体が一緒に待たされます。これがいわゆる「同期テールレイテンシ」で、その増幅効果は千枚規模では劇的です。1枚のカードが100ミリ秒揺れただけで、通信連鎖が深いほど損失は幾重にも積み上がります。
通信を計算に隠す:バケット化とオーバーラップ
データ並列が「計算し終わってから同期」必须的とは限りません。エンジニアリングでよく使われるのは勾配のバケット化です。逆伝播の途中で、パラメータは計算グラフの順序に沿って少しずつ勾配を生成します。あるバケット内の勾配が所定のサイズ(たとえば数十MB)に達したら、すぐに非同期で all-reduce を開始し、後続の層の逆伝播と重ね合わせる。逆伝播は後ろから前へ進むので、先に計算された勾配はネットワークの後端に属します。つまり通信はちょうど残りの層の逆伝播と重なります。
だからこそ、contiguous gradient buckets は任意のチューニング項目ではありません。これが、どれだけの通信を本当に隠せるかを決めるからです。バケットが粗いとオーバーラップ窓が短く、通信時間がむき出しになります。バケットが細かすぎると小メッセージの遅延コストがのぼります。実際のチューニングで注目すべき指標は「通信が隠された割合」であって、「通信の総所要時間」ではありません。
# 典型数据并行启动方式:每节点 8 卡,梯度分桶 25MB,开启重叠
torchrun --nnodes=8 --nproc-per-node=8 \
--max-restarts=3 train.py \
--parallelism.data_parallel_size=64 \
--parallelism.tensor_parallel_size=1 \
--data.batch_size=1 \
--data.seq_length=4096 \
--optim.grad_reduce_in_fp32=true \
--optim.overlap_grad_reduce=true \
--optim.bucket_size_mb=25 \
--distributed.timeout_minutes=30
# 说明:
# bucket_size_mb 决定重叠窗口的粗细,与 overlap_grad_reduce 配套才有意义
# grad_reduce_in_fp32 决定通信负载,代价是缓冲区显存翻倍
# max-restarts 用于容忍单 rank 崩溃后重建进程,是基础容错手段
見落とされやすい方向がもう1つ、通信量の圧縮です。低精度通信(たとえば bf16 の勾配で reduce を行い、fp32 比で通信量を半減させるなど)は数値上たいてい許容できます。勾配それ自体にノイズへの許容度があるからです。さらに勾配の疎化、エラーフィードバック、二次統計量の低ランク近似などの手段もあり、いずれも通信ボトルネックが演算ボトルネックを上回ったときにこそ導入する価値があります。これは明確な適用境界です。まず計算ハードウェアを使い切って、次に通信圧縮を考える。順序を逆にすると、収束品質を理由なく損ないます。
テンソル並列:なぜ1層につき2回の all-reduce が必要なのか
データ並列の前提は「1枚のカードにモデル全体が入る」ことです。モデル自体が単一カードのメモリを使い切ってしまうと、この道は通りません。そこで必要になるのが、1つの行列乗算を横に切り分けること。これがテンソル並列です。
古典的な手法では重み行列を行または列で複数のシャードに分割し、各カードは自分のシャードだけを計算して、必要なら結果をまとめます。シャード同士が正しく連携できるよう、1つの Transformer 層の内部は列分割と行分割が対になって交互に現れる構造に設計されています。この交互構造こそが、「2回の all-reduce」が存在する理由です。
対(共役)な2つの演算子:なぜ1回では済まないのか
これを最もきれいに理解する方法は、アテンションモジュール内の2つの演算子を見ることです。アテンションの隠れ次元またはヘッド数を N 枚のカードに分割すると仮定しましょう。
- 第1の演算子(QKV 投影/フォワードネットワークの第1層)は列分割。重み行列 W の列を N ブロックに切り、各カードが1ブロックを持ち、入力 X は複製されたまま(all-gather するか、単に複製状態を維持する)なので、各カードが計算するのは完全な出力の一部分です。この時点で各カードが持つのは「不完全な結果」で、次の段階へ進む前に必ず集約しなければなりません。
- 第2の演算子(アテンション出力投影/フォワードネットワークの第2層)は行分割。重み行列を行で切ると、各カードに必要なのは入力の該当シャードで、それはちょうど前の段階で各カードが計算した部分です。行分割の利点は、前段の各カードが計算したシャードを足し合わせると、ちょうど完全な行列乗算の結果になり、いかなる並べ替えも不要になることです。
そこで重要な事実が浮上きます。列分割の後には1回の縮約が必要で、行分割が終わった後にもう1回の縮約が必要です。1つの変換の「入り」と「出」で通信が1回ずつ要る。したがってこのようなモジュールは少なくとも2回の集合通信を引き起こすのです。これは実装上の無駄ではなく、数学的に2回の縮約をまとめられないからです。前向段階では「シャード状態」と「完全状態」を往復して切り替える必要があり、この境界は省けません。
ある案がテンソル並列は「1層につき all-reduce は1回だけだ」と主張するなら、必ずどこかで隠れたコストを支払っています。縮約を層の外へ逃がして活性化値が解放できなくなったか、余分な並べ替え通信を持ち込んだか、あるいは別の分割規約を使うことでコストを他所へ转移させたか。いずれにせよ通信の総量は保存されていて、位置が動かされただけです。
TP の並列度を大きくできない理由
TP の通信頻度はすべての戦略の中で最も高いです。Transformer 1層につき最低2回、層数が多ければ数百回、数千回の集合通信になります。これらはすべて最も重要な前向・逆伝播のパス上で起きる強い同期で、同じ層の行列計算と重ねられません。後続の計算は縮約の結果を待たなければならないからです。TP がインターメージ帯域に極端に敏感であり、TP 度が通常ノード内(8枚または16枚)に留められ、ノードをまたがないのもこのためです。
より厳密には、TP 度には演算対帯域比で決まる上界があります。単一ノードの演算力がインターメージ帯域より速く伸びる場合、TP 度を上げると逆に各カードが受け持つ行列が小さくなり、計算時間が比例して短くなる一方で、通信時間はほぼ変わらず、最良でも悪化します。結果として利用率が崖のように落ちます。要するに行列が細かく刻まれるほど通信の相対オーバーヘッドは上がり、やがて通信が計算を丸呑みしてしまうということです。
# 张量并行配置:单节点内 8 卡张量并行,跨节点做数据并行
# 这是一种把 TP 关在高带宽域内的典型拓扑
model:
tensor_model_parallel_size: 8
pipeline_model_parallel_size: 1
expert_model_parallel_size: 1
sequence_parallel: true # 同步归约/归约替代 scatter,进一步压通信
context_parallel_size: 1
data:
micro_batch_size: 1
global_batch_size: 512
seq_length: 8192
optim:
overlap_grad_reduce: true
overlap_param_gather: true
distributed:
backend: nccl
comm_hook: none
# 关键:TP=8 已经吃满节点内 NVLink 域,跨节点不要再叠加 TP
sequence_parallel は触れておく価値があります。本来 TP の下で各カードに複製されていた活性化値と dropout マスクも切り分ける手法です。もともと all-gather だったものをより安価な all-reduce にそのまま退化させ、同時にこれらのテンソルのメモリ使用量を 1/N まで下げます。通信回数を増やすのではなく通信バイト数を減らす最適化であり、通信回数の削減とは別の話になります。
逆伝播段階の通信量
前向の2回の all-reduce だけを数えて、把握できたと思ってしまうケースです。正しい勘定は前向と逆伝播を合わせたものです。逆伝播でも各モジュールが勾配をシャード状態と完全状態の間で往復させる必要があるため、通信回数はほぼ前向の2倍になり、通信総量も同じ桁になります。キャパシティ見積もりの段階で前向の2回を全部とみなすことが、もっともよくある過小評価の原因です。結果として、並列構成が実際のクラスタで想定どおりのスループットを出しません。
通信パターンとハードウェアトポロジーの突き合わせ
並列戦略を一定程度まで絞り込むと、問題はソフトウェアではなくトポロジーの問題になります。クラスタのインターメージは通常2層に分かれます。ノード内の高速ドメイン(マルチGPUの NVLink か NVSwitch)と、ノード間のネットワークドメイン(InfiniBand かイーサネット)です。この2層の帯域・遅延・トポロジーは1〜2桁も異なるため、並列戦略の並列度上限はほぼこの2層によって決まります。
| 次元 | ノード内(NVLink ドメイン) | ノード間(IB / RoCE ドメイン) |
|---|---|---|
| 帯域 | 極めて高い。单向で数百GB/sの桁 | 中程度。单向で数十GB/sの桁 |
| 遅延 | 極小 | 明確に高い |
| 推奨される並列 | テンソル並列、エキスパート並列、シーケンス/コンテキスト並列 | データ並列、パイプライン並列 |
| 典型的な制約 | スロットと電源によってカード数が決まる | ネットワークトポロジーとオーバーサブスクリプション比で決まる |
この表が、そのまま並列度分割の直接的な根拠になります。最も通信が頻繁な次元をノード内へ押し込め、最も通信が疎な次元をノード間へ広げる。8カードノード、64ノードクラスタという典型構成であれば、テンソル並列を8、エキスパート並列を8、パイプライン並列を8(ノード間)、データ並列を8(ノード間)とすれば、この512枚が1つの3D並列グリッドにまとまります。
トポロジー認識は自動では起きない
実クラスタのリンク品質は均一ではありません。同じラック内のノードはトップスイッチを通り、ラックをまたぐとリーフスイッチを通り、AZをまたぐとさらに長い経路を通ります。通信グループが物理トポロジーに揃っていなければ、集合通信は遅いリンクをまたぐことになり、規模が大きくなると系統的なボトルネックになります。
したがってマルチ次元並列では、並列グループどうしのマッピングは明示的に宣言する必要があります。データ並列グループを一番外側に置き、通信は最も遅くて最も「粗くなっても容忍できる」リンクを通す。テンソル並列グループはノード内に置き、最速のリンクを通す。このことは現代のフレームワークでは通常、デバイスメッシュとプロセスグループの明示的な構成で表現されます。自動推論に頼るのではありません。自動推論はラック図を読めないからです。
もう1つ実践上の要点として、集合通信の実装品質がそのまま上限を決める。同じコードでも、通信バックエンドを変えるとか、より適したアルゴリズムを選ぶとかで、1倍以上の差が出ることがあります。選択はメッセージサイズで決まります。メッセージが大きいときはリングなど帯域重視のアルゴリズムに傾き、小さいときは木構造など遅延重視のアルゴリズムに傾きます。多くのフレームワークの集合通信実装は適応的な選択を内蔵しています。あなたのモデル構造が大きく変わる場合(テンソル並列とパイプライン並列が混在する場合)、実際に想定していたパスを通っているかを実測しておく価値があります。
パイプライン並列:バブル、スケジュール、そして2つの古典的手法
単一ノードにモデルが入らず、しかもテンソル並列度も帯域に阻まれるとき、残る自然な選択肢がパイプライン並列です。モデルを層ごとに何段かに切り、各段をまるごと1枚のカードに載せます。前向伝播ではデータが最初段から最終段までパイプラインのように流れ、逆伝播では逆方向へ流れ戻ります。
パイプライン並列の通信は非常に軽いのです。境界の活性化値と勾配だけを送ればよく、ポイントツーポイント送信で足り、集合通信は不要です。ノードをまたぐのに適している理由はまさにこれです。ただし、構造的な効率損失が1つあります。それがバブルです。
バブルはどこから来るのか、その大きさ
理想的には、どのカードも常に役立つ計算をしています。しかし現実には、最初のカードは前向の開始時に満負荷です(複数のマイクロバッチを連続して計算する必要があるため)、最後のカードは後段の逆伝播が始まるまで空いています。この充填と排除にかかる、避けられない時間がバブルです。
バブルの割合は、マイクロバッチ数とパイプライン段数の比で変わります。マイクロバッチが多く、段数が少ないほど、バブルは薄くなります。極限のケースは段数とマイクロバッチ数が等しい場合で、このときバブルは完全にゼロですが、各カードに極端な偏りが出ます。実践でみんなが気にしている量は「各カードの実効利用率」、すなわち稼働時間が総時間に占める割合です。これが「何枚のカードで何倍の高速化を得られるか」を直接決めます。
利用率を上げる方向は2つあります。ひとつはマイクロバッチ数を増やすことでバブルを薄くすること、もうひとつはスケジュールを改善して充填期を短くすることです。前者はピークメモリを押し上げますが、後者はスケジューリングアルゴリズムに依存します。
GPipe と 1F1B:まったく違う2つのトレードオフ
素朴なパイプラインスケジュール(GPipe スタイルと呼ばれることが多い)做法はこうです。まず全マイクロバッチの前向を回し終えて中間活性化値をすべて保存し、まとめて逆伝播を行い、そして1回だけ勾配同期をします。利点はスケジュールロジックが非常に単純で、通信がひとまとまりでまとめて送られるため効率が高いこと。欠点はすべての中間活性化値が同時に駐留しなければならないことで、ピークメモリはマイクロバッチ数に線形して増えます。つまりメモリをスループットと交換していることになります。大規模モデル時代にはこれがしばしば許容できません。
1F1B スケジュール(1回の前向の直後に1回の逆伝播)は、順序を交互進行に変えます。各パイプライン段は、1つのマイクロバッチの前向が終わった直後に、すぐに実行できる逆伝播を見つけて走らせます。これによって活性化値は生成された後すぐに消費されて解放されるため、ピークメモリが「マイクロバッチ数 × 1マイクロバッチ分の活性化値」から、おおよそ「段数 × 1マイクロバッチ分の活性化値」まで下がるようになり、マイクロバッチ数とほぼ切り離されます。これが現在、超大規模モデル訓練の主流の選択ですが、代償として通信が細かくなり、境界テンソルを頻繁にポイントツーポイントで送受信する必要があります。
| 次元 | 素朴なスケジュール(全前向のあと全逆伝播) | 1F1B スケジュール |
|---|---|---|
| ピーク活性化メモリ | マイクロバッチ数に線形して増える | 段数とともに増えるが、マイクロバッチ数とは切り離される |
| 通信の形态 | 集中的な一括送信で、ポイントツーポイント効率が高い | 細切れで錯綜し、遅延感受性に注意が要る |
| スケジュールの複雑さ | 低い | 中程度。アイドルと再割り当てを扱う必要がある |
| 典型的な用途 | 中規模、メモリに余裕がある | 超大規模、メモリが逼迫している |
インターリーブ型スケジュールとより細かい分割
バブルをさらに圧縮する道筋があと2つあります。ひとつはインターリーブスケジュールです。1枚の物理カードに、連続していない複数のモデル段を順番に背負わせるものです。本質的には仮想段数を実段数の整数倍にすることで、バブル比率がその倍数ぶん下がります。その代わり段の順序は慎重に配置する必要があります。代償はメモリの断片化とスケジュール複雑度の増加、そしてより精密なリソース割り当てが必要になることです。
もうひとつは不均一な分割です。層の種類によってメモリと演算コストの差は大きくなります。アテンション層は通常、FFN 層より活性化値を多く消費し、前後の数層はさらに embedding と出力の追加コストを背負います。各カードに割り当てる層数を「枚数」でなく「メモリ占有」で均分すれば、ピークメモリの一部を直接削れます。代償は、パイプラインの各段で計算時間が揃わなくなり、バブルの形が複雑になることです。
# 流水线并行 + 1F1B 的配置片段(示意)
# 目标是:段数 × 每节点卡数 × 数据并行度 = 总卡数
model:
tensor_model_parallel_size: 8 # 节点内
pipeline_model_parallel_size: 8 # 跨节点,1F1B
expert_model_parallel_size: 1
pipeline_model_parallel_schedule: 1F1B
num_layers_per_virtual_pipeline_stage: 2 # 交错调度:每卡承担 2 段
data:
micro_batch_size: 1
global_batch_size: 1024 # = 微批数 × 段数 × DP 度 × 微批大小
seq_length: 8192
# 注意:global_batch_size 必须能被 微批数 × DP 度 整除,否则会静默产生不均匀的尾部
# 气泡率约等于 段数/(微批数+段数-1),交错调度按虚拟段数计算
最後に見落とされがちな点を1つ。パイプライン並列の理論加速比はバブルによって制限され、ノードをまたぐときには境界の活性化値を転送する遅延がそのままパイプラインのタイムステップに計上されます。したがって、段の境界を低速なネットワークリンクに置くのは一般的でありながらも有害な行き方です。段境界の分割はトポロジーとセットで設計すべきです。
エキスパート並列:MoE が持ち込む特殊な制約
密結合モデルのすべてのパラメータは毎回の前向で計算に参加します。これはパラメータ数の拡張がそのまま演算コストに等価になることを意味します。混合エキスパート構造はここを変え。各 token は少数のエキスパートにしかルーティングされず、選ばれなかったエキスパートはまったく計算に参加しません。したがって「拡張」は主にエキスパートを増やすことで達成でき、毎回の前向の演算量は活性化されたエキスパート数に比例してしか増えないのです。
ただし、このメリットには厳密な条件があります。エキスパートは別々のデバイスに配置されなければ並列という名が起着いません。そして一度異なるデバイスに分散させた途端、「token を対応するエキスパートに送る」ということが、層ごとに発生する全体通信になります。これがエキスパート並列の核心的な矛盾です。
主となる通信は all-to-all であり、all-reduce ではない
これまでの3つの並列とは異なり、エキスパート並列の通信プリミティブは all-to-all です。各カードには1つのバッチの token があり、各 token にはルーティング先(どのエキスパートに落ちるか)があり、エキスパートは各カードに配置されています。したがって必ず1回のグローバルな交換が必要になります。各カードが自分の token を目的のエキスパートで束ね、そのエキスパートを持つカードへ送ります。受け取った側はローカルのエキスパートで計算し、同じ経路で結果を返します。
この all-to-all の通信量は token 数 × 隠れ次元 に比例し、しかも各 MoE 層で2回起こります(入力の配布が1回、出力の返送が1回)。MoE 層が密集していれば、通信総量は計算経路上のほかの通信と加算し合わさって、新しいボトルネックになり得ます。
エンジニアリング上重要な判断が3つあります。
- all-to-all はトポロジーに極端に敏感です。ノード内とノード間の両方にまたがっており、マッチしたプロセスグループ分割も必要です。ノード内に置いたエキスパート並列は通信が NVLink を通り、効果が得很好。ノードをまたぐエキスパート並列は極めて高い帯域と低いオーバーサブスクリプションのネットワークを要します。さもないと「ノードをまたぐ token 交換」によって MoE 層全体のレイテンシが引き上がってしまいます。
- all-to-all の実装品質には大きな差があります。一部の実装はチャンク単位のパイプライン化を行い、一部の token の交換と計算をオーバーラップさせます。別の実装はよりブロッキングに近い挙動です。これは通信と計算のオーバーラップ能力における実質的な差であり、実測する価値があります。
- token の複製とエキスパート冗長度が実際の負荷に影響します。ルーターが人気エキスパートに割り当てる token 数は平均を大きく超えることがあり、これが負荷偏りになります。
キャパシティ係数:不均衡とドロップのあいだのスイッチ
all-to-all の通信量は実際に交換された token 数で直接決まります。そして実際に交換される token 数は token 数 × ルーティング先エキスパート数 です。これは自分で調整できるノブをもたらします。キャパシティ係数、すなわち各エキスパートが処理できると予約している token 数です。
キャパシティ係数の上限に達すると、余った token はドロップされます(あるいはフォールバック経路に回されます)。ここに明確なトレードオフが生まれます。キャパシティ係数を下げるとメモリ占用と通信量は減り、スループットは上がりますが、ドロップ率は上がり品質が損なわれます。キャパシティ係数を上げると品質は安定しますが、メモリも通信も高くつく。同時に考慮すべきのが補助ロードバランス損失です。訓練中に通常はペナルティ項が加えられ、異なるエキスパート間の token 負荷を近づけて、訓練初期に少数のエキスパートがトラフィックを独占してしまう退行を防ぎます。この制約がなければ、エキスパート並列は訓練の過程でルーターの不均衡に食らいつくことになります。
EP と TP の衝突
これがエキスパート並列で最も注意を要する点です。エキスパート重み自体がテンソル並列によって分割されている場合、token の行き先である「エキスパート」は「あるエキスパートのあるカード上のシャード」になります。配布先は「1つのエキスパートに送る」から「複数カード上にある同一エキスパート異なるシャードに送る」に変わります。通信パターンはそれに伴い複雑になります。シャード次元でもう1度分割して all-to-all を再利用するか、ある実装によってはより高価な通信列に退行してしまいます。
実践上の判断は単純です。EP と TP は2つの異なる分割次元の競合関係であり、単純に積み上げられるものではありません。どちらか一方を選ぶなら、EP で異なるエキスパートを異なるカードに配置するか(ただし負荷均衡とキャパシティ係数を制約条件とします)、TP で単一のエキスパート重み行列を分割するかです。両方を同時に大きい値まで開く構成はそもそもまれです。両者を上げると通信の複雑さが増える速度が利得を上回ってしまいます。
# 专家并行配置:专家放在节点内 8 卡,模型其余部分用流水线铺开
model:
expert_model_parallel_size: 8 # 8 个专家分散到节点内 8 卡
tensor_model_parallel_size: 1 # 不与 EP 叠加,避开 all-to-all 复杂化
pipeline_model_parallel_size: 16 # 靠 PP 铺开到 16 节点
expert_tensor_parallel_size: 1 # 单个专家内部不再切分
# MoE 层的关键旋钮
expert_capacity_factor: 1.25 # 容量因子:显存/通信 ↔ 丢弃率 的旋钮
moe_router_load_balancing_type: aux_loss # 辅助负载均衡损失
moe_aux_loss_coeff: 0.01 # 太小易退化,太大伤质量,需按步数调
moe_router_topk: 2 # 每 token 路由到几个专家:直接乘在 all-to-all 量上
# 诊断要点:看 all-to-all 字节数是否 ≈ token 数 × topk × 隐维 × 数据类型大小
# 若明显偏大,多半是容量因子过大或 token 复制策略不对
3D 並列:分割次元はどう選ぶか
単一の並列方式ではもう支えられないとき、組み合わせに入ります。これが 3D(あるいはより高次元)並列です。データ・テンソル・パイプライン・エキスパートといった次元を組み合わせ、並列の格子を構成します。各次元が1種類ずつリソースの圧力を吸収します。
組み合わせる上で難しいのは「組み合わせられるかどうか」ではなく、「どの次元から先に切るか」です。順番を間違えれば、最も帯域が豊富なノード内という領域の限られたリソースを消費してしまいます。
分割順序の判断基準:誰がスローリンクを最も嫌うか
中核となる判断基準はただ一つですが、非常に鋭利です。まず、通信が最も稠密でスローリンクに最も耐えられない次元を切り、ノード内に収めます。通信が疎な次元は後から切り、ノードをまたがるようにします。
- 第1手:テンソル並行(およびエキスパート並行)をノード内で使い切る。この2つの通信は各モジュールの前向・後向の経路上で発生し、スローリンクを許容できない最具です。ノード内8枚または16枚のカードをすべて TP/EP に割り当て、モデルを「薄く」します。
- 第2手:エキスパート並行とテンソル並行は二者択一、または慎重に重ねる。モデルが MoE のエキスパート密型なら、EP は自然にノード内のスロットを奪い合います。この場合は TP を小さく、1 にまで下げて、ノード内の帯域を EP に譲ることが多いのです。これは「EP のほうが優れている」からではなく、「このモデルでは EP が主な並列利得の源泉だからです」。
- 第3手:パイプライン並行が薄いモデルをノード間に広げます。PP が伝送するのは境界の活性化値だけなので、ノードをまたぐことはまったく耐えます。その役割は「ノード数が足りない/メモリが全部乗らない」という圧力を吸収し、バブル率と引き換えに拡張性を得ることです。
- 第4手:データ並行が残りの次元で複製します。DP 度数 = 総カード数 ÷ (TP × PP × EP)。1枚のカードに入るモデルのサイズは変わらず、メモリと通信総量だけが影響を受けます。役割はグローバルバッチを広げ、固定オーバーヘッドを薄めることです。
ここで一つの口訣にまとめると、TP / EP はノード内で使い切る → PP でノード間に広げる → DP でバッチを支える。
| 分割次元 | 配置される層 | 主に吸収するもの | 分割順序 | 大きくしたときのコスト |
|---|---|---|---|---|
| テンソル並行 | ノード内 | 1枚のカードにモデルが入らない | 最初。ノード内で使い切る | 通信回数が多く、スローリンクに弱い |
| エキスパート並行 | ノード内 | MoE のエキスパートが入らない | TP と同じスロットを奪い合う | all-to-all + 負荷不均衡 |
| パイプライン並行 | ノード跨ぎ | ノード数が足りない | 中間で、広げるために | バブル + 境界活性化値の遅延 |
| データ並行 | ノード跨ぎ | グローバルバッチの拡大 | 最後に。残った次元で | 同期バリア + テールレイテンシ |
通信総量:並列度が帳簿にどう影響するか
組み合わせが妥当かどうかを判断するうえで最も実用的な物差しは、1回の訓練における通信総量を並列種別ごとに足し合わせ、それぞれの比率を見ることです。
- データ並行の勾配同期:パラメータの桁です。1回の逆伝播のあと1回発生します。DP 度数を上げても1枚あたりのこの部分の量は変わりませんが、同期回数はステップ数で数えられます。帯域が支えられるかどうかは、パラメータ量とステップ間の時間に依存します。
- テンソル並行のレイヤー内通信:隠れ次元 × シーケンス長 × データ型サイズの桁で、N 層 ×(前向+後向)の回数だけ発生します。ここで TP 度数は1回の総量を変更しません(分割した後も総バイト数は保存され、重複分は複製されたものです)が、TP 度が上がると1枚のカードあたりの計算時間は短くなり、相対的な通信オーバーヘッドは上昇します。
- パイプライン並行の境界転送:1つの境界の活性化値と勾配だけを渡すため、量は小さいものの頻度が高く、ノード跨ぎでは主に遅延が問題になります。
- エキスパート並行の all-to-all:token 数 × topk × 隠れ次元 × データ型,かつ各層で2回発生します。EP 度数を上げても総量は显著には減らず、跨カード分の割合が上昇します。
この4つを足し合わせると規則が見えてきます。密モデルでは TP の通信がしばしば主項になります。MoE モデルでは TP の通信項は EP の all-to-all に押し上げられ、桁が逆転することもあります。モデル種別ごとに分割順序が異なる必要がある理由もここにあります。判断基準は1つあっても、異なるモデルに落とすと最適解はまったく違ってくるのです。
ZeRO:1/2/3 が本当に節約するもの
並列戦略が解くのは「モデルをどう切るか」という問題です。ZeRO が解くのはそれとは直交する別の問題、データ並行のとき各カードが完全なオプティマイザ状態・勾配・パラメータをすべて持っているこの冗長を節約できるか。モデルの切り方は変えず、データ並行で各カードに複製されていた状態を、異なるカードへシャードします。
ZeRO を理解するには、まずデータ並行で各カードが持つ4つのものを確認します。パラメータ、勾配、オプティマイザ状態、そして逆伝播に必要な活性化値。ZeRO の3つの段階は「どれを何個シャードするか」で分かれています。
| 段階 | シャード対象 | どこでメモリを節約するか | 通信コスト | 主要な利点 |
|---|---|---|---|---|
| ZeRO-1 | オプティマイザ状態のシャード | オプティマイザ状態(通常はメモリの大部分) | DP と同程度(reduce-scatter/all-gather の組み合わせが1回増える) | 同じカード数でより大きいモデルを動かせる |
| ZeRO-2 | オプティマイザ状態 + 勾配のシャード | さらに勾配 | ZeRO-1 と同程度 | 活性化値と一時バッファにも余裕ができる |
| ZeRO-3 | オプティマイザ状態 + 勾配 + パラメータをすべてシャード | さらにパラメータ。ほぼ全シャードに | 明らかに増加(各層の前向/後向でパラメータを取得する必要がある) | 1枚のカードに載せられるモデルサイズが大幅に増える |
最も誤られやすい点は、ZeRO の各段階が節約する主なのは「オプティマイザ状態や勾配・パラメータといった状態メモリ」であって、「活性化値」ではないことです。活性化値を抑えるのは主に勾配蓄積、チェックポイント、シーケンス並行、コンテキスト並行であり、ZeRO とは別の手段です。この2つを混同すると、「ZeRO-3 を入れたのに活性化値が依然オーバーフローする」という状況に遭遇し、ZeRO が効き 않았다と誤診してしまいます。
ZeRO-3 の通信コストは本物である
ZeRO-1 と ZeRO-2 の通信量は通常のデータ並行とほぼ同程度です。状態をシャードするだけで前向に使うパラメータはシャードしておらず、前向計算のときも各カードは完全なパラメータを保持しています。ZeRO-3 は異なります。パラメータもシャードされるため、各カードが任意の時点で見ているのはパラメータの1つのシャードだけで、前向・後向の計算でパラメータが必要になったときは取得してすぐ使い、終わったら即座に解放しなければなりません。これが各層に追加の all-gather と reduce-scatter を発生させます。
したがって ZeRO-3 は典型的な「通信でメモリを買う」ものです。同じカードでより大きいモデルを動かせるようになりますが、演算がすでに通信で飽和していればスループットが落ちます。判断基準は至って直接的で、訓練における通信の比率を見ればよいのです。本来から余裕がなければ ZeRO-3 は逆効果になり、メモリがボトルネックで演算に余裕があれば ZeRO-3 でモデル規模を上げられる。経験的には ZeRO-2 から ZeRO-3 へのメモリ利得は大きく、しかしスループット損失も明確です。すでに通信が飽和しているクラスタでは、まずパイプライン/テンソル並行の切り分けを行い、そのあとで ZeRO-3 を検討するほうが、最初から ZeRO-3 を入れるより安定します。
ZeRO-3 を無料のメモリ拡張とみなすのは危険です。これは通信とメモリを交換する操作であり、その帳簿はプロファイルの中で計算しなければ、直感では決められません。
ZeRO のシャードの考え方は、パイプラインやテンソル並行とは完全に直交しています。ZeRO はデータ並行の次元で状態をシャードし、TP/PP はモデルの次元で構造を切る。ゆえに「TP + PP + ZeRO」は同時に使えます。組み合わせはおおむね「まず TP/PP で構造をノードが受け取れる規模まで切り、そのあと ZeRO で残った状態の冗長を潰す」ことになります。
ヘテロ構成と障害耐性
数百枚から数千枚のカードに達すると、訓練周期は週単位になり、「一度の障害に耐えられるかどうか」はもうブラッシュアップではなく、プロジェクト成败を左右する前提になります。並列戦略はここで障害耐性の仕組みと互いに牵制し合います。
障害の種類と帰結
- 単一カードの障害 / プロセスのクラッシュ。最もよくあるケースです。影響は戦略に依存します。データ並行で1枚落ちると、通常は通信グループ全体を再構築する必要があります(シャード化された状態を持っているため)。現在のステップをやり直すことになります。パイプライン並行で1枚落ちると、パイプラインの下流すべてがその再計算を待ちます。再起動のコストは「次のチェックポイントまでのステップ数」に比例します。
- リンクの劣化 / タイムアウト。クラッシュよりも目立たないぶん厄介です。あるリンクが遅くなると集合通信全体が引きずり、全カードがそれに合わせて待たされます。表れ方はエラーではなく、スループットが密かに下がることです。このうちはクラッシュのほうが調査が困難です。
- メモリ不足と断片化。ピークメモリは確率的な事象です。ノード間のわずかな負荷の偏りにより、一部のカードだけがオーバーフローし、他のカードは正常、という状態になります。
チェックポイントの粒度:復旧コストと訓練コストのバランス
障害耐性の中心手段は定期的なチェックポイントです。チェックポイントが粗すぎると1回の障害で長い時間を無駄にし、密すぎると書き込みとストレージの帯域が新しいボトルネックになって、訓練を遅らせることもあります。妥当なやり方は、「チェックポイントを書くコスト」を「2つのチェックポイントの間で得られる進捗」より十分小さく保つことです。
分散チェックポイントの複雑さは、モデル状態、並列戦略、オプティマイザ状態、乱数状態、データローディングの進捗をすべて整合させて保存し、しかも並列設定と結び付いている点にあります。並列設定を変更すると、古いチェックポイントはそのままでは使えないことがほとんどです。チェックポイントの内容と並列設定を一緒にバージョン管理することが、障害耐性を本当に使えるものにする前提です。
障害耐性に関連する工学的な要点があと2つあります。ひとつは弾力性スケーリングで、訓練の途中でノードを増減させ並列度を調整できるようにするには、設計の段階で再シャードを許容しておかなければなりません。もうひとつはサスペンド/リカバリの粒度で、プロセスを再起動せずに障害ノードをスキップできるなら、コストはプロセス全体の再起動よりはるかに小さくなります。ただし実装の要求は厳しくなります。
ロングシーケンスと低精度:並列戦略と絡み合う2つの変数
これまでの議論はすべて、シーケンス長と数値精度を与えられた定数として前提としていました。しかし現実の訓練では、この2つの変数は並列戦略と強く結合しています。メモリ使用量と通信量を同時に変えてしまうからです。ほとんどの場合、これらは並列度と一緒に計画すべきです。先に固定して、問題が起きてから回头して埋めるのではありません。
活性化値:メモリのうち最も過小評価される一塊
パラメータ・勾配・オプティマイザ状態の3つはどれも静的なのに対し、活性化値はシーケンス長とともに急速に増えていきます。アテンション関連の中間テンソルは素朴な実装ではシーケンス長に対してほぼ二乗で増加します。つまりシーケンス長が2倍になれば、メモリの圧力は2倍を超えて増える可能性があるということです。超長コンテキストの訓練では,しばしばパラメータではなく活性化値のほうが本物のメモリボトルネックになります。
活性化値に対処する手段はいくつかあり、いずれも並列戦略との接し方が異なります。勾配蓄積は1回の前向のサイズを小さくしてピークを抑える代わりに、ステップ数が増えます。活性化値の再計算は計算をメモリと交換し、後向のときに前向をもう一度走らせて境界テンソルだけを残します。代償は追加の演算量です。シーケンス並行はテンソル並行で各カードに複製されていた活性化値も切り分け、この種のテンソルのメモリを元の数分の1まで下げると同時に、通信バイト数も減らせます。コンテキスト並行はシーケンス全体を位置で切り分け、アテンションを跨カードな集約と結合に退化させます。超長シーケンスに対する最も徹底した切り方です。
ここには寄り道しやすい落とし穴があります。活性化値の手段を選ぶ前に、まずボトルネックが本当に活性化値にあることを証明してください。メモリ統計が活性化値の比率がごく小さいことを示しているなら、活性化値の最適化を続けるのは無駄です。パラメータと状態側にいってシャードで解くべきです。この判断は並列設定を変えるたびにやり直す価値があります。並列度が変われば各部分の比率も変わるので、直前の結論がそのまま成り立つとは限らないからです。
もうひとつのよくある誤りは、「メモリが足りる」ことを「メモリが十分」と同一視することです。ピークメモリはステップ数とともに変動します。計測するときはピークを基準にしてください。断片化を加えると、統計値と実際に割り当てられる値との間にはまだ一段の余裕があります。この2つを混同すると、数万ステップを走りこんだ後に初めてオーバーフローに遭遇し、しかも再現も困難になります。
低精度:演算量と通信量の両方を圧縮する
低精度訓練の価値は「メモリを節約するだけ」と狭められわかってされていますが、実際には3つの場所に同時に作用します。行列乗算の演算コスト、活性化値の体積、そしてネットワークを通るテンソルのバイト数です。通信量はデータ型のビット幅にほぼ比例して線形に下がるため、すでに通信が飽和に近いクラスタでは、通信テンソルを低精度フォーマットに置き換えることは、計算精度を下げるだけより効果が大きいことがあります。それによって、本来なら後続の計算をブロックしていた時間をそのまま短縮できるからです。
ただし低精度がただちに正しいわけではありません。集約時の加算順序、勾配蓄積のステップ数、どのテンソルを高精度のまま残すかは、数値安定性に影響します。精度を交換する価値があるかどうかは、プロファイルを見れば判断できます。1ステップの中で通信の比率が明らかに高いなら、まず通信側のデータ型に手を入れるのが費用対効果の最も高い変更です。演算がボトルネックなら、行列乗算をより低精度の実行パスに通すことにフォーカスを当てます。両者は分けて決定してよいので、一度に全部換える必要もなく、同時に変える必要もありません。
性質の似た変更がもう1つあります。通信を細かく刻むか、逆に粗くまとめるか、というものです。前者は1回の通信量を減らしますが回数を増やし、後者はその逆です。遅延主導と帯域主導のあいだを移動するとき、精度を交換するより大きな利得が得られることがあります。遅延の側はほぼビット幅の影響を受けないからです。
3つを一緒に計画する:検証順序の表
シーケンス長・精度・並列度をまとめて見るときには、次の順序で段階ごとに検証できます。各段階は自分の種類の問題だけを解きます。
- まず、1枚のカードで独立にパラメータと状態を載せられるかを見ます。載るなら、そのままデータ並行と勾配蓄積に掛かればよいので、これは一番気が楽な場合です。
- 載らないなら、構造の分割と状態のシャードのどちらを取るかを決めます。構造の分割はカード上のモデルの形を変えますが、状態のシャードは冗長性の置き場所だけを変えます。両者で通信の特性が異なるため、合計で計算する必要があります。
- 次に、現在のシーケンス長で活性化値がどれだけを占めるかを見ます。比率が高ければ勾配蓄積か再計算でピークを抑え、低いならこの部分に労力を割かないでください。
- そして、1ステップあたりの通信総量を見積もり、現在のリンク帯域で許容できるかを判断します。許容できなければ、カードを足す前に、通信側のデータ型を下げるか通信回数を減らすことを優先します。
- 最後に、収束品質のために高精度を残すかどうかを決めます。これは最適化ではなくトレードオフなので、前の4つが安定したあとで着手すべきです。
どの段階の結論が変わっても、後ろの段階は再計算し直さなければなりません。并列設定の仕事量を過小評価してしまう人も、その理由がここにあります。これは一度きりのパラメータとして設定できるものではなく、与えられたクラスタ形状の下で、互いに牽制し合う一組の制約の解だからです。この順序の価値は、「並列度」という孤立したノブを、メモリ・シーケンス長・精度・トポロジーに同時に拘束された結果へ戻すことにあります。4つを紐づけて初めて、同じモデルコードでもクラスタによって並列設定が変わる理由が説明できます。変わるのはモデルではなく、この4つの制約の相対的な緩さだからです。
並列戦略の誤選択による性能崩壊:現実の訓練ではどう見えるか
これは記事全体で最も実用的なセクションです。並列設定を間違えても即座にエラーにはならず、「動く、しかし遅い」という形で現れます。その遅さの原因は「GPU が足りない」「モデルが大きすぎる」と誤診されがちです。以下、症状ごとに整理します。
症状1:カードを増やしたのにむしろ遅くなった
現れ方:DP 度数を 8 から 16 に上げたのに実時間が下がらず逆に増えた。あるいは TP を 1 から 4 に上げたのに、スループットが昇るのではなく落ちた。
原因:通信がすでに飽和していることです。DP 度数を2倍にしても1回の勾配同期の総量は変わりませんが、同期に参加する主体が2倍になり、テールレイテンシが露出する範囲も2倍になります。TP 度数を上げると1枚のカードあたりの行列が小さくなり、計算時間が比例して短くなる一方、レイヤー内の2回の all-reduce の時間はほぼ変わらず、最悪の場合は悪化します。相対的なオーバーヘッドが急上昇するわけです。
診断:プロファイルの中で、1ステップの時間に占める集合通信の比率を見ます。すでに通信がほとんどの時間を占めているなら、カードを足すのはオーバーヘッドを増やすだけです。演算の比率が高いなら、まだ並列の余地があります。
症状2:スループットがステップ数の経過とともに緩やかに低下する
現れ方:初期は正常ですが、数千ステップが経過すると1ステップあたりの時間が長くなります。
原因:戦略や状態に起因することがほとんどです。ひとつはメモリ断片化が時間とともに蓄積し、もともと余裕があったメモリが徐々にピークへ近づいてしまうことです。ふたつめは負荷の偏りで、MoE の場合は人気エキスパートによって all-to-all のバイト数と通信の不均衡が訓練とともに増えます。みっつめはエキスパート並行でルーターが偏り、大量の token が少数のエキスパートに殺到して分布がいっそう偏ることです。
診断:メモリ使用率が上昇傾向にあるか、all-to-all のバイト数がステップとともに増えているか、エキスパート負荷の分布図が偏っているか、この3点を見ます。
症状3:実効の加速比が理論値より大幅に低い
現れ方:N 倍のカードならほぼ N 倍の加速が得られるはずが、実際には一部しか出ない。極端な場合はゼロ、あるいは負の利得になるほどです。
原因:バブル(パイプラインのステージ数がマイクロバッチ数に対して深すぎる・多すぎる)、ノード跨ぎの境界での遅延、並列グループが物理トポロジーに合っていない(データ並行グループがラックをまたいでいる)、集合通信が最適でないアルゴリズムを通っている、あるいは負荷がパイプラインの各ステージで偏って末尾が空回りしている、といったものです。
診断:各カードの稼働時間を取り出してヒストグラムを作ると、バブルと各ステージの均衡は一目で分かります。特定のカードが空回りしているなら、それはパイプラインまたは負荷の偏りです。すべてのカードが均等に稼働しているのにスループットが低いなら、問題は通信か演算子の実装にあります。
症状4:MoE の訓練品質が上がらない、または唐突に落ちる
現れ方:損失が下がらない、エキスパートがほぼ少数しか使われない、あるいは訓練後期に理由のない劣化が起こる。
原因:キャパシティ係数が小さすぎてドロップ率が高い、補助負荷均衡損失の係数が不適切(小さすぎると偏りを抑えきれず、大きすぎるとルーティングを損なう)、EP と TP の重ね方によってルーティング先のシャードが一致しない、all-to-all の通信が完全にカバーされておらず一部の token のルーティングが失敗している。
診断:損失曲線から推測するのではなく、直接ドロップ率とエキスパート負荷の分布を見ます。通信のレベルではバイト数から逆算します。理論値 = token 数 × topk × 隠れ次元 × データ型サイズ、を測って実測値が明らかに大きければ、余分な通信か token の複製があるということです。
症状5:メモリは足りているはずなのに OOM になる
現れ方:ZeRO で見積もったオプティマイザ+勾配+パラメータの合計が、実際のメモリよりはるかに小さいのに、それでもオーバーフローする。
原因:大きな部分を占めているのは実は活性化値と通信バッファです。ZeRO は活性化値を扱いません。ロングシーケンスでは活性化値はシーケンス長に対して非線形に増加します。集合通信の事前確保バッファは非常に大きくなることがあります。断片化は「足りる」を「足りない」に変えてしまいます。
診断:「パラメータ/勾配/オプティマイザ/活性化値/通信バッファ/断片化」の6項目をそれぞれ計測してください。合計メモリから逆算してはいけません。緩和策は勾配蓄積でピークを抑え、シーケンス並行やコンテキスト並行で1シーケンスあたりの活性化値を抑え、通信バッファに上限を設けることです。
崩壊を「次元の選択ミス」に帰着させる
上記の5種類の崩壊を纽解くと、ほぼすべて分割次元の選択ミスです。スローリンクに弱い次元をスローリンクの場所に置いていないか(TP/EP がノードをまたいでいる)、バブルの予算をきつめすぎていないか(PP が深すぎるのにマイクロバッチが少ない)、状態シャードで解くべき問題を構造の問題として扱っていないか(DP だけで ZeRO を載せない)、あるいは構造分割と状態分割の役割を取り違えていないか。並列設定が正しいかどうかを判断するには、自分に3つの問いを投げてください。
- スローリンクを最も嫌う並列次元(TP、EP)は、すべてノード内に閉じ込められていますか。ノードをまたいでいるなら、まずノード内へ戻します。
- パイプラインのバブル率は、現在のマイクロバッチ数において許容できる値ですか。バブルが時間の大部分を奪っているなら、マイクロバッチを増やす、ステージ数を減らす、交錯スケジュールに切り替えることを考えます。
- メモリが緊張しているのは、状態と活性化値のどちらですか。状態が厳しいなら ZeRO、活性化値が厳しいなら勾配蓄積とシーケンス/コンテキスト並行を使います。この2つを取り違えると、どちらの場合も労力が無駄になります。
選定ガイド:そのまま実行できる判断表
最後に、記事全体をそのまま使える選定ロジックに収束させます。以下では、クラスタが標準的な「複数ノード、ノード内は NVLink、ノード間は高速ネットワーク」であることを前提とします。
| あなたの状況 | 主戦略 | 補助戦略 | 注視すべき指標 |
|---|---|---|---|
| モデルは1枚に載るが、速く計算するにはカードが足りない | データ並行 | ZeRO-1/2 + 勾配蓄積 | 集合通信が1ステップの時間に占める比率 |
| モデルは1枚には載らないが、ノード数は足りている | ノード内でテンソル並行を使い切る | ノード跨ぎのデータ並行 + ZeRO | 1枚のカードあたりの行列サイズ、通信の相対オーバーヘッド |
| 1ノードにも載らず、複数ノードを使う必要がある | TP をノード内、パイプラインをノード跨ぎに | 1F1B スケジュール + ZeRO-2/3 | バブル率、各ステージの計算の均衡、活性化値のピークメモリ |
| MoE でエキスパートが密、パラメータは多いが token あたりの計算は少ない | ノード内のエキスパート並行 | TP は慎重に重ねる。PP はノード跨ぎで広げる | all-to-all のバイト数、ドロップ率、エキスパート負荷分布 |
| 活性化値がメモリボトルネック(ロングシーケンス) | コンテキスト/シーケンス並行 | 勾配蓄積 + チェックポイント | 1シーケンスの活性化値が長さに伴って増える傾き |
| 状態がメモリボトルネック(パラメータが多く状態も大きい) | ZeRO-2/3 | TP/PP と直交して組み合わせ | シャード後の状態使用量と通信の増分 |
さらに、いくつかの経験則は改めて強調する価値があります。
- カードを足す前に、まず帳簿を計算します。4項目で通信総量を足し合わせ、1ステップの時間と比較します。通信の比率が高いなら、加えるべきはカードではなく通信効率の最適化です。
- 一度に動かすのは1つの次元だけです。並列度とハイパーパラメータを同時に調整すると、問題が起きたときに原因を突き止められません。1項目ずつ変更し、十分なステップ数で曲線を確認します。
- トポロジーと並列グループを一緒に設計します。データ並行グループがラックをまたぐ、テンソル並行グループがノードをまたぐ、この種の問題は設定ファイルなら1分で直せますが、実行中は継続的な性能損失になります。
- 通信の圧縮を既定の解にしないこと。まず計算ハードウェアとスケジュール(バケット化、オーバーラップ、適切なアルゴリズムの選択)を限界まで使い切り、そのあとで低精度通信や疎性化によって帯域を交換することを考えます。
- 障害耐性に余裕を持たせること。チェックポイント間隔、訓練設定、並列設定を1つのまとまったものとしてバージョン管理し、障害からの復旧時に続きから再開できるように整合を保つようにします。
まとめ
4種類の並行の違いは、本質的に「メモリ・演算・帯域」という3種類の圧力を「モデルの異なる次元」にどう落とすかの違いです。データ並行はバッチで演算を交換し、代償は1ステップごとの重い通信と硬い同期です。テンソル並行は帯域でモデル容量を交換し、代償は各層で最低2回の避けられない集合通信であり、したがってノード内に閉じ込めなければなりません。パイプライン並行はバブルと引き換えにノード跨ぎの拡張性を交換し、代償は効率の損失で、マイクロバッチ数と 1F1B スケジュールで薄めます。エキスパート並行は all-to-all でパラメータ量を交換し、代償はルーティングと負荷均衡というひとまとまりの追加制約です。ZeRO は直交する方向で、结构には手を触れず、データ並行の次元で状態をシャードし、通信と引き換えに状態のメモリを稼います。
この手がかりをまとめると、3D 並行の分割順序は自明になります。スローリンクを最も嫌うものを最初に切り、ノード内に置く。通信が疎なものを後から切り、ノード間に置く。状態の冗長は ZeRO でデータ並行の次元から潰す。そしてすべての崩壊——カードを増やすと遅くなった、スループットが緩やかに下落した、加速比が大きく割を引かれた、MoE の品質が落ちた、メモリは足りているはずなのに溢れた——は、本質的にこの原則から外したいずれかの現れ方です。プロファイルの中の「1ステップの中で通信がどれだけを占めるか、各カードの稼働時間は均等か、all-to-all のバイト数は合っているか」という3組の指標で位置を掴むことができれば、「性能崩壊」は手探りの玄学から、原因を追える工学の問題に変わります。
並列戦略の進化の方向ははっきりしています。「スローリンクを最も嫌う計算」をできるだけ最速の小さなハードウェアの中に留め、「スローリンクを恐れないもの」を広げて規模と交換し、状態の冗長はシャードで潰す。並列戦略のこの3本目の原則を理解し、具体的な訓練設定に落とし込むことは、ただある1つのフレームワークのパラメータ名を覚えるより重要です。