最新文章
xilinx Vivado?設計套件
如果您正在努力開發計算內核,而且采用常規內存訪問模式,并且循環迭代間的并行性比較容易提取,這時,Vivado?設計套件高層次綜合(HLS)工具是創建高性能加速器的極好資源。通過向C語言高級算法描述中添加一些編譯指示,就可以在賽靈思FPGA上快速實現高吞吐量的處理引擎。結合使用軟件管理的DMA機制,就可以比通用處理器提速數十倍。
然而,實際應用中經常會遇到難以處理的復雜內存訪問問題,尤其是當突破科學計算和信號處理算法領域時更是如此。我們設計出了一種簡單方法,可供您在此類情況下生成高效的處理流水線。在詳細介紹之前,我們首先了解一下Vivado HLS的工作原理,更重要的是了解它何時不起作用。
HLS工具如何起作用?
高層次綜合功能試圖獲取由高級語言描述的控制數據流圖 (CDFG)中的并行性。對計算操作和內存訪問進行分配和調度時,應根據它們之間的依賴約束和目標平臺的資源約束來執行。電路中特定操作的激活與某個時鐘周期相關,同時,沿數據路徑綜合的中央控制器協調整個CDFG的執行。
單純在內核上應用HLS可以建立一條具有眾多指令級并行性的數據路徑。但是當它被激活時,就需要頻繁停下來等待數據送入。
由于調度工作是在靜態下完成的, 因此加速器運行時間的行為相當簡單。所生成電路的不同部分相互之間以相同步調運行;并不需要動態的相關性檢查機制,例如高性能CPU上出現的那種。例如,在圖1(a) 所示的函數中,循環索引添加和curInd的加載可以并行處理。此外,下次迭代可以在當前迭代完成前開始。
圖1 – 設計實例:(a) 包含不規則內存訪問模式的函數;(b) 重構得到的流水線結構
同時,由于浮點乘法通常使用上次迭代的乘法結果
因此可以開始新迭代的最短間隔受到浮點乘法器時延的限制。該函數的執行調度如圖2(a)所示。
圖2 – 不同情形下的執行調度:(a) 當所有數據都在片上高速緩存;(b) 動態取數據;(c) 解耦運算
該方案何時達不到理想效果?
這種方案的問題在于整個數據流圖嚴格按調度運行。片外通信產生的拖延會傳播到整個處理引擎,從而導致性能大幅下降。當內存訪問模式已知,數據能在需要使用之前移動到芯片上,或者如果數據集足夠小,則可完全高速緩存在FPGA上,這類情況下不會有問題。然而,就很多有趣的算法而言,數據訪問取決于計算結果,而且內存占用決定了需要使用片外RAM。現在,在內核上單純應用HLS可建立一條具有眾多指令級并行性的數據路徑。但是,當它被激活時,就需要頻繁停下來等待數據送入。
圖2(b)給出了針對實例函數生成的硬件模塊的執行情況,此時數據集太大,需要動態送入片上高速緩存。注意減速程度如何反映所有高速緩存缺失時延的綜合影響。不過,情況并非一定如此,因為計算圖中有些部分的進展不需要立即提供內存數據。這些部分應該可以向前移動。執行調度中這點額外自由度有可能產生顯著影響,就像我們看到的那樣。
重構/解耦實例
我們看一下剛才的實例函數。假設浮點乘法的執行和數據訪問沒有全部由統一的安排聯系在一起。當一個負載運算符等待數據返回時,另一個負載運算符可以開始新的內存請求,乘法器的執行也能向前移動。為達到此目的,每項內存訪問都應該由一個模塊來負責,并按各自的調度運行。此外,乘法器單元應該與所有內存操作異步執行。
不同模塊間的數據相關性通過硬件FIFO來通信。對于我們的實例而言,可能的重構形式如圖1(b)所示。用于各階段之間通信的硬件隊列可以緩沖已經取回但尚未使用的數據。當內存訪問部件因高速緩存缺失而出現拖延時,當前已產生的積壓數據還可以繼續供乘法器單元使用。在經歷較長時間后,形成的拖延時間會被浮點乘法的長時延掩蓋。
圖2(c)給出了使用解耦處理流水線時的執行調度。這里,通過FIFO的時延沒有考慮在內,不過如果迭代量很大,該時延的影響會達到最小。
我們如何進行重構?
為了給解耦處理模塊生成流水線,首先需要將初始CDFG中的指令進行組合以構成子圖。為使所得的實現方案性能最大化,聚類方法必須滿足幾個要求。
首先,正如我們之前所見,Vivado HLS工具在前面的迭代完成之前使用軟件流水線發起新的迭代。CDFG中最長循環依賴的時延決定可發起新迭代的最小間隔,最終會限制加速器所能實現的總吞吐量。因此,很重要的一點在于這些依賴循環不能遍歷多個子圖,例如用于模塊間通信的FIFO總是會增加時延。
其次,應該將內存操作與涉及長時延計算的依賴循環分開,這樣高速緩存缺失就會被慢速的數據處理所“掩蓋”。在這里,“長時延”是指操作需要一個周期以上的時間才能完成;在這里,我們使用Vivado HLS調度來獲取這一指標。例如,乘法是長時延操作,而整數加法不是。
最后,為了將高速緩存缺失引起的拖延影響限定在局部范圍內,您需要將每個子圖中的內存操作數量減至最少,尤其是在需要尋址存儲空間中的不同部分時更是如此。
第一個要求——防止依賴循環遍歷多個子圖——很容易滿足,只需要找到原始數據流圖中的強連通分量(SCC),并在將它們分為不同集群之前將其打開變成節點。這樣,我們就得到一個有向的非循環圖,其中有些節點是簡單指令,其它則為一組相關的操作。
要滿足第二和第三個要求,即分離內存操作和局部化拖延的影響,我們可以對這些節點進行拓撲排序,然后將它們分區。最簡單的分區方法是在每個內存操作或長時延SCC節點后畫一條“邊界”。圖3展示了如何將此方案應用于我們的實例。集群與圖1中流水線結構之間的對應關系應該做到顯而易見。每個子圖都是一個新的C函數,可獨立通過HLS推送。這些子圖在執行時相互間的步調并不一致。
圖3 – 對子圖的重構
我們構建了一個簡單的源到源轉換工具,用以執行重構。
我們使用賽靈思IP核,支持FIFO,以連接所生成的獨立模塊。當然,重構給定計算內核的方法不止一種,而且設計空間探索仍在進行中。
流水線化內存訪問
有了解耦處理流水線的初步實施方案后,我們就可以對其執行幾項優化,以提高其效率。正如我們所見,當使用HLS映射C函數時,內存讀取出現阻塞。這個問題也出現在流水線中的個別階段。例如,負責加載x[curInd]的模塊在等待數據時可能會產生拖延,即使在下個curInd已經就緒而且FIFO下游有足夠空間的情況下亦是如此。
為了解決這個問題,我們可以做一下轉變以簡化內存訪問。對于某個特定階段,我們不在C函數中執行簡單的內存加載,而是將地址推送到新的FIFO。然后,單獨實例化一個新的硬件模塊,以讀取地址FIFO送出的地址,并將它們發送到內存子系統。返回的數據被直接推送到下游FIFO。現在,內存訪問得到了有效的流水線化。