目录
[1. 简介](#1. 简介)
[2. 用法详解](#2. 用法详解)
[2.1 存储器布局](#2.1 存储器布局)
[2.2 示例展示](#2.2 示例展示)
[2.3 综合报告](#2.3 综合报告)
[3. 总结](#3. 总结)
1. 简介
在 Vitis HLS 中,矢量数据类型是一种特殊的数据类型,它允许你一次处理多个数据元素,就像一排并排的盒子,每个盒子里都装着一个数据元素。这种方式非常适合于同时执行多个相同的操作,这就是所谓的 SIMD(单指令多数据)操作。
矢量数据类型用法
cpp
#include <hls_vector.h>
hls::vector<T,N> aVec;
在代码中,#include <hls_vector.h> 这行告诉程序,我们要使用 Vitis HLS 提供的矢量数据类型。hls::vector<T,N> aVec; 这行代码声明了一个矢量变量 aVec。这里的 T 表示数据的类型,比如整数或浮点数,而 N 表示这个矢量中有多少个元素。
当 T 的位宽(即每个数据元素占用的位数)和 N(矢量中元素的数量)都是 2 的幂(比如 2, 4, 8, 16...)时,这个矢量数据类型就能以最高效的方式运行,因为计算机处理这样的数字时更加高效。
打个简单的比方,就像你在超市买东西时,如果你有一个足够大的购物车,你可以一次性把所有东西放进去,然后一起结账,这样就比每次只买一个东西要快得多。在 Vitis HLS 中,矢量数据类型就像是一个大购物车,让你能够一次性处理很多数据,提高效率。
2. 用法详解
2.1 存储器布局
hls::vector<T,N> aVec;
这个数据结构被定义为 hls::vector<T,N>,其中 T 表示矢量中元素的类型,而 N 表示矢量中元素的数量。
**存储器连续性:**矢量中的元素在内存中是连续存储的。这意味着,如果你知道了矢量中第一个元素的内存地址,就可以通过这个地址和元素的索引(乘以元素的大小)来计算出任何一个元素的内存地址。
**存储大小:**矢量的总大小(以字节为单位)是元素类型大小 sizeof(T) 与元素数量 N 的乘积。这是因为所有元素都紧密地排列在一起,没有任何间隙。
**对齐要求:**矢量的对齐要求是其总大小的最大2的幂值。对齐是指数据的起始内存地址是某个数(通常是2的幂)的倍数。这有助于提高内存访问的效率。特别地,当 N 和 sizeof(T) 都是2的幂时,矢量应该对齐到其总大小。这意味着如果你有一个类型大小为4字节(2的2次幂),包含8个元素(2的3次幂)的矢量,那么这个矢量的总大小是32字节(2的5次幂),它应该对齐到32字节。
这种设计与许多计算机架构上的矢量实现相匹配,因为它们通常也有类似的连续存储和对齐要求。这样的设计可以使得数据结构在这些架构上运行得更有效率。
2.2 示例展示
cpp
#include "hls_vector.h"
#include <ap_int.h>
// Each vector will be 64 bytes (16 x 4 bytes)
typedef hls::vector<float, 16> float16;
template <int N, typename T> void load(T (&out)[N], const T* in) {
#pragma HLS INLINE off
for (int i = 0; i < N; ++i) {
#pragma HLS pipeline
out[i] = in[i];
}
}
template <int N, typename T> void store(T* out, const T (&in)[N]) {
#pragma HLS INLINE off
for (int i = 0; i < N; ++i) {
#pragma HLS pipeline
out[i] = in[i];
}
}
template <int N, typename T, typename S>
void compute(T (&res)[N], const S (&lhs)[N], const S (&rhs)[N]) {
#pragma HLS INLINE off
for (int i = 0; i < N; ++i) {
#pragma HLS pipeline
res[i] = lhs[i] + rhs[i];
}
}
extern "C" void example(float16* res, const float16* lhs, const float16* rhs,
int n) {
#pragma HLS INTERFACE m_axi port = lhs offset = slave bundle = gmem0 depth = 32
#pragma HLS INTERFACE m_axi port = rhs offset = slave bundle = gmem1 depth = 32
#pragma HLS INTERFACE m_axi port = res offset = slave bundle = gmem0 depth = 32
for (int i = 0; i < n; ++i) {
float16 lhs_buf[32];
float16 rhs_buf[32];
float16 res_buf[32];
#pragma HLS DATAFLOW
load(lhs_buf, lhs);
load(rhs_buf, rhs);
compute(res_buf, lhs_buf, rhs_buf);
store(res, res_buf);
}
}
这段代码中,定义了一种特定的向量类型 float16(由16个浮点数组成,总共64字节),并实现了几个基本操作:从内存加载数据 (load)、将数据存储回内存 (store) 以及执行向量之间的加法 (compute)。
类型定义
- float16:定义了一个包含16个float元素的向量,每个float占用4字节,因此整个float16占用64字节内存。
函数模板
- load:从指定的输入指针位置(in)加载N个元素到数组(out)中。这个函数通过循环实现,并使用#pragma HLS pipeline来指示HLS工具将循环的每次迭代实现为一个流水线步骤,以提高执行速度。
- store:将数组(in)中的N个元素存储到指定的输出指针位置(out)。同样使用#pragma HLS pipeline来优化性能。
- compute:对两个输入数组(lhs和rhs)进行逐元素加法,将结果存储在数组(res)中。再次使用#pragma HLS pipeline实现流水线加速。
主函数 example
- 功能:这个函数执行一系列操作,对于给定数量n的float16类型向量(lhs和rhs),它逐个处理这些向量,执行加法运算,并将结果存储在res数组中。
- 接口指令:#pragma HLS INTERFACE指令定义了函数参数与外部世界的接口方式,这里使用m_axi接口,它是一种适用于内存访问的通用接口。offset = slave指定这些接口作为从设备端口,bundle参数定义了不同的接口被分配到的AXI总线接口,depth参数指定了接口期望的数据深度。
- 内部缓冲区:函数内部定义了三个float16类型的数组作为缓冲区(lhs_buf、rhs_buf、res_buf),用于存储加载的数据、临时计算结果和最终结果。
- 数据流:通过#pragma HLS DATAFLOW指令,函数内部的操作被组织成一个数据流图,允许这些操作并行执行,从而提高整体性能。
2.3 综合报告
cpp
================================================================
== SW I/O Information
================================================================
* Top Function Arguments
+----------+-----------+---------------------------+
| Argument | Direction | Datatype |
+----------+-----------+---------------------------+
| res | inout | vector<float, 16>* |
| lhs | inout | vector<float, 16> const * |
| rhs | in | vector<float, 16> const * |
| n | in | int |
+----------+-----------+---------------------------+
通过 Top Function Arguments 报告,可以查看矢量数据类型的具体信息。
cpp
================================================================
== HW Interfaces
================================================================
* M_AXI
+-------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| Interface | Data Width | Address Width | Latency | Offset | Register | Max Widen | Max Read | Max Write | Num Read | Num Write |
| | (SW->HW) | | | | | Bitwidth | Burst Length | Burst Length | Outstanding | Outstanding |
+-------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| m_axi_gmem0 | 512 -> 512 | 64 | 64 | slave | 0 | 512 | 16 | 16 | 16 | 16 |
| m_axi_gmem1 | 512 -> 512 | 64 | 64 | slave | 0 | 512 | 16 | 16 | 16 | 16 |
+-------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
向量类型 float16(由16个浮点数组成,总共64字节),64*8=512,符合综合报告。
3. 总结
在 Vitis HLS 中,矢量数据类型提供了一种高效的数据处理方式,允许开发者利用 SIMD 操作一次性处理多个数据元素。通过使用 hls::vector<T,N>,开发者可以创建一个由 N 个类型为 T 的元素组成的矢量。这种数据结构在内存中连续存储,且当元素类型和数量都是 2 的幂时,对齐到其总大小,可以实现最优的内存访问效率。
示例代码展示了如何定义矢量类型 float16,以及如何实现加载、存储和计算操作。这些操作通过 HLS 指令优化,以流水线的形式执行,从而提高性能。主函数 example 则展示了如何将这些操作组织成数据流,以并行方式执行,进一步提升效率。
综合报告部分突出了矢量数据类型在硬件接口中的配置,如 M_AXI 接口的数据宽度和地址宽度,确保了与向量类型的内存布局相匹配。这种设计使得 Vitis HLS 中的矢量数据类型不仅在软件层面上高效,也在硬件层面上与现代计算机架构紧密对接,实现了数据处理的高效率和高性能。