Skip to content

0006. Reduction order defaults to sequential-equivalent

Context

A parallel reduction can combine partial results in many orders. The fastest is a tree-combine of per-worker partials. But floating-point addition is not associative: (a + b) + c and a + (b + c) can differ in their last bits. A tree-combine of partial sums therefore produces a different result than the left-to-right fold that sequential Python performs.

For a library whose one guarantee is bit-identical results, a reduction that returns different bits than sequential is a broken guarantee, even though the answer is "numerically close."

Decision

The default reduction order is sequential_equivalent: per-element contributions are folded in the exact order the sequential loop would combine them. Chunks are index-contiguous by construction, so the parent folds per-chunk contributions in chunk order, which reproduces the sequential left-to-right fold bit for bit. There is no partial-sum re-association in this mode.

Two opt-in modes exist for users who explicitly want them and understand the trade: stable permits a reproducible tree-combine (where combine parallelism can win), and custom takes a user callable whose associativity is the user's assertion. Both are declared, so choosing them is a conscious act, and they are the only two documented exceptions to naive/expert output parity.

Consequences

  • A parallel float reduction is bit-identical to sequential, on every backend and every interpreter. This is verified continuously by the property suite, which compares float reductions bit-for-bit.
  • The fold cost is proportional to the number of chunks, not the number of elements, so sequential-equivalent ordering is effectively free.
  • Combine parallelism (a tree of partial sums) is unavailable by default. This is deliberate: it is an optimization that would break bit-identity, so it is opt-in behind stable.

Spec: technical specification section 5.12.