1#[must_use]
33pub fn transpose_wgsl(tile_size: u32) -> String {
34 let padded = tile_size + 1;
35 format!(
36 r#"
37struct TransposeParams {{
38 rows: u32,
39 cols: u32,
40}}
41
42@group(0) @binding(0) var<storage, read> src: array<f32>;
43@group(0) @binding(1) var<storage, read_write> dst: array<f32>;
44@group(0) @binding(2) var<uniform> params: TransposeParams;
45
46// Padded by +1 column to avoid shared-memory bank conflicts.
47var<workgroup> tile: array<array<f32, {padded}>, {ts}>;
48
49@compute @workgroup_size({ts}, {ts})
50fn main(
51 @builtin(workgroup_id) wgid: vec3<u32>,
52 @builtin(local_invocation_id) lid: vec3<u32>,
53) {{
54 let lr = lid.y;
55 let lc = lid.x;
56
57 // Read phase: coalesced load of a tile of the source.
58 let in_r = wgid.y * {ts}u + lr;
59 let in_c = wgid.x * {ts}u + lc;
60 if (in_r < params.rows && in_c < params.cols) {{
61 tile[lr][lc] = src[in_r * params.cols + in_c];
62 }} else {{
63 tile[lr][lc] = 0.0;
64 }}
65 workgroupBarrier();
66
67 // Write phase: transposed coordinates, coalesced store to the destination.
68 let out_r = wgid.x * {ts}u + lr;
69 let out_c = wgid.y * {ts}u + lc;
70 if (out_r < params.cols && out_c < params.rows) {{
71 dst[out_r * params.rows + out_c] = tile[lc][lr];
72 }}
73}}
74"#,
75 ts = tile_size,
76 padded = padded,
77 )
78}
79
80#[must_use]
93pub fn softmax_wgsl() -> String {
94 r#"
95struct SoftmaxParams {
96 rows: u32,
97 cols: u32,
98}
99
100@group(0) @binding(0) var<storage, read> input: array<f32>;
101@group(0) @binding(1) var<storage, read_write> output: array<f32>;
102@group(0) @binding(2) var<uniform> params: SoftmaxParams;
103
104var<workgroup> shared_max: array<f32, 256>;
105var<workgroup> shared_sum: array<f32, 256>;
106
107@compute @workgroup_size(256)
108fn main(
109 @builtin(workgroup_id) wgid: vec3<u32>,
110 @builtin(local_invocation_id) lid: vec3<u32>,
111) {
112 let row = wgid.x;
113 if (row >= params.rows) { return; }
114 let tid = lid.x;
115 let base = row * params.cols;
116
117 // Pass 1: per-thread partial max over a strided slice of the row.
118 var local_max: f32 = f32(-1e38);
119 var i: u32 = tid;
120 loop {
121 if (i >= params.cols) { break; }
122 local_max = max(local_max, input[base + i]);
123 i = i + 256u;
124 }
125 shared_max[tid] = local_max;
126 workgroupBarrier();
127 var stride: u32 = 128u;
128 loop {
129 if (stride == 0u) { break; }
130 if (tid < stride) {
131 shared_max[tid] = max(shared_max[tid], shared_max[tid + stride]);
132 }
133 workgroupBarrier();
134 stride = stride >> 1u;
135 }
136 let row_max = shared_max[0];
137 workgroupBarrier();
138
139 // Pass 2: per-thread partial sum of exp(x - row_max).
140 var local_sum: f32 = 0.0;
141 i = tid;
142 loop {
143 if (i >= params.cols) { break; }
144 local_sum = local_sum + exp(input[base + i] - row_max);
145 i = i + 256u;
146 }
147 shared_sum[tid] = local_sum;
148 workgroupBarrier();
149 stride = 128u;
150 loop {
151 if (stride == 0u) { break; }
152 if (tid < stride) {
153 shared_sum[tid] = shared_sum[tid] + shared_sum[tid + stride];
154 }
155 workgroupBarrier();
156 stride = stride >> 1u;
157 }
158 let row_sum = shared_sum[0];
159 let inv_sum = 1.0 / row_sum;
160 workgroupBarrier();
161
162 // Pass 3: write normalised probabilities.
163 i = tid;
164 loop {
165 if (i >= params.cols) { break; }
166 output[base + i] = exp(input[base + i] - row_max) * inv_sum;
167 i = i + 256u;
168 }
169}
170"#
171 .to_string()
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum ScanKind {
178 Inclusive,
180 Exclusive,
182}
183
184#[must_use]
199pub fn scan_wgsl(block_size: u32, kind: ScanKind) -> String {
200 let threads = (block_size / 2).max(1);
201 let inclusive_fixup = match kind {
203 ScanKind::Inclusive => {
204 " // Inclusive: add the original input back to the exclusive result.\n \
205 output[base + 2u * tid] = shared_data[2u * tid] + input[base + 2u * tid];\n \
206 output[base + 2u * tid + 1u] = shared_data[2u * tid + 1u] + input[base + 2u * tid + 1u];"
207 }
208 ScanKind::Exclusive => {
209 " output[base + 2u * tid] = shared_data[2u * tid];\n \
210 output[base + 2u * tid + 1u] = shared_data[2u * tid + 1u];"
211 }
212 };
213 let kind_comment = match kind {
214 ScanKind::Inclusive => "inclusive",
215 ScanKind::Exclusive => "exclusive",
216 };
217
218 format!(
219 r#"
220// Blelloch work-efficient {kind_comment} prefix scan (block size {bs}).
221struct ScanParams {{
222 n: u32,
223}}
224
225@group(0) @binding(0) var<storage, read> input: array<f32>;
226@group(0) @binding(1) var<storage, read_write> output: array<f32>;
227@group(0) @binding(2) var<uniform> params: ScanParams;
228
229var<workgroup> shared_data: array<f32, {bs}>;
230
231@compute @workgroup_size({threads})
232fn main(
233 @builtin(workgroup_id) wgid: vec3<u32>,
234 @builtin(local_invocation_id) lid: vec3<u32>,
235) {{
236 let tid = lid.x;
237 let base = wgid.x * {bs}u;
238
239 // Load two elements per thread (zero-pad out-of-range).
240 let i0 = 2u * tid;
241 let i1 = 2u * tid + 1u;
242 if (base + i0 < params.n) {{ shared_data[i0] = input[base + i0]; }} else {{ shared_data[i0] = 0.0; }}
243 if (base + i1 < params.n) {{ shared_data[i1] = input[base + i1]; }} else {{ shared_data[i1] = 0.0; }}
244
245 // Up-sweep (reduce) phase.
246 var offset: u32 = 1u;
247 var d: u32 = {bs}u >> 1u;
248 loop {{
249 workgroupBarrier();
250 if (tid < d) {{
251 let ai = offset * (2u * tid + 1u) - 1u;
252 let bi = offset * (2u * tid + 2u) - 1u;
253 shared_data[bi] = shared_data[bi] + shared_data[ai];
254 }}
255 offset = offset << 1u;
256 if (d == 1u) {{ break; }}
257 d = d >> 1u;
258 }}
259
260 // Clear the last element (root) for the exclusive down-sweep.
261 if (tid == 0u) {{ shared_data[{bs}u - 1u] = 0.0; }}
262
263 // Down-sweep phase.
264 d = 1u;
265 loop {{
266 offset = offset >> 1u;
267 workgroupBarrier();
268 if (tid < d) {{
269 let ai = offset * (2u * tid + 1u) - 1u;
270 let bi = offset * (2u * tid + 2u) - 1u;
271 let t = shared_data[ai];
272 shared_data[ai] = shared_data[bi];
273 shared_data[bi] = shared_data[bi] + t;
274 }}
275 if (d == {bs}u >> 1u) {{ break; }}
276 d = d << 1u;
277 }}
278 workgroupBarrier();
279
280 // Write results (exclusive in shared_data; inclusive adds input back).
281 if (base + i0 < params.n) {{
282{inclusive_fixup}
283 }}
284}}
285"#,
286 bs = block_size,
287 threads = threads,
288 kind_comment = kind_comment,
289 inclusive_fixup = inclusive_fixup,
290 )
291}
292
293#[must_use]
308pub fn layernorm_wgsl(eps: f32) -> String {
309 format!(
310 r#"
311struct LayerNormParams {{
312 rows: u32,
313 cols: u32,
314}}
315
316@group(0) @binding(0) var<storage, read> input: array<f32>;
317@group(0) @binding(1) var<storage, read> gamma: array<f32>;
318@group(0) @binding(2) var<storage, read> beta: array<f32>;
319@group(0) @binding(3) var<storage, read_write> output: array<f32>;
320@group(0) @binding(4) var<uniform> params: LayerNormParams;
321
322var<workgroup> shared_acc: array<f32, 256>;
323
324@compute @workgroup_size(256)
325fn main(
326 @builtin(workgroup_id) wgid: vec3<u32>,
327 @builtin(local_invocation_id) lid: vec3<u32>,
328) {{
329 let row = wgid.x;
330 if (row >= params.rows) {{ return; }}
331 let tid = lid.x;
332 let base = row * params.cols;
333 let inv_n = 1.0 / f32(params.cols);
334
335 // Pass 1: mean.
336 var local_sum: f32 = 0.0;
337 var i: u32 = tid;
338 loop {{
339 if (i >= params.cols) {{ break; }}
340 local_sum = local_sum + input[base + i];
341 i = i + 256u;
342 }}
343 shared_acc[tid] = local_sum;
344 workgroupBarrier();
345 var stride: u32 = 128u;
346 loop {{
347 if (stride == 0u) {{ break; }}
348 if (tid < stride) {{
349 shared_acc[tid] = shared_acc[tid] + shared_acc[tid + stride];
350 }}
351 workgroupBarrier();
352 stride = stride >> 1u;
353 }}
354 let mean = shared_acc[0] * inv_n;
355 workgroupBarrier();
356
357 // Pass 2: variance (mean of squared deviations).
358 var local_var: f32 = 0.0;
359 i = tid;
360 loop {{
361 if (i >= params.cols) {{ break; }}
362 let d = input[base + i] - mean;
363 local_var = local_var + d * d;
364 i = i + 256u;
365 }}
366 shared_acc[tid] = local_var;
367 workgroupBarrier();
368 stride = 128u;
369 loop {{
370 if (stride == 0u) {{ break; }}
371 if (tid < stride) {{
372 shared_acc[tid] = shared_acc[tid] + shared_acc[tid + stride];
373 }}
374 workgroupBarrier();
375 stride = stride >> 1u;
376 }}
377 let variance = shared_acc[0] * inv_n;
378 let inv_std = 1.0 / sqrt(variance + f32({eps}));
379 workgroupBarrier();
380
381 // Pass 3: normalise + affine.
382 i = tid;
383 loop {{
384 if (i >= params.cols) {{ break; }}
385 let norm = (input[base + i] - mean) * inv_std;
386 output[base + i] = norm * gamma[i] + beta[i];
387 i = i + 256u;
388 }}
389}}
390"#,
391 eps = eps,
392 )
393}
394
395#[must_use]
416pub fn subgroup_reduction_wgsl(op: &str, chromium_experimental: bool) -> String {
417 let (subgroup_fn, neutral) = match op {
418 "max" => ("subgroupMax", "f32(-1e38)"),
419 "min" => ("subgroupMin", "f32(1e38)"),
420 _ => ("subgroupAdd", "f32(0.0)"),
421 };
422 let combine = match op {
424 "max" => "max(acc, val)",
425 "min" => "min(acc, val)",
426 _ => "acc + val",
427 };
428 let enable = if chromium_experimental {
429 "enable chromium_experimental_subgroups;"
430 } else {
431 "enable subgroups;"
432 };
433
434 format!(
435 r#"
436{enable}
437
438struct SubgroupReduceParams {{
439 n: u32,
440}}
441
442@group(0) @binding(0) var<storage, read> input: array<f32>;
443@group(0) @binding(1) var<storage, read_write> partial_sums: array<f32>;
444@group(0) @binding(2) var<uniform> params: SubgroupReduceParams;
445
446// Up to 256 lanes / min-subgroup-size of 4 = 64 leader slots, padded to 64.
447var<workgroup> leader_vals: array<f32, 64>;
448
449@compute @workgroup_size(256)
450fn main(
451 @builtin(global_invocation_id) gid: vec3<u32>,
452 @builtin(local_invocation_id) lid: vec3<u32>,
453 @builtin(workgroup_id) wgid: vec3<u32>,
454 @builtin(subgroup_invocation_id) sg_id: u32,
455 @builtin(subgroup_size) sg_size: u32,
456) {{
457 let tid = lid.x;
458 var v: f32 = {neutral};
459 if (gid.x < params.n) {{ v = input[gid.x]; }}
460
461 // One built-in call reduces the whole subgroup.
462 let sg_reduced = {subgroup_fn}(v);
463
464 // Subgroup leaders publish their reduced value.
465 let leader_index = tid / sg_size;
466 if (sg_id == 0u) {{
467 leader_vals[leader_index] = sg_reduced;
468 }}
469 workgroupBarrier();
470
471 // Thread 0 folds the leader partials and writes the workgroup result.
472 if (tid == 0u) {{
473 let num_leaders = (256u + sg_size - 1u) / sg_size;
474 var acc: f32 = {neutral};
475 for (var i: u32 = 0u; i < num_leaders; i = i + 1u) {{
476 let val = leader_vals[i];
477 acc = {combine};
478 }}
479 partial_sums[wgid.x] = acc;
480 }}
481}}
482"#,
483 enable = enable,
484 subgroup_fn = subgroup_fn,
485 neutral = neutral,
486 combine = combine,
487 )
488}
489
490#[must_use]
502pub fn f64_emul_add_wgsl() -> String {
503 r#"
504// Double-single (emulated f64) element-wise add. No native FP64 on WebGPU.
505// Each value is a (hi, lo) pair: lo carries the round-off residual of hi.
506struct DfParams {
507 n: u32,
508}
509
510@group(0) @binding(0) var<storage, read> a: array<f32>;
511@group(0) @binding(1) var<storage, read> b: array<f32>;
512@group(0) @binding(2) var<storage, read_write> c: array<f32>;
513@group(0) @binding(3) var<uniform> params: DfParams;
514
515// Knuth TwoSum: returns (s, e) with a + b == s + e exactly (in f32).
516fn two_sum(av: f32, bv: f32) -> vec2<f32> {
517 let s = av + bv;
518 let bb = s - av;
519 let err = (av - (s - bb)) + (bv - bb);
520 return vec2<f32>(s, err);
521}
522
523// Add two double-single numbers (hi, lo) + (hi, lo).
524fn df_add(x: vec2<f32>, y: vec2<f32>) -> vec2<f32> {
525 let sh = two_sum(x.x, y.x);
526 let sl = two_sum(x.y, y.y);
527 var hi = sh.x;
528 var lo = sh.y + sl.x;
529 // Renormalise the high/low split.
530 let r1 = two_sum(hi, lo);
531 hi = r1.x;
532 lo = r1.y + sl.y;
533 let r2 = two_sum(hi, lo);
534 return vec2<f32>(r2.x, r2.y);
535}
536
537@compute @workgroup_size(256)
538fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
539 let i = gid.x;
540 if (i >= params.n) { return; }
541 let av = vec2<f32>(a[2u * i], a[2u * i + 1u]);
542 let bv = vec2<f32>(b[2u * i], b[2u * i + 1u]);
543 let r = df_add(av, bv);
544 c[2u * i] = r.x;
545 c[2u * i + 1u] = r.y;
546}
547"#
548 .to_string()
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
558 fn wgsl_transpose_contains_workgroup() {
559 let src = transpose_wgsl(16);
560 assert!(src.contains("@compute @workgroup_size(16, 16)"));
561 assert!(src.contains("TransposeParams"));
562 }
563
564 #[test]
565 fn wgsl_transpose_padded_tile_avoids_bank_conflict() {
566 let src = transpose_wgsl(16);
567 assert!(src.contains("array<array<f32, 17>, 16>"));
569 }
570
571 #[test]
572 fn wgsl_transpose_swaps_indices() {
573 let src = transpose_wgsl(8);
574 assert!(src.contains("src[in_r * params.cols + in_c]"));
576 assert!(src.contains("dst[out_r * params.rows + out_c]"));
577 assert!(src.contains("tile[lc][lr]"));
579 assert!(src.contains("workgroupBarrier"));
580 }
581
582 #[test]
583 fn wgsl_transpose_has_bounds_guards() {
584 let src = transpose_wgsl(16);
585 assert!(src.contains("in_r < params.rows && in_c < params.cols"));
586 assert!(src.contains("out_r < params.cols && out_c < params.rows"));
587 }
588
589 #[test]
592 fn wgsl_softmax_is_numerically_stable() {
593 let src = softmax_wgsl();
594 assert!(src.contains("input[base + i] - row_max"));
596 assert!(src.contains("exp(input[base + i] - row_max)"));
597 assert!(src.contains("inv_sum"));
599 assert!(src.contains("* inv_sum"));
600 }
601
602 #[test]
603 fn wgsl_softmax_does_not_naively_exp_then_divide_without_max() {
604 let src = softmax_wgsl();
605 assert!(!src.contains("exp(input[base + i])"));
608 }
609
610 #[test]
611 fn wgsl_softmax_bindings_and_workgroup() {
612 let src = softmax_wgsl();
613 assert!(src.contains("@compute @workgroup_size(256)"));
614 assert!(src.contains("var<storage, read> input:"));
615 assert!(src.contains("var<storage, read_write> output:"));
616 assert!(src.contains("var<uniform> params:"));
617 assert!(src.contains("shared_max"));
619 assert!(src.contains("shared_sum"));
620 }
621
622 #[test]
623 fn wgsl_softmax_row_per_workgroup() {
624 let src = softmax_wgsl();
625 assert!(src.contains("let row = wgid.x"));
626 assert!(src.contains("if (row >= params.rows) { return; }"));
627 }
628
629 #[test]
632 fn wgsl_scan_inclusive_adds_input_back() {
633 let src = scan_wgsl(256, ScanKind::Inclusive);
634 assert!(src.contains("inclusive"));
635 assert!(src.contains("shared_data[2u * tid] + input[base + 2u * tid]"));
637 }
638
639 #[test]
640 fn wgsl_scan_exclusive_writes_shared_directly() {
641 let src = scan_wgsl(256, ScanKind::Exclusive);
642 assert!(src.contains("exclusive"));
643 assert!(src.contains("output[base + 2u * tid] = shared_data[2u * tid];"));
644 assert!(!src.contains("shared_data[2u * tid] + input[base + 2u * tid]"));
646 }
647
648 #[test]
649 fn wgsl_scan_has_up_and_down_sweep() {
650 let src = scan_wgsl(512, ScanKind::Inclusive);
651 assert!(src.contains("@compute @workgroup_size(256)"));
653 assert!(src.contains("array<f32, 512>"));
654 assert!(src.contains("shared_data[512u - 1u] = 0.0"));
656 assert!(src.contains("workgroupBarrier"));
657 }
658
659 #[test]
660 fn wgsl_scan_block_size_64() {
661 let src = scan_wgsl(64, ScanKind::Exclusive);
662 assert!(src.contains("@compute @workgroup_size(32)"));
663 assert!(src.contains("array<f32, 64>"));
664 }
665
666 #[test]
669 fn wgsl_layernorm_centers_and_scales() {
670 let src = layernorm_wgsl(1e-5);
671 assert!(src.contains("input[base + i] - mean"));
673 assert!(src.contains("sqrt(variance + f32(0.00001"));
674 assert!(src.contains("norm * gamma[i] + beta[i]"));
676 }
677
678 #[test]
679 fn wgsl_layernorm_variance_is_mean_of_squared_dev() {
680 let src = layernorm_wgsl(1e-5);
681 assert!(src.contains("let d = input[base + i] - mean;"));
682 assert!(src.contains("local_var = local_var + d * d;"));
683 assert!(src.contains("let variance = shared_acc[0] * inv_n;"));
684 }
685
686 #[test]
687 fn wgsl_layernorm_bindings() {
688 let src = layernorm_wgsl(1e-6);
689 assert!(src.contains("@group(0) @binding(0) var<storage, read> input:"));
690 assert!(src.contains("@group(0) @binding(1) var<storage, read> gamma:"));
691 assert!(src.contains("@group(0) @binding(2) var<storage, read> beta:"));
692 assert!(src.contains("@group(0) @binding(3) var<storage, read_write> output:"));
693 assert!(src.contains("@group(0) @binding(4) var<uniform> params:"));
694 assert!(src.contains("@compute @workgroup_size(256)"));
695 }
696
697 #[test]
698 fn wgsl_layernorm_embeds_eps() {
699 assert!(layernorm_wgsl(0.001).contains("0.001"));
701 }
702
703 #[test]
706 fn wgsl_subgroup_sum_uses_subgroup_add() {
707 let src = subgroup_reduction_wgsl("sum", false);
708 assert!(src.contains("enable subgroups;"));
709 assert!(src.contains("subgroupAdd(v)"));
710 assert!(src.contains("acc + val"));
711 }
712
713 #[test]
714 fn wgsl_subgroup_max_uses_subgroup_max() {
715 let src = subgroup_reduction_wgsl("max", false);
716 assert!(src.contains("subgroupMax(v)"));
717 assert!(src.contains("max(acc, val)"));
718 assert!(src.contains("f32(-1e38)"));
719 }
720
721 #[test]
722 fn wgsl_subgroup_min_uses_subgroup_min() {
723 let src = subgroup_reduction_wgsl("min", false);
724 assert!(src.contains("subgroupMin(v)"));
725 assert!(src.contains("min(acc, val)"));
726 }
727
728 #[test]
729 fn wgsl_subgroup_chromium_experimental_directive() {
730 let std_src = subgroup_reduction_wgsl("sum", false);
731 assert!(std_src.contains("enable subgroups;"));
732 assert!(!std_src.contains("chromium_experimental"));
733
734 let exp_src = subgroup_reduction_wgsl("sum", true);
735 assert!(exp_src.contains("enable chromium_experimental_subgroups;"));
736 }
737
738 #[test]
739 fn wgsl_subgroup_uses_subgroup_builtins() {
740 let src = subgroup_reduction_wgsl("sum", false);
741 assert!(src.contains("@builtin(subgroup_invocation_id)"));
742 assert!(src.contains("@builtin(subgroup_size)"));
743 assert!(src.contains("@compute @workgroup_size(256)"));
744 }
745
746 #[test]
749 fn wgsl_f64_emul_uses_double_single() {
750 let src = f64_emul_add_wgsl();
751 assert!(src.contains("vec2<f32>"));
753 assert!(src.contains("fn two_sum"));
755 assert!(src.contains("fn df_add"));
756 assert!(src.contains("a[2u * i]"));
758 assert!(src.contains("a[2u * i + 1u]"));
759 }
760
761 #[test]
762 fn wgsl_f64_emul_two_sum_is_error_free() {
763 let src = f64_emul_add_wgsl();
764 assert!(src.contains("let bb = s - av;"));
766 assert!(src.contains("(av - (s - bb)) + (bv - bb)"));
767 }
768
769 #[test]
770 fn wgsl_f64_emul_bindings_and_guard() {
771 let src = f64_emul_add_wgsl();
772 assert!(src.contains("@compute @workgroup_size(256)"));
773 assert!(src.contains("if (i >= params.n) { return; }"));
774 assert!(src.contains("var<storage, read_write> c:"));
775 }
776}