ようやく長い道のりを経て準備が終わりました。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上での推論がそれぞれ行われます。
以下、要点を絞って解説します。
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)がいくつか出ていると思います。これにアクセスして操作を行います。
registers = overlay.kernel_0.register_map
例えば、ユーザのIPコアにPythonで制御するにはregister_mapにアクセスすれば可能です。今回はプラグマでAXIストリームを指定していますので、その操作が簡単に可能です!これはすごい(AXIのバスをRTLで書いたことがある人にとっては)。
DMAの設定ですが、
import pynq.lib.dma
dma = overlay.axi_dma_0
とオーバーレイにアクセスして
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制御ミドルウェアのラッパ)を読んで、配列を確保しておしまいです。楽勝。
ハードウェアのデータ転送と受信ですが
%%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推論時間を確認してみましょう。
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で実際に動作、までを一通りやってみましたが、結果は散々でした。。いかにハードウェア設計のハードルが高いか、ましてはディープラーニングだったら、、ということが理解できたと思います。
このままだと流石にまずいので、とりあえずもうちょっと頑張って速くしてみましょうかね。
ということでもうちょっと続くのじゃ。