Pytorchなら同じGPUで2倍早い学習ができるっていったんだよ!

Pytorchなら同じGPUで2倍早い学習ができるっていったんだよ!

entry_img_11675.jpg

DNNを学習・推論するにはGPUはほぼ必須ですが高いですよね。。

できるだけ安いGPUでも早く学習が終わると助かりますが近年のDNNは巨大化する一方。。

そこでFP16などの技術で高速化すると同じGPUでも2倍早く学習が終わります!

このような技術を使うことで2倍性能のいいGPUを使うのと同義になるのでお財布に優しいですね。。もちろん仕事で使う場合はコストが下がるのでありがたいです。

できらぁ!と言ってしまったので頑張っていきましょう。
(追記:実験では主にRTX2080tiを使用しました。)

tldr;

Pytorchのモデル、tensorをFP16化することで学習と推論を高速化。
またnvidia社のapexライブラリでもかんたんにFP16化。
推論高速化ツールのTensorRTも試し、推論を高速化。

CIFAR10と物体検出でベンチマーク。
- 画像認識では2倍高速化できました。
- 推論に限ればTensorRTでは4~10倍くらい高速化できました。しかしデバッグ大変です。
- 物体検出は高速化できませんでしたが、メモリ使用量を半分にできるのはありがたいです。

FP16ってなんだっけ

昔の記事でも触れましたが、FP16の復習です。

ほぼコピペしちゃいますが。。

コンピュータ内の数字の表現

FP16とは俗にいう”半精度”と呼ばれる浮動小数点における数字の表現方法です。
コンピュータの内部では数字は2進数で保存されており、例えば"3"という数字は:

0011 # 0*8+0*4+1*2+1*1 = 3

といった形で保存されています。

これは一般的な4bitの整数表現(4bit Int)という形ですね。
このビット数を増やしていくと表現できる整数が広がっていきます。

floatって?

それでは負や小数などを表現するためにどうしたら良いのでしょうか。

一般的に使われるのが浮動小数点(float)と呼ばれる方式です。

例えばオレオレ7bit表現で上位3bitが小数(exponent)、下位4bitが整数(mantissa)のような表現を考えてみましょう。

011+0011 # 1e-3*(0*8+0*4+1*2+1*1) = 0.003

と小数を表現できました。この6bit表現でも1e-7~0.7まで表現できダイナミックレンジはとても広いです。

DNNの学習では小さい微分結果をネットワークに伝搬させる必要があるためfloatの対応は必須です。推論ではそこそこ整数表現だけでも上手くいきます。

最後にMSBに正負を見分けるビットをつければ-1e-7~0.7まで表現できる8bitなんちゃってfloatの出来上がりです。

1(正)+011(小数)+0011(正数) # +1e-3*(0*8+0*4+1*2+1*1) = 0.003

fp16やbfloat16って?

最初の話に戻りますが5bの小数+10bitの整数を備えた表現方法がFP16です。

Half-precision IEEE floating pointとも言われ、ちゃんと規格化されています。

https___qiita-image-store.s3.amazonaws.com_0_171915_891e685c-48f6-4afb-9839-1074706d8f73.jpg

一方でDNNの学習にはExponentは重要なものの、整数の表現幅はあまり寄与しません。

そこでgoogleなどはbfloat16というexpoentを3bit増やした表現をTPUに取り入れております。

従来のFP16よりも安定して(FP32に近い挙動で)学習が行われるらしいです。

nVidiaGPUにも対応が待たれますね。。

FP16学習はDNNにどう関係するの?

1) メモリの節約
まずDNN学習時はGPUメモリ上にactivationやweightを保存し無くてはなりません。
FP32方式でそれらのパラメータを保存するよりも、FP16で保存することで必要なメモリ量を半分にへらすことが出来ます。

2) 演算の高速化
次世代GPUはFP16を使うと演算速度が大幅に向上するTensorCoreが搭載されてます(専用回路みたいなものです)。
そのためFP16で学習することでFP32時に対し数十倍の演算速度向上が期待できます!

ただDNN動作は普通はメモリ律速(データの移動が一番時間を食う)のでTensorCoreによる速度向上の恩恵が見られるのはほとんどないと思います。FP16化でメモリ量が半分になり、データ通信時間も半分になることで学習速度が2倍早くなるのが期待値だと思って良いと思います。

