arutema47's blog

書いたり書かなかったり。

Maskをopencv使って縮小する

目的

このようなMask画像を画像に対して縮小したいというマニアックな事例の備忘録。

これを f:id:aru47:20210129144808p:plain

こうする f:id:aru47:20210129144910p:plain

パイプライン

マスクの中心を計算

# 重心を取得
m = cv2.moments(mask)
cx = int(m['m10'] // m['m00'])
cy = int(m['m01'] // m['m00'])
print(cx, cy)

中心に画像をオフセット

Affine変換で画像を平行移動させる。

# 中心にmaskをシフト
num_rows, num_cols = mask.shape[:2]
# Create translation matrix
offsetx = mask.shape[0]//2-cx
offsety = mask.shape[1]//2-cy
translation_matrix = np.float32([ [1,0,offsetx], [0,1,offsety] ])

# Image translation
mask_translation = cv2.warpAffine(mask, translation_matrix, (num_cols,num_rows))

f:id:aru47:20210129144755p:plain

mask全体を縮小

# maskを半分に縮小
mask2 = cv2.resize(mask_translation, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_LINEAR)
mask_resized = cv2.copyMakeBorder(mask2, 200, 200, 200, 200, cv2.BORDER_CONSTANT, 0)

f:id:aru47:20210129144845p:plain

オフセット分を戻す

# オフセットを戻す
translation_matrix = np.float32([ [1,0,-offsetx], [0,1,-offsety] ])

# Image translation
mask_translation = cv2.warpAffine(mask_resized, translation_matrix, (num_cols,num_rows))
plt.imshow(mask_translation)

f:id:aru47:20210129144910p:plain

有名なDeep Learningの特許を調べてみた

目的

有名所のDNN特許を調べてみました。ほとんどがGoogleの特許ですがBatchNorm、transformer以外日本で登録されていないのが多いですね。 調べたところで力尽きてちゃんとクレームはトップ以外読んでません。随時リストはアップデートしていきます。

参考: https://www.reddit.com/r/MachineLearning/comments/c5mdm5/d_googles_patent_on_dropout_just_went_active_today/www.reddit.com

感想

Dropout,Batchnorm,transformerなど根幹特許を多くGoogleに抑えられていますが、基本的にはPatent Trollに対しての防衛でGoogleから権利行使することはないようです。(訴訟は今の所ない)

Tips

特許の状態は大きく分けて3つあります。

出願:特許庁に特許を提出した段階です。この段階では特許内容を他社が読むことは出来ません。

公開: 2年立つと特許は公開されます。この段階では特許内容を他社が読むことは出来ますが、まだ法的に特許は有効ではありません。

登録: 審査官が許可を出すと特許は登録され、特許として効力を持ちます。

ちなみに論文と同じで特許は審査官に拒絶されることもあり、最大2度ほど修正の機会が与えられます。

画像認識

Inception方式のDNN

patents.google.com クレーム:インセプション方式のDNN

Assignee:Google

Status:登録

日本:なし

Faster-RCNN

patents.google.com

qiita.com

クレーム: 2-stageの物体検出器(Fast-RCNN方式)

Assignee:Microsoft

Status:登録

日本:なし

学習系

hirotaka-hachiya.hatenablog.com

Dropout

patents.google.com クレーム:Dropout正則化を用いて学習されたモデル

Assignee:Google

Status:登録

日本:なし

Batchnorm

patents.google.com

トップクレーム範囲:BatchNormを用いて学習されたモデル

Assignee:Google

Status:登録

日本:あり

登録国がこの特許だけ異様に多いことからGの本気度が伺えます。

学習並列化

patents.google.com クレーム:並列化を用いて学習されたDNNモデル

Assignee:Google

Status:登録

日本:なし

蒸留

patents.google.com Assignee:Google

Status:登録

日本:なし

Neural Architecture Search (NAS)

patents.google.com

Assignee:Google

Status:登録

日本:なし

NLP

Transformer

patents.google.com

Assignee:Google

Status:US 登録

日本:あり

この特許も当たり前ですが出願国が多く、本気ですね。

word2vec

patents.google.com Assignee:Google

Status:登録

日本:なし

GAN

Spectral Normalization

patents.google.com Assignee:PFN

Status:公開

日本:あり

Pytorch高速化 (3) TensorRTで推論を10倍高速化

TLdr;

torch2trtというpytorchモデルをTensorRTに簡単に変換するライブラリを使い、Jetson nano+xavier上で画像認識とセグメンテーションの推論処理を10倍高速化できることを確認しました。

ただtorch2trtはカスタムモデルには対応していないため(resnetなどtorchvision標準モデルのみ)、自作モデルのTensorRT変換は大変だと思います。

他高速化シリーズ aru47.hatenablog.com

aru47.hatenablog.com

TensorRTとは

f:id:aru47:20201130141422p:plain https://amzn.to/3q9qrEK

https://amzn.to/37ndtL5

例えばJetsonNanoなどは安価に入手できるエッジデバイスだが搭載しているGPUはローエンドなため大きい画像をリアルタイムで処理するのは難しい。

一般的なpytorch推論は

model.eval()
with torch.no_grad():
        for (data, target, _, _) in tqdm(loader):
            # data = data.half().cuda() # 半精度推論
            logits = model(data)

と書けるが、これでも速度が不十分なことが多い。

TensorRTはnVidiaが提供している推論を高速化させるフレームワークである。モデルをTensorRTで走らせる事でエッジデバイスでも高速な推論が可能となる。

https://camo.qiitausercontent.com/d1d295922add81fb3bd160a2be4f091d2147b916/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3137313931352f62653963613665332d383261612d663163372d303663362d3138616136633363616264612e706e67

引用:http://on-demand.gputechconf.com/gtcdc/2017/presentation/dc7172-shashank-prasanna-deep-learning-deployment-with-nvidia-tensorrt.pdf

その機能の一つとして強力なものにグラフ最適化というものがある。 図はInceptionモジュールのものであるが、通常のpytorch等はconvrelubatchnormと3ステップ別々の計算を行う。 ここで入出力のテンソルは毎回メモリから読み書きする時間も必要であり、計算時間は長くなってしまう。

そこでTensorRTのグラフ最適化(レイヤフュージョン)はDNNではCNN, relu, batchnormと3ステップ計算が頻出することを利用し、その計算をまとめた専用レイヤーを用意する。 そうすることでデータ読み書きと演算はあたかも1度しか行わずにconvrelubatchnormの計算を実行することができる。reluやbatchnormは単純な加算、クリッピングで表現できるため計算をまとめることは容易である(カーネルを用意さえあれば)。

f:id:aru47:20201128114742p:plain

引用:http://on-demand.gputechconf.com/gtcdc/2017/presentation/dc7172-shashank-prasanna-deep-learning-deployment-with-nvidia-tensorrt.pdf

例えばresnet50では等価的にレイヤ数を1/4に削減している。 この最適化は非常に強力であり、大幅な高速化が可能となる。

またPytorchでは対応していないINT8のTensorCoreを使った計算もTensorRTならば可能である。 FP16に対しメモリ量が半分になるため、データ伝送も2倍高速化することが期待できる(実際に大幅に高速化可能)

またTensorRTはV100などの一般的なGPUでも高速化が可能であり、10倍ほどの高速化が公式スライドで報告されている。

f:id:aru47:20201128114416p:plain

TesnorRTを気軽に試す

TensorRTを自分のネットワークに試すにはPytorchモデルをONNXフォーマットに変換→ONNXのモデルをTensorRTで読み込むというフローがあるが、ONNX変換はクセが強く、気軽に試すのは難しい。

そのためnvidiaの出しているtorch2trtという変換ツールを使うことでONNXを介さすに直接PytorchモデルをTensorRTに変換できます。 ただtorch2trtは対応しているレイヤは標準CNNのものがほとんどで(Resnetなど)、カスタム関数などが入っていると上手く変換はできませんでした。 例えば物体認識モデルの変換は難しいです。その場合は公開されているONNXモデルを使うのが良いです。

github.com

以下レポでtensorRTをJetson上で試し、速度向上効果を見てみました。

TensorRT化はモデルと入力画像サイズを渡すことで簡単にできます。

 # define input
 input_size = [1, 3, 256, 256]
 x = torch.zeros(input_size).cuda()

# convert to tensorrt models
model_trt = torch2trt(model, [x], fp16_mode=True, int8_mode=True, max_batch_size=1) # 精度によってモード切り替え

詳しい使い方はレポかtorch2trtのページを読んでみて下さい。

github.com

画像認識

resnet18 resnet34 resnet50
Raw 11 12 16
FP32 3.8 5.6 9.9
FP16 2.1 3.3 4.4
INT8 1.7 2.7 3.0

Xavier上で数種類のネットワークの画像一枚に掛かる時間を調べました(ms)。rawはpytorchそのままで他はtensorRTで記載分解能で量子化したものです。NanoではInt8が何故か動きませんでした。 画像サイズは256x256.

画像セグメンテーション

f:id:aru47:20201128133902j:plain

fcn_resnet50 fcn_resnet101 deeplabv3_resnet50 deeplabv3_resnet101
Raw 200 344 281 426
FP32 173 290 252 366
FP16 36 57 130 151
INT8 21 32 97 108

こちらはセグメンテーションの結果です。画像サイズは512*512と大きく、量子化を積極的にやることで速度が大幅に上がることがわかりました。

Github Actionsでpypiのパッケージを発行

なぜgithub actionsで発行できると楽か

f:id:aru47:20201108162012p:plain

自作ライブラリ開発しているとすると普通ならコーディング、setup.pyを記述、pypiにtwineでアップロードという流れになります。 blog.amedama.jp

ただこのpypiへのアップロードが意外に面倒くさい。。!毎回pypiアカウントの認証などしなくてはならず、ライブラリ発行が億劫になるため出来るなら自動化したいものです。

Github actionsを使うとgithub上でリリースを追加することで自動的にpypiにアップロードすることができ非常に便利だったのでその方法を記述します。

やってみた

他レポの見様見真似でやってみた。

Pypiに書いてあるフローは以下であるが、微妙に今回参考にした設定とは異なる。

https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/

今回用いたライブラリは自分で開発しているkaggle用TTAライブラリであるodachです。

github.com

Github actionsの設定ファイルを追加

レポ下に.github/workflows/python-publish.yml

を追加します。

# https://github.com/kentaroy47/ODA-Object-Detection-ttA/blob/main/.github/workflows/python-publish.yml
name: Upload Python Package

on:
  release:
    types: [created]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.x'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install setuptools wheel twine
    - name: Build and publish
      env:
        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      run: |
        # ソース配布型
        python setup.py sdist bdist_wheel
        twine upload dist/*

Pypiの認証情報をSecretsに追加

レポジトリのSettings以下のSecretsにpypiの認証情報を追加します。

PYPI.PASSWORDPIPY.USER_NAMEにそれぞれpypiのユーザ、パスワードを格納します。secretsの情報は管理者しか見られないため安心です。

f:id:aru47:20201108161856p:plain

Githubにリリースを追加

ここまででgithub actionsの準備は整いました。releaseを追加すればactionが走り、その結果をActions以下で確認できます。

f:id:aru47:20201108162144p:plain

無事に走ればpypiにもパッケージが追加されているのを確認できます。

pypi.org

f:id:aru47:20201108162311p:plain

Pytorch高速化 (2)Mixed Precision学習を試す

Qiitaからのお引越しです。

前編 aru47.hatenablog.com

TLDR; (2021/06/17)

resnet50でCIFAR10をFP16により学習を2倍高速化でき、メモリ使用量も半分にできる。

pytorch1.6からデフォルトでMixed Precision学習をサポートしており、画像認識なら大抵これで上手く学習できます。 一部例外として、swin transformerだとapexを使用したほうが精度が良い場合もありました。

このチュートリアル通りにコードを書くのがおすすめです。 pytorch.org

use_amp = True # ampをオンオフ

# Creates model and optimizer in default precision
model = Net().cuda()
optimizer = optim.SGD(model.parameters(), ...)

# Create a GradScaler once at the beginning of training.
scaler = torch.cuda.amp.GradScaler(enabled=use_amp)

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()

        # Runs the forward pass with autocasting. 自動的にレイヤ毎に最適なビット精度を選択してくれる(convはfp16, bnはfp32等)
        # ベストプラクティスを選択してくれるため、便利。use_amp=Falseではfp32を使用する。
        with torch.cuda.amp.autocast(enabled=use_amp):
            output = net(input)
            loss = loss_fn(output, target)

        # Scales loss. 
        # Scalerはautocasting以下では呼ばないこと。
        # 
        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()
        opt.zero_grad()

実際のcifar10学習コード:

github.com

目的

RTX2080tiを手に入れたのでPytorchにてFP16学習を試す。 Tensorcoreを使うことで演算速度がFP32に対する大幅な高速化が(スペック的に)期待できる。 どれくらい早くなるか、pytorchでどう書けばFP16が使えるかなど記述する。

BatchNormはFP32なので正確にはMixed-precision trainingだ。

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

そもそもFP16って?

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

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

といった形で保存されている。これは一般的な4bitの整数表現(4bit Int)という形である。 ただこれでは0から15までの数字しか表現できない。 更に負や小数などを表現するのに使われるのが浮動小数点(float)と呼ばれる方式である。 image.png

上記の図(Googleより)は32bit浮動小数点(FP32)と16bit浮動小数点(FP16)の形式を示す。 これらはコンピュータの数字表現で最も一般的なフォーマットでFP32が単精度と呼ばれ、メモリを節約したい場合に使われるFP16が半精度と呼ばれる。

特徴として: ・MSB(最も左側のビット)が正負を決める ・Exponent 8bitは小数点位置を決める指数部である。 ・Mantissaは整数部を決める。上記のInt方式と同様である。

image.png http://www.altima.jp/column/fpga_edison/bit_number_float.html

のように指数部をスケールすることで非常に幅広い範囲の数字を表現することができる。

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

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

image.png nVidiaより。

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

環境

Ubuntu 16.04 Pytorch 1.0 CUDA 10.0 cudnn 7.4 GPU RTX2080ti

学習環境

FP16学習repo →https://github.com/kentaroy47/pytorch-cifar10-fp16 CIFAR10の学習repoを元に改造した。

python train_cifar10.py --fp16

でResnet18で学習開始。

FP16化にあたりポイント

https://github.com/fastai/imagenet-fast/tree/master/cifar10 Fast-aiのFP16化レポを参考に学習コードを改造した。

他の参考は: Training with Half Precision https://discuss.pytorch.org/t/training-with-half-precision/11815 https://pytorch.org/docs/stable/tensors.html https://devblogs.nvidia.com/apex-pytorch-easy-mixed-precision-training/

PytorchではFP16(half)がサポートされており、簡単にFP16化が可能。

FastAIのモデル16FP化コードを使わせていただいた。 https://github.com/fastai/imagenet-fast/blob/master/cifar10/fp16util.py のコードを使い、FP16化した。

何をやっているかというと、 入力、CNNのレイヤ→FP16化 BatchNormレイヤ→FP32化 している。

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()))

正直このfp16util.pyをコピーし、network_to_halfをネットワークに対し適応すれば良い。

net = ResNet18()
from fp16util import network_to_half
net = network_to_half(net)

学習速度

resnet18

batch = 128 FP32時:1 epochあたり15sec メモリ資料量:2000MB

FP16時:1 epochあたり10sec メモリ使用量:1500MB

50%高速化。 更にバッチ数、モデルサイズを大きくすると。。?

resnet50

batch = 512 FP32時:1 epochあたり57sec メモリ資料量:21000MB

FP16時:1 epochあたり27sec メモリ使用量:11727MB @TitanRTX

  • メモリ使用量半分に
  • 2倍高速化

両者とも50epochでval.89%ほどに到達。 精度的には更にチューニングが必要で(250 epochで93%くらいはいくはず)、FP16が精度的に問題ないかはまだわからない。

TODO: ちゃんと狙い通り16FPになっているかなど確認する。

CIFAR10より更に大きいデータセット

CIFAR10は画像サイズが小さく、演算よりもメモリ通信やデータローダがボトルネックになってしまっている?50%くらいしか早くなっていない。。 Resnet50ではちゃんと二倍高速化。メモリ半分はかなり嬉しい。

知り合いによるとImagenet学習ではほぼ2倍の速度向上が得られたとのこと。 そのうち試したい。

Nvidia Apex

https://devblogs.nvidia.com/apex-pytorch-easy-mixed-precision-training/

Nvidia謹製のpytorch FP16学習ツールが公開されました。

続編で試します

qiita.com

推論高速化

aru47.hatenablog.com

Pytorch高速化 (1)Multi-GPU学習を試す

Qiitaからのお引越しです。

Pytorch Advent Calender 2018 3日目の記事です。

はじめに

学生に"Pytorchのmulti-GPUはめっちゃ簡単に出来るから試してみ"と言われて重い腰を上げた。

複数GPU環境はあったのだが、これまでsingle GPUしか学習時に使ってこなかった。 試しに2x GPUでCIFAR10を学習しどれくらい速度向上が得られるか実験。 またpitfallなどあったら報告する。

環境

GPU: TitanXp *4 OS: Ubuntu 16.04 Pytorch: 0.4.0 Python: 3.5 Network: Resnet 18

codes:

GitHub - kentaroy47/pytorch-mgpu-cifar10: testing multi gpu for pytorch

GPUコマンドあれこれについて:

【Linux】GPU逆引きコマンド - Qiita

MultiGPUにするには。。

Code的には超単純. モデルにtorch.nn.DataParallelを適応するだけでmultiGPUが使用可能となってしまう。。恐るべし。

Pytorchマニュアル。

pytorch.org

device = 'cuda' if torch.cuda.is_available() else 'cpu'
# ネットワーク宣言
net = ResNet18()
# cuda or cpu?
net = net.to(device)
# 複数GPU使用宣言
if device == 'cuda':
    net = torch.nn.DataParallel(net) # make parallel
    torch.backends.cudnn.benchmark = True

torch.nn.DistributedDataParallelを使用すると1GPUに対し1CPUを割り当てるためより早くなるのですが、学習ループの書き方が変わるのが注意です。 リンクを参照してみてください。

tmyoda.hatenablog.com

single GPUで学習.

まずはシングルGPUで学習する。 使用GPU切り替えはターミナルにて

export CUDA_VISIBLE_DEVICES=0
python train_cifar10.py --net res18

とGPUが一つしか見えないように宣言すれば良い。

gpu1.JPG

1epochあたり21秒かかっている。

multi GPUで学習

ここでは2つのGPUを使って学習してみる。 どれくらい早くなるかな?理想的には2倍だがCIFAR10は小さいのでどうか。

シェルでGPUが2つ見えるように切り替えておく。

export CUDA_VISIBLE_DEVICES=2,3
python train_cifar10.py --net res18

2gpus.JPG 学習を開始し、正常に同じIDのプロセスがGPU2,3に入っている事を確認。

あれ?

2gpu_batch128.JPG 1 epochあたり15秒。 40%ほどしか高速化していなくておかしい。。

batch数を増加させる。

https://pytorch.org/tutorials/beginner/former_torchies/parallelism_tutorial.html

One can wrap a Module in DataParallel and it will be parallelized over multiple GPUs in the batch dimension.

チュートリアルによるとBatchレベルで並列化しているよとある。 Batchが2個あったら一つをGPU1,もうひとつをGPU2に渡しているということだ。

このスクリプトだとBatch数=128でDataloadしているが、GPU1にBatch=64 GPU2にBatch=64が渡されることになりSingle GPU時に比べBatch数が少なくなっている。

つまりGPUを2つ使う場合はDataloaderのBatch数も2倍にしなければならない。

trainloader = torch.utils.data.DataLoader(trainset, batch_size=256, shuffle=True, num_workers=8)

dataloaderのバッチ数を2倍の256に設定。 2gpus_res18_batch256.JPG

すると1 epochあたり13秒に改善。 2GPU化により60%早くなったのでまあまあかな。データセットが小さいのがまだ足をひっぱている気がする。まだ遅い。

Batch=378まで大きくしてみたがあまり変わらず。

Resnet50 + 101で実験

ネットワークを大きくしてみれば恩恵が大きくなるかと思い実験。 Res50では速度改善は77%!理想に近づいてきた。 ImageNetサイズまでスケールアップすると更に良くなりそう。

ネットワークサイズが小さいとマルチGPUにより計算時間はスケールできる。がgradientをreduce,computeする部分はシリアルのためボトルネックとなってしまう。

Res18 Res50 Res101
SingleGPU 21s 72s 126s
MultiGPU(*2) 13s 44s 77s
Improvement 60% 77% 63%

Res101では学習速度の向上は50ほど良くない。 速度コンペでRes50を皆使うのは一番速度がいい感じで出るからなのだろうか(笑)

singleGPUとMultiGPUのロスを比較

普通バッチ数を2倍にすると学習係数を2倍にしないと学習結果が変わってしまう。

MultiGPU時はどう考えればいいんだっけ? →ざっと検索してもわからなかったので試してみた。

image.png

single lr=0.1, batch=128と等価なのはmulti(2GPU) lr=0.2, batch=256であった。multi(2GPU) lr=0.1, batch=256は学習の進みが異なる。

マルチGPUのデータの流れはどうなってる?

演算としてはBatch[0:255]のデータを

GPU1 Batch[0:127]
GPU2 Batch[128:255]

のようにアサインする。するとbackpropによりバッチ数分のgradientが生成される。

GPU1 grad[0:127]
GPU2 grad[128:255]

となり、これらは集約する必要がある。

# parameter update
Weights += -sum(grad[0:255]) * learnrate

のような演算を行うため、パラメータアップデート時には何個のGPUに分散したかどうかは関係ない。

結論 (or main takeaways)

1. PytorchでmultiGPUを使う際はBatchsizeも使用するGPU数に比例させ増加させなければ十分な学習速度向上は得られない。

2. Batch数を増やしたらその分学習係数も増やさなければ学習結果は変わってしまう。またはパラメータアップデート時に何個のGPUに分散したかどうかは関係ない。

3. ネットワークサイズはある程度ないとマルチGPUの恩恵は受けられない

gradientをbatch要素毎に計算→CPUに戻しbackprop時に学習率を適応するのでGPU数を増やしても学習率に影響はないと予想..

さらなる高速化のために

続き: aru47.hatenablog.com

TensorRTで推論高速化 aru47.hatenablog.com

qiita.com

ハードウェアの速度をどう評価するか考える(2) ~メモリ、メモリ律速~

前回のあらすじとこの記事の目的

前編: ハードウェアの速度をどう評価するか考える(1) ~クロック、OPS~

現代ハードウェアの計算性能を評価する尺度であるメモリ律速の概念とルーフラインモデルについて理解を深めることです。 本記事を通し、あるアルゴリズムが速度が十分に出ない時、それがハードウェアのどの性能(メモリか演算)に律速されてるかイメージできるようになるのが目標です。

しかし前編は子供が起きたので前編は演算速度だけで終わってしまった! 今回は起きる前にメモリ律速まで書くぞ!

https://venturebeat.com/wp-content/uploads/2020/05/Jensen-cooking.jpg?resize=1024%2C614&strip=all

  • アチアチのGPUお待ち!

メモリ律速

脱線したが本線のメモリ律速に戻る。前回では演算速度の話をし、演算速度(OPS)はハードウェアの持つ計算能力で決まると書いた。それでは最強・最速のCPU/GPUは無数に演算機を詰め込めば実現できるのだろうか?もちろん違う。

f:id:aru47:20201101173608p:plain
AMD Zen2 chip photo from ISSCC2020

チップの殆どの面積が内蔵メモリ(L3キャッシュ)で占められることがわかる。またCPUコア自体もSRAMエリアが四割ほどを占める(畑みたいなエリアはSRAMセルである)

f:id:aru47:20201101173512p:plain
Apple A12
f:id:aru47:20201101173539p:plain
Apple A12 CPU

Appleの最新A12チップの中身も見てみよう。

CPUエリアの半分以上もSRAM(L2とシステムキャッシュ(L3?))で占められていることがわかる。(GPUはいい画像が見つからなかったけどこれもまたチップの殆どがキャッシュです。。

先端チップの大きな面積はメモリ(L3キャッシュ)であった。要はメモリである。なんでこんなにメモリを敷き詰めているかというとハードウェア性能にメモリはとても影響するからである。

メモリが重要なワケ

当たり前だが、計算機が

out = a + b

という計算をするためにはabの2つのデータが必要であり、outをメモリに格納する必要がある。そのため1演算(OPS)には2データの読み込みと1データの書き込みが必要だ。

ここでもしハードウェアが100GOPSの計算速度と200Gdata/秒の読み込み速度を持つが、1Mdata/秒しか書き込み速度がないとする中で上記演算を繰り返すとしよう。すると1Mデータを出力したところで書き込みリミットに差し掛かってしまい、以後の計算は停止してしまう。CPUのパイプラインストールと同様のことが起きてしまい、例えいくら演算速度が早くても無意味である。 また上記のout = a + bの例でも同様に読み込みデータが極端に遅ければ演算にデータ読み込みが間に合わず、演算はストールしてしまう。

このようにメモリ読み込み/書き込みが原因で演算速度が律速してしまうことをメモリ律速と呼ぶ。

ここまで読むとわかるが、データの読み込み速度というのは演算速度と同等にハードウェア性能を評価するためには重要であることがわかる。 演算性能だけ高めたとしても、実際の計算は早くならないのだ。

GPUから読み解くメモリバンド幅

メモリ読み書きの速度はメモリ帯域やメモリバンド幅とも呼ばれ、ハイエンドGPUでは非常に重視されている。

f:id:aru47:20201101174151p:plain

NVIDIA A100 | NVIDIA

先端nvidia GPU A100は40GBもの専用メモリを備え、それらはHBM2(high bandwidth memory2)という接続IOでGPUーメモリ間を結ぶ。またそのメモリ帯域幅は1.5TB/s!であり、そのデータ量までならばデータを読み書きできることができることを示している。

先程のout = a + bの例で各データが1BのINTであったとすると、1演算で3Bのデータ伝送が必要になる。そのため1秒間で行える演算量は概ね1.5TB/s / 3 = 500GOPSであると試算できる。これはGPUメモリ内にすべての入力が格納されている例を考えた。例えばDNNを走らせる際はDNNの重み、gradient、中間値はすべてGPUメモリ内に格納するようになっており、概ねこのような概算が可能である。ただDNNの入力はCPUからPCIeバスを通じて伝わるため(64GB/s)、そこは律速しないように注意する必要はある。

また実際のGPUはキャッシュと呼ばれる大量の内部メモリ(メモリ帯域は10TBくらいと一桁高速)を備えるため、a,bが固定値であれば一度GPU内に読み込めば自動で再利用してくれる。またCNNのweightのように入力をスライドさせ繰り返し適応するものでは重みをキャッシュ内に格納し外部メモリアクセスを最小化するように賢く再利用している。そのようなトリックがcuDNN等のライブラリにかかれており(暗号化されているが)、頑張って読んでいくと3x3convや5x5convに特化したカーネルなどを見つけることができる。キャッシュの仕組みなどはここでは解説しないが、興味あるならばパターソン&ヘネシーなど読むと良いです。

amzn.to

amzn.to

amzn.to

ルーフラインモデル

f:id:aru47:20201005102653p:plain
TPUのルーフラインモデル

とうとう本題のルーフラインモデル*1まで来ました!今までの話を追えていれば難しい話ではありません。

out = a + b から演算量を求める例え話の時におや、と思った方もいるかと思います。 興味深いことにハードウェアの演算量、メモリ帯域が与えられ、演算したいアルゴリズムのデータ入出力量が既知であればそのハードウェアの演算速度(OPS)が求められるのです!

演算したいアルゴリズムのデータ入出力量をルーフラインモデルでは

ある演算に必要なデータ量=MAC OPS/data[byte]

で表現し、x軸にプロットします。またその時のアルゴリズムでの実測OPSをy軸にプロットします。

注意したいのはCNNのようにデータを計算間で再利用できる文は(キャッシュ利用できる分は)差し引いて考える点です。Alpha GOのCNNはMAC OPS/data=1000とデータ再利用効率が非常に高く、グラフの右上にプロットされます。ここの点ではメモリ帯域で律速されず、ハードウェアのOPSでリミットされます。

一方でRNNやMLPはデータ再利用効率は優れず、MAC OPS/data=100程度です。TPUv1はメモリ帯域が遅く、演算はメモリ律速になってしまいます。このようにルーフラインモデルは複数の演算、メモリヘビーなアルゴリズムを比べることでハードウェアの演算速度とメモリ帯域両方を加味したバランスの良いベンチマークが可能となります。

ちなみにDNNでバッチ数を増やすと速度がちょっとずつ上がる、TPUではバッチ数を出来るだけ上げたほうがよい、と言われるのはバッチ間でweightデータを再利用できるためMAC OPS/dataが上がり、メモリ律速が緩和され高速化できるためです。

Further comments

TPU vs GPU

TPUやGPUの最大速度はCNNといえどもメモリ律速であり、大体メモリバンド幅で決まります。 そしてTPUとGPUは同じIO(HBM)を使用しているので最大速度もとんとんになってしまいます。

https://www.extremetech.com/wp-content/uploads/2018/04/ResNet-50-640x396.png TPUv2の結果ですが、だいたい速度は同じです。

FP16, BF16, TF32などの低精度Mixed Precision学習

qiita.com

https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F171915%2F891e685c-48f6-4afb-9839-1074706d8f73.png?ixlib=rb-1.2.2&auto=format&gif-q=60&q=75&s=f8c18922b1c0b7d21b3f11d95d5e2bba

メモリ律速はデータの読み書きする量で決まるのでFP16といったより少ないデータ量で学習を行うと、理想的には二倍ほど同じGPUでも速度向上するポテンシャルがあります。なのでハードウェア性能を引き出す上でMixed Precision学習は非常に強力なテクニックです。例えばTPUv3ではBF16というフォーマットで学習が行われ、指数表現がFP32フォマットと同等の精度であるため学習精度も落ちないことを売りにしています。

また最近nvidiaが導入した低精度表現TF32(実効的にFP19)の学習がpytorchでデフォルトになりました。 これで更に高速化が達成できるかと思います。

pytorch.org

もしpytorchを使っているならば公式のMixed Precision学習導入説明がとてもわかりやすいので読んでみて下さい。

*1:Roofline: An Insightful Visual Performance Model for Floating-Point Programs and Multicore Architectures https://people.eecs.berkeley.edu/~kubitron/cs252/handouts/papers/RooflineVyNoYellow.pdf