【问题标题】:Optimizing the Vivado HLS code to reduce the latency for image processing algorithm优化 Vivado HLS 代码以减少图像处理算法的延迟
【发布时间】:2021-03-15 07:09:18
【问题描述】:

我正在尝试使用 Vivado HLS 为硬件的色域映射过滤器实现图像处理算法。我从卤化物代码创建了一个可合成的版本。但是对于(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 超大规模板。任何有关优化代码的建议都将受到高度赞赏。

另外,任何人都可以建议哪种类型的界面最适合 ctrl_pts、weights 和 coefs,为了阅读图像,我了解流式界面有帮助,并且对于读取像行数和列数这样的小值,Axi lite 是首选?是否有一种接口可以用于上述变量,以便它可以与数组分区和数组整形齐头并进?

任何建议都将受到高度赞赏,

提前致谢

编辑:我知道定点表示可以进一步降低延迟,但我的首要目标是获得具有最佳结果的浮点表示,然后使用定点表示分析性能

【问题讨论】:

    标签: c++ image-processing vivado vivado-hls


    【解决方案1】:

    您可以采取一些步骤来优化您的设计,但请记住,如果您确实需要浮点平方根运算,那很可能会产生巨大的延迟损失(当然,除非经过适当的流水线处理)。

    您的代码在第二个内部循环中可能有错字:索引应该是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];
          // ...
    

    coefsweights 的推理相同,只需在运行其余代码之前将它们存储在局部变量中即可。 要访问参数,您可以使用主 AXI4 接口 m_axi 并进行相应配置。一旦算法处理了本地缓冲区,HLS 应该能够相应地自动分区缓冲区。如果没有,您可以放置​​ARRAY_PARTITION complete dim=0 pragma 来强制它。

    数据流

    由于您的算法的工作方式,您可以尝试的另一件事是将主循环 (256x512) 分解为在数据流中运行的三个较小的进程,因此并行运行(如果包括设置进程,则 +3)

    整个代码看起来像这样(我希望它能正确呈现):

    [Compute buff_1]-->[FIFO1]-->[compute buff_3]-->[FIFO2a]-->[compute buff_4 and buff_5 + stream out]
                  L-------------------------------->[FIFO2b]----^
    

    一个棘手的事情是将 buff_1 流式传输到 both 下一个进程。

    可能的代码

    我不会尝试这段代码,所以可能会出现编译错误,但整个加速器代码看起来像这样:

      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改变)。

    最后的思考

    在此过程中可能会执行更多更小/更详细的优化,但我会首先从上面的那些开始,因为是最重的。请记住,浮点处理在 FPGA 上并不是最优的(正如您所指出的),通常会被避免。

    编辑:我用上面的修改尝试了代码,我已经实现了 II=1 并具有不错的资源使用率。

    由于 II 现在是 1,加速器所需的理想周期数是 256x512,而我已经接近:理想的 402,653,184 与我的 485,228,587)。我现在必须向您提出的一个疯狂想法是将 Process_2 最内层循环拆分为 两个并行分支(实际上甚至超过 2 个),提供它们自己的 FIFO。 Process_1 将提供两个分支,而一个附加 进程/循环将交替地从两个 FIFO 中读取 256x512 元素并将它们以正确的顺序提供给 Process_3。通过这种方式,所需的总周期数应该减半,因为 Process_2 是数据流中最慢的进程(因此改进它将改进整个设计)。这种方法的一个可能缺点是 FPGA 需要更多的面积/资源。

    祝你好运。

    【讨论】:

    • 非常感谢您的详细回复,由于我出站而未能早点回复,我会尽力执行您在编辑部分的建议。非常感谢。
    • 你好 Stefano,我对 vivado hls 编程比较陌生,我已经理解了你从理论上解释的内容,你能解释一下你是如何启动 fifo 的吗,我已经查看了有关如何启动 fifo 的示例这样做,它在流和先进先出之间有点混乱。如果您仍然有上面提到的实现 II=1 的代码,您是否可以分享它,以便我可以运行它并更好地理解这一点
    • “启动 FIFO”是什么意思? FIFO 最初是空的,并且在生产者将数据写入/推送到 FIFO 之前,消费者无法从 FIFO 中读取/弹出。 FIFO 可以通过多种方式(移位寄存器、LUT、BRAM 等)在硬件中实现,而 Vivado HLS 使用名为 hls::stream 的 C++ 类抽象了硬件推送/弹出方法。
    • 你好@StefanoRibes,在尝试实现你建议的缓存数据局部性部分时,它给了我forums.xilinx.com/t5/High-Level-Synthesis-HLS/…指定的错误,你遇到这个问题了吗?不幸的是,在提出问题的论坛上没有给出答案。你能告诉我你是如何选择fifo的深度的吗?每当我尝试将其分解为三个进程时,它都会与流陷入无限循环,我认为这就是您警告我的产品和消耗率问题。
    • 我寻找了一些此类实现的标准示例,但找不到。我是否可以看一下您尝试过的实现 II=1 的代码的可综合版本以便更好地理解?谢谢
    猜你喜欢
    • 2018-03-01
    • 1970-01-01
    • 2014-07-25
    • 2017-09-20
    • 2020-04-17
    • 2019-10-18
    • 2019-12-12
    • 2022-12-18
    • 2013-03-13
    相关资源
    最近更新 更多