バースト転送は、大規模なデータ チャンクをグローバル メモリに読み書きすることで、カーネルの 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_length
や max_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_request
、write_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();
- シーケンシャル バースト
-
このバーストは、次のコード例に示すように、小さなデータ サイズの読み出し要求、書き込み要求、書き込み応答がループ本体内にあるシーケンシャル バーストです。シーケンシャル バーストの欠点は、将来の要求 (i+1) が前の要求 (i) の終了に依存するところです。これは、その要求が読み出し要求、書き込み要求、書き込み応答を完了するまで待機し、要求間にギャップが生じるためです。シーケンシャル バーストは、小さなデータ サイズを複数回読み書きしてループ境界を補正するので、パイプライン バーストほど効果的ではありません。これにより、スループットの改善は制限されますが、それでもシーケンシャル バーストはバーストなしよりも優れています。
機能と制限
-
m_axi
要素が構造体の場合:- 構造体は幅の広い int にパックされます。構造体の分割はできません。
- 構造体のサイズは 2 のべき乗で、1024 ビットまたは
config_interface -m_axi_max_bitwidth
コマンドで指定された最大幅を超えないようにします。
-
burst_maxi ポートの ARRAY_PARTITION および ARRAY_RESHAPE は使用できません。
-
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 }
- INTERFACE プラグマまたは指示子を使用して、
num_read_outstanding
およびnum_write_outstanding
、max_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 }
- HLS は
hls::burst_maxi
ポートのビット幅を変更しないため、INTERFACE プラグマまたは指示子max_widen_bitwidth
はサポートされません。 -
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); }
- 読み出しグループ (
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]. }
- 複数の
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] … } }
- 異なるデータフロー プロセスの読み出しまたは書き込み要求、および読み出しまたは書き込みはサポートされていません。データフロー チェッカーは、
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 プロトコル違反になることがあります。