Skip to content
View source

Execution modes

A Kernel runs in one of six modes, named by its body. A reduction, a scan, a scatter and a prefix each walk an input, one body run per element, named after over; the order inputs are declared in means nothing.

Mode Body One run per Output Fuses
Map compute { … } element of the output domain that domain, several outputs allowed yes
Reduction compute over x { … } + merge(a, b) { … } element of x Rows of exactly one element no
Scan scan over x with y = init { … } element of x, in order as many as x no
Scatter scatter over x into t with sum|min|max|last { … } element of x the target t’s shape no
Prefix prefix over x with sum|min|max { … } element of x Rows as many as x —
Group group N { … } elements N at a time as a map —

Every backend runs every mode; crates/backends/conformance holds the JIT and the GPU to the interpreter over 36 language features × 7 execution forms.

Once per element of the output domain. A map may write several outputs over one domain, given at once as a record:

kernel shade.surface@1 {
requires { field.write; }
output albedo: Image;
output normal: Grid<Vec3>;
output depth: Grid<Float>;
compute {
let hit = cast(eye, ray());
if hit.t < 0.0 {
return { albedo: sky, normal: vec3(0.0, 0.0, 0.0), depth: 100.0 };
}
{ albedo: vec4(hit.albedo, 1.0), normal: hit.normal, depth: hit.t }
}
}

A graph reads node.albedo. When one consumer is the only reader of every output and reads them all by sample, the two fuse and no output is written.

kernel stats.luma@1 {
requires { field.read; field.write; }
input source: Image;
output result: Rows<Light>; // exactly one element comes out
struct Light { sum: Float; max: Float; count: Int; }
compute over source { // once per element of `source`
let l = luma(sample(source));
return Light { sum: l, max: l, count: 1 };
}
merge(a, b) { // folds two results into one
return Light { sum: a.sum + b.sum, max: max(a.max, b.max), count: a.count + b.count };
}
}

merge must be associative and commutative: the fold’s tree is the backend’s; integers and booleans are exact, floats agree to rounding. A reduce node takes no domain and never fuses.

kernel audio.lowpass@1 {
requires { field.read; field.write; }
input samples: Rows<Float>;
param k: Float = 0.1;
output result: Rows<Float>; // one element per input element
scan over samples with y = 0.0 { // the input it walks, the carried value and its start
return y + (sample(samples) - y) * k;
}
}

The body runs once per element, in order: y is the previous result (init first), the result is this output element and the next y, index() the element index. A scan runs on one thread and never fuses.

kernel machine.step@1 {
requires { field.read; field.write; }
input ticks: Rows<Int>; // one step per element
memory ram: Rows<Int>; // bound like an input, read like an output
output result: Rows<Int>;
scan over ticks with pc = 0 {
let op = at(ram, pc); // what an earlier step wrote is there
put(ram, pc + 100, op * 2); // a write, in step order
return pc + 1;
}
}

ram <~ machine.ram from zeros.result persists across evaluations; ram <- fresh.result starts each over. A put outside is dropped, never clamped; the bound Field is never touched, and a failed evaluation leaves it as it was. The CHIP-8 and ARM Thumb machines (examples/machines) are written this way.

Scatter (scatter): writes where it computes

Section titled “Scatter (scatter): writes where it computes”
kernel particles.splat@1 {
requires { field.read; field.write; }
input particles: Rows<Particle>;
input canvas: Grid<Vec3>;
output result: Grid<Vec3>; // starts as `canvas`
struct Splat { cell: Vec2; heat: Vec3; }
scatter over particles into canvas with sum {
let p = sample(particles);
return Splat { cell: floor(p.position * size(canvas)), heat: tint(p) };
}
}
  • The body yields a struct of two fields: an address into the target (Int for rows, Vec2 grid, Vec3 volume) and a value of its element type.
  • Combining: sum adds fieldwise, min/max take the fieldwise extreme, last replaces; the result equals applying sources in element order.
  • An address outside the target, NaN included, is dropped. The output has the target’s shape, layout and extent.
  • emit(pair); gives more pairs (under min, max, or a sum of no floats).
kernel compact.offsets@1 {
requires { field.read; field.write; }
input flags: Rows<Int>;
output result: Rows<Int>; // one element per input element
prefix over flags with sum { // the rank of each kept element
return if sample(flags) != 0 { 1 } else { 0 };
}
}

The output at i holds the values at 0..=i combined by sum, min or max. A prefix may fold in any grouping, so sum over an element holding a float is refused (use scan), as is last.

kernel fft.row@1 {
requires { field.read; field.write; }
input line: Rows<Vec2>;
output result: Rows<Vec2>;
group 64 { // one row of 64 per group
let x = sample(line);
let pair = exchange(x, lane() ^ 1);
return if lane() % 2 == 0 { x + pair } else { pair - x };
}
}

Element e is lane e % N (lane()) of group e / N. exchange(v, k) is the v lane k holds at that same exchange. Every lane reaches each exchange alike, so one stands under no branch, match, loop count or until that turns on what a lane holds alone (params, literals and the domain may steer one). On a device a group is a workgroup and an exchange a pass through shared memory between barriers; a group exchanging more than a workgroup holds runs on the JIT. packages/signal (the FFT) is built on groups.

Written on a scene’s node, not in the Kernel:

Form Effect
async = true; the node runs off the frame on its own thread; readers take its last output until it lands
times = N; + carry port <- output; the node placed N times, each reading the last one’s output (a Jacobi iteration)
awake = value(source, row); the node sleeps, unrun, while that row is 0

See Graph & state.