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
alternativesinstead.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
roundedto 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()andbackward()are the center's, sonormal()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:
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())
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.