C++ 内核类支持 - 2023.2 简体中文

AI 引擎内核与计算图编程指南 (UG1079)

Document ID
UG1079
Release Date
2023-12-04
Version
2023.2 简体中文

aiecompiler 支持 C++ 内核类。以下示例显示了如何通过构造函数设置滤波器系数以及 FIR 滤波器类的样本数。C++ 内核类允许在对应的类对象内封装每个内核实例的内部状态。在以下代码中提供了一个示例,其中滤波器系数 (coeffs) 是通过构造函数指定的。使用文件作用域变量、全局变量或静态函数作用域变量来存储 C 语言函数的内部状态的问题由此即可得到解决。将此类内核的多个实例映射到相同核时,在多个实例之间会共享内部状态变量并导致冲突。

//fir.h
#pragma once
#include "adf.h"
#define NUM_COEFFS 12
using namespace adf;
class FIR
{
private:
    int32 coeffs[NUM_COEFFS];
    int32 tapDelayLine[NUM_COEFFS];
    uint32 numSamples;

public:
    FIR(const int32(&coefficients)[NUM_COEFFS], uint32 samples);
    void filter(input_buffer<int32> &in, output_buffer<int32> &out);
    static void registerKernelClass()
    {
        REGISTER_FUNCTION(FIR::filter);
    }
};

您必须在头文件中写入 static void registerKernelClass() 方法。在 registerKernelClass() 方法内,您需要调用 REGISTER_FUNCTION 宏。此宏用于寄存将在 AI 引擎核上执行的类的 run 方法,用于执行内核功能。在先前示例中,FIR::filter 是使用此宏寄存的。内核类构造函数和 run 方法应在单独的源文件内实现。内核类的 run 方法的实现与前述章节中所述的编写内核函数的方法相同。

//fir.cpp
//implementation in this example is not optimized and is for illustration purpose
#include "fir.h"
#include <aie_api/aie.hpp>
#include <aie_api/aie_adf.hpp>

FIR::FIR(const int32(&coefficients)[NUM_COEFFS], uint32 samples)
{
    for (int i = 0; i < NUM_COEFFS; i++)
        coeffs[i] = coefficients[i];

    for (int i = 0; i < NUM_COEFFS; i++)
        tapDelayLine[i] = 0;

    numSamples = samples;
}

void FIR::filter(input_buffer<int32> &in, output_buffer<int32> &out){
  auto inIter=aie::begin(in);
  auto outIter=aie::begin(out);
  for (int i = 0; i < numSamples; i++){
    for (int j = NUM_COEFFS-1; j > 0; j--){
      tapDelayLine[j] = tapDelayLine[j - 1];
    }
    tapDelayLine[0] = *inIter++;
    int32 y = 0;
    for (int j = 0; j < NUM_COEFFS; j++){
      y += coeffs[j] * tapDelayLine[j];
    }
    *outIter++=y;
  }
}
//graph.h
#pragma once
#include "adf.h"
#include "fir.h"
using namespace adf;
class mygraph : public graph
{
  public:
    input_plio in1, in2;
    output_plio out1, out2;
    kernel k1, k2;
    mygraph(){
      in1=input_plio::create("Datain1",plio_32_bits,"data/input1.txt");
      in2=input_plio::create("Datain2",plio_32_bits,"data/input2.txt");
      out1=output_plio::create("Dataout1",plio_32_bits,"data/output1.txt");
      out2=output_plio::create("Dataout2",plio_32_bits,"data/output2.txt");
      k1 = kernel::create_object<FIR>(std::vector<int>({ 180, 89, -80,
-391, -720, -834, -478, 505, 2063, 3896, 5535, 6504 }), 8);
      runtime<ratio>(k1) = 0.9;
      source(k1) = "aie/fir.cpp";
      k2 = kernel::create_object<FIR>(std::vector<int>({ -21, -249, 319,
-78, -511, 977, -610, -844, 2574, -2754, -1066, 18539 }), 8);
      runtime<ratio>(k2) = 0.9;
      source(k2) = "aie/fir.cpp";

      connect(in1.out[0], k1.in[0]);
      connect(in2.out[0], k2.in[0]);
      connect(k1.out[0], out1.in[0]);
      connect(k2.out[0], out2.in[0]);

      dimensions(k1.in[0])={8};
      dimensions(k2.in[0])={8};
      dimensions(k1.out[0])={8};
      dimensions(k2.out[0])={8};
    }
};

对于含非默认构造函数的内核类,您可在创建内核实例表示法时,在 kernel::create_object 的实参中指定构造函数参数值。在前述示例中,两个 FIR 滤波器内核(k1k2)使用 kernel::create_object<FIR> 创建内核实例表示法。k1 具有滤波器系数 { 180, 89, -80, -391, -720, -834, -478, 505, 2063, 3896, 5535, 6504 },k2 则具有滤波器系数 { -21, -249, 319, -78, -511, 977, -610, -844, 2574, -2754, -1066, 18539 }。这两个内核每次调用均耗用 8 个样本。

以下代码显示了 aiecompiler 生成的程序。其中的两个 FIR 内核对象均以适当的构造函数参数加以例化。

