AIエッジコンテスト(実装コンテスト)のチュートリアル【6: C++モデルの作成と検証】

前回はPyTorchを操作して学習済みモデルから重みとバイアス、第１層の入出力を読み出しました。今回はその値を使ってハードウェア化する畳み込み層のC++モデルを設計し、正しく動作しているか検証しましょう。
Xilinx社もハードウェア合成を行う前に、C++モデルが正しいことを検証するように言っているのですが、意外と検証しないで合成してるんですよね。。そして動作検証（というかハードウェア合成）に時間がかかってしまい、トータルの時間がかかってしまうことよくあります。

### C++モデルを書く
今回のモデルは[githubに上げています。](https://github.com/HirokiNakahara/FPGA_AI_Edge_Contest_2019)前回、リポジトリをクローンした方は`fetch`で更新してください。`Inference_cpp`というディレクトリが取り込まれ、その中に

+ HW_kernel.cpp
+ HW_main.cpp

が置かれているはずです。また、README.mdに学習済みパラメータとテストベンチへのリンクを貼っています。面倒臭いかたはダウンロードしてください。前回をやってみたかたは

+ testbench_input.txt
+ testbench_output.txt
+ weight_l0.txt
+ bias_l0.txt

をこのディレクトリにコピーしてきてください。

![スクリーンショット 2020-03-02 17.15.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589480/872a46a5-688e-b7ce-8ef1-602292be657a.png)

上図に今回設計する第１層の畳み込み演算回路と検証用の構成を示します。高位合成を用いてハードウェア設計する場合、対象の回路は１つとします。今回は`HW_kernel.cpp`ですね。それを検証するという`HW_main.cpp`で対象の回路（つまり関数）を読み出す構成にします。前回生成したテストベンチ、パラメータはそれぞれテキストファイルにしました。ヘッダファイルにしてコンパイル時に読むこむようにすると、FPGAのオンチップメモリの初期値とすることができます。

### ソースコード解説
まずはCNNのソースコード`HW_kernel.cpp`です。率直に積和演算を実装しています。

```cpp:HW_kernel.cpp
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// macro for HLS synthesis
//#define HLS // 高位合成を行うときにuncommentする

#ifdef HLS
#include "ap_int.h" // 可変精度ライブラリ, 高位合成ツール専用
#include "hls_half.h" // 半精度ライブラリ,
typedef float DTYPE; // 後半のリファクタリングでここをいじります
#else
typedef float DTYPE; // ソフトウェア検証用
#endif

void kernel(
#ifdef HLS
	DTYPE src_img[3*416*416],
	DTYPE dst_img[64*102*102],
	DTYPE weight[64*3*11*11],
	DTYPE bias[64]
#else
	DTYPE *src_img,
	DTYPE *dst_img,
	DTYPE *weight,
	DTYPE *bias
#endif
);

// ---------------------------------------------------
// Convolution(3chx416x416) in the 1st layer
// ---------------------------------------------------
void kernel(
#ifdef HLS
	DTYPE src_img[3*416*416],
	DTYPE dst_img[64*102*102],
	DTYPE weight[64*3*11*11],
	DTYPE bias[64]
#else
	DTYPE *src_img,
	DTYPE *dst_img,
	DTYPE *weight,
	DTYPE *bias
#endif
)
{
//Layer0: Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4))

	for( int och = 0; och < 64; och++){
		int dst_y;
		int dst_x;
		dst_y = 0;

		for( int y = 0; y < 416 - 11; y += 4){
			dst_x = 0;

			for( int x = 0; x < 416 - 11; x+= 4){
				float tmp = 0;

				for( int ich = 0; ich < 3; ich++){
					for( int ky = 0; ky < 11; ky++){
						for( int kx = 0; kx < 11; kx++){
							tmp += src_img[ich*416*416+(y+ky)*416+(x+kx)] 
								* weight[och*3*11*11+ich*11*11+ky*11+kx];
						}
					}
				}

				dst_img[och*102*102 + dst_y*102 + dst_x] = tmp + bias[och];

				dst_x++;
			}

			dst_y++;
		}

		//exit(-1);
	}
}
```

ハードウェア化に向けて２点ポイントがあって、プリプロセッサ

```
#define HLS 
```

を用意して、ソフト検証用コードとハードウェア高位合成用の部分を切り分けています。高位合成はポインタはサポートしていないので、配列を用います。ところが、ソフトウェアはデフォルトのままでは大規模な静的配列をコンパイルしてもsegmentation errorになるので（それ以外にも高位合成は`fprintf`とかも使えない、特殊なライブラリのインクルードが必要）、高位合成用を意識して切り替える部分を明示的にプリプロセッサで切り分けておくと後から行うリファクタリング時に便利です。
`DTYPE`でソフトウェア検証とハードウェア合成時に異なる精度を使い分けます。高位合成ツールでもソフトウェア検証ができますが、コンパイルが遅かったり、デバッグインタフェースが貧弱だったりするので、私はUbuntuの`g++`でソフトウェア検証してから高位合成にかけています。

次に、検証を含めた`HW_main.cpp`です。

```cpp:HW_main.cpp
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//#define HLS

#ifdef HLS
#include "ap_int.h"
#include "hls_half.h"
typedef float DTYPE;
#else
typedef float DTYPE;
#endif

void kernel(
#ifdef HLS
	DTYPE src_img[3*416*416],
	DTYPE dst_img[64*102*102],
	DTYPE weight[64*3*11*11],
	DTYPE bias[64]
#else
	DTYPE *src_img,
	DTYPE *dst_img,
	DTYPE *weight,
	DTYPE *bias
#endif
);

//--------------------------------------------------------------------
// Main Function
//--------------------------------------------------------------------
int main( int argc, char *argv[])
{
	int i, ifeat, ofeat, y, x;
	char fname[256];
	FILE *fp, *fp2;

	// malloc buffers
	DTYPE *in_img   = (float *)malloc(416*416*3*sizeof(float));
	DTYPE *out_fmap = (float *)malloc(64*102*102*sizeof(float));
	DTYPE *weight   = (float *)malloc(64*3*11*11*sizeof(float));
	DTYPE *bias     = (float *)malloc(64*sizeof(float));

	// load parameters -----------------------------------------------
	printf("load parameters");
	sprintf( fname, "weight_l0.txt");
	if( (fp = fopen( fname, "r")) == NULL){
		fprintf(stderr, "CAN'T OPEN FILE\n");
		exit(-1);
	}

	sprintf( fname, "bias_l0.txt");
	if( (fp2 = fopen( fname, "r")) == NULL){
		fprintf(stderr, "CAN'T OPEN FILE\n");
		exit(-1);
	}

	for( int och = 0; och < 64; och++){
		float param;
		fscanf( fp2, "%f", &param);
		bias[ och] = (DTYPE)param;//(int)imgval;

		for( int ich = 0; ich < 3; ich++){
			for( int y = 0; y < 11; y++){
				for( int x = 0; x < 11; x++){
					fscanf( fp, "%f", &param);
					weight[ och*3*11*11+ich*11*11+y*11+x] = (DTYPE)param;//(int)imgval;					
				}
			}
		}
	}

	fclose(fp);
	fclose(fp2);

	// load image ----------------------------------------------------
	sprintf( fname, "testbench_input.txt");

	if( (fp = fopen( fname, "r")) == NULL){
		fprintf(stderr, "CAN'T OPEN FILE\n");
		exit(-1);
	}

	printf("load image to in_img\n");
	for( ifeat = 0; ifeat < 3; ifeat++) {
		for( y = 0; y < 416; y++) {
			for( x = 0; x < 416; x++){
				float imgval;
				fscanf( fp, "%f", &imgval);
				in_img[416*416*ifeat+416*y+x] = (DTYPE)imgval;//(int)imgval;
			}
		}
	}
	fclose(fp);

	// Inference ----------------------------------------------------
	printf("start inference\n");
	kernel( in_img, out_fmap, weight, bias);

    // verify output ------------------------------------------------
	if( (fp = fopen( "testbench_output.txt", "r")) == NULL){
		fprintf( stderr, "CAN'T OPEN bench_output.txt\n");
		exit(-1);
	}

	int error = 0;
	for(int x=0; x<64*102*102; x++){
		float mapval;
		fscanf( fp, "%f\n", &mapval);

		if( abs(mapval - out_fmap[x]) > 0.01){
			error = 1;
			fprintf( stderr, "ERROR idx=%d bench=%f test=%f\n", x, mapval, out_fmap[x]);
		}
	}
	fclose(fp);

	if( error){
		fprintf(stderr, "TEST ERROR\n");	
		exit(-1);
	} else {
		printf("TEST PASS\n");
	}

	// output test result
	if( (fp = fopen( "out_cpp.txt", "w")) == NULL){
		fprintf( stderr, "CAN'T OPEN out.txt\n");
		exit(-1);
	}
	for(int x=0; x<64*102*102; x++){
		float mapval = (float)out_fmap[x];
		fprintf( fp, "%f\n", mapval);
	}
	fclose(fp);
	printf("FILEOUT -> out_cpp.txt, please verify to use 'test_yolov2_demo.py'\n");

	free(in_img);
	free(out_fmap);
	return 0;
}
```

1. パラメータ（重み、バイアス）読み出し
2. テストベンチ（入力値）読み出し
3. 推論関数`kernel`実行
4. 出力値取得
5. テストベンチ（出力値）との誤差比較

の順で行っています。また、出力値は`out_cpp.txt`で出力されますので、ハードウェア合成時にホストPCとの検証で使います。

### ソフトウェア検証
ホストPC上で先ほどgithubからcloneしたディレクトリ`Inference_cpp`に移動して

```
$ g++ -O3 -o HW_l0 HW_kernel.cpp HW_main.cpp
```

コンパイル後

```
$ ./HW_l0
```

を実行してください。PyTorchのconvolution層との期待値の比較が行われてテストがパスするはずです。ようやくハードウェアを合成する準備ができました。次回はいよいよ高位合成ツールVivado HLSを使ってハードウェアを合成しましょう。