標準水平たたみ込み

最初に、次の図のように水平方向のたたみ込みを実行します。

たたみ込みは、K 個のデータ サンプルと K 個のたたみ込み係数を使用して実行されます。上の図では K の値は 5 ですが、この値はコードで定義されます。たたみ込みを実行するには、K 個以上のデータ サンプルが必要です。たたみ込みウィンドウは、画像外にあるピクセルを含む必要があるため、最初のピクセルでは開始できません。

対称たたみ込みを実行すると、src 入力からの最初の K 個のデータ サンプルが水平係数でたたみ込まれ、最初の出力が計算されます。2 つ目の出力を計算するには、次の K 個のデータ サンプルが使用されます。この計算は、最後の出力が書き込まれるまで各行に対して実行されます。

この操作を実行する C コードは次のとおりです。

const int conv_size = K;

const int border_width = int(conv_size / 2);



#ifndef __SYNTHESIS__

T * const local = new T[MAX_IMG_ROWS*MAX_IMG_COLS];

#else // Static storage allocation for HLS, dynamic otherwise

T local[MAX_IMG_ROWS*MAX_IMG_COLS];

#endif



Clear_Local:for(int i = 0; i < height * width; i++){

  local[i]=0;

}



// Horizontal convolution

HconvH:for(int col = 0; col < height; col++){

  HconvWfor(int row = border_width; row < width - border_width; row++){

    int pixel = col * width + row;

    Hconv:for(int i = - border_width; i <= border_width; i++){

      local[pixel] += src[pixel + i] * hcoeff[i + border_width];

    }

  }

}

このコードは簡単でわかりやすいものですが、ハードウェア結果の質に悪影響を及ぼす問題がいくつかあります。

1 つ目の問題は、C コンパイル中に大型ストレージが必要であるということです。アルゴリズムの中間結果は内部 local 配列に格納されます。HEIGHT*WIDTH の配列が必要で、1920*1080 の標準ビデオ画像では 2,073,600 の値が保持されます。

  • Zynq®-7000 All Programmable SoC または Zynq UltraScale+™ MPSoC をターゲットとするクロス コンパイラおよび多くのホスト システムでは、この量のローカル ストレージのためランタイムでスタック オーバーフローが発生します (ターゲット デバイス上での実行、Vivado HLS 内での協調シミュレーションの実行など)。local 配列のデータはスタックに配置され、OS で管理されるヒープには含まれません。arm-linux-gnueabihf-g++ でクロス コンパイルする際は、-Wl,"-z stacksize=4194304" リンカー オプションを使用して十分なスタック空間を割り当てます。このオプションの構文は、リンカーによって異なります。関数がハードウェアでのみ実行される場合は、__SYNTHESIS__ マクロを使用するとこのような問題を回避できます。このマクロは、ハードウェア関数がハードウェアに合成されるときにシステム コンパイラにより自動的に定義されます。上記のコードでは、C シミュレーション中にダイナミック メモリ割り当てを使用してコンパイルの問題を回避しており、合成中はスタティック ストレージのみが使用されます。このマクロを使用する短所は、C シミュレーションで検証されたコードが合成されるコードとは異なるものになることです。この例の場合はコードは複雑ではないので、動作は同じになります。
  • この local 配列の主な問題は、FPGA インプリメンテーションの質です。これは配列なので、内部 FPGA ブロック RAM を使用してインプリメントされます。これは、FPGA 内にインプリメントするにはかなり大きいメモリであり、より大型でコストのかかる FPGA デバイスが必要となる可能性があります。DATAFLOW 最適化を使用して、小型で効率的な FIFO を介してデータをストリーミングすると、ブロック RAM の使用を最小限に抑えることはできますが、データがストリーミングでシーケンシャルに使用されるようにする必要があります。現在のところ、そのような要件はありません。

2 つ目の問題は、パフォーマンスと local 配列の初期化に関するものです。Clear_Local ループは local 配列の値を 0 に設定するために使用されます。高パフォーマンスで実行するためにこのループをハードウェアでパイプライン処理しても、この操作をインプリメントするのに約 2 百万クロック サイクル必要です。このメモリの初期化中、システムで画像処理を実行することはできません。同じデータの初期化は、HConv ループ内の一時的な変数を使用して、書き出し前に累積を初期化することにより実行できます。

最後の問題は、データのスループット、つまりシステム パフォーマンスがデータ アクセス パターンにより制限されることです。

  • 最初のたたみ込み出力を作成するため、最初の K 個の値が入力から読み出されます。
  • 2 つ目の出力の計算には、新しい値が読み出され、その後同じ K-1 個の値が再度読み出されます。

高パフォーマンスの FPGA を得るには、PS へのアクセスを最小限に抑えることが重要です。前に既にフェッチされたデータに再度アクセスすることは、システムのパフォーマンスに悪影響を与えます。FPGA では多数の計算を同時に実行して高パフォーマンスを達成することが可能ですが、データのフローが値を再読み出しするために頻繁に中断されると高パフォーマンスは得られません。

注記: 高パフォーマンスを達成するには、PS からのデータにアクセスするのは 1 回のみにし、小型のローカル ストレージ (小型から中型のサイズの配列) に格納して再利用する必要があります。

上記のコードでは、データを何度も読み出す必要があるので、DMA 操作を使用してプロセッサから直接ストリーミングできません。