深層学習モデルの高速推論を支える TensorRT の概要

GCP の Tesla T4 が安くなったと思ったら元通りの価格にもどっていて、あれは幻だったのか・・・と嘆いている R&D チームの奥村(@izariuo440)です。これまで何度か TensorRT について触れてきましたが、どのように使うのかは触れていませんでした。今回は、TensorRT の C++ API を使うための主要オブジェクトについて紹介します。Python API については触れません。

TensorRT をなぜ使うのかについてはこちら。

TensorRT の更新ウォッチはこちら。

  1. TensorRT 6 でさらに快適な高速推論 - OPTiM TECH BLOG
  2. TensorRT 7 でさらに快適な高速推論 - OPTiM TECH BLOG

TensorRT リファレンスはこちら。

  1. TensorRT C++ API Reference 7.0.0.11

概要

TensorRT の利用フェーズは、「コンパイル時」と「実行時」に分けることができます。

コンパイル時には、ビルダーによってネットワーク定義をエンジンに組み込み、エンジンによってホストメモリに内容をシリアライズします。これによってコンパイル済みモデル(ネットワーク)が生成できます。

実行時とは、コンパイル済みモデルを使った推論実行時のことです。ランタイムによってコンパイル済みモデルからエンジンを復元し、エンジンによって実行コンテキストを作って推論を実行します。

コンパイルは、ネットワーク定義にもよりますが数分程度の時間がかかることがあります。アプリケーションで使うときは、事前にコンパイルしてコンパイル済みモデルを作成しておいて、それを読み込んで使うのがよいでしょう。ただし、コンパイル済みモデルは GPU、TensorRT のバージョン、OS などが異なると使えないので注意してください。また、コンパイル時にはプロファイリングによる最適化が行われるので、GPU が何もしていない状態でコンパイルするのがいいでしょう。

コンパイル時と実行時の手順は以下のとおりです。

  1. コンパイル時
    1. アプリでビルダーを作る
    2. ビルダーでビルダー環境設定を作り、必要に応じて変更する
    3. ビルダーでネットワーク定義を作り、ネットワーク構造を構築する
    4. ビルダーでネットワーク定義とビルダー環境設定から、エンジンを作る
    5. エンジンでネットワークをホストメモリにシリアライズする
    6. アプリがでストメモリにシリアライズされた内容をファイルなどに書き出す
  2. 実行時
    1. アプリでランタイムを作る
    2. アプリでファイルなどからシリアライズされた内容を読み取る
    3. ランタイムで上記内容をデシリアライズして、エンジンを作る
    4. エンジンで実行コンテキストを作る
    5. エンジンで入出力テンソルの情報を取得する
    6. アプリで上記に応じて CUDA デバイスメモリを確保し、推論用バインディングを用意する
    7. アプリで CUDA ストリームを作成する
    8. アプリで入力テンソルを CUDA に転送する
    9. 実行コンテキストでバインディングと CUDA ストリームを指定して、推論を実行する
    10. アプリで出力テンソルを CUDA から転送する
    11. アプリで CUDA ストリームの実行完了を待機する
    12. アプリで出力テンソルを扱う

ビルダー

nvinfer1::IBuilder クラスが該当します。

ネットワーク定義からエンジンをビルドします。

ビルダー環境設定

nvinfer1::IBuilderConfig クラスが該当します。

ネットワーク定義

nvinfer1::INetworkDefinition が該当します。

ビルダーに入力するネットワーク定義です。

ネットワーク定義は、ネットワーク構造を定義します。ビルダーは、ビルダー環境設定を指定してネットワーク定義をエンジンに組み込みます。ネットワーク定義として、バッチ次元を暗黙的にするか(バッチ次元を実行時に指定)、すべての次元を明示的にするかを選択できます。前者を implicit batch size mode、後者を full dims mode と呼びます。createNetwork() でネットワーク定義を作ると、前者しかサポートされません。hasImplicitBatchSize() でどのモードになっているかを確認できます。

エンジン

nvinfer1::ICudaEngine が該当します。

構築したネットワークで推論を実行するためのエンジンです。機能的に安全でない機能が含まれます。

ホストメモリ

nvinfer1::IHostMemory が該当します。

ライブラリによって確保されたメモリをハンドルするクラスで、それをユーザーがアクセスできるようにします。エンジンによってシリアライズされた内容を、ランタイムがデシリアライズできるように橋渡しするために使います。

ホストメモリオブジェクトによって割り当てられたメモリはライブラリによって所有されていて、destroy メソッドを呼び出すと破棄されます。

ランタイム

nvinfer1::IRuntime が該当します。

シリアライズされた機能的に安全でないエンジンをデシリアライズします。

実行コンテキスト

nvinfer1::IExecutionContext が該当します。

エンジンを使って推論を実行するコンテキストです。機能的に安全でない機能が含まれます。

