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


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

必要なファイルをUltra96V2に転送

前回(AIエッジコンテスト(実装コンテスト)のチュートリアル【9: HW合成してビットストリームを生成するまで】)で以下のファイルを作成したはずです。

  • pynq_ultra96_conv_l0_r1.bit
  • pynq_ultra96_conv_l0_r1.tcl
  • pynq_ultra96_conv_l0_r1.hdf
  • pynq_ultra96_conv_l0_r1.hwh

第3回の(AIエッジコンテスト(実装コンテスト)のチュートリアル【3: Ultra96ボードのCPUで推論実行】)を参考にしてこれらのファイルを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のホーム/home/xilinxに置いてください。あとからUltra96V2のJupyter Notebookで読み込みます。

いよいよ推論をハードウェアで実行

Ultra96V2のJupyter Notebookにブラウザから接続します。第3回を参考にしてください。

Uploadをクリックしてノートブック(ultra96v2_pynq_convolution_layer0.ipynb)を読み込んで実行します。あとは上から実行していくと準備、推論実行(ただし遅い)、比較のためのCPU上での推論がそれぞれ行われます。

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

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)がいくつか出ていると思います。これにアクセスして操作を行います。

ultra96v2_pynq_convolution_layer0.ipynb
registers = overlay.kernel_0.register_map

例えば、ユーザのIPコアにPythonで制御するにはregister_mapにアクセスすれば可能です。今回はプラグマでAXIストリームを指定していますので、その操作が簡単に可能です!これはすごい(AXIのバスをRTLで書いたことがある人にとっては)。

DMAの設定ですが、

ultra96v2_pynq_convolution_layer0.ipynb
import pynq.lib.dma

dma = overlay.axi_dma_0

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

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制御ミドルウェアのラッパ)を読んで、配列を確保しておしまいです。楽勝。

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

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推論時間を確認してみましょう。

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で実際に動作、までを一通りやってみましたが、結果は散々でした。。いかにハードウェア設計のハードルが高いか、ましてはディープラーニングだったら、、ということが理解できたと思います。

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