目录
1. Vitis HLS重器-Vitis_Libraries
2. 初识scal()
3. 函数具体实现
3.1 变量命名规则
3.2 t_ParEntries解释
3.3 流类型详解
3.4 双重循环
4. 总结
1. Vitis HLS重器-Vitis_Libraries
在深入探索Vitis HLS(High-Level Synthesis)的旅程中,我们不得不提一个至关重要的里程碑——那就是熟练运用Vitis Libraries。这个库集合了众多领域内关键的函数,并提供了它们的硬件实现版本。这对于那些希望将软件算法高效转换为硬件描述的开发者来说,无疑是一大福音。以BLAS(Basic Linear Algebra Subprograms)库中的scale函数为例,我们可以看到这些库的实用性和强大功能。
scale函数的作用听起来非常直接:计算Y = alpha * X,其中alpha是一个标量,X是一个向量,这个操作将X中的每个元素乘以alpha,得到新的向量Y。乍一看,scale函数的功能似乎异常简单,直白。然而,当我们深入其函数参数时,事情开始变得有些复杂:
template <typename t_DataType, unsigned int t_ParEntries, typename t_IndexType = unsigned int>
void scal(unsigned int p_n,
t_DataType p_alpha,
hls::stream<WideType<t_DataType, t_ParEntries>>& p_x,
hls::stream<WideType<t_DataType, t_ParEntries>>& p_res)
这段代码中充满了模板参数、数据类型和流对象,令初看之下显得颇为复杂。对于初学者来说,这些参数和类型定义可能会让人感到困惑,特别是对于那些刚接触硬件设计领域的学生或工程师而言。但别担心,今天我们就来一步步解析这些参数,揭开scal函数背后的神秘面纱。
2. 初识scal()
- t_DataType指的是数据类型,这意味着scal函数能够支持不同的数据类型操作,增加了函数的通用性。
- t_ParEntries定义了每次操作可以处理的数据量,这直接关联到了算法的并行度和处理效率。
- t_IndexType通常用作索引的数据类型,默认为unsigned int,这提供了足够的灵活性来适应不同大小的数据集。
- p_n参数指定了向量X中元素的数量
- p_alpha是乘法因子alpha。
- p_x和p_res分别是输入和输出数据的流对象,它们使用了WideType模板,这是一种封装了并行数据条目的类型,允许数据以流的形式进行高效处理。
通过这样的设计,scal函数不仅仅是一个简单的比例计算函数,而是一个高度优化且可定制的并行数据处理单元,能够在硬件级别上实现高效的线性代数运算。这种深度优化的实现方式,虽然在一开始可能让人望而生畏,但一旦掌握,便能大幅提升数据处理任务的性能。
3. 函数具体实现
接下来,我们详细研究每一处细节。函数实现如下:
template <typename t_DataType, unsigned int t_ParEntries, typename t_IndexType = unsigned int>
void scal(unsigned int p_n,
t_DataType p_alpha,
hls::stream<typename WideType<t_DataType, t_ParEntries>::t_TypeInt>& p_x,
hls::stream<typename WideType<t_DataType, t_ParEntries>::t_TypeInt>& p_res) {
#ifndef __SYNTHESIS__
assert((p_n % t_ParEntries) == 0);
#endif
const unsigned int l_parEntries = p_n / t_ParEntries;
for (t_IndexType i = 0; i < l_parEntries; ++i) {
#pragma HLS PIPELINE
WideType<t_DataType, t_ParEntries> l_valX;
WideType<t_DataType, t_ParEntries> l_valY;
l_valX = p_x.read();
for (unsigned int j = 0; j < t_ParEntries; ++j) {
l_valY[j] = p_alpha * l_valX[j];
}
p_res.write(l_valY);
}
}
3.1 变量命名规则
- t_DataType,t_前缀表示template,模板参数
- p_n,p_前缀表示parameter,函数参数
- l_abs,l_前缀表示local,函数内部变量(局部变量)
3.2 t_ParEntries解释
const unsigned int l_parEntries = p_n / t_ParEntries;
用途:计算在给定并行度 t_ParEntries 下,需要进行多少次迭代来处理整个输入向量。
- p_n 是输入向量 X 的元素总数。
- t_ParEntries 是每次可以并行处理的元素数。
- 注意p_n / t_ParEntries必须能被整除。
因此,l_parEntries 是迭代的总次数,即必须执行多少次循环迭代来处理整个向量 X,使每个元素都乘以 alpha。
图示说明:
如果向量 p_n 包含9个元素(即 p_n=9),并且设定并行度 t_ParEntries=3,则最多可以有3个并行执行的流水线同时进行计算,通过三次迭代就可以完成整个向量的运算。换句话说,每次迭代处理3个元素,总共需要3次迭代来覆盖所有9个元素。
也可以设置 t_ParEntries=9,这样一来,整个向量的乘法运算可以在单次迭代中完成。
加入迭代间隔(II)为1,这意味着在一个周期内就可以完成所有9个元素的乘法运算。当然,这种配置会消耗更多的硬件资源,因为它需要在同一时刻支持更多的并行乘法操作。
这种权衡是在硬件资源消耗与运算速度之间进行的。选择更高的并行度可以减少所需的总迭代次数,从而加快运算速度,但代价是需要更多的硬件资源来实现这种高并行度。相反,较低的并行度虽然硬件资源消耗更少,但需要更多的迭代次数来完成相同的计算,可能导致整体性能降低。
3.3 流类型详解
hls::stream<typename WideType<t_DataType, t_ParEntries>::t_TypeInt>& p_x
hls::stream<typename WideType<t_DataType, t_ParEntries>::t_TypeInt>& p_res
关键字typename的作用:在模板编程中,编译器并不能自动推断出所有的名字是否代表类型。特别是当类型依赖于模板参数时,这里t_TypeInt就是一个依赖模板参数的类型,我们也称其为依赖类型。typename为了告诉编译器t_TypeInt是一个类型。
进一步我们调查t_TypeInt的定义:
template <typename T, unsigned int t_Width, unsigned int t_DataWidth = sizeof(T) * 8, typename Enable = void>
class WideType {
private:
…
public:
static const unsigned int t_TypeWidth = t_Width * t_DataWidth;
typedef ap_uint<t_TypeWidth> t_TypeInt;
可以看到,t_TypeInt只是一个别名,代指ap_uint<t_TypeWidth>;
绕了半天,发现p_x,p_res就是ap_uint<m>的hls::stream!
3.4 双重循环
for (t_IndexType i = 0; i < l_parEntries; ++i) {
#pragma HLS PIPELINE
WideType<t_DataType, t_ParEntries> l_valX;
WideType<t_DataType, t_ParEntries> l_valY;
l_valX = p_x.read();
for (unsigned int j = 0; j < t_ParEntries; ++j) {
l_valY[j] = p_alpha * l_valX[j];
}
p_res.write(l_valY);
}
这段代码是实现向量缩放操作(即 Y = alpha * X)的核心部分。
内层循环对于硬件实现来说非常关键,因为它被设计为可以完全展开并并行执行。
当外层循环使用了 #pragma HLS PIPELINE 指令时,即使没有显式地使用 #pragma HLS UNROLL 指令,内层循环也可能会默认展开。
l_valX、l_valY变量是Vitis HLS推荐使用的方法。
l_valX = p_x.read()和p_res.write(l_valY)也是hls::stream操作fifo的方法。
4. 总结
虽然Vitis Libraries中的函数在一开始看起来可能令人困惑,但它们提供了强大的工具集,用于构建高效的硬件加速应用程序。通过深入学习和实践,我们可以逐渐解锁这些库的潜力,为自己的项目带来前所未有的性能提升。