gtkwaveをMac(Mojave 10.14.6)にインストール

なんとなくやってみた。
(ここ数年RTL書いてないし、波形も見てない)

環境

前提

以下がインストールされている状態です。

要約

以下のコマンドを実行します。

$ brew cask install xquartz
# passwardを聞かれる場合もあるので、入力
$ brew cask install gtkwave 

インストール完了後にLauchpadにgtkwave.appがあると思うので、ダブルクリックで起動します。

セキュリティの問題で起動しない場合があるので、起動しない場合はこちらを試してください。

「システム環境設定」
  →「セキュリティとプライバシー」
    →「ダウンロードしたアプリケーションの実行許可:」
 があるので、そこで許可すれば画面が立ち上がる。

詳細

やったことをぐだぐたと書いているので、あまり見る価値はないかも。

$ brew install gtkwave
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 2 taps (homebrew/core and homebrew/cask).
==> New Formulae
:
Error: No available formula with the name "gtkwave"
Found a cask named "gtkwave" instead. Try
  brew cask install gtkwave

あらー これではインストール出来ないのね。

$ brew cask install gtkwave
==> Caveats
You may need to install Perl’s Switch module to run gtkwave’s command line tool.

  https://ughe.github.io/2018/11/06/gtkwave-osx

==> Downloading https://downloads.sourceforge.net/gtkwave/gtkwave-3.3.103-osx-app/gtkwave.zip
==> Verifying SHA-256 checksum for Cask 'gtkwave'.
Error: Cask 'gtkwave' requires XQuartz/X11, which can be installed using Homebrew Cask by running:
  brew cask install xquartz

or manually, by downloading the package from:
  https://www.xquartz.org/

まずは、xquartzをインストールしないといけないもよう。
今回、実行したら passwardを聞かれたので入力

$ brew cask install xquartz
==> Downloading https://dl.bintray.com/xquartz/downloads/XQuartz-2.7.11.dmg
==> Downloading from https://d29vzk4ow07wi7.cloudfront.net/32e50e8f1e21542b847041711039fa78d44febfed466f834a9281c44d75cd6c3?response-content-disposition=attachment%3Bfilename%3D%22XQuartz-2.7.11.dmg%22&Po
######################################################################## 100.0%
==> Verifying SHA-256 checksum for Cask 'xquartz'.
==> Installing Cask xquartz
==> Running installer for xquartz; your password may be necessary.
==> Package installers may write to any location; options such as --appdir are ignored.
Password:(入力)

installer: Package name is XQuartz 2.7.11
installer: Installing at base path /
installer: The install was successful.
🍺  xquartz was successfully installed!

さて、いよいよ gtkwaveのインストール

$ brew cask install gtkwave
==> Caveats
You may need to install Perl’s Switch module to run gtkwave’s command line tool.

  https://ughe.github.io/2018/11/06/gtkwave-osx

==> Downloading https://downloads.sourceforge.net/gtkwave/gtkwave-3.3.103-osx-app/gtkwave.zip
==> Verifying SHA-256 checksum for Cask 'gtkwave'.
==> Installing Cask gtkwave
==> Moving App 'gtkwave.app' to '/Applications/gtkwave.app'.
==> Linking Binary 'gtkwave_bin_launcher.sh' to '/usr/local/bin/gtkwave'.
🍺  gtkwave was successfully installed!

コマンドラインで起動しようとすると以下のエラーが出る。

$ gtkwave
/usr/local/bin/gtkwave: line 122: test: too many arguments
/usr/local/bin/gtkwave: line 149: test: ==: unary operator expected
find: /share/locale: No such file or directory
/usr/local/bin/gtkwave: line 215: /usr/local/bin/../../../Contents/Resources/bin/: No such file or directory
/usr/local/bin/gtkwave: line 215: exec: /usr/local/bin/../../../Contents/Resources/bin/: cannot execute: No such file or directory

インストール完了後にLauchpadにgtkwave.appがあると思うので、ダブルクリックで起動

セキュリティの問題で起動しない場合があるので、起動しない場合はこちらを試してください。

「システム環境設定」
  →「セキュリティとプライバシー」
    →「ダウンロードしたアプリケーションの実行許可:」
 があるので、そこで許可すれば画面が立ち上がる。

もしかしたら、今後波形見る機会が来るかもしれない。。。
来なくていいけど。

