9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TensorRTってなんだ?〜深層学習モデルを爆速化するNVIDIA推論エンジンの全貌〜

9
Posted at

この記事の対象読者

  • Pythonの基本文法(関数、クラス、import)を理解している方
  • PyTorchやTensorFlowで学習済みモデルを使った経験がある方
  • 「推論速度をもっと上げたい」「本番環境でGPUを効率的に使いたい」と考えている方

この記事で得られること

  • TensorRTとは何か、なぜ高速化できるのかの本質的な理解
  • PyTorchモデルをONNX経由でTensorRTエンジンに変換する具体的な手順
  • 開発・本番・テスト環境それぞれに対応した設定ファイルテンプレート
  • よくあるエラーと対処法、そして次に学ぶべきステップ

この記事で扱わないこと

  • CUDAプログラミングの詳細(カーネル実装など)
  • モデルの学習方法(学習済みモデルからスタート)
  • C++ APIの詳細(Python APIに焦点)

1. TensorRTとの出会い

「このモデル、精度は十分なのに推論が遅すぎて本番で使えない...」

PyTorchで学習したモデルをそのままデプロイしようとして、そんな壁にぶつかったことはないだろうか。私は何度もあった。

あるプロジェクトで画像認識モデルを本番投入しようとしたとき、レイテンシが100msを超えてしまい、「リアルタイム処理」という要件を満たせなかった。バッチサイズを調整しても、モデルを軽量化しても、根本的な解決には至らない。

そんなとき出会ったのがTensorRTだ。

同じモデルをTensorRTで最適化したところ、推論時間が1/5以下に短縮された。まるでターボチャージャーを取り付けたかのような加速感。「なぜもっと早く知らなかったのか」と本気で悔しくなった。

TensorRTを一言で表現するなら、「学習済みモデルを本番環境向けに爆速化するNVIDIAの秘密兵器」だ。

ここまでで、TensorRTがどんな問題を解決するツールか、なんとなくイメージできただろうか。次は、なぜTensorRTがこれほど高速化を実現できるのか、その技術的背景を見ていこう。


2. 前提知識の確認

本題に入る前に、この記事で使う用語を整理しておく。

2.1 推論(Inference)とは

機械学習における「推論」とは、学習済みモデルに新しいデータを入力して予測結果を得るプロセスのこと。「学習(Training)」がモデルを育てる段階なら、「推論」は育ったモデルを実際に使う段階だ。

TensorRTは推論専用のライブラリであり、学習には使えない。

2.2 ONNX(Open Neural Network Exchange)とは

ONNX(オニックス)は、異なる深層学習フレームワーク間でモデルを交換するための共通フォーマット。PyTorchで学習したモデルをTensorFlowで使いたい、あるいはTensorRTで最適化したい、といった場面で橋渡し役を担う。

「モデルの世界共通語」と考えるとわかりやすい。

2.3 レイテンシとスループットとは

  • レイテンシ(Latency): 1回の推論にかかる時間。「応答速度」のこと
  • スループット(Throughput): 単位時間あたりに処理できる推論回数。「処理能力」のこと

リアルタイムアプリケーションではレイテンシ重視、バッチ処理ではスループット重視となることが多い。

2.4 量子化(Quantization)とは

モデルの重みや活性化値を、FP32(32ビット浮動小数点)からFP16(16ビット)やINT8(8ビット整数)などの低精度フォーマットに変換する技術。メモリ使用量と計算コストを削減しつつ、精度低下を最小限に抑える。

これらの用語が押さえられたら、次に進もう。


3. TensorRTが生まれた背景

3.1 深層学習の「学習と推論のギャップ」問題

深層学習の発展により、PyTorchやTensorFlowといったフレームワークが普及した。これらは柔軟性と開発効率に優れており、研究開発には最適だ。

しかし、本番環境では事情が異なる。

学習フレームワークは「汎用性」を重視するため、特定のハードウェアに最適化されていない。例えるなら、どんな道でも走れる万能車(学習フレームワーク)と、サーキット専用に設計されたレーシングカー(TensorRT)の違いだ。

3.2 NVIDIAが解決に乗り出した理由

NVIDIAはGPUメーカーとして、自社ハードウェアの性能を最大限引き出すソリューションを求められていた。2016年にTensorRTの初版がリリースされ、以来継続的に進化を続けている。

