cera 0.2.2

Rust-native LLM inference engine
Documentation
// Batched affine LayerNorm: process N independent vectors in one dispatch.
// Each workgroup handles one vector (one token row).
//   dst[i] = (src[i] - mean) * inv_std * weight[i] + bias[i]
//   mean   = (1/n) Σ src
//   var    = (1/n) Σ (src - mean)^2          (population variance)
//   inv_std = 1 / sqrt(var + eps)
//
// Mirrors `cpu::layer_norm_inplace` (mean-centered LayerNorm with an explicit
// bias term — distinct from RMSNorm). The CPU reference accumulates mean/var in
// f64; the GPU does two f32 reduction passes (sum, then sum of squared
// deviations), which is numerically close enough for the encoder's tolerance.
//
// Dispatch: (N, 1, 1) workgroups of 256 threads.
//
// Bind group 0:
//   @binding(0) src: array<f32>     (read — input rows, stride = params.z)
//   @binding(1) dst: array<f32>     (read-write — normalized rows, stride = params.w)
//   @binding(2) weight: array<f32>  (read — per-element scale, length n)
//   @binding(3) bias: array<f32>    (read — per-element shift, length n)
//   @binding(4) params: vec4<u32>   (n, eps_bits, src_stride, dst_stride)

#define WG_SUM_REDUCE
#include "common_decls.tmpl"

@group(0) @binding(0) var<storage, read> src: array<f32>;
@group(0) @binding(1) var<storage, read_write> dst: array<f32>;
@group(0) @binding(2) var<storage, read> weight: array<f32>;
@group(0) @binding(3) var<storage, read> bias: array<f32>;
@group(0) @binding(4) var<storage, read> params: vec4<u32>;

var<workgroup> shared_sum: array<f32, 256>;
var<workgroup> shared_mean: f32;

@compute @workgroup_size(256, 1, 1)
fn layernorm_batch(
    @builtin(local_invocation_id) lid: vec3<u32>,
    @builtin(workgroup_id) wid: vec3<u32>,
) {
    let tid = lid.x;
    let n = params.x;
    let eps = bitcast<f32>(params.y);
    let src_off = wid.x * params.z;
    let dst_off = wid.x * params.w;

    // Pass 1: mean = (1/n) Σ src.
    var partial: f32 = 0.0;
    var i = tid;
    while i < n {
        partial += src[src_off + i];
        i += 256u;
    }
    shared_sum[tid] = partial;
    workgroupBarrier();
    workgroup_sum_reduce(tid);
    if tid == 0u {
        shared_mean = shared_sum[0] / f32(n);
    }
    workgroupBarrier();
    let mean = shared_mean;

    // Pass 2: var = (1/n) Σ (src - mean)^2.
    // (No barrier needed here — the one after `shared_mean` is written already
    // separates the mean read from the pass-2 `shared_sum` writes below.)
    partial = 0.0;
    i = tid;
    while i < n {
        let d = src[src_off + i] - mean;
        partial += d * d;
        i += 256u;
    }
    shared_sum[tid] = partial;
    workgroupBarrier();
    workgroup_sum_reduce(tid);
    let inv_std = 1.0 / sqrt(shared_sum[0] / f32(n) + eps);

    // Pass 3: affine normalize.
    i = tid;
    while i < n {
        dst[dst_off + i] = (src[src_off + i] - mean) * inv_std * weight[i] + bias[i];
        i += 256u;
    }
}