Skip to content

Commit 9da7109

Browse files
Reduce CUDA build matrix, better fallback for lib loading (#1980)
1 parent 435b8b3 commit 9da7109

6 files changed

Lines changed: 261 additions & 242 deletions

File tree

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
matrix:
6060
os: [ubuntu-22.04, ubuntu-22.04-arm, windows-2025]
6161
cuda_version:
62-
["11.8.0", "12.0.1", "12.1.1", "12.2.2", "12.3.2", "12.4.1", "12.5.1", "12.6.3", "12.8.1", "12.9.1", "13.0.2", "13.2.0"]
62+
["11.8.0", "12.1.1", "12.4.1", "12.6.3", "12.8.1", "13.0.2", "13.2.0"]
6363
runs-on: ${{ matrix.os }}
6464
steps:
6565
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

bitsandbytes/cextension.py

Lines changed: 99 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -21,63 +21,63 @@
2121

2222
def get_cuda_bnb_library_path(cuda_specs: CUDASpecs) -> Path:
2323
"""
24-
Get the disk path to the CUDA BNB native library specified by the
25-
given CUDA specs, taking into account the `BNB_CUDA_VERSION` override environment variable.
26-
27-
The library is not guaranteed to exist at the returned path.
24+
Get the path to the best matching CUDA/ROCm BNB native library for the given specs.
25+
26+
When no override is set, selects from packaged libraries using the following priority:
27+
1. Exact version match.
28+
2. Highest packaged version <= runtime version, same major (e.g. runtime 12.9, ship 12.8).
29+
3. Lowest packaged version > runtime version, same major (e.g. runtime 12.0, ship 12.1).
30+
No cross-major fallback: if no same-major library exists, returns the exact non-existent
31+
path so the caller raises a clear "not found" error.
32+
A warning is logged when falling back. Override env vars bypass selection entirely
33+
and load the named version with no fallback. The returned path is not guaranteed to
34+
exist when no packaged libs are found, or when an override names an absent version.
2835
"""
36+
is_hip = bool(torch.version.hip)
37+
prefix = "rocm" if is_hip else "cuda"
38+
override_var = "BNB_ROCM_VERSION" if is_hip else "BNB_CUDA_VERSION"
39+
40+
override_value = os.environ.get(override_var)
41+
42+
if override_value is not None:
43+
if not override_value.isdigit():
44+
raise RuntimeError(f"{override_var}={override_value!r}: value must be digits only (e.g. '124' for 12.4).")
45+
library_name = f"libbitsandbytes_{prefix}{override_value}{DYNAMIC_LIBRARY_SUFFIX}"
46+
logger.warning(
47+
f"WARNING: {override_var}={override_value} environment variable detected; loading {library_name}.\n"
48+
f"This overrides automatic {'ROCm' if is_hip else 'CUDA'} version selection.\n"
49+
f"If this was unintended clear the variable and retry: unset {override_var}\n",
50+
)
51+
return PACKAGE_DIR / library_name
2952

30-
prefix = "rocm" if torch.version.hip else "cuda"
31-
library_name = f"libbitsandbytes_{prefix}{cuda_specs.cuda_version_string}{DYNAMIC_LIBRARY_SUFFIX}"
53+
available = _find_cuda_libs(prefix, is_hip)
54+
runtime_version = cuda_specs.cuda_version_tuple
3255

33-
cuda_override_value = os.environ.get("BNB_CUDA_VERSION")
34-
rocm_override_value = os.environ.get("BNB_ROCM_VERSION")
56+
if not available:
57+
return PACKAGE_DIR / f"libbitsandbytes_{prefix}{cuda_specs.cuda_version_string}{DYNAMIC_LIBRARY_SUFFIX}"
3558

36-
if torch.version.hip:
37-
if cuda_override_value:
38-
if not rocm_override_value:
39-
raise RuntimeError(
40-
f"BNB_CUDA_VERSION={cuda_override_value} detected but this is not a CUDA build!\n"
41-
"Use BNB_ROCM_VERSION instead: export BNB_ROCM_VERSION=<version>\n"
42-
"Clear the variable and retry: unset BNB_CUDA_VERSION\n"
43-
)
44-
logger.warning(
45-
f"WARNING: BNB_CUDA_VERSION={cuda_override_value} is set but ignored on this ROCm build. "
46-
"Clear the variable: unset BNB_CUDA_VERSION",
47-
)
48-
if rocm_override_value:
49-
library_name = re.sub(r"rocm\d+", f"rocm{rocm_override_value}", library_name, count=1)
50-
logger.warning(
51-
f"WARNING: BNB_ROCM_VERSION={rocm_override_value} environment variable detected; loading {library_name}.\n"
52-
"This can be used to load a bitsandbytes version built with a ROCm version that is different from the PyTorch ROCm version.\n"
53-
"If this was unintended clear the variable and retry: unset BNB_ROCM_VERSION\n",
54-
)
55-
elif torch.version.cuda:
56-
if rocm_override_value:
57-
if not cuda_override_value:
58-
raise RuntimeError(
59-
f"BNB_ROCM_VERSION={rocm_override_value} detected but this is not a ROCm build!\n"
60-
"Use BNB_CUDA_VERSION instead: export BNB_CUDA_VERSION=<version>\n"
61-
"Clear the variable and retry: unset BNB_ROCM_VERSION\n"
62-
)
63-
logger.warning(
64-
f"WARNING: BNB_ROCM_VERSION={rocm_override_value} is set but ignored on this CUDA build. "
65-
"Clear the variable: unset BNB_ROCM_VERSION",
66-
)
67-
if cuda_override_value:
68-
library_name = re.sub(r"cuda\d+", f"cuda{cuda_override_value}", library_name, count=1)
69-
logger.warning(
70-
f"WARNING: BNB_CUDA_VERSION={cuda_override_value} environment variable detected; loading {library_name}.\n"
71-
"This can be used to load a bitsandbytes version built with a CUDA version that is different from the PyTorch CUDA version.\n"
72-
"If this was unintended clear the variable and retry: unset BNB_CUDA_VERSION\n",
73-
)
74-
else:
75-
if rocm_override_value or cuda_override_value:
76-
raise RuntimeError(
77-
"BNB_ROCM_VERSION / BNB_CUDA_VERSION overrides are not supported on this backend.",
78-
)
59+
if runtime_version in available:
60+
return available[runtime_version]
7961

80-
return PACKAGE_DIR / library_name
62+
lower = [v for v in available if v[0] == runtime_version[0] and v < runtime_version]
63+
if lower:
64+
selected = max(lower)
65+
else:
66+
higher_same = [v for v in available if v[0] == runtime_version[0] and v > runtime_version]
67+
if higher_same:
68+
selected = min(higher_same)
69+
else:
70+
# No same-major library available. Return the non-existent exact path so
71+
# get_native_library() raises a clear "not found" error.
72+
return PACKAGE_DIR / f"libbitsandbytes_{prefix}{cuda_specs.cuda_version_string}{DYNAMIC_LIBRARY_SUFFIX}"
73+
74+
logger.warning(
75+
f"No prebuilt binary for {'ROCm' if is_hip else 'CUDA'} "
76+
f"{runtime_version[0]}.{runtime_version[1]}, loading "
77+
f"{'ROCm' if is_hip else 'CUDA'} {selected[0]}.{selected[1]} instead. "
78+
f"Set {override_var} to override."
79+
)
80+
return available[selected]
8181

8282

8383
class BNBNativeLibrary:
@@ -124,26 +124,48 @@ def __init__(self, lib: ct.CDLL):
124124
lib.cget_managed_ptr.restype = ct.c_void_p
125125

126126

127-
def get_available_cuda_binary_versions() -> list[str]:
128-
"""Get formatted CUDA versions from existing library files using cuda_specs logic"""
129-
lib_pattern = f"libbitsandbytes_{BNB_BACKEND.lower()}*{DYNAMIC_LIBRARY_SUFFIX}"
130-
versions = []
131-
for lib in Path(__file__).parent.glob(lib_pattern):
132-
pattern = rf"{BNB_BACKEND.lower()}(\d+)"
133-
match = re.search(pattern, lib.name)
127+
def _split_cuda_version(compact: str, is_hip: bool) -> tuple[int, int]:
128+
"""Split a compact CUDA/ROCm version string from a library filename into (major, minor).
129+
130+
CUDA: major is always 2 digits (11, 12, 13...), e.g. '118' -> (11, 8), '132' -> (13, 2).
131+
ROCm: major is always 1 digit for now (6, 7...), e.g. '72' -> (7, 2), '713' -> (7, 13).
132+
Note: revisit if ROCm major reaches 10.
133+
"""
134+
if is_hip:
135+
return int(compact[:1]), int(compact[1:])
136+
return int(compact[:2]), int(compact[2:])
137+
138+
139+
def _find_cuda_libs(prefix: str, is_hip: bool) -> dict[tuple[int, int], Path]:
140+
"""Return a {(major, minor): Path} mapping for all packaged CUDA/ROCm library files."""
141+
result = {}
142+
for lib in PACKAGE_DIR.glob(f"libbitsandbytes_{prefix}*{DYNAMIC_LIBRARY_SUFFIX}"):
143+
match = re.search(rf"{prefix}(\d+)", lib.name)
134144
if match:
135-
ver_code = int(match.group(1))
136-
major = ver_code // 10
137-
minor = ver_code % 10
138-
versions.append(f"{major}.{minor}")
139-
return sorted(versions)
145+
try:
146+
result[_split_cuda_version(match.group(1), is_hip)] = lib
147+
except (ValueError, IndexError):
148+
continue
149+
return result
150+
151+
152+
def get_available_cuda_binary_versions() -> list[str]:
153+
"""Get formatted CUDA/ROCm versions from existing library files."""
154+
is_hip = bool(torch.version.hip)
155+
prefix = "rocm" if is_hip else "cuda"
156+
return sorted(f"{major}.{minor}" for major, minor in _find_cuda_libs(prefix, is_hip))
140157

141158

142159
def parse_cuda_version(version_str: str) -> str:
143-
"""Convert raw version string (e.g. '118' from env var) to formatted version (e.g. '11.8')"""
160+
"""Convert a raw version code string (e.g. '118', '713') to a dotted version (e.g. '11.8', '7.13')."""
144161
if version_str.isdigit():
145-
return f"{version_str[:-1]}.{version_str[-1]}"
146-
return version_str # fallback as safety net
162+
is_hip = bool(torch.version.hip)
163+
try:
164+
major, minor = _split_cuda_version(version_str, is_hip)
165+
return f"{major}.{minor}"
166+
except (ValueError, IndexError):
167+
pass
168+
return version_str
147169

148170

149171
class ErrorHandlerMockBNBNativeLibrary(BNBNativeLibrary):
@@ -169,28 +191,19 @@ class ErrorHandlerMockBNBNativeLibrary(BNBNativeLibrary):
169191

170192
def __init__(self, error_msg: str):
171193
self.error_msg = error_msg
172-
self.user_cuda_version = get_cuda_version_tuple()
173194
self.available_versions = get_available_cuda_binary_versions()
174-
self.override_value = (
175-
os.environ.get("BNB_ROCM_VERSION") if HIP_ENVIRONMENT else os.environ.get("BNB_CUDA_VERSION")
176-
)
177-
self.requested_version = (
178-
parse_cuda_version(self.override_value)
179-
if self.override_value
180-
else f"{self.user_cuda_version[0]}.{self.user_cuda_version[1]}"
181-
if self.user_cuda_version
182-
else "unknown"
183-
)
195+
override_value = os.environ.get("BNB_ROCM_VERSION") if HIP_ENVIRONMENT else os.environ.get("BNB_CUDA_VERSION")
196+
user_version = get_cuda_version_tuple()
197+
user_version_str = f"{user_version[0]}.{user_version[1]}" if user_version else "unknown"
198+
self.requested_version = parse_cuda_version(override_value) if override_value else user_version_str
184199

185200
# Pre-generate the error message based on error type
186201
if "cannot open shared object file" in error_msg:
187202
self.formatted_error = self._format_dependency_error()
188203
else: # lib loading errors
189204
self.formatted_error = self._format_lib_error_message(
190205
available_versions=self.available_versions,
191-
user_cuda_version=f"{self.user_cuda_version[0]}.{self.user_cuda_version[1]}"
192-
if self.user_cuda_version
193-
else "unknown",
206+
user_cuda_version=user_version_str,
194207
original_error=f"Original error: {self.error_msg}\n" if self.error_msg else "",
195208
requested_version=self.requested_version,
196209
)
@@ -241,8 +254,8 @@ def _format_lib_error_message(
241254

242255
note = (
243256
(
244-
f"To make bitsandbytes work, the compiled library version MUST exactly match the linked {BNB_BACKEND} version.\n"
245-
f"If your {BNB_BACKEND} version doesn't have a pre-compiled binary, you MUST compile from source.\n\n"
257+
f"bitsandbytes tried to find a compatible {BNB_BACKEND} binary but none could be loaded.\n"
258+
f"If your {BNB_BACKEND} version isn't among the available pre-compiled versions above, you must compile from source.\n\n"
246259
)
247260
if no_cuda_lib_found
248261
else ""
@@ -294,8 +307,8 @@ def _format_dependency_error(self) -> str:
294307
f"1. You have installed {BNB_BACKEND} {cuda_major_version}.x toolkit on your system\n"
295308
f"2. The {BNB_BACKEND} runtime libraries are in your LD_LIBRARY_PATH\n\n"
296309
f"You can add them with (and persist the change by adding the line to your .bashrc):\n"
297-
f" export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/{BNB_BACKEND.lower()}-{cuda_major_version}.x/\
298-
{'lib64' if not HIP_ENVIRONMENT else 'lib'}\n\n"
310+
f" export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/{BNB_BACKEND.lower()}-{cuda_major_version}.x/"
311+
f"{'lib64' if not HIP_ENVIRONMENT else 'lib'}\n\n"
299312
f"Original error: {self.error_msg}\n\n"
300313
f"🔍 Run this command for detailed diagnostics:\n"
301314
f"python -m bitsandbytes\n\n"
@@ -329,7 +342,7 @@ def get_native_library() -> BNBNativeLibrary:
329342
cuda_binary_path = get_cuda_bnb_library_path(cuda_specs)
330343

331344
if not cuda_binary_path.exists():
332-
raise RuntimeError(f"Configured {BNB_BACKEND} binary not found at {cuda_binary_path}")
345+
raise RuntimeError(f"No compatible {BNB_BACKEND} binary found at {cuda_binary_path}")
333346

334347
binary_path = cuda_binary_path
335348

bitsandbytes/diagnostics/cuda.py

Lines changed: 17 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ def _print_cuda_diagnostics(cuda_specs: CUDASpecs) -> None:
120120
if not binary_path.exists():
121121
print_dedented(
122122
f"""
123-
Library not found: {binary_path}. Maybe you need to compile it from source?
123+
No compatible CUDA library found (tried: {binary_path.name}). You may need to compile from source:
124+
https://huggingface.co/docs/bitsandbytes/main/en/installation#cuda-compile
124125
""",
125126
)
126127

@@ -146,12 +147,9 @@ def _print_hip_diagnostics(cuda_specs: CUDASpecs) -> None:
146147
if not binary_path.exists():
147148
print_dedented(
148149
f"""
149-
Library not found: {binary_path}.
150-
Maybe you need to compile it from source? If you compiled from source, check that ROCm version
151-
in PyTorch Settings matches your ROCm install. If not, you can either:
152-
1. Reinstall PyTorch for your ROCm version and rebuild bitsandbytes.
153-
2. Set BNB_ROCM_VERSION to match the version the library was built with.
154-
For example: export BNB_ROCM_VERSION=72
150+
No compatible ROCm library found (tried: {binary_path.name}). You may need to compile from source:
151+
https://huggingface.co/docs/bitsandbytes/main/en/installation#rocm-compile
152+
Use BNB_ROCM_VERSION to force a specific version if needed.
155153
""",
156154
)
157155

@@ -171,62 +169,25 @@ def print_diagnostics(cuda_specs: CUDASpecs) -> None:
171169
_print_cuda_diagnostics(cuda_specs)
172170

173171

174-
def _print_cuda_runtime_diagnostics() -> None:
175-
cudart_paths = list(find_cudart_libraries())
176-
if not cudart_paths:
177-
print("CUDA SETUP: WARNING! CUDA runtime files not found in any environmental path.")
178-
elif len(cudart_paths) > 1:
179-
print_dedented(
180-
f"""
181-
Found duplicate CUDA runtime files (see below).
182-
183-
We select the PyTorch default CUDA runtime, which is {torch.version.cuda},
184-
but this might mismatch with the CUDA version that is needed for bitsandbytes.
185-
To override this behavior set the `BNB_CUDA_VERSION=<version string, e.g. 122>` environmental variable.
186-
187-
For example, if you want to use the CUDA version 122,
188-
BNB_CUDA_VERSION=122 python ...
189-
190-
OR set the environmental variable in your .bashrc:
191-
export BNB_CUDA_VERSION=122
192-
193-
In the case of a manual override, make sure you set LD_LIBRARY_PATH, e.g.
194-
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda-11.2,
195-
""",
196-
)
197-
for pth in cudart_paths:
198-
print(f"* Found CUDA runtime at: {pth}")
199-
172+
def print_runtime_diagnostics() -> None:
173+
backend = "ROCm" if HIP_ENVIRONMENT else "CUDA"
174+
runtime_version = torch.version.hip if HIP_ENVIRONMENT else torch.version.cuda
175+
override_var = "BNB_ROCM_VERSION" if HIP_ENVIRONMENT else "BNB_CUDA_VERSION"
176+
override_example = "72" if HIP_ENVIRONMENT else "122"
200177

201-
def _print_hip_runtime_diagnostics() -> None:
202178
cudart_paths = list(find_cudart_libraries())
203179
if not cudart_paths:
204-
print("ROCm SETUP: WARNING! ROCm runtime files not found in any environmental path.")
180+
print(f"{backend} SETUP: WARNING! {backend} runtime files not found in any environmental path.")
205181
elif len(cudart_paths) > 1:
206182
print_dedented(
207183
f"""
208-
Found duplicate ROCm runtime files (see below).
209-
210-
We select the PyTorch default ROCm runtime, which is {torch.version.hip},
211-
but this might mismatch with the ROCm version that is needed for bitsandbytes.
212-
To override this behavior set the `BNB_ROCM_VERSION=<version string, e.g. 72>` environmental variable.
213-
214-
For example, if you want to use the ROCm version 7.2,
215-
BNB_ROCM_VERSION=72 python ...
216-
217-
OR set the environmental variable in your .bashrc:
218-
export BNB_ROCM_VERSION=72
184+
Found duplicate {backend} runtime files (see below).
219185
220-
In the case of a manual override, make sure you set LD_LIBRARY_PATH, e.g.
221-
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/rocm-7.2.0/lib,
186+
bitsandbytes will use PyTorch's {backend} runtime ({runtime_version}) and auto-select
187+
the closest available library version. If you need to force a specific version,
188+
set {override_var}, e.g.:
189+
export {override_var}={override_example}
222190
""",
223191
)
224192
for pth in cudart_paths:
225-
print(f"* Found ROCm runtime at: {pth}")
226-
227-
228-
def print_runtime_diagnostics() -> None:
229-
if HIP_ENVIRONMENT:
230-
_print_hip_runtime_diagnostics()
231-
else:
232-
_print_cuda_runtime_diagnostics()
193+
print(f"* Found {backend} runtime at: {pth}")

0 commit comments

Comments
 (0)