2022年3月16日水曜日

PYNQとVitis HLSで作るFMA演算IP(4):おまけネタ


前回までで、FMA演算IPを作成することができました。今回はおまけ編で、データ型を変えたり並列数を増やしたりしてみます。



1.いろいろなデータ型に対応させる


 せっかくの専用回路だし、いろいろなデータ型に対応させてみましょう。今回は参考例として16bit固定小数点(q4.12)に対応させてみます。
 ...とはいうものの、高位合成用のサンプルコードはテンプレート関数で記述されているうえに、データ型はFMA.hpp内のusing data_type = float;の部分で一元的に管理しています。なので、ここの部分を固定小数点ように書き換えればいいわけです。
 Vitis HLSでは固定小数点用のライブラリとしてap_fixed.hが提供されており、これを使用して記述すればいいわけです(書き換えるとusing data_type = ap_fixed<data_width, 4>;となる)。あとは、データ幅を16bitに下げればOKです(constexpr data_width = 16;)。
  JupyterNotebookのコードについては、最初のせるのarray_typeをnp.int16、fractionを12、enable_fp2fixをTrueにすれば動くはずです。

2.AXI-Streamのデータ幅を増やす


 こちらに関しては、FMA.cppのbus_widthを書き換え、JupyterNotebookのbus_widthを書き換えます。また回路を作成する際に使用したDMA IPのStream Data Widthを指定の値にします。DMA IPの設定値からわかるように、データ幅は64, 128, 256, 512, 1024といった値しかとることができません。注意しましょう。
 データ幅を増やすことで、並列数を増やすことができます。DMAの転送能力次第な部分はありますが、そこそこの速度向上が達成できそうな雰囲気でした(160万個のデータを流して210MFLOP程度)。

3.遭遇したトラブル


  • AXI-Streamのデータ幅について、fp32+1024bitの時やfix16+256bitの場合でHLSのCoSimの段階から結果がおかしくなっていた。CSimの段階では正常な計算結果を返すし、他のデータ幅の場合とはデータ幅とデータ型以外変えていないので、まったくもって謎だった。
  • float配列とデータバスのunionを作成し、型変換を実装する予定だったが、ap_intやap_fixedをunionのメンバに入れるとコンパイルが通らなくなるのでやめた。
  • float配列とCの組み込み整数型のunionを含むコードをVivado HLSで高位合成かけようとしたところ、合成に失敗した。Vitis HLSを使うと、合成がかかった。
  • 回路が動きそうでも動かない場合、Vivadoのプロジェクトを作り直すと動く場合があった。
  • この記事を書くのに、中の人の睡眠時間とGT7のプレイ時間が削られまくった。



©2022 shts All Right Reserved.

PYNQとVitis HLSで作るFMA演算IP(3):実機での実行


 前回前々回で、とりあえ回路情報の入ったbitstreamファイルまで生成できました。今回は実機実装していきます。





1.実機のコードについて


 今回の回路構成の場合、DMAの関数に64bitの配列を渡す必要があるように見えるのですが、実際には計算で使うデータ型(今回の場合はfp32:np.float32)の配列を使用して問題ないです。ただし、配列の総サイズ(bit)が64bitで割り切れる必要があります。割り切れない場合は、ゼロパディングで埋めるといいでしょう。
 今回作成したIPはAXI-Liteで制御するのですが、開始の制御以外にループ回数の指定を行います(0x10にint32でループ回数を書き込む)。また、0x00に1を書き込むことで、IPの開始を制御できます。そのほかとしては、dmaの転送設定を自作IPの開始前に行うあたりが少し特殊かもしれないです。
 一応参考としてIPの実行時間を計測します。具体的にはメモリからデータを読み出し始めてから、書き込みが終わるまでの時間を計測します(自作IPをスタートさせたタイミングで入力データの転送が開始され、受信側のDMA IPが終了するタイミングでIPの実行も終了するため)。

2.実行結果

