ViennaLS
Loading...
Searching...
No Matches
lsOxidationBiCGSTABInterface.hpp
Go to the documentation of this file.
1// C++ (g++) interface to the GPU BiCGSTAB solver.
2//
3// This header is safe to include from any .cpp file compiled by g++.
4// It only forward-declares GpuBiCGSTABBuffers (opaque handle) and
5// declares free functions that are implemented in
6// ViennaLS_GPU (lsOxidationBiCGSTABKernels.cu, compiled by nvcc).
7//
8// The actual CUDA kernels live in lsOxidationBiCGSTAB.cuh. That file
9// must never be included from a .cpp compiled by g++ — only from .cu files.
10//
11// ── Why dlopen instead of linking ──────────────────────────────────────────
12// ViennaLS must not carry a hard (DT_NEEDED) dependency on libcudart /
13// libcusparse. If it did, the dynamic loader would fail to load ViennaLS at
14// all on a machine without a CUDA runtime — before a single line of our code
15// runs, so no amount of runtime checking could recover. That is exactly what
16// broke the 5.8.3 PyPI wheel.
17//
18// Instead, the CUDA-linked code lives in a separate ViennaLS_GPU shared
19// library which is opened lazily the first time the GPU path is requested.
20// If it (or the CUDA runtime it needs) cannot be loaded, allocGpuBuffers()
21// returns nullptr and callers transparently fall back to the CPU solver, with
22// the reason available from gpuGetLastErrorMessage().
23
24#pragma once
25
26#ifdef VIENNALS_GPU_BICGSTAB
27
29
30#include <cstddef>
31#include <cstdint>
32#include <cstdlib>
33#include <string>
34
35#if defined(_WIN32)
36#include <windows.h>
37#else
38#include <dlfcn.h>
39#endif
40
41namespace viennals {
42namespace gpu {
43
44// Sentinel value used in the neighbor-ID array to mark boundary / out-of-bounds
45// faces (must match the constant in lsOxidationBiCGSTAB.cuh).
46static constexpr uint32_t kNoNode = 0xFFFFFFFFu;
47
48// Opaque handle — complete definition is in lsOxidationBiCGSTAB.cuh /
49// lsOxidationBiCGSTABKernels.cu. Consumers hold a raw pointer only.
50struct GpuBiCGSTABBuffers;
51
52namespace detail {
53
54#if defined(_WIN32)
55using LibraryHandle = HMODULE;
56inline LibraryHandle openLibrary(const char *path) {
57 return LoadLibraryA(path);
58}
59inline void *findSymbol(LibraryHandle lib, const char *name) {
60 return reinterpret_cast<void *>(GetProcAddress(lib, name));
61}
62inline std::string lastLoadError() {
63 return "LoadLibrary failed (error " + std::to_string(GetLastError()) + ")";
64}
65inline const char *gpuLibraryName() { return "ViennaLS_GPU.dll"; }
66inline char pathSeparator() { return '\\'; }
67#else
68using LibraryHandle = void *;
69inline LibraryHandle openLibrary(const char *path) {
70 return dlopen(path, RTLD_NOW | RTLD_LOCAL);
71}
72inline void *findSymbol(LibraryHandle lib, const char *name) {
73 return dlsym(lib, name);
74}
75inline std::string lastLoadError() {
76 const char *err = dlerror();
77 return err ? std::string(err) : std::string("unknown dynamic loader error");
78}
79inline const char *gpuLibraryName() { return "libViennaLS_GPU.so"; }
80inline char pathSeparator() { return '/'; }
81#endif
82
85inline std::string currentModuleDirectory() {
86 std::string path;
87#if defined(_WIN32)
88 HMODULE module = nullptr;
89 if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
90 GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
91 reinterpret_cast<LPCSTR>(&currentModuleDirectory),
92 &module) &&
93 module) {
94 char buffer[MAX_PATH] = {};
95 if (GetModuleFileNameA(module, buffer, MAX_PATH))
96 path = buffer;
97 }
98#else
99 Dl_info info{};
100 if (dladdr(reinterpret_cast<const void *>(&currentModuleDirectory), &info) &&
101 info.dli_fname)
102 path = info.dli_fname;
103#endif
104 const auto pos = path.find_last_of(pathSeparator());
105 return pos == std::string::npos ? std::string() : path.substr(0, pos);
106}
107
111struct GpuRuntime {
112 bool available = false;
113 std::string status;
114
115 int (*abiVersion)(void) = nullptr;
116 void *(*allocBuffers)(uint32_t, int, int) = nullptr;
117 void (*freeBuffers)(void *) = nullptr;
118 int (*isValid)(const void *) = nullptr;
119 const char *(*lastErrorMessage)(void) = nullptr;
120 int (*uploadNeighborIds)(void *, const uint32_t *, std::size_t) = nullptr;
121 int (*setupCSR)(void *, const uint32_t *, uint32_t, int) = nullptr;
122 int (*uploadSolverArrays)(void *, const double *, const double *,
123 const double *, uint32_t, std::size_t) = nullptr;
124 int (*uploadRhs)(void *, const double *, uint32_t) = nullptr;
125 int (*solveBiCGSTAB)(void *, double *, double, unsigned, double, unsigned *,
126 double *) = nullptr;
127
128 GpuRuntime() { load(); }
129
130 void load() {
131 // Search order: explicit override, next to the calling binary, the
132 // wheel's bundled library folder, then the default loader search path.
133 std::string candidates[5];
134 int count = 0;
135 if (const char *override = std::getenv("VIENNALS_GPU_LIBRARY"))
136 candidates[count++] = override;
137 const std::string moduleDir = currentModuleDirectory();
138 if (!moduleDir.empty()) {
139 // Next to the caller (Python package layout), then the wheel's bundled
140 // library folder, then ../lib for a bin/ + lib/ install prefix.
141 candidates[count++] = moduleDir + pathSeparator() + gpuLibraryName();
142 candidates[count++] = moduleDir + pathSeparator() + ".." +
143 pathSeparator() + "viennals.libs" +
144 pathSeparator() + gpuLibraryName();
145 candidates[count++] = moduleDir + pathSeparator() + ".." +
146 pathSeparator() + "lib" + pathSeparator() +
147 gpuLibraryName();
148 }
149 candidates[count++] = gpuLibraryName();
150
151 LibraryHandle lib = nullptr;
152 std::string attempts;
153 for (int i = 0; i < count && !lib; ++i) {
154 lib = openLibrary(candidates[i].c_str());
155 if (!lib) {
156 // Report every attempt. The interesting failure is usually not the
157 // last one: when the library is found but the CUDA runtime is not,
158 // that candidate reports "libcudart.so.12: cannot open shared object
159 // file", while the final bare-soname attempt only reports that the
160 // library itself is missing. Keeping all of them means the message
161 // names the real cause instead of the last symptom.
162 attempts += "\n " + candidates[i] + ": " + lastLoadError();
163 }
164 }
165
166 if (!lib) {
167 status = "ViennaLS_GPU could not be loaded, so the GPU solver is "
168 "unavailable; using the CPU solver instead. Attempts:" +
169 attempts;
170 return;
171 }
172
173 if (!resolveAll(lib)) {
174 status = "ViennaLS_GPU is missing expected entry points; it is probably "
175 "from a different ViennaLS version. Using the CPU solver "
176 "instead.";
177 return;
178 }
179
180 if (abiVersion() != VIENNALS_GPU_ABI_VERSION) {
181 status = "ViennaLS_GPU reports ABI version " +
182 std::to_string(abiVersion()) + " but ViennaLS expects " +
183 std::to_string(VIENNALS_GPU_ABI_VERSION) +
184 ". Using the CPU solver instead.";
185 return;
186 }
187
188 available = true;
189 status = "ViennaLS_GPU loaded.";
190 }
191
194 bool resolveAll(LibraryHandle lib) {
195 return resolve(lib, "viennalsGpuAbiVersion", abiVersion) &&
196 resolve(lib, "viennalsGpuAllocBuffers", allocBuffers) &&
197 resolve(lib, "viennalsGpuFreeBuffers", freeBuffers) &&
198 resolve(lib, "viennalsGpuIsValid", isValid) &&
199 resolve(lib, "viennalsGpuGetLastErrorMessage", lastErrorMessage) &&
200 resolve(lib, "viennalsGpuUploadNeighborIds", uploadNeighborIds) &&
201 resolve(lib, "viennalsGpuSetupCSR", setupCSR) &&
202 resolve(lib, "viennalsGpuUploadSolverArrays", uploadSolverArrays) &&
203 resolve(lib, "viennalsGpuUploadRhs", uploadRhs) &&
204 resolve(lib, "viennalsGpuSolveBiCGSTAB", solveBiCGSTAB);
205 }
206
207 template <class Fn>
208 static bool resolve(LibraryHandle lib, const char *name, Fn &target) {
209 target = reinterpret_cast<Fn>(findSymbol(lib, name));
210 return target != nullptr;
211 }
212};
213
217inline const GpuRuntime &runtime() {
218 static GpuRuntime instance;
219 return instance;
220}
221
222inline void *toHandle(GpuBiCGSTABBuffers *gpu) {
223 return reinterpret_cast<void *>(gpu);
224}
225inline const void *toHandle(const GpuBiCGSTABBuffers *gpu) {
226 return reinterpret_cast<const void *>(gpu);
227}
228
229} // namespace detail
230
233inline bool gpuRuntimeAvailable() { return detail::runtime().available; }
234
236inline const char *gpuRuntimeStatusMessage() {
237 return detail::runtime().status.c_str();
238}
239
240// Allocate GPU buffers for a solver with `n` nodes and `nFaces` (2*D) faces.
241// Returns nullptr if CUDA is unavailable.
242inline GpuBiCGSTABBuffers *allocGpuBuffers(uint32_t n, int nFaces,
243 bool useIlu0Preconditioner) {
244 const auto &rt = detail::runtime();
245 if (!rt.available)
246 return nullptr;
247 return reinterpret_cast<GpuBiCGSTABBuffers *>(
248 rt.allocBuffers(n, nFaces, useIlu0Preconditioner ? 1 : 0));
249}
250
251// Free previously allocated GPU buffers. Safe to call with nullptr.
252inline void freeGpuBuffers(GpuBiCGSTABBuffers *gpu) {
253 const auto &rt = detail::runtime();
254 if (rt.available)
255 rt.freeBuffers(detail::toHandle(gpu));
256}
257
258// Human-readable detail for the last GPU wrapper failure on this thread.
259// Falls back to the loader status when the library never loaded at all.
260inline const char *gpuGetLastErrorMessage() {
261 const auto &rt = detail::runtime();
262 return rt.available ? rt.lastErrorMessage() : rt.status.c_str();
263}
264
265// Is the buffer handle valid (non-null and successfully allocated)?
266inline bool gpuIsValid(const GpuBiCGSTABBuffers *gpu) {
267 const auto &rt = detail::runtime();
268 return rt.available && rt.isValid(detail::toHandle(gpu)) != 0;
269}
270
271// Upload geometry-fixed neighbor-ID array (face-major, kNoNode = 0xFFFFFFFF).
272// `count` must equal nFaces * n.
273inline bool gpuUploadNeighborIds(GpuBiCGSTABBuffers *gpu, const uint32_t *nb,
274 std::size_t count) {
275 const auto &rt = detail::runtime();
276 return rt.available &&
277 rt.uploadNeighborIds(detail::toHandle(gpu), nb, count) != 0;
278}
279
280// Build the CSR sparsity pattern from h_nb (face-major, length nFaces*n),
281// upload to the device, and run CUSPARSE symbolic analysis for ILU(0) and
282// the two triangular solves. Must be called after gpuUploadNeighborIds and
283// before the first gpuUploadSolverArrays / gpuSolveBiCGSTAB call.
284inline bool gpuSetupCSR(GpuBiCGSTABBuffers *gpu, const uint32_t *h_nb,
285 uint32_t n, int nFaces) {
286 const auto &rt = detail::runtime();
287 return rt.available &&
288 rt.setupCSR(detail::toHandle(gpu), h_nb, n, nFaces) != 0;
289}
290
291// Upload per-solve arrays (diag, b, faceCoeffs) and re-factorize ILU(0).
292// `diagLen` == n, `coeffLen` == nFaces * n.
293inline bool gpuUploadSolverArrays(GpuBiCGSTABBuffers *gpu, const double *diag,
294 const double *b, const double *coeff,
295 uint32_t diagLen, std::size_t coeffLen) {
296 const auto &rt = detail::runtime();
297 return rt.available && rt.uploadSolverArrays(detail::toHandle(gpu), diag, b,
298 coeff, diagLen, coeffLen) != 0;
299}
300
301// Upload only the RHS vector (d_b). Use when the matrix geometry is already
302// uploaded and only the right-hand side changes (e.g. successive Stokes
303// component solves that share the same stiffness matrix).
304inline bool gpuUploadRhs(GpuBiCGSTABBuffers *gpu, const double *b, uint32_t n) {
305 const auto &rt = detail::runtime();
306 return rt.available && rt.uploadRhs(detail::toHandle(gpu), b, n) != 0;
307}
308
309// Run GPU BiCGSTAB.
310// x (length n, host): initial guess on entry, solution on exit.
311// outResidual is the raw (unnormalized) max-abs residual on exit.
312// Returns true only when the GPU solve converged and produced finite values.
313inline bool gpuSolveBiCGSTAB(GpuBiCGSTABBuffers *gpu, double *x, double diagEps,
314 unsigned maxIter, double tolerance,
315 unsigned &outIterations, double &outResidual) {
316 const auto &rt = detail::runtime();
317 return rt.available &&
318 rt.solveBiCGSTAB(detail::toHandle(gpu), x, diagEps, maxIter, tolerance,
319 &outIterations, &outResidual) != 0;
320}
321
322} // namespace gpu
323} // namespace viennals
324
325#endif // VIENNALS_GPU_BICGSTAB
#define VIENNALS_GPU_ABI_VERSION
Definition lsOxidationBiCGSTABAbi.hpp:22
Definition lsAdvect.hpp:41