複数の実行コンテキストが、ひとつのエンジン (nvinfer::ICudaEngine) に対して存在することがあります。これにより、同じエンジンを使って、複数のバッチを同時に実行するような使い方ができます。エンジンが動的 shape をサポートしている場合、同時に使用される各実行コンテキストは、個別に最適化プロファイルを使う必要があります。

TensorRT のプラグイン

TensorRT には深層学習で用いられる多くのオペレーター(レイヤー)が実装されていて、大抵はそれを使えば事足ります。しかし、物体検出などは特殊なオペレーターが必要になることが多く、それらは TensorRT では「プラグイン」という形で実装されています。TensorRT が標準で用意しているプラグインを利用することもできますし、自分で実装することもできます。

プラグインは、TensorRT 4 以前とそれ以降でインターフェースが異なります。

TensorRT 4 以前

IPluginIPluginFactory を使う。

TensorRT 5 以降

IPluginV2IPluginCreator を使う。IPluginV2 の拡張もある。IPluginV2 は、TensorRT 7 でもまだ非推奨になっていない。

プラグイン毎に以下が必要になる。

  1. IPluginV2 の派生クラスを実装する
  2. IPluginCreator の派生クラスを実装する
  3. REGISTER_TENSORRT_PLUGIN マクロで IPluginCreator の派生クラスを登録する

IPluginV2

プラグインは、アプリケーションにカスタムレイヤーを実装するためのメカニズムである。IPluginCreator と組み合わせると、プラグインを登録し、デシリアライズするときにプラグインレジストリを検索するするメカニズムを提供する。17 個のインターフェースがあり、うち 16 個のインターフェースを実装する必要がある。

getTensorRTVersion

このプラグインがビルドされた TensorRT API バージョンを返す。プラグインの後方互換性を維持するために TensorRT によって使用されるので、上書きしてはいけない。

virtual int getTensorRTVersion() const TRTNOEXCEPT
{
    return NV_TENSORRT_VERSION;
}
getPluginType

プラグインの種類を返す。対応するプラグイン作成器によって返されるプラグイン名 (IPluginCreator::getPluginName()) と一致すべきである。

virtual const char* getPluginType() const TRTNOEXCEPT = 0;
getPluginVersion

プラグインのバージョンを返す。対応するプラグイン作成器によって返されるプラグインバージョン (IPluginCreator::getPluginVersion()) と一致すべきである。

virtual const char* getPluginVersion() const TRTNOEXCEPT = 0;
getNbOutputs

出力テンソルの個数を返す。この関数は INetworkDefinitionIBuilder の実装によって呼び出される。特に、initialize() を呼び出す前に呼び出される。

virtual int getNbOutputs() const TRTNOEXCEPT = 0;
getOutputDimensions

出力テンソルの次元を返す。この関数は INetworkDefinitionIBuilder の実装によって呼び出される。特に、initialize() を呼び出す前に呼び出される。

virtual Dims getOutputDimensions(int index, const Dims* inputs, int nbInputDims) TRTNOEXCEPT = 0;
supportsFormat

フォーマットのサポートを確認する。指定された typeformat の組み合わせをサポートしている場合、true を返す。この関数は INetworkDefinitionIBuilderICudaEngine の実装によって呼び出される。特に、エンジンを生成するときとエンジンをデシリアライズするときに呼び出される。

format 引数について、PluginFormat::kCHW4PluginFormat::kCHW16PluginFormat::kCHW32 は渡されることがない。これは TensorRT 5.x シリーズとの後方互換性を維持するためである。他の形式については PluginV2IOExtPluginV2DynamicExt を使うこと。

virtual bool supportsFormat(DataType type, PluginFormat format) const TRTNOEXCEPT = 0;
configureWithFormat

レイヤーを環境設定する。この関数は initialize() の前にビルダーによって呼び出される。これにより、レイヤーは重み、次元、最大バッチサイズにもとづいてアルゴリズムを選択する機会を提供する。

引き渡される次元は、バッチサイズを含まない。

format 引数について、PluginFormat::kCHW4PluginFormat::kCHW16PluginFormat::kCHW32 は渡されることがない。これは TensorRT 5.x シリーズとの後方互換性を維持するためである。他の形式については PluginV2IOExtPluginV2DynamicExt を使うこと。

virtual void configureWithFormat(const Dims* inputDims, int nbInputs, const Dims* outputDims, int nbOutputs, DataType type, PluginFormat format, int maxBatchSize) TRTNOEXCEPT = 0;
initialize

レイヤーを実行するために初期化する。エンジンの作成時に呼び出される。

成功したら 0、失敗したら 0 でない値を返す(これによりエンジンが終了する)。

virtual int initialize() TRTNOEXCEPT = 0;
terminate

プラグインのレイヤーの初期化中に確保したリソースを開放する。エンジンを破棄したときに呼び出される。