実行結果。140MFLOPS程度の性能であった。


 サンプルコードではCPUの計算結果との比較を行っているのですが、特段表示が出なければ一致していると思ってください。実行速度に関しては、20万回FMAの計算を実行した場合で大体140MFLOPS程度が出ているようです(実はループ回数をいじると計算回数が変化し、計算速度が変化する)。
 CPUで実行する場合と比べると...まぁ遅いといった感想になるでしょうか。クロック周波数が200MHzしか出ておらず、またFMAの計算に16*5nsかかっていることを踏まえればこの程度かなとは思っています。

3.今後


 自作のFMA演算IPを実機実装することができました。次回はAXI-Streamのデータ幅拡張や異なるデータ型への対応についての話です。




©2022 shts All Right Reserved.

PYNQとVitis HLSで作るFMA演算IP(2):高位合成と回路合成



 前回高位合成用のC++コードを作成しました。今回はそのコードを使って高位合成を行い、その流れで回路まで作成します。




1.高位合成

 今回はVitis HLSを用いてPYNQ-Z2向けに合成を行います。その際の設定は以下の通り。
  • ターゲット周波数:200MHz
  • Uncertainty:12.5%
  • ターゲットデバイス:pynq-z2(xc7z020clg400-1)
  • 言語:C++0x
 今回は2つのソースファイル(FMA.cpp, FMA.hpp)、1つのテストベンチ(FMA_TB.cpp)を登録します。このうちFMA.cppとFMA_TB.cppのCFLAGSに-std=c++0xオプションを付与します。
 合成結果はこんな感じになりました。

高位合成の結果

 Iteration Latencyは16となりましたが、Intervalが1になっていることからいい感じにパイプライン化できたようです。使用率は
  • DSP : 4%(10/220)
  • LUT:3%(1992/53200)
  • FF:1%(1718/106400)
  • BRAM:0%(0/140)
となっています。余裕ありありですね。合成されたデータフローはこんな感じでした。

データフローの一部。2か所ほど"長い棒"がみえるが、
それぞれfmulとfaddの計算ステップとなっていた。

 一応仕様通りに2並列で計算できているようです。また大半のステップがfmulとfaddで占められていることもわかりました。
 この後CoSimを走らせて結果を確認後、IPを出力します(そのまま出力してもいいのだが、Configuration...のボタンからVendor、Library、Description、Display Nameあたりをいじってからいったん出力->Solution Settings内のConfiguration Settingsのconfig_exportにあるipnameをいじってから出力すると、VivadoでIPを読み込んだ際にオリジナルの名前が付き、ベンダー名で検索がかけられる)。

2.回路合成


 高位合成で作成したIPを使って回路を作成します。ZYNQのIPを最初に置いて、Run Connection Automationを走らせるとDDRとFIXED_IOと外部端子がつながるはずです。
 その後、AXI_HPポートの0から3を有効化します。データ幅はすべて64bitにしましょう。

AXI_HPポートの設定。データ幅はすべて64bitとした。

 AXI_HPポートの設定後、IPへ供給するクロックの設定をします。今回は単一のクロックソースかつ周波数が200MHz程度なので、ZYNQのIPから供給します。Clock Configuration->PL Fabric Clocksの順にたどり、FCLK_CLK0を200MHzに設定します。

クロック設定。今回はHLSに合わせて200MHz。

 ZYNQ IPの設定が終わったら、AXI Direct Memory AccessとAXI Interconnectを4つずつ出します。
 AXI Interconnectの方はMasterとSlaveの数をそれぞれ1に設定します。
DMAの方は入力側(W, X, B)と出力側(Y)で少し異なります。入力側のDMA IPについて、Enable Scatter Gather Engineを無効化し、Width of Buffer Legth Registerを26bits、Address Widthを64Bitsにしておきます。データ入力のIPなので、Read Channelのみ有効にしておきます。Memory Map Data WidthとStream Data Widthは64にしておきます。

