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.
Can you post your new kernel? You should not need to set the shift register size to a value higher than the MAC latency using the method I mentioned.
Regarding run time, it is worse than which case? Due to the non-coalescable accesses in your code to the "in" buffer, you are going to experience a huge amount of contention caused by all those memory ports competing for the memory bus, and your unroll factor is also quite large which makes things even worse; it is not very surprising to get worse performance in such cases compared to the non-unrolled case or smaller unroll factors since the kernel would be memory-bound anyway while lower unroll factor will result in less memory contention and better performance. Moreover, in the method I mentioned, you will be doing some extra unused computation in the last iteration; hence, if your iteration count is not divisible by the unroll factor and at the same time, it is not very large compared to the unroll factor, the unused computation in the last iteration can potentially have a substantial effect on run time.
I changed the access patern so that in is now accessed sequentially. With a UF of 64 (fmax=220MHz) this becomes coalesced to a 2048-bit read. This results in a 5x increase in runtime compared to the non-sequential access pattern above. Minding your comment about memory saturation I decreased that UF to 16 (fmax=309MHz) and resynthesized. Area is drastically saved in both cases but performance does not improve much. I would have imagined that sequential reads would have greatly improved performance here, but it does not. I've included some profiled test cases below (from OCL events).
N, M, O: runtime
UF 64, II=1
64, 112, 32, : 14.711209 ms
128,56, 64, : 14.717709 ms
128,56, 128, : 29.326209 ms
256,28, 128, : 10.700833 ms
256,28, 256, : 29.360500 ms
512,14, 256, : 2.439333 ms
512,14, 512, : 21.220958 ms
1024, 7, 512, : 2.243875 ms
1024, 7, 1024, : 3.990750 ms
UF 16, II=1
64, 112, 32, : 10.554042 ms
128,56, 64, : 10.457500 ms
128,56, 128, : 20.892041 ms
256,28, 128, : 10.520417 ms
256,28, 256, : 20.920375 ms
512,14, 256, : 10.513583 ms
512,14, 512, : 21.001666 ms
1024, 7, 512, : 8.579375 ms
1024, 7, 1024, : 21.323458 ms
In comparison, the non-sequential read (as per the kernel code in a previous post) with 64x UF gets this:
64, 112, 112, 32: 7.376835 ms
128, 56, 56, 64 : 3.769259 ms
128, 56, 56, 128: 5.600221 ms
256, 28, 28, 128: 2.882847 ms
256, 28, 28, 256: 4.712566 ms
512, 14, 14, 256: 2.420952 ms
512, 14, 14, 512: 4.206385 ms
1024, 7, 7, 512 : 2.233914 ms
1024, 7, 7, 1024: 4.076918 ms
Regarding the HBMs, I don't think much has changed, only one HBM channel can be assigned to a global memory arg. Creating 32 different kernels as per the bandwidth test example seems to be another way of doing it... but also cannot think of other ways, or if it would be worthwhile...