Vitis HLS では、C/C++ ソース コードに対応するハードウェア データパスが作成されます。パイプライン指示子がない場合は、常に順次実行されるのでツールが依存を考慮する必要はありませんが、デザインがパイプライン処理される場合は、Vitis HLS で生成されるハードウェアで可能な限りの依存が考慮される必要があります。
データ依存性またはメモリ依存性は通常、読み出しまたは書き込みの後に読み出しまたは書き込みが実行される場合に発生します。
- RAW (read-after-write) は true 依存とも呼ばれ、命令 (および命令で読み出させる/使用されるデータ) は前の演算の結果に依存します。
- I1: t = a * b;
- I2: c = t + 1;
I2 の読み出しは、I1 の t の書き込みに依存します。命令の順序を変えると、前の t の値が使用されます。
- WAR (write-after-read) は anti 依存とも呼ばれ、前の命令がデータを読み出すまで、現在の命令の書き込みでレジスタまたはメモリをアップデートできません。
- I1: b = t + a;
- I2: t = 3;
I2 の書き込みを I1 の前に実行することはできません。I1 の前に実行すると、b の結果が無効になりす。
- WAW (write-after-write) は、レジスタまたはメモリを特定の順序で書き込む必要がある場合の依存です。この順序に従わないと、ほかの命令が破損することがあります。
- I1: t = a * b;
- I2: c = t + 1;
- I3: t = 1;
I3 の書き込みは、I1 の書き込み後に実行する必要があります。そうしないと、I2 の結果が無効になります。
- read-after-read では、変数が揮発性と宣言されていない場合は、命令の順序を自由に並べ替えることができるので、依存はありません。変数が揮発性と宣言されている場合は、命令の順序を保持する必要があります。
たとえば、パイプラインが生成される場合、後の段階で読み出されるレジスタまたはメモリの位置が前の書き込みで変更されないようにする必要があります。これが true 依存または RAW (read-after-write) 依存です。次はその具体例です。
int top(int a, int b) {
int t,c;
I1: t = a * b;
I2: c = t + 1;
return c;
}
変数 t
に依存があるので、I2
は I1
が完了するまで評価できません。ハードウェアでは、乗算に 3 クロックかかる場合、I2
がその分だけ遅延されます。上記の関数がパイプライン処理されると、Vitis HLS でこれが true 依存として検出されて、演算が適切にスケジューリングされます。データ転送最適化を使用して RAW 依存が削除されて、関数が II=1 で動作できるようになります。
メモリ依存はこの例が変数でなく、配列に適用される場合に発生します。
int top(int a) {
int r=1,rnext,m,i,out;
static int mem[256];
L1: for(i=0;i<=254;i++) {
#pragma HLS PIPELINE II=1
I1: m = r * a; mem[i+1] = m; // line 7
I2: rnext = mem[i]; r = rnext; // line 8
}
return r;
}
上記の例では、ループ L1
のスケジューリングにより、次のようなスケジューリング警告メッセージが表示されます。
WARNING: [SCHED 204-68] Unable to enforce a carried dependency constraint
(II = 1, distance = 1) between 'store' operation (top.cpp:7) of variable 'm', top.cpp:7
on array 'mem' and 'load' operation ('rnext', top.cpp:8) on array 'mem'.
INFO: [SCHED 204-61] Pipelining result: Target II: 1, Final II: 2, Depth: 3.
ユーザーがインデックスを書き込んで別のインデックスを読み出しているので、このループの同じ反復内には問題はありません。2 つの命令は同時に並列で実行可能ですが、2、3 回の反復間は、読み出しと書き込みを監視するようにしてください。
// Iteration for i=0
I1: m = r * a; mem[1] = m; // line 7
I2: rnext = mem[0]; r = rnext; // line 8
// Iteration for i=1
I1: m = r * a; mem[2] = m; // line 7
I2: rnext = mem[1]; r = rnext; // line 8
// Iteration for i=2
I1: m = r * a; mem[3] = m; // line 7
I2: rnext = mem[2]; r = rnext; // line 8
2 つの連続した反復を見てみると、I1
からの乗算結果 m
(レイテンシ=2) がループの次の反復の I2
で読み出され、rnext
に代入されます。この場合、次の反復は前の計算の書き込みが終了する前に mem[i]
の読み出しを開始できないので、RAW 依存があります。
クロック周波数が増加すると、乗算器でより多くのパイプライン段が必要になり、レイテンシが増加します。これにより、II も増加します。
次のコードでは、演算がスワップされ、機能が変更されています。
int top(int a) {
int r,m,i;
static int mem[256];
L1: for(i=0;i<=254;i++) {
#pragma HLS PIPELINE II=1
I1: r = mem[i]; // line 7
I2: m = r * a , mem[i+1]=m; // line 8
}
return r;
}
次のようなスケジューリング警告が表示されます。
INFO: [SCHED 204-61] Pipelining loop 'L1'.
WARNING: [SCHED 204-68] Unable to enforce a carried dependency constraint (II = 1,
distance = 1)
between 'store' operation (top.cpp:8) of variable 'm', top.cpp:8 on array 'mem'
and 'load' operation ('r', top.cpp:7) on array 'mem'.
WARNING: [SCHED 204-68] Unable to enforce a carried dependency constraint (II = 2,
distance = 1)
between 'store' operation (top.cpp:8) of variable 'm', top.cpp:8 on array 'mem'
and 'load' operation ('r', top.cpp:7) on array 'mem'.
WARNING: [SCHED 204-68] Unable to enforce a carried dependency constraint (II = 3,
distance = 1)
between 'store' operation (top.cpp:8) of variable 'm', top.cpp:8 on array 'mem'
and 'load' operation ('r', top.cpp:7) on array 'mem'.
INFO: [SCHED 204-61] Pipelining result: Target II: 1, Final II: 4, Depth: 4.
この後の読み出しと書き込みを数反復間監視してください。
Iteration with i=0
I1: r = mem[0]; // line 7
I2: m = r * a , mem[1]=m; // line 8
Iteration with i=1
I1: r = mem[1]; // line 7
I2: m = r * a , mem[2]=m; // line 8
Iteration with i=2
I1: r = mem[2]; // line 7
I2: m = r * a , mem[3]=m; // line 8
II が長くなるのは、RAW 依存では mem[i]
から r
が読み出され、乗算が実行されてから、mem[i+1]
に書き込まれるからです。
false 依存の削除によるループのパイプライン改善
false 依存とは、コンパイラが保守的すぎる場合に発生する依存のことです。これらの依存は、実際のコードにはありませんが、コンパイラでは判断できません。これらの依存があると、ループ パイプラインがされないことがあります。
次の例は、フォルス依存を示しています。この例では、読み出しおよび書き込みが同じループ反復内の 2 つの異なるアドレスにアクセスします。これらのアドレスはどちらも入力データに依存し、hist
配列の個別エレメントを指定できます。このため、Vitis HLS ではこれらのアクセスのどちらも同じ位置にアクセスできると想定されます。この結果、配列への読み出しと書き込みが交互のサイクルでスケジュールされ、ループ II が 2 になります。ただし、このコードは、hist[old]
および hist[val]
が if(old ==
val)
条件の else 分岐に含まれるため、これらが同じ位置にアクセスすることがないことを示しています。
void histogram(int in[INPUT SIZE], int hist[VALUE SIZE]) f
int acc = 0;
int i, val;
int old = in[0];
for(i = 0; i < INPUT SIZE; i++)
{
#pragma HLS PIPELINE II=1
val = in[i];
if(old == val)
{
acc = acc + 1;
}
else
{
hist[old] = acc;
acc = hist[val] + 1;
}
old = val;
}
hist[old] = acc;
この非効率性を克服するには、DEPENDENCE 指示子を指定して、Vitis HLS に依存性に関する情報を入力します。
void histogram(int in[INPUT SIZE], int hist[VALUE SIZE]) {
int acc = 0;
int i, val;
int old = in[0];
#pragma HLS DEPENDENCE variable=hist type=intra direction=RAW dependent=false
for(i = 0; i < INPUT SIZE; i++)
{
#pragma HLS PIPELINE II=1
val = in[i];
if(old == val)
{
acc = acc + 1;
}
else
{
hist[old] = acc;
acc = hist[val] + 1;
}
old = val;
}
hist[old] = acc;
依存には、主に二種類あります。
- Inter - 依存性が同じループ内の別の反復間にあることを指定します。Vitis HLS でこのタイプの依存性を FALSE に設定すると、ループが展開されていない場合または部分的に展開されていない場合に並列実行が可能になり、TRUE に設定すると並列実行が不可能になります。
- Intra - 反復の開始と終了でアクセスされる配列など、ループ内の同じ反復内の依存性を指定します。このタイプの依存性を FALSE に設定すると、Vitis HLS によりループ内で演算を自由に移動でき、パフォーマンスまたはエリアを向上できる可能性が高くなります。TRUE に設定すると、操作は指定の順序で実行する必要があります。
スカラーの依存
スカラーの依存は解消するのがより困難で、通常ソース コードの変更が必要になります。スカラー データの依存は、次のようになります。
while (a != b) {
if (a > b) a -= b;
else b -= a;
}
このループの次の反復は、次の図に示すように、現在の反復が計算されて、a
および b
の値がアップデートされるまで開始しません。
ループの反復を始めるのに前のループ反復の結果が必要な場合、ループのパイプライン処理は不可能です。Vitis HLS で指定した開始間隔 (II) でパイプライン処理できない場合は、開始間隔が増加されます。パイプライン処理がまったく不可能な場合は、パイプライン処理が停止され、パイプライン処理されないデザインが出力されます。