1mod blocks;
24mod conv;
25mod fusion;
26mod graph_ext;
27mod lower;
28mod window;
29
30pub use blocks::lower_llama_decoder_block;
31pub use blocks::lower_qwen35_mtp_head;
32pub use fusion::FusionPolicy;
33pub use graph_ext::{GridMode, GridPad, HirGraphExt, HirMut};
34pub use window::{window_token_gather_bsn, window_token_scatter_bsn};
35
36use crate::mir::MirModule;
37use crate::op::Activation;
38use crate::op::MaskKind;
39use crate::quant::QuantScheme;
40use crate::{Op, Shape};
41
42pub use lower::LowerError;
43
44#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
47pub struct HirNodeId(pub u32);
48
49impl std::fmt::Display for HirNodeId {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 write!(f, "h{}", self.0)
52 }
53}
54
55#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
57#[derive(Debug, Clone, PartialEq)]
58pub enum HirOp {
59 Input {
60 name: String,
61 },
62 Param {
63 name: String,
64 },
65 Constant {
66 data: Vec<u8>,
67 },
68
69 Linear {
72 activation: Option<Activation>,
73 has_bias: bool,
74 },
75
76 LinearFused {
79 activation: Option<Activation>,
80 },
81
82 SharedLinearPair {
85 slot: u8,
86 },
87
88 SwiGLU,
91
92 ResidualRmsNorm {
95 eps: f32,
96 },
97
98 Attention {
101 num_heads: usize,
102 head_dim: usize,
103 mask: MaskKind,
104 },
105
106 DepthwiseConv1dCausal {
109 kernel_size: usize,
110 },
111
112 DequantMatMul {
115 scheme: QuantScheme,
116 },
117
118 GatedDeltaNet {
121 state_size: usize,
122 carry_state: bool,
123 },
124
125 Lstm {
128 hidden_size: usize,
129 num_layers: usize,
130 bidirectional: bool,
131 carry: bool,
132 },
133
134 Gru {
138 hidden_size: usize,
139 num_layers: usize,
140 bidirectional: bool,
141 carry: bool,
142 },
143
144 RoPE {
146 head_dim: usize,
147 n_rot: usize,
148 },
149
150 RmsNorm {
152 eps: f32,
153 },
154
155 LlamaDecoderBlock {
160 num_heads: usize,
161 head_dim: usize,
162 num_kv_heads: usize,
163 eps: f32,
164 mask: MaskKind,
165 rope_style: crate::op::RopeStyle,
167 },
168
169 Qwen35MtpHead {
172 num_heads: usize,
173 num_kv_heads: usize,
174 head_dim: usize,
175 n_rot: usize,
176 n_embd: usize,
177 n_ff: usize,
178 mtp_vocab: usize,
179 eps: f32,
180 },
181
182 Mir(Op),
184}
185
186#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
188#[derive(Debug, Clone)]
189pub struct HirNode {
190 pub id: HirNodeId,
191 pub op: HirOp,
192 pub inputs: Vec<HirNodeId>,
193 pub shape: Shape,
194 pub name: Option<String>,
195}
196
197#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
199#[derive(Debug, Clone)]
200pub struct HirModule {
201 pub name: String,
202 nodes: Vec<HirNode>,
203 pub outputs: Vec<HirNodeId>,
204 pub fusion_policy: FusionPolicy,
207}
208
209impl HirModule {
210 pub fn new(name: impl Into<String>) -> Self {
211 Self {
212 name: name.into(),
213 nodes: Vec::new(),
214 outputs: Vec::new(),
215 fusion_policy: FusionPolicy::Direct,
216 }
217 }
218
219 pub fn with_fusion_policy(mut self, policy: FusionPolicy) -> Self {
220 self.fusion_policy = policy;
221 self
222 }
223
224 pub fn len(&self) -> usize {
225 self.nodes.len()
226 }
227
228 pub fn is_empty(&self) -> bool {
229 self.nodes.is_empty()
230 }
231
232 pub fn nodes(&self) -> &[HirNode] {
233 &self.nodes
234 }
235
236 pub fn node(&self, id: HirNodeId) -> &HirNode {
237 &self.nodes[id.0 as usize]
238 }
239
240 pub fn node_mut(&mut self, id: HirNodeId) -> &mut HirNode {
241 &mut self.nodes[id.0 as usize]
242 }
243
244 pub fn named(
246 &mut self,
247 name: impl Into<String>,
248 build: impl FnOnce(&mut Self) -> HirNodeId,
249 ) -> HirNodeId {
250 let id = build(self);
251 self.node_mut(id).name = Some(name.into());
252 id
253 }
254
255 pub fn label_nodes_since(&mut self, from: usize, name: &str) {
267 if name.is_empty() {
268 return;
269 }
270 for i in from..self.nodes.len() {
271 let is_generic = {
272 let n = &self.nodes[i];
273 n.name.is_none() || n.name.as_deref() == default_hir_block_label(&n.op).as_deref()
274 };
275 if is_generic {
276 self.nodes[i].name = Some(name.to_string());
277 }
278 }
279 }
280
281 fn push_block(
282 &mut self,
283 op: HirOp,
284 inputs: Vec<HirNodeId>,
285 shape: Shape,
286 name: Option<String>,
287 ) -> HirNodeId {
288 let name = name.or_else(|| default_hir_block_label(&op));
289 self.push(op, inputs, shape, name)
290 }
291
292 fn push(
293 &mut self,
294 op: HirOp,
295 inputs: Vec<HirNodeId>,
296 shape: Shape,
297 name: Option<String>,
298 ) -> HirNodeId {
299 let id = HirNodeId(self.nodes.len() as u32);
300 self.nodes.push(HirNode {
301 id,
302 op,
303 inputs,
304 shape,
305 name,
306 });
307 id
308 }
309
310 pub fn input(&mut self, name: impl Into<String>, shape: Shape) -> HirNodeId {
311 self.push(HirOp::Input { name: name.into() }, vec![], shape, None)
312 }
313
314 pub fn input_batch_seq(
316 &mut self,
317 name: impl Into<String>,
318 batch: u32,
319 seq: u32,
320 hidden: usize,
321 dtype: crate::DType,
322 ) -> HirNodeId {
323 self.input(name, Shape::batch_seq(batch, seq, hidden, dtype))
324 }
325
326 pub fn param(&mut self, name: impl Into<String>, shape: Shape) -> HirNodeId {
327 self.push(HirOp::Param { name: name.into() }, vec![], shape, None)
328 }
329
330 pub fn linear(
331 &mut self,
332 x: HirNodeId,
333 weight: HirNodeId,
334 bias: Option<HirNodeId>,
335 activation: Option<Activation>,
336 out_shape: Shape,
337 ) -> HirNodeId {
338 let mut inputs = vec![x, weight];
339 if let Some(b) = bias {
340 inputs.push(b);
341 }
342 self.push_block(
343 HirOp::Linear {
344 activation,
345 has_bias: bias.is_some(),
346 },
347 inputs,
348 out_shape,
349 None,
350 )
351 }
352
353 pub fn linear_fused(
355 &mut self,
356 x: HirNodeId,
357 weight: HirNodeId,
358 bias: HirNodeId,
359 activation: Option<Activation>,
360 out_shape: Shape,
361 ) -> HirNodeId {
362 self.push_block(
363 HirOp::LinearFused { activation },
364 vec![x, weight, bias],
365 out_shape,
366 None,
367 )
368 }
369
370 pub fn shared_linear_pair(
372 &mut self,
373 x: HirNodeId,
374 w_first: HirNodeId,
375 w_second: HirNodeId,
376 out_shape: Shape,
377 ) -> (HirNodeId, HirNodeId) {
378 let inputs = vec![x, w_first, w_second];
379 let first = self.push_block(
380 HirOp::SharedLinearPair { slot: 0 },
381 inputs.clone(),
382 out_shape.clone(),
383 None,
384 );
385 let second = self.push_block(HirOp::SharedLinearPair { slot: 1 }, inputs, out_shape, None);
386 (first, second)
387 }
388
389 pub fn swiglu_ffn(
390 &mut self,
391 x: HirNodeId,
392 up_w: HirNodeId,
393 gate_w: HirNodeId,
394 down_w: HirNodeId,
395 out_shape: Shape,
396 ) -> HirNodeId {
397 self.push_block(
398 HirOp::SwiGLU,
399 vec![x, up_w, gate_w, down_w],
400 out_shape,
401 None,
402 )
403 }
404
405 pub fn residual_rms_norm(
406 &mut self,
407 x: HirNodeId,
408 residual: HirNodeId,
409 gamma: HirNodeId,
410 beta: HirNodeId,
411 eps: f32,
412 out_shape: Shape,
413 ) -> HirNodeId {
414 self.push_block(
415 HirOp::ResidualRmsNorm { eps },
416 vec![x, residual, gamma, beta],
417 out_shape,
418 None,
419 )
420 }
421
422 pub fn attention(
424 &mut self,
425 q: HirNodeId,
426 k: HirNodeId,
427 v: HirNodeId,
428 mask: Option<HirNodeId>,
429 num_heads: usize,
430 head_dim: usize,
431 mask_kind: MaskKind,
432 out_shape: Shape,
433 ) -> HirNodeId {
434 let mut inputs = vec![q, k, v];
435 if let Some(m) = mask {
436 inputs.push(m);
437 }
438 self.push_block(
439 HirOp::Attention {
440 num_heads,
441 head_dim,
442 mask: mask_kind,
443 },
444 inputs,
445 out_shape,
446 None,
447 )
448 }
449
450 pub fn depthwise_conv1d_causal(
455 &mut self,
456 input: HirNodeId,
457 weight: HirNodeId,
458 left_pad: HirNodeId,
459 kernel_size: usize,
460 out_shape: Shape,
461 ) -> HirNodeId {
462 self.push_block(
463 HirOp::DepthwiseConv1dCausal { kernel_size },
464 vec![input, weight, left_pad],
465 out_shape,
466 None,
467 )
468 }
469
470 pub fn dequant_matmul(
472 &mut self,
473 x: HirNodeId,
474 w: HirNodeId,
475 scale: Option<HirNodeId>,
476 zp: Option<HirNodeId>,
477 scheme: QuantScheme,
478 out_shape: Shape,
479 ) -> HirNodeId {
480 let mut inputs = vec![x, w];
481 if !scheme.is_gguf() {
482 inputs.push(scale.expect("DequantMatMul: scale required for non-GGUF schemes"));
483 inputs.push(zp.expect("DequantMatMul: zp required for non-GGUF schemes"));
484 }
485 self.push_block(HirOp::DequantMatMul { scheme }, inputs, out_shape, None)
486 }
487
488 pub fn gated_delta_net(
490 &mut self,
491 q: HirNodeId,
492 k: HirNodeId,
493 v: HirNodeId,
494 g: HirNodeId,
495 beta: HirNodeId,
496 state_size: usize,
497 out_shape: Shape,
498 ) -> HirNodeId {
499 self.push_block(
500 HirOp::GatedDeltaNet {
501 state_size,
502 carry_state: false,
503 },
504 vec![q, k, v, g, beta],
505 out_shape,
506 None,
507 )
508 }
509
510 pub fn gated_delta_net_carry(
512 &mut self,
513 q: HirNodeId,
514 k: HirNodeId,
515 v: HirNodeId,
516 g: HirNodeId,
517 beta: HirNodeId,
518 state: HirNodeId,
519 state_size: usize,
520 out_shape: Shape,
521 ) -> HirNodeId {
522 self.push_block(
523 HirOp::GatedDeltaNet {
524 state_size,
525 carry_state: true,
526 },
527 vec![q, k, v, g, beta, state],
528 out_shape,
529 None,
530 )
531 }
532
533 #[allow(clippy::too_many_arguments)]
536 pub fn lstm(
537 &mut self,
538 x: HirNodeId,
539 w_ih: HirNodeId,
540 w_hh: HirNodeId,
541 bias: HirNodeId,
542 hidden_size: usize,
543 num_layers: usize,
544 bidirectional: bool,
545 out_shape: Shape,
546 ) -> HirNodeId {
547 self.push_block(
548 HirOp::Lstm {
549 hidden_size,
550 num_layers,
551 bidirectional,
552 carry: false,
553 },
554 vec![x, w_ih, w_hh, bias],
555 out_shape,
556 None,
557 )
558 }
559
560 #[allow(clippy::too_many_arguments)]
564 pub fn gru(
565 &mut self,
566 x: HirNodeId,
567 w_ih: HirNodeId,
568 w_hh: HirNodeId,
569 b_ih: HirNodeId,
570 b_hh: HirNodeId,
571 hidden_size: usize,
572 num_layers: usize,
573 bidirectional: bool,
574 out_shape: Shape,
575 ) -> HirNodeId {
576 self.push_block(
577 HirOp::Gru {
578 hidden_size,
579 num_layers,
580 bidirectional,
581 carry: false,
582 },
583 vec![x, w_ih, w_hh, b_ih, b_hh],
584 out_shape,
585 None,
586 )
587 }
588
589 #[allow(clippy::too_many_arguments)]
592 pub fn lstm_carry(
593 &mut self,
594 x: HirNodeId,
595 w_ih: HirNodeId,
596 w_hh: HirNodeId,
597 bias: HirNodeId,
598 h0: HirNodeId,
599 c0: HirNodeId,
600 hidden_size: usize,
601 num_layers: usize,
602 bidirectional: bool,
603 out_shape: Shape,
604 ) -> HirNodeId {
605 self.push_block(
606 HirOp::Lstm {
607 hidden_size,
608 num_layers,
609 bidirectional,
610 carry: true,
611 },
612 vec![x, w_ih, w_hh, bias, h0, c0],
613 out_shape,
614 None,
615 )
616 }
617
618 pub fn rope(
620 &mut self,
621 x: HirNodeId,
622 cos: HirNodeId,
623 sin: HirNodeId,
624 head_dim: usize,
625 n_rot: usize,
626 out_shape: Shape,
627 ) -> HirNodeId {
628 self.push_block(
629 HirOp::RoPE { head_dim, n_rot },
630 vec![x, cos, sin],
631 out_shape,
632 None,
633 )
634 }
635
636 pub fn rms_norm(
638 &mut self,
639 x: HirNodeId,
640 gamma: HirNodeId,
641 beta: HirNodeId,
642 eps: f32,
643 out_shape: Shape,
644 ) -> HirNodeId {
645 self.push_block(
646 HirOp::RmsNorm { eps },
647 vec![x, gamma, beta],
648 out_shape,
649 None,
650 )
651 }
652
653 pub fn llama_decoder_block(
655 &mut self,
656 x: HirNodeId,
657 ln1_g: HirNodeId,
658 ln1_b: HirNodeId,
659 q_w: HirNodeId,
660 k_w: HirNodeId,
661 v_w: HirNodeId,
662 o_w: HirNodeId,
663 ln2_g: HirNodeId,
664 ln2_b: HirNodeId,
665 gate_w: HirNodeId,
666 up_w: HirNodeId,
667 down_w: HirNodeId,
668 cos: HirNodeId,
669 sin: HirNodeId,
670 mask: Option<HirNodeId>,
671 num_heads: usize,
672 head_dim: usize,
673 num_kv_heads: usize,
674 eps: f32,
675 mask_kind: MaskKind,
676 rope_style: crate::op::RopeStyle,
677 out_shape: Shape,
678 ) -> HirNodeId {
679 let mut ins = vec![
680 x, ln1_g, ln1_b, q_w, k_w, v_w, o_w, ln2_g, ln2_b, gate_w, up_w, down_w, cos, sin,
681 ];
682 if let Some(m) = mask {
683 ins.push(m);
684 }
685 self.push_block(
686 HirOp::LlamaDecoderBlock {
687 num_heads,
688 head_dim,
689 num_kv_heads,
690 eps,
691 mask: mask_kind,
692 rope_style,
693 },
694 ins,
695 out_shape,
696 Some("llama_decoder_block".into()),
697 )
698 }
699
700 pub fn transformer_block(
703 &mut self,
704 x: HirNodeId,
705 ln1_g: HirNodeId,
706 ln1_b: HirNodeId,
707 q_w: HirNodeId,
708 k_w: HirNodeId,
709 v_w: HirNodeId,
710 o_w: HirNodeId,
711 ln2_g: HirNodeId,
712 ln2_b: HirNodeId,
713 gate_w: HirNodeId,
714 up_w: HirNodeId,
715 down_w: HirNodeId,
716 cos: HirNodeId,
717 sin: HirNodeId,
718 mask: Option<HirNodeId>,
719 num_heads: usize,
720 head_dim: usize,
721 num_kv_heads: usize,
722 eps: f32,
723 mask_kind: MaskKind,
724 out_shape: Shape,
725 ) -> HirNodeId {
726 let id = self.llama_decoder_block(
727 x,
728 ln1_g,
729 ln1_b,
730 q_w,
731 k_w,
732 v_w,
733 o_w,
734 ln2_g,
735 ln2_b,
736 gate_w,
737 up_w,
738 down_w,
739 cos,
740 sin,
741 mask,
742 num_heads,
743 head_dim,
744 num_kv_heads,
745 eps,
746 mask_kind,
747 crate::op::RopeStyle::NeoX,
749 out_shape,
750 );
751 self.node_mut(id).name = Some("transformer_block".into());
752 id
753 }
754
755 #[allow(clippy::too_many_arguments)]
757 pub fn qwen35_mtp_head(
758 &mut self,
759 h_pre_norm: HirNodeId,
760 input_ids: HirNodeId,
761 cos: HirNodeId,
762 sin: HirNodeId,
763 last_token_idx: HirNodeId,
764 embed_w: HirNodeId,
765 hnorm_w: HirNodeId,
766 hnorm_b: HirNodeId,
767 enorm_w: HirNodeId,
768 enorm_b: HirNodeId,
769 eh_w: HirNodeId,
770 fa_attn_norm_w: HirNodeId,
771 fa_attn_norm_b: HirNodeId,
772 fa_q_gate_w: HirNodeId,
773 fa_k_w: HirNodeId,
774 fa_v_w: HirNodeId,
775 fa_q_norm_w: HirNodeId,
776 fa_q_norm_b: HirNodeId,
777 fa_k_norm_w: HirNodeId,
778 fa_k_norm_b: HirNodeId,
779 fa_o_w: HirNodeId,
780 fa_post_norm_w: HirNodeId,
781 fa_post_norm_b: HirNodeId,
782 fa_gate_w: HirNodeId,
783 fa_up_w: HirNodeId,
784 fa_down_w: HirNodeId,
785 head_norm_w: HirNodeId,
786 head_norm_b: HirNodeId,
787 lm_head_w: HirNodeId,
788 num_heads: usize,
789 num_kv_heads: usize,
790 head_dim: usize,
791 n_rot: usize,
792 n_embd: usize,
793 n_ff: usize,
794 mtp_vocab: usize,
795 eps: f32,
796 out_shape: Shape,
797 ) -> HirNodeId {
798 self.push_block(
799 HirOp::Qwen35MtpHead {
800 num_heads,
801 num_kv_heads,
802 head_dim,
803 n_rot,
804 n_embd,
805 n_ff,
806 mtp_vocab,
807 eps,
808 },
809 vec![
810 h_pre_norm,
811 input_ids,
812 cos,
813 sin,
814 last_token_idx,
815 embed_w,
816 hnorm_w,
817 hnorm_b,
818 enorm_w,
819 enorm_b,
820 eh_w,
821 fa_attn_norm_w,
822 fa_attn_norm_b,
823 fa_q_gate_w,
824 fa_k_w,
825 fa_v_w,
826 fa_q_norm_w,
827 fa_q_norm_b,
828 fa_k_norm_w,
829 fa_k_norm_b,
830 fa_o_w,
831 fa_post_norm_w,
832 fa_post_norm_b,
833 fa_gate_w,
834 fa_up_w,
835 fa_down_w,
836 head_norm_w,
837 head_norm_b,
838 lm_head_w,
839 ],
840 out_shape,
841 Some("qwen35_mtp_head".into()),
842 )
843 }
844
845 pub fn mir(&mut self, op: Op, inputs: Vec<HirNodeId>, shape: Shape) -> HirNodeId {
847 self.push(HirOp::Mir(op), inputs, shape, None)
848 }
849
850 pub fn set_outputs(&mut self, outputs: Vec<HirNodeId>) {
851 self.outputs = outputs;
852 }
853
854 pub fn lower_to_mir(self) -> Result<MirModule, LowerError> {
856 lower::lower_module(self)
857 }
858
859 pub fn lower_for_autodiff(self) -> Result<MirModule, LowerError> {
862 self.with_fusion_policy(FusionPolicy::for_autodiff())
863 .lower_to_mir()
864 }
865
866 pub fn wrap_mir_graph(graph: crate::Graph) -> Self {
869 use std::collections::HashMap;
870 let mut hir = Self::new(graph.name.clone()).with_fusion_policy(FusionPolicy::Direct);
871 let mut map: HashMap<crate::NodeId, HirNodeId> = HashMap::new();
872 for node in graph.nodes() {
873 let inputs: Vec<HirNodeId> = node.inputs.iter().map(|&id| map[&id]).collect();
874 let id = hir.mir(node.op.clone(), inputs, node.shape.clone());
875 map.insert(node.id, id);
876 }
877 let outputs: Vec<HirNodeId> = graph.outputs.iter().map(|&id| map[&id]).collect();
878 hir.set_outputs(outputs);
879 hir
880 }
881}
882
883pub(crate) fn default_hir_block_label(op: &HirOp) -> Option<String> {
884 Some(match op {
885 HirOp::Linear { .. } => "linear".into(),
886 HirOp::LinearFused { .. } => "linear_fused".into(),
887 HirOp::SharedLinearPair { slot } => return Some(format!("shared_linear_pair[{slot}]")),
888 HirOp::SwiGLU => "swiglu_ffn".into(),
889 HirOp::ResidualRmsNorm { .. } => "residual_rms_norm".into(),
890 HirOp::Attention { .. } => "attention".into(),
891 HirOp::DepthwiseConv1dCausal { .. } => "depthwise_conv1d_causal".into(),
892 HirOp::DequantMatMul { scheme } => format!("dequant_matmul({scheme})"),
893 HirOp::GatedDeltaNet {
894 carry_state: true, ..
895 } => "gated_delta_net_carry".into(),
896 HirOp::GatedDeltaNet { .. } => "gated_delta_net".into(),
897 HirOp::Lstm { .. } => "lstm".into(),
898 HirOp::Gru { .. } => "gru".into(),
899 HirOp::RoPE { .. } => "rope".into(),
900 HirOp::RmsNorm { .. } => "rms_norm".into(),
901 HirOp::Mir(_) => "mir".into(),
902 HirOp::LlamaDecoderBlock { .. } => "llama_decoder_block".into(),
903 HirOp::Qwen35MtpHead { .. } => "qwen35_mtp_head".into(),
904 HirOp::Input { .. } | HirOp::Param { .. } | HirOp::Constant { .. } => return None,
905 })
906}
907
908impl std::fmt::Display for HirModule {
909 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
910 writeln!(f, "hir @{} {{", self.name)?;
911 for node in &self.nodes {
912 write!(f, " {} = {:?}", node.id, node.op)?;
913 if !node.inputs.is_empty() {
914 write!(f, "(")?;
915 for (i, inp) in node.inputs.iter().enumerate() {
916 if i > 0 {
917 write!(f, ", ")?;
918 }
919 write!(f, "{inp}")?;
920 }
921 write!(f, ")")?;
922 }
923 writeln!(f, " : {}", node.shape)?;
924 }
925 if !self.outputs.is_empty() {
926 write!(f, " return ")?;
927 for (i, o) in self.outputs.iter().enumerate() {
928 if i > 0 {
929 write!(f, ", ")?;
930 }
931 write!(f, "{o}")?;
932 }
933 writeln!(f)?;
934 }
935 write!(f, "}}")
936 }
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use crate::DType;
943
944 fn f32_shape(d: &[usize]) -> Shape {
945 Shape::new(d, DType::F32)
946 }
947
948 #[test]
949 fn hir_depthwise_conv1d_causal_lowers_to_grouped_conv() {
950 use crate::Op;
951
952 let mut hir = HirModule::new("dw");
953 let x = hir.input("x", f32_shape(&[2, 8, 16]));
954 let w = hir.param("w", f32_shape(&[16, 1, 1, 3]));
955 let pad = hir.param("pad", f32_shape(&[2, 2, 16]));
956 let out = hir.depthwise_conv1d_causal(x, w, pad, 3, f32_shape(&[2, 8, 16]));
957 hir.outputs = vec![out];
958
959 let g = hir.lower_to_mir().expect("lower").into_graph();
960 assert!(g.nodes().iter().any(|n| matches!(n.op, Op::Conv { .. })));
961 assert!(g.nodes().iter().any(|n| matches!(n.op, Op::Concat { .. })));
962 }
963
964 #[test]
965 fn hir_swiglu_lowers_to_fusable_mir() {
966 use crate::Op;
967 use crate::hir::FusionPolicy;
968
969 let mut hir = HirModule::new("ffn").with_fusion_policy(FusionPolicy::Fusable);
970 let x = hir.input("x", f32_shape(&[4, 768]));
971 let up_w = hir.param("up", f32_shape(&[768, 2048]));
972 let gate_w = hir.param("gate", f32_shape(&[768, 2048]));
973 let down_w = hir.param("down", f32_shape(&[2048, 768]));
974 let out = hir.swiglu_ffn(x, up_w, gate_w, down_w, f32_shape(&[4, 768]));
975 hir.set_outputs(vec![out]);
976
977 let mir = hir.lower_to_mir().expect("lower");
978 let g = mir.into_graph();
979 assert!(g.nodes().iter().any(|n| matches!(n.op, Op::MatMul)));
980 assert_eq!(g.len(), 9);
981 }
982
983 #[test]
984 fn label_nodes_since_flows_source_name_to_all_mir_nodes() {
985 use crate::hir::HirMut;
990 use crate::op::Activation;
991 use crate::{Op, node_label};
992
993 let mut hir = HirModule::new("m");
994 {
995 let mut b = HirMut::new(&mut hir);
996 let x = b.input("x", f32_shape(&[4]));
997 let from = b.0.len(); let e = b.activation(Activation::Exp, x, f32_shape(&[4]));
999 let two = b.add(e, e);
1000 let out = b.div(two, e);
1001 b.0.label_nodes_since(from, "Softmax_7");
1002 b.set_outputs(vec![out]);
1003 }
1004 let g = hir.lower_to_mir().expect("lower").into_graph();
1005
1006 let mut compute = 0;
1007 for node in g.nodes() {
1008 match node.op {
1009 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => {}
1010 _ => {
1011 compute += 1;
1012 assert_eq!(
1013 node_label(&g, node.id),
1014 "Softmax_7",
1015 "node {} {:?} lost source provenance",
1016 node.id,
1017 node.op
1018 );
1019 }
1020 }
1021 }
1022 assert!(
1023 compute >= 3,
1024 "expected exp/add/div compute nodes, got {compute}"
1025 );
1026
1027 let x_mir = g.input_id("x").expect("input x present");
1029 assert_eq!(node_label(&g, x_mir), "x");
1030 }
1031
1032 #[test]
1033 fn hir_gdn_dequant_rope_rms_lowers() {
1034 use crate::Op;
1035 use crate::quant::QuantScheme;
1036
1037 let mut hir = HirModule::new("qwen_block");
1038 let q = hir.input("q", f32_shape(&[1, 4, 2, 8]));
1039 let k = hir.param("k", f32_shape(&[1, 4, 2, 8]));
1040 let v = hir.param("v", f32_shape(&[1, 4, 2, 8]));
1041 let g_in = hir.param("g", f32_shape(&[1, 4, 2]));
1042 let beta = hir.param("beta", f32_shape(&[1, 4, 2]));
1043 let scan = hir.gated_delta_net(q, k, v, g_in, beta, 8, f32_shape(&[1, 4, 2, 8]));
1044
1045 let cos = hir.param("cos", f32_shape(&[1, 4, 8]));
1046 let sin = hir.param("sin", f32_shape(&[1, 4, 8]));
1047 let x = hir.input("x", f32_shape(&[1, 4, 8]));
1048 let rotated = hir.rope(x, cos, sin, 8, 8, f32_shape(&[1, 4, 8]));
1049
1050 let gamma = hir.param("gamma", f32_shape(&[8]));
1051 let beta_n = hir.param("beta_n", f32_shape(&[8]));
1052 let normed = hir.rms_norm(rotated, gamma, beta_n, 1e-6, f32_shape(&[1, 4, 8]));
1053
1054 let x_in = hir.input("hidden", f32_shape(&[4, 128]));
1055 let w = hir.param("w_q", f32_shape(&[1024]));
1056 let proj = hir.dequant_matmul(
1057 x_in,
1058 w,
1059 None,
1060 None,
1061 QuantScheme::GgufQ4K,
1062 f32_shape(&[4, 128]),
1063 );
1064 hir.set_outputs(vec![scan, normed, proj]);
1065
1066 let g = hir.lower_to_mir().expect("lower").into_graph();
1067 assert!(g.nodes().iter().any(|n| matches!(
1068 n.op,
1069 Op::GatedDeltaNet {
1070 carry_state: false,
1071 ..
1072 }
1073 )));
1074 assert!(g.nodes().iter().any(|n| matches!(n.op, Op::Rope { .. })));
1075 assert!(g.nodes().iter().any(|n| matches!(n.op, Op::RmsNorm { .. })));
1076 assert!(g.nodes().iter().any(|n| matches!(
1077 n.op,
1078 Op::DequantMatMul {
1079 scheme: QuantScheme::GgufQ4K
1080 }
1081 )));
1082 }
1083}