//Work/aie/<COL_ROW>/src/<COL_ROW>.cc
...
FIR i4({180, 89, -80, -391, -720, -834, -478, 505, 2063, 3896, 5535, 6504}, 8);
FIR i5({-21, -249, 319, -78, -511, 977, -610, -844, 2574, -2754, -1066, 18539}, 8);

int main(void) {
    ...
    // Kernel call : i4:filter
      i4.filter(window_buf0_buf0d_i[0],window_buf2_buf2d_o[0]);
    ...
    // Kernel call : i5:filter
      i5.filter(window_buf1_buf1d_i[0],window_buf3_buf3d_o[0]);
    ...
}

内核类可包含一个占用大量存储器空间的成员变量,此变量可能无法置于数据存储器内。内核类成员变量的位置可控。aiecompiler 支持 array reference 成员变量,允许编译器在分配或约束存储器空间的同时,将引用传递给对象。

//fir.h
#pragma once
#include "adf.h"
#define NUM_COEFFS 12
using namespace adf;
class FIR
{
private:
    int32 (&coeffs)[NUM_COEFFS];
    int32 tapDelayLine[NUM_COEFFS];
    uint32 numSamples;

public:
    FIR(int32(&coefficients)[NUM_COEFFS], uint32 samples);
    void filter(input_buffer<int32> &in, output_buffer<int32> &out);
    static void registerKernelClass()
    {
        REGISTER_FUNCTION(FIR::filter);
        REGISTER_PARAMETER(coeffs);
    }
};
//fir.cpp
#include "fir.h"
FIR::FIR(int32(&coefficients)[NUM_COEFFS], uint32 samples)
    : coeffs(coefficients)
{
    for (int i = 0; i < NUM_COEFFS; i++)
        tapDelayLine[i] = 0;

    numSamples = samples;
}

void FIR::filter(input_buffer<int32> &in, output_buffer<int32> &out)
{
...
}

前述示例显示 FIR 内核类的轻度修改版本。此处成员变量 coeffsint32 (&)[NUM_COEFFS] 数据类型。构造函数的初始化程序 coeffs(coefficients) 会对 coeffs 进行初始化,将其设为引用在类对象外部分配的阵列。为了使 aiecompiler 明确知晓可以在编译的映射器阶段中对 coeffs 成员变量进行重定位,您必须使用 REGISTER_PARAMETER 将阵列引用成员变量寄存到 registerKernelClass 内部。

使用 kernel::create_object 创建 FIR 内核实例的表示法并指定构造函数参数的初始值与前述示例中的值相同。请参阅以下代码。

//graph.h
...
class mygraph : public graph
{
...
    mygraph()
    {
        k1 = kernel::create_object<FIR>(std::vector<int>({ 180, 89, -80, -391, -720, -834, -478, 505, 2063, 3896, 5535, 6504 }), 8);
        ...
        k2 = kernel::create_object<FIR>(std::vector<int>({ -21, -249, 319, -78, -511, 977, -610, -844, 2574, -2754, -1066, 18539 }), 8);
        ...
    }
};

以下代码显示了对应 aiecompiler 生成的程序。int32 i4_coeffs[12]int32 i5_coeffs[15] 的存储器空间位于内核对象实例外部,并按引用传递给 FIR 对象。

//Work/aie/<COL_ROW>/src/<COL_ROW>.cc
int32 i4_coeffs[12] = {180, 89, -80, -391, -720, -834, -478, 505, 2063, 3896, 5535, 6504};
FIR i4(i4_coeffs, 8);
int32 i5_coeffs[12] = {-21, -249, 319, -78, -511, 977, -610, -844, 2574, -2754, -1066, 18539};
FIR i5(i5_coeffs, 8);

int main(void) {
    ...
    // Kernel call : i4:filter
    i4.filter(window_buf0_buf0d_i[0],window_buf2_buf2d_o[0]);
    ...
    // Kernel call : i5:filter
    i5.filter(window_buf1_buf1d_i[0],window_buf3_buf3d_o[0]);
    ...
}

由于阵列引用成员变量的存储器空间是由 aiecompiler 分配的,因此可应用位置约束来约束这些阵列的存储器位置,如以下示例代码所示。REGISTER_PARAMETER 宏允许 kernel::create_object 为阵列引用成员变量创建参数句柄,例如,k1.param[0]k2.param[0],随后即可应用 location<parameter> 约束。

//graph.h
...
class mygraph : public graph
{
...
    mygraph()
    {
        k1 = kernel::create_object<FIR>(std::vector<int>({ 180, 89, -80, -391, -720, -834, -478, 505, 2063, 3896, 5535, 6504 }), 8);
        ...
        k2 = kernel::create_object<FIR>(std::vector<int>({ -21, -249, 319, -78, -511, 977, -610, -844, 2574, -2754, -1066, 18539 }), 8);
        ...

        location<parameter>(k1.param[0]) = address(…);
        location<parameter>(k2.param[0]) = bank(…);
    }
};

C++ 内核类头文件和 C++ 内核函数模板(请参阅 C++ 模板支持)不应包含单核专用的内部 API 和编译指示。此编程准则与编写常规 C 语言函数内核相同。这是因为,这些头文件均包含在计算图头文件内,并且可作为 PS 程序的一部分进行交叉编译。 Arm® 交叉编译器无法理解单核内部 API 或编译指示。单个专用编程内容应保留在源文件内。