1use super::core::KernelSpec;
21use crate::wgsl_forge::{ForgeError, Schedule};
22
23pub const MAX_IN: usize = 4;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub struct NodeId(pub u32);
30
31impl NodeId {
32 pub const EXTERNAL: NodeId = NodeId(u32::MAX);
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct TensorId(pub u32);
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub struct Shape {
45 pub dims: [u32; 4],
46 pub rank: u8,
47}
48
49impl Shape {
50 pub fn scalar() -> Self {
52 Shape {
53 dims: [1, 1, 1, 1],
54 rank: 0,
55 }
56 }
57
58 pub fn new(dims: &[u32]) -> Self {
60 let mut d = [1u32; 4];
61 let rank = dims.len().min(4);
62 d[..rank].copy_from_slice(&dims[..rank]);
63 Shape {
64 dims: d,
65 rank: rank as u8,
66 }
67 }
68
69 pub fn elements(&self) -> u64 {
71 self.dims
72 .iter()
73 .take(self.rank.max(1) as usize)
74 .map(|&x| x as u64)
75 .product()
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81pub enum DType {
82 F32,
83 F16,
84 U32,
85 Q4K,
86 Q8_0,
87 Ternary,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum Layout {
93 RowMajor,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub struct TensorRef {
99 pub producer: NodeId,
100 pub tensor: TensorId,
101 pub shape: Shape,
102 pub dtype: DType,
103 pub layout: Layout,
104}
105
106impl TensorRef {
107 pub fn external(shape: Shape, dtype: DType) -> Self {
109 TensorRef {
110 producer: NodeId::EXTERNAL,
111 tensor: TensorId(0),
112 shape,
113 dtype,
114 layout: Layout::RowMajor,
115 }
116 }
117
118 pub fn input(idx: u32, shape: Shape, dtype: DType) -> Self {
122 TensorRef {
123 producer: NodeId::EXTERNAL,
124 tensor: TensorId(idx),
125 shape,
126 dtype,
127 layout: Layout::RowMajor,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
135pub enum EwKind {
136 Mul,
137 Add,
138 Sub,
139 Div,
140 Fma,
141 Scale,
142 Bias,
143 Silu,
144 Gelu,
145 Relu,
146 Exp,
147 RecipSqrt,
148 Recip,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum RedKind {
154 Sum,
155 Max,
156 Mean,
157 L2,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162pub enum Axis {
163 Last,
164 Penultimate,
165 Index(u8),
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
170pub enum StencilKind {
171 Laplacian,
172 Divergence,
173 Advection,
174 RopePair,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum AccumKind {
180 Add,
181 Max,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186pub enum NbKind {
187 Frnn,
189 Knn,
191 Range,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
197pub enum NeighborEnc {
198 Native3D,
200 Project,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq)]
209pub enum OpNode {
210 Elementwise {
211 f: EwKind,
212 },
213 MatMul {
214 m: u32,
215 n: u32,
216 k: u32,
217 tc: bool,
218 trans_b: bool,
219 },
220 Gemv {
221 m: u32,
222 n: u32,
223 tc: bool,
227 },
228 Fft {
229 len: u32,
230 inverse: bool,
231 },
232 Reduce {
233 op: RedKind,
234 axis: Axis,
235 },
236 GatherDequant {
237 scheme: DType,
238 block: u32,
239 },
240 Broadcast {
241 shape: Shape,
242 },
243 Softmax {
244 axis: Axis,
245 },
246 Stencil {
247 kind: StencilKind,
248 halo: u32,
249 axis: Axis,
250 },
251 ScatterAccum {
252 op: AccumKind,
253 },
254 Neighbor {
255 kind: NbKind,
256 k_or_r: f32,
257 dims: u8,
258 enc: NeighborEnc,
259 },
260 Slice {
263 offset: u32,
264 len: u32,
265 },
266 Rope {
271 head_dim: u32,
272 pos: u32,
273 mode: u32,
274 base_bits: u32,
275 },
276 Pool2d {
278 c: u32,
279 h: u32,
280 w: u32,
281 kh: u32,
282 kw: u32,
283 stride_h: u32,
284 stride_w: u32,
285 },
286 Resize2d {
288 c: u32,
289 h_in: u32,
290 w_in: u32,
291 h_out: u32,
292 w_out: u32,
293 },
294 Conv2d {
296 c_in: u32,
297 c_out: u32,
298 h: u32,
299 w: u32,
300 kh: u32,
301 kw: u32,
302 stride_h: u32,
303 stride_w: u32,
304 pad_h: u32,
305 pad_w: u32,
306 },
307}
308
309#[derive(Debug, Clone, PartialEq)]
312pub struct GraphNode {
313 pub op: OpNode,
314 pub ins: [Option<TensorRef>; MAX_IN],
315 pub n_in: u8,
316 pub out: TensorRef,
317 pub sched: Schedule,
318}
319
320#[derive(Debug, Clone, Default)]
324pub struct ComputeGraph {
325 pub nodes: Vec<GraphNode>,
326 pub outputs: Vec<NodeId>,
327}
328
329impl ComputeGraph {
330 pub fn new() -> Self {
332 Self::default()
333 }
334
335 pub fn push(
342 &mut self,
343 op: OpNode,
344 ins: &[TensorRef],
345 out_shape: Shape,
346 out_dtype: DType,
347 sched: Schedule,
348 ) -> Result<TensorRef, ForgeError> {
349 if ins.len() > MAX_IN {
350 return Err(ForgeError::Emission(format!(
351 "ComputeGraph::push: {} inputs exceeds MAX_IN={MAX_IN}",
352 ins.len()
353 )));
354 }
355 for inp in ins {
356 if inp.producer == NodeId::EXTERNAL {
357 continue;
358 }
359 let pidx = inp.producer.0 as usize;
360 if pidx >= self.nodes.len() {
361 return Err(ForgeError::Emission(
362 "ComputeGraph::push: input references a non-existent/future node (would create a cycle)".to_string(),
363 ));
364 }
365 let prod_out = &self.nodes[pidx].out;
366 if prod_out.dtype != inp.dtype || !shapes_compatible(prod_out.shape, inp.shape) {
367 return Err(ForgeError::Emission(
368 "ComputeGraph::push: input edge shape/dtype mismatch vs producer output"
369 .to_string(),
370 ));
371 }
372 }
373 let id = NodeId(self.nodes.len() as u32);
374 let out = TensorRef {
375 producer: id,
376 tensor: TensorId(0),
377 shape: out_shape,
378 dtype: out_dtype,
379 layout: Layout::RowMajor,
380 };
381 let mut arr: [Option<TensorRef>; MAX_IN] = [None; MAX_IN];
382 for (slot, inp) in arr.iter_mut().zip(ins.iter()) {
383 *slot = Some(*inp);
384 }
385 self.nodes.push(GraphNode {
386 op,
387 ins: arr,
388 n_in: ins.len() as u8,
389 out,
390 sched,
391 });
392 Ok(out)
393 }
394
395 pub fn mark_output(&mut self, t: TensorRef) {
397 self.outputs.push(t.producer);
398 }
399
400 pub fn len(&self) -> usize {
402 self.nodes.len()
403 }
404
405 pub fn is_empty(&self) -> bool {
407 self.nodes.is_empty()
408 }
409}
410
411fn shapes_compatible(a: Shape, b: Shape) -> bool {
415 if a == b {
416 return true;
417 }
418 let dynamic = |s: &Shape| s.dims.iter().any(|&d| d == 0);
419 dynamic(&a) || dynamic(&b)
420}
421
422pub trait Lowerer {
426 fn elementwise(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
427 unsupported("elementwise")
428 }
429 fn matmul(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
430 unsupported("matmul")
431 }
432 fn gemv(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
433 unsupported("gemv")
434 }
435 fn fft(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
436 unsupported("fft")
437 }
438 fn reduce(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
439 unsupported("reduce")
440 }
441 fn gather_dequant(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
442 unsupported("gather_dequant")
443 }
444 fn broadcast(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
445 unsupported("broadcast")
446 }
447 fn softmax(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
448 unsupported("softmax")
449 }
450 fn stencil(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
451 unsupported("stencil")
452 }
453 fn scatter_accum(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
454 unsupported("scatter_accum")
455 }
456 fn neighbor(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
457 unsupported("neighbor")
458 }
459 fn slice(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
460 unsupported("slice")
461 }
462 fn rope(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
463 unsupported("rope")
464 }
465 fn pool2d(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
466 unsupported("pool2d")
467 }
468 fn resize2d(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
469 unsupported("resize2d")
470 }
471 fn conv2d(&mut self, _node: &GraphNode) -> Result<(), ForgeError> {
472 unsupported("conv2d")
473 }
474}
475
476fn unsupported(op: &str) -> Result<(), ForgeError> {
477 Err(ForgeError::Emission(format!(
478 "op-node '{op}' has no lowering on this backend yet"
479 )))
480}
481
482pub fn lower_graph<L: Lowerer>(graph: &ComputeGraph, lowerer: &mut L) -> Result<(), ForgeError> {
486 for node in &graph.nodes {
487 match node.op {
488 OpNode::Elementwise { .. } => lowerer.elementwise(node)?,
489 OpNode::MatMul { .. } => lowerer.matmul(node)?,
490 OpNode::Gemv { .. } => lowerer.gemv(node)?,
491 OpNode::Fft { .. } => lowerer.fft(node)?,
492 OpNode::Reduce { .. } => lowerer.reduce(node)?,
493 OpNode::GatherDequant { .. } => lowerer.gather_dequant(node)?,
494 OpNode::Broadcast { .. } => lowerer.broadcast(node)?,
495 OpNode::Softmax { .. } => lowerer.softmax(node)?,
496 OpNode::Stencil { .. } => lowerer.stencil(node)?,
497 OpNode::ScatterAccum { .. } => lowerer.scatter_accum(node)?,
498 OpNode::Neighbor { .. } => lowerer.neighbor(node)?,
499 OpNode::Slice { .. } => lowerer.slice(node)?,
500 OpNode::Rope { .. } => lowerer.rope(node)?,
501 OpNode::Pool2d { .. } => lowerer.pool2d(node)?,
502 OpNode::Resize2d { .. } => lowerer.resize2d(node)?,
503 OpNode::Conv2d { .. } => lowerer.conv2d(node)?,
504 }
505 }
506 Ok(())
507}
508
509impl KernelSpec {
510 pub fn to_graph(&self) -> Result<ComputeGraph, ForgeError> {
516 let mut g = ComputeGraph::new();
517 let sched = Schedule::default();
518 let dyn2 = Shape::new(&[0, 0]);
519 let dyn1 = Shape::new(&[0]);
520 match self.id.as_str() {
521 "gemm" => {
522 let a = TensorRef::external(dyn2, DType::F32);
523 let b = TensorRef::external(dyn2, DType::F32);
524 let out = g.push(
525 OpNode::MatMul {
526 m: 0,
527 n: 0,
528 k: 0,
529 tc: false,
530 trans_b: false,
531 },
532 &[a, b],
533 dyn2,
534 DType::F32,
535 sched,
536 )?;
537 g.mark_output(out);
538 }
539 "gemv" => {
540 let a = TensorRef::external(dyn2, DType::F32);
541 let x = TensorRef::external(dyn1, DType::F32);
542 let out = g.push(
543 OpNode::Gemv {
544 m: 0,
545 n: 0,
546 tc: false,
547 },
548 &[a, x],
549 dyn1,
550 DType::F32,
551 sched,
552 )?;
553 g.mark_output(out);
554 }
555 "fft" => {
556 let inp = TensorRef::external(dyn1, DType::F32);
557 let out = g.push(
558 OpNode::Fft {
559 len: 0,
560 inverse: false,
561 },
562 &[inp],
563 dyn1,
564 DType::F32,
565 sched,
566 )?;
567 g.mark_output(out);
568 }
569 other => {
570 return Err(ForgeError::Emission(format!(
571 "KernelSpec::to_graph: no graph bridge for kernel id '{other}'"
572 )))
573 }
574 }
575 Ok(g)
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::wgsl_forge::ir::BuiltinKernel;
583
584 fn ext(shape: &[u32]) -> TensorRef {
585 TensorRef::external(Shape::new(shape), DType::F32)
586 }
587
588 #[test]
591 fn two_node_chain_builds_in_topo_order() {
592 let mut g = ComputeGraph::new();
593 let a = ext(&[4, 4]);
594 let b = ext(&[4, 4]);
595 let mm = g
596 .push(
597 OpNode::MatMul {
598 m: 4,
599 n: 4,
600 k: 4,
601 tc: false,
602 trans_b: false,
603 },
604 &[a, b],
605 Shape::new(&[4, 4]),
606 DType::F32,
607 Schedule::default(),
608 )
609 .unwrap();
610 assert_eq!(mm.producer, NodeId(0));
611 let relu = g
612 .push(
613 OpNode::Elementwise { f: EwKind::Relu },
614 &[mm],
615 Shape::new(&[4, 4]),
616 DType::F32,
617 Schedule::default(),
618 )
619 .unwrap();
620 assert_eq!(relu.producer, NodeId(1));
621 assert_eq!(g.len(), 2);
622 assert_eq!(g.nodes[1].ins[0].unwrap().producer, NodeId(0));
624 }
625
626 #[test]
628 fn forward_reference_is_rejected() {
629 let mut g = ComputeGraph::new();
630 let phantom = TensorRef {
631 producer: NodeId(7), tensor: TensorId(0),
633 shape: Shape::new(&[2, 2]),
634 dtype: DType::F32,
635 layout: Layout::RowMajor,
636 };
637 let r = g.push(
638 OpNode::Elementwise { f: EwKind::Relu },
639 &[phantom],
640 Shape::new(&[2, 2]),
641 DType::F32,
642 Schedule::default(),
643 );
644 assert!(r.is_err());
645 }
646
647 #[test]
650 fn shape_mismatch_is_rejected_but_dynamic_is_allowed() {
651 let mut g = ComputeGraph::new();
652 let a = ext(&[4, 4]);
653 let b = ext(&[4, 4]);
654 let mm = g
655 .push(
656 OpNode::MatMul {
657 m: 4,
658 n: 4,
659 k: 4,
660 tc: false,
661 trans_b: false,
662 },
663 &[a, b],
664 Shape::new(&[4, 4]),
665 DType::F32,
666 Schedule::default(),
667 )
668 .unwrap();
669 let mut wrong = mm;
671 wrong.shape = Shape::new(&[8, 8]);
672 assert!(g
673 .push(
674 OpNode::Elementwise { f: EwKind::Relu },
675 &[wrong],
676 Shape::new(&[8, 8]),
677 DType::F32,
678 Schedule::default(),
679 )
680 .is_err());
681 let mut dynamic = mm;
683 dynamic.shape = Shape::new(&[0, 0]);
684 assert!(g
685 .push(
686 OpNode::Elementwise { f: EwKind::Relu },
687 &[dynamic],
688 Shape::new(&[4, 4]),
689 DType::F32,
690 Schedule::default(),
691 )
692 .is_ok());
693 }
694
695 #[test]
697 fn too_many_inputs_rejected() {
698 let mut g = ComputeGraph::new();
699 let ins: Vec<TensorRef> = (0..(MAX_IN + 1)).map(|_| ext(&[2])).collect();
700 assert!(g
701 .push(
702 OpNode::Elementwise { f: EwKind::Add },
703 &ins,
704 Shape::new(&[2]),
705 DType::F32,
706 Schedule::default(),
707 )
708 .is_err());
709 }
710
711 #[test]
714 fn to_graph_bridges_seed_kernels() {
715 let gemm = BuiltinKernel::Gemm.spec().to_graph().unwrap();
716 assert_eq!(gemm.len(), 1);
717 assert!(matches!(gemm.nodes[0].op, OpNode::MatMul { .. }));
718
719 let gemv = BuiltinKernel::Gemv.spec().to_graph().unwrap();
720 assert_eq!(gemv.len(), 1);
721 assert!(matches!(gemv.nodes[0].op, OpNode::Gemv { .. }));
722
723 let fft = BuiltinKernel::Fft.spec().to_graph().unwrap();
724 assert_eq!(fft.len(), 1);
725 assert!(matches!(fft.nodes[0].op, OpNode::Fft { .. }));
726
727 assert!(BuiltinKernel::AffineF32.spec().to_graph().is_err());
729 }
730
731 #[test]
734 fn lower_graph_dispatches_in_order() {
735 struct Recorder(Vec<&'static str>);
736 impl Lowerer for Recorder {
737 fn matmul(&mut self, _: &GraphNode) -> Result<(), ForgeError> {
738 self.0.push("matmul");
739 Ok(())
740 }
741 fn elementwise(&mut self, _: &GraphNode) -> Result<(), ForgeError> {
742 self.0.push("elementwise");
743 Ok(())
744 }
745 }
746 let mut g = ComputeGraph::new();
747 let a = ext(&[4, 4]);
748 let b = ext(&[4, 4]);
749 let mm = g
750 .push(
751 OpNode::MatMul {
752 m: 4,
753 n: 4,
754 k: 4,
755 tc: false,
756 trans_b: false,
757 },
758 &[a, b],
759 Shape::new(&[4, 4]),
760 DType::F32,
761 Schedule::default(),
762 )
763 .unwrap();
764 g.push(
765 OpNode::Elementwise { f: EwKind::Silu },
766 &[mm],
767 Shape::new(&[4, 4]),
768 DType::F32,
769 Schedule::default(),
770 )
771 .unwrap();
772 let mut rec = Recorder(Vec::new());
773 lower_graph(&g, &mut rec).unwrap();
774 assert_eq!(rec.0, vec!["matmul", "elementwise"]);
775
776 let mut g2 = ComputeGraph::new();
778 g2.push(
779 OpNode::Reduce {
780 op: RedKind::Sum,
781 axis: Axis::Last,
782 },
783 &[ext(&[8])],
784 Shape::new(&[1]),
785 DType::F32,
786 Schedule::default(),
787 )
788 .unwrap();
789 let mut rec2 = Recorder(Vec::new());
790 assert!(lower_graph(&g2, &mut rec2).is_err());
791 }
792}