PyTorch equivalents#
ExactRef's operations are mathematical operations, not a copy of PyTorch's
API. This page lists the ExactRef call that checks each common PyTorch call,
and where PyTorch rounds more often than the operation requires. Those extra
roundings are described with a chain,
any_of, an
algorithm or an option. In-place and out= variants
map the same way as the plain call.
Matrix products#
PyTorch |
ExactRef |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
a loop of |
er.matmul follows NumPy's batching and 1-D promotion, as torch.matmul
does. er.gemm takes alpha and beta exactly and broadcasts C to the
output's shape; with beta = 0 it does not read C, and with alpha = 0
neither A nor B, as in PyTorch and BLAS. For addr, v and w are
ExactRef arrays or earlier results, which have reshape.
Where PyTorch rounds more#
addbmm rounds each batch's product before adding the next one. That is a
loop of gemm calls on earlier results:
y = C
for i in range(batches):
# Add batch i's product to the stored result, rounding once; beta scales
# only C, in the first call
y = er.gemm(A[i], B[i], y, alpha=alpha, beta=beta if i == 0 else 1)
er.gemm(A, B, C, alpha=alpha, beta=beta, reduce_batch=True) is the exact
form instead: every element is one sum over all batches, rounded once.
Scaling an operand first. An implementation that scales and stores an operand before the product is a chain:
# alpha * A stored first
er.gemm(er.multiply(A, alpha), B, C)
# beta * C stored first
er.gemm(A, B, er.multiply(C, beta), alpha=alpha)
addmv in bfloat16 and float16 may store beta * C in the dtype before the
addition, and, with the matrix stored transposed, also alpha * (A @ x). Each
form is a chain of er.multiply, er.gemm with beta=0 and er.add, and
er.any_of covers both forms.
addr in bfloat16 and float16 computes in the dtype itself. Check it with
covers(out, "bfloat16") or covers(out, "float16").
Batched products in the backward. When both operands keep batch dimensions that broadcast against each other, PyTorch rounds each batch's product before summing the copies. The default tolerance does not budget that.
TF32. A GPU in TF32 mode rounds float32 inputs to TF32 before the product.
Round the inputs the same way and pass those values to er.matmul: TF32
values are float32 values.
Backend parameters#
Parameter |
Express it as |
|---|---|
|
ordinary matrices |
|
one call per group |
|
|
|
a full product, comparing only the triangle the implementation computes |
Elementwise operations and sums#
PyTorch |
ExactRef |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Broadcasting follows NumPy, and mixed formats are promoted as PyTorch promotes
them (bfloat16 with float16 gives float32). An operand of shape () takes
part in the promotion only when every operand has shape (), and one in a
wider format is first rounded once to the promoted format, as PyTorch casts
it. A Python scalar operand is taken exactly and does not change the format.
Integer and bool inputs to sum give int64, as in PyTorch.
torch.sum(x, dtype=d) converts x first, which is the multiply by 1.
Where PyTorch rounds more#
add with alpha converts alpha before using it: PyTorch's CPU kernels
convert it to the tensor's dtype, and its CUDA kernels to the compute type
(float32 for float16 and bfloat16). Pass the converted value. On the CPU:
dtype |
ExactRef |
|---|---|
float32 |
|
bfloat16 |
|
float16 |
|
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)
# Alpha as the CPU kernel uses it: 0.3 rounded to float16
a = torch.tensor(0.3, dtype=x.dtype).item()
print("alpha:", a)
# 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"))
# er.any_of accepts an output of either form, element by element
print("covered:", er.any_of(plain, stored).covers(torch.add(x, y, alpha=0.3)).all())
alpha: 0.300048828125
covered: True
Broadcast operands in the backward. In the backward of add and
multiply with a broadcast operand, PyTorch may round each product before
summing the copies, an extra rounding the default tolerance does not budget.
Special values#
Complex special values follow IEEE rules in each part, as in PyTorch:
(inf + 0j) * (1 + 0j) is inf + nanj. Where mathematics gives an answer,
ExactRef gives it even when PyTorch does not: er.add(1+1j, inf+1j, alpha=1j)
is infj, where PyTorch, multiplying alpha * y out in complex arithmetic,
gives nan+infj.
Attention#
PyTorch |
ExactRef |
|---|---|
|
|
The arguments and shapes are PyTorch's. ExactRef returns two outputs:
output, and the per-row logsumexp that fused kernels return. Dropout is
random and has no reference, so dropout_p must be 0.
Where PyTorch rounds differently#
The math path computes the probabilities P, normalized and rounded,
before the product with V. No tolerance budgets that rounding.
Fused kernels such as FlashAttention round the weights to bfloat16, rescale per block, or normalize by a reciprocal: use an attention algorithm.
Pre-scaled inputs. The math path scales Q and K by the square root of the
scale first. Pass the pre-scaled values with scale=1. For the backward, the
reference is then the gradient for the pre-scaled inputs; the math path's
final product by the square root of the scale is not covered.
Backend parameters#
Parameter |
Express it as |
|---|---|
bottom-right causal alignment |
a bool mask |
|
|
|
a float mask |
|
one call per sequence |
|
Matrix exponential#
PyTorch |
ExactRef |
|---|---|
|
|
its gradient |
|
Guarantees and limits says where PyTorch's and SciPy's results fall outside the tolerance.