PyTorchはじめました(Object Detection)

べ、べつに TensorFlowを嫌いになったわけじゃないんだからね!!!

ただ、NNgenに入力するために、onnxモデル作らないといけなくて公式でサポートしてるやつがいいなぁとか思ってないし

Tutorial見てて、TF Hubに飛ぶんかい!!!ってツッコミどころがあったり
おっ!PyTorchだとめっちゃ簡単に理解できるし、後から色々カスタマイズ出来るじゃん!!!
とか思ってないし、ほんとただのキマグレンです。

っということで、PyTorchの公式だと Segmentationだったのでちょっと修正して
Object Detectionで動かしてみました。

環境は、Google Colabにて実行して確認してます。(必要であれば、Notebook公開します。)
公式Tutorialにも Colab Versionありますので、そちらを見ていただければOKかなと。
(アクセレータのタイプはGPUありのほうが良いと思います。)

前準備

必要らしいので、インストール。

%%shell

# Install pycocotools
git clone https://github.com/cocodataset/cocoapi.git
cd cocoapi/PythonAPI
python setup.py build_ext install

カスタムデータセットをダンロードしてきます。

%%shell

# download the Penn-Fudan dataset
wget https://www.cis.upenn.edu/~jshi/ped_html/PennFudanPed.zip .
# extract it in the current folder
unzip PennFudanPed.zip

2人のおっちゃんが歩いてる。

from PIL import Image
Image.open('PennFudanPed/PNGImages/FudanPed00001.png')

データセット作成

元々が Segmentation用だったので不必要なものをコメントにしてます。
例えば、出力である target["masks"]のところです。

個人的には、PedMasksデータ取得もコメントアウトできるかと思ったのですが、
矩形位置boxesの算出に使用していたので、そのまま使ってます。
なので、boxesっていう情報があればPedMasksのファイル読み込みはいらないです。

今回はLabelが1個だけのようなので複数ある時は修正しないとですね。

import os
import numpy as np
import torch
import torch.utils.data
from PIL import Image

class PennFudanDataset(torch.utils.data.Dataset):
    def __init__(self, root, transforms=None):
        self.root = root
        self.transforms = transforms
        # load all image files, sorting them to
        # ensure that they are aligned
        self.imgs = list(sorted(os.listdir(os.path.join(root, "PNGImages"))))
        self.masks = list(sorted(os.listdir(os.path.join(root, "PedMasks"))))

    def __getitem__(self, idx):
        # load images ad masks
        img_path = os.path.join(self.root, "PNGImages", self.imgs[idx])
        mask_path = os.path.join(self.root, "PedMasks", self.masks[idx])
        img = Image.open(img_path).convert("RGB")
        # note that we haven't converted the mask to RGB,
        # because each color corresponds to a different instance
        # with 0 being background
        mask = Image.open(mask_path)

        mask = np.array(mask)
        # instances are encoded as different colors
        obj_ids = np.unique(mask)
        # first id is the background, so remove it
        obj_ids = obj_ids[1:]

        # split the color-encoded mask into a set
        # of binary masks
        masks = mask == obj_ids[:, None, None]

        # get bounding box coordinates for each mask
        num_objs = len(obj_ids)
        boxes = []
        for i in range(num_objs):
            pos = np.where(masks[i])
            xmin = np.min(pos[1])
            xmax = np.max(pos[1])
            ymin = np.min(pos[0])
            ymax = np.max(pos[0])
            boxes.append([xmin, ymin, xmax, ymax])

        boxes = torch.as_tensor(boxes, dtype=torch.float32)
        # there is only one class
        labels = torch.ones((num_objs,), dtype=torch.int64)
        # masks = torch.as_tensor(masks, dtype=torch.uint8)

        image_id = torch.tensor([idx])
        area = (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0])
        # suppose all instances are not crowd
        iscrowd = torch.zeros((num_objs,), dtype=torch.int64)

        target = {}
        target["boxes"] = boxes
        target["labels"] = labels
        # target["masks"] = masks
        target["image_id"] = image_id
        target["area"] = area
        target["iscrowd"] = iscrowd

        if self.transforms is not None:
            img, target = self.transforms(img, target)

        return img, target

    def __len__(self):
        return len(self.imgs)

出てくる情報は以下のような感じです。

