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向けに設計された演算モードである。入力を指数部8ビット・仮数部10ビットに丸めて計算し、結果はFP32精度で累算する。これにより、FP32と同等の数値範囲を保ちつつ演算を高速化する。詳細は「4. NVIDIA Tensor Coreを利用した高速行列演算」で説明する。

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

GPU世代とアーキテクチャ

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

Compute Capability: NVIDIAがGPUの機能レベルを示すために定めた番号である。数値が大きいほど新しい機能をサポートする。

Tensor Coreとは

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)

効果:

注意: torch.inference_mode()下で生成されたテンソルは、後からautograd計算に再投入できない制約がある。推論結果を学習ループに戻すような用途ではtorch.no_grad()を使用する。

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

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

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

原理

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

「混合精度」と呼ばれる理由は、本来、モデル全体を低精度に変換しつつ、数値安定性が求められる一部の演算でFP32を維持する手法を指すためである。

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

  1. メモリ使用量の削減: 同じメモリ容量でより大きなモデルやバッチサイズを扱える(推論時に固定バッチで運用する場合でも、常駐メモリの削減により他プロセスとの共存が容易になる)。
  2. メモリ帯域幅の軽減: データ転送量が半分になり、メモリボトルネックが緩和される。
  3. Tensor Core活用: 専用ハードウェアによる高速演算が可能になる。

広義の混合精度と狭義の混合精度: PyTorchで「混合精度」と言う場合、以下の2通りの実現手段がある。

実装方法

# --- 広義の混合精度: モデル全体を一律に低精度化する ---
# モデルを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)

# --- 狭義の混合精度: torch.autocastを使用する場合の例 ---
# モデルはFP32のまま保持し、autocast内で自動的に精度が切り替わる
# with torch.inference_mode(), torch.autocast(device_type='cuda', dtype=torch.bfloat16):
#     output = model(input_data)

BF16とFP16の選択:

注意事項:

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

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

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

適用順序の注意: 精度変換(FP16/BF16/TF32等)はtorch.compile適用に行うこと。コンパイル後に精度を変更すると、コンパイル結果が無効化され再コンパイルが発生する場合がある。

原理

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ビットに丸めて計算し、結果はFP32精度で累算する演算モードである。Tensor Coreは低精度演算に特化した設計のため、仮数部を削減したTF32形式を使用することで高速演算が可能になる。これにより、FP32データ型の行列演算を高速に処理できる。

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

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

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

注意(API刷新): PyTorch 2.9以降、TF32をより細かく制御する新しいAPI(torch.backends.fp32_precisiontorch.backends.cuda.matmul.fp32_precisiontorch.backends.cudnn.conv.fp32_precisionなど)が導入された。これらは"ieee"(FP32精度)または"tf32"(TF32を許可)を指定する形式で、バックエンドや演算ごとの制御が可能である。従来のallow_tf32フラグは将来的に非推奨(deprecated)となる予定であり、新旧の設定の混在はサポートされない。本文書では広く普及しているtorch.set_float32_matmul_precisionを中心に説明するが、PyTorch 2.9以降を使用する場合は新APIの利用を検討すること。

実装方法

import torch

# TF32を有効化する(Ampere世代以降で効果がある)
# 'highest': 完全なFP32精度を使用する(デフォルト)
# 'high'   : 行列乗算でAmpere世代以降のGPUを使用している場合はTF32が選択される。
#            一部の演算ではBF16ベースの近似アルゴリズムが選ばれることもある(推奨)
# 'medium' : より積極的にBF16ベースの近似アルゴリズムを使用する(高速だが精度が低い)
torch.set_float32_matmul_precision('high')

注意(適用範囲): torch.set_float32_matmul_precisionは主に行列乗算(matmul)の内部精度に影響する設定である。cuDNNを使用する畳み込み演算でのTF32を制御するには、torch.backends.cudnn.allow_tf32(またはPyTorch 2.9以降のtorch.backends.cudnn.conv.fp32_precision)を使用する。

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

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

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

原理

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

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

実装方法

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系モデルの場合)

    Returns:
        (model, model_dtype) のタプル。model_dtypeは推論時の入力変換に使用する。
    """
    model = YourModel.from_pretrained(...)
    model_dtype = torch.float32  # デフォルト

    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)
                model_dtype = torch.bfloat16
            else:
                # Volta/Turing世代ではFP16を使用する
                model = model.half()
                model_dtype = torch.float16
        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, model_dtype


@torch.inference_mode()
def inference(model, input_data, model_dtype=torch.float32):
    """
    推論を実行する。推論モード設定はモデルロード時ではなく、推論実行時に適用する。

    Args:
        model: load_model()でロード・最適化済みのモデル
        input_data: 入力テンソル
        model_dtype: load_model()が返したdtype。入力テンソルの精度合わせに使用する
                     (torch.compileでラップ後のモデルにはdtype属性が無いため、
                       外部から明示的に渡す設計としている)。
    """
    if device.type == 'cuda' and model_dtype != torch.float32:
        input_data = input_data.to(dtype=model_dtype)

    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を有効化する
Ada / Hopper以降 (RTX 40xx, H100など) inference_mode + BF16混合精度 + torch.compile Ampereと同様。加えてFP8など、より低精度の手法も利用可能(本文書では扱わない)

torch.compileはPyTorch 2.0以上であればVolta/Turing世代でも利用可能である。動的構造を持たない静的なモデルでは、これらの世代でも効果が期待できる。表ではAmpere世代以降での代表的な組み合わせを示すにとどめている。

主要なポイント

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

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

よくある問題と解決方法

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

解決策:

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

解決策:

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

解決策:

問題4: TF32の設定が効かない、または非推奨の警告が出る

解決策: