优化 Vivado HLS 代码以减少图像处理算法的延迟
Optimizing the Vivado HLS code to reduce the latency for image processing algorithm
我正在尝试使用 Vivado HLS 为硬件实现色域映射滤波器的图像处理算法。我已经从 Halide 代码创建了一个可合成的版本。但是对于一张 (256x512) 的图像来说,它花费的时间太长了,它花费了大约 135 秒,这不应该是这样的。我使用了一些优化技术,例如流水线化最内层循环,通过流水线化,我为最内层循环设置了 II=1 的目标(启动间隔),但实现的 II 为 6。从编译器抛出的警告中,我已经理解这是因为访问了像 ctrl_pts & weights 这样的权重,我从教程中看到,使用数组分区和数组重塑将有助于更快地访问权重。我在下面分享了我用来合成的代码:
//header
include "hls_stream.h"
#include <ap_fixed.h>
//#include <ap_int.h>
#include "ap_int.h"
typedef ap_ufixed<24,24> bit_24;
typedef ap_fixed<11,8> fix;
typedef unsigned char uc;
typedef ap_uint<24> stream_width;
//typedef hls::stream<uc> Stream_t;
typedef hls::stream<stream_width> Stream_t;
struct pixel_f
{
float r;
float g;
float b;
};
struct pixel_8
{
uc r;
uc g;
uc b;
};
void gamut_transform(int rows,int cols,Stream_t& in,Stream_t& out, float ctrl_pts[3702][3],float weights[3702][3],float coefs[4][3],float num_ctrl_pts);
//core
//include the header
#include "gamut_header.h"
#include "hls_math.h"
void gamut_transform(int rows,int cols, Stream_t& in,Stream_t& out, float ctrl_pts[3702][3],float weights[3702][3],float coefs[4][3],float num_ctrl_pts)
{
#pragma HLS INTERFACE axis port=in
#pragma HLS INTERFACE axis port=out
//#pragma HLS INTERFACE fifo port=out
#pragma HLS dataflow
pixel_8 input;
pixel_8 new_pix;
bit_24 temp_in,temp_out;
pixel_f buff_1,buff_2,buff_3,buff_4,buff_5;
float dist;
for (int i = 0; i < 256; i++)
{
for (int j = 0; i < 512; i++)
{
temp_in = in.read();
input.r = (temp_in & 0xFF0000)>>16;
input.g = (temp_in & 0x00FF00)>>8;
input.b = (temp_in & 0x0000FF);
buff_1.r = ((float)input.r)/256.0;
buff_1.g = ((float)input.g)/256.0;
buff_1.b = ((float)input.b)/256.0;
for(int idx =0; idx < 3702; idx++)
{
buff_2.r = buff_1.r - ctrl_pts[idx][0];
buff_2.g = buff_1.g - ctrl_pts[idx][1];
buff_2.b = buff_1.b - ctrl_pts[idx][2];
dist = sqrt((buff_2.r*buff_2.r)+(buff_2.g*buff_2.g)+(buff_2.b*buff_2.b));
buff_3.r = buff_2.r + (weights[idx][0] * dist);
buff_3.g = buff_2.g + (weights[idx][1] * dist);
buff_3.b = buff_2.b + (weights[idx][2] * dist);
}
buff_4.r = buff_3.r + coefs[0][0] + buff_1.r* coefs[1][0] + buff_1.g * coefs[2][0] + buff_1.b* coefs[3][0];
buff_4.g = buff_3.g + coefs[0][1] + buff_1.r* coefs[1][1] + buff_1.g * coefs[2][1] + buff_1.b* coefs[3][1];
buff_4.b = buff_3.b + coefs[0][2] + buff_1.r* coefs[1][2] + buff_1.g * coefs[2][2] + buff_1.b* coefs[3][2];
buff_5.r = fmin(fmax((float)buff_4.r, 0.0), 255.0);
buff_5.g = fmin(fmax((float)buff_4.g, 0.0), 255.0);
buff_5.b = fmin(fmax((float)buff_4.b, 0.0), 255.0);
new_pix.r = (uc)buff_4.r;
new_pix.g = (uc)buff_4.g;
new_pix.b = (uc)buff_4.b;
temp_out = ((uc)new_pix.r << 16 | (uc)new_pix.g << 8 | (uc)new_pix.b);
out<<temp_out;
}
}
}
即使达到II=6,耗时也只有6秒左右;给定的目标是以毫秒为单位的时间。我试图为第二个最内层的循环做流水线操作,但是当我这样做时我 运行 资源不足,因为第三个最内层的循环正在展开。我正在使用拥有大量资源的 zynq ultra-scale 开发板。任何关于优化代码的建议将不胜感激。
此外,谁能建议哪种类型的接口最适合 ctrl_pts、权重和系数,对于阅读图像我明白流接口有帮助,并且对于读取行数和列数等小值,Axi lite 是首选?是否有一种类型的接口可以用于上述变量,以便它可以与数组分区和数组重塑齐头并进?
任何建议将不胜感激,
提前致谢
编辑:我知道定点表示可以进一步降低延迟,但我的首要目标是获得具有最佳结果的浮点表示,然后使用定点表示分析性能
您可以采取一些步骤来优化您的设计,但请记住,如果您真的需要一个浮动平方根运算,那很可能会带来巨大的延迟损失(当然,除非正确流水线化)。
您的代码在第二个内循环中可能有拼写错误:索引应该 j
对吗?
数据局部性
首先:ctrl_pts 从主内存中多次读取(我假设)。由于重复使用了256x512次,所以最好将其存储到FPGA上的本地缓冲区中(类似于BRAM,但可以推断),像这样:
for(int i =0; i < 3702; i++) {
for (int j = 0; j < 3; ++j) {
#pragma HLS PIPELINE II=1
ctrl_pts_local[i][j] = ctrl_pts[i][j];
}
}
for (int i = 0; i < 256; i++) {
for (int j = 0; i < 512; i++) {
// ...
buff_2.r = buff_1.r - ctrl_pts_local[idx][0];
// ...
coefs
和 weights
的推理相同,只是将它们存储在 运行 其余代码之前的局部变量中。
要访问参数,您可以使用主 AXI4 接口 m_axi
并进行相应配置。一旦算法处理本地缓冲区,HLS 应该能够相应地自动对缓冲区进行分区。如果没有,您可以放置 ARRAY_PARTITION complete dim=0
编译指示来强制执行它。
数据流
由于您的算法的工作方式,您可以尝试的另一件事是将主循环 (256x512) 分解为数据流中的三个较小的进程 运行,因此并行(+3,如果您包括设置的)
整个代码看起来像这样(我希望它能正确呈现):
[Compute buff_1]-->[FIFO1]-->[compute buff_3]-->[FIFO2a]-->[compute buff_4 and buff_5 + stream out]
L-------------------------------->[FIFO2b]----^
一个棘手的事情是将buff_1流式传输到两个下一个进程。
可能的代码
我不会尝试此代码,因此可能会出现编译错误,但整个加速器代码将如下所示:
for(int i =0; i < 3702; i++) {
for (int j = 0; j < 3; ++j) {
#pragma HLS PIPELINE II=1
ctrl_pts_local[i][j] = ctrl_pts[i][j];
weights_local[i][j] = weights[i][j];
}
}
for(int i =0; i < 4; i++) {
for (int j = 0; j < 3; ++j) {
#pragma HLS PIPELINE II=1
coefs_local[i][j] = coefs[i][j];
}
}
Process_1:
for (int i = 0; i < 256; i++) {
for (int j = 0; i < 512; i++) {
#pragma HLS PIPELINE II=1
temp_in = in.read();
input.r = (temp_in & 0xFF0000)>>16;
input.g = (temp_in & 0x00FF00)>>8;
input.b = (temp_in & 0x0000FF);
buff_1.r = ((float)input.r)/256.0;
buff_1.g = ((float)input.g)/256.0;
buff_1.b = ((float)input.b)/256.0;
fifo_1.write(buff_1); // <--- WRITE TO FIFOs
fifo_2b.write(buff_1);
}
}
Process_2:
for (int i = 0; i < 256; i++) {
for (int j = 0; i < 512; i++) {
for(int idx =0; idx < 3702; idx++) {
#pragma HLS LOOP_FLATTEN // <-- It shouldn't be necessary, since the if statements already help
#pragma HLS PIPELINE II=1 // <-- The PIPELINE directive can go here
if (idx == 0) {
buff_1 = fifo_1.read(); // <--- READ FROM FIFO
}
buff_2.r = buff_1.r - ctrl_pts_local[idx][0];
buff_2.g = buff_1.g - ctrl_pts_local[idx][1];
buff_2.b = buff_1.b - ctrl_pts_local[idx][2];
dist = sqrt((buff_2.r*buff_2.r)+(buff_2.g*buff_2.g)+(buff_2.b*buff_2.b));
buff_3.r = buff_2.r + (weights_local[idx][0] * dist);
buff_3.g = buff_2.g + (weights_local[idx][1] * dist);
buff_3.b = buff_2.b + (weights_local[idx][2] * dist);
if (idx == 3702 - 1) {
fifo_2a.write(buff_3); // <-- WRITE TO FIFO
}
}
}
}
Process_3:
for (int i = 0; i < 256; i++) {
for (int j = 0; i < 512; i++) {
#pragma HLS PIPELINE II=1
buff_3 = fifo_2a.read(); // <--- READ FROM FIFO
buff_1 = fifo_2b.read(); // <--- READ FROM FIFO
buff_4.r = buff_3.r + coefs_local[0][0] + buff_1.r* coefs_local[1][0] + buff_1.g * coefs_local[2][0] + buff_1.b* coefs[3][0];
buff_4.g = buff_3.g + coefs_local[0][1] + buff_1.r* coefs_local[1][1] + buff_1.g * coefs_local[2][1] + buff_1.b* coefs_local[3][1];
buff_4.b = buff_3.b + coefs_local[0][2] + buff_1.r* coefs_local[1][2] + buff_1.g * coefs_local[2][2] + buff_1.b* coefs_local[3][2];
buff_5.r = fmin(fmax((float)buff_4.r, 0.0), 255.0);
buff_5.g = fmin(fmax((float)buff_4.g, 0.0), 255.0);
buff_5.b = fmin(fmax((float)buff_4.b, 0.0), 255.0);
new_pix.r = (uc)buff_4.r;
new_pix.g = (uc)buff_4.g;
new_pix.b = (uc)buff_4.b;
temp_out = ((uc)new_pix.r << 16 | (uc)new_pix.g << 8 | (uc)new_pix.b);
out<<temp_out;
}
}
在调整 FIFO 的 深度 大小时要格外小心,因为进程 2(具有 sqrt
操作的进程)可能具有较慢的数据消耗和生产率!此外,FIFO 2b 需要考虑该延迟。如果速率不匹配,将出现 死锁 。确保有一个有意义的测试台并共同模拟您的设计。
(FIFO 的深度可以用 pragma #pragma HLS STREAM variable=fifo_1 depth=N
改变)。
最后的想法
在此过程中可能还会有进一步的 smaller/detailed 优化,但我会首先从上面的优化开始,因为它是最重要的。请记住,浮点处理在 FPGA 上不是最佳的(如您所指出的)并且通常会被避免。
编辑:我尝试了上面修改的代码,我已经实现了 II=1 并且资源使用得当。
由于 II 现在是一个,加速器需要的理想周期数是 256x512,我接近于:理想的 402,653,184 与我的 485,228,587)。我现在必须向您提出的一个疯狂想法是将 Process_2 最内层循环拆分为 两个并行分支 (实际上甚至超过 2 个),为它们自己的 FIFO 提供数据. Process_1 将提供两个分支,而 additional process/loop 将从两个 FIFO 中交替读取 256x512 元素并以正确的顺序提供它们 到 Process_3。这样,所需的周期总数应该减半,因为 Process_2 是数据流中最慢的进程(因此改进它会改进整个设计)。这种方法的一个可能缺点是 FPGA 需要更多的 area/resource。
祝你好运。
我正在尝试使用 Vivado HLS 为硬件实现色域映射滤波器的图像处理算法。我已经从 Halide 代码创建了一个可合成的版本。但是对于一张 (256x512) 的图像来说,它花费的时间太长了,它花费了大约 135 秒,这不应该是这样的。我使用了一些优化技术,例如流水线化最内层循环,通过流水线化,我为最内层循环设置了 II=1 的目标(启动间隔),但实现的 II 为 6。从编译器抛出的警告中,我已经理解这是因为访问了像 ctrl_pts & weights 这样的权重,我从教程中看到,使用数组分区和数组重塑将有助于更快地访问权重。我在下面分享了我用来合成的代码:
//header
include "hls_stream.h"
#include <ap_fixed.h>
//#include <ap_int.h>
#include "ap_int.h"
typedef ap_ufixed<24,24> bit_24;
typedef ap_fixed<11,8> fix;
typedef unsigned char uc;
typedef ap_uint<24> stream_width;
//typedef hls::stream<uc> Stream_t;
typedef hls::stream<stream_width> Stream_t;
struct pixel_f
{
float r;
float g;
float b;
};
struct pixel_8
{
uc r;
uc g;
uc b;
};
void gamut_transform(int rows,int cols,Stream_t& in,Stream_t& out, float ctrl_pts[3702][3],float weights[3702][3],float coefs[4][3],float num_ctrl_pts);
//core
//include the header
#include "gamut_header.h"
#include "hls_math.h"
void gamut_transform(int rows,int cols, Stream_t& in,Stream_t& out, float ctrl_pts[3702][3],float weights[3702][3],float coefs[4][3],float num_ctrl_pts)
{
#pragma HLS INTERFACE axis port=in
#pragma HLS INTERFACE axis port=out
//#pragma HLS INTERFACE fifo port=out
#pragma HLS dataflow
pixel_8 input;
pixel_8 new_pix;
bit_24 temp_in,temp_out;
pixel_f buff_1,buff_2,buff_3,buff_4,buff_5;
float dist;
for (int i = 0; i < 256; i++)
{
for (int j = 0; i < 512; i++)
{
temp_in = in.read();
input.r = (temp_in & 0xFF0000)>>16;
input.g = (temp_in & 0x00FF00)>>8;
input.b = (temp_in & 0x0000FF);
buff_1.r = ((float)input.r)/256.0;
buff_1.g = ((float)input.g)/256.0;
buff_1.b = ((float)input.b)/256.0;
for(int idx =0; idx < 3702; idx++)
{
buff_2.r = buff_1.r - ctrl_pts[idx][0];
buff_2.g = buff_1.g - ctrl_pts[idx][1];
buff_2.b = buff_1.b - ctrl_pts[idx][2];
dist = sqrt((buff_2.r*buff_2.r)+(buff_2.g*buff_2.g)+(buff_2.b*buff_2.b));
buff_3.r = buff_2.r + (weights[idx][0] * dist);
buff_3.g = buff_2.g + (weights[idx][1] * dist);
buff_3.b = buff_2.b + (weights[idx][2] * dist);
}
buff_4.r = buff_3.r + coefs[0][0] + buff_1.r* coefs[1][0] + buff_1.g * coefs[2][0] + buff_1.b* coefs[3][0];
buff_4.g = buff_3.g + coefs[0][1] + buff_1.r* coefs[1][1] + buff_1.g * coefs[2][1] + buff_1.b* coefs[3][1];
buff_4.b = buff_3.b + coefs[0][2] + buff_1.r* coefs[1][2] + buff_1.g * coefs[2][2] + buff_1.b* coefs[3][2];
buff_5.r = fmin(fmax((float)buff_4.r, 0.0), 255.0);
buff_5.g = fmin(fmax((float)buff_4.g, 0.0), 255.0);
buff_5.b = fmin(fmax((float)buff_4.b, 0.0), 255.0);
new_pix.r = (uc)buff_4.r;
new_pix.g = (uc)buff_4.g;
new_pix.b = (uc)buff_4.b;
temp_out = ((uc)new_pix.r << 16 | (uc)new_pix.g << 8 | (uc)new_pix.b);
out<<temp_out;
}
}
}
即使达到II=6,耗时也只有6秒左右;给定的目标是以毫秒为单位的时间。我试图为第二个最内层的循环做流水线操作,但是当我这样做时我 运行 资源不足,因为第三个最内层的循环正在展开。我正在使用拥有大量资源的 zynq ultra-scale 开发板。任何关于优化代码的建议将不胜感激。
此外,谁能建议哪种类型的接口最适合 ctrl_pts、权重和系数,对于阅读图像我明白流接口有帮助,并且对于读取行数和列数等小值,Axi lite 是首选?是否有一种类型的接口可以用于上述变量,以便它可以与数组分区和数组重塑齐头并进?
任何建议将不胜感激,
提前致谢
编辑:我知道定点表示可以进一步降低延迟,但我的首要目标是获得具有最佳结果的浮点表示,然后使用定点表示分析性能
您可以采取一些步骤来优化您的设计,但请记住,如果您真的需要一个浮动平方根运算,那很可能会带来巨大的延迟损失(当然,除非正确流水线化)。
您的代码在第二个内循环中可能有拼写错误:索引应该 j
对吗?
数据局部性
首先:ctrl_pts 从主内存中多次读取(我假设)。由于重复使用了256x512次,所以最好将其存储到FPGA上的本地缓冲区中(类似于BRAM,但可以推断),像这样:
for(int i =0; i < 3702; i++) {
for (int j = 0; j < 3; ++j) {
#pragma HLS PIPELINE II=1
ctrl_pts_local[i][j] = ctrl_pts[i][j];
}
}
for (int i = 0; i < 256; i++) {
for (int j = 0; i < 512; i++) {
// ...
buff_2.r = buff_1.r - ctrl_pts_local[idx][0];
// ...
coefs
和 weights
的推理相同,只是将它们存储在 运行 其余代码之前的局部变量中。
要访问参数,您可以使用主 AXI4 接口 m_axi
并进行相应配置。一旦算法处理本地缓冲区,HLS 应该能够相应地自动对缓冲区进行分区。如果没有,您可以放置 ARRAY_PARTITION complete dim=0
编译指示来强制执行它。
数据流
由于您的算法的工作方式,您可以尝试的另一件事是将主循环 (256x512) 分解为数据流中的三个较小的进程 运行,因此并行(+3,如果您包括设置的)
整个代码看起来像这样(我希望它能正确呈现):
[Compute buff_1]-->[FIFO1]-->[compute buff_3]-->[FIFO2a]-->[compute buff_4 and buff_5 + stream out]
L-------------------------------->[FIFO2b]----^
一个棘手的事情是将buff_1流式传输到两个下一个进程。
可能的代码
我不会尝试此代码,因此可能会出现编译错误,但整个加速器代码将如下所示:
for(int i =0; i < 3702; i++) {
for (int j = 0; j < 3; ++j) {
#pragma HLS PIPELINE II=1
ctrl_pts_local[i][j] = ctrl_pts[i][j];
weights_local[i][j] = weights[i][j];
}
}
for(int i =0; i < 4; i++) {
for (int j = 0; j < 3; ++j) {
#pragma HLS PIPELINE II=1
coefs_local[i][j] = coefs[i][j];
}
}
Process_1:
for (int i = 0; i < 256; i++) {
for (int j = 0; i < 512; i++) {
#pragma HLS PIPELINE II=1
temp_in = in.read();
input.r = (temp_in & 0xFF0000)>>16;
input.g = (temp_in & 0x00FF00)>>8;
input.b = (temp_in & 0x0000FF);
buff_1.r = ((float)input.r)/256.0;
buff_1.g = ((float)input.g)/256.0;
buff_1.b = ((float)input.b)/256.0;
fifo_1.write(buff_1); // <--- WRITE TO FIFOs
fifo_2b.write(buff_1);
}
}
Process_2:
for (int i = 0; i < 256; i++) {
for (int j = 0; i < 512; i++) {
for(int idx =0; idx < 3702; idx++) {
#pragma HLS LOOP_FLATTEN // <-- It shouldn't be necessary, since the if statements already help
#pragma HLS PIPELINE II=1 // <-- The PIPELINE directive can go here
if (idx == 0) {
buff_1 = fifo_1.read(); // <--- READ FROM FIFO
}
buff_2.r = buff_1.r - ctrl_pts_local[idx][0];
buff_2.g = buff_1.g - ctrl_pts_local[idx][1];
buff_2.b = buff_1.b - ctrl_pts_local[idx][2];
dist = sqrt((buff_2.r*buff_2.r)+(buff_2.g*buff_2.g)+(buff_2.b*buff_2.b));
buff_3.r = buff_2.r + (weights_local[idx][0] * dist);
buff_3.g = buff_2.g + (weights_local[idx][1] * dist);
buff_3.b = buff_2.b + (weights_local[idx][2] * dist);
if (idx == 3702 - 1) {
fifo_2a.write(buff_3); // <-- WRITE TO FIFO
}
}
}
}
Process_3:
for (int i = 0; i < 256; i++) {
for (int j = 0; i < 512; i++) {
#pragma HLS PIPELINE II=1
buff_3 = fifo_2a.read(); // <--- READ FROM FIFO
buff_1 = fifo_2b.read(); // <--- READ FROM FIFO
buff_4.r = buff_3.r + coefs_local[0][0] + buff_1.r* coefs_local[1][0] + buff_1.g * coefs_local[2][0] + buff_1.b* coefs[3][0];
buff_4.g = buff_3.g + coefs_local[0][1] + buff_1.r* coefs_local[1][1] + buff_1.g * coefs_local[2][1] + buff_1.b* coefs_local[3][1];
buff_4.b = buff_3.b + coefs_local[0][2] + buff_1.r* coefs_local[1][2] + buff_1.g * coefs_local[2][2] + buff_1.b* coefs_local[3][2];
buff_5.r = fmin(fmax((float)buff_4.r, 0.0), 255.0);
buff_5.g = fmin(fmax((float)buff_4.g, 0.0), 255.0);
buff_5.b = fmin(fmax((float)buff_4.b, 0.0), 255.0);
new_pix.r = (uc)buff_4.r;
new_pix.g = (uc)buff_4.g;
new_pix.b = (uc)buff_4.b;
temp_out = ((uc)new_pix.r << 16 | (uc)new_pix.g << 8 | (uc)new_pix.b);
out<<temp_out;
}
}
在调整 FIFO 的 深度 大小时要格外小心,因为进程 2(具有 sqrt
操作的进程)可能具有较慢的数据消耗和生产率!此外,FIFO 2b 需要考虑该延迟。如果速率不匹配,将出现 死锁 。确保有一个有意义的测试台并共同模拟您的设计。
(FIFO 的深度可以用 pragma #pragma HLS STREAM variable=fifo_1 depth=N
改变)。
最后的想法
在此过程中可能还会有进一步的 smaller/detailed 优化,但我会首先从上面的优化开始,因为它是最重要的。请记住,浮点处理在 FPGA 上不是最佳的(如您所指出的)并且通常会被避免。
编辑:我尝试了上面修改的代码,我已经实现了 II=1 并且资源使用得当。
由于 II 现在是一个,加速器需要的理想周期数是 256x512,我接近于:理想的 402,653,184 与我的 485,228,587)。我现在必须向您提出的一个疯狂想法是将 Process_2 最内层循环拆分为 两个并行分支 (实际上甚至超过 2 个),为它们自己的 FIFO 提供数据. Process_1 将提供两个分支,而 additional process/loop 将从两个 FIFO 中交替读取 256x512 元素并以正确的顺序提供它们 到 Process_3。这样,所需的周期总数应该减半,因为 Process_2 是数据流中最慢的进程(因此改进它会改进整个设计)。这种方法的一个可能缺点是 FPGA 需要更多的 area/resource。
祝你好运。