diff --git a/.github/scripts/build-rocm.sh b/.github/scripts/build-rocm.sh index 7f971824a..b6eff1250 100644 --- a/.github/scripts/build-rocm.sh +++ b/.github/scripts/build-rocm.sh @@ -1,26 +1,24 @@ #!/bin/bash set -xeuo pipefail -: "${RUNNER_OS:?RUNNER_OS must be set (Linux/Windows)}" -: "${ROCM_VERSION:?ROCM_VERSION must be set}" +if [[ "${RUNNER_OS:-}" != "Linux" && "${RUNNER_OS:-}" != "Windows" ]]; then + echo "Invalid RUNNER_OS '${RUNNER_OS:-}'; expected Linux or Windows." >&2 + exit 1 +fi -rocm_version_at_least() { - local required_version="$1" - local current_major current_minor required_major required_minor +if [[ ! "${ROCM_VERSION:-}" =~ ^([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then + echo "Invalid ROCM_VERSION '${ROCM_VERSION:-}'; expected a dotted ROCm release such as 7.14." >&2 + exit 1 +fi - IFS=. read -r current_major current_minor _ <<< "${ROCM_VERSION}" - IFS=. read -r required_major required_minor _ <<< "${required_version}" +rocm_version_major="$((10#${BASH_REMATCH[1]}))" +rocm_version_minor="$((10#${BASH_REMATCH[2]}))" - if ((current_major > required_major)); then - return 0 - fi - if ((current_major < required_major)); then - return 1 - fi - if ((current_minor >= required_minor)); then - return 0 - fi - return 1 +rocm_version_at_least() { + local required_major required_minor + IFS=. read -r required_major required_minor <<< "$1" + ((rocm_version_major > required_major || + (rocm_version_major == required_major && rocm_version_minor >= required_minor))) } bnb_rocm_arch="gfx90a;gfx942;gfx1100;gfx1101;gfx1102;gfx1103" @@ -94,4 +92,17 @@ fi output_dir="output/${RUNNER_OS}/X64" mkdir -p "${output_dir}" -(shopt -s nullglob && cp bitsandbytes/*.{so,dylib,dll} "${output_dir}") +libraries=() +shopt -s nullglob +for extension in so dylib dll; do + for library in bitsandbytes/libbitsandbytes_rocm*."${extension}"; do + libraries+=("${library}") + done +done +shopt -u nullglob + +if [ "${#libraries[@]}" -eq 0 ]; then + echo "No ROCm-backend library was built (expected bitsandbytes/libbitsandbytes_rocm*.{so,dylib,dll})." >&2 + exit 1 +fi +cp "${libraries[@]}" "${output_dir}/" diff --git a/CMakeLists.txt b/CMakeLists.txt index 950f8d213..9a46467fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,17 +10,22 @@ # Separate by semicolons, i.e. `-DCOMPUTE_CAPABILITY=89;90;100;120` # Check your compute capability here: https://developer.nvidia.com/cuda-gpus # - PTXAS_VERBOSE: Pass the `-v` option to the PTX Assembler -# - ROCM_VERSION: Override the ROCm version shortcode used in the output library name. -# Useful when PyTorch was built against a different ROCm version than the -# system install. For example, `-DROCM_VERSION=70` produces -# libbitsandbytes_rocm70.so even if the system has ROCm 7.2. +# - HIP_VERSION: Override the HIP version used in the ROCm-backend output library name. +# Accepts a dotted version or compact shortcode. For example, +# `-DHIP_VERSION=7.14` and `-DHIP_VERSION=714` both produce +# libbitsandbytes_rocm714.so. cmake_minimum_required(VERSION 3.22.1) # On Windows with HIP backend, auto-detect compilers from ROCM_PATH before project() if(WIN32 AND COMPUTE_BACKEND STREQUAL "hip") - if(DEFINED ENV{ROCM_PATH}) - file(TO_CMAKE_PATH "$ENV{ROCM_PATH}" ROCM_PATH) + if(NOT DEFINED ENV{ROCM_PATH} OR "$ENV{ROCM_PATH}" STREQUAL "") + message(FATAL_ERROR + "ROCM_PATH must be set for HIP builds on Windows. " + "After 'rocm-sdk init', set it from 'rocm-sdk path --root'. " + "PowerShell: $env:ROCM_PATH = (rocm-sdk path --root)" + ) endif() + file(TO_CMAKE_PATH "$ENV{ROCM_PATH}" ROCM_PATH) if(ROCM_PATH AND NOT DEFINED CMAKE_CXX_COMPILER) set(CMAKE_CXX_COMPILER "${ROCM_PATH}/lib/llvm/bin/clang++.exe") endif() @@ -270,6 +275,11 @@ elseif(BUILD_HIP) set(CMAKE_HIP_ARCHITECTURES "gfx90a;gfx942;gfx1100;gfx1101;gfx1102;gfx1103;gfx1150;gfx1151;gfx1152;gfx1153;gfx1200;gfx1201") endif() + find_program(HIPCONFIG_EXECUTABLE hipconfig) + if(NOT HIPCONFIG_EXECUTABLE) + message(FATAL_ERROR "hipconfig was not found on PATH; a ROCm/HIP SDK is required for HIP builds") + endif() + enable_language(HIP) message(STATUS "HIP Compiler: ${CMAKE_HIP_COMPILER}") message(STATUS "HIP Targets: ${CMAKE_HIP_ARCHITECTURES}") @@ -282,20 +292,32 @@ elseif(BUILD_HIP) string(APPEND BNB_OUTPUT_NAME "_rocm") - # get hip version - execute_process(COMMAND hipconfig --version OUTPUT_VARIABLE HIP_CONFIG_VERSION) - string(REGEX MATCH "[0-9]+\\.[0-9]+" HIP_VERSION "${HIP_CONFIG_VERSION}") - string(REPLACE "." "" HIP_VERSION_SHORT "${HIP_VERSION}") + execute_process( + COMMAND "${HIPCONFIG_EXECUTABLE}" --version + OUTPUT_VARIABLE HIPCONFIG_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY + ) - # Expose a cache variable that the user can set to override the ROCm version in the library name - set(ROCM_VERSION "${HIP_VERSION_SHORT}" CACHE STRING "Expected ROCm Version Shortcode") + if(NOT HIPCONFIG_VERSION MATCHES "([0-9]+)\\.([0-9]+)") + message(FATAL_ERROR "Could not parse HIP version: ${HIPCONFIG_VERSION}") + endif() + set(DETECTED_HIP_VERSION_TAG "${CMAKE_MATCH_1}${CMAKE_MATCH_2}") - message(STATUS "ROCm Version: ${HIP_VERSION_SHORT} (from hipconfig)") - if(NOT ROCM_VERSION STREQUAL "${HIP_VERSION_SHORT}") - message(WARNING "Overriding ROCm version in library name: ${HIP_VERSION_SHORT} -> ${ROCM_VERSION}") + set(HIP_VERSION "${DETECTED_HIP_VERSION_TAG}" CACHE STRING + "HIP version used in the ROCm-backend library name" + ) + if(HIP_VERSION MATCHES "^([0-9]+)\\.([0-9]+)(\\.[0-9]+)?$") + set(HIP_VERSION_TAG "${CMAKE_MATCH_1}${CMAKE_MATCH_2}") + elseif(HIP_VERSION MATCHES "^[0-9]+$") + set(HIP_VERSION_TAG "${HIP_VERSION}") + else() + message(FATAL_ERROR "HIP_VERSION must be dotted or compact, such as 7.14 or 714") endif() - string(APPEND BNB_OUTPUT_NAME "${ROCM_VERSION}") + message(STATUS "HIP ${HIPCONFIG_VERSION}; library suffix: rocm${HIP_VERSION_TAG}") + + string(APPEND BNB_OUTPUT_NAME "${HIP_VERSION_TAG}") add_compile_definitions(__HIP_PLATFORM_AMD__) add_compile_definitions(__HIP_PLATFORM_HCC__) add_compile_definitions(BUILD_HIP) @@ -416,11 +438,16 @@ if(BUILD_CUDA) ) endif() if(BUILD_HIP) - # Determine ROCM_PATH from environment variable, fallback to /opt/rocm on Linux - if(DEFINED ENV{ROCM_PATH}) - file(TO_CMAKE_PATH "$ENV{ROCM_PATH}" ROCM_PATH) + # Determine ROCM_PATH from an existing CMake value or environment variable. + if(ROCM_PATH) + file(TO_CMAKE_PATH "${ROCM_PATH}" ROCM_PATH) + elseif(DEFINED ENV{ROCM_PATH} AND NOT "$ENV{ROCM_PATH}" STREQUAL "") + file(TO_CMAKE_PATH "$ENV{ROCM_PATH}" ROCM_PATH) + elseif(WIN32) + message(FATAL_ERROR "ROCM_PATH must be set for HIP builds on Windows") else() - set(ROCM_PATH /opt/rocm) + message(WARNING "ROCM_PATH is not set; falling back to /opt/rocm") + set(ROCM_PATH /opt/rocm) endif() list(APPEND CMAKE_PREFIX_PATH ${ROCM_PATH}) macro(find_package_and_print_version PACKAGE_NAME) @@ -452,12 +479,8 @@ if(BUILD_HIP) set_source_files_properties(${GPU_FILES} PROPERTIES LANGUAGE HIP) set_target_properties(bitsandbytes PROPERTIES LINKER_LANGUAGE CXX) - if(HIP_VERSION VERSION_LESS "6.1") - target_compile_definitions(bitsandbytes PUBLIC NO_HIPBLASLT) - else() - find_package(hipblaslt) - target_link_libraries(bitsandbytes PUBLIC roc::hipblaslt) - endif() + find_package(hipblaslt REQUIRED) + target_link_libraries(bitsandbytes PUBLIC roc::hipblaslt) endif() if(BUILD_XPU) set(SYCL_LINK_FLAGS "-fsycl;--offload-compress;-fsycl-targets=spir64_gen,spir64;-Xs;-device pvc,xe-lpg,ats-m150 -options ' -cl-intel-enable-auto-large-GRF-mode -cl-poison-unsupported-fp64-kernels -cl-intel-greater-than-4GB-buffer-required'") diff --git a/bitsandbytes/cextension.py b/bitsandbytes/cextension.py index e234f20d3..37935d7d6 100644 --- a/bitsandbytes/cextension.py +++ b/bitsandbytes/cextension.py @@ -25,58 +25,89 @@ def get_cuda_bnb_library_path(cuda_specs: CUDASpecs) -> Path: When no override is set, selects from packaged libraries using the following priority: 1. Exact version match. - 2. Highest packaged version <= runtime version, same major (e.g. runtime 12.9, ship 12.8). - 3. Lowest packaged version > runtime version, same major (e.g. runtime 12.0, ship 12.1). - No cross-major fallback: if no same-major library exists, returns the exact non-existent - path so the caller raises a clear "not found" error. - A warning is logged when falling back. Override env vars bypass selection entirely - and load the named version with no fallback. The returned path is not guaranteed to - exist when no packaged libs are found, or when an override names an absent version. + 2. Highest packaged version <= runtime version, same major (e.g. runtime 12.9, packaged 12.8). + 3. Lowest packaged version > runtime version, same major (e.g. runtime 12.0, packaged 12.1). + 4. For the ROCm backend only, repeat the same older-first selection across HIP major versions. + ROCm-backend selection uses the version tuple reported by ``torch.version.hip``; it does + not infer a ROCm release from ``torch.version.rocm``. CUDA does not fall back across major + versions. A warning is logged when falling back. + Overrides select the requested filename directly. The returned path is not guaranteed + to exist when no packaged libraries are found or an override names an absent version. """ is_hip = bool(torch.version.hip) prefix = "rocm" if is_hip else "cuda" override_var = "BNB_ROCM_VERSION" if is_hip else "BNB_CUDA_VERSION" + other_override_var = "BNB_CUDA_VERSION" if is_hip else "BNB_ROCM_VERSION" + + if os.environ.get(other_override_var): + logger.warning( + "%s is ignored because PyTorch is using %s; use %s instead.", + other_override_var, + "ROCm" if is_hip else "CUDA", + override_var, + ) override_value = os.environ.get(override_var) if override_value is not None: - if not override_value.isdigit(): - raise RuntimeError(f"{override_var}={override_value!r}: value must be digits only (e.g. '124' for 12.4).") - library_name = f"libbitsandbytes_{prefix}{override_value}{DYNAMIC_LIBRARY_SUFFIX}" + try: + override_version = _parse_version_override(override_value, is_hip) + except ValueError as error: + example = "7.14 or 714" if is_hip else "12.8 or 128" + raise RuntimeError( + f"{override_var}={override_value!r}: expected a dotted version or shortcode ({example})." + ) from error + + version_tag = _format_native_version_tag(override_version) + library_name = f"libbitsandbytes_{prefix}{version_tag}{DYNAMIC_LIBRARY_SUFFIX}" + override_path = PACKAGE_DIR / library_name logger.warning( - f"WARNING: {override_var}={override_value} environment variable detected; loading {library_name}.\n" + f"WARNING: {override_var}={override_value} environment variable detected; " + f"loading {override_path.name}.\n" f"This overrides automatic {'ROCm' if is_hip else 'CUDA'} version selection.\n" f"If this was unintended clear the variable and retry: unset {override_var}\n", ) - return PACKAGE_DIR / library_name + return override_path available = _find_cuda_libs(prefix, is_hip) runtime_version = cuda_specs.cuda_version_tuple + runtime_tag = _format_native_version_tag(runtime_version) if not available: - return PACKAGE_DIR / f"libbitsandbytes_{prefix}{cuda_specs.cuda_version_string}{DYNAMIC_LIBRARY_SUFFIX}" + return PACKAGE_DIR / f"libbitsandbytes_{prefix}{runtime_tag}{DYNAMIC_LIBRARY_SUFFIX}" if runtime_version in available: return available[runtime_version] - lower = [v for v in available if v[0] == runtime_version[0] and v < runtime_version] + missing_path = PACKAGE_DIR / f"libbitsandbytes_{prefix}{runtime_tag}{DYNAMIC_LIBRARY_SUFFIX}" + same_major = [version for version in available if version[0] == runtime_version[0]] + cross_major = False + lower = [version for version in same_major if version < runtime_version] if lower: selected = max(lower) + elif same_major: + selected = min(same_major) + elif is_hip: + lower = [version for version in available if version < runtime_version] + selected = max(lower) if lower else min(available) + cross_major = True else: - higher_same = [v for v in available if v[0] == runtime_version[0] and v > runtime_version] - if higher_same: - selected = min(higher_same) - else: - # No same-major library available. Return the non-existent exact path so - # get_native_library() raises a clear "not found" error. - return PACKAGE_DIR / f"libbitsandbytes_{prefix}{cuda_specs.cuda_version_string}{DYNAMIC_LIBRARY_SUFFIX}" - - logger.warning( - f"No prebuilt binary for {'ROCm' if is_hip else 'CUDA'} " - f"{runtime_version[0]}.{runtime_version[1]}, loading " - f"{'ROCm' if is_hip else 'CUDA'} {selected[0]}.{selected[1]} instead. " - f"Set {override_var} to override." - ) + return missing_path + + if cross_major: + logger.warning( + f"No prebuilt ROCm-backend binary for HIP {runtime_version[0]}.{runtime_version[1]}, loading " + f"HIP {selected[0]}.{selected[1]} across major versions. This binary may be incompatible " + "or may not contain code for your GPU architecture. " + f"Set {override_var} to override or compile from source." + ) + else: + logger.warning( + f"No prebuilt binary for {'ROCm-backend HIP' if is_hip else 'CUDA'} " + f"{runtime_version[0]}.{runtime_version[1]}, loading " + f"{'HIP' if is_hip else 'CUDA'} {selected[0]}.{selected[1]} instead. " + f"Set {override_var} to override." + ) return available[selected] @@ -124,26 +155,47 @@ def __init__(self, lib: ct.CDLL): lib.cget_managed_ptr.restype = ct.c_void_p -def _split_cuda_version(compact: str, is_hip: bool) -> tuple[int, int]: - """Split a compact CUDA/ROCm version string from a library filename into (major, minor). +def _format_native_version_tag(version: tuple[int, int]) -> str: + major, minor = version + return f"{major}{minor}" + + +def _split_cuda_version(version_tag: str, is_hip: bool) -> tuple[int, int]: + """Split a CUDA/ROCm library filename tag into (major, minor). CUDA: major is always 2 digits (11, 12, 13...), e.g. '118' -> (11, 8), '132' -> (13, 2). - ROCm: major is always 1 digit for now (6, 7...), e.g. '72' -> (7, 2), '713' -> (7, 13). - Note: revisit if ROCm major reaches 10. + ROCm: supported majors 6-9 use one digit, e.g. '72' -> (7, 2) + and '713' -> (7, 13). Tags starting with 1-5 reserve two major digits. """ if is_hip: - return int(compact[:1]), int(compact[1:]) - return int(compact[:2]), int(compact[2:]) + if len(version_tag) >= 3 and version_tag[0] in "12345": + return int(version_tag[:2]), int(version_tag[2:]) + return int(version_tag[:1]), int(version_tag[1:]) + return int(version_tag[:2]), int(version_tag[2:]) + + +def _parse_version_override(value: str, is_hip: bool) -> tuple[int, int]: + dotted = re.fullmatch(r"(\d+)\.(\d+)(?:\.\d+(?:[A-Za-z0-9+_.-]*)?)?", value) + if dotted: + return int(dotted.group(1)), int(dotted.group(2)) + if value.isdigit(): + return _split_cuda_version(value, is_hip) + raise ValueError(f"Invalid version override: {value}") def _find_cuda_libs(prefix: str, is_hip: bool) -> dict[tuple[int, int], Path]: """Return a {(major, minor): Path} mapping for all packaged CUDA/ROCm library files.""" result = {} for lib in PACKAGE_DIR.glob(f"libbitsandbytes_{prefix}*{DYNAMIC_LIBRARY_SUFFIX}"): - match = re.search(rf"{prefix}(\d+)", lib.name) + match = re.fullmatch( + rf"libbitsandbytes_{re.escape(prefix)}(\d+){re.escape(DYNAMIC_LIBRARY_SUFFIX)}", + lib.name, + ) if match: try: - result[_split_cuda_version(match.group(1), is_hip)] = lib + version_tag = match.group(1) + version = _split_cuda_version(version_tag, is_hip) + result[version] = lib except (ValueError, IndexError): continue return result @@ -153,11 +205,11 @@ def get_available_cuda_binary_versions() -> list[str]: """Get formatted CUDA/ROCm versions from existing library files.""" is_hip = bool(torch.version.hip) prefix = "rocm" if is_hip else "cuda" - return sorted(f"{major}.{minor}" for major, minor in _find_cuda_libs(prefix, is_hip)) + return [f"{major}.{minor}" for major, minor in sorted(_find_cuda_libs(prefix, is_hip))] def parse_cuda_version(version_str: str) -> str: - """Convert a raw version code string (e.g. '118', '713') to a dotted version (e.g. '11.8', '7.13').""" + """Convert a compact version tag (e.g. '118', '714') to a dotted version.""" if version_str.isdigit(): is_hip = bool(torch.version.hip) try: @@ -275,7 +327,7 @@ def _format_lib_error_message( "You have two options:\n" "1. COMPILE FROM SOURCE as mentioned here:\n" " https://huggingface.co/docs/bitsandbytes/main/en/installation?backend=AMD+ROCm#amd-gpu\n" - "2. Use BNB_ROCM_VERSION to specify a DIFFERENT ROCm version from the detected one, matching the version the library was built with.\n\n" + "2. Use BNB_ROCM_VERSION to select a DIFFERENT HIP-version suffix for the ROCm-backend binary, matching the version the library was built with.\n\n" ) ) diff --git a/bitsandbytes/diagnostics/cuda.py b/bitsandbytes/diagnostics/cuda.py index 655da84a0..760166129 100644 --- a/bitsandbytes/diagnostics/cuda.py +++ b/bitsandbytes/diagnostics/cuda.py @@ -137,7 +137,7 @@ def _print_cuda_diagnostics(cuda_specs: CUDASpecs) -> None: def _print_hip_diagnostics(cuda_specs: CUDASpecs) -> None: - print(f"PyTorch settings found: ROCM_VERSION={cuda_specs.cuda_version_string}") + print(f"PyTorch settings found: HIP_VERSION={cuda_specs.cuda_version_string}") rocm_override = os.environ.get("BNB_ROCM_VERSION") if rocm_override: @@ -149,17 +149,24 @@ def _print_hip_diagnostics(cuda_specs: CUDASpecs) -> None: f""" No compatible ROCm library found (tried: {binary_path.name}). You may need to compile from source: https://huggingface.co/docs/bitsandbytes/main/en/installation#rocm-compile - Use BNB_ROCM_VERSION to force a specific version if needed. + Use BNB_ROCM_VERSION to force a specific HIP-version suffix if needed. """, ) - hip_major, hip_minor = cuda_specs.cuda_version_tuple - if (hip_major, hip_minor) < (6, 1): - print_dedented( - """ - WARNING: bitsandbytes is fully supported only from ROCm 6.1. - """, - ) + rocm_version = getattr(torch.version, "rocm", None) + if rocm_version is not None: + try: + rocm_major, rocm_minor = map(int, rocm_version.split(".")[:2]) + except (ValueError, IndexError): + pass + else: + if (rocm_major, rocm_minor) < (6, 4): + print_dedented( + """ + WARNING: ROCm 6.4 or newer is required when compiling bitsandbytes from source. + Current prebuilt Linux binaries begin at ROCm 6.4.4. + """, + ) def print_diagnostics(cuda_specs: CUDASpecs) -> None: diff --git a/bitsandbytes/diagnostics/main.py b/bitsandbytes/diagnostics/main.py index a64925c06..3584472ef 100644 --- a/bitsandbytes/diagnostics/main.py +++ b/bitsandbytes/diagnostics/main.py @@ -58,8 +58,9 @@ def show_environment(): print(f"PyTorch: {torch.__version__}") print(f" CUDA: {torch.version.cuda or 'N/A'}") + print(f" ROCm: {getattr(torch.version, 'rocm', None) or 'N/A'}") print(f" HIP: {torch.version.hip or 'N/A'}") - print(f" XPU: {getattr(torch.version, 'xpu', 'N/A') or 'N/A'}") + print(f" XPU: {getattr(torch.version, 'xpu', None) or 'N/A'}") print("Related packages:") for pkg in _RELATED_PACKAGES: diff --git a/docs/source/errors.mdx b/docs/source/errors.mdx index 987488770..20ae8ac8c 100644 --- a/docs/source/errors.mdx +++ b/docs/source/errors.mdx @@ -23,16 +23,20 @@ If this does not work, please open an issue and paste the printed environment if ## Library not found: version mismatch -The library filename encodes the version: `libbitsandbytes_cuda{major}{minor}` for CUDA, `libbitsandbytes_rocm{major}{minor}` for ROCm. bitsandbytes selects which one to load based on what PyTorch reports: +The library filename encodes the CUDA or HIP version by concatenating major and minor: `libbitsandbytes_cuda{major}{minor}` for CUDA and `libbitsandbytes_rocm{major}{minor}` for ROCm. bitsandbytes selects which one to load based on what PyTorch reports: ```python import torch print(torch.version.cuda) # e.g. "12.8" -> looks for libbitsandbytes_cuda128 -print(torch.version.hip) # e.g. "7.2" -> looks for libbitsandbytes_rocm72 +print(torch.version.hip) # e.g. "7.14" -> looks for libbitsandbytes_rocm714 ``` +The ROCm distribution version and HIP version can diverge. `torch.version.rocm` is informational release metadata, while `torch.version.hip` identifies the HIP version that PyTorch was built against. bitsandbytes uses the HIP `(major, minor)` tuple for its ROCm library suffix so that build-time and runtime lookup use the same version line. The `rocm` portion of the filename identifies the backend; its numeric suffix represents the HIP major and minor version. bitsandbytes does not infer a ROCm release from this HIP tuple. + bitsandbytes will automatically fall back to the closest available pre-compiled version if an exact match is not found, and log a warning. For example, if your PyTorch was built with CUDA 12.9 but bitsandbytes only ships 12.8, it will load 12.8 automatically. +For the ROCm backend, selection may also fall back across HIP major versions, preferring the newest available lower version and then the oldest newer version. This compares HIP tuples only and does not imply compatibility between the corresponding ROCm SDK releases. A binary from another HIP major version may also lack code for your GPU architecture, so a cross-major fallback emits a stronger warning. Compile from source or set `BNB_ROCM_VERSION` to select a different HIP-version suffix if the selected binary fails to load or launch kernels. + If you see an error like `No compatible CUDA library found`, it means no compatible pre-compiled library could be found at all. To resolve this: 1. **Compile from source** to produce a library matching your exact toolkit version. See the [installation guide](installation) for instructions. @@ -44,4 +48,4 @@ If you see an error like `No compatible CUDA library found`, it means no compati # Windows (cmd) set BNB_CUDA_VERSION=128 ``` - The value must be digits only, e.g. `128` for CUDA 12.8 or `72` for ROCm 7.2. + Both backends accept dotted versions and compact shortcodes, for example `12.8` or `128` for CUDA and `7.14` or `714` for the ROCm backend. diff --git a/docs/source/installation.mdx b/docs/source/installation.mdx index eb9156a65..6d6a2973e 100644 --- a/docs/source/installation.mdx +++ b/docs/source/installation.mdx @@ -178,14 +178,14 @@ pip install bitsandbytes ### Compile from Source[[rocm-compile]] -bitsandbytes can be compiled from ROCm 6.3 - ROCm 7.14.0. See the `CMakeLists.txt` for additional options. +bitsandbytes can be compiled from ROCm 6.4 - ROCm 7.14. See the `CMakeLists.txt` for additional options. To compile from source, you need CMake >= **3.31.6** and Python >= **3.10** installed. Make sure you have a compiler installed to compile C++ (`gcc`, `make`, headers, etc.). -You should also have a ROCm installation (system-wide or via Docker). The current minimum supported version is **6.3**. +You should also have a ROCm installation (system-wide or via Docker). The current minimum supported version is **6.4**. ```bash # Install bitsandbytes from source diff --git a/tests/test_cuda_setup_evaluator.py b/tests/test_cuda_setup_evaluator.py index 56a52736e..0781e1ca9 100644 --- a/tests/test_cuda_setup_evaluator.py +++ b/tests/test_cuda_setup_evaluator.py @@ -2,134 +2,149 @@ from unittest.mock import patch import pytest +import torch from bitsandbytes.cextension import get_cuda_bnb_library_path from bitsandbytes.consts import DYNAMIC_LIBRARY_SUFFIX from bitsandbytes.cuda_specs import CUDASpecs -@pytest.fixture -def cuda120_spec() -> CUDASpecs: - """Simulates torch+cuda12.0 and a representative Ampere-class capability.""" +def specs(version: tuple[int, int]) -> CUDASpecs: return CUDASpecs( - cuda_version_string="120", - highest_compute_capability=(8, 6), - cuda_version_tuple=(12, 0), - ) - - -@pytest.fixture -def rocm70_spec() -> CUDASpecs: - """Simulates torch+rocm7.0.""" - return CUDASpecs( - cuda_version_string="70", + cuda_version_string=f"{version[0]}{version[1]}", highest_compute_capability=(0, 0), - cuda_version_tuple=(7, 0), + cuda_version_tuple=version, ) @pytest.mark.parametrize( - "spec,fake_libs,hip_version,expected_name,expect_warning", + "backend,backend_version,runtime_version,available,expected,warning", [ - # exact match - ( - CUDASpecs(cuda_version_string="124", highest_compute_capability=(8, 6), cuda_version_tuple=(12, 4)), - {(12, 4): Path(f"libbitsandbytes_cuda124{DYNAMIC_LIBRARY_SUFFIX}")}, - None, - f"libbitsandbytes_cuda124{DYNAMIC_LIBRARY_SUFFIX}", - False, - ), - # forward fallback within major: 12.0 -> 12.1 - ( - CUDASpecs(cuda_version_string="120", highest_compute_capability=(8, 6), cuda_version_tuple=(12, 0)), - { - (12, 1): Path(f"libbitsandbytes_cuda121{DYNAMIC_LIBRARY_SUFFIX}"), - (12, 4): Path(f"libbitsandbytes_cuda124{DYNAMIC_LIBRARY_SUFFIX}"), - }, - None, - f"libbitsandbytes_cuda121{DYNAMIC_LIBRARY_SUFFIX}", - True, - ), - # backward fallback: 12.9 -> 12.8 - ( - CUDASpecs(cuda_version_string="129", highest_compute_capability=(8, 9), cuda_version_tuple=(12, 9)), - { - (12, 4): Path(f"libbitsandbytes_cuda124{DYNAMIC_LIBRARY_SUFFIX}"), - (12, 8): Path(f"libbitsandbytes_cuda128{DYNAMIC_LIBRARY_SUFFIX}"), - }, - None, - f"libbitsandbytes_cuda128{DYNAMIC_LIBRARY_SUFFIX}", - True, - ), - # ROCm double-digit minor: 7.13 -> 7.2 - ( - CUDASpecs(cuda_version_string="713", highest_compute_capability=(0, 0), cuda_version_tuple=(7, 13)), - {(7, 2): Path(f"libbitsandbytes_rocm72{DYNAMIC_LIBRARY_SUFFIX}")}, - "7.13.0", - f"libbitsandbytes_rocm72{DYNAMIC_LIBRARY_SUFFIX}", - True, - ), - # no same-major match: 11.8 with only 12.x -> non-existent exact path, no warning - ( - CUDASpecs(cuda_version_string="118", highest_compute_capability=(7, 5), cuda_version_tuple=(11, 8)), - {(12, 1): Path("libbitsandbytes_cuda121.so"), (12, 4): Path("libbitsandbytes_cuda124.so")}, - None, - f"libbitsandbytes_cuda118{DYNAMIC_LIBRARY_SUFFIX}", - False, - ), - # no libs at all -> non-existent exact path, no warning - ( - CUDASpecs(cuda_version_string="129", highest_compute_capability=(8, 9), cuda_version_tuple=(12, 9)), - {}, - None, - f"libbitsandbytes_cuda129{DYNAMIC_LIBRARY_SUFFIX}", - False, - ), + # Exact match. + ("cuda", "12.4", (12, 4), [(12, 4)], (12, 4), False), + # Same-major fallback to the oldest newer binary. + ("cuda", "12.0", (12, 0), [(12, 1), (12, 4)], (12, 1), True), + # Same-major fallback to the newest older binary. + ("cuda", "12.9", (12, 9), [(12, 4), (12, 8)], (12, 8), True), + # ROCm same-major fallback with a double-digit minor. + ("hip", "7.13.0", (7, 13), [(7, 2)], (7, 2), True), + # ROCm same-major fallback prefers the newest older binary. + ("hip", "7.9.0", (7, 9), [(7, 2), (7, 14)], (7, 2), True), + # ROCm cross-major fallback to the newest older binary. + ("hip", "8.0.0", (8, 0), [(7, 14)], (7, 14), True), + # ROCm cross-major fallback to the oldest newer binary. + ("hip", "6.4.0", (6, 4), [(7, 0)], (7, 0), True), + # CUDA does not fall back across major versions. + ("cuda", "11.8", (11, 8), [(12, 1), (12, 4)], None, False), + # No packaged libraries returns the requested path without a warning. + ("cuda", "12.9", (12, 9), [], None, False), + ("hip", "7.14.0", (7, 14), [], None, False), ], ) -def test_version_selection(monkeypatch, caplog, spec, fake_libs, hip_version, expected_name, expect_warning): +def test_version_selection( + monkeypatch, + caplog, + backend, + backend_version, + runtime_version, + available, + expected, + warning, +): """Library selection: exact match, fallback, no-same-major, no-libs.""" monkeypatch.delenv("BNB_CUDA_VERSION", raising=False) monkeypatch.delenv("BNB_ROCM_VERSION", raising=False) - is_hip = spec.cuda_version_tuple[0] < 10 + other_backend = "cuda" if backend == "hip" else "hip" + prefix = "rocm" if backend == "hip" else "cuda" + paths = { + version: Path(f"libbitsandbytes_{prefix}{version[0]}{version[1]}{DYNAMIC_LIBRARY_SUFFIX}") + for version in available + } + # ROCm release metadata is deliberately unrelated to HIP-based library selection. with ( - patch("torch.version.hip", hip_version if is_hip else None), - patch("bitsandbytes.cextension._find_cuda_libs", return_value=fake_libs), + patch.object(torch.version, backend, backend_version), + patch.object(torch.version, other_backend, None), + patch.object(torch.version, "rocm", "10.0.0", create=True), + patch("bitsandbytes.cextension._find_cuda_libs", return_value=paths), + caplog.at_level("WARNING"), ): - with caplog.at_level("WARNING"): - result = get_cuda_bnb_library_path(spec) - assert result.name == expected_name - if expect_warning: - assert caplog.text + result = get_cuda_bnb_library_path(specs(runtime_version)) + + if expected is None: + tag = f"{runtime_version[0]}{runtime_version[1]}" + assert result.name == f"libbitsandbytes_{prefix}{tag}{DYNAMIC_LIBRARY_SUFFIX}" else: - assert not caplog.text + assert result == paths[expected] + assert bool(caplog.text) is warning -def test_override(monkeypatch, cuda120_spec, caplog): - """BNB_CUDA_VERSION overrides path selection.""" - monkeypatch.setenv("BNB_CUDA_VERSION", "110") - with patch("bitsandbytes.cextension._find_cuda_libs", return_value={}): - with caplog.at_level("WARNING"): - result = get_cuda_bnb_library_path(cuda120_spec) - assert result.stem == "libbitsandbytes_cuda110" - assert "BNB_CUDA_VERSION" in caplog.text +@pytest.mark.parametrize( + "backend,backend_version,runtime_version,override,expected_stem", + [ + ("hip", "7.0.0", (7, 0), "72", "libbitsandbytes_rocm72"), + ("hip", "7.0.0", (7, 0), "7.2", "libbitsandbytes_rocm72"), + ("hip", "7.0.0", (7, 0), "714", "libbitsandbytes_rocm714"), + ("hip", "10.0.0", (10, 0), "1014", "libbitsandbytes_rocm1014"), + ("cuda", "12.0", (12, 0), "128", "libbitsandbytes_cuda128"), + ("cuda", "12.0", (12, 0), "12.8", "libbitsandbytes_cuda128"), + ("cuda", "12.0", (12, 0), "12.8.1", "libbitsandbytes_cuda128"), + ], +) +def test_override_formats(monkeypatch, caplog, backend, backend_version, runtime_version, override, expected_stem): + other_backend = "cuda" if backend == "hip" else "hip" + override_var = "BNB_ROCM_VERSION" if backend == "hip" else "BNB_CUDA_VERSION" + other_override_var = "BNB_CUDA_VERSION" if backend == "hip" else "BNB_ROCM_VERSION" + monkeypatch.setenv(override_var, override) + monkeypatch.delenv(other_override_var, raising=False) + with ( + patch.object(torch.version, backend, backend_version), + patch.object(torch.version, other_backend, None), + patch("bitsandbytes.cextension._find_cuda_libs", return_value={}), + caplog.at_level("WARNING"), + ): + result = get_cuda_bnb_library_path(specs(runtime_version)) + assert result.stem == expected_stem + assert override_var in caplog.text -def test_rocm_override(monkeypatch, rocm70_spec, caplog): - """BNB_ROCM_VERSION overrides path selection.""" - monkeypatch.setenv("BNB_ROCM_VERSION", "72") +@pytest.mark.parametrize( + "backend,torch_version,runtime_version", + [("cuda", "12.0", (12, 0)), ("hip", "7.2.0", (7, 2))], +) +def test_override_invalid_format(monkeypatch, backend, torch_version, runtime_version): + """Reject malformed overrides for both backends.""" + other_backend = "cuda" if backend == "hip" else "hip" + override_var = "BNB_ROCM_VERSION" if backend == "hip" else "BNB_CUDA_VERSION" + other_override_var = "BNB_CUDA_VERSION" if backend == "hip" else "BNB_ROCM_VERSION" + monkeypatch.setenv(override_var, "not-a-version") + monkeypatch.delenv(other_override_var, raising=False) with ( - patch("torch.version.hip", "7.0.0"), - patch("bitsandbytes.cextension._find_cuda_libs", return_value={}), + patch.object(torch.version, backend, torch_version), + patch.object(torch.version, other_backend, None), + pytest.raises(RuntimeError, match="dotted version"), ): - with caplog.at_level("WARNING"): - result = get_cuda_bnb_library_path(rocm70_spec) - assert result.stem == "libbitsandbytes_rocm72" - assert "BNB_ROCM_VERSION" in caplog.text + get_cuda_bnb_library_path(specs(runtime_version)) -def test_override_invalid_format(monkeypatch, cuda120_spec): - """Override value must be digits only (e.g. '124'), not dotted or alphanumeric.""" - monkeypatch.setenv("BNB_CUDA_VERSION", "12.4") - with pytest.raises(RuntimeError, match="digits only"): - get_cuda_bnb_library_path(cuda120_spec) +@pytest.mark.parametrize( + "backend,backend_version,runtime_version,wrong_var,correct_var", + [ + ("cuda", "12.0", (12, 0), "BNB_ROCM_VERSION", "BNB_CUDA_VERSION"), + ("hip", "7.2.0", (7, 2), "BNB_CUDA_VERSION", "BNB_ROCM_VERSION"), + ], +) +def test_opposite_backend_override_warns( + monkeypatch, caplog, backend, backend_version, runtime_version, wrong_var, correct_var +): + other_backend = "cuda" if backend == "hip" else "hip" + monkeypatch.setenv(wrong_var, "72") + monkeypatch.delenv(correct_var, raising=False) + with ( + patch.object(torch.version, backend, backend_version), + patch.object(torch.version, other_backend, None), + patch("bitsandbytes.cextension._find_cuda_libs", return_value={}), + caplog.at_level("WARNING"), + ): + get_cuda_bnb_library_path(specs(runtime_version)) + assert f"{wrong_var} is ignored" in caplog.text + assert f"use {correct_var} instead" in caplog.text