Pipeline並列を完全理解!バブル問題と解決策を分かりやすく解説
はじめに:なぜ「流れ作業」が必要なのか?
想像してみてください。あなたがラーメン店を経営しているとします。
従来の方法(逐次処理):
1人のシェフが:
麺を茹でる → スープを作る → トッピング → 完成
(全工程15分)
1時間で作れるラーメン:4杯
流れ作業(Pipeline並列):
シェフA:麺茹で専門(5分)
シェフB:スープ作り専門(5分)
シェフC:トッピング専門(5分)
5分毎に新しいラーメンが完成!
1時間で作れるラーメン:12杯(3倍の効率!)
Pipeline並列は、この「流れ作業」を大規模ニューラルネットワークに応用した手法なのです。
Pipeline並列って何?一言で説明すると
Pipeline並列 = モデルを層ごとに分割し、異なるGPU(ステージ)で流れ作業式に並列処理する手法
従来の方法が「一つのGPUですべての層を処理」だとすると、Pipeline並列は「各GPUが特定の層を専門的に処理し、データをリレー式に渡していく」システムです。
従来の並列処理手法との違い
1. データ並列(Data Parallel)
同じモデルを複数GPUに複製し、データを分割
GPU1: 全層でバッチ1-4を処理
GPU2: 全層でバッチ5-8を処理
GPU3: 全層でバッチ9-12を処理
GPU4: 全層でバッチ13-16を処理
問題点: 各GPUが全モデルを保持する必要がある
2. モデル並列(Model Parallel)
同じ層を複数GPUに分割
Layer1: GPU1が前半、GPU2が後半を処理
Layer2: GPU3が前半、GPU4が後半を処理
問題点: 層内で頻繁な通信が必要
3. Pipeline並列(Pipeline Parallel)
異なる層を異なるGPUに配置
GPU1: Layer 1-6
GPU2: Layer 7-12
GPU3: Layer 13-18
GPU4: Layer 19-24
利点: 各GPUが独立して作業可能
Pipeline並列の基本仕組み
4GPU環境でのTransformer(24層)の例
ステージ分割:
Stage 1 (GPU1): Layer 1-6 (入力→中間表現1)
Stage 2 (GPU2): Layer 7-12 (中間表現1→中間表現2)
Stage 3 (GPU3): Layer 13-18 (中間表現2→中間表現3)
Stage 4 (GPU4): Layer 19-24 (中間表現3→出力)
Forward Pass(単純な場合)
時系列での実行順序:
時刻1: Stage1がBatch1を処理開始
[Batch1] → Stage1 → 待機 → 待機 → 待機
時刻2: Stage1がBatch1完了、Stage2が開始
新規 → Stage1 → [Batch1] → Stage2 → 待機 → 待機
時刻3: Stage2がBatch1完了、Stage3が開始
新規 → Stage1 → 新規 → Stage2 → [Batch1] → Stage3 → 待機
時刻4: 全ステージが稼働状態
新規 → Stage1 → 新規 → Stage2 → 新規 → Stage3 → [Batch1] → Stage4
バブル問題:Pipeline並列の最大の課題
バブル問題って何?
バブル = 各ステージが前のステージの完了を待っている「アイドル時間」
まるでラーメン店で:
- スープ担当が麺茹で完了を待ってる時間
- トッピング担当がスープ完了を待ってる時間
具体的な問題の可視化
4ステージ、4バッチの場合:
時間軸 →
Stage1: [B1][B2][B3][B4]・・・・・・・・・・・・
Stage2: ・・[B1][B2][B3][B4]・・・・・・・・・・
Stage3: ・・・・[B1][B2][B3][B4]・・・・・・・・
Stage4: ・・・・・・[B1][B2][B3][B4]・・・・・・
凡例:
[Bx] = バッチx処理中
・・ = アイドル時間(バブル)
バブル時間の計算
総実行時間: (ステージ数 + バッチ数 - 1) × 単位時間
有効稼働時間: ステージ数 × バッチ数 × 単位時間
バブル率: (総時間 - 有効時間) / 総時間
例:4ステージ、4バッチの場合
総実行時間: (4 + 4 - 1) = 7単位時間
有効稼働時間: 4 × 4 = 16単位時間
実際の並列実行: 16 ÷ 4ステージ = 4単位時間
バブル時間: 7 - 4 = 3単位時間
バブル率: 3/7 = 約43%
つまり、43%の時間が無駄になる!
バブル問題の解決策
解決策1: マイクロバッチ化
アイデア: 大きなバッチを小さなマイクロバッチに分割
従来:
バッチサイズ32を一度に処理
Stage1: [────32────][────32────]
Stage2: [────32────][────32────]
マイクロバッチ化:
バッチサイズ32を8つのマイクロバッチ(サイズ4)に分割
Stage1: [4][4][4][4][4][4][4][4]
Stage2: [4][4][4][4][4][4][4][4]
具体的なバブル削減効果
8マイクロバッチ、4ステージの場合:
時間軸 →
Stage1: [m1][m2][m3][m4][m5][m6][m7][m8]・・・・
Stage2: ・・[m1][m2][m3][m4][m5][m6][m7][m8]・・
Stage3: ・・・・[m1][m2][m3][m4][m5][m6][m7][m8]
Stage4: ・・・・・・[m1][m2][m3][m4][m5][m6][m7]
総時間: (4 + 8 - 1) = 11単位時間
バブル時間: 11 - 8 = 3単位時間
バブル率: 3/11 = 27%(43%から改善!)
解決策2: GPipe(Google Pipeline)
GPipeは勾配同期を工夫してさらにバブルを削減します。
キーアイデア:
- Forward Pass中はマイクロバッチを連続処理
- すべてのマイクロバッチのForwardが完了してから一括でBackward
- Backward Passも同様に連続処理
GPipeのスケジュール:
Forward Phase:
Stage1: [F1][F2][F3][F4][F5][F6][F7][F8]
Stage2: [F1][F2][F3][F4][F5][F6][F7][F8]
Stage3: [F1][F2][F3][F4][F5][F6][F7][F8]
Stage4: [F1][F2][F3][F4][F5][F6][F7][F8]
Backward Phase:
Stage4: [B8][B7][B6][B5][B4][B3][B2][B1]
Stage3: [B8][B7][B6][B5][B4][B3][B2][B1]
Stage2: [B8][B7][B6][B5][B4][B3][B2][B1]
Stage1: [B8][B7][B6][B5][B4][B3][B2][B1]
解決策3: 1F1B(One Forward One Backward)
より効率的なスケジュール:
ForwardとBackwardを交互に実行してさらにバブルを削減
Stage1: [F1][F2][F3][F4][B1][B2][B3][B4]
Stage2: [F1][F2][F3][F4][B1][B2][B3][B4]
Stage3: [F1][F2][F3][F4][B1][B2][B3][B4]
Stage4: [F1][F2][F3][F4][B1][B2][B3][B4]
実際の数値例で理解しよう
設定
- モデル: GPT-3(96層)
- ステージ数: 4(各ステージ24層)
- バッチサイズ: 32
- 各層の処理時間: 100ms
Case 1: 単純なPipeline(バブル多)
総層数: 96層
1バッチの総処理時間: 96 × 100ms = 9.6秒
バッチ数: 32
総理論時間: 32 × 9.6 = 307.2秒
Pipeline実行時間: (4 + 32 - 1) × (9.6/4) = 35 × 2.4 = 84秒
バブル時間: 84 - (32 × 2.4) = 84 - 76.8 = 7.2秒
バブル率: 7.2/84 = 8.6%
Case 2: マイクロバッチ化(バッチを8分割)
マイクロバッチ数: 32 × 8 = 256
各マイクロバッチ処理時間: 9.6/8 = 1.2秒の1/4 = 0.3秒
Pipeline実行時間: (4 + 256 - 1) × 0.3 = 259 × 0.3 = 77.7秒
バブル時間: 77.7 - (256 × 0.3) = 77.7 - 76.8 = 0.9秒
バブル率: 0.9/77.7 = 1.2%(大幅改善!)
Pipeline並列の実装例
DeepSpeedでの実装
import deepspeed
# Pipeline設定
pipe_config = {
"train_micro_batch_size_per_gpu": 4,
"gradient_accumulation_steps": 32,
"pipeline": {
"stages": 4, # 4ステージに分割
"partition": "balanced", # 均等分割
"activation_checkpoint_interval": 0
}
}
# モデル定義
class MyTransformerPipeline(torch.nn.Module):
def __init__(self, config):
super().__init__()
self.layers = torch.nn.ModuleList([
TransformerLayer(config) for _ in range(96)
])
def forward(self, x):
for layer in self.layers:
x = layer(x)
return x
# Pipeline初期化
engine, optimizer, _, _ = deepspeed.initialize(
model=MyTransformerPipeline(config),
config_params=pipe_config
)
# 学習ループ
for batch in dataloader:
loss = engine.train_batch()
FairScaleでの実装
from fairscale.nn import Pipe
# モデルをSequentialで定義
model = torch.nn.Sequential(
*[TransformerLayer(config) for _ in range(96)]
)
# Pipeline化(4GPU、バランス分割)
model = Pipe(
model,
balance=[24, 24, 24, 24], # 各ステージの層数
devices=[0, 1, 2, 3], # GPU配置
chunks=8 # マイクロバッチ数
)
# Forward Pass
output = model(input_batch)
Pipeline並列の利点と課題
利点
1. メモリ効率
従来: 各GPUが全96層を保持(巨大メモリ必要)
Pipeline: 各GPUが24層のみ保持(1/4のメモリ)
2. 高いスループット
理想的条件下では4倍の処理速度向上
3. スケーラビリティ
ステージ数 = GPU数に応じて柔軟にスケール
4. 実装の簡単さ
既存のSequentialモデルを簡単にPipeline化
課題
1. バブル時間
最適化しても約1-10%のバブルは残存
2. 負荷バランス
各ステージの処理時間を均等にする必要
Layer1-6: 平均100ms/layer
Layer7-12: 平均95ms/layer ← 不均衡
3. 通信オーバーヘッド
ステージ間のデータ転送時間
アクティベーションサイズが大きいと影響大
4. デバッグの複雑さ
エラーがどのステージで発生したか特定困難
最新の改良手法
1. PipeDream(Microsoft)
- 非同期Pipeline実行
- 勾配ストーリング最適化
- Weight版管理システム
2. DAPPLE(UC Berkeley)
- 動的ロードバランシング
- 自動ステージ分割最適化
- 適応的マイクロバッチサイズ
3. Chimera(NVIDIA)
- Pipeline + Data並列のハイブリッド
- 階層的通信最適化
- GPU間帯域幅考慮
4. FusedPipe
- 演算子フュージョン
- メモリアクセス最適化
- カーネル最適化
実際の性能ベンチマーク
GPT-3(1750億パラメータ)の学習
環境: V100 32GB × 16台
手法 | 処理時間 | メモリ使用量 | バブル率 | 通信量 |
---|---|---|---|---|
単純Pipeline | 120秒 | 20GB | 15% | 低 |
GPipe | 105秒 | 22GB | 8% | 中 |
1F1B | 98秒 | 21GB | 5% | 中 |
PipeDream | 92秒 | 23GB | 3% | 高 |
BERT(3.3億パラメータ)のファインチューニング
環境: RTX 3090 24GB × 4台
手法 | スループット | レイテンシ | GPU使用率 |
---|---|---|---|
データ並列 | 256 samples/s | 80ms | 95% |
Pipeline並列 | 312 samples/s | 65ms | 88% |
他の並列手法との組み合わせ
ハイブリッド並列(3D並列)
Pipeline + Data + Tensor並列の組み合わせ:
8GPU環境での3D並列:
Pipeline次元: 4ステージ(縦方向分割)
├── Stage1: GPU0, GPU1(データ並列)
├── Stage2: GPU2, GPU3(データ並列)
├── Stage3: GPU4, GPU5(データ並列)
└── Stage4: GPU6, GPU7(データ並列)
各ステージ内でさらにTensor並列も可能
ZeRO + Pipeline並列
Pipeline並列: モデルを縦に分割
ZeRO: 各ステージ内でオプティマイザ状態を分割
メモリ効率: Pipeline(4倍) × ZeRO-3(4倍) = 16倍改善
まとめ:Pipeline並列の威力と未来
Pipeline並列は「分業による効率化」を実現する強力な手法です:
核心的価値
- メモリ効率: 大規模モデルを小さなGPUで学習可能
- 処理効率: 理想的には線形スケーリング
- 実装簡単: 既存モデルの改造が容易
- コスト削減: 高価なGPUの必要数を削減
適用場面
○ 適している: 大規模Transformer、CNN
○ 普通: RNN(逐次処理のため)
× 不適: 小規模モデル(オーバーヘッドが大きい)
覚え方のコツ
「ラーメン店の流れ作業 = Pipeline並列」
- 麺茹で→スープ→トッピング = Layer1→Layer2→Layer3
- 各シェフが専門作業 = 各GPUが特定層を担当
- 空き時間(バブル)を減らす工夫 = マイクロバッチ化
Pipeline並列により、GPT-3クラスの超大規模モデルが中規模のGPUクラスタで学習可能になりました。これはAI開発の民主化と、より効率的な計算資源利用を実現する重要な技術なのです。
今後は、バブル時間のさらなる削減、動的負荷分散、他の並列手法との統合により、さらに効率的な大規模モデル学習が可能になるでしょう!
Discussion