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.

0 件のコメント:

コメントを投稿