PyTorch推論最適化ガイド

【概要】 本文書は、PyTorchを使用した深層学習モデルの推論速度向上を目指す最適化手法について解説する。ただし効果はモデル構造・GPU世代・入力サイズによって異なるため、実際の環境で検証する必要がある。

関連する外部サイトを以下に示す。

用語リスト

本章では、最適化手法を理解するために必要な用語を解説する。浮動小数点数の表現とGPUアーキテクチャの知識は、混合精度推論やTF32の理解に必要である。

浮動小数点数の表現

浮動小数点数は「符号部」「指数部」「仮数部」の3つで構成される。

各フォーマットの仮数部ビット数は以下のとおりである。FP32は23ビット、FP16は10ビット、BF16は7ビット、TF32は10ビットである。

※ より厳密には、IEEE 754規格では仮数部に暗黙の先頭1ビットがあるため、実質的な精度はフィールドより1ビット多い(例: FP32は24ビット精度)。本文書では便宜上、フィールドのビット数を記載している。

TF32(TensorFloat-32): Ampere世代以降のTensor Core向けに設計された低精度フォーマットである。FP32と同等の数値範囲を保ちつつ、演算を高速化する。詳細は「4. NVIDIA Tensor Coreを利用した高速行列演算」で説明する。

BF16(Brain Float 16): Googleが開発したフォーマットである。FP32と同じ指数部(8ビット)を持ち、仮数部を7ビットに削減している。FP16と比較して数値範囲が広く、深層学習での数値安定性が高い。

GPU世代とアーキテクチャ

NVIDIA GPUは世代ごとに異なる機能を持つ。最適化手法の選択にはGPU世代の把握が必要である。

Tensor Coreとは

NVIDIA GPUに搭載された行列演算専用ハードウェアである。従来の演算ユニット(CUDA Core)はFP32の精度で汎用的な演算を行うが、Tensor Coreは低精度フォーマット(TF32、FP16、BF16など)に特化した行列演算に最適化されている。精度を制限することで同じシリコン面積により多くの演算器を搭載でき、行列演算のスループット(単位時間あたりの処理量)が向上する。

PyTorchにおけるTensor

PyTorchの「Tensor」は多次元配列のデータ構造を指し、数学的なテンソル(多次元配列)をプログラムで扱えるようにしたものである。GPU上で効率的に計算できる。

勾配計算

ニューラルネットワークの学習において、損失関数の各パラメータに対する微分(勾配)を計算する処理である。推論時には不要だが、学習時には必須である。

その他の用語

最適化手法

本章では5つの最適化手法を紹介する。各手法は独立して適用可能だが、組み合わせることでより大きな効果が得られる。以下では、適用範囲が広く基本的な手法から、特定の条件で効果を発揮する手法の順に説明する。

各手法の関係を以下に示す。

1. 推論モード設定

対象: 推論時(勾配計算が不要な場合)

原理

学習時、PyTorchは自動微分のために計算グラフ(演算履歴)を保持する。torch.inference_mode()は、推論時に不要な計算グラフの構築を無効化して、メモリとCPUオーバーヘッドを削減する。

実装方法

# 推論時は必ず使用する
@torch.inference_mode()
def predict(model, data):
    return model(data)

# またはコンテキストマネージャーとして使用する
with torch.inference_mode():
    output = model(input_data)

効果:

適用シーン: すべての推論処理で使用する。なお、学習時に使用すると勾配計算ができなくなるため、学習と推論を同一コード内で行う場合は適用範囲に注意が必要である。

2. 混合精度推論(FP16/BF16)

対象: Tensor Coreを持つGPU(Volta世代以降)

原理

FP16(16ビット浮動小数点)やBF16(Brain Float 16)は、FP32より少ないビット数で数値を表現する。メモリ使用量が半分になり、メモリ帯域幅の制約が軽減される。さらにTensor Coreによる高速演算が可能になる。

混合精度推論を使用すると、以下の3つの効果が得られる。

  1. メモリ使用量の削減: 同じメモリ容量でより大きなモデルやバッチサイズを扱える
  2. メモリ帯域幅の軽減: データ転送量が半分になり、メモリボトルネックが緩和される
  3. Tensor Core活用: 専用ハードウェアによる高速演算が可能になる

