手動バーストの使用 - 2023.2 日本語

Vitis 高位合成ユーザー ガイド (UG1399)

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 日本語

バースト転送は、大規模なデータ チャンクをグローバル メモリに読み書きすることで、カーネルの I/O のスループットを向上させます。バーストのサイズが大きいほどスループットが高くなります。このメトリックは、(転送されたバイト数)*(カーネル周波数)/(時間) で計算されます。カーネル インターフェイスの最大ビット幅は 512 ビットであり、カーネルが 300 MHz で動作するようにコンパイルされている場合、理論的には (DDR の 80-95% 効率)*(512* 300 MHz)/1 秒 = ~17-19 GB/s の DDR を達成できます。説明したように、Vitis HLS は自動的なバースト最適化を実行します。この最適化では、ループ/関数のメモリ アクセスをユーザー コードからインテリジェントに集約し、単一のバースト要求内で特定サイズの読み出し/書き込みを実行します。ただし、バースト転送には、バースト転送の前提条件と制限 に説明されるように、過度に負荷がかかったり、達成が困難になったりするような要件もあります。

自動バースト アクセスが失敗した場合、効率的な解決策は、コードを書き直すか、手動バーストを使用することです。このような場合、AXI4 m_axiプロトコルに精通していて、ハードウェア トランザクション モデリングを理解していれば、次に説明する hls::burst_maxi クラスを使用して手動でバースト転送をインプリメントできます。これらのコンセプトの例は、GitHub の Vitis-HLS-Introductory-Examples/Interface/Memory/manual_burst を参照してください。または、CACHE プラグマまたは指示子を使用して、AXI4 インターフェイスでキャッシュ メモリを使用するという回避策もあります。

hls::burst_maxi クラス

hls::burst_maxi クラスは、DDR メモリへの読み出し/書き込みアクセスを実行するメカニズムを提供します。これらのメソッドは、クラス メソッドの使用動作をそれぞれ AXI4 プロトコルに変換し、AXI4 バス信号 (AW、AR、WDATA、BVALID、RDATA) で要求を送受信します。これらのメソッドは、HLS スケジューラのバースト動作を制御します。DDR メモリにデータを送信するのは、スケジューラからコマンドを受信するアダプターです。これらの要求は、max_read_burst_lengthmax_write_burst_length などのユーザー指定の INTERFACE プラグマのオプションに従います。クラス メソッドはカーネル コードでのみ使用し、テストベンチでは使用しないでください (次に示すクラス コンストラクターを除く)。

  • コンストラクター
    • burst_maxi(const burst_maxi &B) : Ptr(B.Ptr) {}
    • burst_maxi(T *p) : Ptr(p) {}
      重要: burst_maxi(T *p) コンストラクターは C シミュレーション モデルでのみ使用できるため、HLS デザインとテストベンチは異なるファイルに含めておく必要があります。
  • read メソッド:
    • void read_request(size_t offset, size_t len);

      このメソッドは、m_axi アダプターへの読み出し要求を実行するのに使用されます。この関数は、m_axi アダプター内の読み出し要求キューがいっぱいでなければ、すぐに返されます。それ以外の場合は、空間があくまで待機します。

      • offset: データ読み出しのメモリ オフセットを指定します。
      • len: スケジューラのバースト長を指定します。このバースト長はアダプターに送信され、標準の AXI AMBA プロトコルに変換できるようになります
    • T read();

      このメソッドは、m_axi アダプターからスケジューラ FIFO にデータを転送するために使用されます。データが使用できない場合、read() はブロックされます。read() メソッドは、read_request() で指定された len 回数分だけ呼び出されるようにする必要があります。

  • write メソッド:
    • void write_request(size_t offset, size_t len);

      このメソッドは、m_axi アダプターへの書き込み要求を実行するのに使用されます。この関数は、m_axi アダプター内の書き込み要求キューがいっぱいでなければ、すぐに返されます。

      • offset: データ書き込みのメモリ オフセットを指定します。
      • len: スケジューラのバースト長を指定します。このバースト長はアダプターに送信され、標準の AXI AMBA プロトコルに変換できるようになります
    • void write(const T &val, ap_int<sizeof(T)> byteenable_mask = -1); 

      このメソッドは、スケジューラの内部バッファーから m_axi アダプターにデータを転送するために使用されます。内部書き込みバッファーがいっぱいの場合はブロックされます。WDATA のバイトをイネーブルにするには、byteenable_mask を使用します。デフォルトでは、転送のすべてのバイトがイネーブルになります。write() メソッドは、write_request() で指定された len 回数分だけ呼び出されるようにする必要があります。

    • void write_response();

      このメソッドは、すべての書き込み応答がグローバル メモリから返されるまでブロックします。このメソッドは、write_request() と同じ回数呼び出す必要があります。

