AIエッジコンテスト(実装コンテスト)のチュートリアル【10: HWをPythonで制御する・・が、しかし、、】

ようやく長い道のりを経て準備が終わりました。Ultra９６V2ボードで設計した畳み込み回路を動かしてみましょう！

### 必要なファイルをUltra96V2に転送
前回（[AIエッジコンテスト(実装コンテスト)のチュートリアル【9: HW合成してビットストリームを生成するまで】](https://qiita.com/HirokiNakahara/items/7de1099717db960e5251))で以下のファイルを作成したはずです。

+ pynq_ultra96_conv_l0_r1.bit
+ pynq_ultra96_conv_l0_r1.tcl
+ pynq_ultra96_conv_l0_r1.hdf
+ pynq_ultra96_conv_l0_r1.hwh

第３回の([AIエッジコンテスト(実装コンテスト)のチュートリアル【3: Ultra96ボードのCPUで推論実行】](https://qiita.com/HirokiNakahara/items/20742dc7d2a5be209b23))を参考にしてこれらのファイルをUltra96V2ボード上の`/home/xilinx/pynq/overlays/base`に置いてください。

また、これまで使ってきたテストベンチ

+ testbench_input.txt
+ testbench_output.txt

を`/home/xilinx/data`に置いてください。

[ハードウェアを制御するノートブック](https://github.com/HirokiNakahara/FPGA_AI_Edge_Contest_2019/blob/master/Inference_PYNQ_1/ultra96v2_pynq_convolution_layer0.ipynb)をチュートリアルのリポジトリ(`https://github.com/HirokiNakahara/FPGA_AI_Edge_Contest_2019/blob/master/Inference_PYNQ_1`)に置いています。クローンして、Ultra96V2のホーム`/home/xilinx`に置いてください。あとからUltra96V2のJupyter Notebookで読み込みます。

### いよいよ推論をハードウェアで実行
Ultra96V2のJupyter Notebookにブラウザから接続します。第３回を参考にしてください。
<img width="836" alt="スクリーンショット 2020-03-20 11.21.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589480/f9726bdb-5aa7-5e81-8433-d08c9ed1e3b6.png">
Uploadをクリックしてノートブック（`ultra96v2_pynq_convolution_layer0.ipynb`）を読み込んで実行します。あとは上から実行していくと準備、推論実行（ただし遅い）、比較のためのCPU上での推論がそれぞれ行われます。

以下、要点を絞って解説します。

```python:ultra96v2_pynq_convolution_layer0.ipynb
from pynq import Overlay
import pynq

overlay = Overlay('/home/xilinx/pynq/overlays/base/pynq_ultra96_conv_l0_r1.bit')
dir(overlay)
```

PYNQはoverlayという概念でハードウェアを抽象化します。表示してみるとわかりますが、前回のIP接続で使ったコアの名前（kernel_0とかaxi_dma_0）がいくつか出ていると思います。これにアクセスして操作を行います。


```python:ultra96v2_pynq_convolution_layer0.ipynb
registers = overlay.kernel_0.register_map
```
例えば、ユーザのIPコアにPythonで制御するにはregister_mapにアクセスすれば可能です。今回はプラグマでAXIストリームを指定していますので、その操作が簡単に可能です！これはすごい（AXIのバスをRTLで書いたことがある人にとっては）。

DMAの設定ですが、

```python:ultra96v2_pynq_convolution_layer0.ipynb
import pynq.lib.dma

dma = overlay.axi_dma_0
```

とオーバーレイにアクセスして

```python:ultra96v2_pynq_convolution_layer0.ipynb
from pynq import Xlnk

inimg_size = 416*11*3
outfmap_size = 102*64+1

xlnk = Xlnk()

send_buf   = xlnk.cma_array(shape=(inimg_size),dtype=np.int32)
recv_buf = xlnk.cma_array(shape=(outfmap_size),dtype=np.int32)
```

と`Xlnk()`(Xilinx社が設計したDMA制御ミドルウェアのラッパ)を読んで、配列を確保しておしまいです。楽勝。

ハードウェアのデータ転送と受信ですが

```python:ultra96v2_pynq_convolution_layer0.ipynb
%%time
for line in range(102):
    # load input image
    for i in range(11):
        inimg_buf[i] = inimg[i+line*4]
    
    tmp = inimg_buf.copy().transpose((2,0,1)).reshape(-1,) # CH,Y,X
    send_buf[0:inimg_size] = tmp[0:inimg_size]

    # activate DMA
    registers.CTRL.AP_START = 1

    # DMA access
    dma.sendchannel.transfer(send_buf)
    dma.recvchannel.transfer(recv_buf)

    # wait DMA
    dma.sendchannel.wait()
    dma.recvchannel.wait()
    
    # store output buffer
    tmp2 = recv_buf[0:outfmap_size - 1]
    tmp2 = tmp2.reshape((64,102)) # CH, X
    outfmap_buf[line] = tmp2
```

numpyの配列（ここではinimg）を渡してあげて転送開始のレジスタをONに設定（`AP_START`）します。あとは`transfer`にバッファを渡して転送（すなわち畳み込み演算の処理）が終わるまで待ちます（`wait`）。その後、該当するデータをnumpyの配列に渡してあげれば終了。これを出力のライン分繰り返します。

時間を`%%time`で計測しました。Jupyter Notebookで外部コマンドを呼ぶ方法ですね。で、どれどれ

```
CPU times: user 22.5 s, sys: 6.85 ms, total: 22.5 s
Wall time: 22.5 s
```

おそい。。。やっぱり22秒とHLSの見積もりは正確でした。。。
この後に検証もしています。一応動かして正しくHWが動いていることも確認してください。

### おまけ。CPU上の推論時間は？
ついでにPytorchをインストールしているはずなので、CPU推論時間を確認してみましょう。

```python:ultra96v2_pynq_convolution_layer0.ipynb
import torch
x = torch.randn(1,3,416,416)
conv = torch.nn.Conv2d(in_channels=3, out_channels=64, kernel_size=11,stride=4,bias=False)
y = conv(x)
```

```
CPU times: user 259 ms, sys: 7.96 ms, total: 267 ms
Wall time: 93.2 ms
```

え、約100倍速い。。。どーすんの、これ。
（ということが割とよくおきます＞FPGA設計）

### どうするんだよ
ということで一通りPytorchの学習→ソフトウェア設計→ハードウェア設計→FPGAで実際に動作、までを一通りやってみましたが、結果は散々でした。。いかにハードウェア設計のハードルが高いか、ましてはディープラーニングだったら、、ということが理解できたと思います。

このままだと流石にまずいので、とりあえずもうちょっと頑張って速くしてみましょうかね。
ということでもうちょっと続くのじゃ。