Repository: cupy/cupy

Issue:
Bug: HIP builds implicitly assume CUDA is present
In https://github.com/cupy/cupy/issues/4218#issuecomment-737071882 @ipe-zhangyz reported that `import cupy` still fails on both v8.2 and master despite the CUB issue was already fixed in #4327 (a backport of the support added in #4027).

It turns out that it has to do with the build script. We borrow most of the build infrastructure from CUDA, but one of the assumptions is that CUDA Toolkit is present, which both my develop env and the pfnCI do have (but @ipe-zhangyz's does not), so we failed to detect it until someone who has a "clean" environment came and tested it.


Discussion:

Resolving patch:
diff --git a/cupy_setup_build.py b/cupy_setup_build.py
--- a/cupy_setup_build.py
+++ b/cupy_setup_build.py
@@ -96,6 +96,7 @@
     # to leak into all these modules even if unused. It's easier for all of
     # them to link to the same set of shared libraries.
     MODULES.append({
+        # TODO(leofang): call this "rocm" or "hip" to avoid confusion?
         'name': 'cuda',
         'file': cuda_files + [
             'cupy.cuda.nvtx',
@@ -119,6 +120,8 @@
             'rocblas',
             'rocsolver',
         ],
+        'check_method': build.check_hip_version,
+        'version_method': build.get_hip_version,
     })
 else:
     MODULES.append({
@@ -393,6 +396,7 @@ def preconfigure_modules(compiler, settings):
     """
 
     nvcc_path = build.get_nvcc_path()
+    hipcc_path = build.get_hipcc_path()
     summary = [
         '',
         '************************************************************',
@@ -404,12 +408,14 @@ def preconfigure_modules(compiler, settings):
         '  Library directories: {}'.format(str(settings['library_dirs'])),
         '  nvcc command       : {}'.format(
             nvcc_path if nvcc_path else '(not found)'),
+        '  hipcc command      : {}'.format(
+            hipcc_path if hipcc_path else '(not found)'),
         '',
         'Environment Variables:',
     ]
 
     for key in ['CFLAGS', 'LDFLAGS', 'LIBRARY_PATH',
-                'CUDA_PATH', 'NVTOOLSEXT_PATH', 'NVCC',
+                'CUDA_PATH', 'NVTOOLSEXT_PATH', 'NVCC', 'HIPCC',
                 'ROCM_HOME']:
         summary += ['  {:<16}: {}'.format(key, os.environ.get(key, '(none)'))]
 
@@ -464,9 +470,11 @@ def preconfigure_modules(compiler, settings):
             # Fail on per-library condition check (version requirements etc.)
             installed = True
             errmsg = ['The library is installed but not supported.']
-        elif module['name'] in ('thrust', 'cub') and nvcc_path is None:
+        elif (module['name'] in ('thrust', 'cub')
+                and (nvcc_path is None and hipcc_path is None)):
             installed = True
-            errmsg = ['nvcc command could not be found in PATH.',
+            cmd = 'nvcc' if not use_hip else 'hipcc'
+            errmsg = ['{} command could not be found in PATH.'.format(cmd),
                       'Check your PATH environment variable.']
         else:
             installed = True
@@ -487,8 +495,10 @@ def preconfigure_modules(compiler, settings):
             # Skip checking other modules when CUDA is unavailable.
             if module['name'] == 'cuda':
                 break
+
     # Get a list of the CC of the devices connected to this node
-    build.check_compute_capabilities(compiler, settings)
+    if not use_hip:
+        build.check_compute_capabilities(compiler, settings)
 
     if len(ret) != len(MODULES):
         if 'cuda' in ret:
@@ -944,7 +954,7 @@ def _compile_unix_nvcc(self,
                            obj, src, ext, cc_args, extra_postargs, pp_opts):
         # For CUDA C source files, compile them with NVCC.
         nvcc_path = build.get_nvcc_path()
-        base_opts = build.get_compiler_base_options()
+        base_opts = build.get_compiler_base_options(nvcc_path)
         compiler_so = nvcc_path
 
         cuda_version = build.get_cuda_version()
@@ -961,7 +971,7 @@ def _compile_unix_hipcc(self,
                             obj, src, ext, cc_args, extra_postargs, pp_opts):
         # For CUDA C source files, compile them with HIPCC.
         rocm_path = build.get_hipcc_path()
-        base_opts = build.get_compiler_base_options()
+        base_opts = build.get_compiler_base_options(rocm_path)
         compiler_so = rocm_path
         postargs = ['-O2', '-fPIC', '--include', 'hip_runtime.h']
         print('HIPCC options:', postargs)
@@ -1002,7 +1012,6 @@ def _compile_cu(self, sources, output_dir=None, macros=None,
                     include_dirs=None, debug=0, extra_preargs=None,
                     extra_postargs=None, depends=None):
         # Compile CUDA C files, mainly derived from UnixCCompiler._compile().
-
         macros, objects, extra_postargs, pp_opts, _build = \
             self._setup_compile(output_dir, macros, include_dirs, sources,
                                 depends, extra_postargs)
@@ -1028,6 +1037,9 @@ def _compile_cu(self, sources, output_dir=None, macros=None,
 
     def compile(self, sources, **kwargs):
         # Split CUDA C sources and others.
+        if use_hip:
+            raise RuntimeError('ROCm is not supported on Windows')
+
         cu_sources = []
         other_sources = []
         for source in sources:
@@ -1064,7 +1076,8 @@ class custom_build_ext(build_ext.build_ext):
     """Custom `build_ext` command to include CUDA C source files."""
 
     def run(self):
-        if build.get_nvcc_path() is not None:
+        if (build.get_nvcc_path() is not None
+                or build.get_hipcc_path() is not None):
             def wrap_new_compiler(func):
                 def _wrap_new_compiler(*args, **kwargs):
                     try:
diff --git a/install/build.py b/install/build.py
--- a/install/build.py
+++ b/install/build.py
@@ -18,6 +18,8 @@
 minimum_cudnn_version = 7000
 maximum_cudnn_version = 8099
 
+minimum_hip_version = 305  # for ROCm 3.5.0+
+
 _cuda_path = 'NOT_INITIALIZED'
 _rocm_path = 'NOT_INITIALIZED'
 _compiler_base_options = None
@@ -228,28 +230,28 @@ def _match_output_lines(output_lines, regexs):
     return None
 
 
-def get_compiler_base_options():
+def get_compiler_base_options(compiler_path):
     """Returns base options for nvcc compiler.
 
     """
     global _compiler_base_options
     if _compiler_base_options is None:
-        _compiler_base_options = _get_compiler_base_options()
+        _compiler_base_options = _get_compiler_base_options(compiler_path)
     return _compiler_base_options
 
 
-def _get_compiler_base_options():
+def _get_compiler_base_options(compiler_path):
     # Try compiling a dummy code.
     # If the compilation fails, try to parse the output of compilation
     # and try to compose base options according to it.
-    nvcc_path = get_nvcc_path()
+    # compiler_path is the path to nvcc (CUDA) or hipcc (ROCm/HIP)
     with _tempdir() as temp_dir:
         test_cu_path = os.path.join(temp_dir, 'test.cu')
         test_out_path = os.path.join(temp_dir, 'test.out')
         with open(test_cu_path, 'w') as f:
             f.write('int main() { return 0; }')
         proc = subprocess.Popen(
-            nvcc_path + ['-o', test_out_path, test_cu_path],
+            compiler_path + ['-o', test_out_path, test_cu_path],
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE)
         stdoutdata, stderrdata = proc.communicate()
@@ -278,6 +280,7 @@ def _get_compiler_base_options():
 
 
 _cuda_version = None
+_hip_version = None
 _thrust_version = None
 _cudnn_version = None
 _nccl_version = None
@@ -315,10 +318,6 @@ def check_cuda_version(compiler, settings):
     return True
 
 
-def _format_cuda_version(version):
-    return str(version)
-
-
 def get_cuda_version(formatted=False):
     """Return CUDA Toolkit version cached in check_cuda_version()."""
     global _cuda_version
@@ -326,10 +325,48 @@ def get_cuda_version(formatted=False):
         msg = 'check_cuda_version() must be called first.'
         raise RuntimeError(msg)
     if formatted:
-        return _format_cuda_version(_cuda_version)
+        return str(_cuda_version)
     return _cuda_version
 
 
+def check_hip_version(compiler, settings):
+    global _hip_version
+    try:
+        out = build_and_run(compiler, '''
+        #include <hip/hip_version.h>
+        #include <stdio.h>
+        int main() {
+          printf("%d", HIP_VERSION);
+          return 0;
+        }
+        ''', include_dirs=settings['include_dirs'])
+
+    except Exception as e:
+        utils.print_warning('Cannot check HIP version', str(e))
+        return False
+
+    _hip_version = int(out)
+
+    if _hip_version < minimum_hip_version:
+        utils.print_warning(
+            'ROCm/HIP version is too old: %d' % _hip_version,
+            'ROCm 3.5.0 or newer is required')
+        return False
+
+    return True
+
+
+def get_hip_version(formatted=False):
+    """Return ROCm version cached in check_hip_version()."""
+    global _hip_version
+    if _hip_version is None:
+        msg = 'check_hip_version() must be called first.'
+        raise RuntimeError(msg)
+    if formatted:
+        return str(_hip_version)
+    return _hip_version
+
+
 def check_compute_capabilities(compiler, settings):
     """Return compute capabilities of the installed devices."""
     global _compute_capabilities
@@ -417,11 +454,11 @@ def check_cudnn_version(compiler, settings):
     _cudnn_version = int(out)
 
     if not minimum_cudnn_version <= _cudnn_version <= maximum_cudnn_version:
-        min_major = _format_cuda_version(minimum_cudnn_version)
-        max_major = _format_cuda_version(maximum_cudnn_version)
+        min_major = str(minimum_cudnn_version)
+        max_major = str(maximum_cudnn_version)
         utils.print_warning(
             'Unsupported cuDNN version: {}'.format(
-                _format_cuda_version(_cudnn_version)),
+                str(_cudnn_version)),
             'cuDNN v{}= and <=v{} is required'.format(min_major, max_major))
         return False
 
@@ -435,7 +472,7 @@ def get_cudnn_version(formatted=False):
         msg = 'check_cudnn_version() must be called first.'
         raise RuntimeError(msg)
     if formatted:
-        return _format_cuda_version(_cudnn_version)
+        return str(_cudnn_version)
     return _cudnn_version
 
 
@@ -486,7 +523,7 @@ def get_nccl_version(formatted=False):
     if formatted:
         if _nccl_version == 0:
             return '1.x'
-        return _format_cuda_version(_nccl_version)
+        return str(_nccl_version)
     return _nccl_version
 
 


Test patch: