Lowent Manual←↑→

45 Proofs about races and parallelism — discipline instead of a memory model

What to know first

chapter 25, Actors · state is locked inside the actor and ownership moves by message
chapter 27, Parallel loops and atomic operations · the three conditions for a splittable loop, and reduce
chapter 42, Proofs about ownership and borrowing · within one flow, only one name writes

Looking back

In chapter 27, why did the W-PAR-OK note say that the VM running sequentially is a correct implementation?

A. Because a theorem is proven that when the splittable conditions hold, the parallel result is bit-for-bit identical to the sequential one. This chapter covers that theorem and the one that must stand before it — “there are no data races”. They are different conclusions from the same premise.

The need for this chapter, and its context

When two flows touch the same place at the same time and one of them writes, it is a data race. In C and C++ it is undefined behaviour. The usual way to deal with races is to learn a memory model such as memory orderings, which is hard. This language claims something else — code in split loops (level 1) and actors (level 2) never needs to look at a memory model. Races are ruled out by discipline. And a guarantee that answers are the same every time, even without races, is needed separately. This chapter is the story of establishing both claims in pure Coq without heavy concurrency logic. Reading the proposition exactly reduces the tools needed.

By the end of this chapter

You will learn the three-line set condition called Bernstein independence and the definition of a race. You will pick up the pair of theorems that at level 1 independence means no races and the condition is necessary, and the theorem that at level 2 a message always lies between accesses by different actors. You will also see why having no races differs from being deterministic, DET-1 (parallel results are bit-for-bit identical to sequential), DET-2 (freedom for the scheduler), DET-3 (associativity frees the shape of the reduction tree), and the counterexample for each.

The questions this chapter answers

  1. Isn’t “roughly the same answer” good enough?

45.1 The Bernstein condition — an answer from 1966#

Let R and W be the set of places read and set of places written by a task. Two tasks are independent in three lines.

W₁ ∩ W₂ = ∅      the two do not write the same place
W₁ ∩ R₂ = ∅      one does not read what the other writes
R₁ ∩ W₂ = ∅      nor the reverse

Three lines saying intersections are empty are all there is. R₁ ∩ R₂ is not in the condition — reading the same place together is fine. A race is defined as “two tasks touch the same place and at least one writes”. The usual definition adds “the two accesses are not ordered”, but at level 1 that holds automatically. Between tasks after flows split and before they rejoin, there are no ordering edges at all.

The processor’s diagnostic quotes this condition directly.

examples/ch45/par_overlap.low

module par_overlap .
rem expect: E-PAR-WRITE

rem every piece writes the first cell --- what remains depends on who went last
proc stamp input s mut slice u64 . output u64 . effects none .
  parallel s split .
do
  var i u64 be 0 .
  while lt i (len s) . do
    set (index s 0) i .
    set i (add i 1) .
  end
  return len s .
end

Output

$ lowentc --check par_overlap.low
par_overlap.low:10:0 E-PAR-WRITE: a splittable loop may only write its OWN element `index <s> <i>` — this write can collide with another iteration (Bernstein: wr ∩ wr = ∅)
par_overlap.low:10:0 E-PAR-READ: a splittable loop may only read its OWN element of the split slice — reading another index creates a cross-iteration dependence (Bernstein: rd ∩ wr = ∅)

Every piece writes the first element, so wr ∩ wr = ∅ broke, and reading another piece’s element broke rd ∩ wr = ∅ too. chapter 27′s three conditions — read only your own share, write only your own share, do not write places living across iterations — are the Bernstein condition.

45.2 Independence means no races, and the condition is necessary#

The mathematics. Level 1 — split loops (LowentDRF.v)

l1_no_race : forall t1 t2 l, indep t1 t2 -> ~ races t1 t2 l. If two tasks are Bernstein independent, they race at no place. The proof is five lines. Assuming a race means “one writes”, and the independence condition denies the other’s access to that place in all three cases. Contradiction.

l1_condition_is_necessary : ~ indep writer reader /\ races writer reader 0. Actually build a pair breaking the condition — one writes place 0 and the other reads place 0 — and a race exists.

The second theorem is the mark of an honest proof. Proving only “satisfying the condition is safe” leaves the theorem true even if the condition is overly strict — a condition that lets nothing pass is also safe. So a counterexample is proven alongside: loosen the condition and it really breaks. One theorem, one counterexample — a method this part uses in many places.

Without races, the well-known DRF-SC theorem (Adve–Hill) applies and the program behaves with sequential consistency. That is, you need not imagine orders jumbled up.

45.3 Between actors there is always a message#

The mathematics. Level 2 — actors (l2_accesses_are_separated_by_a_message)

If in an execution trace different actors access the same place l, then between the two accesses there is always a message handing over ownership of that place.

The chain of argument runs like this. A place is owned by exactly one actor. Only the owner accesses it. Ownership moves only by message. But the two accesses come from different actors. Therefore an ownership transfer happened between them. A message is an edge fixing before and after, and two accesses with a fixed order are by definition not a race. The most common mistake with actors — sending a reference by message and continuing to use it on the sending side — is stopped because it is an ownership transfer and the sender loses that place (chapter 25′s handoff).

The connection to chapter 42 is the point. Enforcing “only one name writes” within one flow becomes exactly “two tasks do not overlap”. One safety rule pays off in two places.

45.4 Containment — where heavy tools are needed#

level 1 (split loops)          ─┐
level 2 (actor isolation)       ─┤→ no races by discipline → DRF-SC → think sequentially
level 3 (atomics · locks)       ─┘→ races really exist     → a memory model is needed

A common misconception. Proving concurrency always needs heavy tools like separation logic

The first plan was to leave “safe code has no races” as a sketch and do the machine proof with heavy concurrency logic. Looking again, that proposition was not a claim about memory models. “Discipline prevents races” is a set-theoretic claim. Heavy tools are really needed only at level 3, and among the proof files only the lock uses Iris (chapter 47). “It is concurrency, so separation logic” was a reflex.

45.5 No races is not the same as deterministic#

Running twice on the same input can give different answers. Even without races. If several cores add floating-point numbers in parts and combine them, the last bits change when the order of combining changes. Each touched only its own share, so there is no race. Yet the answers differ.

This language’s claim is strong. The result of level 1 parallelism is bit-for-bit identical to the sequential result. No approximation, no tolerance. The conditions establishing that claim were proven to be exactly three (LowentPar.v). Determinism is not a matter of races but of the Bernstein condition. The same condition gives a different conclusion.

ConditionWith itWithout it
DET-1 split without overlapParallel = sequential (det1_par_eq_seq)Order changes the result (overlap_is_nondeterministic)
DET-2 keep dependency edgesThe remaining order is free (det2_schedule_free)— not using the freedom only loses performance
DET-3 associative combiningTree shape does not change the result (assoc_shape_free)Same leaves, different results (nonassoc_shape_matters)

Table 45.1 — Three conditions for deterministic parallelism — with and without

The mathematics. DET-1 — parallel equals sequential

If tasks are pairwise independent and each is local (decides values only from its own read set), the value at every place i after parallel execution equals sequential execution. The corollary’s name is the claim itself — det1_bit_identical. The fork in the proof is instructive. If the first task writes place i, no one else writes i, so the answer is the first task’s. If the first task does not write i, the one writer among the rest gives the answer, and since that task’s reads do not overlap the first task’s writes, reading the original state or the state the first task touched gives the same value. The second case says why “local” is needed.

The counterexample is two tasks writing 1 and 2 to the same place. Depending on order, the answer is 2 or 1.

DET-2 is the theorem that swapping two non-conflicting tasks gives the same result. The proof is induction on adjacent swaps. If adjacent pairs can be swapped, repeating that reaches “any rearrangement that keeps the conflict order” — the same idea as a sorting algorithm. It gives the scheduler maximal freedom without losing determinism, and freedom is performance.

DET-3′s counterexample is subtraction. (5 − 3) − 1 = 1 while 5 − (3 − 1) = 3. If the operation is not associative the language must fix the tree shape, and if the scheduler changes the tree with the number of cores, the result depends on the schedule. The proof did not import IEEE floating point as axioms; it showed “if non-associative, the shape matters” generally, using subtraction as the representative non-associative operation. That is why reduce acc sub is rejected with E-PAR-ASSOC and floating-point addition with E-PAR-FLOAT (chapter 27).

Q. Isn’t “roughly the same answer” good enough?

A. Letting it pass because the values are almost the same makes bit comparison unusable for regression tests. Losing reproducibility loses a debugging tool. Parallel sums giving different answers per core count are the most common cause of irreproducibility in scientific computing. In this language there is no “roughly” — if the three conditions cannot be confirmed, translation refuses.

45.6 What is not proven#

Recap

Bernstein independence is three empty intersections; at level 1 independence means no races and the condition is necessary, both proven with a counterexample. At level 2 an ownership message always lies between accesses by different actors, so races are absent by definition. Heavy tools are needed only at level 3. Having no races differs from being deterministic, and three conditions — splitting without overlap · keeping dependency edges · associative combining — make parallel results bit-for-bit identical to sequential ones. Each condition has a counterexample showing what breaks without it.