Vitis HLS 学习笔记--scal 函数-探究

目录

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中的函数在一开始看起来可能令人困惑,但它们提供了强大的工具集,用于构建高效的硬件加速应用程序。通过深入学习和实践,我们可以逐渐解锁这些库的潜力,为自己的项目带来前所未有的性能提升。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值