多くの PyTorch ユーザーは、検出モデルの後処理部分を TensorRT プラグインを使用して実装しています。これにより、モデル全体を TensorRT にエクスポートできます。Blade は優れた拡張性を提供します。カスタム TensorRT プラグインをお持ちの場合は、Blade と組み合わせて最適化に使用できます。このトピックでは、Blade を使用して、すでに TensorRT プラグインを使用している検出モデルを最適化する方法について説明します。
背景情報
TensorRT は、NVIDIA GPU での推論最適化のための強力なツールです。Blade の基盤となる最適化は、TensorRT の手法と深く統合されています。Blade は、計算グラフの最適化、TensorRT や oneDNN などのベンダーライブラリ、AI コンパイラの最適化、Blade の手動で最適化された演算子ライブラリ、Blade 混合精度、Blade EasyCompression など、複数の最適化技術も統合しています。
RetinaNet は、ワンステージの RCNN 検出ネットワークです。その基本構造は、バックボーン、複数のサブネット、および Non-Maximum Suppression (NMS) の後処理で構成されています。多くのトレーニングフレームワークが RetinaNet を実装しており、Detectron2 はその典型的な例です。以前、このドキュメントでは、scripting_with_instances メソッドを使用して RetinaNet (Detectron2) モデルをエクスポートし、Blade で迅速に最適化する方法について説明しました。詳細については、「RetinaNet 最適化ケース 1: Blade を使用した RetinaNet (Detectron2) モデルの最適化」をご参照ください。
多くの PyTorch ユーザーはモデルを ONNX にエクスポートし、TensorRT でデプロイしますが、このプロセスには制限があります。TensorRT での ONNX エクスポートと ONNX opset のサポートは限定的であり、最適化プロセスが信頼できないものになる可能性があります。特に、検出ネットワークの後処理部分を ONNX にエクスポートして TensorRT で最適化することは困難です。後処理コードも非効率な場合が多いです。そのため、多くのユーザーは TensorRT プラグインメカニズムを使用して後処理部分を実装しています。これにより、モデル全体を TensorRT にエクスポートできます。
比較すると、Blade と TorchScript カスタム C++ 演算子を使用した最適化は、TensorRT プラグインメカニズムを使用して後処理部分を実装するよりも簡単です。詳細については、「RetinaNet 最適化ケース 2: Blade とカスタム C++ 演算子を使用したモデルの最適化」をご参照ください。Blade は優れた拡張性も備えています。すでにカスタム TensorRT プラグインを実装している場合は、Blade と組み合わせて協調的な最適化を行うことができます。
制限事項
このトピックで使用される環境は、次の要件を満たす必要があります。
-
システム環境:Linux (Python 3.6 以降、GCC 5.4 以降)、Nvidia Tesla T4、CUDA 10.2、CuDNN 8.0.5.39、および TensorRT 7.2.2.3。
-
フレームワーク:PyTorch 1.8.1 以降および Detectron2 0.4.1 以降。
-
推論最適化ツール:Blade 3.16.0 以降 (TensorRT に動的リンク)。
操作手順
以下の手順では、Blade と TensorRT プラグインを使用してモデルを最適化する方法について説明します。
-
ステップ 1: TensorRT プラグインを使用した PyTorch モデルの作成
TensorRT プラグインを使用して RetinaNet の後処理部分を実装します。
-
blade.optimizeインターフェイスを呼び出してモデルを最適化し、最適化されたモデルを保存します。 -
元のモデルと最適化されたモデルで性能テストを実行した後、結果に問題がなければ、最適化されたモデルをロードして推論を実行できます。
ステップ 1: TensorRT プラグインを使用した PyTorch モデルの作成
Blade は TensorRT 拡張メカニズムと連携して協調的な最適化を行うことができます。このセクションでは、TensorRT 拡張を使用して RetinaNet の後処理部分を実装する方法について説明します。TensorRT プラグインの開発とコンパイルの詳細については、NVIDIA Deep Learning TensorRT ドキュメントをご参照ください。このトピックの RetinaNet 後処理部分のプログラムロジックは、NVIDIA オープンソースコミュニティのものです。詳細については、Retinanet-Examples をご参照ください。このトピックでは、コアコードを抜粋して、カスタム演算子の開発と実装プロセスを説明します。
-
サンプルコードをダウンロードして解凍します。
wget -nv https://pai-blade.oss-cn-zhangjiakou.aliyuncs.com/tutorials/retinanet_example/retinanet-examples.tar.gz -O retinanet-examples.tar.gz tar xvfz retinanet-examples.tar.gz 1>/dev/null -
TensorRT プラグインをコンパイルします。
サンプルコードには、RetinaNet の
decodeおよびnms後処理のための TensorRT プラグインの実装と登録が含まれています。PyTorch の公式ドキュメントでは、カスタム演算子をコンパイルする 3 つの方法 (CMake でのビルド、Just-in-Time (JIT) コンパイル、Setuptools でのビルド) が提供されています。詳細については、EXTENDING TORCHSCRIPT WITH CUSTOM C++ OPERATORS をご参照ください。これらの 3 つのコンパイル方法は、さまざまなシナリオに適しています。ニーズに応じて選択できます。簡単にするために、このトピックでは JIT コンパイル方法を使用します。以下はサンプルコードです。説明コンパイルする前に、TensorRT、CUDA、CUDNN などの依存ライブラリを設定する必要があります。
import torch.utils.cpp_extension import os codebase="retinanet-examples" sources=['csrc/plugins/plugin.cpp', 'csrc/cuda/decode.cu', 'csrc/cuda/nms.cu',] sources = [os.path.join(codebase,src) for src in sources] torch.utils.cpp_extension.load( name="plugin", sources=sources, build_directory=codebase, extra_include_paths=['/usr/local/TensorRT/include/', '/usr/local/cuda/include/', '/usr/local/cuda/include/thrust/system/cuda/detail'], extra_cflags=['-std=c++14', '-O2', '-Wall'], extra_ldflags=['-L/usr/local/TensorRT/lib/', '-lnvinfer'], extra_cuda_cflags=[ '-std=c++14', '--expt-extended-lambda', '--use_fast_math', '-Xcompiler', '-Wall,-fno-gnu-unique', '-gencode=arch=compute_75,code=sm_75',], is_python_module=False, with_cuda=True, verbose=False, ) -
RetinaNet 畳み込みモデル部分をカプセル化します。
RetinaNet モデル部分を個別の
RetinaNetBackboneAndHeadsモジュールにカプセル化します。import torch from typing import List from torch import Tensor from torch.testing import assert_allclose from detectron2 import model_zoo # このクラスは、RetinaNet のバックボーンと RPN ヘッドをカプセル化します。 class RetinaNetBackboneAndHeads(torch.nn.Module): def __init__(self, model): super().__init__() self.model = model def preprocess(self, img): batched_inputs = [{"image": img}] images = self.model.preprocess_image(batched_inputs) return images.tensor def forward(self, images): features = self.model.backbone(images) features = [features[f] for f in self.model.head_in_features] cls_heads, box_heads = self.model.head(features) cls_heads = [cls.sigmoid() for cls in cls_heads] box_heads = [b.contiguous() for b in box_heads] return cls_heads, box_heads retinanet_model = model_zoo.get("COCO-Detection/retinanet_R_50_FPN_3x.yaml", trained=True).eval() retinanet_bacbone_heads = RetinaNetBackboneAndHeads(retinanet_model) -
TensorRT プラグインを使用して RetinaNet 後処理ネットワークをビルドします。すでに TensorRT エンジンを作成している場合は、このステップをスキップできます。
-
TensorRT エンジンを作成します。
TensorRT プラグインを有効にするには、次の機能を実装する必要があります。
-
ctypes.cdll.LoadLibraryを使用して、コンパイルされた plugin.so ファイルを動的にロードします。 -
build_retinanet_decode関数は、tensorrtPython API を使用して後処理ネットワークをビルドし、それをエンジンにビルドします。
以下はサンプルコードです。
import os import numpy as np import tensorrt as trt import ctypes # TensorRT プラグインのダイナミックリンクライブラリをロードします。 codebase="retinanet-examples" ctypes.cdll.LoadLibrary(os.path.join(codebase, 'plugin.so')) TRT_LOGGER = trt.Logger() trt.init_libnvinfer_plugins(TRT_LOGGER, "") PLUGIN_CREATORS = trt.get_plugin_registry().plugin_creator_list # TensorRT プラグイン関数を取得します。 def get_trt_plugin(plugin_name, field_collection): plugin = None for plugin_creator in PLUGIN_CREATORS: if plugin_creator.name != plugin_name: continue if plugin_name == "RetinaNetDecode": plugin = plugin_creator.create_plugin( name=plugin_name, field_collection=field_collection ) if plugin_name == "RetinaNetNMS": plugin = plugin_creator.create_plugin( name=plugin_name, field_collection=field_collection ) assert plugin is not None, "plugin not found" return plugin # TensorRT ネットワークをビルドする関数。 def build_retinanet_decode(example_outputs, input_image_shape, anchors_list, test_score_thresh = 0.05, test_nms_thresh = 0.5, test_topk_candidates = 1000, max_detections_per_image = 100, ): builder = trt.Builder(TRT_LOGGER) EXPLICIT_BATCH = 1 << (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) network = builder.create_network(EXPLICIT_BATCH) config = builder.create_builder_config() config.max_workspace_size = 3 ** 20 cls_heads, box_heads = example_outputs profile = builder.create_optimization_profile() decode_scores = [] decode_boxes = [] decode_class = [] input_blob_names = [] input_blob_types = [] def _add_input(head_tensor, head_name): input_blob_names.append(head_name) input_blob_types.append("Float") head_shape = list(head_tensor.shape)[-3:] profile.set_shape( head_name, [1] + head_shape, [20] + head_shape, [1000] + head_shape) return network.add_input( name=head_name, dtype=trt.float32, shape=[-1] + head_shape ) # ネットワーク入力をビルドします。 cls_head_inputs = [] cls_head_strides = [input_image_shape[-1] // cls_head.shape[-1] for cls_head in cls_heads] for idx, cls_head in enumerate(cls_heads): cls_head_name = "cls_head" + str(idx) cls_head_inputs.append(_add_input(cls_head, cls_head_name)) box_head_inputs = [] for idx, box_head in enumerate(box_heads): box_head_name = "box_head" + str(idx) box_head_inputs.append(_add_input(box_head, box_head_name)) output_blob_names = [] output_blob_types = [] # デコードネットワークをビルドします。 for idx, anchors in enumerate(anchors_list): field_coll = trt.PluginFieldCollection([ trt.PluginField("topk_candidates", np.array([test_topk_candidates], dtype=np.int32), trt.PluginFieldType.INT32), trt.PluginField("score_thresh", np.array([test_score_thresh], dtype=np.float32), trt.PluginFieldType.FLOAT32), trt.PluginField("stride", np.array([cls_head_strides[idx]], dtype=np.int32), trt.PluginFieldType.INT32), trt.PluginField("num_anchors", np.array([anchors.numel()], dtype=np.int32), trt.PluginFieldType.INT32), trt.PluginField("anchors", anchors.contiguous().cpu().numpy().astype(np.float32), trt.PluginFieldType.FLOAT32),] ) decode_layer = network.add_plugin_v2( inputs=[cls_head_inputs[idx], box_head_inputs[idx]], plugin=get_trt_plugin("RetinaNetDecode", field_coll), ) decode_scores.append(decode_layer.get_output(0)) decode_boxes.append(decode_layer.get_output(1)) decode_class.append(decode_layer.get_output(2)) # NMS ネットワークをビルドします。 scores_layer = network.add_concatenation(decode_scores) boxes_layer = network.add_concatenation(decode_boxes) class_layer = network.add_concatenation(decode_class) field_coll = trt.PluginFieldCollection([ trt.PluginField("nms_thresh", np.array([test_nms_thresh], dtype=np.float32), trt.PluginFieldType.FLOAT32), trt.PluginField("max_detections_per_image", np.array([max_detections_per_image], dtype=np.int32), trt.PluginFieldType.INT32),] ) nms_layer = network.add_plugin_v2( inputs=[scores_layer.get_output(0), boxes_layer.get_output(0), class_layer.get_output(0)], plugin=get_trt_plugin("RetinaNetNMS", field_coll), ) nms_layer.get_output(0).name = "scores" nms_layer.get_output(1).name = "boxes" nms_layer.get_output(2).name = "classes" nms_outputs = [network.mark_output(nms_layer.get_output(k)) for k in range(3)] config.add_optimization_profile(profile) cuda_engine = builder.build_engine(network, config) assert cuda_engine is not None return cuda_engine -
-
RetinaNetBackboneAndHeadsの実際の出力数、出力タイプ、および出力形状に基づいてcuda_engineを作成します。import numpy as np from detectron2.data.detection_utils import read_image !wget http://images.cocodataset.org/val2017/000000439715.jpg -q -O input.jpg img = read_image('./input.jpg') img = torch.from_numpy(np.ascontiguousarray(img.transpose(2, 0, 1))) example_inputs = retinanet_bacbone_heads.preprocess(img) example_outputs = retinanet_bacbone_heads(example_inputs) cell_anchors = [c.contiguous() for c in retinanet_model.anchor_generator.cell_anchors] cuda_engine = build_retinanet_decode( example_outputs, example_inputs.shape, cell_anchors)
-
-
Blade 拡張を使用して、PyTorch と TensorRT エンジンの両方を使用するモデルをサポートします。
次のコードは、
RetinaNetWrapper、RetinaNetBackboneAndHeads、およびRetinaNetPostProcessを使用して、バックボーン、ヘッド、および TensorRT プラグインの後処理部分を再結合します。import blade.torch # Blade TensorRT 拡張でサポートされる後処理部分。 class RetinaNetPostProcess(torch.nn.Module): def __init__(self, cuda_engine): super().__init__() blob_names = [cuda_engine.get_binding_name(idx) for idx in range(cuda_engine.num_bindings)] input_blob_names = blob_names[:-3] input_blob_types = ["Float"] * len(input_blob_names) output_blob_names = blob_names[-3:] output_blob_types = ["Float"] * len(output_blob_names) self.trt_ext_plugin = torch.classes.torch_addons.TRTEngineExtension( bytes(cuda_engine.serialize()), (input_blob_names, output_blob_names, input_blob_types, output_blob_types), ) def forward(self, inputs: List[Tensor]): return self.trt_ext_plugin.forward(inputs) # PyTorch と TensorRT エンジンを併用します。 class RetinaNetWrapper(torch.nn.Module): def __init__(self, model, trt_postproc): super().__init__() self.backbone_and_heads = model self.trt_postproc = torch.jit.script(trt_postproc) def forward(self, images): cls_heads, box_heads = self.backbone_and_heads(images) return self.trt_postproc(cls_heads + box_heads) trt_postproc = RetinaNetPostProcess(cuda_engine) retinanet_mix_trt = RetinaNetWrapper(retinanet_bacbone_heads, trt_postproc) # TorchScript ファイルとしてエクスポートして保存します。 retinanet_script = torch.jit.trace(retinanet_mix_trt, (example_inputs, ), check_trace=False) torch.jit.save(retinanet_script, 'retinanet_script.pt') torch.save(example_inputs, 'example_inputs.pth') outputs = retinanet_script(example_inputs)新しくアセンブルされた
torch.nn.Moduleには、次の特徴があります。-
Blade TensorRT 拡張でサポートされている
torch.classes.torch_addons.TRTEngineExtensionインターフェイスを使用します。 -
TorchScript モデルのエクスポートをサポートします。上記のコードでは
torch.jit.traceを使用してモデルをエクスポートします。 -
TorchScript 形式でのモデルの保存をサポートします。
-
ステップ 2: Blade を呼び出してモデルを最適化
-
Blade 最適化インターフェイスを呼び出します。
blade.optimizeインターフェイスを呼び出してモデルを最適化します。以下はサンプルコードです。blade.optimizeインターフェイスの詳細については、「PyTorch モデルの最適化」をご参照ください。import blade import blade.torch import ctypes import torch import os codebase="retinanet-examples" ctypes.cdll.LoadLibrary(os.path.join(codebase, 'plugin.so')) blade_config = blade.Config() blade_config.gpu_config.disable_fp16_accuracy_check = True script_model = torch.jit.load('retinanet_script.pt') example_inputs = torch.load('example_inputs.pth') test_data = [(example_inputs,)] # PyTorch の入力データはタプルのリストです。 with blade_config: optimized_model, opt_spec, report = blade.optimize( script_model, # 前のステップでエクスポートされた TorchScript モデル。 'o1', # Blade O1 レベルの最適化を有効にします。 device_type='gpu', # ターゲットデバイスは GPU です。 test_data=test_data, # 最適化とテストを支援するために、テストデータのセットを提供します。 ) -
最適化レポートを出力し、モデルを保存します。
Blade で最適化されたモデルは、引き続き TorchScript モデルです。最適化後、次のコードを使用して最適化レポートを出力し、最適化されたモデルを保存できます。
# 最適化レポートを出力します。 print("Report: {}".format(report)) # 最適化されたモデルを保存します。 torch.jit.save(optimized_model, 'optimized.pt')出力された最適化レポートは次のとおりです。最適化レポートのフィールドの詳細については、「最適化レポート」をご参照ください。
Report: { "software_context": [ { "software": "pytorch", "version": "1.8.1+cu102" }, { "software": "cuda", "version": "10.2.0" } ], "hardware_context": { "device_type": "gpu", "microarchitecture": "T4" }, "user_config": "", "diagnosis": { "model": "unnamed.pt", "test_data_source": "user provided", "shape_variation": "undefined", "message": "Unable to deduce model inputs information (data type, shape, value range, etc.)", "test_data_info": "0 shape: (1, 3, 480, 640) data type: float32" }, "optimizations": [ { "name": "PtTrtPassFp16", "status": "effective", "speedup": "4.37", "pre_run": "40.59 ms", "post_run": "9.28 ms" } ], "overall": { "baseline": "40.02 ms", "optimized": "9.27 ms", "speedup": "4.32" }, "model_info": { "input_format": "torch_script" }, "compatibility_list": [ { "device_type": "gpu", "microarchitecture": "T4" } ], "model_sdk": {} } -
元のモデルと最適化されたモデルで性能テストを実行します。
以下は、性能テストのサンプルコードです。
import time @torch.no_grad() def benchmark(model, inp): for i in range(100): model(inp) torch.cuda.synchronize() start = time.time() for i in range(200): model(inp) torch.cuda.synchronize() elapsed_ms = (time.time() - start) * 1000 print("Latency: {:.2f}".format(elapsed_ms / 200)) # 元のモデルの性能をテストします。 benchmark(script_model, example_inputs) # 最適化されたモデルの性能をテストします。 benchmark(optimized_model, example_inputs)以下は、このテストのリファレンス結果です。
Latency: 40.71 Latency: 9.35結果は、200 回の実行後、元のモデルと最適化されたモデルの平均レイテンシーがそれぞれ 40.71 ms と 9.35 ms であることを示しています。
ステップ 3: 最適化されたモデルのロードと実行
オプション: 試用期間中に、認証失敗によるプログラムの予期せぬ終了を防ぐために、次の環境変数を設定します。
export BLADE_AUTH_USE_COUNTING=1PAI-Blade を使用するための認証を取得します。
export BLADE_REGION=<region> export BLADE_TOKEN=<token>ビジネス要件に基づいて、次のパラメーターを設定します。
<region>:PAI-Blade を使用するリージョン。PAI-Blade ユーザーの DingTalk グループに参加して、PAI-Blade を使用できるリージョンを取得できます。DingTalk グループの QR コードについては、「PAI-Blade のインストール」をご参照ください。
<token>:PAI-Blade の使用に必要な認証トークン。PAI-Blade ユーザーの DingTalk グループに参加して、認証トークンを取得できます。DingTalk グループの QR コードについては、「PAI-Blade のインストール」をご参照ください。
-
最適化されたモデルをロードして実行します。
Blade で最適化されたモデルは、引き続き TorchScript モデルです。環境を切り替えることなく、最適化されたモデルをロードできます。
import blade.runtime.torch import torch from torch.testing import assert_allclose import ctypes import os codebase="retinanet-examples" ctypes.cdll.LoadLibrary(os.path.join(codebase, 'plugin.so')) optimized_model = torch.jit.load('optimized.pt') example_inputs = torch.load('example_inputs.pth') with torch.no_grad(): pred = optimized_model(example_inputs)