2025年現在、TensorRTはバージョン10.14に到達し、最新のBlackwell GPUアーキテクチャにも対応している。また、LLM(大規模言語モデル)向けに特化したTensorRT-LLMも登場し、ChatGPTのような生成AIの高速化にも貢献している。

背景がわかったところで、抽象的な概念から順に、TensorRTがどのように高速化を実現するのか、具体的な仕組みを見ていこう。


4. TensorRTの基本概念〜なぜ速いのか〜

TensorRTの高速化は魔法ではなく、4つの最適化技術の組み合わせで実現されている。

4.1 レイヤーフュージョン(Layer Fusion)

ニューラルネットワークは多数のレイヤー(層)から構成される。通常、各レイヤーは個別のGPUカーネルとして実行されるが、これには「カーネル起動オーバーヘッド」と「メモリ帯域のボトルネック」という2つの問題がある。

TensorRTはこれを解決するため、複数のレイヤーを1つの最適化されたカーネルに統合する。

具体例: 畳み込み(Conv)→ バッチ正規化(BatchNorm)→ 活性化関数(ReLU)という3つの連続レイヤーを、1つの「ConvBNReLU」カーネルに融合。

# 最適化前(3回のカーネル起動)
Conv → メモリ書き込み → BatchNorm → メモリ書き込み → ReLU

# 最適化後(1回のカーネル起動)
ConvBNReLU(中間結果はGPUレジスタ内で処理)

これにより、メモリアクセス回数が激減し、処理速度が大幅に向上する。

4.2 精度キャリブレーション(Precision Calibration)

多くの学習済みモデルはFP32(32ビット浮動小数点)で保存されている。しかし、推論においてはFP16やINT8でも十分な精度を維持できることが多い。

TensorRTは自動的に精度変換を行い、以下のメリットを提供する。

精度 メモリ使用量 計算速度 精度低下
FP32 基準 基準 なし
FP16 約1/2 約2倍 ほぼなし
INT8 約1/4 約4倍 要キャリブレーション

INT8を使用する場合は「キャリブレーション」というプロセスが必要だが、FP16なら追加作業なしで高速化できる。

4.3 カーネルオートチューニング(Kernel Auto-Tuning)

同じ演算でも、GPUアーキテクチャによって最適な実装は異なる。TensorRTは「タクティック」と呼ばれる多数の実装パターンを持ち、ビルド時に自動的に最速の組み合わせを探索する。

これは「GPUに合わせたオーダーメイドスーツ」のようなもの。既製品(汎用実装)より、体にフィットした服(最適化カーネル)の方が動きやすいのと同じ原理だ。

4.4 動的テンソルメモリ管理

推論時のメモリ使用量を最小化するため、TensorRTはテンソル間でメモリを効率的に再利用する。不要になったテンソルのメモリ領域を次のテンソルに割り当てることで、GPUメモリの消費を抑える。

基本概念が理解できたところで、これらの抽象的な概念を具体的なコードで実装していこう。


5. 実際に使ってみよう〜PyTorchモデルのTensorRT変換〜

5.1 環境構築

まず、必要なパッケージをインストールする。

# CUDA 12.x環境を前提
# TensorRTとONNX関連パッケージのインストール
pip install tensorrt==10.14.* --break-system-packages
pip install onnx==1.18.0 --break-system-packages
pip install onnxruntime-gpu==1.20.0 --break-system-packages
pip install torch torchvision --break-system-packages

インストール確認:

python -c "import tensorrt; print(f'TensorRT version: {tensorrt.__version__}')"

5.2 設定ファイルの準備

以下の3種類の設定ファイルを用意した。用途に応じて選択してほしい。

開発環境用(config.yaml)

# config.yaml - 開発環境用(このままコピーして使える)
# デバッグしやすさを重視した設定

tensorrt:
  precision: "fp16"          # FP16で開発(INT8キャリブレーション不要)
  workspace_size_gb: 4       # ワークスペースサイズ(GB)
  max_batch_size: 1          # デバッグ時は小さいバッチ
  verbose: true              # 詳細ログを出力
  timing_cache: null         # タイミングキャッシュは無効

onnx:
  opset_version: 17          # ONNXオペセットバージョン
  dynamic_axes: true         # 動的な入力サイズを許可
  simplify: true             # ONNXグラフを簡略化

