Darknet/YOLOのJetson最適化

f:id:tanbalabs:20200507220500j:plain

Mixed Precision学習結果

 

ソースコードこちら

表題の通り、Darknet/YOLOv3をJetson Nanoに最適化しました。

本最適化により、Jetson Nanoでより少ないメモリ利用で学習・検出ができるようになりました。
すなわち、通常Darknet/YOLOv3に対して、より大きい解像度で学習・検出ができます。
学習は二つのクラス 320x320で11日程(416x416で14.5日)でとても遅いけど、Jetson Nanoしか持っていない方は自分のデータで学習ができます。

Jetson Nanoでしか動作確認をしていないが、TX1/2等のMaxwellアーキテクチャを実装するJetsonでは動作すると思います。
Xavierは一部のコード(AtomicAdd)で正しくアーキテクチャの切替ができるのか分からないです。
一方、通常のNvidia GPUカードでは未対応です(現状、JetsonのUnified Memoryに特化)。

最適化内容は下記二つです:
・JetsonのUnified Memoryを利用 
・FP32/16混合学習(Mixed Precision) (論文実装実装NVidia)

 

状況:
・テストは最低限しか行っていないです。
・対応層:convolutional, route, shortcut, upsample, yolo, cost.通常YOLOv3しか動作しないです。v4, v2やTinyは動作しないです。

 

cfg設定ファイルへのMixed Precision追加項目([net]部分のみ)は以下です。説明は設定項目名の通りです。詳細は後述のアルゴリズムを参考のこと。

 

追加項目名 デフォルト値 備考
max_loss_scale 16384.0  
min_loss_scale .0009765625  
initial_loss_scale 1.0  
loss_scale_inc_iterations 1000 burn_in以降に利用


コンパイル
Makefileに二つ項目を追加しました:
* JETMEM: JetsonのUnified Memoryを利用の際、1にします。
* CUDNN_MIX: FP16/Mixed Precisionを利用の際、1にします。
現状、JETMEM=1, CUDNN_MIX=1でしか動作確認していないです。。
また、Jetson用のARCHのコメントも外します。(compute_53を利用)


検出
既存の重み、または独自データで学習した重みが利用できます。
使い方はこちらこちら

学習
学習の実験はCOCOデータセットの猫と犬のみで行いました。
YOLOv3用のCOCOデータセットの取得はこちらに従ってから行いましたが、二つのクラスのみを使うために新規設定ファイルを作成しました:

data/coco/coco_cat_dog.names 猫と犬のみのクラス名
cfg/coco_cat_dog.data 学習および評価ファイルリストを更新(下記二つ)。
data/coco/trainval_cat_dog.txt 学習用ファイルリスト
data/coco/val_cat_dog.txt 評価用ファイルリスト
cfg/yolov3_cat_dog*.cfg 二つのクラスのみを利用するためにフィルタ数対応

また、新しいラベルに対応するファイルも必要です。以下を行います。(注意:上記の通りに取得したCOCOデータセットを上書きします。)

  tar xvf coco_cat_dog_labels.tar.gz

学習例は以下です:
./darknet detector train cfg/coco_cat_dog.data cfg/yolov3_cat_dog.cfg weights/darknet53.conv.74


最適化実装の詳細:


Unified Memory
Darknetは、CPUとGPUに同じデータを保存します。しかし、JetsonのDRAMは両CPU/GPUで共有されるため、同じデータを2回確保する必要はないです。
よって、共有することにより、より少ないメモリで動作が可能になり、より大きい解像度で検出が可能です。

 

FP32/16混合学習(Mixed Precision)
Mixed Precisionは学習の際にNVidia GPUのTensorCore(Turingアーキテクチャ以降)を利用して高速化を達成するために開発されました。FP32に対し、FP16での畳込み演算のみが8倍速くなります。FP16を用いることにより、利用するメモリ量も減ります。
(重み更新用の配列等はFP32/16の両方を保持する必要があるため増えますが、全体は減ります)
Jetson NanoはTensorCoreを実現しないため学習は遅くなりますが(後述)、より少ないメモリ(より大きい解像度)の学習が可能になります。

 演算部の実装はこちらのTensorFlow部分を元に行いました。

論文のように、FP32⇔16変換を行う際、FP16はより少ない数値幅のためloss scaleで乗算/除算を行いますがこれの調整が必要になります。
更新案はこちらこちらにあり、基本は重みを更新するweight_updatesがNaNまたは無限になった場合にloss scaleを半分にし、
NaN/無限が 1000または2000回生じなかった場合に倍にしています。
ここでは倍にする手順は上記と同じにしていますが、YOLOv3の特徴によりburn_in(YOLOv3のcfgファイルでは1000に設定されています)回までは最大の
weight_updatesにより更新し、burn_in後は従来の通りにしています。理由や詳細は次の解析の通りです。

 

