nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
fused_kernels.hpp
1// ops/include/nk/ops/fused/fused_kernels.hpp
2//
3// Fused element-wise kernels: each is a hand-written, fixed-structure SIMD
4// loop that computes a whole common idiom (an element-wise chain) in ONE pass
5// — every array leaf read once, the output written once, intermediates kept in
6// registers. This is the form that actually wins: a runtime micro-VM can't keep
7// intermediates in registers (variable slot indices spill to the stack), so it
8// loses to the per-op SIMD path; a fixed-structure kernel does not.
9//
10// These are the primitives behind VM element-wise fusion (the compiler/walker
11// recognise an idiom and dispatch here, falling back to the per-op path when
12// the runtime types don't fit — see the fusion rule registry). Real double
13// only; `out` may alias an array input (each element reads its inputs before
14// its own store). Large arrays are threaded internally via parallel_for.
15//
16// One Highway TU per kernel (fused_<name>_highway.cpp) keeps MSVC's per-TU
17// inliner budget from leaking between kernels (same reason the FFT kernels are
18// split); the scalar fallbacks share fused_kernels_portable.cpp.
19
20#pragma once
21
22#include <complex>
23#include <cstddef>
24
25namespace nk::ops {
26
27// out[i] = max(lo, min(hi, scale*x[i] + offset)) — affine then clamp.
28// Covers normalize+saturate idioms: max(0,min(1, a.*x+b)), rescale, ReLU(+aff).
29void fusedAffineClamp(const double *x, double scale, double offset,
30 double lo, double hi, double *out, std::size_t n);
31
32// out[i] = min(hi, max(lo, scale*x[i] + offset)) — the min-outer spelling of
33// the same clamp. Identical to fusedAffineClamp for finite inputs, but differs
34// on NaN (min-outer saturates a NaN to lo, max-outer to hi — matching the
35// respective per-op min/max nesting), so it is a separate kernel, not a flag.
36void fusedAffineClampMinOuter(const double *x, double scale, double offset,
37 double lo, double hi, double *out, std::size_t n);
38
39// Divide-inner clamps: out[i] = clamp((x[i] - sub)/div, lo, hi). (x-c)./d is a
40// distinct rounding from scale*x+offset (1/d would round), so these are their
41// own kernels — the canonical rescale-then-saturate `max(0,min(1,(x-lo)./rng))`.
42void fusedAffineClampShiftDiv(const double *x, double sub, double div,
43 double lo, double hi, double *out, std::size_t n);
44void fusedAffineClampMinOuterShiftDiv(const double *x, double sub, double div,
45 double lo, double hi, double *out,
46 std::size_t n);
47
48// out[i] = scale*x[i] + offset — plain affine, NO clamp, NaN-preserving.
49// Covers two-op scale-and-shift chains: a.*x+b, x.*a-b, b+a.*x (rescale,
50// negate, unit conversion). A separate kernel from fusedAffineClamp because a
51// ±inf "clamp" can NOT stand in for "no clamp": fmin(+inf, NaN) = +inf would
52// erase a NaN, whereas scale*NaN+offset = NaN flows through here untouched.
53void fusedAffine(const double *x, double scale, double offset,
54 double *out, std::size_t n);
55
56// out[i] = a*x[i] + b*y[i] — two-array linear combination (x, y same size).
57// Covers blend/lerp (b=1-a), weighted sum, scaled difference (b<0), a.*x+b.*y.
58// Mul, Mul, Add (no FMA) so it matches the per-op `a.*x + b.*y` bit-for-bit.
59void fusedAxpby(const double *x, double a, const double *y, double b,
60 double *out, std::size_t n);
61
62// out[i] = (x[i] - sub) * mul — center then scale (rescale, normalize,
63// z-score by a precomputed reciprocal). Sub then Mul (two roundings) so it is
64// bit-identical to the per-op `(x - c).*s`. NaN/Inf propagate naturally.
65void fusedShiftScaleMul(const double *x, double sub, double mul,
66 double *out, std::size_t n);
67
68// out[i] = (x[i] - sub) / div — center then divide ((x-mu)./sigma,
69// (x-lo)./range). Sub then Div (two roundings) so it is bit-identical to the
70// per-op `(x - c)./d` (which can't be folded into a *mul: 1/d would round).
71void fusedShiftScaleDiv(const double *x, double sub, double div,
72 double *out, std::size_t n);
73
74// out[i] = |scale*x[i] + offset| — magnitude of an affine. Mul, Add, then
75// abs (abs is exact — a sign-bit clear), so bit-identical to `abs(a.*x ± b)`.
76// Also serves `abs(x - c)` (scale 1) since |1*x + (-c)| == |x - c|.
77void fusedAbsAffine(const double *x, double scale, double offset,
78 double *out, std::size_t n);
79
80// out[i] = |x[i] - y[i]| — absolute difference of two arrays (L1 residual /
81// error). Sub then abs (exact) → bit-identical to `abs(x - y)`.
82void fusedAbsDiff(const double *x, const double *y, double *out, std::size_t n);
83
84// out[i] = |(x[i] - sub)/div| — abs of a divide-inner (abs(x./d),
85// abs((x-c)./d)); the divide rounding differs from scale*x+offset, abs exact.
86void fusedAbsShiftDiv(const double *x, double sub, double div,
87 double *out, std::size_t n);
88
89// Unary function applied to an affine: out[i] = f(scale*x[i] + offset).
90// Restricted to the functions whose SIMD form is bit-identical to the scalar
91// libm one — sqrt is correctly-rounded; floor/ceil are exact — so the result
92// matches the per-op `f(a.*x ± b)` regardless of how the array is chunked.
93// (exp/log/sin… are NOT here: Highway's polynomial differs from libm by a few
94// ULP, so they'd only match if the per-op tail-vs-SIMD split were reproduced.)
95// Sqrt's negative-input domain (where MATLAB promotes to complex) is handled by
96// the caller, which declines before invoking this kernel. Fix is hn::Trunc
97// (toward zero, exact); Round is round-half-away-from-zero computed as
98// Trunc(v + CopySign(0.5, v)) — NOT hn::Round (which is round-to-nearest-even,
99// the wrong tie rule for MATLAB) — mirroring nk's RoundLoop bit-for-bit.
100enum class UnaryAffineFn { Sqrt, Floor, Ceil, Fix, Round };
101void fusedUnaryAffine(const double *x, double scale, double offset,
102 UnaryAffineFn fn, double *out, std::size_t n);
103
104// out[i] = f((x[i] - sub) / div) — the divide-inner variant (f(x./d),
105// f((x-c)./d)). Sub-then-Div is a distinct rounding from scale*x+offset, so it
106// can't reuse fusedUnaryAffine; same f-set (sqrt correctly-rounded, floor/ceil
107// exact) → bit-identical to the per-op f((x-c)./d).
108void fusedUnaryShiftDiv(const double *x, double sub, double div,
109 UnaryAffineFn fn, double *out, std::size_t n);
110
111// out[i] = (scale*x[i] + offset)^2 — squared affine (energy, squared error,
112// squared deviation `(x-c).^2`). Mul, Add, then a final Mul (the square); three
113// roundings, matching the per-op `(a.*x ± b).^2` now that x.^2 is x.*x.
114void fusedSqAffine(const double *x, double scale, double offset,
115 double *out, std::size_t n);
116
117// out[i] = (x[i] - y[i])^2 — squared difference of two arrays (SSE term).
118// Sub then Mul → matches `(x - y).^2`.
119void fusedSqDiff(const double *x, const double *y, double *out, std::size_t n);
120
121// out[i] = ((x[i] - sub)/div)^2 — square of a divide-inner ((x./d).^2,
122// ((x-c)./d).^2 = squared z-score); divide rounding then a plain-Mul square.
123void fusedSqShiftDiv(const double *x, double sub, double div,
124 double *out, std::size_t n);
125
126// out[i] = sqrt(x[i]*x[i] + y[i]*y[i]) — magnitude / 2-D length / gradient
127// magnitude, the literal `sqrt(x.^2 + y.^2)`. x*x, y*y, Add, Sqrt (no FMA); a
128// sum of real squares is never negative, so the result stays real (no complex
129// promotion) and sqrt is correctly-rounded → bit-identical to per-op.
130void fusedSqrtSumSq(const double *x, const double *y, double *out, std::size_t n);
131
132// out[i] = sign(x[i]) * max(0, |x[i]| - t) — soft-threshold / wavelet
133// shrinkage / L1 proximal. sign matches MATLAB (NaN→NaN, ±0→0, ±Inf→±1) and
134// max(0,·) omits NaN; every step is exact/IEEE, so it is bit-identical to the
135// per-op `sign(x) .* max(0, abs(x) - t)`.
136void fusedSoftThreshold(const double *x, double t, double *out, std::size_t n);
137
138// out[i] = f(scale*x[i] + offset) for a transcendental f. Unlike sqrt/floor/
139// ceil, these Highway forms (hwy/contrib/math) differ from libm by a few ULP,
140// so to stay bit-identical to the per-op f(a.*x ± b) this kernel mirrors
141// nk's exp/log/sin/… loop exactly: the same hn:: on the SIMD body, the same
142// std:: on the per-chunk scalar tail, chunked by the same kTranscendentalThreshold.
143// Cosh has no Highway primitive — it is composed 0.5*(Exp(v)+Exp(-v)) exactly as
144// nk's CoshLoop. Tan has none either: it mirrors nk's TanLoop — SLEEF's
145// xtan kernel (TanVec) on lanes with |inner| < 1e6, a per-block std::tan fallback
146// otherwise (so the per-block decision lands on the same inner values as per-op).
147// Always-real (no domain guard): Exp, Expm1, Sin, Cos, Tanh, Sinh, Atan, Asinh,
148// Cosh, Tan. Complex-promoting in MATLAB — the caller declines on the offending
149// range (a pre-scan) and lets the per-op path produce the complex result:
150// {Log,Log2,Log10} on a negative; Log1p on < -1; Acosh on < 1; {Asin,Acos,Atanh}
151// on |·| > 1.
152enum class TransAffineFn { Exp, Expm1, Log, Log2, Log10, Sin, Cos, Tanh,
153 Sinh, Atan, Asinh, Asin, Acos, Acosh, Atanh,
154 Log1p, Cosh, Tan };
155void fusedTransAffine(const double *x, double scale, double offset,
156 TransAffineFn fn, double *out, std::size_t n);
157
158// out[i] = f((x[i] - sub) / div) — the divide-inner variant (f(x./d),
159// f((x-c)./d)) of fusedTransAffine. Same mirror-the-nk-loop discipline.
160void fusedTransShiftDiv(const double *x, double sub, double div,
161 TransAffineFn fn, double *out, std::size_t n);
162
163// ---- complex-input kernels ---------------------------------------------
164// When the array operand is complex, fusion routes here. These are scalar
165// std::complex<double> loops (Highway has no complex transcendentals, and
166// nk's per-op complex path is itself scalar std::complex), so each kernel
167// mirrors nk's per-op composition with the SAME std::complex operators /
168// std:: functions on the SAME compiler — bit-identical by construction. They
169// still eliminate the intermediate temporaries (the DRAM-traffic win), just
170// without SIMD. `out` may alias `x`. Complex is "total" — no domain declines.
171using Cx = std::complex<double>;
172
173// out[i] = scale*x[i] + offset (mirrors `a.*z` then `+b`).
174void fusedAffineCx(const Cx *x, Cx scale, Cx offset, Cx *out, std::size_t n);
175// out[i] = a*x[i] + b*y[i].
176void fusedAxpbyCx(const Cx *x, Cx a, const Cx *y, Cx b, Cx *out, std::size_t n);
177// out[i] = (x[i] - sub) * mul / (x[i] - sub) / div.
178void fusedShiftScaleMulCx(const Cx *x, Cx sub, Cx mul, Cx *out, std::size_t n);
179void fusedShiftScaleDivCx(const Cx *x, Cx sub, Cx div, Cx *out, std::size_t n);
180// The complex f(inner) kernels share an inner shape selected by `affine`:
181// affine=true → scale*x[i] + offset (genuine complex multiply: the
182// product-coefficient inner kinds a.*z[±b]).
183// affine=false → (scale==+1 ? x[i] : -x[i]) + offset (a pure shift/neg, where
184// scale is exactly ±1: add/negate, NOT a complex mul by (±1+0i),
185// whose 0*Inf=NaN would diverge from the per-op bare add/sub).
186// This lets every affine spelling (product, shift x±c, c-x, -x) fuse on complex.
187
188// out[i] = std::pow(<inner>, 2). On a COMPLEX base, nk's `.^2` takes the
189// std::pow branch (the .^2 == z.*z fast-path is real-only), so these mirror
190// std::pow(v, 2) — NOT v*v (they differ for complex).
191void fusedSqAffineCx(const Cx *x, Cx scale, Cx offset, bool affine,
192 Cx *out, std::size_t n);
193void fusedSqDiffCx(const Cx *x, const Cx *y, Cx *out, std::size_t n);
194// (z-c).^2 / (c-z).^2 — array-scalar squared diff. `rev` picks the subtract
195// order: complex pow(-w,2) != pow(w,2), so the order is NOT foldable (unlike a
196// real square). The subtract is genuine (no mul-by-1).
197void fusedSqDiffScalarCx(const Cx *z, Cx c, bool rev, Cx *out, std::size_t n);
198// sqrt(x.^2 + y.^2) on complex arrays → std::sqrt(pow(x,2)+pow(y,2)) (complex).
199void fusedSqrtSumSqCx(const Cx *x, const Cx *y, Cx *out, std::size_t n);
200// ((x-sub)/div).^2 — divide-inner square on complex → std::pow((x-sub)/div, 2).
201void fusedSqShiftDivCx(const Cx *x, Cx sub, Cx div, Cx *out, std::size_t n);
202
203// sqrt of a complex affine / divide-inner: std::sqrt(scale*x+offset) /
204// std::sqrt((x-sub)/div) — whole-complex (not component-wise).
205void fusedSqrtAffineCx(const Cx *x, Cx scale, Cx offset, bool affine,
206 Cx *out, std::size_t n);
207void fusedSqrtShiftDivCx(const Cx *x, Cx sub, Cx div, Cx *out, std::size_t n);
208
209// floor/ceil/fix/round of a complex affine / divide-inner — applied COMPONENT-
210// WISE to re and im (nk's per-op roundLikeDispatch) with the same std::
211// floor/ceil/trunc/round. fn must be one of {Floor,Ceil,Fix,Round} (Sqrt has its
212// own whole-complex kernel above).
213void fusedRoundLikeAffineCx(const Cx *x, Cx scale, Cx offset, bool affine,
214 UnaryAffineFn fn, Cx *out, std::size_t n);
215void fusedRoundLikeShiftDivCx(const Cx *x, Cx sub, Cx div, UnaryAffineFn fn,
216 Cx *out, std::size_t n);
217
218// transcendental of a complex affine / divide-inner: f(scale*x+offset) /
219// f((x-sub)/div), mirroring nk's complex op per fn — mostly std::F(z), with
220// log2 = log(z)/log(2), log1p = log(1+z), expm1 = exp(z)-1.
221void fusedTransAffineCx(const Cx *x, Cx scale, Cx offset, bool affine,
222 TransAffineFn fn, Cx *out, std::size_t n);
223void fusedTransShiftDivCx(const Cx *x, Cx sub, Cx div, TransAffineFn fn,
224 Cx *out, std::size_t n);
225
226// abs of a complex affine / div-inner / two-array diff → the REAL magnitude
227// std::abs(z) (= hypot), so `out` is double. |a.*z+b|, |z±c|, |-z|, |z./d|, |z-w|.
228void fusedAbsAffineCx(const Cx *x, Cx scale, Cx offset, bool affine,
229 double *out, std::size_t n);
230void fusedAbsShiftDivCx(const Cx *x, Cx sub, Cx div, double *out, std::size_t n);
231void fusedAbsDiffCx(const Cx *x, const Cx *y, double *out, std::size_t n);
232// |z - c| array-scalar (= |c - z|, order-agnostic) → real magnitude.
233void fusedAbsDiffScalarCx(const Cx *z, Cx c, double *out, std::size_t n);
234
235// complex soft-threshold: out[i] = sign(z) .* max(0, |z| - t), sign(z) = z/|z|
236// (0 at z==0) — MATLAB's complex shrinkage. t real; out complex.
237void fusedSoftThresholdCx(const Cx *x, double t, Cx *out, std::size_t n);
238
239// ---- shared scalar bodies (small-N gate ↔ portable fallback) -----------
240// Internal — NOT part of the fusion API (declared alongside the kernels, in
241// nk::ops rather than ::detail, because the public fused kernels live here
242// too and a bare `detail::` in the Highway TUs already means nk::detail).
243// The kernels that carry a small-N inline gate (fusedAffine, fusedAbs*,
244// fusedSq*) share their scalar body here: the portable fallback and the Highway
245// gate both call these, so they are one definition (can't drift) and the loop
246// lives in a lambda-free TU (fused_scalar.cpp) that auto-vectorizes regardless
247// of the dispatcher's parallel_for capture. See fused_scalar.cpp +
248// bugs/ops/cheap-elementwise-simd-small-n. Bit-identical to the per-op path
249// (one IEEE chain each).
250void fusedAffineScalar(const double *x, double scale, double offset, double *out, std::size_t n);
251void fusedAbsAffineScalar(const double *x, double scale, double offset, double *out, std::size_t n);
252void fusedAbsShiftDivScalar(const double *x, double sub, double div, double *out, std::size_t n);
253void fusedAbsDiffScalar(const double *x, const double *y, double *out, std::size_t n);
254void fusedSqAffineScalar(const double *x, double scale, double offset, double *out, std::size_t n);
255void fusedSqShiftDivScalar(const double *x, double sub, double div, double *out, std::size_t n);
256void fusedSqDiffScalar(const double *x, const double *y, double *out, std::size_t n);
257
258} // namespace nk::ops