inference:
  device: "cuda:0"           # 使用するGPUデバイス
  warmup_iterations: 5       # ウォームアップ回数
  benchmark_iterations: 100  # ベンチマーク回数

logging:
  level: "DEBUG"             # ログレベル
  file: null                 # ファイル出力は無効

本番環境用(config.production.yaml)

# config.production.yaml - 本番環境用(このままコピーして使える)
# パフォーマンスと安定性を重視した設定

tensorrt:
  precision: "fp16"          # FP16(精度と速度のバランス)
  workspace_size_gb: 8       # ワークスペースサイズ(GB)
  max_batch_size: 32         # 本番向けバッチサイズ
  verbose: false             # ログ抑制
  timing_cache: "/var/cache/tensorrt/timing.cache"
  builder_optimization_level: 5  # 最大最適化

onnx:
  opset_version: 17
  dynamic_axes: false        # 固定サイズで最適化
  simplify: true

inference:
  device: "cuda:0"
  warmup_iterations: 10
  benchmark_iterations: 0    # ベンチマーク無効

logging:
  level: "WARNING"
  file: "/var/log/tensorrt/inference.log"

monitoring:
  enable_metrics: true       # メトリクス収集
  prometheus_port: 9090      # Prometheus用ポート

テスト環境用(config.test.yaml)

# config.test.yaml - テスト/CI環境用(このままコピーして使える)
# 再現性と高速なCI実行を重視した設定

tensorrt:
  precision: "fp32"          # FP32で精度検証
  workspace_size_gb: 2       # 最小限のワークスペース
  max_batch_size: 1          # 小さいバッチで高速テスト
  verbose: false
  timing_cache: null
  deterministic: true        # 決定論的な動作を保証

onnx:
  opset_version: 17
  dynamic_axes: false
  simplify: true

inference:
  device: "cuda:0"
  warmup_iterations: 1
  benchmark_iterations: 10

logging:
  level: "ERROR"             # エラーのみ出力
  file: null

validation:
  rtol: 1e-3                 # 相対許容誤差
  atol: 1e-5                 # 絶対許容誤差
  reference_backend: "pytorch"  # 比較対象

5.3 基本的な変換と推論コード

以下は、PyTorchモデルをONNX経由でTensorRTエンジンに変換し、推論を実行する完全なサンプルコードだ。

"""
TensorRT推論サンプル
PyTorchモデルをONNX経由でTensorRTエンジンに変換し、推論を実行する

使い方: python tensorrt_inference.py
必要なパッケージ: pip install tensorrt onnx torch torchvision
"""

import torch
import torch.onnx
import tensorrt as trt
import numpy as np
from pathlib import Path
import yaml


def load_config(config_path: str = "config.yaml") -> dict:
    """設定ファイルを読み込む"""
    with open(config_path, "r") as f:
        return yaml.safe_load(f)


def export_to_onnx(
    model: torch.nn.Module,
    input_shape: tuple,
    onnx_path: str,
    opset_version: int = 17
) -> None:
    """PyTorchモデルをONNXフォーマットでエクスポート"""
    model.eval()
    dummy_input = torch.randn(*input_shape)
    
    torch.onnx.export(
        model,
        dummy_input,
        onnx_path,
        export_params=True,
        opset_version=opset_version,
        do_constant_folding=True,
        input_names=["input"],
        output_names=["output"],
        dynamic_axes={
            "input": {0: "batch_size"},
            "output": {0: "batch_size"}
        }
    )
    print(f"ONNXモデルを保存しました: {onnx_path}")


