| 84 | // Generate optimal thread values |
| 85 | template<typename T> |
| 86 | const dim3 threadsMgt<T>::genThreads() const { |
| 87 | // Performance is mainly dependend on: |
| 88 | // - reducing memory latency, by preferring a sequential read of |
| 89 | // cachelines (principally dim0) |
| 90 | // - more parallel threads --> higher occupation of available |
| 91 | // threads |
| 92 | // - more I/O operations per thread --> dims[3] indicates the # |
| 93 | // of I/Os handled by the kernel inside each thread, and outside |
| 94 | // the scope of the block scheduler |
| 95 | // High performance is achievable with occupation rates as low as |
| 96 | // 30%. Here we aim at 50%, to also cover older hardware with slower |
| 97 | // cores. |
| 98 | // https://stackoverflow.com/questions/7737772/improving-kernel-performance-by-increasing-occupancy |
| 99 | // http://www.nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf |
| 100 | // https://www.cvg.ethz.ch/teaching/2011spring/gpgpu/GPU-Optimization.pdf |
| 101 | // https://en.wikipedia.org/wiki/Graphics_Core_Next#SIMD_Vector_Unit |
| 102 | |
| 103 | // The performance for vectors is independent from array sizes. |
| 104 | if ((d1 == 1) & (d2 == 1)) return dim3(128U); |
| 105 | |
| 106 | // TOTAL OCCUPATION = occup(dim0) * occup(dim1) * occup(dim2). |
| 107 | // For linearized arrays, each linear block is allocated to a dim, |
| 108 | // resulting in large numbers for dim0 & dim1. |
| 109 | // - For dim2, we only return exact dividers of the array dim[3], so |
| 110 | // occup(dim2)=100% |
| 111 | // - For dim0 & dim1, we aim somewhere between 30% and 50% |
| 112 | // * Having 2 blocks filled + 1 thread in block 3 --> occup > |
| 113 | // 2/3=66% |
| 114 | // * Having 3 blocks filled + 1 thread in block 4 --> occup > |
| 115 | // 3/4=75% |
| 116 | // * Having 4 blocks filled + 1 thread in block 5 --> occup > |
| 117 | // 4/5=80% |
| 118 | constexpr unsigned OCCUPANCY_FACTOR{2U}; // at least 2 blocks filled |
| 119 | |
| 120 | // NVIDIA: |
| 121 | // warp = 32 |
| 122 | // possible blocks = [32, 64, 96, 128, 160, 192, 224, 256, .. |
| 123 | // 1024] best performance = [32, 64, 96, 128] optimal perf = |
| 124 | // 128; any combination |
| 125 | // NIVIDA always processes full wavefronts. Allocating partial |
| 126 | // warps |
| 127 | // (<32) reduces throughput. Performance reaches a plateau from |
| 128 | // 128 with a slightly slowing for very large sizes. |
| 129 | // For algorithm below: |
| 130 | // parallelThreads = [32, 64, 96, 128] |
| 131 | constexpr unsigned minThreads{32}; |
| 132 | const unsigned relevantElements{d0 * d1 * d2}; |
| 133 | constexpr unsigned warp{32}; |
| 134 | |
| 135 | // For small array's, we reduce the maximum threads in 1 block to |
| 136 | // improve parallelisme. In worst case the scheduler can have 1 |
| 137 | // block per CU, even when only partly loaded. Range for block is: |
| 138 | // [minThreads ... 4 * warp multiple] |
| 139 | // * NVIDIA: [4*32=128 threads] |
| 140 | // At 4 * warp multiple, full wavefronts (queue of 4 partial |
| 141 | // wavefronts) are all occupied. |
| 142 | |
| 143 | // We need at least maxParallelThreads to occupy all the CU's. |