配列の構成 - 2022.1 日本語

Vitis 統合ソフトウェア プラットフォームの資料: アプリケーション アクセラレーション開発 (UG1393)

Document ID
UG1393
Release Date
2022-05-25
Version
2022.1 日本語

Vitis コンパイラでは、大型の配列は PL 領域にあるブロック RAM メモリにマップされます。これらのブロック RAM のアクセス ポイント (またはポート) は最大 2 つなので、ハードウェアにインプリメントされたときに配列のすべての要素に並列にアクセスできず、アプリケーションのパフォーマンスが制限されることがあります。

パフォーマンス要件によっては、配列の一部またはすべての要素に同じクロック サイクルでアクセスすることが必要な場合があります。これには、 pragma HLS array_partition を使用して、コンパイラで配列の要素が分割され、より小さな配列または個別レジスタにマップされるようにします。コンパイラには、次の図に示すように、3 つの配列分割方法があります。3 つの分割方法は、次のとおりです。

  • block: 元の配列の連続した要素が同じサイズのに分割されます。
  • cyclic: 元の配列の要素がインターリーブされて同じサイズのブロックに分割されます。
  • complete: 配列が個別要素に分割されます。これは、メモリの個別レジスタへの分解に相当します。これは ARRAY_PARTITION プラグマのデフォルトです。
図 1. 配列の分割

block および cyclic 分割では、factor オプションを使用して作成する配列の数を指定できます。前の図では、factor オプション 2 が使用され、配列が 2 つの小さな配列に分割されています。配列の要素数がこの係数の整数倍ではない場合、後の配列に含まれる要素数は少なくなります。

多次元配列を分割する際は、dimension オプションを使用して、分割する次元を指定できます。次の図に、次のコード例を dimension オプションを使用して 3 つの方法で分割した場合を示します。
void foo (...) {
  // my_array[dim=1][dim=2][dim=3] 
  // The following three pragma results are shown in the figure below
  // #pragma HLS ARRAY_PARTITION variable=my_array dim=3 <block|cyclic> factor=2
  // #pragma HLS ARRAY_PARTITION variable=my_array dim=1 <block|cyclic> factor=2
  // #pragma HLS ARRAY_PARTITION variable=my_array dim=0 complete
  int  my_array[10][6][4];
  ...   
}
図 2. 配列の次元の分割

dimension を 3 に設定すると 4 つの配列に、1 に設定すると 10 個の配列に分割されます。dimension を 0 に設定すると、すべての次元が分割されます。

分割の重要性

配列の complete 分割では、すべての配列エレメントが個別レジスタにマップされます。これにより、すべてのレジスタに同じサイクルで同時にアクセスできるので、カーネル パフォーマンスが改善します。

注意:
大型の配列の complete 分割すると、PL 領域が大量に使用されるので、注意が必要です。場合によっては、コンパイル プロセスの速度が遅くなり、容量が足りなくなることもあります。このため、配列は必要な場合にのみ分割する必要があります。特定の次元を分割するか、block または cycle 分割を実行することを考慮してみてください。

分割する次元の選択

A と B が 2 つの行列を示す 2 次元配列で、次のような行列乗算アルゴリズムがあるとします。
int A[64][64];
int B[64][64];
 
ROW_WISE: for (int i = 0; i < 64; i++) {
  COL_WISE : for (int j = 0; j < 64; j++) {
    #pragma HLS PIPELINE
    int result = 0;
    COMPUTE_LOOP: for (int k = 0; k < 64; k++) {
      result += A[i ][ k] * B[k ][ j];
    }
    C[i][ j] = result;
  }
}
PIPELINE プラグマにより、ROW_WISE および COL_WISE ループが一緒に平坦化され、COMPUTE_LOOP が完全に展開されます。COMPUTE_LOOP の各反復 (k) を同時に実行するには、行列 A の各列と行列 B の各行に並列でアクセスできるようにする必要があります。このため、行列 A は 2 つ目の次元で分割し、行列 B は最初の次元で分割する必要があります。
#pragma HLS ARRAY_PARTITION variable=A dim=2 complete
#pragma HLS ARRAY_PARTITION variable=B dim=1 complete

cyclic および block 分割の選択

ここでは、同じ行列乗算アルゴリズムを使用して、cyclic および block 分割を選択し、下位のアルゴリズムの配列アクセス パターンを理解することで、適切な係数を指定します。

int A[64 * 64];
int B[64 * 64];
#pragma HLS ARRAY_PARTITION variable=A dim=1 cyclic factor=64
#pragma HLS ARRAY_PARTITION variable=B dim=1 block factor=64
 
ROW_WISE: for (int i = 0; i < 64; i++) {
  COL_WISE : for (int j = 0; j < 64; j++) {
    #pragma HLS PIPELINE
    int result = 0;
    COMPUTE_LOOP: for (int k = 0; k < 64; k++) {
      result += A[i * 64 +  k] * B[k * 64 + j];
    }
    C[i* 64 + j] = result;
  }
}

ここではコード A と B は 1 次元配列であるとします。行列 A の各列と行列 B の各行に並列でアクセスするには、cyclic と block 分割を上記の例のように使用します。行列 A の各列に並列でアクセスするため、cyclic 分割が行サイズとして指定した factor (この場合 64) で適用されています。同様に、行列 B の各行に並列でアクセスするため、block 分割が列サイズとして指定した factor (この場合 64) で適用されています。

キャッシュを使用した配列アクセスの削減

配列はアクセス ポート数が制限されたブロック RAM にマップされるので、配列の繰り返しにより、アクセラレータのパフォーマンスが制限されることがあります。アルゴリズムの配列アクセス パターンを理解し、データをローカルでキャッシュして配列アクセスを制限して、カーネルのパフォーマンスを改善するようにしてください。

次のコード例では、配列へのアクセスにより最終インプリメンテーションのパフォーマンスが制限されます。この例では、配列 mem[N] に 3 回アクセスして合計を作成しています。
#include "array_mem_bottleneck.h"
dout_t array_mem_bottleneck(din_t mem[N]) {  
  dout_t sum=0;
  int i;
  SUM_LOOP:for(i=2;i<N;++i) 
    sum += mem[i] + mem[i-1] + mem[i-2];    
  return sum;
}
上記のコード例を次のように変更すると、II=1 でパイプライン処理できるようになります。先行読み出しを実行し、データ アクセスを手動でパイプライン処理することで、ループの各反復で指定される配列読み出しを 1 回だけにしています。これにより、パフォーマンスを達成するためには、シングル ポート ブロック RAM だけが必要となります。
#include "array_mem_perform.h"
dout_t array_mem_perform(din_t mem[N]) {  
  din_t tmp0, tmp1, tmp2;
  dout_t sum=0;
  int i;
  tmp0 = mem[0];
  tmp1 = mem[1];
  SUM_LOOP:for (i = 2; i < N; i++) { 
    tmp2 = mem[i];
    sum += tmp2 + tmp1 + tmp0;
    tmp0 = tmp1;
    tmp1 = tmp2;
  }     
  return sum;
}