virtual void terminate() TRTNOEXCEPT = 0;
getWorkspaceSize

レイヤーに必要なワークスペースのサイズを見つける。

initialize() の後、エンジンの起動中に呼び出される。返されるワークスペースのサイズは、最大バッチサイズまでのすべてのバッチサイズに対して十分であるべきである。

virtual size_t getWorkspaceSize(int maxBatchSize) const TRTNOEXCEPT = 0;
enqueue

レイヤーを実行する。

成功したら 0、失敗したら 0 でない値を返す(これによりエンジンが終了する)。

virtual int enqueue(int batchSize, const void* const* inputs, void** outputs, void* workspace, cudaStream_t stream) TRTNOEXCEPT = 0;
getSerializationState

シリアライズするのに必要なバッファのサイズを見つける。

virtual size_t getSerializationSize() const TRTNOEXCEPT = 0;
serialize

レイヤーをシリアライズする。

buffer は、データをシリアライズするためのバッファへのポインタである。このサイズは getSerializationSize() によって返された値と同じでなければならない。

virtual void serialize(void* buffer) const TRTNOEXCEPT = 0;
destroy

プラグインオブジェクトを破棄する。ネットワーク、ビルダー、エンジンが破棄されるときに呼び出される。

virtual void destroy() TRTNOEXCEPT = 0;
clone

プラグインオブジェクトをクローンする。これにより、内部のプラグインパラメーターがコピーされ、これらのパラメーターを持つ新しいプラグインオブジェクトを返す。

virtual IPluginV2* clone() const TRTNOEXCEPT = 0;
setPluginNamespace

このプラグインが属する名前空間を設定する。理想的には、同じプラグインライブラリのすべてのプラグインオブジェクトは、同じ名前空間を持つべきである。

virtual void setPluginNamespace(const char* pluginNamespace) TRTNOEXCEPT = 0;
getPluginNamespace

このプラグインオブジェクトの名前空間を返す。

virtual const char* getPluginNamespace() const TRTNOEXCEPT = 0;

IPluginCreator

プラグイン生成器。IPluginV2 のプラグインを生成する。

9 個のインターフェースがあり、うち 7 個のインターフェースを実装する必要がある。

getTensorRTVersion

このプラグイン生成器がコンパイルされた TensorRT API バージョンを返す。

virtual int getTensorRTVersion() const TRTNOEXCEPT { return NV_TENSORRT_VERSION; }
getPluginName

プラグインの名前を返す。

virtual const char* getPluginName() const TRTNOEXCEPT = 0;
getPluginVersion

プラグインのバージョンを返す。

virtual const char* getPluginVersion() const TRTNOEXCEPT = 0;
getFiledNames

createPlugin に引き渡される必要のあるフィールド名のリストを返す。

virtual const PluginFieldCollection* getFieldNames() TRTNOEXCEPT = 0;
createPlugin

プラグインオブジェクトを返す。エラー時には nullptr を返す。

virtual IPluginV2* createPlugin(const char* name, const PluginFieldCollection* fc) TRTNOEXCEPT = 0;
deserializePlugin

プラグインレイヤーのデシリアライズ中に呼び出される。プラグインオブジェクトを返す。

virtual IPluginV2* deserializePlugin(const char* name, const void* serialData, size_t serialLength) TRTNOEXCEPT = 0;
setPluginNamespace

属するプラグインライブラリにもとづいてプラグイン生成器の名前空間を設定する。プラグイン生成器の登録中 (IPluginRegistry::registerCreator()) に設定できる。

virtual void setPluginNamespace(const char* pluginNamespace) TRTNOEXCEPT = 0;
getPluginNamespace

プラグイン生成器オブジェクトの名前空間を返す。

virtual const char* getPluginNamespace() const TRTNOEXCEPT = 0;
~IPluginCreator

デストラクタ。

virtual ~IPluginCreator() {}

IPluginRegistry

アプリケーション内のすべてのプラグインの単一登録所。エンジンのデシリアライズ中にプラグインの実装を見つけるために使われる。内部的には、プラグインレジストリはシングルトンとみなされるため、アプリケーション内のすべてのプラグインは、同じグローバルなレジストリの一部となる。プラグインレジストリは、IPluginV2 のプラグインのみをサポートしており、それに対応する IPluginCreator の実装も必要であることに注意。

REGISTER_TENSORRT_PLUGIN マクロによって、暗黙的に使用される。このマクロ以外でアクセスする機会はあまりないだろう。

まとめ

ソフトウェアの情報は英語がほとんどなのは世の常ですが、日本語情報が少ないのでざっくりとまとめてみました。

オプティムでは、こうした技術に興味がある・作ってみたい・既に作っている、というエンジニアを募集しています。興味のある方は、こちらをご覧ください。

www.optim.co.jp