読み込み側のDMA IPの設定

 出力側のDMA IPについては、入力側で行ったEnable Scatter Gather Enginenの無効化、Width of Buffer Legth Register->26bits、Address Width->64Bitsの操作を同じく行います。その後、Read Channelを無効化し、Write Channelの有効かを行います。Memory Map Data WidthとStream Data WidthはAUTOのままでOKです。

書き込み側のDMA IPの設定

 最後に各IPを接続します。DMAのIPをCPU側から制御するため、個々のIPに割り当てられている名前が必要になります。サンプルコードをそのまま使いたい場合、
  • S_AXI_HP0 -> axi_dma_0(+interconnect) -> w_axi_0
  • S_AXI_HP1 -> axi_dma_1(+interconnect) -> x_axi_1
  • S_AXI_HP2 -> axi_dma_2(+interconnect) -> b_axi_2
  • y_axi_3 -> axi_dma_3(+interconnect) -> S_AXI_HP3
と接続しましょう。

IPの配線図。
Wが0、Xが1、Bが2、Yが3と覚えると分かりやすい。

 この接続を終えると、Run Connection Automationの表示が出るので、すべてにチェックを入れて実行すれば最終的な回路の完成です。この後、アドレスエディタでDMAと自作IPに対してアドレスを割り当てておきましょう。

 Generate Output Productsを実行し、Create HDL WrapperをLet Vivado manage...の方に入れておきます。Generate Bitsteramをクリックすれば回路の合成が始まります(2回に1回ぐらいで失敗するが、Output Productsをリセットしたのち、Generateしなおして再びBitstreamの生成を行えばいけるはず)。
 Bitstreamの生成が終わったら、.bitのファイルと.hwhのファイルを書きだします。画面左上のFile->Export->Export Hardwareを選択し、Platform typeをFixed、Outputを Include bitstreamに設定します。その後、XSAのファイル名を聞かれるので、適当な名前にします(サンプルコードの通りにしたい場合は、design_1_wrapperをdesign_1に変更)。出力先はどこでもよいです。
 .bitと.hwhはこのXSAファイルに入っているので、7zip等を使い取り出します。最終的にこの.bitと.hwhのファイルを.ipynbから読みだしてプログラムを実行することになります。

3.次回

.bitファイルと.hwhファイルが生成できたので、次回はPYNQ側のコードと速度計測を行っていきます。



©2022 shts All Right Reserved.


2022年3月15日火曜日

PYNQとVitis HLSで作るFMA演算IP(1):簡単な仕様策定とHLSコードの作成



 修論の提出まで終わり、無事修士課程を修了できた中の人です。修論ではとある専用計算回路を作成したのですが、時間の都合上PS-PL間の通信部に関してあまり手を入れることができませんでした。そこで今回はその通信部について、PYNQ-Z2へのFMA(積和)演算IPの実装を題材として、いろいろ試してみようと思います。
 今回のコードについてはこちらからどうぞ。



0.開発環境


  • 使用ボード:PYNQ-Z2(PYNQ v2.6)
  • Vivado:2020.1
  • Vitis HLS:2020.1
  • 高位合成言語:C++0x
  • OS:Ubuntu20.04(on WSL2)
 

1.FMA演算IPの簡単な仕様


 今回は、積和演算を同時に複数実行する演算IPを作成します。演算器自体はそこまで重要ではないのですが、今回重要となるのはPS部とIPを接続する通信部です。

 今回使用するPYNQ-Z2において、PS部とIPの通信方法はいくつかあります。どの通信方法を使うかについて少し考えます。
 まずFMAの演算器の個数よりも多くのデータを流すことを考えると、演算器を複数回使いまわす必要があります。その場合にはパイプライン化した方がより高速に実行できるようになります。パイプライン化を行う際に注意すべき点として、パイプラインに流すデータの取得が滞ってしまうとストールすることが挙げられます。
 となると、PS-IP間の通信として連続してデータを送ることができるAXI-Streamを使うとよさげなことがわかります(Xilinx提供のAXI Direct Memory AccessがPL側とAXI-Streamで接続するのも理由の一つ)。

