データ転送インターフェイスに関する考慮事項 - 2022.1 日本語

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

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

システム デザインでは、アクセラレータとホスト間のデータ転送モードを正しく指定することが重要です。次のセクションでは、その設計に関する詳細を説明します。

グローバル メモリ I/O

VSC では、グローバル メモリとアクセラレータ インターフェイス間で、次の 2 つの転送モードを使用できます (ガイダンス マクロ を参照)。

  1. DATA_COPY: 効率的に設計された RTL IP であるデータ ムーバーをドロップインします。これにより、グローバル メモリへの M_AXI インターフェイスでのバーストやデータ幅の操作などの特定の機能が自動化されます。アクセラレータ側では、このインターフェイスは、Vitis HLS ap_fifo インターフェイス内のデータのシーケンシャル アクセスと、Vitis HLS ap_memory インターフェイス内のデータのランダム アクセスの両方がサポートされます。
  2. ZERO_COPY: グローバル メモリ プラットフォーム ポートからアクセラレータに M_AXI インターフェイスを接続します。

compute() 関数インターフェイスの定義には、次が推奨されます。

  1. データ サイズ (すべての計算引数) が大きすぎてデバイスの DDR に収まらない場合は、複数の送信イテレーションで大きな alloc_buf 計算を小さなチャンクに分割します。
  2. 通常は、DATA_COPY および SEQUENTIAL アクセス パターン (特にアクセラレータ コードがシーケンシャル入力データ アクセスの場合) をお勧めします。
  3. アクセラレータ コードがデータにランダムにアクセスしていて、シーケンシャルにアクセスするようにコードを変更できない場合、データがデバイス RAM (BRAM または URAM) に収まる場合はランダム アクセス パターンを使用します。それ以外の場合は、ZERO_COPY を使用します。

シーケンシャル アクセス パターン

カーネル コードによってシーケンシャルにアクセスされる PE ポート data の場合、ACCESS_PATTERN(data, SEQUENTIAL); を使用します。

VSC では、実行時にデータのサイズが必要です。これには、DATA_COPY マクロを使用する必要があります。たとえば、DATA_COPY(data, data[numData]); のように使用します。

datanumData の両方を引数としてカーネルに渡す必要があり、たとえば、カーネルが numData を使用しない場合でも、引数として指定しておく必要があります。通常、numData は、カーネル関数の引数に関して実行時に評価できる限り、任意の式にできます。次の例は、あまり実用的ではないかもしれませんが、アクセラレータ クラス宣言で使用できます。

DATA_COPY(data, data[m * log(n) + 5]);
...
static void PE_func(int n, int m, float* data); 

入力データと出力データの両方で、DATA_COPY の正確なサイズが重要であり、カーネルが読み出しているデータ量とまったく同じである必要があります。サイズがデザインと合わない場合、次のような機能上の問題が発生する可能性があります。

  • カーネルがアプリケーション コードにより提供されるデータよりも多くのデータを読み出すと、実行時にハングします。
  • 前のカーネル呼び出しが前の計算呼び出しからすべてのデータを読み出さなかった場合、カーネルが不要なデータを読み出します。

これを回避してデバッグするには、ZERO_COPY を使用してカーネル コードが正しく動作することを確認します。

ランダム アクセス パターン

カーネルが compute() 関数引数にアクセスする必要がある場合は、ACCESS_PATTERN(data, RANDOM); のように、data をランダムに指定します。

ヒント: ランダム アクセス パターンには、BRAM によるサイズ制限 (通常 32 Kb ブロック内) を持つローカル FIFO バッファーが必要です。オンチップ メモリには、ユーザー定義引数のサイズと同じ数の BRAM が必要です。

したがって、データがオンチップ FPGA メモリ内に収まるようにする必要があります。カーネル コードは、次の例のように、データをスタティック配列として宣言する必要があります。

ACCESS_PATTERN(data, RANDOM);
...
static void PE_func(int n, int m, float data[64]);

データ サイズがランダムにアクセスされ、オンチップ FPGA メモリには大きすぎる場合は、ZERO_COPY(data) を使用する必要があります。

重要: ACCESS_PATTERN マクロは、ZERO_COPY と一緒に使用しないでください。

ホストとグローバル メモリ間で転送されるデータの単位量は、必ずしも DATA_COPY のサイズと同じではありません。これは、VPP_ACC::alloc_buf() 呼び出しのサイズ引数によって決定されます。このサイズは、たとえば、複数の compute() 呼び出しのデータをワンショットで送信する場合など、各カーネル計算に必要なデータ サイズよりも大きくなることがあります。したがって、compute() への (たとえば N 呼び出しの) PCIe データ転送のクラスターは、次のように簡単に実行できます。

send_while ... { ...
  clustered_buffer = acc::alloc_buf( N * size );
  for (i = 0; i < N; i++) { ...
    acc::compute(&clustered_buf[ i * size ] ...
  }
  1. N 計算呼び出しに適切なデータ バッファー サイズを割り当てます。
  2. compute() を N 回を呼び出すと、各呼び出しがインデックス指定されて、クラスターされたバッファーに入れられます。

計算ペイロード データ型

compute() データ型ではグローバル メモリ上のデータ レイアウトも決まるので、アクセラレータのパフォーマンスに影響します。カーネルができるだけ速くデータにアクセスできるようにするには、compute() 引数に適切なデータ型を選択することが重要です。たとえば、カーネルが整数を処理していて、クロック サイクルごとに 1 つの整数を処理する必要がある場合 (HLS II = 1)、インターフェイスは次のような整数の配列を使用できます。

static void compute ( int* A );

次の 2 つのコーディング スタイルを使用して、計算呼び出しごとに 4 つの整数を追加する別の例を考慮してください。

// --- acc interface
DATA_COPY(data, data[numData*4]); 

// --- application code
int data[numData*4];
...

static void acc::compute(int* data, ... );

// --- 4-cycle kernel code
void PE_func(int* data, int numData, int *out) {
  for (int i=0; i < numData; i++) {
    int o = i * 4;
    out = data[o+0]+data[o+1]+data[o+2]+data[o+3];
  }
// --- acc interface
DATA_COPY(data, data[numData]); 

// --- application code
struct data_t { int i[4]; };
data_t *data;
...
static void acc::compute(data_t* data, ... );

// --- 1-cycle kernel code with packed data type
void PE_func(data_t* di, int numData, int *out) {
  for (int i=0; i < numData; i++) {
    data_t data = di[i];
    out = data.i[0]+data.i[1]+data.i[2]+data.i[3];
  }

カーネルが、入力配列から 4 つの整数を 1 つずつ追加します。左側の簡単なインプリメンテーションでは、メモリ アクセスごとに 1 サイクルとした場合、結果ごとに 4 クロック サイクルが必要になります。ただし、右に示すように、4 つの整数すべてを 1 つのグローバル メモリ アクセスにパックすることをお勧めします。したがって、この場合は次をお勧めします。

  • C 構造体を使ってすべてのデータをパックし、カーネルに渡します。
  • DATA_COPY(data, data[numData]); を使用して、正しいデータ コピー サイズが提供されていることを確認します。

それ以外にも、次の点に注意してください。

  • int* data[4] を使用すると、整数はパックされず、int* data; と同じハードウェアになります。
  • 通常、64 バイト (512 ビットの M_AXI バス幅の場合) を超える容量をパッキングしてもパフォーマンスは向上しませんが、パフォーマンスが低下することはありません。