数据传输接口注意事项 - 2023.2 简体中文

Vitis 统一软件平台文档 应用加速开发 (UG1393)

Document ID
UG1393
Release Date
2023-12-13
Version
2023.2 简体中文

在系统设计中,正确指定加速器与主机之间的数据传输模式至关重要。以下章节提供了有关设计各方面的更多详细信息。

全局存储器 I/O

VSC 支持在全局存储器与加速器之间采用两种传输模式,如 指南宏 中所述:

  1. DATA_COPY:拖入数据移动器,如有效设计的 RTL IP,它将在与全局存储器对接的 M_AXI 接口上自动执行突发和数据宽度操作等功能。在加速器侧,此接口支持顺序数据访问(与 Vitis HLS ap_fifo 接口中相同)以及随机数据访问(与 Vitis HLS ap_memory 接口中相同)。
  2. ZERO_COPY:将 M_AXI 接口从全局存储器平台端口连接到加速器

对于 compute() 函数接口的定义,建议考量下列注意事项:

  1. 如果数据大小(所有计算实参)过大,难以放入器件 DDR,请通过多次发送迭代将大型 alloc_buf 拆分为较小的计算区块。
  2. 一般建议使用 DATA_COPYSEQUENTIAL 访问模式,当加速器代码执行顺序输入数据访问时尤其如此。
  3. 如果加速器代码执行随机数据访问,并且无法将代码修改为顺序访问数据,那么请使用随机访问模式,前提是数据可放入器件 RAM(BRAM 或 URAM)。否则,请使用 ZERO_COPY

顺序访问模式

对于内核代码顺序访问的 PE 端口 data,请使用 ACCESS_PATTERN(data, SEQUENTIAL);

VSC 需获取运行时的数据大小。这可通过 DATA_COPY 宏来完成。例如,DATA_COPY(data, data[numData]);

datanumData 都必须作为实参传递给内核,即使内核不使用 numData,也同样必须作为实参来提供。总体上,numData 可采用任意表达式,前提是可在运行时根据内核函数实参来对其进行求值。在加速器类声明,支持如下示例,但此示例可能并不实用:

DATA_COPY(data, data[m * log(n) + 5]);
...
static void PE_func(int n, int m, float* data); 

对于输入和输出数据,DATA_COPY 的精确大小至关重要,必须与内核当前读取的数据量精确匹配。如果大小与设计不匹配,则可能存在功能问题,如:

  • 如果内核读取的数据量超过应用代码提供的数据量,那么在运行时会发生挂起,或者
  • 如果前一次内核调用未能读取之前的计算调用的所有数据,那么内核将读取垃圾数据

为防止并调试此类问题,您可使用 ZERO_COPY 来确保内核代码正确工作。

随机访问模式

如果内核必须以随机方式访问 compute() 函数实参 data,请使用 ACCESS_PATTERN(data, RANDOM);

提示: 随机访问模式需获取局部 FIFO 缓冲器,其大小由 BRAM 施加限制,通常以 32 Kb 块为单位。片上存储器所需 BRAM 数量与用户定义的实参大小相同。

因此,您必须确保数据能放入片上 FPGA 存储器。内核代码必须将数据声明为静态阵列,例如:

ACCESS_PATTERN(data, RANDOM);
...
static void PE_func(int n, int m, float data[64]);

如果数据大小采用随机方式来访问并且对于片上 FPGA 存储器而言,数据大小过大,那么应使用 ZERO_COPY(data)

重要: 请勿将 ACCESS_PATTERN 宏与 ZERO_COPY 搭配使用。

主机与全局存储器之间传输的数据单位量并非必须与 DATA_COPY 大小相同。它实际上是由 VPP_ACC::alloc_buf() 调用的大小 (size) 实参来确定的。此大小可大于每次内核计算所需的数据大小,例如,一次性为多个 compute() 调用发送数据时就是如此。因此,按如下方式即可轻松聚集 PCIe 数据传输用于执行 N 次 compute() 调用:

send_while ... { ...
  clustered_buffer = acc::alloc_buf( N * size );
  for (i = 0; i < N; i++) { ...
    acc::compute(&clustered_buf[ i * size ] ...
  }
  1. 为 N 次计算调用分配相应的数据缓冲器大小
  2. compute() 进行 N 次调用,每次调用都索引到聚集的缓冲器中

计算有效载荷数据类型

compute() 数据类型还可判定全局存储器上的数据布局,因此将影响加速器性能。为了允许内核尽快访问数据,为 compute() 实参选择相应的数据类型至关重要。例如,如果内核正在处理整数,并且必须在每个时钟周期内处理一个整数 (HLS II = 1),那么接口即可使用整数阵列,例如:

static void compute ( int* A );

再举个例子,以下提供的两种编码样式在每次计算调用中添加 4 个整数。

// --- acc interface
DATA_COPY(data, data[numData*4]); 

// --- application code
int data[numData*4];
...

static void acc::compute(int* data, ... );

// --- 4-cycle kernel code
void PE_func(int* data, int numData, int *out) {
  for (int i=0; i < numData; i++) {
    int o = i * 4;
    out = data[o+0]+data[o+1]+data[o+2]+data[o+3];
  }
// --- acc interface
DATA_COPY(data, data[numData]); 

// --- application code
struct data_t { int i[4]; };
data_t *data;
...
static void acc::compute(data_t* data, ... );

// --- 1-cycle kernel code with packed data type
void PE_func(data_t* di, int numData, int *out) {
  for (int i=0; i < numData; i++) {
    data_t data = di[i];
    out = data.i[0]+data.i[1]+data.i[2]+data.i[3];
  }

内核以来自输入阵列的每 4 个整数为一组进行相加。左侧直接实现每个结果需要 4 个时钟周期,假定每次存储器访问需一个周期。但将全部 4 个整数都打包到单次全局存储器访问中更好,如右侧所示。因此,在此情况下建议:

  • 使用 C 结构体将所有数据打包并传递给内核
  • 使用 DATA_COPY(data, data[numData]); 确保提供的数据副本大小正确

还有些需要注意的关键要点:

  • 使用 int* data[4] 将不会打包这些整数,生成的硬件与 int* data; 相同
  • 通常打包超过 64 字节(对应 512 位 M_AXI 总线宽度)将无法改善性能,但也不会造成性能降级