#[must_use]
pub fn transpose_wgsl(tile_size: u32) -> String {
let padded = tile_size + 1;
format!(
r#"
struct TransposeParams {{
rows: u32,
cols: u32,
}}
@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<uniform> params: TransposeParams;
// Padded by +1 column to avoid shared-memory bank conflicts.
var<workgroup> tile: array<array<f32, {padded}>, {ts}>;
@compute @workgroup_size({ts}, {ts})
fn main(
@builtin(workgroup_id) wgid: vec3<u32>,
@builtin(local_invocation_id) lid: vec3<u32>,
) {{
let lr = lid.y;
let lc = lid.x;
// Read phase: coalesced load of a tile of the source.
let in_r = wgid.y * {ts}u + lr;
let in_c = wgid.x * {ts}u + lc;
if (in_r < params.rows && in_c < params.cols) {{
tile[lr][lc] = src[in_r * params.cols + in_c];
}} else {{
tile[lr][lc] = 0.0;
}}
workgroupBarrier();
// Write phase: transposed coordinates, coalesced store to the destination.
let out_r = wgid.x * {ts}u + lr;
let out_c = wgid.y * {ts}u + lc;
if (out_r < params.cols && out_c < params.rows) {{
dst[out_r * params.rows + out_c] = tile[lc][lr];
}}
}}
"#,
ts = tile_size,
padded = padded,
)
}
#[must_use]
pub fn softmax_wgsl() -> String {
r#"
struct SoftmaxParams {
rows: u32,
cols: u32,
}
@group(0) @binding(0) var<storage, read> input: array<f32>;
@group(0) @binding(1) var<storage, read_write> output: array<f32>;
@group(0) @binding(2) var<uniform> params: SoftmaxParams;
var<workgroup> shared_max: array<f32, 256>;
var<workgroup> shared_sum: array<f32, 256>;
@compute @workgroup_size(256)
fn main(
@builtin(workgroup_id) wgid: vec3<u32>,
@builtin(local_invocation_id) lid: vec3<u32>,
) {
let row = wgid.x;
if (row >= params.rows) { return; }
let tid = lid.x;
let base = row * params.cols;
// Pass 1: per-thread partial max over a strided slice of the row.
var local_max: f32 = f32(-1e38);
var i: u32 = tid;
loop {
if (i >= params.cols) { break; }
local_max = max(local_max, input[base + i]);
i = i + 256u;
}
shared_max[tid] = local_max;
workgroupBarrier();
var stride: u32 = 128u;
loop {
if (stride == 0u) { break; }
if (tid < stride) {
shared_max[tid] = max(shared_max[tid], shared_max[tid + stride]);
}
workgroupBarrier();
stride = stride >> 1u;
}
let row_max = shared_max[0];
workgroupBarrier();
// Pass 2: per-thread partial sum of exp(x - row_max).
var local_sum: f32 = 0.0;
i = tid;
loop {
if (i >= params.cols) { break; }
local_sum = local_sum + exp(input[base + i] - row_max);
i = i + 256u;
}
shared_sum[tid] = local_sum;
workgroupBarrier();
stride = 128u;
loop {
if (stride == 0u) { break; }
if (tid < stride) {
shared_sum[tid] = shared_sum[tid] + shared_sum[tid + stride];
}
workgroupBarrier();
stride = stride >> 1u;
}
let row_sum = shared_sum[0];
let inv_sum = 1.0 / row_sum;
workgroupBarrier();
// Pass 3: write normalised probabilities.
i = tid;
loop {
if (i >= params.cols) { break; }
output[base + i] = exp(input[base + i] - row_max) * inv_sum;
i = i + 256u;
}
}
"#
.to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanKind {
Inclusive,
Exclusive,
}
#[must_use]
pub fn scan_wgsl(block_size: u32, kind: ScanKind) -> String {
let threads = (block_size / 2).max(1);
let inclusive_fixup = match kind {
ScanKind::Inclusive => {
" // Inclusive: add the original input back to the exclusive result.\n \
output[base + 2u * tid] = shared_data[2u * tid] + input[base + 2u * tid];\n \
output[base + 2u * tid + 1u] = shared_data[2u * tid + 1u] + input[base + 2u * tid + 1u];"
}
ScanKind::Exclusive => {
" output[base + 2u * tid] = shared_data[2u * tid];\n \
output[base + 2u * tid + 1u] = shared_data[2u * tid + 1u];"
}
};
let kind_comment = match kind {
ScanKind::Inclusive => "inclusive",
ScanKind::Exclusive => "exclusive",
};
format!(
r#"
// Blelloch work-efficient {kind_comment} prefix scan (block size {bs}).
struct ScanParams {{
n: u32,
}}
@group(0) @binding(0) var<storage, read> input: array<f32>;
@group(0) @binding(1) var<storage, read_write> output: array<f32>;
@group(0) @binding(2) var<uniform> params: ScanParams;
var<workgroup> shared_data: array<f32, {bs}>;
@compute @workgroup_size({threads})
fn main(
@builtin(workgroup_id) wgid: vec3<u32>,
@builtin(local_invocation_id) lid: vec3<u32>,
) {{
let tid = lid.x;
let base = wgid.x * {bs}u;
// Load two elements per thread (zero-pad out-of-range).
let i0 = 2u * tid;
let i1 = 2u * tid + 1u;
if (base + i0 < params.n) {{ shared_data[i0] = input[base + i0]; }} else {{ shared_data[i0] = 0.0; }}
if (base + i1 < params.n) {{ shared_data[i1] = input[base + i1]; }} else {{ shared_data[i1] = 0.0; }}
// Up-sweep (reduce) phase.
var offset: u32 = 1u;
var d: u32 = {bs}u >> 1u;
loop {{
workgroupBarrier();
if (tid < d) {{
let ai = offset * (2u * tid + 1u) - 1u;
let bi = offset * (2u * tid + 2u) - 1u;
shared_data[bi] = shared_data[bi] + shared_data[ai];
}}
offset = offset << 1u;
if (d == 1u) {{ break; }}
d = d >> 1u;
}}
// Clear the last element (root) for the exclusive down-sweep.
if (tid == 0u) {{ shared_data[{bs}u - 1u] = 0.0; }}
// Down-sweep phase.
d = 1u;
loop {{
offset = offset >> 1u;
workgroupBarrier();
if (tid < d) {{
let ai = offset * (2u * tid + 1u) - 1u;
let bi = offset * (2u * tid + 2u) - 1u;
let t = shared_data[ai];
shared_data[ai] = shared_data[bi];
shared_data[bi] = shared_data[bi] + t;
}}
if (d == {bs}u >> 1u) {{ break; }}
d = d << 1u;
}}
workgroupBarrier();
// Write results (exclusive in shared_data; inclusive adds input back).
if (base + i0 < params.n) {{
{inclusive_fixup}
}}
}}
"#,
bs = block_size,
threads = threads,
kind_comment = kind_comment,
inclusive_fixup = inclusive_fixup,
)
}
#[must_use]
pub fn layernorm_wgsl(eps: f32) -> String {
format!(
r#"
struct LayerNormParams {{
rows: u32,
cols: u32,
}}
@group(0) @binding(0) var<storage, read> input: array<f32>;
@group(0) @binding(1) var<storage, read> gamma: array<f32>;
@group(0) @binding(2) var<storage, read> beta: array<f32>;
@group(0) @binding(3) var<storage, read_write> output: array<f32>;
@group(0) @binding(4) var<uniform> params: LayerNormParams;
var<workgroup> shared_acc: array<f32, 256>;
@compute @workgroup_size(256)
fn main(
@builtin(workgroup_id) wgid: vec3<u32>,
@builtin(local_invocation_id) lid: vec3<u32>,
) {{
let row = wgid.x;
if (row >= params.rows) {{ return; }}
let tid = lid.x;
let base = row * params.cols;
let inv_n = 1.0 / f32(params.cols);
// Pass 1: mean.
var local_sum: f32 = 0.0;
var i: u32 = tid;
loop {{
if (i >= params.cols) {{ break; }}
local_sum = local_sum + input[base + i];
i = i + 256u;
}}
shared_acc[tid] = local_sum;
workgroupBarrier();
var stride: u32 = 128u;
loop {{
if (stride == 0u) {{ break; }}
if (tid < stride) {{
shared_acc[tid] = shared_acc[tid] + shared_acc[tid + stride];
}}
workgroupBarrier();
stride = stride >> 1u;
}}
let mean = shared_acc[0] * inv_n;
workgroupBarrier();
// Pass 2: variance (mean of squared deviations).
var local_var: f32 = 0.0;
i = tid;
loop {{
if (i >= params.cols) {{ break; }}
let d = input[base + i] - mean;
local_var = local_var + d * d;
i = i + 256u;
}}
shared_acc[tid] = local_var;
workgroupBarrier();
stride = 128u;
loop {{
if (stride == 0u) {{ break; }}
if (tid < stride) {{
shared_acc[tid] = shared_acc[tid] + shared_acc[tid + stride];
}}
workgroupBarrier();
stride = stride >> 1u;
}}
let variance = shared_acc[0] * inv_n;
let inv_std = 1.0 / sqrt(variance + f32({eps}));
workgroupBarrier();
// Pass 3: normalise + affine.
i = tid;
loop {{
if (i >= params.cols) {{ break; }}
let norm = (input[base + i] - mean) * inv_std;
output[base + i] = norm * gamma[i] + beta[i];
i = i + 256u;
}}
}}
"#,
eps = eps,
)
}
#[must_use]
pub fn subgroup_reduction_wgsl(op: &str, chromium_experimental: bool) -> String {
let (subgroup_fn, neutral) = match op {
"max" => ("subgroupMax", "f32(-1e38)"),
"min" => ("subgroupMin", "f32(1e38)"),
_ => ("subgroupAdd", "f32(0.0)"),
};
let combine = match op {
"max" => "max(acc, val)",
"min" => "min(acc, val)",
_ => "acc + val",
};
let enable = if chromium_experimental {
"enable chromium_experimental_subgroups;"
} else {
"enable subgroups;"
};
format!(
r#"
{enable}
struct SubgroupReduceParams {{
n: u32,
}}
@group(0) @binding(0) var<storage, read> input: array<f32>;
@group(0) @binding(1) var<storage, read_write> partial_sums: array<f32>;
@group(0) @binding(2) var<uniform> params: SubgroupReduceParams;
// Up to 256 lanes / min-subgroup-size of 4 = 64 leader slots, padded to 64.
var<workgroup> leader_vals: array<f32, 64>;
@compute @workgroup_size(256)
fn main(
@builtin(global_invocation_id) gid: vec3<u32>,
@builtin(local_invocation_id) lid: vec3<u32>,
@builtin(workgroup_id) wgid: vec3<u32>,
@builtin(subgroup_invocation_id) sg_id: u32,
@builtin(subgroup_size) sg_size: u32,
) {{
let tid = lid.x;
var v: f32 = {neutral};
if (gid.x < params.n) {{ v = input[gid.x]; }}
// One built-in call reduces the whole subgroup.
let sg_reduced = {subgroup_fn}(v);
// Subgroup leaders publish their reduced value.
let leader_index = tid / sg_size;
if (sg_id == 0u) {{
leader_vals[leader_index] = sg_reduced;
}}
workgroupBarrier();
// Thread 0 folds the leader partials and writes the workgroup result.
if (tid == 0u) {{
let num_leaders = (256u + sg_size - 1u) / sg_size;
var acc: f32 = {neutral};
for (var i: u32 = 0u; i < num_leaders; i = i + 1u) {{
let val = leader_vals[i];
acc = {combine};
}}
partial_sums[wgid.x] = acc;
}}
}}
"#,
enable = enable,
subgroup_fn = subgroup_fn,
neutral = neutral,
combine = combine,
)
}
#[must_use]
pub fn f64_emul_add_wgsl() -> String {
r#"
// Double-single (emulated f64) element-wise add. No native FP64 on WebGPU.
// Each value is a (hi, lo) pair: lo carries the round-off residual of hi.
struct DfParams {
n: u32,
}
@group(0) @binding(0) var<storage, read> a: array<f32>;
@group(0) @binding(1) var<storage, read> b: array<f32>;
@group(0) @binding(2) var<storage, read_write> c: array<f32>;
@group(0) @binding(3) var<uniform> params: DfParams;
// Knuth TwoSum: returns (s, e) with a + b == s + e exactly (in f32).
fn two_sum(av: f32, bv: f32) -> vec2<f32> {
let s = av + bv;
let bb = s - av;
let err = (av - (s - bb)) + (bv - bb);
return vec2<f32>(s, err);
}
// Add two double-single numbers (hi, lo) + (hi, lo).
fn df_add(x: vec2<f32>, y: vec2<f32>) -> vec2<f32> {
let sh = two_sum(x.x, y.x);
let sl = two_sum(x.y, y.y);
var hi = sh.x;
var lo = sh.y + sl.x;
// Renormalise the high/low split.
let r1 = two_sum(hi, lo);
hi = r1.x;
lo = r1.y + sl.y;
let r2 = two_sum(hi, lo);
return vec2<f32>(r2.x, r2.y);
}
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let i = gid.x;
if (i >= params.n) { return; }
let av = vec2<f32>(a[2u * i], a[2u * i + 1u]);
let bv = vec2<f32>(b[2u * i], b[2u * i + 1u]);
let r = df_add(av, bv);
c[2u * i] = r.x;
c[2u * i + 1u] = r.y;
}
"#
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wgsl_transpose_contains_workgroup() {
let src = transpose_wgsl(16);
assert!(src.contains("@compute @workgroup_size(16, 16)"));
assert!(src.contains("TransposeParams"));
}
#[test]
fn wgsl_transpose_padded_tile_avoids_bank_conflict() {
let src = transpose_wgsl(16);
assert!(src.contains("array<array<f32, 17>, 16>"));
}
#[test]
fn wgsl_transpose_swaps_indices() {
let src = transpose_wgsl(8);
assert!(src.contains("src[in_r * params.cols + in_c]"));
assert!(src.contains("dst[out_r * params.rows + out_c]"));
assert!(src.contains("tile[lc][lr]"));
assert!(src.contains("workgroupBarrier"));
}
#[test]
fn wgsl_transpose_has_bounds_guards() {
let src = transpose_wgsl(16);
assert!(src.contains("in_r < params.rows && in_c < params.cols"));
assert!(src.contains("out_r < params.cols && out_c < params.rows"));
}
#[test]
fn wgsl_softmax_is_numerically_stable() {
let src = softmax_wgsl();
assert!(src.contains("input[base + i] - row_max"));
assert!(src.contains("exp(input[base + i] - row_max)"));
assert!(src.contains("inv_sum"));
assert!(src.contains("* inv_sum"));
}
#[test]
fn wgsl_softmax_does_not_naively_exp_then_divide_without_max() {
let src = softmax_wgsl();
assert!(!src.contains("exp(input[base + i])"));
}
#[test]
fn wgsl_softmax_bindings_and_workgroup() {
let src = softmax_wgsl();
assert!(src.contains("@compute @workgroup_size(256)"));
assert!(src.contains("var<storage, read> input:"));
assert!(src.contains("var<storage, read_write> output:"));
assert!(src.contains("var<uniform> params:"));
assert!(src.contains("shared_max"));
assert!(src.contains("shared_sum"));
}
#[test]
fn wgsl_softmax_row_per_workgroup() {
let src = softmax_wgsl();
assert!(src.contains("let row = wgid.x"));
assert!(src.contains("if (row >= params.rows) { return; }"));
}
#[test]
fn wgsl_scan_inclusive_adds_input_back() {
let src = scan_wgsl(256, ScanKind::Inclusive);
assert!(src.contains("inclusive"));
assert!(src.contains("shared_data[2u * tid] + input[base + 2u * tid]"));
}
#[test]
fn wgsl_scan_exclusive_writes_shared_directly() {
let src = scan_wgsl(256, ScanKind::Exclusive);
assert!(src.contains("exclusive"));
assert!(src.contains("output[base + 2u * tid] = shared_data[2u * tid];"));
assert!(!src.contains("shared_data[2u * tid] + input[base + 2u * tid]"));
}
#[test]
fn wgsl_scan_has_up_and_down_sweep() {
let src = scan_wgsl(512, ScanKind::Inclusive);
assert!(src.contains("@compute @workgroup_size(256)"));
assert!(src.contains("array<f32, 512>"));
assert!(src.contains("shared_data[512u - 1u] = 0.0"));
assert!(src.contains("workgroupBarrier"));
}
#[test]
fn wgsl_scan_block_size_64() {
let src = scan_wgsl(64, ScanKind::Exclusive);
assert!(src.contains("@compute @workgroup_size(32)"));
assert!(src.contains("array<f32, 64>"));
}
#[test]
fn wgsl_layernorm_centers_and_scales() {
let src = layernorm_wgsl(1e-5);
assert!(src.contains("input[base + i] - mean"));
assert!(src.contains("sqrt(variance + f32(0.00001"));
assert!(src.contains("norm * gamma[i] + beta[i]"));
}
#[test]
fn wgsl_layernorm_variance_is_mean_of_squared_dev() {
let src = layernorm_wgsl(1e-5);
assert!(src.contains("let d = input[base + i] - mean;"));
assert!(src.contains("local_var = local_var + d * d;"));
assert!(src.contains("let variance = shared_acc[0] * inv_n;"));
}
#[test]
fn wgsl_layernorm_bindings() {
let src = layernorm_wgsl(1e-6);
assert!(src.contains("@group(0) @binding(0) var<storage, read> input:"));
assert!(src.contains("@group(0) @binding(1) var<storage, read> gamma:"));
assert!(src.contains("@group(0) @binding(2) var<storage, read> beta:"));
assert!(src.contains("@group(0) @binding(3) var<storage, read_write> output:"));
assert!(src.contains("@group(0) @binding(4) var<uniform> params:"));
assert!(src.contains("@compute @workgroup_size(256)"));
}
#[test]
fn wgsl_layernorm_embeds_eps() {
assert!(layernorm_wgsl(0.001).contains("0.001"));
}
#[test]
fn wgsl_subgroup_sum_uses_subgroup_add() {
let src = subgroup_reduction_wgsl("sum", false);
assert!(src.contains("enable subgroups;"));
assert!(src.contains("subgroupAdd(v)"));
assert!(src.contains("acc + val"));
}
#[test]
fn wgsl_subgroup_max_uses_subgroup_max() {
let src = subgroup_reduction_wgsl("max", false);
assert!(src.contains("subgroupMax(v)"));
assert!(src.contains("max(acc, val)"));
assert!(src.contains("f32(-1e38)"));
}
#[test]
fn wgsl_subgroup_min_uses_subgroup_min() {
let src = subgroup_reduction_wgsl("min", false);
assert!(src.contains("subgroupMin(v)"));
assert!(src.contains("min(acc, val)"));
}
#[test]
fn wgsl_subgroup_chromium_experimental_directive() {
let std_src = subgroup_reduction_wgsl("sum", false);
assert!(std_src.contains("enable subgroups;"));
assert!(!std_src.contains("chromium_experimental"));
let exp_src = subgroup_reduction_wgsl("sum", true);
assert!(exp_src.contains("enable chromium_experimental_subgroups;"));
}
#[test]
fn wgsl_subgroup_uses_subgroup_builtins() {
let src = subgroup_reduction_wgsl("sum", false);
assert!(src.contains("@builtin(subgroup_invocation_id)"));
assert!(src.contains("@builtin(subgroup_size)"));
assert!(src.contains("@compute @workgroup_size(256)"));
}
#[test]
fn wgsl_f64_emul_uses_double_single() {
let src = f64_emul_add_wgsl();
assert!(src.contains("vec2<f32>"));
assert!(src.contains("fn two_sum"));
assert!(src.contains("fn df_add"));
assert!(src.contains("a[2u * i]"));
assert!(src.contains("a[2u * i + 1u]"));
}
#[test]
fn wgsl_f64_emul_two_sum_is_error_free() {
let src = f64_emul_add_wgsl();
assert!(src.contains("let bb = s - av;"));
assert!(src.contains("(av - (s - bb)) + (bv - bb)"));
}
#[test]
fn wgsl_f64_emul_bindings_and_guard() {
let src = f64_emul_add_wgsl();
assert!(src.contains("@compute @workgroup_size(256)"));
assert!(src.contains("if (i >= params.n) { return; }"));
assert!(src.contains("var<storage, read_write> c:"));
}
}