← Research index
ORIGINAL RESEARCH5 September 2026

How Numerical Contracts Break: Five Cases in Swift Numerics and nntrainer

Permanent archive: Zenodo · 10.5281/zenodo.22696455.

Five bounded audit cases show how documented equivalence, exact identities, input preservation and parameter propagation expose failures that fixed expected values can miss.

Khamit Kadyrbekov
Xamit KadirbekovAuthors · GERO Research
Numerical contractsSoftware testingOpen source
STATUS · AUTHOR'S ANALYSISArguments and proposals are distinguished from the linked laboratories' reported results.

A numerical library can pass its tests and still violate a documented equivalence, return a complex value in the wrong quadrant, mutate its own inputs, or bypass a shared gradient-scaling mechanism.

In an independent audit of Swift Numerics and nntrainer, I looked beyond isolated expected numbers. I tested exact mathematical relations, repeated-call consistency, input preservation, parameter propagation, and agreement between public documentation and runtime behavior.

The result is a set of five compact cases. The first is an accuracy observation rather than a confirmed defect. The other four are open upstream reports with reproducible counterexamples.

This is not a security assessment or a general judgment on either project. It is a practical demonstration of contract-based numerical testing.

Audit method

The workflow varied by case, but followed the same structure:

1. Identify a documented property, mathematical identity, or pipeline contract.

2. Build a minimal reproducer.

3. Compare against an independent oracle or an exact internal relation.

4. Locate the responsible source branch.

5. Test a bounded repair with focused regression checks.

6. Report the result upstream with explicit scope and limitations.

Case 1 — One ULP in Double.root

The initial observation was:

Double.root(3125, 5)

The mathematical fifth root of 3125 is exactly 5, while the runtime result is one unit in the last place above it:

5.000000000000001

This result alone does not establish a library defect. Xiaodi Wu correctly noted that the behavior is not unique to `root` or to Swift Numerics: it can arise from composing approximate floating-point operations.

The useful conclusion is narrower. The case exposes a boundary of observed accuracy and shows why surprising rounding must be separated from violations of exact contracts.

Discussion:

Case 2 — Complex.reciprocal and a documented equivalence

The public documentation says that, when `reciprocal` returns a value rather than `nil`, direct division is always equivalent to multiplication by that value.

A counterexample is:

let a = Complex<Double>(1e300, 0)
let b = Complex<Double>(1e300, 1e200)

print(a / b)
print(a * b.reciprocal!)

The two paths return:

a / b           = (0.9999999999999999, -1e-100)
a * reciprocal  = (0.9999999999999999,  0.0)

The imaginary component of the reciprocal underflows to zero. Direct division uses scaling and preserves its contribution.

This is not a demand that arbitrary algebraically equivalent floating-point expressions be bitwise identical. It is a counterexample to a specific statement in the public API documentation.

Report:

Case 3 — The wrong quadrant in Complex.cosh and Complex.sinh

The more serious Swift Numerics case appears in a branch designed to avoid overflow:

let z = Complex<Double>(-40, 0.5)

print(Complex.cosh(z))
print(Complex.sinh(z))

Observed:

cosh(z) =  1.032850027510405e17 + 5.642485416641618e16 i
sinh(z) = -1.032850027510405e17 - 5.642485416641618e16 i

The imaginary signs should be the opposite. Exact identities determine the expected quadrants:

cosh(-x + iy) = conjugate(cosh(x + iy))
sinh(-x + iy) = -conjugate(sinh(x + iy))

These checks do not depend on another library or on a tolerance near the last bit. An 80-decimal-digit `mpmath` evaluation independently confirms the expected signs.

The cause is in the large-`abs(x)` fast paths. In `cosh`, the sign of the negative real input is not applied to the imaginary component. In `sinh`, it is applied to both components even though it should affect only the real one. The error then propagates to `Complex.cos` and `Complex.sin`.

This is a wrong-quadrant result, not a last-bit discrepancy. Existing near-overflow tests contain the same incorrect sign expectations.

Report:

Case 4 — KLD backward mutates its label

In nntrainer's Kullback–Leibler divergence layer, the derivative path negates the label in place:

label.multiply_i(-1.0f);
label.divide(predicted, deriv);

For P = 0.25 and Q = 0.5, the expected derivative is -0.5. Two consecutive backward calls produce:

before backward:  label = +0.25
first backward:   gradient = -0.5, label = -0.25
second backward:  gradient = +0.5, label = +0.25

The gradient alternates because the computation mutates its input state.

A bounded local repair writes the negative value directly into the output tensor while leaving `label` unchanged. The focused regression check passes after that change.

Report:

Case 5 — Four loss layers bypass loss_scale

nntrainer has a shared dynamic loss-scaling mechanism, but four implemented loss layers do not call the existing helper:

  • sigmoid cross-entropy;
  • softmax cross-entropy;
  • KLD;
  • constant-derivative loss.

With `loss_scale = 8`, a derivative of -0.5 should become -4.0. In the affected layers it remains -0.5.

Adding the existing call:

LossLayer::applyLossScale(context, ret_derivative);

makes all four focused counterexamples and 71 focused regression tests pass. The KLD layer uses the output name `deriv`, but the helper call is the same.

I do not claim a fully green project build: 104 unrelated golden tests could not run correctly because their runtime data was absent from the tested checkout.

Report:

What the cases have in common

There is no demonstrated single root cause across these cases. What unifies them is the detection method: test properties and contracts in addition to fixed expected numbers.

The most productive checks were:

  • exact mathematical relations between outputs;
  • preservation of input data;
  • repeatability under identical calls;
  • propagation of control parameters through the full pipeline;
  • agreement between public documentation and runtime behavior.

One unexpected last digit is not automatically a defect. A wrong component sign, mutation of an input tensor, omission of `loss_scale`, or a direct counterexample to a documented equivalence is a different class of evidence.

The practical conclusion is simple: numerical systems should be tested not only with examples, but also with properties that must hold for entire families of inputs. Those checks can expose failures that ordinary example-based tests miss.

As of 5 September 2026, the four reports presented here as defects — #346, #347, #4328, and #4329 — remain open and have no maintainer comments.

Executable Swift audit materials:

Root-accuracy audit materials:

Independent reproduction and analysis by Xamit Kadirbekov / GERO. No affiliation with Apple, Swift Numerics, Samsung, or the nntrainer maintainers is claimed.