The length that changes the kernel
Worth reading first: The same program, twice · The same arithmetic at a different price · The order they are added in.
Everything else in this field needs either a second worker or a compiler flag. This needs neither. It happens in one thread, in one process, from one binary, and it is caused by a constant somebody chose while tuning a library.
The error steps down by a factor of 1.57 between 63 terms and 64. The data is drawn from the same distribution on both sides. The arithmetic is the same precision. What changes at 64 is that the loop stops accumulating into one register and starts using four, because that is how a tuned kernel uses a vector unit.
Why four accumulators is the right thing to do
The switch is not a compromise. It is the correct implementation for two independent reasons and both are worth stating, because this essay is not an argument against it.
It is faster. A floating-point add has a latency of several cycles and a throughput of one per cycle, so a loop with a single accumulator is serialised on the dependency chain — each add waits for the previous one — and runs at a fraction of the machine’s rate. Four independent accumulators break the chain and let the pipeline fill. This is the same reasoning a block size is a property of the machine applies to a blocked elimination, one level down.
And it is more accurate, which is the direction of the step in the figure. Four accumulators is a four-way tree, each of whose walks is a quarter as long, which is exactly the √p improvement where the disagreement comes from derives — √4 = 2 predicted against 1.57 measured.
So the library is doing the right thing twice over. What it is also doing is putting a discontinuity in the accuracy of a routine at a length that has nothing to do with the caller’s problem.
The same shape one level up
Before leaving the dot product it is worth noting that this is the smallest instance of a pattern the site has already met at the level of a whole factorisation, and that the earlier essay reads differently in the light of it.
The same arithmetic at a different price measures a blocked and an unblocked elimination that perform 72,568 operations each, choose the same pivots, and return a factorisation identical to the last bit — while moving 41,332 and 19,476 words between fast and slow memory. Identical arithmetic, identical answer, half the traffic.
That identity is a property of that blocking: it re-orders which updates happen when, and each entry still receives the same sequence of individual multiply-subtracts. A blocking that accumulates a rank-b update before subtracting it — which is what a matrix-multiply-based factorisation does, and what every high-performance library actually runs — does not have that property. It sums b terms first and rounds once, which is a four-accumulator dot product wearing a larger hat.
So the fleet of blocked routines has a block size, the block size is tuned, and the answer depends on it in exactly the way this essay’s dot product depends on its cutoff. The earlier essay’s identical factorisations are the reassuring case, and it is worth knowing which case a library is in.
Why there is a cutoff at all
Below some length the four-accumulator loop is worse, for a reason that is entirely about bookkeeping: it needs a prologue to start four chains, an epilogue to combine them, and a scalar tail for the remainder. At sixteen elements that overhead is most of the work. So every tuned kernel has a threshold, and the threshold is chosen by measurement on the machine the library was tuned for.
Which means the constant is:
- not a property of the problem — the caller’s data does not appear in the decision;
- not a property of the mathematics — both loops compute the same sum in exact arithmetic;
- a property of the library version and the machine it was tuned on, and therefore something that changes when either does.
What moves when the constant moves
The drag makes it concrete: moving the cutoff moves the step and changes nothing else. The discontinuity sits wherever the library was tuned to put it.
The consequence for a caller is specific and rarely noticed. A version bump that retunes this constant from 64 to 32 changes the answer of every dot product whose length lies between them — about a factor of two in the error, in either direction, on every such call. No release note has ever described that as a change of results, because from the library’s point of view it is a performance change: both kernels are correct, both are backward stable, and the accuracy of neither was specified.
And the caller’s symptom is the one this whole field produces: the answers moved and nothing in the program changed. Which is why algorithm selection belongs in this field rather than in the cost field, where its cause lives.
The refusal, which is the sharper statement
The refusal published with this essay is worth reading as a claim rather than as machinery, because it inverts the natural way to describe the figure.
The natural description is a dot product of 60 terms has this accuracy. That is false. At 60 terms the mean relative error is one number if the library’s cutoff is 64 and a different number if it is 128, on identical data — so the accuracy at a given length is a property of which kernel that length selected, and the length is only a proxy for that.
Stated as a rule: a routine whose implementation is selected by the size of its input has an accuracy that is a step function of the size, and the steps are at sizes the caller cannot see.
How wide the family of such constants is
A dot product’s accumulator count is the smallest instance. Once the shape is recognised it is everywhere in numerical software:
A blocked factorisation’s block size, which decides how many updates are accumulated before being written back. The same arithmetic at a different price measures a blocked and an unblocked elimination performing identical operations and returning identical factors — that measurement is for a blocking that re-associates nothing, and a matrix-multiply-based update does re-associate, at a size threshold.
A crossover to a different algorithm entirely. A small eigenproblem solved by a direct formula and a large one by an iteration; a small sort by insertion and a large one by a merge; a small matrix multiplied conventionally and a large one by a recursive method. Each crossover is a size at which the answer changes because the method did.
A threshold that switches to a different precision. Mixed-precision libraries choose a working precision by size or by conditioning, which is the accuracy worth paying for’s subject — and there the change in the answer is large enough that nobody could miss it.
A dispatch on the machine. The same library, on two processors with different vector widths, runs different kernels with different accumulator counts. The binary is identical; the code path is not.
That last one is the version of this that is hardest to reason about, because there is no constant to look up. The answer depends on which instructions the processor supports.
What the step is worth, in the direction that matters
A reader who has followed the field this far will notice that the step in the figure is down — the four-accumulator kernel is more accurate — and may reasonably ask why a change that improves the answer is a problem.
It is a problem for exactly one reason and it is worth isolating: a discontinuity is not an improvement, whichever direction it points.
A caller who grows their problem from 63 unknowns to 64 sees the accuracy of one operation improve by 57%. If they were comparing against a stored result, the comparison fails. If they were extrapolating a convergence study across sizes, the extrapolation acquires a kink. If they were tuning a tolerance against measured behaviour, the tolerance was tuned on the wrong kernel.
And the same step, met from the other side, is a regression: a caller whose library retunes the cutoff upward finds the accuracy of their 100-term dot products getting worse by the same factor, from a version bump that mentioned only performance.
The site has a standing way of describing this and it applies here without modification. Accurate is not a property of a method makes the case that accuracy belongs to a method-and-matrix pair rather than to a method; this essay adds that on a tuned library it belongs to a method-and-matrix-and-size triple, and that the size dependence is not continuous.
What a caller can do about it
Less than in the rest of the field, and it is worth being straightforward about that.
Nothing about the arithmetic. The disagreement is between two kernels, both correct, chosen inside a library. No summation policy applied at the call site changes which one runs.
Pin the version, and record it. The constant belongs to the version, so a computation whose answer must be reproducible in a year needs the version recorded alongside it. This is ordinary software practice and this field’s contribution is a reason: not that the library might have bugs, but that a correct change to a tuning constant changes results.
Or take the reduction back. A caller who needs a bitwise-stable inner product can write one — three operations an element, pre-rounded, and no dispatch, which is the policy the sum that cannot be wrong sets out — and accept the loss of the vendor’s tuning. What determinism costs prices that trade, and on a memory-bound reduction it is cheaper than it sounds.
And, at minimum, do not test near a cutoff. A test suite whose problem size sits at 63 or 64 is sampling a discontinuity, and a version bump will move it across. Sizes chosen away from any power of two are less likely to sit on one.
The measurement is harder than it looks
Worth describing, because the figure took three attempts to get right and the two failures are instructive about how this class of defect hides.
The first attempt varied the length and the data together, drawing fresh vectors at each length from a fresh stream. That measures the step and the ordinary growth of error with n, and on a short sweep the two are the same size — so the figure showed a jagged line with a step somewhere in it, which is a picture of nothing.
The second attempt measured a single draw at each length, which is a sample of a random variable whose spread is larger than the effect. The step was invisible under the scatter. Forty draws per length is what makes the mean stable enough for a factor of 1.57 to be visible.
The third — the figure above — draws from one stream at every length and averages forty. What is held fixed is the distribution; what varies is the length and, at one length, the kernel.
The general lesson is the one this field keeps arriving at from different directions: an effect of this size is not visible without holding something fixed that nobody thinks to hold fixed. The partitioning effect needs one vector across many divisions; the contraction effect needs one matrix across two compilations; and this one needs one distribution across many lengths. Every one of the three is invisible to a measurement that varies the obvious thing.
Where this leaves the field’s account
Three causes have now been measured, and they are genuinely different mechanisms:
| cause | what varies | fixed by |
|---|---|---|
| the partitioning of a reduction | how many pieces, and where the cuts are | an order-independent policy |
| expression contraction | whether a multiply-add was fused | a compiler flag, or an explicit fma |
| algorithm selection | which kernel a size selected | pinning the version |
The first is the field’s main subject and the one with a clean repair. The second is one instruction wide and has a repair that costs nothing. The third has no repair inside the caller’s program at all, and it is the one that will still be there after everything else has been made deterministic.
That ordering is also the ordering of how visible they are. A reduction’s disagreement can be measured from the call site by varying the thread count. A contraction’s can be measured by compiling twice. A kernel cutoff can only be measured by sweeping the problem size and looking for a step, which is not something anybody does unless they already suspect it.
What a library could publish
The field’s recommendations have so far been addressed to callers, because callers are the ones who can act. This one is addressed to the other side of the interface, and it is short.
Publish the cutoffs. A library that documents this routine uses a four-way accumulator above 64 elements has converted an invisible discontinuity into a fact a caller can design around: they can avoid sizes near it, or force one kernel, or record it beside a stored result. The constant is not a secret and it is already in the source; what is missing is a statement that it affects results.
And treat a change to one as a change to results. A release note that says improved dot product performance describes a change that moves numbers. Nothing about the change is wrong; what is wrong is the category it is announced in, and a note saying the accumulator threshold moved from 64 to 32; dot products of length 32 to 63 now return slightly different values would cost one line and would answer a class of bug report entirely.
Neither of these is a numerical proposal. Both are the same observation this field keeps making from different angles: the property that two runs return the same answer is real, it is currently nobody’s responsibility, and most of what would fix it is a matter of saying out loud what is already true.
At other settings
What links here
Computed from the collection, not written here: the essays that point at this one.
Shares its objects with
Essays that name at least two of the same things, and that neither author linked.
- The fifth author — both name algorithm selection, bitwise reproducibility, reduction order, run-to-run variation
- A bound every answer satisfies — both name error accumulation, reduction order, run-to-run variation
- Accuracy and agreement are different properties — both name bitwise reproducibility, reduction order, run-to-run variation
- An inner product with no fixed sign — both name bitwise reproducibility, reduction order, run-to-run variation
- The tolerance that buys no agreement — both name bitwise reproducibility, reduction order, run-to-run variation
- The variation that comes with a seed — both name bitwise reproducibility, reduction order, run-to-run variation
Named objects
A flat tag is an object no other essay names yet.
Algorithm selectionBitwise reproducibilityBlocked algorithmError accumulationLoop orderMemory hierarchyReduction orderRun-to-run variation