vkML 0.1.0

Element-wise

22 functions — 22 documented.

abs

abs(arg: Tensor, /) → Tensor
CPUVulkan

Absolute value, element-wise.

Dispatches GLSL's abs() on the GPU and std::fabs on the CPU. abs(-0.0) is +0.0 and abs(NaN) is NaN on both.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> x = vkml.tensor(np.array([-2.0, -0.0, 3.0], dtype=np.float32))
>>> vkml.abs(x).numpy()
array([2., 0., 3.], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:38
Graph nodeOpKind::Abs
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:167
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:272
Historycommits touching the CPU kernel
Testsnone found by name

See also sign, neg, square

neg

neg(arg: Tensor, /) → Tensor
CPUVulkan

Arithmetic negation, element-wise.

Compiled as unary minus rather than a multiply by −1, so the sign bit is flipped and nothing else: neg(0.0) is -0.0, and neg(NaN) keeps the payload.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.neg(vkml.tensor(np.array([1.0, -2.5], dtype=np.float32))).numpy()
array([-1. ,  2.5], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:37
Graph nodeOpKind::Neg
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:163
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:270
Historycommits touching the CPU kernel
Testsnone found by name

See also abs, sub

scaled_add

scaled_add(a: Tensor, b: Tensor, alpha: float, beta: float) → Tensor
CPUVulkan

input * alpha + other * beta, as one operation.

Both coefficients are plain numbers and travel in the kernel's push constants, so nothing is materialised for them.

The composed form costs FOUR nodes rather than one: a scalar operand becomes a rank-0 tensor, so a * 0.9 is a full and a mul. Measured on an SGD-with-momentum step over an MNIST MLP, the composed form issued 24 dispatches against 8 here.

Shapes are broadcast by NumPy rules. Broadcasting is implemented by giving the expanded axes stride 0 rather than by materialising a copy — Shape holds strides in bytes and permits 0, so a broadcast operand re-reads the same element instead of allocating an expanded one.

Parameters

input (Tensor)
The left operand.
other (Tensor)
The right operand, broadcastable against input.
alpha (float)
Coefficient applied to input.
beta (float)
Coefficient applied to other.

Returns

A tensor of the broadcast shape.

ⓘ Note

Bit-identical to input * alpha + other * beta, on both backends, checked byte for byte against the composed form and an independent f32 reference. It is a cost change, not a numerical one.

Example

>>> a = vkml.tensor(np.array([1.0, 2.0], dtype=np.float32))
>>> b = vkml.tensor(np.array([10.0, 20.0], dtype=np.float32))
>>> vkml.scaled_add(a, b, 0.5, 2.0).numpy()
array([20.5, 41. ], dtype=float32)

From the header

[[nodiscard]] Tensor scaled_add(const Tensor& a, const Tensor& b, double alpha, double beta);include/vkml/api/ops.h:73

`a * alpha + b * beta`, as one operation.

The composed form costs four nodes, because each coefficient is materialised as a rank-0 tensor before the multiply. This costs one, and the coefficients travel in the push constants.

Bit-identical to the composed form by construction: both multiplies round to f32 before the add, exactly where mul would have stored its result, and the shader is written so the compiler cannot contract them into an FMA (ADR 0005 makes the same argument for GEMM).

Named for what it computes rather than for its caller. Every momentum optimiser is built from this shape -- SGD's velocity and parameter updates, RMSProp's and Adam's moving averages -- which is what earns it an operator.

In the CPU kernel

aalpha + bbeta.

Written as two multiplies and an add, each rounding to f32, so it matches the composed a*alpha then + b*beta exactly -- and matches the shader, which is written the same way for the same reason. float intermediates rather than the double a naive reading might use: widening here would make the oracle disagree with every backend it is meant to check.

src/backend/cpu/kernels_elementwise.cpp:256

Implementation

Declared ininclude/vkml/api/ops.h:73
Graph nodeOpKind::ScaledAdd
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:256
Vulkan shadershaders/scaled_add.comp (67 lines · 2 specialisation constants)
Gradient ruleautograd.cpp:207
DecisionsADR 0013
Historycommits touching the CPU kernel
Tests (≥4)test_invariants.py

See also add, mul

exp

exp(arg: Tensor, /) → Tensor
CPUVulkan

Natural exponential, element-wise.

exp(x) overflows to +inf above about 88.72 in float32 — that bound, ln(FLT_MAX), is the same one that makes a naive tanh return NaN. Operators built on exp here subtract a maximum first for exactly that reason; see softmax and sigmoid.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

ⓘ Note

A subnormal result may be flushed to zero on the GPU. Vulkan permits flush-to-zero for float32 denormals unless shaderDenormPreserveFloat32 is requested, and vkML does not request it — so exp(-89) is 0.0 on the GPU and the subnormal 2.227e-39 on the CPU.

Example

>>> vkml.exp(vkml.tensor(np.array([0.0, 1.0, 88.0], dtype=np.float32))).numpy()
array([1.0000000e+00, 2.7182817e+00, 1.6516363e+38], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:44
Graph nodeOpKind::Exp
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:191
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:289
Benchmarkedexp
Historycommits touching the CPU kernel
Tests (≥5)test_autograd_vs_torch.py test_f16.py test_vulkan_kernels.py

See also log, sigmoid, softmax, tanh

log

log(arg: Tensor, /) → Tensor
CPUVulkan

Natural logarithm, element-wise.

log(0) is -inf and log(x) for x < 0 is NaN, matching IEEE and torch.

Parameters

input (Tensor)
Any shape. float32 or float16. Negative values give NaN.

Returns

A tensor of the same shape and dtype.

⚠ Warning

Taking the log of a softmax output underflows: a probability reaches 0 in float32 once the logit gap passes about 90, and log(0) then poisons every gradient in the batch. Use log_softmax, which never forms the probability.

Example

>>> vkml.log(vkml.tensor(np.array([1.0, np.e], dtype=np.float32))).numpy()
array([0.        , 0.99999994], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:45
Graph nodeOpKind::Log
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:195
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:291
Historycommits touching the CPU kernel
Tests (≥2)test_autograd_vs_torch.py

See also exp, log_softmax, cross_entropy

sqrt

sqrt(arg: Tensor, /) → Tensor
CPUVulkan

Square root, element-wise.

sqrt(-0.0) is -0.0 and sqrt(x) for x < 0 is NaN, both matching IEEE.

Parameters

input (Tensor)
Any shape. float32 or float16. Negative values give NaN.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.sqrt(vkml.tensor(np.array([4.0, 9.0], dtype=np.float32))).numpy()
array([2., 3.], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:41
Graph nodeOpKind::Sqrt
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:179
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:276
Historycommits touching the CPU kernel
Tests (≥1)test_autograd_vs_torch.py

See also rsqrt, square

rsqrt

rsqrt(arg: Tensor, /) → Tensor
CPUVulkan

Reciprocal square root, 1/sqrt(x), element-wise.

Compiled to GLSL's inversesqrt() rather than a divide after a sqrt. It is one instruction on the GPU, and it is the form normalisation layers want — rms_norm and layer_norm both scale by an inverse root rather than dividing.

Parameters

input (Tensor)
Any shape. float32 or float16. Negative values give NaN.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.rsqrt(vkml.tensor(np.array([4.0, 16.0], dtype=np.float32))).numpy()
array([0.5 , 0.25], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:42
Graph nodeOpKind::Rsqrt
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:183
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:282
Historycommits touching the CPU kernel
Testsnone found by name

See also sqrt, reciprocal, rms_norm

reciprocal

reciprocal(arg: Tensor, /) → Tensor
CPUVulkan

1/x, element-wise.

reciprocal(0.0) is +inf and reciprocal(-0.0) is -inf, matching IEEE division rather than raising.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.reciprocal(vkml.tensor(np.array([2.0, 4.0], dtype=np.float32))).numpy()
array([0.5 , 0.25], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:43
Graph nodeOpKind::Reciprocal
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:187
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:287
Historycommits touching the CPU kernel
Testsnone found by name

See also div, rsqrt

square

square(arg: Tensor, /) → Tensor
CPUVulkan

x * x, element-wise.

A multiply, not pow(x, 2): exact for every representable input, one instruction, and its gradient is 2x rather than the general power rule.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.square(vkml.tensor(np.array([-3.0, 4.0], dtype=np.float32))).numpy()
array([ 9., 16.], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:40
Graph nodeOpKind::Square
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:175
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:274
Historycommits touching the CPU kernel
Tests (≥3)test_autograd_vs_torch.py test_ops_vs_torch.py

See also pow, sqrt, mse_loss

sign

sign(arg: Tensor, /) → Tensor
CPUVulkan

The sign of each element: −1, 0 or +1.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> x = vkml.tensor(np.array([-2.0, -0.0, 0.0, 3.0], dtype=np.float32))
>>> vkml.sign(x).numpy()
array([-1.,  0.,  0.,  1.], dtype=float32)

In the Vulkan kernel

float sign_op(float x)shaders/unary.comp:241

sign, matching torch on every input including the ones that surprise.

The fall-through returns +0.0, covering +0.0, -0.0 AND NaN, none of which satisfies either comparison. It used to return x, with a comment claiming that matched torch. Measured, torch agrees with neither half: torch.sign(-0.0) is +0.0 and torch.sign(nan) is +0.0. numpy differs from torch on NaN but agrees on -0.0, so the old behaviour matched neither reference (issue #27).

GLSL's built-in sign() would give the right answer for -0.0 and is undefined for NaN, so the comparisons are written out.

THE SIGN OF THE ZERO IS NORMALISED EXPLICITLY, and two drivers disagree about why that is needed. Written as the obvious chain --

if (x > 0.0) { return 1.0; } if (x < 0.0) { return -1.0; } return 0.0;

-- this returns -0.0 on lavapipe for every input that falls through: +0.0, -0.0, +NaN and -NaN alike, measured by reading the bits back. RADV gives +0.0 for the same SPIR-V, so it went unnoticed until a second driver ran it. The transform that explains it is merging the -1.0 and 0.0 returns into a negation, whose zero case is -0.0; many optimisers treat the sign of a zero as free to change.

Two rewrites were measured on both drivers before this one was chosen:

- Spelling the zero as uintBitsToFloat(0u) does nothing. It compiles to BYTE-IDENTICAL SPIR-V, because glslang folds it long before a driver sees it -- verified by hashing the generated module. - float(x > 0.0) - float(x < 0.0) is branchless and fixes lavapipe, and it is WRONG ON RADV: NaN comes back as -1.0, so the comparison it compiles to is not the ordered one the source asks for. It would have traded one driver's bug for another's.

So the branches stay -- they are correct on RADV, and ordered comparisons make NaN fall through with the zeros -- and the result's sign bit is cleared when its magnitude is zero. That is a no-op for ±1.0 and forces +0.0 for everything else, whatever the optimiser did on the way.

Implementation

Declared ininclude/vkml/api/ops.h:39
Graph nodeOpKind::Sign
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:171
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient rulenone — backward through it raises
Historycommits touching the CPU kernel
Tests (≥2)test_extreme_values.py test_nan_semantics.py

See also abs, relu

sin

sin(arg: Tensor, /) → Tensor
CPUVulkan

Sine, element-wise, in radians.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

⚠ Warning

GLSL does not require sin/cos to be accurate for large arguments, and the driver's argument reduction gives up long before FLT_MAX. Measured on RADV, sin(3.4e38) returns 0.0 against −0.522 on the CPU. The suite treats magnitudes above 1e6 as outside the comparable range; nothing vkML does changes this, because the reduction happens inside the built-in.

Example

>>> vkml.sin(vkml.tensor(np.array([0.0, np.pi / 2], dtype=np.float32))).numpy()
array([0., 1.], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:53
Graph nodeOpKind::Sin
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:207
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:293
Historycommits touching the CPU kernel
Tests (≥1)test_autograd_vs_torch.py

See also cos

cos

cos(arg: Tensor, /) → Tensor
CPUVulkan

Cosine, element-wise, in radians.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

⚠ Warning

The same argument-reduction limit as sin: not required to be accurate above roughly 1e6, and the inaccuracy is the driver's, not vkML's.

Example

>>> vkml.cos(vkml.tensor(np.array([0.0, np.pi], dtype=np.float32))).numpy()
array([ 1., -1.], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:54
Graph nodeOpKind::Cos
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:211
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:295
Historycommits touching the CPU kernel
Testsnone found by name

See also sin

tanh

tanh(arg: Tensor, /) → Tensor
CPUVulkan

Apply the hyperbolic tangent, element-wise.

Saturates to ±1 outside roughly |x| > 10, and the Vulkan kernel clamps there explicitly.

Parameters

input (Tensor)
Any shape, float dtype.

Returns

A tensor of the same shape and dtype.

⚠ Warning

GLSL defines tanh as (eˣ − e⁻ˣ) / (eˣ + e⁻ˣ), which evaluates to inf/inf = NaN once |x| passes ln(FLT_MAX) ≈ 88.72. Whether a driver does that is left to the driver — one AMD driver did. vkML does not use the built-in, so a finite input never yields NaN here.

Example

>>> x = vkml.tensor(np.array([-89.0, -1.0, 0.0, 89.0], dtype=np.float32))
>>> vkml.tanh(x).numpy()
array([-1.       , -0.7615942,  0.       ,  1.       ], dtype=float32)

In the Vulkan kernel

float tanh_op(float x)shaders/unary.comp:151

tanh, saturated before the built-in can overflow.

GLSL defines tanh as (e^x - e^-x) / (e^x + e^-x), and an implementation that evaluates that literally produces inf/inf = NaN once |x| passes ln(FLT_MAX) = 88.7228. A FINITE input then yields NaN, which breaks the oracle contract in ARCHITECTURE.md 7 and fails silently -- a NaN appearing mid-network reads as a diverged model, not a kernel defect. RADV computes it some other way and is unaffected; AMD's Windows driver is not (issue #26).

The same lesson as f16 narrowing in common.glsl: where a built-in's edge behaviour is left to the implementation, do not depend on it.

Clamping the INPUT rather than the output keeps the built-in on the path for every argument it handles. 10.0 is where the answer saturates in f32 -- tanh(10) is exactly 1.0f, while tanh(9.5) is still 0.99999994 -- so no value loses precision to this.

NaN passes through: abs(NaN) > 10.0 is false, as every NaN comparison is, so a NaN reaches the built-in and stays one, matching std::tanh on the CPU.

Implementation

Declared ininclude/vkml/api/ops.h:55
Graph nodeOpKind::Tanh
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:215
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:297
Historycommits touching the CPU kernel
Tests (≥4)test_autograd_vs_torch.py test_extreme_values.py

See also sigmoid, gelu

sigmoid

sigmoid(arg: Tensor, /) → Tensor
CPUVulkan

The logistic function, 1/(1 + exp(−x)), element-wise.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.sigmoid(vkml.tensor(np.array([-1.0, 0.0, 1.0], dtype=np.float32))).numpy()
array([0.26894143, 0.5       , 0.7310586 ], dtype=float32)

In the Vulkan kernel

float sigmoid_op(float x)shaders/unary.comp:124

Logistic function, in the same two-branch form the CPU kernel uses.

The textbook 1/(1+exp(-x)) relies on exp() overflowing to inf and 1/inf giving 0 for large negative x. That happens to be right, but the branch keeps the argument to exp() bounded above by 0 in both tails and does not depend on the driver's overflow behaviour.

Implementation

Declared ininclude/vkml/api/ops.h:56
Graph nodeOpKind::Sigmoid
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:219
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:303
Historycommits touching the CPU kernel
Tests (≥1)test_autograd_vs_torch.py

See also tanh, silu, binary_cross_entropy_with_logits

erf

erf(arg: Tensor, /) → Tensor
CPUVulkan

The error function, element-wise.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.erf(vkml.tensor(np.array([0.0, 1.0], dtype=np.float32))).numpy()
array([0.       , 0.8427008], dtype=float32)

In the Vulkan kernel

float erf_op(float x)shaders/unary.comp:93

erf, split at |x| = 0.5.

Above the split, 1 - erfc is accurate because both terms are O(1). Below it that subtraction cancels -- erf(0.01) is 1.1e-2 against erfc = 0.989 -- so the Maclaurin series is used instead, where every term carries the factor x and no cancellation occurs. Seven terms leave a truncation error of ~3e-8 relative at the split point, below the 1 ULP the policy allows.

Implementation

Declared ininclude/vkml/api/ops.h:46
Graph nodeOpKind::Erf
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:199
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient rulenone — backward through it raises
Historycommits touching the CPU kernel
Testsnone found by name

See also erfc, gelu

erfc

erfc(arg: Tensor, /) → Tensor
CPUVulkan

The complementary error function, 1 − erf(x), element-wise.

Computed directly rather than as 1 − erf(x), which cancels catastrophically in the positive tail — the tail this function exists to serve. gelu is built on it for the same reason.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.erfc(vkml.tensor(np.array([0.0, 1.0], dtype=np.float32))).numpy()
array([1.       , 0.1572992], dtype=float32)

From the header

[[nodiscard]] Tensor erfc(const Tensor& a);include/vkml/api/ops.h:52

Complementary error function, 1 - erf(x), computed without forming that difference. Exists because the subtraction is what destroys the result: erf approaches +/-1, so 1 - erf(x) and 1 + erf(x) cancel and lose the significand exactly where erfc is still perfectly representable. gelu's value and its gradient are both built on it for that reason.

In the Vulkan kernel

float erfc_op(float x)shaders/unary.comp:114

erfc over the whole line, reflecting the x >= 0 approximation.

erfc(-x) = 2 - erfc(x) is used for the negative half, and unlike the erf case that subtraction is safe: erfc_pos(x) is at most 1 there, so the result is O(1) and nothing cancels. The tail this exists to protect is the POSITIVE one, where erfc decays toward zero and is returned by erfc_pos directly.

NaN falls to the second branch, since x >= 0.0 is false for it, and propagates through unchanged. +inf gives 0 and -inf gives 2, both exact.

Implementation

Declared ininclude/vkml/api/ops.h:52
Graph nodeOpKind::Erfc
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:203
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient rulenone — backward through it raises
Historycommits touching the CPU kernel
Testsnone found by name

See also erf, gelu

relu

relu(arg: Tensor, /) → Tensor
CPUVulkan

Apply the rectified linear unit, element-wise.

Computes x <= 0 ? 0 : x.

The comparison is written against zero rather than as max(x, 0) so that NaN propagates: max would return the non-NaN operand on some drivers and silently swallow it.

Parameters

input (Tensor)
Any shape, float dtype.

Returns

A tensor of the same shape and dtype.

ⓘ Note

relu(-0.0) returns +0.0, matching PyTorch.

Example

>>> import numpy as np, vkml
>>> x = vkml.tensor(np.array([-2.0, -0.0, 0.0, 3.0], dtype=np.float32))
>>> vkml.relu(x).numpy()
array([0., 0., 0., 3.], dtype=float32)

In the Vulkan kernel

float relu_op(float x)shaders/unary.comp:198

relu, written so that NaN survives it.

x <= 0.0 ? 0.0 : x, NOT x > 0.0 ? x : 0.0. The two agree on every number and differ on NaN, which fails BOTH comparisons: the first form falls through to x and propagates, the second falls through to 0 and destroys it. torch propagates, as do maximum(x, 0) and clamp_min(x, 0) -- the same function spelled differently, which vkml already got right. relu was the odd one out (issue #27), and a NaN silently becoming 0 mid-network hides a diverged model rather than announcing it.

Implementation

Declared ininclude/vkml/api/ops.h:57
Graph nodeOpKind::Relu
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:223
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:397
DecisionsADR 0011
Benchmarkedrelu
Historycommits touching the CPU kernel
Tests (≥16)test_autograd_vs_torch.py test_device_limits.py test_extreme_values.py test_invariants.py test_nan_semantics.py test_ops_vs_torch.py test_vulkan_kernels.py

See also gelu, silu, sigmoid, clamp_min

gelu

gelu(arg: Tensor, /) → Tensor
CPUVulkan

Apply the Gaussian error linear unit, element-wise.

Computes the exact form x · Φ(x), where Φ is the standard normal CDF — not the tanh approximation.

Both backends evaluate Φ through erfc rather than as 0.5(1 + erf(x/√2)). The second form cancels catastrophically in the negative tail: measured over 512 points on [-6, -3] it reached a relative error of 1.0, returning exactly zero on a domain where the true value never is.

Parameters

input (Tensor)
Any shape, float dtype.

Returns

A tensor of the same shape and dtype.

⚠ Warning

gelu(-inf) is NaN, matching PyTorch. A large finite negative input is not — it underflows smoothly to zero.

Example

>>> x = vkml.tensor(np.array([-6.0, -1.0, 0.0, 1.0], dtype=np.float32))
>>> vkml.gelu(x).numpy()
array([-5.9195355e-09, -1.5865526e-01,  0.0000000e+00,  8.4134471e-01],
      dtype=float32)

In the Vulkan kernel

float gelu_op(float x)shaders/unary.comp:165

Exact GELU: 0.5x(1 + erf(x/sqrt2)), matching torch's default approximate='none'. The tanh approximation differs by up to ~1e-3, far outside the 1e-5 gate, and would look like a bug rather than a choice.

Written through erfc rather than erf because 1 + erf(u) cancels for u < 0: at x = -3 the true value is 2.7e-3 formed from 1 and -0.997, losing most of the significand. 1 + erf(u) == erfc(-u) computes it directly.

Implementation

Declared ininclude/vkml/api/ops.h:74
Graph nodeOpKind::Gelu
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:233
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:411
Historycommits touching the CPU kernel
Tests (≥5)test_autograd_vs_torch.py test_f16.py test_invariants.py test_vulkan_kernels.py

See also erf, erfc, silu, relu

silu

silu(arg: Tensor, /) → Tensor
CPUVulkan

SiLU / Swish: x · sigmoid(x), element-wise.

Composed in the kernel as a multiply against sigmoid_op, so it inherits sigmoid's two-branch form and its overflow safety rather than repeating them.

Parameters

input (Tensor)
Any shape. float32 or float16.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.silu(vkml.tensor(np.array([-1.0, 0.0, 1.0], dtype=np.float32))).numpy()
array([-0.26894143,  0.        ,  0.7310586 ], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:75
Graph nodeOpKind::Silu
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:237
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:436
Historycommits touching the CPU kernel
Tests (≥1)test_autograd_vs_torch.py

See also sigmoid, gelu, relu

clamp

clamp(input: Tensor, min: float, max: float) → Tensor
CPUVulkan

Limit every element to [min, max].

Implemented as two nested comparisons, x < lo ? lo : (x > hi ? hi : x), rather than min(max(x, lo), hi). The bounds arrive as push constants, so no extra tensor is allocated for them.

Parameters

input (Tensor)
Any shape, float dtype.
min (float)
Lower bound, inclusive.
max (float)
Upper bound, inclusive.

Returns

A tensor of the same shape and dtype.

ⓘ Note

NaN fails both comparisons and falls through unchanged, so it propagates.

Example

>>> x = vkml.tensor(np.array([-3.0, 0.5, 7.0], dtype=np.float32))
>>> vkml.clamp(x, -1.0, 1.0).numpy()
array([-1. ,  0.5,  1. ], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:76
Graph nodeOpKind::Clamp
CPU kernelsrc/backend/cpu/kernels_elementwise.cpp:316
Vulkan shadershaders/unary.comp (292 lines · 3 specialisation constants)
Gradient ruleautograd.cpp:444
Historycommits touching the CPU kernel
Tests (≥4)test_autograd_vs_torch.py test_ops_vs_torch.py test_vulkan_kernels.py

See also clamp_min, clamp_max, maximum, minimum, relu

clamp_min

clamp_min(input: Tensor, min: float) → Tensor
CPUVulkan

Raise every element to at least min.

clamp_min(x, 0) is relu computed a different way, and the two agree on every input including NaN — which is the point of relu's comparison form.

Parameters

input (Tensor)
Any shape, float dtype.
min (float)
Lower bound, inclusive.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.clamp_min(vkml.tensor(np.array([-2.0, 3.0], dtype=np.float32)), 0.0).numpy()
array([0., 3.], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:77
CPU kernelcomposed from other operators
Vulkan shadercomposed, or dispatched through a shared kernel
Gradient rulenone — backward through it raises
Tests (≥3)test_nan_semantics.py test_ops_vs_torch.py test_vulkan_kernels.py

See also clamp, clamp_max, relu, maximum

clamp_max

clamp_max(input: Tensor, max: float) → Tensor
CPUVulkan

Lower every element to at most max.

Parameters

input (Tensor)
Any shape, float dtype.
max (float)
Upper bound, inclusive.

Returns

A tensor of the same shape and dtype.

Example

>>> vkml.clamp_max(vkml.tensor(np.array([-2.0, 3.0], dtype=np.float32)), 0.0).numpy()
array([-2.,  0.], dtype=float32)

Implementation

Declared ininclude/vkml/api/ops.h:78
CPU kernelcomposed from other operators
Vulkan shadercomposed, or dispatched through a shared kernel
Gradient rulenone — backward through it raises
Tests (≥1)test_vulkan_kernels.py

See also clamp, clamp_min, minimum

vkML — Vulkan-first machine learning in C++20. Apache-2.0. Signatures on this page are generated from the installed module.