1pub fn gemm_wgsl(tile_size: u32) -> String {
38 format!(
39 r#"
40struct GemmParams {{
41 m: u32,
42 n: u32,
43 k: u32,
44 alpha: f32,
45 beta: f32,
46 trans_a: u32,
47 trans_b: u32,
48 lda: u32,
49 ldb: u32,
50 ldc: u32,
51 _pad0: u32,
52 _pad1: u32,
53}}
54
55@group(0) @binding(0) var<storage, read> a: array<f32>;
56@group(0) @binding(1) var<storage, read> b: array<f32>;
57@group(0) @binding(2) var<storage, read_write> c: array<f32>;
58@group(0) @binding(3) var<uniform> params: GemmParams;
59
60var<workgroup> tile_a: array<array<f32, {ts}>, {ts}>;
61var<workgroup> tile_b: array<array<f32, {ts}>, {ts}>;
62
63// op(A)[r, i] — logical m×k left operand. `lda` is the physical row stride of
64// the stored buffer (>= the packed width), supporting padded / sub-matrix views.
65fn load_a(r: u32, i: u32) -> f32 {{
66 if (r >= params.m || i >= params.k) {{ return 0.0; }}
67 if (params.trans_a == 0u) {{
68 return a[r * params.lda + i];
69 }}
70 return a[i * params.lda + r];
71}}
72
73// op(B)[i, col] — logical k×n right operand. `ldb` is the physical row stride.
74fn load_b(i: u32, col: u32) -> f32 {{
75 if (i >= params.k || col >= params.n) {{ return 0.0; }}
76 if (params.trans_b == 0u) {{
77 return b[i * params.ldb + col];
78 }}
79 return b[col * params.ldb + i];
80}}
81
82@compute @workgroup_size({ts}, {ts})
83fn main(
84 @builtin(global_invocation_id) gid: vec3<u32>,
85 @builtin(local_invocation_id) lid: vec3<u32>,
86) {{
87 let row = gid.y;
88 let col = gid.x;
89 let lr = lid.y;
90 let lc = lid.x;
91
92 var acc: f32 = 0.0;
93 let num_tiles = (params.k + {ts}u - 1u) / {ts}u;
94 for (var t: u32 = 0u; t < num_tiles; t = t + 1u) {{
95 let a_col = t * {ts}u + lc;
96 let b_row = t * {ts}u + lr;
97 tile_a[lr][lc] = load_a(row, a_col);
98 tile_b[lr][lc] = load_b(b_row, col);
99 workgroupBarrier();
100
101 for (var e: u32 = 0u; e < {ts}u; e = e + 1u) {{
102 acc += tile_a[lr][e] * tile_b[e][lc];
103 }}
104 workgroupBarrier();
105 }}
106
107 if (row >= params.m || col >= params.n) {{ return; }}
108 let idx = row * params.ldc + col;
109 c[idx] = params.alpha * acc + params.beta * c[idx];
110}}
111"#,
112 ts = tile_size
113 )
114}
115
116pub fn batched_gemm_wgsl(tile_size: u32) -> String {
134 format!(
135 r#"
136struct BatchedGemmParams {{
137 m: u32,
138 n: u32,
139 k: u32,
140 alpha: f32,
141 beta: f32,
142 batch_count: u32,
143 stride_a: u32,
144 stride_b: u32,
145 stride_c: u32,
146 trans_a: u32,
147 trans_b: u32,
148 lda: u32,
149 ldb: u32,
150 ldc: u32,
151 _pad0: u32,
152 _pad1: u32,
153}}
154
155@group(0) @binding(0) var<storage, read> a: array<f32>;
156@group(0) @binding(1) var<storage, read> b: array<f32>;
157@group(0) @binding(2) var<storage, read_write> c: array<f32>;
158@group(0) @binding(3) var<uniform> params: BatchedGemmParams;
159
160var<workgroup> tile_a: array<array<f32, {ts}>, {ts}>;
161var<workgroup> tile_b: array<array<f32, {ts}>, {ts}>;
162
163// op(A_b)[r, i] — logical m×k left operand for batch `a_offset`. `lda` is the
164// physical per-batch row stride of the stored buffer (>= the packed width).
165fn load_a(a_offset: u32, r: u32, i: u32) -> f32 {{
166 if (r >= params.m || i >= params.k) {{ return 0.0; }}
167 if (params.trans_a == 0u) {{
168 return a[a_offset + r * params.lda + i];
169 }}
170 return a[a_offset + i * params.lda + r];
171}}
172
173// op(B_b)[i, col] — logical k×n right operand for batch `b_offset`. `ldb` is
174// the physical per-batch row stride.
175fn load_b(b_offset: u32, i: u32, col: u32) -> f32 {{
176 if (i >= params.k || col >= params.n) {{ return 0.0; }}
177 if (params.trans_b == 0u) {{
178 return b[b_offset + i * params.ldb + col];
179 }}
180 return b[b_offset + col * params.ldb + i];
181}}
182
183@compute @workgroup_size({ts}, {ts})
184fn main(
185 @builtin(global_invocation_id) gid: vec3<u32>,
186 @builtin(local_invocation_id) lid: vec3<u32>,
187) {{
188 let row = gid.y;
189 let col = gid.x;
190 let batch_index = gid.z;
191 let lr = lid.y;
192 let lc = lid.x;
193 if (batch_index >= params.batch_count) {{ return; }}
194
195 let a_offset = batch_index * params.stride_a;
196 let b_offset = batch_index * params.stride_b;
197 let c_offset = batch_index * params.stride_c;
198
199 var acc: f32 = 0.0;
200 let num_tiles = (params.k + {ts}u - 1u) / {ts}u;
201 for (var t: u32 = 0u; t < num_tiles; t = t + 1u) {{
202 let a_col = t * {ts}u + lc;
203 let b_row = t * {ts}u + lr;
204 tile_a[lr][lc] = load_a(a_offset, row, a_col);
205 tile_b[lr][lc] = load_b(b_offset, b_row, col);
206 workgroupBarrier();
207
208 for (var e: u32 = 0u; e < {ts}u; e = e + 1u) {{
209 acc += tile_a[lr][e] * tile_b[e][lc];
210 }}
211 workgroupBarrier();
212 }}
213
214 if (row >= params.m || col >= params.n) {{ return; }}
215 let idx = c_offset + row * params.ldc + col;
216 c[idx] = params.alpha * acc + params.beta * c[idx];
217}}
218"#,
219 ts = tile_size
220 )
221}
222
223pub fn gemm_wgsl_f16(tile_size: u32) -> String {
248 format!(
249 r#"
250enable f16;
251
252struct GemmParams {{
253 m: u32,
254 n: u32,
255 k: u32,
256 alpha: f32,
257 beta: f32,
258 trans_a: u32,
259 trans_b: u32,
260 lda: u32,
261 ldb: u32,
262 ldc: u32,
263 _pad0: u32,
264 _pad1: u32,
265}}
266
267@group(0) @binding(0) var<storage, read> a: array<f16>;
268@group(0) @binding(1) var<storage, read> b: array<f16>;
269@group(0) @binding(2) var<storage, read_write> c: array<f16>;
270@group(0) @binding(3) var<uniform> params: GemmParams;
271
272var<workgroup> tile_a: array<array<f16, {ts}>, {ts}>;
273var<workgroup> tile_b: array<array<f16, {ts}>, {ts}>;
274
275// op(A)[r, i] — logical m×k left operand. `lda` is the physical row stride of
276// the stored buffer (>= the packed width), supporting padded / sub-matrix
277// views. The `r`/`i` bounds guard is required here — unlike a per-thread
278// dot-product loop that only ever indexes `i < params.k` — because the tiled
279// staging loop below reads `t * {ts}u + lc` / `t * {ts}u + lr`, which
280// routinely exceeds `params.k` / `params.m` on the last (partial) tile; an
281// unguarded read would silently pull in a neighbouring row/column via WGSL's
282// robust-access semantics instead of contributing a proper zero pad.
283fn load_a(r: u32, i: u32) -> f16 {{
284 if (r >= params.m || i >= params.k) {{ return 0.0h; }}
285 if (params.trans_a == 0u) {{
286 return a[r * params.lda + i];
287 }}
288 return a[i * params.lda + r];
289}}
290
291// op(B)[i, col] — logical k×n right operand. `ldb` is the physical row
292// stride. Same last-tile bounds guard as `load_a`.
293fn load_b(i: u32, col: u32) -> f16 {{
294 if (i >= params.k || col >= params.n) {{ return 0.0h; }}
295 if (params.trans_b == 0u) {{
296 return b[i * params.ldb + col];
297 }}
298 return b[col * params.ldb + i];
299}}
300
301@compute @workgroup_size({ts}, {ts})
302fn main(
303 @builtin(global_invocation_id) gid: vec3<u32>,
304 @builtin(local_invocation_id) lid: vec3<u32>,
305) {{
306 let row = gid.y;
307 let col = gid.x;
308 let lr = lid.y;
309 let lc = lid.x;
310
311 var acc: f32 = 0.0;
312 let num_tiles = (params.k + {ts}u - 1u) / {ts}u;
313 for (var t: u32 = 0u; t < num_tiles; t = t + 1u) {{
314 let a_col = t * {ts}u + lc;
315 let b_row = t * {ts}u + lr;
316 tile_a[lr][lc] = load_a(row, a_col);
317 tile_b[lr][lc] = load_b(b_row, col);
318 workgroupBarrier();
319
320 for (var e: u32 = 0u; e < {ts}u; e = e + 1u) {{
321 acc += f32(tile_a[lr][e]) * f32(tile_b[e][lc]);
322 }}
323 workgroupBarrier();
324 }}
325
326 if (row >= params.m || col >= params.n) {{ return; }}
327 let idx = row * params.ldc + col;
328 let prev = f32(c[idx]);
329 c[idx] = f16(params.alpha * acc + params.beta * prev);
330}}
331"#,
332 ts = tile_size
333 )
334}
335
336pub fn elementwise_wgsl(op: &str) -> String {
346 let op_expr = match op {
347 "relu" => "max(x, 0.0)",
348 "sigmoid" => "1.0 / (1.0 + exp(-x))",
349 "tanh" => "tanh(x)",
350 "exp" => "exp(x)",
351 "log" => "log(x)",
352 "sqrt" => "sqrt(x)",
353 "abs" => "abs(x)",
354 "neg" => "-x",
355 _ => "x",
356 };
357
358 format!(
359 r#"
360@group(0) @binding(0) var<storage, read> input: array<f32>;
361@group(0) @binding(1) var<storage, read_write> output: array<f32>;
362
363@compute @workgroup_size(256)
364fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
365 let i = gid.x;
366 if (i >= arrayLength(&input)) {{ return; }}
367 let x = input[i];
368 output[i] = {op};
369}}
370"#,
371 op = op_expr
372 )
373}
374
375pub fn binary_wgsl(op: &str) -> String {
385 let op_expr = match op {
386 "add" => "a + b",
387 "sub" => "a - b",
388 "mul" => "a * b",
389 "div" => "a / b",
390 "max" => "max(a, b)",
391 "min" => "min(a, b)",
392 "pow" => "pow(a, b)",
393 _ => "a",
394 };
395
396 format!(
397 r#"
398@group(0) @binding(0) var<storage, read> lhs: array<f32>;
399@group(0) @binding(1) var<storage, read> rhs: array<f32>;
400@group(0) @binding(2) var<storage, read_write> output: array<f32>;
401
402@compute @workgroup_size(256)
403fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
404 let i = gid.x;
405 if (i >= arrayLength(&lhs)) {{ return; }}
406 let a = lhs[i];
407 let b = rhs[i];
408 output[i] = {op};
409}}
410"#,
411 op = op_expr
412 )
413}
414
415pub fn reduction_wgsl(op: &str) -> String {
428 let (neutral, combine) = match op {
434 "max" => ("bitcast<f32>(0xFF800000u)", "max(acc, val)"),
435 "min" => ("bitcast<f32>(0x7F800000u)", "min(acc, val)"),
436 _ => ("f32(0.0)", "acc + val"),
438 };
439
440 format!(
441 r#"
442// Reduction params: total element count.
443struct ReduceParams {{
444 n: u32,
445}}
446
447@group(0) @binding(0) var<storage, read> input: array<f32>;
448@group(0) @binding(1) var<storage, read_write> partial_sums: array<f32>;
449@group(0) @binding(2) var<uniform> params: ReduceParams;
450
451var<workgroup> shared_data: array<f32, 256>;
452
453@compute @workgroup_size(256)
454fn main(
455 @builtin(global_invocation_id) gid: vec3<u32>,
456 @builtin(local_invocation_id) lid: vec3<u32>,
457 @builtin(workgroup_id) wgid: vec3<u32>,
458) {{
459 let tid = lid.x;
460 let global_idx = gid.x;
461
462 // Load or use neutral element when out of range.
463 if (global_idx < params.n) {{
464 shared_data[tid] = input[global_idx];
465 }} else {{
466 shared_data[tid] = {neutral};
467 }}
468 workgroupBarrier();
469
470 // Parallel tree reduction within the workgroup.
471 var stride: u32 = 128u;
472 loop {{
473 if (stride == 0u) {{ break; }}
474 if (tid < stride) {{
475 let acc = shared_data[tid];
476 let val = shared_data[tid + stride];
477 shared_data[tid] = {combine};
478 }}
479 workgroupBarrier();
480 stride = stride >> 1u;
481 }}
482
483 // Thread 0 writes the workgroup result to the partial-sums buffer.
484 if (tid == 0u) {{
485 partial_sums[wgid.x] = shared_data[0];
486 }}
487}}
488"#,
489 neutral = neutral,
490 combine = combine,
491 )
492}
493
494#[allow(clippy::too_many_arguments)]
511pub fn conv2d_wgsl(
512 n: u32,
513 c_in: u32,
514 h_in: u32,
515 w_in: u32,
516 k_out: u32,
517 fh: u32,
518 fw: u32,
519 oh: u32,
520 ow: u32,
521 stride_h: u32,
522 stride_w: u32,
523 pad_h: u32,
524 pad_w: u32,
525) -> String {
526 format!(
527 r#"
528// Conv2D NCHW — generated by oxicuda-webgpu
529// input : [{n}, {c_in}, {h_in}, {w_in}]
530// kernel_w: [{k_out}, {c_in}, {fh}, {fw}]
531// output : [{n}, {k_out}, {oh}, {ow}]
532
533@group(0) @binding(0) var<storage, read> input: array<f32>;
534@group(0) @binding(1) var<storage, read> kernel_w: array<f32>;
535@group(0) @binding(2) var<storage, read_write> output: array<f32>;
536
537@compute @workgroup_size(8, 8)
538fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
539 // gid.x = output x (ox mapped across batches*k_out*oh)
540 // We flatten (batch, k, oy) into gid.y and ox into gid.x
541 let ox = gid.x;
542 let linear_y = gid.y;
543
544 let batch_k_oh = {n}u * {k_out}u * {oh}u;
545 if (ox >= {ow}u || linear_y >= batch_k_oh) {{ return; }}
546
547 let b = linear_y / ({k_out}u * {oh}u);
548 let rem = linear_y % ({k_out}u * {oh}u);
549 let kf = rem / {oh}u;
550 let oy = rem % {oh}u;
551
552 var acc: f32 = 0.0;
553 for (var ci: u32 = 0u; ci < {c_in}u; ci = ci + 1u) {{
554 for (var fy: u32 = 0u; fy < {fh}u; fy = fy + 1u) {{
555 for (var fx: u32 = 0u; fx < {fw}u; fx = fx + 1u) {{
556 let iy_raw = i32(oy * {stride_h}u + fy) - i32({pad_h}u);
557 let ix_raw = i32(ox * {stride_w}u + fx) - i32({pad_w}u);
558 if (iy_raw >= 0 && iy_raw < i32({h_in}u) && ix_raw >= 0 && ix_raw < i32({w_in}u)) {{
559 let iy = u32(iy_raw);
560 let ix = u32(ix_raw);
561 let in_idx = ((b * {c_in}u + ci) * {h_in}u + iy) * {w_in}u + ix;
562 let f_idx = ((kf * {c_in}u + ci) * {fh}u + fy) * {fw}u + fx;
563 acc += input[in_idx] * kernel_w[f_idx];
564 }}
565 }}
566 }}
567 }}
568
569 let o_idx = ((b * {k_out}u + kf) * {oh}u + oy) * {ow}u + ox;
570 output[o_idx] = acc;
571}}
572"#,
573 n = n,
574 c_in = c_in,
575 h_in = h_in,
576 w_in = w_in,
577 k_out = k_out,
578 fh = fh,
579 fw = fw,
580 oh = oh,
581 ow = ow,
582 stride_h = stride_h,
583 stride_w = stride_w,
584 pad_h = pad_h,
585 pad_w = pad_w,
586 )
587}
588
589pub fn attention_wgsl(
605 batch_heads: u32,
606 seq_q: u32,
607 seq_kv: u32,
608 head_dim: u32,
609 scale: f32,
610 causal: bool,
611) -> String {
612 let neg_inf = "bitcast<f32>(0xFF800000u)";
621
622 let causal_check = if causal {
623 "if (sk > sq) { score = bitcast<f32>(0xFF800000u); } else {"
624 } else {
625 "{"
626 };
627
628 format!(
629 r#"
630// Scaled dot-product attention — generated by oxicuda-webgpu
631// Q, K, V : [{batch_heads}, seq, {head_dim}]
632// O : [{batch_heads}, {seq_q}, {head_dim}]
633// scale : {scale}
634// causal : {causal}
635
636@group(0) @binding(0) var<storage, read> q_buf: array<f32>;
637@group(0) @binding(1) var<storage, read> k_buf: array<f32>;
638@group(0) @binding(2) var<storage, read> v_buf: array<f32>;
639@group(0) @binding(3) var<storage, read_write> o_buf: array<f32>;
640
641@compute @workgroup_size(64)
642fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
643 let linear = gid.x;
644 let total = {batch_heads}u * {seq_q}u;
645 if (linear >= total) {{ return; }}
646
647 let bh = linear / {seq_q}u;
648 let sq = linear % {seq_q}u;
649
650 let q_base = (bh * {seq_q}u + sq) * {head_dim}u;
651 // Q and O share the same [batch_heads, seq_q, head_dim] shape/strides.
652 let o_base = q_base;
653
654 // Zero-initialise this thread's output row *before* accumulating below.
655 // Pass 2 does a read-modify-write (`o_buf[..] += ..`) so a reused
656 // (non-fresh) output buffer must not leak stale contents into the sum.
657 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
658 o_buf[o_base + d] = 0.0;
659 }}
660
661 // Pass 1: find max score for numerical stability
662 var max_score: f32 = {neg_inf};
663 for (var sk: u32 = 0u; sk < {seq_kv}u; sk = sk + 1u) {{
664 var score: f32 = 0.0;
665 {causal_check}
666 let k_base = (bh * {seq_kv}u + sk) * {head_dim}u;
667 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
668 score += q_buf[q_base + d] * k_buf[k_base + d];
669 }}
670 score *= f32({scale});
671 }}
672 if (score > max_score) {{ max_score = score; }}
673 }}
674
675 // Pass 2: compute exp(score - max), accumulate weighted V
676 var sum_exp: f32 = 0.0;
677 for (var sk: u32 = 0u; sk < {seq_kv}u; sk = sk + 1u) {{
678 var score: f32 = 0.0;
679 {causal_check}
680 let k_base = (bh * {seq_kv}u + sk) * {head_dim}u;
681 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
682 score += q_buf[q_base + d] * k_buf[k_base + d];
683 }}
684 score *= f32({scale});
685 }}
686 let w = exp(score - max_score);
687 sum_exp += w;
688 let v_base = (bh * {seq_kv}u + sk) * {head_dim}u;
689 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
690 // Accumulate in-place (we normalise after the loop).
691 o_buf[o_base + d] += w * v_buf[v_base + d];
692 }}
693 }}
694
695 // Pass 3: normalise, or write zeros if no key contributed (`sum_exp` is
696 // `0.0` or `NaN`; `sum_exp > 0.0` is false for both, so this else branch
697 // catches both without a separate NaN check).
698 if (sum_exp > 0.0) {{
699 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
700 o_buf[o_base + d] /= sum_exp;
701 }}
702 }} else {{
703 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
704 o_buf[o_base + d] = 0.0;
705 }}
706 }}
707}}
708"#,
709 batch_heads = batch_heads,
710 seq_q = seq_q,
711 seq_kv = seq_kv,
712 head_dim = head_dim,
713 scale = scale,
714 causal = causal,
715 causal_check = causal_check,
716 neg_inf = neg_inf,
717 )
718}
719
720pub fn reduction_nd_wgsl(op: &str) -> String {
744 let (neutral, combine, combine_alias) = match op {
749 "max" => (
750 "bitcast<f32>(0xFF800000u)",
751 "max(acc, val)",
752 "max(acc2, val)",
753 ),
754 "min" => (
755 "bitcast<f32>(0x7F800000u)",
756 "min(acc, val)",
757 "min(acc2, val)",
758 ),
759 _ => ("f32(0.0)", "acc + val", "acc2 + val"),
761 };
762
763 let final_expr = if op == "mean" {
765 "shared_data[0] / f32(params.dk)"
766 } else {
767 "shared_data[0]"
768 };
769
770 format!(
771 r#"
772struct ReduceNdParams {{
773 outer: u32,
774 dk: u32,
775 inner: u32,
776 outer_stride: u32,
777 dk_stride: u32,
778 inner_stride: u32,
779 grid_x: u32,
780 _pad: u32,
781}}
782
783@group(0) @binding(0) var<storage, read> input: array<f32>;
784@group(0) @binding(1) var<storage, read_write> output: array<f32>;
785@group(0) @binding(2) var<uniform> params: ReduceNdParams;
786
787var<workgroup> shared_data: array<f32, 256>;
788
789@compute @workgroup_size(256)
790fn main(
791 @builtin(local_invocation_id) lid: vec3<u32>,
792 @builtin(workgroup_id) wgid: vec3<u32>,
793) {{
794 let tid = lid.x;
795 let total = params.outer * params.inner;
796
797 // Decode 2-D workgroup id back to a linear output slot.
798 let slot = wgid.y * params.grid_x + wgid.x;
799 if (slot >= total) {{ return; }}
800
801 let o = slot / params.inner;
802 let j = slot % params.inner;
803 let base = o * params.outer_stride + j * params.inner_stride;
804
805 // Strided per-thread reduction across the dk axis.
806 var acc: f32 = {neutral};
807 var i: u32 = tid;
808 loop {{
809 if (i >= params.dk) {{ break; }}
810 let val = input[base + i * params.dk_stride];
811 acc = {combine};
812 i = i + 256u;
813 }}
814
815 shared_data[tid] = acc;
816 workgroupBarrier();
817
818 // Tree reduction within the workgroup.
819 var stride: u32 = 128u;
820 loop {{
821 if (stride == 0u) {{ break; }}
822 if (tid < stride) {{
823 let acc2 = shared_data[tid];
824 let val = shared_data[tid + stride];
825 shared_data[tid] = {combine_alias};
826 }}
827 workgroupBarrier();
828 stride = stride >> 1u;
829 }}
830
831 if (tid == 0u) {{
832 output[slot] = {final_expr};
833 }}
834}}
835"#,
836 neutral = neutral,
837 combine = combine,
838 combine_alias = combine_alias,
839 final_expr = final_expr,
840 )
841}
842
843pub fn reduction_final_wgsl(op: &str) -> String {
852 let (neutral, combine) = match op {
853 "max" => ("bitcast<f32>(0xFF800000u)", "max(acc, val)"),
854 "min" => ("bitcast<f32>(0x7F800000u)", "min(acc, val)"),
855 _ => ("f32(0.0)", "acc + val"),
856 };
857
858 format!(
859 r#"
860struct FinalReduceParams {{
861 num_groups: u32,
862}}
863
864@group(0) @binding(0) var<storage, read> partial_sums: array<f32>;
865@group(0) @binding(1) var<storage, read_write> output: array<f32>;
866@group(0) @binding(2) var<uniform> params: FinalReduceParams;
867
868var<workgroup> shared_data: array<f32, 256>;
869
870@compute @workgroup_size(256)
871fn main(
872 @builtin(local_invocation_id) lid: vec3<u32>,
873) {{
874 let tid = lid.x;
875
876 // Grid-stride fold: each of the 256 threads accumulates every partial at
877 // index tid, tid+256, tid+512, … Without this, partials beyond index 255
878 // (num_groups > 256, i.e. > 65 536 input elements) would be silently
879 // dropped.
880 var acc: f32 = {neutral};
881 var i: u32 = tid;
882 loop {{
883 if (i >= params.num_groups) {{ break; }}
884 let val = partial_sums[i];
885 acc = {combine};
886 i = i + 256u;
887 }}
888 shared_data[tid] = acc;
889 workgroupBarrier();
890
891 var stride: u32 = 128u;
892 loop {{
893 if (stride == 0u) {{ break; }}
894 if (tid < stride) {{
895 let acc = shared_data[tid];
896 let val = shared_data[tid + stride];
897 shared_data[tid] = {combine};
898 }}
899 workgroupBarrier();
900 stride = stride >> 1u;
901 }}
902
903 if (tid == 0u) {{
904 output[0] = shared_data[0];
905 }}
906}}
907"#,
908 neutral = neutral,
909 combine = combine,
910 )
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916
917 #[test]
918 fn wgsl_gemm_contains_workgroup() {
919 let src = gemm_wgsl(16);
920 assert!(src.contains("@compute @workgroup_size(16, 16)"));
921 assert!(src.contains("GemmParams"));
922 assert!(src.contains("alpha"));
923 assert!(src.contains("beta"));
924 }
925
926 #[test]
927 fn wgsl_gemm_tile_size_embedded() {
928 let src8 = gemm_wgsl(8);
929 assert!(src8.contains("@workgroup_size(8, 8)"));
930 let src32 = gemm_wgsl(32);
931 assert!(src32.contains("@workgroup_size(32, 32)"));
932 }
933
934 #[test]
935 fn wgsl_gemm_has_transpose_flags() {
936 let src = gemm_wgsl(8);
937 assert!(src.contains("trans_a: u32"));
939 assert!(src.contains("trans_b: u32"));
940 assert!(src.contains("lda: u32"));
942 assert!(src.contains("ldb: u32"));
943 assert!(src.contains("ldc: u32"));
944 assert!(src.contains("a[r * params.lda + i]"));
947 assert!(src.contains("a[i * params.lda + r]"));
948 assert!(src.contains("b[i * params.ldb + col]"));
949 assert!(src.contains("b[col * params.ldb + i]"));
950 assert!(src.contains("row * params.ldc + col"));
951 }
952
953 #[test]
954 fn wgsl_gemm_uses_shared_memory_tiling() {
955 let src = gemm_wgsl(16);
956 assert!(src.contains("var<workgroup> tile_a"));
957 assert!(src.contains("var<workgroup> tile_b"));
958 assert!(src.contains("workgroupBarrier"));
959 assert!(src.contains("array<array<f32, 16>, 16>"));
961 }
962
963 #[test]
964 fn wgsl_elementwise_relu_contains_max() {
965 let src = elementwise_wgsl("relu");
966 assert!(src.contains("max(x, 0.0)"));
967 }
968
969 #[test]
970 fn wgsl_elementwise_all_ops() {
971 assert!(elementwise_wgsl("sigmoid").contains("exp(-x)"));
972 assert!(elementwise_wgsl("tanh").contains("tanh(x)"));
973 assert!(elementwise_wgsl("exp").contains("exp(x)"));
974 assert!(elementwise_wgsl("log").contains("log(x)"));
975 assert!(elementwise_wgsl("sqrt").contains("sqrt(x)"));
976 assert!(elementwise_wgsl("abs").contains("abs(x)"));
977 assert!(elementwise_wgsl("neg").contains("-x"));
978 assert!(elementwise_wgsl("identity_op").contains("output[i] = x;"));
980 }
981
982 #[test]
983 fn wgsl_reduction_sum_contains_addition() {
984 let src = reduction_wgsl("sum");
985 assert!(src.contains("acc + val"));
986 assert!(src.contains("workgroupBarrier"));
987 }
988
989 #[test]
990 fn wgsl_reduction_max_uses_max_fn() {
991 let src = reduction_wgsl("max");
992 assert!(src.contains("max(acc, val)"));
993 }
994
995 #[test]
996 fn wgsl_reduction_min_uses_min_fn() {
997 let src = reduction_wgsl("min");
998 assert!(src.contains("min(acc, val)"));
999 }
1000
1001 #[test]
1002 fn wgsl_reduction_mean_same_as_sum() {
1003 let sum_src = reduction_wgsl("sum");
1005 let mean_src = reduction_wgsl("mean");
1006 assert_eq!(sum_src, mean_src);
1007 }
1008
1009 #[test]
1010 fn wgsl_reduction_final_sum() {
1011 let src = reduction_final_wgsl("sum");
1012 assert!(src.contains("num_groups"));
1013 assert!(src.contains("output[0]"));
1014 }
1015
1016 #[test]
1017 fn wgsl_reduction_final_grid_strides_over_all_groups() {
1018 for op in ["sum", "max", "min", "mean"] {
1022 let src = reduction_final_wgsl(op);
1023 assert!(
1024 src.contains("i = i + 256u"),
1025 "final reduction for {op} lacks the grid-stride loop"
1026 );
1027 assert!(
1028 src.contains("if (i >= params.num_groups)"),
1029 "final reduction for {op} lacks the num_groups loop bound"
1030 );
1031 }
1032 }
1033
1034 #[test]
1037 fn wgsl_reduction_nd_sum_contains_addition() {
1038 let src = reduction_nd_wgsl("sum");
1039 assert!(src.contains("acc + val"));
1040 assert!(src.contains("acc2 + val"));
1042 assert!(src.contains("workgroupBarrier"));
1043 assert!(src.contains("ReduceNdParams"));
1044 }
1045
1046 #[test]
1047 fn wgsl_reduction_nd_max_uses_max_fn() {
1048 let src = reduction_nd_wgsl("max");
1049 assert!(src.contains("max(acc, val)"));
1050 assert!(src.contains("max(acc2, val)"));
1051 }
1052
1053 #[test]
1054 fn wgsl_reduction_nd_min_uses_min_fn() {
1055 let src = reduction_nd_wgsl("min");
1056 assert!(src.contains("min(acc, val)"));
1057 assert!(src.contains("min(acc2, val)"));
1058 }
1059
1060 #[test]
1061 fn wgsl_reduction_nd_mean_divides_by_dk() {
1062 let src = reduction_nd_wgsl("mean");
1063 assert!(src.contains("shared_data[0] / f32(params.dk)"));
1064 assert!(src.contains("acc + val"));
1065 }
1066
1067 #[test]
1068 fn wgsl_reduction_nd_sum_does_not_divide() {
1069 let src = reduction_nd_wgsl("sum");
1070 assert!(!src.contains("/ f32(params.dk)"));
1071 }
1072
1073 #[test]
1074 fn wgsl_reduction_nd_decodes_2d_dispatch() {
1075 let src = reduction_nd_wgsl("sum");
1076 assert!(src.contains("wgid.y * params.grid_x + wgid.x"));
1077 }
1078
1079 #[test]
1080 fn wgsl_reduction_nd_uses_strided_loop() {
1081 let src = reduction_nd_wgsl("sum");
1082 assert!(src.contains("i = i + 256u"));
1083 }
1084
1085 #[test]
1088 fn wgsl_binary_add() {
1089 let src = binary_wgsl("add");
1090 assert!(src.contains("a + b"));
1091 assert!(src.contains("lhs"));
1092 assert!(src.contains("rhs"));
1093 }
1094
1095 #[test]
1096 fn wgsl_binary_all_ops() {
1097 assert!(binary_wgsl("sub").contains("a - b"));
1098 assert!(binary_wgsl("mul").contains("a * b"));
1099 assert!(binary_wgsl("div").contains("a / b"));
1100 assert!(binary_wgsl("max").contains("max(a, b)"));
1101 assert!(binary_wgsl("min").contains("min(a, b)"));
1102 assert!(binary_wgsl("pow").contains("pow(a, b)"));
1103 assert!(binary_wgsl("unknown_op").contains("output[i] = a;"));
1105 }
1106
1107 #[test]
1108 fn wgsl_binary_workgroup_size() {
1109 let src = binary_wgsl("add");
1110 assert!(src.contains("@workgroup_size(256)"));
1111 }
1112
1113 #[test]
1116 fn wgsl_conv2d_contains_workgroup() {
1117 let src = conv2d_wgsl(1, 3, 32, 32, 16, 3, 3, 30, 30, 1, 1, 0, 0);
1118 assert!(src.contains("@compute @workgroup_size(8, 8)"));
1119 }
1120
1121 #[test]
1122 fn wgsl_conv2d_contains_storage_bindings() {
1123 let src = conv2d_wgsl(1, 3, 32, 32, 16, 3, 3, 30, 30, 1, 1, 0, 0);
1124 assert!(src.contains("var<storage, read> input:"));
1125 assert!(src.contains("var<storage, read> kernel_w:"));
1127 assert!(src.contains("var<storage, read_write> output:"));
1128 }
1129
1130 #[test]
1131 fn wgsl_conv2d_embeds_dimensions() {
1132 let src = conv2d_wgsl(2, 8, 64, 64, 32, 5, 5, 60, 60, 1, 1, 0, 0);
1133 assert!(src.contains("8u")); assert!(src.contains("64u")); assert!(src.contains("32u")); assert!(src.contains("5u")); assert!(src.contains("60u")); }
1140
1141 #[test]
1142 fn wgsl_conv2d_has_padding_check() {
1143 let src = conv2d_wgsl(1, 1, 8, 8, 1, 3, 3, 8, 8, 1, 1, 1, 1);
1144 assert!(src.contains("iy_raw >= 0"));
1146 assert!(src.contains("ix_raw >= 0"));
1147 }
1148
1149 #[test]
1150 fn wgsl_conv2d_has_stride() {
1151 let src = conv2d_wgsl(1, 1, 8, 8, 1, 3, 3, 3, 3, 2, 2, 0, 0);
1152 assert!(src.contains("2u")); }
1154
1155 #[test]
1158 fn wgsl_attention_contains_workgroup() {
1159 let src = attention_wgsl(4, 8, 8, 64, 0.125, false);
1160 assert!(src.contains("@compute @workgroup_size(64)"));
1161 }
1162
1163 #[test]
1164 fn wgsl_attention_contains_storage_bindings() {
1165 let src = attention_wgsl(4, 8, 8, 64, 0.125, false);
1166 assert!(src.contains("var<storage, read> q_buf:"));
1167 assert!(src.contains("var<storage, read> k_buf:"));
1168 assert!(src.contains("var<storage, read> v_buf:"));
1169 assert!(src.contains("var<storage, read_write> o_buf:"));
1170 }
1171
1172 #[test]
1173 fn wgsl_attention_stable_softmax() {
1174 let src = attention_wgsl(1, 4, 4, 32, 0.25, false);
1175 assert!(src.contains("max_score"));
1176 assert!(src.contains("exp(score - max_score)"));
1177 assert!(src.contains("sum_exp"));
1178 }
1179
1180 #[test]
1181 fn wgsl_attention_causal_mask() {
1182 let src_causal = attention_wgsl(1, 4, 4, 32, 0.25, true);
1183 assert!(src_causal.contains("sk > sq"));
1184
1185 let src_non_causal = attention_wgsl(1, 4, 4, 32, 0.25, false);
1186 assert!(!src_non_causal.contains("sk > sq"));
1187 }
1188
1189 #[test]
1190 fn wgsl_attention_embeds_scale() {
1191 let src = attention_wgsl(2, 16, 16, 64, 0.125, false);
1192 assert!(src.contains("0.125"));
1193 }
1194
1195 #[test]
1198 fn wgsl_batched_gemm_contains_batch_params() {
1199 let src = batched_gemm_wgsl(16);
1200 assert!(src.contains("batch_count"));
1201 assert!(src.contains("stride_a"));
1202 assert!(src.contains("stride_b"));
1203 assert!(src.contains("stride_c"));
1204 }
1205
1206 #[test]
1207 fn wgsl_batched_gemm_contains_workgroup() {
1208 let src = batched_gemm_wgsl(16);
1209 assert!(src.contains("@compute @workgroup_size(16, 16)"));
1210 assert!(src.contains("BatchedGemmParams"));
1211 }
1212
1213 #[test]
1214 fn wgsl_batched_gemm_uses_batch_index() {
1215 let src = batched_gemm_wgsl(8);
1216 assert!(src.contains("batch_index"));
1217 assert!(src.contains("gid.z"));
1218 }
1219
1220 #[test]
1221 fn wgsl_batched_gemm_tile_size_embedded() {
1222 let src8 = batched_gemm_wgsl(8);
1223 assert!(src8.contains("@workgroup_size(8, 8)"));
1224 let src32 = batched_gemm_wgsl(32);
1225 assert!(src32.contains("@workgroup_size(32, 32)"));
1226 }
1227
1228 #[test]
1229 fn wgsl_batched_gemm_has_transpose_flags() {
1230 let src = batched_gemm_wgsl(8);
1231 assert!(src.contains("trans_a: u32"));
1232 assert!(src.contains("trans_b: u32"));
1233 assert!(src.contains("lda: u32"));
1234 assert!(src.contains("ldb: u32"));
1235 assert!(src.contains("ldc: u32"));
1236 assert!(src.contains("a[a_offset + r * params.lda + i]"));
1239 assert!(src.contains("a[a_offset + i * params.lda + r]"));
1240 assert!(src.contains("b[b_offset + i * params.ldb + col]"));
1241 assert!(src.contains("b[b_offset + col * params.ldb + i]"));
1242 assert!(src.contains("row * params.ldc + col"));
1243 }
1244
1245 #[test]
1246 fn wgsl_batched_gemm_uses_shared_memory_tiling() {
1247 let src = batched_gemm_wgsl(8);
1248 assert!(src.contains("var<workgroup> tile_a"));
1249 assert!(src.contains("var<workgroup> tile_b"));
1250 assert!(src.contains("workgroupBarrier"));
1251 assert!(src.contains("array<array<f32, 8>, 8>"));
1252 }
1253
1254 #[test]
1257 fn wgsl_gemm_f16_enables_extension() {
1258 let src = gemm_wgsl_f16(16);
1259 assert!(src.contains("enable f16;"));
1260 }
1261
1262 #[test]
1263 fn wgsl_gemm_f16_uses_f16_storage() {
1264 let src = gemm_wgsl_f16(16);
1265 assert!(src.contains("array<f16>"));
1266 }
1267
1268 #[test]
1269 fn wgsl_gemm_f16_accumulates_in_f32() {
1270 let src = gemm_wgsl_f16(16);
1271 assert!(src.contains("var acc: f32 = 0.0;"));
1272 assert!(src.contains("f32(tile_a["));
1276 assert!(src.contains("f32(tile_b["));
1277 }
1278
1279 #[test]
1280 fn wgsl_gemm_f16_uses_shared_memory_tiling() {
1281 let src = gemm_wgsl_f16(16);
1282 assert!(src.contains("var<workgroup> tile_a"));
1283 assert!(src.contains("var<workgroup> tile_b"));
1284 assert!(src.contains("workgroupBarrier"));
1285 assert!(src.contains("array<array<f16, 16>, 16>"));
1288 }
1289
1290 #[test]
1291 fn wgsl_gemm_f16_load_helpers_guard_last_partial_tile() {
1292 let src = gemm_wgsl_f16(16);
1300 assert!(src.contains("if (r >= params.m || i >= params.k) { return 0.0h; }"));
1301 assert!(src.contains("if (i >= params.k || col >= params.n) { return 0.0h; }"));
1302 }
1303
1304 #[test]
1305 fn wgsl_gemm_f16_contains_workgroup() {
1306 let src = gemm_wgsl_f16(8);
1307 assert!(src.contains("@compute @workgroup_size(8, 8)"));
1308 assert!(src.contains("GemmParams"));
1309 }
1310
1311 #[test]
1312 fn wgsl_gemm_f16_has_transpose_flags() {
1313 let src = gemm_wgsl_f16(8);
1314 assert!(src.contains("trans_a: u32"));
1317 assert!(src.contains("trans_b: u32"));
1318 assert!(src.contains("lda: u32"));
1319 assert!(src.contains("ldb: u32"));
1320 assert!(src.contains("ldc: u32"));
1321 assert!(src.contains("return a[r * params.lda + i];"));
1327 assert!(src.contains("return a[i * params.lda + r];"));
1328 assert!(src.contains("return b[i * params.ldb + col];"));
1329 assert!(src.contains("return b[col * params.ldb + i];"));
1330 assert!(src.contains("row * params.ldc + col"));
1331 }
1332
1333 #[test]
1334 fn wgsl_attention_embeds_dimensions() {
1335 let src = attention_wgsl(8, 32, 32, 128, 0.088, true);
1336 assert!(src.contains("128u")); assert!(src.contains("32u")); assert!(src.contains("8u")); }
1340}