As you will be aware, I do a lot of High-Level Synthesis (HLS) design for clients, especially for image processing applications. One of the great things about HLS is the productivity it brings when creating the application and its verification.
However, when the performance of our HLS block is not as expected, being able to find the critical path which impacts the violation is crucial. We have looked before at the analysis view and the potential optimizations which can be used to increase performance.
In this blog, we are going to examine how we can focus on finding the timing and initiation violations within our HLS designs and of course, correcting them.
Let’s take a simple example of a Test Pattern Generator (TPG). The custom TPG will load in image over the S AXI link from the PS of a Zynq device and then output this image at very fast frame rates. Such an approach is often used to verify image processing algorithms. The image, once loaded, is stored in BRAM / URAM depending upon the device which has been selected for implementation. Crucially to achieve the high frame rates required we are going to output multiple pixels per clock.
This code can be written simply using memcopy to read in the image from the PS DDR and two for loops to output the data correctly.
However, the desire to output two (or more) pixels per clocks makes for a bottleneck in reading from the array which stores the image.
Of course, this bottleneck exists as the image is stored as 16-bit words in each memory location. Reading out two pixels requires reading of two memory locations. This cannot be achieved in one clock cycle unless we correctly partition the BRAM.
When we open the analysis view, we will be presented with information under the module hierarchy, indicating which module if any, is presenting a timing violation or initiation interval violation.
If we only want to focus on the violations, we can click on the timing or II violation button at the top of the module hierarchy.
As it stands, our design indicates a II violation in the tpg_gen function, which is the core of the function that reads the memory and outputs the data over the AXI Stream.
At this point we know we have a II violation, but we need to be able to find the root cause within our design and correct it. We can find the root cause by setting the analysis focus to II violation. This will focus the analysis view on the design element which is causing the II violation.
Once the focus is set to II violation in our example, we see that the root cause of the issue reads from the BRAM blocks. The failing element will be colored blue. This will be the case for all analysis views indicating there is an issue. Knowing the failing element, we need to be able to identify which line of the code is causing the potential issue. We can select the operation / control step by right clicking and selecting “goto source”. The source line will be cross probed.
Now that we know what the issue is and have identified the source line of code causing the issue, we can begin to implement optimization strategies. For this case, we can partition the Block Memory into a cyclic buffer such that we get two Block RAMs. Each Block RAM stores the data cyclically. For example, BRAM A contains data elements 0, 2, 4, 6 etc., while BRAM B contains 1,3,5,7 etc. This allows the two pixel values to be read in parallel and the desired initiation interval achieved.
Of course, more complex algorithms may need a little more analysis and optimization, but at least we now know how we can focus in on what the root cause might be.