def build_tensorrt_engine(
    onnx_path: str,
    engine_path: str,
    precision: str = "fp16",
    workspace_size_gb: int = 4,
    verbose: bool = False
) -> trt.ICudaEngine:
    """ONNXモデルからTensorRTエンジンをビルド"""
    
    # ロガーの設定
    logger = trt.Logger(trt.Logger.VERBOSE if verbose else trt.Logger.WARNING)
    
    # ビルダーとネットワークの作成
    builder = trt.Builder(logger)
    network_flags = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
    network = builder.create_network(network_flags)
    
    # ONNXパーサーでモデルを読み込み
    parser = trt.OnnxParser(network, logger)
    with open(onnx_path, "rb") as f:
        if not parser.parse(f.read()):
            for error in range(parser.num_errors):
                print(f"ONNXパースエラー: {parser.get_error(error)}")
            raise RuntimeError("ONNXモデルのパースに失敗しました")
    
    # ビルダー設定
    config = builder.create_builder_config()
    config.set_memory_pool_limit(
        trt.MemoryPoolType.WORKSPACE,
        workspace_size_gb * (1 << 30)  # GB -> バイト変換
    )
    
    # 精度設定
    if precision == "fp16":
        if builder.platform_has_fast_fp16:
            config.set_flag(trt.BuilderFlag.FP16)
            print("FP16精度を有効化しました")
        else:
            print("警告: このGPUはFP16に対応していません。FP32で続行します")
    elif precision == "int8":
        if builder.platform_has_fast_int8:
            config.set_flag(trt.BuilderFlag.INT8)
            print("INT8精度を有効化しました(キャリブレーションが必要です)")
        else:
            print("警告: このGPUはINT8に対応していません。FP32で続行します")
    
    # エンジンのビルド
    print("TensorRTエンジンをビルド中...(数分かかる場合があります)")
    serialized_engine = builder.build_serialized_network(network, config)
    
    if serialized_engine is None:
        raise RuntimeError("TensorRTエンジンのビルドに失敗しました")
    
    # エンジンの保存
    with open(engine_path, "wb") as f:
        f.write(serialized_engine)
    print(f"TensorRTエンジンを保存しました: {engine_path}")
    
    # デシリアライズしてエンジンを返す
    runtime = trt.Runtime(logger)
    return runtime.deserialize_cuda_engine(serialized_engine)


def load_tensorrt_engine(engine_path: str) -> trt.ICudaEngine:
    """保存済みTensorRTエンジンを読み込む"""
    logger = trt.Logger(trt.Logger.WARNING)
    runtime = trt.Runtime(logger)
    
    with open(engine_path, "rb") as f:
        return runtime.deserialize_cuda_engine(f.read())


class TensorRTInference:
    """TensorRT推論を実行するクラス"""
    
    def __init__(self, engine: trt.ICudaEngine):
        self.engine = engine
        self.context = engine.create_execution_context()
        
        # 入出力テンソル情報の取得
        self.input_name = engine.get_tensor_name(0)
        self.output_name = engine.get_tensor_name(1)
        
        # CUDAストリームの作成
        import cuda  # cuda-pythonが必要
        self.stream = cuda.cudart.cudaStreamCreate()[1]
    
    def infer(self, input_data: np.ndarray) -> np.ndarray:
        """推論を実行"""
        import cuda
        
        # 入力データの準備
        input_data = np.ascontiguousarray(input_data.astype(np.float32))
        output_shape = self._get_output_shape(input_data.shape)
        output_data = np.empty(output_shape, dtype=np.float32)
        
        # GPUメモリの確保
        d_input = cuda.cudart.cudaMalloc(input_data.nbytes)[1]
        d_output = cuda.cudart.cudaMalloc(output_data.nbytes)[1]
        
        # ホスト→デバイス転送
        cuda.cudart.cudaMemcpyAsync(
            d_input, input_data.ctypes.data,
            input_data.nbytes,
            cuda.cudart.cudaMemcpyKind.cudaMemcpyHostToDevice,
            self.stream
        )
        
        # テンソルアドレスの設定
        self.context.set_tensor_address(self.input_name, int(d_input))
        self.context.set_tensor_address(self.output_name, int(d_output))
        
        # 推論実行
        self.context.execute_async_v3(self.stream)
        
        # デバイス→ホスト転送
        cuda.cudart.cudaMemcpyAsync(
            output_data.ctypes.data, d_output,
            output_data.nbytes,
            cuda.cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost,
            self.stream
        )
        
        # 同期
        cuda.cudart.cudaStreamSynchronize(self.stream)
        
        # メモリ解放
        cuda.cudart.cudaFree(d_input)
        cuda.cudart.cudaFree(d_output)
        
        return output_data
    
    def _get_output_shape(self, input_shape: tuple) -> tuple:
        """出力テンソルのシェイプを取得"""
        self.context.set_input_shape(self.input_name, input_shape)
        return tuple(self.context.get_tensor_shape(self.output_name))


