NumSharp.Interop.OnnxRuntime
0.80.0
dotnet add package NumSharp.Interop.OnnxRuntime --version 0.80.0
NuGet\Install-Package NumSharp.Interop.OnnxRuntime -Version 0.80.0
<PackageReference Include="NumSharp.Interop.OnnxRuntime" Version="0.80.0" />
<PackageVersion Include="NumSharp.Interop.OnnxRuntime" Version="0.80.0" />
<PackageReference Include="NumSharp.Interop.OnnxRuntime" />
paket add NumSharp.Interop.OnnxRuntime --version 0.80.0
#r "nuget: NumSharp.Interop.OnnxRuntime, 0.80.0"
#:package NumSharp.Interop.OnnxRuntime@0.80.0
#addin nuget:?package=NumSharp.Interop.OnnxRuntime&version=0.80.0
#tool nuget:?package=NumSharp.Interop.OnnxRuntime&version=0.80.0
NumSharp.Interop.OnnxRuntime
Zero-copy interop between NumSharp NDArray and ONNX Runtime tensors. Feed an
NDArray to InferenceSession.Run as an OrtValue built over the very same unmanaged buffer — no
DenseTensor fill loop, no copy — read outputs back as NDArrays (an owning copy, or a zero-copy view),
and do the pre-/post-processing (normalize, transpose, softmax, argmax, top-k, clip) as vectorized NumSharp
ops instead of the per-pixel for loops every ORT C# tutorial hand-writes.
using NumSharp;
using NumSharp.Interop.OnnxRuntime;
using Microsoft.ML.OnnxRuntime;
using var session = new InferenceSession("resnet50-v2-7.onnx");
NDArray input = /* (1,3,224,224) float32, C-contiguous */;
using NDArray logits = session.Run(input); // zero-copy in, owning NDArray out
using NDArray probs = Postprocess.Softmax(logits); // (1,1000)
var (top5, classes) = Postprocess.TopK(probs, 5); // values + int64 indices
This is a conversion library, in the mould of NumSharp.Interop.pythonnet, not an engine backend: ONNX
Runtime does not compute NumSharp operations, it consumes tensors NumSharp produces and produces tensors
NumSharp reads. So there is no TensorEngine seam, no [ModuleInitializer], and referencing the package
changes nothing until a verb is called.
Verified against ONNX Runtime 1.16.0 (the package floor) and 1.31.0 (current) on net8.0 / net10.0, by
test/NumSharp.Tests.Interop.OnnxRuntime: committed.onnxmodels that ONNX Runtime really executes, with an every-dtype × every-layout matrix and property-based, metamorphic, bit-pattern-fuzz, concurrency, boundary, reference-accounting, lifetime and API-approval suites. The package's first release is 0.80.0.
Install
dotnet add package NumSharp.Interop.OnnxRuntime
dotnet add package Microsoft.ML.OnnxRuntime # or .Gpu / .DirectML / .QNN — exactly ONE native flavour
The package depends only on Microsoft.ML.OnnxRuntime.Managed (the C# API). ORT ships its native
runtime as mutually exclusive flavours — Microsoft.ML.OnnxRuntime (CPU), .Gpu, .DirectML, ... — and
two of them side by side is a documented misconfiguration, so the flavour is yours to pick: the one you
already reference to run inference at all. The dependency is 1.16.0 or later with no upper bound (a cap
below a consumer's ORT would be a restore error, NU1107, the day they upgrade); a consumer's own ORT
reference (say 1.31.1) carries the same .Managed version and NuGet unifies upward to it. The NuGet range is
the only thing that sets the floor: the 1.16.0 managed assembly reports assembly version 0.0.0.0, so the
runtime's binder refuses nothing.
On Windows, forgetting the native flavour does not fail at once: the loader finds the onnxruntime.dll
that Windows ships in C:\Windows\System32 for Windows ML (1.17 on a current Windows 11), and the app runs
on that build: another version than the one you reference, whatever the machine has, and absent on Linux
and macOS (DllNotFoundException). Reference the native flavour explicitly.
Views, copies and OnnxCodecMode
An ORT tensor is a pointer, a row-major shape and a fixed-size element type. It has no strides. So ORT can
read a NumSharp buffer in place only when the array is C-contiguous and its dtype crosses bit for bit (bool,
the eight integers, float16, float32, float64, and char as UInt16). Every entry point that could share
memory takes an OnnxCodecMode, with the member names and meanings of the pythonnet bridge's
NumpyCodecMode:
| Mode | What it does |
|---|---|
Auto (default) |
A zero-copy view when the dtype and the layout permit it, an independent copy otherwise. It never fails because of a layout or a convertible dtype. |
View |
Always a view. A crossing that needs a copy throws InvalidOperationException and names what prevents the view. Use it to prove that a hot loop copies nothing, or that ORT writes into your array. |
Copy |
Always an independent copy. |
A copy is needed for a Fortran-ordered, stepped, reversed, transposed or broadcast view (a C-order copy), for
a Decimal array (a float64 conversion) and for text. No mode repairs a dtype with no ONNX element type
(Complex), a released buffer, or a size the target cannot hold.
The verbs
As… gives a view when it can (and a handle or an array that says which it gave), To… always copies.
| Direction | Verb | Semantics |
|---|---|---|
| NumSharp → ORT | nd.AsOrtValue(mode) → OrtTensor |
An OrtValue over the NDArray's buffer (CreateTensorValueWithData on NumSharp's pointer) when a view is possible; otherwise, under Auto, an OrtValue over a temporary copy that the handle owns. handle.IsView says which, handle.Source is the array ORT reads. The handle owns the OrtValue and a counted reference on the buffer; keep it alive across Run. |
| NumSharp → ORT | nd.ToOrtValue() → OrtValue |
An ORT-allocated copy, any layout (a view is copied in logical order); no lifetime coupling. ORT owns and frees it; NumSharp books its bytes until it is disposed or collected, so a forgotten one is seen by the GC. |
| NumSharp → ORT | nd.AsDenseTensor<T>(mode) → OrtTensor<T> / nd.ToDenseTensor<T>() |
The legacy DenseTensor<T> twins for NamedOnnxValue.CreateFromTensor. The view's Buffer is a Memory<T> over NumSharp's memory. The tensor is always row-major: a Fortran-ordered array is a copy under Auto (no ORT route reads a column-major tensor correctly). |
| ORT → NumSharp | ortValue.ToNDArray() → NDArray |
A new owning, C-contiguous copy — the safe default; dispose the OrtValue whenever you like. A string tensor comes back as a text array. |
| ORT → NumSharp | ortValue.AsNDArray(ownsValue = false, mode) → NDArray |
A zero-copy view over the tensor's buffer (CPU tensors only), mutations visible both ways. ownsValue: true makes the last view dispose the OrtValue. mode: Copy returns an independent copy. A string tensor cannot be viewed: Auto copies it, View throws. |
| ORT → NumSharp | denseTensor.ToNDArray() / tensor.ToNDArray() / denseTensor.AsNDArray(mode) / namedOnnxValue.ToNDArray() |
The legacy-surface twins (a column-major tensor transposes back on copy and stays Fortran-ordered as a view; NamedOnnxValue dispatches on the element type at run time, char, Half and string tensors included). ToNDArray takes the abstract Tensor<T> that AsTensor<T>() is declared to return, so the tutorials' Tensor<float> output = results.First().AsTensor<float>(); output.ToNDArray() compiles as written. |
| ORT → NumSharp | ortValue.ToNDArrays() / ToMap() / ToMaps() / GetMapKeys() / ReadStringTensor() |
Non-tensor outputs: a sequence of tensors → NDArray[]; a map, or a scikit-learn ZipMap sequence(map(K, V)) → (keys, values) arrays; a string tensor → string[]. A numeric array is a view that owns ORT's copy of its element (one copy per element, made by ORT); a string part is a text array. |
Plus the dtype maps ToTensorElementType / FromTensorElementType (+ Try… forms, ToTensorElementClrType),
IsCpuAccessible, and two counters, NDArrayOnnxInterop.LiveExports / LiveImports, that make every live
crossing observable.
The ergonomic tier — session.Run(NDArray)
InferenceSessionExtensions hides every OrtValue lifetime:
NDArray y = session.Run(x); // single input/output: names from metadata
NDArray y2 = session.Run("X", x, "Y2"); // named
IReadOnlyDictionary<string, NDArray> outs = session.Run( // several inputs, all (or some) outputs
new Dictionary<string, NDArray> { { "A", a }, { "B", b } });
var views = session.Run(inputs, zeroCopyOutputs: true); // views that OWN their OrtValue
session.RunInto(inputs, new Dictionary<string, NDArray> { { "Y", preallocated } }); // ORT writes INTO your array
await session.RunIntoAsync(inputs, outputs); // the same, without blocking a thread (ORT 1.30+)
- Inputs. An input that is C-contiguous and of the declared dtype is shared. Any other is fed as a
temporary under
Auto(a cast, a C-order copy of a view), refused underView, and every input is copied underCopy. A dtype that differs from the declaration is cast to it when NumPy'scastingrule allows it ("safe"by default — so an int32 array into an int64 BERT input or float32 into float64 just works; a lossy float64 → float16, and aDecimalarray into a float64 input, needcasting: "same_kind"or anastypeat the call site).Charinto a UInt16 input is safe and stays zero-copy. The rule name is checked on every call. Declared fixed extents and rank are checked before ORT sees the tensor, with a message naming the input (input 'data' expects shape (1, 3, 4, 4), but the array has shape (1, 3, 4, 5): dimension 3 must be 4, not 5); symbolic dims ('batch','seq') are free. - Names. An input name is looked up among the model's inputs, then among its overridable initializers. An
input declared
optional(tensor)takes a plain array. - The feed and the fetch are checked before the run. Every input that is not
optionalmust be fed (Python onnxruntime's rule and message); an unknown output name is refused with the list of outputs; and an outputRuncannot return (a sequence, a map, a sparse tensor) is refused with the readers that take it, before the model runs. - Outputs are copied into owning arrays by default and every ORT value is released before the call
returns.
zeroCopyOutputs: truereturns views that own theirOrtValue— ORT's memory is freed when the array (and every slice derived from it) dies — for large outputs you consume in place. RunIntowrites into arrays you allocated: zero-copy in both directions, the idiom for a hot loop that reuses its buffers (Stable Diffusion's UNet step). An output array must be C-contiguous, writeable and of the declared dtype (it cannot be cast). ORT may write an output while it still reads an input, so an output that shares memory with an input is computed from a copy of that input underAuto(NumPy's rule forout=) and refused underView; two outputs that overlap are always refused.- An output that is a fed input. ONNX permits a model output that no node produces (a model input passed
through, or an overridable initializer you feed). ORT does not write a pre-allocated array for it and returns
a value over the fed memory. The tier handles it:
RunIntowrites the fed values into your array, the copy form copies them, and the zero-copy form returns a counted view of the fed buffer. - Text. A text array (
UorS) feeds a string input, and a string output comes back as a text array; see below.
Asynchronous runs
RunIntoAsync and RunAsync build on ORT's own native asynchronous run: the model executes on the session's
intra-op thread pool and no .NET thread waits.
- They need ONNX Runtime 1.30.0 or newer (
InferenceSessionExtensions.IsRunAsyncSupported). Older managed assemblies do not pin the arrays the native run keeps reading (microsoft/onnxruntime#32015), so both forms throwNotSupportedExceptionthere. - The session needs 2 or more intra-op threads (
SessionOptions.IntraOpNumThreadsis not 1). With one, ORT refuses and the task is faulted. RunIntoAsyncisRunIntofor every model.RunAsyncallocates the outputs itself, which it can do only before the run: for a numeric tensor with a fixed declared shape, and for an output that is a fed input. For a dimension fixed only at run time, a declaration without dimensions, a string tensor or a non-tensor output it throwsNotSupportedExceptionwith what to do instead.- Usage errors throw from the call. A failure inside ORT faults the task. To cancel, pass a
RunOptionsand setTerminate = true. - ORT completes its own task on an intra-op thread of the session. Code that continues there and disposes the session never returns. The bridge's task runs your continuation off that thread, and a thread that blocks on it under a synchronization context is released (ORT's own task waits forever there).
- Until the task completes, ORT reads the shared inputs and writes the outputs: do not write an input (pass
OnnxCodecMode.Copyto feed a snapshot). Disposing an array in flight is safe; the run holds a reference. - An
NDScopedoes not cross theawait: a scope belongs to the thread that opened it, and the continuation resumes on another. Held acrossawait session.RunAsync(...), it reaches its end on the continuation's thread, whereDisposethrowsInvalidOperationExceptionand leaves the scope's arrays to the finalizer: closing it there would free the opening thread's later arrays (one filled with 7.0 would read another array's 9.0). Scope the code before theawaitand after it, or mark the async method[NDScopedAsync]. RunAsyncallocates its outputs when the call starts, so a scope open then tracks them: hand the task out withscope.ReturnsTask(task)and close the scope withNDScope.CloseUnlessDeferred(scope)(the form[NDScopedAsync]emits); the result dictionary carries its outputs, so the scope yields them to you when the task completes. A plainusingcloses the scope at once and the task returns arrays over freed memory. Under the synchronousRun, yield each output you keep withscope.Returns(outputs[name]).
Text
NumSharp's fixed-width text dtypes (U: UCS-4 code points; S: bytes) cross as ORT string tensors, in both
directions and through every route (ToOrtValue, AsOrtValue, ToNDArray, AsNDArray, the readers, Run).
- Everything is a copy. ORT keeps each string as a separate object in its own memory.
AutoandCopydo the same;Viewthrows. - A
Uelement crosses as its text (ORT stores UTF-8). AnSelement crosses as its bytes, unchecked, as Python'sonnxruntimedoes. A string tensor reads back as aUarray as wide as its longest string. - The fixed width costs two things, because the bridge reads string tensors as
U, not as NumSharp's variable-widthStringDType: a string that ENDS in NUL characters loses them (ReadStringTensorkeeps them), and one long string widens every element. AStringDTypearray is not a bridge input (Python'sonnxruntimerefuses it too): cast it to a sizedUfirst, wide enough for its longest string (nd.astype(np.dtype("U16")); an unsized"U"target is refused forStringDType, as in NumPy). - The bridge does not cast between text and numbers: a numeric array for a string input, or a text array
for a numeric input, is refused under every casting rule. Convert first with NumSharp's own casts,
nd.astype(np.dtype("U"))(each number as itsstr()) ornd.astype(np.float32)(NumPy's text-to-number parse). RunIntoandRunAsynccannot take a string output (ORT cannot write its strings into NumSharp memory); the synchronousRunreturns it.
Dtypes
| NumSharp | ORT TensorElementType |
Crossing |
|---|---|---|
| Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double | Bool, UInt8, Int8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Float, Double | zero-copy |
| Half | Float16 | zero-copy reinterpret — ORT's Float16 is a blittable 16-bit IEEE struct, binary-identical to System.Half; NaN payloads and subnormals cross untouched |
| Char | UInt16 | zero-copy as UTF-16 code units; directional — an ORT UInt16 comes back as UInt16, not Char |
String (U, S) |
String | always a copy; see Text |
| Decimal | — | no ONNX type: the array-producing verbs convert to Double under Auto and Copy (a float64 temporary the handle owns; lossy beyond ~16 digits), View refuses; Run needs casting: "same_kind"; the low-level maps refuse it |
| Complex | Complex64/128 exist in the enum but no ORT API or kernel accepts them | refused — split into np.real / np.imag |
| — | BFloat16 | no NumSharp dtype — refused with the ORT-side alternative named |
The maps answer for every descriptor: TryToTensorElementType returns false and ToTensorElementClrType
returns null for a dtype without storage (a datetime64 descriptor).
Post-processing
Postprocess is the output half of every tutorial as np compositions — no new kernels, kept apart from
the verbs so those stay conversion-only:
| Replaces | |
|---|---|
Postprocess.Softmax(logits, axis = -1) (max-shifted, rows sum to 1), LogSoftmax, Sigmoid |
the Math.Exp / Sum / Select loop |
Postprocess.Argmax(scores, axis = -1) → int64, first occurrence |
GetMaxValueIndex |
Postprocess.TopK(scores, k, axis = -1, largest = true) → (values, indices) |
OrderByDescending().Take(k) — ONNX TopK semantics, ties lower-index-first |
They agree with ORT's own Softmax / ArgMax / TopK operators run on the same data (the tests do exactly
that). Float32 in gives float32 out. A bool or integer input is promoted to NumPy's float tier before the
computation (bool, int8, uint8 → float16; int16, uint16, char → float32; wider → float64), so the result is
the mathematical one, not a wrapped integer. LogSoftmax follows scipy.special.log_softmax for a row whose
maximum is not finite. Argmax and TopK check their axis.
The #512 example — a BGRA image to an NCHW tensor, no pixel loop
The issue fills a DenseTensor<float> one pixel at a time
(/255, channel reorder). With NumSharp it is four vectorized ops feeding ORT zero-copy:
NDArray bgra = np.frombuffer(data, NPTypeCode.Byte).reshape(H, W, 4); // interleaved B,G,R,A
NDArray chw = np.stack(new[] { bgra[":,:,2"], bgra[":,:,1"], bgra[":,:,0"] }, axis: 0) // (3,H,W) RGB
.astype(NPTypeCode.Single) / 255f; // fresh C-contiguous f32
NDArray input = chw.reshape(1, 3, H, W); // NCHW
using OrtTensor t = input.AsOrtValue(OnnxCodecMode.View); // ZERO COPY, or it throws
using var results = session.Run(runOptions, new[] { "data" }, new[] { t.Value }, session.OutputNames);
NDArray output = results[0].ToNDArray();
The one copy left — stack + astype/divide producing the planar normalized array — is genuine work
(layout + dtype + scale), not marshaling. Or, one line: NDArray output = session.Run(input);.
Lifetime & memory safety
- Exports (
AsOrtValue/AsDenseTensor<T>) take their own atomic reference on the NumSharp buffer (NumSharp's ARC), so the memory survives even if the sourceNDArrayis disposed or collected while ORT holds the tensor. Release is deterministic and owned by the returned handle (OrtTensor.Dispose/OrtTensor<T>.Dispose), which also disposes the temporary copy when the handle holds one. Keep the handle (or itsValue) reachable acrossRun— passhandle.Value, dispose after the run (using): ORT's ownRunholds no reference to the managed inputs while native code reads them, and neither doOrtIoBinding,SessionOptions.AddInitializerorOrtValue.CreateSequence. While exported,ndarray.resize(refcheck: true)on the source refuses (cannot resize an array that references or is referenced by another array), exactly NumPy's rule. ToOrtValuereturns ORT's own value — dispose it. ORT allocates and frees the copy, so the value stays valid for every native holder (anOrtIoBindingit was bound to, a run's feeds) after the managedOrtValueis disposed or collected. NumSharp books the copy's bytes with its own memory accounting until the caller disposes the value (the booking ends when ONNX Runtime releases it: at once, or after the last view of it) or the GC collects it: 5,000 forgotten values of 602 KB peaked at about +70 MB of private memory (unbooked: +2.9 GB until an unrelated collection ran), and a value that is disposed never triggers a collection. The booking is a token stored in ORT's privateOrtValue._memHandleslot, whoseMemoryHandleORT'sDisposereleases; on an ONNX Runtime without that field (1.16.0 through 1.31.1 have it) the value is returned unbooked, and a string tensor is never booked.- Reachable means valid. Holding only
handle.Value, or onlyTensor/Memory, keeps the handle — and its reference on the buffer — alive, so a collection never disposes anOrtValueor disables a tensor you still hold. A forgotten handle is released by a finalizer safety net once nothing reaches any of them; it only gives the reference back (never frees eagerly), so an alias of the buffer you still hold stays valid, and it never disposes theOrtValueor aSourceconversion you can still reach. - A value of a disposed handle is refused, not passed to ORT. After
handle.Dispose(), ONNX Runtime has releasedhandle.Value, and ORT does not guard a released value (a native call on one ends the process). Every verb that takes anOrtValuethrowsObjectDisposedExceptionfor such a value. So does a value you disposed yourself, and everyRunform on a disposedInferenceSession(both were access violations): the bridge reads ONNX Runtime's own private disposal flag, and skips the check on a version without it. - A
Disposeon another thread is safe for the bridge's verbs. ONNX Runtime's ownDisposetakes no lock (two disposers corrupted the heap; aDisposeduring a verb was an access violation). Every value the bridge touches carries an ARC: a verb holds a lease while it calls ORT on the value, a view for its whole life, and aDispose- from any thread, any number of times at once - waits for the verbs in flight and releases the value once, after its last view. AToOrtValueresult and anAsOrtValuevalue have it from birth; a value ORT allocated (aRunoutput of ORT's own API) gets it at its first bridge verb. For sessions, useNDInferenceSession(a drop-inInferenceSessionwith ORT's eight constructors): itsDisposereleases the native session once, after the bridge's runs on it returned (the asynchronous release runs on the thread pool), and refuses new runs. ORT's own calls (itsRunoverloads,GetTensorDataAsSpan, anOrtIoBinding) take no lease: dispose only after those returned. - Pins of an
AsDenseTensor<T>handle'sMemory<T>outlive the handle. Whoever pins that memory — ORT'sCreateTensorValueFromMemory, anyMemory<T>.Pin()— takes a reference of its own on the NumSharp buffer, given back exactly once: when the pin is disposed (two copies of oneMemoryHandlecount once) or when it is collected undisposed (ORT'sOrtValuefinalizer never disposes the pin it holds). A disposed handle only refuses NEW access: its tensor'sMemory<T>throwsObjectDisposedExceptionfor a new span or pin, and so doToNDArray/AsNDArrayof that tensor (before allocating anything). - A released source is refused with
ObjectDisposedExceptionby every verb that reads it (the exports, the copies,Run's cast and C-order copy): each takes its reference on the buffer before its first read, so it never reads memory the pool may have handed to another array. Dtype and size refusals come first, so nothing is converted, copied or allocated for an array that cannot cross. - Imports of a buffer ORT owns (
AsNDArrayof a run output) lease it through NumSharp's memory-block reference counting: the lease fires when the last NumSharp view over the memory — derived slices included — is disposed or collected. A non-owning view holds a lease on theOrtValue: the caller may dispose it at any time and the memory stays valid until the last view is gone (a value over memory ORT does not own, or built from members, keeps ORT's rule forGetTensorDataAsSpan: do not dispose it while a view is in use).ownsValue: truetransfers ownership: the last view disposes theOrtValue, and the caller must not. A call that throws never disposes the value. An owning view books the buffer's size with NumSharp's memory accounting (not for aToOrtValueresult, which the value has booked already), and so does aDenseTensor<T>view of a managed array, whose pin keeps the array alive until the last view is gone (a forgotten view is then collected like a forgotten NumSharp array); a view of a legacy run's output books nothing. Views do not own their data (flags.owndata == False): a size-changingresizerefuses instead of detaching. - Imports of NumSharp memory are ordinary NumSharp views. A value this package made
(
nd.AsOrtValue().Value.AsNDArray()), atensor.AsNDArray()of anAsDenseTensor<T>handle and an output that is a fed input are views of a NumSharp buffer. They hold their own counted reference, stay valid after the handle and the source are disposed, and are read-only when the exported array was. ORT itself has no read-only tensors: a read-only array is a legal model input, and ORT could write a tensor it was given. - A value ORT returned over memory that is not ORT's. When you run the session with ORT's own API and feed
an
AsOrtValuehandle, ORT returns a value over the handle's memory for an output that is a fed input.AsNDArraycannot know that: such a view is valid only while the handle, or the array you fed, is alive.session.Run(…, zeroCopyOutputs: true)of this package knows what it fed and returns a counted view. - A
DenseTensor<T>view keeps its tensor reachable.denseTensor.AsNDArray()over a legacysession.Runoutput (DisposableNamedOnnxValue.AsTensor<T>()) roots the tensor — and through it theOrtValue— so results that are merely dropped are not finalized under the view. Disposing the results explicitly while a view lives still frees the memory under it: keep them alive, or copy withToNDArray. - Only CPU-addressable tensors can be read or viewed. A CUDA / DirectML / TensorRT-resident output is
refused (
the tensor lives in 'Cuda' memory ... which the CPU cannot address) — bind a CPU output or copy to CPU first. Host-accessible device memory (CudaPinned,HipPinned,QnnHtpShared) andCpuAligned4Kare fine: the bridge asks ORT for the device memory kind where the loaded ORT can tell, and uses the allocator name otherwise. - No GIL, no interpreter lifetime, no thread-affinity: ORT is thread-safe and the verbs are plain calls.
Limits (stated up front)
- Complex (ORT has no complex tensors), BFloat16 / float8 / 4-bit (no NumSharp dtype). A BFloat16
input goes through ORT's own
OrtValue.CreateTensorValueFromMemory(new BFloat16(bits)) andsession.Run; a float8 or 4-bit model does not load in ORT's C# API itself (Unregistered TensorElementType value). - Sparse tensors. ORT's C# API cannot create or read one. A model input declared sparse passes the checks
of
Runand fails inside ORT. - Non-tensor outputs through
Run.Runreturns tensors only (text included). Fetch a sequence or a map with ORT's ownsession.Runand read it withToNDArrays/ToMap/ToMaps. A string-keyed map's keys come back sorted: ORT keeps a map ordered by key. - GPU-resident tensors cannot be viewed as
NDArrays. - ORT's managed data API (
GetTensorMutableRawDataand every span over it) isint-indexed — it hands out a tensor's memory only as aSpanof bytes, whose length it casts tointunchecked. Export: an array over 2 GB is refused byToOrtValuebefore anything is copied or allocated — share it zero-copy withAsOrtValue, which hands ORT a native pointer (no limit). Import: a tensor of 2 GB or more that ORT owns can be neither copied (ToNDArray) nor viewed (AsNDArray) out of anOrtValue; both refuse up front, before anything is allocated. Let ORT write such an output into NumSharp's memory instead: pre-allocate it and callRunInto. A value this package made, and aDenseTensor<T>(up toint.MaxValueelements), cross at any byte size. ToDenseTensor<T>copies into a managedT[], which holds at mostArray.MaxLength(2,147,483,591) elements; a larger array is refused up front.AsDenseTensor<T>takes up toint.MaxValueelements (aMemory<T>cannot index more),AsOrtValuehas no element limit.Run(...)shape validation can only see whatNodeMetadatareports: a declaration without dimensions is either a scalar or an unknown-rank tensor (indistinguishable there) and is left to ORT's own check. For the same reasonRunAsyncdoes not allocate such an output.- The plan and the record of this surface:
docs/plans/onnxruntime-rework.md.
What's new in 0.80.0
We are at 95.2% NumPy API coverage, and everything except datetime, complex64 and other exotic dtypes matches NumPy byte for byte. This release brings NumPy's text dtypes with numpy.strings and numpy.char, numpy.ma, numpy.polynomial, np.emath and 48 new np.* functions. Integration rather than competition goes on, this time inside .NET: an NDArray now runs through ONNX Runtime models, serves as an ML.NET IDataView and shares its memory with System.Numerics.Tensors.
🧭 Overview
- NumPy 2.x API coverage around 95.2%, everything except datetime, complex64 and other exotic dtypes is fully supported, with byte-to-byte equality and compatibility to NumPy.
- Text - NumPy's fixed-width
bytes_(S) andstr_(U) dtypes and NEP 55's variable-widthStringDType, with every computation NumPy runs on text, all 46numpy.stringsfunctions andnumpy.charwithchararray. np.polynomial.*- the wholenumpy.polynomialpackage, all 193 functions and the six series classes for all six bases, bit-exact.np.*- +48 functions, the ten most used on GitHub:np.hypot,np.histogram,np.logspace,np.gradient,np.vectorize,np.hanning,np.geomspace,np.apply_along_axis,np.trapezoidandnp.histogram2d.np.random.*- every NumPy bit generator, the wholeGeneratordistribution surface, and a legacyRandomStatethat is NumPy's own sampler code line for line, all byte-identical streams.np.ma.*- the masked-array module;np.emath.*- the automatic-domain math module;np.matrix-numpy.matrixas the newNDMatrixtype.
- Text - NumPy's fixed-width
DTypeis NumPy's two-level dtype model -NDArray.dtypereturns anumpy.dtype-shaped descriptor, withnp.dtypes, byte order anddatetime64/timedelta64descriptors, and NumPy's scalar classes (np.integer,np.floating,np.number, ...) withnp.typing.- Reductions on NumPy's own schedules -
max,min,ptp,nanmax,nanmin,argmax, the flatsum/meanand the fusednp.evaluatereductions match NumPy bit for bit, down to the sign of a ±0 tie and a NaN's payload;mean,var,stdand their NaN-aware forms are NumPy's own statements; and most of them run faster. - NumPy's structure for the engine - every computing
np.*function is aTensorEnginemember named after its NumPy source file,numpy.linalgandnumpy.fftare replaceable sub-engines, and the default engine is settable. - The 2026-11-02 parity QA - 53 of its 55 findings fixed on the owner's decisions (one kept as a documented divergence, one deferred), and NumPy's own dot functions behind
np.dot/np.vdot/np.vecdot. - 86 NumPy-parity, memory-safety and tooling fixes, 40 measured speedups and 44 breaking changes, across the release's 1,670 commits.
- 3 new NuGet packages - NumSharp.Interop.System.Numerics.Tensors, NumSharp.Interop.OnnxRuntime and NumSharp.Interop.MLNet.
- 1,119,305 NumPy 2.4.2 oracle test cases (0.70.0: 119,517) and 20,506 test methods (0.70.0: 14,416).
📦 New NuGet Packages
- NumSharp.Interop.System.Numerics.Tensors - zero-copy interop between
NDArrayand the BCL'sTensor<T>/TensorSpan<T>(System.Numerics.Tensors), all 15 dtypes. - NumSharp.Interop.OnnxRuntime - run ONNX Runtime models on
NDArrays, fed from the arrays' own memory, with the outputs read back asNDArrays. - NumSharp.Interop.MLNet - an
NDArrayas an ML.NETIDataView, and a transformer's output column back as anNDArray.
| 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. |
-
net10.0
- Microsoft.ML.OnnxRuntime.Managed (>= 1.16.0)
- NumSharp (>= 0.80.0)
-
net8.0
- Microsoft.ML.OnnxRuntime.Managed (>= 1.16.0)
- NumSharp (>= 0.80.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on NumSharp.Interop.OnnxRuntime:
| Package | Downloads |
|---|---|
|
NumSharp.Interop.MLNet
Interop between NumSharp NDArray and ML.NET (Microsoft.ML) data. Expose an NDArray to an ML.NET pipeline as an IDataView over the very same unmanaged buffer (no row-by-row POCO fill, no copy — read lazily through the array's strides), read a transformer's output column back as an NDArray, and convert to/from ML.NET's VBuffer vector primitive. Conversion library only: no engine backend, no ModuleInitializer, no native dependency — depends on the standalone Microsoft.ML.DataView contract package (2.0.0 or later) and on NumSharp.Interop.OnnxRuntime for post-processing (Softmax, Argmax, TopK), and composes with whatever Microsoft.ML (MLContext, trainers, transforms) you already reference. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.80.0 | 71 | 11/8/2026 |