Repository navigation
Expand file tree
/
Copy pathOpenBlasEngine.Binding.cs
More file actions
407 lines (381 loc) · 22.7 KB
/
Copy pathOpenBlasEngine.Binding.cs
File metadata and controls
407 lines (381 loc) · 22.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
using System;
using System.Runtime.CompilerServices;
using NumSharp;
using NumSharp.Backends;
namespace NumSharp.Interop.OpenBLAS
{
/// <summary>
/// Installs (and describes) the external-CBLAS matrix-product backend.
/// </summary>
/// <remarks>
/// <para>
/// <b>Referencing this package is the opt-in.</b> A module initializer runs
/// <see cref="TryEnable"/> when the assembly loads: if a CBLAS library can be found, an
/// <see cref="OpenBlasBackend"/> is assigned to the engine's <see cref="TensorEngine.Blas"/>
/// property and <c>np.dot</c> / <c>np.matmul</c> compute float32/float64 products through
/// it; if none can be found NOTHING changes and NumSharp keeps doing its own managed math.
/// Loading never throws at module load — an app that merely references the package cannot
/// be broken by a missing native library.
/// </para>
/// <para>
/// Call <see cref="Enable"/> explicitly to pin a specific library or thread count — which is
/// what byte-parity with a given NumPy requires, since the result bits depend on both. Call
/// <see cref="Disable"/> to go back to NumSharp's own SIMD GEMM.
/// </para>
/// <example>
/// <code>
/// // Byte-identical with a pip-installed numpy: name that wheel's own BLAS.
/// OpenBlasEngine.Enable(@"…\site-packages\numpy.libs\libscipy_openblas64_-74a4….dll", threads: 1);
/// Console.WriteLine(OpenBlasEngine.Info);
/// OpenBlasEngine.Disable(); // back to the built-in managed engine
/// </code>
/// </example>
/// </remarks>
public static partial class OpenBlasEngine
{
/// <summary>
/// True when this package's backend is installed on the default engine AND has a CBLAS
/// library to call.
/// </summary>
/// <remarks>
/// Both halves are load-bearing. The backend declines every operand when no library is
/// loaded, so an installed-but-unbound backend silently computes nothing at all — every
/// product falls back to NumSharp's managed kernels and the answer stays plausible.
/// Reporting true there would make this property claim parity it is not delivering.
/// </remarks>
public static bool Enabled
=> OpenBlasNative.IsLoaded && BackendFactory.GetEngine().Blas is OpenBlasBackend;
/// <summary>
/// True when the loaded library also provides the LAPACK LU routines the <c>np.linalg</c>
/// factorisations need (<c>det</c>/<c>slogdet</c>/<c>solve</c>/<c>inv</c>, and everything
/// built on them — <c>matrix_power</c> with a negative exponent, <c>tensorinv</c>,
/// <c>tensorsolve</c>).
/// </summary>
/// <remarks>
/// A full OpenBLAS (including the bundled scipy-openblas) exports LAPACK; a bare reference
/// CBLAS does not. When it does not, those functions raise exactly as they would with no
/// backend installed, while <c>dot</c>/<c>matmul</c> still accelerate — the factorisations
/// have no managed fallback, unlike the products.
/// </remarks>
public static bool LapackAvailable => OpenBlasNative.IsLoaded && OpenBlasNative.IsLapackLoaded;
/// <summary>
/// True when the loaded library also exports the complex128 CBLAS products the parity
/// backend needs for <c>np.dot</c>/<c>np.matmul</c> and the five product gufuncs
/// (<c>inner</c>/<c>vdot</c>/<c>vecdot</c>/<c>matvec</c>/<c>vecmat</c>) on complex128 —
/// <c>zgemm</c>/<c>zgemv</c>/<c>zsyrk</c>/<c>zdotu_sub</c>/<c>zdotc_sub</c>/<c>zaxpy</c>.
/// </summary>
/// <remarks>
/// A full OpenBLAS (including the bundled scipy-openblas) exports the complex products; a
/// bare real-only reference CBLAS does not. When it does not, complex <c>np.dot</c>/
/// <c>np.matmul</c> and the complex product gufuncs fall through to NumSharp's managed
/// complex kernels — a correct answer, but NOT byte-identical to NumPy (complex float
/// accumulation is not associative). This is the mirror of <see cref="LapackAvailable"/>
/// for the products, and is what a byte-exact live-parity gate keys off to decline a host
/// where the complex products are not loaded rather than assert against the managed kernel.
/// </remarks>
public static bool ComplexProductsAvailable
=> OpenBlasNative.IsLoaded && OpenBlasNative.IsComplexBlasLoaded;
/// <summary>
/// Path, symbol scheme, integer width, thread count and build string of the loaded
/// library, or null when none is loaded.
/// </summary>
public static string Info
{
get
{
if (!OpenBlasNative.IsLoaded)
return null;
var config = OpenBlasNative.GetConfig();
var image = OpenBlasNative.LoadedImagePath;
bool relocated = !string.IsNullOrEmpty(image) &&
!string.Equals(image, OpenBlasNative.LibraryPath, StringComparison.Ordinal);
return $"{OpenBlasNative.LibraryPath} [symbols {OpenBlasNative.SymbolScheme}, " +
$"{(OpenBlasNative.IsIlp64 ? "ILP64" : "LP64")}, threads {OpenBlasNative.GetNumThreads()}]" +
(relocated ? $" (mapped from {image})" : string.Empty) +
(config == null ? string.Empty : " " + config);
}
}
/// <summary>
/// Loads a CBLAS library and installs it as the engine's matrix-product backend.
/// </summary>
/// <param name="library">
/// Path of the CBLAS shared library (or a directory holding one), or null to
/// auto-discover: the <c>NUMSHARP_OPENBLAS_LIBRARY</c> environment variable (binding), then
/// the override path(s) (<c>NUMSHARP_OPENBLAS_SEARCH_PATH</c> / a build-recorded
/// <c>OpenBlasPath</c>), then a build-staged version override (required — a miss
/// throws), then an explicit OpenBLAS root (<c>OPENBLAS_HOME</c>/<c>OPENBLAS_ROOT</c>),
/// then the bundled runtime asset, then ambient machine tooling (system install
/// directories, bare loader names, a PATH sweep).
/// <b>A named library is binding</b> — it is used as given and never silently replaced
/// with another, because parity is a claim about one specific binary.
/// </param>
/// <param name="threads">
/// BLAS worker threads to pin, or 0 to leave the current setting. <b>The result bits
/// depend on this</b> — measured: 1, 2, 4 and 24 threads give four different answers —
/// so a parity run must pin both sides to the same count.
/// </param>
/// <remarks>
/// <b>A failed call is a no-op.</b> When the requested library cannot be loaded this
/// throws having changed nothing — an already-working parity setup keeps working, and a
/// process that had none still has none. (It used to unload the good library first, so
/// a mistyped path demoted every subsequent product back to the managed kernels while
/// <see cref="Enabled"/> still said true.)
/// </remarks>
/// <param name="coreType">
/// The OpenBLAS <c>DYNAMIC_ARCH</c> micro-kernel to force (e.g. <see cref="CoreType"/>),
/// or null to let OpenBLAS pick the best one this CPU supports — which is what NumPy
/// does, and therefore the right default for matching a NumPy on the same machine.
/// Pin it only to make results reproducible across DIFFERENT machines; see
/// <see cref="CoreType"/> for the trade-off and the hazard.
/// </param>
/// <exception cref="DllNotFoundException">No CBLAS library could be located or loaded.</exception>
/// <exception cref="EntryPointNotFoundException">The library is not a CBLAS provider.</exception>
/// <exception cref="PlatformNotSupportedException">
/// <paramref name="coreType"/> names kernels this CPU cannot execute.
/// </exception>
public static void Enable(string library = null, int threads = 0, string coreType = null)
{
EnsureThreadingRegistered();
if (!OpenBlasNative.IsLoaded || (library != null && library != OpenBlasNative.LibraryPath))
{
OpenBlasNative.Load(library, coreType);
}
else if (!string.IsNullOrWhiteSpace(coreType))
{
// The library is already loaded, so its micro-kernel is already chosen and this
// request cannot be honoured. Dropping it silently would leave the caller believing
// the kernel is pinned while the bits are whatever this CPU picked — so it either
// already holds or it throws.
OpenBlasNative.VerifyCoreTypeAlreadyInEffect(coreType);
}
if (threads > 0)
// Route through the unified surface so TensorEngine.Threading and OpenBLAS agree; the
// OpenBLAS knob's applier (registered above) is what actually calls
// openblas_set_num_threads — the same native effect Enable had before.
TensorEngine.Threading.SetThreads(TensorEngine.Threading.OpenBlas, threads);
if (!(BackendFactory.GetEngine().Blas is OpenBlasBackend))
BackendFactory.GetEngine().Blas = new OpenBlasBackend();
}
private static int _threadingRegistered;
/// <summary>
/// Upgrades <see cref="TensorEngine.Threading"/>'s OpenBLAS knob with a native reader and
/// applier — the per-module extension point. Core registers the knob as a managed-only env
/// var (it cannot reach a native <c>getenv</c>); this attaches a reader that reports the live
/// <c>openblas_get_num_threads()</c> and an applier that pushes a change through the CRT and
/// <c>openblas_set_num_threads</c>. Idempotent, and re-registration re-seeds from any
/// pre-existing <c>OPENBLAS_NUM_THREADS</c> (the source of truth).
/// </summary>
private static void EnsureThreadingRegistered()
{
if (System.Threading.Interlocked.Exchange(ref _threadingRegistered, 1) == 1)
return;
TensorEngine.Threading.Register(
TensorEngine.Threading.OpenBlas,
"OPENBLAS_NUM_THREADS",
reader: () =>
{
int n = OpenBlasNative.IsLoaded ? OpenBlasNative.GetNumThreads() : -1;
return n > 0 ? n : (int?)null;
},
applier: OpenBlasNative.ApplyThreadCount);
}
/// <summary>
/// The OpenBLAS micro-kernel the committed <c>matmul_parity</c> corpus was generated
/// with, and the one to pin for results that reproduce on another machine.
/// </summary>
/// <remarks>
/// <para>
/// Bundling the binary fixes only two of the three things the result bits depend on.
/// The library build is now pinned by the package and the worker-thread count by
/// <see cref="Enable"/>'s <c>threads</c> argument — but OpenBLAS is built
/// <c>DYNAMIC_ARCH</c>, so it still chooses a micro-kernel from the CPU it finds itself
/// on, and different kernels accumulate in a different order. Measured on one host by
/// forcing the choice: Haswell, Nehalem, Sandybridge and Katmai each produced different
/// bytes for the same float32 product. So "same binary" is necessary and not sufficient.
/// </para>
/// <para>
/// Passing this as <c>coreType</c> closes that last gap for any x86-64 CPU with AVX2 and
/// FMA (Haswell 2013 and later, and every AMD Zen), which is what makes the host-pinned
/// corpus replayable on a machine that would otherwise dispatch a newer kernel.
/// </para>
/// <para>
/// <b>It is NOT the default, deliberately.</b> The package's main promise is matching the
/// NumPy on the same machine, and that NumPy dispatches by CPU — so pinning here would
/// CREATE a divergence on any host newer than Haswell. Pin both sides or neither: the
/// same <c>OPENBLAS_CORETYPE</c> variable drives NumPy too, because it is the same
/// library.
/// </para>
/// <para>
/// <b>Hazard:</b> this overrides OpenBLAS' CPU detection rather than requesting a
/// preference, so naming kernels the CPU cannot run terminates the process with an
/// illegal instruction. <see cref="Enable"/> rejects the combinations it can recognise
/// with a <see cref="PlatformNotSupportedException"/> first.
/// </para>
/// <para>
/// Only effective when the call actually loads the library: OpenBLAS reads the variable
/// while initialising and never re-reads it, so it cannot be changed afterwards.
/// </para>
/// </remarks>
public const string CoreType = "Haswell";
/// <summary>
/// The <c>DYNAMIC_ARCH</c> micro-kernel family OpenBLAS actually dispatched to
/// (e.g. "Haswell"), or null when no library is loaded or it does not report one.
/// </summary>
public static string CoreName => OpenBlasNative.IsLoaded ? OpenBlasNative.GetCoreName() : null;
/// <summary>
/// Full path of the CBLAS library currently loaded, or null when none is.
/// </summary>
/// <remarks>
/// Exposed because "which binary answered?" is a correctness question here, not a
/// diagnostic one — the file this points at is what a parity claim is about, and it is
/// the input to reproducing or hashing a run. <see cref="Info"/> embeds it in prose;
/// this is the machine-readable form.
/// </remarks>
public static string LibraryPath => OpenBlasNative.IsLoaded ? OpenBlasNative.LibraryPath : null;
/// <summary>
/// Full path of the file the OS loader actually mapped, or null when none is loaded.
/// </summary>
/// <remarks>
/// Equals <see cref="LibraryPath"/> everywhere except on macOS when the bundled
/// scipy-openblas dylib could not be loaded in place: its load commands name the vendored
/// Fortran runtime through <c>@loader_path/../.dylibs/…</c>, a path NuGet never delivers,
/// so when that folder cannot be created next to the dylib (a read-only install, or a
/// RID-specific/single-file publish whose flattened layout puts <c>../.dylibs</c> OUTSIDE
/// the application) the dylib and its dependencies are copied to a per-user cache
/// (<c>NUMSHARP_OPENBLAS_CACHE_DIR</c>, else <c>~/.cache/NumSharp/openblas/dyld/<sha256>/</c>)
/// in the exact relative layout the load commands encode, and THAT copy is mapped. It is
/// verified byte-identical to <see cref="LibraryPath"/> on every load, which is why
/// <see cref="LibraryPath"/> — the file a parity pin hashes — keeps naming the original.
/// When the folder was writable the dependencies are symlinked in place instead and the
/// two paths are equal.
/// </remarks>
public static string LoadedImagePath => OpenBlasNative.IsLoaded ? OpenBlasNative.LoadedImagePath : null;
/// <summary>
/// True when the loaded library is the one this package bundles (as opposed to one
/// discovered on the machine, staged by a build-time override, or named by the caller).
/// </summary>
/// <remarks>
/// <para>
/// Decided by CONTENT: the loaded file must hash to one of the manifest-pinned bundled
/// libraries (the pin is embedded in this assembly — <see cref="BundledAssetPin"/>), the
/// same identity rule the host-pinned parity corpus uses. Folder layout is not the
/// criterion, because it is not stable: a PackageReference restore keeps
/// <c>runtimes/<rid>/native/</c>, while a RID-specific or single-file publish
/// flattens the very same binary into the app root — and it is still the bundle there.
/// (A build with no embedded pin falls back to that layout heuristic.)
/// </para>
/// <para>
/// Content alone is not enough either, in the other direction: a build-staged version
/// override can be content-identical to the bundle (the delivery design's "same binary"
/// invariant) yet carries the opposite contract. The source marker the build writes
/// (<c>openblas.source.json</c>) is what tells them apart — a folder it declares an
/// override read-location is an override, not the bundle, whatever the bytes. Both checks
/// are evaluated live, so removing a marker flips the answer without a reload.
/// </para>
/// </remarks>
public static bool IsBundledLibrary
{
get
{
var path = LibraryPath;
if (string.IsNullOrEmpty(path))
return false;
string dir;
try
{
if (!System.IO.File.Exists(path))
return false; // a bare loader name resolved by the OS is never the bundle
dir = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path));
}
catch (Exception e) when (e is ArgumentException or System.IO.IOException or NotSupportedException or System.Security.SecurityException)
{
return false;
}
if (!string.IsNullOrEmpty(dir) && OpenBlasSourceMarker.DeclaresOverrideFor(dir))
return false;
if (BundledAssetPin.Available)
return BundledAssetPin.IsPinnedBinary(path);
return !string.IsNullOrEmpty(dir) &&
System.IO.Path.GetFileName(dir).Equals("native", StringComparison.OrdinalIgnoreCase) &&
dir.IndexOf("runtimes", StringComparison.OrdinalIgnoreCase) >= 0;
}
}
/// <summary>
/// Uninstalls the backend: NumSharp goes back to its own managed SIMD GEMM. The native
/// library stays loaded (unloading a BLAS mid-process is not worth the risk), so a later
/// <see cref="Enable"/> is free.
/// </summary>
public static void Disable()
{
var engine = BackendFactory.GetEngine();
if (engine.Blas is OpenBlasBackend)
engine.Blas = null;
}
/// <summary>
/// <see cref="Enable"/> that reports failure instead of throwing — the module-load path.
/// </summary>
/// <returns>True when the backend is installed.</returns>
/// <remarks>
/// Catches EVERYTHING on purpose. This runs from a <c>[ModuleInitializer]</c>, where an
/// escaping exception is not a failed opt-in but a <c>TypeInitializationException</c> on
/// every later touch of this assembly — i.e. merely referencing the package would break
/// the app, which is the one thing the design promises it cannot do. Library discovery
/// walks PATH and the filesystem, so the reachable failures are not limited to the
/// three loader exceptions: an unreadable directory alone raises
/// <see cref="System.UnauthorizedAccessException"/>.
/// </remarks>
public static bool TryEnable(string library = null, int threads = 0)
{
try
{
Enable(library, threads);
return true;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Runs when this assembly is loaded: referencing the package is all the wiring there
/// is. Named for its defining default job — making the BUNDLED runtime asset work with
/// zero configuration — though it runs the whole discovery (override path → staged
/// version override → bundle → machine tooling). Deliberately silent on failure — a
/// missing native library leaves NumSharp exactly as it was, computing everything
/// itself — with ONE exception: a version override the build staged as a hard
/// requirement that fails to load is reported to stderr (the backend still stays
/// uninstalled rather than substituted, and an explicit <see cref="Enable"/> throws
/// <see cref="OpenBlasRequiredOverrideException"/> with the same message).
/// </summary>
/// <remarks>
/// Opt out with <c>NUMSHARP_OPENBLAS_BUNDLE_AUTOINSTALL=0</c>. (The pre-rename
/// <c>NUMSHARP_BLAS_BUNDLE_AUTOINSTALL</c> and the earlier <c>NUMSHARP_BLAS_AUTOINSTALL</c>
/// spellings are RETIRED and deliberately ignored — each rename shipped unreleased, so
/// there is no installed base to migrate.)
/// </remarks>
[ModuleInitializer]
internal static void BundleAutoinstall()
{
if (!EnvVars.OpenBlasBundleAutoinstall)
return;
try
{
Enable();
}
catch (OpenBlasRequiredOverrideException e)
{
// The consumer PINNED a version at build time; silently running with different bits
// is the one failure a pin exists to prevent. Throwing here would surface as a
// TypeInitializationException at some unrelated first touch of this assembly, so:
// loud on stderr, backend left uninstalled (never substituted), and any explicit
// OpenBlasEngine.Enable() raises the full exception.
Console.Error.WriteLine("NumSharp.Interop.OpenBLAS: " + e.Message);
}
catch (Exception)
{
// Everything else is the ordinary no-BLAS-anywhere case: an app that merely
// references the package cannot be broken by a missing native library.
}
}
}
}