全体のブロック図(W〇Xは要素積を表す)

 転送規格は決まったので、より詳しい部分を決めていきます。AXI-Streamのデータ幅はとりあえず64bitにします(後で広げる)。また実際に計算するデータの型を単精度浮動小数点(FP32)に仮決定しておきます。64bit幅のバスであればFP32のデータを2つ同時に流すことができるので、同時に2のFMA命令を実行できる回路を作成することにします。

自作IPのブロック図

2.HLS用のコード


 さてここからはHLS用のコードについてです(コードについてはこちら)。IP内部のブロック図を基にコードを作成すると、必要になるのが
  • slicer:64bitのデータを32bit x 2個のデータに分割する
  • array_(w, x, b, y):32bit x 2個のデータを保持する
  • fma_unit:y=wx+bを2並列同時に行う
  • packer:32bit x 2個のデータを64bitのデータに分割する
となります。array_(w, x, b, y)に関しては、C言語の配列(C++の生配列)とarray_partitionプラグマを使用します(std::arrayはHLSで合成できないので)。
 以下のコード例では配列を要素ごとに分割して実装しています(data_type=float, field_length=2)。  

data_type w_vec[field_length];
#pragma HLS array_partition variable=w_vec complete dim=0

 fma_unitはfor-loopとloop_unrollプラグマで2並列分(length=field_length=2)の演算器を実装します。
template<typename data_t, uint64_t length>
void fma(data_t w[], data_t x[], data_t b[], data_t y[]){
  for (uint64_t i = 0; i < length; i++){
#pragma HLS UNROLL
    y[i] = w[i] * x[i] + b[i];
  }
}

 slicerとpackerについては、ビットマスクとシフトを使っていい感じにスライス・パックしていくだけです。しかしながら、floatからAXI-Streamで使用するデータ型(ap_axiu)へ単純に代入した場合型キャストが発生し整数部分しか読み取られない問題や、floatのフォーマットでPSから送信されたビット列をintとして解釈し、floatにキャストした際にデータが化ける問題があります。
 これはap_axiu(ap_axis)のデータ部がap_uint(ap_int)で記述されていることに起因しており、c言語でいうところのfloatとunsigned int(int)間のキャストと同じことが発生しています。
 この対策として、変数のポインタをキャストすることで、ビット列の解釈をプログラム上で変更するコードを入れています(floatの内部表現を確認するときにやるやつ)。
inline void slicer(axi_bus_type bit_data, data_type array[]){
#pragma HLS INLINE
  ap_uint<data_bit_width> buffer, bit_mask = -1;
  ap_uint<data_bus_width> bus_buffer;
  bus_buffer = bit_data.data;
  for (uint64_t i = 0; i < field_length; i++){
#pragma HLS UNROLL
    buffer = (bus_buffer >> (data_bit_width * i)) & bit_mask;
    array[i] = *((data_type*) &buffer);
  }
} 

inline void packer(data_type data[], packed_type bus_buffer[]){
#pragma HLS INLINE
  ap_uint<data_bit_width> buffer;
  bus_buffer[0] = 0;
  for (uint64_t i = 0; i < field_length; i++){
#pragma HLS UNROLL
    buffer = *((ap_uint<data_bit_width>*) &data[i]);
    bus_buffer[0] |= ((packed_type)buffer) << (data_bit_width * i);
  }
}

 最後これらの関数をまとめ、HLS向けにラップした関数を準備します(コードはこちら).このラップ関数の実体は.hppに記述するのではなく.cppに記述します(.hppに書くと高位合成のターゲット関数にならない).
 今回はこれに加えてテストベンチを準備します(コードはこちら).基本的には入力データを仕込み,通常のC言語経由とRTL経由で計算を行い比較を行う流れになっています.

3.次回


今回はHLS用のコードまで作成しました。この後は実際の合成をおこないます。
合成して動作確認まで行った後は、
あたりをやっていこうと考えています。


©2022 shts All Right Reserved.