| 172 | // BLOCK OPERATIONS, as are: matmul, etc. |
| 173 | template<typename T> |
| 174 | inline cl::NDRange threadsMgt<T>::genLocal(const cl::Kernel& ker) const { |
| 175 | // Performance is mainly dependend on: |
| 176 | // - reducing memory latency, by preferring a sequential read of |
| 177 | // cachelines (principally dim0) |
| 178 | // - more parallel threads --> higher occupation of available |
| 179 | // threads |
| 180 | // - more I/O operations per thread --> dims[3] indicates the # |
| 181 | // of I/Os handled by the kernel inside each thread, and outside |
| 182 | // the scope of the block scheduler |
| 183 | // High performance is achievable with occupation rates as low as |
| 184 | // 30%. Here we aim at 50%, to also cover older hardware with slower |
| 185 | // cores. |
| 186 | // https://stackoverflow.com/questions/7737772/improving-kernel-performance-by-increasing-occupancy |
| 187 | // http://www.nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf |
| 188 | // https://www.cvg.ethz.ch/teaching/2011spring/gpgpu/GPU-Optimization.pdf |
| 189 | // https://en.wikipedia.org/wiki/Graphics_Core_Next#SIMD_Vector_Unit |
| 190 | |
| 191 | // The performance for vectors is independent from array sizes. |
| 192 | if ((d1 == 1) & (d2 == 1)) return cl::NDRange{128ULL}; |
| 193 | |
| 194 | // TOTAL OCCUPATION = occup(dim0) * occup(dim1) * occup(dim2). |
| 195 | // For linearized arrays, each linear block is allocated to a dim, |
| 196 | // resulting in large numbers for dim0 & dim1. |
| 197 | // - For dim2, we only return exact dividers of the array dim[3], so |
| 198 | // occup(dim2)=100% |
| 199 | // - For dim0 & dim1, we aim somewhere between 30% and 50% |
| 200 | // * Having 2 blocks filled + 1 thread in block 3 --> occup > |
| 201 | // 2/3=66% |
| 202 | // * Having 3 blocks filled + 1 thread in block 4 --> occup > |
| 203 | // 3/4=75% |
| 204 | // * Having 4 blocks filled + 1 thread in block 5 --> occup > |
| 205 | // 4/5=80% |
| 206 | constexpr unsigned OCCUPANCY_FACTOR{2U}; // at least 2 blocks filled |
| 207 | |
| 208 | // NVIDIA: |
| 209 | // WG multiple = 32 |
| 210 | // possible blocks = [32, 64, 96, 128, 160, 192, 224, 256, .. 1024] |
| 211 | // best performance = [32, 64, 96, 128] |
| 212 | // optimal perf = 128; any combination |
| 213 | // NIVIDA always processes full wavefronts. Allocating partial WG |
| 214 | // (<32) reduces throughput. Performance reaches a plateau from |
| 215 | // 128 with a slightly slowing for very large sizes. |
| 216 | // AMD: |
| 217 | // WG multiple = 64 |
| 218 | // possible block = [16, 32, 48, 64, 128, 192, 256] |
| 219 | // best performance = [(32, low #threads) 64, 128, 256] |
| 220 | // optimal perf = (128,2,1); max 128 for 1 dimension |
| 221 | // AMD can process partial wavefronts (multiple of 16), although |
| 222 | // all threads of a full WG are allocated, only the active ones |
| 223 | // are executed, so the same number of WGs will fit a CU. When we |
| 224 | // have insufficent threads to occupy all the CU's, partial |
| 225 | // wavefronts (<64) are usefull to distribute all threads over the |
| 226 | // available CU's iso all concentrating on the 1st CU. |
| 227 | // For algorithm below: |
| 228 | // parallelThreads = [32, 64, (96 for NIVIDA), 128, (256 for AMD)] |
| 229 | constexpr unsigned minThreads{32}; |
| 230 | const unsigned relevantElements{d0 * d1 * d2}; |
| 231 | const unsigned WG{static_cast<unsigned>( |