ループ - 2023.2 日本語

AI エンジン カーネルおよびグラフ プログラミング ガイド (UG1079)

Document ID
UG1079
Release Date
2023-12-04
Version
2023.2 日本語

AI エンジンには、比較および分岐のための分岐制御オーバーヘッドを発生させない 0 オーバーヘッド ループ構造があり、内部ループ サイクル カウントを削減できます。パイプライン処理により、コンパイラでプリアンブルおよびポストアンブルが追加され、ループ実行中に命令パイプラインが常にフルに保たれます。パイプライン ループを使用すると、前の反復が終了する前に新しい反復を開始して、より高い命令レベルの並列処理を実現できます。

次の図に、0 オーバーヘッド ループのアセンブリ コードを示します。2 つのベクター ロード、1 つのベクター ストア、1 つのスカラー命令、2 つのデータ移動、および 1 つのベクター命令が順に異なるスロットに表示されています。

図 1. 0 オーバーヘッド ループのアセンブリ コード

次のプラグマは、コンパイラにループをパイプライン処理するように指示し、ループが常に 3 回以上実行されることを示します。

for (int i=0; i<N; i+=2)
   chess_prepare_for_pipelining
   chess_loop_range(3,)

chess_loop_range(<minimum>, <maximum>) は、対応するループが <minimum> 回以上、<maximum> 回以下実行されることをコンパイラに通知します (<minimum> および <maximum> は負でない定数式、または省略可能)。省略すると、<minimum> はデフォルトで 0 に、<maximum> はコンパイラの最大プリセットになります。<maximum> はパイプラインのインプリメンテーションには関係ありませんが、<minimum> はパイプラインのインプリメンテーションをガイドする値になります。

<minimum> の数値は、ループが実行されるたびに、最小で実行されるループ反復の数を定義します。ソフトウェア パイプラインは、可能であれば、少なくともその数の反復を並行して実行できるように調整されます。また、反復が <minimum> 回繰り返される前にループの境界をチェックする必要がないことも指定します。

ループ範囲がコンパイル時間定数の場合、ループ範囲プラグマは必要ありません。通常、AI エンジン コンパイラはアルゴリズムのパイプラインが最適になるように理論上最適な数値をレポートします。範囲仕様が最適でない場合、コンパイラは警告メッセージを表示し、最適な範囲を提案します。この場合、最初に <minimum> を 1 つの [chess_loop_range(1,)] に設定して、コンパイラでレポートされる理論的に最適な <minimum> を確認することにしても問題ありません。

Warning in "matmul_vec16.cc", line 10: (loop #39)
further loop software pipelining (to 4 cycles) is feasible with `chess_prepare_for_pipelining'
but requires a minimum loop count of 3
... consider annotating the loop with `chess_loop_range(3,)' if applicable,
... or remove the current `chess_loop_range(1,)` pragma

この時点で、レポートされた最適な値に <minimum> をアップデートすることもできます。

この 2 つ目のパイプライン インプリメンテーション部分により、実際の反復回数 <minimum> 回に到達しない場合に AI エンジン カーネルでデッドロックが発生することもあります。このため、反復回数は常に chess_loop_range 指示子で指定された数以上にする必要があります。

ループの依存関係は、コードのベクター化に影響します。内部ループの依存関係を削除できない場合は、レベルをステップアウトし、内部ループの複数のコピーが並列で実行される箇所を手動で展開します。

データをループする場合に、特定のオフセットで増減するには、循環バッファーに cyclic_add 組み込み関数を使用します。fft_data_incr 組み込み関数は、バタフライ演算の現在のターゲットであるポインターの反復を有効にします。これらの関数を使用すると、標準の C で同等の機能をコーディングする際に複数のクロック サイクルを節約できます。データ型によっては、パラメーターと戻り値の型キャストを実行する必要があります。

次の例では、実数の行列での演算に fft_data_incr 組み込み関数を使用しています。

pC = (v8float*)fft_data_incr( (v4cfloat*)pC, colB_tiled, pTarget);

使用する前に、ベクター レジスタを完全に満たす連続的なロードがないようにしてください。現在の MAC と次のロードを同じサイクルで実行できる MAC 組み込み関数を使用してロードをインターリーブすることをお勧めします。

acc = mul4_sym(lbuff, 4, 0x3210, 1, rbuff, 11, coeff, 0, 0x0000, 1);
lbuff = upd_w(lbuff, 0, *left);
acc = mac4_sym(acc, lbuff, 8, 0x3210, 1, rbuff, 7, coeff, 4, 0x0000, 1);

ユース ケースによっては、ループ内の命令を回転させるループ回転が役立つことがあります。ループの開始時にデータをベクターにロードするのではなく、ループの前に最初の反復のデータ ブロックをロードしてから、ループの終了時に近い次の反復のデータ ブロックをロードすることを検討してください。これにより、命令が追加されますが、ループの依存関係の長さが短くなり、ループ範囲が小さい理想的なループを実現しやすくなります。

// Load starting data for first iteration
sbuff = upd_w(sbuff, 0, window_readincr_v8(cb_input)); // 0..7
 
for ( int l=0; l<LSIZE; ++l )
chess_loop_range(5,)
chess_prepare_for_pipelining
{
   sbuff = upd_w(sbuff, 1, window_readincr_v8(cb_input)); // 8..15
   acc0 = mul4_sym(     sbuff,5 ,0x3210,1 ,12 ,coe,4,0x0000,1);

   sbuff = upd_w(sbuff, 2, window_readdecr_v8(cb_input)); // 16..23
   acc0 = mac4_sym(acc0,sbuff,1 ,0x3210,1 ,16,coe,0,0x0000,1);
   acc1 = mul4_sym(     sbuff,5 ,0x3210,1 ,20,coe,0,0x0000,1);
   window_writeincr(cb_output, srs(acc0, shift));
   // Load data for next iteration
   sbuff = upd_w(sbuff, 0, window_readincr_v8(cb_input)); // 0..7
   acc1 = mac4_sym(acc1,sbuff,9,0x3210,1,16,coe,4,0x0000,1);
   window_writeincr(cb_output, srs(acc1, shift));
}