AI エンジンへの IIR フィルターのインプリメント - パート 2a
バージョン: Vitis 2024.1
下準備
パート 1a では、Emulation-SW モードを使用して、IIR フィルターの 2 次セクションの計算をベクター化することに焦点を当てました。パート 1a の式 (4) を使用すれば、8x12 の定数行列と 12x1 のベクター (連続する 8 つの入力と 4 つのステートを含有) を乗算することで、連続する 8 つの出力を計算できます。
AM009 の図 26 から、浮動小数点ベクター プロセッサは、E6 および E7 の 2 サイクルで、浮動小数点オペランドに対して 8 回の乗累算演算を実行できます。
注記: 図中の赤い破線矢印は、アキュムレータのフィードバック パスを示しています。この結果、8 つの浮動小数点出力を計算するには、理想的には最小で 12*2=24 サイクルが必要となります。
このセクション以降では、デザインを解析して最適化する典型的な手順を示しながら、レイテンシを最小化し、スループットを最大化してみます。
注記:
Julia スクリプト
aie_iir_2a.jl
は、指定された IIR フィルターの係数と、プログラムに必要なヘッダー ファイルを生成します。インパルス信号とフィルターの応答も生成します。生成されたヘッダー ファイルは、
src
ディレクトリに移動する必要があります。生成された
*.dat
ファイルはdata
ディレクトリに移動する必要があります。
Julia スクリプト
check.jl
は、aie_iir_2a.jl
によって生成されたゴールデン インパルス応答と AI エンジンの出力との差を計算します。
カーネル コード
最初の手順では、カーネル コードを次のように使用します。
template<unsigned id>
void SecondOrderSection(
adf::input_buffer<float> & __restrict idata, // 8 input samples per iteration
adf::output_buffer<float> & __restrict odata, // 8 output samples per iteration
const float (&C)[96] // RTP port for coefficient matrix
) {
static Vector8f state_reg = aie::zeros<float, 8>(); // clear states
// input/output iterators
auto inIter = aie::begin_vector<8>(idata);
auto outIter = aie::begin_vector<8>(odata);
Vector8f xreg_hi = *inIter++; // fetch input samples
Vector16f xreg = aie::concat(state_reg, xreg_hi); // xreg[4]: ym2; xreg[5]: ym1; xreg[6]: xm2; xreg[7]: xm1; xreg[8:15]: x0:x7
Vector8f coeff;
VAcc8f acc = aie::zeros<accfloat, 8>();
for (auto i = 0; i < 12; i++) {
coeff = aie::load_v<8>(&C[8 * i]);
float xval = xreg[i + 4];
acc = aie::mac(acc, coeff, xval);
} // end for (auto i = 0; i < 12; i++)
Vector8f yout = acc; // transfer accumulator register to vector register to update states
// update states
state_reg = xreg_hi;
state_reg[4] = yout[6];
state_reg[5] = yout[7];
*outIter++ = yout;
} // end SecondOrderSection()
for
ループは、係数行列の各列を xreg
のエレメントでスケーリングし、結果を累積します。これは、式 (4) の行列とベクター乗算を実行します。
テストベンチ コード
#include "kernel.hpp"
#include "graph.hpp"
#include "C1.h"
using namespace std;
using namespace adf;
// specify the dataflow graph (DFG)
the_graph my_graph;
const unsigned num_pts = 256; // number of sample points in "input.dat"
const unsigned num_iterations = num_pts/8; // number of iterations to run
// main simulation program
int main() {
my_graph.init(); // load the DFG into the AI Engine array, establish connectivity, etc.
my_graph.update(my_graph.cmtx1, C1, 96); // transfer coefficients
my_graph.run(num_iterations); // run the DFG for the specified number of iterations
my_graph.end(); // terminate AI Engine processing
return (0);
} // end main()
テストベンチは、次を実行します。
グラフを初期化。
フィルター係数をロード。
グラフを 32 回実行。
すべての処理を終了。
解析
まず、Vitis Components
ペインの Settings
の下にある launch.json
ファイルを開きます。Part2a_aiesim_1
を選択して AIE Simulator パラメーターの Enable Profile
チェック ボックスをオンにします。ビルドしてから、シミュレーションを実行します。
シミュレーション完了後、次を実行すると、結果の良し悪しをチェックできます。
$ julia check.jl aie
maximum(abs.(err))
が eps(Float32)
より小さい場合、結果は「良好」です。
FLOW
ペインでプロファイラー結果を確認するには、AIE SIMULATOR / HARDWARE
の下で、REPORTS
(Debug
アイコンの下) を展開し、Profile
をクリックします。
AIE SIMULATION
ペインで Total Function Time
をクリックすると、各関数で消費されたサイクル数が表示されます。
注記: カーネル関数 SecondOrderSection<1>
が 32 回実行され、2,313 サイクル間実行されました。各関数呼び出しは、2,313/32 = 72.28 サイクルを消費しました。最小の関数時間は 72 サイクル、最大は 81 サイクルです。これは、最初の呼び出しが 9 サイクル多く消費したことを意味します (81 + 31 * 72 = 2,313)。
もう 1 つ注目する必要があるのは、my_graph.run()
を呼び出し、SecondOrderSection<1>
を呼び出す最上位の main
関数です。Total Function + Descendants Time (cycles)
列は、その関数で消費されたサイクル数を示し、その関数内で呼び出されたほかのすべてのルーチンを含みます。これには、ヒープとスタックの設定、初期化、実際の処理などが含まれます。このインプリメンテーションでは、256 サンプルを処理するのに 4579 サイクル、つまり 4579/256 = 17.89 サイクル/サンプルが使用されました。AI エンジンが 1 GHz クロックで動作する場合、スループットは 1e9 サイクル/秒 ÷ 17.89 サイクル/サンプル = 55.897 Msamples/秒となります。
注記: 主な処理は SecondOrderSection<1>
で実行され、2,313 サイクルを消費します。このため、サンプル処理には 4,579 - 2,313 = 2,266 の「オーバーヘッド」サイクルは使用されません。
生成されたアセンブリ コードを表示するには、Profile Details
をクリックします。
VFPMAC
アセンブラーのニーモニックが表示されるところまでスクロールダウンします。
カーネル コードの次の文は、VFPMAC
ニーモニック (ベクター浮動小数点の乗累算演算) を生成します。また、for
ループが展開され、2 サイクルの浮動小数点累積レイテンシを含めるため、各 VFPMAC
間には NOP
(演算なし) があります。
acc = aie::mac(acc, coeff, xval); // acc[] += coeff[] * xval
VFPMAC
は入力バッファーとして 1024 ビットの y
レジスタを使用します (AM009 の表 9 を参照)。
ya
レジスタは、4 つの 256 ビットの wr[0:3]
レジスタで構成されます。この例では、VLDA
(vector load A) というニーモニックを使って、wr0
レジスタを係数行列の列で更新されます。VLDA
ニーモニックは、8 つの浮動小数点値をデータ メモリからベクター レジスタに転送します。この例では、VLDA
ニーモニック (wr0
にデータをロード) から、VFPMAC
でデータが計算に使用されるまで、7 ~ 8 サイクルのレイテンシがあります。
まとめ
for
ループと AI エンジン API を使用して、浮動小数点の 8x12 行列と 12x1 ベクター乗算を 73 サイクルで計算する方法を示しました。また、生成されたプログラムの統計情報 (消費サイクルなど) を表示し、生成されたアセンブラー コードを調べる方法も紹介しました。
パート 2b では、プログラムをさらに最適化し、すべてのサイクルで乗累算演算を達成します。
サポート
GitHub 問題は、リクエストやバグの追跡に使用します。質問については、forums.xilinx.com を参照してください。
Copyright © 2020-2024 Advanced Micro Devices, Inc