Apply clang-format to source tree
[alexxy/gromacs.git] / src / gromacs / gpu_utils / ocl_compiler.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2012,2013,2014,2015,2016,2017,2018,2019, by the GROMACS development team, led by
5  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6  * and including many others, as listed in the AUTHORS file in the
7  * top-level source directory and at http://www.gromacs.org.
8  *
9  * GROMACS is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Lesser General Public License
11  * as published by the Free Software Foundation; either version 2.1
12  * of the License, or (at your option) any later version.
13  *
14  * GROMACS is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with GROMACS; if not, see
21  * http://www.gnu.org/licenses, or write to the Free Software Foundation,
22  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
23  *
24  * If you want to redistribute modifications to GROMACS, please
25  * consider that scientific software is very special. Version
26  * control is crucial - bugs must be traceable. We will be happy to
27  * consider code for inclusion in the official distribution, but
28  * derived work must not be called official GROMACS. Details are found
29  * in the README & COPYING files - if they are missing, get the
30  * official version at http://www.gromacs.org.
31  *
32  * To help us fund GROMACS development, we humbly ask that you cite
33  * the research papers on the package. Check out http://www.gromacs.org.
34  */
35 /*! \internal \file
36  *  \brief Define infrastructure for OpenCL JIT compilation for Gromacs
37  *
38  *  \author Dimitrios Karkoulis <dimitris.karkoulis@gmail.com>
39  *  \author Anca Hamuraru <anca@streamcomputing.eu>
40  *  \author Teemu Virolainen <teemu@streamcomputing.eu>
41  *  \author Mark Abraham <mark.j.abraham@gmail.com>
42  */
43
44 #include "gmxpre.h"
45
46 #include "ocl_compiler.h"
47
48 #include "config.h"
49
50 #include <cstdio>
51
52 #include <algorithm>
53 #include <string>
54 #include <vector>
55
56 #include "gromacs/gpu_utils/oclutils.h"
57 #include "gromacs/utility/cstringutil.h"
58 #include "gromacs/utility/exceptions.h"
59 #include "gromacs/utility/gmxassert.h"
60 #include "gromacs/utility/path.h"
61 #include "gromacs/utility/programcontext.h"
62 #include "gromacs/utility/smalloc.h"
63 #include "gromacs/utility/stringutil.h"
64 #include "gromacs/utility/textreader.h"
65 #include "gromacs/utility/unique_cptr.h"
66
67 #include "ocl_caching.h"
68
69 namespace gmx
70 {
71 namespace ocl
72 {
73
74 /*! \brief True if OpenCL binary caching is enabled.
75  *
76  *  Currently caching is disabled by default unless the env var override
77  *  is used until we resolve concurrency issues. */
78 static bool useBuildCache = getenv("GMX_OCL_GENCACHE") != nullptr;
79
80 /*! \brief Handles writing the OpenCL JIT compilation log to \c fplog.
81  *
82  * If \c fplog is non-null and either the GMX_OCL_DUMP_LOG environment
83  * variable is set or the compilation failed, then the OpenCL
84  * compilation log is written.
85  *
86  * \param fplog               Open file pointer to log file
87  * \param program             OpenCL program that was compiled
88  * \param deviceId            Id of the device for which compilation took place
89  * \param kernelFilename      File name containing the kernel
90  * \param preprocessorOptions String containing the preprocessor command-line options used for the
91  * build \param buildFailed         Whether the OpenCL build succeeded
92  *
93  * \throws std::bad_alloc if out of memory */
94 static void writeOclBuildLog(FILE*              fplog,
95                              cl_program         program,
96                              cl_device_id       deviceId,
97                              const std::string& kernelFilename,
98                              const std::string& preprocessorOptions,
99                              bool               buildFailed)
100 {
101     bool writeOutput = ((fplog != nullptr) && (buildFailed || (getenv("GMX_OCL_DUMP_LOG") != nullptr)));
102
103     if (!writeOutput)
104     {
105         return;
106     }
107
108     // Get build log string size
109     size_t buildLogSize;
110     cl_int cl_error =
111             clGetProgramBuildInfo(program, deviceId, CL_PROGRAM_BUILD_LOG, 0, nullptr, &buildLogSize);
112     if (cl_error != CL_SUCCESS)
113     {
114         GMX_THROW(InternalError("Could not get OpenCL program build log size, error was "
115                                 + ocl_get_error_string(cl_error)));
116     }
117
118     char*             buildLog = nullptr;
119     unique_cptr<char> buildLogGuard;
120     if (buildLogSize != 0)
121     {
122         /* Allocate memory to fit the build log,
123            it can be very large in case of errors */
124         snew(buildLog, buildLogSize);
125         buildLogGuard.reset(buildLog);
126
127         /* Get the actual compilation log */
128         cl_error = clGetProgramBuildInfo(program, deviceId, CL_PROGRAM_BUILD_LOG, buildLogSize,
129                                          buildLog, nullptr);
130         if (cl_error != CL_SUCCESS)
131         {
132             GMX_THROW(InternalError("Could not get OpenCL program build log, error was "
133                                     + ocl_get_error_string(cl_error)));
134         }
135     }
136
137     std::string message;
138     if (buildFailed)
139     {
140         message += "Compilation of source file " + kernelFilename + " failed!\n";
141     }
142     else
143     {
144         message += "Compilation of source file " + kernelFilename + " was successful!\n";
145     }
146     message += "-- Used build options: " + preprocessorOptions + "\n";
147     message += "--------------LOG START---------------\n";
148     message += buildLog;
149     message += "---------------LOG END----------------\n";
150     ;
151
152     fputs(message.c_str(), fplog);
153 }
154
155 /*! \brief Construct compiler options string
156  *
157  * \param deviceVendorId  Device vendor id. Used to
158  *          automatically enable some vendor-specific options
159  * \return The string with the compiler options
160  */
161 static std::string selectCompilerOptions(ocl_vendor_id_t deviceVendorId)
162 {
163     std::string compilerOptions;
164
165     if (getenv("GMX_OCL_NOOPT"))
166     {
167         compilerOptions += " -cl-opt-disable";
168     }
169
170     /* Fastmath imprves performance on all supported arch */
171     if (getenv("GMX_OCL_DISABLE_FASTMATH") == nullptr)
172     {
173         compilerOptions += " -cl-fast-relaxed-math";
174
175         // Hint to the compiler that it can flush denorms to zero.
176         // In CUDA this is triggered by the -use_fast_math flag, equivalent with
177         // -cl-fast-relaxed-math, hence the inclusion on the conditional block.
178         compilerOptions += " -cl-denorms-are-zero";
179     }
180
181     if ((deviceVendorId == OCL_VENDOR_NVIDIA) && getenv("GMX_OCL_VERBOSE"))
182     {
183         compilerOptions += " -cl-nv-verbose";
184     }
185
186     if ((deviceVendorId == OCL_VENDOR_AMD) && getenv("GMX_OCL_DUMP_INTERM_FILES"))
187     {
188         /* To dump OpenCL build intermediate files, caching must be off */
189         if (!useBuildCache)
190         {
191             compilerOptions += " -save-temps";
192         }
193     }
194
195     if (getenv("GMX_OCL_DEBUG"))
196     {
197         compilerOptions += " -g";
198     }
199
200     return compilerOptions;
201 }
202
203 /*! \brief Get the path to the folder storing an OpenCL source file.
204  *
205  * By default, this function constructs the full path to the OpenCL from
206  * the known location of the binary that is running, so that we handle
207  * both in-source and installed builds. The user can override this
208  * behavior by defining GMX_OCL_FILE_PATH environment variable.
209  *
210  * \param[in] sourceRelativePath    Relative path to the kernel or other file in the source tree,
211  *                                  from src, e.g. "gromacs/mdlib/nbnxn_ocl" for NB kernels.
212  * \return OS-normalized path string to the folder storing OpenCL source file
213  *
214  * \throws std::bad_alloc    if out of memory.
215  *         FileIOError  if GMX_OCL_FILE_PATH does not specify a readable path
216  */
217 static std::string getSourceRootPath(const std::string& sourceRelativePath)
218 {
219     std::string sourceRootPath;
220     /* Use GMX_OCL_FILE_PATH if the user has defined it */
221     const char* gmxOclFilePath = getenv("GMX_OCL_FILE_PATH");
222
223     if (gmxOclFilePath == nullptr)
224     {
225         /* Normal way of getting ocl_root_dir. First get the right
226            root path from the path to the binary that is running. */
227         InstallationPrefixInfo info           = getProgramContext().installationPrefix();
228         std::string            dataPathSuffix = (info.bSourceLayout ? "src" : GMX_INSTALL_OCLDIR);
229         sourceRootPath = Path::join(info.path, dataPathSuffix, sourceRelativePath);
230     }
231     else
232     {
233         if (!Directory::exists(gmxOclFilePath))
234         {
235             GMX_THROW(FileIOError(
236                     formatString("GMX_OCL_FILE_PATH must point to the directory where OpenCL"
237                                  "kernels are found, but '%s' does not exist",
238                                  gmxOclFilePath)));
239         }
240         sourceRootPath = Path::join(gmxOclFilePath, sourceRelativePath);
241     }
242
243     // Make sure we return an OS-correct path format
244     return Path::normalize(sourceRootPath);
245 }
246
247 size_t getKernelWarpSize(cl_kernel kernel, cl_device_id deviceId)
248 {
249     size_t warpSize = 0;
250     cl_int cl_error =
251             clGetKernelWorkGroupInfo(kernel, deviceId, CL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE,
252                                      sizeof(warpSize), &warpSize, nullptr);
253     if (cl_error != CL_SUCCESS)
254     {
255         GMX_THROW(InternalError("Could not query OpenCL preferred workgroup size, error was "
256                                 + ocl_get_error_string(cl_error)));
257     }
258     if (warpSize == 0)
259     {
260         GMX_THROW(InternalError(formatString("Invalid OpenCL warp size encountered")));
261     }
262     return warpSize;
263 }
264
265 size_t getDeviceWarpSize(cl_context context, cl_device_id deviceId)
266 {
267     cl_int      cl_error;
268     const char* warpSizeKernel =
269             "__kernel void test(__global int* test){test[get_local_id(0)] = 0;}";
270     cl_program program = clCreateProgramWithSource(context, 1, &warpSizeKernel, nullptr, &cl_error);
271     if (cl_error != CL_SUCCESS)
272     {
273         GMX_THROW(InternalError("Could not create OpenCL program to determine warp size, error was "
274                                 + ocl_get_error_string(cl_error)));
275     }
276
277     cl_error = clBuildProgram(program, 0, nullptr, nullptr, nullptr, nullptr);
278     if (cl_error != CL_SUCCESS)
279     {
280         GMX_THROW(InternalError("Could not build OpenCL program to determine warp size, error was "
281                                 + ocl_get_error_string(cl_error)));
282     }
283
284     cl_kernel kernel = clCreateKernel(program, "test", &cl_error);
285     if (cl_error != CL_SUCCESS)
286     {
287         GMX_THROW(InternalError("Could not create OpenCL kernel to determine warp size, error was "
288                                 + ocl_get_error_string(cl_error)));
289     }
290
291     size_t warpSize = getKernelWarpSize(kernel, deviceId);
292
293     cl_error = clReleaseKernel(kernel);
294     if (cl_error != CL_SUCCESS)
295     {
296         GMX_THROW(InternalError("Could not release OpenCL warp-size kernel, error was "
297                                 + ocl_get_error_string(cl_error)));
298     }
299     cl_error = clReleaseProgram(program);
300     if (cl_error != CL_SUCCESS)
301     {
302         GMX_THROW(InternalError("Could not release OpenCL warp-size program, error was "
303                                 + ocl_get_error_string(cl_error)));
304     }
305
306     return warpSize;
307 }
308
309 /*! \brief Select a compilation-line define for a vendor-specific kernel choice from vendor id
310  *
311  * \param[in] vendorId Vendor id enumerator
312  *
313  * \return The appropriate compilation-line define
314  */
315 static const char* makeVendorFlavorChoice(ocl_vendor_id_t vendorId)
316 {
317     const char* choice;
318     switch (vendorId)
319     {
320         case OCL_VENDOR_AMD: choice = "-D_AMD_SOURCE_"; break;
321         case OCL_VENDOR_NVIDIA: choice = "-D_NVIDIA_SOURCE_"; break;
322         case OCL_VENDOR_INTEL: choice = "-D_INTEL_SOURCE_"; break;
323         default: choice = ""; break;
324     }
325     return choice;
326 }
327
328 /*! \brief Create include paths for kernel sources.
329  *
330  * All OpenCL kernel files are expected to be stored in one single folder.
331  *
332  * \throws std::bad_alloc  if out of memory.
333  */
334 static std::string makeKernelIncludePathOption(const std::string& unescapedKernelRootPath)
335 {
336     std::string includePathOption;
337
338     /* Apple does not seem to accept the quoted include paths other
339      * OpenCL implementations are happy with. Since the standard still says
340      * it should be quoted, we handle Apple as a special case.
341      */
342 #ifdef __APPLE__
343     includePathOption += "-I";
344
345     // Prepend all the spaces with a backslash
346     for (std::string::size_type i = 0; i < unescapedKernelRootPath.length(); i++)
347     {
348         if (unescapedKernelRootPath[i] == ' ')
349         {
350             includePathOption.push_back('\\');
351         }
352         includePathOption.push_back(unescapedKernelRootPath[i]);
353     }
354 #else
355     includePathOption += "-I\"" + unescapedKernelRootPath + "\"";
356 #endif
357
358     return includePathOption;
359 }
360
361 /*! \brief Replace duplicated spaces with a single one in string
362  *
363  * Only the first character will be kept for multiple adjacent characters that
364  * are both identical and where the first one returns true for isspace().
365  *
366  * \param str String that will be modified.
367  */
368 static void removeExtraSpaces(std::string* str)
369 {
370     GMX_RELEASE_ASSERT(str != nullptr, "A pointer to an actual string must be provided");
371     std::string::iterator newEnd = std::unique(
372             str->begin(), str->end(), [=](char a, char b) { return isspace(a) != 0 && (a == b); });
373     str->erase(newEnd, str->end());
374 }
375
376 /*! \brief Builds a string with build options for the OpenCL kernels
377  *
378  * \throws std::bad_alloc  if out of memory. */
379 static std::string makePreprocessorOptions(const std::string& kernelRootPath,
380                                            const std::string& includeRootPath,
381                                            size_t             warpSize,
382                                            ocl_vendor_id_t    deviceVendorId,
383                                            const std::string& extraDefines)
384 {
385     std::string preprocessorOptions;
386
387     /* Compose the complete build options */
388     preprocessorOptions = formatString("-DWARP_SIZE_TEST=%d", static_cast<int>(warpSize));
389     preprocessorOptions += ' ';
390     preprocessorOptions += makeVendorFlavorChoice(deviceVendorId);
391     preprocessorOptions += ' ';
392     preprocessorOptions += extraDefines;
393     preprocessorOptions += ' ';
394     preprocessorOptions += selectCompilerOptions(deviceVendorId);
395     preprocessorOptions += ' ';
396     preprocessorOptions += makeKernelIncludePathOption(kernelRootPath);
397     preprocessorOptions += ' ';
398     preprocessorOptions += makeKernelIncludePathOption(includeRootPath);
399
400     // Mac OS (and maybe some other implementations) does not accept double spaces in options
401     removeExtraSpaces(&preprocessorOptions);
402
403     return preprocessorOptions;
404 }
405
406 cl_program compileProgram(FILE*              fplog,
407                           const std::string& kernelRelativePath,
408                           const std::string& kernelBaseFilename,
409                           const std::string& extraDefines,
410                           cl_context         context,
411                           cl_device_id       deviceId,
412                           ocl_vendor_id_t    deviceVendorId)
413 {
414     cl_int cl_error;
415     // Let the kernel find include files from its module.
416     std::string kernelRootPath = getSourceRootPath(kernelRelativePath);
417     // Let the kernel find include files from other modules.
418     std::string rootPath = getSourceRootPath("");
419
420     GMX_RELEASE_ASSERT(fplog != nullptr, "Need a valid log file for building OpenCL programs");
421
422     /* Load OpenCL source files */
423     std::string kernelFilename = Path::join(kernelRootPath, kernelBaseFilename);
424
425     /* Make the build options */
426     std::string preprocessorOptions = makePreprocessorOptions(
427             kernelRootPath, rootPath, getDeviceWarpSize(context, deviceId), deviceVendorId, extraDefines);
428
429     bool buildCacheWasRead = false;
430
431     std::string cacheFilename;
432     if (useBuildCache)
433     {
434         cacheFilename = makeBinaryCacheFilename(kernelBaseFilename, deviceId);
435     }
436
437     /* Create OpenCL program */
438     cl_program program = nullptr;
439     if (useBuildCache)
440     {
441         if (File::exists(cacheFilename, File::returnFalseOnError))
442         {
443             /* Check if there's a valid cache available */
444             try
445             {
446                 program           = makeProgramFromCache(cacheFilename, context, deviceId);
447                 buildCacheWasRead = true;
448             }
449             catch (FileIOError& e)
450             {
451                 // Failing to read from the cache is not a critical error
452                 formatExceptionMessageToFile(fplog, e);
453             }
454         }
455         else
456         {
457             fprintf(fplog,
458                     "No OpenCL binary cache file was present, so will compile kernels normally.\n");
459         }
460     }
461     if (program == nullptr)
462     {
463         // Compile OpenCL program from source
464         std::string kernelSource = TextReader::readFileToString(kernelFilename);
465         if (kernelSource.empty())
466         {
467             GMX_THROW(FileIOError("Error loading OpenCL code " + kernelFilename));
468         }
469         const char* kernelSourcePtr  = kernelSource.c_str();
470         size_t      kernelSourceSize = kernelSource.size();
471         /* Create program from source code */
472         program = clCreateProgramWithSource(context, 1, &kernelSourcePtr, &kernelSourceSize, &cl_error);
473         if (cl_error != CL_SUCCESS)
474         {
475             GMX_THROW(InternalError("Could not create OpenCL program, error was "
476                                     + ocl_get_error_string(cl_error)));
477         }
478     }
479
480     /* Build the OpenCL program, keeping the status to potentially
481        write to the simulation log file. */
482     cl_int buildStatus =
483             clBuildProgram(program, 0, nullptr, preprocessorOptions.c_str(), nullptr, nullptr);
484
485     /* Write log first, and then throw exception that the user know what is
486        the issue even if the build fails. */
487     writeOclBuildLog(fplog, program, deviceId, kernelFilename, preprocessorOptions,
488                      buildStatus != CL_SUCCESS);
489
490     if (buildStatus != CL_SUCCESS)
491     {
492         GMX_THROW(InternalError("Could not build OpenCL program, error was "
493                                 + ocl_get_error_string(buildStatus)));
494     }
495
496     if (useBuildCache)
497     {
498         if (!buildCacheWasRead)
499         {
500             /* If OpenCL caching is ON, but the current cache is not
501                valid => update it */
502             try
503             {
504                 writeBinaryToCache(program, cacheFilename);
505             }
506             catch (GromacsException& e)
507             {
508                 // Failing to write the cache is not a critical error
509                 formatExceptionMessageToFile(fplog, e);
510             }
511         }
512     }
513     if ((OCL_VENDOR_NVIDIA == deviceVendorId) && getenv("GMX_OCL_DUMP_INTERM_FILES"))
514     {
515         /* If dumping intermediate files has been requested and this is an NVIDIA card
516            => write PTX to file */
517         char buffer[STRLEN];
518
519         cl_error = clGetDeviceInfo(deviceId, CL_DEVICE_NAME, sizeof(buffer), buffer, nullptr);
520         if (cl_error != CL_SUCCESS)
521         {
522             GMX_THROW(InternalError("Could not get OpenCL device info, error was "
523                                     + ocl_get_error_string(cl_error)));
524         }
525         std::string ptxFilename = buffer;
526         ptxFilename += ".ptx";
527
528         try
529         {
530             writeBinaryToCache(program, ptxFilename);
531         }
532         catch (GromacsException& e)
533         {
534             // Failing to write the cache is not a critical error
535             formatExceptionMessageToFile(fplog, e);
536         }
537     }
538
539     return program;
540 }
541
542 } // namespace ocl
543 } // namespace gmx