1use std::fmt::Write;
2
3use super::GeneratedShader;
4use crate::wgsl_forge::{ForgeError, KernelSpec, Op, Schedule, FORGE_SCHEMA_VERSION};
5
6pub fn emit_wgsl(kernel: &KernelSpec, schedule: Schedule) -> Result<GeneratedShader, ForgeError> {
7 kernel.validate()?;
8 let semantic_hash = kernel.semantic_hash()?;
9 let mut source = String::with_capacity(2_048);
10 writeln!(
11 source,
12 "// Generated by Qualia WGSL Forge schema {FORGE_SCHEMA_VERSION}."
13 )
14 .map_err(|error| ForgeError::Emission(error.to_string()))?;
15 writeln!(
16 source,
17 "// Kernel: {}@{}",
18 kernel.id, kernel.semantic_version
19 )
20 .map_err(|error| ForgeError::Emission(error.to_string()))?;
21 writeln!(source, "// Semantic hash: {semantic_hash}")
22 .map_err(|error| ForgeError::Emission(error.to_string()))?;
23 writeln!(
24 source,
25 "// Schedule: workgroup={}, items={}, vector={}",
26 schedule.workgroup_size, schedule.items_per_invocation, schedule.vector_width
27 )
28 .map_err(|error| ForgeError::Emission(error.to_string()))?;
29
30 emit_kernel_body(&mut source, kernel, schedule)?;
31
32 let source_hash = blake3::hash(source.as_bytes()).to_hex().to_string();
33 Ok(GeneratedShader {
34 kernel_id: kernel.id.clone(),
35 semantic_hash,
36 source_hash,
37 schedule,
38 source,
39 })
40}
41
42fn emit_kernel_body(
43 source: &mut String,
44 kernel: &KernelSpec,
45 schedule: Schedule,
46) -> Result<(), ForgeError> {
47 if kernel.id == "topk" {
48 return emit_topk_wgsl(source, kernel, schedule);
49 }
50
51 if kernel.id == "fused-ffn" {
52 return emit_ffn_wgsl(source, kernel, schedule);
53 }
54
55 if kernel.id == "p64-project" {
56 return emit_p64_wgsl(source, kernel, schedule);
57 }
58
59 if kernel.id == "ternary-gemv" {
60 return emit_ternary_gemv_wgsl(source, kernel, schedule);
61 }
62
63 if matches!(kernel.id.as_str(), "gemm" | "gemv" | "fft") {
71 let graph = kernel.to_graph()?;
72 let mut lowerer = WgslDelegateLowerer {
73 source,
74 kernel,
75 schedule,
76 };
77 return crate::wgsl_forge::ir::graph::lower_graph(&graph, &mut lowerer);
78 }
79
80 if kernel.id == "ray-probe" {
81 writeln!(source, "enable wgpu_ray_query;")
83 .map_err(|error| ForgeError::Emission(error.to_string()))?;
84 }
85
86 if kernel.id == "affine-f32" {
87 writeln!(
88 source,
89 r#"
90struct AffineParams {{
91 length: u32,
92 scale: f32,
93 bias: f32,
94 _pad: u32,
95}}"#
96 )
97 .map_err(|error| ForgeError::Emission(error.to_string()))?;
98 }
99
100 writeln!(source, "").map_err(|error| ForgeError::Emission(error.to_string()))?;
101 for buffer in &kernel.buffers {
102 if buffer.element == crate::wgsl_forge::ir::BufferElement::AccelerationStructure {
104 writeln!(
105 source,
106 "@group({}) @binding({}) var {}: acceleration_structure;",
107 buffer.group, buffer.binding, buffer.name
108 )
109 .map_err(|error| ForgeError::Emission(error.to_string()))?;
110 continue;
111 }
112 let access = match buffer.access {
113 crate::wgsl_forge::ir::BufferAccess::StorageRead => "storage, read",
114 crate::wgsl_forge::ir::BufferAccess::StorageReadWrite => "storage, read_write",
115 crate::wgsl_forge::ir::BufferAccess::Uniform => "uniform",
116 };
117 let type_decl = match buffer.element {
118 crate::wgsl_forge::ir::BufferElement::Scalar(
119 crate::wgsl_forge::ir::ScalarType::F32,
120 ) => "array<f32>",
121 crate::wgsl_forge::ir::BufferElement::Scalar(
122 crate::wgsl_forge::ir::ScalarType::U32,
123 ) => "array<u32>",
124 crate::wgsl_forge::ir::BufferElement::Scalar(
125 crate::wgsl_forge::ir::ScalarType::I32,
126 ) => "array<i32>",
127 crate::wgsl_forge::ir::BufferElement::Scalar(
128 crate::wgsl_forge::ir::ScalarType::U64Words,
129 ) => "array<vec2<u32>>",
130 crate::wgsl_forge::ir::BufferElement::AffineParams => "AffineParams",
131 crate::wgsl_forge::ir::BufferElement::P64Words64 => "array<P64Words64>",
132 crate::wgsl_forge::ir::BufferElement::AccelerationStructure => {
133 unreachable!("handled above")
134 }
135 };
136 writeln!(
137 source,
138 "@group({}) @binding({}) var<{}> {}: {};",
139 buffer.group, buffer.binding, access, buffer.name, type_decl
140 )
141 .map_err(|error| ForgeError::Emission(error.to_string()))?;
142 }
143
144 writeln!(
145 source,
146 r#"
147const ITEMS_PER_INVOCATION: u32 = {}u;
148const VECTOR_WIDTH: u32 = {}u;
149
150@compute @workgroup_size({})
151fn {}(@builtin(global_invocation_id) gid: vec3<u32>) {{"#,
152 schedule.items_per_invocation,
153 schedule.vector_width,
154 schedule.workgroup_size,
155 kernel.entry_point
156 )
157 .map_err(|error| ForgeError::Emission(error.to_string()))?;
158
159 if kernel.id == "ray-probe" {
160 writeln!(source, " let i = gid.x;")
162 .map_err(|error| ForgeError::Emission(error.to_string()))?;
163 writeln!(source, " if (i >= arrayLength(&hits)) {{ return; }}")
164 .map_err(|error| ForgeError::Emission(error.to_string()))?;
165 writeln!(source, " let base = i * 8u;")
166 .map_err(|error| ForgeError::Emission(error.to_string()))?;
167 writeln!(
168 source,
169 " let origin = vec3<f32>(rays[base], rays[base + 1u], rays[base + 2u]);"
170 )
171 .map_err(|error| ForgeError::Emission(error.to_string()))?;
172 writeln!(
173 source,
174 " let direction = vec3<f32>(rays[base + 3u], rays[base + 4u], rays[base + 5u]);"
175 )
176 .map_err(|error| ForgeError::Emission(error.to_string()))?;
177 writeln!(source, " let t_min = rays[base + 6u];")
178 .map_err(|error| ForgeError::Emission(error.to_string()))?;
179 writeln!(source, " let t_max = rays[base + 7u];")
180 .map_err(|error| ForgeError::Emission(error.to_string()))?;
181 emit_ops(source, &kernel.ops, " ")?;
182 writeln!(source, " hits[i] = hit_t;")
183 .map_err(|error| ForgeError::Emission(error.to_string()))?;
184 writeln!(source, "}}").map_err(|error| ForgeError::Emission(error.to_string()))?;
185 } else if kernel.id == "affine-f32" {
186 writeln!(
188 source,
189 " for (var item: u32 = 0u; item < ITEMS_PER_INVOCATION; item = item + 1u) {{"
190 )
191 .map_err(|error| ForgeError::Emission(error.to_string()))?;
192 writeln!(
193 source,
194 " let global_id = (gid.x * ITEMS_PER_INVOCATION + item) * VECTOR_WIDTH;"
195 )
196 .map_err(|error| ForgeError::Emission(error.to_string()))?;
197
198 if schedule.vector_width == 1 {
199 writeln!(source, " if (global_id < params.length) {{")
200 .map_err(|error| ForgeError::Emission(error.to_string()))?;
201 emit_ops(source, &kernel.ops, " ")?;
202 writeln!(source, " }}")
203 .map_err(|error| ForgeError::Emission(error.to_string()))?;
204 } else {
205 writeln!(
206 source,
207 " if (global_id + {}u < params.length) {{",
208 schedule.vector_width - 1
209 )
210 .map_err(|error| ForgeError::Emission(error.to_string()))?;
211 let constructor = (0..schedule.vector_width)
212 .map(|index| format!("input[global_id + {index}u]"))
213 .collect::<Vec<_>>()
214 .join(", ");
215 writeln!(
216 source,
217 " let value = vec{}<f32>({});",
218 schedule.vector_width, constructor
219 )
220 .map_err(|error| ForgeError::Emission(error.to_string()))?;
221 writeln!(source, " let transformed = value * vec{}<f32>(params.scale) + vec{}<f32>(params.bias);", schedule.vector_width, schedule.vector_width).map_err(|error| ForgeError::Emission(error.to_string()))?;
222 for index in 0..schedule.vector_width {
223 let components = ["x", "y", "z", "w"];
224 writeln!(
225 source,
226 " output[global_id + {index}u] = transformed.{};",
227 components[index as usize]
228 )
229 .map_err(|error| ForgeError::Emission(error.to_string()))?;
230 }
231 writeln!(source, " }} else {{")
232 .map_err(|error| ForgeError::Emission(error.to_string()))?;
233 writeln!(source, " for (var component: u32 = 0u; component < VECTOR_WIDTH; component = component + 1u) {{").map_err(|error| ForgeError::Emission(error.to_string()))?;
234 writeln!(
235 source,
236 " let global_id = global_id + component;"
237 )
238 .map_err(|error| ForgeError::Emission(error.to_string()))?;
239 writeln!(source, " if (global_id < params.length) {{")
240 .map_err(|error| ForgeError::Emission(error.to_string()))?;
241 emit_ops(source, &kernel.ops, " ")?;
242 writeln!(source, " }}")
243 .map_err(|error| ForgeError::Emission(error.to_string()))?;
244 writeln!(source, " }}")
245 .map_err(|error| ForgeError::Emission(error.to_string()))?;
246 writeln!(source, " }}")
247 .map_err(|error| ForgeError::Emission(error.to_string()))?;
248 }
249 writeln!(source, " }}\n}}").map_err(|error| ForgeError::Emission(error.to_string()))?;
250 } else {
251 writeln!(
253 source,
254 " for (var item: u32 = 0u; item < ITEMS_PER_INVOCATION; item = item + 1u) {{"
255 )
256 .map_err(|error| ForgeError::Emission(error.to_string()))?;
257 writeln!(
258 source,
259 " let global_id = gid.x * ITEMS_PER_INVOCATION + item;"
260 )
261 .map_err(|error| ForgeError::Emission(error.to_string()))?;
262 emit_ops(source, &kernel.ops, " ")?;
263 writeln!(source, " }}\n}}").map_err(|error| ForgeError::Emission(error.to_string()))?;
264 }
265
266 Ok(())
267}
268
269fn emit_topk_wgsl(
277 source: &mut String,
278 kernel: &KernelSpec,
279 schedule: Schedule,
280) -> Result<(), ForgeError> {
281 let wg = schedule.workgroup_size;
282
283 writeln!(
284 source,
285 r#"
286struct TopKParams {{
287 length: u32,
288 k: u32,
289 block_size: u32,
290 _pad: u32,
291}}
292"#
293 )
294 .map_err(|error| ForgeError::Emission(error.to_string()))?;
295
296 for buffer in &kernel.buffers {
297 let access = match buffer.access {
298 crate::wgsl_forge::ir::BufferAccess::StorageRead => "storage, read",
299 crate::wgsl_forge::ir::BufferAccess::StorageReadWrite => "storage, read_write",
300 crate::wgsl_forge::ir::BufferAccess::Uniform => "uniform",
301 };
302 let type_decl = match buffer.access {
303 crate::wgsl_forge::ir::BufferAccess::Uniform => "TopKParams",
304 _ => "array<f32>",
305 };
306 writeln!(
307 source,
308 "@group({}) @binding({}) var<{}> {}: {};",
309 buffer.group, buffer.binding, access, buffer.name, type_decl
310 )
311 .map_err(|error| ForgeError::Emission(error.to_string()))?;
312 }
313
314 writeln!(source, "").map_err(|error| ForgeError::Emission(error.to_string()))?;
315 for shared in &kernel.shared_memory {
316 writeln!(
317 source,
318 "var<workgroup> {}: array<{}, {}>;",
319 shared.name,
320 shared.element.wgsl_name(),
321 shared.length.resolve(wg)
322 )
323 .map_err(|error| ForgeError::Emission(error.to_string()))?;
324 }
325
326 writeln!(
327 source,
328 r#"
329@compute @workgroup_size({wg})
330fn {entry}(
331 @builtin(local_invocation_id) lid: vec3<u32>,
332 @builtin(workgroup_id) wid: vec3<u32>,
333) {{
334 let tid = lid.x;
335 let block = wid.x;
336 let base = block * {wg}u;
337 let gidx = base + tid;
338 // Sentinel = f32::MIN (0xff7fffff); threads past the input load it so they
339 // never win, and selected slots are reset to it between extractions.
340 let sentinel = bitcast<f32>(0xff7fffffu);
341 var v = sentinel;
342 if (gidx < params.length) {{
343 v = input[gidx];
344 }}
345 s_val[tid] = v;
346 s_idx[tid] = tid;
347 workgroupBarrier();
348
349 for (var i: u32 = 0u; i < params.k; i = i + 1u) {{
350 r_val[tid] = s_val[tid];
351 r_idx[tid] = s_idx[tid];
352 workgroupBarrier();
353 // Tree arg-max reduction over the working copy.
354 for (var stride: u32 = {wg}u / 2u; stride > 0u; stride = stride / 2u) {{
355 if (tid < stride) {{
356 if (r_val[tid + stride] > r_val[tid]) {{
357 r_val[tid] = r_val[tid + stride];
358 r_idx[tid] = r_idx[tid + stride];
359 }}
360 }}
361 workgroupBarrier();
362 }}
363 if (tid == 0u) {{
364 output[block * params.k + i] = r_val[0];
365 s_val[r_idx[0]] = sentinel;
366 }}
367 workgroupBarrier();
368 }}
369}}"#,
370 wg = wg,
371 entry = kernel.entry_point
372 )
373 .map_err(|error| ForgeError::Emission(error.to_string()))?;
374
375 Ok(())
376}
377
378fn emit_ffn_wgsl(
382 source: &mut String,
383 kernel: &KernelSpec,
384 schedule: Schedule,
385) -> Result<(), ForgeError> {
386 let wg = schedule.workgroup_size;
387 writeln!(
388 source,
389 r#"
390struct FfnParams {{
391 input_size: u32,
392 hidden_size: u32,
393 output_size: u32,
394 _pad: u32,
395}}
396
397@group(0) @binding(0) var<storage, read> input: array<f32>;
398@group(0) @binding(1) var<storage, read> w1: array<f32>;
399@group(0) @binding(2) var<storage, read> w2: array<f32>;
400@group(0) @binding(3) var<storage, read_write> output: array<f32>;
401@group(0) @binding(4) var<uniform> params: FfnParams;
402
403@compute @workgroup_size({wg})
404fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
405 let o = gid.x;
406 if (o >= params.output_size) {{ return; }}
407 var acc = 0.0;
408 for (var h: u32 = 0u; h < params.hidden_size; h = h + 1u) {{
409 var hv = 0.0;
410 let w1_row = h * params.input_size;
411 for (var i: u32 = 0u; i < params.input_size; i = i + 1u) {{
412 hv = hv + w1[w1_row + i] * input[i];
413 }}
414 // GELU(hv)
415 let g = 0.5 * hv * (1.0 + tanh(0.7978845608 * (hv + 0.044715 * hv * hv * hv)));
416 acc = acc + w2[o * params.hidden_size + h] * g;
417 }}
418 output[o] = acc;
419}}"#,
420 wg = wg,
421 entry = kernel.entry_point
422 )
423 .map_err(|error| ForgeError::Emission(error.to_string()))?;
424 Ok(())
425}
426
427fn emit_p64_wgsl(
430 source: &mut String,
431 kernel: &KernelSpec,
432 schedule: Schedule,
433) -> Result<(), ForgeError> {
434 let wg = schedule.workgroup_size;
435 writeln!(
436 source,
437 r#"
438struct P64Words64 {{
439 lanes: array<vec4<u32>, 4>,
440}}
441
442@group(0) @binding(0) var<storage, read> input: array<P64Words64>;
443@group(0) @binding(1) var<storage, read> weights: array<f32>;
444@group(0) @binding(2) var<storage, read_write> output: array<f32>;
445
446@compute @workgroup_size({wg})
447fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
448 let r = gid.x;
449 if (r >= arrayLength(&output)) {{ return; }}
450 let rec = input[r];
451 var acc = 0.0;
452 for (var w: u32 = 0u; w < 16u; w = w + 1u) {{
453 let word = rec.lanes[w / 4u][w % 4u];
454 acc = acc + weights[w] * f32(word);
455 }}
456 output[r] = acc;
457}}"#,
458 wg = wg,
459 entry = kernel.entry_point
460 )
461 .map_err(|error| ForgeError::Emission(error.to_string()))?;
462 Ok(())
463}
464
465fn emit_ternary_gemv_wgsl(
472 source: &mut String,
473 kernel: &KernelSpec,
474 schedule: Schedule,
475) -> Result<(), ForgeError> {
476 let wg = schedule.workgroup_size;
477 writeln!(
478 source,
479 r#"
480struct TernaryGemvParams {{
481 m: u32,
482 k: u32,
483 k_words: u32,
484 _pad: u32,
485}}
486
487@group(0) @binding(0) var<storage, read> x: array<f32>;
488@group(0) @binding(1) var<storage, read> w_packed: array<u32>;
489@group(0) @binding(2) var<storage, read> scale: array<f32>;
490@group(0) @binding(3) var<storage, read_write> output: array<f32>;
491@group(0) @binding(4) var<uniform> params: TernaryGemvParams;
492
493@compute @workgroup_size({wg})
494fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
495 let o = gid.x;
496 if (o >= params.m) {{ return; }}
497 var acc = 0.0;
498 let row_base = o * params.k_words;
499 for (var word_idx: u32 = 0u; word_idx < params.k_words; word_idx = word_idx + 1u) {{
500 let word = w_packed[row_base + word_idx];
501 let lane_base = word_idx * 16u;
502 for (var lane: u32 = 0u; lane < 16u; lane = lane + 1u) {{
503 let i = lane_base + lane;
504 if (i >= params.k) {{ break; }}
505 // Extract the 2-bit ternary code for this lane (low-to-high).
506 let code = (word >> (lane * 2u)) & 3u;
507 // Map code -> value: 0->0.0, 1->+1.0, 2->-1.0, 3->0.0 (unused).
508 var tern = 0.0;
509 if (code == 1u) {{
510 tern = 1.0;
511 }} else if (code == 2u) {{
512 tern = -1.0;
513 }}
514 acc = acc + tern * x[i];
515 }}
516 }}
517 output[o] = scale[o] * acc;
518}}"#,
519 wg = wg,
520 entry = kernel.entry_point
521 )
522 .map_err(|error| ForgeError::Emission(error.to_string()))?;
523 Ok(())
524}
525
526struct WgslDelegateLowerer<'a> {
538 source: &'a mut String,
539 kernel: &'a KernelSpec,
540 schedule: Schedule,
541}
542
543impl crate::wgsl_forge::ir::graph::Lowerer for WgslDelegateLowerer<'_> {
544 fn matmul(
545 &mut self,
546 _node: &crate::wgsl_forge::ir::graph::GraphNode,
547 ) -> Result<(), ForgeError> {
548 emit_gemm_wgsl(self.source, self.kernel, self.schedule)
549 }
550 fn gemv(&mut self, _node: &crate::wgsl_forge::ir::graph::GraphNode) -> Result<(), ForgeError> {
551 emit_gemv_wgsl(self.source, self.kernel, self.schedule)
552 }
553 fn fft(&mut self, _node: &crate::wgsl_forge::ir::graph::GraphNode) -> Result<(), ForgeError> {
554 emit_fft_wgsl(self.source, self.kernel, self.schedule)
555 }
556}
557
558struct WgslGraphLowerer<'a> {
563 source: &'a mut String,
564}
565
566impl crate::wgsl_forge::ir::graph::Lowerer for WgslGraphLowerer<'_> {
567 fn reduce(&mut self, node: &crate::wgsl_forge::ir::graph::GraphNode) -> Result<(), ForgeError> {
568 use crate::wgsl_forge::ir::graph::OpNode;
569 if let OpNode::Reduce { op, .. } = node.op {
570 self.source
571 .push_str(&crate::wgsl_forge::graph_ops::reduce::reduce_wgsl(
572 op,
573 node.sched.workgroup_size,
574 ));
575 Ok(())
576 } else {
577 Err(ForgeError::Emission(
578 "WgslGraphLowerer::reduce received a non-Reduce node".to_string(),
579 ))
580 }
581 }
582
583 fn broadcast(
584 &mut self,
585 node: &crate::wgsl_forge::ir::graph::GraphNode,
586 ) -> Result<(), ForgeError> {
587 self.source
588 .push_str(&crate::wgsl_forge::graph_ops::broadcast::broadcast_wgsl(
589 node.sched.workgroup_size,
590 ));
591 Ok(())
592 }
593
594 fn elementwise(
595 &mut self,
596 node: &crate::wgsl_forge::ir::graph::GraphNode,
597 ) -> Result<(), ForgeError> {
598 use crate::wgsl_forge::ir::graph::OpNode;
599 if let OpNode::Elementwise { f } = node.op {
600 let src = crate::wgsl_forge::graph_ops::elementwise::elementwise_wgsl(
601 f,
602 node.sched.workgroup_size,
603 )?;
604 self.source.push_str(&src);
605 Ok(())
606 } else {
607 Err(ForgeError::Emission(
608 "WgslGraphLowerer::elementwise received a non-Elementwise node".to_string(),
609 ))
610 }
611 }
612}
613
614pub fn emit_graph_wgsl(
618 graph: &crate::wgsl_forge::ir::graph::ComputeGraph,
619 schedule: Schedule,
620) -> Result<GeneratedShader, ForgeError> {
621 let mut source = String::with_capacity(1_024);
622 writeln!(
623 source,
624 "// Generated by Qualia WGSL Forge schema {FORGE_SCHEMA_VERSION} (compute-graph)."
625 )
626 .map_err(|error| ForgeError::Emission(error.to_string()))?;
627 let mut lowerer = WgslGraphLowerer {
628 source: &mut source,
629 };
630 crate::wgsl_forge::ir::graph::lower_graph(graph, &mut lowerer)?;
631 let source_hash = blake3::hash(source.as_bytes()).to_hex().to_string();
632 Ok(GeneratedShader {
633 kernel_id: "graph".to_string(),
634 semantic_hash: source_hash.clone(),
635 source_hash,
636 schedule,
637 source,
638 })
639}
640
641fn emit_gemm_wgsl(
642 source: &mut String,
643 kernel: &KernelSpec,
644 schedule: Schedule,
645) -> Result<(), ForgeError> {
646 let wg = schedule.workgroup_size;
647 writeln!(
648 source,
649 r#"
650struct GemmParams {{
651 m: u32,
652 n: u32,
653 k: u32,
654 _pad: u32,
655}}
656
657@group(0) @binding(0) var<storage, read> a: array<f32>;
658@group(0) @binding(1) var<storage, read> b: array<f32>;
659@group(0) @binding(2) var<storage, read_write> c: array<f32>;
660@group(0) @binding(3) var<uniform> params: GemmParams;
661
662@compute @workgroup_size({wg})
663fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
664 let o = gid.x;
665 if (o >= params.m * params.n) {{ return; }}
666 let row = o / params.n;
667 let col = o % params.n;
668 var acc = 0.0;
669 let a_row = row * params.k;
670 for (var kk: u32 = 0u; kk < params.k; kk = kk + 1u) {{
671 acc = acc + a[a_row + kk] * b[kk * params.n + col];
672 }}
673 c[o] = acc;
674}}"#,
675 wg = wg,
676 entry = kernel.entry_point
677 )
678 .map_err(|error| ForgeError::Emission(error.to_string()))?;
679 Ok(())
680}
681
682fn emit_gemv_wgsl(
687 source: &mut String,
688 kernel: &KernelSpec,
689 schedule: Schedule,
690) -> Result<(), ForgeError> {
691 let wg = schedule.workgroup_size;
692 writeln!(
693 source,
694 r#"
695struct GemvParams {{
696 m: u32,
697 n: u32,
698 _pad0: u32,
699 _pad1: u32,
700}}
701
702@group(0) @binding(0) var<storage, read> a: array<f32>;
703@group(0) @binding(1) var<storage, read> x: array<f32>;
704@group(0) @binding(2) var<storage, read_write> y: array<f32>;
705@group(0) @binding(3) var<uniform> params: GemvParams;
706
707@compute @workgroup_size({wg})
708fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
709 let i = gid.x;
710 if (i >= params.m) {{ return; }}
711 var acc = 0.0;
712 let a_row = i * params.n;
713 for (var j: u32 = 0u; j < params.n; j = j + 1u) {{
714 acc = acc + a[a_row + j] * x[j];
715 }}
716 y[i] = acc;
717}}"#,
718 wg = wg,
719 entry = kernel.entry_point
720 )
721 .map_err(|error| ForgeError::Emission(error.to_string()))?;
722 Ok(())
723}
724
725fn emit_fft_wgsl(
737 source: &mut String,
738 kernel: &KernelSpec,
739 schedule: Schedule,
740) -> Result<(), ForgeError> {
741 let wg = schedule.workgroup_size;
742
743 writeln!(
744 source,
745 r#"
746struct FftParams {{
747 n: u32,
748 log2n: u32,
749 _pad0: u32,
750 _pad1: u32,
751}}
752"#
753 )
754 .map_err(|error| ForgeError::Emission(error.to_string()))?;
755
756 for buffer in &kernel.buffers {
757 let access = match buffer.access {
758 crate::wgsl_forge::ir::BufferAccess::StorageRead => "storage, read",
759 crate::wgsl_forge::ir::BufferAccess::StorageReadWrite => "storage, read_write",
760 crate::wgsl_forge::ir::BufferAccess::Uniform => "uniform",
761 };
762 let type_decl = match buffer.access {
763 crate::wgsl_forge::ir::BufferAccess::Uniform => "FftParams",
764 _ => "array<f32>",
765 };
766 writeln!(
767 source,
768 "@group({}) @binding({}) var<{}> {}: {};",
769 buffer.group, buffer.binding, access, buffer.name, type_decl
770 )
771 .map_err(|error| ForgeError::Emission(error.to_string()))?;
772 }
773
774 writeln!(source, "").map_err(|error| ForgeError::Emission(error.to_string()))?;
775 for shared in &kernel.shared_memory {
776 writeln!(
777 source,
778 "var<workgroup> {}: array<{}, {}>;",
779 shared.name,
780 shared.element.wgsl_name(),
781 shared.length.resolve(wg)
782 )
783 .map_err(|error| ForgeError::Emission(error.to_string()))?;
784 }
785
786 writeln!(
790 source,
791 r#"
792@compute @workgroup_size({wg})
793fn {entry}(
794 @builtin(local_invocation_id) local_id: vec3<u32>,
795 @builtin(global_invocation_id) global_id: vec3<u32>,
796) {{
797 let t = local_id.x; // thread index 0..N-1
798 let n = params.n;
799 let logn = params.log2n;
800 // 1. Bit-reversal load: element t goes to reverse(t).
801 let rev = reverseBits(t) >> (32u - logn);
802 s_re[rev] = input[2u * t];
803 s_im[rev] = input[2u * t + 1u];
804 workgroupBarrier();
805 // 2. log2(N) butterfly stages. Stage s: span = 1<<s, m = 2*span.
806 // N/2 butterflies; threads 0..N/2-1 active.
807 for (var s: u32 = 0u; s < logn; s = s + 1u) {{
808 let span = 1u << s;
809 let m = span << 1u;
810 if (t < (n >> 1u)) {{
811 let k = t & (span - 1u); // position within the butterfly group
812 let j = ((t >> s) << (s + 1u)) + k; // lower index = (t/span)*m + k
813 let jp = j + span; // upper index
814 // twiddle w = exp(-2*pi*i*k/m)
815 let ang = -6.28318548f * f32(k) / f32(m);
816 let wr = cos(ang);
817 let wi = sin(ang);
818 let ur = s_re[j];
819 let ui = s_im[j];
820 let vr = s_re[jp];
821 let vi = s_im[jp];
822 // v' = v * w
823 let tr = vr * wr - vi * wi;
824 let ti = vr * wi + vi * wr;
825 s_re[j] = ur + tr;
826 s_im[j] = ui + ti;
827 s_re[jp] = ur - tr;
828 s_im[jp] = ui - ti;
829 }}
830 workgroupBarrier();
831 }}
832 // 3. Store.
833 output[2u * t] = s_re[t];
834 output[2u * t + 1u] = s_im[t];
835}}"#,
836 wg = wg,
837 entry = kernel.entry_point
838 )
839 .map_err(|error| ForgeError::Emission(error.to_string()))?;
840
841 Ok(())
842}
843
844fn emit_ops(source: &mut String, ops: &[Op], indent: &str) -> Result<(), ForgeError> {
845 for op in ops {
846 match op {
847 Op::StructLoad {
848 buffer,
849 field,
850 destination,
851 } => {
852 writeln!(source, "{indent}let {destination} = {buffer}.{field};")
853 .map_err(|error| ForgeError::Emission(error.to_string()))?;
854 }
855 Op::Load {
856 buffer,
857 index,
858 destination,
859 } => {
860 writeln!(source, "{indent}let {destination} = {buffer}[{index}];")
861 .map_err(|error| ForgeError::Emission(error.to_string()))?;
862 }
863 Op::Store {
864 buffer,
865 index,
866 value,
867 } => {
868 writeln!(source, "{indent}{buffer}[{index}] = {value};")
869 .map_err(|error| ForgeError::Emission(error.to_string()))?;
870 }
871 Op::Fma {
872 a,
873 b,
874 c,
875 destination,
876 } => {
877 writeln!(source, "{indent}let {destination} = {a} * {b} + {c};")
878 .map_err(|error| ForgeError::Emission(error.to_string()))?;
879 }
880 Op::Mul {
881 left,
882 right,
883 destination,
884 } => {
885 writeln!(source, "{indent}let {destination} = {left} * {right};")
886 .map_err(|error| ForgeError::Emission(error.to_string()))?;
887 }
888 Op::Add {
889 left,
890 right,
891 destination,
892 } => {
893 writeln!(source, "{indent}let {destination} = {left} + {right};")
894 .map_err(|error| ForgeError::Emission(error.to_string()))?;
895 }
896 Op::DotProduct {
897 left_buffer,
898 left_base,
899 right_buffer,
900 right_base,
901 len,
902 destination,
903 } => {
904 writeln!(source, "{indent}var {destination} = 0.0;")
905 .map_err(|error| ForgeError::Emission(error.to_string()))?;
906 writeln!(
907 source,
908 "{indent}for (var i: u32 = 0u; i < {len}; i = i + 1u) {{"
909 )
910 .map_err(|error| ForgeError::Emission(error.to_string()))?;
911 writeln!(source, "{indent} {destination} = {destination} + {left_buffer}[{left_base} + i] * {right_buffer}[{right_base} + i];").map_err(|error| ForgeError::Emission(error.to_string()))?;
912 writeln!(source, "{indent}}}")
913 .map_err(|error| ForgeError::Emission(error.to_string()))?;
914 }
915 Op::Loop {
916 induction_var,
917 start,
918 end,
919 step,
920 body,
921 } => {
922 writeln!(source, "{indent}for (var {induction_var}: u32 = {start}; {induction_var} < {end}; {induction_var} = {induction_var} + {step}) {{").map_err(|error| ForgeError::Emission(error.to_string()))?;
923 emit_ops(source, body, &format!("{indent} "))?;
924 writeln!(source, "{indent}}}")
925 .map_err(|error| ForgeError::Emission(error.to_string()))?;
926 }
927 Op::Relu {
928 operand,
929 destination,
930 } => {
931 writeln!(source, "{indent}let {destination} = max(0.0, {operand});")
932 .map_err(|error| ForgeError::Emission(error.to_string()))?;
933 }
934 Op::Gelu {
935 operand,
936 destination,
937 } => {
938 writeln!(source, "{indent}let {destination} = 0.5 * {operand} * (1.0 + tanh(0.7978845608 * ({operand} + 0.044715 * {operand} * {operand} * {operand})));").map_err(|error| ForgeError::Emission(error.to_string()))?;
940 }
941 Op::MatrixMultiply { .. } => {
942 return Err(ForgeError::Emission(
946 "Op::MatrixMultiply has no scalar WGSL lowering; use coopmat::matmul_tc_wgsl (cooperative-matrix) or the CUDA WMMA path".to_string(),
947 ));
948 }
949 Op::Barrier => {
950 writeln!(source, "{indent}workgroupBarrier();")
951 .map_err(|error| ForgeError::Emission(error.to_string()))?;
952 }
953 Op::Intrinsic(crate::wgsl_forge::ir::Intrinsic::RayQuery {
954 acceleration_structure,
955 origin,
956 direction,
957 t_min,
958 t_max,
959 destination,
960 }) => {
961 writeln!(source, "{indent}var rq: ray_query;")
965 .map_err(|error| ForgeError::Emission(error.to_string()))?;
966 writeln!(source, "{indent}rayQueryInitialize(&rq, {acceleration_structure}, RayDesc(0u, 0xFFu, {t_min}, {t_max}, {origin}, {direction}));").map_err(|error| ForgeError::Emission(error.to_string()))?;
967 writeln!(
968 source,
969 "{indent}loop {{ if (!rayQueryProceed(&rq)) {{ break; }} }}"
970 )
971 .map_err(|error| ForgeError::Emission(error.to_string()))?;
972 writeln!(
973 source,
974 "{indent}let committed = rayQueryGetCommittedIntersection(&rq);"
975 )
976 .map_err(|error| ForgeError::Emission(error.to_string()))?;
977 writeln!(source, "{indent}var {destination} = -1.0;")
978 .map_err(|error| ForgeError::Emission(error.to_string()))?;
979 writeln!(source, "{indent}if (committed.kind != RAY_QUERY_INTERSECTION_NONE) {{ {destination} = committed.t; }}").map_err(|error| ForgeError::Emission(error.to_string()))?;
980 }
981 Op::Intrinsic(_) => {
982 return Err(ForgeError::Emission(
983 "Intrinsics not implemented for WGSL yet".to_string(),
984 ));
985 }
986 }
987 }
988 Ok(())
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994 use crate::wgsl_forge::{BuiltinKernel, Schedule};
995
996 #[test]
997 fn emission_is_byte_deterministic() {
998 let kernel = BuiltinKernel::AffineF32.spec();
999 let first = emit_wgsl(&kernel, Schedule::default()).unwrap();
1000 let second = emit_wgsl(&kernel, Schedule::default()).unwrap();
1001 assert_eq!(first.source, second.source);
1002 }
1003
1004 #[test]
1010 fn graph_routing_is_byte_equal_for_gemm_gemv_fft() {
1011 let sched = Schedule::default();
1012 let cases: [(&str, KernelSpec); 3] = [
1013 ("gemm", BuiltinKernel::Gemm.spec()),
1014 ("gemv", BuiltinKernel::Gemv.spec()),
1015 ("fft", BuiltinKernel::Fft.spec()),
1016 ];
1017 for (id, kernel) in cases {
1018 let routed = emit_wgsl(&kernel, sched).unwrap();
1019 let mut legacy_body = String::new();
1020 match id {
1021 "gemm" => emit_gemm_wgsl(&mut legacy_body, &kernel, sched).unwrap(),
1022 "gemv" => emit_gemv_wgsl(&mut legacy_body, &kernel, sched).unwrap(),
1023 "fft" => emit_fft_wgsl(&mut legacy_body, &kernel, sched).unwrap(),
1024 _ => unreachable!(),
1025 }
1026 assert!(
1027 routed.source.ends_with(&legacy_body),
1028 "{id}: graph-routed WGSL body must be byte-identical to the legacy leaf body"
1029 );
1030 }
1031 }
1032
1033 #[test]
1037 fn graph_native_reduce_lowers_to_template() {
1038 use crate::wgsl_forge::graph_ops::reduce::{reduce_wgsl, REDUCE_ENTRY};
1039 use crate::wgsl_forge::ir::graph::{
1040 Axis, ComputeGraph, DType, OpNode, RedKind, Shape, TensorRef,
1041 };
1042
1043 let sched = Schedule {
1044 workgroup_size: 256,
1045 ..Default::default()
1046 };
1047 let mut g = ComputeGraph::new();
1048 let inp = TensorRef::external(Shape::new(&[1024]), DType::F32);
1049 g.push(
1050 OpNode::Reduce {
1051 op: RedKind::Sum,
1052 axis: Axis::Last,
1053 },
1054 &[inp],
1055 Shape::new(&[1]),
1056 DType::F32,
1057 sched,
1058 )
1059 .unwrap();
1060 let shader = emit_graph_wgsl(&g, sched).unwrap();
1061 assert!(shader.source.contains(REDUCE_ENTRY));
1062 assert!(
1063 shader.source.ends_with(&reduce_wgsl(RedKind::Sum, 256)),
1064 "native graph emission must equal the reduce template"
1065 );
1066 }
1067
1068 #[test]
1069 fn p64_kernel_lays_64bit_fields_as_paired_u32_words() {
1070 let kernel = BuiltinKernel::P64Project.spec();
1076 let generated = emit_wgsl(&kernel, Schedule::default()).unwrap();
1077 let source = &generated.source;
1078
1079 assert!(
1080 source.contains("lanes: array<vec4<u32>, 4>"),
1081 "expected paired/packed vec4<u32> word layout, got:\n{source}"
1082 );
1083 assert!(
1084 source.contains("var<storage, read> input: array<P64Words64>"),
1085 "expected the P64 record buffer bound as packed u32 words, got:\n{source}"
1086 );
1087 assert!(
1090 source.contains("f32(word)"),
1091 "expected per-u32-word projection, got:\n{source}"
1092 );
1093 assert!(
1094 !source.contains("u64") && !source.contains("f64"),
1095 "the portable p64 kernel must not emit any native 64-bit scalar, got:\n{source}"
1096 );
1097 }
1098}