def benchmark(inference: TensorRTInference, input_shape: tuple, iterations: int = 100):
    """推論速度のベンチマーク"""
    import time
    
    # ウォームアップ
    dummy_input = np.random.randn(*input_shape).astype(np.float32)
    for _ in range(10):
        inference.infer(dummy_input)
    
    # ベンチマーク
    times = []
    for _ in range(iterations):
        start = time.perf_counter()
        inference.infer(dummy_input)
        end = time.perf_counter()
        times.append((end - start) * 1000)  # ミリ秒
    
    avg_time = np.mean(times)
    std_time = np.std(times)
    
    print(f"\n===== ベンチマーク結果 =====")
    print(f"平均推論時間: {avg_time:.2f} ms")
    print(f"標準偏差: {std_time:.2f} ms")
    print(f"スループット: {1000 / avg_time:.2f} FPS")
    
    return avg_time


def main():
    """メイン関数"""
    # ResNet18をサンプルモデルとして使用
    from torchvision.models import resnet18, ResNet18_Weights
    
    model = resnet18(weights=ResNet18_Weights.DEFAULT)
    input_shape = (1, 3, 224, 224)
    
    onnx_path = "resnet18.onnx"
    engine_path = "resnet18.engine"
    
    # Step 1: ONNXエクスポート
    if not Path(onnx_path).exists():
        export_to_onnx(model, input_shape, onnx_path)
    
    # Step 2: TensorRTエンジンビルド
    if not Path(engine_path).exists():
        engine = build_tensorrt_engine(
            onnx_path,
            engine_path,
            precision="fp16",
            workspace_size_gb=4,
            verbose=True
        )
    else:
        engine = load_tensorrt_engine(engine_path)
        print(f"既存のTensorRTエンジンを読み込みました: {engine_path}")
    
    # Step 3: 推論実行
    inference = TensorRTInference(engine)
    
    # テスト推論
    test_input = np.random.randn(*input_shape).astype(np.float32)
    output = inference.infer(test_input)
    print(f"\n入力シェイプ: {test_input.shape}")
    print(f"出力シェイプ: {output.shape}")
    
    # ベンチマーク
    benchmark(inference, input_shape, iterations=100)


if __name__ == "__main__":
    main()

5.4 実行結果

上記のコードを実行すると、以下のような出力が得られる。

$ python tensorrt_inference.py
ONNXモデルを保存しました: resnet18.onnx
FP16精度を有効化しました
TensorRTエンジンをビルド中...(数分かかる場合があります)
TensorRTエンジンを保存しました: resnet18.engine

入力シェイプ: (1, 3, 224, 224)
出力シェイプ: (1, 1000)

===== ベンチマーク結果 =====
平均推論時間: 1.23 ms
標準偏差: 0.15 ms
スループット: 813.01 FPS

PyTorchでの推論(約5-6ms)と比較して、約5倍の高速化が実現できている。

5.5 よくあるエラーと対処法

エラー 原因 対処法
CUDA out of memory GPUメモリ不足 workspace_size_gbを小さくする、またはバッチサイズを減らす
ONNXパースエラー: Unsupported operator TensorRTが未対応のオペレータ ONNXのopset_versionを下げる、またはカスタムプラグインを実装
Engine build failed ビルド時のメモリ不足 他のGPUプロセスを終了し、ワークスペースサイズを調整
Serialized engine version mismatch TensorRTバージョンの不一致 エンジンファイルを削除し、現在のTensorRTで再ビルド
Dynamic shape not supported 動的シェイプの設定ミス Optimization Profileを正しく設定する

基本的な使い方をマスターしたので、次は具体的なユースケース別の実装パターンを見ていこう。


6. ユースケース別ガイド

6.1 ユースケース1: リアルタイム画像分類

  • 想定読者: Webカメラやストリーミング映像をリアルタイムで分類したい開発者
  • 推奨構成: FP16精度、バッチサイズ1、最小レイテンシ設定
  • サンプルコード:
"""
リアルタイム画像分類向けTensorRT推論
レイテンシ重視の設定でストリーミング処理を実現
"""

import cv2
import numpy as np
import tensorrt as trt
from typing import Tuple


