docs / specification / gpu

Per-lane conditionals: comparison masks and select

A Vector<T,N> / Matrix<T,R,C> holds many values in one register. You cannot if a single lane — a branch is whole-value, and on a GPU a data-dependent branch serializes the warp. The mask→select pattern expresses “do X to some lanes, Y to the rest” as data, not control flow:

mask   = a < b              // <N x i1>: one bool per lane, no branch
result = mask.select(a, b)  // per lane: mask[i] ? a[i] : b[i]

Comparisons (== != < <= > >=) return the mask; .all() / .any() collapse a mask to one boolean for ordinary control flow. This is the whole vocabulary.

Vectors

Each thread carries a small vector — an RGBA pixel, a 3-D point, a feature tile. Per-element decisions are the norm, and they must stay branchless.

ReLU (keep positives, zero the rest) — the canonical activation:

Vector<float32,4> y = (x > 0.0f).select(x, zeros);

Without masks you scalarize — a per-lane branch that defeats vectorization:

for (uint32 i = 0; i < 4; i = i + 1) {        // serial; no SIMD / GPU vector op
    if (x[i] > 0.0f) { y[i] = x[i]; } else { y[i] = 0.0f; }
}

select is the general form of min/max/clamp — a.min(b) is:

Vector<float32,4> m = (a < b).select(a, b);   // element-wise minimum

Reduce to a branch — .all() / .any() answer whole-vector questions:

if ((p >= lo).all() && (p <= hi).all()) { ... }   // point inside an AABB
if ((v != v).any()) { ... }                        // any NaN lane? (NaN != NaN)

Matrices

The same pattern over R·C lanes. It wins wherever a per-element decision must run over a whole matrix without a scalar loop.

Prune small weights (ML) — zero everything below a threshold:

Matrix<float32,4,4> pruned = (w > eps).select(w, zeros);

Converged? — whole-matrix equality is a reduction over the mask:

if ((m == prev).all()) { return; }   // fixed point reached

Why not the alternatives?

AlternativeWhat it forces
Nested for r,c + if per elementScalar — no <R*C x T> vector op; on a GPU the per-element branch diverges the warp.
A parallel boolean[R][C] + a second apply passTwo passes, extra memory, still scalar — you hand-roll what select already is.
Arithmetic mask m * cond (cond as 0.0/1.0)Only selects against zero; can’t pick between two non-zero values, and you must hand-convert the bool to a float first.

(w > eps).select(w, zeros) is one expression, flat <R*C x T> SSA, register-resident, branchless — the same source on CPU, Vulkan, and AMD.


Rules. Comparison operators on value types yield per-lane masks (typed Vector<boolean,N> / Matrix<boolean,R,C>); equality is not special, so whole-object equality is (a == b).all(). Masks are register-only values — KernelBuffer<Vector<boolean,N>> and bool-vector kernel args stay rejected (the <N x i1> memory layout is ABI-ambiguous). Runnable end to end in samples/Tour/xpu (the mask / select section). See ValueTypeCatalog.md for the Vector / Matrix surfaces.

Source: docs/specification/gpu/MaskSelect.md · 2 min read · 380 words