HLS基础(1):循环展开与存储分块
目录
两个向量对应元素相加
实验概述
实验步骤
使用Vits HLS新建工程
使用自有开发板型号:xc7z020clg400-2
编写vector_add头文件
编写vector_add源文件
设置顶层函数为“vector_add”
进行HLS综合
数据类型对生成电路有影响吗?
使用int变量
使用ap_fixed变量
总结
如何加速?
优化目标与理论预期
实际优化效果
性能瓶颈分析
改进方案建议
基本语法
分区类型(type)
(1) complete(完全分区)
(2) block(块分区)
(3) cyclic(循环分区)
两个向量对应元素相加
实验概述
本实验基于Xilinx Zynq-7000系列xc7z020clg400-2开发板,使用Vitis HLS工具实现两个向量的元素级加法运算。通过对比不同数据类型的实现效果,评估HLS综合性能差异。
实验步骤
使用Vits HLS新建工程
使用自有开发板型号:xc7z020clg400-2
编写vector_add头文件
#define MAXNUM 50000void vector_add(float A[MAXNUM],float B[MAXNUM],float C[MAXNUM]);
编写vector_add源文件
#include "vector_add.h"void vector_add(float A[MAXNUM],float B[MAXNUM],float C[MAXNUM]){for(int i=0;i<MAXNUM;i++){C[i] = A[i] + B[i];}
}
设置顶层函数为“vector_add”
进行HLS综合
从综合结果可以看出:
时序表现:
- 目标周期:10.00 ns
- 实际估计周期:6.329 ns(裕量3.671 ns)
- 时序不确定性:2.70 ns
循环结构:
- 循环总延迟:50,009周期(对应50006次迭代)
- 迭代间隔:1周期(已流水线化)
- 启动间隔(II)=1,但循环总延迟偏高
数据类型对生成电路有影响吗?
上文中我们使用的数据类型为float类型, 这里实验验证一下不同数据类型的结果差异。对float/int/ap_fixed三种数据类型进行对比测试:
使用int变量
#define MAXNUM 50000void vector_add(int A[MAXNUM],int B[MAXNUM],int C[MAXNUM]);
#include "vector_add.h"void vector_add(int A[MAXNUM],int B[MAXNUM],int C[MAXNUM]){for(int i=0;i<MAXNUM;i++){C[i] = A[i] + B[i];}
}
使用ap_fixed变量
#include <ap_fixed.h>typedef ap_fixed<32,16,AP_RND,AP_SAT> D32;#define MAXNUM 50000void vector_add(D32 A[MAXNUM],D32 B[MAXNUM],D32 C[MAXNUM]);
#include "vector_add.h"void vector_add(D32 A[MAXNUM],D32 B[MAXNUM],D32 C[MAXNUM]){for(int i=0;i<MAXNUM;i++){C[i] = A[i] + B[i];}
}
总结
- 浮点运算会触发DSP硬核调用,增加布线延迟
- 定点数(ap_fixed)在保持精度的同时,比float节省时序开销
- 整型实现性能最优,但需考虑数值范围限制
如何加速?
优化目标与理论预期
我们尝试通过循环展开(UNROLL)技术对向量加法进行加速优化:
- 原始循环延迟:50,000周期(每次迭代处理1个元素)
- 展开因子(UNROLLNUM):50
- 理论预期延迟:50,000/50 = 1,000周期
#include "vector_add.h"#define UNROLLNUM 50void vector_add(D32 A[MAXNUM],D32 B[MAXNUM],D32 C[MAXNUM]){for(int i=0;i<MAXNUM;i+=UNROLLNUM){for(int j=0;j<UNROLLNUM;j++){
#pragma HLS UNROLLC[i+j] = A[i+j] + B[i+j];}}
}
实际优化效果
实际综合结果显示:
- 实测延迟:25,000周期
- 加速比:仅2倍(未达预期的50倍)
性能瓶颈分析
造成此现象的根本原因是BRAM的端口访问限制:
BRAM架构特性:
- 每个BRAM模块仅支持:
- 最多2个并发读端口
- 1个写端口
当前实现限制:
C[i+j] = A[i+j] + B[i+j]; // 需要同时读取A和B各1个元素
- 每次迭代需要:
- 2个读操作(A和B各1个)
- 1个写操作(C)
- 实际每周期最多只能完成:
- 读取2个A元素 + 2个B元素
- 即每次处理2组加法运算
改进方案建议
调整展开因子匹配硬件限制。
这里ARRAY_PARTITION的参数介绍一下:
在Vivado HLS中,ARRAY_PARTITION指令用于优化数组的存储方式,通过改变数组在硬件中的实现结构来提高并行性和访问效率。
基本语法
#pragma HLS ARRAY_PARTITION variable=<数组名> type=<分区类型> factor=<分区因子> dim=<维度>
-
variable
: 要分区的数组名(必需) -
type
: 分区类型(complete
、block
、cyclic
,必需) -
factor
: 分区因子(指定分区数量,可选,默认为2) -
dim
: 多维数组的分区维度(从1开始,可选)
分区类型(type)
(1) complete
(完全分区)
- 作用:将数组完全拆分为独立的寄存器。
- 硬件实现:每个数组元素都变成单独的寄存器。
- 适用场景:小型数组(元素数量≤64),需最大化并行访问。
(2) block
(块分区)
- 作用:将数组连续元素分组到不同存储单元。
- 硬件实现:按块分配到多个BRAM或寄存器。
- 适用场景:需要按块顺序访问的数组。
- 示例(分4块):
int buffer[128]; #pragma HLS ARRAY_PARTITION variable=buffer type=block factor=4
- 分区后:
- buffer[0:31] → 存储单元0
- buffer[32:63] → 存储单元1
- ...以此类推
- 分区后:
(3) cyclic
(循环分区)
- 作用:轮询分配元素到不同存储单元。
- 硬件实现:元素按轮询方式分布(类似交织)。
- 适用场景:需要同时访问非连续元素(如循环展开)。
-
示例(分4块):int buffer[128]; #pragma HLS ARRAY_PARTITION variable=buffer type=cyclic factor=4
- 分区后:
- buffer[0,4,8,...] → 存储单元0
- buffer[1,5,9,...] → 存储单元1
- ...以此类推
- 分区后:
type=cyclic | + UNROLL | 配合循环展开实现并行访问 |
type=block | + 顺序访问模式 | 适合按块顺序处理的算法(如卷积核) |
成功将周期从50000降到1000!!