HLS中優化for循環總結!

寫在前邊的話

本博客是轉載B站高亞軍老師所講解的內容。覺得高老師講的太快了,稍不留神就會跳過去很多。本人看了看視頻,截了個圖,寫了個總結。如侵則刪。

一、基本概念,pipeline,unrolling

第一章,先上代碼,注意代碼中的註釋,非常重要。後邊幾章就不上代碼了。

//頭文件部分
#ifndef FOROPT_H_
#define FOROPT_H_

#include <ap_int.h>

#define N	3
#define WX 	8
#define	BW	16

typedef ap_int<WX>		dx_t;
//當然也可以用ap_uint<>;代表無符號的數據
//ap_fixed<W,Q>,用來定義定點有符號小數
//ap_ufixed<W,Q>,用來定義定點無符號小數
typedef ap_int<BW>		db_t;
typedef ap_int<BW+1>	do_t;

void foo(dx_t xin[N],dx_t a,db_t b,db_t c,do_t yo[N]);

#endif

源代碼部分:

#include "for_opt.h"

void foo(dx_t xin[N],dx_t a,db_t b,db_t c,do_t yo[N]){
    int i=0;
/*
 *總循環的次數是N,N稱之爲LOOP trip COUNT
 *
 *總的操作流程如下(考慮到IP是一個一直工作的,也就能理解爲啥這個模塊是循環往復的):
 *循環開始
 * C0:獲取b,c的數據;
 * C1:獲取xin的i個地址
 * C2:讀取xin[i]中的數據
 * C3:完成相應的計算
 * C1:獲取xin的i個地址
 * C2:讀取xin[i]中的數據
 * C3:完成相應的計算
 * ...
 *循環結束
 *
 *循環開始
 * ...
 * */
/*
 * for循環一次需要3個時鐘週期,就是上邊的  C1,C2,C3
 * 那麼這個3就是LOOP iteration latency.(iteration:迭代)
 *
 * i次for循環與i+1次for循環的間隔是3,那麼LOOP iteration Interval(LOOP II)也是3
 *
 * loop latency =3*N,也就是循環次數N與單次循環所佔的時鐘週期的乘積
 *
 * 如果loop latency 加上取b,c兩數的時鐘週期,就是整個function latency=3*N+1
 *
 * 從這個函數的第一次初始化開始,到下一次初始化結束,這個稱之爲函數初始間隔(interval)
 * 那麼function initial interval(Function II)=11
 *
 * */
    loop:
    for(i=0;i<N;i++){
        yo[i]=a*xin[i]+b+c;
    }

}

點擊C synthesis就能查看生成後的綜合報告了。函數的initial interval =10,這證明軟件版本有所優化了。

如果想要查看仿真波形,編寫一個簡單的main.c。這個main是不規範的,規範的測試文件需要與真實值對比,進而讓函數返回0(正確)還是其他值(錯誤)。

#include "stdio.h"
#include "for_opt.h"

db_t a=1,b=2,c=3;
dx_t aa[N]={1,2,3};
do_t yo[N];

int main(){
	foo(aa,a,b,c,yo);//傳遞數組中的數據時,要用地址作爲接口

	return 0;
}

運行C/RTL聯合仿真。選擇all,等待完成。

對於for循環常見的優化就是pipeline,在directive窗口中選中for循環的的標誌,然後選擇pipeline即可。如圖:

爲什麼要用pipeline呢?看個圖就明白了:

也就是說,可以使FPGA儘可能的同步的處理大量的數據。(在第i次循環處理第m步的時候,第i+1次循環正在處理m-1步)

除了pipeline也可以對for循環unrolling(展開、鋪開)。因爲for循環在默認情況下是摺疊的。所謂摺疊就是:每次循環都是採用的同一塊電路,只是電路被分時複用了。所謂展開就是把這一塊電路複製了,可能複製成N份,也可能複製成N/2份(我們是可以選擇的)。

比如在一個循環次數爲6for循環中,可以把它展開成3for循環,每個for循環只計算2步。(個人認爲,在for循環很長的時候,可以採用此方法)