https___qiita-image-store.s3.amazonaws.com_0_171915_0bc68b99-aefd-c039-1eb8-99bb6561d16e.jpg

Pytorchの学習でFP16を使う

本題のFP16を使ったPytorch学習に入りましょう。

  • 追記 2020/11月

下記の方法は古く、現在はPytorch>=1.6で低精度学習の方法は変わってます。

https://pytorch.org/docs/stable/notes/amp_examples.html

を見て下さい。

1)入力、重みテンソルを手動でFP16化

1つ目のアプローチは手動でネットワークに流れるテンソルをFP16化(.half())にすることです。

pytorchでは作成したtensorはデフォルトでは32FP(float)だが、.half()と渡すことでかんたんにFP16化することができます。

hoge = torch.randn([100,100])
hoge.type() #torch.FloatTensor
hoge_fp16 = hoge.half() # FP16化
hoge_fp16().type() # torch.HalfTensor

FastAIのrepoを参考にネットワークをhalf化する関数を作ります。

class tofp16(nn.Module):
    def __init__(self):
        super(tofp16, self).__init__()

    def forward(self, input):
        return input.half()

def copy_in_params(net, params):
    net_params = list(net.parameters())
    for i in range(len(params)):
        net_params[i].data.copy_(params[i].data)

def set_grad(params, params_with_grad):

    for param, param_w_grad in zip(params, params_with_grad):
        if param.grad is None:
            param.grad = torch.nn.Parameter(param.data.new().resize_(*param.data.size()))
        param.grad.data.copy_(param_w_grad.grad.data)

def BN_convert_float(module):
# BatchNormのみFP32フォーマットにしないと性能が出ない。
# BatchNormレイヤを検索し、このレイヤのみFP32に設定。
    '''
    BatchNorm layers to have parameters in single precision.
    Find all layers and convert them back to float. This can't
    be done with built in .apply as that function will apply
    fn to all modules, parameters, and buffers. Thus we wouldn't
    be able to guard the float conversion based on the module type.
    '''
    if isinstance(module, torch.nn.modules.batchnorm._BatchNorm):
        module.float()
    for child in module.children():
        BN_convert_float(child)
    return module


def network_to_half(network):
    return nn.Sequential(tofp16(), BN_convert_float(network.half()))

とnetwork_to_halfのような関数が作れる。

学習の安定化のため、batchnormだけは32bitで行うのが味噌です。

2)ライブラリ(nvidia apex, amp)を使ったFP16化

nvidiaがpytorchで利用できるFP16用ライブラリのapexを使用するのが流行っているらしい。

https://github.com/NVIDIA/apex

インストール方法

gitcloneしてインストールするかpipでインストールできます。

$ git clone https://github.com/NVIDIA/apex
$ cd apex
$ pip install -v --no-cache-dir --global-option="--cpp_ext" --global-option="--cuda_ext" ./

or

pip install -v --no-cache-dir ./

CUDAとcppを使用したインストールの方が使用できる機能が多く、高速化が期待できるようです。

単にFP16化を試してみたいのならばpipで良いと思います。

使用方法

このライブラリは既存モデルに3行追加するだけでFP16化することができます。

基本的にはmodelとoptimizerを学習前にampでwrapします。

from apex import amp, optimizers
# Initialization
opt_level = 'O1'
model, optimizer = amp.initialize(model, optimizer, opt_level=opt_level)

そして学習時の勾配計算時に、

# Train your model
with amp.scale_loss(loss, optimizer) as scaled_loss:
    scaled_loss.backward()

と記述を足すだけでオッケーです!

ね、かんたんでしょ?

opt_level="O1"としておくと、batch_normもFP32に設定してくれます。

手動で設定したい場合は、

model, optimizer = amp.initialize(model, optimizer,
                                      opt_level=args.opt_level,
                                    keep_batchnorm_fp32=True)

と渡せばいいみたいです。

example.pyにImagenetの例があるので、それをみて感覚をつかめばいいかなと思います。

おまけ)TensorRTを使用したJetson Nanoの推論高速化

ちょっと毛色の違う話ですが、推論の高速化についても触れます。

この実験で使ったデバイスはJetsonNanoです。一万円で買えるデバイスにも関わらずPytorchが走ります。特にメモリ周りは弱いのでFP16化によって大きな速度向上が生まれます。

