- はじめに
- Summary
- 環境構築
- Microsoft Azure
- iPadでAzureのRDP
- Ver.0:6重ループのまま合成
- Ver.1:多重化してみる
- Ver.2:ラインバッファを入れる
- Ver.3:パイプライン化
- Ver.4:PL多重化
- Ver.5:さらに多重化する
- まとめ
- おまけ:Vitis HLS と Vivado HLS
- ソースコード
- LeNet.h
- LeNet.cpp
- LeNet_tb.cpp
はじめに
気がつくともう周りは全部ニューラルネットワークと機械学習、というご時世ですね。その流れを多少なりともキャッチアップすべく、画像処理系のモデルでよく使われるCNNのHW化を題材に少し勉強してみました。
ターゲットとして古典的な所でLeNetの畳み込み層(C3)を選択。練習台であまり大きなものを作ってもしょうがないので。

こいつは入力画像サイズが14×14、前層のプレーン数(以下CH)が6、カーネルが5×5、計算するプレーン数(以下PL)が16プレーン存在しますので、それを高位合成でHW化するとどうなるか、というトライです。
ちなみにこのカーネルサイズは今時では無い、らしいです。3×3とかの小さいカーネルを重ねる方が現在の主流っぽいです。演算回数が減るというのが理由の一つだそうですが、HWに実装する場合はどうでしょうか。ネットワークの層数が増えるとその分データパスが増えてしまうので微妙な所だと思います。
で、まずはテストベンチ用のリファレンス関数を作ります。単純な6重ループ。
これを基準として計算結果が間違っていないことを確認しつつ、高位合成コードを作って行くという流れです。
Summary
| \ | Ver.0 | Ver.1 | Ver.2 | Ver.3 | Ver.4 | Ver.5 |
|---|---|---|---|---|---|---|
| Latency (cyc) (ms) | 2,769,954 9.2 | 273,602 0.91 | 1,637,479 5.5 | 240,026 0.80 | 47,056 0.16 | 22,558 0.075 |
| Interval (cyc) (ms) | 2,769,954 9.2 | 273,602 0.91 | 1,637,480 5.5 | 240,012 0.80 | 29,409 0.097 | 17,822 0.059 |
| BRAM | 6 | 6 | 12 | 7 | 35 | 195 |
| DSP | 1 | 1 | 1 | 151 | 16 | 96 |
| FF (HLS) (Impl.) | 1,347 1,614 | 1,512 1,600 | 8,206 3,283 | 17,092 9,178 | 9,128 9,832 | 24,205 25,014 |
| LUT (HLS) (Impl.) | 1,992 972 | 2,241 1,009 | 13,472 10,401 | 15,191 5,074 | 31,896 5,788 | 62,002 16,523 |
環境構築
使用するツールは Vivado 2020.1 を選択しました。ターゲットデバイスが小さければ高位合成まで無料で使えてしまいます。
こいつを自宅デスクトップにインストールして…まあ普通に使えます。思ったほど遅く無い。
が、家庭内の諸事情により、手持ちのiPadでも作業がしたいな〜と、わがままな要求を自分にしてみます。当然、VivadoのiOS版は無いです。さて困った。
ぱっと思いつく方法としては
・ノートPCを買い足す
・自宅PCをRDPで使う
・クラウドの仮想マシンを用意して、RDPで使う。
といったところですが、元のPCを家族でシェアできて、あまり金も掛からなくて、外でも作業ができて、となると・・・
一つしか解が無いよなと、適当に自分の中で理由をつけて、クラウドサービスもついでにトライしてみることにしました。
Microsoft Azure
無料アカウントで、1ヶ月はそこそこ好き放題使えるらしい。ということで早速アカウントを作成してみました。
パブリッククラウドサービスなんて初めて使うわけですが、会社で管理しているオンプレのVMwareとそう変わらんでしょ、とイージーにトライ。
まず選択できる仮想マシンの構成(Microsoft用語で「サイズ」)が多様すぎて、頭が痛くなる。
ツール動かすのに8GBでなんとかなるか。足りなそうなら後で増やせばいいし。このあたりは仮想マシンサービスのいいところですね。
でそこそこ早くて値段は手頃で ということで、D2ds_v4 を選択。
ちなみに「サイズ」という用語にはどうも馴染めないです。
Windows server datacenter(!) の構成でデプロイ。これだとOSのライセンスに悩む必要が無い(VM利用料に上乗せ)。別にWindows10proでよかったんですが、こっちは別途ライセンスが要る?ような気がする。Microsoftのライセンス体系は複雑でよくわからん。
次に、XilinxからVivadoをインストール。ダウンロードが案外遅いです。帯域制限されてるのかな?動かすのは普通にOK
次に、自宅PCとのファイル共有用にOneDriveをインストール。Server版はデフォルトでは入ってないんですな。ちょっと芸の無い選択ですが、この程度の用事になら手軽で良いです。
一通りセットアップができた所でストレージのスナップショットを取っておきます。ちなみにスナップショットはストレージとは別の課金対象らしい…。
で特にハマる事もなく、普通に使えるようになりました。会社のリモートワーク用RDPより断然レスポンス速いな。
iPadでAzureのRDP
ノートPCを買い足さずに手持ちのiPADでRDPを使いたい。
結論から言うと、意外とあっけなく動きました。
Azureのポータルから、言われるままにAzureアプリとRD Clientをインストール。そして「接続」をポチればそこには もうWindows on iPad の世界。
これで、ガストにiPad持ち込んでWindowsの仕事をする(無料WiFi&ドリンクバー飲み放題)という一昔前なら夢のような環境が、いとも簡単にできてしまいました。
実際の運用としては、キーボードとマウス、それからVPN を忘れずに。
マウスで右クリック操作ができないのが一番の難点ですね。(画面の長押しで右クリックになるが、マウス長押しはドラッグ操作と解釈されて右クリックにならない。また逆に、画面ではドラッグ操作ができない。たぶん)
Ver.0:6重ループのまま合成
さて、ようやく高位合成の話に入ります。
とりあえずの仕様として
動作デバイス:Zynq MPSoC
動作周波数:300Mhz
入出力IF:AXIマスター
kernelの値:AXI-liteスレーブのレジスタ
としました。入出力はストリーム化も検討したいところですが、まずはシンプルに合成できる構成にします。
まず最初に6重ループの構造を全く変更せずに合成してみます。
インターフェース合成が優秀なので、ソースコードに全く手を入れなくても合成が通って動く物が出来てしまいます。恐ろしや。
合成結果を見てみると、1回の積和演算に11クロックかかっています。そのほとんどがメモリリードのバスレスポンス待ちです。読み書き先にDRAMを使う場合はもっと遅くなるかもしれません。
これで約9ms。これを基準として色々といじっていきます。
Ver.1:多重化してみる
ここで多少はHWらしく、多重化やパイプライン化をしてみます。6重ループのコード構造はまだ変えません。
ためしにループのUNROLLをかけてみますが、乗算器の使用数ばっかり増えて全然速くなりませんでした。計算の順序依存性が足を引っ張っていると言うことでしょう。
ではPIPLINEはどうか。これはカーネルの計算部分に効果がありました。
0.9ms。1行追加しただけですがそこそこ速くなりました。積和演算部分のインターバルも1になっています。
ただし、パイプライン指定する対象のループの選択を間違えるとインターバルが2や3になったり乗算器が無意味に増えたりして、かなり繊細です。
また、6重ループ構造の宿命で、同じ画素データを何回もバスから参照しています。波形シミュレーションの結果を見ると、AXIバスの使用率はざっくり25%程度でした。
ロジックがコンパクトであることと、開発に全く手間がかからないのはそれなりのメリットなので、メモリ帯域に余裕があるシステムであればこのまま使用するのもアリですね。
Ver.2:ラインバッファを入れる
次は、HWへの実装らしく、ラインバッファを入れてみます。
データの参照でメモリアクセスが大量に発生することが許容できない場合や、入力がストリームの場合に選択する構成です。
C言語でラインバッファ構造を作るのはかなり面倒なのですが、今ではxf_video_mem.h にその名もズバリ
xf::LineBuffer
xf::Window
というクラスが用意されています。HWでの一般的な実装とはメモリの使い方がやや異なるのですが、これが使えるのは非常にありがたいです。
普通はラインバッファから切り出したウインドウバッファに対する演算を同じループの中に記述してしまいますが、
・ソースコードがごちゃごちゃして見難くなる
(ループ回数にカーネルの先読み分を増やしたり、ループの最初や最後で演算や入出力をスキップしたり、等が必要です)
・そのごちゃごちゃのせいで演算部分の最適化に制限が生じる場合がある
といったことが起こります。
そこで、プロセスを分離して記述するというワザを使います。具体的には
[ウインドウバッファを切り出すプロセス] ⇒ [カーネルの計算をするプロセス]
と分けて、DataFlowでそれぞれを並列実行させます。
HWでブロックを分けてTOP階層で繋ぐイメージですね。高位合成ではプロセス間をストリームのクラスでつなぎます。普通の変数や配列でも合成できるのですが、うっかりDataFlowを崩してしまうことが多いので私はストリームを使うことにしています。
とりあえずこの状態で合成をかけると、5.5msとなりました。演算に対して全く最適化を行っていないので、まずはこんなもんでしょう。
AXIバスのアクセスはスカスカになりました。使用率は見た目1%以下です。
ただし、その代償としてロジックの使用量は増えています。
Ver.3:パイプライン化
こちらの構成も同様にパイプライン化します。
カーネル計算部内と、ラインバッファのプロセスにも入れます。うまく高速化できる箇所を探すと0.8msまで速くなりました。しかしDSP使用量が150…
要するに、カーネル計算部を全展開したという事ですね。パイプライン化した6重ループと比べてちょっとだけ速いですが、リソース消費量が非常に多いのでこのままでは失敗です。
Ver.4:PL多重化
失敗したままでは引き下がれません。さらにガリガリと手を入れます。ここからが本番。
とは言っても、カーネル計算部(積和演算)はインターバルが1になった後は本質的にあまり速くならないと思われます。乗算機を並列化することはできますが、その後で全部足し込むプロセスのステップ間に結果依存性があるので積極的な並列化が難しいからです。というわけで、上位の階層を並列化することを考えます。まずはPL層のUNROLLですね。
このレベルまでくると、演算の並列化と共にデータフローが大事です。演算に使用するデータのフェッチがボトルネックになるケースが多々あります。
で、ここでプロセス分離した構造が生きてきます。
データのフェッチと演算が同じループを共有していると、UNROLLやPIPELINEをうまく効かせるのが難しくなります。ざっくりいうと「お互い自分のペースで動けない」ということが起こりやすいのです。そこでプロセスが分離されていればそれぞれ勝手に好きなペースで仕事をすることができるので効率的に動けるのです。
具体的には、演算部分を多重化した分だけプロセス間のストリームも多重化します。ただし、入力データのフェッチ自体もソースコード上は多重化しますが合成されたコードはシーケンシャルに実行されます。そもそもの入り口が一つしか無いので意味のある多重化ができないのです。その代わり、演算部と異なるループ構造でデータをフェッチすることができます。
とかなり凝った最適化をかけて、0.16ms。やっとμsのオーダーに入りました。使用リソースは…当然それなりに増えてます。ざっくりと5〜10倍のリソースを使って5倍速くした、という解釈でいいでしょうか。
高速化されたのでAXIバス使用率は少し上がって、見た目3%といった所です。
Ver.5:さらに多重化する
調子に乗ってさらにCHのループも多重化してみます。要領は同じです(というか、実際の作業では先にこちら(PL/CH多重)を作ってから要素を削ってPL単独多重を派生させました)。
特に能書きは無いので結果から…0.075ms。
さらに6倍に多重化したのに2倍「しか」速くなってないという結果です。全くお得感は無いですね。
せっかくインターバル1で調子よく回っている積和演算のループを途中でぶった切ってムリヤリ並列化している格好なのであまり効率が良くない、という所でしょうか。データパスが複雑になってロジックが大幅に増えている点もイマイチです。
まとめ
いくつかの最適化バージョンを作って比較してみました。その中で使えそうなのは
・Ver.1:6重ループ構造+パイプライン化
・Ver.4:プロセス並列化+PL並列化
の2つですね。いずれかを要求要件次第で使い分ける感じです。
パフォーマンスに関して、CPUには十分勝っていると思いますが、GPUと比較したらどうでしょうか。CNN1層単独だと分が悪いかもしれません。ただ、HW実装の真価は多層になった時に各層を同時実行できる、という点です。スループットで勝負、ですね。
MaxPooling層や全結合層を作ってLeNet全体のパフォーマンスを検証するのは今後の宿題としておきます。
単独の最適化検討の方向性もまだあります。
・6重ループ構造のままPL多重化できないか?
・6重ループ構造とラインバッファ構造の中間解は?
こちらも今後の宿題としておきます。
おまけ:Vitis HLS と Vivado HLS
Xilinxのツールセットは2020.1だとVitis HLS と Vivado HLS の2種類の高位合成ツールが存在している(ように見える)。中身は同じ物なのか?少し試してみました。
Vivado HLSでVer.3あたりまで作った所でVitis HLSでプロジェクトを開いてみる。と、プロジェクトをアップグレードするかと聞いてきます。内部のエンジンはともかく一応別物のツールと考えた方が良さそうです。
で、合成をかけてみましたら…
UNROLLもPIPELINEも指定していない設定でも、何かしらの並列化した合成結果が出てきました。どうやら「HWのことが分からなくてもSWで書いたコードをロジックでお手軽にアクセラレートする」がコンセプトのような雰囲気です。
ただ私みたいにガリガリと最適化を目指す場合には、結果が予想できないので使い難いという印象です。またある程度並列化のプラグマが埋まっている状態で合成をかけると、エラーで止まるとか、ハングしてしまうとかの現象もありました。
いずれ置き換えられるのかもしれませんが、私は少し様子見ですね。
インストール時の注意として、最初にVitisをインストールしてしまうとVitisHLSしか表に見えてきません(Windowsの場合)。一度VivadoHLSをインストールしてから改めてVitisを追加インストールすると、両方のツールが使えるようになりました。
ソースコード
LeNet.h
// LeNet.h
// 2020/8
// (C) UNDO / m-ando.com
#include <ap_int.h>
#include <ap_fixed.h>
#include <hls_stream.h>
#define KOFFSET(KSIZE) ((KSIZE - 1) / 2)
#define DSIZE_OUT(DSIZE,KSIZE) (DSIZE - (KSIZE - 1))
// DEBUG
#if 0
#define C3_PL 3
#define C3_CH 2
#define C3_DSIZE 8
#define C3_KSIZE 5
#else
#define C3_PL 16
#define C3_CH 6
#define C3_DSIZE 14
#define C3_KSIZE 5
#endif
#define C3_DSIZE_OUT DSIZE_OUT(C3_DSIZE,C3_KSIZE)
#define C4_DSIZE C3_DSIZE_OUT
typedef ap_fixed< 16, 2, AP_TRN, AP_SAT > DTYPE_REF;
typedef ap_fixed< 32, 16 > ATYPE_REF;
typedef ap_fixed< 16, 2, AP_TRN, AP_SAT > DTYPE_HLS;
typedef ap_fixed< 32, 16 > ATYPE_HLS;
template < typename T_DATA, int NUM_CH, int NUM_KSIZE >
class DWINDOW_T
{
public:
T_DATA d[ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ];
};
void Conv_hls_C3(
DTYPE_HLS inData[ C3_DSIZE ][ C3_DSIZE ][ C3_CH ],
DTYPE_HLS outData[ C3_PL ][ C3_DSIZE_OUT ][ C3_DSIZE_OUT ],
DTYPE_HLS kernel[ C3_PL ][ C3_CH ][ C3_KSIZE ][ C3_KSIZE ] );
// Pipelined MAC II=1
template < typename T_D1, typename T_D2, typename T_ACC,
int NUM >
void MacSt(
hls::stream< T_D1 > &sD1,
hls::stream< T_D2 > &sD2,
hls::stream< T_ACC > &sAcc )
{
T_D1 d1;
T_D2 d2;
T_ACC acc;
acc = 0;
MAC:for( int i = 0 ; i < NUM ; i++ ){
#pragma HLS PIPELINE
d1 = sD1.read();
d2 = sD2.read();
acc += d1 * d2;
}
sAcc.write( acc );
}
template < typename T, int NUM_PL, int NUM_CH >
class Stream2D{
public:
hls::stream< T > st[ NUM_PL ][ NUM_CH ];
};
template < typename T, int NUM_PL >
class Stream1D{
public:
hls::stream< T > st[ NUM_PL ];
};
LeNet.cpp
// LeNet.cpp
// 2020/8
// (C) UNDO / m-ando.com
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <math.h>
#include <ap_int.h>
#include <ap_fixed.h>
#include <hls_stream.h>
#include <ap_axi_sdata.h>
#include "common/xf_video_mem.h"
#include "LeNet.h"
#define VER 3
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v0(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_DATA outData[ NUM_PL ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ] );
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v1(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_DATA outData[ NUM_PL ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ] );
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v2_MI(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
hls::stream< DWINDOW_T< T_DATA, NUM_CH, NUM_KSIZE > > &wBufSt );
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v2_MO(
hls::stream< DWINDOW_T< T_DATA, NUM_CH, NUM_KSIZE > > &wBufSt,
T_DATA outData[ NUM_PL ][ NUM_DSIZE ][ NUM_DSIZE ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ] );
#if 4 == VER
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MI(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ],
Stream2D< T_DATA, NUM_PL, 1 > &stWindow,
Stream2D< T_KERNEL, NUM_PL, 1 > &stKernel );
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MC(
Stream2D< T_DATA, NUM_PL, 1 > &stWindow,
Stream2D< T_KERNEL, NUM_PL, 1 > &stKernel,
Stream1D< T_ACC, NUM_PL > &stSum );
#endif
#if 5 <= VER
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MI(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ],
Stream2D< T_DATA, NUM_PL, NUM_CH > &stWindow,
Stream2D< T_KERNEL, NUM_PL, NUM_CH > &stKernel );
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MC(
Stream2D< T_DATA, NUM_PL, NUM_CH > &stWindow,
Stream2D< T_KERNEL, NUM_PL, NUM_CH > &stKernel,
Stream1D< T_ACC, NUM_PL > &stSum );
#endif
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MO(
Stream1D< T_ACC, NUM_PL > &stSum,
T_DATA outData[ NUM_PL ][ NUM_DSIZE ][ NUM_DSIZE ] );
void Conv_hls_C3(
DTYPE_HLS inData[ C3_DSIZE ][ C3_DSIZE ][ C3_CH ],
DTYPE_HLS outData[ C3_PL ][ C3_DSIZE_OUT ][ C3_DSIZE_OUT ],
DTYPE_HLS kernel[ C3_PL ][ C3_CH ][ C3_KSIZE ][ C3_KSIZE ] )
{
#if VER >= 2
#pragma HLS DATAFLOW
#endif
#pragma HLS INTERFACE s_axilite port=kernel bundle=reg
#pragma HLS INTERFACE m_axi depth=65536 port=outData offset=slave bundle=axi4
#pragma HLS INTERFACE m_axi depth=65536 port=inData offset=slave bundle=axi4
#if 4 <= VER
#pragma HLS ARRAY_PARTITION variable=kernel complete dim=1
#endif
#if 5 <= VER
#pragma HLS ARRAY_PARTITION variable=kernel complete dim=2
#endif
#if 1 <= VER and VER <= 3
hls::stream< DWINDOW_T< DTYPE_HLS, C3_CH, C3_KSIZE > > wBufSt;
#pragma HLS STREAM variable=wBufSt depth=1 dim=1
#endif
#if 4 == VER
Stream2D< DTYPE_HLS, C3_PL, 1 > stWindow;
Stream2D< DTYPE_HLS, C3_PL, 1 > stKernel;
Stream1D< ATYPE_HLS, C3_PL > stSum;
#pragma HLS STREAM variable=stWindow.st depth=1 dim=2
#pragma HLS STREAM variable=stKernel.st depth=1 dim=2
#pragma HLS STREAM variable=stSum.st depth=1 dim=1
#endif
#if 5 <= VER
Stream2D< DTYPE_HLS, C3_PL, C3_CH > stWindow;
Stream2D< DTYPE_HLS, C3_PL, C3_CH > stKernel;
Stream1D< ATYPE_HLS, C3_PL > stSum;
#pragma HLS STREAM variable=stWindow.st depth=1 dim=2
#pragma HLS STREAM variable=stKernel.st depth=1 dim=2
#pragma HLS STREAM variable=stSum.st depth=1 dim=1
#endif
#if 4 <= VER
Conv_hls_v4_MI< DTYPE_HLS, DTYPE_HLS, ATYPE_HLS, C3_PL, C3_DSIZE, C3_CH, C3_KSIZE >( inData, kernel, stWindow, stKernel );
Conv_hls_v4_MC< DTYPE_HLS, DTYPE_HLS, ATYPE_HLS, C3_PL, C4_DSIZE, C3_CH, C3_KSIZE >( stWindow, stKernel, stSum );
Conv_hls_v4_MO< DTYPE_HLS, DTYPE_HLS, ATYPE_HLS, C3_PL, C4_DSIZE, C3_CH, C3_KSIZE >( stSum, outData );
#endif
#if 2 <= VER and VER <= 3
Conv_hls_v2_MI< DTYPE_HLS, DTYPE_HLS, ATYPE_HLS, C3_PL, C3_DSIZE, C3_CH, C3_KSIZE >( inData, wBufSt );
Conv_hls_v2_MO< DTYPE_HLS, DTYPE_HLS, ATYPE_HLS, C3_PL, C4_DSIZE, C3_CH, C3_KSIZE >( wBufSt, outData, kernel );
#endif
#if 0
Conv_hls_v1< DTYPE_HLS, DTYPE_HLS, ATYPE_HLS, C3_PL, C3_DSIZE, C3_CH, C3_KSIZE >( inData, outData, kernel );
#endif
#if VER <= 1
Conv_hls_v0< DTYPE_HLS, DTYPE_HLS, ATYPE_HLS, C3_PL, C3_DSIZE, C3_CH, C3_KSIZE >( inData, outData, kernel );
#endif
}
#if VER <= 4
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MI(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ],
Stream2D< T_DATA, NUM_PL, 1 > &stWindow,
Stream2D< T_KERNEL, NUM_PL, 1 > &stKernel )
#endif
#if 5 <= VER
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MI(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ],
Stream2D< T_DATA, NUM_PL, NUM_CH > &stWindow,
Stream2D< T_KERNEL, NUM_PL, NUM_CH > &stKernel )
#endif
{
xf::LineBuffer< NUM_KSIZE-1, NUM_DSIZE, T_DATA > lBuf[ NUM_CH ];
xf::Window< NUM_KSIZE, NUM_KSIZE, T_DATA > wBuf[ NUM_CH ];
T_DATA inDataTemp;
LP_Y:for( int y = 0 ; y < NUM_DSIZE ; y++ ){
LP_X:for( int x = 0 ; x < NUM_DSIZE ; x++ ){
#pragma HLS PIPELINE
LP_CH_BUF:for( int ch = 0 ; ch < NUM_CH ; ch++ ){
#if 5 <= VER
#pragma HLS UNROLL
#endif
inDataTemp = inData[ y ][ x ][ ch ];
wBuf[ ch ].shift_pixels_left();
IPIX:for( int i = 0 ; i < NUM_KSIZE - 1 ; i++ ){
#pragma HLS UNROLL
wBuf[ ch ].insert_pixel( lBuf[ ch ].getval( i, x ), i, NUM_KSIZE - 1 );
}
wBuf[ ch ].insert_pixel( inDataTemp, NUM_KSIZE - 1, NUM_KSIZE - 1 );
lBuf[ ch ].shift_pixels_up( x );
lBuf[ ch ].insert_bottom_row( inDataTemp, x );
if( y >= NUM_KSIZE - 1 && x >= NUM_KSIZE - 1 ){
LP_PL:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
#pragma HLS UNROLL
LP_KY_A:for( int ky = 0 ; ky < NUM_KSIZE ; ky++ ){
#pragma HLS PIPELINE
LP_KX_A:for( int kx = 0 ; kx < NUM_KSIZE ; kx++ ){
#if 4 == VER
stWindow.st[ pl ][ 0 ].write( wBuf[ ch ]( ky, kx ));
stKernel.st[ pl ][ 0 ].write( kernel[ pl ][ ch ][ ky ][ kx ] );
#endif
#if 5 <= VER
stWindow.st[ pl ][ ch ].write( wBuf[ ch ]( ky, kx ));
stKernel.st[ pl ][ ch ].write( kernel[ pl ][ ch ][ ky ][ kx ] );
#endif
}
}
}
}
}
}
}
}
#if VER <= 4
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MC(
Stream2D< T_DATA, NUM_PL, 1 > &stWindow,
Stream2D< T_KERNEL, NUM_PL, 1 > &stKernel,
Stream1D< T_ACC, NUM_PL > &stSum )
#endif
#if 5 <= VER
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MC(
Stream2D< T_DATA, NUM_PL, NUM_CH > &stWindow,
Stream2D< T_KERNEL, NUM_PL, NUM_CH > &stKernel,
Stream1D< T_ACC, NUM_PL > &stSum )
#endif
{
T_ACC sum[ NUM_PL ];
#pragma HLS ARRAY_PARTITION variable=sum complete dim=0
T_ACC sumX[ NUM_PL ][ NUM_CH ];
#pragma HLS ARRAY_PARTITION variable=sumX complete dim=0
hls::stream< T_ACC > sAcc[ NUM_PL ][ NUM_CH ];
#pragma HLS STREAM variable=sAcc depth=1 dim=1
LP_Y:for( int y = 0 ; y < NUM_DSIZE ; y++ ){
LP_X:for( int x = 0 ; x < NUM_DSIZE ; x++ ){
LP_PL:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
#pragma HLS UNROLL
#if 4 == VER
MacSt< T_DATA, T_KERNEL, T_ACC, NUM_CH * NUM_KSIZE * NUM_KSIZE >
( stWindow.st[ pl ][ 0 ], stKernel.st[ pl ][ 0 ], sAcc[ pl ][ 0 ] );
sum[ pl ] = sAcc[ pl ][ 0 ].read();
#endif
#if 5 <= VER
sum[ pl ] = 0;
LP_CH:for( int ch = 0 ; ch < NUM_CH ; ch++ ){
#pragma HLS UNROLL
MacSt< T_DATA, T_KERNEL, T_ACC, NUM_KSIZE * NUM_KSIZE >
( stWindow.st[ pl ][ ch ], stKernel.st[ pl ][ ch ], sAcc[ pl ][ ch ] );
sum[ pl ] += sAcc[ pl ][ ch ].read();
}
#endif
stSum.st[ pl ].write( sum[ pl ] );
}
}
}
}
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v4_MO(
Stream1D< T_ACC, NUM_PL > &stSum,
T_DATA outData[ NUM_PL ][ NUM_DSIZE ][ NUM_DSIZE ] )
{
T_ACC sum[ NUM_PL ];
T_DATA relu[ NUM_PL ];
LP_Y:for( int y = 0 ; y < NUM_DSIZE ; y++ ){
LP_X:for( int x = 0 ; x < NUM_DSIZE ; x++ ){
LP_PL:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
sum[ pl ] = stSum.st[ pl ].read();
if( sum[ pl ] < 0 ){
relu[ pl ] = 0;
} else {
relu[ pl ] = sum[ pl ];
}
outData[ pl ][ y ][ x ] = relu[ pl ];
//printf("h>>%d\n",sum);
}
}
}
}
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v2_MI(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
hls::stream< DWINDOW_T< T_DATA, NUM_CH, NUM_KSIZE > > &wBufSt )
{
xf::LineBuffer< NUM_KSIZE-1, NUM_DSIZE, T_DATA > lBuf[ NUM_CH ];
xf::Window< NUM_KSIZE, NUM_KSIZE, T_DATA > wBuf[ NUM_CH ];
DWINDOW_T< T_DATA, NUM_CH, NUM_KSIZE > wBufTmp;
T_DATA inDataTemp;
LP_PL:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
LP_Y:for( int y = 0 ; y < NUM_DSIZE ; y++ ){
LP_X:for( int x = 0 ; x < NUM_DSIZE ; x++ ){
#if VER >= 3
#pragma HLS PIPELINE
#endif
LP_CH_BUF:for( int ch = 0 ; ch < NUM_CH ; ch++ ){
inDataTemp = inData[ y ][ x ][ ch ];
wBuf[ ch ].shift_pixels_left();
for( int i = 0 ; i < NUM_KSIZE - 1 ; i++ ){
wBuf[ ch ].insert_pixel( lBuf[ ch ].getval( i, x ), i, NUM_KSIZE - 1 );
}
wBuf[ ch ].insert_pixel( inDataTemp, NUM_KSIZE - 1, NUM_KSIZE - 1 );
lBuf[ ch ].shift_pixels_up( x );
lBuf[ ch ].insert_bottom_row( inDataTemp, x );
LP_KY_A:for( int ky = 0 ; ky < NUM_KSIZE ; ky++ ){
LP_KX_A:for( int kx = 0 ; kx < NUM_KSIZE ; kx++ ){
wBufTmp.d[ ch ][ ky ][ kx ] = wBuf[ ch ]( ky, kx );
}
}
}
if( y >= NUM_KSIZE - 1 && x >= NUM_KSIZE - 1 ){
wBufSt.write( wBufTmp );
}
}
}
}
}
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v2_MO(
hls::stream< DWINDOW_T< T_DATA, NUM_CH, NUM_KSIZE > > &wBufSt,
T_DATA outData[ NUM_PL ][ NUM_DSIZE ][ NUM_DSIZE ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ] )
{
DWINDOW_T< T_DATA, NUM_CH, NUM_KSIZE > wBufTmp;
T_ACC sum;
T_DATA relu;
LP_PL:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
LP_Y:for( int y = 0 ; y < NUM_DSIZE ; y++ ){
LP_X:for( int x = 0 ; x < NUM_DSIZE ; x++ ){
#if VER == 3
#pragma HLS PIPELINE
#endif
sum = 0;
wBufTmp = wBufSt.read();
LP_CH:for( int ch = 0 ; ch < NUM_CH ; ch++ ){
LP_KY:for( int ky = 0 ; ky < NUM_KSIZE ; ky++ ){
LP_KX:for( int kx = 0 ; kx < NUM_KSIZE ; kx++ ){
sum += wBufTmp.d[ ch ][ ky ][ kx ] * kernel[ pl ][ ch ][ ky ][ kx ];
//printf( "h> %d(%d) * %d\n", wBuf[ ch ]( ky, kx ), wBufTmp.d[ ch ][ ky ][ kx ], kernel[ pl ][ ch ][ ky ][ kx ] );
}
}
}
if( sum < 0 ){
relu = 0;
} else {
relu = sum;
}
outData[ pl ][ y ][ x ] = relu;
//printf("h>>%d\n",sum);
}
}
}
}
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v1(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_DATA outData[ NUM_PL ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ] )
{
xf::LineBuffer< NUM_KSIZE-1, NUM_DSIZE, T_DATA > lBuf[ NUM_CH ];
xf::Window< NUM_KSIZE, NUM_KSIZE, T_DATA > wBuf[ NUM_CH ];
DWINDOW_T< T_DATA, NUM_CH, NUM_KSIZE > wBufTmp;
hls::stream< DWINDOW_T< T_DATA, NUM_CH, NUM_KSIZE > > wBufSt;
#pragma HLS STREAM variable=wBufSt depth=65536 dim=1
T_DATA inDataTemp;
T_ACC sum;
T_DATA relu;
LP_PL:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
LP_Y:for( int y = 0 ; y < NUM_DSIZE ; y++ ){
LP_X:for( int x = 0 ; x < NUM_DSIZE ; x++ ){
LP_CH_BUF:for( int ch = 0 ; ch < NUM_CH ; ch++ ){
inDataTemp = inData[ y ][ x ][ ch ];
wBuf[ ch ].shift_pixels_left();
for( int i = 0 ; i < NUM_KSIZE - 1 ; i++ ){
wBuf[ ch ].insert_pixel( lBuf[ ch ].getval( i, x ), i, NUM_KSIZE - 1 );
}
wBuf[ ch ].insert_pixel( inDataTemp, NUM_KSIZE - 1, NUM_KSIZE - 1 );
lBuf[ ch ].shift_pixels_up( x );
lBuf[ ch ].insert_bottom_row( inDataTemp, x );
LP_KY_A:for( int ky = 0 ; ky < NUM_KSIZE ; ky++ ){
LP_KX_A:for( int kx = 0 ; kx < NUM_KSIZE ; kx++ ){
wBufTmp.d[ ch ][ ky ][ kx ] = wBuf[ ch ]( ky, kx );
}
}
}
if( y >= NUM_KSIZE - 1 && x >= NUM_KSIZE - 1 ){
wBufSt.write( wBufTmp );
}
}
}
}
LP_PL2:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
LP_Y2:for( int y = 0 ; y < DSIZE_OUT( NUM_DSIZE, NUM_KSIZE ) ; y++ ){
LP_X2:for( int x = 0 ; x < DSIZE_OUT( NUM_DSIZE, NUM_KSIZE ) ; x++ ){
sum = 0;
wBufTmp = wBufSt.read();
LP_CH:for( int ch = 0 ; ch < NUM_CH ; ch++ ){
LP_KY:for( int ky = 0 ; ky < NUM_KSIZE ; ky++ ){
LP_KX:for( int kx = 0 ; kx < NUM_KSIZE ; kx++ ){
sum += wBufTmp.d[ ch ][ ky ][ kx ] * kernel[ pl ][ ch ][ ky ][ kx ];
//printf( "h> %d(%d) * %d\n", wBuf[ ch ]( ky, kx ), wBufTmp.d[ ch ][ ky ][ kx ], kernel[ pl ][ ch ][ ky ][ kx ] );
}
}
}
if( sum < 0 ){
relu = 0;
} else {
relu = sum;
}
outData[ pl ][ y ][ x ] = relu;
//printf("h>>%d\n",sum);
}
}
}
}
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_hls_v0(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_DATA outData[ NUM_PL ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ] )
{
T_ACC sum;
T_DATA relu;
LP_PL:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
LP_Y:for( int y = 0 ; y < DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ; y++ ){
LP_X:for( int x = 0 ; x < DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ; x++ ){
sum = 0;
LP_CH:for( int ch = 0 ; ch < NUM_CH ; ch++ ){
LP_KY:for( int ky = 0 ; ky < NUM_KSIZE ; ky++ ){
LP_KX:for( int kx = 0 ; kx < NUM_KSIZE ; kx++ ){
#if VER == 1
#pragma HLS PIPELINE
#endif
sum += inData[ y + ky ][ x + kx ][ ch ] * kernel[ pl ][ ch ][ ky ][ kx ];
//printf( "r> %d * %d\n", inData[ y + ky ][ x + kx ][ ch ], kernel[ pl ][ ch ][ ky ][ kx ] );
}
}
}
if( sum < 0 ){
relu = 0;
} else {
relu = sum;
}
outData[ pl ][ y ][ x ] = relu;
//printf("r>>%d\n",sum);
}
}
}
}
LeNet_tb.cpp
// LeNet_tb.cpp
// 2020/8
// (C) UNDO / m-ando.com
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <iostream>
using namespace std;
#include <ap_int.h>
#include <ap_fixed.h>
#include <hls_stream.h>
#include <ap_axi_sdata.h>
#include "common/xf_video_mem.h"
#include "LeNet.h"
int main( void );
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_ref(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_DATA outData[ NUM_PL ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ] );
int main( void )
{
DTYPE_REF C3_inData[ C3_DSIZE ][ C3_DSIZE ][ C3_CH ];
DTYPE_REF C3_outData[ C3_PL ][ C4_DSIZE ][ C4_DSIZE ];
DTYPE_REF C3_kernel[ C3_PL ][ C3_CH ][ C3_KSIZE ][ C3_KSIZE ];
DTYPE_HLS C3_outDataHLS[ C3_PL ][ C4_DSIZE ][ C4_DSIZE ];
srand( 0 );
for( int y = 0 ; y < C3_DSIZE ; y++ ){
for( int x = 0 ; x < C3_DSIZE ; x++ ){
for( int ch = 0 ; ch < C3_CH ; ch++ ){
C3_inData[ y ][ x ][ ch ] = (DTYPE_REF)( ( ( rand() & 0x1FFF ) - 4096 ) / 4096.0 );
//cout << C3_inData[ y ][ x ][ ch ] << endl;
}
}
}
for( int pl = 0 ; pl < C3_PL ; pl++ ){
for( int ch = 0 ; ch < C3_CH ; ch++ ){
for( int ky = 0 ; ky < C3_KSIZE ; ky++ ){
for( int kx = 0 ; kx < C3_KSIZE ; kx++ ){
C3_kernel[ pl ][ ch ][ ky ][ kx ] = (DTYPE_REF)( ( ( rand() & 0x1FFF ) - 4096 ) / 4096.0 );
//cout << C3_kernel[ pl ][ ch ][ ky ][ kx ] << endl;
}
}
}
}
Conv_ref< DTYPE_REF, DTYPE_REF, ATYPE_HLS, C3_PL, C3_DSIZE, C3_CH, C3_KSIZE >( C3_inData, C3_outData, C3_kernel );
//Conv_hls< DTYPE_HLS, DTYPE_HLS, long, C3_PL, C3_DSIZE, C3_CH, C3_KSIZE >( C3_inData, C3_outDataHLS, C3_kernel );
Conv_hls_C3( C3_inData, C3_outDataHLS, C3_kernel );
for( int pl = 0 ; pl < C3_PL ; pl++ ){
for( int y = 0 ; y < C4_DSIZE ; y++ ){
for( int x = 0 ; x < C4_DSIZE ; x++ ){
if( C3_outDataHLS[ pl ][ y ][ x ] == C3_outData[ pl ][ y ][ x ] ){
cout << ".";
} else {
cout << endl << C3_outData[ pl ][ y ][ x ] << "\t" << C3_outDataHLS[ pl ][ y ][ x ];
}
}
}
cout << endl;
}
Conv_hls_C3( C3_inData, C3_outDataHLS, C3_kernel );
}
template < typename T_DATA, typename T_KERNEL, typename T_ACC,
int NUM_PL, int NUM_DSIZE , int NUM_CH, int NUM_KSIZE >
void Conv_ref(
T_DATA inData[ NUM_DSIZE ][ NUM_DSIZE ][ NUM_CH ],
T_DATA outData[ NUM_PL ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ][ DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ],
T_KERNEL kernel[ NUM_PL ][ NUM_CH ][ NUM_KSIZE ][ NUM_KSIZE ] )
{
T_ACC sum;
T_DATA relu;
LP_PL:for( int pl = 0 ; pl < NUM_PL ; pl++ ){
LP_Y:for( int y = 0 ; y < DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ; y++ ){
LP_X:for( int x = 0 ; x < DSIZE_OUT(NUM_DSIZE,NUM_KSIZE) ; x++ ){
sum = 0;
LP_CH:for( int ch = 0 ; ch < NUM_CH ; ch++ ){
LP_KY:for( int ky = 0 ; ky < NUM_KSIZE ; ky++ ){
LP_KX:for( int kx = 0 ; kx < NUM_KSIZE ; kx++ ){
sum += inData[ y + ky ][ x + kx ][ ch ] * kernel[ pl ][ ch ][ ky ][ kx ];
//printf( "r> %d * %d\n", inData[ y + ky ][ x + kx ][ ch ], kernel[ pl ][ ch ][ ky ][ kx ] );
}
}
}
if( sum < 0 ){
relu = 0;
} else {
relu = sum;
}
outData[ pl ][ y ][ x ] = relu;
//printf("r>>%d\n",sum);
}
}
}
}