f:id:tanbalabs:20200507222448p:plain

上記図は各回毎の、全て畳込層のweight_updatesの最大絶対値グラフの形よびロスのFP32例です。
パラメータ調整や、二回目の学習だと別結果になりますが、雰囲気は同じです。
最初はすぐ大きくなります。この例だと、最大が165,055です。この時点でFP16の最大値(65,504)を超えています。
よって、この時点でloss scaleの適切な値は0.25です。その後、徐々に下がり、数百回後に2^10程下がります。
これにより、1000回に一回倍にするとFP32⇔16では精度を失う可能性があるため、より早く更新するために今回および今回までのweight_updatesの最大絶対値に基づいて
burn_inまで更新するようにしました。


また、下記が層毎のweight_updatesです。(最大絶対値が大きい場合)
ここでは500回目程度まで、一番大きい絶対値は層81、93、105です。これらはlossが計算されるyolo_layerの直前の畳み込み層です。
500回目以降になると、最も大きい絶対値は最初の方の層になります。

f:id:tanbalabs:20200507222708p:plain

 

大きさ変更による水増し
Darknet/YOLOv3のデータ水増しとして、色調整や、大きさ変更(Resize)、横反転、切り出しがあります。
Resizeは10回毎に、ランダムに1/1.4~1.4倍に、width/heightが32の倍数で調整されまる。
320x320の場合、256x256から480x480に変更されますが、最初はメモリを最大で初期化するため、必ず最大の480x480になります。


メモリ使用量評価方法

Tegraでは下記方法があります:
* tegrastats コマンド(メモリ使用量を指定周期で指定ファイルに出力可能)
* jtop コマンド(tegrastatsと同じ情報であるが、ファイルに出力できない)
* cudaMemGetInfo (c ファイルに埋め込み)
* /usr/bin/time -v (Maximum resident set sizeを参考)

ここではファイルに指定周期で出力可能なtegrastatsを使うことにしました。
なお、cudaMemGetInfo, /usr/bin/time, tegrastatsは別数値を出力します。


Nano上での実験結果
以下が学習中のメモリ使用量です。
メモリ使用量はtegrastatsによる RAM + SWAPであり、他のプロセスのメモリ使用量も含まれています。
上記のように、Darknet/YOLOv3の最大メモリ使用量はResizeが最大の場合であり、最初の10回はこのResizeに設定されます。
よって、最初の10回のメモリ使用量が最大になること考えられるけど、学習中、他のプロセスのため全体のメモリ使用量はさらに増えます。
このため、Darknetは下記二つの理由により強制終了(Killed)される可能性があります:
1) 最初のメモリ確保の場合
2) 学習中、他のプロセスによるメモリ増加のため

例えば以下が320x320のメモリ使用量です。
FP32の方が最初に完了します。最初の10回のメモリ使用量(RAM + SWAP)は 3800MB(FP32)および3200MB(Mixed Precision)です。
しかし、学習中、メモリ使用量はさらに増えますが、解析が必要です。

f:id:tanbalabs:20200507222952p:plain

 

なお、上記のNanoの設定として、SWAPがデフォルトの2GBと、ターミナルモードです。

以下が学習一覧です。

 

設定 元Darknet/YOLOv3 本最適化
yolov3 320x320 OK OK
yolov3 416x416 100回程度で強制終了 OK
yolov3 544x544 初期に強制終了 300回程度で強制終了
yolov3 tiny 解像度依存 未実装
yolov2 解像度依存 未実装

 

実行時間解析

320x320設定で、Resize 480x480の一回の学習にかかる時間は以下です。

  FP32 : 186 秒

  Mix : 272 秒 

以下の情報は次のコマンドのログからまとめています。

  nvprof --print-gpu-trace --log-file log.txt ./darknet detector train ...

 上記で、学習を3回繰り返し、二回目の結果を分析しました。

 FP32の場合、GPUの合計時間は、上記186秒とほぼ同じです、

 しかし、Mixed Precisionでは、GPU時間が232秒であり、上記272秒より40秒少ないです。

 一方、私の解析が正しければ、下記のようにMixとFP32で最も大きい差分は畳み込み演算です(Mixが47秒遅い)。よって、畳み込み演算が大きな差分でありますが、さらに40秒の解析が必要です。

 

f:id:tanbalabs:20200507224446p:plain

mAP@0.50%結果(猫、犬のみ)

w x h モード 最良mAP
320x320 2クラス (FP32) 66.4%
320x320 2 クラス (Mixed Precision) 68.0%
416x416 2 クラス (Mixed Precision) 73.3%

ここでは5000回学習し、最も良いmAPの重みになります(100回毎に重みを保存)。
mAPは同じ設定でも、学習毎にかわります。
よって、上記結果はMixed Precision方式がFP32より良いと限りません。