実装方法

# モデルをFP16に変換する
model = model.half()
input_data = input_data.half()

# またはBF16に変換する(Ampere世代以降)
model = model.to(dtype=torch.bfloat16)
input_data = input_data.to(dtype=torch.bfloat16)

# 推論を実行する
with torch.inference_mode():
    output = model(input_data)

BF16とFP16の選択:

注意事項:

適用シーン: 大規模モデル(GPT、BERT、Stable Diffusionなど)で効果的である。小規模モデルでは効果が限定的な場合がある。

3. torch.compileによるモデル全体のコンパイル最適化

対象: PyTorch 2.0以上。効果はモデルの構造に依存する(動的処理が多いモデルでは効果が薄い)

原理

PyTorchは通常、各演算を逐次的に実行する(eager mode)。torch.compileは実行前にモデル全体を解析し、以下の最適化を行う。

  1. 演算の融合: 複数の小さな演算(例: 行列演算→活性化関数→正規化)を1つのGPUカーネルにまとめ、中間結果のメモリ書き込み・読み込みを削減する
  2. メモリアクセスの最適化: データの配置を工夫し、GPUメモリへのアクセス回数や帯域幅の使用を削減する
  3. 不要な演算の削除: 結果に影響しない演算を事前に除外する

これにより、GPUの実行効率を向上させる。

実装方法

# PyTorch 2.0以上で利用可能である
if hasattr(torch, 'compile'):
    model = torch.compile(model)

注意事項:

適用シーン: 静的な構造を持つモデル(ResNet、EfficientNetなど)で効果的である。動的な分岐を持つモデル(RNNの可変長処理など)では効果が限定的である。

4. NVIDIA Tensor Coreを利用した高速行列演算

対象: PyTorch 1.12以上、Ampere世代以降のCUDA GPU(RTX 30xxシリーズ以降など)

原理

TF32(TensorFloat-32)は、FP32の指数部(8ビット)を保持しつつ、仮数部を10ビットに削減したフォーマットである。Tensor Coreは低精度演算に特化した設計のため、仮数部を削減したTF32形式を使用することで高速演算が可能になる。これにより、FP32データ型の行列演算を高速に処理できる。

仮数部の削減により多少の精度低下がある。例えば、FP32では1.234567890が表現できるが、TF32では1.2345程度の精度となる。ただし深層学習のような統計的処理では、この精度で十分な場合が多い。

トレードオフ: 計算速度の向上 vs 仮数部精度の低下

注意: PyTorch 1.12以降、行列乗算(matmul)におけるTF32はデフォルトで無効である(torch.backends.cuda.matmul.allow_tf32False)。一方、cuDNNを使用する畳み込み演算では、TF32はすべてのバージョンでデフォルト有効である(torch.backends.cudnn.allow_tf32True)。これは、深層学習以外で数値計算の厳密性を要求するアプリケーションに配慮したためである。

実装方法

import torch

# TF32を有効化する(Ampere世代以降で効果がある)
# 'highest': 完全なFP32精度を使用する(デフォルト)
# 'high': TF32またはBF16ベースのアルゴリズムを使用する(推奨)
# 'medium': BF16を使用する(高速だが精度が低い)
torch.set_float32_matmul_precision('high')

注意: torch.set_float32_matmul_precisionは行列乗算(matmul)に影響する設定である。cuDNNを使用する畳み込み演算には影響しない。畳み込み演算でのTF32を制御するには、torch.backends.cudnn.allow_tf32を使用する。

適用シーン: 大規模な行列演算が多いモデル(Transformer、大規模な全結合層など)で効果的である。混合精度推論を使用する場合は、主要な演算が低精度で行われるため、TF32の効果は限定的となる。

5. cuDNN最適化による畳み込み演算高速化

対象: GPU使用時、cuDNNインストール済み環境、入力サイズが固定のCNN系モデル

原理

cuDNN(CUDA Deep Neural Network library)は、NVIDIAが提供する深層学習演算ライブラリである。cudnn.benchmarkは、最初の実行時に複数の畳み込みアルゴリズムを試し、最速のものを自動選択する。

トレードオフ: 初回実行の探索時間 vs 以降の実行速度向上

