Part2b - 2024.1 日本語 - XD100

Vitis チュートリアル: AI エンジン

Document ID
XD100
Release Date
2024-06-19
Version
2024.1 日本語

AI エンジンへの IIR フィルターのインプリメント - パート 2b

バージョン: Vitis 2024.1

下準備

パート 2a では、生成されたアセンブラー コードを調べ、VFPMAC (ベクター浮動小数点の乗累算演算) ニーモニックの間に NOP (演算なし) を見つけました。浮動小数点累積には 2 サイクルを要するため、この NOP は避けられません (AM009 の図 26 を参照)。

行列ベクター乗算を 2 つの別々の乗累算演算に分割し、各サイクルで浮動小数点の累算を実行します。

注記: 行列の各行と列ベクターを乗算する従来の方法ではなく、乗累算演算 API を使用して、行列の各列をベクターの対応するエレメントで効果的にスケーリングします。

図 1

この結果、ベクター加算を偶数部と奇数部に分けることで、独立した乗累算演算ができます。

図 2

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 を使用)

生成されたコード

図 3 生成されたアセンブリ コードには、偶数列と奇数列それぞれに 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 を定義またはコメントアウトすると実行されます。

生成されたアセンブリ コードは次のとおりです。 図 4 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 のスループットを比較します。 図 5

  • LLI は、同じ burst_cnt で API よりも優れたスループットを提供します。

  • スループットは約 burst_cnt = 64 で飽和します。

まとめ

AI エンジン API は、下位組み込み関数に対する抽象度を高めることで、生産性を向上させることを目的としています。 図 6 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

Terms and Conditions