前回は、Vivado 2019.1 を使用して、もう一度、end2end_example の tfc_end2end_example.ipynb をやり直したところ、 4. PYNQ hardware generation and deployment の Inserting the IP into a PYNQ Overlay Shell で Vivado プロジェクトが生成された。今回は、 4. PYNQ hardware generation and deployment の Driver Generation からやってみよう。
今回も end2end_example の tfc_end2end_example.ipynb の図や文章の翻訳、コードを引用して勉強していく。
Driver Generation
ネットワークのビットファイルを合成したので、このビットファイルのドライバーとして機能するPYNQのPythonコードを生成し、すべてを展開フォルダーにパッケージ化して、PYNQボードにコピーする。
生成されたドライバーは、pynq_driver_dirトップレベルメタデータで示されるフォルダーに配置される。生成されたPYNQ Pythonドライバーコードを次のように確認できる。
実際のドライバコードは、/tmp/finn_dev_masaaki/pynq_deployment_0fgmr9qo にあって、そのディレクトリには、 driver.py, finn ディレクトリ、 input.npy, resizer.bit, resizer.hwh ファイルがある。
driver.py を引用する。
import argparse
from pynq import Overlay
import numpy as np
from pynq import allocate
import time
from finn.util.data_packing import (
finnpy_to_packed_bytearray,
packed_bytearray_to_finnpy
)
from finn.core.datatype import DataType
class FINNAccelDriver():
def __init__(self, N, bitfile):
"""Instantiate the FINN accelerator driver.
Gets batchsize (N) as integer and path to bitfile as string."""
self.N = N
# input FINN DataType
self.idt = DataType.BINARY
# output FINN DataType
self.odt = DataType.UINT32
# input and output shapes
self.ishape_normal = (N, 784)
self.oshape_normal = (N, 10)
self.ishape_folded = (N, 16, 49)
self.oshape_folded = (N, 1, 10)
self.ishape_packed = (N, 16, 7) # datatype np.uint8
self.oshape_packed = (N, 1, 40) # datatype np.uint8
# load bitfile and set up accelerator
self.ol = Overlay(bitfile)
self.dma = self.ol.axi_dma_0
self.ctrl_regs = self.ol.resize_accel_0
# neuron folding factor of output = iterations per sample
self.itersPerSample = self.oshape_packed[-2]
# AXI lite register offset for number of iterations
# used by TLastMarker to signal end of transmission for AXI CDMA
self.REG_OFFSET_NUM_ITERS = 0x10
# set up TLastMarker with correct num. samples
self.ctrl_regs.write(self.REG_OFFSET_NUM_ITERS, self.N*self.itersPerSample)
# allocate a PYNQ buffer for the packed input and buffer
self.ibuf_packed_device = allocate(shape=self.ishape_packed, dtype=np.uint8)
self.obuf_packed_device = allocate(shape=self.oshape_packed, dtype=np.uint8)
def fold_input(self, ibuf_normal):
"""Reshapes input in desired shape.
Gets input data (ibuf_normal), checks if data is in expected normal shape.
Returns folded input."""
# ensure that shape is as expected
assert ibuf_normal.shape == self.ishape_normal
# convert to folded form
ibuf_folded = ibuf_normal.reshape(self.ishape_folded)
return ibuf_folded
def pack_input(self, ibuf_folded):
"""Packs folded input and reverses both SIMD dim and endianness.
Gets input data in folded shape and returns packed input data."""
ibuf_packed = finnpy_to_packed_bytearray(
ibuf_folded, self.idt, reverse_endian=True, reverse_inner=True
)
return ibuf_packed
def unpack_output(self, obuf_packed):
"""Unpacks the packed output buffer from accelerator.
Gets packed output and returns output data in folded shape."""
obuf_folded = packed_bytearray_to_finnpy(
obuf_packed, self.odt, self.oshape_folded, reverse_endian=True, reverse_inner=True
)
return obuf_folded
def unfold_output(self, obuf_folded):
"""Unfolds output data to normal shape.
Gets folded output data and returns output data in normal shape."""
obuf_normal = obuf_folded.reshape(self.oshape_normal)
return obuf_normal
def copy_input_data_to_device(self, data):
"""Copies given input data to PYNQ buffer."""
np.copyto(self.ibuf_packed_device, data)
def execute(self):
"""Executes accelerator by setting up the DMA and
waiting until all transfers complete. Uses only member variables and
returns nothing."""
dma = self.dma
dma.sendchannel.transfer(self.ibuf_packed_device)
dma.recvchannel.transfer(self.obuf_packed_device)
dma.sendchannel.wait()
dma.recvchannel.wait()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Set exec mode, batchsize N, bitfile name, inputfile name and outputfile name')
parser.add_argument('--exec_mode', help='Please select functional verification ("execute") or throughput test ("throughput_test")', default="execute")
parser.add_argument('--batchsize', help='number of samples for inference', type=int, default=1)
parser.add_argument('--bitfile', help='name of bitfile (i.e. "resizer.bit")', default="resizer.bit")
parser.add_argument('--inputfile', help='name of input npy file (i.e. "input.npy")', default="input.npy")
parser.add_argument('--outputfile', help='name of output npy file (i.e. "output.npy")', default="output.npy")
# parse arguments
args = parser.parse_args()
exec_mode = args.exec_mode
N = args.batchsize
bitfile = args.bitfile
inputfile = args.inputfile
outputfile = args.outputfile
# instantiate FINN accelerator driver and pass batchsize and bitfile
finnDriver = FINNAccelDriver(N, bitfile)
# for the remote execution the data from the input npy file has to be loaded,
# packed and copied to the PYNQ buffer
if exec_mode == "execute":
# load desired input .npy file
ibuf_normal = np.load(inputfile)
ibuf_folded = finnDriver.fold_input(ibuf_normal)
ibuf_packed = finnDriver.pack_input(ibuf_folded)
finnDriver.copy_input_data_to_device(ibuf_packed)
elif exec_mode != "throughput_test":
raise Exception("Exec mode has to be set to remote_pynq or throughput_test")
# for the throughput test the runtime of the network has to be measured
if exec_mode == "throughput_test":
# measure runtime of network
start = time.time()
# dictionary for results of throughput test
res={}
# execute accelerator
finnDriver.execute()
# measure run time and fill dictionary with results of the throughput test
if exec_mode == "throughput_test":
end = time.time()
runtime = end - start
res["runtime[ms]"] = runtime*1000
res["throughput[images/s]"] = N / runtime
res["DRAM_in_bandwidth[Mb/s]"] = np.prod(finnDriver.ishape_packed)*0.000001 / runtime
res["DRAM_out_bandwidth[Mb/s]"] = np.prod(finnDriver.oshape_packed)*0.000001 / runtime
file = open("nw_metrics.txt", "w")
file.write(str(res))
file.close()
# if execution is selected unpack, unfold and save output to output npy file
else:
obuf_folded = finnDriver.unpack_output(finnDriver.obuf_packed_device)
obuf_normal = finnDriver.unfold_output(obuf_folded)
np.save(outputfile, obuf_normal)
生成されたドライバーには、FINNアクセラレーターを実装するクラスが実装されていることがわかる。コンストラクタは、バッチサイズ(N)を整数として、ビットファイルを文字列として取得する。また、予想される入力/出力形状も含まれており、ビットファイルをロードしてdmaとバッファーを設定することにより、アクセラレーターのインスタンス化を処理する。いくつかのメンバー関数がデータの折りたたみとパッキングを処理する。関数copy_input_data_to_deviceは、入力データをPYNQバッファーにコピーして実行し、dmaチャネルをセットアップして、転送が完了するまで待機する。このクラスはmain関数で使用される。ただし、最初に引数が解析され、スクリプトに渡される。このドライバーは、「execute」と「throughput_test」の2つのモードで使用できる。デフォルトでは、すべての引数が「execute」モードに設定されている。このモードでは、バッチサイズは1であり、渡されたファイルはFINN変換で使用される名前に設定される。
「execute」モードでは、次のように機能する。
1. データは 「inputfile」 からロードされる
2. データは fold_input を使用して折りたたまれる
3. データは pack_input を使用してパックされる
4. データは copy_input_data_to_device を使用してデバイスにコピーされる
5. FINNAccelDriver は、 execute で実行される
6. データは unpack_output で解凍される
7. データは unfold_output で展開される
8. データは 「outputfile」 に保存される
「throughput_test」がexec_modeとして選択されている場合、実際のデータをロードする必要はない。バッチサイズNは高い値(つまり1000)に設定する必要があり、時間測定はPythonで実装される。空の辞書(res)が作成され、測定されたランタイムでアクセラレーターを実行した後、メトリックが入力され、.txtファイルに保存される。
ドライバーを変更してアクセラレーターを中心に独自のアプリケーションを構築するか、FINNが提供するリモート実行機能を使用して、それが機能しているかどうかを確認することができる。