DeepstreamをJetsonで動かすためのガイド
この記事はPartyのTechチーム、「PY」の2024年アドベントカレンダー向けに作ったものです。
僕の担当回は、ちょっとニッチなのを投下しておきます。
備忘録メモ程度ですが、導入の参考になれば幸いです。
あと、実際に動かす実践編と分けるか迷ったのですが面倒なのでまとめます。長くてごめんなさい。あと内容間違ってたらごめんなさい。
はじめに
Jetson、便利ですよね。いろんな案件でお世話になっています。
先日Jetson Orin Nano Superも発表され、値段が4万程度まで下がったので、今後さらに出番が増えそうです。
そんなJetsonをフルに性能使い切るのに、DeepstreamというNVidiaが提供している便利なフレームワークがあります(もちろん普通にデスクトップでも使えます)。
今回は導入編ということで、どういうノリのものなのか?どんな感じで使えるのか?を紹介します。
何が便利なの?
たとえばこういうシチュエーション。これが4万程度のマシンでできる。
複数の(たとえば8カメラのFullHD入力)ストリームをリアルタイムあつかいたい
それぞれのストリームに対して、複数のDNNモデルを組み合わせて走らせ解析したい
しかも小型、低電力、低価格なJetsonで実現したい
Deepstreamを使うことで、こんな複雑な要件を叶えることができます。
Jetson Orin nanoであればいまや4万円程度で買えるので、60万のGPUマシンは必要ありません。
どうでしょう?魅力的じゃないでしょうか?
Deepstreamとは
どういうノリのものか
個人的な印象でかなり雑に乱暴にいうと、
って感じです。
なので基本はGStreamerです。
映像の入力から前処理、DNNでの解析、結果の処理までのパイプラインを、さまざまなモジュールを組み合わせて構築するためのフレームワークというノリで考えておくとまずはよいのかなと。
モジュールはプラグインという形でいろいろと提供されています(もちろん自作も可能)。
なお中身はC++で、Python bindingsも提供されていますが情報少ないです。
複数ストリームをGPU処理しやすいよう変換し
学習済モデルをTensorRT化してGPU処理しやすく最適化し
複数のモデルを効率よく実行・推論・連携・処理
上記のパイプラインを、ノンコード(設定ファイルのみ)で構築をすることができます。
とりま実行に必要なもの
手前の準備はいろいろと必要ですが、基本的には以下のものがあれば実行できます。
全体のパイプラインの設定ファイル(.txt):ストリームの入力、処理の仕方や学習済モデルのPathや使い方、解析結果をどうするか?など、パイプライン全体の設定を記述
各学習済モデル用の設定ファイル(.txt):各々のモデルに関する設定を記述
使う学習済モデル(.onnx):初回実行時にTensorRT(.engine)に変換されます。ptのものは、事前にonnxに変換しておきましょう
各モデルの後処理を記述した.so:C++で書いてビルドしたオブジェクトファイル(taoから引っ張ってきたモデル使う場合は不要)
実行の際に必要(引数として渡すやつ)なのは、全体のパイプラインの内容を記述した設定ファイルのみです。
おすすめの開発スタイル
初めの一歩の環境構築、ベストプラクティス
結論から行くと、Dockerが一番良いです。
仮想環境作って一からライブラリなどをインストールしていくのももちろん可能ですが、いろいろとはまります(はまりました)
NVidiaが公式で提供しているDockerや学習済モデルは、NGCというサービスにまとまっています。
非常に見にくい、わかりにくい、対応バージョンがカオス。というポータルなのですが、この中のcatalogueの中に、さまざまな公式のリソースが格納されています。(Jetsonというワードではなく、l4tというワードで検索しましょう)
どのDocker使うのがよいか
今回のDeepsteamに関してはこれです。
この中を見ると、
deepstream:7.1-triton-multiarch
deepstream-l4t:7.1-samples
の2種類があるのがわかりますね。
multiarchを選びます。
気軽にsamplesのほうを選ぶと罠があります。
ファインチューニングしたyoloなんかを使う時は、multiarchのほうあるライブラリが必要なのですが、samplesでは省略されています。迂闊にsampleの方でサンプル動作確認して、さぁ開発やでー!とそのままやると色々ライブラリがなくてビルドエラーでぐぬぬ、、、となるわけです。(なった)
(注釈)
ちなみにtritonっていうのは、onnxをTensorRTに変換して走らせる場合結構モデルに制限がでちゃうんですが、それをTensorRTに変換せず、もうちょっと抽象的な形でいろんなモデルを走らせれるようにしたNVidiaのサービスです。具体的には、Triton serverというサーバーでモデルを実行できるので、一個のモデルをいろんなプラットフォーム展開できるっていう便利なやつです。
その分、TensorRTに比べて速度は落ちます。
サンプルを色々試すには
上記のsampleの方は、意外とサンプルが少ないです。(なぜだ)
なので、実際たくさんのモデルを試したいときはmultiarchのコンテナに下記レポジトリをcloneして色々実行してみるのがおすすめです。
Nvidiaが提供しているTaoのモデルを使ったExample集です。
(注釈)
このTaoっていうのは、正式にはTao toolkitというNvidiaが提供してる転移学習用のツールキットです。要はよく使う学習済モデルを用意してくれていて、それを基に自分で転移学習・デプロイをするのを簡単にするためのものですね。いろいろとよく使うモデルがすぐ使える形で公開されてるので、気軽に試すことが可能です。
(補足)Yolo使う場合
Taoにない、Yoloを使うシーンも多いかと思うので補足を。
Yoloを使う場合、推論の最終段の処理(結果をパースするとこですね)をDeepstream用に編集し(具体的にはC++でextern Cとして処理する関数を書く)、その処理する関数自体を.soとしてバイナリ化する必要があります。
なぜかというと、Deepstreamはテキストの設定ファイルで動作を規定するので、最終段のパース処理もバイナリファイル名&関数名として指定するスタイルなのですね(他のやり方もあると思うけど、これが標準ぽいのでまずは従おう)
このあたりを踏まえつつ、ファインチューニングしたYoloを使おうって場合、変換やビルドなど、以下のレポジトリを使うのが一番シンプルでわかりやすくおすすめです。
Yoloのonnx化や、パースする関数のビルドは、とりあえずこのレポジトリの言うことに従いましょう。そうすればうまくいきます。(これもmultiarchのコンテナにしかないライブラリに依存してるので、sampleではビルドこけます)
ググって他の手法も出てきますが、うまくいかないことが多かったです。
何が素敵かって、Yoloのバージョンに応じたやり方が一通り用意されている。
・・・てことで、Yolo使う場合、このDeepstream-yoloをmultiarchのDocker内にCloneしておきましょう。
なお、Yolo11なんかを出してるUltralyticsにも解説記事がアップされていました。ご参考までに
実践編
さて、ここからは実際に動かしながら、どんなものかを確認しましょう。
Jetsonの初期セットアップはできているという前提ですすめます。
なおJetson Nanoでは対応Jetpackが古く、最新のDeepsteamが使用できないのでおすすめではないためここでは省きます。
まずはJetson Xaver以上の、Jetson OrinシリーズなんかのJetpack5.1.2に対応したものについて書いていきます。
Deepstreamは6.3を使います。Yoloも使いたいよってことにして、環境を作っていきます。
Docker pull
docker pull nvcr.io/nvidia/deepstream:6.3-triton-multiarch
multiarchのDockerをPullします
Dockerの起動(カメラ入力なし)
xhost +
docker run -it --rm --net=host --runtime nvidia -e DISPLAY=$DISPLAY -w /opt/nvidia/deepstream/deepstream-6.3 -v /tmp/.X11-unix/:/tmp/.X11-unix nvcr.io/nvidia/deepstream:6.3-triton-multiarch
Dockerの起動(カメラ入力あり)
USBカメラなんかを使いたい場合は、以下のオプションをつけて起動します。複数台の場合は複数記述する必要があります(ほかにいい方法あれば教えてください、6台のときとか面倒くさい)。
(一台の場合)
--device /dev/video0
(二台の場合)
--device /dev/video0 --device /dev/video1
実際の起動
docker run -it --rm --net=host --runtime nvidia -e DISPLAY=$DISPLAY --device /dev/video0 -w /opt/nvidia/deepstream/deepstream-6.3 -v /tmp/.X11-unix/:/tmp/.X11-unix nvcr.io/nvidia/deepstream:6.3-triton-multiarch
(注釈)
カメラが接続されているか?や接続されているカメラの詳細、device0なの?1なの?ってのは、v4l2というライブラリを使うと簡単に確認できます(使い方は検索してね!)
Deepstream関連の追加のライブラリをDL
/opt/nvidia/deepstream/deepstream/user_additional_install.sh
ExampleのClone
cd /opt/nvidia/deepstream/deepstream-6.3/
git clone https://github.com/NVIDIA-AI-IOT/deepstream_tao_apps.git
クローンしたら、モデルをDLします。その他必要なものはDeepstreamのDockerに含まれているはずです。
cd ./deepstream_tao_apps
./download_models.sh
Yolo用にDeepstream-yoloのClone
cd /opt/nvidia/deepstream/deepstream-6.3/
git clone https://github.com/marcoslucianops/DeepStream-Yolo.git
Deepsteram実行の基本
ここまでで環境はおおよそ整いました。
それではDeepstreamがどういったノリのものか、実際に動かしてみましょう。
クローンしたdeepstream_tao_appsの中から、人物検知(PeopleNet)のExampleを起動してみましょう。
deepstreamの起動はいたってシンプルで、
deepstream-app -c xxx.txt
と、引数にテキストファイルを渡すだけとなります(基本的には)。
で、このテキストファイルが、パイプラインの内容を記述してものとなります。
CloneしたExampleでは、以下のパスにこのテキストファイルがあります。
/opt/nvidia/deepstream/deepstream-6.3/deepstream_app_tao_configs/deepstream_app_source1_peoplenet.txt
命名規則は規定されているわけではないですが、
deepstream_app_source[ストリームの数]_[使うメインのモデル]
ってパターンが多そうです。
以下のようにして実行しましょう。(最初はエラーが出るはずです)
deepstream-app -c /opt/nvidia/deepstream/deepstream-6.3/deepstream_app_tao_configs/deepstream_app_source1_peoplenet.txt
パイプラインの内容を書いたtextファイル
さて、このテキストファイルは何か?
中身を見てみましょう。
[application]
enable-perf-measurement=1
perf-measurement-interval-sec=1
[tiled-display]
enable=1
rows=1
columns=1
width=1280
height=720
gpu-id=0
[source0]
enable=1
#Type - 1=CameraV4L2 2=URI 3=MultiURI
type=3
num-sources=1
uri=file:///opt/nvidia/deepstream/deepstream/samples/streams/sample_1080p_h265.mp4
gpu-id=0
[streammux]
gpu-id=0
batch-size=1
batched-push-timeout=40000
## Set muxer output width and height
width=1920
height=1080
[sink0]
enable=1
#Type - 1=FakeSink 2=EglSink 3=File
type=2
sync=1
source-id=0
gpu-id=0
[osd]
enable=1
gpu-id=0
border-width=3
text-size=15
text-color=1;1;1;1;
text-bg-color=0.3;0.3;0.3;1
font=Arial
[primary-gie]
enable=1
#(0): nvinfer; (1): nvinferserver
plugin-type=0
gpu-id=0
# Modify as necessary
batch-size=1
#Required by the app for OSD, not a plugin property
bbox-border-color0=1;0;0;1
bbox-border-color1=0;1;1;1
bbox-border-color2=0;0;1;1
bbox-border-color3=0;1;0;1
gie-unique-id=1
config-file=nvinfer/config_infer_primary_peoplenet.txt
#config-file=triton/config_infer_primary_peoplenet.txt
#config-file=triton-grpc/config_infer_primary_peoplenet.txt
[sink1]
enable=0
type=3
#1=mp4 2=mkv
container=1
#1=h264 2=h265 3=mpeg4
codec=1
#encoder type 0=Hardware 1=Software
enc-type=0
sync=0
bitrate=2000000
#H264 Profile - 0=Baseline 2=Main 4=High
#H265 Profile - 0=Main 1=Main10
profile=0
output-file=out.mp4
source-id=0
[sink2]
enable=0
#Type - 1=FakeSink 2=EglSink 3=File 4=RTSPStreaming 5=Overlay
type=4
#1=h264 2=h265
codec=1
#encoder type 0=Hardware 1=Software
enc-type=0
sync=0
bitrate=4000000
#H264 Profile - 0=Baseline 2=Main 4=High
#H265 Profile - 0=Main 1=Main10
profile=0
# set below properties in case of RTSPStreaming
rtsp-port=8554
udp-port=5400
[tracker]
enable=1
# For NvDCF and DeepSORT tracker, tracker-width and tracker-height must be a multiple of 32, respectively
tracker-width=640
tracker-height=384
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
# ll-config-file required to set different tracker types
# ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_IOU.yml
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
# ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_accuracy.yml
# ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_DeepSORT.yml
gpu-id=0
enable-batch-process=1
enable-past-frame=1
display-tracking-id=1
[tests]
file-loop=0
ここには、処理に使用するプラグインたちの設定を書いていきます。
長いですが構造は非常にシンプルです。
[plugin name]
settings
ってかんじで、上からプラグインをつなげていくっていう記述になっています。
プラグインとは機能のモジュールで、NVidiaが独自に提供しているものや、GStreamerのものなど、さまざまな機能が提供されています。
そして、このプラグインを順番にならべて処理をつないでいくことで、全体のストリームの処理を実現していきます。ここも非常にわかりやすいかと思います。
(注釈)上記の設定ファイルに記述されているファイルパスが、おそらく現在のものと異なっているはずです。ここを修正しておきましょう。
例えば以下の場所ですね。
[tracker]
...
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
...
それぞれのパラメータなどの詳細は、ここを読み解くことでだいぶ内容がわかると思います。
まぁChatGPTなんかでも情報出てきますので、そっちのが楽かもしれませんね。
よく使いそうなとこだけ紹介すると、
[source0]
映像ソースの選択。ここのtypeを変えると、Webカムだったり、mp4だったり、いろんなソースを選べます。
[primary-gie]
このなんちゃらgieってとこに、使うモデルに関する記述をしていきます。
各学習済モデル用の設定ファイルのパスを指定します。今見ている例だと、
deepstream_app_tao_configs/nvinfer/config_infer_primary_peoplenet.txt
ですね。
中身を見てみましょう。
[property]
gpu-id=0
net-scale-factor=0.0039215697906911373
#tlt-model-key=tlt_encode
onnx-file=../../../models/tao_pretrained_models/peopleNet/resnet34_peoplenet_int8.onnx
labelfile-path=../labels_peoplenet.txt
model-engine-file=../../../models/tao_pretrained_models/peopleNet/resnet34_peoplenet_int8.onnx_b1_gpu0_int8.engine
int8-calib-file=../../../models/tao_pretrained_models/peopleNet/resnet34_peoplenet_int8.txt
input-dims=3;544;960;0
uff-input-blob-name=input_1:0
batch-size=1
process-mode=1
model-color-format=0
## 0=FP32, 1=INT8, 2=FP16 mode
network-mode=1
num-detected-classes=3
cluster-mode=2
interval=0
gie-unique-id=1
output-blob-names=output_bbox/BiasAdd:0;output_cov/Sigmoid:0
#Use the config params below for dbscan clustering mode
#[class-attrs-all]
#detected-min-w=4
#detected-min-h=4
#minBoxes=3
#eps=0.7
#Use the config params below for NMS clustering mode
[class-attrs-all]
topk=20
nms-iou-threshold=0.5
pre-cluster-threshold=0.2
## Per class configurations
[class-attrs-0]
topk=20
nms-iou-threshold=0.5
pre-cluster-threshold=0.4
#[class-attrs-1]
#pre-cluster-threshold=0.05
#eps=0.7
#dbscan-min-score=0.5
学習済モデル(onnx)のパスや、設定などを記述します。
書き方がむずかしいのですが(そもそもどこにドキュメントがあるかわかりにくい)、taoのモデルを使用する場合は、この辺に関しても記述があったりします。参考にしてみてください。
[secondary-gie0]
primary, secondary,…みたいな形でどんどんモデルを追加できます。そして、この段階のモデルがどのモデルの後段の処理か?みたいなのも設定できます。
たとえば、primaryで車体を検出し、その車体の中のナンバープレートをsecondaryで検出。さらにOCRで車番解析。みたいなことができるわけですね。
なお、gie-unique-idというやつはモデルごとにユニークなものをちゃんと設定しましょう。
ここまでで、おおよそDeepstreamの基本はおさえられたかなと思います
YOLO使う場合
Yolov4のExampleが格納されているので見てみましょう。
[property]
gpu-id=0
net-scale-factor=1.0
offsets=103.939;116.779;123.68
model-color-format=1
labelfile-path=../yolov4_labels.txt
model-engine-file=../../../models/tao_pretrained_models/yolov4/yolov4_resnet18_epoch_080.onnx_b1_gpu0_int8.engine
##For Jetson platform, change the calibration file to cal_trt10_jetson.bin
int8-calib-file=../../../models/tao_pretrained_models/yolov4/cal_trt10_x86.bin
onnx-file=../../../models/tao_pretrained_models/yolov4/yolov4_resnet18_epoch_080.onnx
infer-dims=3;544;960
maintain-aspect-ratio=1
uff-input-order=0
uff-input-blob-name=Input
batch-size=1
## 0=FP32, 1=INT8, 2=FP16 mode
network-mode=1
num-detected-classes=4
interval=0
gie-unique-id=1
is-classifier=0
#network-type=0
cluster-mode=3
output-blob-names=BatchedNMS
parse-bbox-func-name=NvDsInferParseCustomBatchedNMSTLT
custom-lib-path=/opt/nvidia/deepstream/deepstream/lib/libnvds_infercustomparser.so
[class-attrs-all]
pre-cluster-threshold=0.3
roi-top-offset=0
roi-bottom-offset=0
detected-min-w=0
detected-min-h=0
detected-max-w=0
detected-max-h=0
[class-attrs-1]
nms-iou-threshold=0.9
大事なとこだけピックアップ
model-engine-file=../../../models/tao_pretrained_models/yolov4/yolov4_resnet18_epoch_080.onnx_b1_gpu0_int8.engine
onnx-file=../../../models/tao_pretrained_models/yolov4/yolov4_resnet18_epoch_080.onnx
model-engine-file
Deepstreamにおいて、モデルは自動的にTensorRT(.engine)に変換されます。(なので初回走らせるときはめちゃ時間かかる)
なので、ここに指定したファイル名、パスに生成されて、生成されたあとは自動的にこのファイルを使用してくれます。
onnx-file
ここには、自分で用意した(もしくはtaoから引っ張ってきた)学習済モデルのパスを指定します
labelfile-path=labels.txt
...
num-detected-classes=4
これはラベルと検出するクラスの数の設定ですね。
さて、ここがYoloとかで大事な部分です。
parse-bbox-func-name=NvDsInferParseCustomBatchedNMSTLT
custom-lib-path=/opt/nvidia/deepstream/deepstream/lib/libnvds_infercustomparser.so
各モデルの後処理を記述します
この場合、libnvds_infercustomparser.soっていうオブジェクトに記載されているNvDsInferParseCustomBatchedNMSTLTって言う関数を実行してね。
ってことになります。
この関数の実装内容はどこにあるかというと、
post_processor/nvdsinfer_custombboxparser_tao.cpp
に記載があります。
関数を見てみましょう。
extern "C"
bool NvDsInferParseCustomBatchedNMSTLT (
std::vector<NvDsInferLayerInfo> const &outputLayersInfo,
NvDsInferNetworkInfo const &networkInfo,
NvDsInferParseDetectionParams const &detectionParams,
std::vector<NvDsInferObjectDetectionInfo> &objectList);
とあり、NvDsInferParseCustomBatchedNMSTLT はこの部分に該当します。なお、extern “C”はよく使うので調べて覚えておきましょう。
つまり、このCPPで書かれたバウンディングボックスの処理を.soとしてビルドして、それを使う!と言うイメージですね。
そして、このCPPをビルドして、.soとして使用します。
その際、multiarchのコンテナじゃないとビルドするのに必要なライブラリがなくてビルド通らないので注意してください。
YOLOのバージョンによっても処理が違いますが、前述のdeepstream-yoloのレポジトリを参考にしてください。
その他
実行時に、.engeneへの変換でうまくいかないことが結構あるので、そのときはログを見て、エラーをGPTと一緒に解決すると案外なんとかなります。
あと、メモリがそもそも足りなくて、、、って言うエラーが出ることがありますが、実際はメモリ不足じゃなくて環境や変換でミスってることが大多数なので他の状態を見直すと良いです。
設定ファイルのbatchサイズとかもメモリ対策には結構効きますし、フォーラムでこれ関連の議論も出ているので参考にすると良いかと思います。
最後に
やたら長い記事になってしまった、、、
note向きじゃないですね、、、とか思いつつ、気が向いたら応用というか、実際のプロジェクトでの使い方とかも紹介していきたいと思います。