Algorithms#
An operation's own tolerance covers every implementation that rounds only where the operation must. Real implementations sometimes fix a particular order of additions, accumulate in another format, or, like fused attention kernels, round in extra places.
A named algorithm is one concrete way of computing an operation, with its
own bounds measured against the operation's rounded.
Algorithms in the Guide explains when to use one.
matmul algorithms#
The constructors live in er.linalg.products.
- matmul_sequential(format, *, fma: bool = True, rounding: Rounding = Rounding.NEAREST_EVEN) MatmulAlgorithm#
Products added one at a time in index order, in
format.
- matmul_pairwise(format, *, fma: bool = True, rounding: Rounding = Rounding.NEAREST_EVEN) MatmulAlgorithm#
Products summed as a balanced binary tree, in
format.
- matmul_blocked(format, block_size: int, *, fma: bool = True, rounding: Rounding = Rounding.NEAREST_EVEN) MatmulAlgorithm#
Products summed sequentially in blocks of
block_size, then the block sums sequentially, informat.
- matmul_exact_accumulation(format, *, rounding: Rounding = Rounding.NEAREST_EVEN) MatmulAlgorithm#
Products summed exactly (a long accumulator), then rounded once to
format.
- matmul_slots16(format, *, rounding: Rounding = Rounding.NEAREST_EVEN) MatmulSlots16#
Products rounded to
formatand summed in 16 magnitude slots (MatmulSlots16).
Each returns an algorithm object with these methods:
- class MatmulAlgorithm#
One fully specified matmul: each output element's k products, exact (fused multiply-add) or rounded to the accumulator format, summed by one accumulation method in that format, rounding to nearest (
rounding: ties to even or away), then rounded to the output format. Real floating-point inputs only.- run(A, B, *, dtype=None, rounding: Rounding = Rounding.NEAREST_EVEN, overflow: str | None = None) Array#
This algorithm's output, emulated exactly (on the
roundedvalues of operands that are earlier results).
- error_model(k: int) ErrorModel#
The bound on the accumulated sum of k products, before the output rounding.
- covers(output, A, B, *, dtype=None, rounding: Rounding = Rounding.NEAREST_EVEN, standard: str = 'normal', flush_to_zero: bool = False) Array#
Where this algorithm can produce
output, asResult.coversdescribes; the other arguments aretolerance's.
- finite_range(A, B, *, dtype=None, rounding: Rounding = Rounding.NEAREST_EVEN, standard: str = 'normal', flush_to_zero: bool = False) tuple#
The lowest and highest finite output of this algorithm per element, as
Result.finite_rangedescribes; the arguments aretolerance's.
- may_be(kind: str, A, B, *, dtype=None, rounding: Rounding = Rounding.NEAREST_EVEN, standard: str = 'normal', flush_to_zero: bool = False) Array#
Where this algorithm may output
kind("+inf","-inf"or"nan"), asResult.may_bedescribes; the other arguments aretolerance's.
- tolerance(A, B, *, dtype=None, rounding: Rounding = Rounding.NEAREST_EVEN, standard: str = 'normal', flush_to_zero: bool = False) Array#
Worst-case
|output - matmul(A, B).rounded|per element over this algorithm's finite outputs, asResult.tolerancedescribes.AandBmay be earlier results; their tolerances are then taken withstandardandflush_to_zero(which change nothing for arrays).
- class MatmulSlots16#
The 16-slot matmul (
proofs/matmul.md, The 16-slot algorithm): per output element, each product rounded to the accumulator format (to nearest,rounding: ties to even or away), the products split by magnitude into 16 slots of one sixteenth of the format's precision each (in binades below the largest product; anything smaller goes to the last slot), each slot adding its values in increasing magnitude, alternating positive and negative values, and the slot sums then added from the smallest slot up. Real floating-point inputs only.
import exactref as er
# Products rounded to float32 (no fused multiply-add), added in blocks of 4
alg = er.linalg.products.matmul_blocked("float32", 4, fma=False)
# Its name, and what it does in words
print("name:", alg.name)
print("description:")
print(alg.description)
# A 1x8 row of 0.1 and an 8x1 column of 1.0, in float32
A = er.asarray([[0.1] * 8], "float32")
B = er.asarray([[1.0]] * 8, "float32")
# The algorithm's output, emulated exactly
print("run:", alg.run(A, B).tolist())
# matmul's exact answer, rounded once to float32
print("rounded:", er.matmul(A, B).rounded.tolist())
# The bound on the error of a sum of 8 products, before the output rounding
print("error model:", alg.error_model(8))
name: matmul_blocked
description:
Blocked summation of products rounded to float32 in float32 in blocks of 4
run: [[0.800000011920929]]
rounded: [[0.800000011920929]]
error model: 4.17e-07 * sum over k of |A[i,k] * B[k,j]| + 1.26e-44
Attention algorithms#
- class AttentionAlgorithm(*, online_softmax: bool = False, block_size: int | None = None, exp2: bool = False, weights=None, accumulate=None, bias=None, reciprocal: bool = False)#
A fused-kernel way of computing attention, described by its extra rounding points.
The reference is unchanged (
scaled_dot_product_attention); an algorithm only has atolerance(): how far an implementation that rounds at the required boundaries and at these points can be from the correct answer. It has norun(), because kernels differ in details these methods leave open. Methods, any combination:online_softmax: keys processed in blocks with a running maximum and sum, rescaling earlier partial sums byexp(old max - new max). Withblock_sizeNone, any blocking and order; otherwise at most ceil(S /block_size) blocks of at mostblock_sizekeys (as kernels split the keys: every block full but one), in any order.exp2: weights computed asexp2of logits in base-2 units (the scale times log2(e), and mask values times log2(e), held in the compute format withinulps).weights: the weights rounded to this format before the value product (and in the backward, P and dS before their products).accumulate: every accumulation in this format instead of the compute format.bias: float-mask values rounded to this format before they are added (a kernel computing ALiBi biases in lower precision).reciprocal: normalization by multiplying with the row sum's reciprocal, rounded once, instead of dividing: for the output, and for the backward's probabilities (PyTorch's CPU softmax and its CPU flash kernel do this). That kernel's logsumexp for bfloat16 and float16 inputs uses an exp less accurate than the default 2 ulps:ulps=8covers it.
backward_tolerancegiven the forward's roundedoutandlogsumexpdescribes a fused backward kernel: it recomputesP = exp(logit - logsumexp)and takesD = sum grad * out. Without them it describes a backward that recomputes the softmax, as PyTorch's math path does.online_softmaxapplies to the forward only.Every input of
toleranceandbackward_tolerance,outandlogsumexpincluded, may also be an earlier result, asResultdescribes. The bounds are proven inproofs/attention_algorithms.md.- tolerance(query, key, value, attn_mask=None, dropout_p: float = 0.0, is_causal: bool = False, scale=None, enable_gqa: bool = False, *, dtype=None, rounding: Rounding = Rounding.NEAREST_EVEN, compute=None, ulps=None, flush_to_zero: bool = False, standard: str = 'normal') Outputs#
Worst-case
|output - reference|per element ofoutputandlogsumexp, over this algorithm's finite outputs (Result.tolerance).The arguments are
scaled_dot_product_attention's;compute,ulps,flush_to_zeroandstandardare as inResult.tolerance. Withonline_softmaxboth standards give the any-order bound.
- finite_range(*args, **kwargs) Outputs#
Per output, the lowest and highest finite output of this algorithm, as
Result.finite_rangedescribes; the arguments aretolerance's.
- may_be(kind: str, *args, **kwargs) Outputs#
Per output, where this algorithm may output
kind("+inf","-inf"or"nan"), asResult.may_bedescribes; the other arguments aretolerance's.
- covers(outputs: dict, *args, **kwargs) Outputs#
For each output named in
outputs({"output": ..., "logsumexp": ...}, either or both), where this algorithm can produce that value, asResult.coversdescribes; the other arguments aretolerance's.
- backward_tolerance(query, key, value, grad, out=None, logsumexp=None, attn_mask=None, dropout_p: float = 0.0, is_causal: bool = False, scale=None, enable_gqa: bool = False, *, dtype=None, rounding: Rounding = Rounding.NEAREST_EVEN, compute=None, ulps=None, flush_to_zero: bool = False, standard: str = 'normal') Outputs#
Worst-case
|gradient - reference|per element ofgrad_query,grad_keyandgrad_value, over this algorithm's finite outputs. Given the forward's roundedoutandlogsumexp(any values of the right shapes; a non-finite logsumexp or output gives an infinite bound where it is used), for a fused backward kernel; without them, for a backward that recomputes the softmax.compute,ulps,flush_to_zeroandstandardare as inResult.tolerance.
- backward_finite_range(*args, **kwargs) Outputs#
Per gradient, the lowest and highest finite output of this algorithm, as
Result.finite_rangedescribes; the arguments arebackward_tolerance's.
Base classes#
- class Algorithm#
Base for named algorithms (see the module docstring).
- class ErrorModel(relative: Fraction, absolute: Fraction, scale: str)#
|error| <= relative * scale + absolute, withscalenaming the input quantity.
Accumulation methods#
Accumulation methods: how an implementation may add up a list of terms.
Each method is a fully specified summation order. emulate reproduces it
with a rounding function for the accumulator format. For tolerances, a method
reports depth (the most roundings any one term passes through) and
roundings (the total), which give the standard worst-case bound
|computed - exact| <= gamma(depth) * sum|t_i| + roundings * eta * (1 + gamma(depth))
with gamma(n) = n*u / (1 - n*u) (Higham, Accuracy and Stability of
Numerical Algorithms, 2nd ed., chapters 3-4). The exact method sums
exactly and rounds once, which no implementation can beat.
- class AccumulationMethod(key: 'str', label: 'str', description: 'str', order_dependent: 'bool', parameters: 'tuple[str, ...]' = (), references: 'tuple[str, ...]' = ())#
- emulate(terms: list, round_: Callable, add: Callable = operator.add, **params)#
Sum
termsin this method's order, callinground_wherever the accumulator rounds.
from exactref.accumulation import get_method
# The pairwise summation method
pairwise = get_method("pairwise")
# The most roundings any one of 8 terms passes through
print("depth:", pairwise.depth(8))
# The number of roundings in a sum of 8 terms
print("roundings:", pairwise.roundings(8))
depth: 4
roundings: 15
Pairwise summation rounds each term, then adds the two halves recursively, rounding after every addition. For 8 terms, a term passes through its own rounding and 3 levels of additions (a depth of 4), and there are 8 + 7 = 15 roundings in all.