any_of#

Some implementations do not always compute the same expression. A library may associate a chain of products either way, (A @ B) @ C or A @ (B @ C), or compute some elements by a different code path that rounds an intermediate value first. Each expression has its own exact answer and bounds, and a correct implementation's output can come from any of them.

er.any_of(center, *alternatives) combines the results for the expressions into one result whose bounds accept an output from any of them.

any_of(center: Result, *alternatives: Result) -> Result#

The center's result, with its bounds widened to cover an implementation that computes any one of alternatives instead.

Per element (each part of a complex element), and for every set of tolerance arguments (including the standard): the finite range is the smallest range holding the center's and every alternative's, the infinities and NaN are those any of them allows, and the tolerance is the largest distance from the center's rounded to an end of those ranges, rounded upward. A center part that is not finite has a NaN tolerance, as in every tolerance; a part that has no bound in the center or in an alternative has an infinite one. rounded, exact, guarantee, normal() and backward() are the center's, so normal() is the center expression's normal answer. Every result must have one output, with the same shape and dtype (proofs/any_of.md).

Examples#

PyTorch's float16 add with alpha on the CPU computes some elements as x + alpha * y with one rounding and others with alpha * y stored in float16 first. Neither form alone covers it; any_of covers both:

Code
import random
import torch
import exactref as er

# Seeded, so every run uses the same values
rng = random.Random(0)
# Two float16 tensors of 1000 normal values, mean 0 and standard deviation 1
x = torch.tensor([rng.gauss(0, 1) for _ in range(1000)], dtype=torch.float16)
y = torch.tensor([rng.gauss(0, 1) for _ in range(1000)], dtype=torch.float16)

# PyTorch's output, computed on the CPU
out = torch.add(x, y, alpha=0.3)
# Alpha as the CPU kernel uses it: 0.3 rounded to float16
a = torch.tensor(0.3, dtype=x.dtype).item()

# x + a * y, rounded once to float16
plain = er.add(x, y, alpha=a)
# a * y rounded to float16 first, then added to x
stored = er.add(x, er.multiply(y, a, dtype="float16"))

# Does each form alone accept every element of out?
print("plain covers:", plain.covers(out).all())
print("stored covers:", stored.covers(out).all())

# One result that accepts an output of either form, element by element
either = er.any_of(plain, stored)
print("either covers:", either.covers(out).all())
Output
plain covers: False
stored covers: False
either covers: True

either is plain's result, with rounded, exact, guarantee, normal() and backward() unchanged and only its bounds widened. Chains and any_of shows a case where only the widened bounds accept the implementation.

See also#