Forum Discussion
Reducing initiation interval, relaxing loop-carried dependency
- 6 years ago
I did some testing on your kernel, the problem is from your overly large unroll factor and all the memory ports it creates. If you set II_CYCLES to 5 (but not below that) and reduce UF to 16, you will get an II of 1. Moreover, at least with v19.4 (probably 19.0+ but I didn't test), you will get an II of 1 even with II_CYCLES set to 1 on Arria 10 because the compiler optimizes out the shift register and implements single-cycle accumulation instead. This bring me to a better solution to your problem if you are targeting Arria 10:
#define UF 16 kernel void shift_reg(global float* restrict compute, global float* restrict in, global float* restrict w, int N, int M, int O) { for (int i = 0; i < N; ++i) { for (int yy = 0; yy < M; ++yy) { for (int xx = 0; xx < M; ++xx) { int yy_curr = yy * M; int i_curr = i * O; float final_sum = 0.0f; int exit = O / UF; for (int j = 0; j < exit; j++) { float acc_i = 0.0; #pragma unroll for (int k = 0; k < UF; k++) { int rc = j * UF + k; acc_i += in[((((rc * M) + yy) * M) + xx)] * w[((i_curr) + rc)]; } final_sum += acc_i; } compute[((yy_curr) + xx)] = final_sum; } } } }This will give an II of 1 regardless of UF; though I think single-cycle accumulation is not available on Stratix 10 since the default target frequency on Stratix 10 is 480 MHz which is too high for single-cycle accumulation. Either way, you can still use the shift register method on Stratix 10, just don't use an unroll factor larger than 16. My guess is that if you compile your kernel with varying values of UF from 1 to 32, performance will probably go up until 4 or 8, but it will start going down after that.
P.S. You should also consider the operating frequency when doing performance comparison between different kernels.
In general you should avoid partially unrolling a loop unless the loop has a compile-time-known trip count and that trip count is divisible by the unroll factor. In the particular case of reductions loops, partial unrolling will also break the shift register optimization since when you partially unroll the loop, you are effectively increasing the latency of each loop iteration, which means that you will have to use a larger shift register to solve the dependency on the reduction variable and you will have to keep increasing the size of the shift register as you increase the unroll factor; this is certainly not a scalable method. To solve this issue, check Section 3.2.2.1 in the following document:
https://arxiv.org/ftp/arxiv/papers/1810/1810.09773.pdf
Specifically, the code segment in Figure 3-5 shows how you can implement partial unrolling manually by splitting your reduction loop into two loops, the innermost one having a trip count equal to the unroll factor and being fully unrolled (which will not require shift register anymore), and the outer loop's trip count being adjusted accordingly and the shift register optimization being used only for the outer loop which is not unrolled, with a fixed size that is independent of the unroll factor.
One more thing you should consider doing is to re-order the content of your "in" buffer on the host side so that accesses to that buffer in the kernel also become consecutive or else your external memory performance will suffer greatly.