automation for setting GMX_GPU & cmake GPU detection
authorSzilard Pall <pszilard@cbr.su.se>
Mon, 12 Nov 2012 02:55:17 +0000 (03:55 +0100)
committerGerrit Code Review <gerrit@gerrit.gromacs.org>
Thu, 29 Nov 2012 01:34:59 +0000 (02:34 +0100)
Implemented detection of NVIDIA GPUs in CMake using:
 - output of nvidia-smi (if available Linux/Mac/Win);
 - presence and content of of /proc/driver/nvidia/gpus/*/information
   (Linux)
 - output of lspci (Linux)
Although the current implementation is not able to decide whether a GPU
is compatible with GROMACS, the build system is now able to hint the
user that there are potentially useful GPU compute resources.

Additionally, if GMX_GPU is not set explicitly by the user, its value
is considered to be and implicit "auto" with OFF as default. In this
case CUDA detection will be attempted and if successful, GMX_GPU is
set to "ON", otherwise kept "OFF".
If CUDA is not found and the user requested GPU acceleration, an
immediate fatal error is issued. If the user did not set GMX_GPU, a
non-fatal warning is issued at the end of the configuration with
additional information if GPUs were detected.

Fixes #1018

Change-Id: Iffa9ed343bed4278cffba5e2eb9f8b81f590b31d

CMakeLists.txt
cmake/gmxDetectGpu.cmake [new file with mode: 0644]
cmake/gmxManageGPU.cmake [new file with mode: 0644]

index e2aea85f4ef38e70a29d9b644f31bc0e044556e5..ff746e1bec5ba0b4c17239f1ed2a0e2c28b9a594 100644 (file)
@@ -108,10 +108,14 @@ endif()
 ########################################################################
 # User input options - enable C++ - before any CXX flags are changed   #
 ########################################################################
-option(GMX_GPU  "Enable GPU acceleration" ON)
+
+# decide on GPU settings based on user-settings and GPU/CUDA detection
+include(gmxManageGPU)
+
 option(GMX_OPENMM "Accelerated execution on GPUs through the OpenMM library (rerun cmake after changing to see relevant options)" OFF)
 option(GMX_FORCE_CXX "Enable C++ compilation even if not necessary" OFF)
 mark_as_advanced(GMX_FORCE_CXX)
+
 if(GMX_GPU OR GMX_OPENMM OR GMX_FORCE_CXX)
     enable_language(CXX)
 endif()
@@ -534,66 +538,11 @@ if(GMX_OPENMM)
     find_package(OpenMM) 
 endif(GMX_OPENMM)
 
-
 if(GMX_GPU)
-    if(GMX_DOUBLE)
-        message(WARNING "GPU acceleration is not available in double precision, disabled!")
-        set(GMX_GPU OFF CACHE BOOL "Enable GPU acceleration" FORCE)
-    endif()
+    # now that we have detected the dependencies, do the second configure pass
+    gmx_gpu_setup()
 endif(GMX_GPU)
 
-if(GMX_GPU)
-
-    # We support CUDA >=v3.2 on *nix, but <= v4.1 doesn't work with MSVC
-    if(MSVC)
-        find_package(CUDA 4.1)
-    else()
-        find_package(CUDA 3.2)
-    endif()
-
-    if (NOT EXISTS ${CUDA_TOOLKIT_ROOT_DIR})
-        message(FATAL_ERROR "
-    mdrun supports native GPU acceleration on NVIDIA hardware with compute
-    capability >=2.0. This requires the NVIDIA CUDA library, which was not
-    found; the location can be hinted by setting CUDA_TOOLKIT_ROOT_DIR as
-    a CMake option (It does not work as an environment variable).
-    The typical location would be /usr/local/cuda.
-
-    CPU or GPU acceleration can be selected at runtime, but if you are
-    sure you can not make use of GPU acceleration, disable it by setting
-    the CMake variable GMX_GPU=OFF.")
-    endif()
-
-    if(NOT GMX_OPENMP)
-        message(WARNING "
-    In order to use GPU acceleration efficiently, mdrun requires OpenMP multithreading.
-    Without OpenMP only a single CPU core per GPU can be used which is suboptimal.
-    Note that with MPI multiple processes can be forced to use a single GPU, but this
-    typically inefficient. Note that you need to set both C and C++ compilers that
-    support OpenMP (CC and CXX environment variables, respectively) when using GPUs.")
-    endif()
-
-    include(gmxManageNvccConfig)
-
-    # Check whether we can use atomic operations needed for polling wait for GPU
-    # (to avoid the cudaStreamSynchronize + ECC bug).
-    # With thread-MPI testing atomics has already been carried out, but without
-    # thread-MPI we need to invoke the atomics test independently.
-    if (NOT GMX_THREAD_MPI)
-        set(TEST_TMPI_ATOMICS_ONLY ON CACHE INTERNAL
-            "Test only the atomic operations of thread-MPI.")
-        include(ThreadMPI)
-    endif()
-
-    # Version info (semicolon used as line separator) for nvcc.
-    get_nvcc_version_info()
-
-endif()
-# Annoyingly enough, FindCUDA leaves a few variables behind as non-advanced.
-# We need to mark these advanced outside the conditional, otherwise, if the user
-# tuns GMX_GPU=OFF after a failed cmake pass, these variables will be left behind.
-mark_as_advanced(CUDA_BUILD_CUBIN CUDA_BUILD_EMULATION CUDA_SDK_ROOT_DIR CUDA_VERBOSE_BUILD)
-
 if(APPLE)
    find_library(ACCELERATE_FRAMEWORK Accelerate)
    list(APPEND GMX_EXTRA_LIBRARIES ${ACCELERATE_FRAMEWORK})
@@ -1159,6 +1108,15 @@ add_subdirectory(include)
 add_subdirectory(src)
 add_subdirectory(scripts)
 
+# issue GPU acceleration/CUDA-related warning or error if GMX_GPU was set
+# Auto and NVIDIA GPUs were detected.
+if (CUDA_NOTFOUND_AUTO AND NOT GMX_GPU_DETECTION_DONE)
+    message(WARNING "${CUDA_NOTFOUND_MESSAGE}")
+    unset(CUDA_NOTFOUND_AUTO)
+    unset(CUDA_NOTFOUND_MESSAGE)
+endif()
+set(GMX_GPU_DETECTION_DONE TRUE CACHE INTERNAL "Whether GPU detection has already been done")
+
 #######################
 ## uninstall target
 #######################
@@ -1172,7 +1130,6 @@ ADD_CUSTOM_TARGET(uninstall
                   "${CMAKE_CURRENT_BINARY_DIR}/cmake/cmake_uninstall.cmake")
 ###########################
 
-
 ########################################################################
 # Tests                                                                #
 ########################################################################
diff --git a/cmake/gmxDetectGpu.cmake b/cmake/gmxDetectGpu.cmake
new file mode 100644 (file)
index 0000000..3af484c
--- /dev/null
@@ -0,0 +1,187 @@
+# The gmx_detect_gpu() macro aims to detect GPUs available in the build machine
+# and provide the number, names, and compute-capabilities of these devices.
+#
+# The current version is limited to checking the availability of NVIDIA GPUs
+# without compute-capability information.
+#
+# The current detection relies on the following checks in the order of listing:
+# - output of nvidia-smi (if available);
+# - presence and content of of /proc/driver/nvidia/gpus/*/information (Linux)
+# - output of lspci (Linux)
+#
+# If any of the checks succeeds in finding devices, consecutive checks will not
+# be carried out. Additionally, when lspci is used and a device with unknown
+# PCI ID is encountered, lspci tries to check the online PCI ID database. If
+# this is not possible or the device is simply not recognized, no device names
+# will be available.
+#
+# The following advanced variables are defined:
+# - GMX_DETECT_GPU_AVAILABLE - TRUE if any GPUs were detected, otherwise FALSE
+# - GMX_DETECT_GPU_COUNT     - # of GPUs detected
+# - GMX_DETECT_GPU_INFO      - list of information strings of the detected GPUs
+#
+# NOTE: The proper solution is to detect hardware compatible with the native
+# GPU acceleration. However, this requires checking the compute capability
+# of the device which is not possible with the current checks and requires
+# interfacing with the CUDA driver API.
+#
+
+# check whether the number of GPUs machetes the number of elements in the GPU info list
+macro(check_num_gpu_info NGPU GPU_INFO)
+    list(LENGTH ${GPU_INFO} _len)
+    if (NOT NGPU EQUAL _len)
+        list(APPEND ${GMX_DETECT_GPU_INFO} "NOTE: information about some GPU(s) missing!")
+    endif()
+endmacro()
+
+macro(gmx_detect_gpu)
+
+    if (NOT DEFINED GMX_DETECT_GPU_COUNT OR NOT DEFINED GMX_DETECT_GPU_INFO)
+
+        set(GMX_DETECT_GPU_COUNT 0)
+        set(GMX_DETECT_GPU_INFO  "")
+
+        message(STATUS "Looking for NVIDIA GPUs present in the system")
+
+        # nvidia-smi-based detection.
+        # Requires the nvidia-smi tool to be installed and available in the path
+        # or in one of the default search locations
+        if (NOT DEFINED GMX_DETECT_GPU_COUNT_NVIDIA_SMI)
+            # try to find the nvidia-smi binary
+            # TODO add location hints
+            find_program(_nvidia_smi "nvidia-smi")
+            if (_nvidia_smi)
+                set(GMX_DETECT_GPU_COUNT_NVIDIA_SMI 0)
+                # execute nvidia-smi -L to get a short list of GPUs available
+                exec_program(${_nvidia_smi_path} ARGS -L
+                    OUTPUT_VARIABLE _nvidia_smi_out
+                    RETURN_VALUE    _nvidia_smi_ret)
+                # process the stdout of nvidia-smi
+                if (_nvidia_smi_ret EQUAL 0)
+                    # convert string with newlines to list of strings
+                    string(REGEX REPLACE "\n" ";" _nvidia_smi_out "${_nvidia_smi_out}")
+                    foreach(_line ${_nvidia_smi_out})
+                        if (_line MATCHES "^GPU [0-9]+:")
+                            math(EXPR GMX_DETECT_GPU_COUNT_NVIDIA_SMI "${GMX_DETECT_GPU_COUNT_NVIDIA_SMI}+1")
+                            # the UUID is not very useful for the user, remove it
+                            string(REGEX REPLACE " \\(UUID:.*\\)" "" _gpu_info "${_line}")
+                            if (NOT _gpu_info STREQUAL "")
+                                list(APPEND GMX_DETECT_GPU_INFO "${_gpu_info}")
+                            endif()
+                        endif()
+                    endforeach()
+
+                    check_num_gpu_info(${GMX_DETECT_GPU_COUNT_NVIDIA_SMI} GMX_DETECT_GPU_INFO)
+                    set(GMX_DETECT_GPU_COUNT ${GMX_DETECT_GPU_COUNT_NVIDIA_SMI})
+                endif()
+            endif()
+
+            unset(_nvidia_smi CACHE)
+            unset(_nvidia_smi_ret)
+            unset(_nvidia_smi_out)
+            unset(_gpu_name)
+            unset(_line)
+        endif()
+
+        if (UNIX AND NOT (APPLE OR CYGWIN))
+            # /proc/driver/nvidia/gpus/*/information-based detection.
+            # Requires the NVDIA closed source driver to be installed and loaded
+            if (NOT DEFINED GMX_DETECT_GPU_COUNT_PROC AND GMX_DETECT_GPU_COUNT EQUAL 0)
+                set(GMX_DETECT_GPU_COUNT_PROC 0)
+                file(GLOB _proc_nv_gpu_info "/proc/driver/nvidia/gpus/*/information")
+                foreach (_file ${_proc_nv_gpu_info})
+                    math(EXPR GMX_DETECT_GPU_COUNT_PROC "${GMX_DETECT_GPU_COUNT_PROC}+1")
+                    # assemble information strings similar to the nvidia-smi output
+                    # GPU ID = directory name on /proc/driver/nvidia/gpus/
+                    string(REGEX REPLACE "/proc/driver/nvidia/gpus.*([0-9]+).*information" "\\1" _gpu_id ${_file})
+                    # GPU name
+                    file(STRINGS ${_file} _gpu_name LIMIT_COUNT 1 REGEX "^Model:.*" NO_HEX_CONVERSION)
+                    string(REGEX REPLACE "^Model:[ \t]*(.*)" "\\1" _gpu_name "${_gpu_name}")
+                    if (NOT _gpu_id STREQUAL "" AND NOT _gpu_name STREQUAL "")
+                        list(APPEND GMX_DETECT_GPU_INFO "GPU ${_gpu_id}: ${_gpu_name}")
+                    endif()
+                endforeach()
+
+                check_num_gpu_info(${GMX_DETECT_GPU_COUNT_PROC} GMX_DETECT_GPU_INFO)
+                set(GMX_DETECT_GPU_COUNT ${GMX_DETECT_GPU_COUNT_PROC})
+
+                unset(_proc_nv_gpu_info)
+                unset(_gpu_name)
+                unset(_gpu_id)
+                unset(_file)
+            endif()
+
+            # lspci-based detection (does not provide GPU information).
+            # Requires lspci and for GPU names to be fetched from the central
+            # PCI ID db if not available locally.
+            if (NOT DEFINED GMX_DETECT_GPU_COUNT_LSPCI AND GMX_DETECT_GPU_COUNT EQUAL 0)
+                set(GMX_DETECT_GPU_COUNT_LSPCI 0)
+                exec_program(lspci ARGS -q
+                    OUTPUT_VARIABLE _lspci_out
+                    RETURN_VALUE    _lspci_ret)
+                # prehaps -q is not supported, try running without
+                if (NOT RETURN_VALUE EQUAL 0)
+                    exec_program(lspci
+                        OUTPUT_VARIABLE _lspci_out
+                        RETURN_VALUE    _lspci_ret)
+                endif()
+                if (_lspci_ret EQUAL 0)
+                    # convert string with newlines to list of strings
+                    STRING(REGEX REPLACE ";" "\\\\;" _lspci_out "${_lspci_out}")
+                    string(REGEX REPLACE "\n" ";" _lspci_out "${_lspci_out}")
+                    foreach(_line ${_lspci_out})
+                        string(TOUPPER "${_line}" _line_upper)
+                        if (_line_upper MATCHES ".*VGA.*NVIDIA.*" OR _line_upper MATCHES ".*3D.*NVIDIA.*")
+                            math(EXPR GMX_DETECT_GPU_COUNT_LSPCI "${GMX_DETECT_GPU_COUNT_LSPCI}+1")
+                            # Try to parse out the device name which should be
+                            # included in the lspci -q output between []-s
+                            string(REGEX REPLACE ".*\\[(.*)\\].*" "\\1" _gpu_name "${_line}")
+                            if (NOT _gpu_name EQUAL "")
+                                list(APPEND GMX_DETECT_GPU_INFO "${_gpu_name}")
+                            endif()
+                        endif()
+                    endforeach()
+
+                    check_num_gpu_info(${GMX_DETECT_GPU_COUNT_LSPCI} GMX_DETECT_GPU_INFO)
+                    set(GMX_DETECT_GPU_COUNT ${GMX_DETECT_GPU_COUNT_LSPCI})
+                endif()
+
+                unset(_lspci_ret)
+                unset(_lspci_out)
+                unset(_gpu_name)
+                unset(_line)
+                unset(_line_upper)
+            endif()
+        endif()
+
+        if (GMX_DETECT_GPU_COUNT GREATER 0)
+            set(GMX_DETECT_GPU_AVAILABLE YES)
+        else()
+            set(GMX_DETECT_GPU_AVAILABLE NO)
+        endif()
+        set(GMX_DETECT_GPU_AVAILABLE YES CACHE BOOL "Whether any NVIDIA GPU was detected" FORCE)
+
+        set(GMX_DETECT_GPU_COUNT ${GMX_DETECT_GPU_COUNT}
+            CACHE STRING "Number of NVIDIA GPUs detected")
+        set(GMX_DETECT_GPU_INFO ${GMX_DETECT_GPU_INFO}
+            CACHE STRING "basic information on the detected NVIDIA GPUs")
+
+        set(GMX_DETECT_GPU_COUNT_NVIDIA_SMI ${GMX_DETECT_GPU_COUNT_NVIDIA_SMI}
+            CACHE INTERNAL "Number of NVIDIA GPUs detected using nvidia-smi")
+        set(GMX_DETECT_GPU_COUNT_PROC ${GMX_DETECT_GPU_COUNT_PROC}
+            CACHE INTERNAL "Number of NVIDIA GPUs detected in /proc/driver/nvidia/gpus")
+        set(GMX_DETECT_GPU_COUNT_LSPCI ${GMX_DETECT_GPU_COUNT_LSPCI}
+            CACHE INTERNAL "Number of NVIDIA GPUs detected using lspci")
+
+        mark_as_advanced(GMX_DETECT_GPU_AVAILABLE
+                         GMX_DETECT_GPU_COUNT
+                         GMX_DETECT_GPU_INFO)
+
+        if (GMX_DETECT_GPU_AVAILABLE)
+            message(STATUS "Number of NVIDIA GPUs detected: ${GMX_DETECT_GPU_COUNT} ")
+        else()
+            message(STATUS "Could not detect NVIDIA GPUs")
+        endif()
+
+    endif (NOT DEFINED GMX_DETECT_GPU_COUNT OR NOT DEFINED GMX_DETECT_GPU_INFO)
+endmacro(gmx_detect_gpu)
diff --git a/cmake/gmxManageGPU.cmake b/cmake/gmxManageGPU.cmake
new file mode 100644 (file)
index 0000000..382a969
--- /dev/null
@@ -0,0 +1,130 @@
+# If the user did not set GMX_GPU we'll consider this option to be
+# in "auto" mode meaning that we will:
+# - search for CUDA and set GMX_GPU=ON we find it
+# - check whether GPUs are present
+# - if CUDA is not found but GPUs were detected issue a warning
+if (NOT DEFINED GMX_GPU)
+    set(GMX_GPU_AUTO TRUE CACHE INTERNAL "GPU acceleration will be selected automatically")
+endif()
+option(GMX_GPU "Enable GPU acceleration" OFF)
+
+if(GMX_GPU AND GMX_DOUBLE)
+    message(FATAL_ERROR "GPU acceleration is not available in double precision!")
+endif()
+if(GMX_GPU_AUTO AND GMX_DOUBLE)
+    message(WARNING "GPU acceleration is not available in double precision, disabled!")
+    set_property(CACHE GMX_GPU PROPERTY VALUE OFF)
+    set_property(CACHE GMX_GPU_AUTO PROPERTY VALUE OFF)
+endif()
+
+# detect GPUs in the build host machine
+if (GMX_GPU OR GMX_GPU_AUTO AND NOT GMX_GPU_DETECTION_DONE)
+    include(gmxDetectGpu)
+    gmx_detect_gpu()
+endif()
+
+# Depending on the current vale of GMX_GPU and GMX_GPU_AUTO:
+# - OFF, FALSE: Will skip this detection.
+# - OFF, TRUE : Will keep GMX_GPU=OFF if no CUDA is detected, but will assemble
+#               a warning message which will be issued at the end of the
+#               configuration if GPU(s) were found in the build system.
+# - ON , FALSE: The user requested GPU builds, will require CUDA and will fail
+#               if it is not available.
+# - ON , TRUE : Can't happen (GMX_GPU=ON can only be user-set at this point)
+if(GMX_GPU OR GMX_GPU_AUTO AND NOT GMX_GPU_DETECTION_DONE)
+    # We support CUDA >=v3.2 on *nix, but <= v4.1 doesn't work with MSVC
+    if(MSVC)
+        find_package(CUDA 4.1)
+    else()
+        find_package(CUDA 3.2)
+    endif()
+
+    if (EXISTS ${CUDA_TOOLKIT_ROOT_DIR})
+        set(CUDA_FOUND TRUE CACHE INTERNAL "Whether the CUDA toolkit was found" FORCE)
+    else()
+        set(CUDA_FOUND FALSE CACHE INTERNAL "Whether the CUDA toolkit was found" FORCE)
+    endif()
+
+    # assemble warning/error message
+    if (GMX_DETECT_GPU_AVAILABLE)
+        set(_msg "
+    ${GMX_DETECT_GPU_COUNT} NVIDIA GPU(s) found in the system")
+
+        # append GPU names
+        if (NOT GMX_DETECT_GPU_INFO STREQUAL "")
+            set(_msg "${_msg}:")
+            foreach(gpu ${GMX_DETECT_GPU_INFO})
+                set(_msg "${_msg}
+                ${gpu}")
+            endforeach()
+        endif()
+
+        # TODO remove the second part of the message when we'll have compute
+        # capability information from the detection.
+        set(_msg "${_msg}
+    Compute capability information not available, consult the NVIDIA website:
+    https://developer.nvidia.com/cuda-gpus
+            ")
+    endif()
+
+        set(CUDA_NOTFOUND_MESSAGE "
+    mdrun supports native GPU acceleration on NVIDIA hardware with compute
+    capability >=2.0. This requires the NVIDIA CUDA library, which was not
+    found; the location can be hinted by setting CUDA_TOOLKIT_ROOT_DIR as
+    a CMake option (It does not work as an environment variable).
+    The typical location would be /usr/local/cuda[-version].
+    Note that CPU or GPU acceleration can be selected at runtime!
+
+    ${_msg}")
+        unset(_msg)
+
+    if (NOT CUDA_FOUND)
+        if (GMX_GPU_AUTO)
+            # Disable GPU acceleration in auto mode
+            message(STATUS "Disabling native GPU acceleration")
+            set_property(CACHE GMX_GPU PROPERTY VALUE OFF)
+            set(CUDA_NOTFOUND_AUTO ON)
+        else ()
+            # the user requested CUDA, but it wasn't found
+            message(FATAL_ERROR "${CUDA_NOTFOUND_MESSAGE}")
+        endif()
+    else()
+        if (GMX_GPU_AUTO)
+            message(STATUS "Enabling native GPU acceleration")
+            set_property(CACHE GMX_GPU PROPERTY VALUE ON)
+        endif()
+    endif() # NOT CUDA_FOUND
+endif()
+# Annoyingly enough, FindCUDA leaves a few variables behind as non-advanced.
+# We need to mark these advanced outside the conditional, otherwise, if the
+# user turns GMX_GPU=OFF after a failed cmake pass, these variables will be
+# left behind in the cache.
+mark_as_advanced(CUDA_BUILD_CUBIN CUDA_BUILD_EMULATION CUDA_SDK_ROOT_DIR CUDA_VERBOSE_BUILD)
+
+macro(gmx_gpu_setup)
+    # set up nvcc options
+    include(gmxManageNvccConfig)
+
+    # Version info (semicolon used as line separator) for nvcc.
+    get_nvcc_version_info()
+
+    # Check whether we can use atomic operations needed for polling wait for GPU
+    # (to avoid the cudaStreamSynchronize + ECC bug).
+    # With thread-MPI testing atomics has already been carried out, but without
+    # thread-MPI we need to invoke the atomics test independently.
+    if (NOT GMX_THREAD_MPI)
+        set(TEST_TMPI_ATOMICS_ONLY ON CACHE INTERNAL
+            "Test only the atomic operations of thread-MPI.")
+        include(ThreadMPI)
+    endif()
+
+    # no OpenMP is no good!
+    if(NOT GMX_OPENMP)
+        message(WARNING "
+    To use GPU acceleration efficiently, mdrun requires OpenMP multi-threading.
+    With no OpenMP a single CPU core can be used with a GPU which is not optimal.
+    Note that with MPI multiple processes can be forced to use a single GPU, but this
+    typically inefficient. Note that you need to set both C and C++ compilers that
+    support OpenMP (CC and CXX environment variables, respectively) when using GPUs.")
+    endif()
+endmacro()