在使用高層次綜合,創造高質量的RTL設計時,一個重要部分就是對C代碼進行優化。Vivado Hls總是試圖最小化loop和function的latency,為了實現這一點,它在loop和function上并行執行盡可能多的操作。比如說,在function級別上,高級綜合總是試圖并行執行function。
除了這些自動優化,directive是用來:
(1) 并行執行多個tasks,例如,同一個function的多次執行或同一loop的多次迭代。這是流水線結構。
(2) 調整數組的物理實現((block RAM),函數,循環和端口,以提高數據的可用性,并幫助數據流更快地通過設計。
(3) 提供關于數據dependency的信息,或者缺乏數據dependency,允許執行更多的優化。最終的優化是修改C源代碼,以消除在代碼中意外的dependency,但是這可能會限制硬件的性能。
本文使用的sample設計是一個matrix multiplier函數。目標是在每一個時鐘周期處理一個新的sample,并實現數據流接口。
優化matrix multiplier
solution1
這里使用矩陣乘法器設計,來顯示如何可以完全優化基于loop的設計。設計目標是在每個時鐘周期讀取一個使用FIFO接口的sample,同時最大限度地減少了面積。此分析包括一個比較在loop級優化和在function級優化的方法。
對比loops和function pipeline的使用,創建一個可以處理采樣時鐘的設計。分析設計不符合性能要求的兩個最常見的原因:loops dependency和數據流的限制(或瓶頸)。
Step 1:創建并打開Project
找到Design_Optimization lab1文件夾,依次在Command Prompt 窗口輸入vivado_hls –f run_hls.tcl和vivado_hls –p matrixmul_prj
Step 2:綜合分析設計
綜合后的結果:
?
(1)圖中,總的interval為80個時鐘周期。因為每個輸入數組中都有九個元素,所以設計每輸入讀取需要約九個周期。
(2)interval比latency多一個時鐘周期,所以沒有在硬件上并行執行。
(3)interval/latency是由于嵌套loops
I.Product inner loop:
-有一個2個時鐘周期的延遲。
-總的迭代有6個時鐘周期。
II.COL loop:
-它需要1個時鐘輸入和1個時鐘退出。
-它需要8個時鐘周期為每個迭代(1 + 6 + 1)。
-總的有24個周期完成所有迭代。
III.頂層loops每次迭代需要26個時鐘周期,總的loops迭代共78時鐘周期。
為了改善initiation interval,則需要:pipeline loops或pipeline整個function,并比較這兩種結果。當pipelining loops時,loops的initiation interval是監控的重要度量指標。即使設計達到loop可以在每個時鐘周期處理一個sample,函數的initiation interval仍然需要包含函數內的loops來完成所有數據的處理。
solution2: Pipeline the Product Loop
Step 1創建solution2,在Product loop下面插入pipeline directive(這里在Directive Editor下選擇pipeline)
?
注意:當pipeline嵌套loop時,通過pipeline最內部Loop最大的好處就是,即有利于處理數據的sample。高級綜合自動應用loop flattening,折疊嵌套loop,刪除loop轉換(本質上是創建一個更多迭代的單循環,但時鐘周期整體較少)。
Step 2 綜合設計到RTL級
在綜合過程中,我們得到Console pane中報告的信息,顯示loop flattening是loop Row上執行,默認內部Interval target為1由于依賴關系不能在loop Product上完成。
?
圖中表明,雖然Product loop已經被pipeline,interval為2,但是頂層loop沒有被pipeline。頂層loop不能pipeline的原因是,loop flattening只發生在loop Row,在loop Col 到Product loop上沒有loop flattening。下面解釋loop flattening不能flatten所有nested loop的原因。
Step 3 打開Analysis窗口,選擇state C1的write operation,右擊選擇Goto Source
?
狀態C1下的寫操作是由于代碼在Product loop前就已經設置res為0。因為res是在頂層函數,在RTL里這是在寫入一個端口:這個操作必須發生在loop Product執行之前。因為它不是一個內部操作,而是會對I / O行為產生影響,這種操作不能移動或優化。這可以阻止Product loop被flatten進入row_col loop。
更重要的是,對于Product loop來說,為什么只有II為2是可能flatten的。
這個問題被稱作carried dependency,這個dependency發生在一個loop的迭代操作和相同loop的不同迭代操作。例如,一個操作分別發生在K = 1時,當K = 2時(其中k是循環指數)。
第一個操作是在line 60數組res上的存儲(內存讀操作)。
第二個操作是在line 60數組res上的下載(內存寫操作)。
從圖中可以看到line 60是從數組res的讀取(由于+=操作符)和寫入數組res。數組默認映射到RAM block,下面Performance View的細節解釋了為什么會發生這種沖突。
?
成功的schedule中,Product loop的下一次迭代如上所示。在這個schedule中,initiation interval(II)= 2,即loop操作每兩個周期重啟。任何block RAM之間的訪問沒有沖突。(沒有突出顯示的單元迭代重疊。)
不成功的schedule顯示了為什么loop不能在II = 1時pipeline。此時,下一次迭代將需要1個時鐘周期后開始。當第二次迭代嘗試一個地址去讀入時,第一次迭代中寫入block RAM的操作仍然發生。這些地址是不同的,并且都不能在同一時間被應用到block RAM。
因此,你不能將Product loop的initiation interval 設置為1。下一步是pipeline Col loop。這將自動展開Product loop,并創建更多的operators,因此需要更多的硬件資源,但它確保在Product loop的不同迭代之間沒有dependency。
?
?
在綜合過程中,在控制臺窗格中報告的信息顯示loop Product被展開,loop flattening是在loop Row上執行,默認initiation intervalv為1的目標不能在loop Row_Col上實現,這是由于數組a上memory資源的限制。
?
綜合報告顯示,如上所述,對loop Row_Col的interval只有2:目標是每個周期處理一個sample。你可以再一次使用Analysis窗口來證明為什么不實現initiation target。
Step 3 打開Analysis perspective,在Performance View里,展開Row_Col loop
?
在數組a中有三個讀操作。兩個讀操作開始于C1狀態,第三個讀操作開始與C2狀態。數組被實現為block RAMs和數組,這是參數的函數是實現塊內存端口。在這兩種情況下,一個block RAM最大只能有兩個端口(對于雙端口block RAM來說)。訪問數組a通過一個單一block RAM接口,沒有足夠的端口能夠在一個時鐘周期中讀取所有三個值。
另一種查看該資源限制的方法是使用到Resource窗口。
Step 4打開Resource tab,擴展Memory Ports
狀態C1下面兩個讀操作與那些狀態C2下的讀操作重疊,因此,只有一個單一的周期是可見的:很顯然該資源被用于在多個狀態。即使當端口a的問題得到解決,相同的問題會發生在端口b:它也有三個讀操作。
?
高級綜合允許數組被partitioned, mapped together和 re-shaped。這些都允許在不改變源代碼的情況下,對數組進行修改。
solution4: Reshape the Arrays
Step 1 創建solution4,在Directive里給數組a和數組b插入ARRAY_RESHAPE,選擇dimension分別為2和1
?
Step 2 Run C Synthesis
綜合報告顯示頂層loop Row_Col現在是每個時鐘周期處理數據的一個sample。
? 頂層模塊需要12個時鐘周期才能完成。
? 經過3次循環,Row_Col loop輸出sample(迭代延遲)。
? 然后每個周期讀取1個sample(Initiation Interval)。
? 9次iterations/samples(Trip count)完成所有samples。
? 3 + 9 = 12個時鐘周期
函數可以完成并返回開始處理下一組數據?,F在,把block RAM接口設置為FIFO接口,數據流形式。
solution5: Apply FIFO Interfaces
Step 1 創建solution5,在Directive里給數組a,b,res插入INTERFACE,在mode里選擇ap_fifo
?
Step 2 Run C Synthesis,Console窗口報錯
?在line57寫[ 0 ] [ 0 ]。
?然后在line60寫[ 0 ] [ 0 ]。
?然后在line60一個寫[ 0 ] [ 0 ]。
?然后在line60一個寫[ 0 ] [ 0 ]。
?在line57寫入[ 0 ] [ 1 ](在增量指標J后)。
?然后在line60寫[ 0 ] [ 1 ]。
連續四個寫入地址[ 0 ] [ 0 ]不能構成一個數據流模式,而是隨機存取。
?
檢查代碼后發現在數組a和數組b存在類似的問題。它是使用一個FIFO接口訪問代碼已經寫好的數據,這是不可能實現的。在使用FIFO接口時,Vivado Hls的優化directives會有不足,因為當前代碼執行了一定的讀寫順序。下面pipeline整個function,對比這兩種方法的差異。
solution6: Pipeline the Function
Step 1 創建solution6,刪掉loop Col下面的Directive,給matrixmul函數插入PIPELINE Directive
?
Step 2 Run C Synthesis,并比較各個report
?
solution6在較少的時鐘就可以完成,并可以每5個時鐘周期開始一個新的transaction。然而,消耗的資源也大幅增加,因為所有的循環在設計中被打開。
Pipelining loops允許循環保持rolling,從而提供了一個很好的方法來控制area。當pipeline一個函數時,函數中包含的所有 loops都是打開,這是一個pipeline的要求。流水線功能設計可以每5個時鐘周期處理一組(9個)新的samples。這超過了每個時鐘處理1個sample的要求,因為高級綜合的默認行為是產生一個最高性能的設計。pipeline function會產生最好的性能,然而,如果它超過所需性能,它可能需要多個額外的directives。
優化 I/O Accesses的C代碼
進一步優化需要重新編寫代碼,下面介紹如何修改matrixmul.cpp的代碼,來幫助克服一些在代碼中固有的性能限制。
Step 1 創建并打開Project
找到Design_Optimization lab2文件夾,依次在Command Prompt 窗口輸入vivado_hls –f run_hls.tcl和vivado_hls –p matrixmul_prj
Step 2 打開matrixmul.cpp源代碼
?
審查代碼并確認以下:
?之前的directives在這里(包括FIFO接口)以pragmas形式指定的代碼。
? for循環已被添加到緩存行和列讀取。
?當最終的結果是計算為每個值時,一個臨時變量被用于累計,并且端口res只能被寫入。
?因為對于for循環緩存行和列需要多個周期去執行讀取,pipeline directive已應用于Col for循環,來確保這些緩存for循環自動打開。
Step 3 Run C Synthesis
?
該設計已被完全綜合,每個時鐘周期讀取一個使用數據流FIFO接口的sample。
總結
本文介紹了如何分析pipelined loops,并準確地理解哪些限制阻止優化目標的實現。以及對function 和 loop 進行pipeline的優點和缺點。代碼中意外dependencies可以阻止硬件設計目標實現,如何通過修改源代碼來克服它們。
評論
查看更多