🤖

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は勾配同期を工夫してさらにバブルを削減します。

キーアイデア:

  1. Forward Pass中はマイクロバッチを連続処理
  2. すべてのマイクロバッチのForwardが完了してから一括でBackward
  3. 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並列は「分業による効率化」を実現する強力な手法です:

核心的価値

  1. メモリ効率: 大規模モデルを小さなGPUで学習可能
  2. 処理効率: 理想的には線形スケーリング
  3. 実装簡単: 既存モデルの改造が容易
  4. コスト削減: 高価なGPUの必要数を削減

適用場面

○ 適している: 大規模Transformer、CNN
○ 普通: RNN(逐次処理のため)
× 不適: 小規模モデル(オーバーヘッドが大きい)

覚え方のコツ

ラーメン店の流れ作業 = Pipeline並列

  • 麺茹で→スープ→トッピング = Layer1→Layer2→Layer3
  • 各シェフが専門作業 = 各GPUが特定層を担当
  • 空き時間(バブル)を減らす工夫 = マイクロバッチ化

Pipeline並列により、GPT-3クラスの超大規模モデルが中規模のGPUクラスタで学習可能になりました。これはAI開発の民主化と、より効率的な計算資源利用を実現する重要な技術なのです。

今後は、バブル時間のさらなる削減、動的負荷分散、他の並列手法との統合により、さらに効率的な大規模モデル学習が可能になるでしょう!

Discussion