循環變量i(請注意是循環變量i

聲明成int i;和聲明成ap_int<4> i;在生成後的模塊中所佔用的資源是不變的,變量的範圍決定了資源量,並不是聲明類型決定了資源量。

二、for循環的合併(MERGE

假如有兩個毫不相關的for循環,我們期望的電路如右邊所示。

但實際上,這兩個for循環是串行執行的,只有先執行完加法才能執行減法(循環都是8for循環的切換需要額外的時鐘週期)

 

這時候我們希望能對這兩個for循環合併。在合併之前,需要理解一個新的概念,那就是region.比如:loop_region{},在兩個花括弧之間就是這個region,有了region才能對for循環進行合併(MERGE)。

 

合併循環能夠在一定程度上降低latency,並且消耗的資源更少。

如果循環邊界不同呢?

合併之後的trip count變成了4max={N,M}

如果兩個for循環的邊界分別是一個常數和一個變量(variable),應該怎麼處理呢?

這時候是不能直接進行合併的。

如果循環邊界都是變量:

這時如果強行合併也會顯示錯誤信息。

如果想要合併,應該採取以下的處理方法(這裏假設有:J<K):

 

三、for循環優化之DATAFLOW

首先來看個簡單的例子,正常的流程肯定是執行完A再執行B最後執行C

如果按照上圖的流程,肯定是串行處理的。這裏就能用到dataflow了,給出簡易的優化圖:

降低latency提高了數據吞吐率。

 

 

Dataflow使用的時候是有限制的,這裏也舉兩個例子:

1:當loop1輸出一組數據,被兩組循環引用的時候是不能用dataflow的:

    如果中間添加一個loop_copy是可以對上述例子進行優化的(loop_copy僅僅是吧temp1拷貝成了temp2temp3兩份數據,兩個輸出對應兩個輸入):

 

2loop1輸出的兩個數據一個繞過了loop2一個沒有繞過loop2,輸出的數據被loop3使用,這時不能用合併。也不能用dataflow.

解決方案就是loop2中添加了一個copy模塊,這樣就能用dataflow了。

 

對於ABC之間的通道的類型是啥樣的呢,我們可以配置。可以是ping-pong RAM也可以是FIFO如果參數是個scalar(標量)、pointer(指針)或reference(引用)那麼HLS會把它歸類爲FIFO。如果參數是個數組,通道的類型可能是FIFO(數據流是按順序)也可能是RAM(這時就會把通道變成一個ping-pong RAM)。

可以選擇默認的配置方式,當然我們也可以通過工具來設定這個通道是ping-pong RAM還是FIFO(注意FIFO的深度)。

 

四、嵌套for循環的優化

嵌套for循環分爲以下幾種:

  1. perfect loop nest
  2. semi-perfect loop nest
  3. imperfect loop nest(1)
  4. imperfect loop nest(2)

1.perfect loop nest的優化

舉個優化perfect loop nest的例子.只對內部做流水處理和只對外部做流水處理的結果如下:

結果這樣是因爲,我們對外部的循環做流水,內部的循環也會跟着做流水,因此這時候所消耗的資源有所增加。

 

如果我們只對內部的循環做流水,trip count變成了8.

 

2.imperfect loop nest的優化

(1)對最內層做流水

如果只對product部分(最內層)做流水,結果如下圖:

(2)對中間層做流水

如果只對col部分(中間層)做流水,這時候就trip count成了9.

 

(3)對最外層做流水

如果對最外部的for循環做流水,這時候的trip count是最小的。但是DSP48消耗的也是最多的。

(4)3種優化方式的對比

不加任何約束、最內部、中間層、最外層4種情況作比較,資源和速度的對比(可以發現中間層優化效果的性價比最高):

(5)矩陣乘法的優化

實際上我們可以對矩陣乘法做出優化,首先看矩陣乘法的流程,在這個流程中ab取值都取了27次,一共取值51次:

 

因此在Xilinx官方的例程中給出了矩陣乘法優化的代碼。將a矩陣的每一行和b矩陣的每一列都做了個緩存。這樣做的好處是避免端口重複多次讀取數據,減少了尋址的次數,從而加速了矩陣運算的過程。

優化後的流程以及自己做的流程圖(也不知道理解的對不對):

五、for循環的其他優化方法

1.for循環的並行性

For循環的並行執行。Merge是可以的(如果兩個循環次數不一樣就不能並行執行了)。

上述執行後的latency結果是一致的,但是資源節省了一半。

採用allocation可以使兩個函數並行執行。

ALLOCATION instances=Accumulator limit=2 function

這個語法就是把Accumulator這個函數複製了2份。結果如下(pipeline + allocation):

2.在循環流水中使用rewind

Rewind的優化:

從綜合的結果來看,pipeline + rewind還是具有很大優勢的:

 

如果一個函數中包含了多個for循環,這時是不能執行rewind的。

3.for循環邊界是變量時候的處理方法(3種)

循環邊界是變量的時候:

如何處理這種情況呢?

(1)使用tripcount指令

Tripcount不會影響綜合後的結果,不對綜合做任何優化,只是比較不同的solution比較方便。

(2)定義循環邊界用ap_int<W>/ ap_uint<W>

循環最邊界LOOP_N定義成ap_int<W>,那麼trip count的最大值是15.使用這種方法,能大大減少資源的使用。

(3)使用assert語句

//loop_n是循環邊界,是變量
//LOOP_N是循環次數最大不能超過的值
assert(loop_n<LOOP_N);

(4)上述三種方法的對比

三種方法的對比(很顯然assert這種方式,是最好的):

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章