use std/gpu.{with_gpu_fake, last_out}
use tileref/ref.{relu_ref}
Each function takes the formats (dtypes) and contents (ins) of every
argument buffer, the output buffer last, and answers what the output buffer
holds after the kernel has run. A kernel that masks the lanes past n leaves
them as they were, so each reference takes that n and copies the output's
own tail past it. Fixing n and any constants gives the function the fake
device wants (examples/projects/gpu_fake).
The package depends on std alone, never on tileir: a reference is the second
opinion on a kernel and must not share its code path. The format arithmetic
(round_to, wrap_*, nibble_*) is std/gpu's, so a reference and the fake
device agree on what a format can hold.
Changes between versions: CHANGELOG.md.
API#
tileref/ref
Host reference implementations for the Tile IR kernels: for each kernel scripts/tile-golden records, a plain function of the argument buffers' formats and contents that answers what the device's output buffer holds afterwards. scripts/tile-gpu-diff compares the device against these, and scripts/leetgpu-diff/problems.txt names one per leetgpu problem.
ref.TWO_OVER_SQRT_PI
const TWO_OVER_SQRT_PI: Float
Two over the square root of pi, to the nearest Float: the series below and Abramowitz-Stegun 7.1.26 both carry it.
ref.PI
const PI: Float
Pi to the nearest Float, and pi over two split into a high part whose
low 21 bits are zero and a remainder. The split is Cody and Waite's:
k * PIO2_HI is exact for every |k| this file meets, so the reduced
argument (x - k * PIO2_HI) - k * PIO2_LO keeps its leading bits where
a single-word x - k * (pi / 2) would have lost them to cancellation.
ref.INV_SQRT2
const INV_SQRT2: Float
One over the square root of two, to the nearest Float. leetgpu 65 writes
x / sqrt(2); the kernel and this reference both multiply by the
reciprocal, which differs from the division in the last bit and is far
inside the tier either way.
ref.masked_vadd_ref
fn masked_vadd_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Element-wise sum of the first two buffers into the third, over the first
n lanes: leetgpu 1 (Vector Addition) and 8 (Matrix Addition).
ref.masked_copy_ref
fn masked_copy_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The first buffer into the second, over the first n lanes: leetgpu 31
(Matrix Copy).
ref.relu_ref
fn relu_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
max(x, 0) over the first n lanes: leetgpu 21 (ReLU). The kernel is
maxf with neither propagate_nan nor flush_to_zero, whose answer on a
NaN operand is the other operand and on (-0.0, +0.0) is either zero;
this answers +0.0 for both, so a corpus that exercised them would be
asserting something this release has not pinned.
ref.leaky_relu_ref
fn leaky_relu_ref(n: Int, alpha: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
x where x >= 0 and alpha * x where x < 0, over the first n
lanes: leetgpu 23 (Leaky ReLU). The kernel is an ordered lt and a
select, so NaN and -0.0 both answer themselves here as they do there.
ref.clip_ref
fn clip_ref(n: Int, lo: Float, hi: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
min(max(x, lo), hi) over the first n lanes: leetgpu 62 (Value
Clipping). maxf and minf again, so the NaN and signed-zero caveat of
relu_ref holds.
ref.ref_exp
fn ref_exp(x: Float) -> Float
e raised to the power x. Range reduction to x = k * ln2 + r with
|r| <= ln2 / 2, then the Taylor series of exp(r), then 2^k by exact
doubling. Answers 0.0 where the result underflows and the infinity where
it overflows.
ref.ref_log
fn ref_log(x: Float) -> Float
The natural logarithm of x. x = m * 2^e with m in [1, 2), then
the atanh series of log(m), which converges fastest where the
argument is near 1. Answers NaN for a negative argument and the negative
infinity for zero.
ref.ref_sqrt
fn ref_sqrt(x: Float) -> Float
The square root of x, by Newton's iteration from a crude guess on
x = m * 4^k with m in [1, 4). Eight iterations from within a factor
of two converge to the last bit or two. Answers NaN for a negative
argument.
ref.ref_tanh
fn ref_tanh(x: Float) -> Float
The hyperbolic tangent of x. Small arguments go through the series for
exp(y) - 1 rather than through ref_exp, because (e - 1) / (e + 1)
loses every significant digit of a small answer to the subtraction; past
|x| = 20 the answer is 1 to the last bit of a Float. This is the second
opinion for the device's tanh, which is a hardware function, and for
the tanh approximation of the gelu that leetgpu 74's feed-forward asks
for.
ref.ref_erf
fn ref_erf(x: Float) -> Float
The error function of x, by the series
erf(x) = (2 / sqrt(pi)) * exp(-x^2) * sum 2^n x^(2n+1) / (1*3*..*(2n+1)).
Every term of that sum is positive, so nothing cancels and the sum keeps
its relative accuracy however large x^2 makes it grow; the exp(-x^2)
in front brings it back. Past |x| = 6 the answer is 1 to the last bit
of a Float (erfc(6) is 2.2e-17) and this answers exactly 1 there.
Accurate to about 4e-15 over [-8, 8], measured against a correctly
rounded erf.
This is the SECOND OPINION for the device composition in packages/tileir/src/dev.dawn, which is Abramowitz-Stegun 7.1.26 and has an absolute error of 1.5e-7. A reference running the same five-term rational would agree with a WRONG one, and would also leave the approximation's error unmeasurable; against this series the difference the device shows is 7.1.26's error and nothing else.
ref.ref_sin
fn ref_sin(x: Float) -> Float
The sine of x. Range reduction to a quadrant and a remainder in
[-pi/4, pi/4], then the Taylor series of that quadrant's function.
This is the SECOND OPINION for the device's sin, which is a libdevice
approximation; the two are compared against each other and neither is
the definition of the other.
ref.ref_cos
fn ref_cos(x: Float) -> Float
The cosine of x, by the same reduction one quadrant along.
ref.ref_tan
fn ref_tan(x: Float) -> Float
The tangent of x, as the quotient of the two above. Near a pole the
denominator is small and the answer is large, and the relative part of
the tolerance tier is what carries the comparison there; a corpus is not
allowed to sit ON a pole, where both sides answer different infinities.
ref.ref_sinh
fn ref_sinh(x: Float) -> Float
The hyperbolic sine of x. Small arguments go through the series, for
ref_tanh's reason: (e - 1/e) / 2 loses every significant digit of a
small answer to the subtraction. Past that it is the definition, with
1/e underflowing to zero on its own where e is enormous.
ref.ref_cosh
fn ref_cosh(x: Float) -> Float
The hyperbolic cosine of x. No cancellation anywhere: both halves of
(e + 1/e) / 2 are positive, so the definition is accurate over the
whole range and there is no small-argument branch.
ref.ref_atan
fn ref_atan(x: Float) -> Float
The arc tangent of x, in [-pi/2, pi/2]. Two reductions bring the
argument inside 2 - sqrt(3): atan(x) = pi/2 - atan(1/x) for |x| > 1
and atan(x) = pi/6 + atan((x sqrt3 - 1) / (x + sqrt3)) for what is left
above the pivot.
ref.ref_atan2
fn ref_atan2(y: Float, x: Float) -> Float
The atan2 of y over x, with the quadrant taken from the signs of
both. THE NUMERATOR IS FIRST, which is the order
packages/tileir/src/dev.atan2 takes, the order the dialect's own
sentence gives ("the arc tangent of the ratio of first and second input
arguments x / y", its names swapped) and C's atan2(y, x). The
parameters were (a, b) until 2026-10-01 (#210), which hid that order
from a caller naming them.
The axes are the special cases and they are where a corpus earns its
keep: x < 0 moves the answer by a whole pi and a reference that
forgot it would agree with the device on the right half plane and
nowhere else. Signed zeros are not distinguished here: 0.0 is read as
+0.0, so atan2(0, -1) is pi and not -pi, and the corpus in
scripts/tile-gpu-diff carries no negative zero.
ref.ref_remf
fn ref_remf(a: Float, b: Float) -> Float
The floating-point remainder of a by b under TRUNCATED division:
a - trunc(a / b) * b, which takes the sign of the dividend. This is
C's fmod and not C's remainder, and the dialect's remf is stated
as the first (Ops.td, RemFOp); which one the DEVICE runs is measured in
scripts/tile-gpu-diff and not assumed here.
Unlike its neighbours above this one is EXACT, and it is exact by construction rather than by accuracy: the remainder of two representable numbers is representable, and shifting the divisor by powers of two and subtracting where it fits computes it with no rounding at all. So a disagreement with the device is a disagreement about the OPERATION and never about the last bit.
ref.reduce_sum_ref
fn reduce_sum_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The sum of the first n elements of the first buffer, at element 0 of the
second: leetgpu 4 (Reduction).
ref.softmax_ref
fn softmax_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
exp(x - max(x)) / sum(exp(x - max(x))) over the first n lanes:
leetgpu 5 (Softmax). The maximum is subtracted here as it is in the
kernel; a reference that did not would still agree on a small corpus,
which is why scripts/tile-gpu-diff's softmax-no-max-subtract mutant uses
a corpus of large values.
ref.dot_ref
fn dot_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
sum(a * b) over the first n lanes, at element 0 of the third buffer:
leetgpu 17 (Dot Product).
ref.mse_ref
fn mse_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
mean((p - t)^2) over the first n lanes: leetgpu 27 (Mean Squared
Error). The division by n is one rounded operation on both sides, so it
costs the comparison nothing.
ref.monte_carlo_ref
fn monte_carlo_ref(n: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
scale * sum(y) over the first n lanes: leetgpu 35 (Monte Carlo
Integration), where scale is (b - a) / n, the constant the kernel was
recorded with.
ref.rms_norm_ref
fn rms_norm_ref(n: Int, eps: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
x / sqrt(mean(x^2) + eps) over the first n lanes: leetgpu 50 (RMS
Normalization).
ref.silu_ref
fn silu_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
x / (1 + exp(-x)) over the first n lanes: leetgpu 52 (Sigmoid Linear
Unit).
ref.sigmoid_ref
fn sigmoid_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
1 / (1 + exp(-x)) over the first n lanes: leetgpu 68 (Sigmoid
Activation).
ref.ppo_loss_ref
fn ppo_loss_ref(n: Int, eps: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
-mean(min(r * A, clip(r, 1 - eps, 1 + eps) * A)) with r = exp(new -
old), over the first n lanes: leetgpu 107 (PPO Clipped Surrogate
Loss). ins is the new log probabilities, the old ones, the advantages
and the output buffer.
ref.dpo_loss_ref
fn dpo_loss_ref(n: Int, beta: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
-mean(log(sigmoid(beta * ((pc - pr) - (rc - rr))))) over the first n
lanes: leetgpu 108 (DPO Sequence Loss). ins is the policy's chosen and
rejected log probabilities, the reference model's two, and the output
buffer.
ref.matmul_ref
fn matmul_ref(m: Int, k: Int, n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
c = a * b for an m by k matrix and a k by n one, both row-major:
leetgpu 2 (Matrix Multiplication). ins is [a, b, old c].
ref.batched_matmul_ref
fn batched_matmul_ref(batch: Int, m: Int, k: Int, n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
batch independent m by k times k by n products, the batches laid
out one after another: leetgpu 30 (Batched Matrix Multiplication).
ref.view_padding_ref
fn view_padding_ref(n: Int, out_n: Int, pad: Float, x: List[Float], old: List[Float]) -> List[Float]
What a partition_view load answers when the tile runs past the end of
the tensor and the store that follows is into a view wide enough to
keep it: the first n lanes are the input's, the lanes from there to
out_n are the view's padding value, and anything past out_n is what
the output buffer already held. Knife T11's oracle for the
PaddingValue enum.
x and old are BIT PATTERNS and not numbers: scripts/tile-gpu-diff's
view_padding runs an f32 kernel over i32 buffers, so what the host
compares is the exact pattern each padding value put on the device,
which is the only way zero and neg_zero are two answers rather than
one. pad is that pattern for this output.
ref.strided_pad_ref
fn strided_pad_ref(n: Int, tile: Int, trav: Int, pad: Float, x: List[Float], old: List[Float]) -> List[Float]
The tile a STRIDED view reads at grid index b, laid end to end:
element b * tile + j of the answer is element b * trav + j of x
where that is inside the tensor, and pad where it is not.
scripts/tile-gpu-diff's view_stride_pad (knife T13).
The traversal is what makes this different from a partition view's
reference: the grid steps trav elements and the tile covers tile of
them, so with trav the larger the reader SKIPS and with trav the
smaller it reads each element more than once. The output has one tile per
grid index, and the index space rounds up, which is why the last tile can
run past the end of the tensor and the padding value is visible at all.
ref.gather_pad_ref
fn gather_pad_ref(rows: Int, cols: Int, gcols: Int, pad: Float, ids: List[Float], x: List[Float], old: List[Float]) -> List[Float]
The tile a GATHER/SCATTER view reads: row r of the answer is row
ids[r] of a rows by cols row-major tensor, widened to gcols
columns with pad in the columns past the end of the tensor.
scripts/tile-gpu-diff's view_gather_pad (knife T13).
The row numbers come from the kernel at run time and the columns do not: that asymmetry IS the gather view, and the padding here is on the column side because a row the tensor does not have is a different question from a column it does not have.
ref.view_atomic_bf16_ref
fn view_atomic_bf16_ref(n: Int, bins: Int, tile: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The bin counts view_atomic_bf16 reduces into a BF16 view (knife TA).
ins is [x, out] and the answer rewrites the second buffer's first
bins lanes, leaving the rest of it alone.
This is view_atomic_ref's add output and nothing else, at another
element format. The kernel exists because atomic_red_view_tko's addf
over BF16 is the one thing in the view family that Ops.td gates on an
architecture, so what is being asked is whether a card above that gate
answers the same counts a card below it cannot be asked for at all.
NO ROUNDING APPEARS HERE, and that is a property of the corpus rather than of bf16. Eight tile blocks reduce into each bin in no particular order, so an inexact sum would depend on the order they arrived in; bf16 carries eight significand bits, whole numbers up to 256 are exact in it, and scripts/tile-gpu-diff/arch_diff.dawn holds the largest bin count below that.
ref.view_atomic_ref
fn view_atomic_ref(n: Int, bins: Int, tile: Int, bias: Int, dtypes: List[String], ins: List[List[Float]]) -> List[(Int, List[Float])]
The nine reductions atomic_red_view_tko can make, each over the SAME
per-block contribution: scripts/tile-gpu-diff's view_atomic (knife T13).
ins is [x, and the nine output buffers as the launch found them] and
the answer rewrites positions 1 to 9.
The contribution of block b to bin j is how many of its tile lanes
hold the value j, counting only lanes below n. Summed over the blocks
that is exactly histogram_ref, which is why the add output can share
that reference; the other eight fold the same counts LESS bias, and the
bias is what puts negative numbers in so that umax and umin are not
max and min spelled twice.
Every fold here is exact whatever order the blocks arrive in, which is the whole point of an atomic reduction: the eight integer modes are associative and commutative over i32, and the ninth adds small integers in binary64.
ref.masked_double_ref
fn masked_double_ref(limit: Int, x: List[Float], old: List[Float]) -> List[Float]
The first limit lanes of x doubled, with every lane from limit on
kept from old: scripts/tile-gpu-diff's view_tensor_shape (knife T12).
limit is not a number the kernel was recorded with. It is what
get_tensor_shape answered on the device for a tensor whose extents are
?, and the kernel uses it as the bound of the mask on both its load and
its store, so a wrong answer shows up as lanes that were doubled and
should not have been, or the other way about.
ref.view_grid_sum_ref
fn view_grid_sum_ref(rows: Int, cols: Int, tile: Int, x: List[Float], old: List[Float]) -> List[Float]
One tile by tile tile holding the sum of EVERY tile of a rows by
cols row-major tensor: lane (p, q) is the sum over the grid of the
element at row i * tile + p and column j * tile + q, counting zero
where that falls outside the tensor. scripts/tile-gpu-diff's
view_index_space (knife T12).
The grid is ceil(rows / tile) by ceil(cols / tile), which is exactly
what get_index_space_shape answers on the device and what the kernel
uses as its two loop bounds. The zero outside the tensor is the partition
view's padding_value, so this reference is the padding rule and the
index space rule at once.
ref.transpose_ref
fn transpose_ref(rows: Int, cols: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The transpose of a rows by cols row-major matrix, as a cols by
rows one. No leetgpu problem: this is the oracle for a store whose
strides are not row-major.
ref.layer_norm_ref
fn layer_norm_ref(rows: Int, d: Int, eps: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
(x - mean) / sqrt(var + eps) * gamma + beta per ROW of a rows by d
matrix: leetgpu 113 (Layer Normalization). ins is
[x, gamma, beta, old], with gamma and beta d long.
ref.batch_norm_ref
fn batch_norm_ref(rows: Int, channels: Int, eps: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The same normalisation per CHANNEL of a rows by channels matrix, over
the column: leetgpu 40 (Batch Normalization). gamma and beta are
channels long.
ref.group_norm_ref
fn group_norm_ref(n: Int, c: Int, hw: Int, groups: Int, eps: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The same normalisation over each (sample, group) of an n by c by hw
tensor split into groups groups of channels: leetgpu 105 (Group
Normalization). gamma and beta are c long, one per channel.
ref.fused_rms_norm_ref
fn fused_rms_norm_ref(rows: Int, d: Int, eps: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
(x + residual) / sqrt(mean((x + residual)^2) + eps) * w per ROW of a
rows by d matrix: leetgpu 83 (Fused Residual Add + RMS
Normalization). ins is [x, residual, w, old], with w d long.
ref.conv1d_ref
fn conv1d_ref(n: Int, k: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
out[i] = sum(x[i + t] * w[t]) over k taps, for the first n lanes:
leetgpu 9 (1D Convolution), valid mode, so n is len(x) - k + 1.
ins is [x, w, old]. The taps are folded in increasing t, which is
the order the kernel unrolls them in.
ref.conv2d_ref
fn conv2d_ref(h: Int, w: Int, k: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The same in two dimensions over an h by w image and a k by k
kernel, valid mode: leetgpu 10 (2D Convolution). The output is
(h - k + 1) by (w - k + 1), and the taps are folded in row-major
order over the kernel, which is the order the two host loops unroll in.
ref.max_pool_ref
fn max_pool_ref(h: Int, w: Int, k: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The largest element of each k by k window of an h by w image,
the windows k apart in both directions: leetgpu 42 (Max Pooling), with
the stride equal to the window and no padding, so the output is h / k
by w / k. ins is [x, old]. The fold starts from the negative
infinity the kernel's accumulator does, and takes the later element only
when it is strictly larger, which is what maxf answers on a tie.
ref.interleave_ref
fn interleave_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
out[2i] = a[i] and out[2i + 1] = b[i] for the first n pairs:
leetgpu 63 (Interleave). ins is [a, b, old].
ref.rgb_gray_ref
fn rgb_gray_ref(n: Int, wr: Float, wg: Float, wb: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The luminance of the first n pixels of an interleaved RGB buffer:
leetgpu 66 (RGB to Grayscale). ins is [x, old], x three elements a
pixel. Red and green are summed first and blue added to that, which is
how the kernel brackets it.
ref.jacobi_ref
fn jacobi_ref(h: Int, w: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
One sweep of the five-point stencil over an h by w grid: leetgpu 69
(Jacobi Stencil). An interior point becomes a quarter of the sum of its
four neighbours, summed up, down, left, right in that order; a boundary
point keeps its value. ins is [x, old].
ref.depthwise_conv1d_ref
fn depthwise_conv1d_ref(c: Int, l: Int, k: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
c independent 1D convolutions, one a channel, each with its own k
taps: leetgpu 90 (Depthwise 1D Convolution). x is c by l, w is
c by k, and the output is c by l - k + 1. ins is
[x, w, old].
ref.gaussian_blur_ref
fn gaussian_blur_ref(h: Int, w: Int, k: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
A k by k convolution over an h by w image that keeps the image's
size, everything outside it counting as zero: leetgpu 28 (Gaussian
Blur), where the weights are the ones the caller uploads. ins is
[x, f, old]. A tap that reaches outside contributes 0.0 * f[t]
rather than being skipped, which is what the kernel's padded load leaves
in that lane.
ref.count_eq_ref
fn count_eq_ref(n: Int, target: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The number of the first n elements of the first buffer that equal
target, at element 0 of the second: leetgpu 43 (Count Array Element)
and its 2D and 3D shapes, 44 and 45, which are the same count over a
buffer the caller reads with more indices.
The kernel counts by widening the comparison mask with exti and folding
with addi; a widening that sign-extended would count -1 a lane and
answer the negated count, which is what makes exti's UNSIGNED
signedness a claim and not a detail.
ref.subarray_sum_ref
fn subarray_sum_ref(lo: Int, hi: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The wrapping sum of the elements of the first buffer whose index is in
[lo, hi), at element 0 of the second: leetgpu 47 (Subarray Sum). The
sum wraps modulo 2^32, as the device's addi does, so a corpus whose
total leaves the i32 range is part of the claim rather than a hazard.
ref.rainbow_round
fn rainbow_round(h0: Int, k1: Int, k2: Int, k3: Int) -> Int
One round of the hash chain rainbow_ref and the rainbow kernel share:
multiply, xor with a LOGICAL right shift, add, xor with an ARITHMETIC
one, multiply, xor with a logical one. The two kinds of shift differ on
every negative value and nowhere else, which is the whole of what
scripts/tile-gpu-diff's shri-always-logical mutant moves.
ref.rainbow_ref
fn rainbow_ref(n: Int, rounds: Int, k1: Int, k2: Int, k3: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
rounds applications of rainbow_round to each of the first n
elements of the first buffer, into the second: leetgpu 24 (Rainbow
Table), whose chain is a hash applied over and over. The constants are
the caller's, fixed when the kernel was recorded.
ref.int_ops_ref
fn int_ops_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Every integer operation and every conversion the package can emit, folded
into one lane: the reference for the int_ops coverage kernel, as
elemops and mathops are for the float families. ins is [a, b, old].
The divisor is b | 1, so it is odd and never zero; the corpus still owes
the kernel two things the device leaves undefined and this does not:
no lane of a may be -2^31 (its divi by a b | 1 of -1 would overflow,
and so would absi and negi in a way this reference cannot follow), and
the shift counts are host constants inside [0, 32).
ref.sum_diff_ref
fn sum_diff_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[(Int, List[Float])]
Element-wise sum into the third buffer AND element-wise difference into
the fourth, over the first n lanes: the multi-output shape, and the
reference that a device writing only its last argument cannot satisfy.
ins is [a, b, old sum, old diff]; each output keeps its own tail past
n, as every masked reference does.
ref.reverse_ref
fn reverse_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[(Int, List[Float])]
The first n elements of the ONE buffer, reversed IN PLACE: leetgpu 19
(Reverse Array). ins is [x] and the answer rewrites position 0, which
is the position it read.
n must be EVEN and is, in every recording: the kernel pairs lane i
with lane n - 1 - i and covers the first n / 2 of them, so an odd n
would leave the middle element to a block that does not exist. Anything
past n is the buffer's own tail, kept, as under a mask.
ref.invert_ref
fn invert_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[(Int, List[Float])]
255 - x over the first n elements of the ONE buffer, IN PLACE:
leetgpu 7 (Color Inversion) over a u8 image. ins is [img] and the
answer rewrites position 0.
The subtraction is written on the values a u8 buffer holds, which are
in [0, 255], so the answer is in [0, 255] and round_to has nothing to
wrap. That is the whole reason the host format is U8 and not I8: on
an i8 buffer the same bytes read as [-128, 127] and 255 - x would be a
different number in both places at once.
ref.f16_ops_ref
fn f16_ops_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
((a + b) * a) - b over the first n lanes, ROUNDED AFTER EACH STEP to
the output buffer's format: the f16 twin of int_ops, and the kernel
that holds addf, mulf and subf at that width to the bit.
The three roundings are not decoration. The device's tile is f16 and
every operation lands its result there, so a reference that computed the
whole expression in Float and rounded once would answer a different
number wherever an intermediate falls off the f16 grid. Each step here
is narrow.round_f16 of an exactly computed operation, which by the
double-rounding theorem is what the device's addf / mulf / subf
with rounding<nearest_even> answer, bit for bit (the theorem covers
+ - * / and sqrt, and none of those three is outside it).
ref.dot_f16_ref
fn dot_f16_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The dot product of the first n lanes of two f16 buffers, accumulated at
the OUTPUT buffer's width: leetgpu 58 (F16 Dot Product). ins is
[a, b, old out] and the answer is element 0 of the output, the rest of
it kept.
The kernel widens each operand with ftof before multiplying, so the
products and the sum are the output format's and not f16's. The fold
here is left to right and the device's is unspecified; the corpus is
what makes the two agree (see wide_diff).
ref.token_embed_ref
fn token_embed_ref(n: Int, rows: Int, dim: Int, miss: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Row i of the answer is row ids[i] of table, and miss where that
id is outside [0, rows): leetgpu 106 (Token Embedding). ins is
[ids, table, old out]; n is how many ids the launch covered and
dim the width of a row.
An id outside the table is a defined answer here, not a lane to leave alone: the kernel masks its GATHER and not its store. Which is what gives the mask something an output buffer can show, and the reason scripts/tile-gpu-diff's corpus carries such ids.
ref.sort_ref
fn sort_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The first n values of the first buffer in ascending order, in the
second: leetgpu 15 (Sorting). ins is [x, old out].
This is a SORT, not the kernel's rank-and-scatter written twice: the device computes where each value goes and writes it there, and this grows an ordered list by insertion. Equal values are interchangeable in the answer, so the kernel's tie-break (which keeps its ranks distinct, so that no two lanes scatter to one element) does not have to be repeated here to agree with it.
ref.merge_ref
fn merge_ref(m: Int, n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The first m values of the first buffer and the first n of the
second, both already ascending, merged into the third: leetgpu 71
(Parallel Merge). ins is [a, b, old out].
A two-pointer merge, which is the algorithm the kernel does NOT use: the
device computes each value's rank against the other run and scatters it
there. Ties go to a, matching the kernel's strict comparison on one
side and loose one on the other, which is also what keeps the two rank
sets disjoint.
ref.scatter_perm_ref
fn scatter_perm_ref(lanes: Int, n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
out[perm[i]] = x[i] for every lane whose destination is inside
[0, n), and nothing at all for the others: the scatter twin of
int_ops_ref, with no leetgpu problem behind it. ins is
[perm, x, old out], and lanes is how many lanes the launch covered.
The caller must hand this a perm whose in-range entries are DISTINCT.
A scatter that names one element twice has no answer on the device (the
dialect does not order the lanes of one store against each other), so a
reference that answered anything for it would be inventing a rule.
ref.prefix_sum_ref
fn prefix_sum_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The inclusive prefix sum of the first n lanes of the first buffer, into
the second: leetgpu 16 (Prefix Sum). ins is [x, old out], and the
lanes past n keep what they held.
ref.max_subarray_ref
fn max_subarray_ref(n: Int, w: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The largest sum of w consecutive of the first n elements of the first
buffer, at element 0 of the second: leetgpu 51 (Max Subarray Sum). ins
is [x, old out].
A direct sum over each window, which is NOT what the kernel computes (it subtracts two prefix sums). Both are exact in the integers modulo 2^32, which is what makes this an independent opinion rather than the same arithmetic written twice.
ref.seg_scan_ref
fn seg_scan_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The exclusive prefix sum WITHIN each segment of the first n lanes, into
the third buffer: leetgpu 70 (Segmented Exclusive Prefix Sum). ins is
[values, flags, old out]; a segment begins at every lane whose flag is
not zero, and lane 0's flag must be one of them.
Written the way leetgpu's own reference is: the global exclusive prefix sum, less the global exclusive prefix sum at the lane's segment start. Summing inside each segment instead would be a different number in floating point, and the kernel does this one.
ref.compact_ref
fn compact_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The positive values among the first n lanes of the first buffer, packed
to the front of the second: leetgpu 72 (Stream Compaction). ins is
[a, old out]. Everything after the last kept value keeps what it held,
which is what leetgpu's reference does (it writes out[:k] and no more).
ref.linrec_ref
fn linrec_ref(b: Int, l: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
h[t] = a[t] * h[t-1] + x[t] along each of b rows of l, with
h[0] = x[0]: leetgpu 82 (Linear Recurrence). ins is [a, x, old h].
The recurrence spelled out, which is the definition; the kernel folds affine maps instead, and the two agree only up to the tier. The kernel also fuses its multiply and add into one rounding and this does not, which is one more reason the tier is tolerance and not the bit.
ref.gae_ref
fn gae_ref(b: Int, s: Int, gamma: Float, lam: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The generalised advantage estimate along each of b rows of s:
leetgpu 110 (Parallel Reverse Scan). ins is
[rewards, values, old advantages].
delta[t] = rewards[t] + gamma * values[t+1] - values[t], with
values[t+1] past the row's end taken as zero, and
A[t] = delta[t] + gamma * lam * A[t+1] from the end. A loop backwards,
where the kernel is one reverse scan.
ref.ssm_scan_ref
fn ssm_scan_ref(batch: Int, steps: Int, channels: Int, states: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The Mamba-style selective scan: leetgpu 94 (SSM Selective Scan). ins
is [u, delta, A, B, C, skip, old y], with u and delta and y
shaped (batch, steps, channels), A (channels, states), B and C
(batch, steps, states) and skip (channels).
The recurrence written out, one channel at a time, where the kernel
folds affine maps over a whole (step, state) plane at once. ref_exp is
the host's own series, so the discretisation is a second opinion rather
than the same call twice, and the tier is tolerance for that reason
before any question of fold order arises.
ref.histogram_ref
fn histogram_ref(n: Int, bins: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
How many of the first n values of the first buffer fall in each of the
first bins bins, into the second: leetgpu 13 (Histogramming). ins is
[input, old histogram], and the lanes past bins keep what they held.
Values outside [0, bins) are not counted, which is leetgpu's own rule:
its reference builds a validity mask before it counts. The problem's
reference also ZEROES the histogram first, and so does this one, so a
caller that hands it a buffer of anything but zeroes still gets the
counts. The KERNEL cannot do that -- it only adds -- which is why every
corpus for it starts the bins at zero and the two agree.
ref.cas_swap_ref
fn cas_swap_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[(Int, List[Float])]
Slot i of the first buffer takes lane i of the third where it held
lane i of the second, and keeps what it had otherwise; the fourth
buffer takes what the slot held BEFORE. A lane whose expected value is
negative does not take part, and leaves both buffers alone. ins is
[state, cmp, val, old] and the answer rewrites positions 0 and 3,
which is the in-place shape knife 11 built.
n is how many slots the launch covered. Every slot is one lane's, so
this says the whole of what the device does and there is no order in it.
ref.erf_sweep_ref
fn erf_sweep_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
erf(x) over the first n lanes: a coverage reference with no leetgpu
problem behind it, the way int_ops and cas_swap have none. It is the
one place the composition is compared to an accurate erf on its own,
with nothing multiplied on top to hide where the error is.
ref.geglu_ref
fn geglu_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
x1 * (0.5 * x2 * (1 + erf(x2 / sqrt(2)))) where x1 is the first half
of the input buffer and x2 the second, over the first n lanes of the
output: leetgpu 65 (Gaussian Error Gated Linear Unit). n is half the
input length, which is what the problem calls N // 2.
The gate is the EXACT gelu, 0.5 x (1 + erf(x / sqrt(2))), and not the
tanh approximation: 65 says torch.erf. (74 GPT-2 Block, which the plan
paired with this one, says F.gelu(approximate="tanh") instead, so it
does not need an erf at all.)
ref.matmul_bt_ref
fn matmul_bt_ref(m: Int, k: Int, n: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
scale * (a * b^T) for an m by k matrix a and an n by k matrix
b, both row-major, into an m by n output: the shape a linear layer
has when its weights are stored out by in, which is how both leetgpu
85 (LoRA Linear) and leetgpu 6 (Softmax Attention) state theirs.
ins is [a, b, old out].
Folded LEFT over k, and so a TOLERANCE-tier oracle for the same reason
matmul_ref is: mmaf promises a product and nothing about the order
of the sum.
ref.lora_out_ref
fn lora_out_ref(m: Int, r: Int, n: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
base + scale * (h * b^T) for the m by r hidden product h and the
n by r weight b: the last of leetgpu 85's three launches, the one
that adds the low-rank delta to the base linear it did not compute.
ins is [base, h, b, old out].
ref.row_softmax_ref
fn row_softmax_ref(rows: Int, cols: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The softmax of each of rows rows of cols values: leetgpu 6's middle
launch. The maximum is subtracted per row, as the kernel does.
ins is [scores, old out].
ref.swiglu_act_ref
fn swiglu_act_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
silu(gate) * up over the first n lanes: the third of leetgpu 84's
four launches, the only one of them that is not a matrix product.
ins is [gate, up, old out].
ref.apsp_step_ref
fn apsp_step_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
One Floyd-Warshall round over an n by n distance matrix:
d = min(d, d[:, k] + d[k, :]), with k read from the SECOND buffer's
element 0. leetgpu 73 (All-Pairs Shortest Paths) is n of these, and the
round index is a buffer rather than an argument because a launch here
carries a grid and buffers and nothing else.
Writes back to argument 0: the round rewrites the matrix it read.
ins is [dist, k].
Every read is of the matrix as it was BEFORE the round, which is what the problem's reference does as well (it builds a new tensor each round). On the device that is one block over one tile, so the loads all precede the store.
ref.flash_attn_ref
fn flash_attn_ref(m: Int, n: Int, d: Int, bk: Int, scale: Float, dtypes: List[String], ins: List[List[Float]], p_round: String = "") -> List[Float]
FlashAttention's ONLINE softmax, softmax(scale * q k^T) v for m
queries against n keys of d features, one query row at a time and
the keys bk at a time: the running maximum mx, the running sum l
of the exponentials and the running weighted sum acc of the values,
the first two rescaled by exp(mx_old - mx_new) whenever a block raises
the maximum, and acc / l at the end. ins is [q, k, v, old o].
A second opinion on the fused kernel, and deliberately not the
three-launch chain (matmul_bt_ref, row_softmax_ref, matmul_ref):
that chain is what this is held to on the host, by the test at the end
of this file, so the two references are two algorithms that must agree
rather than one algorithm written twice. The first block's rescale is
exp(-inf), which ref_exp answers 0.0, so nothing special-cases it.
p_round names the format the probabilities are narrowed to before the
second product, "" for none: "bf16" is scripts/tile-golden's
flash_attn_bf16, which writes p.to(BF16) there. Only that product
sees the narrowed p; the running sum l adds the unrounded ones, as
the kernel does, so the two halves of acc / l are not rounded alike on
either side.
ref.causal_scores_ref
fn causal_scores_ref(m: Int, k: Int, n: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 53 (Causal Self-Attention): scale * (q k^T) where the key is
not in the query's future, -inf where it is. ins is [q, k, old s].
ref.alibi_scores_ref
fn alibi_scores_ref(m: Int, k: Int, n: Int, scale: Float, alpha: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 55 (Attention with Linear Biases): ALiBi adds alpha * (i - j)
to every score and masks nothing, so every lane here is finite.
ins is [q, k, old s].
ref.window_scores_ref
fn window_scores_ref(m: Int, k: Int, n: Int, scale: Float, w: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 59 (Sliding Window Self-Attention): a band of half-width w
about the diagonal, in both directions, and -inf outside it. The one
rule of this family that is not causal. ins is [q, k, old s].
ref.sink_scores_ref
fn sink_scores_ref(m: Int, k: Int, n: Int, scale: Float, sinks: Int, w: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 112 (Attention with Sinks): causal, and within that either one
of the first sinks keys or inside the trailing window of w.
ins is [q, k, old s].
ref.decay_scores_ref
fn decay_scores_ref(m: Int, k: Int, n: Int, scale: Float, ln_gamma: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 92 (Decaying Causal Attention): the score times gamma^(i - j)
where the key is not in the future, and zero where it is. There is no
softmax in this problem at all, so these weights are what the context
product reads. ins is [q, k, old s].
gamma^d is exp(d * ln gamma) here because that is what the kernel
computes: ln gamma is a host constant of the recording, and a pow on
each side would be two approximations of one power rather than one
formula computed twice.
ref.cce_row_ref
fn cce_row_ref(rows: Int, cols: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 25's first launch: logsumexp(row) - row[label] for each of
rows rows of cols logits. The maximum is subtracted per row, as the
kernel does. ins is [logits, labels, old out], and labels is an
i32 buffer, so its elements are whole numbers carried as Floats.
ref.mean_ref
fn mean_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The mean of the first n lanes into element 0: leetgpu 25's second
launch, and the reason its answer is a scalar rather than a row.
ins is [x, old out].
ref.matvec_ref
fn matvec_ref(m: Int, n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
y = A x for a DENSE m by n matrix and an n-vector: leetgpu 18
(Sparse Matrix-Vector Multiplication), whose own reference is
torch.matmul over a buffer with zeroes in it. ins is [a, x, old y].
ref.subarray_sum2d_ref
fn subarray_sum2d_ref(cols: Int, r0: Int, r1: Int, c0: Int, c1: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The wrapping i32 sum of the rectangle [r0, r1] x [c0, c1] of a matrix
cols wide, at element 0 of the second buffer: leetgpu 48 (2D Subarray
Sum). Both bounds are INCLUSIVE, as the problem states them.
ref.subarray_sum3d_ref
fn subarray_sum3d_ref(rows: Int, cols: Int, d0: Int, d1: Int, r0: Int, r1: Int, c0: Int, c1: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The same over the box [d0, d1] x [r0, r1] x [c0, c1] of an array whose
planes are rows by cols: leetgpu 49 (3D Subarray Sum).
ref.swiglu_half_ref
fn swiglu_half_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
silu(x1) * x2 with x1 and x2 the two halves of ONE buffer of n
lanes each: leetgpu 54 (Swish-Gated Linear Unit). ins is
[x, old out]. geglu_ref's shape with the swish gate in place of the
error function one, and NOT swiglu_act_ref, which is handed two
buffers a projection wrote.
ref.trig_sweep_ref
fn trig_sweep_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
sin cos tan sinh cosh atan2 remf over n lanes each, laid down in
that order in ONE output buffer: segment j holds function j at
offset j * n. The kernel writes seven masked stores at seven base
offsets, so every lane of every function is compared and nothing is
summed; a sum would let two wrong functions cancel, which is what the
older mathops accepts and this one does not.
ins is [x, y, z, old out]: x is the argument of the five unary
ones and the numerator of the atan2, y is that atan2's
denominator (and carries the zeros the axes need), and z is the
remainder's divisor (and carries no zero, because a remainder by zero is
a NaN and a NaN is never within a tolerance of anything).
ref.rope61_ref
fn rope61_ref(m: Int, d: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Q * cos + rotate_half(Q) * sin over an m by d buffer: leetgpu 61
(Rotary Positional Embedding). rotate_half swaps the two halves of a
row and negates what lands first, so the answer at column j below
d / 2 is q[j] * cos[j] - q[j + d/2] * sin[j] and at j above it is
q[j - d/2] * sin[j] + q[j] * cos[j].
THIS READS cos AND sin AT THE FULL COLUMN j, and the kernel reads
them at j mod (d / 2). Both are right because the problem guarantees
the two tables are HALF-SPLIT (column j and column j + d/2 hold the
same value), and the difference is the point: the kernel takes the
shortcut the guarantee allows and the reference does not, so a corpus
whose tables were not half-split would separate them. The corpus in
scripts/tile-gpu-diff builds them half-split, which is what the problem
hands a solver.
ins is [q, cos, sin, old out].
ref.dequant_ref
fn dequant_ref(m: Int, n: Int, ts: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Y = X * S where one element of S scales a ts by ts block of an
m by n matrix: leetgpu 64 (Weight Dequantization). S is
ceil(m / ts) by ceil(n / ts), which is the problem's
repeat_interleave then truncate read from the other side.
ins is [x, s, old y].
ref.conv3d_ref
fn conv3d_ref(d: Int, h: Int, w: Int, kd: Int, kh: Int, kw: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The valid-mode convolution of a d by h by w volume with a kd by
kh by kw kernel: leetgpu 11 (3D Convolution). The output is
(d - kd + 1) by (h - kh + 1) by (w - kw + 1) and the taps are
folded in the order the kernel's three host loops unroll them.
ins is [x, w, old out].
ref.mha_scores_ref
fn mha_scores_ref(n: Int, dmodel: Int, heads: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Q_head K_head^T * scale for every head of an n by dmodel pair,
laid out one head's n by n block after another: leetgpu 12's first
launch. Head hh owns the columns [hh * dk, (hh + 1) * dk) of both
matrices, dk = dmodel / heads. ins is [q, k, old s].
ref.mha_context_ref
fn mha_context_ref(n: Int, dmodel: Int, heads: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
P_head V_head for every head, the answer written into the head's own
columns of an n by dmodel output: leetgpu 12's third launch.
ins is [p, v, old o].
ref.agent_step_ref
fn agent_step_ref(n: Int, r2: Float, alpha: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
One step of the flocking rule of leetgpu 14 (Multi-Agent Simulation):
every agent moves by its velocity after being pulled alpha of the way
towards the mean velocity of the agents within sqrt(r2) of it, and an
agent with no neighbour keeps its own. ins is [agents, old out], and
both are n agents of four floats.
ref.nearest_idx_ref
fn nearest_idx_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The nearest other point of each of n three-dimensional points, as an
i32 index: leetgpu 38 (Nearest Neighbor). ins is [points, old out].
ref.xattn_scores_ref
fn xattn_scores_ref(m: Int, n: Int, dmodel: Int, heads: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Q_head K_head^T * scale where the query has m rows and the key n,
laid out one head's m by n block after another: leetgpu 26's first
launch. Head hh owns the columns [hh * dk, (hh + 1) * dk) of a row of
dmodel, dk = dmodel / heads, which is leetgpu 12's layout at two
sequence lengths. ins is [q, k, old s].
ref.xattn_context_ref
fn xattn_context_ref(m: Int, n: Int, dmodel: Int, heads: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
P_head V_head for every head of leetgpu 26: m query rows over n
key rows, the answer written into the head's own columns of an m by
dmodel output. ins is [p, v, old o].
ref.gqa_scores_ref
fn gqa_scores_ref(hq: Int, hkv: Int, s: Int, d: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Q_h K_kv^T * scale for every query head of leetgpu 80 (Grouped Query
Attention), where head h reads key head h / (hq / hkv). Every head
is a CONTIGUOUS s by d matrix here, so the group is a division and
not a column offset. ins is [q, k, old s].
ref.gqa_context_ref
fn gqa_context_ref(hq: Int, hkv: Int, s: Int, d: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
P_h V_kv for every query head of leetgpu 80, with the same grouping.
ins is [p, v, old o].
ref.grpo_adv_ref
fn grpo_adv_ref(b: Int, g: Int, eps: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The group-normalised advantage of every reward of leetgpu 109 (GRPO
Surrogate Loss): b groups of g rewards, each centred on its group's
mean and divided by its group's BIASED standard deviation plus eps.
ins is [rewards, old adv].
ref.grpo_row_ref
fn grpo_row_ref(rows: Int, s: Int, clip: Float, beta: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Minus the mean over s tokens of the clipped surrogate less beta
times the k3 KL penalty, one row a lane: leetgpu 109's second launch.
The negation is here and not after the mean because negation is exact,
so the mean of the negated rows is the negated mean and the third launch
can be a plain average. ins is
[log_pi, log_pi_old, log_ref, adv, old rows].
ref.kmeans_assign_ref
fn kmeans_assign_ref(n: Int, k: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The nearest centroid of each of n two-dimensional samples, as an i32
label: leetgpu 20's first launch, run once a round. ins is
[data_x, data_y, centroid_x, centroid_y, old labels].
ref.kmeans_centroid_ref
fn kmeans_centroid_ref(n: Int, k: Int, dtypes: List[String], ins: List[List[Float]]) -> List[(Int, List[Float])]
Both centroid buffers of leetgpu 20 after one round, IN PLACE: the two
coordinates are two output buffers, and a cluster nothing was assigned
to keeps the centroid it had. ins is
[data_x, data_y, labels, centroid_x, centroid_y], and the answer names
the last two argument positions.
ref.kv_scores_ref
fn kv_scores_ref(heads: Int, seq: Int, d: Int, inv_sqrt_d: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 96's first launch: s[h, j] = (Q[h] . dequant(K)[h, j]) /
sqrt(head_dim), heads rows of seq. ins is
[Q, K_int8, k_scale, old s].
ref.kv_context_ref
fn kv_context_ref(heads: Int, seq: Int, d: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 96's third launch: o[h, dd] = sum_j p[h, j] *
dequant(V)[h, j, dd], heads rows of head_dim. ins is
[p, V_int8, v_scale, old o].
ref.attn_bwd_mmt_ref
fn attn_bwd_mmt_ref(m: Int, n: Int, d: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
c = a^T b for an m by n matrix a and an m by d one, into an
n by d output: leetgpu 111's dV = P^T dO and its dK = dS^T Q,
which are the same product on different operands. ins is
[a, b, old c].
Folded LEFT over m, so a TOLERANCE-tier oracle for mmaf's reason.
ref.attn_bwd_ds_ref
fn attn_bwd_ds_ref(rows: Int, cols: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The softmax Jacobian applied to a row: dS = dP * P - P * rowsum(dP * P)
over rows rows of cols, leetgpu 111's fifth launch. ins is
[dP, P, old dS].
The 1 / sqrt(d) the problem divides by is NOT here: the expression is
linear in dP, and the launch that produced dP is attn_scores,
which already divided. Moving a scale to the launch that has one is what
makes this problem cost two kernels instead of four.
ref.elu_plus_one
fn elu_plus_one(x: Float) -> Float
phi(x) = ELU(x) + 1: x + 1 above zero and exp(x) at or below it.
leetgpu 56's feature map, and the reason its two kernels have no expm1
in them: ELU(x) = exp(x) - 1 there and the + 1 cancels exactly.
ref.lin_attn_s_ref
fn lin_attn_s_ref(m: Int, d: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 56's first launch: S[a, b] = sum_j phi(K[j, a]) * V[j, b], a
d by d answer over m rows. ins is [K, V, old S].
Its SECOND launch is this same function with a buffer of ones where V
is, which answers z[a] = sum_j phi(K[j, a]) in every column.
ref.lin_attn_out_ref
fn lin_attn_out_ref(m: Int, d: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 56's third launch: out[i, b] = (phi(Q) S)[i, b] /
(phi(Q) z)[i], where z is the first COLUMN of the d by d matrix
the second launch wrote. ins is [Q, S, Z, old out].
ref.ols_gram_ref
fn ols_gram_ref(n: Int, f: Int, w: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 33's first launch: the augmented normal equations of
X^T X beta = X^T y as an f by w matrix, columns 0..f-1 holding
X^T X, column f holding X^T y and the rest zero. ins is
[X, y, old A].
Folded LEFT over the samples, which is the order the kernel's d_range
adds them in, so this oracle is EXACT and not a tolerance claim: there
is no mmaf here and no reduction whose tree is unspecified.
ref.ols_elim_ref
fn ols_elim_ref(f: Int, w: Int, dtypes: List[String], ins: List[List[Float]]) -> List[(Int, List[Float])]
One Gauss-Jordan step of leetgpu 33 IN PLACE: row k becomes itself
divided by its pivot and every other row loses its k-th multiple of
it, with k read from the SECOND buffer's element 0 the way
apsp_step_ref reads its round. ins is [A, k], and the answer names
argument 0.
No pivot search: A's leading f columns are a Gram matrix, which is
symmetric positive definite, so every leading minor is positive and
every pivot this visits is non-zero.
ref.ols_beta_ref
fn ols_beta_ref(f: Int, w: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
leetgpu 33's last launch: beta is column f of the reduced augmented
matrix. ins is [A, old beta].
ref.linear_bias_ref
fn linear_bias_ref(m: Int, k: Int, n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
a * W + b, an m by k matrix against a k by n weight stored IN
by OUT with one bias per output column: the shape every linear layer of
leetgpu 74 has, at four different (k, n). ins is
[a, w, bias, old out].
Folded LEFT over k, so a TOLERANCE-tier oracle for matmul_ref's
reason: mmaf promises a product and nothing about the order of its sum.
ref.gpt_scores_ref
fn gpt_scores_ref(s: Int, d: Int, heads: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Q_h K_h^T * scale for every head of leetgpu 74, both operands read out
of the PACKED projection buffer: s rows of 3 * d values, Q in the
first d columns and K in the next d, head h owning columns
h * d / heads of each. ins is [qkv, old s].
ref.gpt_context_ref
fn gpt_context_ref(s: Int, d: Int, heads: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
P_h V_h for every head of leetgpu 74, V read from the LAST third of
the same packed buffer and the answer written into head h's columns of
an s by d output. ins is [p, qkv, old o].
ref.gelu_tanh_ref
fn gelu_tanh_ref(n: Int, c: Float, kk: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
0.5 * x * (1 + tanh(c * (x + kk * x^3))) over the first n lanes: the
TANH approximation of the gaussian error linear unit, which is what
leetgpu 74's feed-forward asks for (F.gelu(x, approximate="tanh")) and
not the erf form of leetgpu 65. ins is [x, old out].
ref.llama_rms_ref
fn llama_rms_ref(rows: Int, d: Int, eps: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
x * rsqrt(mean(x^2) + eps) * w per ROW of a rows by d matrix: the
root-mean-square normalisation of leetgpu 93, which has a learned weight
and no residual (leetgpu 83's has both, leetgpu 50's has neither).
ins is [x, w, old], with w d long.
ref.head_major_bt_ref
fn head_major_bt_ref(s: Int, din: Int, hd: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The head COUNT is read off the output buffer rather than fixed here, the way the kernel reads it off the grid: leetgpu 93 launches the same projection at four query heads and at two key heads, and the buffer's total width enters neither.
One projection of leetgpu 93 written HEAD-MAJOR: out[h][i][c] = sum_j
x[i][j] * W[h * hd + c][j] for a weight stored OUT by IN. The same
numbers matmul_bt_ref computes, laid out head by head instead of token
by token, which is the layout knife 19's grouped-query kernels read.
ins is [x, w, old out].
ref.rope_ref
fn rope_ref(s: Int, hd: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The head count is read off the output buffer, for head_major_bt_ref's
reason: the same rotation runs over four heads and over two.
The rotary position embedding of a head-major heads by s by hd
buffer: the two halves of each head's row rotated by the angle of that
POSITION, [x1 * cos - x2 * sin, x1 * sin + x2 * cos]. cos and sin
are s by hd / 2 tables every head shares, and leetgpu 93 hands them
in as buffers, so nothing here computes a trigonometric function.
ins is [x, cos, sin, old out].
ref.gqa_causal_scores_ref
fn gqa_causal_scores_ref(hq: Int, hkv: Int, s: Int, d: Int, scale: Float, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
gqa_scores_ref with a CAUSAL mask: the score at a key in the query's
future is -inf, so leetgpu 93's softmax gives it weight exactly zero.
ins is [q, k, old s].
ref.llama_out_ref
fn llama_out_ref(s: Int, d: Int, hd: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Leetgpu 93's output projection: the head-major attention output read as
one s by d matrix and multiplied by a weight stored OUT by IN,
out[i][o] = sum_h sum_t a[h][i][t] * W[o][h * hd + t]. The merge of
the heads is this indexing and no launch of its own.
ins is [a, w, old out].
ref.shape_ops_ref
fn shape_ops_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The three shape operations over one block of 128 f64, read two ways:
ins is [x, old out]. Block b reads x[b * 128 ..] and fills 192
lanes: the first 64 are the 16 by 8 tile with its two row halves
exchanged and only its first four columns kept (extract twice, then
cat the other way round), and the next 128 are the 4 by 4 by 8 tile
with its first two axes swapped (permute [1, 0, 2]).
ref.grid_stride_ref
fn grid_stride_ref(tiles: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
2 * x over tiles tiles of 128 lanes: ins is [x, old out].
The reference does not model the grid at all, and that IS the claim. The kernel covers the tiles with a grid-stride loop over a grid smaller than the work, so a loop that visited every tile exactly once answers this and one that missed a tile (leaving the sentinel) or repeated one instead of another does not.
ref.token_join_ref
fn token_join_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The low half of each 128-lane chunk doubled and the high half tripled,
written into a scratch buffer by two INDEPENDENT chains of stores and
read back through the token that joins them: ins is
[x, scratch, old out].
The scratch buffer's own contents never appear here, which is the judgement: it is handed a sentinel no lane of the answer holds, so a load that was not ordered after both stores would show.
ref.ptr_roundtrip_ref
fn ptr_roundtrip_ref(blocks: Int, delta: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
x[i + delta] over blocks tiles of 128 lanes, the address having gone
through i64 and back: ins is [x, old out]. The input buffer carries
delta elements past the last block's chunk, which is why the answer is
an ordinary shifted copy and not a masked one.
ref.ptr_recast_ref
fn ptr_recast_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The binary32 value whose bit pattern is the i32 lane: ins is
[patterns, old out]. The kernel reads the same four bytes through a
pointer of another element format; this reads them by taking the pattern
apart, which is a second opinion about the FORMAT and not only about the
address.
ref.insert_tile_ref
fn insert_tile_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The insert kernel over one block of 128 f64 read as a 16 by 8 tile:
ins is [x, old out]. Block b fills 256 lanes from x[b * 128 ..].
The first 128 are the tile itself, which is what putting the quarter at
slice [1, 0] back where it came from answers. The next 128 are the tile
with its quarter at slice [0, 1] (rows 0 to 7, columns 4 to 7) replaced by
the one at [1, 0] (rows 8 to 15, columns 0 to 3); every other lane is the
destination's own.
ref.ref_fpowi
fn ref_fpowi(x: Float, n: Int) -> Float
x raised to the integer power n the way the device's fpowi does it
over f64, which is NOT the correctly rounded power. Measured on this
tree's RTX 3080 (tileiras 13.4.92, sm_86, docs/tile-backend-design.md
6.19): the device answers right-to-left binary exponentiation of x to
|n| -- square the base once a bit, multiply it into the result where the
bit is set, every step one rounded binary64 product -- and for a negative
n the RECIPROCAL of that, one rounded division at the end. Bit for bit
on every lane of the probe corpus, where the power of the reciprocal
((1/x)^|n|) agrees on two lanes in three and repeated multiplication on
fewer than half.
What falls out of that sequence is the whole of the edge-case
semantics, and every one of these is the device's answer too: x^0 is
1.0 for EVERY x, NaN, infinities and both zeroes included (the loop
never runs); 0^n and (-0)^n for a negative n are the infinities the
final division by a zero of that sign gives, so (-0)^-1 is -inf and
(-0)^-2 is +inf; a power that overflows on the way is inf before
the division, so its reciprocal is a zero, and one that underflows to a
subnormal is divided as the subnormal it rounded to.
ref.powi_sweep_ref
fn powi_sweep_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The fpowi kernel over 128-lane blocks: ins is [x, n, old out], x
the f64 bases and n the i32 exponents. Block b fills 256 lanes: the
first 128 are x^n, the next 128 are x raised to the LOW OCTET of n
read as a signed i8 (the kernel truncis the exponent to i8 and the
dialect reads every exponent width as signed), so an exponent of 200 is
the power -56 there.
ref.f32_flush
fn f32_flush(x: Float) -> Float
The number a flush_to_zero operation sees or answers in place of x:
a subnormal binary32 becomes the zero of its sign, everything else is
itself. The smallest normal binary32 is 2^-126.
ref.attr_round_ref
fn attr_round_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Six directed roundings of one pair of binary32 operands: the sum, the
product and the quotient, each towards negative and towards positive
infinity, in blocks blocks of 128 lanes and six segments each. ins
is [x, y, old out].
The sum and the product of two binary32 values are EXACT in binary64 (48 significand bits at worst, and both exponents well inside the range), so the two rounding modes are applied to the exact result and the answer is the format's own. The quotient is not exact, and the corpus is what makes it safe: the divisors are small odd integers scaled by powers of two, so the exact quotient is never within 2^-53 of a binary32 grid point and rounding the binary64 quotient in the named direction lands where rounding the exact one would. attr_diff prints the corpus's divisors so that a change to it is visible.
ref.attr_nan_ref
fn attr_nan_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The two comparison orderings and the two NaN policies over one pair of
f64 tiles, in blocks blocks of 128 lanes and sixteen segments each:
the six predicates ordered, the same six unordered, then maxf and
minf with and without propagate_nan. ins is [x, y, old out].
What the dialect says, and what this therefore says: an ORDERED
comparison answers false whenever either operand is a NaN, an UNORDERED
one answers true; maxf without propagate_nan is IEEE 754-2019's
maximumNumber (the non-NaN operand, and a canonical NaN only when both
are), with it maximum (a canonical NaN if either is).
ref.attr_ftz_ref
fn attr_ftz_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The sum and the product of one pair of binary32 operands with and
without flush_to_zero, in blocks blocks of 128 lanes and four
segments each. ins is [x, y, old out].
The attribute flushes SUBNORMAL INPUTS as well as subnormal results, which is what the dialect's own wording says ("flushes subnormal inputs and results to sign-preserving zero") and what this reference therefore does: flush both operands, do the operation, flush the answer.
ref.attr_approx_ref
fn attr_approx_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The square root of one binary32 tile, twice: rounding<nearest_even>
and rounding<approx>, in blocks blocks of 128 lanes and two
segments. ins is [x, old out].
Both segments answer the same thing here, which is the whole of what this reference can say: the approximate root is the ARCHITECTURE's approximation and no host expression is its definition. This is a tolerance-tier kernel, and what says the attribute reached the device is the DISTANCE between the two segments, which attr_diff measures and run.sh pins above zero.
ref.attr_overflow_ref
fn attr_overflow_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The sum, the difference and the product of two i32 tiles under the
three overflow assumptions, in blocks blocks of 128 lanes and three
segments each. ins is [x, y, old out].
wrap_i32 is applied as it is everywhere else here, and on this corpus
it never has anything to do: an operation that wrapped under
no_signed_wrap would be undefined and the reference would be
answering a question the dialect refuses to answer.
ref.attr_memsem_ref
fn attr_memsem_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Three atomic integer additions under three memory orderings and scopes,
each into its own 128 slots of a buffer the kernel also reads back:
ins is [x, old out] and the answer is the new contents of the
second. blocks blocks own 384 slots each, so no two lanes anywhere
name one slot.
That is the point and the limit: an ordering constrains CONCURRENT accesses, this corpus has none, and so what the device can say here is that the atomic ran and added. attrs.txt records the five values at layer 1 with that reason.
ref.attr_addf_ref
fn attr_addf_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The float atomic read-modify-write: every one of blocks blocks adds
its own chunk of x into the SAME 128 slots. ins is [x, old out].
The lanes collide across blocks, so the order they arrive in is not defined; the corpus is small integers, whose sum is exact in binary64 under any order, so the tier is exact by algebra rather than by luck.
ref.attr_ucmp_ref
fn attr_ucmp_ref(blocks: Int, lo: Int, hi: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The iteration counts of two loops over one range that straddles 2^31,
one comparing its bound as a signed i32 and one as an unsigned one:
ins is [old out] and each of blocks blocks fills 256 slots, the
first 128 with the signed count and the next 128 with the unsigned one.
lo and hi are the bounds the kernel was recorded with. Read as
signed, hi is negative and below lo, so the loop does not run at
all; read as unsigned it is hi - lo above it. This is the one unit
attribute of knife T4 whose effect on the device is an integer rather
than a rounding.
ref.attr_sat_ref
fn attr_sat_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
ftoi from f64 to i32 with and without the saturating modifier (knife
T17), in blocks blocks of two 128-lane segments. ins is
[x, old out].
The first segment is the modifier's definition in Ops.td: truncate toward
zero, then a lane above i32's largest value is that value, one below its
smallest is that one, and a NaN is 0. The second holds only the lanes
whose truncation IS an i32 (-2^31 - 1 < x < 2^31) and keeps the output
buffer's own value everywhere else, because the kernel stores it only
there: without the modifier the other lanes are undefined behaviour, and
what one device answers for them is not a reference.
ref.attr_ftof_ref
fn attr_ftof_ref(blocks: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
ftof under the rounding modes bytecode 13.4 newly allows (knife T17),
each beside the nearest_even it differs from, in blocks blocks of
eleven 128-lane segments. ins is [x, old out]. Segment by segment:
0-3 x to f32: nearest_even, zero, negative_inf, positive_inf 4-6 segment 0 to tf32: nearest_even, zero, nearest_away 7-8 segment 0 to f16: nearest_even, zero 9-10 segment 0 to bf16: nearest_even, zero
Every one is std/narrow's rounding of the exact value: x is a binary64, so the first four round it once, and segment 0 is a binary32, which every later target rounds once more from. The widening back to f64 that the kernel does after each is exact, whatever mode it is written in, and so does not appear here.
ref.attr_xchg_ref
fn attr_xchg_ref(slots: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The atomic exchange (knife T17): every lane i of the first slots of
ins[0] swaps itself into slot slots - 1 - i of the output, and what
that slot held is stored at slots + i. ins is [x, old out]. No two
lanes name one slot, so the order they arrive in cannot matter, and the
answer is two reversals: the input into the first half, the output
buffer's own first half into the second.
ref.dtype_i16_ref
fn dtype_i16_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The five segments of dtype_i16: the sum, the difference, the product,
(a << 3) >> 2 and the itof / ftoi round trip, each wrapped into
[-32768, 32767] as the device's i16 arithmetic wraps. ins is
[a, b, out].
The round trip is the IDENTITY and is here for that reason: every i16 is a binary64 exactly, so widening and truncating back cannot lose anything, and a segment that is not the input is a conversion at the wrong width.
ref.dtype_i64_ref
fn dtype_i64_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The five segments of dtype_i64: the sum, the difference, the product,
the sum with the constant 2^32 and the itof / ftoi round trip. ins is
[a, b, out].
NO WRAPPING HERE, which is the difference from the i16 reference and a
statement about the corpus rather than about the format: the device wraps
at 2^64 and this cannot follow it there, so the corpus stays inside
+/-2^52 (dtype_diff.dawn counts the products that leave the i32 range,
which is what makes the width itself visible).
ref.dtype_i64_ref_int
fn dtype_i64_ref_int(n: Int, a: List[Int], b: List[Int]) -> List[Int]
dtype_i64_ref over Ints, for a corpus that leaves +/-2^53: the same
five segments (n lanes each, 5 * n answers, no output buffer to keep a
tail of), computed in the Ints the device computes in. Int addition,
subtraction and multiplication wrap at 2^64 here as the device's addi,
subi and muli do, so nothing in the three arithmetic segments is
approximate, and the constant segment is exact too. The last segment is
the kernel's own itof / ftoi round trip and it is NOT the identity past
2^53: to_float rounds to the nearest binary64 (ties to even) and
to_int truncates it back, which is the device's itof and ftoi.
Why a twin and not a change to dtype_i64_ref: the fake device hands
its references List[Float] (WideRefFn), and an i64 past 2^53 cannot
be one. This function is called by a driver that holds the corpus as
Ints and compares with a real device's typed download.
ref.dtype_i4_ref
fn dtype_i4_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The five segments of dtype_i4: the nibble-wise sum, the sign-extended
nibble shifted right by one, the zero-extended one shifted right by one,
the nibble-wise product, and a's nibble on even lanes with b's on odd
ones. ins is [a, b, out], and every buffer is i32: there is no
buffer of i4 at all, so a word carries eight lanes and this reference
reads them the way unpack does (nibble_at).
SEGMENTS 1 AND 2 ARE THE SAME SHIFT under the two widenings, and they differ on exactly the lanes whose top bit is set. Nothing else here can see the difference: + - * agree modulo 16 whichever widening produced the operands, which is why the kernel carries a shift at all.
ref.pack_roundtrip_ref
fn pack_roundtrip_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The two segments of pack_roundtrip: the bytes that went in, twice.
ins is [a, out] and both buffers are i32.
This reference names no nibble and no half word, which is what makes it the CONTROL for pack-halves-swapped: a round trip through a format is blind to the order the lanes were laid down in, because it lays them back the same way. The kernel is the identity and so is this.
ref.dtype_convert_ref
fn dtype_convert_ref(n: Int, dtype: String, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The two segments of dtype_tf32, dtype_e4m3, dtype_e5m2 and
dtype_e8m0: the buffer of the format widened to f64, and an f64 taken
into the format and back. ins is [x, y, out] and dtype names the
format.
Segment 0 is not a rounding: the buffer already holds values of the
format (copy_from_host rounded them, encode_floats laid them down) and widening is
exact, so the reference is the buffer. That is the whole of what a device
can be wrong about there, and it is exactly the storage layout.
Segment 1 for f8E8M0FNU goes through binary32, because the KERNEL does:
ftof from f64 straight into that format kills tileiras 13.3.36, so
scripts/tile-golden's kernel detours and this rounds twice to match. For
the other three it is one rounding.
ref.dtype_e2m1_ref
fn dtype_e2m1_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The two segments of dtype_e2m1: the packed nibbles of ins[0] read as
f4E2M1FN values and widened to f64, and an f64 taken into the format and
back. ins is [x, y, out], x is an i32 buffer and y and out are
f64 ones.
THIS IS NOT dtype_convert_ref WITH ANOTHER NAME, and segment 0 is the
reason. Every other narrow float format has a buffer of its own, so
copy_from_host rounds the corpus into the format and segment 0 is the buffer as
it stands. f4E2M1FN has none (element_bytes refuses it), so its kernel
brings the bits in as i32 words and unpacks them, and the reference has
to read the same words the same way: lane i is nibble i % 8 of word
i / 8, low nibble first, and narrow.f4e2m1_of_bits says which of the
sixteen values that nibble is. What segment 0 asks is therefore the
ENCODING and not a rounding.
Segment 1 is one rounding, as it is for f8E4M3FN and f8E5M2: round_to
dispatches to narrow.round_f4e2m1, and the widening back to f64 is
exact.
ref.mmaf_scaled_ref
fn mmaf_scaled_ref(m: Int, k: Int, n: Int, v: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The block-scaled matrix product mmaf_scaled_e4m3 computes, as one
m by n tile widened to f64. ins is [a, b, sa, sb, out]: a is
m by k and b is k by n in the operand format, sa is
m by k / v and sb is k / v by n in the scale format, and every
buffer is laid out row-major.
THE FORMULA IS Ops.td's, transcribed rather than guessed. MmaFScaledOp
writes it out:
result[i][j] = acc[i][j] + sum over k of (a[i][k] sa[i][k / v]) (b[k][j] * sb[k / v][j])
so v is the BLOCK: one scale element covers a run of v consecutive
elements along k, and k / v is integer division. v is not an operand
and not an attribute -- Ops.td has no such parameter -- it is the ratio
of the two shapes, which is why it is an argument here and not something
this could read off the buffers.
The accumulator is zero and is therefore not a parameter: the kernel
builds it with f_const and no host buffer carries it.
THE FOLD ORDER IS NOT PINNED, and the corpus is what makes that harmless. The device accumulates in f32 in an order the dialect does not specify, so a reference that folded left would be one reading among many; the corpus scripts/tile-gpu-diff/arch_diff.dawn ships keeps every product and every partial sum a whole number well inside 2^24, where f32 addition is exact and therefore associative. What is left for the device to be wrong about is the scaling itself.
ref.collatz_step_ref
fn collatz_step_ref(v: Int, steps: Int) -> (Int, Int)
The Collatz successor of v, saturating at 1: a lane that has reached 1
stays there, and the step is counted only while the lane is still
moving. The device computes both branches and selects, and wraps at
32 bits; this does the same, so 3 * v + 1 past 2^31 wraps here too.
ref.loop_count_ref
fn loop_count_ref(n: Int, fuel: Int, segs: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The step count of every one of the first n lanes of ins[0], and the
number of iterations the loop ran repeated across the next n: the
reference for loop_count and, without the second segment, for
loop_bound.
The round count is the LARGEST step count, and that is an argument rather than a definition: every lane that is still moving adds one on every iteration and the loop stops on the first iteration where none is, so the number of iterations that changed anything is the longest chain. The kernel computes it with a second reduction over the counts and this computes it with a fold; both are the maximum, and the device is where the two are held together.
ref.loop_until_ref
fn loop_until_ref(n: Int, eps: Float, fuel: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The square root of each of the first n lanes of ins[0] by Newton's
iteration from 1.0, and the number of rounds it took repeated across the
next n: the reference for loop_until.
EXACT TIER BY ALGEBRA. Every operation in the iteration is one IEEE 754 requires to be correctly rounded, the lanes are independent, and the order is the same on both sides, so the tiles agree bit for bit. What the round count adds is that the two sides also agree on WHEN to stop, and that is the part only a data-dependent loop can be wrong about: the residual falls by five orders of magnitude between the last iteration and the one before it for the corpus this is used with, so the two sides cannot disagree about the comparison by a rounding.
ref.loop_none_ref
fn loop_none_ref(n: Int, threshold: Float, step: Float, fuel: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
The first n lanes of ins[0], unchanged, when every one of them is
under threshold, and step added to each otherwise: the reference for
loop_none, whose loop stops before its body has changed anything.
It is written as the general loop rather than as "the input" so that it is a reference and not a restatement of the corpus: hand it a tile over the threshold and it iterates, exactly as the kernel does.
The step moves the tile AWAY from the exit, so a tile that starts over
the threshold never reaches it and fuel is what stops this function.
That asymmetry is the kernel's and it is deliberate: it is what makes
the loop-break-condition-inverted mutant stop after one iteration
rather than never (scripts/tile-gpu-diff/run.sh says so at the mutant).
ref.loop_return_ref
fn loop_return_ref(blocks: Int, at: Int, limit: Int, fuel: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
loop_return (knife T17): each of blocks blocks of 128 lanes of
ins[0] runs the kernel's loop, and its 256 output slots say how. The
first 128 hold how many iterations the block BEGAN (the kernel stores the
count at the top of every iteration); the next 128 hold the tile the loop
broke with, if it broke, and the output buffer's own value if the block
returned instead, because the store after the loop is then never reached.
The order inside an iteration is the kernel's: the count is stored, then
the block returns if its ENTERING tile holds a value at or above at,
then it breaks if the count has reached limit, then every lane goes up
by one. So a block that reaches at on its limit-th iteration returns
rather than breaks. fuel bounds this function and is never what stops
it for a corpus the kernel can finish.
ref.assert_pass_ref
fn assert_pass_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
Knife T6's references. All four are one line of arithmetic each, and
that is the point: what these kernels are FOR is an operation that does
not change the answer (assume), one that stops the launch
(assert) or one that writes to the process's standard output
(print_tko), so the buffer they leave behind has to be something a
host can restate without effort. Anything harder would move the
comparison off the operation under test.
assert_pass: the first n lanes plus one. The kernel's assertion holds
on every lane of the corpus it is run with, so this is what a launch that
does not fire answers. There is no reference for assert_fail: a launch
that fires has no answer to compare, and its verdict is the error the
host is given.
ref.assume_divby_ref
fn assume_divby_ref(n: Int, divisor: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
assume_divby: the first n lanes divided by divisor. The kernel
assumes every lane is a multiple of it, and the corpus makes that true,
so the quotient is exact on both sides and the assumption is a claim
about code generation and not about the answer.
ref.assume_same_ref
fn assume_same_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
assume_same: the first n lanes times three.
ref.assume_bounded_ref
fn assume_bounded_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
assume_bounded: the first n lanes plus five.
ref.print_tile_ref
fn print_tile_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
print_tile: three segments of n lanes -- the input unchanged, then
the sum and the maximum of the first n lanes repeated across the other
two. The same two numbers the kernel PRINTS, which is what lets the bytes
on standard output and the bytes in the buffer be two readings of one
answer.
ref.print_tile_sum
fn print_tile_sum(x: List[Float], n: Int) -> Int
The sum of the first n lanes, modulo 2^32 as an i32 addi folds it.
Public because scripts/tile-gpu-diff computes the line print_tile
prints from it, and that line is a judgement of its own.
ref.print_tile_max
fn print_tile_max(x: List[Float], n: Int) -> Int
The largest of the first n lanes, as a signed i32 maxi folds it.
ref.global_table_a
fn global_table_a(n: Int) -> List[Float]
The two tables global_table's module declares as static globals, and
the values the host says they hold. They are here rather than in the
kernel source because BOTH sides need them: the kernel bakes them into
the Global section's initializer and the reference below answers with
them, and the whole judgement is that the device reads back what the
host declared.
Neither table is a palindrome and neither is a splat, which is what the two writer mutants need: an initializer written backwards and one written as a one-element splat are both legal Tile IR, and only a corpus that can tell them from the truth says so.
ref.global_table_b
fn global_table_b(n: Int) -> List[Float]
The second of global_table's two tables, declared with an alignment:
-(i * i) - 0.25 at lane i, negative everywhere and falling, so it
disagrees with global_table_a on every lane.
ref.global_table_ref
fn global_table_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
global_table: two segments of n lanes, each the corresponding table
plus the input, and the rest of the output buffer left alone. ins is
[x, out].
EXACT TIER BY ALGEBRA: one addf per lane with rounding<nearest_even>,
which is what a binary64 + is.
ref.global_ctl_ref
fn global_ctl_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
global_ctl: the same two segments computed from two BUFFERS instead of
two globals. ins is [a, b, x, out]. The kernel it answers for holds
neither global nor get_global, so it is the T7 family's kernel-level
control and a second opinion on the tables at the same time.
ref.global_scratch_ref
fn global_scratch_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
global_scratch: the output buffer plus the input, lane by lane. ins
is [x, out].
THIS ONE IS NOT THE KERNEL'S ALGORITHM, and that is the point. The
kernel adds its input to a mutable GLOBAL and writes the sum to both;
the reference adds it to the OUTPUT BUFFER, which is where the host can
see the same running total. The two agree only while the global keeps
its value from one launch to the next, so a sequence of launches of the
same module is a judgement about the lifetime of a static allocation and
not about arithmetic. The global's initializer is zeros and so is the
output buffer's first n lanes, which is what starts the two totals
together.
ref.global_syms_c
fn global_syms_c(n: Int) -> List[Float]
The third table global_syms's module declares, the one its constant
global holds. global_table_a and global_table_b are the other two.
Like them it is neither a palindrome nor a splat, and it disagrees with
both of them on every lane, so the three segments of one output buffer
tell three symbols apart.
ref.global_syms_tables
fn global_syms_tables(n: Int) -> List[List[Float]]
The three tables global_syms's module declares, in declaration order:
@shown public, @hidden private, @frozen constant.
ref.global_syms_ref
fn global_syms_ref(n: Int, tables: List[List[Float]], dtypes: List[String], ins: List[List[Float]]) -> List[Float]
global_syms: three segments of n lanes, each one of the module's
three globals plus the input, and the rest of the output buffer left
alone. ins is [x, out].
tables is what the three globals hold AT THE TIME OF THE LAUNCH, which
is a parameter rather than global_syms_tables(n) because the host can
write a module's global between launches: whether a private or a
constant global accepts such a write, and whether the next launch reads
it, is a question about the device, and a reference that baked the
initializers in could not ask it.
EXACT TIER BY ALGEBRA: one addf per lane with rounding<nearest_even>,
which is what a binary64 + is.
ref.alloca_scratch_ref
fn alloca_scratch_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
alloca_scratch: three times the input, lane by lane. ins is
[x, out].
THE ALGEBRA IS THE JUDGEMENT HERE, and not the arithmetic. The kernel
writes x + x into a fresh automatic allocation, reads it back through
the same pointers in the same launch and adds the input to what it read;
this answers (x + x) + x directly. The two agree only while the
allocation is memory that keeps what was put in it, so a scratch that
read back zeros would answer x and a store that never landed would
leave the sentinel.
EXACT TIER BY THE VALUES: every lane of the corpus is a small multiple
of a power of two, so both sums are exact in binary64 and the
rounding<nearest_even> the kernel carries has nothing to round.
ref.alloca_two_ref
fn alloca_two_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
alloca_two: 2 * x + 1, lane by lane, which is x - (-x - 1). ins
is [x, out].
THIS ONE IS A DISTINCTNESS CLAIM. The kernel makes two allocations in
one block, stores x into the first and -x - 1 into the second, and
answers the difference. Two allocations at the same address would answer
0 for every lane, because the second store would have overwritten the
first; the corpus keeps every lane away from -0.5, the one input where
2 * x + 1 is 0 in its own right.
ref.alloca_ctl_ref
fn alloca_ctl_ref(n: Int, dtypes: List[String], ins: List[List[Float]]) -> List[Float]
alloca_ctl: alloca_scratch's answer with no allocation in it. ins
is [x, out]. The kernel it answers for holds no alloca at all, so it
is this family's kernel-level control: no mutant of the allocation may
move its bytes or its verdict.
ref.scalar_scale_ref
fn scalar_scale_ref(n: Int, ins: List[List[Float]], scalars: List[Float]) -> List[Float]
scalar_scale: ins is [x, out] and scalars is [s]. The first n
lanes of out are x * s in binary64 (the kernel widens its f32 scalar
first, which is exact); the rest of out is its own tail.
ref.scalar_len_ref
fn scalar_len_ref(ins: List[List[Float]], scalars: List[Float]) -> List[Float]
scalar_len: ins is [x, out] and scalars is [n]. Lane i of out
is x[i] for i < n and what out held for every other lane: the mask
bound is the launch's value, and a negative one masks every lane.
ref.scalar_loop_ref
fn scalar_loop_ref(tile: Int, ins: List[List[Float]], scalars: List[Float]) -> List[Float]
scalar_loop: ins is [x, out], scalars is [n] and tile is the
block's width. The first tile lanes of out are the first n tiles of
x summed lane by lane in order, starting from zero, which is the loop's
own fold; n below one is the empty sum, zeros. The rest of out is its
own tail.
ref.scalar_wide_ref
fn scalar_wide_ref(n: Int, ins: List[List[Float]], scalars: List[Float]) -> List[(Int, List[Float])]
scalar_wide: ins is [xf, xi, out_f, out_i] and scalars is
[a, k]. out_f is xf * a and out_i is xi + k, each over the whole
of its buffer's first n lanes. Answers the two buffers written, as the
(position, contents) pairs a WideRefFn answers.