* [Stratus HLS User Guide](https://365nthu-my.sharepoint.com/:b:/g/personal/113065507_office365_nthu_edu_tw/EdQ8Otkd9tNDlMYwN11uwksBxDHfa5WHvMtT7FPJRboAGQ?e=WQyzbL) * [Stratus HLS Reference Guide](https://365nthu-my.sharepoint.com/:b:/g/personal/113065507_office365_nthu_edu_tw/EeCSIwh-jpFGgZ2cYMuaUeMB7DqOXvB8EuHKuWnUTAU9vA?e=vwCTeH) # HLS (High Level Synthesis) 重點 * HLS 使用 C/C++ 產生 RTL code (e.g. SystemVerilog) 使得不需要做 explicit details on FSM (Finite State Machine)。 > 因為寫 RTL 時,FSM 都要定義好每個 state 在所有 cycle 的行為 * Quality: 實作 DSP 演算法下,HLS 合成的 RTL code 通常會有近似或更好於 hand-written RTL code 的 PPA (Power, Performance, Area);而 CPU/DSP cores 通常 hand-written RTL code 會有更好的 PPA > 例如 cache,會有許多 resource allocation optimization 的演算法,這些 HLS 不容易合成 ## Flow Chart ![Stratus_Ch1_Introduction_頁面_05](https://hackmd.io/_uploads/BJCrFui01g.png) * simulation model (Lab 1 - Lab 4 做的) 跟 design model (SystemC 寫的 HLS) 可以 reuse 的好處: 1. 可以快速驗證 interface (但執行效率差) 2. simulation model 的 testbench 可以使用在 RTL 的 testbench ### High-level Synthesis ![Stratus_Ch1_Introduction_頁面_08](https://hackmd.io/_uploads/SyBXXUkklx.png) * Control and Data Flow Extraction * 建立 **control flow diagram (graph) (CFG)** 及 **data flow diagram (graph) (DFG)** * control flow diagram (graph) (CFG) 表達 multiplex, jump 等 control flow * data flow diagram (graph) (DFG) 表達 data flow > ex: > ![image](https://hackmd.io/_uploads/rJ4xrLkyxe.png) * Resource Allocation 根據所有 operations 及 objective 計算需要用的 resources (multiplexer, register, memory, ...) > Example 1: > ```cpp= > if (a < b) {a = a + c; b = b - c;} > else {b = b + c; a = a - c;} > ``` > 若 objective 為 area: 只要一個 adder resource 就好 > 若 objective 為 performance: 使用兩個 adder resources * Scheduling 根據所有 operations 依照 Resource Allocation 產生的 resources 做每個 cycle 的 scheduling。最後可產生一 FSM > 在 [Example 1](https://hackmd.io/pzrTfGbjTbyT2SIKr_mF7A?both=&stext=1263%3A10%3A0%3A1744951305%3ACyM-uy) 若 objective 為 performance 時,schedule 兩個 add operation 執行在同一個 cycle * Functional Unit & Register Binding * Functional Unit Binding 依照 Resource Allocation 產生的 functional units (ex: ALU、multiplier),binding operators 與這些 functional units * Register Binding 依照 Resource Allocation 產生的 registers (ex: 變數或中間結果當需要用到 register 暫存結果時,Resource Allocation 會產生 registers),binding 變數或中間結果與這些 registers ## Cadence Stratus Architecture ![image](https://hackmd.io/_uploads/BJjmpdXJlx.png) * `dut.cc` 即為要合成成 RTL 的 SystemC module Architecture: ![image](https://hackmd.io/_uploads/rJfW0OQJge.png) * `system.cc` 即將 testbench、dut 包成一個 top module :::info ++Remark++ 1. memory 的合成是用 **memory compiler**,因為 memory compiler 要根據製程決定,故不在 HLS 的工具裡面。memory compiler 會輸出所需 memory 的 area、power、latency 等。目前課堂的範例合成時都沒有用 memory compiler,是用 dummy 的 ::: # Coding Style * 只使用 **static** data structure,不要做 dynamic memory allocation > 因為 dynamic memory 是由 OS 管理。HLS 不會做 OS 的事情。任何用 OS API 的都是不可 synthesizable 的。而且 dynamic 的行為在許多 optimization algorithm 不適用 * Hardware constraints * 分派 memory access pattern: 哪些資料要優先帶到 memory、盡量讓 memory access amortize (平攤)、盡量達到 memory reuse > 相關概念: Memory Arithmetic Intensity (每 byte memory 可以做多少 operation) * Computation complexity > ex: multiply 改成 shift * I/O protocols (什麼時候 I/O data) * 多個 module 間不使用 global variables、沒有 pointers 指向其他 module 的空間。且 variable 的 scope 越小越好 * 當用參數來客製化一個 module 時,使用 **template** 比起宣告在 constructor 更容易讓 code 易讀 * 當使用 `cynw_p2p` 傳自定義的 struct 時,**需要手動寫此 struct 的 copy constructor, `==`、`=` operator overloading、`sc_trace()` function,否則會合成失敗!!!** :::spoiler Example (array) ```cpp struct input_array_t { input_t data[SIZE]; // Default Constructor input_array_t() {} // Copy Constructor input_array_t( const input_array_t & obj ) { for(int i=0;i<SIZE;i++) { data[i]=obj.data[i]; } } // == operator inline bool operator==(const input_array_t & other) { for(int i=0;i<SIZE;i++) { if(data[i]!=other.data[i]) return false; } return true; } // Assignment operator inline input_array_t & operator=(const input_array_t & other) { for(int i=0;i<SIZE;i++){ data[i]=other.data[i]; } return *this; } // sc_trace function inline void sc_trace(sc_trace_file * tf, const input_array_t & object, const sc_string & name) { for (int i=0; i < SIZE; i++ ) { char buf[16]; sprintf(buf,".data%d",i); sc_trace(tf, object.data[i], name + sc_string(buf)); } } // 非必要;可用來做 cout inline ostream & operator <<(ostream & os, const input_array_t & a) { #ifndef STRATUS for (int i=0; i < SIZE; i++ ) { os << " " << a.data[i].to_string().c_str(); } #endif return os; } // 非必要;可用來做 cin inline istream & operator >> ( istream & is, input_array_t & obj ) { bool eof = !is.good(); is >> std::ws; for (int i=0; i < SIZE; i++ ) { eof = is.eof(); if(!eof) { is >> obj.data[i]; } } return is; } } ``` ::: * 在 thread 內做 reset 而不是在 constructor * 使用 Stratus 時就用他們的 fixed-point types (ex: `cynw_float`, `cynw_fixed`),**Stratus 目前已不支援 `sc_fixed`**。並且 `cynw_fixed` 的 template parameter 如果沒有要使用 saturation 就不用宣告成要做 saturation (因為做 saturation 也會消耗時間) * 能用 unsigned type 就用 unsigned,因為 signed 會耗更多的空間及運算 (二補數)。故把減法改成加法也可以避免出現 signed 情況 > ex: > `if (y > x - 1)` 改成 `if (y + 1 > x)` * 可以把 if 條件寫成互斥 (越清楚越好),這樣 Stratus 做 optimization 時就能對此更容易做 optimization > ex: > ```cpp > if (a < b) > // ... > if (a >= b) > // ... > ``` > 改成 > ```cpp > tmp = a < b; > if (tmp) > // ... > if (!tmp) > // ... * 使用 C++ native types 時,變數要先初始化 (除了 array,因為 array 初始化要用 loop,會花很多時間。可看 report 並試過 performance 再做決定)。SystemC type 會自己初始化 * **SystemC code 決定 functionality** (非 implementation details);**Stratus directives 做 Design Space Exploration** => **除了 I/O,不要做太多決定 cycle-level 的事**,讓 HLS tool 自己做決定 * Bit Trimming: 在一個 expression 內,HLS tool 能自己推出 l-value 所需的 bit 數的地方就不要自己指定 bit 數,**除了 loop**,因為 HLS tool 無法準確推出 loop 內變數所需的 bit 數 * Bit Width Extension: 如果 expression 的 l-value 的 bit 數指定比 r-value 少,HLS tool **不會** 自己 trim r-value 的 bit 數 (因為可能會讓結果錯誤) > ex: > ```cpp= > sc_uint<5> a = f1(); > sc_uint<16> b = f2(); > sc_uint<9> c = f3(); > sc_uint<16> result; > > result = (a * b) + c; > ``` > > HLS tool 不會自己 trim r-value 成 16 bit => 可能會需要多 allocate 一個 22 bit register 暫存 a * b 的結果 * Constant Propagation: HLS tool 會自己做 constant propagation,減少不必要的硬體資源 > ex: > ```cpp= > unsigned func(int val) { > return (val * val); > } > > for(int i = 1; i < 4; i++ ) > result += func(i); > ``` > > HLS tool 會自動轉成: > ```cpp= > result = (1 * 1) + (2 * 2) + (3 * 3); > ``` * 盡量使用前再宣告變數 (因為宣告變數就表示佔用一個 register) * ++Def++ (Loop-Carried Dependency (LCD)) An LCD is a *variable* that is live from one iteration of a loop to the next > ex: > ```cpp= > for(int i = 1; i < 4; i++ ) > { > a = b*i; > result += func(i)+a; > } > > outputPort.put(result); > ``` > `result` 變數即為 LCD > 可以看 HLS tool 產生的 LCD report (`--message_detail=1`),如果裡面有不是自己設計需要的就可以自己改寫程式移除 * Synthesizable Data Types ![image](https://hackmd.io/_uploads/rkuFC6Slex.png) ![image](https://hackmd.io/_uploads/Hyoc06Segl.png) * `cynw_utilities` functions ![image](https://hackmd.io/_uploads/Sk4W0MJWgl.png) * `div()` 會讓速度變慢、面積減少 * 由於 memory access 皆需要花 cycle 數,故建議不要寫重複 access 相同記憶體位置、相同值的 code > ex: > ```cpp= > a1 = mem[1] + mem[2] > a2 = mem[1] + mem[3] > ``` > 至少需要 6 個 cycle (access mem[1], access mem[2], add, access mem[1], access mem[3], add) > > ```cpp= > a = mem[1] > a1 = a + mem[2] > a2 = a + mem[3[] > ``` > 只至少需要 5 個 cycle * Loop * **Loop bound 不要用變數!!!** (因為 loop unroll, loop pipeline 都無法用在 loop bound 為未知數時)。解決方法為可以把 loop 整體變成一個 function template * Function * **預設 function 皆為 inline** * 可用 `HLS_SCHEDULE_REGION()` 強迫把 function 成為一個可 reuse 的 hardware # 語法 :::success 範例可至 [Lab 5](https://hackmd.io/TZEWmooHSxytHxg5F6mmPA) 查看 ::: ## Interface * `cynw_p2p<datatype>` (channel)、`cynw_p2p<datatype>::in` 及 `cynw_p2p<datatype>::out` (port) * 即為包裝好的 [ready/valid interface](https://hackmd.io/bFO7RLWYR76UMaVvXV4LUQ#adder-rdyvld) ## Directives 放在 C++ 程式的一個 block 的開頭 * `HLS_DEFINE_PROTOCOL("protocol 名稱")` * 表示此 block 是一個 protocol,要當作一個 atomic operation,需合成成同一個 clock cycle 或 pipeline stage > ex: > ```cpp > { > HLS_DEFINE_PROTOCOL("reset"); > > // 此 block 可以放所有 reset cycle 要做的事 > > } > ``` * Array 相關 * Mapping * `HLS_MAP_TO_MEMORY(array, "自訂memory名稱")` **預設方式**,將 array 合成為一 memory,r/w 一個 element 需要 **1** 個 cycle ![image](https://hackmd.io/_uploads/Sk8oyCBexg.png) * `HLS_FLATTEN_ARRAY(array)` 將 array 的每一個 element 都 flatten 成一個 register。小型 array 建議可以 flatten,flatten 後有機會增加 constant propagation ![image](https://hackmd.io/_uploads/ByOZe0Hxle.png) * `HLS_MAP_TO_REG_BANK(array)` ![image](https://hackmd.io/_uploads/SJCLl0reex.png) * Splitting * `HLS_SPLIT_ARRAY_DATA(array, 要split的bit長度)` > ex: > ```cpp= > sc_uint<64> arr[1024]; > HLS_MAP_TO_MEMORY(arr,"mem1024X8"); > HLS_SPLIT_ARRAY_DATA( arr, 8 ); > ``` > > ![image](https://hackmd.io/_uploads/BJoAW6O-ge.png) * `HLS_SPLIT_ARRAY_ADDR_LSB(array, addr_bits)` * 根據第 addr_bits 個 LSB 作分割 > ex: > ```cpp= > sc_uint<8> arr[1024]; > HLS_SPLIT_ARRAY_ADDR_LSB( arr, 1 ); > ``` > > ![image](https://hackmd.io/_uploads/H1hhMTdZee.png) * `HLS_SPLIT_ARRAY_ADDR_MSB(array, addr_bits)` * 同理 `HLS_SPLIT_ARRAY_ADDR_LSB()` * Merging * `HLS_MERGE_ARRAYS_DATA()` * `HLS_MERGE_ARRAYS_ADDR()` * Struct Packing * `HLS_PACK_ARRAYS(一個struct的array)` * 原本 Stratus 會把一個 struct 內的每個 member 產生一個 separate 的 memory 或 register bank。此 directive 可以把一個 struct 的 array 內的每個 element 排成一行 array * `HLS_UNROLL_LOOP(ON/OFF/數字[, count], "loop 名稱")` * unroll 此 loop > ex: > Loop unrolling with 2 copies > ![image](https://hackmd.io/_uploads/rJ2sndQkxe.png) > throughput = 2 outputs / 4 cycles > latency = 4 > 每個 cycle 內最多需要 2 個 adder * `count` 為一整數表示每一個 iteration 只展開幾個 loop。第一個參數為 `COMPLETE`、`CONSERVATIVE` 及 `AGGRESSIVE` 會需要指定 `count` 參數 * 第一個參數表示每次展開幾個 loop body * `OFF`: 不展開 * `ON`: 展開全部 (不含 inner loop) * `COMPLETE`: 展開全部 (含所有 inner loop) * `CONSERVATIVE` (保守式展開): 如果此 loop 的 upper bound 不確定 (為變數),此設定會自動 handle 避免 loop 次數無法被 `count` 整除的情況 * `AGGRESSIVE` (攻擊性展開): 只有在此 loop 的 upper bound 確定、且 loop 次數可以被 `count` 整除時才可使用。會不合成迴圈終止條件的硬體,直接複製迴圈至指定次數 * `HLS_PIPELINE_LOOP(stall方式, initiation interval, "pipeline loop 名稱")` * **flatten 此 loop 及此 loop 的所有 inner loop**,並做 pipelining ++Def++ (Initiation Interval (II)) A **Initiation Interval (II)** is the time period between the start times of two successive iterations of a loop-body execution > ex: > 1. No loop pipelining, II = 4 > ![image](https://hackmd.io/_uploads/rJxi5d71xl.png) > throughput = 1 output / 4 cycles > latency = 4 > 每個 cycle 內最多需要 1 個 adder > 2. Loop pipelining, II = 1 > ![image](https://hackmd.io/_uploads/ry9csOXyxe.png) > throughput = 1 output / 1 cycle > latency = 4 > 每個 cycle 內最多需要 3 個 adder > 3. Loop pipelining, II = 1 > ![image](https://hackmd.io/_uploads/SkEy2d71gx.png) > throughput = 1 output / 1 cycle > latency = 3 > 每個 cycle 內最多需要 3 個 adder * stall 方式 * `HARD_STALL`: 在沒有新的 input 被讀取進來、或沒有新的 output 被輸出,pipeline 就會 stall 住以 keep 住 state,需要等下一個 input 輸入/output 輸出,pipeline 才會停止 stall 並輸出/輸入 > ex: > ![image](https://hackmd.io/_uploads/SySr62dbgl.png) > cycle 6 沒有 input 被讀取進來,output D 前被 stall * `SOFT_STALL`: `HARD_STALL` 的情況改成不會讓 pipeline stall > ex: > ![image](https://hackmd.io/_uploads/HkLKandbee.png) > cycle 6 沒有 input 被讀取進來,但output D 不會被 stall :::info ++Remark++ 1. HLS tools 可針對不同 latency, II 需求做 **Design Space Exploration!** (hand-written RTL 很難) 2. 需注意太低的 initiation interval 可能造成 data dependency 無法滿足或 port conflict > ++Example 1 (data dependency)++ > ```cpp= > for(i = 0; i < 4; i++) { > HLS_PIPELINE_LOOP(HARD_STALL, 1, "loop_pipe"); > X = A * ( i + X ); > } > ``` > > 時序圖會變成這樣而 error: > ![Screenshot 2025-05-19 220608](https://hackmd.io/_uploads/Hke4jh_-gx.png) > ++Example 2 (port conflict)++ > ```cpp= > while( true ) { > HLS_PIPELINE_LOOP( HARD_STALL, 1, "main_loop_pipeline" ); > { > HLS_DEFINE_PROTOCOL("read"); > in_rdy= 1; > wait(1); > A = A_in.read(); > B = B_in.read(); > in_rdy = 0; > } > { > HLS_CONSTRAIN_LATENCY( 0, 1 "algorithm_latency" ); > X = A + B; > Y = A * B; > } > { > HLS_DEFINE_PROTOCOL("write"); > out_vld = 1; > data_out.write( X ); wait(1); > data_out.write( Y ); wait(1); > out_vld= 0; > } > } > ``` > > 時序圖會變成這樣: > ![Screenshot 2025-05-19 221335](https://hackmd.io/_uploads/HJ1an3_-ex.png) > cycle 4 會需要一次 output 兩個。但此 module 只有 1 個 output port ::: * `HLS_CONSTRAIN_LATENCY(最小允許的延遲 cycle 數, 最大允許的延遲 cycle 數, "constraint 名稱")` * 嘗試將此 block 的延遲控制在此區間 * 第二個參數可為 `HLS_ACHIEVABLE`,表示盡可能減少至最低的 latency upper bound