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 {
237 format!(
238 r#"
239enable f16;
240
241struct GemmParams {{
242 m: u32,
243 n: u32,
244 k: u32,
245 alpha: f32,
246 beta: f32,
247}}
248
249@group(0) @binding(0) var<storage, read> a: array<f16>;
250@group(0) @binding(1) var<storage, read> b: array<f16>;
251@group(0) @binding(2) var<storage, read_write> c: array<f16>;
252@group(0) @binding(3) var<uniform> params: GemmParams;
253
254@compute @workgroup_size({ts}, {ts})
255fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
256 let row = gid.y;
257 let col = gid.x;
258 if (row >= params.m || col >= params.n) {{ return; }}
259
260 var acc: f32 = 0.0;
261 for (var i: u32 = 0u; i < params.k; i = i + 1u) {{
262 acc += f32(a[row * params.k + i]) * f32(b[i * params.n + col]);
263 }}
264
265 let idx = row * params.n + col;
266 let prev = f32(c[idx]);
267 c[idx] = f16(params.alpha * acc + params.beta * prev);
268}}
269"#,
270 ts = tile_size
271 )
272}
273
274pub fn elementwise_wgsl(op: &str) -> String {
284 let op_expr = match op {
285 "relu" => "max(x, 0.0)",
286 "sigmoid" => "1.0 / (1.0 + exp(-x))",
287 "tanh" => "tanh(x)",
288 "exp" => "exp(x)",
289 "log" => "log(x)",
290 "sqrt" => "sqrt(x)",
291 "abs" => "abs(x)",
292 "neg" => "-x",
293 _ => "x",
294 };
295
296 format!(
297 r#"
298@group(0) @binding(0) var<storage, read> input: array<f32>;
299@group(0) @binding(1) var<storage, read_write> output: array<f32>;
300
301@compute @workgroup_size(256)
302fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
303 let i = gid.x;
304 if (i >= arrayLength(&input)) {{ return; }}
305 let x = input[i];
306 output[i] = {op};
307}}
308"#,
309 op = op_expr
310 )
311}
312
313pub fn binary_wgsl(op: &str) -> String {
323 let op_expr = match op {
324 "add" => "a + b",
325 "sub" => "a - b",
326 "mul" => "a * b",
327 "div" => "a / b",
328 "max" => "max(a, b)",
329 "min" => "min(a, b)",
330 "pow" => "pow(a, b)",
331 _ => "a",
332 };
333
334 format!(
335 r#"
336@group(0) @binding(0) var<storage, read> lhs: array<f32>;
337@group(0) @binding(1) var<storage, read> rhs: array<f32>;
338@group(0) @binding(2) var<storage, read_write> output: array<f32>;
339
340@compute @workgroup_size(256)
341fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
342 let i = gid.x;
343 if (i >= arrayLength(&lhs)) {{ return; }}
344 let a = lhs[i];
345 let b = rhs[i];
346 output[i] = {op};
347}}
348"#,
349 op = op_expr
350 )
351}
352
353pub fn reduction_wgsl(op: &str) -> String {
366 let (neutral, combine) = match op {
368 "max" => ("f32(-1e38)", "max(acc, val)"),
369 "min" => ("f32(1e38)", "min(acc, val)"),
370 _ => ("f32(0.0)", "acc + val"),
372 };
373
374 format!(
375 r#"
376// Reduction params: total element count.
377struct ReduceParams {{
378 n: u32,
379}}
380
381@group(0) @binding(0) var<storage, read> input: array<f32>;
382@group(0) @binding(1) var<storage, read_write> partial_sums: array<f32>;
383@group(0) @binding(2) var<uniform> params: ReduceParams;
384
385var<workgroup> shared_data: array<f32, 256>;
386
387@compute @workgroup_size(256)
388fn main(
389 @builtin(global_invocation_id) gid: vec3<u32>,
390 @builtin(local_invocation_id) lid: vec3<u32>,
391 @builtin(workgroup_id) wgid: vec3<u32>,
392) {{
393 let tid = lid.x;
394 let global_idx = gid.x;
395
396 // Load or use neutral element when out of range.
397 if (global_idx < params.n) {{
398 shared_data[tid] = input[global_idx];
399 }} else {{
400 shared_data[tid] = {neutral};
401 }}
402 workgroupBarrier();
403
404 // Parallel tree reduction within the workgroup.
405 var stride: u32 = 128u;
406 loop {{
407 if (stride == 0u) {{ break; }}
408 if (tid < stride) {{
409 let acc = shared_data[tid];
410 let val = shared_data[tid + stride];
411 shared_data[tid] = {combine};
412 }}
413 workgroupBarrier();
414 stride = stride >> 1u;
415 }}
416
417 // Thread 0 writes the workgroup result to the partial-sums buffer.
418 if (tid == 0u) {{
419 partial_sums[wgid.x] = shared_data[0];
420 }}
421}}
422"#,
423 neutral = neutral,
424 combine = combine,
425 )
426}
427
428#[allow(clippy::too_many_arguments)]
445pub fn conv2d_wgsl(
446 n: u32,
447 c_in: u32,
448 h_in: u32,
449 w_in: u32,
450 k_out: u32,
451 fh: u32,
452 fw: u32,
453 oh: u32,
454 ow: u32,
455 stride_h: u32,
456 stride_w: u32,
457 pad_h: u32,
458 pad_w: u32,
459) -> String {
460 format!(
461 r#"
462// Conv2D NCHW — generated by oxicuda-webgpu
463// input : [{n}, {c_in}, {h_in}, {w_in}]
464// kernel_w: [{k_out}, {c_in}, {fh}, {fw}]
465// output : [{n}, {k_out}, {oh}, {ow}]
466
467@group(0) @binding(0) var<storage, read> input: array<f32>;
468@group(0) @binding(1) var<storage, read> kernel_w: array<f32>;
469@group(0) @binding(2) var<storage, read_write> output: array<f32>;
470
471@compute @workgroup_size(8, 8)
472fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
473 // gid.x = output x (ox mapped across batches*k_out*oh)
474 // We flatten (batch, k, oy) into gid.y and ox into gid.x
475 let ox = gid.x;
476 let linear_y = gid.y;
477
478 let batch_k_oh = {n}u * {k_out}u * {oh}u;
479 if (ox >= {ow}u || linear_y >= batch_k_oh) {{ return; }}
480
481 let b = linear_y / ({k_out}u * {oh}u);
482 let rem = linear_y % ({k_out}u * {oh}u);
483 let kf = rem / {oh}u;
484 let oy = rem % {oh}u;
485
486 var acc: f32 = 0.0;
487 for (var ci: u32 = 0u; ci < {c_in}u; ci = ci + 1u) {{
488 for (var fy: u32 = 0u; fy < {fh}u; fy = fy + 1u) {{
489 for (var fx: u32 = 0u; fx < {fw}u; fx = fx + 1u) {{
490 let iy_raw = i32(oy * {stride_h}u + fy) - i32({pad_h}u);
491 let ix_raw = i32(ox * {stride_w}u + fx) - i32({pad_w}u);
492 if (iy_raw >= 0 && iy_raw < i32({h_in}u) && ix_raw >= 0 && ix_raw < i32({w_in}u)) {{
493 let iy = u32(iy_raw);
494 let ix = u32(ix_raw);
495 let in_idx = ((b * {c_in}u + ci) * {h_in}u + iy) * {w_in}u + ix;
496 let f_idx = ((kf * {c_in}u + ci) * {fh}u + fy) * {fw}u + fx;
497 acc += input[in_idx] * kernel_w[f_idx];
498 }}
499 }}
500 }}
501 }}
502
503 let o_idx = ((b * {k_out}u + kf) * {oh}u + oy) * {ow}u + ox;
504 output[o_idx] = acc;
505}}
506"#,
507 n = n,
508 c_in = c_in,
509 h_in = h_in,
510 w_in = w_in,
511 k_out = k_out,
512 fh = fh,
513 fw = fw,
514 oh = oh,
515 ow = ow,
516 stride_h = stride_h,
517 stride_w = stride_w,
518 pad_h = pad_h,
519 pad_w = pad_w,
520 )
521}
522
523pub fn attention_wgsl(
539 batch_heads: u32,
540 seq_q: u32,
541 seq_kv: u32,
542 head_dim: u32,
543 scale: f32,
544 causal: bool,
545) -> String {
546 let causal_check = if causal {
547 "if (sk > sq) { score = f32(-1e38); } else {"
548 } else {
549 "{"
550 };
551
552 format!(
553 r#"
554// Scaled dot-product attention — generated by oxicuda-webgpu
555// Q, K, V : [{batch_heads}, seq, {head_dim}]
556// O : [{batch_heads}, {seq_q}, {head_dim}]
557// scale : {scale}
558// causal : {causal}
559
560@group(0) @binding(0) var<storage, read> q_buf: array<f32>;
561@group(0) @binding(1) var<storage, read> k_buf: array<f32>;
562@group(0) @binding(2) var<storage, read> v_buf: array<f32>;
563@group(0) @binding(3) var<storage, read_write> o_buf: array<f32>;
564
565@compute @workgroup_size(64)
566fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
567 let linear = gid.x;
568 let total = {batch_heads}u * {seq_q}u;
569 if (linear >= total) {{ return; }}
570
571 let bh = linear / {seq_q}u;
572 let sq = linear % {seq_q}u;
573
574 let q_base = (bh * {seq_q}u + sq) * {head_dim}u;
575
576 // Pass 1: find max score for numerical stability
577 var max_score: f32 = f32(-1e38);
578 for (var sk: u32 = 0u; sk < {seq_kv}u; sk = sk + 1u) {{
579 var score: f32 = 0.0;
580 {causal_check}
581 let k_base = (bh * {seq_kv}u + sk) * {head_dim}u;
582 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
583 score += q_buf[q_base + d] * k_buf[k_base + d];
584 }}
585 score *= f32({scale});
586 }}
587 if (score > max_score) {{ max_score = score; }}
588 }}
589
590 // Pass 2: compute exp(score - max), accumulate weighted V
591 var sum_exp: f32 = 0.0;
592 for (var sk: u32 = 0u; sk < {seq_kv}u; sk = sk + 1u) {{
593 var score: f32 = 0.0;
594 {causal_check}
595 let k_base = (bh * {seq_kv}u + sk) * {head_dim}u;
596 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
597 score += q_buf[q_base + d] * k_buf[k_base + d];
598 }}
599 score *= f32({scale});
600 }}
601 let w = exp(score - max_score);
602 sum_exp += w;
603 let v_base = (bh * {seq_kv}u + sk) * {head_dim}u;
604 let o_base = (bh * {seq_q}u + sq) * {head_dim}u;
605 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
606 // Accumulate in-place (we normalise after the loop).
607 o_buf[o_base + d] += w * v_buf[v_base + d];
608 }}
609 }}
610
611 // Pass 3: normalise
612 if (sum_exp > 0.0) {{
613 let o_base = (bh * {seq_q}u + sq) * {head_dim}u;
614 for (var d: u32 = 0u; d < {head_dim}u; d = d + 1u) {{
615 o_buf[o_base + d] /= sum_exp;
616 }}
617 }}
618}}
619"#,
620 batch_heads = batch_heads,
621 seq_q = seq_q,
622 seq_kv = seq_kv,
623 head_dim = head_dim,
624 scale = scale,
625 causal = causal,
626 causal_check = causal_check,
627 )
628}
629
630pub fn reduction_nd_wgsl(op: &str) -> String {
654 let (neutral, combine, combine_alias) = match op {
659 "max" => ("f32(-1e38)", "max(acc, val)", "max(acc2, val)"),
660 "min" => ("f32(1e38)", "min(acc, val)", "min(acc2, val)"),
661 _ => ("f32(0.0)", "acc + val", "acc2 + val"),
663 };
664
665 let final_expr = if op == "mean" {
667 "shared_data[0] / f32(params.dk)"
668 } else {
669 "shared_data[0]"
670 };
671
672 format!(
673 r#"
674struct ReduceNdParams {{
675 outer: u32,
676 dk: u32,
677 inner: u32,
678 outer_stride: u32,
679 dk_stride: u32,
680 inner_stride: u32,
681 grid_x: u32,
682 _pad: u32,
683}}
684
685@group(0) @binding(0) var<storage, read> input: array<f32>;
686@group(0) @binding(1) var<storage, read_write> output: array<f32>;
687@group(0) @binding(2) var<uniform> params: ReduceNdParams;
688
689var<workgroup> shared_data: array<f32, 256>;
690
691@compute @workgroup_size(256)
692fn main(
693 @builtin(local_invocation_id) lid: vec3<u32>,
694 @builtin(workgroup_id) wgid: vec3<u32>,
695) {{
696 let tid = lid.x;
697 let total = params.outer * params.inner;
698
699 // Decode 2-D workgroup id back to a linear output slot.
700 let slot = wgid.y * params.grid_x + wgid.x;
701 if (slot >= total) {{ return; }}
702
703 let o = slot / params.inner;
704 let j = slot % params.inner;
705 let base = o * params.outer_stride + j * params.inner_stride;
706
707 // Strided per-thread reduction across the dk axis.
708 var acc: f32 = {neutral};
709 var i: u32 = tid;
710 loop {{
711 if (i >= params.dk) {{ break; }}
712 let val = input[base + i * params.dk_stride];
713 acc = {combine};
714 i = i + 256u;
715 }}
716
717 shared_data[tid] = acc;
718 workgroupBarrier();
719
720 // Tree reduction within the workgroup.
721 var stride: u32 = 128u;
722 loop {{
723 if (stride == 0u) {{ break; }}
724 if (tid < stride) {{
725 let acc2 = shared_data[tid];
726 let val = shared_data[tid + stride];
727 shared_data[tid] = {combine_alias};
728 }}
729 workgroupBarrier();
730 stride = stride >> 1u;
731 }}
732
733 if (tid == 0u) {{
734 output[slot] = {final_expr};
735 }}
736}}
737"#,
738 neutral = neutral,
739 combine = combine,
740 combine_alias = combine_alias,
741 final_expr = final_expr,
742 )
743}
744
745pub fn reduction_final_wgsl(op: &str) -> String {
754 let (neutral, combine) = match op {
755 "max" => ("f32(-1e38)", "max(acc, val)"),
756 "min" => ("f32(1e38)", "min(acc, val)"),
757 _ => ("f32(0.0)", "acc + val"),
758 };
759
760 format!(
761 r#"
762struct FinalReduceParams {{
763 num_groups: u32,
764}}
765
766@group(0) @binding(0) var<storage, read> partial_sums: array<f32>;
767@group(0) @binding(1) var<storage, read_write> output: array<f32>;
768@group(0) @binding(2) var<uniform> params: FinalReduceParams;
769
770var<workgroup> shared_data: array<f32, 256>;
771
772@compute @workgroup_size(256)
773fn main(
774 @builtin(local_invocation_id) lid: vec3<u32>,
775) {{
776 let tid = lid.x;
777
778 // Grid-stride fold: each of the 256 threads accumulates every partial at
779 // index tid, tid+256, tid+512, … Without this, partials beyond index 255
780 // (num_groups > 256, i.e. > 65 536 input elements) would be silently
781 // dropped.
782 var acc: f32 = {neutral};
783 var i: u32 = tid;
784 loop {{
785 if (i >= params.num_groups) {{ break; }}
786 let val = partial_sums[i];
787 acc = {combine};
788 i = i + 256u;
789 }}
790 shared_data[tid] = acc;
791 workgroupBarrier();
792
793 var stride: u32 = 128u;
794 loop {{
795 if (stride == 0u) {{ break; }}
796 if (tid < stride) {{
797 let acc = shared_data[tid];
798 let val = shared_data[tid + stride];
799 shared_data[tid] = {combine};
800 }}
801 workgroupBarrier();
802 stride = stride >> 1u;
803 }}
804
805 if (tid == 0u) {{
806 output[0] = shared_data[0];
807 }}
808}}
809"#,
810 neutral = neutral,
811 combine = combine,
812 )
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818
819 #[test]
820 fn wgsl_gemm_contains_workgroup() {
821 let src = gemm_wgsl(16);
822 assert!(src.contains("@compute @workgroup_size(16, 16)"));
823 assert!(src.contains("GemmParams"));
824 assert!(src.contains("alpha"));
825 assert!(src.contains("beta"));
826 }
827
828 #[test]
829 fn wgsl_gemm_tile_size_embedded() {
830 let src8 = gemm_wgsl(8);
831 assert!(src8.contains("@workgroup_size(8, 8)"));
832 let src32 = gemm_wgsl(32);
833 assert!(src32.contains("@workgroup_size(32, 32)"));
834 }
835
836 #[test]
837 fn wgsl_gemm_has_transpose_flags() {
838 let src = gemm_wgsl(8);
839 assert!(src.contains("trans_a: u32"));
841 assert!(src.contains("trans_b: u32"));
842 assert!(src.contains("lda: u32"));
844 assert!(src.contains("ldb: u32"));
845 assert!(src.contains("ldc: u32"));
846 assert!(src.contains("a[r * params.lda + i]"));
849 assert!(src.contains("a[i * params.lda + r]"));
850 assert!(src.contains("b[i * params.ldb + col]"));
851 assert!(src.contains("b[col * params.ldb + i]"));
852 assert!(src.contains("row * params.ldc + col"));
853 }
854
855 #[test]
856 fn wgsl_gemm_uses_shared_memory_tiling() {
857 let src = gemm_wgsl(16);
858 assert!(src.contains("var<workgroup> tile_a"));
859 assert!(src.contains("var<workgroup> tile_b"));
860 assert!(src.contains("workgroupBarrier"));
861 assert!(src.contains("array<array<f32, 16>, 16>"));
863 }
864
865 #[test]
866 fn wgsl_elementwise_relu_contains_max() {
867 let src = elementwise_wgsl("relu");
868 assert!(src.contains("max(x, 0.0)"));
869 }
870
871 #[test]
872 fn wgsl_elementwise_all_ops() {
873 assert!(elementwise_wgsl("sigmoid").contains("exp(-x)"));
874 assert!(elementwise_wgsl("tanh").contains("tanh(x)"));
875 assert!(elementwise_wgsl("exp").contains("exp(x)"));
876 assert!(elementwise_wgsl("log").contains("log(x)"));
877 assert!(elementwise_wgsl("sqrt").contains("sqrt(x)"));
878 assert!(elementwise_wgsl("abs").contains("abs(x)"));
879 assert!(elementwise_wgsl("neg").contains("-x"));
880 assert!(elementwise_wgsl("identity_op").contains("output[i] = x;"));
882 }
883
884 #[test]
885 fn wgsl_reduction_sum_contains_addition() {
886 let src = reduction_wgsl("sum");
887 assert!(src.contains("acc + val"));
888 assert!(src.contains("workgroupBarrier"));
889 }
890
891 #[test]
892 fn wgsl_reduction_max_uses_max_fn() {
893 let src = reduction_wgsl("max");
894 assert!(src.contains("max(acc, val)"));
895 }
896
897 #[test]
898 fn wgsl_reduction_min_uses_min_fn() {
899 let src = reduction_wgsl("min");
900 assert!(src.contains("min(acc, val)"));
901 }
902
903 #[test]
904 fn wgsl_reduction_mean_same_as_sum() {
905 let sum_src = reduction_wgsl("sum");
907 let mean_src = reduction_wgsl("mean");
908 assert_eq!(sum_src, mean_src);
909 }
910
911 #[test]
912 fn wgsl_reduction_final_sum() {
913 let src = reduction_final_wgsl("sum");
914 assert!(src.contains("num_groups"));
915 assert!(src.contains("output[0]"));
916 }
917
918 #[test]
919 fn wgsl_reduction_final_grid_strides_over_all_groups() {
920 for op in ["sum", "max", "min", "mean"] {
924 let src = reduction_final_wgsl(op);
925 assert!(
926 src.contains("i = i + 256u"),
927 "final reduction for {op} lacks the grid-stride loop"
928 );
929 assert!(
930 src.contains("if (i >= params.num_groups)"),
931 "final reduction for {op} lacks the num_groups loop bound"
932 );
933 }
934 }
935
936 #[test]
939 fn wgsl_reduction_nd_sum_contains_addition() {
940 let src = reduction_nd_wgsl("sum");
941 assert!(src.contains("acc + val"));
942 assert!(src.contains("acc2 + val"));
944 assert!(src.contains("workgroupBarrier"));
945 assert!(src.contains("ReduceNdParams"));
946 }
947
948 #[test]
949 fn wgsl_reduction_nd_max_uses_max_fn() {
950 let src = reduction_nd_wgsl("max");
951 assert!(src.contains("max(acc, val)"));
952 assert!(src.contains("max(acc2, val)"));
953 }
954
955 #[test]
956 fn wgsl_reduction_nd_min_uses_min_fn() {
957 let src = reduction_nd_wgsl("min");
958 assert!(src.contains("min(acc, val)"));
959 assert!(src.contains("min(acc2, val)"));
960 }
961
962 #[test]
963 fn wgsl_reduction_nd_mean_divides_by_dk() {
964 let src = reduction_nd_wgsl("mean");
965 assert!(src.contains("shared_data[0] / f32(params.dk)"));
966 assert!(src.contains("acc + val"));
967 }
968
969 #[test]
970 fn wgsl_reduction_nd_sum_does_not_divide() {
971 let src = reduction_nd_wgsl("sum");
972 assert!(!src.contains("/ f32(params.dk)"));
973 }
974
975 #[test]
976 fn wgsl_reduction_nd_decodes_2d_dispatch() {
977 let src = reduction_nd_wgsl("sum");
978 assert!(src.contains("wgid.y * params.grid_x + wgid.x"));
979 }
980
981 #[test]
982 fn wgsl_reduction_nd_uses_strided_loop() {
983 let src = reduction_nd_wgsl("sum");
984 assert!(src.contains("i = i + 256u"));
985 }
986
987 #[test]
990 fn wgsl_binary_add() {
991 let src = binary_wgsl("add");
992 assert!(src.contains("a + b"));
993 assert!(src.contains("lhs"));
994 assert!(src.contains("rhs"));
995 }
996
997 #[test]
998 fn wgsl_binary_all_ops() {
999 assert!(binary_wgsl("sub").contains("a - b"));
1000 assert!(binary_wgsl("mul").contains("a * b"));
1001 assert!(binary_wgsl("div").contains("a / b"));
1002 assert!(binary_wgsl("max").contains("max(a, b)"));
1003 assert!(binary_wgsl("min").contains("min(a, b)"));
1004 assert!(binary_wgsl("pow").contains("pow(a, b)"));
1005 assert!(binary_wgsl("unknown_op").contains("output[i] = a;"));
1007 }
1008
1009 #[test]
1010 fn wgsl_binary_workgroup_size() {
1011 let src = binary_wgsl("add");
1012 assert!(src.contains("@workgroup_size(256)"));
1013 }
1014
1015 #[test]
1018 fn wgsl_conv2d_contains_workgroup() {
1019 let src = conv2d_wgsl(1, 3, 32, 32, 16, 3, 3, 30, 30, 1, 1, 0, 0);
1020 assert!(src.contains("@compute @workgroup_size(8, 8)"));
1021 }
1022
1023 #[test]
1024 fn wgsl_conv2d_contains_storage_bindings() {
1025 let src = conv2d_wgsl(1, 3, 32, 32, 16, 3, 3, 30, 30, 1, 1, 0, 0);
1026 assert!(src.contains("var<storage, read> input:"));
1027 assert!(src.contains("var<storage, read> kernel_w:"));
1029 assert!(src.contains("var<storage, read_write> output:"));
1030 }
1031
1032 #[test]
1033 fn wgsl_conv2d_embeds_dimensions() {
1034 let src = conv2d_wgsl(2, 8, 64, 64, 32, 5, 5, 60, 60, 1, 1, 0, 0);
1035 assert!(src.contains("8u")); assert!(src.contains("64u")); assert!(src.contains("32u")); assert!(src.contains("5u")); assert!(src.contains("60u")); }
1042
1043 #[test]
1044 fn wgsl_conv2d_has_padding_check() {
1045 let src = conv2d_wgsl(1, 1, 8, 8, 1, 3, 3, 8, 8, 1, 1, 1, 1);
1046 assert!(src.contains("iy_raw >= 0"));
1048 assert!(src.contains("ix_raw >= 0"));
1049 }
1050
1051 #[test]
1052 fn wgsl_conv2d_has_stride() {
1053 let src = conv2d_wgsl(1, 1, 8, 8, 1, 3, 3, 3, 3, 2, 2, 0, 0);
1054 assert!(src.contains("2u")); }
1056
1057 #[test]
1060 fn wgsl_attention_contains_workgroup() {
1061 let src = attention_wgsl(4, 8, 8, 64, 0.125, false);
1062 assert!(src.contains("@compute @workgroup_size(64)"));
1063 }
1064
1065 #[test]
1066 fn wgsl_attention_contains_storage_bindings() {
1067 let src = attention_wgsl(4, 8, 8, 64, 0.125, false);
1068 assert!(src.contains("var<storage, read> q_buf:"));
1069 assert!(src.contains("var<storage, read> k_buf:"));
1070 assert!(src.contains("var<storage, read> v_buf:"));
1071 assert!(src.contains("var<storage, read_write> o_buf:"));
1072 }
1073
1074 #[test]
1075 fn wgsl_attention_stable_softmax() {
1076 let src = attention_wgsl(1, 4, 4, 32, 0.25, false);
1077 assert!(src.contains("max_score"));
1078 assert!(src.contains("exp(score - max_score)"));
1079 assert!(src.contains("sum_exp"));
1080 }
1081
1082 #[test]
1083 fn wgsl_attention_causal_mask() {
1084 let src_causal = attention_wgsl(1, 4, 4, 32, 0.25, true);
1085 assert!(src_causal.contains("sk > sq"));
1086
1087 let src_non_causal = attention_wgsl(1, 4, 4, 32, 0.25, false);
1088 assert!(!src_non_causal.contains("sk > sq"));
1089 }
1090
1091 #[test]
1092 fn wgsl_attention_embeds_scale() {
1093 let src = attention_wgsl(2, 16, 16, 64, 0.125, false);
1094 assert!(src.contains("0.125"));
1095 }
1096
1097 #[test]
1100 fn wgsl_batched_gemm_contains_batch_params() {
1101 let src = batched_gemm_wgsl(16);
1102 assert!(src.contains("batch_count"));
1103 assert!(src.contains("stride_a"));
1104 assert!(src.contains("stride_b"));
1105 assert!(src.contains("stride_c"));
1106 }
1107
1108 #[test]
1109 fn wgsl_batched_gemm_contains_workgroup() {
1110 let src = batched_gemm_wgsl(16);
1111 assert!(src.contains("@compute @workgroup_size(16, 16)"));
1112 assert!(src.contains("BatchedGemmParams"));
1113 }
1114
1115 #[test]
1116 fn wgsl_batched_gemm_uses_batch_index() {
1117 let src = batched_gemm_wgsl(8);
1118 assert!(src.contains("batch_index"));
1119 assert!(src.contains("gid.z"));
1120 }
1121
1122 #[test]
1123 fn wgsl_batched_gemm_tile_size_embedded() {
1124 let src8 = batched_gemm_wgsl(8);
1125 assert!(src8.contains("@workgroup_size(8, 8)"));
1126 let src32 = batched_gemm_wgsl(32);
1127 assert!(src32.contains("@workgroup_size(32, 32)"));
1128 }
1129
1130 #[test]
1131 fn wgsl_batched_gemm_has_transpose_flags() {
1132 let src = batched_gemm_wgsl(8);
1133 assert!(src.contains("trans_a: u32"));
1134 assert!(src.contains("trans_b: u32"));
1135 assert!(src.contains("lda: u32"));
1136 assert!(src.contains("ldb: u32"));
1137 assert!(src.contains("ldc: u32"));
1138 assert!(src.contains("a[a_offset + r * params.lda + i]"));
1141 assert!(src.contains("a[a_offset + i * params.lda + r]"));
1142 assert!(src.contains("b[b_offset + i * params.ldb + col]"));
1143 assert!(src.contains("b[b_offset + col * params.ldb + i]"));
1144 assert!(src.contains("row * params.ldc + col"));
1145 }
1146
1147 #[test]
1148 fn wgsl_batched_gemm_uses_shared_memory_tiling() {
1149 let src = batched_gemm_wgsl(8);
1150 assert!(src.contains("var<workgroup> tile_a"));
1151 assert!(src.contains("var<workgroup> tile_b"));
1152 assert!(src.contains("workgroupBarrier"));
1153 assert!(src.contains("array<array<f32, 8>, 8>"));
1154 }
1155
1156 #[test]
1159 fn wgsl_gemm_f16_enables_extension() {
1160 let src = gemm_wgsl_f16(16);
1161 assert!(src.contains("enable f16;"));
1162 }
1163
1164 #[test]
1165 fn wgsl_gemm_f16_uses_f16_storage() {
1166 let src = gemm_wgsl_f16(16);
1167 assert!(src.contains("array<f16>"));
1168 }
1169
1170 #[test]
1171 fn wgsl_gemm_f16_accumulates_in_f32() {
1172 let src = gemm_wgsl_f16(16);
1173 assert!(src.contains("var acc: f32 = 0.0;"));
1174 assert!(src.contains("f32(a["));
1175 assert!(src.contains("f32(b["));
1176 }
1177
1178 #[test]
1179 fn wgsl_gemm_f16_contains_workgroup() {
1180 let src = gemm_wgsl_f16(8);
1181 assert!(src.contains("@compute @workgroup_size(8, 8)"));
1182 assert!(src.contains("GemmParams"));
1183 }
1184
1185 #[test]
1186 fn wgsl_attention_embeds_dimensions() {
1187 let src = attention_wgsl(8, 32, 32, 128, 0.088, true);
1188 assert!(src.contains("128u")); assert!(src.contains("32u")); assert!(src.contains("8u")); }
1192}