Layer NormalizationΒΆ
Layer normalization computes a mean and variance for each row, then applies
per-column scale and bias:
output = (input - mean) / sqrt(variance + epsilon) * weight + bias.
The variance divides by the row width, as in numpy.var with its default
ddof=0.
import numpy as np
import metile
@metile.kernel
def layernorm(
input_ptr, weight_ptr, bias_ptr, output_ptr,
rows, columns, epsilon, BLOCK: metile.constexpr,
):
inputs = metile.tensor(input_ptr, shape=(rows, columns), access="read")
weights = metile.tensor(weight_ptr, shape=(columns,), access="read")
biases = metile.tensor(bias_ptr, shape=(columns,), access="read")
outputs = metile.tensor(output_ptr, shape=(rows, columns), access="write")
row = metile.program_id(0)
local_sum = 0.0
for start in metile.tile_range(0, columns, BLOCK):
positions = start + metile.arange(0, BLOCK)
local_sum = local_sum + inputs.load((row, positions))
mean = metile.sum(local_sum) / columns
local_variance = 0.0
for start in metile.tile_range(0, columns, BLOCK):
positions = start + metile.arange(0, BLOCK)
values = inputs.load((row, positions))
difference = metile.where(positions < columns, values - mean, 0.0)
local_variance = local_variance + difference * difference
variance = metile.sum(local_variance) / columns
inverse_std = 1.0 / metile.sqrt(variance + epsilon)
for start in metile.tile_range(0, columns, BLOCK):
positions = start + metile.arange(0, BLOCK)
values = inputs.load((row, positions))
normalized = (values - mean) * inverse_std
result = normalized * weights.load((positions,)) + biases.load((positions,))
outputs.store((row, positions), result)
rows, columns = 8, 300
epsilon = 1e-5
rng = np.random.default_rng(0)
data = rng.standard_normal((rows, columns)).astype(np.float32) + 3.0
weight_data = rng.standard_normal(columns).astype(np.float32)
bias_data = rng.standard_normal(columns).astype(np.float32)
input_buffer = metile.Buffer(data=data)
weight_buffer = metile.Buffer(data=weight_data)
bias_buffer = metile.Buffer(data=bias_data)
output_buffer = metile.Buffer.zeros(data.shape, dtype=np.float32)
layernorm[(rows,)](
input_buffer, weight_buffer, bias_buffer, output_buffer,
rows, columns, epsilon, BLOCK=256,
)
mean = data.mean(axis=1, keepdims=True)
variance = data.var(axis=1, keepdims=True)
reference = (data - mean) / np.sqrt(variance + epsilon) * weight_data + bias_data
np.testing.assert_allclose(output_buffer.numpy(), reference, rtol=1e-4, atol=1e-5)
Each program handles one row. The three passes keep statistics inside the
kernel but reread the input from device memory. Computing variance from
centered values avoids the subtraction of large, nearly equal quantities in
mean(x * x) - mean(x) ** 2.
The variance pass needs its explicit mask. A padded load returns zero, but
subtracting the mean would give it a nonzero squared difference. where
removes that contribution before reduction. The 300-column example checks
this case with a nonzero input mean.
Use contiguous float32 inputs, positive dimensions, and a positive epsilon.
Weights and biases each contain columns elements and apply to every row.
Floating-point reductions may differ slightly from NumPy because they add
values in a different order.