~/bend-docscommunity

scan.bend relies on unsafe/foreign

raw source on the hub · import 0xeef3a8486d410ea6f25709aac90dbf40/scan.bend as Scan

scan.bend: parallel prefix sums over an Array<U32>, 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> & 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.

1 import
import Base

Types

type Two source · line 88 · raw

Type

Definitions

def sum.go source · line 41 · raw

@+n:Nat -> @+j:U32 -> @+s:U32 -> @r:Pair(Array<U32>, U32) -> Pair(Array<U32>, U32)

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 ex.go source · line 54 · raw

@+n:Nat -> @+j:U32 -> @+s:U32 -> @r:Pair(Array<U32>, U32) -> Pair(Array<U32>, U32)

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 in.go source · line 65 · raw

@+n:Nat -> @+j:U32 -> @+s:U32 -> @r:Pair(Array<U32>, U32) -> Pair(Array<U32>, U32)

As ex.go, but a[j] becomes the sum through it.

def go source · line 78 · raw

@+inc:Bool -> @+n:Nat -> @+j:U32 -> @+s:U32 -> @r:Pair(Array<U32>, U32) -> Pair(Array<U32>, U32)

The scan of the n + 1 entries from j, from base s.

def two.join source · line 91 · raw

@l:Two -> @r:Two -> Two

def two.rejoin source · line 97 · raw

@a2:Array<U32> -> @s2:Array<U32> -> @r:Two -> Two

A leaf's spare handles, joined back after its work.

def sums.leaf.fin source · line 104 · raw

@+i:U32 -> @s:Array<U32> -> @r:Pair(Array<U32>, U32) -> Two

def sums.leaf source · line 109 · raw

@+i:U32 -> @+e:Nat -> @a:Array<U32> -> @s:Array<U32> -> Two

Block i (2^e entries): its sum into s[i].

def sums source · line 116 · raw

@+d:Nat -> @+i:U32 -> @+e:Nat -> @fa:Pair(Array<U32>, Array<U32>) -> @fs:Pair(Array<U32>, Array<U32>) -> Two

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 puts.leaf.fin source · line 133 · raw

@s:Array<U32> -> @r:Pair(Array<U32>, U32) -> Two

def puts.leaf.at source · line 137 · raw

@+inc:Bool -> @+i:U32 -> @+e:Nat -> @a:Array<U32> -> @r:Pair(Array<U32>, U32) -> Two

def puts.leaf source · line 143 · raw

@+inc:Bool -> @+i:U32 -> @+e:Nat -> @a:Array<U32> -> @s:Array<U32> -> Two

Block i: its scan in place, from its base s[i].

def puts source · line 148 · raw

@+inc:Bool -> @+d:Nat -> @+i:U32 -> @+e:Nat -> @fa:Pair(Array<U32>, Array<U32>) -> @fs:Pair(Array<U32>, Array<U32>) -> Two

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 half source · line 166 · raw

@+d:Nat -> Nat

floor(d / 2).

def run.fin source · line 177 · raw

@r:Two -> @+total:U32 -> Pair(Array<U32>, U32)

def run.mid source · line 182 · raw

@+inc:Bool -> @+d:Nat -> @+e:Nat -> @a:Array<U32> -> @r:Pair(Array<U32>, U32) -> Pair(Array<U32>, U32)

Pass 3, from pass 2's bases and total.

def run.pre source · line 187 · raw

@+d:Nat -> @+e:Nat -> @a:Array<U32> -> Two

Pass 1 over a (2^lg entries) at depth d, into a new s.

def run0.sums source · line 191 · raw

@+inc:Bool -> @+d:Nat -> @+e:Nat -> @r:Two -> Pair(Array<U32>, U32)

The scan with pass 2 on one lane.

def run0 source · line 195 · raw

@+inc:Bool -> @+b:Nat -> @r:Pair(Array<U32>, U32) -> Pair(Array<U32>, U32)

def run1.sums source · line 203 · raw

@+inc:Bool -> @+d:Nat -> @+e:Nat -> @r:Two -> Pair(Array<U32>, U32)

The scan with pass 2 by run0 at depth d / 2.

def run.pick source · line 210 · raw

@lo:Bool -> @+inc:Bool -> @+d:Nat -> @+e:Nat -> @r:Two -> Pair(Array<U32>, U32)

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 source · line 217 · raw

@+inc:Bool -> @+b:Nat -> @r:Pair(Array<U32>, U32) -> Pair(Array<U32>, U32)

def exclusive source · line 229 · raw

@+b:Nat -> @a:Array<U32> -> Pair(Array<U32>, U32)

a[j] becomes a[0] + .. + a[j - 1] (a[0] becomes 0); returns the array and the total, a[0] + .. + a[n - 1].

def inclusive source · line 233 · raw

@+b:Nat -> @a:Array<U32> -> Pair(Array<U32>, U32)

a[j] becomes a[0] + .. + a[j]; returns the array and the total.