dataset = PennFudanDataset('PennFudanPed/')
dataset[0]
(<PIL.Image.Image image mode=RGB size=559x536 at 0x7F59B993C6A0>,
 {'area': tensor([35358., 36225.]), 'boxes': tensor([[159., 181., 301., 430.],
          [419., 170., 534., 485.]]), 'image_id': tensor([0]), 'iscrowd': tensor([0, 0]), 'labels': tensor([1, 1])})

モデル定義

SegmentationではMask R-CNNでしたが、Object Detectionなので Faster R-CNNに変えてます。

import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
# from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor
 
def get_instance_segmentation_model(num_classes):
    # load a model pre-trained pre-trained on COCO
    model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True)

    # get the number of input features for the classifier
    in_features = model.roi_heads.box_predictor.cls_score.in_features
    # replace the pre-trained head with a new one
    model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

    return model

上記は、1 - Finetuning from a pretrained model ですが、
なんと!公式チュートリアルでは 2 - Modifying the model to add a different backbone についても記載されてますので、必要な方は見てみると理解できると思います。(サイコーかよ

学習と評価

データ準備

補助APIですかね!?(詳細を確認してないので、後々)

%%shell

# Download TorchVision repo to use some files from
# references/detection
git clone https://github.com/pytorch/vision.git
cd vision
git checkout v0.3.0

cp references/detection/utils.py ../
cp references/detection/transforms.py ../
cp references/detection/coco_eval.py ../
cp references/detection/engine.py ../
cp references/detection/coco_utils.py ../

data augmentationや変換するための補助関数を定義するようです。

from engine import train_one_epoch, evaluate
import utils
import transforms as T

def get_transform(train):
    transforms = []
    # converts the image, a PIL image, into a PyTorch Tensor
    transforms.append(T.ToTensor())
    if train:
        # during training, randomly flip the training images
        # and ground-truth for data augmentation
        transforms.append(T.RandomHorizontalFlip(0.5))
    return T.Compose(transforms)

trainevaluateに渡す DataLoaderを作成するようです。

# use our dataset and defined transformations
dataset = PennFudanDataset('PennFudanPed', get_transform(train=True))
dataset_test = PennFudanDataset('PennFudanPed', get_transform(train=False))

# split the dataset in train and test set
torch.manual_seed(1)
indices = torch.randperm(len(dataset)).tolist()
dataset = torch.utils.data.Subset(dataset, indices[:-50])
dataset_test = torch.utils.data.Subset(dataset_test, indices[-50:])

# define training and validation data loaders
data_loader = torch.utils.data.DataLoader(
    dataset, batch_size=2, shuffle=True, num_workers=4,
    collate_fn=utils.collate_fn)

data_loader_test = torch.utils.data.DataLoader(
    dataset_test, batch_size=1, shuffle=False, num_workers=4,
    collate_fn=utils.collate_fn)

モデルをインスタンスおよび Optimizerの設定ですね。

device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')

# our dataset has two classes only - background and person
num_classes = 2

# get the model using our helper function
model = get_instance_segmentation_model(num_classes)
# move model to the right device
model.to(device)

# construct an optimizer
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005,
                            momentum=0.9, weight_decay=0.0005)

# and a learning rate scheduler which decreases the learning rate by
# 10x every 3 epochs
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer,
                                               step_size=3,
                                               gamma=0.1)

いよいよ学習です。

# let's train it for 10 epochs
num_epochs = 10

for epoch in range(num_epochs):
    # train for one epoch, printing every 10 iterations
    train_one_epoch(model, optimizer, data_loader, device, epoch, print_freq=10)
    # update the learning rate
    lr_scheduler.step()
    # evaluate on the test dataset
    evaluate(model, data_loader_test, device=device)

実行結果の最後はこんな感じでした。

