A type system for floating-point reproducibility
The system checks reproducibility, so nobody has to remember to.
Vertical autoscaling can change CPU limits without restarting the container.
Running container
2-way parallelism
Same running container
16-way parallelism
Go 1.25 can adjust execution parallelism live as CPU limits change.
Java, Rust and C libraries can select different worker counts after restarting with a new CPU allocation.
You wanted the same answer, sooner. But now you’ve got a different answer.
Same code. Same data. A numerical simulation on autoscaled infrastructure.
Did a parameter drift, or did the cluster scale? A decision about capacity has become a decision about what my application computes.
Same 5,000 pseudorandom values. Fixed seed: 1.
Four bit patterns versus one.
Results shown to 13 decimal places.
Change only the degree of parallelism and the analysis returns a different evolutionary tree.
Same binary. Same seeds.
“May contain nuts.”
Settled at the dish that carries it. Nothing added elsewhere can make it false.
The implementation may choose the order.
“Contains no nuts.”
A claim about every ingredient that ever entered the pot. One nut anywhere falsifies it, and no later stirring takes it back out.
A promise about the result, so everything upstream has to keep it.
These bits must not depend on the schedule is the second kind of sentence. And unlike nuts, you cannot test the dish: one run looks fine, and it takes two runs to disagree.
A grant is true where it is written. A demand was settled upstream, by everything that ever touched the value.
Every addition in both sums is correctly rounded. IEEE 754 requires it.
| System | The knob | Attached to | Direction | Enforced? |
|---|---|---|---|---|
| LLVM / MLIR | reassoc, contract | an instruction | grant | no |
| SPIR-V | FPFastMathMode | an instruction | grant | no |
| Icing opt: | a scoped grant | an expression | grant | verified |
| Futhark reduce | a promise | an operator | grant | no |
| Halide rfactor | proved associativity | a directive | grant | in the reals |
| OpenMP | reduction | a construct | grant | no |
| MPI_REDUCE | implicit reorder licence | a collective | grant | no |
| CCCL require | a level | one call | demand | that call only |
| oneTBB reduce | split/join per partitioner | one call | demand | no |
| Batch invariance | order fixed by extent | one kernel | demand | no |
| Kernel contracts | three-level class | one kernel | demand | by invocation |
| IEEE 754 | reproducibility attribute | a lexical block | demand | by the language |
| LS-DYNA | consistency flag | a run | demand | no |
| GLSL precise | a qualifier | a value + producers | demand | no computed class |
| This work | a lane | a value | demand | whole path |
Three classes, carried on every value of a pure dataflow array language. One such label on one value is a lane.
The bits equal the serial reference.
The bits must not move with the schedule. Tomorrow's run agrees with today's.
The bits may move with the schedule. The default, because only some results need the promise.
It matches the serial reference, rounding error and all.
Determinism today does not mean determinism tomorrow.
When two values meet, the result takes the worse of the two.
The label is set by the worst thing that ever touched the value, and it never improves. One free fold is the nut in the pot.
Once the rounding has happened, the low bits are gone.
$ ./build/bin/lanelang check src/tests/lanelang/reject_poison.lane src/tests/lanelang/reject_poison.lane:21:5: error: determinism lane violation: this value computes approx where replay is demanded. An unpinned schedule cannot be recovered downstream: the rounding already happened. Pin the producer by naming it with a replay or exact qualifier, or weaken the demand 21 | replay demanded := sum(x + pre) | ^^^^^^ the demand is stated here ... the guarantee was lost 2 steps upstream: 20 | approx pre := sum(x * 0.25) | ~~~ introduced here src/tests/lanelang/reject_poison.lane:20:19: note: 'sum' is a fold/free, so it contributes approx to the join 20 | approx pre := sum(x * 0.25) | ^^^ lanelang: 1 determinism lane violation (exit 1)
One qualifier, on the one value you care about.
replay demanded := sum(x + pre)The same program, one word deleted.
$ ./build/bin/lanelang check talk/demo/heal_poison.lane accepted: 1 declaration checked, 1 producer healed, result lane replay (exit 0)
With replay, the backend picks the order. With exact, your program picks it.
| approx | replay | exact | |
|---|---|---|---|
| Who picks the order | each run | the backend | your program: serial, by default |
| Same bits | never promised | every rerun on the same setup, any worker count | anything that computes the same order, in principle |
| What can change your answer | any run | a new GPU, library or kernel | only an edit to your code |
| Speed | fastest | tuned to vector and warp width | serial, by default |
| Reach for it when | nothing depends on the bits | shipping; reruns and audits on the same setup | matching a plain loop; goldens shared across machines; a CPU checking a GPU |
The lane goes on the call, not inside the function.
Both demands are accepted, and the checker keeps the two kernels apart.
The schedule gets none. The input bytes get exactly one.
The lattice handles it. Once the rounding has happened, nothing can waive it.
A source is ungrounded: the program does not fix its bytes, so replay over it is refused.
“These bytes will be the same next run.”
The run checks it before computing anything.
The last slide's program, without the blessing. The bits move with the world, not the schedule.
$ ./build/bin/lanelang check src/tests/lanelang/reject_ungrounded_replay.lane src/tests/lanelang/reject_ungrounded_replay.lane:19:5: error: replay demands a cone the program fixes or a blessing vouches for, and this value is neither. The bits do not move with the schedule here, they move with the world: vouch for it with @bless(...) on the binding that names it, take the value as a parameter, which makes it the caller's obligation, or weaken the demand to approx 19 | replay energy := sum(x * 2.0) | ^^^^^^ src/tests/lanelang/reject_ungrounded_replay.lane:18:10: note: 'source' is a leaf this program does not fix, so its value is not a function of the program 18 | x := source(count: 1000.0) | ^^^^^^ lanelang: 1 ungrounded demand (exit 1)
The lane is part of the value's identity.
The lane is part of the value's identity, so common-subexpression elimination can't merge them.
Unannotated code lowers to the identical free kernel.
At most d + 2 passes across d carried edges.
LS-DYNA, pinned order, shared-memory build, serial assembly: “at least 15 percent”.
CCCL gpu_to_gpu, large problem sizes: “20% to 30%”.
The unannotated program and its approx twin lower to the identical free kernel. Pinned by two fixtures.
With no recurrence. At most d + 2 passes when lane information crosses d carried edges.
d counts the carried edges lane information must cross. The ceiling is d + 2.
total := iterate(..., with: (outer, t) => {
nested := iterate(..., with: (inner, u) => {
return inner + sum(x)
})
return outer + nested
})Two recurrences, one inside the other.
a := iterate(..., with: (acc, t) => {
return acc + sum(x)
})
b := iterate(..., with: (acc, t) => {
return acc + a
})
c := iterate(..., with: (acc, t) => {
return acc + b
})Three recurrences in sequence; each reads the previous result.
The checker stops when a pass changes no lanes. These are graph-analysis passes, not loop iterations. The sketches omit iteration settings.
Both passes visit the whole function. The colours show the information discovered so far.
func main() Float {x := indices(count: 1000.0)total := iterate(times: 3.0, from: 0.0,with: (acc, t) => {replay hello := accapprox part := sum(x)return hello + part})return total}
Body and total: approx. The earlier demand still appears satisfied.
func main() Float {x := indices(count: 1000.0)total := iterate(times: 3.0, from: 0.0,with: (acc, t) => {replay hello := accapprox part := sum(x)return hello + part})return total}
Body and total: approx. The replay demand on hello is violated.
Amber: approx - Green: demand appears satisfied - Red: demand violated
Pass 3 confirms no change.
A demand can change its private producers, but cannot change a value observed outside its scope.
func main() Float {x := indices(count: 1000.0)s := sum(x)t := s + 1.0replay e := sum(x * t)return e}
s reaches the result only through e. The demand permits pinning s.
Replay demand satisfied.
func main() Float {x := indices(count: 1000.0)s := sum(x)t := s + 1.0replay e := sum(x * t)return t + e}
t reaches the result without passing through e. Pinning s could change that outside result.
Replay demand cannot be met.
Kernel class sets the strongest guarantee. Input lanes can weaken the result.
| Kernel | Its bits depend on | Class | Witnessing check |
|---|---|---|---|
| Elementwise arithmetic | No reassociable combination; host compiler outside S | neutral | L2 (combination) |
| Data movement, literals | No combination | neutral | L2 |
| External source | Outside world; grounding refuses unsupported demands | neutral, ungrounded | L3, CC1, L2 |
| Carried-state recurrence | Strictly sequential; no schedule choice of its own | neutral | L3, L4, L2 |
| Fixed-tree reduction | Data and shape | replay | L4, L2, CC2 |
| Fixed-block scan | Data and shape; serial block scans and serial base pass | replay | L3, L4, CC3, L2 |
| Oracle-order strict fold | Data; matches the serial reference by construction | exact | L4 (reference), CC4, L2 |
| Exact-integer reduction | Data; matches serial while integers’ absolute values sum below 2⁵³. Evaluation fails outside that range. | exact | L3, L4, L2 (chunking) |
| Free reduction, free scan | Schedule; reassociation allowed | approx | L4, L2 |
| Parallel scatter | Unchecked: no parallel scatter implemented | approx | None |
| Transcendental functions | Unchecked: no maths library specified | approx | None |
Neutral kernels add no weakening. The table covers the demonstration language’s kernels.
An accepted demand delivers the determinism it asks for.
Replay covers deployment schedules. Exact also agrees with the serial reference.
A stronger kernel cannot repair a weaker input guarantee.
The join keeps the weakest guarantee among the kernel and all its inputs.
Every downstream dependency inherits approx, however strong its own kernel.
This is a law about labels. It does not claim the actual answers must differ.