一:定義
脈動陣列:數據流同步流過相鄰的二維陣列單元的處理器結構,一般不同方向流過不同數據。如下圖:
二維不同數據在同一時鐘下依次輸入每個處理單元,而後完成乘法並存在其寄存器中。
二:舉例
三:設計
結構:
單個PE的代碼
module pe(clk, reset, coeff, in_x, in_y, out_x, out_y);
parameter size = 8;
input?????????????? clk, reset;
input???? [size-1:0]????????? in_x, coeff;
input???? [size+size-1:0]? in_y;
output?? [size-1:0]????????? out_x;
output?? [size+size-1:0]? out_y;
reg??????? [size+size-1:0]? out_y;
reg??????? [size-1:0]?????????? out_x;
always@(posedge clk)
?begin
?? if(reset) begin
???? out_x <= 0;
???? out_y <= 0;
???? end
?? else begin
???? out_y <= in_y + (in_x * coeff);
???? out_x <= in_x;
???? end
?end
endmodule
?
四個PE,其餘類推
//***** main ****************************
module systolic(clk, reset, input_x, output_y);
parameter?? size = 8;
input?? clk, reset;
input?? [size-1:0]????????? input_x;
output [size+size-1:0]? output_y;
wire??? [size-1:0]?????????? pe0_x, pe1_x, pe2_x, pe3_x;???????
wire??? [size+size-1:0]? pe1_y, pe2_y, pe3_y;
wire?? [size-1:0]?????? h0 = 8'h01;
wire?? [size-1:0]?????? h1 = 8'h01;
wire?? [size-1:0]?????? h2 = 8'h01;
wire?? [size-1:0]?????? h3 = 8'h01;
wire?? [size+size-1:0]? pe4_y = 16'h0000;??
pe pe_0(clk, reset, h0, input_x, pe1_y, pe0_x, output_y);
pe pe_1(clk, reset, h1, pe0_x, pe2_y, pe1_x, pe1_y);
pe pe_2(clk, reset, h2, pe1_x, pe3_y, pe2_x, pe2_y);
pe pe_3(clk, reset, h3, pe2_x, pe4_y, pe3_x, pe3_y);
endmodule
=========================================================================================================
脈動陣列(Systolic Array)計算矩陣乘法(Array Multiplication)
下一個目標是實現流水線輸出,提升硬件資源的利用率。
脈動陣列(Systolic Array):數據流同步流過相鄰的二維陣列單元的處理器結構,一般不同方向流過不同數據。
結構:
矩陣計算:
C語言描述每個輸出矩陣中的值:
For I = 1 to N
???? For J = 1 to N
????????? For K = 1 to N
?????????????? C[I,J] = C[I,J] + A[J,K] * B[K,J];
運用N x N processing units,輸入數據呈批次輸入:
二維不同數據在同一時鐘下依次輸入每個處理單元,而後完成乘法並存在其寄存器中。
?
?
其中每個PE(處理單元)結構如下:
是一個乘加單元? c=c+(a*b)
?
例子:計算兩個3×3矩陣的乘積
結構:
?
?
在CLK驅動下的每一個步驟如下:
Clk1:
?
Clk2:
Clk3:
Clk4:
????????
Clk5:
?
?
Clk6:
Clk7:
Clk8:輸出
功能仿真圖:
在start 上升沿到來後的第一個CLK上升沿開始計數
Count_start高電平期間
Cout=1時,準備a11和b11;
Cout=2時,將數據打入寄存器,並計數出a11*b11;
Cout=3時,計數a11*b11+a12*b21
Cout=4時,計數a11*b11+a12*b21+a13*b31
Cout=5時,用寄存器打一拍輸出Y11。
其他類似。
時序仿真圖:
連續運算,中間忘了將乘加單元寄存器清零的情況,功能仿真:
每次計算出結果後清零寄存器,修改後的功能仿真圖:
數據在送入運算單元之前,採用寄存器打一拍,功能仿真圖:
?
?
?
狀態機便於實現控制。
狀態機控制:功能仿真
時序仿真圖:
?
=======================================================================================================
二維流水線結構矩陣乘法(Array Multiplication)
上一篇文中建立了矩陣乘法運算的數據路徑,從仿真結構中可以看出整個計算方案的可行性,但是存在一個問題,就是硬件運算單元的資源利用率不高。這是什麼意思呢?就是說,在每次計算兩個3*3矩陣的乘法之前,需要將整個運算單元中的每個寄存器都清零,但是9個輸出結果不是在同一個時刻輸出,有先有後。當最先出結果的P11計算出第一個結果之後,它就不再輸出新值了。其實這時硬件電路是存在的,而且是在不斷計算出新值,只是這些值不是我們需要的有效值。那麼如果將這些硬件資源完全被利用起來,讓它們在最短的時間間隔裏都有有效正確數據輸出呢。
其實要說的就是流水線設計思想,我們只需要當P11單元計算出新的值,下個CLK將其計算結果輸出(只要有另一個機制接收這些值),然後將其清零(如果不清零,那個會累加了上次的計算結果),再然後就可以將下個要計算的兩個3*3矩陣對應位置上面的新值輸入給P11單元,讓其計算兩個數的乘積。
其他單元類似,思想就是在輸出結果後立即做清零和賦新值,不必等整個計算單元都出結果之後纔開始進行下個矩陣乘法的計算。
那麼從大局來看,就出現個這樣的情況:在上個矩陣計算還沒有完全計算出9個值的時候(右下角部分還在計算),左上角的單元就已經開始下個矩陣乘法的計算。關於輸出就是,每個CLK都有有效的數據輸出,產生了流水輸出。而這些輸出是從這整個二維矩陣計算單元的某些地方同時輸出的,就像水流即向下流也向右流,只是在這裏水流對應了數據輸出。
回顧一下這樣做的目的是什麼:爲了增加整個運算單元的利用率。做了改進,我們可以看出,每個CLK到來時,每個計算單元中的乘法和加法器的運算都是有效的計算。我們一定要記住FPGA做運算和CPU做計算的不同點。FPGA做運算,那些設計好的運算單元,不管你對這些運算單元操作或者不操作,它們都已經以硬件電路的形式存在了,也就是說,每個CLK到來時,它們都進行了一次計算。那麼如何有效的利用這些運算單元,就是不要讓它們做無效的計算,讓它們每一次的計算都對我們來說是有效的,是一個對整個單元輸出有影響的計算。而想實現這樣的功能流水線設計方法是必須要採用的。只是有時候單向流水(一維)更容易被我們所理解。其實這二維運算單元也是如此,也可以產生流水。
相對於不採用流水線操作,數據路徑不必做太多修改,主要是控制單元更爲複雜,要考慮何時對某個單元清零,何時再對其賦值,何時把數據讀走等。這些需要比較精準的調度。有人會想,那麼這樣是不是需要更多狀態機狀態?答案是肯定的。那麼是不是需要更多的硬件資源的開銷呢?答案也是肯定的。那麼這種流水線的優勢在哪呢?其實是速度上面的優勢,有時候在不能提高整個系統的工作頻率的情況下,再消耗一點資源來提高其它部分(運算單元)的利用率也是一個很好的提高運算速度的辦法。(注:這並不等同於面積與速度的法則)其實這也是一個比較好的,硬件算法優化方案。我們可以將其歸納爲以優化算法路徑的算法優化方法。
下面是仿真結果:
?
矩陣還是之前的矩陣。可以看出每個CLK都有2-3個有效輸出。
增加了Valid信號,當其爲高電平時,說明對應運算單元輸出是有效值。其實在算法成熟之後,這些Valid信號是可以撤銷的,因爲我們完全知道了,輸出的規律,只需要在特定的時間讀走數據即可。而且我們可以將這些數據合併到幾根連續有效的總線上面。讓每個總線上面每個CLK都是有效值。(筆者將按照此方法,繼續優化輸出總線)
下面是對輸入最大值時的輸出仿真,主要是看數據會不會益處:
測試8bits輸入最大值255對應的輸出值:
時序仿真:
255*255+255*255+255*255=195075
255*254+255*254+255*254=194310