CIFAR10データを推論するResnet18の推論速度はFP16化により4倍高速化できました。

https://github.com/kentaroy47/pytorch-onnx-tensorrt-CIFAR10

またPytorchとはちょっと違うのですが、TensorRTというnvidia製のコンパイルツールを使うことでデバイスにフィットしたバイナリを作成し高速化できます。

ただpytorchモデルをONNX形式にエキスポートし、TensorRTで読むというツールチェインになるので複雑なモデルでコンパイルを通すのは至難の業です。ONNXはエラーの温床で趣味でやることではないです。。

FP16化は圧倒的に少ない労力でできるため、まずはそちらを試すのを勧めます。
SSDをTensorRT化しようかと思いましたが、挫折しました.

ONNX→TensorRT化はかなりキツイため、個人で試したいならばtorch2trtというコンバータを使うことをおすすめします。画像処理系モデルならサンプルを見ながらモデルを組めばコンパイル通せます(ちょっと違う事しようとするとハマりますが..)。
このツールはONNXを介さないため、精神衛生上良いです :)

https://github.com/NVIDIA-AI-IOT/torch2trt

Resnet18 inference @JetsonNano

model type FPS
FP32 pytorch 314
FP16 pytorch 1208 (4x)
FP16 TensorRT 4716 (16x)

TensorRT、生のPytorch推論と比べ16倍も早くなるため速度的には圧倒的ですね。たまたま3x3convの多いresnet18がスイートスポットだった気もします。

(追記)
Xavierを使った画像認識モデルとセグメンテーションモデルのINT8量子化推論もできました。最大で10倍ほど高速化することができました。

https://github.com/kentaroy47/benchmark-FP32-FP16-INT8-with-TensorRT
にハードウェアセットアップ方法や結果をまとめています。

高速化実験

CIFAR10(画像認識モデル)

CIFAR10の学習高速化は前記事でやりましたが結果がわかりやすいので再掲。

https://github.com/kentaroy47/pytorch-cifar10-fp16

1)の入力、重みテンソルを手動でFP16化でやってみました

Model second/epoch speedup?
Resnet18@FP32 15 -
Resnet18@FP16 8 87%
Resnet50@FP32 58 -
Resnet50@FP16 27 114%

約2倍高速化しました。CIFAR10ではvalidation精度もほとんど変わらなかったのでデメリットも見えません。

画像認識は素直に高速化できるのでオススメです。

EfficientDetとRetinaNet(物体認識モデル)

ちょうど趣味で学習しているEfficientDetがクソ遅いのでFP16化して高速化してみましょう。

https://github.com/kentaroy47/efficientdet.pytorch

1)の入力、重みテンソルを手動でFP16化でやってみました

batchは32 imagesで実験。

物体検出はロス計算が煩雑なのでFP16化はメンドイですががんばります。。

Input resolution:300

Model second/10 iter メモリ使用量 speedup?
EfficientDetD0@FP32 3.2 7200MB -
EfficientDetD0@FP16 6.5 4511MB -100%..
RetinaNet(Resnet18)@FP32 1.9 5000MB -
RetinaNet(Resnet18)@FP16 2.3 2500MB -18%

FP16にすると逆に遅くなるという結果になりました(爆)

メモリ使用量は半分になっているのでその点は利点ありますね。

EfficientDetは変なレイヤ(DepthWiseConv)など多く、Resnet系よりもGPU効率が低いのがあるのではないかと思っています。ResnetバックボーンのRetinaNetではFP16にしても速度はあまり変わらなかったです。

2)のapexでFP16化をやってみました

Input resolution:300

Model second/10 iter メモリ使用量 speedup?
EfficientDetD0@FP32 3.2 7200MB -
EfficientDetD0@FP16 3.6 4615MB -10%
RetinaNet(Resnet18)@FP32 1.9 5000MB -
RetinaNet(Resnet18)@FP16 2.1 2427MB -18%

ライブラリがしっかりしているのでFP16化時の速度もかなり良くなりました。

ただマシにはなりましたが速度改善はあまり得られず。。

Input resolution:512

Model second/10 iter メモリ使用量 speedup?
EfficientDetD0@FP32 6.2 10000MB*2 -
EfficientDetD0@FP16 5.5 11200MB 12%
RetinaNet(Resnet18)@FP32 1.9 5000MB -
RetinaNet(Resnet18)@FP16 2.1 2427MB -18%