Epoch: [9]  [ 0/60]  eta: 0:01:07  lr: 0.000005  loss: 0.0580 (0.0580)  loss_classifier: 0.0373 (0.0373)  loss_box_reg: 0.0113 (0.0113)  loss_objectness: 0.0001 (0.0001)  loss_rpn_box_reg: 0.0093 (0.0093)  time: 1.1315  data: 0.4001  max mem: 3573
Epoch: [9]  [10/60]  eta: 0:00:35  lr: 0.000005  loss: 0.0449 (0.0446)  loss_classifier: 0.0234 (0.0247)  loss_box_reg: 0.0105 (0.0124)  loss_objectness: 0.0001 (0.0002)  loss_rpn_box_reg: 0.0093 (0.0072)  time: 0.7051  data: 0.0396  max mem: 3573
Epoch: [9]  [20/60]  eta: 0:00:28  lr: 0.000005  loss: 0.0382 (0.0474)  loss_classifier: 0.0233 (0.0266)  loss_box_reg: 0.0087 (0.0124)  loss_objectness: 0.0001 (0.0010)  loss_rpn_box_reg: 0.0079 (0.0075)  time: 0.6812  data: 0.0043  max mem: 3573
Epoch: [9]  [30/60]  eta: 0:00:20  lr: 0.000005  loss: 0.0452 (0.0480)  loss_classifier: 0.0235 (0.0266)  loss_box_reg: 0.0089 (0.0124)  loss_objectness: 0.0002 (0.0011)  loss_rpn_box_reg: 0.0080 (0.0079)  time: 0.6868  data: 0.0052  max mem: 3573
Epoch: [9]  [40/60]  eta: 0:00:13  lr: 0.000005  loss: 0.0456 (0.0480)  loss_classifier: 0.0261 (0.0271)  loss_box_reg: 0.0101 (0.0125)  loss_objectness: 0.0002 (0.0011)  loss_rpn_box_reg: 0.0073 (0.0074)  time: 0.6860  data: 0.0054  max mem: 3573
Epoch: [9]  [50/60]  eta: 0:00:06  lr: 0.000005  loss: 0.0410 (0.0469)  loss_classifier: 0.0253 (0.0267)  loss_box_reg: 0.0101 (0.0119)  loss_objectness: 0.0001 (0.0010)  loss_rpn_box_reg: 0.0052 (0.0072)  time: 0.7023  data: 0.0055  max mem: 3573
Epoch: [9]  [59/60]  eta: 0:00:00  lr: 0.000005  loss: 0.0391 (0.0457)  loss_classifier: 0.0226 (0.0260)  loss_box_reg: 0.0085 (0.0118)  loss_objectness: 0.0001 (0.0008)  loss_rpn_box_reg: 0.0056 (0.0070)  time: 0.6736  data: 0.0054  max mem: 3573
Epoch: [9] Total time: 0:00:41 (0.6898 s / it)
creating index...
index created!
Test:  [ 0/50]  eta: 0:00:13  model_time: 0.1189 (0.1189)  evaluator_time: 0.0014 (0.0014)  time: 0.2754  data: 0.1539  max mem: 3573
Test:  [49/50]  eta: 0:00:00  model_time: 0.1331 (0.1296)  evaluator_time: 0.0009 (0.0014)  time: 0.1370  data: 0.0027  max mem: 3573
Test: Total time: 0:00:06 (0.1398 s / it)
Averaged stats: model_time: 0.1331 (0.1296)  evaluator_time: 0.0009 (0.0014)
Accumulating evaluation results...
DONE (t=0.01s).
IoU metric: bbox
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.815
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.992
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.950
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.600
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.825
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.376
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.854
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.854
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.762
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.860

推論

テストデータの一つで実施。
model.eval()っていう実行が必要なんですね。

# pick one image from the test set
img, _ = dataset_test[0]
# put the model in evaluation mode
model.eval()
with torch.no_grad():
    prediction = model([img.to(device)])

推論結果として出てくるデータ:precisionは以下のようです。

[{'boxes': tensor([[ 61.1901,  38.2282, 195.3746, 323.0859],
          [276.6442,  24.0823, 290.8779,  74.9430]], device='cuda:0'),
  'labels': tensor([1, 1], device='cuda:0'),
  'scores': tensor([0.9997, 0.0974], device='cuda:0')}]

画像で見てみるとこんな感じでした。

from PIL import ImageDraw

im = Image.fromarray(img.mul(255).permute(1, 2, 0).byte().numpy())

draw = ImageDraw.Draw(im)
draw.rectangle(prediction[0]['boxes'][0].cpu().numpy())

im

f:id:kocha2012:20191220194433p:plain

感想

簡単に実行できて、やりたいことに対してどういうところを変えていかないといけないかイメージすることができました。
理解出来た気になっているだけだと思うので、これからもう少し PyTorchやっていきたいと思います。

本記事は「Pytorch Advent Calendar 2019 - 18日目」になります。

第2回AIエッジコンテストからFPGA実装を考えてみる

概要

現在、SIGNATEより「第2回AIエッジコンテスト(実装コンテスト①)FPGAを使った自動車走行画像認識」が開催されています。
本コンテストの詳しい概要については、EDA-Expressさんのほうで記事になってますので
詳しくはこちらのほうを参照されるのが良いかと思います。

さて、実は本コンテストの「評価方法」にこんな一文があります。
※本ブログの執筆時点(12/17 7:30)の話

※オリジナルのTiny Yoloのネットワークに基づく精度基準は、12月初旬までに情報提供(暫定精度値)し、最終必要精度は年内に確定予定。

ですが、まだ暫定精度値が出てきておりません。
っということで、運営側がどうなっているのか想像ながらFPGA実装について考えていきたいと思います。

コンテスト内容の話

ガッと要約すると、アルゴリズムを開発しハードウェアに実装する。

  • アルゴリズム開発
    1936 x 1216 の車両前方カメラ画像から特定の物体を検出する。(Object Detection)

  • アルゴリズム実装
    Avnet社Ultra96-V2(Zynq UltraScale+ MPSoC ZU3EG A484)FPGAボードに実装

  • 評価基準
    精度、処理性能(fps)、エリア(回路規模) の3点

つまり、精度を確保しながら、処理性能をだし、エリアを小さく実装したものが勝ちというコンテストです。

本題 - FPGA実装の話へ

アルゴリズム開発

本コンテストは 第2回の開催となってます。
つまり、第1回が開催されています。
第1回は同じデータセット?で「オブジェクト検出」と「セグメンテーション」の各アルゴリズムを競うコンテストでした。
すでに、コンテストは終わっており入賞者レポートが掲載されてます。

つまり、このアルゴリズムFPGAに実装すれば 精度は十分!っていうことです。

公開されているため、少し見ていきたいと思います。
Faster-RCNN で、backboneモデルは ResNeXt101+FPN(Feature Pryamid Networks) です。
多少違うところもありますが、話をしたい部分ではないので省略します。

こちらの図ですが、ILSVRCの歴史とともにCNNレイヤの層が深くなっていっていることがわかるかと思います。
層が深くなることで、パラメータ数や演算コストが増えていきます。

f:id:kocha2012:20191219005921p:plain http://cs231n.stanford.edu/slides/2019/cs231n_2019_lecture09.pdf より

また、こちらの図は横軸が演算コスト、縦軸が ImageNetのTop-1の認識率になります。
円グラフはパラメータ数になります。
理想は 認識率が高く 演算コストが低く パラメータ数が小さいものになるかと思います。

f:id:kocha2012:20191219005957p:plain

Benchmark Analysis of Representative Deep Neural Network Architecturesより

モデルとパラメータ数を少しピックアップすると以下になります。

models params
AlexNet 61MB
VGG19 143MB
GoogLeNet 13MB
Resnext50 25MB
MobileNet-v2 3.5MB
resnext101_32x8d 339MB

モデル名にもなってますが、モバイル用途で提案されている MobileNetのパラメータが小さいことが分かると思います。

さて、今回のコンテストの基準値になる YOLO(You Only Look Once)についても少し。

上のサイトはYOLO-v3のCOCOデータセットの結果が記載されてます。
一部だけ抜粋すると、こんな感じです。

models mAP FLOPS FPS コメント
FPN FRCN 59.1 - 6 第一回のベースに近い
YOLOv3-608 57.9 140.69Bn 20
YOLOv3-tiny 33.1 5.56Bn 220

更に今回のコンテストを想定した場合だと旧バージョンである「YOLO v2」のVOCデータセットの比較が想定しやすいと思います。

抜粋

models mAP FLOPS FPS コメント
SSD300 63.4 40.19Bn 45
YOLOv2 76.8 34.90Bn 67
Tiny YOLO 57.1 6.97Bn 207

というような感じで、整理してみると第一回アルゴリズムでは処理性能やエリア(回路規模)といった点で
不利かもしれないというのがなんとなく感じることが出来たのではないでしょうか。
更に、上記の測定表は VOCやCOCO, ImageNetといった世界中の人がこれまでやってきたデータでの結果です。
コンテスト用のデータで、VOCやCOCOで結果が出たアルゴリズムが上手くいくはやってみないとわかりません。

特に、画像サイズ(1936 x 1216)デカイし、夜の画像とかかなりヘンタイだと思います。
基準値がまだ出てこないのってもしかしてアルゴリズム開発から・・・みたいな想像もしてみたり。

アルゴリズム実装(本題の本題)

序章が長すぎました。本題のアルゴリズム実装(FPGA実装)についてです。
っということなんですが、こちらに素晴らしい資料がありリンク先を読んでいただくほうが良いと思います(笑)

概要は、リンク先の資料に頼るとして今回のコンテストの対象となるFPGAZU3EG」を見ていきたいと思います。

f:id:kocha2012:20191219010025p:plain

https://japan.xilinx.com/support/documentation/selection-guides/zynq-ultrascale-plus-product-selection-guide.pdf#EG より

知っている人には当たり前の話ですが、「CPU(SoC)+FPGA」が一体となっているデバイスです。
CPU(ARM Cortex-A53)を PS, FPGA部分を PLという分類分けになってます。
アルゴリズムの実装方法は自由なので、CPUだけに処理をやらせても問題ないのですが、
それであればRasPi 4で実装して確認しても面白いかもしれません。たぶん処理性能(FPS)はそんなに出ないでしょう。

DNN推論を考えた場合、個人的に押さえておくポイントをあげるとすれば
※用途によって異なるため、あくまでの目安

  • Memory(Total Block RAM)
  • DSP Slices

では、ちょっと実装を考えてみましょう。
一番処理性能が良いのは、DNNモデルをFPGAに実装することでしょう。
しかし、残念ながらそれを実現するには非常に設計工数がかかり専門的な知識と技術が必要になります。
(本コンテストの試験内容およびFPGAバイスの規模から99%無理だと思いますが)

そのため、ハードウェアアーキテクチャとしては WP514資料内での呼び名「DPUアーキテクチャ」になるかと思います。
また、DNNでは Convolution(畳み込み演算) が支配的とされています。
そのためConvolutionをどうやって高速化するかが肝になります。

参考になるか分かりませんが、ハードウェア実装のイメージを掴んでもらえればなと。
例えば、Convolution 2D(3x3kernel(filter))を想定した場合には、
最低垂直方向2line分のメモリを使います。(1チャネル当たり)
5x5の場合には最低4line分ですね。

  +-------------+
  | | | - -     | 1line目保持用
  +-------------+
  | | | - -     | 2line目保持用
  +-------------+
->

なので、416x416の入力画像を考えた場合

416(pix) x 2(line) x 3(RGB) x 4(byte) = 9,984[byte]

と、約10KBのメモリが必要になります。
なので、チャネル数やカーネルサイズにより並列数をメモリ容量を考える必要があります。

次に積和演算についてです。
積和演算は上で記載したDSP Sliceに割り当てることで低遅延で処理可能です。

f:id:kocha2012:20191219010048p:plain

DSP Sliceのファンクション的なものは上図の通りです。
真ん中に乗算器があり、前後に+/-演算器があります。
気づく人は気づくと思いますが、27 x 18(bit)の乗算器です。うまくやりましょう。

ZU3EGはDSP Slices:360なので、積和演算の並列数の限界もここまでとなってしまいます。
ついこないだ発表された最新のADAS/自動運転に対応するZynq UltraScale+ MPSoC製品
XAZU7EV:1,728, XAZU11EG:2,928」と比べると如何に小さいか分かりますね!

設計手法的には、「Vitis-AI」だったり、「Vivado HLS」を使用して設計するのかもしれませんが、いざ設計してみると、デバイスに入らない!!!となってしまう可能性も十分にありえるのではないかと思います。

まとめ

概要レベルのざっくりした内容でしたが、まだコンテスト基準値が公表されてない裏側を想像しつつ、コンテストの難しさやFPGA実装について記載させていただきました。

(そういえば、Ultra96-V2持ってないので欲しい)

本記事は「FPGA Advent Calendar 2019 - 19日目」になります。
空きがいっぱいあるので、良かったら書いてください。

nngenインストール

NNgen: A Fully-Customizable Hardware Synthesis Compiler for Deep Neural Network

をインストールしたので、メモ。

環境:macOS Mojave 10.14.6

$ python3 --version
Python 3.7.4

nngenインストール

READMEを見ながらポチポチと。

Icarus Verilog

$ brew install icarus-verilog

pyverilog, veriloggen, onnx

$ pip3 install jinja2 pyverilog veriloggen numpy onnx
:
Installing collected packages: pyverilog, veriloggen, onnx
Successfully installed onnx-1.6.0 pyverilog-1.2.0 veriloggen-1.8.1

pytestやPyTorchインストール

私は、先にPyTorchはインストールしてしまっていたのでスキップされて必要なものだけ。

$ pip3 install pytest pytest-pythonpath torch torchvision
:
Installing collected packages: packaging, pluggy, py, pytest, pytest-pythonpath
Successfully installed packaging-19.2 pluggy-0.13.1 py-1.8.0 pytest-5.3.2 pytest-pythonpath-0.7.3

テスト実行

$ cd nngen/tests
$ python3 -m pytest .
================================================= test session starts ==================================================
platform darwin -- Python 3.7.4, pytest-5.3.2, py-1.8.0, pluggy-0.13.1
rootdir: /Users/users/nngen, inifile: pytest.ini
plugins: pythonpath-0.7.3
collected 284 items

matrix_add/test_matrix_add_int16_par2.py .                                                                       [  0%]
matrix_add/test_matrix_add_int16_wrap_par2.py .                                                                  [  0%]
matrix_add/test_matrix_add_int2_par16.py .                                                                       [  1%]
matrix_add/test_matrix_add_int32.py .                                                                            [  1%]

ってことで、まだ終わってなくてファンがファンファンいってる。

最適化抑制属性(optnone)

会社の技術情報共有ということでLLVMを紹介しようと思ってコードを書きながら試していたら昔(LLVM3.4世代)と違ったところがあり、ちょっとハマったのでメモしておく。

$ clang -v
clang version 8.0.1 (tags/RELEASE_801/final)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /usr/local/opt/llvm/bin

やりたかったこと

ある関数のLLVM-IRを出力して、PASSで最適化する。

実際にやったこと

  • ある関数(func_add.c)
int func_add(int a, int b) {
  int c;
  c = a + b;
  return c;
}
  • LLVM-IRを出力する
$ clang -S -emit-llvm func_add.c

これで、func_add.llが生成される。

  • PASS(-mem2reg)で最適化する。
$ opt -S -mem2reg func_add.ll -o func_add_opt.ll

問題

PASSが効いていない!

func_add_opt.llの抜粋(func_add.llと同じ結果に...)

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @func_add(i32, i32) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  %6 = load i32, i32* %3, align 4
  %7 = load i32, i32* %4, align 4
  %8 = add nsw i32 %6, %7
  store i32 %8, i32* %5, align 4
  %9 = load i32, i32* %5, align 4
  ret i32 %9
}

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

原因

最適化抑制属性:optnone が追加されていたもよう。

対応

LLVM-IR生成時に-Xclang -disable-O0-optnoneを追加する。

  • LLVM-IRを出力する
$ clang -S -emit-llvm -Xclang -disable-O0-optnone func_add.c

結果

  • func_add.llの比較
    optnone属性が削除されている。
--- func_add.ll  2019-08-16 16:05:48.000000000 +0900
+++ func_add_O0.ll    2019-08-16 16:38:32.000000000 +0900
@@ -3,7 +3,7 @@
 target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
 target triple = "x86_64-apple-macosx10.14.0"

-; Function Attrs: noinline nounwind optnone ssp uwtable
+; Function Attrs: noinline nounwind ssp uwtable
 define i32 @func_add(i32, i32) #0 {
   %3 = alloca i32, align 4
   %4 = alloca i32, align 4
@@ -18,7 +18,7 @@
   ret i32 %9
 }

-attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
+attributes #0 = { noinline nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

 !llvm.module.flags = !{!0, !1}
 !llvm.ident = !{!2}
  • PASS(-mem2reg)適用後の結果抜粋
; Function Attrs: noinline nounwind ssp uwtable
define i32 @func_add(i32, i32) #0 {
  %3 = add nsw i32 %0, %1
  ret i32 %3
}

できた!
全然watchできてないからこんな事に。。。