Ultra96-v2でCNN推論エンジン(DPU)を動かすまで

# はじめに
この記事はAIエッジコンテスト( https://signate.jp/competitions/191 )向けにXilinxのCNN推論エンジン(DPU)をUltra96-v2で動かすまでの記録です。
（一応コンテスト向けのレポートを兼ねています。）

この記事ではDPUをなるべく直接触る方向で環境を作ります。

カスタマイズが不要な場合やVitisAI経由でDPUをつかたい方、Linux環境とメモリ16GB以上でストレージの空きも余裕がある方は[Ultra96V2向けVitis AI(2019.2)の組み立て方](https://qiita.com/basaro_k/items/e71a7fcb1125cf8df7d2) を参照してください（強く強く推奨）

また、Petalinux周りは今回初めて触ったため理解が怪しいところがあります。
ツールのバグも踏んだため、再現できない可能性がありますが、ご了承ください。


## 環境
メモリ要件が厳しく、短時間で用意できなかったため割と変な環境で無理やり進めました。

#### FPGAボード
* AVNET Ultra96 v2
* ボードファイル(Vivadoで回路を合成する際に利用)   
https://github.com/Avnet/bdf/tree/master/ultra96v2/1.0
* BSP(Petalinuxのビルドで利用)　  
https://www.element14.com/community/community/designcenter/zedboardcommunity/ultra96 においてあるUltra96-V2 – PetaLinux 2019.2 BSP

#### ホストPC（回路合成用）
* Windows 10
* Vitis/Vivado 2019.2(2020/1月インストール)
* メモリ16GB

#### 仮想マシン（Petalinuxビルド用）
* Ubuntu 18.04.3 LTS
* Vitis/Vivado 2019.2(2020/2月インストール)
* Petalinux 2019.2(2020/2月インストール)
* メモリ8GB 
* ストレージ (ツールインストール後で)50GB以上の空きスペース

#### DPU
* DPU-v2 (Zynq DPU 3.0)
* ドライバーのソースコード  
https://github.com/xelalin/Ultra96v2-DPU

## 背景
12月某日

お友達「データサイエンス系のコンペでいこんなんがあるんやけど興味ある？」
お友達「FPGA上で動くように実装しやなあかんくて」
お友達「青ペンギンくんとチームくんでやったら楽しそうやなとおもって」
お友達「論文終わってからでいいけど」

と言われました。

論文は片付けないと卒業できないので実質稼働可能期間は2ヶ月。
回路を作ったことあるから知ってるんです。**2ヶ月で速度も出てまともに動く回路を作るのは困難**だと。

最近は高位合成（やったことない）で開発工数が圧縮できるとしても、行列演算回路(作ったことない)を作るより**USBオーディオ回路のほうが作りたい**、**人生最後の貴重な春休みは音声に費やしたい**ので、手抜きを考えたところ、そういえば**昨年Xilinxがなにか発表していた**し、それを載せてしまえと思ったわけです。

発表されたVitisAIで推論ができるのですが、Xilinx Runtime上で動くと書いてあります。サンプルとかを眺めているとなんとなくパーシャルリコンフィグ的なものと使って動的に回路を展開しているように見えます。

けど、知っているんです。**間に挟まる仕組みが増えるほど、バグを踏む可能性が上がる**ことを。
そして**バグを踏んだ場合、対処にとても時間がかかるか、最悪詰む**ことも。

VitisAIを調べるとその裏で動いているのはDPUというIPでした。

そこで [冒頭のリンク](https://qiita.com/basaro_k/items/e71a7fcb1125cf8df7d2)で紹介されている方法とは別で、DPUを直接書き込んだ状態でなるべく直れる環境を作りたいと思いました。

一応余った時間で後段の回路の実装も検討(精度をあげるため、前半INT8、後半FP16で演算とか、非対応活性化関数をAXI Streamingでサンドイッチするとか）していましたが、**評価基準が速度のみ**、**2月下旬にコンテストの勉強会でDPUの使い方の説明があった([冒頭のリンク](https://qiita.com/basaro_k/items/e71a7fcb1125cf8df7d2))**ため、みんなDPUを使ってくることを考えると、**int8でちゃんと作り込んでいる（はず）のDPU**を使って同じ土俵に立つ必要があると思い、とりあえずはDPU1本化（**チューニングはしますよ**）の方向で進めました。

こうして青ペンギンはUSBオーディオの開発時間の捻出工作を始めたのです。

# DPUについて

DPU(Deep Learning Processing Unit)はXilinxが提供しているCNNの推論IP(Intellectual Property-回路のブロック)です。
**細かな制御はできず、重みと処理内容が一体化したバイナリ(ELF)を送り込むと処理**してくれるエンジンです。
>最終レイヤーの書き込み検知とかできれば、AXIで書き込み中の値を改変して更に1レイヤー処理ができると思っていたのに....

普段ベアメタルでZynqを使っているので、ベアメタルで開発をしたかったのですが、制御ソフトがLinuxのドライバーとしてビルド済みのものしか配布されていなかったので諦めてPetalinux経由で触る必要があります。

> と、当時は思っていたのですが、作業をすすめるに連れドライバーの（古いバージョンですが）ソースコードを見つけたりはしますが、非推奨らしいですね...
> 一応IPドキュメントの方に一部のレジスタは書いてありますが、DPUの状態取得系は非公開ですね。

DPUでは**int8に量子化**して、推論を行います。また、**対応しているレイヤーは限定的**なため、推論ネットワーク設計時に考慮して学習してあげる必要があると思います。

詳しくはIPドキュメントをご覧ください。
https://japan.xilinx.com/support/documentation/ip_documentation/dpu/v3_1/j_pg338-dpu.pdf

量子化やコンパイルをしてELFにする方法はここでは扱いませんが、専用のツールが提供されています。


>  **罠です!**
> DPUはDPU-v2やZynq DPU 3.1、DPU Target 1.4.0など、名称とバージョンがやや異なるように表記されていますが、多分同じものを指していると思われます。DPUのIPバージョンは3.1なのに、付属しているReadmeには1.3.0と書いてあったり。。。バージョン番号が色々混在しているので取り扱い注意です。

# Vivado
ここではDPUだけを載せたブロックデザインを作成し、FPGA上に乗せるビットストリームを作成します。
## 考慮事項
### クロック
まずはVivado上でDPUを含む回路を作成します。DPUのIPドキュメントによれば、DPUは**演算＋AXI用のクロック**(m_axi_dpu_aclk)、**DSP用のクロック**(dpu_2x_aclk)、**DPUの制御レジスタ用クロック**(s_axi_clk)の3本が必要です。
このうち、m_axi_dpu_aclkはdpu_2x_aclkを**1/2に分周したクロック(要同期)**にする必要があります。

s_axi_clkとm_axi_dpu_aclkは同じくクロックでもいいのですが、レジスタアクセスに速度は求めないので低速なクロックを入れて省エネを図ることが推奨されています。
また、DPUが動いていないときはdpu_2x_aclkを止めて省エネを図れるようにクロックゲーティングオプションも用意されています。

### リセット
各クロックに対応する同期リセットを入れることが必要で、同期リセットの生成にProcessor System Reset IPを使用することが推奨されています。

### AXI(データバス)
DPUはデータのアクセスや制御用レジスタのアクセスに毎度おなじみAXIバスを用います。
データアクセス用に2本(マスター)、命令読み込み用に1本(マスター)、CPUから制御用に1本(スレーブ)の計4本です。

DPUはパフォーマンス命なため、**AXI Interconnect IPなどは用いず**に可能であれば**直接PSのAXIポートに接続**することが推奨されています。

## ブロックデザイン

[TCLファイルはこちら](https://gist.github.com/mmitti/e25ecf51cd7a2c3fec27745277dd46f1)

PLとDPU、クロック生成、リセット回路を配置して接続するだけです。

![bitmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140084/c602c7fa-5b9c-cd21-82ff-bf36e089fadd.png)

注意点としてはクロックは同期させる必要があるため、ClockingWizardの「Output Clock」タブの「Matched Routing」にチェックを入れます。これにより2つのクロックが同期されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140084/e1a272c0-c831-b71a-e237-2258faa22af7.png)


チューニングの際はDPUとクロック生成回路のパラメータを変更することで、パフォーマンスの向上を狙えます。

最後にAdrees Editorで割り当てを行いValidate Designをして完成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140084/6dac7ea8-ba67-8feb-0df9-801fd990168f.png)


## ビルド
あとは通常と同じように論理合成、配置配線、ビットストリームの生成をし、ハードウェアファイルをエクスポートします。
Vivado2019.2からはハードウェアファイルがxsaに変わりましたので、生成されたxsaファイルだけ取り出してPetalinuxのビルドに進みます。

## xsaファイルの修正

> **罠です!**
xsaファイルにふくまれるpsu_init.cが間違っているため、Petalinuxビルド時に生成されるブートローダが破損し、ブートできませんので修正します。

数日はまったのですが、petalinuxを手順通りビルドしてもブートしないし、シリアルに何も出ないという謎現象に見舞われました。

あれこれ試した結果、プリビルドされたFSBLのelfを使ってビルドすれば動いたので、これを使うことにしようと思ったところ

> **罠です!**
FSBLはPSの初期化を行うので、AXIのビット幅が合わない可能性があります。

という罠に見事はまり、DPUがうまく動かないという現象に追加で数日悩みました。（そしてFSBLで何をやってるのかを知るという。）

仕方がないのでFSBLのソースを調べで原因を調査したところxsaに含まれているpsu_init.cを差し替えると動いたので、どこが原因か探りました。

結果

DDRメモリのアドレス線に流す信号をどうするかの設定が間違っていて（自動生成）、メモリと通信でいていなかったみたいです。（わかるかよ）


xsaファイルはzipファイルなので拡張子を書き換えてpsu_init.cを編集し

```
PSU_DDR_PHY_PGCR0_ADCP 0x01
```

としてください。
また、**動いているからって他環境のFSBLのelfは流用しない**でください。闇が深まります。

> このあたりでやめたくなりました。
???「**撤退は許可できない**」

# Petalinux

続けて、FPGA（のARM）で動かすLinuxのビルドを行います。

## 事前準備
> **罠です!**
2019.2のPetalinuxはインストールされるファイルが一部間違っているため手動修正が必要です。（arduinoのパッケージの配置場所URL-使わないから無視してくれればいいのに...)

https://github.com/Xilinx/meta-petalinux/issues/12 のissueに従って、petalinuxのインストール先に保存されているarduino-toolchain_1.0.bbを修正します。

また、Petalinuxが使えるように作業環境のセットアップを行っておきます。([UG 1144 2章参照](https://www.xilinx.com/support/documentation/sw_manuals_j/xilinx2019_2/ug1144-petalinux-tools-reference-guide.pdf))

パスを通すのは例えば以下のようなコマンドを叩いてパスを通しておきます。

```bash
source /opt/Vitis/2019.2/settings64.sh 
source /opt/Vivado/2019.2/settings64.sh 
source /opt/petalinux/settings.sh
```


## プロジェクトの作成

```bash
petalinux-create -t project -s <path to bsp> --name <project name>
```
このコマンドでBSPファイルを利用したpetalinuxのプロジェクトが生成され、--nameで指定したフォルダーに格納されます。

また、ドライバー類をビルドできるように https://github.com/xelalin/Ultra96v2-DPU からファイルをコピーします。

```bash
git cloen git@github.com:xelalin/Ultra96v2-DPU.git
cd <project name>
# dpu+dnndk driver
cp -rp ../Ultra96v2-DPU/files/recipes-apps/dnndk/ project-spec/meta-user/recipes-apps/
cp -rp ../Ultra96v2-DPU/files/recipes-modules/ project-spec/meta-user/
# autostart.sh
cp -rp ../Ultra96v2-DPU/files/recipes-apps/autostart/ project-spec/meta-user/recipes-apps/
# fstab kernel moduleの追記
cp -rp ../Ultra96v2-DPU/files/recipes-core/base-files/ project-spec/meta-user/recipes-core/
```

> **罠です!**
petalinuxの仕様が変わったのか上記のrecipes-apps/dnndk/dnndk.bbだと、ファイルシステムにヘッダー類が配置されないため、修正が必要です。
[こちらに修正したものを置いておきました。](https://gist.github.com/mmitti/664a99743e4b491ee47d1f1353995265)

追加したドライバー類をビルドできるようにコンフィグファイルに追記します。

> **罠です!**
2019.2からビルド対象のモジュールを変更するコンフィグファイルが変わりました。
([UG 1144 P116参照](https://www.xilinx.com/support/documentation/sw_manuals_j/xilinx2019_2/ug1144-petalinux-tools-reference-guide.pdf))
> これも数日かかったよ...まずはドキュメントを読もうね


```bash:project-spec/meta-user/conf/user-rootfsconfig
CONFIG_dpu
CONFIG_autostart
CONFIG_dnndk
```

## プロジェクトの初期化
xsaファイルを使ってプロジェクトを初期化します。

```bash
cd <project name>
petalinux-config --get-hw-description=<xsaファイルが入っているフォルダー>
```

しばらくするとコンフィグ画面が出るので

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140084/9ba5cb15-8788-b458-28a8-bfb26d508dfb.png)

「Image Packaging Configuration」＞「RootFilesystem formats」にext4を追加してsave&exit
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140084/70f9db3b-1b9f-3a6e-3fb7-07a1256e3712.png)



## rootfsの設定
Linuxのファイルシステムに含めるファイル（ライブラリとか）の設定です。

```bash
cd <project name>
petalinux-config -c rootfs
```

しばらくすると設定画面が表示されるので、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140084/9c538fc5-7306-712e-bf23-b101206aa075.png)

以下の項目にチェックを入れてファイルシステムに含めるようにします。

```terminal
Filesystem Packages>libs
-libmali-xlnx

Petalinux Package Groups
-matchbox
-opencv
-python-modules
-v4lutils
-x11

apps
-autostart

mdules
-dpu

user packages
-dnndk
```

これらにチェックを入れたらsave&exit

ビルド用のスクリプトの確認作業でしばらく時間がかかるので終わるまで待ちます。

## カーネルの設定
Linuxカーネルの設定です。

```bash
cd <project name>
petalinux-config -c kernel
```
設定画面が(なぜか別タブで)表示され、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140084/e0565851-bd4f-3fec-bbe6-54cc4897ffd4.png)

とくになければこのままsave&exitなのですが...

> **罠です!**
PetalinuxのバージョンによりなぜかDMA用の予約領域の大きさが1GBになっており、Ultra96のメモリ範囲を超えてDMA用予約領域が0でブートしDPUのドライバーが動かなくなるという問題が起きたため、DMA用予約領域のサイズを変更する必要があります。
てか、ブートするのか...(警告は出たよ)

という罠にまた、1日はまったので、DMA用予約領域のサイズを変更します。
petalinux-configを叩いた後、設定画面が表示されるので(なお、なぜか別タブで開きます。)

「Device Drivers」＞「Generic Driver Options」>「Size in MegaBytes(DMA)」を256MBなどにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140084/076de6f7-610b-ae88-a27e-915dc86347f9.png)

DMAのサイズを確認・修正したらsave&exit

## デバイスツリー
DPUのドライバーから回路を扱えるように回路側の情報を登録します。

```py:project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
# amba_bl内に追記
amba_bl{
  dpu {
   compatible = "xilinx,dpu";
   base-addr = <0x80000000>;
   dpucore {
    compatible = "xilinx,dpucore";
    interrupt-parent = <&gic>;
    interrupts = <0x0 89 0x4>;
    core-num = <0x1>;
   };
  };
};

```

ベースアドレスはVivadoのブロックデザインのAddress Editorで設定しいたDPUのAXI LiteのOffset Address、interruptsはPS-PL IRQ0の0bit目は89らしいので（ドキュメントは見つけてないです）、89番に、0x4はactive highの時に割り込みを入れる設定らしいです。

> **罠です!**
IP ドキュメント([PG338](https://japan.xilinx.com/support/documentation/ip_documentation/dpu/v3_0/j_pg338-dpu.pdf))のデバイスツリーのサンプルで割り込みはlow to high edge(0x1)になっていますが、動きませんでした。ドキュメントに書いてある**「固定値であり変更する必要はありません」**は[本部の罠](https://dic.nicovideo.jp/a/%E6%9C%AC%E9%83%A8%E3%81%AE%E7%BD%A0)。


## ビルド

```bash
cd <project name>
petalinux-build
```

このコマンドでビルドが走ります。ひたすら待ちます(1~2時間くらい)
Petalinux本体の修正をしていないとここでこけます。

## ブートイメージの作成
ビルドが完了すると<project name>/images/linuxいかにファイルシステムやブートローダーやらが生成されるので、統合します。

```bash
cd <project name>
petalinux-package --boot --force --u-boot
```

FSBLやbitstreamだけ差し替える場合は

```bash
petalinux-package --boot --force --fsbl <fsbl.elf> --fpga <system.bit> --u-boot
```
で指定できます。

## イメージの書き込み

今回はVM上で作業しているため、ループデバイスを作成し、SDイメージを作りました。
SDイメージを作成してパーティションを切ってfilesystemの書き込みとブート用ファイルの書き込みを行います。

```bash
cd <project name>
$ dd if=/dev/zero of=sd.img bs=1M count=8096
$ sudo losetup  -f sd.img 
$ sudo parted /dev/loopxx
====
mklabel msdos
mkpart primary fat32 0% 8%
mkpart primary ext4 8% 100%
q
===
$ sudo mkfs.vfat -F 32 /dev/loopxxp1
$ dd if=images/linux/rootfs.ext4 of=/dev/loopxxp2 bs=4096
$ sync
$ sudo resize2fs /dev/loopxxp2
$ sync
$ mount /dev/loopxxp1 <mnt>
$ cp images/linux/BOOT.BIN images/linux/system.dtb images/linux/image.ub <mnt>
$ umount <mnt>
$ losetup -D
```

最後に出来上がったsd.imgを適当なライターでSDカードに書き込めばブートします。

# テスト

DNNDKに含まれているresnetなどを実機に入れてビルドして動作確認を行います。なお、調子に乗ってクロックを上げまくると電力制約以下でも発熱量が増え、排熱をする必要が出ます。（冷却が追いつかないと70度でリセットが入ります。）

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">エッジコンピューティングの闇 <a href="https://t.co/uA9SvNYwGK">pic.twitter.com/uA9SvNYwGK</a></p>&mdash; mmitti (青ペンギン) (@mi_mmitti) <a href="https://twitter.com/mi_mmitti/status/1244623496417697792?ref_src=twsrc%5Etfw">March 30, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

4/30追記
ボードの電力制限は素の状態で3A、コンテスト提供ボードは改造されて（ファームウェアの書き換え）5Aです。
加えて温度の制限もあるのですが、当初電力制限しか思い当たるものがなかったのでVivado上で2Aなのになぜか落ちる。これが突入電流かとか、闇の中で手探り状態でした。
まぁ、原因も表示せずいきなりリセットが入るので、いっそことリセット線を切ってしまおうかとも考えていましたが、後輩が温度じゃない？と言っていたので試しに表示して見たらビンゴでした（締め切り2日前)

# 終わりに

数々の罠にはめられて、何とかDPUが動く環境を作りました。
後はモデル班(お友達)が高速で精度もそこそこなモデルを作ってくれれば優勝です。

え、動いていたモデル一式紛失しただと

**青ペンギンは匙を投げたのだ!**

# 免責
本記事には**1ペンギンの個人的な見解**を含み、こちらの内容を実装・実施した上で起きた損害等及び内容は一切保証しません。また、本記事に関するお問い合わせ等は受け付けませんのであしからず。（もうVitisAI触りたくない）

# 参考
https://xelalin.github.io/2019-10-10/%E5%A6%82%E4%BD%95%E5%9C%A8Ultra96v2%E4%B8%8A%E6%95%B4%E5%90%88DPU%E5%8F%8A%E5%AE%89%E8%A3%9DDNNDK
https://qiita.com/AngryMane/items/61d2fa47246a9f9217f5
https://qiita.com/daiki0321/items/fee01d227271cbecdea3
https://marsee101.blog.fc2.com/blog-entry-4664.html