# scan.bend: parallel prefix sums over an Array, in place. # # import ./scan.bend as Scan # Scan.exclusive(8n, a) # (a, total): a[j] becomes a[0] + .. + a[j - 1] # Scan.inclusive(8n, a) # (a, total): a[j] becomes a[0] + .. + a[j] # # A call's result is taken apart by a def's parameter (a def of yours # taking r: Array & U32), not by a let or match on the call itself. # # Sums wrap at 2^32, as U32.add does. The first argument is the fork # depth b: the array is cut into 2^b blocks of equal size (b is lowered to # log2 of the size when larger). Every b gives the same bits; b only sets # how many lanes share the work. A call under `!` (Scan.exclusive!(b, a), # or a def of yours that calls it, marked with `!`) runs its forks on the # GPU; without `!` they run on the CPU threads. # # Three passes, with no atomics: # 1. per block, in parallel: the block's sum into s[i]; # 2. s becomes its exclusive scan (the blocks' bases) and the total: on # one lane below 2^12 sums; else by this same scan at half the depth # (the 2^b sums in 2^(b/2) blocks), whose own pass 2 runs on one lane; # 3. per block, in parallel: the block's scan from its base, in place. # ref.bend's exclusive and inclusive are the sequential references; the # tests check both passes against them bit for bit at many sizes and b. # # Sharing. The blocks' lanes read and write one array through handles # from Array.fork (Base, @unsafe): two O(1) handles to one block, joined # back by Array.join. No def here is @unsafe itself: the fork trees below # recurse on their depth, which the checker verifies. But in Bend's theory # Array.fork(a) is (a, a) and Array.join(a, b) is a, so the theory drops # the writes made through the second handle: never evaluate these defs in # the checker (a main that returns a value); call them from an IO main, # which runs compiled. `bend` lists every def that relies on the fork. import Base # Loops (one lane) # ---------------- # r holds the array and the entry x read at j: adds x and the n entries # after it to s. Returns the array and the sum. def sum.go(+n: Nat, +j: U32, +s: U32, r: Array & U32) -> Array & U32: match n: case 0n: (a, +x) = r (a, (s + x : U32)) case 1n+p: (a, +x) = r +q = (j + 1 : U32) sum.go(p, q, (s + x : U32), a[q]) # r holds the array and the entry x read at j: a[j] becomes s, the sum # before it, and so on for the n entries after it. Returns the array and # the running sum past the last entry. def ex.go(+n: Nat, +j: U32, +s: U32, r: Array & U32) -> Array & U32: match n: case 0n: (a, +x) = r (a[j] <- s, (s + x : U32)) case 1n+p: (a, +x) = r +q = (j + 1 : U32) ex.go(p, q, (s + x : U32), (a[j] <- s)[q]) # As ex.go, but a[j] becomes the sum through it. def in.go(+n: Nat, +j: U32, +s: U32, r: Array & U32) -> Array & U32: match n: case 0n: (a, +x) = r +t = (s + x : U32) (a[j] <- t, t) case 1n+p: (a, +x) = r +q = (j + 1 : U32) +t = (s + x : U32) in.go(p, q, t, (a[j] <- t)[q]) # The scan of the n + 1 entries from j, from base s. def go(+inc: Bool, +n: Nat, +j: U32, +s: U32, r: Array & U32) -> Array & U32: match inc: case False{}: ex.go(n, j, s, r) case True{}: in.go(n, j, s, r) # Handles # ------- type Two is Type: Two{a: Array, s: Array} def two.join(l: Two, r: Two) -> Two: Two{la, ls} = l Two{ra, rs} = r Two{Array.join(U32, la, ra), Array.join(U32, ls, rs)} # A leaf's spare handles, joined back after its work. def two.rejoin(a2: Array, s2: Array, r: Two) -> Two: Two{a, s} = r Two{Array.join(U32, a, a2), Array.join(U32, s, s2)} # Pass 1 # ------ def sums.leaf.fin(+i: U32, s: Array, r: Array & U32) -> Two: (a, +v) = r Two{a, s[i] <- v} # Block i (2^e entries): its sum into s[i]. def sums.leaf(+i: U32, +e: Nat, a: Array, s: Array) -> Two: +at = U32.shln(i, e) sums.leaf.fin(i, s, sum.go(U32.to_nat((U32.shln(1, e) - 1 : U32)), at, 0, a[at])) # Pass 1's forks: d levels down to the leaves under leaf index i. fa and # fs are two handles each to the data a and the sums s. Leaf i reads only # its block of a and writes only s[i], so no two lanes touch one slot. def sums(+d: Nat, +i: U32, +e: Nat, fa: Array & Array, fs: Array & Array) -> Two: match d: case 0n: (a, a2) = fa (s, s2) = fs two.rejoin(a2, s2, sums.leaf(i, e, a, s)) case 1n+p: (a0, a1) = fa (s0, s1) = fs +j = (i * 2 : U32) l r = sums(p, j, e, Array.fork(U32, a0), Array.fork(U32, s0)) sums(p, (j + 1 : U32), e, Array.fork(U32, a1), Array.fork(U32, s1)) two.join(l, r) # Pass 3 # ------ def puts.leaf.fin(s: Array, r: Array & U32) -> Two: (a, +t) = r Two{a, s} def puts.leaf.at(+inc: Bool, +i: U32, +e: Nat, a: Array, r: Array & U32) -> Two: (s, +base) = r +at = U32.shln(i, e) puts.leaf.fin(s, go(inc, U32.to_nat((U32.shln(1, e) - 1 : U32)), at, base, a[at])) # Block i: its scan in place, from its base s[i]. def puts.leaf(+inc: Bool, +i: U32, +e: Nat, a: Array, s: Array) -> Two: puts.leaf.at(inc, i, e, a, s[i]) # Pass 3's forks, shaped as pass 1's. Leaf i reads s[i] and reads and # writes only its block of a: no two lanes touch one slot. def puts(+inc: Bool, +d: Nat, +i: U32, +e: Nat, fa: Array & Array, fs: Array & Array) -> Two: match d: case 0n: (a, a2) = fa (s, s2) = fs two.rejoin(a2, s2, puts.leaf(inc, i, e, a, s)) case 1n+p: (a0, a1) = fa (s0, s1) = fs +j = (i * 2 : U32) l r = puts(inc, p, j, e, Array.fork(U32, a0), Array.fork(U32, s0)) puts(inc, p, (j + 1 : U32), e, Array.fork(U32, a1), Array.fork(U32, s1)) two.join(l, r) # Driver # ------ # floor(d / 2). def half(+d: Nat) -> Nat: match d: case 0n: 0n case 1n+p: match p: case 0n: 0n case 1n+q: 1n+half(q) def run.fin(r: Two, +total: U32) -> Array & U32: Two{a, s} = r (a, total) # Pass 3, from pass 2's bases and total. def run.mid(+inc: Bool, +d: Nat, +e: Nat, a: Array, r: Array & U32) -> Array & U32: (s, +total) = r run.fin(puts(inc, d, 0, e, Array.fork(U32, a), Array.fork(U32, s)), total) # Pass 1 over a (2^lg entries) at depth d, into a new s. def run.pre(+d: Nat, +e: Nat, a: Array) -> Two: sums(d, 0, e, Array.fork(U32, a), Array.fork(U32, Array.new(U32, d, 0))) # The scan with pass 2 on one lane. def run0.sums(+inc: Bool, +d: Nat, +e: Nat, r: Two) -> Array & U32: Two{a, s} = r run.mid(inc, d, e, a, ex.go(U32.to_nat((U32.shln(1, d) - 1 : U32)), 0, 0, s[0])) def run0(+inc: Bool, +b: Nat, r: Array & U32) -> Array & U32: (a, +n) = r +lg = U32.log2(n) +d = Nat.min(b, lg) +e = Nat.sub(lg, d) run0.sums(inc, d, e, run.pre(d, e, a)) # The scan with pass 2 by run0 at depth d / 2. def run1.sums(+inc: Bool, +d: Nat, +e: Nat, r: Two) -> Array & U32: Two{a, s} = r run.mid(inc, d, e, a, run0(False{}, half(d), Array.size(U32, s))) # Pass 2 by run1 from 2^12 sums up, else on one lane: on the GPU we # measured, the nested scan's forks cost more than one lane's walk below # that. def run.pick(lo: Bool, +inc: Bool, +d: Nat, +e: Nat, r: Two) -> Array & U32: match lo: case True{}: run0.sums(inc, d, e, r) case False{}: run1.sums(inc, d, e, r) def run(+inc: Bool, +b: Nat, r: Array & U32) -> Array & U32: (a, +n) = r +lg = U32.log2(n) +d = Nat.min(b, lg) +e = Nat.sub(lg, d) run.pick(U32.is_lt(U32.from_nat(d), 12), inc, d, e, run.pre(d, e, a)) # API # --- # a[j] becomes a[0] + .. + a[j - 1] (a[0] becomes 0); returns the array # and the total, a[0] + .. + a[n - 1]. def exclusive(+b: Nat, a: Array) -> Array & U32: run(False{}, b, Array.size(U32, a)) # a[j] becomes a[0] + .. + a[j]; returns the array and the total. def inclusive(+b: Nat, a: Array) -> Array & U32: run(True{}, b, Array.size(U32, a))