実装方法

if device.type == 'cuda':
    torch.backends.cudnn.enabled = True      # cuDNNを有効化する(デフォルトで有効)
    torch.backends.cudnn.benchmark = True    # 最適なアルゴリズムを自動選択する

注意: 入力サイズが変動する場合は逆効果である。毎回アルゴリズム探索が実行され、処理が遅くなる。入力サイズが固定であることを確認してから有効化する。

適用シーン: CNN系のモデル(ResNet、EfficientNet、YOLOなど)で、入力サイズが固定の場合に効果的である。

適用タイミング

モデルロード後、推論開始前に最適化を適用する。以下は、これらの最適化手法を統合した実装例である。本文で説明した順序(適用範囲順)とは異なり、実装上の依存関係に基づいた順序で記述している。具体的には、精度変換をtorch.compileより前に行う必要がある。

def load_model(use_mixed_precision=True, fixed_input_size=True):
    """
    モデルをロードし、最適化を適用する。

    Args:
        use_mixed_precision: 混合精度を使用するかどうか
        fixed_input_size: 入力サイズが固定かどうか(CNN系モデルの場合)
    """
    model = YourModel.from_pretrained(...)

    if device.type == 'cuda':
        # cuDNN最適化(CNN系モデルで入力サイズ固定の場合のみ有効化する)
        torch.backends.cudnn.enabled = True
        if fixed_input_size:
            torch.backends.cudnn.benchmark = True

        # 混合精度またはTF32を選択する
        if use_mixed_precision:
            if torch.cuda.get_device_capability()[0] >= 8:
                # Ampere世代以降ではBF16を使用する
                model = model.to(dtype=torch.bfloat16)
            else:
                # Volta/Turing世代ではFP16を使用する
                model = model.half()
        else:
            # 混合精度を使用しない場合はTF32を有効化する
            # 混合精度使用時も一部のFP32演算にTF32が適用される可能性があるが、
            # 主要な演算が低精度で行われるため効果は限定的である
            torch.set_float32_matmul_precision('high')

        # torch.compile最適化(PyTorch 2.0以上で利用可能)
        # 精度変換後にcompileすることで、変換後の精度に基づいた最適化が行われる
        if hasattr(torch, 'compile'):
            model = torch.compile(model)

    return model


@torch.inference_mode()
def inference(model, input_data):
    """
    推論を実行する。推論モード設定はモデルロード時ではなく、推論実行時に適用する。
    """
    if device.type == 'cuda':
        if model.dtype == torch.bfloat16:
            input_data = input_data.to(dtype=torch.bfloat16)
        elif model.dtype == torch.float16:
            input_data = input_data.half()

    return model(input_data)

最適化手法の選択ガイド

実際の適用では、以下の判断基準に従って最適化手法を選択することを推奨する。

GPU世代別の推奨設定

GPU世代 推奨される最適化 備考
Volta (V100) inference_mode + FP16混合精度 TF32非対応、BF16非対応
Turing (T4, RTX 20xx) inference_mode + FP16混合精度 TF32非対応、BF16非対応
Ampere (A100, RTX 30xx) inference_mode + BF16混合精度 + torch.compile 混合精度を使用しない場合はTF32を有効化する

主要なポイント

  1. GPU世代に応じた最適化手法の選択: Ampere世代ではBF16を、Volta/Turing世代ではFP16を使用する。TF32はAmpere世代以降で混合精度を使用しない場合に有効である
  2. 混合精度(BF16/FP16)の効果: メモリ、帯域幅、演算速度の3つの面で効果がある
  3. 複数の最適化の組み合わせ: 組み合わせることで相乗効果が得られる。ただし、組み合わせ方には注意が必要である(例: 混合精度使用時はTF32の効果が限定的となる)
  4. 環境での検証: 効果はモデルとハードウェアに依存するため、実際の環境で検証する必要がある。本文書のガイドラインは一般的な傾向であり、個別のケースでは異なる結果となる可能性がある

付録: トラブルシューティング

よくある問題と解決方法

問題1: 混合精度で精度が低下する

解決策:

問題2: torch.compileでエラーが発生する

解決策:

問題3: cuDNN benchmarkを有効にしたら遅くなった

解決策: