2121
2222def 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
8383class 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
142159def 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
149171class 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
0 commit comments