|
18 | 18 | ) |
19 | 19 | from typing_extensions import TypeAlias |
20 | 20 |
|
| 21 | +_DLL_DIRECTORY_HANDLES = [] |
| 22 | +_REGISTERED_DLL_DIRECTORIES = set() |
| 23 | + |
21 | 24 | def _format_library_dir_contents(base_paths: list[pathlib.Path]) -> str: |
22 | 25 | """Format directory contents for diagnostics after library loading fails.""" |
23 | 26 | sections = [] |
@@ -50,102 +53,197 @@ def _format_library_dir_contents(base_paths: list[pathlib.Path]) -> str: |
50 | 53 |
|
51 | 54 | return "\n".join(sections) |
52 | 55 |
|
53 | | -# Load the library |
| 56 | +def _add_library_search_paths(paths: list[pathlib.Path]): |
| 57 | + """ |
| 58 | + Register dynamic library search paths. |
| 59 | +
|
| 60 | + Priority: |
| 61 | + lib/ |
| 62 | + bin/ |
| 63 | +
|
| 64 | + The order is intentionally reversed when registering on Windows |
| 65 | + because Windows searches the latest added DLL directory first. |
| 66 | + """ |
| 67 | + |
| 68 | + if sys.platform != "win32": |
| 69 | + return |
| 70 | + |
| 71 | + # Remove duplicate paths to avoid redundant registration. |
| 72 | + unique_paths = [] |
| 73 | + for path in paths: |
| 74 | + p = pathlib.Path(path) |
| 75 | + if ( |
| 76 | + p.exists() |
| 77 | + and p.is_dir() |
| 78 | + and p not in unique_paths |
| 79 | + ): |
| 80 | + unique_paths.append(p) |
| 81 | + |
| 82 | + # Windows DLL search priority: lib/ > bin/ |
| 83 | + # The reversed iteration ensures that bin/ directories are registered first, |
| 84 | + # since Windows searches the most recently added directory first. |
| 85 | + for p in reversed(unique_paths): |
| 86 | + p_str = str(p) |
| 87 | + if p_str in _REGISTERED_DLL_DIRECTORIES: |
| 88 | + continue |
| 89 | + handle = os.add_dll_directory(p_str) |
| 90 | + _DLL_DIRECTORY_HANDLES.append(handle) |
| 91 | + _REGISTERED_DLL_DIRECTORIES.add(p_str) |
| 92 | + |
| 93 | +def _add_optional_backend_paths(): |
| 94 | + """ |
| 95 | + Collect optional backend library paths from environment variables. |
| 96 | +
|
| 97 | + Environment variable sources (in priority order): |
| 98 | + - CUDA_PATH |
| 99 | + - HIP_PATH |
| 100 | + - VULKAN_SDK |
| 101 | +
|
| 102 | + Each source may contain subdirectories that are added to the search path. |
| 103 | + """ |
| 104 | + |
| 105 | + if sys.platform != "win32": |
| 106 | + return |
| 107 | + |
| 108 | + backend_paths = [] |
| 109 | + |
| 110 | + # Collect CUDA backend paths from environment variable |
| 111 | + if "CUDA_PATH" in os.environ: |
| 112 | + cuda_path = os.environ["CUDA_PATH"] |
| 113 | + for sub_dir in [ |
| 114 | + "bin", # Default CUDA binaries |
| 115 | + os.path.join("bin", "x64"), # 64-bit x86 CUDA binaries |
| 116 | + "lib", # Default CUDA libraries |
| 117 | + os.path.join("lib", "x64"), # 64-bit x86 CUDA libraries |
| 118 | + ]: |
| 119 | + path = pathlib.Path(cuda_path) / sub_dir |
| 120 | + if path.exists(): |
| 121 | + backend_paths.append(path) |
| 122 | + |
| 123 | + # Collect HIP backend paths from environment variable |
| 124 | + if "HIP_PATH" in os.environ: |
| 125 | + hip_path = pathlib.Path(os.environ["HIP_PATH"]) |
| 126 | + backend_paths.extend([ |
| 127 | + hip_path / "bin", # HIP binaries |
| 128 | + hip_path / "lib", # HIP libraries |
| 129 | + ]) |
| 130 | + |
| 131 | + # Collect Vulkan SDK paths from environment variable |
| 132 | + if "VULKAN_SDK" in os.environ: |
| 133 | + sdk = pathlib.Path(os.environ["VULKAN_SDK"]) |
| 134 | + backend_paths.extend([ |
| 135 | + sdk / "Bin", # Vulkan SDK binaries (Windows) |
| 136 | + sdk / "Lib", # Vulkan SDK libraries (Windows) |
| 137 | + ]) |
| 138 | + |
| 139 | + _add_library_search_paths(backend_paths) |
| 140 | + |
| 141 | +def _normalize_unique_paths( |
| 142 | + paths: list[pathlib.Path] |
| 143 | +) -> list[pathlib.Path]: |
| 144 | + |
| 145 | + unique_paths = [] |
| 146 | + seen = set() |
| 147 | + |
| 148 | + for path in paths: |
| 149 | + p = pathlib.Path(path) |
| 150 | + |
| 151 | + if not p.exists() or not p.is_dir(): |
| 152 | + continue |
| 153 | + |
| 154 | + key = str(p.resolve()).lower() |
| 155 | + |
| 156 | + if key not in seen: |
| 157 | + seen.add(key) |
| 158 | + unique_paths.append(p) |
| 159 | + |
| 160 | + return unique_paths |
| 161 | + |
| 162 | +# Load a shared library based on platform-specific naming conventions |
54 | 163 | def load_shared_library(lib_base_name: str, base_paths: Union[pathlib.Path, list[pathlib.Path]]): |
| 164 | + """ |
| 165 | + Load a shared library dynamically by searching through provided paths |
| 166 | + and system fallback directories according to the target platform. |
| 167 | +
|
| 168 | + Platform-specific behavior: |
| 169 | + - Linux/FreeBSD: lib{base}.so |
| 170 | + - macOS (Darwin): lib{base}.dylib OR lib{base}.so (fallback) |
| 171 | + - Windows: {base}.dll OR lib{base}.dll |
| 172 | +
|
| 173 | + Search order: |
| 174 | + 1. Package bundled libraries from provided paths |
| 175 | + 2. System-level fallback via find_library() |
| 176 | + """ |
| 177 | + |
55 | 178 | if isinstance(base_paths, pathlib.Path): |
56 | 179 | base_paths = [base_paths] |
57 | 180 |
|
| 181 | + base_paths = _normalize_unique_paths(base_paths) |
| 182 | + |
58 | 183 | lib_names = [] |
59 | 184 |
|
| 185 | + # Linux/FreeBSD: use .so extension with system library directories |
60 | 186 | if sys.platform.startswith("linux") or sys.platform.startswith("freebsd"): |
61 | 187 | lib_names = [f"lib{lib_base_name}.so"] |
62 | | - |
63 | 188 | base_paths.extend([ |
64 | | - "/usr/local/lib", |
65 | | - "/usr/lib", |
66 | | - "/usr/lib64", |
| 189 | + "/usr/local/lib", # Local user libraries |
| 190 | + "/usr/lib", # System libraries |
| 191 | + "/usr/lib64", # 64-bit system libraries (some distros) |
67 | 192 | ]) |
68 | 193 |
|
69 | 194 | elif sys.platform == "darwin": |
70 | 195 | lib_names = [ |
71 | | - f"lib{lib_base_name}.dylib", |
72 | | - f"lib{lib_base_name}.so", |
| 196 | + f"lib{lib_base_name}.dylib", # macOS native dynamic library |
| 197 | + f"lib{lib_base_name}.so", # Fallback for cross-platform compatibility |
73 | 198 | ] |
74 | | - |
75 | 199 | base_paths.extend([ |
76 | | - "/usr/local/lib", |
77 | | - "/opt/homebrew/lib", |
78 | | - "/usr/lib", |
| 200 | + "/usr/local/lib", # Local user libraries |
| 201 | + "/opt/homebrew/lib", # Homebrew installation (macOS) |
| 202 | + "/usr/lib", # macOS system libraries |
79 | 203 | ]) |
80 | 204 |
|
81 | 205 | elif sys.platform == "win32": |
82 | 206 | lib_names = [ |
83 | | - f"{lib_base_name}.dll", |
84 | | - f"lib{lib_base_name}.dll", |
| 207 | + f"{lib_base_name}.dll", # Standard Windows dynamic library |
| 208 | + f"lib{lib_base_name}.dll", # Some frameworks use 'lib' prefix |
85 | 209 | ] |
86 | 210 | else: |
87 | | - raise RuntimeError("Unsupported platform") |
| 211 | + raise RuntimeError(f"Unsupported platform: {sys.platform}") |
88 | 212 |
|
89 | | - cdll_args = dict() # type: ignore |
| 213 | + cdll_args = {} |
90 | 214 |
|
91 | | - # Add the library directory to the DLL search path on Windows (if needed) |
92 | 215 | if sys.platform == "win32": |
93 | | - for base_path in base_paths: |
94 | | - p = pathlib.Path(base_path) |
95 | | - if p.exists() and p.is_dir(): |
96 | | - os.add_dll_directory(str(p)) |
97 | | - os.environ["PATH"] = str(p) + os.pathsep + os.environ["PATH"] |
98 | | - |
99 | | - if sys.platform == "win32" and sys.version_info >= (3, 9): |
100 | | - for base_path in base_paths: |
101 | | - p = pathlib.Path(base_path) |
102 | | - if p.exists() and p.is_dir(): |
103 | | - os.add_dll_directory(str(p)) |
104 | | - if "CUDA_PATH" in os.environ: |
105 | | - cuda_path = os.environ["CUDA_PATH"] |
106 | | - sub_dirs_to_add = [ |
107 | | - "bin", |
108 | | - os.path.join("bin", "x64"), # CUDA 13.0+ |
109 | | - "lib", |
110 | | - os.path.join("lib", "x64") |
111 | | - ] |
112 | | - for sub_dir in sub_dirs_to_add: |
113 | | - full_path = os.path.join(cuda_path, sub_dir) |
114 | | - if os.path.exists(full_path): |
115 | | - os.add_dll_directory(full_path) |
116 | | - |
117 | | - if "HIP_PATH" in os.environ: |
118 | | - os.add_dll_directory(os.path.join(os.environ["HIP_PATH"], "bin")) |
119 | | - os.add_dll_directory(os.path.join(os.environ["HIP_PATH"], "lib")) |
120 | | - |
121 | | - if "VULKAN_SDK" in os.environ: |
122 | | - os.add_dll_directory(os.path.join(os.environ["VULKAN_SDK"], "Bin")) |
123 | | - os.add_dll_directory(os.path.join(os.environ["VULKAN_SDK"], "Lib")) |
124 | | - |
125 | | - cdll_args["winmode"] = ctypes.RTLD_GLOBAL |
| 216 | + _add_library_search_paths(base_paths) |
| 217 | + _add_optional_backend_paths() |
| 218 | + if sys.version_info >= (3, 9): |
| 219 | + cdll_args["winmode"] = ctypes.RTLD_GLOBAL |
126 | 220 |
|
127 | 221 | errors = [] |
128 | 222 |
|
129 | | - # First, try to find an available library through the system |
130 | | - lib_path = find_library(lib_base_name) |
131 | | - if lib_path: |
132 | | - try: |
133 | | - return ctypes.CDLL(lib_path, **cdll_args) |
134 | | - except Exception as e: |
135 | | - errors.append(f"{lib_path}: {e}") |
136 | | - |
137 | | - # Then fallback to manually checking the list of paths. |
| 223 | + # Step 1: Try to load bundled libraries from provided paths |
138 | 224 | for base_path in base_paths: |
139 | 225 | for lib_name in lib_names: |
140 | 226 | lib_path = pathlib.Path(base_path) / lib_name |
141 | | - |
142 | 227 | if lib_path.exists(): |
143 | 228 | try: |
144 | | - return ctypes.CDLL(str(lib_path), **cdll_args) |
| 229 | + lib = ctypes.CDLL(str(lib_path), **cdll_args) |
| 230 | + print(f"[llama_cpp_python] Loaded {lib_base_name}: {lib_path}") |
| 231 | + return lib |
145 | 232 | except Exception as e: |
146 | 233 | errors.append(f"{lib_path}: {e}") |
147 | 234 |
|
148 | | - # Include directory contents only in the failure path to avoid extra work during successful imports. |
| 235 | + # Step 2: System-level fallback using find_library() |
| 236 | + lib_path = find_library(lib_base_name) |
| 237 | + if lib_path: |
| 238 | + try: |
| 239 | + lib = ctypes.CDLL(lib_path, **cdll_args) |
| 240 | + print(f"[llama_cpp_python] Loaded {lib_base_name}: {lib_path}") |
| 241 | + return lib |
| 242 | + except Exception as e: |
| 243 | + errors.append(f"{lib_path}: {e}") |
| 244 | + |
| 245 | + # Failure path: Only include directory contents to avoid redundant work |
| 246 | + # during successful imports. |
149 | 247 | raise RuntimeError( |
150 | 248 | f"Failed to load '{lib_base_name}' from {base_paths}\n" |
151 | 249 | + "\n".join(errors) |
|
0 commit comments