HLS デザインでの手動バーストの使用

HLS デザインでは、自動バースト転送が指定どおりに機能しておらず、デザインを思ったとおりに最適化できない場合、hls::burst_maxi オブジェクトを使用して読み出しおよび書き込みトランザクションをインプリメントできます。この場合、元のポインター引数を関数引数として burst_maxi に置き換えるようにコードを変更する必要があります。これらの引数には、次の例に示すように、burst_maxi クラスの明示的な read メソッドと write メソッドからアクセスする必要があります。

次は、グローバル メモリからポインター引数を使用する、元のコード例です。
void dut(int *A) {
  for (int i = 0; i < 64; i++) {
  #pragma pipeline II=1
      ... = A[i]
  }
}

次の修正されたコードでは、ポインターが hls::burst_maxi<> クラス オブジェクトおよびメソッドに置き換えられます。この例では、HLS スケジューラは A ポートから m_axi アダプターに 4 つの len 16 を要求します。アダプターはこれらを FIFO 内に格納し、AW/AR バスが利用可能になると、要求をグローバル メモリに送信します。64 回のループ反復で read() コマンドはブロッキング呼び出しを発行し、データがグローバル メモリから返されるまで待機します。データが使用可能になると、HLS スケジューラは m_axi アダプター FIFO からデータを読み出します。

#include "hls_burst_maxi.h"
void dut(hls::burst_maxi<int> A) {
  // Issue 4 burst requests
  A.read_request(0, 16); // request 16 elements, starting from A[0]
  A.read_request(128, 16); // request 16 elements, starting from A[128]
  A.read_request(256, 16); // request 16 elements, starting from A[256]
  A.read_request(384, 16); // request 16 elements, starting from A[384]
  for (int i = 0; i < 64; i++) {
  #pragma pipeline II=1
      ... = A.read(); // Read the requested data
  }
}

次の例 2 では、HLS スケジューラ/カーネルは、合計 2 つの書き込み要求に対して、ポート A からアダプターへ 2 つの要求 (1 つ目の要求は len 2、2 つ目の要求は len 1) を準備します。合計バースト長は 3 つの書き込みコマンドなので、この後それらを送信します。アダプターはこれらの要求を FIFO 内に格納し、AW、W バスが利用可能になると、要求をグローバル メモリに送信します。最後に、2 つの write_response コマンドを使用して、その 2 つの write_requests の応答を待ちます。

void trf(hls::burst_maxi<int> A) {
  A.write_request(0, 2);
  A.write(x); // write A[0]
  A.write_request(10, 1);
  A.write(x, 2); // write A[1] with byte enable 0010
  A.write(x); // write A[10]
  A.write_response(); // response of write_request(0, 2)
  A.write_response(); // response of write_request(10, 1)
}

C シミュレーションでの手動バーストの使用

通常の配列を最上位関数に渡すと、その配列はコンストラクターによって hls::burst_maxi に自動的に変換されます。

重要: burst_maxi(T *p) コンストラクターは C シミュレーション モデルでのみ使用できるため、HLS デザインとテストベンチは異なるファイルに含めておく必要があります。
#include "hls_burst_maxi.h"
void dut(hls::burst_maxi<int> A);
 
int main() {
  int Array[1000];
  dut(Array);
  ......
}

手動バーストを使用したパフォーマンスの最適化

Vitis HLS は、パイプライン バーストとシーケンシャル バーストの 2 種類のバースト動作を特性評価します。

