NumSharp.Interop.OpenBLAS
0.70.0
dotnet add package NumSharp.Interop.OpenBLAS --version 0.70.0
NuGet\Install-Package NumSharp.Interop.OpenBLAS -Version 0.70.0
<PackageReference Include="NumSharp.Interop.OpenBLAS" Version="0.70.0" />
<PackageVersion Include="NumSharp.Interop.OpenBLAS" Version="0.70.0" />
<PackageReference Include="NumSharp.Interop.OpenBLAS" />
paket add NumSharp.Interop.OpenBLAS --version 0.70.0
#r "nuget: NumSharp.Interop.OpenBLAS, 0.70.0"
#:package NumSharp.Interop.OpenBLAS@0.70.0
#addin nuget:?package=NumSharp.Interop.OpenBLAS&version=0.70.0
#tool nuget:?package=NumSharp.Interop.OpenBLAS&version=0.70.0
NumSharp.Interop.OpenBLAS
Optional external-CBLAS matrix-product backend for NumSharp.
NumSharp.Core is 100 % managed C# with no native dependency. Its own SIMD GEMM computes every
matrix product out of the box. This package is the alternative: reference it and np.dot /
np.matmul compute float32 / float64 products through OpenBLAS — both faster on large
matrices and bit-identical to NumPy.
dotnet add package NumSharp.Interop.OpenBLAS
using NumSharp;
using NumSharp.Interop.OpenBLAS;
// Referencing the package is the whole opt-in. A module initializer loads the bundled OpenBLAS
// for this machine's RID and installs the backend; if that fails, NOTHING changes and NumSharp
// keeps doing its own managed math.
var c = np.dot(a, b);
Console.WriteLine(OpenBlasEngine.Info);
// …\runtimes\win-x64\native\libscipy_openblas64_.dll [symbols scipy_*64_, ILP64, threads 24]
// OpenBLAS 0.3.31.dev USE64BITINT DYNAMIC_ARCH NO_AFFINITY Haswell MAX_THREADS=24
OpenBlasEngine.Disable(); // clears engine.Blas — back to NumSharp's managed SIMD GEMM
The bundled binary
The package ships the OpenBLAS binaries NumPy itself ships — not "an" OpenBLAS. NumPy 2.x does
not build its own: its wheels bundle the prebuilt scipy-openblas32 / scipy-openblas64 artifacts
published on PyPI by the openblas-libs project, pinned
in numpy's requirements/ci_requirements.txt. This package bundles those same artifacts at the same
pinned version, verified byte-for-byte: the DLL here and the one in numpy 2.4.2's numpy.libs/
have the identical SHA-256 (74a40872…, which is also where NumPy's mangled file name comes from —
delvewheel names the file by its hash).
So bit-parity no longer depends on having Python installed, or on which copy of a BLAS won a loader race. Bundled version: scipy-openblas 0.3.31.22.0 (OpenBLAS 0.3.31.dev), matching numpy 2.4.2.
| RID | distribution | RID | distribution |
|---|---|---|---|
win-x64 |
scipy-openblas64 | linux-musl-x64 |
scipy-openblas64 |
win-arm64 |
scipy-openblas32 † | linux-musl-arm64 |
scipy-openblas64 |
linux-x64 |
scipy-openblas64 | osx-x64 |
scipy-openblas64 |
linux-arm64 |
scipy-openblas64 | osx-arm64 |
scipy-openblas64 |
† not an inconsistency — NumPy's own build selects the LP64 distribution there, so matching it is
what parity requires. It exports scipy_cblas_sgemm with a 32-bit BLAS integer, and both schemes are
bound automatically. Both scipy distributions are first-class: scipy-openblas64 (ILP64, the
default on the seven other RIDs) and scipy-openblas32 (LP64). The 32-bit distribution is also the
only build PyPI publishes for 32-bit x86 (win32 / i686), so any 32-bit target must use it.
NuGet resolves runtimes/<rid>/native/ per machine, so a RID-specific publish carries only the one
asset. Licences travel with them in THIRD-PARTY-NOTICES.txt (OpenBLAS and its bundled LAPACK are
BSD-3-Clause; the statically linked libgfortran carries the GCC Runtime Library Exception, the same
basis on which NumPy and SciPy ship it in every wheel).
The vendored Fortran runtime, and why macOS needs a loader step
The Linux and macOS wheels do not statically link the GCC runtime: libgfortran (+ libquadmath,
libgcc_s) travel beside the main library under content-hashed names, and the main library's
DT_NEEDED / LC_LOAD_DYLIB entries name those files, so a system libgfortran can never
satisfy them. The package co-stages them, pinned in the manifest like the main library. On Linux
they sit beside the main in native/ and its $ORIGIN RUNPATH finds them — nothing to do.
macOS is different, and it is the one place the layout NuGet can deliver and the layout the binary
expects disagree. delocate parks the deps in a .dylibs/ folder beside the wheel's lib/
and rewrites the dylib's load commands to @loader_path/../.dylibs/<name>; NuGet delivers native
assets only from under runtimes/<rid>/native/ (nested paths included — a sibling folder is
silently dropped from every consumer). So the deps are packed nested, as
runtimes/osx-*/native/.dylibs/, and the package's loader (MacOsVendoredRuntime) makes the dylib
loadable at first load, after a plain dlopen has failed:
- In place — for the NuGet layout (
runtimes/<rid>/native/) it creates the relative directory symlinkruntimes/<rid>/.dylibs → native/.dylibs(or copies the files when a link cannot be made). Nothing is written outside the app's ownruntimes/tree, and it is free after the first process. This is the normal outcome for adotnet buildor a portable publish. - A per-user cache copy — when that folder is read-only, or the layout is flattened (a
RID-specific / self-contained / single-file publish drops the sub-folder, leaving the deps beside
the main in the app root, where
../.dylibswould be the parent of the app): the dylib and its deps are copied to<cache>/dyld/<sha256 of the dylib>/native/+.dylibs/— the exact relative layout the load commands encode — and that copy is what dyld maps. The entry is keyed by the dylib's content hash, every file is byte-compared to its source on every hit, and writes go to a temp directory renamed into place. Cache root:NUMSHARP_OPENBLAS_CACHE_DIR, else~/.cache/NumSharp/openblas(the same root the build-time override cache uses).
Either way OpenBlasEngine.LibraryPath keeps naming the file discovery chose — the one the parity
pin hashes — while OpenBlasEngine.LoadedImagePath reports the file dyld actually mapped, and
OpenBlasEngine.Info says (mapped from …) when the two differ. Pre-loading the deps by absolute
path cannot replace this (dyld matches loaded images by path or install name, and delocate's deps
carry the placeholder install name /DLC/scipy_openblas64/.dylibs/…), and relinking with
install_name_tool would need macOS tooling, re-signing, and would change the bytes the pin hashes.
Gate: tools/verify_package_consumer.sh — a PackageReference consumer, on all three OSes, in every
layout (portable, read-only, flattened publish).
Discovery order
(The delivery/discovery model is specified in docs/stale-docs/OPENBLAS_DELIVERY_DESIGN.md.)
- An explicit path, or
NUMSHARP_OPENBLAS_LIBRARY— binding, never substituted. - Override path(s):
NUMSHARP_OPENBLAS_SEARCH_PATH— one or more files/directories (path-separator delimited) — then a build-recordedOpenBlasPath(amode: "path"entry in theopenblas.source.jsonsource marker). Non-binding: they take priority when they hold a loadable BLAS, and are silently skipped (fall through to the rest) when they do not. - A build-staged version override (an
openblas.source.jsonmarker withmode: "version", written when the build pinnedOpenBlasVersion/NUMSHARP_OPENBLAS_VERSION) — hard-required: a miss throwsOpenBlasRequiredOverrideExceptionrather than falling through (the pin is a contract, and quietly substituting the bundle or a system BLAS would break it invisibly). When the marker pins a sha256, only a file hashing to it is loaded. - An explicit OpenBLAS root —
OPENBLAS_HOME/OPENBLAS_ROOT(OpenBLAS' own convention). A deliberate "use the OpenBLAS installed here" signal, so it outranks the bundle. Not byte-parity with NumPy, so setting it trades parity-by-default for the named build. - The bundled asset — the zero-config parity default.
- A machine-wide OpenBLAS from an OS package manager or a source build — apt multiarch, a Homebrew
keg, MacPorts, a conda tree, a vcpkg triplet,
/opt/OpenBLAS,/usr/lib64… honouring the ambientVCPKG_ROOT/CONDA_PREFIX. Not byte-parity with NumPy (a different compiler and build), so it ranks below the parity sources: a correct, fast BLAS, not a bit-identical one. - Bare loader names (
libscipy_openblas64_,libscipy_openblas,scipy_openblas,libopenblas,libblas, …). - Every directory on
PATH, swept for a BLAS under a non-standard name — the broadest, last-resort scan, reached only if nothing above binds.
A pip-installed numpy's own numpy.libs/ is deliberately never scanned (tier removed
2026-08-13): the bundled asset already is that binary at the pinned version, so results stay a
property of the package version rather than of whichever python happens to be installed — a
library's numeric output should not change because an unrelated pip install ran. (A conda/system
OpenBLAS is still machine tooling, tier 6; a numpy is not.) To match a NumPy whose OpenBLAS
differs from the bundled one, name it (binding, all-or-nothing) — or point
NUMSHARP_OPENBLAS_SEARCH_PATH at it to take priority over the bundled default while still falling back
to it if the path holds no BLAS:
OpenBlasEngine.Enable(@"…\site-packages\numpy.libs\libscipy_openblas64_-<hash>.dll");
Set NUMSHARP_OPENBLAS_USE_BUNDLED=0 to drop the bundled entry and make machine tooling the discovery
default.
Overriding the OpenBLAS version (build-time)
The override is configured on the PackageReference — no second package:
<PackageReference Include="NumSharp.Interop.OpenBLAS" Version="0.60.0">
<OpenBlasVersion>0.3.31.22.0</OpenBlasVersion>
</PackageReference>
At build, the pinned scipy-openblas wheel is fetched from PyPI, verified twice (the wheel's sha256
from the index, then the extracted library's), the one native library is extracted into a global
per-user cache (%LOCALAPPDATA%\NumSharp\openblas / ~/.cache/NumSharp/openblas — the wheel is
deleted; repeated builds and offline builds serve from the cache, and every cache hit re-verifies
the entry's content hash: a truncated or tampered entry is discarded and re-downloaded), staged
over the bundle in the output, and an openblas.source.json source marker is written so the
runtime treats the folder as a required override (discovery tier 3 above). A version override is a hard requirement: a
build that cannot download/verify it FAILS, and a runtime that cannot load it THROWS
(OpenBlasRequiredOverrideException) rather than quietly substituting the bundle or a system BLAS.
OpenBlasPath alone (no version) is the soft form — "read tooling from here", non-binding.
Environment variables win over the metadata: NUMSHARP_OPENBLAS_VERSION, NUMSHARP_OPENBLAS_SEARCH_PATH,
plus NUMSHARP_OPENBLAS_{DISTRIBUTION,FEED,SHA256,DELIVERY} (OpenBlasDistribution picks
scipy-openblas64/scipy-openblas32 — 64/32 aliases; a non-pypi.org OpenBlasFeed mirror
requires an explicit OpenBlasSha256).
Transitive: the machinery ships in buildTransitive/, so a package that merely depends on
this one gives its consumers the identical experience — the targets read the OpenBLAS metadata off
any PackageReference, including one pointing at the intermediate package. A package author can
also bake a pinned version into their own nupkg with <OpenBlasDelivery>package</OpenBlasDelivery>
(stages all supported RIDs plus their markers as runtimes/<rid>/native/ pack content; NuGet's
nearest-wins rule makes that copy beat this package's transitive bundle). OpenBlasDelivery=none
opts a reference out; build (the default) stages into the consuming app's output only.
Full model: docs/stale-docs/OPENBLAS_DELIVERY_DESIGN.md. Integration gate:
tools/verify_build_override.sh (scripted end-to-end nupkg-flow run; needs network once).
What it does
OpenBlasBackend implements NumSharp's IBlasBackend — Try-shaped members only — and the package
assigns it to the engine's TensorEngine.Blas property. The engine itself is NOT replaced or
subclassed: every other operation, dtype, kernel and iterator is untouched, and anything the backend
does not serve (any dtype other than float32/float64, or a shape outside the ported surface) is
computed by the built-in managed kernels as before.
// The entire Core-side surface:
public interface IBlasBackend {
string Info { get; }
bool TryDot(NDArray left, NDArray right, out NDArray result);
bool TryMatMul2D(NDArray left, NDArray right, NDArray result);
bool TryMatMulBatched(NDArray left, NDArray right, NDArray result) => false; // optional
// IBlasBackend.LinearAlgebra.cs — 15 more, every one optional (defaults to false):
// products TryInner TryVdot TryVecdot TryMatvec TryVecmat
// factorisations TryCholesky TryDet TrySlogdet TryEig TryEigh TryInv
// TryLstsq TryQr TrySolve TrySvd
}
public abstract partial class TensorEngine { public IBlasBackend Blas { get; set; } }
TryMatMulBatched takes a whole stacked product at once. It is a default interface method, so a
backend that only implements the first two members compiles and runs unchanged — the engine falls
back to calling TryMatMul2D once per batch element.
OpenBlasBackend implements none of the 15 linear-algebra members, and it compiles and behaves
exactly as before because of that same default-implementation rule. They exist so the seam for the
rest of what NumPy routes through this binary is fixed: np.inner/np.vdot/np.vecdot/
np.matvec/np.vecmat, and the LAPACK routines — gesv, getrf, potrf, geev, syevd/heevd,
gesdd, geqrf/orgqr, gelsd — which scipy-openblas ships inside the very library this package
already bundles, so a backend that has loaded it for sgemm already holds the handle.
The two groups differ in what happens when a backend declines. The products fall back to
NumSharp's managed kernels, so the invariant above still holds. The factorisations have no
managed fallback — Core carries no LU, QR, SVD or eigensolver — so np.linalg.inv and friends raise
NotSupportedException naming the routine and the Try* member to implement. Their NumPy-exact
validation runs first and is already gated, so an implementation only has to supply numerics. Implementing it lets a backend amortise
per-product setup across the stack (the trailing strides never vary within one), which is what NumPy
does in its matmul gufunc's outer loop; measured on 2000 stacked 8×8 float32 products, doing that
setup per element instead made this backend 0.80× — slower than NumSharp's own managed GEMM —
against 6.5× with the work hoisted.
Writing your own backend needs no access to NumSharp's internals: read operands as
(T*)a.GetData().Address + a.Shape.Offset with a.Shape.Strides (element units). Do not use
a.GetData<T>().Address — it densifies a non-contiguous view, so pairing it with Shape.Strides
reads outside the buffer.
The two entry points are a route-for-route port of NumPy 2.4.2's two matrix-product dispatchers —
cblas_matrixproduct (behind np.dot) and the @TYPE@_matmul gufunc (behind np.matmul and
@). They are genuinely different C code and disagree on some inputs, so both are mirrored:
gemm (with the a @ a.T syrk shortcut and NumPy's copy-if-not-blasable rule), both gemv
directions, the row·column ?dot with NumPy's double accumulator, the portable no-blas loop,
stacked/batched products, and the N-D np.dot route that is not gemm at all.
Why a port, and not just "call gemm"
Both NumSharp's answer and NumPy's are correct floating-point matrix products; they sum in
different orders. NumSharp's managed GEMM differed from NumPy on 94.5 % of the elements of a
(128,784)@(784,128) float32 product (max ~976 000 ULP) — enough that training the same network in
both stacks diverges. NumPy's bits are its BLAS's bits, and OpenBLAS' multi-accumulator
micro-kernels match neither a sequential mul + add chain nor a sequential FMA chain. Reproducing
them means calling the same binary through the same route with the same flags.
Three knobs decide the bits — the binary is only one of them
Bundling fixes the library build. Two variables remain, and a parity claim that ignores either is wrong:
- The worker-thread count is part of the answer. 1, 2, 4 and 24 threads give four different
results.
OpenBlasEngine.Enable(threads: n)pins it; it is a correctness knob, not a performance one. - The CPU is part of the answer. OpenBLAS is built
DYNAMIC_ARCHand picks a micro-kernel from the machine it finds itself on, and different kernels accumulate in a different order. Measured by forcing the choice on one host: Haswell, Nehalem, Sandybridge and Katmai each produced different bytes for the same float32 product. So "same binary" is necessary and not sufficient.
// Reproducible on ANY x86-64 with AVX2+FMA — pins the last free variable.
OpenBlasEngine.Enable(threads: 1, coreType: OpenBlasEngine.CoreType); // "Haswell"
coreType is not the default, deliberately: the package's main promise is matching the NumPy on
this machine, and that NumPy dispatches by CPU too — so pinning here would create a divergence on
any host newer than Haswell. Pin both sides or neither (the same OPENBLAS_CORETYPE variable drives
NumPy, because it is the same library).
Hazard.
coreTypeoverrides OpenBLAS' CPU detection rather than expressing a preference, so naming kernels the CPU cannot run terminates the process with an illegal instruction — measured.OpenBlasEngine.Enablerefuses the combinations it recognises with aPlatformNotSupportedExceptionfirst. It also only takes effect on the call that actually loads the library: OpenBLAS reads the variable while initialising and never re-reads it, so a later request that cannot be honoured throws rather than being silently dropped.
And unchanged from before:
- A named library is binding — an explicit path (or
NUMSHARP_OPENBLAS_LIBRARY) is used as given and never silently replaced, not even by the bundled copy sitting in the output folder. A failedOpenBlasEngine.Enableis a no-op: it throws having changed nothing, so a mistyped path cannot demote a working setup back to the managed kernels behind your back.
Supported providers
The bundled OpenBLAS is not privileged — any CBLAS export set is bound, in this order: numpy's
renamed ILP64 wheels (scipy_cblas_sgemm64_), plain ILP64 (cblas_sgemm64_ / cblas_sgemm_64),
scipy's LP64 build (scipy_cblas_sgemm), and stock LP64 (cblas_sgemm) — so MKL, Accelerate, a
system libblas or a reference CBLAS all work. Integer widths are marshalled per call. OpenBLAS'
thread, core-name and build-info entry points are used when present; a provider without them works
fine.
Environment
| Variable | Effect |
|---|---|
NUMSHARP_OPENBLAS_LIBRARY |
Path (or directory) of the CBLAS library to load. Binding, like an explicit argument — used as given, never substituted. |
NUMSHARP_OPENBLAS_SEARCH_PATH |
File(s)/dir(s) to scan for a CBLAS, path-separator delimited. Tried first (priority over the bundled asset), non-binding — falls through to the rest if it holds no BLAS. |
NUMSHARP_OPENBLAS_USE_BUNDLED=0 |
Skip the bundled asset; machine tooling becomes the discovery default. |
NUMSHARP_OPENBLAS_BUNDLE_AUTOINSTALL=0 |
Skip the module-load auto-install; OpenBlasEngine.Enable(...) still works. (The pre-rename NUMSHARP_BLAS_BUNDLE_AUTOINSTALL and NUMSHARP_BLAS_AUTOINSTALL spellings are retired and ignored.) |
NUMSHARP_OPENBLAS_CACHE_DIR |
Root of the per-user OpenBLAS cache. At runtime (macOS only) it holds the relocated dylib + vendored-runtime copies made when .dylibs cannot be materialized next to the dylib (read-only install, flattened publish) — under dyld/<sha256>/; at build time the version override's extracted-library cache. Default: %LOCALAPPDATA% / $XDG_CACHE_HOME / ~/.cache + NumSharp/openblas. |
NUMSHARP_OPENBLAS_VERSION |
Build-time: scipy-openblas version to download from PyPI and stage over the bundle — a hard requirement; beats <OpenBlasVersion> metadata. |
NUMSHARP_OPENBLAS_DISTRIBUTION / NUMSHARP_OPENBLAS_PYPI_FEED_URL / NUMSHARP_OPENBLAS_SHA256 / NUMSHARP_OPENBLAS_DELIVERY |
Build-time: distribution pick (64/32), PyPI mirror base (needs the sha), expected extracted-lib sha256, delivery mode (none/build/package); each beats its OpenBlas* metadata twin. |
OPENBLAS_HOME / OPENBLAS_ROOT |
Explicit OpenBLAS install root (OpenBLAS' own convention). A deliberate signal, so it ranks above the bundle (discovery tier 4) — not byte-parity with NumPy. |
VCPKG_ROOT / CONDA_PREFIX |
Ambient roots consulted when scanning for a machine-wide OpenBLAS (discovery tier 6, below the bundle). |
OPENBLAS_CORETYPE |
Read by OpenBLAS itself at load. Set it in the environment to pin both NumPy and NumSharp at once. |
Maintainers
The binaries are not in git. Stage them before packing:
python src/NumSharp.Interop.OpenBLAS/tools/fetch_openblas.py # download + verify + extract
python src/NumSharp.Interop.OpenBLAS/tools/fetch_openblas.py --check # verify only
Every artifact is checked twice against the committed tools/openblas-manifest.json — the
wheel's SHA-256, then the extracted library's own SHA-256, and likewise each vendored runtime file
(vendored per RID) — so a re-pointed URL or a mutated artifact fails loudly instead of quietly
changing the bits the package produces, and a partial staging (main without its deps, or the macOS
deps in the pre-fix runtimes/<rid>/.dylibs/ sibling that NuGet drops) fails --check and is
re-staged. Staged layout: runtimes/<rid>/native/<main>, the Linux deps beside it, the macOS deps
under native/.dylibs/. Building without the assets staged is fine and simply produces an asset-free
package; -p:RequireOpenBlasAssets=true turns that into a hard error for release builds.
Two scripted gates, both run by CI: tools/verify_package_consumer.sh packs the real nupkg, restores
it into a scratch consumer by PackageReference and proves the bundle loads in every layout (the
one path a ProjectReference test suite cannot exercise — which is how the macOS defect above shipped
green), and tools/verify_build_override.sh drives the build-time version override end to end.
Full design record, probe results and traps: docs/stale-docs/GEMM_PARITY.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.70.0 | 70 | 9/6/2026 |