寫在前邊的話
本博客是轉載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份(我們是可以選擇的)。
比如在一個循環次數爲6的for循環中,可以把它展開成3個for循環,每個for循環只計算2步。(個人認爲,在for循環很長的時候,可以採用此方法)
循環變量i(請注意是循環變量i)
聲明成int i;和聲明成ap_int<4> i;在生成後的模塊中所佔用的資源是不變的,變量的範圍決定了資源量,並不是聲明類型決定了資源量。
二、for循環的合併(MERGE)
假如有兩個毫不相關的for循環,我們期望的電路如右邊所示。
但實際上,這兩個for循環是串行執行的,只有先執行完加法才能執行減法(循環都是8,for循環的切換需要額外的時鐘週期)。
這時候我們希望能對這兩個for循環合併。在合併之前,需要理解一個新的概念,那就是region.比如:loop_region{},在兩個花括弧之間就是這個region,有了region才能對for循環進行合併(MERGE)。
合併循環能夠在一定程度上降低latency,並且消耗的資源更少。
如果循環邊界不同呢?
合併之後的trip count變成了4(max={N,M})
如果兩個for循環的邊界分別是一個常數和一個變量(variable),應該怎麼處理呢?
這時候是不能直接進行合併的。
如果循環邊界都是變量:
這時如果強行合併也會顯示錯誤信息。
如果想要合併,應該採取以下的處理方法(這裏假設有:J<K):
三、for循環優化之DATAFLOW
首先來看個簡單的例子,正常的流程肯定是執行完A再執行B最後執行C:
如果按照上圖的流程,肯定是串行處理的。這裏就能用到dataflow了,給出簡易的優化圖:
降低latency提高了數據吞吐率。
Dataflow使用的時候是有限制的,這裏也舉兩個例子:
例1:當loop1輸出一組數據,被兩組循環引用的時候是不能用dataflow的:
如果中間添加一個loop_copy是可以對上述例子進行優化的(loop_copy僅僅是吧temp1拷貝成了temp2和temp3兩份數據,兩個輸出對應兩個輸入):
例2:loop1輸出的兩個數據一個繞過了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循環分爲以下幾種:
- perfect loop nest
- semi-perfect loop nest
- imperfect loop nest(1)
- 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)矩陣乘法的優化
實際上我們可以對矩陣乘法做出優化,首先看矩陣乘法的流程,在這個流程中a和b取值都取了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這種方式,是最好的):