class RealtimeClassifier:
    """リアルタイム画像分類クラス"""
    
    def __init__(self, engine_path: str, labels_path: str):
        self.engine = self._load_engine(engine_path)
        self.context = self.engine.create_execution_context()
        self.labels = self._load_labels(labels_path)
        
        # 推論用バッファの事前確保(レイテンシ削減)
        self._allocate_buffers()
    
    def _load_engine(self, path: str) -> trt.ICudaEngine:
        logger = trt.Logger(trt.Logger.WARNING)
        runtime = trt.Runtime(logger)
        with open(path, "rb") as f:
            return runtime.deserialize_cuda_engine(f.read())
    
    def _load_labels(self, path: str) -> list:
        with open(path, "r") as f:
            return [line.strip() for line in f]
    
    def _allocate_buffers(self):
        """CUDAバッファの事前確保"""
        import cuda
        
        self.input_shape = (1, 3, 224, 224)
        self.output_shape = (1, 1000)
        
        input_size = np.prod(self.input_shape) * 4  # float32
        output_size = np.prod(self.output_shape) * 4
        
        self.d_input = cuda.cudart.cudaMalloc(input_size)[1]
        self.d_output = cuda.cudart.cudaMalloc(output_size)[1]
        self.h_output = np.empty(self.output_shape, dtype=np.float32)
        self.stream = cuda.cudart.cudaStreamCreate()[1]
    
    def preprocess(self, frame: np.ndarray) -> np.ndarray:
        """画像の前処理"""
        # リサイズ
        img = cv2.resize(frame, (224, 224))
        # BGR -> RGB
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        # 正規化
        img = img.astype(np.float32) / 255.0
        img = (img - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
        # NHWC -> NCHW
        img = np.transpose(img, (2, 0, 1))
        return np.expand_dims(img, axis=0).astype(np.float32)
    
    def classify(self, frame: np.ndarray) -> Tuple[str, float]:
        """画像を分類し、クラス名と確信度を返す"""
        import cuda
        
        # 前処理
        input_data = np.ascontiguousarray(self.preprocess(frame))
        
        # 推論(事前確保バッファを使用)
        cuda.cudart.cudaMemcpyAsync(
            self.d_input, input_data.ctypes.data,
            input_data.nbytes,
            cuda.cudart.cudaMemcpyKind.cudaMemcpyHostToDevice,
            self.stream
        )
        
        input_name = self.engine.get_tensor_name(0)
        output_name = self.engine.get_tensor_name(1)
        self.context.set_tensor_address(input_name, int(self.d_input))
        self.context.set_tensor_address(output_name, int(self.d_output))
        
        self.context.execute_async_v3(self.stream)
        
        cuda.cudart.cudaMemcpyAsync(
            self.h_output.ctypes.data, self.d_output,
            self.h_output.nbytes,
            cuda.cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost,
            self.stream
        )
        cuda.cudart.cudaStreamSynchronize(self.stream)
        
        # 結果の解釈
        probs = np.exp(self.h_output) / np.sum(np.exp(self.h_output))
        class_idx = np.argmax(probs)
        confidence = probs[0, class_idx]
        
        return self.labels[class_idx], confidence


def main():
    classifier = RealtimeClassifier(
        engine_path="resnet18.engine",
        labels_path="imagenet_labels.txt"
    )
    
    cap = cv2.VideoCapture(0)
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        label, confidence = classifier.classify(frame)
        
        # 結果を表示
        text = f"{label}: {confidence:.2%}"
        cv2.putText(frame, text, (10, 30), 
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
        cv2.imshow("Realtime Classification", frame)
        
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

6.2 ユースケース2: バッチ処理による大量画像推論

  • 想定読者: 大量の画像を効率的に処理したいデータサイエンティスト
  • 推奨構成: FP16精度、大きめのバッチサイズ、スループット重視
  • サンプルコード:
"""
バッチ処理向けTensorRT推論
スループット重視で大量データを効率的に処理
"""

import numpy as np
import tensorrt as trt
from pathlib import Path
from typing import Generator, List
import time


class BatchInference:
    """バッチ推論クラス"""
    
    def __init__(self, engine_path: str, batch_size: int = 32):
        self.batch_size = batch_size
        self.engine = self._load_engine(engine_path)
        self.context = self.engine.create_execution_context()
    
    def _load_engine(self, path: str) -> trt.ICudaEngine:
        logger = trt.Logger(trt.Logger.WARNING)
        runtime = trt.Runtime(logger)
        with open(path, "rb") as f:
            return runtime.deserialize_cuda_engine(f.read())
    
    def infer_batch(self, images: np.ndarray) -> np.ndarray:
        """バッチ推論を実行"""
        import cuda
        
        actual_batch = images.shape[0]
        input_name = self.engine.get_tensor_name(0)
        output_name = self.engine.get_tensor_name(1)
        
        # 入力シェイプを動的に設定
        self.context.set_input_shape(input_name, images.shape)
        output_shape = tuple(self.context.get_tensor_shape(output_name))
        
        # メモリ確保
        images = np.ascontiguousarray(images.astype(np.float32))
        output = np.empty(output_shape, dtype=np.float32)
        
        d_input = cuda.cudart.cudaMalloc(images.nbytes)[1]
        d_output = cuda.cudart.cudaMalloc(output.nbytes)[1]
        stream = cuda.cudart.cudaStreamCreate()[1]
        
        # 転送と推論
        cuda.cudart.cudaMemcpyAsync(
            d_input, images.ctypes.data, images.nbytes,
            cuda.cudart.cudaMemcpyKind.cudaMemcpyHostToDevice, stream
        )
        
        self.context.set_tensor_address(input_name, int(d_input))
        self.context.set_tensor_address(output_name, int(d_output))
        self.context.execute_async_v3(stream)
        
        cuda.cudart.cudaMemcpyAsync(
            output.ctypes.data, d_output, output.nbytes,
            cuda.cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost, stream
        )
        cuda.cudart.cudaStreamSynchronize(stream)
        
        # クリーンアップ
        cuda.cudart.cudaFree(d_input)
        cuda.cudart.cudaFree(d_output)
        cuda.cudart.cudaStreamDestroy(stream)
        
        return output
    
    def process_dataset(
        self, 
        image_paths: List[str],
        preprocess_fn
    ) -> Generator[np.ndarray, None, None]:
        """データセット全体をバッチ処理"""
        total = len(image_paths)
        processed = 0
        
        for i in range(0, total, self.batch_size):
            batch_paths = image_paths[i:i + self.batch_size]
            batch_images = np.stack([
                preprocess_fn(path) for path in batch_paths
            ])
            
            yield self.infer_batch(batch_images)
            
            processed += len(batch_paths)
            print(f"処理中: {processed}/{total} ({processed/total:.1%})")


def preprocess_image(path: str) -> np.ndarray:
    """画像の前処理"""
    import cv2
    img = cv2.imread(path)
    img = cv2.resize(img, (224, 224))
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = img.astype(np.float32) / 255.0
    img = (img - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
    return np.transpose(img, (2, 0, 1))


def main():
    # 画像パスのリスト(例)
    image_dir = Path("./images")
    image_paths = list(image_dir.glob("*.jpg"))
    
    print(f"処理対象: {len(image_paths)} 画像")
    
    # バッチ推論
    batch_inference = BatchInference(
        engine_path="resnet18.engine",
        batch_size=32
    )
    
    start = time.time()
    results = []
    for batch_output in batch_inference.process_dataset(
        [str(p) for p in image_paths],
        preprocess_image
    ):
        results.append(batch_output)
    
    elapsed = time.time() - start
    throughput = len(image_paths) / elapsed
    
    print(f"\n===== 処理完了 =====")
    print(f"処理時間: {elapsed:.2f} 秒")
    print(f"スループット: {throughput:.2f} 画像/秒")


if __name__ == "__main__":
    main()

6.3 ユースケース3: Torch-TensorRT による簡易最適化

  • 想定読者: 最小限のコード変更で高速化したいPyTorchユーザー
  • 推奨構成: torch_tensorrtによるワンライナー最適化
  • サンプルコード:
"""
Torch-TensorRTによる簡易最適化
既存のPyTorchコードを最小限の変更で高速化
"""

import torch
import torch_tensorrt
from torchvision.models import resnet18, ResNet18_Weights
import time


def optimize_with_torch_tensorrt():
    """Torch-TensorRTでモデルを最適化"""
    
    # PyTorchモデルのロード
    model = resnet18(weights=ResNet18_Weights.DEFAULT).eval().cuda()
    
    # サンプル入力
    sample_input = torch.randn(1, 3, 224, 224).cuda()
    
    # Torch-TensorRTによる最適化(ワンライナー)
    optimized_model = torch_tensorrt.compile(
        model,
        inputs=[
            torch_tensorrt.Input(
                shape=[1, 3, 224, 224],
                dtype=torch.float32
            )
        ],
        enabled_precisions={torch.float16},  # FP16を有効化
        workspace_size=1 << 30,  # 1GB
        truncate_double=True
    )
    
    return model, optimized_model


def compare_performance(original, optimized, iterations=100):
    """オリジナルと最適化版のパフォーマンス比較"""
    
    sample_input = torch.randn(1, 3, 224, 224).cuda()
    
    # ウォームアップ
    for _ in range(10):
        _ = original(sample_input)
        _ = optimized(sample_input)
    
    torch.cuda.synchronize()
    
    # オリジナルのベンチマーク
    start = time.perf_counter()
    for _ in range(iterations):
        _ = original(sample_input)
        torch.cuda.synchronize()
    original_time = (time.perf_counter() - start) / iterations * 1000
    
    # 最適化版のベンチマーク
    start = time.perf_counter()
    for _ in range(iterations):
        _ = optimized(sample_input)
        torch.cuda.synchronize()
    optimized_time = (time.perf_counter() - start) / iterations * 1000
    
    speedup = original_time / optimized_time
    
    print(f"\n===== パフォーマンス比較 =====")
    print(f"PyTorch (オリジナル): {original_time:.2f} ms")
    print(f"Torch-TensorRT: {optimized_time:.2f} ms")
    print(f"高速化率: {speedup:.2f}x")
    
    return speedup


def main():
    print("Torch-TensorRTでモデルを最適化中...")
    original, optimized = optimize_with_torch_tensorrt()
    
    print("パフォーマンス比較を実行中...")
    compare_performance(original, optimized)
    
    # 最適化モデルの保存
    torch.jit.save(
        torch.jit.trace(optimized, torch.randn(1, 3, 224, 224).cuda()),
        "resnet18_trt.ts"
    )
    print("\n最適化モデルを保存しました: resnet18_trt.ts")


if __name__ == "__main__":
    main()

ユースケースが把握できたところで、この記事を読んだ後の学習パスを確認しよう。


7. 学習ロードマップ

この記事を読んだ後、次のステップとして以下をおすすめする。

初級者向け(まずはここから)

  1. 公式クイックスタートガイドを試す

  2. trtexecコマンドラインツールを使う

    • GUIやコードなしでONNXモデルをTensorRTエンジンに変換できる
    • trtexec --onnx=model.onnx --saveEngine=model.engine --fp16

中級者向け(実践に進む)

  1. INT8量子化にチャレンジ

  2. 動的シェイプ(Dynamic Shape)対応

    • Optimization Profileの設定方法
    • 可変バッチサイズ・可変解像度への対応
  3. TensorRT-LLMを試す

上級者向け(さらに深く)

  1. カスタムプラグインの実装

  2. Triton Inference Serverでのデプロイ

  3. プロファイリングと最適化

    • Nsight Systems / Nsight Deep Learning Designerの活用
    • ボトルネックの特定と解消

8. まとめ

この記事では、TensorRTについて以下を解説した。

  1. TensorRTとは: 学習済みモデルをNVIDIA GPU向けに最適化する推論エンジン
  2. 高速化の仕組み: レイヤーフュージョン、精度キャリブレーション、カーネルオートチューニング、動的メモリ管理の4つの最適化技術
  3. 実践的な使い方: PyTorchモデルをONNX経由でTensorRTエンジンに変換する具体的な手順
  4. ユースケース別の実装: リアルタイム処理、バッチ処理、Torch-TensorRTによる簡易最適化

私の所感

TensorRTを使いこなすと、「推論速度がボトルネック」という問題の大半は解決できる。特にFP16最適化は、追加のキャリブレーション作業なしで2倍近い高速化が得られるため、まず試すべき選択肢だと考えている。

一方で、TensorRTは「ビルド時にターゲットGPUが必要」「ONNXへの変換が必要」といった制約もある。本番環境のGPUと開発環境が異なる場合は、CI/CDパイプラインでのエンジンビルドを検討するとよいだろう。

深層学習モデルの本番投入を考えているなら、TensorRTは間違いなく学ぶ価値のある技術だ。この記事が、その第一歩になれば幸いである。


参考文献

9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?