2.6. HTMLレポートの情報に基づいたOpenCLのデザイン例の最適化
行列の正方形AxAを実行するOpenCLのデザイン例:
// performs matrix square A*A
// A is a square len*len matrix
kernel void matrix_square (global float* restrict A,
unsigned len,
global float* restrict out) {
for(unsigned oi = 0; oi < len*len; oi++) {
float sum = 0;
int row = oi % len;
for (int col = 0; col < len; col++) {
unsigned i = (row * len) + col; // 0, 1, 2, 3, 4,...
unsigned j = (col * len) + row; // 0, 3, 6, 9, 1,...
sum += A[i] * A[j];
}
out[oi] = sum;
}
}
カーネルmatrix_squareのエリアレポートのシステムビューは、 ブロック3のフリップフロップ(FF)とRAMの推定使用量が高いことを示しています。システムビューアのBlock3をさらに調べると、Block3のレイテンシー値も高いことがわかります。
これらのパフォーマンスのボトルネックの原因は、システムがループ内からグローバルメモリーからデータをロードしているためです。したがって、次の変更されたコードに示すように、データをローカルメモリーにプリロードすることが、最初に行う最適化のステップです。
kernel void matrix_square_v1 (global float* restrict A,
unsigned len,
global float* restrict out)
{
// 1. preload the data into local memory
// - suppose we know the max size is 4X4
local int cache_a[16];for(unsigned k = 0; k < len*len; k++) { cache_a[k] = A[k];}
for(unsigned oi = 0; oi < len*len; oi++) {
float sum = 0;
int row = oi % len;
for(int col = 0; col < len; col++) {
unsigned i = (row * len) + col; // 0, 1, 2, 3, 4,...
unsigned j = (col * len) + row; // 0, 3, 6, 9, 1,...
sum += cache_a[i] * cache_a[j];
}
out[oi] = sum;
}
}
エリアレポートとシステムビューアの結果に示されているように、ローカルメモリーにデータをプリロードすると、RAMの使用量が3分の1に減少し、レイテンシー値が255から97に低下します。
matrix_square_v1のエリアレポートをさらに調べると、以下のエリアレポートの行30であるコードラインint row = oi%lenは、法計算のために異常に大きなエリアが使用されます。
モジュラス計算を削除して列カウンターに置き換えると、修正されたカーネルmatrix_square_v2に示すように、適応ルックアップテーブル(ALUT)およびFF使用量を50%削減できます。
kernel void matrix_square_v2 (global float* restrict A,
unsigned len,
global float* restrict out)
{
// 1. preload the data into local memory
// - suppose we know the max size is 4X4
// 2. remove the modulus computation
local int cache_a[16];
for (unsigned k = 0; k < len*len; k++) {
cache_a[k] = A[k];
}
unsigned row = 0;
unsigned ci = 0;
for (unsigned oi = 0; oi < len*len; oi++) {
float sum = 0;
// keep a column counter to know when to increment rowif (ci == len) { ci = 0; row += 1;}ci += 1;
for (int col = 0; col < len; col++) {
unsigned i = (row * len) + col; // 0, 1, 2, 3, 4,...
unsigned j = (col * len) + row; // 0, 3, 6, 9, 1,...
sum += cache_a[i] * cache_a[j];
}
out[oi] = sum;
}
}
matrix_square_v2のエリアレポートをさらに調べると、インデックスiとjの計算(つまり、符号なしi =(row * len)+ colおよび符号なしj =(col * len)+ row)ではALUTとFFの使用量の見積もりが非常に異なることがわかります。さらに、エリアレポートは、これらの2つの計算がデジタル信号処理(DSP)ブロックを使用していることも示しています。
インデックス計算のためにDSPとRAMブロックの使用を最適化する方法は、乗算計算を削除して、下記の修正されたカーネルmatrix_square_v3に示すように、加算を追跡するだけです。
kernel void matrix_square_v3 (global float* restrict A,
unsigned len,
global float* restrict out) {
// 1. preload the data into local memory
// - suppose we know the max size is 4X4
// 2. remove the modulus computation
// 3. remove DSP and RAM blocks for index calculation helps reduce the latency
local int cache_a[16];
for (unsigned k = 0; k < len*len; k++) {
cache_a[k] = A[k];
}
unsigned row_i = 0;
unsigned row_j = 0;
unsigned ci = 0;
for (unsigned oi = 0; oi < len*len; oi++) {
float sum = 0;
unsigned i, j;
// keep a column counter to know when to increment row
if (ci == len) {
ci = 0;
row_i += len;
row_j += 1;
}
ci += 1;
i = row_i; // initialize i and jj = row_j;for (int col = 0; col < len; col++) {i += 1; // 0, 1, 2, 3, 0,...j += len; // 0, 3, 6, 9, 1,...
sum += cache_a[i] * cache_a[j];
}
out[oi] = sum;
}
}
乗算ステップを削除することで、下記のエリアレポートに示すように、DSP使用率を50%削減できます。さらに、この修正はレイテンシーを短縮するのに役立ちます。
レイテンシーをさらに削減するために、修正されたカーネルmatrix_square_v3のループ分析レポートを確認することができます。以下に示すように、解析ペインと詳細ペインでは、 sum + = cache_a [i] * cache_a [j]のループに依存する依存関係があるため、Block27にIIボトルネックが発生しています。
ループで運ばれる依存関係を解決するには、修正されたカーネルmatrix_square_v4で強調表示されているコードに示すように、計算の乗算部分と加算部分を分けることができます。
kernel void matrix_square_v4 (global float* restrict A,
unsigned len,
global float* restrict out)
{
// 1. preload the data into local memory
// - suppose we know the max size is 4X4
// 2. remove the modulus computation
// 3. remove DSP and RAM blocks for index calculation helps reduce the latency
// 4. remove loop-carried dependency 'sum' to improve throughput by trading off area
local int cache_a[16];
for (unsigned k = 0; k < len*len; k++) {
cache_a[k] = A[k];
}
unsigned row_i = 0;
unsigned row_j = 0;
unsigned ci = 0;
for (unsigned oi = 0; oi < len*len; oi++) {
float sum = 0;
unsigned i, j;
float prod[4]; // make register
#pragma unroll
for (unsigned k = 0; k < 4; k++) {
prod[k] = 0;
}
// keep a column counter to know when to increment row
if (ci == len) {
ci = 0;
row_i += len;
row_j += 1;
}
ci += 1;
i = row_i; // initialize i and j
j = row_j;
for (int col = 0; col < len; col++) {
i += 1; // 0, 1, 2, 3, 0,...
j += len; // 0, 3, 6, 9, 1,...
prod[col] = cache_a[i] * cache_a[j];
}
sum = prod[0];#pragma unrollfor (unsigned k = 1; k < 4; k++) { sum += prod[k];}
out[oi] = sum;
}
}
以下のエリアレポートおよびシステムビューアの結果に示されているように、計算ステップを分割することで、エリア使用量の増加を犠牲にしてより高いスループットを達成できます。この変更により、ループのII値が1に減少し、レイテンシーが30から24に減少します。