パイプライン バースト
パイプライン バーストは、1 回の要求で最大量のデータを読み書きしてスループットを向上させます。次のコード例に示すように、コンパイラは、read_requestwrite_request、および write_response 呼び出しがループの外側にある場合、パイプライン バーストを推論します。次の例では、size はテストベンチから送信される変数です。
9  int buf[8192];
10  in.read_request(0, size);
11  for (int i = 0; i < size; i++) {
12  #pragma HLS PIPELINE II=1
13     buf[i] = in.read();
14     out.write_request(0, size*NT);
17     for (int i = 0; i < NT; i++) {
19        for (int j = 0; j < size; j++) {
20        #pragma HLS PIPELINE II=1
21           int a = buf[j];
22           out.write(a);
23  }
24 }
25 out.write_response();
図 1. 合成結果

上の図からわかるように、ユーザー コードからバーストが推論され、長さはコンパイル時に変数として記述されます。

図 2. パフォーマンスの利点

HLS コンパイラは、実行時に length = size のバースト要求を送信し、アダプターはユーザー指定の burst_length プラグマ オプションにそれらを分割します。この場合、デフォルトのバースト長は 16 に設定され、それが ARlen および AWlen チャネルで使用されます。転送中にはバブルがないので、読み出し/書き込みチャネルは最大スループットを達成しています。

図 3. 協調シミュレーションの結果
シーケンシャル バースト

このバーストは、次のコード例に示すように、小さなデータ サイズの読み出し要求、書き込み要求、書き込み応答がループ本体内にあるシーケンシャル バーストです。シーケンシャル バーストの欠点は、将来の要求 (i+1) が前の要求 (i) の終了に依存するところです。これは、その要求が読み出し要求、書き込み要求、書き込み応答を完了するまで待機し、要求間にギャップが生じるためです。シーケンシャル バーストは、小さなデータ サイズを複数回読み書きしてループ境界を補正するので、パイプライン バーストほど効果的ではありません。これにより、スループットの改善は制限されますが、それでもシーケンシャル バーストはバーストなしよりも優れています。

  void transfer_kernel(hls::burst_maxi<int> in,hls::burst_maxi<int> out, const int size )
{
  #pragma HLS INTERFACE m_axi port=in depth=512 latency=32 offset=slave
  #pragma HLS INTERFACE m_axi port=out depth=5120 offset=slave latency=32
 
        int buf[8192];
 
 
        for (int i = 0; i < size; i++) {
             in.read_request(i, 1);
        #pragma HLS PIPELINE II=1
            buf[i] = in.read();
        }
 
 
 
        for (int i = 0; i < NT; i++) {
            for (int j = 0; j < size; j++) {
                out.write_request(j, 1);
#pragma HLS PIPELINE II=1
                int a = buf[j];
                out.write(a);
                out.write_response();
 
            }
 
        }
 
    }
図 4. 合成結果

上記のレポート例からわかるように、長さ 1 のバーストが達成されています。

図 5. パフォーマンスへの影響

AXI4 マスター インターフェイス で説明するように、読み出し/書き込みループの R/WDATA チャネルには、読み出し/書き込みレイテンシと同じギャップがあります。読み出しチャネルの場合、ループはすべての読み出しデータがグローバル メモリから戻るまで待機します。書き込みチャネルの場合、最も内側のループが、グローバル メモリから応答 (BVALID) が戻るまで待機します。その結果、パフォーマンスが低下します。また、協調シミュレーションの結果は、このバースト セマンティクスのパフォーマンスが 2 倍低下していることを示します。

図 6. パフォーマンス見積もり

機能と制限

  1. m_axi 要素が構造体の場合:

    • 構造体は幅の広い int にパックされます。構造体の分割はできません。
    • 構造体のサイズは 2 のべき乗で、1024 ビットまたは config_interface -m_axi_max_bitwidth コマンドで指定された最大幅を超えないようにします。
  2. burst_maxi ポートの ARRAY_PARTITION および ARRAY_RESHAPE は使用できません。

  3. INTERFACE プラグマまたは指示子を hls::burst_maxi に適用すると、m_axi インターフェイスを定義できます。burst_maxi ポートがほかのポートにバンドルされている場合は、このバンドル内のすべてのポートが hls::burst_maxi で、同じ要素タイプである必要があります。

    void dut(hls::burst_maxi<int> A, hls::burst_maxi<int> B, int *C, hls::burst_maxi<short> D) {
      #pragma HLS interface m_axi port=A offset=slave bundle=gmem // OK
      #pragma HLS interface m_axi port=B offset=slave bundle=gmem // OK
      #pragma HLS interface m_axi port=C offset=slave bundle=gmem // Bad. C must also be hls::burst_maxi type, because it shares the same bundle 'gmem' with A and B
      #pragma HLS interface m_axi port=D offset=slave bundle=gmem  // Bad. D should have 'int' element type,  because it shares the same bundle 'gmem' with A and B
    }
  4. INTERFACE プラグマまたは指示子を使用して、num_read_outstanding および num_write_outstandingmax_read_burst_length および max_write_burst_length を指定すると、m_axi アダプターの内部バッファーのサイズを定義できます。
    void dut(hls::burst_maxi<int> A) {
      #pragma HLS interface m_axi port=A num_read_outstanding=32 num_write_outstanding=32 max_read_burst_length=16 max_write_burst_length=16
    }
  5. HLS は hls::burst_maxi ポートのビット幅を変更しないため、INTERFACE プラグマまたは指示子 max_widen_bitwidth はサポートされません。
  6. read の前に read_request または write の前に write_request を作成する必要があります。
    void dut(hls::burst_maxi<int> A) {
      ... = A.read();  // Bad because read() before read_request(). You can catch this error in C-sim.
      A.read_request(0, 1); 
    }
  7. 読み出しグループ (read_request() > read()) と書き込みグループ (write_request() > write() > write_response()) のアドレスと有効期間が重複している場合、ツールはアクセス順序を保証できません。C シミュレーションはエラーをレポートします。
    void dut(hls::burst_maxi<int> A) {
      A.write_request(0, 1);
      A.write(x);
      A.read_request(0, 1);
      ... = A.read();  // What value is read? It is undefined. It could be original A[0] or updated A[0].
      A.write_response();
    }
     
    void dut(hls::burst_maxi<int> A) {
      A.write_request(0, 1);
      A.write(x);
      A.write_response();
      A.read_request(0, 1);
      ... = A.read();  // this will read the updated A[0].
    }
  8. 複数の hls::burst_maxi ポートが同じ m_axi アダプターにバンドルされており、そのトランザクション有効期間が重複している場合、動作は想定通りにはなりません。
    void dut(hls::burst_maxi<int> A, hls::burst_maxi<int> B) {
        #pragma HLS INTERFACE m_axi port=A bundle=gmem depth = 10
        #pragma HLS INTERFACE m_axi port=B bundle=gmem depth = 10 
        A.read_request(0, 10);
        B.read_request(0, 10);
         
        for (int i = 0; i < 10; i++) {
            #pragma HLS pipeline II=1
            …… = A.read(); // get value of A[0], A[2], A[4] …
            …… = B.read();  // get value of A[1], A[3], A[5] …
        }
    }
  9. 異なるデータフロー プロセスの読み出しまたは書き込み要求、および読み出しまたは書き込みはサポートされていません。データフロー チェッカーは、multiple writes in different dataflow processes are not allowed というエラーをレポートします。
    次に例を示します。
    void transfer(hls::burst_maxi<int> A)  {
    #pragma HLS dataflow
       IssueRequests(A); // issue multiple wirte_request() of A
       Write(A); // multiple writes to A
       GetResponse(A); // write_response() of  A
    }

懸念事項

次は、手動バースト手法をインプリメントする際に注意する必要がある懸念事項を示しています。

  • デッドロック: 手動バーストを不適切に使用すると、デッドロックが発生する可能性があります。

    read_request ループが要求を読み出し要求 FIFO にプッシュし、この FIFO が空になるのはグローバル メモリからの読み出しが完了した後に限られるため、read() コマンドの実行前に read_requests が多すぎると、デッドロックが発生します。read() コマンドのジョブは、アダプター FIFO からデータを読み出し、要求が完了したことをマークすることで、その後は read_request が FIFO からポップされたら、新しい要求をプッシュできます。

    //reads/writes. will deadlock if N is larger
    for (i = 0; i < N; i++)
     {   A.read_request(i * 128, 16);} 
    for (i = 0; i < 16 *N; i++) {  … = A.read();}
     
     
    for (int i = 0; i < N; i++) {
        p.write_request(i * 128, 16);
      }
      
      for (int i = 0; i < N * 16; i++) {
        p.write(i);
      }
      
      for (int i = 0; i < N; i++) {
        p.write_response();
      }

    上記の例の場合、N が大きいと、N/2 になる傾向があるので、read_request および読み出し FIFO がいっぱいになります。読み出し要求ループが終了せず、読み出しコマンド ループが開始されないので、デッドロックが発生します。

    注記: これは、write_request() コマンドおよび write() コマンドの場合も発生します。
  • AXI プロトコル違反: 書き込み要求と書き込み応答の数は同じである必要があります。要求と応答の数が異なると、AXI プロトコル違反になることがあります。