ViennaLS
Loading...
Searching...
No Matches
lsOxidationGpuInterface.hpp
Go to the documentation of this file.
1// C++ (g++) interface to the GPU linear solves of the oxidation solvers.
2//
3// This header is safe to include from any .cpp file compiled by g++. It
4// declares free functions that call into ViennaLS_GPU (gpu/lsOxidationGpu.cu,
5// compiled by nvcc) through the C ABI of lsOxidationGpuAbi.hpp.
6//
7// ── Why dlopen instead of linking ──────────────────────────────────────────
8// ViennaLS must not carry a hard (DT_NEEDED) dependency on libcudart /
9// libcusparse. If it did, the dynamic loader would fail to load ViennaLS at
10// all on a machine without a CUDA runtime — before a single line of our code
11// runs, so no amount of runtime checking could recover. That is exactly what
12// broke the 5.8.3 PyPI wheel.
13//
14// Instead, the CUDA-linked code lives in a separate ViennaLS_GPU shared
15// library which is opened lazily the first time the GPU path is requested.
16// If it (or the CUDA runtime it needs) cannot be loaded, the solves report
17// the GPU as unavailable and callers fall back to the CPU solver, with the
18// reason available from gpuGetLastErrorMessage().
19
20#pragma once
21
22#ifdef VIENNALS_GPU_BICGSTAB
23
24#include "lsOxidationGpuAbi.hpp"
25
26#include <cstddef>
27#include <cstdint>
28#include <cstdlib>
29#include <string>
30
31#if defined(_WIN32)
32#include <windows.h>
33#else
34#include <dlfcn.h>
35#endif
36
37namespace viennals {
38namespace gpu {
39namespace detail {
40
41#if defined(_WIN32)
42using LibraryHandle = HMODULE;
43inline LibraryHandle openLibrary(const char *path) {
44 return LoadLibraryA(path);
45}
46inline void *findSymbol(LibraryHandle lib, const char *name) {
47 return reinterpret_cast<void *>(GetProcAddress(lib, name));
48}
49inline std::string lastLoadError() {
50 return "LoadLibrary failed (error " + std::to_string(GetLastError()) + ")";
51}
52inline const char *gpuLibraryName() { return "ViennaLS_GPU.dll"; }
53inline char pathSeparator() { return '\\'; }
54#else
55using LibraryHandle = void *;
56inline LibraryHandle openLibrary(const char *path) {
57 return dlopen(path, RTLD_NOW | RTLD_LOCAL);
58}
59inline void *findSymbol(LibraryHandle lib, const char *name) {
60 return dlsym(lib, name);
61}
62inline std::string lastLoadError() {
63 const char *err = dlerror();
64 return err ? std::string(err) : std::string("unknown dynamic loader error");
65}
66inline const char *gpuLibraryName() { return "libViennaLS_GPU.so"; }
67inline char pathSeparator() { return '/'; }
68#endif
69
72inline std::string currentModuleDirectory() {
73 std::string path;
74#if defined(_WIN32)
75 HMODULE module = nullptr;
76 if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
77 GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
78 reinterpret_cast<LPCSTR>(&currentModuleDirectory),
79 &module) &&
80 module) {
81 char buffer[MAX_PATH] = {};
82 if (GetModuleFileNameA(module, buffer, MAX_PATH))
83 path = buffer;
84 }
85#else
86 Dl_info info{};
87 if (dladdr(reinterpret_cast<const void *>(&currentModuleDirectory), &info) &&
88 info.dli_fname)
89 path = info.dli_fname;
90#endif
91 const auto pos = path.find_last_of(pathSeparator());
92 return pos == std::string::npos ? std::string() : path.substr(0, pos);
93}
94
98struct GpuRuntime {
99 bool available = false;
100 std::string status;
101
102 int (*abiVersion)(void) = nullptr;
103 const char *(*lastErrorMessage)(void) = nullptr;
104 int (*diffusionSolve)(int64_t, const int64_t *, const int64_t *,
105 const double *, const double *, double *, unsigned,
106 double, unsigned *) = nullptr;
107 void *(*macSetup)(int64_t, const int64_t *, const int64_t *, const double *,
108 int64_t, int, const double *) = nullptr;
109 int (*macSolve)(void *, int64_t, const int64_t *, const int64_t *,
110 const double *, const double *, double *, unsigned, double,
111 unsigned *, double *) = nullptr;
112 void (*macFree)(void *) = nullptr;
113
114 GpuRuntime() { load(); }
115
116 void load() {
117 // Search order: explicit override, next to the calling binary, the
118 // wheel's bundled library folder, then the default loader search path.
119 std::string candidates[5];
120 int count = 0;
121 if (const char *override = std::getenv("VIENNALS_GPU_LIBRARY"))
122 candidates[count++] = override;
123 const std::string moduleDir = currentModuleDirectory();
124 if (!moduleDir.empty()) {
125 // Next to the caller (Python package layout), then the wheel's bundled
126 // library folder, then ../lib for a bin/ + lib/ install prefix.
127 candidates[count++] = moduleDir + pathSeparator() + gpuLibraryName();
128 candidates[count++] = moduleDir + pathSeparator() + ".." +
129 pathSeparator() + "viennals.libs" +
130 pathSeparator() + gpuLibraryName();
131 candidates[count++] = moduleDir + pathSeparator() + ".." +
132 pathSeparator() + "lib" + pathSeparator() +
133 gpuLibraryName();
134 }
135 candidates[count++] = gpuLibraryName();
136
137 LibraryHandle lib = nullptr;
138 std::string attempts;
139 for (int i = 0; i < count && !lib; ++i) {
140 lib = openLibrary(candidates[i].c_str());
141 if (!lib) {
142 // Report every attempt. The interesting failure is usually not the
143 // last one: when the library is found but the CUDA runtime is not,
144 // that candidate reports "libcudart.so.12: cannot open shared object
145 // file", while the final bare-soname attempt only reports that the
146 // library itself is missing. Keeping all of them means the message
147 // names the real cause instead of the last symptom.
148 attempts += "\n " + candidates[i] + ": " + lastLoadError();
149 }
150 }
151
152 if (!lib) {
153 status = "ViennaLS_GPU could not be loaded, so the GPU solver is "
154 "unavailable; using the CPU solver instead. Attempts:" +
155 attempts;
156 return;
157 }
158
159 if (!resolveAll(lib)) {
160 status = "ViennaLS_GPU is missing expected entry points; it is probably "
161 "from a different ViennaLS version. Using the CPU solver "
162 "instead.";
163 return;
164 }
165
166 if (abiVersion() != VIENNALS_GPU_ABI_VERSION) {
167 status = "ViennaLS_GPU reports ABI version " +
168 std::to_string(abiVersion()) + " but ViennaLS expects " +
169 std::to_string(VIENNALS_GPU_ABI_VERSION) +
170 ". Using the CPU solver instead.";
171 return;
172 }
173
174 available = true;
175 status = "ViennaLS_GPU loaded.";
176 }
177
180 bool resolveAll(LibraryHandle lib) {
181 return resolve(lib, "viennalsGpuAbiVersion", abiVersion) &&
182 resolve(lib, "viennalsGpuGetLastErrorMessage", lastErrorMessage) &&
183 resolve(lib, "viennalsGpuDiffusionSolve", diffusionSolve) &&
184 resolve(lib, "viennalsGpuMacSetup", macSetup) &&
185 resolve(lib, "viennalsGpuMacSolve", macSolve) &&
186 resolve(lib, "viennalsGpuMacFree", macFree);
187 }
188
189 template <class Fn>
190 static bool resolve(LibraryHandle lib, const char *name, Fn &target) {
191 target = reinterpret_cast<Fn>(findSymbol(lib, name));
192 return target != nullptr;
193 }
194};
195
199inline const GpuRuntime &runtime() {
200 static GpuRuntime instance;
201 return instance;
202}
203
204} // namespace detail
205
208inline bool gpuRuntimeAvailable() { return detail::runtime().available; }
209
211inline const char *gpuRuntimeStatusMessage() {
212 return detail::runtime().status.c_str();
213}
214
215// Human-readable detail for the last GPU failure on this thread. Falls back
216// to the loader status when the library never loaded at all.
217inline const char *gpuGetLastErrorMessage() {
218 const auto &rt = detail::runtime();
219 return rt.available ? rt.lastErrorMessage() : rt.status.c_str();
220}
221
222// The diffusion solve of OxidationDiffusion (see viennalsGpuDiffusionSolve):
223// 1 when it ran and gave finite values, 0 when the GPU is unavailable (x
224// untouched), -1 when it failed.
225inline int gpuDiffusionSolve(std::int64_t n, const std::int64_t *ptr,
226 const std::int64_t *col, const double *val,
227 const double *rhs, double *x,
228 unsigned maxIterations, double absTolerance,
229 unsigned &outIterations) {
230 const auto &rt = detail::runtime();
231 if (!rt.available)
232 return 0;
233 return rt.diffusionSolve(n, ptr, col, val, rhs, x, maxIterations,
234 absTolerance, &outIterations);
235}
236
237// The 3-D velocity-pressure solve of OxidationDeformation: preconditioner
238// setup for the equilibrated CSR system (see viennalsGpuMacSetup), nullptr
239// when the GPU is unavailable or the setup fails.
240inline void *gpuMacSetup(std::int64_t n, const std::int64_t *ptr,
241 const std::int64_t *col, const double *val,
242 std::int64_t nvel, int dim, const double *nullspace) {
243 const auto &rt = detail::runtime();
244 return rt.available ? rt.macSetup(n, ptr, col, val, nvel, dim, nullspace)
245 : nullptr;
246}
247
248// FGMRES with a setup's preconditioner on A x = rhs (x: initial guess on
249// entry, solution on exit). False when the solve did not run or gave
250// non-finite values.
251inline bool gpuMacSolve(void *handle, std::int64_t n, const std::int64_t *ptr,
252 const std::int64_t *col, const double *val,
253 const double *rhs, double *x, unsigned maxIterations,
254 double tolerance, unsigned &outIterations,
255 double &outResidual) {
256 const auto &rt = detail::runtime();
257 return rt.available &&
258 rt.macSolve(handle, n, ptr, col, val, rhs, x, maxIterations, tolerance,
259 &outIterations, &outResidual) != 0;
260}
261
262// Free a setup. Safe to call with nullptr.
263inline void gpuMacFree(void *handle) {
264 const auto &rt = detail::runtime();
265 if (rt.available)
266 rt.macFree(handle);
267}
268
269} // namespace gpu
270} // namespace viennals
271
272#endif // VIENNALS_GPU_BICGSTAB
#define VIENNALS_GPU_ABI_VERSION
Definition lsOxidationGpuAbi.hpp:22
Definition lsAdvect.hpp:41