お、入力解像度を増やすと速度改善が見られました!(10%ですけど。。)

FP32ではメモリが入り切らなかったため、バッチ数を減らしておりそれが影響したのかも。

肝心の精度どうなん?というところはしばしお待ちを..

え、Pytorchで同じGPUで2倍早い学習を?(まとめ)

news4vip_1524831822_101.jpg

  • PytorchのFP16化は手軽でリターンも大きい。メモリが入り切らないときはみんなやろう! :)
  • 画像認識モデルでは学習速度を手軽に2倍高速化できる。メモリ使用量も半減。
  • 物体検出モデルは速度改善できませんでした :(。許してヒヤシンス。でもメモリ使用量を半減できるのは大きなメリット。
  • TensorRTはめちゃはや。10倍くらいまで早くなるかも。でもデバッグ大変なので自作モデルを手軽に高速化する用途には向かず、リソースがかけられる業務用だと思う。
  • PytorchカレンダーだからPytorch押しだけどTensorFlowでも同じような事はできる(笑)

物体検出で高速化のメリットが小さい原因究明には時間がかかりそうです。

Issueにも上がっている通りnvidia GPU、apexにおける物体検出における高速化はまだon goingなのかもしれません。

arutema47
Ph.D hardware researcher、Kaggle Master https://aru47.hatenablog.com/ ほぼQiitaに書くことはないのでブログ見て下さい~;)
https://aru47.hatenablog.com/
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
    この記事は以下の記事からリンクされています
    コメント

    apexでも、ResNet50はともかく、複雑な構成だと厳しいのですね。(勉強になりました。)
    https://github.com/NVIDIA/apex/issues/441#issuecomment-522368712

    ちなみに、TensorRTが早い理由ってどこかに書いてありますでしょうか?

    @sakaia
    こういうのは試してみないとわからないですよね。。笑

    TensorRTを業務で使っているわけではないのであまり詳しくないですが。。
    デバイスに応じて(メモリ量やキャッシュ量)最適なコンピュテーショングラフを生成するため高速化しているのではないかと思ってます。PytorchはCuDNNである程度最適化されたグラフを使っているのですがそれは多分FP32が念頭でそこまで最適化されてないはず。。
    TensorRTはFP16を念頭にグラフ最適化してるのがデカイのではないかと予想してるのですが専門家の意見を聞きたいですね。。

    NVIDIAの中の人です。ApexにTensorRTまで試していただき、ありがとうございます。

    TensorRTを適用すると高速になる背景は以下のセッションが (ちょっと古いですが) 参考になると思います。
    http://on-demand-gtc.gputechconf.com/gtc-quicklink/X26Ux
    ざっくりまとめると

    • まとめて一気に計算できる処理を一括して処理 (e.g., 連続するConv-bias-reluをひとつのCUDAカーネルにする)
    • デバイスごとに最適化されたCUDAカーネルを使う
    • メモリ管理の効率化 (e.g., 再利用可能なものは再利用する)
    • 量子化 (FP16 / INT8)

    などによって高速化が図られています。
    どれがどの程度、というのは状況次第なので、一概に言えない部分ではありますが。。。
    また、いわゆるpruningのような処理は (少なくとも現時点では) 行われていないはずです。

    ちなみにFP16での学習については、認識されている通り一部をFP32にする必要がある等々があるので、ちょっと大変です。
    Apexに実装されているAMPは、上記の「等々」を裏側で有効にしてくれるので、比較的楽にモデル自体の性能を保ったまま学習ができるので、最近推している、という背景があったりします。

    大変参考になりました。便乗質問させてください。
    TensorRTの性能向上の割合って、単にAMP(というかNVIDIA/Apex)使ったより上の気がしています。
    TensorRTの機能は、AMPへ移植されつつあると理解してよいのでしょうか?

    @sakaia さん、
    AMPとTensorRTの関係ですが、基本的には全く別のもの、とお考えください。

    • AMP: Tensorコアを簡単に使うための機能群
    • TensorRT: 推論用最適化兼ランタイムエンジン

    AMPで施される処理は主に混合精度演算適用 (~=Tensorコア利用)、ロススケーリング適用、FP32重み更新の3つです。
    これらは、TensorRTで実行される処理とは異なります。
    FP16についてのみ (ざっくり) 言うと、AMPでは各レイヤーや演算をFP16化する/しない、を判断してFP32の関数を呼ぶか、FP16の関数を呼ぶか、ということをしているに過ぎません。

    一方TensorRTでは、前述の通り、複数のレイヤーや演算を一つにまとめるような、ドラスティックな処理を行っていたりします。
    こうした処理はAMPにはないもので、どちらかと言うとcuDNNにあるfused operationsが近いものです。
    https://docs.nvidia.com/deeplearning/sdk/cudnn-api/index.html#cudnnFusedOps_t

    今後、AMPへTensorRTで行われている種類の最適化が統合される可能性は否定できませんが、現時点では上記の状況で、それぞれ全く別のものと理解していただくのが適切と思います。

    (編集済み)

    @lazykyama
    中の人から補足いただいて有り難いです!
    学習の記事なのに推論専用のTensorRTについて挟んでしまったので混乱しやすくなってしまいました。。

    http://on-demand.gputechconf.com/gtcdc/2017/presentation/dc7172-shashank-prasanna-deep-learning-deployment-with-nvidia-tensorrt.pdf
    image.png
    この図が端的にTensorRTの強力なグラフ最適化を指していて良いなと思いました。

    量子化 (FP16 / INT8)

    個人的には更に数倍早くなりそうなのでINT8化に興味があります。
    昔手動でネットワークのINT8化など書いたことはあったのですが、物体検出くらい複雑なモデルだと手動でやるのは厳しいですね。。手軽にINT8化できるライブラリやラッパーなどありましたら教えて頂けると幸いです。

    FixstarsのブログでInt8+TensorRTをやっている方もいました。
    https://proc-cpuinfo.fixstars.com/2019/09/tensorrt-pspnet/
    rangeなどを手動で設定する必要があるみたいですね。。

    (編集済み)

    @arutema47 さん、

    手軽にINT8化できるライブラリやラッパーなどありましたら教えて頂けると幸いです。

    INT8化については大前提として、以下のセッションで説明されているようなポイントが、重要かと思います。
    http://on-demand-gtc.gputechconf.com/gtc-quicklink/9oZFh2N
    (学習しながら量子化、などの場合は別と思いますが)

    その上で、TensorRTでINT8対応する場合はFixstarsさんの記事にも書かれている通り、

    • 手動でdynamic rangeを指定する、もしくは
    • Calibrationによる自動設定

    のいずれかが必要になります。

    そしておっしゃるとおり、複雑なモデルになるとやはりTensorRTでも対応していない演算が多数含まれることがありますので、custom pluginとして個別に実装して頂く必要があります。
    https://github.com/NVIDIA/TensorRT にpluginの実装例などはありますので、そのまま使えるものは流用いただけるかと。
    (TensorFlowだと状況が少し変わるのですが、ここはPyTorchの記事なので……)

    またTensorRT以外の量子化用のライブラリ等もありますが、もっと詳しい方がいらっしゃると思いますので、私からの言及は控えたいと思います。。。

    大変いい勉強になりました! ありがとうございます。

    ローレベルで申し訳ございませんが、2点の質問をさせてください。

    Q1:
    Pytorch をFP16化したいなら、ライブラリのapexを使用しますし、
    TensorFlow 等なら、AMPを利用するのでしょうか

    Q2:
    Pytorch や TensorFlowがNVIDIAのGPUで高速化する時に、デフォルトとして自動的にTensorRTを利用する事になるのでしょうか。

    (私がコメント返して良いものやらという気はしつつ……)
    @QiitaFan さん、

    Q1についてはYesです。より正確には、AMPを実現するためのライブラリがapex、ということになるでしょうか。。。
    ちなみに、AMP自体はPyTorch本体へのマージが以下のissueで議論されているようです。
    https://github.com/pytorch/pytorch/issues/25081

    Q2については、なんとも言い難いところですが、推論をGPUで高速化する際の一つのライブラリがTensorRTとご認識いただくのが適切と思います。
    学習については前述の通り、TensorRTの対象外です。
    推論についても、TensorRTはフレームワーク側などから自動的に使われるものではなく、ユーザのみなさまが選択して使うもの、とご理解ください。

    あなたもコメントしてみませんか :)
    すでにアカウントを持っている方は