AI エンジンへの IIR フィルターのインプリメント - パート 2b
バージョン: Vitis 2024.1
下準備
パート 2a では、生成されたアセンブラー コードを調べ、VFPMAC
(ベクター浮動小数点の乗累算演算) ニーモニックの間に NOP
(演算なし) を見つけました。浮動小数点累積には 2 サイクルを要するため、この NOP
は避けられません (AM009 の図 26 を参照)。
行列ベクター乗算を 2 つの別々の乗累算演算に分割し、各サイクルで浮動小数点の累算を実行します。
注記: 行列の各行と列ベクターを乗算する従来の方法ではなく、乗累算演算 API を使用して、行列の各列をベクターの対応するエレメントで効果的にスケーリングします。
この結果、ベクター加算を偶数部と奇数部に分けることで、独立した乗累算演算ができます。
AI エンジンには 2 つのロード ユニットがあります。Julia プログラム aie_iir_2b.jl
は、行列を偶数列と奇数列に分割し、2 つの別々のヘッダー ファイルを生成するように変更されます。
まず、AI エンジン API を使用して開始します。
カーネル ヘッダー
#ifndef __KERNEL_HPP__ // include guard to prevent multiple inclusion
#define __KERNEL_HPP__
#include <adf.h> // Adaptive DataFlow header
#include <aie_api/aie.hpp> // header files for high-level intrinsics
using Vector8f = aie::vector<float, 8>; // vector of 8 floating-point elements
using Vector16f = aie::vector<float, 16>; // vector of 16 floating-point elements
using VAcc8f = aie::accum<accfloat, 8>; // accumulator with 8 floating-point elements
define USE_API // comment out to use low-level intrinsics
const unsigned burst_cnt = 256; // process burst_cnt * 8 samples per function invocation
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_e)[48], // run-time parameter: SIMD matrix of coefficients (even columns)
const float (&C_o)[48] // run-time parameter: SIMD matrix of coefficients (odd columns)
);
#endif // __KERNEL_HPP__
カーネル コード (AI エンジン API)
#include <aie_api/aie_adf.hpp>
#include "kernel.hpp"
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_e)[48], // run-time parameter: SIMD matrix of coefficients (even columns)
const float (&C_o)[48] // run-time parameter: SIMD matrix of coefficients (odd columns)
) {
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);
for (auto i = 0; i < burst_cnt; i++) {
Vector8f xreg_hi = *inIter++; // fetch input samples
Vector16f xreg = aie::concat(state_reg, xreg_hi);
auto ecoeff_iter = aie::begin_vector<8>(&C_e[0]);
auto ocoeff_iter = aie::begin_vector<8>(&C_o[0]);
VAcc8f acc_e = aie::zeros<accfloat, 8>(); // even accumulator
VAcc8f acc_o = aie::zeros<accfloat, 8>(); // odd accumulator
for (auto j = 0; j < 6; j++) {
acc_e = aie::mac(acc_e, xreg.get(2 * j + 4), *ecoeff_iter++); // even columns
acc_o = aie::mac(acc_o, xreg.get(2 * j + 5), *ocoeff_iter++); // odd columns
} // end for (auto j = 0; j < 6; j ++)
acc_o = aie::add(acc_o, acc_e.to_vector()); // acc_o += acc_e
Vector8f yout = acc_o.to_vector();
// update states
state_reg = xreg_hi;
state_reg[4] = yout[6];
state_reg[5] = yout[7];
*outIter++ = yout;
} // end for (auto i = 0; i < burst_cnt; i++)
} // end SecondOrderSection()
関数には、ループが 2 つあります。
for (auto i = 0; i < burst_cnt; i++) { // process more samples to reduce overhead
...
for (auto j = 0; j < 6; j++) { // matrix-vector multiplication
...
}
}
外側の for
ループは、各関数呼び出し間でより多くのサンプルを処理して、その結果、関数呼び出しサイクルと処理サイクルの比率を減らし、スループットを向上させるために追加されます。
グラフ コード
#ifndef __GRAPH_H__ // include guard to prevent multiple inclusion
#define __GRAPH_H__
#include <adf.h> // Adaptive DataFlow header
#include "kernel.hpp"
using namespace adf;
// dataflow graph declaration
class the_graph : public graph { // inherit all properties of the adaptive dataflow graph
public:
input_plio pl_in;
output_plio pl_out;
kernel section1;
input_port cmtx_e; // input port for SIMD matrix coefficients (even columns)
input_port cmtx_o; // input port for SIMD matrix coefficients (odd columns)
// constructor
the_graph() {
// associate the kernel with the function to be executed
section1 = kernel::create(SecondOrderSection<1>);
pl_in = input_plio::create("Input", plio_32_bits, "data/input.dat");
pl_out = output_plio::create("Output", plio_32_bits, "output.dat");
const unsigned num_samples = 8 * burst_cnt;
// declare buffer sizes
dimensions(section1.in[0]) = {num_samples};
dimensions(section1.out[0]) = {num_samples};
// establish connections
connect<parameter>(cmtx_e, adf::async(section1.in[1]));
connect<parameter>(cmtx_o, adf::async(section1.in[2]));
connect(pl_in.out[0], section1.in[0]);
connect(section1.out[0], pl_out.in[0]);
// specify which source code file contains the kernel function
source(section1) = "kernel.cpp";
// !!! temporary value: assumes this kernel dominates the AI engine tile !!!
runtime<ratio>(section1) = 1.0;
} // end the_graph()
}; // end class the_graph
#endif // __GRAPH_H__
テストベンチ コード
#include "kernel.hpp"
#include "graph.hpp"
#include "C1_e.h"
#include "C1_o.h"
using namespace std;
using namespace adf;
// specify the DFG
the_graph my_graph;
// main simulation program
int main() {
my_graph.init(); // load the DFG into the AI engine array, establish connectivity, etc.
my_graph.update(my_graph.cmtx_e, C1_e, 48);
my_graph.update(my_graph.cmtx_o, C1_o, 48);
my_graph.run(1); // run the DFG for the specified number of iterations
my_graph.end(); // housekeeping
return (0);
} // end main()
解析 (AI エンジン API を使用)
生成されたコード
生成されたアセンブリ コードには、偶数列と奇数列それぞれに 6 つずつ、そして最終的なアキュムレータの結果を合計するためにもう 1 つ、合計 13 個の
VFPMAC
があります。VFPMAC
命令はしっかりとパックされていません。つまり、VFPMAC
の中にはその間にほかの命令を含むものもあります。13 個の VFPMACs
が発生するセクションが 2 つあり、外側のループの反復回数が実質的に半分になります。
スループット
burst_cnt
変数は、各関数呼び出し中に処理されるサンプル数を決定します。内側のループは、1 回の反復で 8 サンプルを処理するので、処理されたサンプルの総数は burst_cnt
* 8 になります。
スループットは、次のように計算されます (api_thruput.xlsx
を参照)。
デザインをビルドおよび実行します。
aiesimulator_output/default.aierun_summary
を開きます。main
関数 (num_cycles
) のTotal Function + Descendants Time (cycles)
を取得します。スループット =
clk_freq
(burst_cnt
8)/num_cycles です。
burst_cnt
の値を変えた場合の 1 GHz クロックでのスループットは、次のとおりです。
IIR スループット (API を使用した場合) | | | | | | | | | |—————————|——-|——-|——-|——-|——-|——-|——-| |burst_cnt |1 |8 |16 |32 |64 |128 |256 | |num_samples |8 |64 |128 |256 |512 |1024 |2048 | |num_cycles (API) |187 |492 |940 |1836 |3628 |7212 |14379 | |API Throughput (Msa/sec) |42.78 |130.08 |136.17 |139.43 |141.12 |141.99 |142.43 |
*clk_freq: 1 GHz
AI エンジン API はヘッダーのみのインプリメンテーションで、ユーザー組み込み関数と下位組み込み関数 (LLI) の間の「バッファー」として機能し、抽象度を高めます。
下位組み込み関数 (LLI) を使用するようにカーネル コードを修正してください。
カーネル コード (LLI)
#include <aie_api/aie_adf.hpp>
#include "kernel.hpp"
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_e)[48], // run-time parameter: SIMD matrix of coefficients (even columns)
const float (&C_o)[48] // run-time parameter: SIMD matrix of coefficients (odd columns)
) {
static v8float state_reg = null_v8float();
// input/output iterators
auto inIter = aie::begin_vector<8>(idata);
auto outIter = aie::begin_vector<8>(odata);
for (auto i = 0; i < burst_cnt; i++) {
v8float xreg_hi = *inIter++;
v16float xreg = concat(state_reg, xreg_hi);
v8float acc_e = null_v8float();
v8float acc_o = null_v8float();
v8float *ptr_coeff_e = (v8float *)(&C_e[0]);
v8float *ptr_coeff_o = (v8float *)(&C_o[0]);
for (auto j = 0; j < 6; j++)
chess_flatten_loop
{
acc_e = fpmac(acc_e, xreg, (2 * j + 4), 0, *ptr_coeff_e++, 0, 0x76543210); // even columns
acc_o = fpmac(acc_o, xreg, (2 * j + 5), 0, *ptr_coeff_o++, 0, 0x76543210); // odd columns
} // end for (auto j = 0; j < 6; j++)
acc_o = fpadd(acc_o, acc_e);
*outIter++ = acc_o;
// update states
state_reg = xreg_hi;
state_reg = upd_elem(state_reg, 4, ext_elem(acc_o, 6));
state_reg = upd_elem(state_reg, 5, ext_elem(acc_o, 7));
} // end for (auto i = 0; i < burst_cnt; i++)
} // end SecondOrderSection()
注記:
chess_flatten_loop
プラグマを使用します。このプラグマはループを完全に展開し、ループ コンストラクトを削除します。コンパイラ プラグマに関する資料は、AI エンジン ラウンジにあります。提供されたコードでは、API と LLI 間の選択は、
kernel.hpp
の 17 行目でUSE_API
を定義またはコメントアウトすると実行されます。
生成されたアセンブリ コードは次のとおりです。
VFPMAC
間のスペースがきつくなっていることに注意してください。また、SecondOrderSection<1>
関数は main 関数に「吸収」されます。2 つの展開された行列ベクター乗算ループがあるので、外側ループの反復回数が実質的に半分になっています。
測定されたスループットは、次のように計算されます (lli_thruput.xlsx
を参照)。
IIR スループット (LLI を使用した場合) | | | | | | | | | |—————————|——-|——-|——-|——-|——-|——-|——-| |burst_cnt |1 |8 |16 |32 |64 |128 |256 | |num_samples |8 |64 |128 |256 |512 |1024 |2048 | |num_cycles (LLI) |186 |250 |458 |874 |1706 |3370 |6698 | |LLI Throughput (Msa/sec) |43.01 |256.00 |279.48 |292.91 |300.12 |303.86 |305.76 |
*clk_freq: 1GHz
API と LLI のスループットを比較します。
LLI は、同じ
burst_cnt
で API よりも優れたスループットを提供します。スループットは約
burst_cnt
= 64 で飽和します。
まとめ
AI エンジン API は、下位組み込み関数に対する抽象度を高めることで、生産性を向上させることを目的としています。
AI エンジン API を使用し、下位組み込み関数のみを使用することで、ターゲット仕様を満たすパフォーマンスを達成することをお勧めします。
スループットは、次の方法を使って向上できます。
できるだけ多くのサンプルを関数内で処理することで、関数呼び出しのオーバーヘッドを減らします。
浮動小数点の累積には、下位組み込み関数を持つ 2 つのアキュムレータを使用します。
スループットをさらに向上させることはできますか。
浮動小数点では、1 サイクルあたり 8 MAC が可能です。16 ビットのデータで 32 ビットの固定小数点係数を使用すると、1 サイクルあたり 16 回の MAC が可能になり、スループットが 2 倍になる可能性があります。16 ビットのデータで 16 ビットの固定小数点係数の場合は、1 サイクルあたり 32 回の MAC が可能になり、スループットが 4 倍になる可能性があります。8 ビットのデータで 16 ビットの固定小数点係数の場合は、1 サイクルあたり 64 回の MAC が可能になり、スループットが 8 倍向上する可能性があります。
浮動小数点のインプリメンテーションの場合、処理済みサンプルの数と AI エンジンの数を 2 倍にすると (つまり、2 つの AI エンジンで、それぞれ 16 サンプル ウィンドウから 8 サンプルを処理すると)、スループットが 2 倍になる可能性があります。
サポート
GitHub 問題は、リクエストやバグの追跡に使用します。質問については、forums.xilinx.com を参照してください。
Copyright © 2020-2024 Advanced Micro Devices, Inc