Interface ISlidingDotBackend
An optional capability a IBlasBackend MAY also implement to supply the
byte-parity level-1 dot product (NumPy's per-dtype dotfunc) behind the sliding
multiply-accumulate family — np.correlate and np.convolve.
public interface ISlidingDotBackend
Remarks
Why a second, separate interface. IBlasBackend is the matrix-product
seam: every member there takes whole NDArray operands. correlate/
convolve are not matrix products — they are a sliding dot whose inner reduction runs
once per OUTPUT position (thousands of tiny dots), so the primitive they need is a raw
strided vector dot called in a hot loop, not an allocating whole-array method. Rather than
put a pointer method on the product interface, this capability is discovered by a cast:
engine.Blas as ISlidingDotBackend. A backend that does not implement it simply leaves
the managed sliding kernels in place — the exact optional-capability pattern
TryMatMulBatched(NDArray, NDArray, NDArray) expresses as a default method.
What it is byte-for-byte. This is NumPy's @name@_dot from
numpy/_core/src/multiarray/arraytypes.c.src — the very dotfunc that
_pyarray_correlate (multiarraymodule.c) calls for every ramp position and for the
middle whenever small_correlate declines. NumPy routes only float32,
float64 and complex128 through cblas here (its #USEBLAS = 1,1,0,0,1,1,…
over FLOAT/DOUBLE/…/CFLOAT/CDOUBLE), summing each dot in a double accumulator "for
stability" (a chunked ?dot, ?dotu for complex). A backend supplies exactly that
so np.correlate/np.convolve match NumPy to the last bit on the long float
kernels a portable reduction reorders — the same reason
NumSharp.Interop.OpenBLAS exists for the matrix products.
Which positions the engine routes here. The engine keeps the whole
small_correlate-eligible regime — real float32/float64 kernels of length
≤ 11 — on its own managed kernel, which is already byte-identical to NumPy there (a plain
sequential sum). Only real kernels longer than 11 and every complex kernel reach this
interface, and for those NumPy sends every ramp AND middle position through cblas, so the
engine does too — no mixed managed/native path within one call.
Reading the operands. Element strides, not bytes (NumPy's are bytes — the same logic
with itemsize == 1). The operands the engine hands over are always contiguous,
offset-0 buffers of the result dtype, so the stride is one element; the pointers advance to
the sub-array a given output position dots. Byte-parity still depends on the same three
levers the products do (the OpenBLAS build, its thread count, the dispatched DYNAMIC_ARCH
kernel) — level-1 ?dot is typically single-threaded, but a host whose CBLAS differs is
free to produce different bits, exactly as with dot/matmul.
Methods
Dot(NPTypeCode, void*, long, void*, long, void*, long)
Computes one strided vector dot, NumPy's @name@_dot:
*result = Σ a[i·strideA] · b[i·strideB] for i in [0, count), summed
the way NumPy sums it (chunked cblas ?dot accumulated in double;
?dotu — UNCONJUGATED — for complex, matching np.correlate/np.convolve,
which never conjugate the kernel here because correlate has already conjugated it).
void Dot(NPTypeCode dtype, void* a, long strideA, void* b, long strideB, void* result, long count)
Parameters
dtypeNPTypeCodeOne of Single/Double/Complex — the caller has checked SupportsDot(NPTypeCode) first.
avoid*Base pointer of the first operand's sub-array.
strideAlongElement stride of
a(one, for a contiguous buffer).bvoid*Base pointer of the second operand's sub-array.
strideBlongElement stride of
b.resultvoid*Where the scalar dot is written (one element of
dtype).countlongNumber of terms in the dot (may be 0, which writes the zero sum).
DotBatch(NPTypeCode, void*, void*, void*, long, long)
The uniform, fully-overlapping MIDDLE region of a sliding correlate, computed in ONE
call — the hot loop np.correlate/np.convolve spend nearly all their time
in. For i in [0, count):
result[i] = Σ_{t=0}^{n2-1} a[i + t] · b[t] (contiguous, element stride 1) — i.e.
count dots each of length n2 over the sliding
window of a, each summed exactly like Dot(NPTypeCode, void*, long, void*, long, void*, long) (NumPy's
@name@_dot). This is the SAME per-position dot NumPy's _pyarray_correlate
runs across the fully-overlapping positions; hoisting it behind one interface call is
what lets the virtual-dispatch + dtype-switch cost be paid ONCE for the whole region
instead of once per output. For a 100K signal that is a ~100K-fold reduction in
dispatch — measured to remove ~1.45 ms of pure per-position overhead from a 2.4 ms
call, leaving only the native ?dot cost that already matches NumPy.
The default implementation just calls Dot(NPTypeCode, void*, long, void*, long, void*, long) per position (so a backend that does not override it keeps the exact old behaviour and byte-parity); a backend overrides it to run the tight loop against its native primitive directly, with no per-position interface dispatch. The result is bit-identical either way — only the dispatch is hoisted.
void DotBatch(NPTypeCode dtype, void* a, void* b, void* result, long count, long n2)
Parameters
dtypeNPTypeCodeOne of Single/Double/Complex — the caller has checked SupportsDot(NPTypeCode) first.
avoid*Base pointer of the data array; the
i-th dot starts ata[i].bvoid*Base pointer of the kernel (the same for every position), length
n2.resultvoid*Base pointer of the contiguous output; the
i-th dot is written toresult[i].countlongNumber of output positions (fully-overlapping middle length).
n2longKernel length — the term count of each dot.
SupportsDot(NPTypeCode)
Whether this backend supplies the byte-parity dot for dtype. NumPy
routes only float32/float64/complex128 through cblas here; complex is
served only when the loaded library also exports the complex products. Every other dtype
returns false and the engine keeps its managed kernel — which is bit-exact by
construction for the integer/bool families and matches NumPy's own scalar loops for the
rest.
bool SupportsDot(NPTypeCode dtype)
Parameters
dtypeNPTypeCode