1#![allow(unsafe_op_in_unsafe_fn)]
24use crate::arena::Arena;
27use crate::op_registry::CpuKernel;
28use rlx_ir::op::{Activation, BinaryOp, CmpOp, ReduceOp};
29use rlx_ir::{Graph, NodeId, Op, Shape};
30use std::collections::HashMap;
31use std::sync::Arc;
32
33pub static FUSED_NOMIC_LAYER_COUNT: std::sync::atomic::AtomicU64 =
39 std::sync::atomic::AtomicU64::new(0);
40
41#[derive(Clone)]
43pub enum Thunk {
44 Nop,
46 ElementwiseRegion {
55 dst: usize,
56 len: u32,
57 input_offs: Vec<usize>,
58 chain: Vec<rlx_ir::op::ChainStep>,
59 scalar_input_mask: u32,
60 input_modulus: [u32; 16],
61 },
62 Sgemm {
64 a: usize,
65 b: usize,
66 c: usize,
67 m: u32,
68 k: u32,
69 n: u32,
70 },
71 SgemmT {
77 a: usize,
78 b: usize,
79 c: usize,
80 m: u32,
81 k: u32,
82 n: u32,
83 ta: bool,
84 tb: bool,
85 },
86 SgdMomentum {
94 param: usize,
95 vel: usize,
96 grad: usize,
97 p_out: usize,
98 v_out: usize,
99 lr: f32,
100 mom: f32,
101 len: u32,
102 },
103 CgemmC64 {
107 a: usize,
108 b: usize,
109 c: usize,
110 m: u32,
111 k: u32,
112 n: u32,
113 },
114 DenseSolveF64 {
120 a: usize,
121 b: usize,
122 x: usize,
123 n: u32,
124 nrhs: u32,
125 },
126 DenseSolveF32 {
129 a: usize,
130 b: usize,
131 x: usize,
132 n: u32,
133 nrhs: u32,
134 },
135 BatchedDenseSolveF64 {
140 a: usize,
141 b: usize,
142 x: usize,
143 batch: u32,
144 n: u32,
145 nrhs: u32,
146 },
147 BatchedDenseSolveF32 {
149 a: usize,
150 b: usize,
151 x: usize,
152 batch: u32,
153 n: u32,
154 nrhs: u32,
155 },
156 BatchedDgemmF64 {
162 a: usize,
163 b: usize,
164 c: usize,
165 batch: u32,
166 m: u32,
167 k: u32,
168 n: u32,
169 },
170 BatchedSgemm {
177 a: usize,
178 b: usize,
179 c: usize,
180 batch: u32,
181 m: u32,
182 k: u32,
183 n: u32,
184 },
185 Dgemm {
187 a: usize,
188 b: usize,
189 c: usize,
190 m: u32,
191 k: u32,
192 n: u32,
193 },
194 TransposeF64 {
198 src: usize,
199 dst: usize,
200 in_total: u32,
201 out_dims: Vec<u32>,
202 in_strides: Vec<u32>,
203 },
204 ActivationF64 {
208 src: usize,
209 dst: usize,
210 len: u32,
211 kind: Activation,
212 },
213 ComplexNormSqF32 {
217 src: usize,
218 dst: usize,
219 len: u32,
221 },
222 ComplexNormSqBackwardF32 {
226 z: usize,
227 g: usize,
228 dz: usize,
229 len: u32,
230 },
231 ConjugateC64 {
234 src: usize,
235 dst: usize,
236 len: u32,
237 },
238 ActivationC64 {
245 src: usize,
246 dst: usize,
247 len: u32,
248 kind: Activation,
249 },
250 ReduceSumF64 {
254 src: usize,
255 dst: usize,
256 outer: u32,
257 reduced: u32,
258 inner: u32,
259 },
260 CopyF64 {
263 src: usize,
264 dst: usize,
265 len: u32,
266 },
267 CopyI64 {
269 src: usize,
270 dst: usize,
271 len: u32,
272 },
273 CastF32ToI64 {
275 src: usize,
276 dst: usize,
277 len: u32,
278 },
279 CastF32ToF64 {
280 src: usize,
281 dst: usize,
282 len: u32,
283 },
284 CastF32ToI32 {
285 src: usize,
286 dst: usize,
287 len: u32,
288 },
289 CastI64ToF32 {
291 src: usize,
292 dst: usize,
293 len: u32,
294 },
295 CastBoolToI32 {
297 src: usize,
298 dst: usize,
299 len: u32,
300 },
301 CastBoolToF32 {
302 src: usize,
303 dst: usize,
304 len: u32,
305 },
306 CastI32ToF32 {
308 src: usize,
309 dst: usize,
310 len: u32,
311 },
312 BinaryFullF64 {
316 lhs: usize,
317 rhs: usize,
318 dst: usize,
319 len: u32,
320 lhs_len: u32,
321 rhs_len: u32,
322 op: BinaryOp,
323 out_dims_bcast: Vec<u32>,
326 bcast_lhs_strides: Vec<u32>,
327 bcast_rhs_strides: Vec<u32>,
328 },
329 ConcatF64 {
333 dst: usize,
334 outer: u32,
335 inner: u32,
336 total_axis: u32,
337 inputs: Vec<(usize, u32, u32)>,
338 },
339 BinaryFullC64 {
347 lhs: usize,
348 rhs: usize,
349 dst: usize,
350 len: u32,
353 lhs_len: u32,
354 rhs_len: u32,
355 op: BinaryOp,
356 out_dims_bcast: Vec<u32>,
357 bcast_lhs_strides: Vec<u32>,
358 bcast_rhs_strides: Vec<u32>,
359 },
360 Scan {
369 body: Arc<ThunkSchedule>,
370 body_init: Arc<Vec<u8>>, body_input_off: usize, body_output_off: usize, outer_init_off: usize, outer_final_off: usize, length: u32,
376 carry_bytes: u32, save_trajectory: bool,
382 xs_inputs: Arc<Vec<(usize, usize, u32)>>,
387 bcast_inputs: Arc<Vec<(usize, usize, u32)>>,
393 num_checkpoints: u32,
399 },
400
401 ScanBackward {
409 body_vjp: Arc<ThunkSchedule>,
410 body_init: Arc<Vec<u8>>,
411 body_carry_in_off: usize, body_x_offs: Arc<Vec<usize>>, body_d_output_off: usize, body_dcarry_out_off: usize, outer_init_off: usize, outer_traj_off: usize, outer_upstream_off: usize, outer_xs_offs: Arc<Vec<(usize, u32)>>,
421 outer_dinit_off: usize, length: u32,
423 carry_bytes: u32,
424 carry_elem_size: u32,
430 save_trajectory: bool, num_checkpoints: u32,
437 forward_body: Option<Arc<ThunkSchedule>>,
441 forward_body_init: Option<Arc<Vec<u8>>>,
443 forward_body_carry_in_off: usize,
446 forward_body_output_off: usize,
447 forward_body_x_offs: Arc<Vec<usize>>,
450 },
451
452 ScanBackwardXs {
459 body_vjp: Arc<ThunkSchedule>,
460 body_init: Arc<Vec<u8>>,
461 body_carry_in_off: usize,
462 body_x_offs: Arc<Vec<usize>>,
463 body_d_output_off: usize,
464 body_dcarry_out_off: usize,
465 body_dxs_out_off: usize, outer_init_off: usize,
467 outer_traj_off: usize,
468 outer_upstream_off: usize,
469 outer_xs_offs: Arc<Vec<(usize, u32)>>,
470 outer_dxs_off: usize, length: u32,
472 carry_bytes: u32,
473 carry_elem_size: u32,
475 per_step_bytes: u32, save_trajectory: bool,
477 num_checkpoints: u32,
485 forward_body: Option<Arc<ThunkSchedule>>,
486 forward_body_init: Option<Arc<Vec<u8>>>,
487 forward_body_carry_in_off: usize,
488 forward_body_output_off: usize,
489 forward_body_x_offs: Arc<Vec<usize>>,
490 },
491 CustomFn {
496 body: Arc<ThunkSchedule>,
497 body_init: Arc<Vec<u8>>,
498 inputs: Arc<Vec<(usize, usize, u32)>>,
500 body_output_off: usize,
501 outer_output_off: usize,
502 out_bytes: u32,
503 },
504 FusedMmBiasAct {
506 a: usize,
507 w: usize,
508 bias: usize,
509 c: usize,
510 m: u32,
511 k: u32,
512 n: u32,
513 act: Option<Activation>,
514 },
515 FusedResidualLN {
517 x: usize,
518 res: usize,
519 bias: usize,
520 g: usize,
521 b: usize,
522 out: usize,
523 rows: u32,
524 h: u32,
525 eps: f32,
526 has_bias: bool,
527 },
528 FusedResidualRmsNorm {
530 x: usize,
531 res: usize,
532 bias: usize,
533 g: usize,
534 b: usize,
535 out: usize,
536 rows: u32,
537 h: u32,
538 eps: f32,
539 has_bias: bool,
540 },
541 BiasAdd {
543 src: usize,
544 bias: usize,
545 dst: usize,
546 m: u32,
547 n: u32,
548 },
549 BinaryFull {
564 lhs: usize,
565 rhs: usize,
566 dst: usize,
567 len: u32,
568 lhs_len: u32,
569 rhs_len: u32,
570 op: BinaryOp,
571 out_dims_bcast: Vec<u32>,
573 bcast_lhs_strides: Vec<u32>,
575 bcast_rhs_strides: Vec<u32>,
577 elem_bytes: u8,
579 },
580 ActivationInPlace {
582 data: usize,
583 len: u32,
584 act: Activation,
585 },
586 Gather {
588 table: usize,
589 table_len: u32,
590 idx: usize,
591 dst: usize,
592 num_idx: u32,
593 trailing: u32,
594 idx_i64: u8,
596 table_bytes: u8,
598 },
599 Narrow {
601 src: usize,
602 dst: usize,
603 outer: u32,
604 src_stride: u32,
605 dst_stride: u32,
606 inner: u32,
607 elem_bytes: u8,
608 },
609 Reverse {
613 src: usize,
614 dst: usize,
615 dims: Vec<u32>,
616 rev_mask: Vec<bool>,
617 elem_bytes: u8,
618 },
619 Copy {
621 src: usize,
622 dst: usize,
623 len: u32,
624 },
625 LayerNorm {
627 src: usize,
628 g: usize,
629 b: usize,
630 dst: usize,
631 rows: u32,
632 h: u32,
633 eps: f32,
634 },
635 GroupNorm {
637 src: usize,
638 g: usize,
639 b: usize,
640 dst: usize,
641 n: u32,
642 c: u32,
643 h: u32,
644 w: u32,
645 num_groups: u32,
646 eps: f32,
647 },
648 BatchNormInference {
650 src: usize,
651 g: usize,
652 b: usize,
653 mean: usize,
654 var: usize,
655 dst: usize,
656 count: u32,
657 channels: u32,
658 eps: f32,
659 },
660 BatchNormInferenceBackwardInput {
661 x: usize,
662 gamma: usize,
663 mean: usize,
664 var: usize,
665 dy: usize,
666 dx: usize,
667 count: u32,
668 channels: u32,
669 eps: f32,
670 },
671 BatchNormInferenceBackwardGamma {
672 x: usize,
673 mean: usize,
674 var: usize,
675 dy: usize,
676 dgamma: usize,
677 count: u32,
678 channels: u32,
679 eps: f32,
680 },
681 BatchNormInferenceBackwardBeta {
682 dy: usize,
683 dbeta: usize,
684 count: u32,
685 channels: u32,
686 },
687 LayerNorm2d {
689 src: usize,
690 g: usize,
691 b: usize,
692 dst: usize,
693 n: u32,
694 c: u32,
695 h: u32,
696 w: u32,
697 eps: f32,
698 },
699 ConvTranspose2d {
701 src: usize,
702 weight: usize,
703 dst: usize,
704 n: u32,
705 c_in: u32,
706 h: u32,
707 w_in: u32,
708 c_out: u32,
709 h_out: u32,
710 w_out: u32,
711 kh: u32,
712 kw: u32,
713 sh: u32,
714 sw: u32,
715 ph: u32,
716 pw: u32,
717 dh: u32,
718 dw: u32,
719 groups: u32,
720 },
721 Conv3d {
725 src: usize,
726 weight: usize,
727 dst: usize,
728 n: u32,
729 c_in: u32,
730 d: u32,
731 h: u32,
732 w: u32,
733 c_out: u32,
734 d_out: u32,
735 h_out: u32,
736 w_out: u32,
737 kd: u32,
738 kh: u32,
739 kw: u32,
740 sd: u32,
741 sh: u32,
742 sw: u32,
743 pd: u32,
744 ph: u32,
745 pw: u32,
746 dd: u32,
747 dh: u32,
748 dw: u32,
749 groups: u32,
750 },
751 ConvTranspose3d {
754 src: usize,
755 weight: usize,
756 dst: usize,
757 n: u32,
758 c_in: u32,
759 d: u32,
760 h: u32,
761 w_in: u32,
762 c_out: u32,
763 d_out: u32,
764 h_out: u32,
765 w_out: u32,
766 kd: u32,
767 kh: u32,
768 kw: u32,
769 sd: u32,
770 sh: u32,
771 sw: u32,
772 pd: u32,
773 ph: u32,
774 pw: u32,
775 dd: u32,
776 dh: u32,
777 dw: u32,
778 groups: u32,
779 },
780 ResizeNearest2x {
782 src: usize,
783 dst: usize,
784 n: u32,
785 c: u32,
786 h: u32,
787 w: u32,
788 },
789 AxialRope2d {
791 src: usize,
792 dst: usize,
793 batch: u32,
794 seq: u32,
795 hidden: u32,
796 end_x: u32,
797 end_y: u32,
798 head_dim: u32,
799 num_heads: u32,
800 theta: f32,
801 repeat_factor: u32,
802 },
803 RmsNorm {
806 src: usize,
807 g: usize,
808 b: usize,
809 dst: usize,
810 rows: u32,
811 h: u32,
812 eps: f32,
813 },
814 Softmax {
816 data: usize,
817 rows: u32,
818 cols: u32,
819 },
820 Cumsum {
823 src: usize,
824 dst: usize,
825 rows: u32,
826 cols: u32,
827 exclusive: bool,
828 },
829 SelectiveScan {
833 x: usize,
834 delta: usize,
835 a: usize,
836 b: usize,
837 c: usize,
838 dst: usize,
839 batch: u32,
840 seq: u32,
841 hidden: u32,
842 state_size: u32,
843 },
844
845 GatedDeltaNet {
849 q: usize,
850 k: usize,
851 v: usize,
852 g: usize,
853 beta: usize,
854 state: usize,
857 dst: usize,
858 batch: u32,
859 seq: u32,
860 heads: u32,
861 state_size: u32,
862 },
863
864 Lstm {
868 x: usize,
869 w_ih: usize,
870 w_hh: usize,
871 bias: usize,
872 h0: usize,
873 c0: usize,
874 dst: usize,
875 batch: u32,
876 seq: u32,
877 input_size: u32,
878 hidden: u32,
879 num_layers: u32,
880 bidirectional: bool,
881 carry: bool,
882 },
883 Gru {
885 x: usize,
886 w_ih: usize,
887 w_hh: usize,
888 b_ih: usize,
889 b_hh: usize,
890 h0: usize,
891 dst: usize,
892 batch: u32,
893 seq: u32,
894 input_size: u32,
895 hidden: u32,
896 num_layers: u32,
897 bidirectional: bool,
898 carry: bool,
899 },
900 Rnn {
902 x: usize,
903 w_ih: usize,
904 w_hh: usize,
905 bias: usize,
906 h0: usize,
907 dst: usize,
908 batch: u32,
909 seq: u32,
910 input_size: u32,
911 hidden: u32,
912 num_layers: u32,
913 bidirectional: bool,
914 carry: bool,
915 relu: bool,
916 },
917 Mamba2 {
919 x: usize,
920 dt: usize,
921 a: usize,
922 b: usize,
923 c: usize,
924 dst: usize,
925 batch: u32,
926 seq: u32,
927 heads: u32,
928 head_dim: u32,
929 state_size: u32,
930 },
931
932 Conv2D1x1 {
942 src: usize,
943 weight: usize,
944 dst: usize,
945 n: u32,
946 c_in: u32,
947 c_out: u32,
948 hw: u32,
949 },
950
951 DequantMatMul {
955 x: usize,
956 w_q: usize, scale: usize, zp: usize, dst: usize,
960 m: u32,
961 k: u32,
962 n: u32,
963 block_size: u32,
964 is_asymmetric: bool,
965 },
966
967 DequantMatMulGguf {
977 x: usize, w_q: usize, dst: usize, m: u32,
981 k: u32,
982 n: u32,
983 scheme: rlx_ir::quant::QuantScheme,
984 },
985
986 DequantMatMulInt4 {
988 x: usize,
989 w_q: usize,
990 scale: usize,
991 zp: usize,
992 dst: usize,
993 m: u32,
994 k: u32,
995 n: u32,
996 block_size: u32,
997 is_asymmetric: bool,
998 },
999
1000 DequantMatMulFp8 {
1002 x: usize,
1003 w_q: usize,
1004 scale: usize,
1005 dst: usize,
1006 m: u32,
1007 k: u32,
1008 n: u32,
1009 e5m2: bool,
1010 },
1011
1012 DequantMatMulNvfp4 {
1014 x: usize,
1015 w_q: usize,
1016 scale: usize,
1017 global_scale: usize,
1018 dst: usize,
1019 m: u32,
1020 k: u32,
1021 n: u32,
1022 },
1023
1024 ScaledMatMul {
1027 lhs: usize,
1028 rhs: usize,
1029 lhs_scale: usize,
1030 rhs_scale: usize,
1031 bias: usize, dst: usize,
1033 m: u32,
1034 k: u32,
1035 n: u32,
1036 lhs_fmt: rlx_ir::ScaledFormat,
1037 rhs_fmt: rlx_ir::ScaledFormat,
1038 layout: rlx_ir::ScaleLayout,
1039 has_bias: bool,
1040 },
1041
1042 ScaledQuantize {
1044 x: usize,
1045 scale: usize,
1046 dst: usize,
1047 rows: u32,
1048 cols: u32,
1049 fmt: rlx_ir::ScaledFormat,
1050 layout: rlx_ir::ScaleLayout,
1051 },
1052
1053 ScaledQuantScale {
1055 x: usize,
1056 dst: usize,
1057 rows: u32,
1058 cols: u32,
1059 fmt: rlx_ir::ScaledFormat,
1060 layout: rlx_ir::ScaleLayout,
1061 },
1062
1063 ScaledDequantize {
1065 codes: usize,
1066 scale: usize,
1067 dst: usize,
1068 rows: u32,
1069 cols: u32,
1070 fmt: rlx_ir::ScaledFormat,
1071 layout: rlx_ir::ScaleLayout,
1072 },
1073
1074 LoraMatMul {
1078 x: usize,
1079 w: usize,
1080 a: usize,
1081 b: usize,
1082 dst: usize,
1083 m: u32,
1084 k: u32,
1085 n: u32,
1086 r: u32,
1087 scale: f32,
1088 },
1089 Sample {
1093 logits: usize,
1094 dst: usize,
1095 batch: u32,
1096 vocab: u32,
1097 top_k: u32, top_p: f32, temperature: f32, seed: u64,
1101 },
1102 RngNormal {
1104 dst: usize,
1105 len: u32,
1106 mean: f32,
1107 scale: f32,
1108 key: u64,
1109 op_seed: Option<f32>,
1110 },
1111 RngUniform {
1113 dst: usize,
1114 len: u32,
1115 low: f32,
1116 high: f32,
1117 key: u64,
1118 op_seed: Option<f32>,
1119 },
1120 Attention {
1131 q: usize,
1132 k: usize,
1133 v: usize,
1134 mask: usize,
1135 out: usize,
1136 batch: u32,
1137 seq: u32,
1139 kv_seq: u32,
1141 heads: u32,
1142 kv_heads: u32,
1146 head_dim: u32,
1147 mask_kind: rlx_ir::op::MaskKind,
1148 scale: f32,
1152 softcap: f32,
1156 q_row_stride: u32,
1157 k_row_stride: u32,
1158 v_row_stride: u32,
1159 bhsd: bool,
1167 },
1168 AttentionBackward {
1170 q: usize,
1171 k: usize,
1172 v: usize,
1173 dy: usize,
1174 mask: usize,
1175 out: usize,
1176 batch: u32,
1177 seq: u32,
1178 kv_seq: u32,
1179 heads: u32,
1180 head_dim: u32,
1181 mask_kind: rlx_ir::op::MaskKind,
1182 wrt: rlx_ir::op::AttentionBwdWrt,
1183 bhsd: bool,
1184 },
1185 Rope {
1191 src: usize,
1192 cos: usize,
1193 sin: usize,
1194 dst: usize,
1195 batch: u32,
1196 seq: u32,
1197 hidden: u32,
1198 head_dim: u32,
1199 n_rot: u32,
1200 cos_len: u32,
1201 src_row_stride: u32,
1202 interleaved: bool,
1205 },
1206 FusedAttnBlock {
1209 hidden: usize,
1210 qkv_w: usize,
1211 out_w: usize,
1212 mask: usize,
1213 mask_kind: rlx_ir::op::MaskKind,
1221 out: usize,
1222 qkv_b: usize,
1223 out_b: usize, cos: usize,
1225 sin: usize,
1226 cos_len: u32, batch: u32,
1228 seq: u32,
1229 hs: u32,
1230 nh: u32,
1231 dh: u32,
1232 has_bias: bool,
1233 has_rope: bool,
1234 interleaved: bool,
1239 },
1240 FusedBertLayer {
1243 hidden: usize,
1245 qkv_w: usize,
1246 qkv_b: usize,
1247 out_w: usize,
1248 out_b: usize,
1249 mask: usize,
1250 ln1_g: usize,
1252 ln1_b: usize,
1253 eps1: f32,
1254 fc1_w: usize,
1256 fc1_b: usize,
1257 fc2_w: usize,
1258 fc2_b: usize,
1259 ln2_g: usize,
1261 ln2_b: usize,
1262 eps2: f32,
1263 out: usize,
1265 batch: u32,
1267 seq: u32,
1268 hs: u32,
1269 nh: u32,
1270 dh: u32,
1271 int_dim: u32,
1272 },
1273 FusedNomicLayer {
1275 hidden: usize,
1276 qkv_w: usize,
1277 out_w: usize,
1278 mask: usize,
1279 cos: usize,
1280 sin: usize,
1281 cos_len: u32,
1282 ln1_g: usize,
1283 ln1_b: usize,
1284 eps1: f32,
1285 fc11_w: usize,
1286 fc12_w: usize,
1287 fc2_w: usize,
1288 ln2_g: usize,
1289 ln2_b: usize,
1290 eps2: f32,
1291 out: usize,
1292 batch: u32,
1293 seq: u32,
1294 hs: u32,
1295 nh: u32,
1296 dh: u32,
1297 int_dim: u32,
1298 interleaved: bool,
1301 },
1302 FusedSwiGLU {
1306 src: usize,
1307 dst: usize,
1308 n_half: u32,
1309 total: u32,
1310 gate_first: bool,
1311 },
1312 Concat {
1317 dst: usize,
1318 outer: u32,
1319 inner: u32,
1320 total_axis: u32,
1321 inputs: Vec<(usize, u32, u32)>,
1324 },
1325 Compare {
1327 lhs: usize,
1328 rhs: usize,
1329 dst: usize,
1330 len: u32,
1331 op: CmpOp,
1332 inputs_i64: u8,
1334 inputs_elem_bytes: u8,
1336 dst_elem_bytes: u8,
1338 },
1339 Reduce {
1347 src: usize,
1348 dst: usize,
1349 outer: u32,
1350 reduced: u32,
1351 inner: u32,
1352 op: ReduceOp,
1353 },
1354 ArgReduce {
1357 src: usize,
1358 dst: usize,
1359 outer: u32,
1360 reduced: u32,
1361 inner: u32,
1362 is_max: bool,
1363 },
1364 TopK {
1368 src: usize,
1369 dst: usize,
1370 outer: u32,
1371 axis_dim: u32,
1372 k: u32,
1373 indices_i64: u8,
1374 },
1375 GroupedMatMul {
1379 input: usize,
1380 weight: usize,
1381 expert_idx: usize,
1382 dst: usize,
1383 m: u32,
1384 k_dim: u32,
1385 n: u32,
1386 num_experts: u32,
1387 },
1388 DequantGroupedMatMulGguf {
1390 input: usize,
1391 w_q: usize,
1392 expert_idx: usize,
1393 dst: usize,
1394 m: u32,
1395 k_dim: u32,
1396 n: u32,
1397 num_experts: u32,
1398 scheme: rlx_ir::quant::QuantScheme,
1399 },
1400 DequantMoEWeightsGguf {
1402 w_q: usize,
1403 dst: usize,
1404 k_dim: u32,
1405 n: u32,
1406 num_experts: u32,
1407 scheme: rlx_ir::quant::QuantScheme,
1408 },
1409 ScatterAdd {
1412 updates: usize,
1413 indices: usize,
1414 dst: usize,
1415 num_updates: u32,
1416 out_dim: u32,
1417 trailing: u32,
1418 },
1419 Where {
1421 cond: usize,
1422 on_true: usize,
1423 on_false: usize,
1424 dst: usize,
1425 len: u32,
1426 elem_bytes: u8,
1427 cond_elem_bytes: u8,
1429 },
1430 Fma {
1433 a: usize,
1434 b: usize,
1435 c: usize,
1436 dst: usize,
1437 len: u32,
1438 elem_bytes: u8,
1439 },
1440 Transpose {
1446 src: usize,
1447 dst: usize,
1448 in_total: u32,
1449 out_dims: Vec<u32>,
1450 in_strides: Vec<u32>,
1451 elem_bytes: u8,
1452 },
1453 GatherAxis {
1458 table: usize,
1459 idx: usize,
1460 dst: usize,
1461 outer: u32,
1462 axis_dim: u32,
1463 num_idx: u32,
1464 trailing: u32,
1465 idx_i64: u8,
1466 table_bytes: u8,
1467 },
1468 Pool2D {
1472 src: usize,
1473 dst: usize,
1474 n: u32,
1475 c: u32,
1476 h: u32,
1477 w: u32,
1478 h_out: u32,
1479 w_out: u32,
1480 kh: u32,
1481 kw: u32,
1482 sh: u32,
1483 sw: u32,
1484 ph: u32,
1485 pw: u32,
1486 kind: ReduceOp,
1487 },
1488 Conv2D {
1493 src: usize,
1494 weight: usize,
1495 dst: usize,
1496 n: u32,
1497 c_in: u32,
1498 h: u32,
1499 w: u32,
1500 c_out: u32,
1501 h_out: u32,
1502 w_out: u32,
1503 kh: u32,
1504 kw: u32,
1505 sh: u32,
1506 sw: u32,
1507 ph: u32,
1508 pw: u32,
1509 dh: u32,
1510 dw: u32,
1511 groups: u32,
1512 },
1513
1514 QMatMul {
1522 x: usize,
1523 w: usize,
1524 bias: usize,
1525 out: usize,
1526 m: u32,
1527 k: u32,
1528 n: u32,
1529 x_zp: i32,
1530 w_zp: i32,
1531 out_zp: i32,
1532 mult: f32,
1533 },
1534
1535 QConv2d {
1539 x: usize,
1540 w: usize,
1541 bias: usize,
1542 out: usize,
1543 n: u32,
1544 c_in: u32,
1545 h: u32,
1546 w_in: u32,
1547 c_out: u32,
1548 h_out: u32,
1549 w_out: u32,
1550 kh: u32,
1551 kw: u32,
1552 sh: u32,
1553 sw: u32,
1554 ph: u32,
1555 pw: u32,
1556 dh: u32,
1557 dw: u32,
1558 groups: u32,
1559 x_zp: i32,
1560 w_zp: i32,
1561 out_zp: i32,
1562 mult: f32,
1563 },
1564
1565 Quantize {
1572 x: usize,
1573 q: usize,
1574 len: u32,
1575 chan_axis: u32,
1576 chan_dim: u32,
1577 inner: u32,
1578 scales: Vec<f32>,
1579 zero_points: Vec<i32>,
1580 },
1581
1582 Dequantize {
1584 q: usize,
1585 x: usize,
1586 len: u32,
1587 chan_axis: u32,
1588 chan_dim: u32,
1589 inner: u32,
1590 scales: Vec<f32>,
1591 zero_points: Vec<i32>,
1592 },
1593
1594 FakeQuantize {
1605 x: usize,
1606 out: usize,
1607 len: u32,
1608 chan_axis: u32,
1609 chan_dim: u32,
1610 inner: u32,
1611 bits: u8,
1612 ste: rlx_ir::op::SteKind,
1616 scale_mode: rlx_ir::op::ScaleMode,
1621 state_off: Option<usize>,
1625 },
1626
1627 FakeQuantizeBackward {
1632 x: usize,
1633 dy: usize,
1634 dx: usize,
1635 len: u32,
1636 chan_axis: u32,
1637 chan_dim: u32,
1638 inner: u32,
1639 bits: u8,
1640 ste: rlx_ir::op::SteKind,
1641 },
1642
1643 FakeQuantizeLSQ {
1646 x: usize,
1647 scale_off: usize,
1648 out: usize,
1649 len: u32,
1650 chan_axis: u32,
1651 chan_dim: u32,
1652 inner: u32,
1653 bits: u8,
1654 },
1655
1656 FakeQuantizeLSQBackwardX {
1659 x: usize,
1660 scale_off: usize,
1661 dy: usize,
1662 dx: usize,
1663 len: u32,
1664 chan_axis: u32,
1665 chan_dim: u32,
1666 inner: u32,
1667 bits: u8,
1668 },
1669
1670 FakeQuantizeLSQBackwardScale {
1675 x: usize,
1676 scale_off: usize,
1677 dy: usize,
1678 dscale: usize,
1679 len: u32,
1680 chan_axis: u32,
1681 chan_dim: u32,
1682 inner: u32,
1683 bits: u8,
1684 },
1685
1686 ReluBackward {
1688 x: usize,
1689 dy: usize,
1690 dx: usize,
1691 len: u32,
1692 },
1693 ReluBackwardF64 {
1699 x: usize,
1700 dy: usize,
1701 dx: usize,
1702 len: u32,
1703 },
1704
1705 ActivationBackward {
1710 x: usize,
1711 dy: usize,
1712 dx: usize,
1713 len: u32,
1714 kind: Activation,
1715 },
1716 ActivationBackwardF64 {
1722 x: usize,
1723 dy: usize,
1724 dx: usize,
1725 len: u32,
1726 kind: Activation,
1727 },
1728
1729 LayerNormBackwardInput {
1732 x: usize,
1733 gamma: usize,
1734 dy: usize,
1735 dx: usize,
1736 rows: u32,
1737 h: u32,
1738 eps: f32,
1739 },
1740
1741 LayerNormBackwardGamma {
1743 x: usize,
1744 dy: usize,
1745 dgamma: usize,
1746 rows: u32,
1747 h: u32,
1748 eps: f32,
1749 },
1750
1751 RmsNormBackwardInput {
1752 x: usize,
1753 gamma: usize,
1754 beta: usize,
1755 dy: usize,
1756 dx: usize,
1757 rows: u32,
1758 h: u32,
1759 eps: f32,
1760 },
1761 RmsNormBackwardGamma {
1762 x: usize,
1763 gamma: usize,
1764 beta: usize,
1765 dy: usize,
1766 dgamma: usize,
1767 rows: u32,
1768 h: u32,
1769 eps: f32,
1770 },
1771 RmsNormBackwardBeta {
1772 x: usize,
1773 gamma: usize,
1774 beta: usize,
1775 dy: usize,
1776 dbeta: usize,
1777 rows: u32,
1778 h: u32,
1779 eps: f32,
1780 },
1781 RopeBackward {
1782 dy: usize,
1783 cos: usize,
1784 sin: usize,
1785 dx: usize,
1786 batch: u32,
1787 seq: u32,
1788 hidden: u32,
1789 head_dim: u32,
1790 n_rot: u32,
1791 cos_len: u32,
1792 },
1793 CumsumBackward {
1794 dy: usize,
1795 dx: usize,
1796 rows: u32,
1797 cols: u32,
1798 exclusive: bool,
1799 },
1800 GatherBackward {
1801 dy: usize,
1802 indices: usize,
1803 dst: usize,
1804 outer: u32,
1805 axis_dim: u32,
1806 num_idx: u32,
1807 trailing: u32,
1808 },
1809
1810 GroupNormBackwardInput {
1811 x: usize,
1812 gamma: usize,
1813 beta: usize,
1814 dy: usize,
1815 dx: usize,
1816 n: u32,
1817 c: u32,
1818 h: u32,
1819 w: u32,
1820 num_groups: u32,
1821 eps: f32,
1822 },
1823 GroupNormBackwardGamma {
1824 x: usize,
1825 dy: usize,
1826 dgamma: usize,
1827 n: u32,
1828 c: u32,
1829 h: u32,
1830 w: u32,
1831 num_groups: u32,
1832 eps: f32,
1833 },
1834 GroupNormBackwardBeta {
1835 dy: usize,
1836 dbeta: usize,
1837 n: u32,
1838 c: u32,
1839 h: u32,
1840 w: u32,
1841 },
1842
1843 MaxPool2dBackward {
1849 x: usize,
1850 dy: usize,
1851 dx: usize,
1852 n: u32,
1853 c: u32,
1854 h: u32,
1855 w: u32,
1856 h_out: u32,
1857 w_out: u32,
1858 kh: u32,
1859 kw: u32,
1860 sh: u32,
1861 sw: u32,
1862 ph: u32,
1863 pw: u32,
1864 },
1865
1866 Conv2dBackwardInput {
1870 dy: usize,
1871 w: usize,
1872 dx: usize,
1873 n: u32,
1874 c_in: u32,
1875 h: u32,
1876 w_in: u32,
1877 c_out: u32,
1878 h_out: u32,
1879 w_out: u32,
1880 kh: u32,
1881 kw: u32,
1882 sh: u32,
1883 sw: u32,
1884 ph: u32,
1885 pw: u32,
1886 dh: u32,
1887 dw: u32,
1888 groups: u32,
1889 },
1890
1891 Conv2dBackwardWeight {
1895 x: usize,
1896 dy: usize,
1897 dw: usize,
1898 n: u32,
1899 c_in: u32,
1900 h: u32,
1901 w: u32,
1902 c_out: u32,
1903 h_out: u32,
1904 w_out: u32,
1905 kh: u32,
1906 kw: u32,
1907 sh: u32,
1908 sw: u32,
1909 ph: u32,
1910 pw: u32,
1911 dh: u32,
1912 dw_dil: u32,
1913 groups: u32,
1914 },
1915
1916 Im2Col {
1919 x: usize,
1920 col: usize,
1921 n: u32,
1922 c_in: u32,
1923 h: u32,
1924 w: u32,
1925 h_out: u32,
1926 w_out: u32,
1927 kh: u32,
1928 kw: u32,
1929 sh: u32,
1930 sw: u32,
1931 ph: u32,
1932 pw: u32,
1933 dh: u32,
1934 dw_dil: u32,
1935 },
1936
1937 SoftmaxCrossEntropyDense {
1942 logits: usize,
1943 targets: usize,
1944 dst: usize,
1945 n: u32,
1946 c: u32,
1947 },
1948
1949 SoftmaxCrossEntropy {
1953 logits: usize,
1954 labels: usize,
1955 dst: usize,
1956 n: u32,
1957 c: u32,
1958 },
1959
1960 SoftmaxCrossEntropyBackward {
1963 logits: usize,
1964 labels: usize,
1965 d_loss: usize,
1966 dlogits: usize,
1967 n: u32,
1968 c: u32,
1969 },
1970
1971 CustomOp {
1977 kernel: Arc<dyn CpuKernel>,
1978 inputs: Vec<(usize, u32, Shape)>, output: (usize, u32, Shape), attrs: Vec<u8>,
1981 },
1982
1983 GaussianSplatRender {
1993 positions_off: usize,
1994 positions_len: usize,
1995 scales_off: usize,
1996 scales_len: usize,
1997 rotations_off: usize,
1998 rotations_len: usize,
1999 opacities_off: usize,
2000 opacities_len: usize,
2001 colors_off: usize,
2002 colors_len: usize,
2003 sh_coeffs_off: usize,
2004 sh_coeffs_len: usize,
2005 meta_off: usize,
2006 dst_off: usize,
2007 dst_len: usize,
2008 width: u32,
2009 height: u32,
2010 tile_size: u32,
2011 radius_scale: f32,
2012 alpha_cutoff: f32,
2013 max_splat_steps: u32,
2014 transmittance_threshold: f32,
2015 max_list_entries: u32,
2016 },
2017 GaussianSplatRenderBackward {
2018 positions_off: usize,
2019 positions_len: usize,
2020 scales_off: usize,
2021 scales_len: usize,
2022 rotations_off: usize,
2023 rotations_len: usize,
2024 opacities_off: usize,
2025 opacities_len: usize,
2026 colors_off: usize,
2027 colors_len: usize,
2028 sh_coeffs_off: usize,
2029 sh_coeffs_len: usize,
2030 meta_off: usize,
2031 d_loss_off: usize,
2032 d_loss_len: usize,
2033 packed_off: usize,
2034 packed_len: usize,
2035 width: u32,
2036 height: u32,
2037 tile_size: u32,
2038 radius_scale: f32,
2039 alpha_cutoff: f32,
2040 max_splat_steps: u32,
2041 transmittance_threshold: f32,
2042 max_list_entries: u32,
2043 loss_grad_clip: f32,
2044 sh_band: u32,
2045 max_anisotropy: f32,
2046 },
2047 GaussianSplatPrepare {
2049 positions_off: usize,
2050 positions_len: usize,
2051 scales_off: usize,
2052 scales_len: usize,
2053 rotations_off: usize,
2054 rotations_len: usize,
2055 opacities_off: usize,
2056 opacities_len: usize,
2057 colors_off: usize,
2058 colors_len: usize,
2059 sh_coeffs_off: usize,
2060 sh_coeffs_len: usize,
2061 meta_off: usize,
2062 meta_len: usize,
2063 prep_off: usize,
2064 prep_len: usize,
2065 width: u32,
2066 height: u32,
2067 tile_size: u32,
2068 radius_scale: f32,
2069 alpha_cutoff: f32,
2070 max_splat_steps: u32,
2071 transmittance_threshold: f32,
2072 max_list_entries: u32,
2073 },
2074 GaussianSplatRasterize {
2076 prep_off: usize,
2077 prep_len: usize,
2078 meta_off: usize,
2079 meta_len: usize,
2080 dst_off: usize,
2081 dst_len: usize,
2082 count: usize,
2083 width: u32,
2084 height: u32,
2085 tile_size: u32,
2086 alpha_cutoff: f32,
2087 max_splat_steps: u32,
2088 transmittance_threshold: f32,
2089 max_list_entries: u32,
2090 },
2091 Fft1d {
2092 src: usize,
2093 dst: usize,
2094 outer: u32,
2095 n_complex: u32,
2096 inverse: bool,
2097 norm_tag: u32,
2098 dtype: rlx_ir::DType,
2099 },
2100 FftButterflyStage {
2101 state_src: usize,
2102 state_dst: usize,
2103 gate_src: usize,
2104 rev_src: usize,
2105 tw_re_src: usize,
2106 tw_im_src: usize,
2107 batch: u32,
2108 n_fft: u32,
2109 stage: u32,
2110 },
2111 LogMel {
2112 spec: usize,
2113 filters: usize,
2114 dst: usize,
2115 outer: u32,
2116 n_fft: u32,
2117 n_bins: u32,
2118 n_mels: u32,
2119 },
2120 LogMelBackward {
2121 spec: usize,
2122 filters: usize,
2123 dy: usize,
2124 dst: usize,
2125 outer: u32,
2126 n_fft: u32,
2127 n_bins: u32,
2128 n_mels: u32,
2129 },
2130 WelchPeaks {
2131 spec: usize,
2132 dst: usize,
2133 welch_batch: u32,
2134 n_fft: u32,
2135 n_segments: u32,
2136 k: u32,
2137 },
2138}
2139
2140#[derive(Clone)]
2143pub struct ThunkSchedule {
2144 pub thunks: Vec<Thunk>,
2145 pub moe_resident: Option<std::sync::Arc<[bool]>>,
2147 pub moe_resident_layers: Option<std::sync::Arc<Vec<std::sync::Arc<[bool]>>>>,
2149 pub moe_topk_capture: Option<std::sync::Arc<crate::moe_topk_capture::MoeTopkCapture>>,
2151 pub mask_threshold: f32,
2153 pub mask_neg_inf: f32,
2154 pub score_skip: f32,
2155 pub compiled_fns: Vec<Arc<dyn Fn(*mut u8) + Send + Sync>>,
2161 pub rng: Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
2163}
2164
2165impl ThunkSchedule {
2166 pub fn strip_nops(&mut self) {
2167 self.thunks.retain(|t| !matches!(t, Thunk::Nop));
2168 self.compiled_fns.clear();
2171 }
2172}
2173
2174fn node_offset(arena: &Arena, id: NodeId) -> usize {
2176 if arena.has_buffer(id) {
2177 arena.byte_offset(id)
2178 } else {
2179 usize::MAX
2180 }
2181}
2182
2183fn thunk_read_offsets(t: &Thunk) -> Vec<usize> {
2189 match t {
2190 Thunk::Sgemm { a, b, .. } => vec![*a, *b],
2191 Thunk::SgemmT { a, b, .. } => vec![*a, *b],
2192 Thunk::SgdMomentum {
2193 param, vel, grad, ..
2194 } => vec![*param, *vel, *grad],
2195 Thunk::DenseSolveF64 { a, b, .. } => vec![*a, *b],
2196 Thunk::DenseSolveF32 { a, b, .. } => vec![*a, *b],
2197 Thunk::BatchedDenseSolveF64 { a, b, .. } => vec![*a, *b],
2198 Thunk::BatchedDgemmF64 { a, b, .. } => vec![*a, *b],
2199 Thunk::BatchedSgemm { a, b, .. } => vec![*a, *b],
2200 Thunk::FusedMmBiasAct { a, w, bias, .. } => vec![*a, *w, *bias],
2201 Thunk::ElementwiseRegion { input_offs, .. } => input_offs.clone(),
2202 Thunk::BiasAdd { src, bias, .. } => vec![*src, *bias],
2203 Thunk::BinaryFull { lhs, rhs, .. } => vec![*lhs, *rhs],
2204 Thunk::BinaryFullF64 { lhs, rhs, .. } => vec![*lhs, *rhs],
2205 Thunk::BinaryFullC64 { lhs, rhs, .. } => vec![*lhs, *rhs],
2206 Thunk::ComplexNormSqF32 { src, .. } => vec![*src],
2207 Thunk::ComplexNormSqBackwardF32 { z, g, .. } => vec![*z, *g],
2208 Thunk::ConjugateC64 { src, .. } => vec![*src],
2209 Thunk::Scan {
2210 outer_init_off,
2211 xs_inputs,
2212 ..
2213 } => {
2214 let mut v = vec![*outer_init_off];
2215 for (_, outer_xs_off, _) in xs_inputs.iter() {
2216 v.push(*outer_xs_off);
2217 }
2218 v
2219 }
2220 Thunk::ScanBackward {
2221 outer_init_off,
2222 outer_traj_off,
2223 outer_upstream_off,
2224 outer_xs_offs,
2225 ..
2226 } => {
2227 let mut v = vec![*outer_init_off, *outer_traj_off, *outer_upstream_off];
2228 for (off, _) in outer_xs_offs.iter() {
2229 v.push(*off);
2230 }
2231 v
2232 }
2233 Thunk::ScanBackwardXs {
2234 outer_init_off,
2235 outer_traj_off,
2236 outer_upstream_off,
2237 outer_xs_offs,
2238 ..
2239 } => {
2240 let mut v = vec![*outer_init_off, *outer_traj_off, *outer_upstream_off];
2241 for (off, _) in outer_xs_offs.iter() {
2242 v.push(*off);
2243 }
2244 v
2245 }
2246 Thunk::CustomFn { inputs, .. } => {
2247 inputs.iter().map(|(_, outer_off, _)| *outer_off).collect()
2248 }
2249 Thunk::ActivationInPlace { data, .. } => vec![*data],
2250 Thunk::LayerNorm { src, g, b, .. } | Thunk::GroupNorm { src, g, b, .. } => {
2251 vec![*src, *g, *b]
2252 }
2253 Thunk::BatchNormInference {
2254 src,
2255 g,
2256 b,
2257 mean,
2258 var,
2259 ..
2260 } => vec![*src, *g, *b, *mean, *var],
2261 Thunk::ResizeNearest2x { src, .. } => vec![*src],
2262 Thunk::AxialRope2d { src, .. } => vec![*src],
2263 Thunk::FusedResidualLN {
2264 x, res, bias, g, b, ..
2265 } => vec![*x, *res, *bias, *g, *b],
2266 Thunk::FusedResidualRmsNorm {
2267 x, res, bias, g, b, ..
2268 } => vec![*x, *res, *bias, *g, *b],
2269 Thunk::RmsNorm { src, g, b, .. } => vec![*src, *g, *b],
2270 Thunk::Softmax { data, .. } => vec![*data],
2271 Thunk::Cumsum { src, .. } => vec![*src],
2272 Thunk::Sample { logits, .. } => vec![*logits],
2273 Thunk::RngNormal { .. } | Thunk::RngUniform { .. } => vec![],
2274 Thunk::LoraMatMul { x, w, a, b, .. } => vec![*x, *w, *a, *b],
2275 Thunk::DequantMatMul {
2276 x, w_q, scale, zp, ..
2277 } => vec![*x, *w_q, *scale, *zp],
2278 Thunk::DequantMatMulGguf { x, w_q, .. } => vec![*x, *w_q],
2279 Thunk::DequantMatMulInt4 {
2280 x, w_q, scale, zp, ..
2281 } => vec![*x, *w_q, *scale, *zp],
2282 Thunk::DequantMatMulFp8 { x, w_q, scale, .. } => vec![*x, *w_q, *scale],
2283 Thunk::DequantMatMulNvfp4 {
2284 x,
2285 w_q,
2286 scale,
2287 global_scale,
2288 ..
2289 } => vec![*x, *w_q, *scale, *global_scale],
2290 Thunk::ScaledMatMul {
2291 lhs,
2292 rhs,
2293 lhs_scale,
2294 rhs_scale,
2295 bias,
2296 has_bias,
2297 ..
2298 } => {
2299 let mut v = vec![*lhs, *rhs, *lhs_scale, *rhs_scale];
2300 if *has_bias {
2301 v.push(*bias);
2302 }
2303 v
2304 }
2305 Thunk::ScaledQuantize { x, scale, .. } => vec![*x, *scale],
2306 Thunk::ScaledQuantScale { x, .. } => vec![*x],
2307 Thunk::ScaledDequantize { codes, scale, .. } => vec![*codes, *scale],
2308 Thunk::Conv2D1x1 { src, weight, .. } => vec![*src, *weight],
2309 Thunk::SelectiveScan {
2310 x, delta, a, b, c, ..
2311 } => vec![*x, *delta, *a, *b, *c],
2312 Thunk::GatedDeltaNet {
2313 q,
2314 k,
2315 v,
2316 g,
2317 beta,
2318 state,
2319 ..
2320 } => {
2321 let mut v = vec![*q, *k, *v, *g, *beta];
2322 if *state != 0 {
2323 v.push(*state);
2324 }
2325 v
2326 }
2327 Thunk::Attention { q, k, v, mask, .. } => vec![*q, *k, *v, *mask],
2328 Thunk::AttentionBackward {
2329 q, k, v, dy, mask, ..
2330 } => {
2331 let mut v = vec![*q, *k, *v, *dy];
2332 if *mask != 0 {
2333 v.push(*mask);
2334 }
2335 v
2336 }
2337 Thunk::Rope { src, cos, sin, .. } => vec![*src, *cos, *sin],
2338 Thunk::FusedAttnBlock {
2339 hidden,
2340 qkv_w,
2341 out_w,
2342 mask,
2343 qkv_b,
2344 out_b,
2345 cos,
2346 sin,
2347 ..
2348 } => vec![*hidden, *qkv_w, *out_w, *mask, *qkv_b, *out_b, *cos, *sin],
2349 Thunk::FusedSwiGLU { src, .. } => vec![*src],
2350 Thunk::Concat { inputs, .. } => inputs.iter().map(|(off, _, _)| *off).collect(),
2351 Thunk::ConcatF64 { inputs, .. } => inputs.iter().map(|(off, _, _)| *off).collect(),
2352 Thunk::Narrow { src, .. } => vec![*src],
2353 Thunk::Copy { src, .. } => vec![*src],
2354 Thunk::Gather { table, idx, .. } => vec![*table, *idx],
2355 _ => vec![],
2359 }
2360}
2361
2362#[allow(clippy::too_many_arguments)]
2376pub fn dequant_matmul_int8(
2377 x: &[f32], w_bytes: &[i8], scales: &[f32], zps: &[f32], out: &mut [f32], m: usize,
2383 k: usize,
2384 n: usize,
2385 block_size: usize,
2386 asym: bool,
2387) {
2388 let blocks_per_col = k.div_ceil(block_size);
2389 for i in 0..m {
2390 for j in 0..n {
2391 let mut acc = 0f32;
2392 for p in 0..k {
2393 let block = p / block_size;
2394 let s = scales[block * n + j];
2395 let z = if asym { zps[block * n + j] } else { 0.0 };
2396 let q = w_bytes[p * n + j] as f32;
2397 let dequantized = (q - z) * s;
2398 acc += x[i * k + p] * dequantized;
2399 }
2400 out[i * n + j] = acc;
2401 }
2402 }
2403 let _ = blocks_per_col;
2404}
2405
2406#[allow(clippy::too_many_arguments)]
2407fn dequant_matmul_int4(
2408 x: &[f32],
2409 w_bytes: &[u8],
2410 scales: &[f32],
2411 zps: &[f32],
2412 out: &mut [f32],
2413 m: usize,
2414 k: usize,
2415 n: usize,
2416 block_size: usize,
2417 asym: bool,
2418) {
2419 for i in 0..m {
2420 for j in 0..n {
2421 let mut acc = 0f32;
2422 for p in 0..k {
2423 let block = p / block_size;
2424 let s = scales[block * n + j];
2425 let z = if asym { zps[block * n + j] } else { 0.0 };
2426 let byte_idx = (p * n + j) / 2;
2427 let nibble = if (p * n + j) & 1 == 0 {
2428 w_bytes[byte_idx] & 0x0F
2429 } else {
2430 w_bytes[byte_idx] >> 4
2431 };
2432 let dequantized = (nibble as f32 - z) * s;
2433 acc += x[i * k + p] * dequantized;
2434 }
2435 out[i * n + j] = acc;
2436 }
2437 }
2438}
2439
2440fn fp8_e4m3_to_f32(b: u8) -> f32 {
2441 let sign = if b & 0x80 != 0 { -1.0 } else { 1.0 };
2442 let exp = (b >> 3) & 0x0F;
2443 let mant = b & 0x07;
2444 if exp == 0 {
2445 if mant == 0 {
2446 return 0.0;
2447 }
2448 return sign * (mant as f32) * 2f32.powi(-9);
2449 }
2450 if exp == 0x0F {
2451 return if mant == 0 {
2452 sign * f32::INFINITY
2453 } else {
2454 f32::NAN
2455 };
2456 }
2457 sign * (1.0 + mant as f32 / 8.0) * 2f32.powi(exp as i32 - 7)
2458}
2459
2460fn fp8_e5m2_to_f32(b: u8) -> f32 {
2461 let sign = if b & 0x80 != 0 { -1.0 } else { 1.0 };
2462 let exp = (b >> 2) & 0x1F;
2463 let mant = b & 0x03;
2464 if exp == 0 {
2465 if mant == 0 {
2466 return 0.0;
2467 }
2468 return sign * (mant as f32) * 2f32.powi(-16);
2469 }
2470 if exp == 0x1F {
2471 return if mant == 0 {
2472 sign * f32::INFINITY
2473 } else {
2474 f32::NAN
2475 };
2476 }
2477 sign * (1.0 + mant as f32 / 4.0) * 2f32.powi(exp as i32 - 15)
2478}
2479
2480#[allow(clippy::too_many_arguments)]
2481fn dequant_matmul_fp8(
2482 x: &[f32],
2483 w_bytes: &[u8],
2484 scales: &[f32],
2485 out: &mut [f32],
2486 m: usize,
2487 k: usize,
2488 n: usize,
2489 e5m2: bool,
2490) {
2491 let dequant = if e5m2 {
2492 fp8_e5m2_to_f32
2493 } else {
2494 fp8_e4m3_to_f32
2495 };
2496 for i in 0..m {
2497 for j in 0..n {
2498 let mut acc = 0f32;
2499 for p in 0..k {
2500 let w = dequant(w_bytes[p * n + j]);
2501 let s = scales.get(j).copied().unwrap_or(1.0);
2502 acc += x[i * k + p] * w * s;
2503 }
2504 out[i * n + j] = acc;
2505 }
2506 }
2507}
2508
2509#[allow(clippy::too_many_arguments)]
2510pub fn dequant_matmul_nvfp4(
2511 x: &[f32],
2512 w_bytes: &[u8],
2513 scale_bytes: &[u8],
2514 global_scale: f32,
2515 out: &mut [f32],
2516 m: usize,
2517 k: usize,
2518 n: usize,
2519) {
2520 use rlx_ir::{NVFP4_GROUP_SIZE, fp4_e2m1_to_f32, fp8_e4m3_scale_to_f32};
2521 let gs = NVFP4_GROUP_SIZE;
2522 for i in 0..m {
2523 for j in 0..n {
2524 let mut acc = 0f32;
2525 for p in 0..k {
2526 let byte_idx = (p * n + j) / 2;
2527 let nibble = if (p * n + j) & 1 == 0 {
2528 w_bytes[byte_idx] & 0x0F
2529 } else {
2530 w_bytes[byte_idx] >> 4
2531 };
2532 let block = p / gs;
2533 let scale = fp8_e4m3_scale_to_f32(scale_bytes[block * n + j]);
2534 let w = fp4_e2m1_to_f32(nibble) * scale * global_scale;
2535 acc += x[i * k + p] * w;
2536 }
2537 out[i * n + j] = acc;
2538 }
2539 }
2540}
2541
2542#[inline]
2552fn lowp_nblk(len: usize, layout: rlx_ir::ScaleLayout) -> usize {
2553 match layout {
2554 rlx_ir::ScaleLayout::PerTensor => 1,
2555 _ => len.div_ceil(layout.block() as usize),
2556 }
2557}
2558
2559#[inline]
2562fn lowp_snap_scale(layout: rlx_ir::ScaleLayout, s: f32) -> f32 {
2563 use rlx_ir::lowp_codec;
2564 match layout {
2565 rlx_ir::ScaleLayout::PerTensor => s,
2566 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
2567 lowp_codec::e8m0_to_f32(lowp_codec::f32_to_e8m0(s))
2568 }
2569 rlx_ir::ScaleLayout::Nvfp4 { .. } => lowp_codec::decode(
2570 rlx_ir::ScaledFormat::F8E4M3,
2571 lowp_codec::encode(rlx_ir::ScaledFormat::F8E4M3, s),
2572 ),
2573 }
2574}
2575
2576#[inline]
2578fn lowp_scale_at(
2579 layout: rlx_ir::ScaleLayout,
2580 scales: &[f32],
2581 free: usize,
2582 contract: usize,
2583 nblk: usize,
2584) -> f32 {
2585 match layout {
2586 rlx_ir::ScaleLayout::PerTensor => scales.first().copied().unwrap_or(1.0),
2587 _ => scales[free * nblk + contract / layout.block() as usize],
2588 }
2589}
2590
2591fn lowp_compute_scales(
2594 x: &[f32],
2595 fmt: rlx_ir::ScaledFormat,
2596 layout: rlx_ir::ScaleLayout,
2597 rows: usize,
2598 cols: usize,
2599) -> Vec<f32> {
2600 let maxf = fmt.max_finite();
2601 let to_scale = |amax: f32| if amax > 0.0 { amax / maxf } else { 1.0 };
2602 match layout {
2603 rlx_ir::ScaleLayout::PerTensor => {
2604 let amax = x.iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2605 vec![to_scale(amax)]
2606 }
2607 _ => {
2608 let block = layout.block() as usize;
2609 let nblk = cols.div_ceil(block);
2610 let mut out = vec![1.0f32; rows * nblk];
2611 for r in 0..rows {
2612 for b in 0..nblk {
2613 let lo = b * block;
2614 let hi = (lo + block).min(cols);
2615 let mut amax = 0.0f32;
2616 for c in lo..hi {
2617 amax = amax.max(x[r * cols + c].abs());
2618 }
2619 out[r * nblk + b] = lowp_snap_scale(layout, to_scale(amax));
2620 }
2621 }
2622 out
2623 }
2624 }
2625}
2626
2627fn lowp_quantize(
2630 x: &[f32],
2631 scales: &[f32],
2632 fmt: rlx_ir::ScaledFormat,
2633 layout: rlx_ir::ScaleLayout,
2634 rows: usize,
2635 cols: usize,
2636 out: &mut [u8],
2637) {
2638 let nblk = lowp_nblk(cols, layout);
2639 for r in 0..rows {
2640 for c in 0..cols {
2641 let s = lowp_scale_at(layout, scales, r, c, nblk);
2642 let v = if s != 0.0 { x[r * cols + c] / s } else { 0.0 };
2643 out[r * cols + c] = rlx_ir::lowp_codec::encode(fmt, v);
2644 }
2645 }
2646}
2647
2648#[allow(clippy::too_many_arguments)]
2650fn lowp_scaled_matmul(
2651 lhs: &[u8],
2652 rhs: &[u8],
2653 lhs_scales: &[f32],
2654 rhs_scales: &[f32],
2655 bias: Option<&[f32]>,
2656 out: &mut [f32],
2657 m: usize,
2658 n: usize,
2659 k: usize,
2660 layout: rlx_ir::ScaleLayout,
2661 lhs_fmt: rlx_ir::ScaledFormat,
2662 rhs_fmt: rlx_ir::ScaledFormat,
2663) {
2664 use rlx_ir::lowp_codec::decode;
2665 let nblk = lowp_nblk(k, layout);
2666 for i in 0..m {
2667 for j in 0..n {
2668 let mut acc = 0f32;
2669 for p in 0..k {
2670 let a =
2671 decode(lhs_fmt, lhs[i * k + p]) * lowp_scale_at(layout, lhs_scales, i, p, nblk);
2672 let b =
2673 decode(rhs_fmt, rhs[j * k + p]) * lowp_scale_at(layout, rhs_scales, j, p, nblk);
2674 acc += a * b;
2675 }
2676 out[i * n + j] = acc + bias.map_or(0.0, |bb| bb[j]);
2677 }
2678 }
2679}
2680
2681fn lowp_dequantize(
2684 codes: &[u8],
2685 scales: &[f32],
2686 fmt: rlx_ir::ScaledFormat,
2687 layout: rlx_ir::ScaleLayout,
2688 rows: usize,
2689 cols: usize,
2690 out: &mut [f32],
2691) {
2692 use rlx_ir::lowp_codec::decode;
2693 let nblk = lowp_nblk(cols, layout);
2694 for r in 0..rows {
2695 for c in 0..cols {
2696 let s = lowp_scale_at(layout, scales, r, c, nblk);
2697 out[r * cols + c] = decode(fmt, codes[r * cols + c]) * s;
2698 }
2699 }
2700}
2701
2702unsafe fn lowp_read_scales(
2705 layout: rlx_ir::ScaleLayout,
2706 base: *mut u8,
2707 offset: usize,
2708 n: usize,
2709) -> Vec<f32> {
2710 use rlx_ir::lowp_codec;
2711 match layout {
2712 rlx_ir::ScaleLayout::PerTensor => {
2713 unsafe { std::slice::from_raw_parts(base.add(offset) as *const f32, n) }.to_vec()
2714 }
2715 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
2716 let bytes = unsafe { std::slice::from_raw_parts(base.add(offset), n) };
2717 bytes.iter().map(|&b| lowp_codec::e8m0_to_f32(b)).collect()
2718 }
2719 rlx_ir::ScaleLayout::Nvfp4 { .. } => {
2720 let bytes = unsafe { std::slice::from_raw_parts(base.add(offset), n) };
2721 bytes
2722 .iter()
2723 .map(|&b| lowp_codec::decode(rlx_ir::ScaledFormat::F8E4M3, b))
2724 .collect()
2725 }
2726 }
2727}
2728
2729fn sample_row(
2738 logits: &[f32],
2739 top_k: usize,
2740 top_p: f32,
2741 temperature: f32,
2742 rng: &mut rlx_ir::Philox4x32,
2743) -> usize {
2744 let v = logits.len();
2745 if v == 0 {
2746 return 0;
2747 }
2748 let temp = temperature.max(1e-6);
2749 let mut scaled: Vec<f32> = logits.iter().map(|&x| x / temp).collect();
2751
2752 if top_k > 0 && top_k < v {
2754 let mut indexed: Vec<(usize, f32)> = scaled.iter().copied().enumerate().collect();
2756 indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
2759 let cutoff = indexed[top_k - 1].1;
2760 for x in scaled.iter_mut() {
2761 if *x < cutoff {
2762 *x = f32::NEG_INFINITY;
2763 }
2764 }
2765 }
2766
2767 let mut max_l = f32::NEG_INFINITY;
2769 for &x in &scaled {
2770 if x > max_l {
2771 max_l = x;
2772 }
2773 }
2774 let mut sum = 0.0f32;
2775 for x in scaled.iter_mut() {
2776 *x = (*x - max_l).exp();
2777 sum += *x;
2778 }
2779 let inv = 1.0 / sum.max(f32::MIN_POSITIVE);
2780 for x in scaled.iter_mut() {
2781 *x *= inv;
2782 }
2783
2784 if top_p < 1.0 {
2787 let mut indexed: Vec<(usize, f32)> = scaled.iter().copied().enumerate().collect();
2788 indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
2789 let mut cum = 0.0f32;
2790 let mut keep = vec![false; v];
2791 for (idx, p) in indexed.iter() {
2792 keep[*idx] = true;
2793 cum += *p;
2794 if cum >= top_p {
2795 break;
2796 }
2797 }
2798 let mut new_sum = 0.0f32;
2799 for (i, x) in scaled.iter_mut().enumerate() {
2800 if !keep[i] {
2801 *x = 0.0;
2802 }
2803 new_sum += *x;
2804 }
2805 let inv = 1.0 / new_sum.max(f32::MIN_POSITIVE);
2806 for x in scaled.iter_mut() {
2807 *x *= inv;
2808 }
2809 }
2810
2811 let r = rng.next_f32();
2813 let mut acc = 0.0f32;
2814 for (i, &p) in scaled.iter().enumerate() {
2815 acc += p;
2816 if r <= acc {
2817 return i;
2818 }
2819 }
2820 v - 1 }
2822
2823#[inline]
2827fn apply_synthetic_mask(
2828 scores: &mut [f32],
2829 q_seq: usize,
2830 k_seq: usize,
2831 kind: rlx_ir::op::MaskKind,
2832) {
2833 let neg = crate::config::RuntimeConfig::global().attn_mask_neg_inf;
2834 let q_offset = k_seq.saturating_sub(q_seq);
2835 match kind {
2836 rlx_ir::op::MaskKind::None | rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias => {}
2837 rlx_ir::op::MaskKind::Causal => {
2838 for qi in 0..q_seq {
2839 let abs_q = q_offset + qi;
2840 for ki in (abs_q + 1)..k_seq {
2841 scores[qi * k_seq + ki] = neg;
2842 }
2843 }
2844 }
2845 rlx_ir::op::MaskKind::SlidingWindow(w) => {
2846 for qi in 0..q_seq {
2847 let abs_q = q_offset + qi;
2848 let lo = abs_q.saturating_sub(w);
2849 for ki in 0..k_seq {
2850 if ki < lo || ki > abs_q {
2851 scores[qi * k_seq + ki] = neg;
2852 }
2853 }
2854 }
2855 }
2856 }
2857}
2858
2859fn conv_nchw_dims(shape: &Shape) -> (u32, u32, u32, u32) {
2861 match shape.rank() {
2862 3 => (
2863 shape.dim(0).unwrap_static() as u32,
2864 shape.dim(1).unwrap_static() as u32,
2865 1,
2866 shape.dim(2).unwrap_static() as u32,
2867 ),
2868 4 => (
2869 shape.dim(0).unwrap_static() as u32,
2870 shape.dim(1).unwrap_static() as u32,
2871 shape.dim(2).unwrap_static() as u32,
2872 shape.dim(3).unwrap_static() as u32,
2873 ),
2874 r => panic!("conv_nchw_dims: expected rank 3 or 4, got {r}"),
2875 }
2876}
2877
2878fn conv_ncdhw_dims(shape: &Shape) -> (u32, u32, u32, u32, u32) {
2880 assert_eq!(
2881 shape.rank(),
2882 5,
2883 "conv_ncdhw_dims: expected rank 5, got {}",
2884 shape.rank()
2885 );
2886 (
2887 shape.dim(0).unwrap_static() as u32,
2888 shape.dim(1).unwrap_static() as u32,
2889 shape.dim(2).unwrap_static() as u32,
2890 shape.dim(3).unwrap_static() as u32,
2891 shape.dim(4).unwrap_static() as u32,
2892 )
2893}
2894
2895pub fn compile_thunks(graph: &Graph, arena: &Arena) -> ThunkSchedule {
2897 compile_thunks_with_rng(graph, arena, rlx_ir::RngOptions::default())
2898}
2899
2900pub struct ScanBodyPlan {
2906 pub body: ThunkSchedule,
2907 pub body_init: Vec<u8>,
2908 pub body_input_off: usize,
2909 pub body_output_off: usize,
2910 pub carry_bytes: usize,
2911 pub bcast_body_offs: Vec<usize>,
2913 pub xs_body_offs: Vec<usize>,
2915}
2916
2917impl std::fmt::Debug for ScanBodyPlan {
2918 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2919 f.debug_struct("ScanBodyPlan")
2920 .field("carry_bytes", &self.carry_bytes)
2921 .field("body_input_off", &self.body_input_off)
2922 .field("body_output_off", &self.body_output_off)
2923 .field("num_bcast", &self.bcast_body_offs.len())
2924 .field("num_xs", &self.xs_body_offs.len())
2925 .finish()
2926 }
2927}
2928
2929pub fn compile_scan_body(body: &Graph, num_bcast: usize, num_xs: usize) -> ScanBodyPlan {
2934 let body_plan = rlx_opt::memory::plan_memory(body);
2935 let body_offsets: HashMap<NodeId, usize> = body_plan
2936 .assignments
2937 .iter()
2938 .map(|(id, slot)| (*id, slot.offset))
2939 .collect();
2940 let body_inputs: Vec<NodeId> = body
2941 .nodes()
2942 .iter()
2943 .filter(|n| matches!(n.op, Op::Input { .. }))
2944 .map(|n| n.id)
2945 .collect();
2946 assert_eq!(
2947 body_inputs.len(),
2948 1 + num_bcast + num_xs,
2949 "compile_scan_body: body has {} inputs, expected {}",
2950 body_inputs.len(),
2951 1 + num_bcast + num_xs
2952 );
2953 let body_input_off = body_offsets[&body_inputs[0]];
2954 let carry_bytes = body
2955 .node(body_inputs[0])
2956 .shape
2957 .size_bytes()
2958 .expect("scan carry must have static shape");
2959 let body_output_id = *body
2960 .outputs
2961 .first()
2962 .expect("scan body must declare one output");
2963 let body_output_off = body_offsets[&body_output_id];
2964 let bcast_body_offs: Vec<usize> = (0..num_bcast)
2965 .map(|i| body_offsets[&body_inputs[1 + i]])
2966 .collect();
2967 let xs_body_offs: Vec<usize> = (0..num_xs)
2968 .map(|i| body_offsets[&body_inputs[1 + num_bcast + i]])
2969 .collect();
2970
2971 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
2972 for n in body.nodes() {
2973 if let Op::Constant { data } = &n.op
2974 && body_arena.has_buffer(n.id)
2975 && !data.is_empty()
2976 {
2977 match n.shape.dtype() {
2978 rlx_ir::DType::F64 => {
2979 let off = body_arena.byte_offset(n.id);
2980 let buf = body_arena.raw_buf_mut();
2981 let nbytes = (buf.len() - off).min(data.len());
2982 buf[off..off + nbytes].copy_from_slice(&data[..nbytes]);
2983 }
2984 _ => {
2985 let buf = body_arena.slice_mut(n.id);
2986 let n_floats = data.len() / 4;
2987 let n_lim = buf.len().min(n_floats);
2988 for i in 0..n_lim {
2989 buf[i] = f32::from_le_bytes([
2990 data[i * 4],
2991 data[i * 4 + 1],
2992 data[i * 4 + 2],
2993 data[i * 4 + 3],
2994 ]);
2995 }
2996 }
2997 }
2998 }
2999 }
3000 let body_init = body_arena.raw_buf().to_vec();
3001 let body_schedule = compile_thunks(body, &body_arena);
3002 ScanBodyPlan {
3003 body: body_schedule,
3004 body_init,
3005 body_input_off,
3006 body_output_off,
3007 carry_bytes,
3008 bcast_body_offs,
3009 xs_body_offs,
3010 }
3011}
3012
3013pub unsafe fn execute_scan_host(
3024 base: *mut u8,
3025 plan: &ScanBodyPlan,
3026 outer_init_off: usize,
3027 outer_final_off: usize,
3028 length: u32,
3029 save_trajectory: bool,
3030 xs_outer: &[(usize, usize)],
3031 bcast_outer: &[(usize, usize)],
3032) {
3033 let cb = plan.carry_bytes;
3034 let mut body_buf: Vec<u8> = plan.body_init.clone();
3035 unsafe {
3036 std::ptr::copy_nonoverlapping(
3037 base.add(outer_init_off),
3038 body_buf.as_mut_ptr().add(plan.body_input_off),
3039 cb,
3040 );
3041 for (i, &(outer_b_off, total)) in bcast_outer.iter().enumerate() {
3042 std::ptr::copy_nonoverlapping(
3043 base.add(outer_b_off),
3044 body_buf.as_mut_ptr().add(plan.bcast_body_offs[i]),
3045 total,
3046 );
3047 }
3048 }
3049 for t in 0..length as usize {
3050 for (i, &(outer_xs_off, psb)) in xs_outer.iter().enumerate() {
3051 unsafe {
3052 std::ptr::copy_nonoverlapping(
3053 base.add(outer_xs_off + t * psb),
3054 body_buf.as_mut_ptr().add(plan.xs_body_offs[i]),
3055 psb,
3056 );
3057 }
3058 }
3059 execute_thunks(&plan.body, &mut body_buf);
3060 if save_trajectory {
3061 unsafe {
3062 std::ptr::copy_nonoverlapping(
3063 body_buf.as_ptr().add(plan.body_output_off),
3064 base.add(outer_final_off + t * cb),
3065 cb,
3066 );
3067 }
3068 }
3069 if plan.body_output_off != plan.body_input_off {
3070 body_buf.copy_within(
3071 plan.body_output_off..plan.body_output_off + cb,
3072 plan.body_input_off,
3073 );
3074 }
3075 }
3076 if !save_trajectory {
3077 unsafe {
3078 std::ptr::copy_nonoverlapping(
3079 body_buf.as_ptr().add(plan.body_output_off),
3080 base.add(outer_final_off),
3081 cb,
3082 );
3083 }
3084 }
3085}
3086
3087#[allow(unused_variables)]
3089pub fn compile_thunks_with_rng(
3090 graph: &Graph,
3091 arena: &Arena,
3092 rng: rlx_ir::RngOptions,
3093) -> ThunkSchedule {
3094 let rng_shared = Arc::new(std::sync::RwLock::new(rng));
3095 let mut thunks = Vec::with_capacity(graph.len());
3096
3097 let mut use_counts: std::collections::HashMap<NodeId, usize> = std::collections::HashMap::new();
3103 for n in graph.nodes() {
3104 for &i in &n.inputs {
3105 *use_counts.entry(i).or_insert(0) += 1;
3106 }
3107 }
3108 let is_t2 = |g: &Graph, id: NodeId| -> bool {
3109 matches!(&g.node(id).op, Op::Transpose { perm } if perm.as_slice() == [1, 0])
3110 && g.node(id).shape.rank() == 2
3111 };
3112 let mut folded_transpose: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
3113 let mut matmul_fold: std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)> =
3114 std::collections::HashMap::new();
3115 for n in graph.nodes() {
3116 if !matches!(n.op, Op::MatMul) {
3117 continue;
3118 }
3119 let (a_id, b_id) = (n.inputs[0], n.inputs[1]);
3120 if graph.node(a_id).shape.rank() != 2
3121 || graph.node(b_id).shape.rank() != 2
3122 || n.shape.dtype() != rlx_ir::DType::F32
3123 {
3124 continue;
3125 }
3126 let fold_a = is_t2(graph, a_id) && use_counts.get(&a_id) == Some(&1);
3127 let fold_b = is_t2(graph, b_id) && use_counts.get(&b_id) == Some(&1);
3128 if !fold_a && !fold_b {
3129 continue;
3130 }
3131 let (asrc, ta) = if fold_a {
3132 (graph.node(a_id).inputs[0], true)
3133 } else {
3134 (a_id, false)
3135 };
3136 let (bsrc, tb) = if fold_b {
3137 (graph.node(b_id).inputs[0], true)
3138 } else {
3139 (b_id, false)
3140 };
3141 matmul_fold.insert(n.id, (asrc, ta, bsrc, tb));
3142 if fold_a {
3143 folded_transpose.insert(a_id);
3144 }
3145 if fold_b {
3146 folded_transpose.insert(b_id);
3147 }
3148 }
3149
3150 let out_set: std::collections::HashSet<NodeId> = graph.outputs.iter().copied().collect();
3161 let const_scalar = |g: &Graph, id: NodeId, n: usize| -> Option<f32> {
3162 if let Op::Constant { data } = &g.node(id).op {
3163 if data.len() == n * 4 {
3164 return Some(f32::from_le_bytes([data[0], data[1], data[2], data[3]]));
3165 }
3166 }
3167 None
3168 };
3169 let mut sgd_fold: std::collections::HashMap<
3171 NodeId,
3172 (NodeId, NodeId, NodeId, NodeId, f32, f32, usize),
3173 > = std::collections::HashMap::new();
3174 let mut sgd_elim: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
3175 for n in graph.nodes() {
3176 if !matches!(n.op, Op::Binary(BinaryOp::Sub)) || n.shape.dtype() != rlx_ir::DType::F32 {
3178 continue;
3179 }
3180 if !out_set.contains(&n.id) {
3181 continue;
3182 }
3183 let len = match n.shape.num_elements() {
3184 Some(l) => l,
3185 None => continue,
3186 };
3187 let (param, lr_v) = (n.inputs[0], n.inputs[1]);
3188 let lrv = graph.node(lr_v);
3190 if !matches!(lrv.op, Op::Binary(BinaryOp::Mul)) || use_counts.get(&lr_v) != Some(&1) {
3191 continue;
3192 }
3193 let (v_new, lr_c) = (lrv.inputs[0], lrv.inputs[1]);
3194 let lr = match const_scalar(graph, lr_c, len) {
3195 Some(v) => v,
3196 None => continue,
3197 };
3198 let vnew = graph.node(v_new);
3200 if !matches!(vnew.op, Op::Binary(BinaryOp::Add))
3201 || use_counts.get(&v_new) != Some(&1)
3202 || !out_set.contains(&v_new)
3203 {
3204 continue;
3205 }
3206 let (v_scaled, grad) = (vnew.inputs[0], vnew.inputs[1]);
3207 let vs = graph.node(v_scaled);
3209 if !matches!(vs.op, Op::Binary(BinaryOp::Mul)) || use_counts.get(&v_scaled) != Some(&1) {
3210 continue;
3211 }
3212 let (vel, mom_c) = (vs.inputs[0], vs.inputs[1]);
3213 let mom = match const_scalar(graph, mom_c, len) {
3214 Some(v) => v,
3215 None => continue,
3216 };
3217 sgd_fold.insert(n.id, (param, vel, grad, v_new, lr, mom, len));
3218 sgd_elim.insert(v_scaled);
3219 sgd_elim.insert(v_new);
3220 sgd_elim.insert(lr_v);
3221 }
3222
3223 for node in graph.nodes() {
3224 if rlx_opt::is_pure_view(graph, node) {
3228 thunks.push(Thunk::Nop);
3229 continue;
3230 }
3231 if folded_transpose.contains(&node.id) {
3233 thunks.push(Thunk::Nop);
3234 continue;
3235 }
3236 if sgd_elim.contains(&node.id) {
3238 thunks.push(Thunk::Nop);
3239 continue;
3240 }
3241 let t = match &node.op {
3242 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => Thunk::Nop,
3243
3244 Op::FusedMatMulBiasAct { activation } => {
3245 compile_fused_mat_mul_bias_act(node, graph, arena, &matmul_fold, &rng_shared, rng)
3246 }
3247 Op::FusedResidualLN { has_bias, eps } => {
3248 compile_fused_residual_l_n(node, graph, arena, &matmul_fold, &rng_shared, rng)
3249 }
3250 Op::FusedResidualRmsNorm { has_bias, eps } => {
3251 compile_fused_residual_rms_norm(node, graph, arena, &matmul_fold, &rng_shared, rng)
3252 }
3253 Op::MatMul => compile_mat_mul(node, graph, arena, &matmul_fold, &rng_shared, rng),
3254 Op::Binary(op) => {
3255 if let Some(&(param, vel, grad, v_new, lr, mom, len)) = sgd_fold.get(&node.id) {
3256 thunks.push(Thunk::SgdMomentum {
3259 param: node_offset(arena, param),
3260 vel: node_offset(arena, vel),
3261 grad: node_offset(arena, grad),
3262 p_out: node_offset(arena, node.id),
3263 v_out: node_offset(arena, v_new),
3264 lr,
3265 mom,
3266 len: len as u32,
3267 });
3268 continue;
3269 }
3270 let lhs_len = get_len(graph, node.inputs[0]);
3271 let rhs_len = get_len(graph, node.inputs[1]);
3272 let out_len = node.shape.num_elements().unwrap();
3273 if node.shape.dtype() == rlx_ir::DType::C64 {
3274 match op {
3278 BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div => {}
3279 BinaryOp::Max | BinaryOp::Min | BinaryOp::Pow => panic!(
3280 "Op::Binary({op:?}) on DType::C64: complex \
3281 max/min/pow have no single natural definition \
3282 — caller should drop to 2N-real-block (see \
3283 spike-ac) and pick a convention there"
3284 ),
3285 }
3286 }
3287 let (out_dims_bcast, bcast_lhs_strides, bcast_rhs_strides) =
3291 if lhs_len == out_len && rhs_len == out_len {
3292 (Vec::new(), Vec::new(), Vec::new())
3293 } else {
3294 let lhs_dims = get_static_dims(graph, node.inputs[0]);
3295 let rhs_dims = get_static_dims(graph, node.inputs[1]);
3296 let out_dims_v = get_static_dims(graph, node.id);
3297 if lhs_dims.is_empty() || rhs_dims.is_empty() || out_dims_v.is_empty() {
3298 (Vec::new(), Vec::new(), Vec::new())
3303 } else {
3304 let ls = broadcast_strides(&lhs_dims, &out_dims_v);
3305 let rs = broadcast_strides(&rhs_dims, &out_dims_v);
3306 let od: Vec<u32> = out_dims_v.iter().map(|x| *x as u32).collect();
3307 (od, ls, rs)
3308 }
3309 };
3310 if node.shape.dtype() == rlx_ir::DType::C64 {
3311 Thunk::BinaryFullC64 {
3312 lhs: node_offset(arena, node.inputs[0]),
3313 rhs: node_offset(arena, node.inputs[1]),
3314 dst: node_offset(arena, node.id),
3315 len: out_len as u32,
3316 lhs_len: lhs_len as u32,
3317 rhs_len: rhs_len as u32,
3318 op: *op,
3319 out_dims_bcast,
3320 bcast_lhs_strides,
3321 bcast_rhs_strides,
3322 }
3323 } else if node.shape.dtype() == rlx_ir::DType::F64 {
3324 Thunk::BinaryFullF64 {
3327 lhs: node_offset(arena, node.inputs[0]),
3328 rhs: node_offset(arena, node.inputs[1]),
3329 dst: node_offset(arena, node.id),
3330 len: out_len as u32,
3331 lhs_len: lhs_len as u32,
3332 rhs_len: rhs_len as u32,
3333 op: *op,
3334 out_dims_bcast,
3335 bcast_lhs_strides,
3336 bcast_rhs_strides,
3337 }
3338 } else if matches!(op, BinaryOp::Add)
3339 && rhs_len < out_len
3340 && out_len % rhs_len == 0
3341 && is_trailing_bias_broadcast(
3342 graph.node(node.inputs[1]).shape.dims(),
3343 graph.node(node.id).shape.dims(),
3344 )
3345 {
3346 Thunk::BiasAdd {
3356 src: node_offset(arena, node.inputs[0]),
3357 bias: node_offset(arena, node.inputs[1]),
3358 dst: node_offset(arena, node.id),
3359 m: (out_len / rhs_len) as u32,
3360 n: rhs_len as u32,
3361 }
3362 } else {
3363 let lhs_len = get_len(graph, node.inputs[0]);
3364 Thunk::BinaryFull {
3365 lhs: node_offset(arena, node.inputs[0]),
3366 rhs: node_offset(arena, node.inputs[1]),
3367 dst: node_offset(arena, node.id),
3368 len: out_len as u32,
3369 lhs_len: lhs_len as u32,
3370 rhs_len: rhs_len as u32,
3371 op: *op,
3372 out_dims_bcast,
3373 bcast_lhs_strides,
3374 bcast_rhs_strides,
3375 elem_bytes: node.shape.dtype().size_bytes() as u8,
3376 }
3377 }
3378 }
3379
3380 Op::Activation(act) => {
3381 let len = node.shape.num_elements().unwrap();
3382 let in_off = node_offset(arena, node.inputs[0]);
3383 let out_off = node_offset(arena, node.id);
3384 if node.shape.dtype() == rlx_ir::DType::C64 {
3385 match act {
3390 Activation::Neg | Activation::Exp | Activation::Log | Activation::Sqrt => {}
3391 other => panic!(
3392 "Op::Activation({other:?}) on DType::C64: no \
3393 natural complex extension — supported on C64: \
3394 Neg, Exp, Log, Sqrt"
3395 ),
3396 }
3397 Thunk::ActivationC64 {
3398 src: in_off,
3399 dst: out_off,
3400 len: len as u32,
3401 kind: *act,
3402 }
3403 } else if node.shape.dtype() == rlx_ir::DType::F64 {
3404 Thunk::ActivationF64 {
3405 src: in_off,
3406 dst: out_off,
3407 len: len as u32,
3408 kind: *act,
3409 }
3410 } else if in_off == out_off {
3411 Thunk::ActivationInPlace {
3415 data: out_off,
3416 len: len as u32,
3417 act: *act,
3418 }
3419 } else {
3420 thunks.push(Thunk::Copy {
3424 src: in_off,
3425 dst: out_off,
3426 len: len as u32,
3427 });
3428 Thunk::ActivationInPlace {
3429 data: out_off,
3430 len: len as u32,
3431 act: *act,
3432 }
3433 }
3434 }
3435
3436 Op::Gather { axis } if *axis == 0 => {
3437 let table_shape = &graph.node(node.inputs[0]).shape;
3438 let table_total = table_shape.num_elements().unwrap();
3439 let trailing: usize = (1..table_shape.rank())
3440 .map(|i| table_shape.dim(i).unwrap_static())
3441 .product();
3442 let idx_len = get_len(graph, node.inputs[1]);
3443 let idx_i64 =
3444 u8::from(graph.node(node.inputs[1]).shape.dtype() == rlx_ir::DType::I64);
3445 let table_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
3446 Thunk::Gather {
3447 table: node_offset(arena, node.inputs[0]),
3448 table_len: table_total as u32,
3449 idx: node_offset(arena, node.inputs[1]),
3450 dst: node_offset(arena, node.id),
3451 num_idx: idx_len as u32,
3452 trailing: trailing as u32,
3453 idx_i64,
3454 table_bytes,
3455 }
3456 }
3457
3458 Op::Gather { axis } => {
3459 compile_gather(node, graph, arena, &matmul_fold, &rng_shared, rng)
3460 }
3461 Op::Narrow { axis, start, len } => {
3462 compile_narrow(node, graph, arena, &matmul_fold, &rng_shared, rng)
3463 }
3464 Op::Reverse { axes } => {
3465 compile_reverse(node, graph, arena, &matmul_fold, &rng_shared, rng)
3466 }
3467 Op::Reshape { .. } | Op::StopGradient => {
3468 let len = node.shape.num_elements().unwrap();
3470 let src = node_offset(arena, node.inputs[0]);
3471 let dst = node_offset(arena, node.id);
3472 match node.shape.dtype() {
3473 rlx_ir::DType::F64 => Thunk::CopyF64 {
3474 src,
3475 dst,
3476 len: len as u32,
3477 },
3478 rlx_ir::DType::I64 => Thunk::CopyI64 {
3479 src,
3480 dst,
3481 len: len as u32,
3482 },
3483 _ => Thunk::Copy {
3484 src,
3485 dst,
3486 len: len as u32,
3487 },
3488 }
3489 }
3490
3491 Op::Cast { to } => compile_cast(node, graph, arena, &matmul_fold, &rng_shared, rng),
3492 Op::Quantize {
3493 axis,
3494 scales,
3495 zero_points,
3496 } => compile_quantize(node, graph, arena, &matmul_fold, &rng_shared, rng),
3497 Op::FakeQuantize {
3498 bits,
3499 axis,
3500 ste,
3501 scale_mode,
3502 } => compile_fake_quantize(node, graph, arena, &matmul_fold, &rng_shared, rng),
3503 Op::FakeQuantizeLSQ { bits, axis } => {
3504 compile_fake_quantize_l_s_q(node, graph, arena, &matmul_fold, &rng_shared, rng)
3505 }
3506 Op::FakeQuantizeLSQBackwardX { bits, axis } => compile_fake_quantize_l_s_q_backward_x(
3507 node,
3508 graph,
3509 arena,
3510 &matmul_fold,
3511 &rng_shared,
3512 rng,
3513 ),
3514 Op::FakeQuantizeLSQBackwardScale { bits, axis } => {
3515 compile_fake_quantize_l_s_q_backward_scale(
3516 node,
3517 graph,
3518 arena,
3519 &matmul_fold,
3520 &rng_shared,
3521 rng,
3522 )
3523 }
3524 Op::FakeQuantizeBackward { bits, axis, ste } => {
3525 compile_fake_quantize_backward(node, graph, arena, &matmul_fold, &rng_shared, rng)
3526 }
3527 Op::Dequantize {
3528 axis,
3529 scales,
3530 zero_points,
3531 } => compile_dequantize(node, graph, arena, &matmul_fold, &rng_shared, rng),
3532 Op::Expand { .. } => compile_expand(node, graph, arena, &matmul_fold, &rng_shared, rng),
3533 Op::RmsNorm { eps, .. } => {
3534 compile_rms_norm(node, graph, arena, &matmul_fold, &rng_shared, rng)
3535 }
3536 Op::LayerNorm { eps, .. } => {
3537 compile_layer_norm(node, graph, arena, &matmul_fold, &rng_shared, rng)
3538 }
3539 Op::GroupNorm { num_groups, eps } => {
3540 compile_group_norm(node, graph, arena, &matmul_fold, &rng_shared, rng)
3541 }
3542 Op::BatchNormInference { eps } => {
3543 compile_batch_norm_inference(node, graph, arena, &matmul_fold, &rng_shared, rng)
3544 }
3545 Op::BatchNormInferenceBackwardInput { eps } => {
3546 compile_batch_norm_inference_backward_input(
3547 node,
3548 graph,
3549 arena,
3550 &matmul_fold,
3551 &rng_shared,
3552 rng,
3553 )
3554 }
3555 Op::BatchNormInferenceBackwardGamma { eps } => {
3556 compile_batch_norm_inference_backward_gamma(
3557 node,
3558 graph,
3559 arena,
3560 &matmul_fold,
3561 &rng_shared,
3562 rng,
3563 )
3564 }
3565 Op::BatchNormInferenceBackwardBeta => compile_batch_norm_inference_backward_beta(
3566 node,
3567 graph,
3568 arena,
3569 &matmul_fold,
3570 &rng_shared,
3571 rng,
3572 ),
3573 Op::LayerNorm2d { eps } => {
3574 compile_layer_norm2d(node, graph, arena, &matmul_fold, &rng_shared, rng)
3575 }
3576 Op::ConvTranspose2d {
3577 kernel_size,
3578 stride,
3579 padding,
3580 dilation,
3581 output_padding: _,
3582 groups,
3583 } => compile_conv_transpose2d(node, graph, arena, &matmul_fold, &rng_shared, rng),
3584 Op::ResizeNearest2x => {
3585 compile_resize_nearest2x(node, graph, arena, &matmul_fold, &rng_shared, rng)
3586 }
3587 Op::AxialRope2d {
3588 end_x,
3589 end_y,
3590 head_dim,
3591 num_heads,
3592 theta,
3593 repeat_factor,
3594 } => compile_axial_rope2d(node, graph, arena, &matmul_fold, &rng_shared, rng),
3595 Op::Softmax { axis } => {
3596 let rank = node.shape.rank();
3597 let ax = if *axis < 0 {
3598 (rank as i32 + axis) as usize
3599 } else {
3600 *axis as usize
3601 };
3602 let cols = node.shape.dim(ax).unwrap_static();
3603 let total = node.shape.num_elements().unwrap();
3604 let in_off = node_offset(arena, node.inputs[0]);
3605 let out_off = node_offset(arena, node.id);
3606 if in_off != out_off {
3612 thunks.push(Thunk::Copy {
3613 src: in_off,
3614 dst: out_off,
3615 len: total as u32,
3616 });
3617 }
3618 Thunk::Softmax {
3619 data: out_off,
3620 rows: (total / cols) as u32,
3621 cols: cols as u32,
3622 }
3623 }
3624
3625 Op::SelectiveScan { state_size } => {
3626 compile_selective_scan(node, graph, arena, &matmul_fold, &rng_shared, rng)
3627 }
3628 Op::GatedDeltaNet {
3629 state_size,
3630 carry_state,
3631 } => compile_gated_delta_net(node, graph, arena, &matmul_fold, &rng_shared, rng),
3632 Op::Lstm {
3633 hidden_size,
3634 num_layers,
3635 bidirectional,
3636 carry,
3637 } => compile_lstm(node, graph, arena, &matmul_fold, &rng_shared, rng),
3638 Op::Gru {
3639 hidden_size,
3640 num_layers,
3641 bidirectional,
3642 carry,
3643 } => compile_gru(node, graph, arena, &matmul_fold, &rng_shared, rng),
3644 Op::Rnn {
3645 hidden_size,
3646 num_layers,
3647 bidirectional,
3648 carry,
3649 relu,
3650 } => compile_rnn(node, graph, arena, &matmul_fold, &rng_shared, rng),
3651 Op::Mamba2 {
3652 head_dim,
3653 state_size,
3654 } => compile_mamba2(node, graph, arena, &matmul_fold, &rng_shared, rng),
3655 Op::QMatMul {
3656 x_zp,
3657 w_zp,
3658 out_zp,
3659 mult,
3660 } => compile_q_mat_mul(node, graph, arena, &matmul_fold, &rng_shared, rng),
3661 Op::QConv2d {
3662 kernel_size,
3663 stride,
3664 padding,
3665 dilation,
3666 groups,
3667 x_zp,
3668 w_zp,
3669 out_zp,
3670 mult,
3671 } => compile_q_conv2d(node, graph, arena, &matmul_fold, &rng_shared, rng),
3672 Op::DequantMatMul { scheme } => {
3673 compile_dequant_mat_mul(node, graph, arena, &matmul_fold, &rng_shared, rng)
3674 }
3675 Op::ScaledMatMul {
3676 lhs_format,
3677 rhs_format,
3678 scale_layout,
3679 has_bias,
3680 } => compile_scaled_mat_mul(node, graph, arena, &matmul_fold, &rng_shared, rng),
3681 Op::ScaledQuantize {
3682 format,
3683 scale_layout,
3684 } => compile_scaled_quantize(node, graph, arena, &matmul_fold, &rng_shared, rng),
3685 Op::ScaledQuantScale {
3686 format,
3687 scale_layout,
3688 } => compile_scaled_quant_scale(node, graph, arena, &matmul_fold, &rng_shared, rng),
3689 Op::ScaledDequantize {
3690 format,
3691 scale_layout,
3692 } => compile_scaled_dequantize(node, graph, arena, &matmul_fold, &rng_shared, rng),
3693 Op::LoraMatMul { scale } => {
3694 compile_lora_mat_mul(node, graph, arena, &matmul_fold, &rng_shared, rng)
3695 }
3696 Op::Sample {
3697 top_k,
3698 top_p,
3699 temperature,
3700 seed,
3701 } => compile_sample(node, graph, arena, &matmul_fold, &rng_shared, rng),
3702 Op::RngNormal {
3703 mean,
3704 scale,
3705 key,
3706 op_seed,
3707 } => compile_rng_normal(node, graph, arena, &matmul_fold, &rng_shared, rng),
3708 Op::RngUniform {
3709 low,
3710 high,
3711 key,
3712 op_seed,
3713 } => compile_rng_uniform(node, graph, arena, &matmul_fold, &rng_shared, rng),
3714 Op::Cumsum { axis, exclusive } => {
3715 compile_cumsum(node, graph, arena, &matmul_fold, &rng_shared, rng)
3716 }
3717 Op::Attention {
3718 num_heads,
3719 head_dim,
3720 mask_kind,
3721 score_scale,
3722 attn_logit_softcap,
3723 } => compile_attention(node, graph, arena, &matmul_fold, &rng_shared, rng),
3724 Op::AttentionBackward {
3725 num_heads,
3726 head_dim,
3727 mask_kind,
3728 wrt,
3729 } => compile_attention_backward(node, graph, arena, &matmul_fold, &rng_shared, rng),
3730 Op::FusedAttentionBlock {
3731 num_heads,
3732 head_dim,
3733 has_bias,
3734 has_rope,
3735 } => compile_fused_attention_block(node, graph, arena, &matmul_fold, &rng_shared, rng),
3736 Op::Rope {
3737 head_dim,
3738 n_rot,
3739 style,
3740 } => compile_rope(node, graph, arena, &matmul_fold, &rng_shared, rng),
3741 Op::FusedSwiGLU {
3742 cast_to: _,
3743 gate_first,
3744 } => compile_fused_swi_g_l_u(node, graph, arena, &matmul_fold, &rng_shared, rng),
3745 Op::Conv {
3746 kernel_size,
3747 stride,
3748 padding,
3749 dilation,
3750 groups,
3751 } => compile_conv(node, graph, arena, &matmul_fold, &rng_shared, rng),
3752 Op::Conv3d { .. } => compile_conv3d(node, graph, arena, &matmul_fold, &rng_shared, rng),
3753 Op::ConvTranspose3d { .. } => {
3754 compile_conv_transpose3d(node, graph, arena, &matmul_fold, &rng_shared, rng)
3755 }
3756 Op::Pool {
3757 kind,
3758 kernel_size,
3759 stride,
3760 padding,
3761 } => compile_pool(node, graph, arena, &matmul_fold, &rng_shared, rng),
3762 Op::Transpose { perm } => {
3763 compile_transpose(node, graph, arena, &matmul_fold, &rng_shared, rng)
3764 }
3765 Op::ScatterAdd => {
3766 compile_scatter_add(node, graph, arena, &matmul_fold, &rng_shared, rng)
3767 }
3768 Op::GroupedMatMul => {
3769 compile_grouped_mat_mul(node, graph, arena, &matmul_fold, &rng_shared, rng)
3770 }
3771 Op::DequantGroupedMatMul { scheme } => {
3772 compile_dequant_grouped_mat_mul(node, graph, arena, &matmul_fold, &rng_shared, rng)
3773 }
3774 Op::DequantMoEWeights { scheme } => {
3775 compile_dequant_mo_e_weights(node, graph, arena, &matmul_fold, &rng_shared, rng)
3776 }
3777 Op::TopK { k } => compile_top_k(node, graph, arena, &matmul_fold, &rng_shared, rng),
3778 Op::Reduce {
3779 op,
3780 axes,
3781 keep_dim: _,
3782 } => compile_reduce(node, graph, arena, &matmul_fold, &rng_shared, rng),
3783 Op::ArgMax { axis, keep_dim: _ } | Op::ArgMin { axis, keep_dim: _ } => {
3784 let in_shape = &graph.node(node.inputs[0]).shape;
3785 let rank = in_shape.rank();
3786 let outer: usize = (0..*axis)
3787 .map(|i| in_shape.dim(i).unwrap_static())
3788 .product::<usize>()
3789 .max(1);
3790 let reduced = in_shape.dim(*axis).unwrap_static();
3791 let inner: usize = (*axis + 1..rank)
3792 .map(|i| in_shape.dim(i).unwrap_static())
3793 .product::<usize>()
3794 .max(1);
3795 Thunk::ArgReduce {
3796 src: node_offset(arena, node.inputs[0]),
3797 dst: node_offset(arena, node.id),
3798 outer: outer as u32,
3799 reduced: reduced as u32,
3800 inner: inner as u32,
3801 is_max: matches!(node.op, Op::ArgMax { .. }),
3802 }
3803 }
3804
3805 Op::Compare(cmp) => compile_compare(node, graph, arena, &matmul_fold, &rng_shared, rng),
3806 Op::Where => compile_where(node, graph, arena, &matmul_fold, &rng_shared, rng),
3807 Op::Fma => compile_fma(node, graph, arena, &matmul_fold, &rng_shared, rng),
3808 Op::ReluBackward => {
3809 compile_relu_backward(node, graph, arena, &matmul_fold, &rng_shared, rng)
3810 }
3811 Op::ComplexNormSq => {
3812 compile_complex_norm_sq(node, graph, arena, &matmul_fold, &rng_shared, rng)
3813 }
3814 Op::ComplexNormSqBackward => {
3815 compile_complex_norm_sq_backward(node, graph, arena, &matmul_fold, &rng_shared, rng)
3816 }
3817 Op::Conjugate => compile_conjugate(node, graph, arena, &matmul_fold, &rng_shared, rng),
3818 Op::ActivationBackward { kind } => {
3819 compile_activation_backward(node, graph, arena, &matmul_fold, &rng_shared, rng)
3820 }
3821 Op::LayerNormBackwardInput { eps, .. } => compile_layer_norm_backward_input(
3822 node,
3823 graph,
3824 arena,
3825 &matmul_fold,
3826 &rng_shared,
3827 rng,
3828 ),
3829 Op::LayerNormBackwardGamma { eps, .. } => compile_layer_norm_backward_gamma(
3830 node,
3831 graph,
3832 arena,
3833 &matmul_fold,
3834 &rng_shared,
3835 rng,
3836 ),
3837 Op::RmsNormBackwardInput { eps, .. }
3838 | Op::RmsNormBackwardGamma { eps, .. }
3839 | Op::RmsNormBackwardBeta { eps, .. } => {
3840 let x_shape = &graph.node(node.inputs[0]).shape;
3841 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static();
3842 let rows = (x_shape.num_elements().unwrap() / h) as u32;
3843 let off = |i: usize| node_offset(arena, node.inputs[i]);
3844 let common = (off(0), off(1), off(2), off(3), rows, h as u32, *eps);
3845 match &node.op {
3846 Op::RmsNormBackwardInput { .. } => Thunk::RmsNormBackwardInput {
3847 x: common.0,
3848 gamma: common.1,
3849 beta: common.2,
3850 dy: common.3,
3851 dx: node_offset(arena, node.id),
3852 rows: common.4,
3853 h: common.5,
3854 eps: common.6,
3855 },
3856 Op::RmsNormBackwardGamma { .. } => Thunk::RmsNormBackwardGamma {
3857 x: common.0,
3858 gamma: common.1,
3859 beta: common.2,
3860 dy: common.3,
3861 dgamma: node_offset(arena, node.id),
3862 rows: common.4,
3863 h: common.5,
3864 eps: common.6,
3865 },
3866 Op::RmsNormBackwardBeta { .. } => Thunk::RmsNormBackwardBeta {
3867 x: common.0,
3868 gamma: common.1,
3869 beta: common.2,
3870 dy: common.3,
3871 dbeta: node_offset(arena, node.id),
3872 rows: common.4,
3873 h: common.5,
3874 eps: common.6,
3875 },
3876 _ => unreachable!(),
3877 }
3878 }
3879
3880 Op::RopeBackward { head_dim, n_rot } => {
3881 compile_rope_backward(node, graph, arena, &matmul_fold, &rng_shared, rng)
3882 }
3883 Op::CumsumBackward { exclusive, .. } => {
3884 compile_cumsum_backward(node, graph, arena, &matmul_fold, &rng_shared, rng)
3885 }
3886 Op::GatherBackward { .. } => {
3887 compile_gather_backward(node, graph, arena, &matmul_fold, &rng_shared, rng)
3888 }
3889 Op::GroupNormBackwardInput { num_groups, eps }
3890 | Op::GroupNormBackwardGamma { num_groups, eps }
3891 | Op::GroupNormBackwardBeta { num_groups, eps } => {
3892 let x_shape = &graph.node(node.inputs[0]).shape;
3893 let n = x_shape.dim(0).unwrap_static() as u32;
3894 let c = x_shape.dim(1).unwrap_static() as u32;
3895 let h = x_shape.dim(2).unwrap_static() as u32;
3896 let w = x_shape.dim(3).unwrap_static() as u32;
3897 match &node.op {
3898 Op::GroupNormBackwardInput { .. } => Thunk::GroupNormBackwardInput {
3899 x: node_offset(arena, node.inputs[0]),
3900 gamma: node_offset(arena, node.inputs[1]),
3901 beta: node_offset(arena, node.inputs[2]),
3902 dy: node_offset(arena, node.inputs[3]),
3903 dx: node_offset(arena, node.id),
3904 n,
3905 c,
3906 h,
3907 w,
3908 num_groups: *num_groups as u32,
3909 eps: *eps,
3910 },
3911 Op::GroupNormBackwardGamma { .. } => Thunk::GroupNormBackwardGamma {
3912 x: node_offset(arena, node.inputs[0]),
3913 dy: node_offset(arena, node.inputs[1]),
3914 dgamma: node_offset(arena, node.id),
3915 n,
3916 c,
3917 h,
3918 w,
3919 num_groups: *num_groups as u32,
3920 eps: *eps,
3921 },
3922 Op::GroupNormBackwardBeta { .. } => Thunk::GroupNormBackwardBeta {
3923 dy: node_offset(arena, node.inputs[1]),
3924 dbeta: node_offset(arena, node.id),
3925 n,
3926 c,
3927 h,
3928 w,
3929 },
3930 _ => unreachable!(),
3931 }
3932 }
3933
3934 Op::MaxPool2dBackward {
3935 kernel_size,
3936 stride,
3937 padding,
3938 } => compile_max_pool2d_backward(node, graph, arena, &matmul_fold, &rng_shared, rng),
3939 Op::Conv2dBackwardInput {
3940 kernel_size,
3941 stride,
3942 padding,
3943 dilation,
3944 groups,
3945 } => compile_conv2d_backward_input(node, graph, arena, &matmul_fold, &rng_shared, rng),
3946 Op::Conv2dBackwardWeight {
3947 kernel_size,
3948 stride,
3949 padding,
3950 dilation,
3951 groups,
3952 } => compile_conv2d_backward_weight(node, graph, arena, &matmul_fold, &rng_shared, rng),
3953 Op::Im2Col {
3954 kernel_size,
3955 stride,
3956 padding,
3957 dilation,
3958 } => compile_im2_col(node, graph, arena, &matmul_fold, &rng_shared, rng),
3959 Op::SoftmaxCrossEntropy => {
3960 compile_softmax_cross_entropy(node, graph, arena, &matmul_fold, &rng_shared, rng)
3961 }
3962 Op::SoftmaxCrossEntropyWithLogits => compile_softmax_cross_entropy_with_logits(
3963 node,
3964 graph,
3965 arena,
3966 &matmul_fold,
3967 &rng_shared,
3968 rng,
3969 ),
3970 Op::SoftmaxCrossEntropyBackward => compile_softmax_cross_entropy_backward(
3971 node,
3972 graph,
3973 arena,
3974 &matmul_fold,
3975 &rng_shared,
3976 rng,
3977 ),
3978 Op::DenseSolve => {
3979 compile_dense_solve(node, graph, arena, &matmul_fold, &rng_shared, rng)
3980 }
3981 Op::BatchedDenseSolve => {
3982 compile_batched_dense_solve(node, graph, arena, &matmul_fold, &rng_shared, rng)
3983 }
3984 Op::Scan {
3985 body,
3986 length,
3987 save_trajectory,
3988 num_bcast,
3989 num_xs,
3990 num_checkpoints,
3991 } => compile_scan(node, graph, arena, &matmul_fold, &rng_shared, rng),
3992 Op::ScanBackward {
3993 body_vjp,
3994 length,
3995 save_trajectory,
3996 num_xs,
3997 num_checkpoints,
3998 forward_body,
3999 } => compile_scan_backward(node, graph, arena, &matmul_fold, &rng_shared, rng),
4000 Op::ScanBackwardXs {
4001 body_vjp,
4002 length,
4003 save_trajectory,
4004 num_xs,
4005 xs_idx,
4006 num_checkpoints,
4007 forward_body,
4008 } => compile_scan_backward_xs(node, graph, arena, &matmul_fold, &rng_shared, rng),
4009 Op::Concat { axis } => {
4010 compile_concat(node, graph, arena, &matmul_fold, &rng_shared, rng)
4011 }
4012 Op::GaussianSplatRender {
4013 width,
4014 height,
4015 tile_size,
4016 radius_scale,
4017 alpha_cutoff,
4018 max_splat_steps,
4019 transmittance_threshold,
4020 max_list_entries,
4021 } => compile_gaussian_splat_render(node, graph, arena, &matmul_fold, &rng_shared, rng),
4022 Op::GaussianSplatRenderBackward {
4023 width,
4024 height,
4025 tile_size,
4026 radius_scale,
4027 alpha_cutoff,
4028 max_splat_steps,
4029 transmittance_threshold,
4030 max_list_entries,
4031 loss_grad_clip,
4032 sh_band,
4033 max_anisotropy,
4034 } => compile_gaussian_splat_render_backward(
4035 node,
4036 graph,
4037 arena,
4038 &matmul_fold,
4039 &rng_shared,
4040 rng,
4041 ),
4042 Op::GaussianSplatPrepare {
4043 width,
4044 height,
4045 tile_size,
4046 radius_scale,
4047 alpha_cutoff,
4048 max_splat_steps,
4049 transmittance_threshold,
4050 max_list_entries,
4051 } => compile_gaussian_splat_prepare(node, graph, arena, &matmul_fold, &rng_shared, rng),
4052 Op::GaussianSplatRasterize {
4053 width,
4054 height,
4055 tile_size,
4056 alpha_cutoff,
4057 max_splat_steps,
4058 transmittance_threshold,
4059 max_list_entries,
4060 } => {
4061 compile_gaussian_splat_rasterize(node, graph, arena, &matmul_fold, &rng_shared, rng)
4062 }
4063 Op::Custom { name, attrs, .. } => {
4064 compile_custom(node, graph, arena, &matmul_fold, &rng_shared, rng)
4065 }
4066 Op::Fft { inverse, norm } => {
4067 compile_fft(node, graph, arena, &matmul_fold, &rng_shared, rng)
4068 }
4069 Op::FftButterflyStage { stage, n_fft } => {
4070 compile_fft_butterfly_stage(node, graph, arena, &matmul_fold, &rng_shared, rng)
4071 }
4072 Op::LogMel => compile_log_mel(node, graph, arena, &matmul_fold, &rng_shared, rng),
4073 Op::LogMelBackward => {
4074 compile_log_mel_backward(node, graph, arena, &matmul_fold, &rng_shared, rng)
4075 }
4076 Op::WelchPeaks { k, n_segments } => {
4077 compile_welch_peaks(node, graph, arena, &matmul_fold, &rng_shared, rng)
4078 }
4079 Op::CustomFn {
4080 fwd_body,
4081 num_inputs,
4082 ..
4083 } => compile_custom_fn(node, graph, arena, &matmul_fold, &rng_shared, rng),
4084 Op::ElementwiseRegion {
4085 chain,
4086 scalar_input_mask,
4087 input_modulus,
4088 prologue,
4089 ..
4090 } => compile_elementwise_region(node, graph, arena, &matmul_fold, &rng_shared, rng),
4091 _ => Thunk::Nop,
4092 };
4093 thunks.push(t);
4094 }
4095
4096 let cfg = crate::config::RuntimeConfig::global();
4097 let mask_thr = cfg.mask_binary_threshold;
4098 let mask_neg = cfg.attn_mask_neg_inf;
4099 let score_skip = cfg.score_skip_threshold;
4100
4101 let compiled_fns: Vec<Arc<dyn Fn(*mut u8) + Send + Sync>> = thunks
4103 .iter()
4104 .filter(|t| !matches!(t, Thunk::Nop))
4105 .map(|thunk| {
4106 match thunk.clone() {
4107 Thunk::Nop => Arc::new(|_: *mut u8| {}) as Arc<dyn Fn(*mut u8) + Send + Sync>,
4108
4109 Thunk::Sgemm { a, b, c, m, k, n } => {
4110 let (m, k, n) = (m as usize, k as usize, n as usize);
4111 Arc::new(move |base: *mut u8| unsafe {
4112 crate::blas::sgemm(
4113 sl(a, base, m * k),
4114 sl(b, base, k * n),
4115 sl_mut(c, base, m * n),
4116 m,
4117 k,
4118 n,
4119 );
4120 })
4121 }
4122
4123 Thunk::CgemmC64 { a, b, c, m, k, n } => {
4124 let (m, k, n) = (m as usize, k as usize, n as usize);
4125 Arc::new(move |base: *mut u8| unsafe {
4126 cgemm_c64(a, b, c, m, k, n, base);
4127 })
4128 }
4129
4130 Thunk::DenseSolveF64 { a, b, x, n, nrhs } => {
4131 let (n_, nrhs_) = (n as usize, nrhs as usize);
4132 Arc::new(move |base: *mut u8| unsafe {
4133 let a_src = sl_f64(a, base, n_ * n_);
4134 let b_src = sl_f64(b, base, n_ * nrhs_);
4135 let mut a_scratch: Vec<f64> = a_src.to_vec();
4136 let mut x_buf: Vec<f64> = b_src.to_vec();
4137 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
4138 if info != 0 {
4139 panic!("DenseSolveF64: singular (info={info})");
4140 }
4141 sl_mut_f64(x, base, n_ * nrhs_).copy_from_slice(&x_buf);
4142 })
4143 }
4144
4145 Thunk::DenseSolveF32 { a, b, x, n, nrhs } => {
4146 let (n_, nrhs_) = (n as usize, nrhs as usize);
4147 Arc::new(move |base: *mut u8| unsafe {
4148 let a_src = sl(a, base, n_ * n_);
4149 let b_src = sl(b, base, n_ * nrhs_);
4150 let mut a_scratch: Vec<f32> = a_src.to_vec();
4151 let mut x_buf: Vec<f32> = b_src.to_vec();
4152 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
4153 if info != 0 {
4154 panic!("DenseSolveF32: singular (info={info})");
4155 }
4156 sl_mut(x, base, n_ * nrhs_).copy_from_slice(&x_buf);
4157 })
4158 }
4159
4160 Thunk::FusedMmBiasAct {
4161 a,
4162 w,
4163 bias,
4164 c,
4165 m,
4166 k,
4167 n,
4168 act,
4169 } => {
4170 let (m, k, n) = (m as usize, k as usize, n as usize);
4171 Arc::new(move |base: *mut u8| unsafe {
4172 let out = sl_mut(c, base, m * n);
4173 crate::blas::sgemm(sl(a, base, m * k), sl(w, base, k * n), out, m, k, n);
4174 match act {
4182 Some(Activation::Gelu) => {
4183 crate::kernels::par_bias_gelu(out, sl(bias, base, n), m, n)
4184 }
4185 Some(other) => {
4186 crate::blas::bias_add(out, sl(bias, base, n), m, n);
4187 apply_activation_inplace(out, other);
4188 }
4189 None => crate::blas::bias_add(out, sl(bias, base, n), m, n),
4190 }
4191 })
4192 }
4193
4194 Thunk::FusedResidualLN {
4195 x,
4196 res,
4197 bias,
4198 g,
4199 b,
4200 out,
4201 rows,
4202 h,
4203 eps,
4204 has_bias,
4205 } => {
4206 let (rows, h) = (rows as usize, h as usize);
4207 Arc::new(move |base: *mut u8| unsafe {
4208 let zero = vec![0f32; h]; let bi = if has_bias { sl(bias, base, h) } else { &zero };
4210 let xp = sl(x, base, rows * h).as_ptr() as usize;
4211 let rp = sl(res, base, rows * h).as_ptr() as usize;
4212 let op = sl_mut(out, base, rows * h).as_mut_ptr() as usize;
4213 let bp = bi.as_ptr() as usize;
4214 let gp = sl(g, base, h).as_ptr() as usize;
4215 let bbp = sl(b, base, h).as_ptr() as usize;
4216 crate::pool::par_for(rows, 4, &|off, cnt| {
4217 let xs = std::slice::from_raw_parts(
4218 (xp as *const f32).add(off * h),
4219 cnt * h,
4220 );
4221 let rs = std::slice::from_raw_parts(
4222 (rp as *const f32).add(off * h),
4223 cnt * h,
4224 );
4225 let os = std::slice::from_raw_parts_mut(
4226 (op as *mut f32).add(off * h),
4227 cnt * h,
4228 );
4229 let bi = std::slice::from_raw_parts(bp as *const f32, h);
4230 let g = std::slice::from_raw_parts(gp as *const f32, h);
4231 let b = std::slice::from_raw_parts(bbp as *const f32, h);
4232 crate::kernels::residual_bias_layer_norm(
4233 xs, rs, bi, g, b, os, cnt, h, eps,
4234 );
4235 });
4236 })
4237 }
4238
4239 Thunk::BiasAdd {
4240 src,
4241 bias,
4242 dst,
4243 m,
4244 n,
4245 } => {
4246 let (m, n) = (m as usize, n as usize);
4247 let len = m * n;
4248 Arc::new(move |base: *mut u8| unsafe {
4249 let out = sl_mut(dst, base, len);
4250 if src != dst {
4251 let src_ptr = base.add(src) as *const f32;
4252 let dst_ptr = base.add(dst) as *mut f32;
4253 if src_ptr != dst_ptr {
4254 std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, len);
4255 }
4256 }
4257 crate::blas::bias_add(out, sl(bias, base, n), m, n);
4258 })
4259 }
4260
4261 Thunk::Gather {
4262 table,
4263 table_len,
4264 idx,
4265 dst,
4266 num_idx,
4267 trailing,
4268 idx_i64,
4269 table_bytes,
4270 } => {
4271 let (ni, tr, tl) = (num_idx as usize, trailing as usize, table_len as usize);
4272 let rows = tl / tr.max(1);
4273 let (idx_i64, table_bytes) = (idx_i64, table_bytes);
4274 Arc::new(move |base: *mut u8| unsafe {
4275 if table_bytes == 8 {
4276 let tab = sl_i64(table, base, tl);
4277 let out = sl_mut_i64(dst, base, ni * tr);
4278 if idx_i64 != 0 {
4279 let ids = sl_i64(idx, base, ni);
4280 for i in 0..ni {
4281 let row = ids[i].max(0) as usize;
4282 if row < rows {
4283 out[i * tr..(i + 1) * tr]
4284 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
4285 }
4286 }
4287 } else {
4288 let ids = sl(idx, base, ni);
4289 for i in 0..ni {
4290 let row = ids[i] as usize;
4291 if row < rows {
4292 out[i * tr..(i + 1) * tr]
4293 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
4294 }
4295 }
4296 }
4297 } else {
4298 let tab = sl(table, base, tl);
4299 let out = sl_mut(dst, base, ni * tr);
4300 if idx_i64 != 0 {
4301 let ids = sl_i64(idx, base, ni);
4302 for i in 0..ni {
4303 let row = ids[i].max(0) as usize;
4304 if row < rows {
4305 out[i * tr..(i + 1) * tr]
4306 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
4307 }
4308 }
4309 } else {
4310 let ids = sl(idx, base, ni);
4311 for i in 0..ni {
4312 let row = ids[i] as usize;
4313 if row < rows {
4314 out[i * tr..(i + 1) * tr]
4315 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
4316 }
4317 }
4318 }
4319 }
4320 })
4321 }
4322
4323 Thunk::Narrow {
4324 src,
4325 dst,
4326 outer,
4327 src_stride,
4328 dst_stride,
4329 inner,
4330 elem_bytes,
4331 } => {
4332 narrow_thunk_closure(src, dst, outer, src_stride, dst_stride, inner, elem_bytes)
4333 }
4334
4335 Thunk::Reverse {
4336 src,
4337 dst,
4338 dims,
4339 rev_mask,
4340 elem_bytes,
4341 } => {
4342 let eb = elem_bytes as usize;
4343 let rank = dims.len();
4344 let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
4345 let mut strides = vec![1usize; rank];
4347 for i in (0..rank.saturating_sub(1)).rev() {
4348 strides[i] = strides[i + 1] * dims[i + 1] as usize;
4349 }
4350 let dims_u: Vec<usize> = dims.iter().map(|&d| d as usize).collect();
4351 Arc::new(move |base: *mut u8| unsafe {
4352 let src_base = base.add(src);
4353 let dst_base = base.add(dst);
4354 for o in 0..total {
4355 let mut rem = o;
4358 let mut in_flat = 0usize;
4359 for ax in 0..rank {
4360 let idx = rem / strides[ax];
4361 rem %= strides[ax];
4362 let in_idx = if rev_mask[ax] {
4363 dims_u[ax] - 1 - idx
4364 } else {
4365 idx
4366 };
4367 in_flat += in_idx * strides[ax];
4368 }
4369 std::ptr::copy_nonoverlapping(
4370 src_base.add(in_flat * eb),
4371 dst_base.add(o * eb),
4372 eb,
4373 );
4374 }
4375 })
4376 }
4377
4378 Thunk::Copy { src, dst, len } => {
4379 let len = len as usize;
4380 Arc::new(move |base: *mut u8| unsafe {
4381 if src == dst || len == 0 {
4382 return;
4383 }
4384 let src_ptr = base.add(src) as *const f32;
4385 let dst_ptr = base.add(dst) as *mut f32;
4386 if src_ptr == dst_ptr {
4387 return;
4388 }
4389 std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, len);
4390 })
4391 }
4392
4393 Thunk::Softmax { data, rows, cols } => {
4394 let (rows, cols) = (rows as usize, cols as usize);
4395 Arc::new(move |base: *mut u8| unsafe {
4396 crate::naive::softmax(sl_mut(data, base, rows * cols), rows, cols);
4397 })
4398 }
4399
4400 Thunk::Cumsum {
4401 src,
4402 dst,
4403 rows,
4404 cols,
4405 exclusive,
4406 } => {
4407 let (rows, cols) = (rows as usize, cols as usize);
4408 Arc::new(move |base: *mut u8| unsafe {
4409 let s = sl(src, base, rows * cols);
4410 let d = sl_mut(dst, base, rows * cols);
4411 if exclusive {
4412 for r in 0..rows {
4413 let mut acc = 0.0f32;
4414 for c in 0..cols {
4415 d[r * cols + c] = acc;
4416 acc += s[r * cols + c];
4417 }
4418 }
4419 } else {
4420 for r in 0..rows {
4421 let mut acc = 0.0f32;
4422 for c in 0..cols {
4423 acc += s[r * cols + c];
4424 d[r * cols + c] = acc;
4425 }
4426 }
4427 }
4428 })
4429 }
4430
4431 Thunk::Sample {
4432 logits,
4433 dst,
4434 batch,
4435 vocab,
4436 top_k,
4437 top_p,
4438 temperature,
4439 seed,
4440 } => {
4441 let (b, v) = (batch as usize, vocab as usize);
4442 let k = (top_k as usize).min(v);
4443 Arc::new(move |base: *mut u8| unsafe {
4444 let lg = sl(logits, base, b * v);
4445 let out = sl_mut(dst, base, b);
4446 let mut rng =
4447 rlx_ir::Philox4x32::new(if seed == 0 { 0xDEADBEEF } else { seed });
4448 for bi in 0..b {
4449 let row = &lg[bi * v..(bi + 1) * v];
4450 out[bi] = sample_row(row, k, top_p, temperature, &mut rng) as f32;
4451 }
4452 })
4453 }
4454
4455 Thunk::RngNormal {
4456 dst,
4457 len,
4458 mean,
4459 scale,
4460 key,
4461 op_seed,
4462 } => {
4463 let n = len as usize;
4464 let rng = rng_shared.clone();
4465 Arc::new(move |base: *mut u8| unsafe {
4466 let out = sl_mut(dst, base, n);
4467 let opts = *rng.read().unwrap();
4468 rlx_ir::fill_normal_like(out, mean, scale, opts, key, op_seed);
4469 })
4470 }
4471
4472 Thunk::RngUniform {
4473 dst,
4474 len,
4475 low,
4476 high,
4477 key,
4478 op_seed,
4479 } => {
4480 let n = len as usize;
4481 let rng = rng_shared.clone();
4482 Arc::new(move |base: *mut u8| unsafe {
4483 let out = sl_mut(dst, base, n);
4484 let opts = *rng.read().unwrap();
4485 rlx_ir::fill_uniform_like(out, low, high, opts, key, op_seed);
4486 })
4487 }
4488
4489 Thunk::DequantMatMul {
4490 x,
4491 w_q,
4492 scale,
4493 zp,
4494 dst,
4495 m,
4496 k,
4497 n,
4498 block_size,
4499 is_asymmetric,
4500 } => {
4501 let (m, k, n, bs) = (m as usize, k as usize, n as usize, block_size as usize);
4502 let n_blocks_per_col = k.div_ceil(bs);
4503 Arc::new(move |base: *mut u8| unsafe {
4504 let xs = sl(x, base, m * k);
4505 let raw = base.add(w_q);
4507 let w_bytes = std::slice::from_raw_parts(raw as *const i8, k * n);
4508 let scales = sl(scale, base, n_blocks_per_col * n);
4509 let zps = if is_asymmetric {
4510 sl(zp, base, n_blocks_per_col * n)
4511 } else {
4512 &[][..]
4513 };
4514 let out = sl_mut(dst, base, m * n);
4515 dequant_matmul_int8(
4516 xs,
4517 w_bytes,
4518 scales,
4519 zps,
4520 out,
4521 m,
4522 k,
4523 n,
4524 bs,
4525 is_asymmetric,
4526 );
4527 })
4528 }
4529
4530 Thunk::DequantMatMulGguf {
4531 x,
4532 w_q,
4533 dst,
4534 m,
4535 k,
4536 n,
4537 scheme,
4538 } => {
4539 let (m, k, n) = (m as usize, k as usize, n as usize);
4540 let block_bytes = scheme.gguf_block_bytes() as usize;
4541 let block_elems = scheme.gguf_block_size() as usize;
4542 let total_bytes = (k * n) / block_elems * block_bytes;
4543 Arc::new(move |base: *mut u8| unsafe {
4544 let xs = sl(x, base, m * k);
4545 let w_bytes =
4546 std::slice::from_raw_parts(base.add(w_q) as *const u8, total_bytes);
4547 let out = sl_mut(dst, base, m * n);
4548 crate::gguf_matmul::gguf_matmul_bt_dispatch(xs, w_bytes, out, m, k, n, scheme);
4549 })
4550 }
4551
4552 Thunk::DequantMatMulInt4 {
4553 x,
4554 w_q,
4555 scale,
4556 zp,
4557 dst,
4558 m,
4559 k,
4560 n,
4561 block_size,
4562 is_asymmetric,
4563 } => {
4564 let (m, k, n, bs) = (m as usize, k as usize, n as usize, block_size as usize);
4565 let n_blocks = k.div_ceil(bs);
4566 Arc::new(move |base: *mut u8| unsafe {
4567 let xs = sl(x, base, m * k);
4568 let w_bytes = std::slice::from_raw_parts(
4569 base.add(w_q) as *const u8,
4570 (k * n).div_ceil(2),
4571 );
4572 let scales = sl(scale, base, n_blocks * n);
4573 let zps = if is_asymmetric {
4574 sl(zp, base, n_blocks * n)
4575 } else {
4576 &[][..]
4577 };
4578 let out = sl_mut(dst, base, m * n);
4579 dequant_matmul_int4(
4580 xs,
4581 w_bytes,
4582 scales,
4583 zps,
4584 out,
4585 m,
4586 k,
4587 n,
4588 bs,
4589 is_asymmetric,
4590 );
4591 })
4592 }
4593
4594 Thunk::DequantMatMulFp8 {
4595 x,
4596 w_q,
4597 scale,
4598 dst,
4599 m,
4600 k,
4601 n,
4602 e5m2,
4603 } => {
4604 let (m, k, n) = (m as usize, k as usize, n as usize);
4605 Arc::new(move |base: *mut u8| unsafe {
4606 let xs = sl(x, base, m * k);
4607 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, k * n);
4608 let scales = sl(scale, base, n);
4609 let out = sl_mut(dst, base, m * n);
4610 dequant_matmul_fp8(xs, w_bytes, scales, out, m, k, n, e5m2);
4611 })
4612 }
4613
4614 Thunk::DequantMatMulNvfp4 {
4615 x,
4616 w_q,
4617 scale,
4618 global_scale,
4619 dst,
4620 m,
4621 k,
4622 n,
4623 } => {
4624 let (m, k, n) = (m as usize, k as usize, n as usize);
4625 let n_scale = k.div_ceil(rlx_ir::NVFP4_GROUP_SIZE) * n;
4626 Arc::new(move |base: *mut u8| unsafe {
4627 let xs = sl(x, base, m * k);
4628 let w_bytes = std::slice::from_raw_parts(
4629 base.add(w_q) as *const u8,
4630 (k * n).div_ceil(2),
4631 );
4632 let scale_bytes =
4633 std::slice::from_raw_parts(base.add(scale) as *const u8, n_scale);
4634 let gs = sl(global_scale, base, 1)[0];
4635 let out = sl_mut(dst, base, m * n);
4636 dequant_matmul_nvfp4(xs, w_bytes, scale_bytes, gs, out, m, k, n);
4637 })
4638 }
4639
4640 Thunk::ScaledMatMul {
4641 lhs,
4642 rhs,
4643 lhs_scale,
4644 rhs_scale,
4645 bias,
4646 dst,
4647 m,
4648 k,
4649 n,
4650 lhs_fmt,
4651 rhs_fmt,
4652 layout,
4653 has_bias,
4654 } => {
4655 let (m, k, n) = (m as usize, k as usize, n as usize);
4656 let nblk = lowp_nblk(k, layout);
4657 let per_tensor = matches!(layout, rlx_ir::ScaleLayout::PerTensor);
4658 let n_lscale = if per_tensor { 1 } else { m * nblk };
4659 let n_rscale = if per_tensor { 1 } else { n * nblk };
4660 Arc::new(move |base: *mut u8| unsafe {
4661 let lhs_b = std::slice::from_raw_parts(base.add(lhs) as *const u8, m * k);
4662 let rhs_b = std::slice::from_raw_parts(base.add(rhs) as *const u8, n * k);
4663 let ls = lowp_read_scales(layout, base, lhs_scale, n_lscale);
4664 let rs = lowp_read_scales(layout, base, rhs_scale, n_rscale);
4665 let bias_s = if has_bias { Some(sl(bias, base, n)) } else { None };
4666 let out = sl_mut(dst, base, m * n);
4667 lowp_scaled_matmul(
4668 lhs_b, rhs_b, &ls, &rs, bias_s, out, m, n, k, layout, lhs_fmt, rhs_fmt,
4669 );
4670 })
4671 }
4672
4673 Thunk::ScaledQuantize {
4674 x,
4675 scale,
4676 dst,
4677 rows,
4678 cols,
4679 fmt,
4680 layout,
4681 } => {
4682 let (rows, cols) = (rows as usize, cols as usize);
4683 let nblk = lowp_nblk(cols, layout);
4684 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
4685 1
4686 } else {
4687 rows * nblk
4688 };
4689 Arc::new(move |base: *mut u8| unsafe {
4690 let xs = sl(x, base, rows * cols);
4691 let scales = lowp_read_scales(layout, base, scale, n_scale);
4692 let out =
4693 std::slice::from_raw_parts_mut(base.add(dst), rows * cols);
4694 lowp_quantize(xs, &scales, fmt, layout, rows, cols, out);
4695 })
4696 }
4697
4698 Thunk::ScaledQuantScale {
4699 x,
4700 dst,
4701 rows,
4702 cols,
4703 fmt,
4704 layout,
4705 } => {
4706 let (rows, cols) = (rows as usize, cols as usize);
4707 let nblk = lowp_nblk(cols, layout);
4708 Arc::new(move |base: *mut u8| unsafe {
4709 let xs = sl(x, base, rows * cols);
4710 let scales = lowp_compute_scales(xs, fmt, layout, rows, cols);
4711 match layout {
4712 rlx_ir::ScaleLayout::PerTensor => {
4713 sl_mut(dst, base, 1)[0] = scales[0];
4714 }
4715 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
4716 let out = std::slice::from_raw_parts_mut(
4717 base.add(dst),
4718 rows * nblk,
4719 );
4720 for (o, &s) in out.iter_mut().zip(&scales) {
4721 *o = rlx_ir::lowp_codec::f32_to_e8m0(s);
4722 }
4723 }
4724 rlx_ir::ScaleLayout::Nvfp4 { .. } => {
4725 let out = std::slice::from_raw_parts_mut(
4726 base.add(dst),
4727 rows * nblk,
4728 );
4729 for (o, &s) in out.iter_mut().zip(&scales) {
4730 *o = rlx_ir::lowp_codec::encode(rlx_ir::ScaledFormat::F8E4M3, s);
4731 }
4732 }
4733 }
4734 })
4735 }
4736
4737 Thunk::ScaledDequantize {
4738 codes,
4739 scale,
4740 dst,
4741 rows,
4742 cols,
4743 fmt,
4744 layout,
4745 } => {
4746 let (rows, cols) = (rows as usize, cols as usize);
4747 let nblk = lowp_nblk(cols, layout);
4748 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
4749 1
4750 } else {
4751 rows * nblk
4752 };
4753 Arc::new(move |base: *mut u8| unsafe {
4754 let cs = std::slice::from_raw_parts(base.add(codes) as *const u8, rows * cols);
4755 let scales = lowp_read_scales(layout, base, scale, n_scale);
4756 let out = sl_mut(dst, base, rows * cols);
4757 lowp_dequantize(cs, &scales, fmt, layout, rows, cols, out);
4758 })
4759 }
4760
4761 Thunk::LoraMatMul {
4762 x,
4763 w,
4764 a,
4765 b,
4766 dst,
4767 m,
4768 k,
4769 n,
4770 r,
4771 scale,
4772 } => {
4773 let (m, k, n, r) = (m as usize, k as usize, n as usize, r as usize);
4774 Arc::new(move |base: *mut u8| unsafe {
4775 let xs = sl(x, base, m * k);
4776 let ws = sl(w, base, k * n);
4777 let a_s = sl(a, base, k * r);
4778 let bs = sl(b, base, r * n);
4779 let out = sl_mut(dst, base, m * n);
4780 crate::blas::sgemm(xs, ws, out, m, k, n);
4782 let mut tmp = vec![0f32; m * r];
4784 crate::blas::sgemm(xs, a_s, &mut tmp, m, k, r);
4785 if scale != 1.0 {
4789 for v in tmp.iter_mut() {
4790 *v *= scale;
4791 }
4792 }
4793 crate::blas::sgemm_accumulate(&tmp, bs, out, m, r, n);
4794 })
4795 }
4796
4797 Thunk::LayerNorm {
4798 src,
4799 g,
4800 b,
4801 dst,
4802 rows,
4803 h,
4804 eps,
4805 } => {
4806 let (rows, h) = (rows as usize, h as usize);
4807 Arc::new(move |base: *mut u8| unsafe {
4808 let inp = sl(src, base, rows * h);
4809 let gamma = sl(g, base, h);
4810 let beta = sl(b, base, h);
4811 let out = sl_mut(dst, base, rows * h);
4812 for row in 0..rows {
4813 crate::kernels::layer_norm_row(
4814 &inp[row * h..(row + 1) * h],
4815 gamma,
4816 beta,
4817 &mut out[row * h..(row + 1) * h],
4818 h,
4819 eps,
4820 );
4821 }
4822 })
4823 }
4824
4825 Thunk::BatchNormInference {
4826 src,
4827 g,
4828 b,
4829 mean,
4830 var,
4831 dst,
4832 count,
4833 channels,
4834 eps,
4835 } => {
4836 let count = count as usize;
4837 let c = channels as usize;
4838 let n = count * c;
4839 let (src, g, b, mean, var, dst) = (src, g, b, mean, var, dst);
4840 Arc::new(move |base: *mut u8| unsafe {
4841 crate::kernels::batch_norm_inference(
4842 sl(src, base, n),
4843 sl(g, base, c),
4844 sl(b, base, c),
4845 sl(mean, base, c),
4846 sl(var, base, c),
4847 sl_mut(dst, base, n),
4848 c,
4849 eps,
4850 );
4851 })
4852 }
4853
4854 Thunk::Attention {
4855 q,
4856 k,
4857 v,
4858 mask,
4859 out,
4860 batch,
4861 seq,
4862 kv_seq,
4863 heads,
4864 kv_heads,
4865 head_dim,
4866 mask_kind,
4867 scale,
4868 softcap,
4869 q_row_stride,
4870 k_row_stride,
4871 v_row_stride,
4872 bhsd,
4873 } => {
4874 if std::env::var("RLX_ATTN_DEBUG").is_ok() {
4875 eprintln!("[attn-compile] batch={batch} seq={seq} kv_seq={kv_seq} heads={heads} bhsd={bhsd}");
4876 }
4877 let (b, q_s, k_s, nh, dh) = (
4886 batch as usize,
4887 seq as usize,
4888 kv_seq as usize,
4889 heads as usize,
4890 head_dim as usize,
4891 );
4892 let hs = nh * dh;
4893 let nkv = (kv_heads as usize).max(1);
4896 let group = (nh / nkv).max(1);
4897 let qrs = q_row_stride as usize;
4898 let krs = k_row_stride as usize;
4899 let vrs = v_row_stride as usize;
4900 Arc::new(move |base: *mut u8| unsafe {
4902 if std::env::var("RLX_ATTN_DEBUG").is_ok() {
4903 eprintln!("[attn] b={b} q_s={q_s} k_s={k_s} nh={nh} dh={dh} bhsd={bhsd} mask_kind={:?}", mask_kind);
4904 }
4905 let (q_len, k_len, v_len, o_len) = if bhsd {
4910 let qn = b * nh * q_s * dh;
4911 let kn = b * nkv * k_s * dh;
4912 (qn, kn, kn, qn)
4913 } else {
4914 (b * q_s * qrs, b * k_s * krs, b * k_s * vrs, b * q_s * hs)
4915 };
4916 let q_d = sl(q, base, q_len);
4917 let k_d = sl(k, base, k_len);
4918 let v_d = sl(v, base, v_len);
4919 let m_d: &[f32] = match mask_kind {
4920 rlx_ir::op::MaskKind::Custom => sl(mask, base, b * k_s),
4921 rlx_ir::op::MaskKind::Bias => sl(mask, base, b * nh * q_s * k_s),
4922 _ => &[],
4923 };
4924 let o_d = sl_mut(out, base, o_len);
4925 let mut qh = vec![0f32; q_s * dh];
4926 let mut kh = vec![0f32; k_s * dh];
4927 let mut vh = vec![0f32; k_s * dh];
4928 let mut sc = vec![0f32; q_s * k_s];
4929 let mut oh = vec![0f32; q_s * dh];
4930 for bi in 0..b {
4931 for hi in 0..nh {
4932 for si in 0..q_s {
4934 let q_off = if bhsd {
4935 bi * nh * q_s * dh + hi * q_s * dh + si * dh
4936 } else {
4937 bi * q_s * qrs + si * qrs + hi * dh
4938 };
4939 qh[si * dh..(si + 1) * dh]
4940 .copy_from_slice(&q_d[q_off..q_off + dh]);
4941 }
4942 let kv_hi = hi / group;
4946 for si in 0..k_s {
4947 let (k_off, v_off) = if bhsd {
4948 (
4949 bi * nkv * k_s * dh + kv_hi * k_s * dh + si * dh,
4950 bi * nkv * k_s * dh + kv_hi * k_s * dh + si * dh,
4951 )
4952 } else {
4953 (
4954 bi * k_s * krs + si * krs + kv_hi * dh,
4955 bi * k_s * vrs + si * vrs + kv_hi * dh,
4956 )
4957 };
4958 kh[si * dh..(si + 1) * dh]
4959 .copy_from_slice(&k_d[k_off..k_off + dh]);
4960 vh[si * dh..(si + 1) * dh]
4961 .copy_from_slice(&v_d[v_off..v_off + dh]);
4962 }
4963 for qi in 0..q_s {
4964 for ki in 0..k_s {
4965 let mut dot = 0f32;
4966 for d in 0..dh {
4967 dot += qh[qi * dh + d] * kh[ki * dh + d];
4968 }
4969 sc[qi * k_s + ki] = dot * scale;
4970 }
4971 }
4972 let q_offset = k_s.saturating_sub(q_s);
4976 match mask_kind {
4977 rlx_ir::op::MaskKind::None => {}
4978 rlx_ir::op::MaskKind::Causal => {
4979 for qi in 0..q_s {
4980 let abs_q = q_offset + qi;
4981 for ki in (abs_q + 1)..k_s {
4982 sc[qi * k_s + ki] = mask_neg;
4983 }
4984 }
4985 }
4986 rlx_ir::op::MaskKind::SlidingWindow(w) => {
4987 for qi in 0..q_s {
4988 let abs_q = q_offset + qi;
4989 let lo = abs_q.saturating_sub(w);
4990 for ki in 0..k_s {
4991 if ki < lo || ki > abs_q {
4992 sc[qi * k_s + ki] = mask_neg;
4993 }
4994 }
4995 }
4996 }
4997 rlx_ir::op::MaskKind::Custom => {
4998 for qi in 0..q_s {
4999 for ki in 0..k_s {
5000 if m_d[bi * k_s + ki] < mask_thr {
5001 sc[qi * k_s + ki] = mask_neg;
5002 }
5003 }
5004 }
5005 }
5006 rlx_ir::op::MaskKind::Bias => {
5007 let per_bh = q_s * k_s;
5008 let off = (bi * nh + hi) * per_bh;
5009 for i in 0..per_bh {
5010 sc[i] += m_d[off + i];
5011 }
5012 }
5013 }
5014 if softcap > 0.0 {
5018 for s in sc.iter_mut() {
5019 *s = softcap * (*s / softcap).tanh();
5020 }
5021 }
5022 crate::naive::softmax(&mut sc, q_s, k_s);
5023 oh.fill(0.0);
5024 for qi in 0..q_s {
5025 for ki in 0..k_s {
5026 let w = sc[qi * k_s + ki];
5027 if w > score_skip {
5028 for d in 0..dh {
5029 oh[qi * dh + d] += w * vh[ki * dh + d];
5030 }
5031 }
5032 }
5033 }
5034 for si in 0..q_s {
5035 let off = if bhsd {
5036 bi * nh * q_s * dh + hi * q_s * dh + si * dh
5037 } else {
5038 bi * q_s * hs + si * hs + hi * dh
5039 };
5040 o_d[off..off + dh].copy_from_slice(&oh[si * dh..(si + 1) * dh]);
5041 }
5042 }
5043 }
5044 })
5045 }
5046
5047 Thunk::FusedSwiGLU {
5048 src,
5049 dst,
5050 n_half,
5051 total,
5052 gate_first,
5053 } => {
5054 let n = n_half as usize;
5055 let t = total as usize;
5056 let outer = t / n;
5057 let in_total = outer * 2 * n;
5058 Arc::new(move |base: *mut u8| unsafe {
5059 let inp = sl(src, base, in_total);
5060 let out = sl_mut(dst, base, t);
5061 for o in 0..outer {
5062 let in_row = &inp[o * 2 * n..(o + 1) * 2 * n];
5063 let out_row = &mut out[o * n..(o + 1) * n];
5064 for i in 0..n {
5065 let (up, gate) = if gate_first {
5066 (in_row[n + i], in_row[i])
5067 } else {
5068 (in_row[i], in_row[n + i])
5069 };
5070 out_row[i] = up * (gate / (1.0 + (-gate).exp()));
5071 }
5072 }
5073 })
5074 }
5075
5076 Thunk::Concat {
5077 dst,
5078 outer,
5079 inner,
5080 total_axis,
5081 inputs,
5082 } => {
5083 let outer = outer as usize;
5084 let inner = inner as usize;
5085 let total_axis = total_axis as usize;
5086 let out_total = outer * total_axis * inner;
5087 let mut layout: Vec<(usize, usize, usize, usize)> =
5088 Vec::with_capacity(inputs.len());
5089 let mut cum: usize = 0;
5090 for (src_off, in_axis, in_numel) in &inputs {
5091 let in_axis = *in_axis as usize;
5092 layout.push((*src_off, cum * inner, in_axis * inner, *in_numel as usize));
5093 cum += in_axis;
5094 }
5095 Arc::new(move |base: *mut u8| unsafe {
5096 let out = sl_mut(dst, base, out_total);
5097 let row_stride = total_axis * inner;
5098 for (src_off, dst_col_off, copy_per_row, in_numel) in &layout {
5099 let inp = sl(*src_off, base, (*in_numel).max(1));
5100 concat_copy_rows_f32(
5101 out,
5102 inp,
5103 outer,
5104 *copy_per_row,
5105 row_stride,
5106 *dst_col_off,
5107 *in_numel,
5108 );
5109 }
5110 })
5111 }
5112
5113 Thunk::CustomOp {
5114 kernel,
5115 inputs,
5116 output,
5117 attrs,
5118 } => {
5119 let kernel = kernel.clone();
5125 let attrs = attrs.clone();
5126 let inputs = inputs.clone();
5127 let (out_off, out_len, out_shape) = output.clone();
5128 Arc::new(move |base: *mut u8| unsafe {
5129 dispatch_custom_op(
5130 &*kernel, &inputs, out_off, out_len, &out_shape, &attrs, base,
5131 );
5132 })
5133 }
5134
5135 Thunk::GaussianSplatRender {
5136 positions_off,
5137 positions_len,
5138 scales_off,
5139 scales_len,
5140 rotations_off,
5141 rotations_len,
5142 opacities_off,
5143 opacities_len,
5144 colors_off,
5145 colors_len,
5146 sh_coeffs_off,
5147 sh_coeffs_len,
5148 meta_off,
5149 dst_off,
5150 dst_len,
5151 width,
5152 height,
5153 tile_size,
5154 radius_scale,
5155 alpha_cutoff,
5156 max_splat_steps,
5157 transmittance_threshold,
5158 max_list_entries,
5159 } => Arc::new(move |base: *mut u8| unsafe {
5160 crate::splat::execute_gaussian_splat_render(
5161 positions_off,
5162 positions_len,
5163 scales_off,
5164 scales_len,
5165 rotations_off,
5166 rotations_len,
5167 opacities_off,
5168 opacities_len,
5169 colors_off,
5170 colors_len,
5171 sh_coeffs_off,
5172 sh_coeffs_len,
5173 meta_off,
5174 dst_off,
5175 dst_len,
5176 width,
5177 height,
5178 tile_size,
5179 radius_scale,
5180 alpha_cutoff,
5181 max_splat_steps,
5182 transmittance_threshold,
5183 max_list_entries,
5184 base,
5185 );
5186 }),
5187
5188 Thunk::GaussianSplatRenderBackward {
5189 positions_off,
5190 positions_len,
5191 scales_off,
5192 scales_len,
5193 rotations_off,
5194 rotations_len,
5195 opacities_off,
5196 opacities_len,
5197 colors_off,
5198 colors_len,
5199 sh_coeffs_off,
5200 sh_coeffs_len,
5201 meta_off,
5202 d_loss_off,
5203 d_loss_len,
5204 packed_off,
5205 packed_len,
5206 width,
5207 height,
5208 tile_size,
5209 radius_scale,
5210 alpha_cutoff,
5211 max_splat_steps,
5212 transmittance_threshold,
5213 max_list_entries,
5214 loss_grad_clip,
5215 sh_band,
5216 max_anisotropy,
5217 } => Arc::new(move |base: *mut u8| unsafe {
5218 crate::splat::execute_gaussian_splat_render_backward(
5219 positions_off,
5220 positions_len,
5221 scales_off,
5222 scales_len,
5223 rotations_off,
5224 rotations_len,
5225 opacities_off,
5226 opacities_len,
5227 colors_off,
5228 colors_len,
5229 sh_coeffs_off,
5230 sh_coeffs_len,
5231 meta_off,
5232 d_loss_off,
5233 d_loss_len,
5234 packed_off,
5235 packed_len,
5236 width,
5237 height,
5238 tile_size,
5239 radius_scale,
5240 alpha_cutoff,
5241 max_splat_steps,
5242 transmittance_threshold,
5243 max_list_entries,
5244 loss_grad_clip,
5245 sh_band,
5246 max_anisotropy,
5247 base,
5248 );
5249 }),
5250
5251 Thunk::GaussianSplatPrepare {
5252 positions_off,
5253 positions_len,
5254 scales_off,
5255 scales_len,
5256 rotations_off,
5257 rotations_len,
5258 opacities_off,
5259 opacities_len,
5260 colors_off,
5261 colors_len,
5262 sh_coeffs_off,
5263 sh_coeffs_len,
5264 meta_off,
5265 meta_len,
5266 prep_off,
5267 prep_len,
5268 width,
5269 height,
5270 tile_size,
5271 radius_scale,
5272 alpha_cutoff,
5273 max_splat_steps,
5274 transmittance_threshold,
5275 max_list_entries,
5276 } => Arc::new(move |base: *mut u8| unsafe {
5277 crate::splat::execute_gaussian_splat_prepare(
5278 positions_off,
5279 positions_len,
5280 scales_off,
5281 scales_len,
5282 rotations_off,
5283 rotations_len,
5284 opacities_off,
5285 opacities_len,
5286 colors_off,
5287 colors_len,
5288 sh_coeffs_off,
5289 sh_coeffs_len,
5290 meta_off,
5291 meta_len,
5292 prep_off,
5293 prep_len,
5294 width,
5295 height,
5296 tile_size,
5297 radius_scale,
5298 alpha_cutoff,
5299 max_splat_steps,
5300 transmittance_threshold,
5301 max_list_entries,
5302 base,
5303 );
5304 }),
5305
5306 Thunk::GaussianSplatRasterize {
5307 prep_off,
5308 prep_len,
5309 meta_off,
5310 meta_len,
5311 dst_off,
5312 dst_len,
5313 count,
5314 width,
5315 height,
5316 tile_size,
5317 alpha_cutoff,
5318 max_splat_steps,
5319 transmittance_threshold,
5320 max_list_entries,
5321 } => Arc::new(move |base: *mut u8| unsafe {
5322 crate::splat::execute_gaussian_splat_rasterize(
5323 prep_off,
5324 prep_len,
5325 meta_off,
5326 meta_len,
5327 dst_off,
5328 dst_len,
5329 count,
5330 width,
5331 height,
5332 tile_size,
5333 alpha_cutoff,
5334 max_splat_steps,
5335 transmittance_threshold,
5336 max_list_entries,
5337 base,
5338 );
5339 }),
5340
5341 Thunk::Fft1d {
5342 src,
5343 dst,
5344 outer,
5345 n_complex,
5346 inverse,
5347 norm_tag,
5348 dtype,
5349 } => {
5350 let f: Arc<dyn Fn(*mut u8) + Send + Sync> = match dtype {
5351 rlx_ir::DType::F64 => Arc::new(move |base: *mut u8| unsafe {
5352 execute_fft1d_f64(
5353 src,
5354 dst,
5355 outer as usize,
5356 n_complex as usize,
5357 inverse,
5358 norm_tag,
5359 base,
5360 );
5361 }),
5362 rlx_ir::DType::F32 => Arc::new(move |base: *mut u8| unsafe {
5363 execute_fft1d_f32(
5364 src,
5365 dst,
5366 outer as usize,
5367 n_complex as usize,
5368 inverse,
5369 norm_tag,
5370 base,
5371 );
5372 }),
5373 rlx_ir::DType::C64 => Arc::new(move |base: *mut u8| unsafe {
5374 execute_fft1d_c64(
5375 src,
5376 dst,
5377 outer as usize,
5378 n_complex as usize,
5379 inverse,
5380 norm_tag,
5381 base,
5382 );
5383 }),
5384 other => panic!("Op::Fft on CPU requires F32/F64/C64, got {other:?}"),
5385 };
5386 f
5387 }
5388
5389 Thunk::FftButterflyStage {
5390 state_src,
5391 state_dst,
5392 gate_src,
5393 rev_src,
5394 tw_re_src,
5395 tw_im_src,
5396 batch,
5397 n_fft,
5398 stage,
5399 } => Arc::new(move |base: *mut u8| unsafe {
5400 execute_fft_butterfly_stage_f32(
5401 state_src,
5402 state_dst,
5403 gate_src,
5404 rev_src,
5405 tw_re_src,
5406 tw_im_src,
5407 batch as usize,
5408 n_fft as usize,
5409 stage as usize,
5410 base,
5411 );
5412 }),
5413
5414 Thunk::LogMel {
5415 spec,
5416 filters,
5417 dst,
5418 outer,
5419 n_fft,
5420 n_bins,
5421 n_mels,
5422 } => Arc::new(move |base: *mut u8| unsafe {
5423 execute_log_mel_f32(
5424 spec,
5425 filters,
5426 dst,
5427 outer as usize,
5428 n_fft as usize,
5429 n_bins as usize,
5430 n_mels as usize,
5431 base,
5432 );
5433 }),
5434
5435 Thunk::LogMelBackward {
5436 spec,
5437 filters,
5438 dy,
5439 dst,
5440 outer,
5441 n_fft,
5442 n_bins,
5443 n_mels,
5444 } => Arc::new(move |base: *mut u8| unsafe {
5445 execute_log_mel_backward_f32(
5446 spec,
5447 filters,
5448 dy,
5449 dst,
5450 outer as usize,
5451 n_fft as usize,
5452 n_bins as usize,
5453 n_mels as usize,
5454 base,
5455 );
5456 }),
5457
5458 Thunk::WelchPeaks {
5459 spec,
5460 dst,
5461 welch_batch,
5462 n_fft,
5463 n_segments,
5464 k,
5465 } => Arc::new(move |base: *mut u8| unsafe {
5466 execute_welch_peaks_f32(
5467 spec,
5468 dst,
5469 welch_batch as usize,
5470 n_fft as usize,
5471 n_segments as usize,
5472 k as usize,
5473 base,
5474 );
5475 }),
5476
5477 Thunk::SgdMomentum { param, vel, grad, p_out, v_out, lr, mom, len } => {
5478 let len = len as usize;
5479 Arc::new(move |base: *mut u8| unsafe {
5480 let p = sl(param, base, len);
5481 let v = sl(vel, base, len);
5482 let g = sl(grad, base, len);
5483 let po = sl_mut(p_out, base, len);
5484 let vo = sl_mut(v_out, base, len);
5485 for i in 0..len {
5486 let vn = mom * v[i] + g[i];
5487 vo[i] = vn;
5488 po[i] = p[i] - lr * vn;
5489 }
5490 })
5491 }
5492
5493 _ => Arc::new(|_: *mut u8| {}),
5494 }
5495 })
5496 .collect();
5497
5498 let fuse_threshold: usize = rlx_ir::env::var("RLX_FUSE_ATTN_THRESHOLD")
5502 .and_then(|v| v.parse().ok())
5503 .unwrap_or(64);
5504 let should_fuse = thunks.iter().any(|t| match t {
5505 Thunk::Attention { batch, seq, .. } => {
5506 (*batch as usize) * (*seq as usize) <= fuse_threshold
5507 }
5508 _ => false,
5509 });
5510
5511 if should_fuse {
5512 let active: Vec<usize> = thunks
5514 .iter()
5515 .enumerate()
5516 .filter(|(_, t)| !matches!(t, Thunk::Nop))
5517 .map(|(i, _)| i)
5518 .collect();
5519
5520 let mut kill = vec![false; thunks.len()]; let mut insertions: Vec<(usize, Thunk)> = Vec::new(); let mut ai = 0;
5524 while ai < active.len() {
5525 let a = |off: usize| -> Option<(usize, &Thunk)> {
5527 active.get(ai + off).map(|&idx| (idx, &thunks[idx]))
5528 };
5529
5530 let matched = (|| {
5532 let (_i0, t0) = a(0)?;
5533 let (_, t1) = a(1)?;
5534 let (_, t2) = a(2)?;
5535 let (_, t3) = a(3)?;
5536
5537 let (hidden, qkv_w, qkv_b, has_b) = match t0 {
5539 Thunk::FusedMmBiasAct {
5540 a,
5541 w,
5542 bias,
5543 n: _,
5544 act: None,
5545 ..
5546 } => (*a, *w, *bias, true),
5547 Thunk::Sgemm { a, b, n: _, .. } => (*a, *b, 0, false),
5548 _ => return None,
5549 };
5550
5551 if !matches!(t1, Thunk::Narrow { .. }) {
5553 return None;
5554 }
5555 if !matches!(t2, Thunk::Narrow { .. }) {
5556 return None;
5557 }
5558 if !matches!(t3, Thunk::Narrow { .. }) {
5559 return None;
5560 }
5561
5562 let (has_rope, attn_ai, cos_off, sin_off, cl, rope_interleaved) = if let Some((
5567 _,
5568 Thunk::Rope {
5569 cos,
5570 sin,
5571 cos_len,
5572 interleaved,
5573 ..
5574 },
5575 )) = a(4)
5576 {
5577 let q_il = *interleaved;
5578 match a(5).map(|x| x.1) {
5579 Some(Thunk::Rope {
5580 interleaved: k_il, ..
5581 }) if *k_il == q_il => {
5582 if matches!(a(6).map(|x| x.1), Some(Thunk::Attention { .. })) {
5583 (true, 6, *cos, *sin, *cos_len, q_il)
5584 } else {
5585 return None;
5586 }
5587 }
5588 _ => return None,
5589 }
5590 } else if matches!(a(4).map(|x| x.1), Some(Thunk::Attention { .. })) {
5591 (false, 4, 0, 0, 0, false)
5592 } else {
5593 return None;
5594 };
5595
5596 let (_attn_real_idx, attn_t) = a(attn_ai)?;
5597 let (batch, seq, heads, head_dim, mask, mask_kind, kv_seq, softcap) = match attn_t {
5598 Thunk::Attention {
5599 batch,
5600 seq,
5601 heads,
5602 head_dim,
5603 mask,
5604 mask_kind,
5605 kv_seq,
5606 softcap,
5607 ..
5608 } => (
5609 *batch, *seq, *heads, *head_dim, *mask, *mask_kind, *kv_seq, *softcap,
5610 ),
5611 _ => return None,
5612 };
5613 if matches!(mask_kind, rlx_ir::op::MaskKind::Bias)
5620 || kv_seq != seq
5621 || softcap != 0.0
5622 {
5623 return None;
5624 }
5625
5626 let (_out_real_idx, out_t) = a(attn_ai + 1)?;
5628 let (out_w, out_b, out_dst) = match out_t {
5629 Thunk::FusedMmBiasAct {
5630 w,
5631 bias,
5632 c,
5633 act: None,
5634 ..
5635 } => (*w, *bias, *c),
5636 Thunk::Sgemm { b: w, c, .. } => (*w, 0, *c),
5637 _ => return None,
5638 };
5639
5640 let hs = heads * head_dim;
5641 let total_active = attn_ai + 2; Some((
5644 total_active,
5645 Thunk::FusedAttnBlock {
5646 hidden,
5647 qkv_w,
5648 out_w,
5649 mask,
5650 mask_kind,
5651 out: out_dst,
5652 qkv_b: if has_b { qkv_b } else { 0 },
5653 out_b: if has_b { out_b } else { 0 },
5654 cos: cos_off,
5655 sin: sin_off,
5656 cos_len: cl,
5657 batch,
5658 seq,
5659 hs,
5660 nh: heads,
5661 dh: head_dim,
5662 has_bias: has_b,
5663 has_rope,
5664 interleaved: rope_interleaved,
5665 },
5666 ))
5667 })();
5668
5669 if let Some((count, fused_thunk)) = matched {
5670 for off in 0..count {
5672 if let Some(&idx) = active.get(ai + off) {
5673 kill[idx] = true;
5674 }
5675 }
5676 insertions.push((active[ai], fused_thunk));
5678 ai += count;
5679 } else {
5680 ai += 1;
5681 }
5682 }
5683
5684 if !insertions.is_empty() {
5686 let mut new_thunks = Vec::with_capacity(thunks.len());
5687 let mut insert_idx = 0;
5688 for (i, t) in thunks.into_iter().enumerate() {
5689 if insert_idx < insertions.len() && insertions[insert_idx].0 == i {
5690 new_thunks.push(insertions[insert_idx].1.clone());
5691 insert_idx += 1;
5692 }
5693 if !kill[i] {
5694 new_thunks.push(t);
5695 }
5696 }
5697 if cfg.verbose >= 1 {
5698 eprintln!(
5699 "[rlx] fused_attention: {} attention blocks fused",
5700 insertions.len()
5701 );
5702 }
5703 thunks = new_thunks;
5704 }
5705 }
5706
5707 if should_fuse {
5712 let active: Vec<usize> = thunks
5713 .iter()
5714 .enumerate()
5715 .filter(|(_, t)| !matches!(t, Thunk::Nop))
5716 .map(|(i, _)| i)
5717 .collect();
5718
5719 let mut kill = vec![false; thunks.len()];
5720 let mut insertions: Vec<(usize, Thunk)> = Vec::new();
5721
5722 let a = |ai: usize| -> Option<&Thunk> { active.get(ai).map(|&i| &thunks[i]) };
5723
5724 let mut ai = 0;
5725 while ai < active.len() {
5726 let bert_match = (|| -> Option<usize> {
5728 let fab = a(ai)?;
5729 let rln1 = a(ai + 1)?;
5730 let ffn1 = a(ai + 2)?;
5731 let ffn2 = a(ai + 3)?;
5732 let rln2 = a(ai + 4)?;
5733
5734 let (hidden, qkv_w, qkv_b, out_w, out_b, mask, batch, seq, hs, nh, dh) = match fab {
5735 Thunk::FusedAttnBlock {
5736 hidden,
5737 qkv_w,
5738 qkv_b,
5739 out_w,
5740 out_b,
5741 mask,
5742 mask_kind: rlx_ir::op::MaskKind::Custom,
5746 batch,
5747 seq,
5748 hs,
5749 nh,
5750 dh,
5751 has_bias: true,
5752 has_rope: false,
5753 ..
5754 } => (
5755 *hidden, *qkv_w, *qkv_b, *out_w, *out_b, *mask, *batch, *seq, *hs, *nh, *dh,
5756 ),
5757 _ => return None,
5758 };
5759 let (ln1_g, ln1_b, eps1) = match rln1 {
5760 Thunk::FusedResidualLN { g, b, eps, .. } => (*g, *b, *eps),
5761 _ => return None,
5762 };
5763 let (fc1_w, fc1_b, int_dim) = match ffn1 {
5764 Thunk::FusedMmBiasAct {
5765 w,
5766 bias,
5767 n,
5768 act: Some(Activation::Gelu),
5769 ..
5770 } => (*w, *bias, *n),
5771 _ => return None,
5772 };
5773 let (fc2_w, fc2_b) = match ffn2 {
5774 Thunk::FusedMmBiasAct {
5775 w, bias, act: None, ..
5776 } => (*w, *bias),
5777 _ => return None,
5778 };
5779 let (ln2_g, ln2_b, eps2, out) = match rln2 {
5780 Thunk::FusedResidualLN { g, b, eps, out, .. } => (*g, *b, *eps, *out),
5781 _ => return None,
5782 };
5783
5784 for off in 0..5 {
5785 kill[active[ai + off]] = true;
5786 }
5787 insertions.push((
5788 active[ai],
5789 Thunk::FusedBertLayer {
5790 hidden,
5791 qkv_w,
5792 qkv_b,
5793 out_w,
5794 out_b,
5795 mask,
5796 ln1_g,
5797 ln1_b,
5798 eps1,
5799 fc1_w,
5800 fc1_b,
5801 fc2_w,
5802 fc2_b,
5803 ln2_g,
5804 ln2_b,
5805 eps2,
5806 out,
5807 batch,
5808 seq,
5809 hs,
5810 nh,
5811 dh,
5812 int_dim,
5813 },
5814 ));
5815 Some(5)
5816 })();
5817 if let Some(n) = bert_match {
5818 ai += n;
5819 continue;
5820 }
5821
5822 let nomic_match = (|| -> Option<usize> {
5839 if rlx_ir::env::flag("RLX_DISABLE_NOMIC_FUSION") {
5840 return None;
5841 }
5842 let (
5843 hidden,
5844 qkv_w,
5845 out_w,
5846 mask,
5847 cos,
5848 sin,
5849 cos_len,
5850 batch,
5851 seq,
5852 hs,
5853 nh,
5854 dh,
5855 interleaved,
5856 ) = match a(ai)? {
5857 Thunk::FusedAttnBlock {
5858 hidden,
5859 qkv_w,
5860 out_w,
5861 mask,
5862 cos,
5863 sin,
5864 cos_len,
5865 batch,
5866 seq,
5867 hs,
5868 nh,
5869 dh,
5870 has_bias: false,
5871 has_rope: true,
5872 mask_kind: rlx_ir::op::MaskKind::Custom,
5873 interleaved,
5874 ..
5875 } => (
5876 *hidden,
5877 *qkv_w,
5878 *out_w,
5879 *mask,
5880 *cos,
5881 *sin,
5882 *cos_len,
5883 *batch,
5884 *seq,
5885 *hs,
5886 *nh,
5887 *dh,
5888 *interleaved,
5889 ),
5890 _ => return None,
5891 };
5892 let (ln1_g, ln1_b, eps1) = match a(ai + 1)? {
5894 Thunk::FusedResidualLN { g, b, eps, .. } => (*g, *b, *eps),
5895 _ => return None,
5896 };
5897 let mut kills: Vec<usize> = vec![ai, ai + 1];
5900 let mut o = 2;
5902 if matches!(a(ai + o)?, Thunk::Concat { .. }) {
5903 o += 1;
5904 }
5905 let fused_fc_w = match a(ai + o)? {
5906 Thunk::Sgemm { b: w, .. } => *w,
5907 _ => return None,
5908 };
5909 kills.push(ai + o);
5910 o += 1;
5911 let int_dim = match a(ai + o)? {
5914 Thunk::FusedSwiGLU {
5915 n_half,
5916 gate_first: false,
5917 ..
5918 } => *n_half,
5919 _ => return None,
5920 };
5921 kills.push(ai + o);
5922 o += 1;
5923 let fc2_w = match a(ai + o)? {
5925 Thunk::Sgemm { b: w, .. } => *w,
5926 _ => return None,
5927 };
5928 kills.push(ai + o);
5929 o += 1;
5930 let (ln2_g, ln2_b, eps2, out) = match a(ai + o)? {
5932 Thunk::FusedResidualLN { g, b, eps, out, .. } => (*g, *b, *eps, *out),
5933 _ => return None,
5934 };
5935 kills.push(ai + o);
5936 let consumed = o + 1;
5937
5938 for ki in kills {
5939 kill[active[ki]] = true;
5940 }
5941 FUSED_NOMIC_LAYER_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5942 insertions.push((
5947 active[ai + o],
5948 Thunk::FusedNomicLayer {
5949 hidden,
5950 qkv_w,
5951 out_w,
5952 mask,
5953 cos,
5954 sin,
5955 cos_len,
5956 ln1_g,
5957 ln1_b,
5958 eps1,
5959 fc11_w: fused_fc_w,
5960 fc12_w: 0,
5961 fc2_w,
5962 ln2_g,
5963 ln2_b,
5964 eps2,
5965 out,
5966 batch,
5967 seq,
5968 hs,
5969 nh,
5970 dh,
5971 int_dim,
5972 interleaved,
5973 },
5974 ));
5975 Some(consumed)
5976 })();
5977 if let Some(n) = nomic_match {
5978 ai += n;
5979 continue;
5980 }
5981
5982 ai += 1;
5983 }
5984
5985 if !insertions.is_empty() {
5986 let mut new_thunks = Vec::with_capacity(thunks.len());
5987 let mut ins_idx = 0;
5988 for (i, t) in thunks.into_iter().enumerate() {
5989 if ins_idx < insertions.len() && insertions[ins_idx].0 == i {
5990 new_thunks.push(insertions[ins_idx].1.clone());
5991 ins_idx += 1;
5992 }
5993 if !kill[i] {
5994 new_thunks.push(t);
5995 }
5996 }
5997 if cfg.verbose >= 1 {
5998 eprintln!(
5999 "[rlx] fused_layer: {} full transformer layers fused",
6000 insertions.len()
6001 );
6002 }
6003 thunks = new_thunks;
6004 }
6005 }
6006
6007 {
6019 let mut read_offsets: HashMap<usize, usize> = HashMap::new();
6022 for t in &thunks {
6023 for off in thunk_read_offsets(t) {
6024 *read_offsets.entry(off).or_insert(0) += 1;
6025 }
6026 }
6027
6028 let mut fused_count = 0usize;
6029 for i in 0..thunks.len().saturating_sub(1) {
6030 let narrow = match &thunks[i] {
6033 Thunk::Narrow { .. } => i,
6034 _ => continue,
6035 };
6036 let mut j = narrow + 1;
6038 while j < thunks.len() && matches!(thunks[j], Thunk::Nop) {
6039 j += 1;
6040 }
6041 if j >= thunks.len() {
6042 continue;
6043 }
6044 let (n_src, n_dst, n_src_stride) = match &thunks[narrow] {
6046 Thunk::Narrow {
6047 src,
6048 dst,
6049 src_stride,
6050 ..
6051 } => (*src, *dst, *src_stride),
6052 _ => continue,
6053 };
6054 let rope_reads_narrow = matches!(&thunks[j],
6055 Thunk::Rope { src, .. } if *src == n_dst);
6056 if !rope_reads_narrow {
6057 continue;
6058 }
6059 if read_offsets.get(&n_dst).copied().unwrap_or(0) != 1 {
6063 continue;
6064 }
6065
6066 if let Thunk::Rope {
6069 src,
6070 src_row_stride,
6071 ..
6072 } = &mut thunks[j]
6073 {
6074 *src = n_src;
6075 *src_row_stride = n_src_stride;
6076 }
6077 thunks[narrow] = Thunk::Nop;
6078 fused_count += 1;
6079 }
6080
6081 if fused_count > 0 && cfg.verbose >= 1 {
6082 eprintln!(
6083 "[rlx] fused_qk_rope: {} Narrow→Rope pairs collapsed",
6084 fused_count
6085 );
6086 }
6087 }
6088
6089 {
6101 let mut read_counts: HashMap<usize, usize> = HashMap::new();
6102 for t in &thunks {
6103 for off in thunk_read_offsets(t) {
6104 *read_counts.entry(off).or_insert(0) += 1;
6105 }
6106 }
6107 let mut dst_to_idx: HashMap<usize, usize> = HashMap::new();
6109 for (i, t) in thunks.iter().enumerate() {
6110 if let Thunk::Narrow { dst, .. } = t {
6111 dst_to_idx.insert(*dst, i);
6112 }
6113 }
6114
6115 let mut fused_count = 0usize;
6116 for i in 0..thunks.len() {
6117 let (q_off, k_off, v_off) = match &thunks[i] {
6118 Thunk::Attention { q, k, v, .. } => (*q, *k, *v),
6119 _ => continue,
6120 };
6121 let q_n = match dst_to_idx.get(&q_off).copied() {
6123 Some(x) => x,
6124 None => continue,
6125 };
6126 let k_n = match dst_to_idx.get(&k_off).copied() {
6127 Some(x) => x,
6128 None => continue,
6129 };
6130 let v_n = match dst_to_idx.get(&v_off).copied() {
6131 Some(x) => x,
6132 None => continue,
6133 };
6134 if read_counts.get(&q_off).copied().unwrap_or(0) != 1 {
6136 continue;
6137 }
6138 if read_counts.get(&k_off).copied().unwrap_or(0) != 1 {
6139 continue;
6140 }
6141 if read_counts.get(&v_off).copied().unwrap_or(0) != 1 {
6142 continue;
6143 }
6144
6145 let (q_src, q_stride) = match &thunks[q_n] {
6146 Thunk::Narrow {
6147 src, src_stride, ..
6148 } => (*src, *src_stride),
6149 _ => continue,
6150 };
6151 let (k_src, k_stride) = match &thunks[k_n] {
6152 Thunk::Narrow {
6153 src, src_stride, ..
6154 } => (*src, *src_stride),
6155 _ => continue,
6156 };
6157 let (v_src, v_stride) = match &thunks[v_n] {
6158 Thunk::Narrow {
6159 src, src_stride, ..
6160 } => (*src, *src_stride),
6161 _ => continue,
6162 };
6163
6164 if let Thunk::Attention {
6165 q,
6166 k,
6167 v,
6168 q_row_stride,
6169 k_row_stride,
6170 v_row_stride,
6171 ..
6172 } = &mut thunks[i]
6173 {
6174 *q = q_src;
6175 *k = k_src;
6176 *v = v_src;
6177 *q_row_stride = q_stride;
6178 *k_row_stride = k_stride;
6179 *v_row_stride = v_stride;
6180 }
6181 thunks[q_n] = Thunk::Nop;
6182 thunks[k_n] = Thunk::Nop;
6183 thunks[v_n] = Thunk::Nop;
6184 fused_count += 1;
6185 }
6186
6187 if fused_count > 0 && cfg.verbose >= 1 {
6188 eprintln!(
6189 "[rlx] fused_strided_attn: {} Narrow×3→Attention rewrites",
6190 fused_count
6191 );
6192 }
6193 }
6194
6195 ThunkSchedule {
6196 thunks,
6197 moe_resident: None,
6198 moe_resident_layers: None,
6199 moe_topk_capture: None,
6200 mask_threshold: cfg.mask_binary_threshold,
6201 mask_neg_inf: cfg.attn_mask_neg_inf,
6202 score_skip: cfg.score_skip_threshold,
6203 compiled_fns,
6204 rng: rng_shared,
6205 }
6206}
6207
6208#[allow(unused_variables)]
6209fn compile_fused_mat_mul_bias_act(
6210 node: &rlx_ir::Node,
6211 graph: &Graph,
6212 arena: &crate::arena::Arena,
6213 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6214 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6215 rng: rlx_ir::RngOptions,
6216) -> Thunk {
6217 let Op::FusedMatMulBiasAct { activation } = &node.op else {
6218 unreachable!()
6219 };
6220 {
6221 let shape = &node.shape;
6222 let n = shape.dim(shape.rank() - 1).unwrap_static();
6223 let total = shape.num_elements().unwrap();
6224 let m = total / n;
6225 let a_len = get_len(graph, node.inputs[0]);
6226 let k = a_len / m;
6227 Thunk::FusedMmBiasAct {
6228 a: node_offset(arena, node.inputs[0]),
6229 w: node_offset(arena, node.inputs[1]),
6230 bias: node_offset(arena, node.inputs[2]),
6231 c: node_offset(arena, node.id),
6232 m: m as u32,
6233 k: k as u32,
6234 n: n as u32,
6235 act: *activation,
6236 }
6237 }
6238}
6239
6240#[allow(unused_variables)]
6241fn compile_fused_residual_l_n(
6242 node: &rlx_ir::Node,
6243 graph: &Graph,
6244 arena: &crate::arena::Arena,
6245 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6246 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6247 rng: rlx_ir::RngOptions,
6248) -> Thunk {
6249 let Op::FusedResidualLN { has_bias, eps } = &node.op else {
6250 unreachable!()
6251 };
6252 {
6253 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
6254 let total = node.shape.num_elements().unwrap();
6255 let rows = total / h;
6256 let (g_idx, b_idx) = if *has_bias { (3, 4) } else { (2, 3) };
6257 Thunk::FusedResidualLN {
6258 x: node_offset(arena, node.inputs[0]),
6259 res: node_offset(arena, node.inputs[1]),
6260 bias: if *has_bias {
6261 node_offset(arena, node.inputs[2])
6262 } else {
6263 0
6264 },
6265 g: node_offset(arena, node.inputs[g_idx]),
6266 b: node_offset(arena, node.inputs[b_idx]),
6267 out: node_offset(arena, node.id),
6268 rows: rows as u32,
6269 h: h as u32,
6270 eps: *eps,
6271 has_bias: *has_bias,
6272 }
6273 }
6274}
6275
6276#[allow(unused_variables)]
6277fn compile_fused_residual_rms_norm(
6278 node: &rlx_ir::Node,
6279 graph: &Graph,
6280 arena: &crate::arena::Arena,
6281 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6282 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6283 rng: rlx_ir::RngOptions,
6284) -> Thunk {
6285 let Op::FusedResidualRmsNorm { has_bias, eps } = &node.op else {
6286 unreachable!()
6287 };
6288 {
6289 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
6290 let total = node.shape.num_elements().unwrap();
6291 let rows = total / h;
6292 let (g_idx, b_idx) = if *has_bias { (3, 4) } else { (2, 3) };
6293 Thunk::FusedResidualRmsNorm {
6294 x: node_offset(arena, node.inputs[0]),
6295 res: node_offset(arena, node.inputs[1]),
6296 bias: if *has_bias {
6297 node_offset(arena, node.inputs[2])
6298 } else {
6299 0
6300 },
6301 g: node_offset(arena, node.inputs[g_idx]),
6302 b: node_offset(arena, node.inputs[b_idx]),
6303 out: node_offset(arena, node.id),
6304 rows: rows as u32,
6305 h: h as u32,
6306 eps: *eps,
6307 has_bias: *has_bias,
6308 }
6309 }
6310}
6311
6312#[allow(unused_variables)]
6313fn compile_mat_mul(
6314 node: &rlx_ir::Node,
6315 graph: &Graph,
6316 arena: &crate::arena::Arena,
6317 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6318 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6319 rng: rlx_ir::RngOptions,
6320) -> Thunk {
6321 let Op::MatMul = &node.op else { unreachable!() };
6322 {
6323 let shape = &node.shape;
6324 let a_shape = &graph.node(node.inputs[0]).shape;
6325 let b_shape = &graph.node(node.inputs[1]).shape;
6326 let eff = rlx_ir::shape::matmul_shape(a_shape, b_shape).unwrap_or_else(|_| shape.clone());
6329 let rank = eff.rank().max(2);
6330 let n = eff.dim(rank - 1).unwrap_static();
6331 let k_dim = a_shape.dim(a_shape.rank().max(2) - 1).unwrap_static();
6332 if shape.dtype() == rlx_ir::DType::C64 {
6333 let both = a_shape.rank() >= 3 && b_shape.rank() >= 3;
6337 assert!(!both, "batched (both-operand) C64 matmul not yet supported");
6338 let m: usize = if a_shape.rank() >= 3 {
6339 (0..a_shape.rank() - 1)
6340 .map(|d| a_shape.dim(d).unwrap_static())
6341 .product()
6342 } else {
6343 a_shape.dim(a_shape.rank() - 2).unwrap_static()
6344 };
6345 Thunk::CgemmC64 {
6346 a: node_offset(arena, node.inputs[0]),
6347 b: node_offset(arena, node.inputs[1]),
6348 c: node_offset(arena, node.id),
6349 m: m as u32,
6350 k: k_dim as u32,
6351 n: n as u32,
6352 }
6353 } else {
6354 let both_batched = a_shape.rank() >= 3 && b_shape.rank() >= 3;
6357 let batched_3d = rank >= 3 && both_batched && a_shape.rank() + b_shape.rank() > 4;
6358 if batched_3d && shape.dtype() == rlx_ir::DType::F64 {
6359 let mut batch_prod = 1usize;
6360 for d in 0..rank - 2 {
6361 batch_prod *= eff.dim(d).unwrap_static();
6362 }
6363 let m_dim = eff.dim(rank - 2).unwrap_static();
6364 Thunk::BatchedDgemmF64 {
6365 a: node_offset(arena, node.inputs[0]),
6366 b: node_offset(arena, node.inputs[1]),
6367 c: node_offset(arena, node.id),
6368 batch: batch_prod as u32,
6369 m: m_dim as u32,
6370 k: k_dim as u32,
6371 n: n as u32,
6372 }
6373 } else if batched_3d && shape.dtype() == rlx_ir::DType::F32 {
6374 let mut batch_prod = 1usize;
6375 for d in 0..rank - 2 {
6376 batch_prod *= eff.dim(d).unwrap_static();
6377 }
6378 let m_dim = eff.dim(rank - 2).unwrap_static();
6379 Thunk::BatchedSgemm {
6380 a: node_offset(arena, node.inputs[0]),
6381 b: node_offset(arena, node.inputs[1]),
6382 c: node_offset(arena, node.id),
6383 batch: batch_prod as u32,
6384 m: m_dim as u32,
6385 k: k_dim as u32,
6386 n: n as u32,
6387 }
6388 } else {
6389 let m = if a_shape.rank() >= 3 && b_shape.rank() <= 2 {
6390 let mut m_prod = 1usize;
6391 for d in 0..a_shape.rank() - 1 {
6392 m_prod *= a_shape.dim(d).unwrap_static();
6393 }
6394 m_prod
6395 } else if a_shape.rank() >= 2 {
6396 a_shape.dim(a_shape.rank() - 2).unwrap_static()
6397 } else {
6398 eff.num_elements().unwrap_or(1) / n.max(1)
6399 };
6400 match shape.dtype() {
6401 rlx_ir::DType::F64 => Thunk::Dgemm {
6402 a: node_offset(arena, node.inputs[0]),
6403 b: node_offset(arena, node.inputs[1]),
6404 c: node_offset(arena, node.id),
6405 m: m as u32,
6406 k: k_dim as u32,
6407 n: n as u32,
6408 },
6409 _ => {
6410 if let Some(&(asrc, ta, bsrc, tb)) = matmul_fold.get(&node.id) {
6411 Thunk::SgemmT {
6414 a: node_offset(arena, asrc),
6415 b: node_offset(arena, bsrc),
6416 c: node_offset(arena, node.id),
6417 m: m as u32,
6418 k: k_dim as u32,
6419 n: n as u32,
6420 ta,
6421 tb,
6422 }
6423 } else {
6424 Thunk::Sgemm {
6425 a: node_offset(arena, node.inputs[0]),
6426 b: node_offset(arena, node.inputs[1]),
6427 c: node_offset(arena, node.id),
6428 m: m as u32,
6429 k: k_dim as u32,
6430 n: n as u32,
6431 }
6432 }
6433 }
6434 }
6435 }
6436 }
6437 }
6438}
6439
6440#[allow(unused_variables)]
6441fn compile_gather(
6442 node: &rlx_ir::Node,
6443 graph: &Graph,
6444 arena: &crate::arena::Arena,
6445 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6446 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6447 rng: rlx_ir::RngOptions,
6448) -> Thunk {
6449 let Op::Gather { axis } = &node.op else {
6450 unreachable!()
6451 };
6452 {
6453 let table_shape = &graph.node(node.inputs[0]).shape;
6455 let rank = table_shape.rank();
6456 let outer: usize = (0..*axis)
6457 .map(|i| table_shape.dim(i).unwrap_static())
6458 .product::<usize>()
6459 .max(1);
6460 let trailing: usize = (*axis + 1..rank)
6461 .map(|i| table_shape.dim(i).unwrap_static())
6462 .product::<usize>()
6463 .max(1);
6464 let axis_dim = table_shape.dim(*axis).unwrap_static();
6465 let idx_len = get_len(graph, node.inputs[1]);
6466 let idx_i64 = u8::from(graph.node(node.inputs[1]).shape.dtype() == rlx_ir::DType::I64);
6467 let table_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
6468 Thunk::GatherAxis {
6469 table: node_offset(arena, node.inputs[0]),
6470 idx: node_offset(arena, node.inputs[1]),
6471 dst: node_offset(arena, node.id),
6472 outer: outer as u32,
6473 axis_dim: axis_dim as u32,
6474 num_idx: idx_len as u32,
6475 trailing: trailing as u32,
6476 idx_i64,
6477 table_bytes,
6478 }
6479 }
6480}
6481
6482#[allow(unused_variables)]
6483fn compile_narrow(
6484 node: &rlx_ir::Node,
6485 graph: &Graph,
6486 arena: &crate::arena::Arena,
6487 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6488 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6489 rng: rlx_ir::RngOptions,
6490) -> Thunk {
6491 let Op::Narrow { axis, start, len } = &node.op else {
6492 unreachable!()
6493 };
6494 {
6495 let in_shape = &graph.node(node.inputs[0]).shape;
6496 let elem_bytes = in_shape.dtype().size_bytes() as u8;
6497 let rank = in_shape.rank();
6498 let outer: usize = (0..*axis)
6499 .map(|i| in_shape.dim(i).unwrap_static())
6500 .product::<usize>()
6501 .max(1);
6502 let inner: usize = (*axis + 1..rank)
6503 .map(|i| in_shape.dim(i).unwrap_static())
6504 .product::<usize>()
6505 .max(1);
6506 let in_axis = in_shape.dim(*axis).unwrap_static();
6507 let src_byte_offset =
6508 node_offset(arena, node.inputs[0]) + start * inner * elem_bytes as usize;
6509 Thunk::Narrow {
6510 src: src_byte_offset,
6511 dst: node_offset(arena, node.id),
6512 outer: outer as u32,
6513 src_stride: (in_axis * inner) as u32, dst_stride: (*len * inner) as u32, inner: (*len * inner) as u32, elem_bytes,
6517 }
6518 }
6519}
6520
6521#[allow(unused_variables)]
6522fn compile_reverse(
6523 node: &rlx_ir::Node,
6524 graph: &Graph,
6525 arena: &crate::arena::Arena,
6526 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6527 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6528 rng: rlx_ir::RngOptions,
6529) -> Thunk {
6530 let Op::Reverse { axes } = &node.op else {
6531 unreachable!()
6532 };
6533 {
6534 let in_shape = &graph.node(node.inputs[0]).shape;
6535 let rank = in_shape.rank();
6536 let dims: Vec<u32> = (0..rank)
6537 .map(|i| in_shape.dim(i).unwrap_static() as u32)
6538 .collect();
6539 let mut rev_mask = vec![false; rank];
6540 for &a in axes {
6541 if a < rank {
6542 rev_mask[a] = true;
6543 }
6544 }
6545 Thunk::Reverse {
6546 src: node_offset(arena, node.inputs[0]),
6547 dst: node_offset(arena, node.id),
6548 dims,
6549 rev_mask,
6550 elem_bytes: in_shape.dtype().size_bytes() as u8,
6551 }
6552 }
6553}
6554
6555#[allow(unused_variables)]
6556fn compile_cast(
6557 node: &rlx_ir::Node,
6558 graph: &Graph,
6559 arena: &crate::arena::Arena,
6560 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6561 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6562 rng: rlx_ir::RngOptions,
6563) -> Thunk {
6564 let Op::Cast { to } = &node.op else {
6565 unreachable!()
6566 };
6567 {
6568 let in_node = graph.node(node.inputs[0]);
6569 let in_dtype = in_node.shape.dtype();
6570 let out_dtype = *to;
6571 let len = node.shape.num_elements().unwrap();
6572 let src = node_offset(arena, node.inputs[0]);
6573 let dst = node_offset(arena, node.id);
6574 if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::I64 {
6575 Thunk::CastF32ToI64 {
6576 src,
6577 dst,
6578 len: len as u32,
6579 }
6580 } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::F64 {
6581 Thunk::CastF32ToF64 {
6582 src,
6583 dst,
6584 len: len as u32,
6585 }
6586 } else if in_dtype == rlx_ir::DType::F32 && out_dtype == rlx_ir::DType::I32 {
6587 Thunk::CastF32ToI32 {
6588 src,
6589 dst,
6590 len: len as u32,
6591 }
6592 } else if in_dtype == rlx_ir::DType::I64 && out_dtype == rlx_ir::DType::F32 {
6593 Thunk::CastI64ToF32 {
6594 src,
6595 dst,
6596 len: len as u32,
6597 }
6598 } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::I32 {
6599 Thunk::CastBoolToI32 {
6600 src,
6601 dst,
6602 len: len as u32,
6603 }
6604 } else if in_dtype == rlx_ir::DType::Bool && out_dtype == rlx_ir::DType::F32 {
6605 Thunk::CastBoolToF32 {
6608 src,
6609 dst,
6610 len: len as u32,
6611 }
6612 } else if in_dtype == rlx_ir::DType::I32 && out_dtype == rlx_ir::DType::F32 {
6613 Thunk::CastI32ToF32 {
6614 src,
6615 dst,
6616 len: len as u32,
6617 }
6618 } else if in_dtype == out_dtype {
6619 match out_dtype {
6620 rlx_ir::DType::F64 => Thunk::CopyF64 {
6621 src,
6622 dst,
6623 len: len as u32,
6624 },
6625 rlx_ir::DType::I64 => Thunk::CopyI64 {
6626 src,
6627 dst,
6628 len: len as u32,
6629 },
6630 _ => Thunk::Copy {
6631 src,
6632 dst,
6633 len: len as u32,
6634 },
6635 }
6636 } else {
6637 Thunk::Copy {
6638 src,
6639 dst,
6640 len: len as u32,
6641 }
6642 }
6643 }
6644}
6645
6646#[allow(unused_variables)]
6647fn compile_quantize(
6648 node: &rlx_ir::Node,
6649 graph: &Graph,
6650 arena: &crate::arena::Arena,
6651 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6652 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6653 rng: rlx_ir::RngOptions,
6654) -> Thunk {
6655 let Op::Quantize {
6656 axis,
6657 scales,
6658 zero_points,
6659 } = &node.op
6660 else {
6661 unreachable!()
6662 };
6663 {
6664 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
6665 Thunk::Quantize {
6666 x: node_offset(arena, node.inputs[0]),
6667 q: node_offset(arena, node.id),
6668 len: node.shape.num_elements().unwrap() as u32,
6669 chan_axis: chan_axis as u32,
6670 chan_dim: chan_dim as u32,
6671 inner: inner as u32,
6672 scales: scales.clone(),
6673 zero_points: zero_points.clone(),
6674 }
6675 }
6676}
6677
6678#[allow(unused_variables)]
6679fn compile_fake_quantize(
6680 node: &rlx_ir::Node,
6681 graph: &Graph,
6682 arena: &crate::arena::Arena,
6683 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6684 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6685 rng: rlx_ir::RngOptions,
6686) -> Thunk {
6687 let Op::FakeQuantize {
6688 bits,
6689 axis,
6690 ste,
6691 scale_mode,
6692 } = &node.op
6693 else {
6694 unreachable!()
6695 };
6696 {
6697 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
6698 let state_off = match scale_mode {
6699 rlx_ir::op::ScaleMode::PerBatch => None,
6700 rlx_ir::op::ScaleMode::EMA { .. } | rlx_ir::op::ScaleMode::Fixed => {
6701 debug_assert_eq!(
6703 node.inputs.len(),
6704 2,
6705 "EMA/Fixed FakeQuantize needs a state input"
6706 );
6707 Some(node_offset(arena, node.inputs[1]))
6708 }
6709 };
6710 Thunk::FakeQuantize {
6711 x: node_offset(arena, node.inputs[0]),
6712 out: node_offset(arena, node.id),
6713 len: node.shape.num_elements().unwrap() as u32,
6714 chan_axis: chan_axis as u32,
6715 chan_dim: chan_dim as u32,
6716 inner: inner as u32,
6717 bits: *bits,
6718 ste: *ste,
6719 scale_mode: *scale_mode,
6720 state_off,
6721 }
6722 }
6723}
6724
6725#[allow(unused_variables)]
6726fn compile_fake_quantize_l_s_q(
6727 node: &rlx_ir::Node,
6728 graph: &Graph,
6729 arena: &crate::arena::Arena,
6730 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6731 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6732 rng: rlx_ir::RngOptions,
6733) -> Thunk {
6734 let Op::FakeQuantizeLSQ { bits, axis } = &node.op else {
6735 unreachable!()
6736 };
6737 {
6738 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
6739 Thunk::FakeQuantizeLSQ {
6740 x: node_offset(arena, node.inputs[0]),
6741 scale_off: node_offset(arena, node.inputs[1]),
6742 out: node_offset(arena, node.id),
6743 len: node.shape.num_elements().unwrap() as u32,
6744 chan_axis: chan_axis as u32,
6745 chan_dim: chan_dim as u32,
6746 inner: inner as u32,
6747 bits: *bits,
6748 }
6749 }
6750}
6751
6752#[allow(unused_variables)]
6753fn compile_fake_quantize_l_s_q_backward_x(
6754 node: &rlx_ir::Node,
6755 graph: &Graph,
6756 arena: &crate::arena::Arena,
6757 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6758 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6759 rng: rlx_ir::RngOptions,
6760) -> Thunk {
6761 let Op::FakeQuantizeLSQBackwardX { bits, axis } = &node.op else {
6762 unreachable!()
6763 };
6764 {
6765 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
6766 Thunk::FakeQuantizeLSQBackwardX {
6767 x: node_offset(arena, node.inputs[0]),
6768 scale_off: node_offset(arena, node.inputs[1]),
6769 dy: node_offset(arena, node.inputs[2]),
6770 dx: node_offset(arena, node.id),
6771 len: node.shape.num_elements().unwrap() as u32,
6772 chan_axis: chan_axis as u32,
6773 chan_dim: chan_dim as u32,
6774 inner: inner as u32,
6775 bits: *bits,
6776 }
6777 }
6778}
6779
6780#[allow(unused_variables)]
6781fn compile_fake_quantize_l_s_q_backward_scale(
6782 node: &rlx_ir::Node,
6783 graph: &Graph,
6784 arena: &crate::arena::Arena,
6785 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6786 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6787 rng: rlx_ir::RngOptions,
6788) -> Thunk {
6789 let Op::FakeQuantizeLSQBackwardScale { bits, axis } = &node.op else {
6790 unreachable!()
6791 };
6792 {
6793 let in_shape = &graph.node(node.inputs[0]).shape;
6796 let (chan_axis, chan_dim, inner) = quant_layout(in_shape, *axis);
6797 Thunk::FakeQuantizeLSQBackwardScale {
6798 x: node_offset(arena, node.inputs[0]),
6799 scale_off: node_offset(arena, node.inputs[1]),
6800 dy: node_offset(arena, node.inputs[2]),
6801 dscale: node_offset(arena, node.id),
6802 len: in_shape.num_elements().unwrap() as u32,
6803 chan_axis: chan_axis as u32,
6804 chan_dim: chan_dim as u32,
6805 inner: inner as u32,
6806 bits: *bits,
6807 }
6808 }
6809}
6810
6811#[allow(unused_variables)]
6812fn compile_fake_quantize_backward(
6813 node: &rlx_ir::Node,
6814 graph: &Graph,
6815 arena: &crate::arena::Arena,
6816 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6817 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6818 rng: rlx_ir::RngOptions,
6819) -> Thunk {
6820 let Op::FakeQuantizeBackward { bits, axis, ste } = &node.op else {
6821 unreachable!()
6822 };
6823 {
6824 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
6825 Thunk::FakeQuantizeBackward {
6826 x: node_offset(arena, node.inputs[0]),
6827 dy: node_offset(arena, node.inputs[1]),
6828 dx: node_offset(arena, node.id),
6829 len: node.shape.num_elements().unwrap() as u32,
6830 chan_axis: chan_axis as u32,
6831 chan_dim: chan_dim as u32,
6832 inner: inner as u32,
6833 bits: *bits,
6834 ste: *ste,
6835 }
6836 }
6837}
6838
6839#[allow(unused_variables)]
6840fn compile_dequantize(
6841 node: &rlx_ir::Node,
6842 graph: &Graph,
6843 arena: &crate::arena::Arena,
6844 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6845 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6846 rng: rlx_ir::RngOptions,
6847) -> Thunk {
6848 let Op::Dequantize {
6849 axis,
6850 scales,
6851 zero_points,
6852 } = &node.op
6853 else {
6854 unreachable!()
6855 };
6856 {
6857 let (chan_axis, chan_dim, inner) = quant_layout(&node.shape, *axis);
6858 Thunk::Dequantize {
6859 q: node_offset(arena, node.inputs[0]),
6860 x: node_offset(arena, node.id),
6861 len: node.shape.num_elements().unwrap() as u32,
6862 chan_axis: chan_axis as u32,
6863 chan_dim: chan_dim as u32,
6864 inner: inner as u32,
6865 scales: scales.clone(),
6866 zero_points: zero_points.clone(),
6867 }
6868 }
6869}
6870
6871#[allow(unused_variables)]
6872fn compile_expand(
6873 node: &rlx_ir::Node,
6874 graph: &Graph,
6875 arena: &crate::arena::Arena,
6876 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6877 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6878 rng: rlx_ir::RngOptions,
6879) -> Thunk {
6880 let Op::Expand { .. } = &node.op else {
6881 unreachable!()
6882 };
6883 {
6884 let in_shape = &graph.node(node.inputs[0]).shape;
6889 let out_shape = &node.shape;
6890 let in_rank = in_shape.rank();
6891 let out_rank = out_shape.rank();
6892 let pad = out_rank.saturating_sub(in_rank);
6894 let in_dims: Vec<usize> = (0..out_rank)
6895 .map(|i| {
6896 if i < pad {
6897 1
6898 } else {
6899 in_shape.dim(i - pad).unwrap_static()
6900 }
6901 })
6902 .collect();
6903 let mut in_strides_full = vec![1usize; out_rank];
6905 for d in (0..out_rank.saturating_sub(1)).rev() {
6906 in_strides_full[d] = in_strides_full[d + 1] * in_dims[d + 1];
6907 }
6908 let out_dims: Vec<u32> = (0..out_rank)
6909 .map(|i| out_shape.dim(i).unwrap_static() as u32)
6910 .collect();
6911 let in_strides: Vec<u32> = (0..out_rank)
6913 .map(|i| {
6914 if in_dims[i] == 1 && (out_dims[i] as usize) > 1 {
6915 0
6916 } else {
6917 in_strides_full[i] as u32
6918 }
6919 })
6920 .collect();
6921 let in_total = in_dims.iter().product::<usize>() as u32;
6922 let src = node_offset(arena, node.inputs[0]);
6923 let dst = node_offset(arena, node.id);
6924 let elem_bytes = node.shape.dtype().size_bytes() as u8;
6925 match node.shape.dtype() {
6926 rlx_ir::DType::F64 => Thunk::TransposeF64 {
6927 src,
6928 dst,
6929 in_total,
6930 out_dims,
6931 in_strides,
6932 },
6933 _ => Thunk::Transpose {
6934 src,
6935 dst,
6936 in_total,
6937 out_dims,
6938 in_strides,
6939 elem_bytes,
6940 },
6941 }
6942 }
6943}
6944
6945#[allow(unused_variables)]
6946fn compile_rms_norm(
6947 node: &rlx_ir::Node,
6948 graph: &Graph,
6949 arena: &crate::arena::Arena,
6950 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6951 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6952 rng: rlx_ir::RngOptions,
6953) -> Thunk {
6954 let Op::RmsNorm { eps, .. } = &node.op else {
6955 unreachable!()
6956 };
6957 {
6958 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
6959 let total = node.shape.num_elements().unwrap();
6960 Thunk::RmsNorm {
6961 src: node_offset(arena, node.inputs[0]),
6962 g: node_offset(arena, node.inputs[1]),
6963 b: node_offset(arena, node.inputs[2]),
6964 dst: node_offset(arena, node.id),
6965 rows: (total / h) as u32,
6966 h: h as u32,
6967 eps: *eps,
6968 }
6969 }
6970}
6971
6972#[allow(unused_variables)]
6973fn compile_layer_norm(
6974 node: &rlx_ir::Node,
6975 graph: &Graph,
6976 arena: &crate::arena::Arena,
6977 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
6978 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
6979 rng: rlx_ir::RngOptions,
6980) -> Thunk {
6981 let Op::LayerNorm { eps, .. } = &node.op else {
6982 unreachable!()
6983 };
6984 {
6985 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
6986 let total = node.shape.num_elements().unwrap();
6987 Thunk::LayerNorm {
6988 src: node_offset(arena, node.inputs[0]),
6989 g: node_offset(arena, node.inputs[1]),
6990 b: node_offset(arena, node.inputs[2]),
6991 dst: node_offset(arena, node.id),
6992 rows: (total / h) as u32,
6993 h: h as u32,
6994 eps: *eps,
6995 }
6996 }
6997}
6998
6999#[allow(unused_variables)]
7000fn compile_group_norm(
7001 node: &rlx_ir::Node,
7002 graph: &Graph,
7003 arena: &crate::arena::Arena,
7004 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7005 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7006 rng: rlx_ir::RngOptions,
7007) -> Thunk {
7008 let Op::GroupNorm { num_groups, eps } = &node.op else {
7009 unreachable!()
7010 };
7011 {
7012 let in_shape = &graph.node(node.inputs[0]).shape;
7013 let (n, c, h, w) = conv_nchw_dims(in_shape);
7014 Thunk::GroupNorm {
7015 src: node_offset(arena, node.inputs[0]),
7016 g: node_offset(arena, node.inputs[1]),
7017 b: node_offset(arena, node.inputs[2]),
7018 dst: node_offset(arena, node.id),
7019 n,
7020 c,
7021 h,
7022 w,
7023 num_groups: *num_groups as u32,
7024 eps: *eps,
7025 }
7026 }
7027}
7028
7029#[allow(unused_variables)]
7030fn compile_batch_norm_inference(
7031 node: &rlx_ir::Node,
7032 graph: &Graph,
7033 arena: &crate::arena::Arena,
7034 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7035 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7036 rng: rlx_ir::RngOptions,
7037) -> Thunk {
7038 let Op::BatchNormInference { eps } = &node.op else {
7039 unreachable!()
7040 };
7041 {
7042 let in_shape = &graph.node(node.inputs[0]).shape;
7043 let rank = in_shape.rank();
7044 let channels = in_shape.dim(rank - 1).unwrap_static();
7045 let total = in_shape.num_elements().unwrap_or(0);
7046 let count = (total / channels.max(1)) as u32;
7047 Thunk::BatchNormInference {
7048 src: node_offset(arena, node.inputs[0]),
7049 g: node_offset(arena, node.inputs[1]),
7050 b: node_offset(arena, node.inputs[2]),
7051 mean: node_offset(arena, node.inputs[3]),
7052 var: node_offset(arena, node.inputs[4]),
7053 dst: node_offset(arena, node.id),
7054 count,
7055 channels: channels as u32,
7056 eps: *eps,
7057 }
7058 }
7059}
7060
7061#[allow(unused_variables)]
7062fn compile_batch_norm_inference_backward_input(
7063 node: &rlx_ir::Node,
7064 graph: &Graph,
7065 arena: &crate::arena::Arena,
7066 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7067 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7068 rng: rlx_ir::RngOptions,
7069) -> Thunk {
7070 let Op::BatchNormInferenceBackwardInput { eps } = &node.op else {
7071 unreachable!()
7072 };
7073 {
7074 let x_shape = &graph.node(node.inputs[0]).shape;
7075 let rank = x_shape.rank();
7076 let channels = x_shape.dim(rank - 1).unwrap_static();
7077 let total = x_shape.num_elements().unwrap_or(0);
7078 Thunk::BatchNormInferenceBackwardInput {
7079 x: node_offset(arena, node.inputs[0]),
7080 gamma: node_offset(arena, node.inputs[1]),
7081 mean: node_offset(arena, node.inputs[2]),
7082 var: node_offset(arena, node.inputs[3]),
7083 dy: node_offset(arena, node.inputs[4]),
7084 dx: node_offset(arena, node.id),
7085 count: (total / channels.max(1)) as u32,
7086 channels: channels as u32,
7087 eps: *eps,
7088 }
7089 }
7090}
7091
7092#[allow(unused_variables)]
7093fn compile_batch_norm_inference_backward_gamma(
7094 node: &rlx_ir::Node,
7095 graph: &Graph,
7096 arena: &crate::arena::Arena,
7097 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7098 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7099 rng: rlx_ir::RngOptions,
7100) -> Thunk {
7101 let Op::BatchNormInferenceBackwardGamma { eps } = &node.op else {
7102 unreachable!()
7103 };
7104 {
7105 let x_shape = &graph.node(node.inputs[0]).shape;
7106 let rank = x_shape.rank();
7107 let channels = x_shape.dim(rank - 1).unwrap_static();
7108 let total = x_shape.num_elements().unwrap_or(0);
7109 let _gamma_shape = &graph.node(node.id).shape;
7110 Thunk::BatchNormInferenceBackwardGamma {
7111 x: node_offset(arena, node.inputs[0]),
7112 mean: node_offset(arena, node.inputs[1]),
7113 var: node_offset(arena, node.inputs[2]),
7114 dy: node_offset(arena, node.inputs[3]),
7115 dgamma: node_offset(arena, node.id),
7116 count: (total / channels.max(1)) as u32,
7117 channels: channels as u32,
7118 eps: *eps,
7119 }
7120 }
7121}
7122
7123#[allow(unused_variables)]
7124fn compile_batch_norm_inference_backward_beta(
7125 node: &rlx_ir::Node,
7126 graph: &Graph,
7127 arena: &crate::arena::Arena,
7128 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7129 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7130 rng: rlx_ir::RngOptions,
7131) -> Thunk {
7132 let Op::BatchNormInferenceBackwardBeta = &node.op else {
7133 unreachable!()
7134 };
7135 {
7136 let dy_shape = &graph.node(node.inputs[0]).shape;
7137 let rank = dy_shape.rank();
7138 let channels = dy_shape.dim(rank - 1).unwrap_static();
7139 let total = dy_shape.num_elements().unwrap_or(0);
7140 Thunk::BatchNormInferenceBackwardBeta {
7141 dy: node_offset(arena, node.inputs[0]),
7142 dbeta: node_offset(arena, node.id),
7143 count: (total / channels.max(1)) as u32,
7144 channels: channels as u32,
7145 }
7146 }
7147}
7148
7149#[allow(unused_variables)]
7150fn compile_layer_norm2d(
7151 node: &rlx_ir::Node,
7152 graph: &Graph,
7153 arena: &crate::arena::Arena,
7154 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7155 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7156 rng: rlx_ir::RngOptions,
7157) -> Thunk {
7158 let Op::LayerNorm2d { eps } = &node.op else {
7159 unreachable!()
7160 };
7161 {
7162 let in_shape = &graph.node(node.inputs[0]).shape;
7163 let (n, c, h, w) = conv_nchw_dims(in_shape);
7164 Thunk::LayerNorm2d {
7165 src: node_offset(arena, node.inputs[0]),
7166 g: node_offset(arena, node.inputs[1]),
7167 b: node_offset(arena, node.inputs[2]),
7168 dst: node_offset(arena, node.id),
7169 n,
7170 c,
7171 h,
7172 w,
7173 eps: *eps,
7174 }
7175 }
7176}
7177
7178#[allow(unused_variables)]
7179fn compile_conv_transpose2d(
7180 node: &rlx_ir::Node,
7181 graph: &Graph,
7182 arena: &crate::arena::Arena,
7183 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7184 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7185 rng: rlx_ir::RngOptions,
7186) -> Thunk {
7187 let Op::ConvTranspose2d {
7188 kernel_size,
7189 stride,
7190 padding,
7191 dilation,
7192 output_padding: _,
7193 groups,
7194 } = &node.op
7195 else {
7196 unreachable!()
7197 };
7198 {
7199 let in_shape = &graph.node(node.inputs[0]).shape;
7200 let out_shape = &node.shape;
7201 let (n, c_in, h, w_in) = conv_nchw_dims(in_shape);
7202 let (_, c_out, h_out, w_out) = conv_nchw_dims(out_shape);
7203 Thunk::ConvTranspose2d {
7204 src: node_offset(arena, node.inputs[0]),
7205 weight: node_offset(arena, node.inputs[1]),
7206 dst: node_offset(arena, node.id),
7207 n,
7208 c_in,
7209 h,
7210 w_in,
7211 c_out,
7212 h_out,
7213 w_out,
7214 kh: kernel_size[0] as u32,
7215 kw: kernel_size[1] as u32,
7216 sh: stride.first().copied().unwrap_or(1) as u32,
7217 sw: stride.get(1).copied().unwrap_or(1) as u32,
7218 ph: padding.first().copied().unwrap_or(0) as u32,
7219 pw: padding.get(1).copied().unwrap_or(0) as u32,
7220 dh: dilation.first().copied().unwrap_or(1) as u32,
7221 dw: dilation.get(1).copied().unwrap_or(1) as u32,
7222 groups: *groups as u32,
7223 }
7224 }
7225}
7226
7227#[allow(unused_variables)]
7228fn compile_resize_nearest2x(
7229 node: &rlx_ir::Node,
7230 graph: &Graph,
7231 arena: &crate::arena::Arena,
7232 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7233 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7234 rng: rlx_ir::RngOptions,
7235) -> Thunk {
7236 let Op::ResizeNearest2x = &node.op else {
7237 unreachable!()
7238 };
7239 {
7240 let in_shape = &graph.node(node.inputs[0]).shape;
7241 let (n, c, h, w) = conv_nchw_dims(in_shape);
7242 Thunk::ResizeNearest2x {
7243 src: node_offset(arena, node.inputs[0]),
7244 dst: node_offset(arena, node.id),
7245 n,
7246 c,
7247 h,
7248 w,
7249 }
7250 }
7251}
7252
7253#[allow(unused_variables)]
7254fn compile_axial_rope2d(
7255 node: &rlx_ir::Node,
7256 graph: &Graph,
7257 arena: &crate::arena::Arena,
7258 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7259 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7260 rng: rlx_ir::RngOptions,
7261) -> Thunk {
7262 let Op::AxialRope2d {
7263 end_x,
7264 end_y,
7265 head_dim,
7266 num_heads,
7267 theta,
7268 repeat_factor,
7269 } = &node.op
7270 else {
7271 unreachable!()
7272 };
7273 {
7274 let in_shape = &graph.node(node.inputs[0]).shape;
7275 let batch = in_shape.dim(0).unwrap_static() as u32;
7276 let seq = in_shape.dim(1).unwrap_static() as u32;
7277 let hidden = in_shape.dim(2).unwrap_static() as u32;
7278 Thunk::AxialRope2d {
7279 src: node_offset(arena, node.inputs[0]),
7280 dst: node_offset(arena, node.id),
7281 batch,
7282 seq,
7283 hidden,
7284 end_x: *end_x as u32,
7285 end_y: *end_y as u32,
7286 head_dim: *head_dim as u32,
7287 num_heads: *num_heads as u32,
7288 theta: *theta,
7289 repeat_factor: *repeat_factor as u32,
7290 }
7291 }
7292}
7293
7294#[allow(unused_variables)]
7295fn compile_selective_scan(
7296 node: &rlx_ir::Node,
7297 graph: &Graph,
7298 arena: &crate::arena::Arena,
7299 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7300 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7301 rng: rlx_ir::RngOptions,
7302) -> Thunk {
7303 let Op::SelectiveScan { state_size } = &node.op else {
7304 unreachable!()
7305 };
7306 {
7307 let in_shape = &graph.node(node.inputs[0]).shape;
7308 let (batch, seq, hidden) = (
7309 in_shape.dim(0).unwrap_static(),
7310 in_shape.dim(1).unwrap_static(),
7311 in_shape.dim(2).unwrap_static(),
7312 );
7313 Thunk::SelectiveScan {
7314 x: node_offset(arena, node.inputs[0]),
7315 delta: node_offset(arena, node.inputs[1]),
7316 a: node_offset(arena, node.inputs[2]),
7317 b: node_offset(arena, node.inputs[3]),
7318 c: node_offset(arena, node.inputs[4]),
7319 dst: node_offset(arena, node.id),
7320 batch: batch as u32,
7321 seq: seq as u32,
7322 hidden: hidden as u32,
7323 state_size: *state_size as u32,
7324 }
7325 }
7326}
7327
7328#[allow(unused_variables)]
7329fn compile_gated_delta_net(
7330 node: &rlx_ir::Node,
7331 graph: &Graph,
7332 arena: &crate::arena::Arena,
7333 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7334 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7335 rng: rlx_ir::RngOptions,
7336) -> Thunk {
7337 let Op::GatedDeltaNet {
7338 state_size,
7339 carry_state,
7340 } = &node.op
7341 else {
7342 unreachable!()
7343 };
7344 {
7345 let q_shape = &graph.node(node.inputs[0]).shape;
7346 let (batch, seq, heads) = (
7347 q_shape.dim(0).unwrap_static(),
7348 q_shape.dim(1).unwrap_static(),
7349 q_shape.dim(2).unwrap_static(),
7350 );
7351 let state_off = if *carry_state {
7352 node_offset(arena, node.inputs[5])
7353 } else {
7354 0
7355 };
7356 Thunk::GatedDeltaNet {
7357 q: node_offset(arena, node.inputs[0]),
7358 k: node_offset(arena, node.inputs[1]),
7359 v: node_offset(arena, node.inputs[2]),
7360 g: node_offset(arena, node.inputs[3]),
7361 beta: node_offset(arena, node.inputs[4]),
7362 state: state_off,
7363 dst: node_offset(arena, node.id),
7364 batch: batch as u32,
7365 seq: seq as u32,
7366 heads: heads as u32,
7367 state_size: *state_size as u32,
7368 }
7369 }
7370}
7371
7372#[allow(unused_variables)]
7373fn compile_lstm(
7374 node: &rlx_ir::Node,
7375 graph: &Graph,
7376 arena: &crate::arena::Arena,
7377 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7378 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7379 rng: rlx_ir::RngOptions,
7380) -> Thunk {
7381 let Op::Lstm {
7382 hidden_size,
7383 num_layers,
7384 bidirectional,
7385 carry,
7386 } = &node.op
7387 else {
7388 unreachable!()
7389 };
7390 {
7391 let x_shape = &graph.node(node.inputs[0]).shape;
7392 let (batch, seq, input_size) = (
7393 x_shape.dim(0).unwrap_static(),
7394 x_shape.dim(1).unwrap_static(),
7395 x_shape.dim(2).unwrap_static(),
7396 );
7397 let (h0, c0) = if *carry {
7398 (
7399 node_offset(arena, node.inputs[4]),
7400 node_offset(arena, node.inputs[5]),
7401 )
7402 } else {
7403 (0, 0)
7404 };
7405 Thunk::Lstm {
7406 x: node_offset(arena, node.inputs[0]),
7407 w_ih: node_offset(arena, node.inputs[1]),
7408 w_hh: node_offset(arena, node.inputs[2]),
7409 bias: node_offset(arena, node.inputs[3]),
7410 h0,
7411 c0,
7412 dst: node_offset(arena, node.id),
7413 batch: batch as u32,
7414 seq: seq as u32,
7415 input_size: input_size as u32,
7416 hidden: *hidden_size as u32,
7417 num_layers: *num_layers as u32,
7418 bidirectional: *bidirectional,
7419 carry: *carry,
7420 }
7421 }
7422}
7423
7424#[allow(unused_variables)]
7425fn compile_gru(
7426 node: &rlx_ir::Node,
7427 graph: &Graph,
7428 arena: &crate::arena::Arena,
7429 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7430 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7431 rng: rlx_ir::RngOptions,
7432) -> Thunk {
7433 let Op::Gru {
7434 hidden_size,
7435 num_layers,
7436 bidirectional,
7437 carry,
7438 } = &node.op
7439 else {
7440 unreachable!()
7441 };
7442 {
7443 let x_shape = &graph.node(node.inputs[0]).shape;
7444 let (batch, seq, input_size) = (
7445 x_shape.dim(0).unwrap_static(),
7446 x_shape.dim(1).unwrap_static(),
7447 x_shape.dim(2).unwrap_static(),
7448 );
7449 let h0 = if *carry {
7451 node_offset(arena, node.inputs[5])
7452 } else {
7453 0
7454 };
7455 Thunk::Gru {
7456 x: node_offset(arena, node.inputs[0]),
7457 w_ih: node_offset(arena, node.inputs[1]),
7458 w_hh: node_offset(arena, node.inputs[2]),
7459 b_ih: node_offset(arena, node.inputs[3]),
7460 b_hh: node_offset(arena, node.inputs[4]),
7461 h0,
7462 dst: node_offset(arena, node.id),
7463 batch: batch as u32,
7464 seq: seq as u32,
7465 input_size: input_size as u32,
7466 hidden: *hidden_size as u32,
7467 num_layers: *num_layers as u32,
7468 bidirectional: *bidirectional,
7469 carry: *carry,
7470 }
7471 }
7472}
7473
7474#[allow(unused_variables)]
7475fn compile_rnn(
7476 node: &rlx_ir::Node,
7477 graph: &Graph,
7478 arena: &crate::arena::Arena,
7479 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7480 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7481 rng: rlx_ir::RngOptions,
7482) -> Thunk {
7483 let Op::Rnn {
7484 hidden_size,
7485 num_layers,
7486 bidirectional,
7487 carry,
7488 relu,
7489 } = &node.op
7490 else {
7491 unreachable!()
7492 };
7493 {
7494 let x_shape = &graph.node(node.inputs[0]).shape;
7495 let (batch, seq, input_size) = (
7496 x_shape.dim(0).unwrap_static(),
7497 x_shape.dim(1).unwrap_static(),
7498 x_shape.dim(2).unwrap_static(),
7499 );
7500 let h0 = if *carry {
7502 node_offset(arena, node.inputs[4])
7503 } else {
7504 0
7505 };
7506 Thunk::Rnn {
7507 x: node_offset(arena, node.inputs[0]),
7508 w_ih: node_offset(arena, node.inputs[1]),
7509 w_hh: node_offset(arena, node.inputs[2]),
7510 bias: node_offset(arena, node.inputs[3]),
7511 h0,
7512 dst: node_offset(arena, node.id),
7513 batch: batch as u32,
7514 seq: seq as u32,
7515 input_size: input_size as u32,
7516 hidden: *hidden_size as u32,
7517 num_layers: *num_layers as u32,
7518 bidirectional: *bidirectional,
7519 carry: *carry,
7520 relu: *relu,
7521 }
7522 }
7523}
7524
7525#[allow(unused_variables)]
7526fn compile_mamba2(
7527 node: &rlx_ir::Node,
7528 graph: &Graph,
7529 arena: &crate::arena::Arena,
7530 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7531 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7532 rng: rlx_ir::RngOptions,
7533) -> Thunk {
7534 let Op::Mamba2 {
7535 head_dim,
7536 state_size,
7537 } = &node.op
7538 else {
7539 unreachable!()
7540 };
7541 {
7542 let x_shape = &graph.node(node.inputs[0]).shape;
7544 Thunk::Mamba2 {
7545 x: node_offset(arena, node.inputs[0]),
7546 dt: node_offset(arena, node.inputs[1]),
7547 a: node_offset(arena, node.inputs[2]),
7548 b: node_offset(arena, node.inputs[3]),
7549 c: node_offset(arena, node.inputs[4]),
7550 dst: node_offset(arena, node.id),
7551 batch: x_shape.dim(0).unwrap_static() as u32,
7552 seq: x_shape.dim(1).unwrap_static() as u32,
7553 heads: x_shape.dim(2).unwrap_static() as u32,
7554 head_dim: *head_dim as u32,
7555 state_size: *state_size as u32,
7556 }
7557 }
7558}
7559
7560#[allow(unused_variables)]
7561fn compile_q_mat_mul(
7562 node: &rlx_ir::Node,
7563 graph: &Graph,
7564 arena: &crate::arena::Arena,
7565 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7566 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7567 rng: rlx_ir::RngOptions,
7568) -> Thunk {
7569 let Op::QMatMul {
7570 x_zp,
7571 w_zp,
7572 out_zp,
7573 mult,
7574 } = &node.op
7575 else {
7576 unreachable!()
7577 };
7578 {
7579 let x_shape = &graph.node(node.inputs[0]).shape;
7580 let w_shape = &graph.node(node.inputs[1]).shape;
7581 let m = x_shape.dim(0).unwrap_static();
7582 let k = x_shape.dim(1).unwrap_static();
7583 let n = w_shape.dim(1).unwrap_static();
7584 Thunk::QMatMul {
7585 x: node_offset(arena, node.inputs[0]),
7586 w: node_offset(arena, node.inputs[1]),
7587 bias: node_offset(arena, node.inputs[2]),
7588 out: node_offset(arena, node.id),
7589 m: m as u32,
7590 k: k as u32,
7591 n: n as u32,
7592 x_zp: *x_zp,
7593 w_zp: *w_zp,
7594 out_zp: *out_zp,
7595 mult: *mult,
7596 }
7597 }
7598}
7599
7600#[allow(unused_variables)]
7601fn compile_q_conv2d(
7602 node: &rlx_ir::Node,
7603 graph: &Graph,
7604 arena: &crate::arena::Arena,
7605 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7606 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7607 rng: rlx_ir::RngOptions,
7608) -> Thunk {
7609 let Op::QConv2d {
7610 kernel_size,
7611 stride,
7612 padding,
7613 dilation,
7614 groups,
7615 x_zp,
7616 w_zp,
7617 out_zp,
7618 mult,
7619 } = &node.op
7620 else {
7621 unreachable!()
7622 };
7623 {
7624 let in_shape = &graph.node(node.inputs[0]).shape;
7625 let w_shape = &graph.node(node.inputs[1]).shape;
7626 let out_shape = &node.shape;
7627 if kernel_size.len() == 2
7628 && in_shape.rank() == 4
7629 && w_shape.rank() == 4
7630 && out_shape.rank() == 4
7631 {
7632 Thunk::QConv2d {
7633 x: node_offset(arena, node.inputs[0]),
7634 w: node_offset(arena, node.inputs[1]),
7635 bias: node_offset(arena, node.inputs[2]),
7636 out: node_offset(arena, node.id),
7637 n: in_shape.dim(0).unwrap_static() as u32,
7638 c_in: in_shape.dim(1).unwrap_static() as u32,
7639 h: in_shape.dim(2).unwrap_static() as u32,
7640 w_in: in_shape.dim(3).unwrap_static() as u32,
7641 c_out: out_shape.dim(1).unwrap_static() as u32,
7642 h_out: out_shape.dim(2).unwrap_static() as u32,
7643 w_out: out_shape.dim(3).unwrap_static() as u32,
7644 kh: kernel_size[0] as u32,
7645 kw: kernel_size[1] as u32,
7646 sh: stride.first().copied().unwrap_or(1) as u32,
7647 sw: stride.get(1).copied().unwrap_or(1) as u32,
7648 ph: padding.first().copied().unwrap_or(0) as u32,
7649 pw: padding.get(1).copied().unwrap_or(0) as u32,
7650 dh: dilation.first().copied().unwrap_or(1) as u32,
7651 dw: dilation.get(1).copied().unwrap_or(1) as u32,
7652 groups: *groups as u32,
7653 x_zp: *x_zp,
7654 w_zp: *w_zp,
7655 out_zp: *out_zp,
7656 mult: *mult,
7657 }
7658 } else {
7659 Thunk::Nop
7660 }
7661 }
7662}
7663
7664#[allow(unused_variables)]
7665fn compile_dequant_mat_mul(
7666 node: &rlx_ir::Node,
7667 graph: &Graph,
7668 arena: &crate::arena::Arena,
7669 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7670 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7671 rng: rlx_ir::RngOptions,
7672) -> Thunk {
7673 let Op::DequantMatMul { scheme } = &node.op else {
7674 unreachable!()
7675 };
7676 {
7677 use rlx_ir::quant::QuantScheme;
7678 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
7679 let total = node.shape.num_elements().unwrap();
7680 let m = total / n.max(1);
7681 let x_total = graph.node(node.inputs[0]).shape.num_elements().unwrap();
7682 let k = x_total / m.max(1);
7683 if scheme.is_gguf() {
7684 Thunk::DequantMatMulGguf {
7685 x: node_offset(arena, node.inputs[0]),
7686 w_q: node_offset(arena, node.inputs[1]),
7687 dst: node_offset(arena, node.id),
7688 m: m as u32,
7689 k: k as u32,
7690 n: n as u32,
7691 scheme: *scheme,
7692 }
7693 } else {
7694 match scheme {
7695 QuantScheme::Nvfp4Block => Thunk::DequantMatMulNvfp4 {
7696 x: node_offset(arena, node.inputs[0]),
7697 w_q: node_offset(arena, node.inputs[1]),
7698 scale: node_offset(arena, node.inputs[2]),
7699 global_scale: node_offset(arena, node.inputs[3]),
7700 dst: node_offset(arena, node.id),
7701 m: m as u32,
7702 k: k as u32,
7703 n: n as u32,
7704 },
7705 QuantScheme::Int4Block { block_size } => Thunk::DequantMatMulInt4 {
7706 x: node_offset(arena, node.inputs[0]),
7707 w_q: node_offset(arena, node.inputs[1]),
7708 scale: node_offset(arena, node.inputs[2]),
7709 zp: node_offset(arena, node.inputs[3]),
7710 dst: node_offset(arena, node.id),
7711 m: m as u32,
7712 k: k as u32,
7713 n: n as u32,
7714 block_size: *block_size,
7715 is_asymmetric: false,
7716 },
7717 QuantScheme::Fp8E4m3 => Thunk::DequantMatMulFp8 {
7718 x: node_offset(arena, node.inputs[0]),
7719 w_q: node_offset(arena, node.inputs[1]),
7720 scale: node_offset(arena, node.inputs[2]),
7721 dst: node_offset(arena, node.id),
7722 m: m as u32,
7723 k: k as u32,
7724 n: n as u32,
7725 e5m2: false,
7726 },
7727 QuantScheme::Fp8E5m2 => Thunk::DequantMatMulFp8 {
7728 x: node_offset(arena, node.inputs[0]),
7729 w_q: node_offset(arena, node.inputs[1]),
7730 scale: node_offset(arena, node.inputs[2]),
7731 dst: node_offset(arena, node.id),
7732 m: m as u32,
7733 k: k as u32,
7734 n: n as u32,
7735 e5m2: true,
7736 },
7737 QuantScheme::Int8Block { block_size } => Thunk::DequantMatMul {
7738 x: node_offset(arena, node.inputs[0]),
7739 w_q: node_offset(arena, node.inputs[1]),
7740 scale: node_offset(arena, node.inputs[2]),
7741 zp: node_offset(arena, node.inputs[3]),
7742 dst: node_offset(arena, node.id),
7743 m: m as u32,
7744 k: k as u32,
7745 n: n as u32,
7746 block_size: *block_size,
7747 is_asymmetric: false,
7748 },
7749 QuantScheme::Int8BlockAsym { block_size } => Thunk::DequantMatMul {
7750 x: node_offset(arena, node.inputs[0]),
7751 w_q: node_offset(arena, node.inputs[1]),
7752 scale: node_offset(arena, node.inputs[2]),
7753 zp: node_offset(arena, node.inputs[3]),
7754 dst: node_offset(arena, node.id),
7755 m: m as u32,
7756 k: k as u32,
7757 n: n as u32,
7758 block_size: *block_size,
7759 is_asymmetric: true,
7760 },
7761 other => panic!(
7762 "DequantMatMul on CPU supports Int8/Int4/FP8/NVFP4 legacy or GGUF schemes; got {other}"
7763 ),
7764 }
7765 }
7766 }
7767}
7768
7769#[allow(unused_variables)]
7770fn compile_scaled_mat_mul(
7771 node: &rlx_ir::Node,
7772 graph: &Graph,
7773 arena: &crate::arena::Arena,
7774 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7775 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7776 rng: rlx_ir::RngOptions,
7777) -> Thunk {
7778 let Op::ScaledMatMul {
7779 lhs_format,
7780 rhs_format,
7781 scale_layout,
7782 has_bias,
7783 } = &node.op
7784 else {
7785 unreachable!()
7786 };
7787 {
7788 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
7790 let total = node.shape.num_elements().unwrap();
7791 let m = total / n.max(1);
7792 let lhs_total = graph.node(node.inputs[0]).shape.num_elements().unwrap();
7793 let k = lhs_total / m.max(1);
7794 Thunk::ScaledMatMul {
7795 lhs: node_offset(arena, node.inputs[0]),
7796 rhs: node_offset(arena, node.inputs[1]),
7797 lhs_scale: node_offset(arena, node.inputs[2]),
7798 rhs_scale: node_offset(arena, node.inputs[3]),
7799 bias: if *has_bias {
7800 node_offset(arena, node.inputs[4])
7801 } else {
7802 0
7803 },
7804 dst: node_offset(arena, node.id),
7805 m: m as u32,
7806 k: k as u32,
7807 n: n as u32,
7808 lhs_fmt: *lhs_format,
7809 rhs_fmt: *rhs_format,
7810 layout: *scale_layout,
7811 has_bias: *has_bias,
7812 }
7813 }
7814}
7815
7816#[allow(unused_variables)]
7817fn compile_scaled_quantize(
7818 node: &rlx_ir::Node,
7819 graph: &Graph,
7820 arena: &crate::arena::Arena,
7821 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7822 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7823 rng: rlx_ir::RngOptions,
7824) -> Thunk {
7825 let Op::ScaledQuantize {
7826 format,
7827 scale_layout,
7828 } = &node.op
7829 else {
7830 unreachable!()
7831 };
7832 {
7833 let xs = &graph.node(node.inputs[0]).shape;
7834 let cols = xs.dim(xs.rank() - 1).unwrap_static();
7835 let rows = xs.num_elements().unwrap() / cols.max(1);
7836 Thunk::ScaledQuantize {
7837 x: node_offset(arena, node.inputs[0]),
7838 scale: node_offset(arena, node.inputs[1]),
7839 dst: node_offset(arena, node.id),
7840 rows: rows as u32,
7841 cols: cols as u32,
7842 fmt: *format,
7843 layout: *scale_layout,
7844 }
7845 }
7846}
7847
7848#[allow(unused_variables)]
7849fn compile_scaled_quant_scale(
7850 node: &rlx_ir::Node,
7851 graph: &Graph,
7852 arena: &crate::arena::Arena,
7853 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7854 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7855 rng: rlx_ir::RngOptions,
7856) -> Thunk {
7857 let Op::ScaledQuantScale {
7858 format,
7859 scale_layout,
7860 } = &node.op
7861 else {
7862 unreachable!()
7863 };
7864 {
7865 let xs = &graph.node(node.inputs[0]).shape;
7866 let cols = xs.dim(xs.rank() - 1).unwrap_static();
7867 let rows = xs.num_elements().unwrap() / cols.max(1);
7868 Thunk::ScaledQuantScale {
7869 x: node_offset(arena, node.inputs[0]),
7870 dst: node_offset(arena, node.id),
7871 rows: rows as u32,
7872 cols: cols as u32,
7873 fmt: *format,
7874 layout: *scale_layout,
7875 }
7876 }
7877}
7878
7879#[allow(unused_variables)]
7880fn compile_scaled_dequantize(
7881 node: &rlx_ir::Node,
7882 graph: &Graph,
7883 arena: &crate::arena::Arena,
7884 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7885 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7886 rng: rlx_ir::RngOptions,
7887) -> Thunk {
7888 let Op::ScaledDequantize {
7889 format,
7890 scale_layout,
7891 } = &node.op
7892 else {
7893 unreachable!()
7894 };
7895 {
7896 let xs = &graph.node(node.inputs[0]).shape;
7897 let cols = xs.dim(xs.rank() - 1).unwrap_static();
7898 let rows = xs.num_elements().unwrap() / cols.max(1);
7899 Thunk::ScaledDequantize {
7900 codes: node_offset(arena, node.inputs[0]),
7901 scale: node_offset(arena, node.inputs[1]),
7902 dst: node_offset(arena, node.id),
7903 rows: rows as u32,
7904 cols: cols as u32,
7905 fmt: *format,
7906 layout: *scale_layout,
7907 }
7908 }
7909}
7910
7911#[allow(unused_variables)]
7912fn compile_lora_mat_mul(
7913 node: &rlx_ir::Node,
7914 graph: &Graph,
7915 arena: &crate::arena::Arena,
7916 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7917 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7918 rng: rlx_ir::RngOptions,
7919) -> Thunk {
7920 let Op::LoraMatMul { scale } = &node.op else {
7921 unreachable!()
7922 };
7923 {
7924 let n = node.shape.dim(node.shape.rank() - 1).unwrap_static();
7926 let total = node.shape.num_elements().unwrap();
7927 let m = total / n.max(1);
7928 let x_total = graph.node(node.inputs[0]).shape.num_elements().unwrap();
7929 let k = x_total / m.max(1);
7930 let a_total = graph.node(node.inputs[2]).shape.num_elements().unwrap();
7931 let r = a_total / k.max(1);
7932 Thunk::LoraMatMul {
7933 x: node_offset(arena, node.inputs[0]),
7934 w: node_offset(arena, node.inputs[1]),
7935 a: node_offset(arena, node.inputs[2]),
7936 b: node_offset(arena, node.inputs[3]),
7937 dst: node_offset(arena, node.id),
7938 m: m as u32,
7939 k: k as u32,
7940 n: n as u32,
7941 r: r as u32,
7942 scale: *scale,
7943 }
7944 }
7945}
7946
7947#[allow(unused_variables)]
7948fn compile_sample(
7949 node: &rlx_ir::Node,
7950 graph: &Graph,
7951 arena: &crate::arena::Arena,
7952 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7953 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7954 rng: rlx_ir::RngOptions,
7955) -> Thunk {
7956 let Op::Sample {
7957 top_k,
7958 top_p,
7959 temperature,
7960 seed,
7961 } = &node.op
7962 else {
7963 unreachable!()
7964 };
7965 {
7966 let in_shape = &graph.node(node.inputs[0]).shape;
7967 let (batch, vocab) = if in_shape.rank() >= 2 {
7969 (
7970 in_shape.dim(0).unwrap_static(),
7971 in_shape.dim(in_shape.rank() - 1).unwrap_static(),
7972 )
7973 } else {
7974 (1, in_shape.num_elements().unwrap_or(0))
7975 };
7976 Thunk::Sample {
7977 logits: node_offset(arena, node.inputs[0]),
7978 dst: node_offset(arena, node.id),
7979 batch: batch as u32,
7980 vocab: vocab as u32,
7981 top_k: *top_k as u32,
7982 top_p: *top_p,
7983 temperature: *temperature,
7984 seed: *seed,
7985 }
7986 }
7987}
7988
7989#[allow(unused_variables)]
7990fn compile_rng_normal(
7991 node: &rlx_ir::Node,
7992 graph: &Graph,
7993 arena: &crate::arena::Arena,
7994 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
7995 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
7996 rng: rlx_ir::RngOptions,
7997) -> Thunk {
7998 let Op::RngNormal {
7999 mean,
8000 scale,
8001 key,
8002 op_seed,
8003 } = &node.op
8004 else {
8005 unreachable!()
8006 };
8007 Thunk::RngNormal {
8008 dst: node_offset(arena, node.id),
8009 len: node.shape.num_elements().unwrap_or(0) as u32,
8010 mean: *mean,
8011 scale: *scale,
8012 key: *key,
8013 op_seed: *op_seed,
8014 }
8015}
8016
8017#[allow(unused_variables)]
8018fn compile_rng_uniform(
8019 node: &rlx_ir::Node,
8020 graph: &Graph,
8021 arena: &crate::arena::Arena,
8022 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8023 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8024 rng: rlx_ir::RngOptions,
8025) -> Thunk {
8026 let Op::RngUniform {
8027 low,
8028 high,
8029 key,
8030 op_seed,
8031 } = &node.op
8032 else {
8033 unreachable!()
8034 };
8035 Thunk::RngUniform {
8036 dst: node_offset(arena, node.id),
8037 len: node.shape.num_elements().unwrap_or(0) as u32,
8038 low: *low,
8039 high: *high,
8040 key: *key,
8041 op_seed: *op_seed,
8042 }
8043}
8044
8045#[allow(unused_variables)]
8046fn compile_cumsum(
8047 node: &rlx_ir::Node,
8048 graph: &Graph,
8049 arena: &crate::arena::Arena,
8050 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8051 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8052 rng: rlx_ir::RngOptions,
8053) -> Thunk {
8054 let Op::Cumsum { axis, exclusive } = &node.op else {
8055 unreachable!()
8056 };
8057 {
8058 let rank = node.shape.rank();
8063 let ax = if *axis < 0 {
8064 (rank as i32 + axis) as usize
8065 } else {
8066 *axis as usize
8067 };
8068 assert_eq!(
8069 ax,
8070 rank - 1,
8071 "Cumsum only supports the last axis on CPU today"
8072 );
8073 let cols = node.shape.dim(ax).unwrap_static();
8074 let total = node.shape.num_elements().unwrap();
8075 Thunk::Cumsum {
8076 src: node_offset(arena, node.inputs[0]),
8077 dst: node_offset(arena, node.id),
8078 rows: (total / cols) as u32,
8079 cols: cols as u32,
8080 exclusive: *exclusive,
8081 }
8082 }
8083}
8084
8085#[allow(unused_variables)]
8086fn compile_attention(
8087 node: &rlx_ir::Node,
8088 graph: &Graph,
8089 arena: &crate::arena::Arena,
8090 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8091 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8092 rng: rlx_ir::RngOptions,
8093) -> Thunk {
8094 let Op::Attention {
8095 num_heads,
8096 head_dim,
8097 mask_kind,
8098 score_scale,
8099 attn_logit_softcap,
8100 } = &node.op
8101 else {
8102 unreachable!()
8103 };
8104 {
8105 let q_shape = &graph.node(node.inputs[0]).shape;
8111 let k_shape = &graph.node(node.inputs[1]).shape;
8112 let rank = q_shape.rank();
8113 let (batch, seq, kv_seq, bhsd) = if rank == 4 {
8114 let d1 = q_shape.dim(1).unwrap_static();
8115 let d2 = q_shape.dim(2).unwrap_static();
8116 if d1 == *num_heads {
8117 (
8119 q_shape.dim(0).unwrap_static(),
8120 d2,
8121 k_shape.dim(2).unwrap_static(),
8122 true,
8123 )
8124 } else {
8125 (
8127 q_shape.dim(0).unwrap_static(),
8128 d1,
8129 k_shape.dim(1).unwrap_static(),
8130 false,
8131 )
8132 }
8133 } else if rank >= 3 {
8134 (
8135 q_shape.dim(0).unwrap_static(),
8136 q_shape.dim(1).unwrap_static(),
8137 k_shape.dim(1).unwrap_static(),
8138 false,
8139 )
8140 } else {
8141 (
8142 1,
8143 q_shape.dim(0).unwrap_static(),
8144 k_shape.dim(0).unwrap_static(),
8145 false,
8146 )
8147 };
8148 let mask_off = if matches!(
8149 mask_kind,
8150 rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias
8151 ) {
8152 node_offset(arena, node.inputs[3])
8153 } else {
8154 0
8155 };
8156 let hs = (*num_heads * *head_dim) as u32;
8157 let k_numel = k_shape
8161 .num_elements()
8162 .unwrap_or(batch * kv_seq * *num_heads * *head_dim);
8163 let nkv = (k_numel / (batch.max(1) * kv_seq.max(1) * (*head_dim).max(1))).max(1) as u32;
8164 let kv_hs = nkv * *head_dim as u32;
8165 Thunk::Attention {
8166 q: node_offset(arena, node.inputs[0]),
8167 k: node_offset(arena, node.inputs[1]),
8168 v: node_offset(arena, node.inputs[2]),
8169 mask: mask_off,
8170 out: node_offset(arena, node.id),
8171 batch: batch as u32,
8172 seq: seq as u32,
8173 kv_seq: kv_seq as u32,
8174 heads: *num_heads as u32,
8175 kv_heads: nkv,
8176 head_dim: *head_dim as u32,
8177 mask_kind: *mask_kind,
8178 scale: score_scale.unwrap_or((*head_dim as f32).powf(-0.5)),
8179 softcap: attn_logit_softcap.unwrap_or(0.0),
8180 q_row_stride: hs,
8184 k_row_stride: kv_hs,
8185 v_row_stride: kv_hs,
8186 bhsd,
8187 }
8188 }
8189}
8190
8191#[allow(unused_variables)]
8192fn compile_attention_backward(
8193 node: &rlx_ir::Node,
8194 graph: &Graph,
8195 arena: &crate::arena::Arena,
8196 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8197 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8198 rng: rlx_ir::RngOptions,
8199) -> Thunk {
8200 let Op::AttentionBackward {
8201 num_heads,
8202 head_dim,
8203 mask_kind,
8204 wrt,
8205 } = &node.op
8206 else {
8207 unreachable!()
8208 };
8209 {
8210 let q_shape = &graph.node(node.inputs[0]).shape;
8211 let k_shape = &graph.node(node.inputs[1]).shape;
8212 let rank = q_shape.rank();
8213 let (batch, seq, kv_seq, bhsd) = if rank == 4 {
8214 let d1 = q_shape.dim(1).unwrap_static();
8215 let d2 = q_shape.dim(2).unwrap_static();
8216 if d1 == *num_heads {
8217 (
8218 q_shape.dim(0).unwrap_static(),
8219 d2,
8220 k_shape.dim(2).unwrap_static(),
8221 true,
8222 )
8223 } else {
8224 (
8225 q_shape.dim(0).unwrap_static(),
8226 d1,
8227 k_shape.dim(1).unwrap_static(),
8228 false,
8229 )
8230 }
8231 } else if rank >= 3 {
8232 (
8233 q_shape.dim(0).unwrap_static(),
8234 q_shape.dim(1).unwrap_static(),
8235 k_shape.dim(1).unwrap_static(),
8236 false,
8237 )
8238 } else {
8239 (
8240 1,
8241 q_shape.dim(0).unwrap_static(),
8242 k_shape.dim(0).unwrap_static(),
8243 false,
8244 )
8245 };
8246 let mask_off = if matches!(
8247 mask_kind,
8248 rlx_ir::op::MaskKind::Custom | rlx_ir::op::MaskKind::Bias
8249 ) {
8250 node_offset(arena, node.inputs[4])
8251 } else {
8252 0
8253 };
8254 Thunk::AttentionBackward {
8255 q: node_offset(arena, node.inputs[0]),
8256 k: node_offset(arena, node.inputs[1]),
8257 v: node_offset(arena, node.inputs[2]),
8258 dy: node_offset(arena, node.inputs[3]),
8259 mask: mask_off,
8260 out: node_offset(arena, node.id),
8261 batch: batch as u32,
8262 seq: seq as u32,
8263 kv_seq: kv_seq as u32,
8264 heads: *num_heads as u32,
8265 head_dim: *head_dim as u32,
8266 mask_kind: *mask_kind,
8267 wrt: *wrt,
8268 bhsd,
8269 }
8270 }
8271}
8272
8273#[allow(unused_variables)]
8274fn compile_fused_attention_block(
8275 node: &rlx_ir::Node,
8276 graph: &Graph,
8277 arena: &crate::arena::Arena,
8278 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8279 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8280 rng: rlx_ir::RngOptions,
8281) -> Thunk {
8282 let Op::FusedAttentionBlock {
8283 num_heads,
8284 head_dim,
8285 has_bias,
8286 has_rope,
8287 } = &node.op
8288 else {
8289 unreachable!()
8290 };
8291 {
8292 let x_shape = &graph.node(node.inputs[0]).shape;
8293 let (batch, seq) = if x_shape.rank() >= 3 {
8294 (
8295 x_shape.dim(0).unwrap_static(),
8296 x_shape.dim(1).unwrap_static(),
8297 )
8298 } else {
8299 let total = x_shape.num_elements().unwrap();
8300 let s = x_shape.dim(x_shape.rank() - 2).unwrap_static();
8301 (total / (s * num_heads * head_dim), s)
8302 };
8303 let hs = (*num_heads * *head_dim) as u32;
8304 let mut idx = 4;
8306 let (qkv_b_off, out_b_off) = if *has_bias {
8307 let qb = node_offset(arena, node.inputs[idx]);
8308 let ob = node_offset(arena, node.inputs[idx + 1]);
8309 idx += 2;
8310 (qb, ob)
8311 } else {
8312 (0, 0)
8313 };
8314 let (cos_off, sin_off, cl) = if *has_rope {
8315 let c = node_offset(arena, node.inputs[idx]);
8316 let s = node_offset(arena, node.inputs[idx + 1]);
8317 let clen = get_len(graph, node.inputs[idx]);
8318 (c, s, clen as u32)
8319 } else {
8320 (0, 0, 0)
8321 };
8322
8323 Thunk::FusedAttnBlock {
8324 hidden: node_offset(arena, node.inputs[0]),
8325 qkv_w: node_offset(arena, node.inputs[1]),
8326 out_w: node_offset(arena, node.inputs[2]),
8327 mask: node_offset(arena, node.inputs[3]),
8328 mask_kind: rlx_ir::op::MaskKind::Custom,
8332 out: node_offset(arena, node.id),
8333 qkv_b: qkv_b_off,
8334 out_b: out_b_off,
8335 cos: cos_off,
8336 sin: sin_off,
8337 cos_len: cl,
8338 batch: batch as u32,
8339 seq: seq as u32,
8340 hs,
8341 nh: *num_heads as u32,
8342 dh: *head_dim as u32,
8343 has_bias: *has_bias,
8344 has_rope: *has_rope,
8345 interleaved: false,
8347 }
8348 }
8349}
8350
8351#[allow(unused_variables)]
8352fn compile_rope(
8353 node: &rlx_ir::Node,
8354 graph: &Graph,
8355 arena: &crate::arena::Arena,
8356 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8357 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8358 rng: rlx_ir::RngOptions,
8359) -> Thunk {
8360 let Op::Rope {
8361 head_dim,
8362 n_rot,
8363 style,
8364 } = &node.op
8365 else {
8366 unreachable!()
8367 };
8368 {
8369 let x_shape = &graph.node(node.inputs[0]).shape;
8370 let (batch, seq, hidden) = if x_shape.rank() >= 3 {
8371 (
8372 x_shape.dim(0).unwrap_static(),
8373 x_shape.dim(1).unwrap_static(),
8374 x_shape.dim(2).unwrap_static(),
8375 )
8376 } else {
8377 let total = x_shape.num_elements().unwrap();
8378 (
8379 1,
8380 x_shape.dim(0).unwrap_static(),
8381 total / x_shape.dim(0).unwrap_static(),
8382 )
8383 };
8384 let cos_len = get_len(graph, node.inputs[1]);
8385 Thunk::Rope {
8386 src: node_offset(arena, node.inputs[0]),
8387 cos: node_offset(arena, node.inputs[1]),
8388 sin: node_offset(arena, node.inputs[2]),
8389 dst: node_offset(arena, node.id),
8390 batch: batch as u32,
8391 seq: seq as u32,
8392 hidden: hidden as u32,
8393 head_dim: *head_dim as u32,
8394 n_rot: *n_rot as u32,
8395 cos_len: cos_len as u32,
8396 src_row_stride: hidden as u32,
8400 interleaved: matches!(style, rlx_ir::op::RopeStyle::GptJ),
8401 }
8402 }
8403}
8404
8405#[allow(unused_variables)]
8406fn compile_fused_swi_g_l_u(
8407 node: &rlx_ir::Node,
8408 graph: &Graph,
8409 arena: &crate::arena::Arena,
8410 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8411 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8412 rng: rlx_ir::RngOptions,
8413) -> Thunk {
8414 let Op::FusedSwiGLU {
8415 cast_to: _,
8416 gate_first,
8417 } = &node.op
8418 else {
8419 unreachable!()
8420 };
8421 {
8422 let n_half = node.shape.dim(node.shape.rank() - 1).unwrap_static();
8423 let total = node.shape.num_elements().unwrap();
8424 Thunk::FusedSwiGLU {
8425 src: node_offset(arena, node.inputs[0]),
8426 dst: node_offset(arena, node.id),
8427 n_half: n_half as u32,
8428 total: total as u32,
8429 gate_first: *gate_first,
8430 }
8431 }
8432}
8433
8434#[allow(unused_variables)]
8435fn compile_conv(
8436 node: &rlx_ir::Node,
8437 graph: &Graph,
8438 arena: &crate::arena::Arena,
8439 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8440 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8441 rng: rlx_ir::RngOptions,
8442) -> Thunk {
8443 let Op::Conv {
8444 kernel_size,
8445 stride,
8446 padding,
8447 dilation,
8448 groups,
8449 } = &node.op
8450 else {
8451 unreachable!()
8452 };
8453 {
8454 let in_shape = &graph.node(node.inputs[0]).shape;
8455 let w_shape = &graph.node(node.inputs[1]).shape;
8456 let out_shape = &node.shape;
8457 let is_1x1_simple = kernel_size.len() == 2
8461 && kernel_size[0] == 1
8462 && kernel_size[1] == 1
8463 && stride.iter().all(|&s| s == 1)
8464 && padding.iter().all(|&p| p == 0)
8465 && dilation.iter().all(|&d| d == 1)
8466 && *groups == 1;
8467 if is_1x1_simple && in_shape.rank() >= 3 && out_shape.rank() >= 3 && w_shape.rank() >= 2 {
8468 let (n, c_in, h, w) = conv_nchw_dims(in_shape);
8469 let (_, c_out, _, _) = conv_nchw_dims(out_shape);
8470 Thunk::Conv2D1x1 {
8471 src: node_offset(arena, node.inputs[0]),
8472 weight: node_offset(arena, node.inputs[1]),
8473 dst: node_offset(arena, node.id),
8474 n,
8475 c_in,
8476 c_out,
8477 hw: h.saturating_mul(w),
8478 }
8479 } else if kernel_size.len() == 2
8480 && in_shape.rank() >= 3
8481 && w_shape.rank() >= 2
8482 && out_shape.rank() >= 3
8483 {
8484 let (n, c_in, h, w_in) = conv_nchw_dims(in_shape);
8485 let (_, c_out, h_out, w_out) = conv_nchw_dims(out_shape);
8486 let one_d_w = h == 1
8494 && w_in > 1
8495 && kernel_size[0] > 1
8496 && kernel_size.get(1).copied().unwrap_or(1) == 1;
8497 let (h, w_in, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw) = if one_d_w {
8498 (
8499 w_in,
8500 1,
8501 w_out,
8502 1,
8503 kernel_size[0] as u32,
8504 1,
8505 stride.first().copied().unwrap_or(1) as u32,
8506 1,
8507 padding.first().copied().unwrap_or(0) as u32,
8508 0,
8509 dilation.first().copied().unwrap_or(1) as u32,
8510 1,
8511 )
8512 } else {
8513 (
8514 h,
8515 w_in,
8516 h_out,
8517 w_out,
8518 kernel_size[0] as u32,
8519 kernel_size[1] as u32,
8520 stride.first().copied().unwrap_or(1) as u32,
8521 stride.get(1).copied().unwrap_or(1) as u32,
8522 padding.first().copied().unwrap_or(0) as u32,
8523 padding.get(1).copied().unwrap_or(0) as u32,
8524 dilation.first().copied().unwrap_or(1) as u32,
8525 dilation.get(1).copied().unwrap_or(1) as u32,
8526 )
8527 };
8528 Thunk::Conv2D {
8529 src: node_offset(arena, node.inputs[0]),
8530 weight: node_offset(arena, node.inputs[1]),
8531 dst: node_offset(arena, node.id),
8532 n,
8533 c_in,
8534 h,
8535 w: w_in,
8536 c_out,
8537 h_out,
8538 w_out,
8539 kh,
8540 kw,
8541 sh,
8542 sw,
8543 ph,
8544 pw,
8545 dh,
8546 dw,
8547 groups: *groups as u32,
8548 }
8549 } else {
8550 Thunk::Nop
8551 }
8552 }
8553}
8554
8555#[allow(unused_variables)]
8556fn compile_conv3d(
8557 node: &rlx_ir::Node,
8558 graph: &Graph,
8559 arena: &crate::arena::Arena,
8560 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8561 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8562 rng: rlx_ir::RngOptions,
8563) -> Thunk {
8564 let Op::Conv3d {
8565 stride,
8566 padding,
8567 dilation,
8568 groups,
8569 } = &node.op
8570 else {
8571 unreachable!()
8572 };
8573 {
8574 let in_shape = &graph.node(node.inputs[0]).shape;
8575 let w_shape = &graph.node(node.inputs[1]).shape;
8576 let out_shape = &node.shape;
8577 if in_shape.rank() == 5 && w_shape.rank() == 5 && out_shape.rank() == 5 {
8578 let (n, c_in, d, h, w) = conv_ncdhw_dims(in_shape);
8579 let (_, c_out, d_out, h_out, w_out) = conv_ncdhw_dims(out_shape);
8580 let kd = w_shape.dim(2).unwrap_static() as u32;
8582 let kh = w_shape.dim(3).unwrap_static() as u32;
8583 let kw = w_shape.dim(4).unwrap_static() as u32;
8584 Thunk::Conv3d {
8585 src: node_offset(arena, node.inputs[0]),
8586 weight: node_offset(arena, node.inputs[1]),
8587 dst: node_offset(arena, node.id),
8588 n,
8589 c_in,
8590 d,
8591 h,
8592 w,
8593 c_out,
8594 d_out,
8595 h_out,
8596 w_out,
8597 kd,
8598 kh,
8599 kw,
8600 sd: stride[0] as u32,
8601 sh: stride[1] as u32,
8602 sw: stride[2] as u32,
8603 pd: padding[0] as u32,
8604 ph: padding[1] as u32,
8605 pw: padding[2] as u32,
8606 dd: dilation[0] as u32,
8607 dh: dilation[1] as u32,
8608 dw: dilation[2] as u32,
8609 groups: *groups as u32,
8610 }
8611 } else {
8612 Thunk::Nop
8613 }
8614 }
8615}
8616
8617#[allow(unused_variables)]
8618fn compile_conv_transpose3d(
8619 node: &rlx_ir::Node,
8620 graph: &Graph,
8621 arena: &crate::arena::Arena,
8622 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8623 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8624 rng: rlx_ir::RngOptions,
8625) -> Thunk {
8626 let Op::ConvTranspose3d {
8627 stride,
8628 padding,
8629 dilation,
8630 output_padding: _,
8631 groups,
8632 } = &node.op
8633 else {
8634 unreachable!()
8635 };
8636 {
8637 let in_shape = &graph.node(node.inputs[0]).shape;
8638 let w_shape = &graph.node(node.inputs[1]).shape;
8639 let out_shape = &node.shape;
8640 if in_shape.rank() == 5 && w_shape.rank() == 5 && out_shape.rank() == 5 {
8641 let (n, c_in, d, h, w_in) = conv_ncdhw_dims(in_shape);
8642 let (_, c_out, d_out, h_out, w_out) = conv_ncdhw_dims(out_shape);
8643 let kd = w_shape.dim(2).unwrap_static() as u32;
8645 let kh = w_shape.dim(3).unwrap_static() as u32;
8646 let kw = w_shape.dim(4).unwrap_static() as u32;
8647 Thunk::ConvTranspose3d {
8648 src: node_offset(arena, node.inputs[0]),
8649 weight: node_offset(arena, node.inputs[1]),
8650 dst: node_offset(arena, node.id),
8651 n,
8652 c_in,
8653 d,
8654 h,
8655 w_in,
8656 c_out,
8657 d_out,
8658 h_out,
8659 w_out,
8660 kd,
8661 kh,
8662 kw,
8663 sd: stride[0] as u32,
8664 sh: stride[1] as u32,
8665 sw: stride[2] as u32,
8666 pd: padding[0] as u32,
8667 ph: padding[1] as u32,
8668 pw: padding[2] as u32,
8669 dd: dilation[0] as u32,
8670 dh: dilation[1] as u32,
8671 dw: dilation[2] as u32,
8672 groups: *groups as u32,
8673 }
8674 } else {
8675 Thunk::Nop
8676 }
8677 }
8678}
8679
8680#[allow(unused_variables)]
8681fn compile_pool(
8682 node: &rlx_ir::Node,
8683 graph: &Graph,
8684 arena: &crate::arena::Arena,
8685 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8686 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8687 rng: rlx_ir::RngOptions,
8688) -> Thunk {
8689 let Op::Pool {
8690 kind,
8691 kernel_size,
8692 stride,
8693 padding,
8694 } = &node.op
8695 else {
8696 unreachable!()
8697 };
8698 {
8699 let in_shape = &graph.node(node.inputs[0]).shape;
8701 let out_shape = &node.shape;
8702 if kernel_size.len() == 2 && in_shape.rank() == 4 && out_shape.rank() == 4 {
8703 Thunk::Pool2D {
8704 src: node_offset(arena, node.inputs[0]),
8705 dst: node_offset(arena, node.id),
8706 n: in_shape.dim(0).unwrap_static() as u32,
8707 c: in_shape.dim(1).unwrap_static() as u32,
8708 h: in_shape.dim(2).unwrap_static() as u32,
8709 w: in_shape.dim(3).unwrap_static() as u32,
8710 h_out: out_shape.dim(2).unwrap_static() as u32,
8711 w_out: out_shape.dim(3).unwrap_static() as u32,
8712 kh: kernel_size[0] as u32,
8713 kw: kernel_size[1] as u32,
8714 sh: stride.first().copied().unwrap_or(1) as u32,
8715 sw: stride.get(1).copied().unwrap_or(1) as u32,
8716 ph: padding.first().copied().unwrap_or(0) as u32,
8717 pw: padding.get(1).copied().unwrap_or(0) as u32,
8718 kind: *kind,
8719 }
8720 } else {
8721 Thunk::Nop
8722 }
8723 }
8724}
8725
8726#[allow(unused_variables)]
8727fn compile_transpose(
8728 node: &rlx_ir::Node,
8729 graph: &Graph,
8730 arena: &crate::arena::Arena,
8731 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8732 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8733 rng: rlx_ir::RngOptions,
8734) -> Thunk {
8735 let Op::Transpose { perm } = &node.op else {
8736 unreachable!()
8737 };
8738 {
8739 let in_shape = &graph.node(node.inputs[0]).shape;
8742 let in_rank = in_shape.rank();
8743 if perm.iter().any(|&p| p >= in_rank) {
8744 Thunk::Nop
8745 } else {
8746 let in_dims: Vec<usize> = (0..in_rank)
8747 .map(|i| in_shape.dim(i).unwrap_static())
8748 .collect();
8749 let mut in_strides_full = vec![1usize; in_rank];
8751 for d in (0..in_rank.saturating_sub(1)).rev() {
8752 in_strides_full[d] = in_strides_full[d + 1] * in_dims[d + 1];
8753 }
8754 let out_dims: Vec<u32> = perm.iter().map(|&p| in_dims[p] as u32).collect();
8755 let in_strides: Vec<u32> = perm.iter().map(|&p| in_strides_full[p] as u32).collect();
8756 let in_total = in_dims.iter().product::<usize>() as u32;
8757 let src = node_offset(arena, node.inputs[0]);
8758 let dst = node_offset(arena, node.id);
8759 let elem_bytes = node.shape.dtype().size_bytes() as u8;
8760 match node.shape.dtype() {
8761 rlx_ir::DType::F64 => Thunk::TransposeF64 {
8762 src,
8763 dst,
8764 in_total,
8765 out_dims,
8766 in_strides,
8767 },
8768 _ => Thunk::Transpose {
8769 src,
8770 dst,
8771 in_total,
8772 out_dims,
8773 in_strides,
8774 elem_bytes,
8775 },
8776 }
8777 }
8778 }
8779}
8780
8781#[allow(unused_variables)]
8782fn compile_scatter_add(
8783 node: &rlx_ir::Node,
8784 graph: &Graph,
8785 arena: &crate::arena::Arena,
8786 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8787 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8788 rng: rlx_ir::RngOptions,
8789) -> Thunk {
8790 let Op::ScatterAdd = &node.op else {
8791 unreachable!()
8792 };
8793 {
8794 let upd_shape = &graph.node(node.inputs[0]).shape;
8797 let out_shape = &node.shape;
8798 let num_updates = upd_shape.dim(0).unwrap_static();
8799 let out_dim = out_shape.dim(0).unwrap_static();
8800 let trailing: usize = (1..out_shape.rank())
8801 .map(|i| out_shape.dim(i).unwrap_static())
8802 .product::<usize>()
8803 .max(1);
8804 Thunk::ScatterAdd {
8805 updates: node_offset(arena, node.inputs[0]),
8806 indices: node_offset(arena, node.inputs[1]),
8807 dst: node_offset(arena, node.id),
8808 num_updates: num_updates as u32,
8809 out_dim: out_dim as u32,
8810 trailing: trailing as u32,
8811 }
8812 }
8813}
8814
8815#[allow(unused_variables)]
8816fn compile_grouped_mat_mul(
8817 node: &rlx_ir::Node,
8818 graph: &Graph,
8819 arena: &crate::arena::Arena,
8820 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8821 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8822 rng: rlx_ir::RngOptions,
8823) -> Thunk {
8824 let Op::GroupedMatMul = &node.op else {
8825 unreachable!()
8826 };
8827 {
8828 let in_shape = &graph.node(node.inputs[0]).shape;
8830 let w_shape = &graph.node(node.inputs[1]).shape;
8831 let m = in_shape.dim(in_shape.rank() - 2).unwrap_static();
8832 let k_dim = in_shape.dim(in_shape.rank() - 1).unwrap_static();
8833 let num_experts = w_shape.dim(0).unwrap_static();
8834 let n = w_shape.dim(2).unwrap_static();
8835 Thunk::GroupedMatMul {
8836 input: node_offset(arena, node.inputs[0]),
8837 weight: node_offset(arena, node.inputs[1]),
8838 expert_idx: node_offset(arena, node.inputs[2]),
8839 dst: node_offset(arena, node.id),
8840 m: m as u32,
8841 k_dim: k_dim as u32,
8842 n: n as u32,
8843 num_experts: num_experts as u32,
8844 }
8845 }
8846}
8847
8848#[allow(unused_variables)]
8849fn compile_dequant_grouped_mat_mul(
8850 node: &rlx_ir::Node,
8851 graph: &Graph,
8852 arena: &crate::arena::Arena,
8853 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8854 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8855 rng: rlx_ir::RngOptions,
8856) -> Thunk {
8857 let Op::DequantGroupedMatMul { scheme } = &node.op else {
8858 unreachable!()
8859 };
8860 {
8861 let in_shape = &graph.node(node.inputs[0]).shape;
8862 let w_shape = &graph.node(node.inputs[1]).shape;
8863 let m = in_shape.dim(in_shape.rank() - 2).unwrap_static();
8864 let k_dim = in_shape.dim(in_shape.rank() - 1).unwrap_static();
8865 let out_shape = &node.shape;
8866 let n = out_shape.dim(out_shape.rank() - 1).unwrap_static();
8867 let block_elems = scheme.gguf_block_size() as usize;
8868 let block_bytes = scheme.gguf_block_bytes() as usize;
8869 let slab_bytes = (k_dim * n) / block_elems * block_bytes;
8870 let total_bytes = w_shape.num_elements().unwrap();
8871 let num_experts = total_bytes / slab_bytes.max(1);
8872 Thunk::DequantGroupedMatMulGguf {
8873 input: node_offset(arena, node.inputs[0]),
8874 w_q: node_offset(arena, node.inputs[1]),
8875 expert_idx: node_offset(arena, node.inputs[2]),
8876 dst: node_offset(arena, node.id),
8877 m: m as u32,
8878 k_dim: k_dim as u32,
8879 n: n as u32,
8880 num_experts: num_experts as u32,
8881 scheme: *scheme,
8882 }
8883 }
8884}
8885
8886#[allow(unused_variables)]
8887fn compile_dequant_mo_e_weights(
8888 node: &rlx_ir::Node,
8889 graph: &Graph,
8890 arena: &crate::arena::Arena,
8891 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8892 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8893 rng: rlx_ir::RngOptions,
8894) -> Thunk {
8895 let Op::DequantMoEWeights { scheme } = &node.op else {
8896 unreachable!()
8897 };
8898 {
8899 let w_shape = &graph.node(node.inputs[0]).shape;
8900 let out_shape = &node.shape;
8901 let num_experts = out_shape.dim(0).unwrap_static();
8902 let k_dim = out_shape.dim(1).unwrap_static();
8903 let n = out_shape.dim(2).unwrap_static();
8904 let block_elems = scheme.gguf_block_size() as usize;
8905 let block_bytes = scheme.gguf_block_bytes() as usize;
8906 let slab_bytes = (k_dim * n) / block_elems * block_bytes;
8907 let total_bytes = w_shape.num_elements().unwrap();
8908 assert_eq!(
8909 total_bytes,
8910 num_experts * slab_bytes,
8911 "DequantMoEWeights packed bytes mismatch"
8912 );
8913 Thunk::DequantMoEWeightsGguf {
8914 w_q: node_offset(arena, node.inputs[0]),
8915 dst: node_offset(arena, node.id),
8916 k_dim: k_dim as u32,
8917 n: n as u32,
8918 num_experts: num_experts as u32,
8919 scheme: *scheme,
8920 }
8921 }
8922}
8923
8924#[allow(unused_variables)]
8925fn compile_top_k(
8926 node: &rlx_ir::Node,
8927 graph: &Graph,
8928 arena: &crate::arena::Arena,
8929 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8930 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8931 rng: rlx_ir::RngOptions,
8932) -> Thunk {
8933 let Op::TopK { k } = &node.op else {
8934 unreachable!()
8935 };
8936 {
8937 let in_shape = &graph.node(node.inputs[0]).shape;
8938 let rank = in_shape.rank();
8939 let axis_dim = in_shape.dim(rank - 1).unwrap_static();
8940 let outer = in_shape.num_elements().unwrap() / axis_dim;
8941 let indices_i64 = u8::from(graph.node(node.id).shape.dtype() == rlx_ir::DType::I64);
8942 Thunk::TopK {
8943 src: node_offset(arena, node.inputs[0]),
8944 dst: node_offset(arena, node.id),
8945 outer: outer as u32,
8946 axis_dim: axis_dim as u32,
8947 k: *k as u32,
8948 indices_i64,
8949 }
8950 }
8951}
8952
8953#[allow(unused_variables)]
8954fn compile_reduce(
8955 node: &rlx_ir::Node,
8956 graph: &Graph,
8957 arena: &crate::arena::Arena,
8958 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
8959 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
8960 rng: rlx_ir::RngOptions,
8961) -> Thunk {
8962 let Op::Reduce {
8963 op,
8964 axes,
8965 keep_dim: _,
8966 } = &node.op
8967 else {
8968 unreachable!()
8969 };
8970 {
8971 let in_shape = &graph.node(node.inputs[0]).shape;
8977 let rank = in_shape.rank();
8978 let mut sorted = axes.clone();
8979 sorted.sort();
8980 sorted.dedup();
8981 let contiguous = sorted.windows(2).all(|w| w[1] == w[0] + 1)
8982 && !sorted.is_empty()
8983 && *sorted.last().unwrap() < rank;
8984 if !contiguous {
8985 Thunk::Nop
8986 } else {
8987 let first = sorted[0];
8988 let last = *sorted.last().unwrap();
8989 let outer: usize = (0..first)
8990 .map(|i| in_shape.dim(i).unwrap_static())
8991 .product::<usize>()
8992 .max(1);
8993 let reduced: usize = (first..=last)
8994 .map(|i| in_shape.dim(i).unwrap_static())
8995 .product();
8996 let inner: usize = (last + 1..rank)
8997 .map(|i| in_shape.dim(i).unwrap_static())
8998 .product::<usize>()
8999 .max(1);
9000 let src = node_offset(arena, node.inputs[0]);
9001 let dst = node_offset(arena, node.id);
9002 if node.shape.dtype() == rlx_ir::DType::F64 && matches!(op, ReduceOp::Sum) {
9003 Thunk::ReduceSumF64 {
9004 src,
9005 dst,
9006 outer: outer as u32,
9007 reduced: reduced as u32,
9008 inner: inner as u32,
9009 }
9010 } else {
9011 Thunk::Reduce {
9012 src,
9013 dst,
9014 outer: outer as u32,
9015 reduced: reduced as u32,
9016 inner: inner as u32,
9017 op: *op,
9018 }
9019 }
9020 }
9021 }
9022}
9023
9024#[allow(unused_variables)]
9025fn compile_compare(
9026 node: &rlx_ir::Node,
9027 graph: &Graph,
9028 arena: &crate::arena::Arena,
9029 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9030 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9031 rng: rlx_ir::RngOptions,
9032) -> Thunk {
9033 let Op::Compare(cmp) = &node.op else {
9034 unreachable!()
9035 };
9036 {
9037 let len = node.shape.num_elements().unwrap();
9038 let in_dtype = graph.node(node.inputs[0]).shape.dtype();
9039 let inputs_i64 = u8::from(in_dtype == rlx_ir::DType::I64);
9040 Thunk::Compare {
9041 lhs: node_offset(arena, node.inputs[0]),
9042 rhs: node_offset(arena, node.inputs[1]),
9043 dst: node_offset(arena, node.id),
9044 len: len as u32,
9045 op: *cmp,
9046 inputs_i64,
9047 inputs_elem_bytes: in_dtype.size_bytes() as u8,
9048 dst_elem_bytes: node.shape.dtype().size_bytes() as u8,
9049 }
9050 }
9051}
9052
9053#[allow(unused_variables)]
9054fn compile_where(
9055 node: &rlx_ir::Node,
9056 graph: &Graph,
9057 arena: &crate::arena::Arena,
9058 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9059 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9060 rng: rlx_ir::RngOptions,
9061) -> Thunk {
9062 let Op::Where = &node.op else { unreachable!() };
9063 {
9064 let len = node.shape.num_elements().unwrap();
9065 let elem_bytes = node.shape.dtype().size_bytes() as u8;
9066 let cond_elem_bytes = graph.node(node.inputs[0]).shape.dtype().size_bytes() as u8;
9067 Thunk::Where {
9068 cond: node_offset(arena, node.inputs[0]),
9069 on_true: node_offset(arena, node.inputs[1]),
9070 on_false: node_offset(arena, node.inputs[2]),
9071 dst: node_offset(arena, node.id),
9072 len: len as u32,
9073 elem_bytes,
9074 cond_elem_bytes,
9075 }
9076 }
9077}
9078
9079#[allow(unused_variables)]
9080fn compile_fma(
9081 node: &rlx_ir::Node,
9082 graph: &Graph,
9083 arena: &crate::arena::Arena,
9084 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9085 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9086 rng: rlx_ir::RngOptions,
9087) -> Thunk {
9088 let Op::Fma = &node.op else { unreachable!() };
9089 {
9090 let len = node.shape.num_elements().unwrap();
9091 Thunk::Fma {
9092 a: node_offset(arena, node.inputs[0]),
9093 b: node_offset(arena, node.inputs[1]),
9094 c: node_offset(arena, node.inputs[2]),
9095 dst: node_offset(arena, node.id),
9096 len: len as u32,
9097 elem_bytes: node.shape.dtype().size_bytes() as u8,
9098 }
9099 }
9100}
9101
9102#[allow(unused_variables)]
9103fn compile_relu_backward(
9104 node: &rlx_ir::Node,
9105 graph: &Graph,
9106 arena: &crate::arena::Arena,
9107 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9108 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9109 rng: rlx_ir::RngOptions,
9110) -> Thunk {
9111 let Op::ReluBackward = &node.op else {
9112 unreachable!()
9113 };
9114 {
9115 let len: usize = (0..node.shape.rank())
9116 .map(|i| node.shape.dim(i).unwrap_static())
9117 .product();
9118 let x = node_offset(arena, node.inputs[0]);
9119 let dy = node_offset(arena, node.inputs[1]);
9120 let dx = node_offset(arena, node.id);
9121 match node.shape.dtype() {
9122 rlx_ir::DType::F64 => Thunk::ReluBackwardF64 {
9123 x,
9124 dy,
9125 dx,
9126 len: len as u32,
9127 },
9128 _ => Thunk::ReluBackward {
9129 x,
9130 dy,
9131 dx,
9132 len: len as u32,
9133 },
9134 }
9135 }
9136}
9137
9138#[allow(unused_variables)]
9139fn compile_complex_norm_sq(
9140 node: &rlx_ir::Node,
9141 graph: &Graph,
9142 arena: &crate::arena::Arena,
9143 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9144 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9145 rng: rlx_ir::RngOptions,
9146) -> Thunk {
9147 let Op::ComplexNormSq = &node.op else {
9148 unreachable!()
9149 };
9150 {
9151 let len: usize = (0..node.shape.rank())
9152 .map(|i| node.shape.dim(i).unwrap_static())
9153 .product();
9154 let src = node_offset(arena, node.inputs[0]);
9155 let dst = node_offset(arena, node.id);
9156 Thunk::ComplexNormSqF32 {
9157 src,
9158 dst,
9159 len: len as u32,
9160 }
9161 }
9162}
9163
9164#[allow(unused_variables)]
9165fn compile_complex_norm_sq_backward(
9166 node: &rlx_ir::Node,
9167 graph: &Graph,
9168 arena: &crate::arena::Arena,
9169 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9170 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9171 rng: rlx_ir::RngOptions,
9172) -> Thunk {
9173 let Op::ComplexNormSqBackward = &node.op else {
9174 unreachable!()
9175 };
9176 {
9177 let len: usize = (0..node.shape.rank())
9178 .map(|i| node.shape.dim(i).unwrap_static())
9179 .product();
9180 let z = node_offset(arena, node.inputs[0]);
9181 let g = node_offset(arena, node.inputs[1]);
9182 let dz = node_offset(arena, node.id);
9183 Thunk::ComplexNormSqBackwardF32 {
9184 z,
9185 g,
9186 dz,
9187 len: len as u32,
9188 }
9189 }
9190}
9191
9192#[allow(unused_variables)]
9193fn compile_conjugate(
9194 node: &rlx_ir::Node,
9195 graph: &Graph,
9196 arena: &crate::arena::Arena,
9197 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9198 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9199 rng: rlx_ir::RngOptions,
9200) -> Thunk {
9201 let Op::Conjugate = &node.op else {
9202 unreachable!()
9203 };
9204 {
9205 let len: usize = (0..node.shape.rank())
9206 .map(|i| node.shape.dim(i).unwrap_static())
9207 .product();
9208 Thunk::ConjugateC64 {
9209 src: node_offset(arena, node.inputs[0]),
9210 dst: node_offset(arena, node.id),
9211 len: len as u32,
9212 }
9213 }
9214}
9215
9216#[allow(unused_variables)]
9217fn compile_activation_backward(
9218 node: &rlx_ir::Node,
9219 graph: &Graph,
9220 arena: &crate::arena::Arena,
9221 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9222 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9223 rng: rlx_ir::RngOptions,
9224) -> Thunk {
9225 let Op::ActivationBackward { kind } = &node.op else {
9226 unreachable!()
9227 };
9228 {
9229 let len: usize = (0..node.shape.rank())
9230 .map(|i| node.shape.dim(i).unwrap_static())
9231 .product();
9232 let x = node_offset(arena, node.inputs[0]);
9233 let dy = node_offset(arena, node.inputs[1]);
9234 let dx = node_offset(arena, node.id);
9235 match node.shape.dtype() {
9236 rlx_ir::DType::F64 => Thunk::ActivationBackwardF64 {
9237 x,
9238 dy,
9239 dx,
9240 len: len as u32,
9241 kind: *kind,
9242 },
9243 _ => Thunk::ActivationBackward {
9244 x,
9245 dy,
9246 dx,
9247 len: len as u32,
9248 kind: *kind,
9249 },
9250 }
9251 }
9252}
9253
9254#[allow(unused_variables)]
9255fn compile_layer_norm_backward_input(
9256 node: &rlx_ir::Node,
9257 graph: &Graph,
9258 arena: &crate::arena::Arena,
9259 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9260 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9261 rng: rlx_ir::RngOptions,
9262) -> Thunk {
9263 let Op::LayerNormBackwardInput { eps, .. } = &node.op else {
9264 unreachable!()
9265 };
9266 {
9267 let h = node.shape.dim(node.shape.rank() - 1).unwrap_static();
9269 let total = node.shape.num_elements().unwrap();
9270 Thunk::LayerNormBackwardInput {
9271 x: node_offset(arena, node.inputs[0]),
9272 gamma: node_offset(arena, node.inputs[1]),
9273 dy: node_offset(arena, node.inputs[2]),
9274 dx: node_offset(arena, node.id),
9275 rows: (total / h) as u32,
9276 h: h as u32,
9277 eps: *eps,
9278 }
9279 }
9280}
9281
9282#[allow(unused_variables)]
9283fn compile_layer_norm_backward_gamma(
9284 node: &rlx_ir::Node,
9285 graph: &Graph,
9286 arena: &crate::arena::Arena,
9287 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9288 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9289 rng: rlx_ir::RngOptions,
9290) -> Thunk {
9291 let Op::LayerNormBackwardGamma { eps, .. } = &node.op else {
9292 unreachable!()
9293 };
9294 {
9295 let x_shape = &graph.node(node.inputs[0]).shape;
9296 let h = x_shape.dim(x_shape.rank() - 1).unwrap_static();
9297 let x_total = x_shape.num_elements().unwrap();
9298 Thunk::LayerNormBackwardGamma {
9299 x: node_offset(arena, node.inputs[0]),
9300 dy: node_offset(arena, node.inputs[1]),
9301 dgamma: node_offset(arena, node.id),
9302 rows: (x_total / h) as u32,
9303 h: h as u32,
9304 eps: *eps,
9305 }
9306 }
9307}
9308
9309#[allow(unused_variables)]
9310fn compile_rope_backward(
9311 node: &rlx_ir::Node,
9312 graph: &Graph,
9313 arena: &crate::arena::Arena,
9314 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9315 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9316 rng: rlx_ir::RngOptions,
9317) -> Thunk {
9318 let Op::RopeBackward { head_dim, n_rot } = &node.op else {
9319 unreachable!()
9320 };
9321 {
9322 let dy_shape = &graph.node(node.inputs[0]).shape;
9323 let (batch, seq, hidden) = if dy_shape.rank() >= 3 {
9324 (
9325 dy_shape.dim(0).unwrap_static(),
9326 dy_shape.dim(1).unwrap_static(),
9327 dy_shape.dim(2).unwrap_static(),
9328 )
9329 } else {
9330 (
9331 1,
9332 dy_shape.dim(0).unwrap_static(),
9333 dy_shape.dim(1).unwrap_static(),
9334 )
9335 };
9336 let cos_shape = &graph.node(node.inputs[1]).shape;
9337 let cos_len = cos_shape.num_elements().unwrap();
9338 Thunk::RopeBackward {
9339 dy: node_offset(arena, node.inputs[0]),
9340 cos: node_offset(arena, node.inputs[1]),
9341 sin: node_offset(arena, node.inputs[2]),
9342 dx: node_offset(arena, node.id),
9343 batch: batch as u32,
9344 seq: seq as u32,
9345 hidden: hidden as u32,
9346 head_dim: *head_dim as u32,
9347 n_rot: *n_rot as u32,
9348 cos_len: cos_len as u32,
9349 }
9350 }
9351}
9352
9353#[allow(unused_variables)]
9354fn compile_cumsum_backward(
9355 node: &rlx_ir::Node,
9356 graph: &Graph,
9357 arena: &crate::arena::Arena,
9358 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9359 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9360 rng: rlx_ir::RngOptions,
9361) -> Thunk {
9362 let Op::CumsumBackward { exclusive, .. } = &node.op else {
9363 unreachable!()
9364 };
9365 {
9366 let dy_shape = &graph.node(node.inputs[0]).shape;
9367 let rank = dy_shape.rank();
9368 let cols = dy_shape.dim(rank - 1).unwrap_static();
9369 let rows = dy_shape.num_elements().unwrap() / cols;
9370 Thunk::CumsumBackward {
9371 dy: node_offset(arena, node.inputs[0]),
9372 dx: node_offset(arena, node.id),
9373 rows: rows as u32,
9374 cols: cols as u32,
9375 exclusive: *exclusive,
9376 }
9377 }
9378}
9379
9380#[allow(unused_variables)]
9381fn compile_gather_backward(
9382 node: &rlx_ir::Node,
9383 graph: &Graph,
9384 arena: &crate::arena::Arena,
9385 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9386 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9387 rng: rlx_ir::RngOptions,
9388) -> Thunk {
9389 let Op::GatherBackward { .. } = &node.op else {
9390 unreachable!()
9391 };
9392 {
9393 let dy_shape = &graph.node(node.inputs[0]).shape;
9394 let idx_shape = &graph.node(node.inputs[1]).shape;
9395 let out_shape = &node.shape;
9396 let rank = out_shape.rank();
9397 let axis = match &node.op {
9398 Op::GatherBackward { axis } => *axis,
9399 _ => 0,
9400 };
9401 let axis_u = if axis < 0 {
9402 (rank as i32 + axis) as usize
9403 } else {
9404 axis as usize
9405 };
9406 let outer: usize = (0..axis_u)
9407 .map(|i| dy_shape.dim(i).unwrap_static())
9408 .product::<usize>()
9409 .max(1);
9410 let num_idx = idx_shape.dim(axis_u).unwrap_static();
9411 let trailing: usize = (axis_u + 1..dy_shape.rank())
9412 .map(|i| dy_shape.dim(i).unwrap_static())
9413 .product::<usize>()
9414 .max(1);
9415 let axis_dim = out_shape.dim(axis_u).unwrap_static();
9416 Thunk::GatherBackward {
9417 dy: node_offset(arena, node.inputs[0]),
9418 indices: node_offset(arena, node.inputs[1]),
9419 dst: node_offset(arena, node.id),
9420 outer: outer as u32,
9421 axis_dim: axis_dim as u32,
9422 num_idx: num_idx as u32,
9423 trailing: trailing as u32,
9424 }
9425 }
9426}
9427
9428#[allow(unused_variables)]
9429fn compile_max_pool2d_backward(
9430 node: &rlx_ir::Node,
9431 graph: &Graph,
9432 arena: &crate::arena::Arena,
9433 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9434 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9435 rng: rlx_ir::RngOptions,
9436) -> Thunk {
9437 let Op::MaxPool2dBackward {
9438 kernel_size,
9439 stride,
9440 padding,
9441 } = &node.op
9442 else {
9443 unreachable!()
9444 };
9445 {
9446 let x_shape = &graph.node(node.inputs[0]).shape;
9447 let dy_shape = &graph.node(node.inputs[1]).shape;
9448 if kernel_size.len() == 2 && x_shape.rank() == 4 && dy_shape.rank() == 4 {
9449 Thunk::MaxPool2dBackward {
9450 x: node_offset(arena, node.inputs[0]),
9451 dy: node_offset(arena, node.inputs[1]),
9452 dx: node_offset(arena, node.id),
9453 n: x_shape.dim(0).unwrap_static() as u32,
9454 c: x_shape.dim(1).unwrap_static() as u32,
9455 h: x_shape.dim(2).unwrap_static() as u32,
9456 w: x_shape.dim(3).unwrap_static() as u32,
9457 h_out: dy_shape.dim(2).unwrap_static() as u32,
9458 w_out: dy_shape.dim(3).unwrap_static() as u32,
9459 kh: kernel_size[0] as u32,
9460 kw: kernel_size[1] as u32,
9461 sh: stride.first().copied().unwrap_or(1) as u32,
9462 sw: stride.get(1).copied().unwrap_or(1) as u32,
9463 ph: padding.first().copied().unwrap_or(0) as u32,
9464 pw: padding.get(1).copied().unwrap_or(0) as u32,
9465 }
9466 } else {
9467 Thunk::Nop
9468 }
9469 }
9470}
9471
9472#[allow(unused_variables)]
9473fn compile_conv2d_backward_input(
9474 node: &rlx_ir::Node,
9475 graph: &Graph,
9476 arena: &crate::arena::Arena,
9477 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9478 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9479 rng: rlx_ir::RngOptions,
9480) -> Thunk {
9481 let Op::Conv2dBackwardInput {
9482 kernel_size,
9483 stride,
9484 padding,
9485 dilation,
9486 groups,
9487 } = &node.op
9488 else {
9489 unreachable!()
9490 };
9491 {
9492 let dy_shape = &graph.node(node.inputs[0]).shape;
9493 let w_shape = &graph.node(node.inputs[1]).shape;
9494 let out_shape = &node.shape;
9495 if kernel_size.len() == 2
9496 && dy_shape.rank() == 4
9497 && w_shape.rank() == 4
9498 && out_shape.rank() == 4
9499 {
9500 Thunk::Conv2dBackwardInput {
9501 dy: node_offset(arena, node.inputs[0]),
9502 w: node_offset(arena, node.inputs[1]),
9503 dx: node_offset(arena, node.id),
9504 n: out_shape.dim(0).unwrap_static() as u32,
9505 c_in: out_shape.dim(1).unwrap_static() as u32,
9506 h: out_shape.dim(2).unwrap_static() as u32,
9507 w_in: out_shape.dim(3).unwrap_static() as u32,
9508 c_out: dy_shape.dim(1).unwrap_static() as u32,
9509 h_out: dy_shape.dim(2).unwrap_static() as u32,
9510 w_out: dy_shape.dim(3).unwrap_static() as u32,
9511 kh: kernel_size[0] as u32,
9512 kw: kernel_size[1] as u32,
9513 sh: stride.first().copied().unwrap_or(1) as u32,
9514 sw: stride.get(1).copied().unwrap_or(1) as u32,
9515 ph: padding.first().copied().unwrap_or(0) as u32,
9516 pw: padding.get(1).copied().unwrap_or(0) as u32,
9517 dh: dilation.first().copied().unwrap_or(1) as u32,
9518 dw: dilation.get(1).copied().unwrap_or(1) as u32,
9519 groups: *groups as u32,
9520 }
9521 } else {
9522 Thunk::Nop
9523 }
9524 }
9525}
9526
9527#[allow(unused_variables)]
9528fn compile_conv2d_backward_weight(
9529 node: &rlx_ir::Node,
9530 graph: &Graph,
9531 arena: &crate::arena::Arena,
9532 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9533 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9534 rng: rlx_ir::RngOptions,
9535) -> Thunk {
9536 let Op::Conv2dBackwardWeight {
9537 kernel_size,
9538 stride,
9539 padding,
9540 dilation,
9541 groups,
9542 } = &node.op
9543 else {
9544 unreachable!()
9545 };
9546 {
9547 let x_shape = &graph.node(node.inputs[0]).shape;
9548 let dy_shape = &graph.node(node.inputs[1]).shape;
9549 let dw_shape = &node.shape;
9550 if kernel_size.len() == 2
9551 && x_shape.rank() == 4
9552 && dy_shape.rank() == 4
9553 && dw_shape.rank() == 4
9554 {
9555 Thunk::Conv2dBackwardWeight {
9556 x: node_offset(arena, node.inputs[0]),
9557 dy: node_offset(arena, node.inputs[1]),
9558 dw: node_offset(arena, node.id),
9559 n: x_shape.dim(0).unwrap_static() as u32,
9560 c_in: x_shape.dim(1).unwrap_static() as u32,
9561 h: x_shape.dim(2).unwrap_static() as u32,
9562 w: x_shape.dim(3).unwrap_static() as u32,
9563 c_out: dy_shape.dim(1).unwrap_static() as u32,
9564 h_out: dy_shape.dim(2).unwrap_static() as u32,
9565 w_out: dy_shape.dim(3).unwrap_static() as u32,
9566 kh: kernel_size[0] as u32,
9567 kw: kernel_size[1] as u32,
9568 sh: stride.first().copied().unwrap_or(1) as u32,
9569 sw: stride.get(1).copied().unwrap_or(1) as u32,
9570 ph: padding.first().copied().unwrap_or(0) as u32,
9571 pw: padding.get(1).copied().unwrap_or(0) as u32,
9572 dh: dilation.first().copied().unwrap_or(1) as u32,
9573 dw_dil: dilation.get(1).copied().unwrap_or(1) as u32,
9574 groups: *groups as u32,
9575 }
9576 } else {
9577 Thunk::Nop
9578 }
9579 }
9580}
9581
9582#[allow(unused_variables)]
9583fn compile_im2_col(
9584 node: &rlx_ir::Node,
9585 graph: &Graph,
9586 arena: &crate::arena::Arena,
9587 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9588 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9589 rng: rlx_ir::RngOptions,
9590) -> Thunk {
9591 let Op::Im2Col {
9592 kernel_size,
9593 stride,
9594 padding,
9595 dilation,
9596 } = &node.op
9597 else {
9598 unreachable!()
9599 };
9600 {
9601 let x_shape = &graph.node(node.inputs[0]).shape;
9602 let out_shape = &node.shape;
9603 if kernel_size.len() == 2 && x_shape.rank() == 4 && out_shape.rank() == 2 {
9604 let n = match x_shape.dim(0) {
9605 rlx_ir::shape::Dim::Static(v) => v as u32,
9606 _ => 0,
9607 };
9608 let c_in = x_shape.dim(1).unwrap_static() as u32;
9609 let h = x_shape.dim(2).unwrap_static() as u32;
9610 let w = x_shape.dim(3).unwrap_static() as u32;
9611 let kh = kernel_size[0] as u32;
9612 let kw = kernel_size[1] as u32;
9613 let sh = stride.first().copied().unwrap_or(1) as u32;
9614 let sw = stride.get(1).copied().unwrap_or(1) as u32;
9615 let ph = padding.first().copied().unwrap_or(0) as u32;
9616 let pw = padding.get(1).copied().unwrap_or(0) as u32;
9617 let dh = dilation.first().copied().unwrap_or(1) as u32;
9618 let dw_dil = dilation.get(1).copied().unwrap_or(1) as u32;
9619 let h_out = rlx_ir::shape::conv2d_spatial_output(
9620 h as usize,
9621 kh as usize,
9622 sh as usize,
9623 ph as usize,
9624 dh as usize,
9625 ) as u32;
9626 let w_out = rlx_ir::shape::conv2d_spatial_output(
9627 w as usize,
9628 kw as usize,
9629 sw as usize,
9630 pw as usize,
9631 dw_dil as usize,
9632 ) as u32;
9633 Thunk::Im2Col {
9634 x: node_offset(arena, node.inputs[0]),
9635 col: node_offset(arena, node.id),
9636 n,
9637 c_in,
9638 h,
9639 w,
9640 h_out,
9641 w_out,
9642 kh,
9643 kw,
9644 sh,
9645 sw,
9646 ph,
9647 pw,
9648 dh,
9649 dw_dil,
9650 }
9651 } else {
9652 Thunk::Nop
9653 }
9654 }
9655}
9656
9657#[allow(unused_variables)]
9658fn compile_softmax_cross_entropy(
9659 node: &rlx_ir::Node,
9660 graph: &Graph,
9661 arena: &crate::arena::Arena,
9662 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9663 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9664 rng: rlx_ir::RngOptions,
9665) -> Thunk {
9666 let Op::SoftmaxCrossEntropy = &node.op else {
9667 unreachable!()
9668 };
9669 {
9670 let logits_shape = &graph.node(node.inputs[0]).shape;
9671 if logits_shape.rank() == 2 {
9672 Thunk::SoftmaxCrossEntropyDense {
9673 logits: node_offset(arena, node.inputs[0]),
9674 targets: node_offset(arena, node.inputs[1]),
9675 dst: node_offset(arena, node.id),
9676 n: logits_shape.dim(0).unwrap_static() as u32,
9677 c: logits_shape.dim(1).unwrap_static() as u32,
9678 }
9679 } else {
9680 Thunk::Nop
9681 }
9682 }
9683}
9684
9685#[allow(unused_variables)]
9686fn compile_softmax_cross_entropy_with_logits(
9687 node: &rlx_ir::Node,
9688 graph: &Graph,
9689 arena: &crate::arena::Arena,
9690 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9691 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9692 rng: rlx_ir::RngOptions,
9693) -> Thunk {
9694 let Op::SoftmaxCrossEntropyWithLogits = &node.op else {
9695 unreachable!()
9696 };
9697 {
9698 let logits_shape = &graph.node(node.inputs[0]).shape;
9699 if logits_shape.rank() == 2 {
9700 Thunk::SoftmaxCrossEntropy {
9701 logits: node_offset(arena, node.inputs[0]),
9702 labels: node_offset(arena, node.inputs[1]),
9703 dst: node_offset(arena, node.id),
9704 n: logits_shape.dim(0).unwrap_static() as u32,
9705 c: logits_shape.dim(1).unwrap_static() as u32,
9706 }
9707 } else {
9708 Thunk::Nop
9709 }
9710 }
9711}
9712
9713#[allow(unused_variables)]
9714fn compile_softmax_cross_entropy_backward(
9715 node: &rlx_ir::Node,
9716 graph: &Graph,
9717 arena: &crate::arena::Arena,
9718 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9719 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9720 rng: rlx_ir::RngOptions,
9721) -> Thunk {
9722 let Op::SoftmaxCrossEntropyBackward = &node.op else {
9723 unreachable!()
9724 };
9725 {
9726 let logits_shape = &graph.node(node.inputs[0]).shape;
9727 if logits_shape.rank() == 2 {
9728 Thunk::SoftmaxCrossEntropyBackward {
9729 logits: node_offset(arena, node.inputs[0]),
9730 labels: node_offset(arena, node.inputs[1]),
9731 d_loss: node_offset(arena, node.inputs[2]),
9732 dlogits: node_offset(arena, node.id),
9733 n: logits_shape.dim(0).unwrap_static() as u32,
9734 c: logits_shape.dim(1).unwrap_static() as u32,
9735 }
9736 } else {
9737 Thunk::Nop
9738 }
9739 }
9740}
9741
9742#[allow(unused_variables)]
9743fn compile_dense_solve(
9744 node: &rlx_ir::Node,
9745 graph: &Graph,
9746 arena: &crate::arena::Arena,
9747 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9748 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9749 rng: rlx_ir::RngOptions,
9750) -> Thunk {
9751 let Op::DenseSolve = &node.op else {
9752 unreachable!()
9753 };
9754 {
9755 let a_shape = &graph.node(node.inputs[0]).shape;
9757 let n = a_shape.dim(0).unwrap_static();
9758 debug_assert_eq!(
9759 n,
9760 a_shape.dim(1).unwrap_static(),
9761 "DenseSolve: A must be square"
9762 );
9763 let b_elems = node.shape.num_elements().unwrap();
9764 let nrhs = b_elems / n;
9765 match node.shape.dtype() {
9766 rlx_ir::DType::F64 => Thunk::DenseSolveF64 {
9767 a: node_offset(arena, node.inputs[0]),
9768 b: node_offset(arena, node.inputs[1]),
9769 x: node_offset(arena, node.id),
9770 n: n as u32,
9771 nrhs: nrhs as u32,
9772 },
9773 rlx_ir::DType::F32 => Thunk::DenseSolveF32 {
9774 a: node_offset(arena, node.inputs[0]),
9775 b: node_offset(arena, node.inputs[1]),
9776 x: node_offset(arena, node.id),
9777 n: n as u32,
9778 nrhs: nrhs as u32,
9779 },
9780 other => panic!(
9781 "DenseSolve: F32 + F64 lowered; got {other:?}. \
9782 Add another variant when needed."
9783 ),
9784 }
9785 }
9786}
9787
9788#[allow(unused_variables)]
9789fn compile_batched_dense_solve(
9790 node: &rlx_ir::Node,
9791 graph: &Graph,
9792 arena: &crate::arena::Arena,
9793 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9794 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9795 rng: rlx_ir::RngOptions,
9796) -> Thunk {
9797 let Op::BatchedDenseSolve = &node.op else {
9798 unreachable!()
9799 };
9800 {
9801 let a_shape = &graph.node(node.inputs[0]).shape;
9803 assert_eq!(a_shape.rank(), 3, "BatchedDenseSolve: A rank must be 3");
9804 let batch = a_shape.dim(0).unwrap_static();
9805 let n = a_shape.dim(1).unwrap_static();
9806 debug_assert_eq!(
9807 n,
9808 a_shape.dim(2).unwrap_static(),
9809 "BatchedDenseSolve: A's last two dims must match"
9810 );
9811 let total = node.shape.num_elements().unwrap();
9812 let nrhs = total / (batch * n);
9813 match node.shape.dtype() {
9814 rlx_ir::DType::F32 => Thunk::BatchedDenseSolveF32 {
9815 a: node_offset(arena, node.inputs[0]),
9816 b: node_offset(arena, node.inputs[1]),
9817 x: node_offset(arena, node.id),
9818 batch: batch as u32,
9819 n: n as u32,
9820 nrhs: nrhs as u32,
9821 },
9822 rlx_ir::DType::F64 => Thunk::BatchedDenseSolveF64 {
9823 a: node_offset(arena, node.inputs[0]),
9824 b: node_offset(arena, node.inputs[1]),
9825 x: node_offset(arena, node.id),
9826 batch: batch as u32,
9827 n: n as u32,
9828 nrhs: nrhs as u32,
9829 },
9830 other => panic!("BatchedDenseSolve: F32 + F64 only, got {other:?}"),
9831 }
9832 }
9833}
9834
9835#[allow(unused_variables)]
9836fn compile_scan(
9837 node: &rlx_ir::Node,
9838 graph: &Graph,
9839 arena: &crate::arena::Arena,
9840 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
9841 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
9842 rng: rlx_ir::RngOptions,
9843) -> Thunk {
9844 let Op::Scan {
9845 body,
9846 length,
9847 save_trajectory,
9848 num_bcast,
9849 num_xs,
9850 num_checkpoints,
9851 } = &node.op
9852 else {
9853 unreachable!()
9854 };
9855 {
9856 assert!(
9857 *num_checkpoints == 0 || *num_checkpoints <= *length,
9858 "Op::Scan: num_checkpoints={} must be 0 or ≤ length={}",
9859 *num_checkpoints,
9860 *length
9861 );
9862 if *num_checkpoints != 0 && *num_checkpoints != *length {
9863 assert!(
9864 *save_trajectory,
9865 "Op::Scan: num_checkpoints<length only meaningful when save_trajectory=true"
9866 );
9867 }
9868 let body_plan = rlx_opt::memory::plan_memory(body);
9879 let _body_arena_size = body_plan.arena_size;
9880 let body_offsets: HashMap<NodeId, usize> = body_plan
9883 .assignments
9884 .iter()
9885 .map(|(id, slot)| (*id, slot.offset))
9886 .collect();
9887
9888 let mut body_inputs: Vec<NodeId> = body
9891 .nodes()
9892 .iter()
9893 .filter(|n| matches!(n.op, Op::Input { .. }))
9894 .map(|n| n.id)
9895 .collect();
9896 body_inputs.sort();
9897 let n_body_inputs = body_inputs.len();
9898 let expected = 1 + *num_bcast as usize + *num_xs as usize;
9899 if n_body_inputs != expected {
9900 let names: Vec<String> = body
9901 .nodes()
9902 .iter()
9903 .filter_map(|n| match &n.op {
9904 Op::Input { name } => Some(format!("{}={}", n.id, name)),
9905 _ => None,
9906 })
9907 .collect();
9908 panic!(
9909 "Op::Scan body has {} Op::Input nodes; expected {} \
9910 (1 carry + {} bcast + {} xs). Inputs by NodeId: [{}]",
9911 n_body_inputs,
9912 expected,
9913 *num_bcast,
9914 *num_xs,
9915 names.join(", ")
9916 );
9917 }
9918
9919 let body_input_id = body_inputs[0];
9920 let body_input_off = body_offsets[&body_input_id];
9921 let body_output_id = body
9922 .outputs
9923 .first()
9924 .copied()
9925 .expect("Op::Scan body must declare one output");
9926 let body_output_off = body_offsets[&body_output_id];
9927
9928 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
9929 for n in body.nodes() {
9932 if let Op::Constant { data } = &n.op
9933 && body_arena.has_buffer(n.id)
9934 && !data.is_empty()
9935 {
9936 match n.shape.dtype() {
9937 rlx_ir::DType::F64 => {
9938 let off = body_arena.byte_offset(n.id);
9939 let buf = body_arena.raw_buf_mut();
9940 let nbytes = (buf.len() - off).min(data.len());
9941 buf[off..off + nbytes].copy_from_slice(&data[..nbytes]);
9942 }
9943 _ => {
9944 let buf = body_arena.slice_mut(n.id);
9945 let n_floats = data.len() / 4;
9946 let n_lim = buf.len().min(n_floats);
9947 for i in 0..n_lim {
9948 let bytes = [
9949 data[i * 4],
9950 data[i * 4 + 1],
9951 data[i * 4 + 2],
9952 data[i * 4 + 3],
9953 ];
9954 buf[i] = f32::from_le_bytes(bytes);
9955 }
9956 }
9957 }
9958 }
9959 }
9960 let body_init = body_arena.raw_buf().to_vec();
9961 let body_schedule = compile_thunks_with_rng(body, &body_arena, rng);
9962
9963 let carry_bytes = if *save_trajectory {
9968 let total = node
9969 .shape
9970 .size_bytes()
9971 .expect("Op::Scan trajectory output must have static shape");
9972 total / *length as usize
9973 } else {
9974 node.shape
9975 .size_bytes()
9976 .expect("Op::Scan carry must have static shape")
9977 };
9978
9979 let mut bcast_inputs: Vec<(usize, usize, u32)> = Vec::with_capacity(*num_bcast as usize);
9984 for i in 0..*num_bcast as usize {
9985 let body_b_id = body_inputs[1 + i];
9986 let body_b_off = body_offsets[&body_b_id];
9987 let outer_b_id = node.inputs[1 + i];
9988 let outer_b_off = node_offset(arena, outer_b_id);
9989 let outer_b_shape = &graph.node(outer_b_id).shape;
9990 let total = outer_b_shape
9991 .size_bytes()
9992 .expect("Op::Scan bcast must have static shape");
9993 bcast_inputs.push((body_b_off, outer_b_off, total as u32));
9994 }
9995
9996 let mut xs_inputs: Vec<(usize, usize, u32)> = Vec::with_capacity(*num_xs as usize);
10000 let xs_base = 1 + *num_bcast as usize;
10001 for i in 0..*num_xs as usize {
10002 let body_x_id = body_inputs[xs_base + i];
10003 let body_x_off = body_offsets[&body_x_id];
10004 let outer_xs_id = node.inputs[xs_base + i];
10005 let outer_xs_off = node_offset(arena, outer_xs_id);
10006 let outer_xs_shape = &graph.node(outer_xs_id).shape;
10007 let total = outer_xs_shape
10008 .size_bytes()
10009 .expect("Op::Scan xs must have static shape");
10010 let per_step = total / *length as usize;
10011 xs_inputs.push((body_x_off, outer_xs_off, per_step as u32));
10012 }
10013
10014 Thunk::Scan {
10015 body: Arc::new(body_schedule),
10016 body_init: Arc::new(body_init),
10017 body_input_off,
10018 body_output_off,
10019 outer_init_off: node_offset(arena, node.inputs[0]),
10020 outer_final_off: node_offset(arena, node.id),
10021 length: *length,
10022 carry_bytes: carry_bytes as u32,
10023 save_trajectory: *save_trajectory,
10024 xs_inputs: Arc::new(xs_inputs),
10025 bcast_inputs: Arc::new(bcast_inputs),
10026 num_checkpoints: *num_checkpoints,
10027 }
10028 }
10029}
10030
10031#[allow(unused_variables)]
10032fn compile_scan_backward(
10033 node: &rlx_ir::Node,
10034 graph: &Graph,
10035 arena: &crate::arena::Arena,
10036 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10037 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10038 rng: rlx_ir::RngOptions,
10039) -> Thunk {
10040 let Op::ScanBackward {
10041 body_vjp,
10042 length,
10043 save_trajectory,
10044 num_xs,
10045 num_checkpoints,
10046 forward_body,
10047 } = &node.op
10048 else {
10049 unreachable!()
10050 };
10051 {
10052 let is_recursive = *num_checkpoints != 0 && *num_checkpoints != *length;
10053 if is_recursive {
10054 assert!(
10055 forward_body.is_some(),
10056 "Op::ScanBackward with num_checkpoints<length requires forward_body"
10057 );
10058 }
10059 let body_plan = rlx_opt::memory::plan_memory(body_vjp);
10067 let body_offsets: HashMap<NodeId, usize> = body_plan
10068 .assignments
10069 .iter()
10070 .map(|(id, slot)| (*id, slot.offset))
10071 .collect();
10072 let mut body_d_output_off: Option<usize> = None;
10073 let mut body_other_inputs: Vec<(NodeId, usize)> = Vec::new();
10074 for n in body_vjp.nodes() {
10075 if let Op::Input { name } = &n.op {
10076 let off = body_offsets[&n.id];
10077 if name == "d_output" {
10078 body_d_output_off = Some(off);
10079 } else {
10080 body_other_inputs.push((n.id, off));
10081 }
10082 }
10083 }
10084 body_other_inputs.sort_by_key(|(id, _)| *id);
10085 let body_d_output_off =
10086 body_d_output_off.expect("ScanBackward body_vjp missing 'd_output' Input");
10087 let expected_others = 1 + *num_xs as usize;
10088 assert_eq!(
10089 body_other_inputs.len(),
10090 expected_others,
10091 "ScanBackward body_vjp has {} non-d_output Inputs; \
10092 expected {} (1 carry + {} xs)",
10093 body_other_inputs.len(),
10094 expected_others,
10095 num_xs
10096 );
10097 let body_carry_in_off = body_other_inputs[0].1;
10098 let body_x_offs: Vec<usize> = body_other_inputs
10099 .iter()
10100 .skip(1)
10101 .map(|(_, off)| *off)
10102 .collect();
10103 let body_dcarry_out_off = body_offsets[&body_vjp.outputs[0]];
10104
10105 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
10106 for n in body_vjp.nodes() {
10108 if let Op::Constant { data } = &n.op
10109 && body_arena.has_buffer(n.id)
10110 && !data.is_empty()
10111 {
10112 match n.shape.dtype() {
10113 rlx_ir::DType::F64 => {
10114 let off = body_arena.byte_offset(n.id);
10115 let buf = body_arena.raw_buf_mut();
10116 let nb = (buf.len() - off).min(data.len());
10117 buf[off..off + nb].copy_from_slice(&data[..nb]);
10118 }
10119 _ => {
10120 let buf = body_arena.slice_mut(n.id);
10121 let nf = data.len() / 4;
10122 let nl = buf.len().min(nf);
10123 for i in 0..nl {
10124 let bytes = [
10125 data[i * 4],
10126 data[i * 4 + 1],
10127 data[i * 4 + 2],
10128 data[i * 4 + 3],
10129 ];
10130 buf[i] = f32::from_le_bytes(bytes);
10131 }
10132 }
10133 }
10134 }
10135 }
10136 let body_init = body_arena.raw_buf().to_vec();
10137 let body_schedule = compile_thunks_with_rng(body_vjp, &body_arena, rng);
10138
10139 let carry_bytes = body_vjp
10141 .node(body_vjp.outputs[0])
10142 .shape
10143 .size_bytes()
10144 .expect("ScanBackward dcarry must be statically shaped");
10145 let carry_elem_size = body_vjp
10146 .node(body_vjp.outputs[0])
10147 .shape
10148 .dtype()
10149 .size_bytes() as u32;
10150
10151 let mut outer_xs_offs: Vec<(usize, u32)> = Vec::with_capacity(*num_xs as usize);
10154 for i in 0..*num_xs as usize {
10155 let outer_xs_id = node.inputs[3 + i];
10156 let outer_xs_off = node_offset(arena, outer_xs_id);
10157 let outer_xs_shape = &graph.node(outer_xs_id).shape;
10158 let total = outer_xs_shape
10159 .size_bytes()
10160 .expect("ScanBackward xs must have static shape");
10161 let per_step = total / *length as usize;
10162 outer_xs_offs.push((outer_xs_off, per_step as u32));
10163 }
10164
10165 let (fb_schedule, fb_init, fb_carry_in_off, fb_output_off, fb_x_offs) = if is_recursive {
10170 let fb = forward_body.as_ref().unwrap();
10171 let fb_plan = rlx_opt::memory::plan_memory(fb);
10172 let fb_offsets: HashMap<NodeId, usize> = fb_plan
10173 .assignments
10174 .iter()
10175 .map(|(id, slot)| (*id, slot.offset))
10176 .collect();
10177 let mut fb_inputs: Vec<NodeId> = fb
10178 .nodes()
10179 .iter()
10180 .filter(|n| matches!(n.op, Op::Input { .. }))
10181 .map(|n| n.id)
10182 .collect();
10183 fb_inputs.sort();
10184 let fb_carry = fb_offsets[&fb_inputs[0]];
10185 let fb_xs: Vec<usize> = (1..fb_inputs.len())
10186 .map(|i| fb_offsets[&fb_inputs[i]])
10187 .collect();
10188 let fb_out = fb_offsets[&fb.outputs[0]];
10189 let mut fb_arena = crate::arena::Arena::from_plan(fb_plan);
10190 for n in fb.nodes() {
10191 if let Op::Constant { data } = &n.op
10192 && fb_arena.has_buffer(n.id)
10193 && !data.is_empty()
10194 {
10195 let off = fb_arena.byte_offset(n.id);
10202 let buf = fb_arena.raw_buf_mut();
10203 let nb = (buf.len() - off).min(data.len());
10204 buf[off..off + nb].copy_from_slice(&data[..nb]);
10205 }
10206 }
10207 let fb_init_bytes = fb_arena.raw_buf().to_vec();
10208 let fb_sched = compile_thunks_with_rng(fb, &fb_arena, rng);
10209 (
10210 Some(Arc::new(fb_sched)),
10211 Some(Arc::new(fb_init_bytes)),
10212 fb_carry,
10213 fb_out,
10214 fb_xs,
10215 )
10216 } else {
10217 (None, None, 0, 0, Vec::new())
10218 };
10219
10220 Thunk::ScanBackward {
10221 body_vjp: Arc::new(body_schedule),
10222 body_init: Arc::new(body_init),
10223 body_carry_in_off,
10224 body_x_offs: Arc::new(body_x_offs),
10225 body_d_output_off,
10226 body_dcarry_out_off,
10227 outer_init_off: node_offset(arena, node.inputs[0]),
10228 outer_traj_off: node_offset(arena, node.inputs[1]),
10229 outer_upstream_off: node_offset(arena, node.inputs[2]),
10230 outer_xs_offs: Arc::new(outer_xs_offs),
10231 outer_dinit_off: node_offset(arena, node.id),
10232 length: *length,
10233 carry_bytes: carry_bytes as u32,
10234 carry_elem_size,
10235 save_trajectory: *save_trajectory,
10236 num_checkpoints: *num_checkpoints,
10237 forward_body: fb_schedule,
10238 forward_body_init: fb_init,
10239 forward_body_carry_in_off: fb_carry_in_off,
10240 forward_body_output_off: fb_output_off,
10241 forward_body_x_offs: Arc::new(fb_x_offs),
10242 }
10243 }
10244}
10245
10246#[allow(unused_variables)]
10247fn compile_scan_backward_xs(
10248 node: &rlx_ir::Node,
10249 graph: &Graph,
10250 arena: &crate::arena::Arena,
10251 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10252 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10253 rng: rlx_ir::RngOptions,
10254) -> Thunk {
10255 let Op::ScanBackwardXs {
10256 body_vjp,
10257 length,
10258 save_trajectory,
10259 num_xs,
10260 xs_idx,
10261 num_checkpoints,
10262 forward_body,
10263 } = &node.op
10264 else {
10265 unreachable!()
10266 };
10267 {
10268 assert!(
10269 *num_checkpoints == 0 || *num_checkpoints <= *length,
10270 "Op::ScanBackwardXs: num_checkpoints={} must be 0 or ≤ length={}",
10271 *num_checkpoints,
10272 *length
10273 );
10274 let is_recursive = *num_checkpoints != 0 && *num_checkpoints != *length;
10275 if is_recursive {
10276 assert!(
10277 forward_body.is_some(),
10278 "Op::ScanBackwardXs with num_checkpoints<length \
10279 requires forward_body"
10280 );
10281 }
10282 let body_plan = rlx_opt::memory::plan_memory(body_vjp);
10290 let body_offsets: HashMap<NodeId, usize> = body_plan
10291 .assignments
10292 .iter()
10293 .map(|(id, slot)| (*id, slot.offset))
10294 .collect();
10295 let mut body_d_output_off: Option<usize> = None;
10296 let mut body_other_inputs: Vec<(NodeId, usize)> = Vec::new();
10297 for n in body_vjp.nodes() {
10298 if let Op::Input { name } = &n.op {
10299 let off = body_offsets[&n.id];
10300 if name == "d_output" {
10301 body_d_output_off = Some(off);
10302 } else {
10303 body_other_inputs.push((n.id, off));
10304 }
10305 }
10306 }
10307 body_other_inputs.sort_by_key(|(id, _)| *id);
10308 let body_d_output_off =
10309 body_d_output_off.expect("ScanBackwardXs body_vjp missing 'd_output' Input");
10310 let expected_others = 1 + *num_xs as usize;
10311 assert_eq!(
10312 body_other_inputs.len(),
10313 expected_others,
10314 "ScanBackwardXs body_vjp has {} non-d_output Inputs; expected {}",
10315 body_other_inputs.len(),
10316 expected_others
10317 );
10318 let body_carry_in_off = body_other_inputs[0].1;
10319 let body_x_offs: Vec<usize> = body_other_inputs
10320 .iter()
10321 .skip(1)
10322 .map(|(_, off)| *off)
10323 .collect();
10324 let body_dcarry_out_off = body_offsets[&body_vjp.outputs[0]];
10325 let dxs_out_node = body_vjp.outputs[1 + *xs_idx as usize];
10326 let body_dxs_out_off = body_offsets[&dxs_out_node];
10327
10328 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
10329 for n in body_vjp.nodes() {
10330 if let Op::Constant { data } = &n.op
10331 && body_arena.has_buffer(n.id)
10332 && !data.is_empty()
10333 {
10334 match n.shape.dtype() {
10335 rlx_ir::DType::F64 => {
10336 let off = body_arena.byte_offset(n.id);
10337 let buf = body_arena.raw_buf_mut();
10338 let nb = (buf.len() - off).min(data.len());
10339 buf[off..off + nb].copy_from_slice(&data[..nb]);
10340 }
10341 _ => {
10342 let buf = body_arena.slice_mut(n.id);
10343 let nf = data.len() / 4;
10344 let nl = buf.len().min(nf);
10345 for i in 0..nl {
10346 let bytes = [
10347 data[i * 4],
10348 data[i * 4 + 1],
10349 data[i * 4 + 2],
10350 data[i * 4 + 3],
10351 ];
10352 buf[i] = f32::from_le_bytes(bytes);
10353 }
10354 }
10355 }
10356 }
10357 }
10358 let body_init = body_arena.raw_buf().to_vec();
10359 let body_schedule = compile_thunks_with_rng(body_vjp, &body_arena, rng);
10360
10361 let carry_bytes = body_vjp
10362 .node(body_vjp.outputs[0])
10363 .shape
10364 .size_bytes()
10365 .expect("ScanBackwardXs dcarry must be statically shaped");
10366 let carry_elem_size = body_vjp
10367 .node(body_vjp.outputs[0])
10368 .shape
10369 .dtype()
10370 .size_bytes() as u32;
10371 let per_step_bytes = body_vjp
10372 .node(dxs_out_node)
10373 .shape
10374 .size_bytes()
10375 .expect("ScanBackwardXs dxs body output must be statically shaped");
10376
10377 let mut outer_xs_offs: Vec<(usize, u32)> = Vec::with_capacity(*num_xs as usize);
10378 for i in 0..*num_xs as usize {
10379 let outer_xs_id = node.inputs[3 + i];
10380 let outer_xs_off = node_offset(arena, outer_xs_id);
10381 let outer_xs_shape = &graph.node(outer_xs_id).shape;
10382 let total = outer_xs_shape
10383 .size_bytes()
10384 .expect("ScanBackwardXs xs must have static shape");
10385 let per_step = total / *length as usize;
10386 outer_xs_offs.push((outer_xs_off, per_step as u32));
10387 }
10388
10389 let (fb_schedule, fb_init, fb_carry_in_off, fb_output_off, fb_x_offs) = if is_recursive {
10392 let fb = forward_body.as_ref().unwrap();
10393 let fb_plan = rlx_opt::memory::plan_memory(fb);
10394 let fb_offsets: HashMap<NodeId, usize> = fb_plan
10395 .assignments
10396 .iter()
10397 .map(|(id, slot)| (*id, slot.offset))
10398 .collect();
10399 let mut fb_inputs: Vec<NodeId> = fb
10400 .nodes()
10401 .iter()
10402 .filter(|n| matches!(n.op, Op::Input { .. }))
10403 .map(|n| n.id)
10404 .collect();
10405 fb_inputs.sort();
10406 let fb_carry = fb_offsets[&fb_inputs[0]];
10407 let fb_xs: Vec<usize> = (1..fb_inputs.len())
10408 .map(|i| fb_offsets[&fb_inputs[i]])
10409 .collect();
10410 let fb_out = fb_offsets[&fb.outputs[0]];
10411 let mut fb_arena = crate::arena::Arena::from_plan(fb_plan);
10412 for n in fb.nodes() {
10413 if let Op::Constant { data } = &n.op
10414 && fb_arena.has_buffer(n.id)
10415 && !data.is_empty()
10416 {
10417 let off = fb_arena.byte_offset(n.id);
10424 let buf = fb_arena.raw_buf_mut();
10425 let nb = (buf.len() - off).min(data.len());
10426 buf[off..off + nb].copy_from_slice(&data[..nb]);
10427 }
10428 }
10429 let fb_init_bytes = fb_arena.raw_buf().to_vec();
10430 let fb_sched = compile_thunks_with_rng(fb, &fb_arena, rng);
10431 (
10432 Some(Arc::new(fb_sched)),
10433 Some(Arc::new(fb_init_bytes)),
10434 fb_carry,
10435 fb_out,
10436 fb_xs,
10437 )
10438 } else {
10439 (None, None, 0, 0, Vec::new())
10440 };
10441
10442 Thunk::ScanBackwardXs {
10443 body_vjp: Arc::new(body_schedule),
10444 body_init: Arc::new(body_init),
10445 body_carry_in_off,
10446 body_x_offs: Arc::new(body_x_offs),
10447 body_d_output_off,
10448 body_dcarry_out_off,
10449 body_dxs_out_off,
10450 outer_init_off: node_offset(arena, node.inputs[0]),
10451 outer_traj_off: node_offset(arena, node.inputs[1]),
10452 outer_upstream_off: node_offset(arena, node.inputs[2]),
10453 outer_xs_offs: Arc::new(outer_xs_offs),
10454 outer_dxs_off: node_offset(arena, node.id),
10455 length: *length,
10456 carry_bytes: carry_bytes as u32,
10457 carry_elem_size,
10458 per_step_bytes: per_step_bytes as u32,
10459 save_trajectory: *save_trajectory,
10460 num_checkpoints: *num_checkpoints,
10461 forward_body: fb_schedule,
10462 forward_body_init: fb_init,
10463 forward_body_carry_in_off: fb_carry_in_off,
10464 forward_body_output_off: fb_output_off,
10465 forward_body_x_offs: Arc::new(fb_x_offs),
10466 }
10467 }
10468}
10469
10470#[allow(unused_variables)]
10471fn compile_concat(
10472 node: &rlx_ir::Node,
10473 graph: &Graph,
10474 arena: &crate::arena::Arena,
10475 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10476 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10477 rng: rlx_ir::RngOptions,
10478) -> Thunk {
10479 let Op::Concat { axis } = &node.op else {
10480 unreachable!()
10481 };
10482 {
10483 let out_shape = &node.shape;
10487 let rank = out_shape.rank();
10488 let outer: usize = (0..*axis)
10489 .map(|i| out_shape.dim(i).unwrap_static())
10490 .product::<usize>()
10491 .max(1);
10492 let inner: usize = (*axis + 1..rank)
10493 .map(|i| out_shape.dim(i).unwrap_static())
10494 .product::<usize>()
10495 .max(1);
10496 let total_axis = out_shape.dim(*axis).unwrap_static();
10497 let inputs: Vec<(usize, u32, u32)> = node
10498 .inputs
10499 .iter()
10500 .map(|&in_id| {
10501 let in_shape = &graph.node(in_id).shape;
10502 let in_axis = concat_axis_extent(in_shape, *axis, rank);
10503 let in_numel = in_shape.num_elements().unwrap_or(0) as u32;
10504 (node_offset(arena, in_id), in_axis as u32, in_numel)
10505 })
10506 .collect();
10507 let dst = node_offset(arena, node.id);
10508 match out_shape.dtype() {
10509 rlx_ir::DType::F64 => Thunk::ConcatF64 {
10510 dst,
10511 outer: outer as u32,
10512 inner: inner as u32,
10513 total_axis: total_axis as u32,
10514 inputs,
10515 },
10516 _ => Thunk::Concat {
10517 dst,
10518 outer: outer as u32,
10519 inner: inner as u32,
10520 total_axis: total_axis as u32,
10521 inputs,
10522 },
10523 }
10524 }
10525}
10526
10527#[allow(unused_variables)]
10528fn compile_gaussian_splat_render(
10529 node: &rlx_ir::Node,
10530 graph: &Graph,
10531 arena: &crate::arena::Arena,
10532 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10533 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10534 rng: rlx_ir::RngOptions,
10535) -> Thunk {
10536 let Op::GaussianSplatRender {
10537 width,
10538 height,
10539 tile_size,
10540 radius_scale,
10541 alpha_cutoff,
10542 max_splat_steps,
10543 transmittance_threshold,
10544 max_list_entries,
10545 } = &node.op
10546 else {
10547 unreachable!()
10548 };
10549 {
10550 let elem_len = |id: NodeId| -> usize { graph.node(id).shape.num_elements().unwrap_or(0) };
10551 Thunk::GaussianSplatRender {
10552 positions_off: node_offset(arena, node.inputs[0]),
10553 positions_len: elem_len(node.inputs[0]),
10554 scales_off: node_offset(arena, node.inputs[1]),
10555 scales_len: elem_len(node.inputs[1]),
10556 rotations_off: node_offset(arena, node.inputs[2]),
10557 rotations_len: elem_len(node.inputs[2]),
10558 opacities_off: node_offset(arena, node.inputs[3]),
10559 opacities_len: elem_len(node.inputs[3]),
10560 colors_off: node_offset(arena, node.inputs[4]),
10561 colors_len: elem_len(node.inputs[4]),
10562 sh_coeffs_off: node_offset(arena, node.inputs[5]),
10563 sh_coeffs_len: elem_len(node.inputs[5]),
10564 meta_off: node_offset(arena, node.inputs[6]),
10565 dst_off: node_offset(arena, node.id),
10566 dst_len: node.shape.num_elements().unwrap_or(0),
10567 width: *width,
10568 height: *height,
10569 tile_size: *tile_size,
10570 radius_scale: *radius_scale,
10571 alpha_cutoff: *alpha_cutoff,
10572 max_splat_steps: *max_splat_steps,
10573 transmittance_threshold: *transmittance_threshold,
10574 max_list_entries: *max_list_entries,
10575 }
10576 }
10577}
10578
10579#[allow(unused_variables)]
10580fn compile_gaussian_splat_render_backward(
10581 node: &rlx_ir::Node,
10582 graph: &Graph,
10583 arena: &crate::arena::Arena,
10584 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10585 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10586 rng: rlx_ir::RngOptions,
10587) -> Thunk {
10588 let Op::GaussianSplatRenderBackward {
10589 width,
10590 height,
10591 tile_size,
10592 radius_scale,
10593 alpha_cutoff,
10594 max_splat_steps,
10595 transmittance_threshold,
10596 max_list_entries,
10597 loss_grad_clip,
10598 sh_band,
10599 max_anisotropy,
10600 } = &node.op
10601 else {
10602 unreachable!()
10603 };
10604 {
10605 let elem_len = |id: NodeId| -> usize { graph.node(id).shape.num_elements().unwrap_or(0) };
10606 Thunk::GaussianSplatRenderBackward {
10607 positions_off: node_offset(arena, node.inputs[0]),
10608 positions_len: elem_len(node.inputs[0]),
10609 scales_off: node_offset(arena, node.inputs[1]),
10610 scales_len: elem_len(node.inputs[1]),
10611 rotations_off: node_offset(arena, node.inputs[2]),
10612 rotations_len: elem_len(node.inputs[2]),
10613 opacities_off: node_offset(arena, node.inputs[3]),
10614 opacities_len: elem_len(node.inputs[3]),
10615 colors_off: node_offset(arena, node.inputs[4]),
10616 colors_len: elem_len(node.inputs[4]),
10617 sh_coeffs_off: node_offset(arena, node.inputs[5]),
10618 sh_coeffs_len: elem_len(node.inputs[5]),
10619 meta_off: node_offset(arena, node.inputs[6]),
10620 d_loss_off: node_offset(arena, node.inputs[7]),
10621 d_loss_len: elem_len(node.inputs[7]),
10622 packed_off: node_offset(arena, node.id),
10623 packed_len: node.shape.num_elements().unwrap_or(0),
10624 width: *width,
10625 height: *height,
10626 tile_size: *tile_size,
10627 radius_scale: *radius_scale,
10628 alpha_cutoff: *alpha_cutoff,
10629 max_splat_steps: *max_splat_steps,
10630 transmittance_threshold: *transmittance_threshold,
10631 max_list_entries: *max_list_entries,
10632 loss_grad_clip: *loss_grad_clip,
10633 sh_band: *sh_band,
10634 max_anisotropy: *max_anisotropy,
10635 }
10636 }
10637}
10638
10639#[allow(unused_variables)]
10640fn compile_gaussian_splat_prepare(
10641 node: &rlx_ir::Node,
10642 graph: &Graph,
10643 arena: &crate::arena::Arena,
10644 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10645 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10646 rng: rlx_ir::RngOptions,
10647) -> Thunk {
10648 let Op::GaussianSplatPrepare {
10649 width,
10650 height,
10651 tile_size,
10652 radius_scale,
10653 alpha_cutoff,
10654 max_splat_steps,
10655 transmittance_threshold,
10656 max_list_entries,
10657 } = &node.op
10658 else {
10659 unreachable!()
10660 };
10661 {
10662 let elem_len = |id: NodeId| -> usize { graph.node(id).shape.num_elements().unwrap_or(0) };
10663 Thunk::GaussianSplatPrepare {
10664 positions_off: node_offset(arena, node.inputs[0]),
10665 positions_len: elem_len(node.inputs[0]),
10666 scales_off: node_offset(arena, node.inputs[1]),
10667 scales_len: elem_len(node.inputs[1]),
10668 rotations_off: node_offset(arena, node.inputs[2]),
10669 rotations_len: elem_len(node.inputs[2]),
10670 opacities_off: node_offset(arena, node.inputs[3]),
10671 opacities_len: elem_len(node.inputs[3]),
10672 colors_off: node_offset(arena, node.inputs[4]),
10673 colors_len: elem_len(node.inputs[4]),
10674 sh_coeffs_off: node_offset(arena, node.inputs[5]),
10675 sh_coeffs_len: elem_len(node.inputs[5]),
10676 meta_off: node_offset(arena, node.inputs[6]),
10677 meta_len: elem_len(node.inputs[6]),
10678 prep_off: node_offset(arena, node.id),
10679 prep_len: node.shape.num_elements().unwrap_or(0),
10680 width: *width,
10681 height: *height,
10682 tile_size: *tile_size,
10683 radius_scale: *radius_scale,
10684 alpha_cutoff: *alpha_cutoff,
10685 max_splat_steps: *max_splat_steps,
10686 transmittance_threshold: *transmittance_threshold,
10687 max_list_entries: *max_list_entries,
10688 }
10689 }
10690}
10691
10692#[allow(unused_variables)]
10693fn compile_gaussian_splat_rasterize(
10694 node: &rlx_ir::Node,
10695 graph: &Graph,
10696 arena: &crate::arena::Arena,
10697 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10698 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10699 rng: rlx_ir::RngOptions,
10700) -> Thunk {
10701 let Op::GaussianSplatRasterize {
10702 width,
10703 height,
10704 tile_size,
10705 alpha_cutoff,
10706 max_splat_steps,
10707 transmittance_threshold,
10708 max_list_entries,
10709 } = &node.op
10710 else {
10711 unreachable!()
10712 };
10713 {
10714 let elem_len = |id: NodeId| -> usize { graph.node(id).shape.num_elements().unwrap_or(0) };
10715 let prep_id = node.inputs[0];
10716 let count = match &graph.node(prep_id).op {
10717 rlx_ir::Op::GaussianSplatPrepare { .. } => elem_len(graph.node(prep_id).inputs[0]) / 3,
10718 _ => 1,
10719 };
10720 Thunk::GaussianSplatRasterize {
10721 prep_off: node_offset(arena, prep_id),
10722 prep_len: elem_len(prep_id),
10723 meta_off: node_offset(arena, node.inputs[1]),
10724 meta_len: elem_len(node.inputs[1]),
10725 dst_off: node_offset(arena, node.id),
10726 dst_len: node.shape.num_elements().unwrap_or(0),
10727 count,
10728 width: *width,
10729 height: *height,
10730 tile_size: *tile_size,
10731 alpha_cutoff: *alpha_cutoff,
10732 max_splat_steps: *max_splat_steps,
10733 transmittance_threshold: *transmittance_threshold,
10734 max_list_entries: *max_list_entries,
10735 }
10736 }
10737}
10738
10739#[allow(unused_variables)]
10740fn compile_custom(
10741 node: &rlx_ir::Node,
10742 graph: &Graph,
10743 arena: &crate::arena::Arena,
10744 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10745 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10746 rng: rlx_ir::RngOptions,
10747) -> Thunk {
10748 let Op::Custom { name, attrs, .. } = &node.op else {
10749 unreachable!()
10750 };
10751 {
10752 let kernel = crate::op_registry::lookup_cpu_kernel(name).unwrap_or_else(|| {
10753 panic!(
10754 "compile_thunks: no CPU kernel registered for \
10755 Op::Custom('{name}'). Register one via \
10756 rlx_cpu::op_registry::register_cpu_kernel \
10757 before compiling on the CPU backend."
10758 )
10759 });
10760 let inputs_v: Vec<(usize, u32, Shape)> = node
10761 .inputs
10762 .iter()
10763 .map(|&in_id| {
10764 let s = graph.node(in_id).shape.clone();
10765 let len = s.num_elements().unwrap_or(0) as u32;
10766 (node_offset(arena, in_id), len, s)
10767 })
10768 .collect();
10769 let out_len = node.shape.num_elements().unwrap_or(0) as u32;
10770 Thunk::CustomOp {
10771 kernel,
10772 inputs: inputs_v,
10773 output: (node_offset(arena, node.id), out_len, node.shape.clone()),
10774 attrs: attrs.clone(),
10775 }
10776 }
10777}
10778
10779#[allow(unused_variables)]
10780fn compile_fft(
10781 node: &rlx_ir::Node,
10782 graph: &Graph,
10783 arena: &crate::arena::Arena,
10784 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10785 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10786 rng: rlx_ir::RngOptions,
10787) -> Thunk {
10788 let Op::Fft { inverse, norm } = &node.op else {
10789 unreachable!()
10790 };
10791 {
10792 let shape = &node.shape;
10793 let meta = rlx_ir::fft::fft_meta(shape);
10794 let dtype = shape.dtype();
10795 assert!(
10796 matches!(
10797 dtype,
10798 rlx_ir::DType::F32 | rlx_ir::DType::F64 | rlx_ir::DType::C64
10799 ),
10800 "Op::Fft on CPU requires F32, F64, or C64, got {dtype:?}"
10801 );
10802 Thunk::Fft1d {
10803 src: node_offset(arena, node.inputs[0]),
10804 dst: node_offset(arena, node.id),
10805 outer: meta.outer as u32,
10806 n_complex: meta.n_complex as u32,
10807 inverse: *inverse,
10808 norm_tag: norm.tag(),
10809 dtype,
10810 }
10811 }
10812}
10813
10814#[allow(unused_variables)]
10815fn compile_fft_butterfly_stage(
10816 node: &rlx_ir::Node,
10817 graph: &Graph,
10818 arena: &crate::arena::Arena,
10819 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10820 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10821 rng: rlx_ir::RngOptions,
10822) -> Thunk {
10823 let Op::FftButterflyStage { stage, n_fft } = &node.op else {
10824 unreachable!()
10825 };
10826 {
10827 let state_shape = graph.node(node.inputs[0]).shape.clone();
10828 assert_eq!(
10829 state_shape.dtype(),
10830 rlx_ir::DType::F32,
10831 "Op::FftButterflyStage requires F32 state"
10832 );
10833 let batch = state_shape.dim(0).unwrap_static() as u32;
10834 Thunk::FftButterflyStage {
10835 state_src: node_offset(arena, node.inputs[0]),
10836 state_dst: node_offset(arena, node.id),
10837 gate_src: node_offset(arena, node.inputs[1]),
10838 rev_src: node_offset(arena, node.inputs[2]),
10839 tw_re_src: node_offset(arena, node.inputs[3]),
10840 tw_im_src: node_offset(arena, node.inputs[4]),
10841 batch,
10842 n_fft: *n_fft,
10843 stage: *stage,
10844 }
10845 }
10846}
10847
10848#[allow(unused_variables)]
10849fn compile_log_mel(
10850 node: &rlx_ir::Node,
10851 graph: &Graph,
10852 arena: &crate::arena::Arena,
10853 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10854 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10855 rng: rlx_ir::RngOptions,
10856) -> Thunk {
10857 let Op::LogMel = &node.op else { unreachable!() };
10858 {
10859 let spec_shape = graph.node(node.inputs[0]).shape.clone();
10860 let filt_shape = graph.node(node.inputs[1]).shape.clone();
10861 let meta = rlx_ir::audio::log_mel_meta(&spec_shape, &filt_shape)
10862 .unwrap_or_else(|e| panic!("Op::LogMel: {e}"));
10863 Thunk::LogMel {
10864 spec: node_offset(arena, node.inputs[0]),
10865 filters: node_offset(arena, node.inputs[1]),
10866 dst: node_offset(arena, node.id),
10867 outer: meta.outer as u32,
10868 n_fft: meta.n_fft as u32,
10869 n_bins: meta.n_bins as u32,
10870 n_mels: meta.n_mels as u32,
10871 }
10872 }
10873}
10874
10875#[allow(unused_variables)]
10876fn compile_log_mel_backward(
10877 node: &rlx_ir::Node,
10878 graph: &Graph,
10879 arena: &crate::arena::Arena,
10880 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10881 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10882 rng: rlx_ir::RngOptions,
10883) -> Thunk {
10884 let Op::LogMelBackward = &node.op else {
10885 unreachable!()
10886 };
10887 {
10888 let spec_shape = graph.node(node.inputs[0]).shape.clone();
10889 let filt_shape = graph.node(node.inputs[1]).shape.clone();
10890 let meta = rlx_ir::audio::log_mel_meta(&spec_shape, &filt_shape)
10891 .unwrap_or_else(|e| panic!("Op::LogMelBackward: {e}"));
10892 Thunk::LogMelBackward {
10893 spec: node_offset(arena, node.inputs[0]),
10894 filters: node_offset(arena, node.inputs[1]),
10895 dy: node_offset(arena, node.inputs[2]),
10896 dst: node_offset(arena, node.id),
10897 outer: meta.outer as u32,
10898 n_fft: meta.n_fft as u32,
10899 n_bins: meta.n_bins as u32,
10900 n_mels: meta.n_mels as u32,
10901 }
10902 }
10903}
10904
10905#[allow(unused_variables)]
10906fn compile_welch_peaks(
10907 node: &rlx_ir::Node,
10908 graph: &Graph,
10909 arena: &crate::arena::Arena,
10910 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10911 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10912 rng: rlx_ir::RngOptions,
10913) -> Thunk {
10914 let Op::WelchPeaks { k, n_segments } = &node.op else {
10915 unreachable!()
10916 };
10917 {
10918 let spec_shape = graph.node(node.inputs[0]).shape.clone();
10919 let meta = rlx_ir::audio::welch_peaks_meta(&spec_shape, *k, *n_segments)
10920 .unwrap_or_else(|e| panic!("Op::WelchPeaks: {e}"));
10921 Thunk::WelchPeaks {
10922 spec: node_offset(arena, node.inputs[0]),
10923 dst: node_offset(arena, node.id),
10924 welch_batch: meta.welch_batch as u32,
10925 n_fft: meta.n_fft as u32,
10926 n_segments: meta.n_segments as u32,
10927 k: meta.k as u32,
10928 }
10929 }
10930}
10931
10932#[allow(unused_variables)]
10933fn compile_custom_fn(
10934 node: &rlx_ir::Node,
10935 graph: &Graph,
10936 arena: &crate::arena::Arena,
10937 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
10938 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
10939 rng: rlx_ir::RngOptions,
10940) -> Thunk {
10941 let Op::CustomFn {
10942 fwd_body,
10943 num_inputs,
10944 ..
10945 } = &node.op
10946 else {
10947 unreachable!()
10948 };
10949 {
10950 let body_plan = rlx_opt::memory::plan_memory(fwd_body);
10956 let body_offsets: HashMap<NodeId, usize> = body_plan
10957 .assignments
10958 .iter()
10959 .map(|(id, slot)| (*id, slot.offset))
10960 .collect();
10961
10962 let mut body_input_ids: Vec<NodeId> = fwd_body
10963 .nodes()
10964 .iter()
10965 .filter(|n| matches!(n.op, Op::Input { .. }))
10966 .map(|n| n.id)
10967 .collect();
10968 body_input_ids.sort();
10969 assert_eq!(
10970 body_input_ids.len(),
10971 *num_inputs as usize,
10972 "Op::CustomFn fwd_body has {} Op::Input(s); declared num_inputs={}",
10973 body_input_ids.len(),
10974 *num_inputs,
10975 );
10976
10977 let mut body_arena = crate::arena::Arena::from_plan(body_plan);
10978 for n in fwd_body.nodes() {
10979 if let Op::Constant { data } = &n.op
10980 && body_arena.has_buffer(n.id)
10981 && !data.is_empty()
10982 {
10983 match n.shape.dtype() {
10984 rlx_ir::DType::F64 => {
10985 let off = body_arena.byte_offset(n.id);
10986 let buf = body_arena.raw_buf_mut();
10987 let nb = (buf.len() - off).min(data.len());
10988 buf[off..off + nb].copy_from_slice(&data[..nb]);
10989 }
10990 _ => {
10991 let buf = body_arena.slice_mut(n.id);
10992 let nf = data.len() / 4;
10993 let nl = buf.len().min(nf);
10994 for i in 0..nl {
10995 let bytes = [
10996 data[i * 4],
10997 data[i * 4 + 1],
10998 data[i * 4 + 2],
10999 data[i * 4 + 3],
11000 ];
11001 buf[i] = f32::from_le_bytes(bytes);
11002 }
11003 }
11004 }
11005 }
11006 }
11007 let body_init = body_arena.raw_buf().to_vec();
11008 let body_schedule = compile_thunks_with_rng(fwd_body, &body_arena, rng);
11009
11010 let inputs_v: Vec<(usize, usize, u32)> = (0..*num_inputs as usize)
11012 .map(|i| {
11013 let body_in = body_input_ids[i];
11014 let body_off = body_offsets[&body_in];
11015 let outer_in = node.inputs[i];
11016 let outer_off = node_offset(arena, outer_in);
11017 let bytes = graph
11018 .node(outer_in)
11019 .shape
11020 .size_bytes()
11021 .expect("Op::CustomFn primal input must have static shape");
11022 (body_off, outer_off, bytes as u32)
11023 })
11024 .collect();
11025
11026 let body_output_id = fwd_body
11027 .outputs
11028 .first()
11029 .copied()
11030 .expect("Op::CustomFn fwd_body must declare exactly one output");
11031 let body_output_off = body_offsets[&body_output_id];
11032 let out_bytes = node
11033 .shape
11034 .size_bytes()
11035 .expect("Op::CustomFn output must have static shape");
11036
11037 Thunk::CustomFn {
11038 body: Arc::new(body_schedule),
11039 body_init: Arc::new(body_init),
11040 inputs: Arc::new(inputs_v),
11041 body_output_off,
11042 outer_output_off: node_offset(arena, node.id),
11043 out_bytes: out_bytes as u32,
11044 }
11045 }
11046}
11047
11048#[allow(unused_variables)]
11049fn compile_elementwise_region(
11050 node: &rlx_ir::Node,
11051 graph: &Graph,
11052 arena: &crate::arena::Arena,
11053 matmul_fold: &std::collections::HashMap<NodeId, (NodeId, bool, NodeId, bool)>,
11054 rng_shared: &std::sync::Arc<std::sync::RwLock<rlx_ir::RngOptions>>,
11055 rng: rlx_ir::RngOptions,
11056) -> Thunk {
11057 let Op::ElementwiseRegion {
11058 chain,
11059 scalar_input_mask,
11060 input_modulus,
11061 prologue,
11062 ..
11063 } = &node.op
11064 else {
11065 unreachable!()
11066 };
11067 {
11068 if *prologue != rlx_ir::op::RegionPrologue::None {
11072 Thunk::Nop
11073 } else {
11074 let input_offs: Vec<usize> = node
11075 .inputs
11076 .iter()
11077 .map(|&id| node_offset(arena, id))
11078 .collect();
11079 Thunk::ElementwiseRegion {
11080 dst: node_offset(arena, node.id),
11081 len: node.shape.num_elements().unwrap_or(0) as u32,
11082 input_offs,
11083 chain: chain.clone(),
11084 scalar_input_mask: *scalar_input_mask,
11085 input_modulus: *input_modulus,
11086 }
11087 }
11088 }
11089}
11090
11091fn get_len(graph: &Graph, id: NodeId) -> usize {
11092 graph.node(id).shape.num_elements().unwrap_or(0)
11093}
11094
11095fn get_static_dims(graph: &Graph, id: NodeId) -> Vec<usize> {
11097 let dims = graph.node(id).shape.dims();
11098 let mut out = Vec::with_capacity(dims.len());
11099 for d in dims {
11100 if let Some(s) = match d {
11101 rlx_ir::Dim::Static(s) => Some(*s),
11102 _ => None,
11103 } {
11104 out.push(s);
11105 } else {
11106 return Vec::new();
11107 }
11108 }
11109 out
11110}
11111
11112fn concat_axis_extent(input: &rlx_ir::Shape, axis: usize, out_rank: usize) -> usize {
11115 let in_rank = input.rank();
11116 if axis >= out_rank {
11117 return 1;
11118 }
11119 if axis < in_rank {
11120 input.dim(axis).unwrap_static()
11121 } else {
11122 1
11123 }
11124}
11125
11126fn broadcast_src_index(src_idx: usize, in_len: usize) -> usize {
11127 if in_len == 0 { 0 } else { src_idx % in_len }
11128}
11129
11130fn concat_copy_rows_f32(
11131 out: &mut [f32],
11132 inp: &[f32],
11133 outer: usize,
11134 copy_per_row: usize,
11135 row_stride: usize,
11136 dst_col_off: usize,
11137 in_numel: usize,
11138) {
11139 let need = outer.saturating_mul(copy_per_row.max(1));
11140 let broadcast_outer = in_numel < need;
11141 for o in 0..outer {
11142 let dst_row_start = o * row_stride + dst_col_off;
11143 if broadcast_outer {
11144 if in_numel == 1 {
11145 if copy_per_row == 1 {
11146 out[dst_row_start] = inp[0];
11147 } else {
11148 out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
11149 }
11150 } else if copy_per_row <= inp.len() {
11151 out[dst_row_start..dst_row_start + copy_per_row]
11152 .copy_from_slice(&inp[..copy_per_row]);
11153 } else if !inp.is_empty() {
11154 out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
11155 }
11156 } else {
11157 let src_row_start = o * copy_per_row;
11158 out[dst_row_start..dst_row_start + copy_per_row]
11159 .copy_from_slice(&inp[src_row_start..src_row_start + copy_per_row]);
11160 }
11161 }
11162}
11163
11164fn concat_copy_rows_f64(
11165 out: &mut [f64],
11166 inp: &[f64],
11167 outer: usize,
11168 copy_per_row: usize,
11169 row_stride: usize,
11170 dst_col_off: usize,
11171 in_numel: usize,
11172) {
11173 let need = outer.saturating_mul(copy_per_row.max(1));
11174 let broadcast_outer = in_numel < need;
11175 for o in 0..outer {
11176 let dst_row_start = o * row_stride + dst_col_off;
11177 if broadcast_outer {
11178 if in_numel == 1 {
11179 if copy_per_row == 1 {
11180 out[dst_row_start] = inp[0];
11181 } else {
11182 out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
11183 }
11184 } else if copy_per_row <= inp.len() {
11185 out[dst_row_start..dst_row_start + copy_per_row]
11186 .copy_from_slice(&inp[..copy_per_row]);
11187 } else if !inp.is_empty() {
11188 out[dst_row_start..dst_row_start + copy_per_row].fill(inp[0]);
11189 }
11190 } else {
11191 let src_row_start = o * copy_per_row;
11192 out[dst_row_start..dst_row_start + copy_per_row]
11193 .copy_from_slice(&inp[src_row_start..src_row_start + copy_per_row]);
11194 }
11195 }
11196}
11197
11198fn is_trailing_bias_broadcast(rhs_dims: &[rlx_ir::Dim], out_dims: &[rlx_ir::Dim]) -> bool {
11216 if rhs_dims.len() > out_dims.len() {
11217 return false;
11218 }
11219 let off = out_dims.len() - rhs_dims.len();
11220 for i in 0..rhs_dims.len() {
11221 let r = match rhs_dims[i] {
11222 rlx_ir::Dim::Static(n) => n,
11223 _ => return false,
11224 };
11225 let o = match out_dims[off + i] {
11226 rlx_ir::Dim::Static(n) => n,
11227 _ => return false,
11228 };
11229 if r != o {
11230 return false;
11231 }
11232 }
11233 true
11234}
11235
11236fn broadcast_strides(in_dims: &[usize], out_dims: &[usize]) -> Vec<u32> {
11237 let r_out = out_dims.len();
11238 let r_in = in_dims.len();
11239 assert!(
11240 r_in <= r_out,
11241 "broadcast: input rank {r_in} > output rank {r_out}"
11242 );
11243 let pad = r_out - r_in;
11244 let mut strides = vec![0u32; r_out];
11245 let mut acc: usize = 1;
11246 for d in (0..r_out).rev() {
11247 let in_size = if d < pad { 1 } else { in_dims[d - pad] };
11248 if in_size == 1 {
11249 strides[d] = 0;
11250 } else {
11251 assert_eq!(
11252 in_size, out_dims[d],
11253 "broadcast: input dim {in_size} doesn't match output dim {} at axis {d}",
11254 out_dims[d]
11255 );
11256 strides[d] = acc as u32;
11257 acc *= in_size;
11258 }
11259 }
11260 strides
11261}
11262
11263pub fn execute_compiled(schedule: &ThunkSchedule, arena_buf: &mut [u8]) {
11267 let base = arena_buf.as_mut_ptr();
11268 for f in &schedule.compiled_fns {
11269 f(base);
11270 }
11271}
11272
11273pub fn execute_thunks_active(
11278 schedule: &ThunkSchedule,
11279 _arena_buf: &mut [u8],
11280 _actual: usize,
11281 _upper: usize,
11282) -> bool {
11283 let _ = schedule;
11284 false
11285}
11286
11287struct MoeResidencyGuard;
11289impl Drop for MoeResidencyGuard {
11290 fn drop(&mut self) {
11291 if let Some(stats) = crate::moe_residency::take_stats() {
11292 crate::moe_residency::stash_last_forward_stats(stats);
11293 } else {
11294 crate::moe_residency::clear_mask();
11295 }
11296 }
11297}
11298
11299fn thunk_kind_name(t: &Thunk) -> &'static str {
11300 match t {
11301 Thunk::Nop => "Nop",
11302 Thunk::Gather { .. } => "Gather",
11303 Thunk::GatherAxis { .. } => "GatherAxis",
11304 Thunk::TopK { .. } => "TopK",
11305 Thunk::Copy { .. } => "Copy",
11306 Thunk::CopyF64 { .. } => "CopyF64",
11307 Thunk::CopyI64 { .. } => "CopyI64",
11308 Thunk::CastF32ToI64 { .. } => "CastF32ToI64",
11309 Thunk::CastI64ToF32 { .. } => "CastI64ToF32",
11310 Thunk::CastBoolToI32 { .. } => "CastBoolToI32",
11311 Thunk::CastBoolToF32 { .. } => "CastBoolToF32",
11312 Thunk::CastI32ToF32 { .. } => "CastI32ToF32",
11313 Thunk::Transpose { .. } => "Transpose",
11314 Thunk::TransposeF64 { .. } => "TransposeF64",
11315 Thunk::Where { .. } => "Where",
11316 Thunk::Fma { .. } => "Fma",
11317 Thunk::Compare { .. } => "Compare",
11318 Thunk::BinaryFull { .. } => "BinaryFull",
11319 Thunk::BinaryFullF64 { .. } => "BinaryFullF64",
11320 Thunk::Sgemm { .. } => "Sgemm",
11321 Thunk::SgemmT { .. } => "SgemmT",
11322 Thunk::SgdMomentum { .. } => "SgdMomentum",
11323 Thunk::Dgemm { .. } => "Dgemm",
11324 Thunk::FusedMmBiasAct { .. } => "FusedMmBiasAct",
11325 Thunk::BiasAdd { .. } => "BiasAdd",
11326 Thunk::LayerNorm { .. } => "LayerNorm",
11327 Thunk::Softmax { .. } => "Softmax",
11328 Thunk::Conv2D { .. } => "Conv2D",
11329 Thunk::Conv2D1x1 { .. } => "Conv2D1x1",
11330 Thunk::Conv3d { .. } => "Conv3d",
11331 Thunk::ConvTranspose3d { .. } => "ConvTranspose3d",
11332 Thunk::CustomOp { .. } => "CustomOp",
11333 Thunk::ActivationInPlace { .. } => "ActivationInPlace",
11334 Thunk::Narrow { .. } => "Narrow",
11335 Thunk::Cumsum { .. } => "Cumsum",
11336 Thunk::Reduce { .. } => "Reduce",
11337 Thunk::BatchedSgemm { .. } => "BatchedSgemm",
11338 Thunk::DequantMatMul { .. } => "DequantMatMul",
11339 Thunk::Quantize { .. } => "Quantize",
11340 Thunk::Dequantize { .. } => "Dequantize",
11341 Thunk::ConvTranspose2d { .. } => "ConvTranspose2d",
11342 Thunk::ResizeNearest2x { .. } => "ResizeNearest2x",
11343 Thunk::ElementwiseRegion { .. } => "ElementwiseRegion",
11344 Thunk::Conv2dBackwardInput { .. } => "Conv2dBackwardInput",
11345 Thunk::Conv2dBackwardWeight { .. } => "Conv2dBackwardWeight",
11346 Thunk::Pool2D { .. } => "Pool2D",
11347 Thunk::MaxPool2dBackward { .. } => "MaxPool2dBackward",
11348 Thunk::ReluBackward { .. } => "ReluBackward",
11349 Thunk::ActivationBackward { .. } => "ActivationBackward",
11350 Thunk::Im2Col { .. } => "Im2Col",
11351 Thunk::SoftmaxCrossEntropyDense { .. } => "SoftmaxCrossEntropyDense",
11352 Thunk::SoftmaxCrossEntropy { .. } => "SoftmaxCrossEntropy",
11353 Thunk::SoftmaxCrossEntropyBackward { .. } => "SoftmaxCrossEntropyBackward",
11354 _ => "Other",
11355 }
11356}
11357
11358static THUNK_PROFILE: std::sync::Mutex<
11362 Option<std::collections::BTreeMap<&'static str, (u128, u64)>>,
11363> = std::sync::Mutex::new(None);
11364
11365#[inline]
11366fn profile_record(name: &'static str, d: std::time::Duration) {
11367 let mut g = THUNK_PROFILE.lock().unwrap();
11368 let map = g.get_or_insert_with(std::collections::BTreeMap::new);
11369 let e = map.entry(name).or_insert((0, 0));
11370 e.0 += d.as_nanos();
11371 e.1 += 1;
11372}
11373
11374pub fn dump_thunk_profile() {
11377 let mut g = THUNK_PROFILE.lock().unwrap();
11378 if let Some(map) = g.take() {
11379 let mut v: Vec<_> = map.into_iter().collect();
11380 v.sort_by_key(|b| std::cmp::Reverse(b.1.0));
11381 let total: u128 = v.iter().map(|(_, (ns, _))| *ns).sum();
11382 eprintln!(
11383 "[thunk-profile] total {:.1}ms across kinds:",
11384 total as f64 / 1e6
11385 );
11386 for (name, (ns, c)) in v.iter().take(25) {
11387 eprintln!(" {name:<28} {:>8.1}ms ({c} calls)", *ns as f64 / 1e6);
11388 }
11389 }
11390}
11391
11392pub fn execute_thunks(schedule: &ThunkSchedule, arena_buf: &mut [u8]) {
11393 crate::moe_residency::reset_gmm_counters();
11394 if let Some(layers) = schedule.moe_resident_layers.clone() {
11395 crate::moe_residency::set_per_layer_masks(Some(layers));
11396 } else {
11397 crate::moe_residency::set_mask(schedule.moe_resident.clone());
11398 }
11399 if let Some(cap) = schedule.moe_topk_capture.as_ref() {
11400 cap.clear();
11401 }
11402 let _moe_guard = MoeResidencyGuard;
11403 let base = arena_buf.as_mut_ptr();
11404 let mask_thr = schedule.mask_threshold;
11405 let mask_neg = schedule.mask_neg_inf;
11406 let score_thr = schedule.score_skip;
11407 let thunks = &schedule.thunks;
11408 let len = thunks.len();
11409
11410 let max_h = thunks
11412 .iter()
11413 .filter_map(|t| match t {
11414 Thunk::FusedResidualLN { h, .. }
11415 | Thunk::FusedResidualRmsNorm { h, .. }
11416 | Thunk::LayerNorm { h, .. } => Some(*h as usize),
11417 _ => None,
11418 })
11419 .max()
11420 .unwrap_or(0);
11421 let zero_bias = vec![0f32; max_h];
11422
11423 let max_sdpa = thunks
11426 .iter()
11427 .filter_map(|t| match t {
11428 Thunk::Attention {
11429 batch,
11430 seq,
11431 kv_seq,
11432 heads,
11433 head_dim,
11434 ..
11435 } => Some((
11436 *batch as usize,
11437 (*seq as usize).max(*kv_seq as usize),
11438 *heads as usize,
11439 *head_dim as usize,
11440 )),
11441 _ => None,
11442 })
11443 .fold((0, 0, 0, 0), |(mb, ms, mh, md), (b, s, h, d)| {
11444 (mb.max(b), ms.max(s), mh.max(h), md.max(d))
11445 });
11446 let (max_batch, max_seq, max_heads, _max_dh) = max_sdpa;
11447 let max_units = max_batch * max_heads;
11448 let mut sdpa_scores = vec![0f32; max_units * max_seq * max_seq];
11449
11450 let fl = thunks
11452 .iter()
11453 .filter_map(|t| match t {
11454 Thunk::FusedBertLayer {
11455 batch,
11456 seq,
11457 hs,
11458 int_dim,
11459 ..
11460 } => {
11461 let m = (*batch as usize) * (*seq as usize);
11462 let h = *hs as usize;
11463 let id = *int_dim as usize;
11464 Some((m, h, id, m * (*seq as usize)))
11465 }
11466 Thunk::FusedNomicLayer {
11467 batch,
11468 seq,
11469 hs,
11470 int_dim,
11471 ..
11472 } => {
11473 let m = (*batch as usize) * (*seq as usize);
11474 let h = *hs as usize;
11475 let id = *int_dim as usize;
11476 Some((m, h, id, m * (*seq as usize)))
11477 }
11478 _ => None,
11479 })
11480 .fold((0, 0, 0, 0), |(mm, mh, mi, ms), (m, h, id, ss)| {
11481 (mm.max(m), mh.max(h), mi.max(id), ms.max(ss))
11482 });
11483 let (fl_m, fl_h, fl_int, fl_ss) = fl;
11484 let mut fl_qkv = vec![0f32; fl_m * 3 * fl_h];
11485 let mut fl_attn = vec![0f32; fl_m * fl_h];
11486 let mut fl_res = vec![0f32; fl_m * fl_h];
11487 let mut fl_normed = vec![0f32; fl_m * fl_h];
11488 let mut fl_ffn = vec![0f32; fl_m * fl_int.max(2 * fl_int)]; let mut fl_sc = vec![0f32; fl_ss.max(1)];
11490
11491 let trace_thunks = std::env::var_os("RLX_TRACE_THUNK").is_some();
11492 if trace_thunks {
11493 eprintln!(
11494 "[thunk] prealloc max_h={max_h} sdpa={} fl_m={fl_m} fl_h={fl_h} fl_int={fl_int}",
11495 max_units * max_seq * max_seq
11496 );
11497 }
11498 let profile = std::env::var_os("RLX_PROFILE_THUNKS").is_some();
11499 let mut prof_prev: Option<(&'static str, std::time::Instant)> = None;
11503 for i in 0..len {
11504 if profile {
11505 if let Some((pn, pt)) = prof_prev.take() {
11506 profile_record(pn, pt.elapsed());
11507 }
11508 }
11509 let thunk = unsafe { thunks.get_unchecked(i) };
11510 if trace_thunks && (i < 120 || i % 200 == 0 || i + 1 == len) {
11511 eprintln!("[thunk {i}/{len}] {}", thunk_kind_name(thunk));
11512 }
11513 let trace_done = trace_thunks && i < 120;
11514 if profile {
11515 prof_prev = Some((thunk_kind_name(thunk), std::time::Instant::now()));
11516 }
11517 match thunk {
11518 Thunk::Nop => exec_nop(thunk),
11519 Thunk::ElementwiseRegion { .. } => exec_elementwise_region(thunk, base),
11520 Thunk::GaussianSplatRender { .. } => exec_gaussian_splat_render(thunk, base),
11521 Thunk::GaussianSplatRenderBackward { .. } => {
11522 exec_gaussian_splat_render_backward(thunk, base)
11523 }
11524 Thunk::GaussianSplatPrepare { .. } => exec_gaussian_splat_prepare(thunk, base),
11525 Thunk::GaussianSplatRasterize { .. } => exec_gaussian_splat_rasterize(thunk, base),
11526 Thunk::Fft1d { .. } => exec_fft1d(thunk, base),
11527 Thunk::FftButterflyStage { .. } => exec_fft_butterfly_stage(thunk, base),
11528 Thunk::LogMel { .. } => exec_log_mel(thunk, base),
11529 Thunk::LogMelBackward { .. } => exec_log_mel_backward(thunk, base),
11530 Thunk::WelchPeaks { .. } => exec_welch_peaks(thunk, base),
11531 Thunk::CustomFn { .. } => exec_custom_fn(thunk, base),
11532 Thunk::Sgemm { a, b, c, m, k, n } => {
11533 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
11534 if trace_thunks {
11535 eprintln!("[sgemm] m={m} k={k} n={n} a={} b={} c={}", *a, *b, *c);
11536 }
11537 let c_len = m.saturating_mul(n);
11538 let a_len = m.saturating_mul(k);
11539 let b_len = k.saturating_mul(n);
11540 let arena_len = arena_buf.len();
11541 let max_a = (arena_len.saturating_sub(*a)) / 4;
11542 let max_b = (arena_len.saturating_sub(*b)) / 4;
11543 let max_c = (arena_len.saturating_sub(*c)) / 4;
11544 let a_len = a_len.min(max_a);
11545 let b_len = b_len.min(max_b);
11546 let c_len = c_len.min(max_c);
11547 unsafe {
11548 let a_sl = sl(*a, base, a_len);
11549 let b_sl = sl(*b, base, b_len);
11550 let c_sl = sl_mut(*c, base, c_len);
11551 if std::ptr::eq(a_sl.as_ptr(), c_sl.as_ptr())
11552 || std::ptr::eq(b_sl.as_ptr(), c_sl.as_ptr())
11553 {
11554 let mut tmp = vec![0.0f32; c_len];
11555 crate::blas::sgemm_auto(a_sl, b_sl, &mut tmp, m, k, n);
11556 c_sl.copy_from_slice(&tmp);
11557 } else {
11558 crate::blas::sgemm_auto(a_sl, b_sl, c_sl, m, k, n);
11559 }
11560 }
11561 }
11562
11563 Thunk::SgemmT {
11564 a,
11565 b,
11566 c,
11567 m,
11568 k,
11569 n,
11570 ta,
11571 tb,
11572 } => {
11573 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
11578 let lda = if *ta { m } else { k };
11579 let ldb = if *tb { k } else { n };
11580 let arena_len = arena_buf.len();
11581 let a_len = (m * k).min((arena_len.saturating_sub(*a)) / 4);
11582 let b_len = (k * n).min((arena_len.saturating_sub(*b)) / 4);
11583 let c_len = (m * n).min((arena_len.saturating_sub(*c)) / 4);
11584 unsafe {
11585 let a_sl = sl(*a, base, a_len);
11586 let b_sl = sl(*b, base, b_len);
11587 let c_sl = sl_mut(*c, base, c_len);
11588 let (ap, bp) = (a_sl.as_ptr(), b_sl.as_ptr());
11589 if std::ptr::eq(ap, c_sl.as_ptr()) || std::ptr::eq(bp, c_sl.as_ptr()) {
11590 let mut tmp = vec![0.0f32; c_len];
11591 crate::blas::sgemm_general(
11592 ap,
11593 bp,
11594 tmp.as_mut_ptr(),
11595 m,
11596 n,
11597 k,
11598 1.0,
11599 0.0,
11600 lda,
11601 ldb,
11602 n,
11603 *ta,
11604 *tb,
11605 );
11606 c_sl.copy_from_slice(&tmp);
11607 } else {
11608 crate::blas::sgemm_general(
11609 ap,
11610 bp,
11611 c_sl.as_mut_ptr(),
11612 m,
11613 n,
11614 k,
11615 1.0,
11616 0.0,
11617 lda,
11618 ldb,
11619 n,
11620 *ta,
11621 *tb,
11622 );
11623 }
11624 }
11625 }
11626
11627 Thunk::SgdMomentum { .. } => exec_sgd_momentum(thunk, base),
11628 Thunk::CgemmC64 { .. } => exec_cgemm_c64(thunk, base),
11629 Thunk::DenseSolveF64 { .. } => exec_dense_solve_f64(thunk, base),
11630 Thunk::DenseSolveF32 { .. } => exec_dense_solve_f32(thunk, base),
11631 Thunk::BatchedDenseSolveF64 { .. } => exec_batched_dense_solve_f64(thunk, base),
11632 Thunk::BatchedDenseSolveF32 { .. } => exec_batched_dense_solve_f32(thunk, base),
11633 Thunk::BatchedDgemmF64 { .. } => exec_batched_dgemm_f64(thunk, base),
11634 Thunk::BatchedSgemm {
11635 a,
11636 b,
11637 c,
11638 batch,
11639 m,
11640 k,
11641 n,
11642 } => {
11643 let (b_, m_, k_, n_) = (*batch as usize, *m as usize, *k as usize, *n as usize);
11644 if trace_thunks {
11645 eprintln!(
11646 "[batched-sgemm] batch={b_} m={m_} k={k_} n={n_} a={} b={} c={}",
11647 *a, *b, *c
11648 );
11649 }
11650 let a_stride = m_.saturating_mul(k_);
11651 let b_stride = k_.saturating_mul(n_);
11652 let c_stride = m_.saturating_mul(n_);
11653 let arena_len = arena_buf.len();
11654 let a_cap = (arena_len.saturating_sub(*a)) / 4;
11655 let b_cap = (arena_len.saturating_sub(*b)) / 4;
11656 let c_cap = (arena_len.saturating_sub(*c)) / 4;
11657 let a_elems = (b_ * a_stride).min(a_cap);
11658 let b_elems = (b_ * b_stride).min(b_cap);
11659 let c_elems = (b_ * c_stride).min(c_cap);
11660 let b_eff = b_
11661 .min(a_elems.checked_div(a_stride).unwrap_or(0))
11662 .min(b_elems.checked_div(b_stride).unwrap_or(0))
11663 .min(c_elems.checked_div(c_stride).unwrap_or(0));
11664 unsafe {
11665 let a_full = sl(*a, base, a_elems);
11666 let b_full = sl(*b, base, b_elems);
11667 let c_full = sl_mut(*c, base, c_elems);
11668 for bi in 0..b_eff {
11669 let a0 = bi * a_stride;
11670 let b0 = bi * b_stride;
11671 let c0 = bi * c_stride;
11672 if a0 + a_stride > a_full.len()
11673 || b0 + b_stride > b_full.len()
11674 || c0 + c_stride > c_full.len()
11675 {
11676 break;
11677 }
11678 let a_slice = &a_full[a0..a0 + a_stride];
11679 let b_slice = &b_full[b0..b0 + b_stride];
11680 let c_slice = &mut c_full[c0..c0 + c_stride];
11681 if std::ptr::eq(a_slice.as_ptr(), c_slice.as_mut_ptr())
11682 || std::ptr::eq(b_slice.as_ptr(), c_slice.as_mut_ptr())
11683 {
11684 let mut tmp = vec![0.0f32; c_stride];
11685 crate::blas::sgemm_auto(a_slice, b_slice, &mut tmp, m_, k_, n_);
11686 c_slice.copy_from_slice(&tmp);
11687 } else {
11688 crate::blas::sgemm_auto(a_slice, b_slice, c_slice, m_, k_, n_);
11689 }
11690 }
11691 }
11692 }
11693
11694 Thunk::Dgemm { .. } => exec_dgemm(thunk, base),
11695 Thunk::TransposeF64 { .. } => exec_transpose_f64(thunk, base),
11696 Thunk::ActivationF64 { .. } => exec_activation_f64(thunk, base),
11697 Thunk::ReduceSumF64 { .. } => exec_reduce_sum_f64(thunk, base),
11698 Thunk::CopyF64 { src, dst, len } => {
11699 let mut len = *len as usize;
11700 if *src == *dst || len == 0 {
11701 continue;
11702 }
11703 let arena_len = arena_buf.len();
11704 let max_from_src = (arena_len.saturating_sub(*src)) / 8;
11705 let max_from_dst = (arena_len.saturating_sub(*dst)) / 8;
11706 len = len.min(max_from_src).min(max_from_dst);
11707 if len == 0 {
11708 continue;
11709 }
11710 let byte_len = len.saturating_mul(8);
11711 unsafe {
11712 std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
11713 }
11714 }
11715
11716 Thunk::CopyI64 { src, dst, len } => {
11717 let mut len = *len as usize;
11718 if *src == *dst || len == 0 {
11719 continue;
11720 }
11721 let arena_len = arena_buf.len();
11722 let max_from_src = (arena_len.saturating_sub(*src)) / 8;
11723 let max_from_dst = (arena_len.saturating_sub(*dst)) / 8;
11724 len = len.min(max_from_src).min(max_from_dst);
11725 if len == 0 {
11726 continue;
11727 }
11728 let byte_len = len.saturating_mul(8);
11729 unsafe {
11730 std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
11731 }
11732 }
11733
11734 Thunk::CastF32ToI64 { src, dst, len } => {
11735 let len = *len as usize;
11736 if len == 0 {
11737 continue;
11738 }
11739 unsafe {
11740 let inp = sl(*src, base, len);
11741 let out = sl_mut_i64(*dst, base, len);
11742 for i in 0..len {
11743 out[i] = inp[i].round() as i64;
11744 }
11745 }
11746 }
11747
11748 Thunk::CastF32ToF64 { src, dst, len } => {
11749 let len = *len as usize;
11750 if len == 0 {
11751 continue;
11752 }
11753 unsafe {
11754 let inp = sl(*src, base, len);
11755 let out = sl_mut_f64(*dst, base, len);
11756 for i in 0..len {
11757 out[i] = inp[i] as f64;
11758 }
11759 }
11760 }
11761
11762 Thunk::CastF32ToI32 { src, dst, len } => {
11763 let len = *len as usize;
11764 if len == 0 {
11765 continue;
11766 }
11767 unsafe {
11768 let inp = sl(*src, base, len);
11769 let out = sl_mut_i32(*dst, base, len);
11770 for i in 0..len {
11771 out[i] = inp[i].round() as i32;
11772 }
11773 }
11774 }
11775
11776 Thunk::CastI64ToF32 { src, dst, len } => {
11777 let len = *len as usize;
11778 if len == 0 {
11779 continue;
11780 }
11781 unsafe {
11782 let inp = sl_i64(*src, base, len);
11783 let out = sl_mut(*dst, base, len);
11784 for i in 0..len {
11785 out[i] = inp[i] as f32;
11786 }
11787 }
11788 }
11789
11790 Thunk::CastBoolToI32 { src, dst, len } => {
11791 let len = *len as usize;
11792 if len == 0 {
11793 continue;
11794 }
11795 unsafe {
11796 let inp = &arena_buf[*src..*src + len];
11797 let out = sl_mut_i32(*dst, base, len);
11798 for i in 0..len {
11799 out[i] = i32::from(inp[i] != 0);
11800 }
11801 }
11802 }
11803
11804 Thunk::CastI32ToF32 { src, dst, len } => {
11805 let len = *len as usize;
11806 if len == 0 {
11807 continue;
11808 }
11809 unsafe {
11810 let inp = sl_i32(*src, base, len);
11811 let out = sl_mut(*dst, base, len);
11812 for i in 0..len {
11813 out[i] = inp[i] as f32;
11814 }
11815 }
11816 }
11817
11818 Thunk::CastBoolToF32 { src, dst, len } => {
11819 let len = *len as usize;
11820 if len == 0 {
11821 continue;
11822 }
11823 unsafe {
11824 let inp = &arena_buf[*src..*src + len];
11825 let out = sl_mut(*dst, base, len);
11826 for i in 0..len {
11827 out[i] = if inp[i] != 0 { 1.0 } else { 0.0 };
11828 }
11829 }
11830 }
11831
11832 Thunk::BinaryFullF64 { .. } => exec_binary_full_f64(thunk, base),
11833 Thunk::BinaryFullC64 { .. } => exec_binary_full_c64(thunk, base),
11834 Thunk::ComplexNormSqF32 { .. } => exec_complex_norm_sq_f32(thunk, base),
11835 Thunk::ComplexNormSqBackwardF32 { .. } => {
11836 exec_complex_norm_sq_backward_f32(thunk, base)
11837 }
11838 Thunk::ConjugateC64 { .. } => exec_conjugate_c64(thunk, base),
11839 Thunk::ActivationC64 { .. } => exec_activation_c64(thunk, base),
11840 Thunk::Scan { .. } => exec_scan(thunk, base),
11841 Thunk::ScanBackward {
11842 body_vjp,
11843 body_init,
11844 body_carry_in_off,
11845 body_x_offs,
11846 body_d_output_off,
11847 body_dcarry_out_off,
11848 outer_init_off,
11849 outer_traj_off,
11850 outer_upstream_off,
11851 outer_xs_offs,
11852 outer_dinit_off,
11853 length,
11854 carry_bytes,
11855 save_trajectory,
11856 num_checkpoints,
11857 forward_body,
11858 forward_body_init,
11859 forward_body_carry_in_off,
11860 forward_body_output_off,
11861 forward_body_x_offs,
11862 carry_elem_size,
11863 } => {
11864 let cb = *carry_bytes as usize;
11877 let n_steps = *length as usize;
11878 let k_total = *num_checkpoints as usize;
11879 let is_recursive = k_total != 0 && k_total != n_steps;
11880 let checkpoint_t_for_k = |k: usize| -> usize {
11881 ((k + 1) * n_steps)
11882 .div_ceil(k_total)
11883 .saturating_sub(1)
11884 .min(n_steps - 1)
11885 };
11886
11887 let mut fwd_buf: Vec<u8> = if is_recursive {
11888 (**forward_body_init.as_ref().unwrap()).clone()
11889 } else {
11890 Vec::new()
11891 };
11892
11893 let mut dcarry: Vec<u8> = vec![0u8; cb];
11894 if !*save_trajectory {
11895 unsafe {
11896 std::ptr::copy_nonoverlapping(
11897 base.add(*outer_upstream_off),
11898 dcarry.as_mut_ptr(),
11899 cb,
11900 );
11901 }
11902 }
11903
11904 let mut body_buf: Vec<u8> = (**body_init).clone();
11905
11906 let process_iter =
11911 |t: usize, carry_in: &[u8], dcarry: &mut Vec<u8>, body_buf: &mut Vec<u8>| {
11912 if *save_trajectory {
11913 unsafe {
11914 let up_off = *outer_upstream_off + t * cb;
11915 match *carry_elem_size {
11916 4 => {
11917 let up_ptr = base.add(up_off) as *const f32;
11918 let dc_ptr = dcarry.as_mut_ptr() as *mut f32;
11919 let n_elems = cb / 4;
11920 for i in 0..n_elems {
11921 *dc_ptr.add(i) += *up_ptr.add(i);
11922 }
11923 }
11924 8 => {
11925 let up_ptr = base.add(up_off) as *const f64;
11926 let dc_ptr = dcarry.as_mut_ptr() as *mut f64;
11927 let n_elems = cb / 8;
11928 for i in 0..n_elems {
11929 *dc_ptr.add(i) += *up_ptr.add(i);
11930 }
11931 }
11932 other => panic!(
11933 "ScanBackward: unsupported carry elem size {other} \
11934 (only f32/f64 carries are supported today)"
11935 ),
11936 }
11937 }
11938 }
11939 body_buf[*body_carry_in_off..*body_carry_in_off + cb]
11940 .copy_from_slice(carry_in);
11941 unsafe {
11942 for (i, body_x_off) in body_x_offs.iter().enumerate() {
11943 let (outer_xs_off, per_step_bytes) = outer_xs_offs[i];
11944 let psb = per_step_bytes as usize;
11945 std::ptr::copy_nonoverlapping(
11946 base.add(outer_xs_off + t * psb),
11947 body_buf.as_mut_ptr().add(*body_x_off),
11948 psb,
11949 );
11950 }
11951 std::ptr::copy_nonoverlapping(
11952 dcarry.as_ptr(),
11953 body_buf.as_mut_ptr().add(*body_d_output_off),
11954 cb,
11955 );
11956 }
11957 execute_thunks(body_vjp, body_buf);
11958 unsafe {
11959 std::ptr::copy_nonoverlapping(
11960 body_buf.as_ptr().add(*body_dcarry_out_off),
11961 dcarry.as_mut_ptr(),
11962 cb,
11963 );
11964 }
11965 };
11966
11967 if is_recursive {
11968 let leaf_threshold = 4usize;
11976 let fb_sched = forward_body.as_ref().unwrap();
11977 let fb_init = forward_body_init.as_ref().unwrap().as_slice();
11978 let mut segment_end = n_steps - 1;
11979 for seg_k in (0..k_total).rev() {
11980 let segment_start = if seg_k == 0 {
11981 0
11982 } else {
11983 checkpoint_t_for_k(seg_k - 1) + 1
11984 };
11985 let mut anchor: Vec<u8> = vec![0u8; cb];
11986 unsafe {
11987 let src = if seg_k == 0 {
11988 base.add(*outer_init_off)
11989 } else {
11990 base.add(*outer_traj_off + (seg_k - 1) * cb)
11991 };
11992 std::ptr::copy_nonoverlapping(src, anchor.as_mut_ptr(), cb);
11993 }
11994 let mut leaf_action = |t: usize, carry_in: &[u8]| {
11997 process_iter(t, carry_in, &mut dcarry, &mut body_buf);
11998 };
11999 unsafe {
12000 griewank_process_segment(
12001 segment_start,
12002 segment_end,
12003 &anchor,
12004 cb,
12005 fb_sched,
12006 fb_init,
12007 *forward_body_carry_in_off,
12008 *forward_body_output_off,
12009 forward_body_x_offs,
12010 base,
12011 outer_xs_offs,
12012 &mut fwd_buf,
12013 leaf_threshold,
12014 &mut leaf_action,
12015 );
12016 }
12017 if seg_k == 0 {
12018 break;
12019 }
12020 segment_end = segment_start - 1;
12021 }
12022 } else {
12023 let mut carry_buf: Vec<u8> = vec![0u8; cb];
12026 for t in (0..n_steps).rev() {
12027 unsafe {
12028 let src = if t == 0 {
12029 base.add(*outer_init_off)
12030 } else {
12031 base.add(*outer_traj_off + (t - 1) * cb)
12032 };
12033 std::ptr::copy_nonoverlapping(src, carry_buf.as_mut_ptr(), cb);
12034 }
12035 process_iter(t, &carry_buf, &mut dcarry, &mut body_buf);
12036 }
12037 }
12038
12039 unsafe {
12040 std::ptr::copy_nonoverlapping(dcarry.as_ptr(), base.add(*outer_dinit_off), cb);
12041 }
12042 }
12043
12044 Thunk::ScanBackwardXs { .. } => exec_scan_backward_xs(thunk, base),
12045 Thunk::FusedMmBiasAct { .. } => exec_fused_mm_bias_act(thunk, base),
12046 Thunk::FusedResidualLN {
12047 x,
12048 res,
12049 bias,
12050 g,
12051 b,
12052 out,
12053 rows,
12054 h,
12055 eps,
12056 has_bias,
12057 } => {
12058 let (rows, h) = (*rows as usize, *h as usize);
12059 unsafe {
12060 let zero = &zero_bias[..h];
12061 let bi = if *has_bias { sl(*bias, base, h) } else { zero };
12062 let x_ptr = sl(*x, base, rows * h).as_ptr() as usize;
12063 let r_ptr = sl(*res, base, rows * h).as_ptr() as usize;
12064 let o_ptr = sl_mut(*out, base, rows * h).as_mut_ptr() as usize;
12065 let bi_ptr = bi.as_ptr() as usize;
12066 let g_ptr = sl(*g, base, h).as_ptr() as usize;
12067 let b_ptr = sl(*b, base, h).as_ptr() as usize;
12068 let e = *eps;
12069 crate::pool::par_for(rows, 4, &|off, cnt| {
12070 let xs =
12071 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
12072 let rs =
12073 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
12074 let os = std::slice::from_raw_parts_mut(
12075 (o_ptr as *mut f32).add(off * h),
12076 cnt * h,
12077 );
12078 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
12079 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
12080 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
12081 crate::kernels::residual_bias_layer_norm(xs, rs, bi, g, b, os, cnt, h, e);
12082 });
12083 }
12084 }
12085
12086 Thunk::FusedResidualRmsNorm {
12087 x,
12088 res,
12089 bias,
12090 g,
12091 b,
12092 out,
12093 rows,
12094 h,
12095 eps,
12096 has_bias,
12097 } => {
12098 let (rows, h) = (*rows as usize, *h as usize);
12099 unsafe {
12100 let zero = &zero_bias[..h];
12101 let bi = if *has_bias { sl(*bias, base, h) } else { zero };
12102 let x_ptr = sl(*x, base, rows * h).as_ptr() as usize;
12103 let r_ptr = sl(*res, base, rows * h).as_ptr() as usize;
12104 let o_ptr = sl_mut(*out, base, rows * h).as_mut_ptr() as usize;
12105 let bi_ptr = bi.as_ptr() as usize;
12106 let g_ptr = sl(*g, base, h).as_ptr() as usize;
12107 let b_ptr = sl(*b, base, h).as_ptr() as usize;
12108 let e = *eps;
12109 crate::pool::par_for(rows, 4, &|off, cnt| {
12110 let xs =
12111 std::slice::from_raw_parts((x_ptr as *const f32).add(off * h), cnt * h);
12112 let rs =
12113 std::slice::from_raw_parts((r_ptr as *const f32).add(off * h), cnt * h);
12114 let os = std::slice::from_raw_parts_mut(
12115 (o_ptr as *mut f32).add(off * h),
12116 cnt * h,
12117 );
12118 let bi = std::slice::from_raw_parts(bi_ptr as *const f32, h);
12119 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
12120 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
12121 crate::kernels::residual_bias_rms_norm(xs, rs, bi, g, b, os, cnt, h, e);
12122 });
12123 }
12124 }
12125
12126 Thunk::BiasAdd { .. } => exec_bias_add(thunk, base),
12127 Thunk::BinaryFull {
12128 lhs,
12129 rhs,
12130 dst,
12131 len,
12132 lhs_len,
12133 rhs_len,
12134 op,
12135 out_dims_bcast,
12136 bcast_lhs_strides,
12137 bcast_rhs_strides,
12138 elem_bytes,
12139 } => {
12140 let len = *len as usize;
12141 let ll = (*lhs_len as usize).max(1);
12142 let rl = (*rhs_len as usize).max(1);
12143 let eb = (*elem_bytes).max(1) as usize;
12144 let arena_len = arena_buf.len();
12145 let ll = ll.min((arena_len.saturating_sub(*lhs)) / eb);
12146 let rl = rl.min((arena_len.saturating_sub(*rhs)) / eb);
12147 let len = len.min((arena_len.saturating_sub(*dst)) / eb);
12148 unsafe {
12149 if eb == 8 {
12150 let l = sl_i64(*lhs, base, ll);
12151 let r = sl_i64(*rhs, base, rl);
12152 let o = sl_mut_i64(*dst, base, len);
12153 if !out_dims_bcast.is_empty() {
12154 let rank = out_dims_bcast.len();
12155 let mut coords = vec![0u32; rank];
12156 for i in 0..len {
12157 let mut rem = i;
12158 for ax in (0..rank).rev() {
12159 let sz = out_dims_bcast[ax] as usize;
12160 coords[ax] = (rem % sz) as u32;
12161 rem /= sz;
12162 }
12163 let mut li = 0usize;
12164 let mut ri = 0usize;
12165 for ax in 0..rank {
12166 li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
12167 ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
12168 }
12169 o[i] = match op {
12170 BinaryOp::Add => l[li].wrapping_add(r[ri]),
12171 BinaryOp::Sub => l[li].wrapping_sub(r[ri]),
12172 BinaryOp::Mul => l[li].wrapping_mul(r[ri]),
12173 BinaryOp::Div => {
12174 if r[ri] == 0 {
12175 0
12176 } else {
12177 l[li] / r[ri]
12178 }
12179 }
12180 BinaryOp::Max => l[li].max(r[ri]),
12181 BinaryOp::Min => l[li].min(r[ri]),
12182 BinaryOp::Pow => l[li].pow(r[ri].max(0) as u32),
12183 };
12184 }
12185 } else {
12186 for i in 0..len {
12187 let li = if ll == 1 { 0 } else { i % ll };
12188 let ri = if rl == 1 { 0 } else { i % rl };
12189 o[i] = match op {
12190 BinaryOp::Add => l[li].wrapping_add(r[ri]),
12191 BinaryOp::Sub => l[li].wrapping_sub(r[ri]),
12192 BinaryOp::Mul => l[li].wrapping_mul(r[ri]),
12193 BinaryOp::Div => {
12194 if r[ri] == 0 {
12195 0
12196 } else {
12197 l[li] / r[ri]
12198 }
12199 }
12200 BinaryOp::Max => l[li].max(r[ri]),
12201 BinaryOp::Min => l[li].min(r[ri]),
12202 BinaryOp::Pow => l[li].pow(r[ri].max(0) as u32),
12203 };
12204 }
12205 }
12206 } else {
12207 let l = sl(*lhs, base, ll);
12208 let r = sl(*rhs, base, rl);
12209 let o = sl_mut(*dst, base, len);
12210 if ll == len && rl == len {
12211 #[cfg(target_arch = "aarch64")]
12212 if matches!(op, BinaryOp::Add | BinaryOp::Mul) {
12213 use std::arch::aarch64::*;
12214 let chunks = len / 4;
12215 for c in 0..chunks {
12216 let off = c * 4;
12217 let vl = vld1q_f32(l.as_ptr().add(off));
12218 let vr = vld1q_f32(r.as_ptr().add(off));
12219 let res = match op {
12220 BinaryOp::Add => vaddq_f32(vl, vr),
12221 BinaryOp::Mul => vmulq_f32(vl, vr),
12222 _ => unreachable!(),
12223 };
12224 vst1q_f32(o.as_mut_ptr().add(off), res);
12225 }
12226 for i in (chunks * 4)..len {
12227 o[i] = match op {
12228 BinaryOp::Add => l[i] + r[i],
12229 BinaryOp::Mul => l[i] * r[i],
12230 _ => unreachable!(),
12231 };
12232 }
12233 continue;
12234 }
12235 }
12236 if !out_dims_bcast.is_empty() {
12237 let rank = out_dims_bcast.len();
12238 let mut coords = vec![0u32; rank];
12239 for i in 0..len {
12240 let mut rem = i;
12241 for ax in (0..rank).rev() {
12242 let sz = out_dims_bcast[ax] as usize;
12243 coords[ax] = (rem % sz) as u32;
12244 rem /= sz;
12245 }
12246 let mut li = 0usize;
12247 let mut ri = 0usize;
12248 for ax in 0..rank {
12249 li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
12250 ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
12251 }
12252 o[i] = match op {
12253 BinaryOp::Add => l[li] + r[ri],
12254 BinaryOp::Sub => l[li] - r[ri],
12255 BinaryOp::Mul => l[li] * r[ri],
12256 BinaryOp::Div => l[li] / r[ri],
12257 BinaryOp::Max => l[li].max(r[ri]),
12258 BinaryOp::Min => l[li].min(r[ri]),
12259 BinaryOp::Pow => l[li].powf(r[ri]),
12260 };
12261 }
12262 } else {
12263 for i in 0..len {
12264 let li = if ll == 1 { 0 } else { i % ll };
12265 let ri = if rl == 1 { 0 } else { i % rl };
12266 o[i] = match op {
12267 BinaryOp::Add => l[li] + r[ri],
12268 BinaryOp::Sub => l[li] - r[ri],
12269 BinaryOp::Mul => l[li] * r[ri],
12270 BinaryOp::Div => l[li] / r[ri],
12271 BinaryOp::Max => l[li].max(r[ri]),
12272 BinaryOp::Min => l[li].min(r[ri]),
12273 BinaryOp::Pow => l[li].powf(r[ri]),
12274 };
12275 }
12276 }
12277 }
12278 }
12279 }
12280
12281 Thunk::Gather { .. } => exec_gather(thunk, base),
12282 Thunk::Narrow {
12283 src,
12284 dst,
12285 outer,
12286 src_stride,
12287 dst_stride,
12288 inner,
12289 elem_bytes,
12290 } => {
12291 let (outer, ss, ds, inner, eb) = (
12292 *outer as usize,
12293 *src_stride as usize,
12294 *dst_stride as usize,
12295 *inner as usize,
12296 *elem_bytes as usize,
12297 );
12298 let row_bytes = inner.saturating_mul(eb);
12299 let src_row_stride = ss.saturating_mul(eb);
12300 let dst_row_stride = ds.saturating_mul(eb);
12301 if trace_thunks {
12302 eprintln!(
12303 "[narrow] src={} dst={} outer={outer} ss={ss} ds={ds} inner={inner} eb={eb} row={row_bytes} arena={}",
12304 *src,
12305 *dst,
12306 arena_buf.len()
12307 );
12308 }
12309 if row_bytes > 0 && *src != *dst {
12310 let arena_len = arena_buf.len();
12311 for o in 0..outer {
12312 let s_off = *src + o * src_row_stride;
12313 let d_off = *dst + o * dst_row_stride;
12314 if s_off == d_off {
12315 continue;
12316 }
12317 if s_off.saturating_add(row_bytes) > arena_len
12318 || d_off.saturating_add(row_bytes) > arena_len
12319 {
12320 break;
12321 }
12322 unsafe {
12323 std::ptr::copy_nonoverlapping(
12324 base.add(s_off),
12325 base.add(d_off),
12326 row_bytes,
12327 );
12328 }
12329 }
12330 }
12331 }
12332
12333 Thunk::Copy { src, dst, len } => {
12334 let mut len = *len as usize;
12335 if *src == *dst || len == 0 {
12336 continue;
12337 }
12338 let arena_len = arena_buf.len();
12339 let max_from_src = (arena_len.saturating_sub(*src)) / 4;
12340 let max_from_dst = (arena_len.saturating_sub(*dst)) / 4;
12341 len = len.min(max_from_src).min(max_from_dst);
12342 if len == 0 {
12343 continue;
12344 }
12345 let byte_len = len.saturating_mul(4);
12346 unsafe {
12347 std::ptr::copy(base.add(*src), base.add(*dst), byte_len);
12348 }
12349 }
12350
12351 Thunk::LayerNorm { .. } => exec_layer_norm(thunk, base),
12352 Thunk::GroupNorm { .. } => exec_group_norm(thunk, base),
12353 Thunk::BatchNormInference { .. } => exec_batch_norm_inference(thunk, base),
12354 Thunk::LayerNorm2d { .. } => exec_layer_norm2d(thunk, base),
12355 Thunk::ConvTranspose2d { .. } => exec_conv_transpose2d(thunk, base),
12356 Thunk::ResizeNearest2x { .. } => exec_resize_nearest2x(thunk, base),
12357 Thunk::AxialRope2d { .. } => exec_axial_rope2d(thunk, base),
12358 Thunk::RmsNorm { .. } => exec_rms_norm(thunk, base),
12359 Thunk::Softmax { .. } => exec_softmax(thunk, base),
12360 Thunk::Cumsum { .. } => exec_cumsum(thunk, base),
12361 Thunk::Sample { .. } => exec_sample(thunk, base),
12362 Thunk::RngNormal {
12363 dst,
12364 len,
12365 mean,
12366 scale,
12367 key,
12368 op_seed,
12369 } => {
12370 let n = *len as usize;
12371 unsafe {
12372 let out = sl_mut(*dst, base, n);
12373 let opts = *schedule.rng.read().unwrap();
12374 rlx_ir::fill_normal_like(out, *mean, *scale, opts, *key, *op_seed);
12375 }
12376 }
12377
12378 Thunk::RngUniform {
12379 dst,
12380 len,
12381 low,
12382 high,
12383 key,
12384 op_seed,
12385 } => {
12386 let n = *len as usize;
12387 unsafe {
12388 let out = sl_mut(*dst, base, n);
12389 let opts = *schedule.rng.read().unwrap();
12390 rlx_ir::fill_uniform_like(out, *low, *high, opts, *key, *op_seed);
12391 }
12392 }
12393
12394 Thunk::GatedDeltaNet { .. } => exec_gated_delta_net(thunk, base),
12395 Thunk::Lstm { .. } => exec_lstm(thunk, base),
12396 Thunk::Gru { .. } => exec_gru(thunk, base),
12397 Thunk::Rnn { .. } => exec_rnn(thunk, base),
12398 Thunk::Mamba2 { .. } => exec_mamba2(thunk, base),
12399 Thunk::SelectiveScan { .. } => exec_selective_scan(thunk, base),
12400 Thunk::DequantMatMul { .. } => exec_dequant_mat_mul(thunk, base),
12401 Thunk::DequantMatMulGguf { .. } => exec_dequant_mat_mul_gguf(thunk, base),
12402 Thunk::DequantMatMulInt4 { .. } => exec_dequant_mat_mul_int4(thunk, base),
12403 Thunk::DequantMatMulFp8 { .. } => exec_dequant_mat_mul_fp8(thunk, base),
12404 Thunk::DequantMatMulNvfp4 { .. } => exec_dequant_mat_mul_nvfp4(thunk, base),
12405 Thunk::ScaledMatMul { .. } => exec_scaled_mat_mul(thunk, base),
12406 Thunk::ScaledQuantize { .. } => exec_scaled_quantize(thunk, base),
12407 Thunk::ScaledQuantScale { .. } => exec_scaled_quant_scale(thunk, base),
12408 Thunk::ScaledDequantize { .. } => exec_scaled_dequantize(thunk, base),
12409 Thunk::LoraMatMul { .. } => exec_lora_mat_mul(thunk, base),
12410 Thunk::Attention {
12411 q,
12412 k,
12413 v,
12414 mask,
12415 out,
12416 batch,
12417 seq,
12418 kv_seq,
12419 heads,
12420 head_dim,
12421 mask_kind,
12422 scale,
12423 softcap,
12424 q_row_stride,
12425 k_row_stride,
12426 v_row_stride,
12427 bhsd,
12428 kv_heads,
12429 } => {
12430 let (b, q_s, k_s, nh, dh) = (
12431 *batch as usize,
12432 *seq as usize,
12433 *kv_seq as usize,
12434 *heads as usize,
12435 *head_dim as usize,
12436 );
12437 let nkv = (*kv_heads as usize).max(1);
12438 let group = (nh / nkv).max(1); let hs = nh * dh;
12440 let (qrs, krs, vrs) = if *bhsd {
12443 (dh, dh, dh)
12444 } else {
12445 (
12446 *q_row_stride as usize,
12447 *k_row_stride as usize,
12448 *v_row_stride as usize,
12449 )
12450 };
12451 let bhsd = *bhsd;
12452 let _ = (q_row_stride, k_row_stride, v_row_stride);
12453 let scale = *scale;
12454 let ss = q_s * k_s;
12455 let cfg = crate::config::RuntimeConfig::global();
12456 unsafe {
12457 let q_len = if bhsd {
12464 b * nh * q_s * dh
12465 } else {
12466 b * q_s * qrs
12467 };
12468 let k_len = if bhsd {
12469 b * nkv * k_s * dh
12470 } else {
12471 b * k_s * krs
12472 };
12473 let v_len = if bhsd {
12474 b * nkv * k_s * dh
12475 } else {
12476 b * k_s * vrs
12477 };
12478 let q_data = sl(*q, base, q_len);
12479 let k_data = sl(*k, base, k_len);
12480 let v_data = sl(*v, base, v_len);
12481 let mask_data: &[f32] = match mask_kind {
12482 rlx_ir::op::MaskKind::Custom => sl(*mask, base, b * k_s),
12483 rlx_ir::op::MaskKind::Bias => sl(*mask, base, b * nh * q_s * k_s),
12484 _ => &[],
12485 };
12486 let out_len = if bhsd {
12487 b * nh * q_s * dh
12488 } else {
12489 b * q_s * hs
12490 };
12491 let out_data = sl_mut(*out, base, out_len);
12492
12493 if bhsd {
12504 let scores = &mut sdpa_scores[..ss];
12505 for bi in 0..b {
12506 for hi in 0..nh {
12507 let kv_hi = hi / group; let q_head_base = bi * nh * q_s * dh + hi * q_s * dh;
12509 let k_head_base = bi * nkv * k_s * dh + kv_hi * k_s * dh;
12510 for qi in 0..q_s {
12512 let q_base = q_head_base + qi * dh;
12513 for ki in 0..k_s {
12514 let k_base = k_head_base + ki * dh;
12515 let mut dot = 0f32;
12516 for d in 0..dh {
12517 dot += q_data[q_base + d] * k_data[k_base + d];
12518 }
12519 scores[qi * k_s + ki] = dot * scale;
12520 if matches!(mask_kind, rlx_ir::op::MaskKind::Custom)
12521 && !mask_data.is_empty()
12522 && mask_data[bi * k_s + ki] < mask_thr
12523 {
12524 scores[qi * k_s + ki] = mask_neg;
12525 }
12526 }
12527 }
12528 if matches!(mask_kind, rlx_ir::op::MaskKind::Bias) {
12529 let off = (bi * nh + hi) * q_s * k_s;
12530 for i in 0..q_s * k_s {
12531 scores[i] += mask_data[off + i];
12532 }
12533 }
12534 apply_synthetic_mask(scores, q_s, k_s, *mask_kind);
12535 if *softcap > 0.0 {
12537 for s in scores.iter_mut() {
12538 *s = *softcap * (*s / *softcap).tanh();
12539 }
12540 }
12541 crate::kernels::neon_softmax(scores, q_s, k_s);
12542 for qi in 0..q_s {
12544 let o_base = q_head_base + qi * dh;
12545 for d in 0..dh {
12546 out_data[o_base + d] = 0.0;
12547 }
12548 for ki in 0..k_s {
12549 let sc = scores[qi * k_s + ki];
12550 if sc > score_thr {
12551 let v_base = k_head_base + ki * dh;
12552 for d in 0..dh {
12553 out_data[o_base + d] += sc * v_data[v_base + d];
12554 }
12555 }
12556 }
12557 }
12558 }
12559 }
12560 continue;
12561 }
12562
12563 if b == 1 && q_s.max(k_s) <= cfg.sdpa_seq_threshold {
12570 let scores = &mut sdpa_scores[..ss];
12572 #[cfg(target_arch = "aarch64")]
12573 let neon_chunks = dh / 4;
12574
12575 for bi in 0..b {
12576 for hi in 0..nh {
12577 let kv_hi = hi / group; for qi in 0..q_s {
12580 let q_off = bi * q_s * qrs + qi * qrs + hi * dh;
12581 for ki in 0..k_s {
12582 let k_off = bi * k_s * krs + ki * krs + kv_hi * dh;
12583 #[cfg(target_arch = "aarch64")]
12584 let mut dot;
12585 #[cfg(not(target_arch = "aarch64"))]
12586 let mut dot = 0f32;
12587 #[cfg(target_arch = "aarch64")]
12588 {
12589 use std::arch::aarch64::*;
12590 let mut acc = vdupq_n_f32(0.0);
12591 for c in 0..neon_chunks {
12592 let vq =
12593 vld1q_f32(q_data.as_ptr().add(q_off + c * 4));
12594 let vk =
12595 vld1q_f32(k_data.as_ptr().add(k_off + c * 4));
12596 acc = vfmaq_f32(acc, vq, vk);
12597 }
12598 dot = vaddvq_f32(acc);
12599 for d in (neon_chunks * 4)..dh {
12600 dot += q_data[q_off + d] * k_data[k_off + d];
12601 }
12602 }
12603 #[cfg(not(target_arch = "aarch64"))]
12604 for d in 0..dh {
12605 dot += q_data[q_off + d] * k_data[k_off + d];
12606 }
12607 scores[qi * k_s + ki] = dot * scale;
12608 if matches!(mask_kind, rlx_ir::op::MaskKind::Custom)
12615 && !mask_data.is_empty()
12616 && mask_data[bi * k_s + ki] < mask_thr
12617 {
12618 scores[qi * k_s + ki] = mask_neg;
12619 }
12620 }
12621 }
12622
12623 if matches!(mask_kind, rlx_ir::op::MaskKind::Bias) {
12624 let off = (bi * nh + hi) * q_s * k_s;
12625 for i in 0..q_s * k_s {
12626 scores[i] += mask_data[off + i];
12627 }
12628 }
12629 apply_synthetic_mask(scores, q_s, k_s, *mask_kind);
12630 crate::kernels::neon_softmax(scores, q_s, k_s);
12631
12632 for qi in 0..q_s {
12634 let o_off = bi * q_s * hs + qi * hs + hi * dh;
12635 for d in 0..dh {
12637 out_data[o_off + d] = 0.0;
12638 }
12639 for ki in 0..k_s {
12640 let sc = scores[qi * k_s + ki];
12641 if sc > score_thr {
12642 let v_off = bi * k_s * vrs + ki * vrs + kv_hi * dh;
12643 #[cfg(target_arch = "aarch64")]
12644 {
12645 use std::arch::aarch64::*;
12646 let vsc = vdupq_n_f32(sc);
12647 for c in 0..neon_chunks {
12648 let off = c * 4;
12649 let vo = vld1q_f32(
12650 out_data.as_ptr().add(o_off + off),
12651 );
12652 let vv =
12653 vld1q_f32(v_data.as_ptr().add(v_off + off));
12654 vst1q_f32(
12655 out_data.as_mut_ptr().add(o_off + off),
12656 vfmaq_f32(vo, vsc, vv),
12657 );
12658 }
12659 }
12660 #[cfg(not(target_arch = "aarch64"))]
12661 for d in 0..dh {
12662 out_data[o_off + d] += sc * v_data[v_off + d];
12663 }
12664 }
12665 }
12666 }
12667 }
12668 }
12669 } else {
12670 let total_work = b * nh;
12672 let q_addr = q_data.as_ptr() as usize;
12673 let k_addr = k_data.as_ptr() as usize;
12674 let v_addr = v_data.as_ptr() as usize;
12675 let m_addr = mask_data.as_ptr() as usize;
12676 let o_addr = out_data.as_mut_ptr() as usize;
12677 let sc_addr = sdpa_scores.as_mut_ptr() as usize;
12678
12679 crate::pool::par_for(total_work, 1, &|off, cnt| {
12680 for idx in off..off + cnt {
12681 let bi = idx / nh;
12682 let hi = idx % nh;
12683 let kv_hi = hi / group; let q_start = (q_addr as *const f32).add(bi * q_s * qrs + hi * dh);
12686 let k_start =
12687 (k_addr as *const f32).add(bi * k_s * krs + kv_hi * dh);
12688 let v_start =
12689 (v_addr as *const f32).add(bi * k_s * vrs + kv_hi * dh);
12690 let o_start = (o_addr as *mut f32).add(bi * q_s * hs + hi * dh);
12691 let sc = std::slice::from_raw_parts_mut(
12692 (sc_addr as *mut f32).add(idx * ss),
12693 ss,
12694 );
12695
12696 crate::blas::sgemm_general(
12699 q_start,
12700 k_start,
12701 sc.as_mut_ptr(),
12702 q_s,
12703 k_s,
12704 dh,
12705 scale,
12706 0.0,
12707 qrs,
12708 krs,
12709 k_s,
12710 false,
12711 true,
12712 );
12713
12714 match mask_kind {
12715 rlx_ir::op::MaskKind::Custom => {
12716 let mask_bi = std::slice::from_raw_parts(
12717 (m_addr as *const f32).add(bi * k_s),
12718 k_s,
12719 );
12720 for ki in 0..k_s {
12721 if mask_bi[ki] < mask_thr {
12722 for qi in 0..q_s {
12723 sc[qi * k_s + ki] = mask_neg;
12724 }
12725 }
12726 }
12727 }
12728 rlx_ir::op::MaskKind::Bias => {
12729 let bias = std::slice::from_raw_parts(
12731 (m_addr as *const f32).add((bi * nh + hi) * q_s * k_s),
12732 q_s * k_s,
12733 );
12734 for i in 0..q_s * k_s {
12735 sc[i] += bias[i];
12736 }
12737 }
12738 _ => apply_synthetic_mask(sc, q_s, k_s, *mask_kind),
12739 }
12740
12741 crate::kernels::neon_softmax(sc, q_s, k_s);
12742
12743 crate::blas::sgemm_general(
12747 sc.as_ptr(),
12748 v_start,
12749 o_start,
12750 q_s,
12751 dh,
12752 k_s,
12753 1.0,
12754 0.0,
12755 k_s,
12756 vrs,
12757 hs,
12758 false,
12759 false,
12760 );
12761 }
12762 });
12763 }
12764 }
12765 }
12766
12767 Thunk::AttentionBackward { .. } => exec_attention_backward(thunk, base),
12768 Thunk::ActivationInPlace { .. } => exec_activation_in_place(thunk, base),
12769 Thunk::FusedAttnBlock {
12770 hidden,
12771 qkv_w,
12772 out_w,
12773 mask,
12774 mask_kind,
12775 out,
12776 qkv_b,
12777 out_b,
12778 cos,
12779 sin,
12780 cos_len,
12781 batch,
12782 seq,
12783 hs,
12784 nh,
12785 dh,
12786 has_bias,
12787 has_rope,
12788 interleaved,
12789 } => {
12790 let (b, s) = (*batch as usize, *seq as usize);
12791 let (h, n_h, d_h) = (*hs as usize, *nh as usize, *dh as usize);
12792 let interleaved = *interleaved;
12793 let m = b * s;
12794 let scale = (d_h as f32).powf(-0.5);
12795 let half = d_h / 2;
12796 let use_custom_mask = matches!(mask_kind, rlx_ir::op::MaskKind::Custom);
12802 unsafe {
12803 let inp = sl(*hidden, base, m * h);
12804 let wq = sl(*qkv_w, base, h * 3 * h);
12805 let wo = sl(*out_w, base, h * h);
12806 let mk = if use_custom_mask {
12807 sl(*mask, base, b * s)
12808 } else {
12809 &[]
12810 };
12811 let dst = sl_mut(*out, base, m * h);
12812
12813 let mut qkv = vec![0f32; m * 3 * h];
12815 let mut attn_out = vec![0f32; m * h];
12816 let mut scores_buf = vec![0f32; s * s]; crate::blas::sgemm(inp, wq, &mut qkv, m, h, 3 * h);
12820 if *has_bias {
12821 let bias = sl(*qkv_b, base, 3 * h);
12822 crate::blas::bias_add(&mut qkv, bias, m, 3 * h);
12823 }
12824
12825 #[cfg(target_arch = "aarch64")]
12828 let neon_chunks = d_h / 4;
12829 #[cfg(target_arch = "aarch64")]
12830 let _rope_chunks = half / 4;
12831
12832 for bi in 0..b {
12833 for hi in 0..n_h {
12834 for qi in 0..s {
12836 let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
12837 for ki in 0..s {
12838 let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
12839 let mut dot = 0f32;
12840
12841 if *has_rope {
12842 let q_cos = qi * half;
12844 let k_cos = ki * half;
12845 let cos_tab = sl(*cos, base, *cos_len as usize);
12846 let sin_tab = sl(*sin, base, *cos_len as usize);
12847 for i in 0..half {
12853 let (qo1, qo2, ko1, ko2) = if interleaved {
12854 (2 * i, 2 * i + 1, 2 * i, 2 * i + 1)
12855 } else {
12856 (i, half + i, i, half + i)
12857 };
12858 let q1 = qkv[q_base + qo1];
12859 let q2 = qkv[q_base + qo2];
12860 let k1 = qkv[k_base + ko1];
12861 let k2 = qkv[k_base + ko2];
12862 let c_q = cos_tab[q_cos + i];
12863 let s_q = sin_tab[q_cos + i];
12864 let c_k = cos_tab[k_cos + i];
12865 let s_k = sin_tab[k_cos + i];
12866 let qr1 = q1 * c_q - q2 * s_q;
12867 let kr1 = k1 * c_k - k2 * s_k;
12868 let qr2 = q2 * c_q + q1 * s_q;
12869 let kr2 = k2 * c_k + k1 * s_k;
12870 dot += qr1 * kr1 + qr2 * kr2;
12871 }
12872 } else {
12873 #[cfg(target_arch = "aarch64")]
12875 {
12876 use std::arch::aarch64::*;
12877 let mut acc = vdupq_n_f32(0.0);
12878 for c in 0..neon_chunks {
12879 let vq =
12880 vld1q_f32(qkv.as_ptr().add(q_base + c * 4));
12881 let vk =
12882 vld1q_f32(qkv.as_ptr().add(k_base + c * 4));
12883 acc = vfmaq_f32(acc, vq, vk);
12884 }
12885 dot = vaddvq_f32(acc);
12886 for d in (neon_chunks * 4)..d_h {
12887 dot += qkv[q_base + d] * qkv[k_base + d];
12888 }
12889 }
12890 #[cfg(not(target_arch = "aarch64"))]
12891 for d in 0..d_h {
12892 dot += qkv[q_base + d] * qkv[k_base + d];
12893 }
12894 }
12895
12896 scores_buf[qi * s + ki] = dot * scale;
12897 let pos_masked = match mask_kind {
12901 rlx_ir::op::MaskKind::Causal => ki > qi,
12902 rlx_ir::op::MaskKind::SlidingWindow(w) => {
12903 ki > qi || ki + *w < qi
12904 }
12905 _ => false,
12906 };
12907 if pos_masked || (use_custom_mask && mk[bi * s + ki] < mask_thr)
12908 {
12909 scores_buf[qi * s + ki] = mask_neg;
12910 }
12911 }
12912 }
12913
12914 crate::kernels::neon_softmax(&mut scores_buf[..s * s], s, s);
12916
12917 for qi in 0..s {
12919 let o_base = bi * s * h + qi * h + hi * d_h;
12920 for d in 0..d_h {
12921 attn_out[o_base + d] = 0.0;
12922 }
12923 for ki in 0..s {
12924 let sc = scores_buf[qi * s + ki];
12925 if sc > score_thr {
12926 let v_base = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
12927 #[cfg(target_arch = "aarch64")]
12928 {
12929 use std::arch::aarch64::*;
12930 let vsc = vdupq_n_f32(sc);
12931 for c in 0..neon_chunks {
12932 let off = c * 4;
12933 let vo =
12934 vld1q_f32(attn_out.as_ptr().add(o_base + off));
12935 let vv = vld1q_f32(qkv.as_ptr().add(v_base + off));
12936 vst1q_f32(
12937 attn_out.as_mut_ptr().add(o_base + off),
12938 vfmaq_f32(vo, vsc, vv),
12939 );
12940 }
12941 }
12942 #[cfg(not(target_arch = "aarch64"))]
12943 for d in 0..d_h {
12944 attn_out[o_base + d] += sc * qkv[v_base + d];
12945 }
12946 }
12947 }
12948 }
12949 }
12950 }
12951
12952 crate::blas::sgemm(&attn_out, wo, dst, m, h, h);
12954 if *has_bias {
12955 let bias = sl(*out_b, base, h);
12956 crate::blas::bias_add(dst, bias, m, h);
12957 }
12958 }
12959 }
12960
12961 Thunk::Rope { .. } => exec_rope(thunk, base),
12962 Thunk::FusedBertLayer {
12963 hidden,
12964 qkv_w,
12965 qkv_b,
12966 out_w,
12967 out_b,
12968 mask,
12969 ln1_g,
12970 ln1_b,
12971 eps1,
12972 fc1_w,
12973 fc1_b,
12974 fc2_w,
12975 fc2_b,
12976 ln2_g,
12977 ln2_b,
12978 eps2,
12979 out,
12980 batch,
12981 seq,
12982 hs,
12983 nh,
12984 dh,
12985 int_dim,
12986 } => {
12987 let (b, s, h, n_h, d_h) = (
12988 *batch as usize,
12989 *seq as usize,
12990 *hs as usize,
12991 *nh as usize,
12992 *dh as usize,
12993 );
12994 let m = b * s;
12995 let id = *int_dim as usize;
12996 let scale = (d_h as f32).powf(-0.5);
12997 let _half = d_h / 2;
12998 #[cfg(target_arch = "aarch64")]
12999 let neon_chunks = d_h / 4;
13000 unsafe {
13001 let inp = sl(*hidden, base, m * h);
13002 let dst = sl_mut(*out, base, m * h);
13003 let mk = sl(*mask, base, b * s);
13004
13005 let qkv = std::slice::from_raw_parts_mut(fl_qkv.as_mut_ptr(), m * 3 * h);
13007 let attn = std::slice::from_raw_parts_mut(fl_attn.as_mut_ptr(), m * h);
13008 let res = std::slice::from_raw_parts_mut(fl_res.as_mut_ptr(), m * h);
13009 let normed = std::slice::from_raw_parts_mut(fl_normed.as_mut_ptr(), m * h);
13010 let ffn = std::slice::from_raw_parts_mut(fl_ffn.as_mut_ptr(), m * id);
13011 let sc = std::slice::from_raw_parts_mut(fl_sc.as_mut_ptr(), s * s);
13012
13013 crate::blas::par_sgemm_bias(
13015 inp,
13016 sl(*qkv_w, base, h * 3 * h),
13017 sl(*qkv_b, base, 3 * h),
13018 qkv,
13019 m,
13020 h,
13021 3 * h,
13022 );
13023
13024 for bi in 0..b {
13026 for hi in 0..n_h {
13027 for qi in 0..s {
13028 for ki in 0..s {
13029 let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
13030 let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
13031 #[cfg(target_arch = "aarch64")]
13032 let dot;
13033 #[cfg(not(target_arch = "aarch64"))]
13034 let mut dot = 0f32;
13035 #[cfg(target_arch = "aarch64")]
13036 {
13037 use std::arch::aarch64::*;
13038 let mut acc = vdupq_n_f32(0.0);
13039 for c in 0..neon_chunks {
13040 acc = vfmaq_f32(
13041 acc,
13042 vld1q_f32(qkv.as_ptr().add(q_base + c * 4)),
13043 vld1q_f32(qkv.as_ptr().add(k_base + c * 4)),
13044 );
13045 }
13046 dot = vaddvq_f32(acc);
13047 }
13048 #[cfg(not(target_arch = "aarch64"))]
13049 for d in 0..d_h {
13050 dot += qkv[q_base + d] * qkv[k_base + d];
13051 }
13052 sc[qi * s + ki] = dot * scale;
13053 if mk[bi * s + ki] < mask_thr {
13054 sc[qi * s + ki] = mask_neg;
13055 }
13056 }
13057 }
13058 crate::kernels::neon_softmax(&mut sc[..s * s], s, s);
13059 for qi in 0..s {
13060 let o = bi * s * h + qi * h + hi * d_h;
13061 for d in 0..d_h {
13062 attn[o + d] = 0.0;
13063 }
13064 for ki in 0..s {
13065 let w = sc[qi * s + ki];
13066 if w > score_thr {
13067 let v = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
13068 #[cfg(target_arch = "aarch64")]
13069 {
13070 use std::arch::aarch64::*;
13071 let vw = vdupq_n_f32(w);
13072 for c in 0..neon_chunks {
13073 let off = c * 4;
13074 vst1q_f32(
13075 attn.as_mut_ptr().add(o + off),
13076 vfmaq_f32(
13077 vld1q_f32(attn.as_ptr().add(o + off)),
13078 vw,
13079 vld1q_f32(qkv.as_ptr().add(v + off)),
13080 ),
13081 );
13082 }
13083 }
13084 #[cfg(not(target_arch = "aarch64"))]
13085 for d in 0..d_h {
13086 attn[o + d] += w * qkv[v + d];
13087 }
13088 }
13089 }
13090 }
13091 }
13092 }
13093
13094 crate::blas::sgemm_bias(
13096 attn,
13097 sl(*out_w, base, h * h),
13098 sl(*out_b, base, h),
13099 res,
13100 m,
13101 h,
13102 h,
13103 );
13104 #[cfg(target_arch = "aarch64")]
13105 {
13106 use std::arch::aarch64::*;
13107 let chunks_h = (m * h) / 4;
13108 for c in 0..chunks_h {
13109 let off = c * 4;
13110 vst1q_f32(
13111 res.as_mut_ptr().add(off),
13112 vaddq_f32(
13113 vld1q_f32(res.as_ptr().add(off)),
13114 vld1q_f32(inp.as_ptr().add(off)),
13115 ),
13116 );
13117 }
13118 for i in (chunks_h * 4)..(m * h) {
13119 res[i] += inp[i];
13120 }
13121 }
13122 #[cfg(not(target_arch = "aarch64"))]
13123 for i in 0..m * h {
13124 res[i] += inp[i];
13125 }
13126
13127 let g1 = sl(*ln1_g, base, h);
13129 let b1 = sl(*ln1_b, base, h);
13130 for r in 0..m {
13131 crate::kernels::layer_norm_row(
13132 &res[r * h..(r + 1) * h],
13133 g1,
13134 b1,
13135 &mut normed[r * h..(r + 1) * h],
13136 h,
13137 *eps1,
13138 );
13139 }
13140
13141 crate::blas::par_sgemm_bias(
13143 normed,
13144 sl(*fc1_w, base, h * id),
13145 sl(*fc1_b, base, id),
13146 ffn,
13147 m,
13148 h,
13149 id,
13150 );
13151 crate::kernels::par_gelu_inplace(ffn);
13152
13153 crate::blas::par_sgemm_bias(
13155 ffn,
13156 sl(*fc2_w, base, id * h),
13157 sl(*fc2_b, base, h),
13158 res,
13159 m,
13160 id,
13161 h,
13162 );
13163 #[cfg(target_arch = "aarch64")]
13164 {
13165 use std::arch::aarch64::*;
13166 let chunks_h = (m * h) / 4;
13167 for c in 0..chunks_h {
13168 let off = c * 4;
13169 vst1q_f32(
13170 res.as_mut_ptr().add(off),
13171 vaddq_f32(
13172 vld1q_f32(res.as_ptr().add(off)),
13173 vld1q_f32(normed.as_ptr().add(off)),
13174 ),
13175 );
13176 }
13177 for i in (chunks_h * 4)..(m * h) {
13178 res[i] += normed[i];
13179 }
13180 }
13181 #[cfg(not(target_arch = "aarch64"))]
13182 for i in 0..m * h {
13183 res[i] += normed[i];
13184 }
13185
13186 let g2 = sl(*ln2_g, base, h);
13188 let b2 = sl(*ln2_b, base, h);
13189 for r in 0..m {
13190 crate::kernels::layer_norm_row(
13191 &res[r * h..(r + 1) * h],
13192 g2,
13193 b2,
13194 &mut dst[r * h..(r + 1) * h],
13195 h,
13196 *eps2,
13197 );
13198 }
13199 }
13200 }
13201
13202 Thunk::FusedNomicLayer {
13203 hidden,
13204 qkv_w,
13205 out_w,
13206 mask,
13207 cos,
13208 sin,
13209 cos_len,
13210 ln1_g,
13211 ln1_b,
13212 eps1,
13213 fc11_w,
13214 fc12_w: _,
13215 fc2_w,
13216 ln2_g,
13217 ln2_b,
13218 eps2,
13219 out,
13220 batch,
13221 seq,
13222 hs,
13223 nh,
13224 dh,
13225 int_dim,
13226 interleaved,
13227 } => {
13228 let interleaved = *interleaved;
13229 let (b, s, h, n_h, d_h) = (
13230 *batch as usize,
13231 *seq as usize,
13232 *hs as usize,
13233 *nh as usize,
13234 *dh as usize,
13235 );
13236 let m = b * s;
13237 let id = *int_dim as usize;
13238 let scale = (d_h as f32).powf(-0.5);
13239 let half_dh = d_h / 2;
13240 #[cfg(target_arch = "aarch64")]
13241 let neon_chunks = d_h / 4;
13242 unsafe {
13243 let inp = sl(*hidden, base, m * h);
13244 let dst = sl_mut(*out, base, m * h);
13245 let mk = sl(*mask, base, b * s);
13246 let cos_tab = sl(*cos, base, *cos_len as usize);
13247 let sin_tab = sl(*sin, base, *cos_len as usize);
13248 let fused_fc_w = sl(*fc11_w, base, h * 2 * id);
13250
13251 let mut qkv = vec![0f32; m * 3 * h];
13252 let mut attn = vec![0f32; m * h];
13253 let mut res = vec![0f32; m * h];
13254 let mut normed = vec![0f32; m * h];
13255 let mut ffn_concat = vec![0f32; m * 2 * id]; let mut sc = vec![0f32; s * s];
13257
13258 crate::blas::sgemm(inp, sl(*qkv_w, base, h * 3 * h), &mut qkv, m, h, 3 * h);
13260
13261 for bi in 0..b {
13263 for hi in 0..n_h {
13264 for qi in 0..s {
13265 for ki in 0..s {
13266 let q_base = bi * s * 3 * h + qi * 3 * h + hi * d_h;
13267 let k_base = bi * s * 3 * h + ki * 3 * h + h + hi * d_h;
13268 let mut dot = 0f32;
13269 for i in 0..half_dh {
13270 let (o1, o2) = if interleaved {
13272 (2 * i, 2 * i + 1)
13273 } else {
13274 (i, half_dh + i)
13275 };
13276 let q1 = qkv[q_base + o1];
13277 let q2 = qkv[q_base + o2];
13278 let k1 = qkv[k_base + o1];
13279 let k2 = qkv[k_base + o2];
13280 let cq = cos_tab[qi * half_dh + i];
13281 let sq = sin_tab[qi * half_dh + i];
13282 let ck = cos_tab[ki * half_dh + i];
13283 let sk = sin_tab[ki * half_dh + i];
13284 dot += (q1 * cq - q2 * sq) * (k1 * ck - k2 * sk)
13285 + (q2 * cq + q1 * sq) * (k2 * ck + k1 * sk);
13286 }
13287 sc[qi * s + ki] = dot * scale;
13288 if mk[bi * s + ki] < mask_thr {
13289 sc[qi * s + ki] = mask_neg;
13290 }
13291 }
13292 }
13293 crate::kernels::neon_softmax(&mut sc[..s * s], s, s);
13294 for qi in 0..s {
13295 let o = bi * s * h + qi * h + hi * d_h;
13296 for d in 0..d_h {
13297 attn[o + d] = 0.0;
13298 }
13299 for ki in 0..s {
13300 let w = sc[qi * s + ki];
13301 if w > score_thr {
13302 let v = bi * s * 3 * h + ki * 3 * h + 2 * h + hi * d_h;
13303 #[cfg(target_arch = "aarch64")]
13304 {
13305 use std::arch::aarch64::*;
13306 let vw = vdupq_n_f32(w);
13307 for c in 0..neon_chunks {
13308 let off = c * 4;
13309 vst1q_f32(
13310 attn.as_mut_ptr().add(o + off),
13311 vfmaq_f32(
13312 vld1q_f32(attn.as_ptr().add(o + off)),
13313 vw,
13314 vld1q_f32(qkv.as_ptr().add(v + off)),
13315 ),
13316 );
13317 }
13318 }
13319 #[cfg(not(target_arch = "aarch64"))]
13320 for d in 0..d_h {
13321 attn[o + d] += w * qkv[v + d];
13322 }
13323 }
13324 }
13325 }
13326 }
13327 }
13328
13329 crate::blas::sgemm(&attn, sl(*out_w, base, h * h), &mut res, m, h, h);
13331 for i in 0..m * h {
13332 res[i] += inp[i];
13333 }
13334
13335 let g1 = sl(*ln1_g, base, h);
13337 let b1 = sl(*ln1_b, base, h);
13338 for r in 0..m {
13339 crate::kernels::layer_norm_row(
13340 &res[r * h..(r + 1) * h],
13341 g1,
13342 b1,
13343 &mut normed[r * h..(r + 1) * h],
13344 h,
13345 *eps1,
13346 );
13347 }
13348
13349 crate::blas::sgemm(&normed, fused_fc_w, &mut ffn_concat, m, h, 2 * id);
13351 for row in 0..m {
13354 let bo = row * 2 * id;
13355 for j in 0..id {
13357 let x = ffn_concat[bo + id + j];
13358 ffn_concat[bo + id + j] = x / (1.0 + (-x).exp());
13359 }
13360 for j in 0..id {
13362 ffn_concat[bo + j] *= ffn_concat[bo + id + j];
13363 }
13364 }
13365
13366 let mut swiglu_contig = vec![0f32; m * id];
13372 for row in 0..m {
13373 let bo = row * 2 * id;
13374 swiglu_contig[row * id..(row + 1) * id]
13375 .copy_from_slice(&ffn_concat[bo..bo + id]);
13376 }
13377 crate::blas::sgemm(
13378 &swiglu_contig,
13379 sl(*fc2_w, base, id * h),
13380 &mut res,
13381 m,
13382 id,
13383 h,
13384 );
13385 for i in 0..m * h {
13386 res[i] += normed[i];
13387 }
13388
13389 let g2 = sl(*ln2_g, base, h);
13391 let b2 = sl(*ln2_b, base, h);
13392 for r in 0..m {
13393 crate::kernels::layer_norm_row(
13394 &res[r * h..(r + 1) * h],
13395 g2,
13396 b2,
13397 &mut dst[r * h..(r + 1) * h],
13398 h,
13399 *eps2,
13400 );
13401 }
13402 }
13403 }
13404
13405 Thunk::FusedSwiGLU { .. } => exec_fused_swi_g_l_u(thunk, base),
13406 Thunk::Concat { .. } => exec_concat(thunk, base),
13407 Thunk::ConcatF64 { .. } => exec_concat_f64(thunk, base),
13408 Thunk::Compare {
13409 lhs,
13410 rhs,
13411 dst,
13412 len,
13413 op,
13414 inputs_i64,
13415 inputs_elem_bytes,
13416 dst_elem_bytes,
13417 } => {
13418 let len = *len as usize;
13419 let arena_len = arena_buf.len();
13420 let elem = (*inputs_elem_bytes).max(1) as usize;
13421 let dst_eb = (*dst_elem_bytes).max(1) as usize;
13422 let max_l = (arena_len.saturating_sub(*lhs)) / elem;
13423 let max_r = (arena_len.saturating_sub(*rhs)) / elem;
13424 let max_d = (arena_len.saturating_sub(*dst)) / dst_eb;
13425 let len = len.min(max_l).min(max_r).min(max_d);
13426 if trace_thunks && len > 0 {
13427 eprintln!("[compare] len={len} lhs={} rhs={} dst={}", *lhs, *rhs, *dst);
13428 }
13429 if elem == 1 {
13430 let l = arena_buf[*lhs..*lhs + len].to_vec();
13431 let r = arena_buf[*rhs..*rhs + len].to_vec();
13432 for i in 0..len {
13433 let v = match op {
13434 CmpOp::Eq => l[i] == r[i],
13435 CmpOp::Ne => l[i] != r[i],
13436 CmpOp::Lt => l[i] < r[i],
13437 CmpOp::Le => l[i] <= r[i],
13438 CmpOp::Gt => l[i] > r[i],
13439 CmpOp::Ge => l[i] >= r[i],
13440 };
13441 if *dst_elem_bytes == 1 {
13442 arena_buf[*dst + i] = u8::from(v);
13443 } else {
13444 unsafe {
13445 let o = sl_mut(*dst, base, len);
13446 o[i] = if v { 1.0 } else { 0.0 };
13447 }
13448 }
13449 }
13450 } else if *inputs_i64 != 0 {
13451 unsafe {
13452 let l = sl_i64(*lhs, base, len);
13453 let r = sl_i64(*rhs, base, len);
13454 for i in 0..len {
13455 let v = match op {
13456 CmpOp::Eq => l[i] == r[i],
13457 CmpOp::Ne => l[i] != r[i],
13458 CmpOp::Lt => l[i] < r[i],
13459 CmpOp::Le => l[i] <= r[i],
13460 CmpOp::Gt => l[i] > r[i],
13461 CmpOp::Ge => l[i] >= r[i],
13462 };
13463 if *dst_elem_bytes == 1 {
13464 arena_buf[*dst + i] = u8::from(v);
13465 } else {
13466 let o = sl_mut(*dst, base, len);
13467 o[i] = if v { 1.0 } else { 0.0 };
13468 }
13469 }
13470 }
13471 } else {
13472 unsafe {
13473 let l = sl(*lhs, base, len);
13474 let r = sl(*rhs, base, len);
13475 for i in 0..len {
13476 let v = match op {
13477 CmpOp::Eq => l[i] == r[i],
13478 CmpOp::Ne => l[i] != r[i],
13479 CmpOp::Lt => l[i] < r[i],
13480 CmpOp::Le => l[i] <= r[i],
13481 CmpOp::Gt => l[i] > r[i],
13482 CmpOp::Ge => l[i] >= r[i],
13483 };
13484 if *dst_elem_bytes == 1 {
13485 arena_buf[*dst + i] = u8::from(v);
13486 } else {
13487 let o = sl_mut(*dst, base, len);
13488 o[i] = if v { 1.0 } else { 0.0 };
13489 }
13490 }
13491 }
13492 }
13493 }
13494
13495 Thunk::Where {
13496 cond,
13497 on_true,
13498 on_false,
13499 dst,
13500 len,
13501 elem_bytes,
13502 cond_elem_bytes,
13503 } => {
13504 let len = *len as usize;
13505 let eb = *elem_bytes as usize;
13506 let cond_eb = (*cond_elem_bytes).max(1) as usize;
13507 let arena_len = arena_buf.len();
13508 let len = len
13509 .min((arena_len.saturating_sub(*cond)) / cond_eb)
13510 .min((arena_len.saturating_sub(*on_true)) / eb)
13511 .min((arena_len.saturating_sub(*on_false)) / eb)
13512 .min((arena_len.saturating_sub(*dst)) / eb);
13513 unsafe {
13514 if *elem_bytes == 8 {
13515 let t = sl_i64(*on_true, base, len);
13516 let e = sl_i64(*on_false, base, len);
13517 let o = sl_mut_i64(*dst, base, len);
13518 if *cond_elem_bytes == 1 {
13519 let c = &arena_buf[*cond..*cond + len];
13520 for i in 0..len {
13521 o[i] = if c[i] != 0 { t[i] } else { e[i] };
13522 }
13523 } else {
13524 let c = sl_i64(*cond, base, len);
13525 for i in 0..len {
13526 o[i] = if c[i] != 0 { t[i] } else { e[i] };
13527 }
13528 }
13529 } else if *cond_elem_bytes == 1 {
13530 let c = &arena_buf[*cond..*cond + len];
13531 let t = sl(*on_true, base, len);
13532 let e = sl(*on_false, base, len);
13533 let o = sl_mut(*dst, base, len);
13534 for i in 0..len {
13535 o[i] = if c[i] != 0 { t[i] } else { e[i] };
13536 }
13537 } else {
13538 let c = sl(*cond, base, len);
13539 let t = sl(*on_true, base, len);
13540 let e = sl(*on_false, base, len);
13541 let o = sl_mut(*dst, base, len);
13542 for i in 0..len {
13543 o[i] = if c[i] != 0.0 { t[i] } else { e[i] };
13544 }
13545 }
13546 }
13547 }
13548
13549 Thunk::Fma {
13550 a,
13551 b,
13552 c,
13553 dst,
13554 len,
13555 elem_bytes,
13556 } => {
13557 let len = *len as usize;
13558 let eb = (*elem_bytes).max(1) as usize;
13559 let arena_len = arena_buf.len();
13560 let len = len
13561 .min(arena_len.saturating_sub(*a) / eb)
13562 .min(arena_len.saturating_sub(*b) / eb)
13563 .min(arena_len.saturating_sub(*c) / eb)
13564 .min(arena_len.saturating_sub(*dst) / eb);
13565 unsafe {
13566 if *elem_bytes == 8 {
13567 let av = sl_f64(*a, base, len);
13568 let bv = sl_f64(*b, base, len);
13569 let cv = sl_f64(*c, base, len);
13570 let o = sl_mut_f64(*dst, base, len);
13571 for i in 0..len {
13572 o[i] = av[i].mul_add(bv[i], cv[i]);
13573 }
13574 } else {
13575 let av = sl(*a, base, len);
13576 let bv = sl(*b, base, len);
13577 let cv = sl(*c, base, len);
13578 let o = sl_mut(*dst, base, len);
13579 for i in 0..len {
13580 o[i] = av[i].mul_add(bv[i], cv[i]);
13581 }
13582 }
13583 }
13584 }
13585
13586 Thunk::ScatterAdd { .. } => exec_scatter_add(thunk, base),
13587 Thunk::GroupedMatMul {
13588 input,
13589 weight,
13590 expert_idx,
13591 dst,
13592 m,
13593 k_dim,
13594 n,
13595 num_experts,
13596 } => {
13597 let m = *m as usize;
13598 let k_dim = *k_dim as usize;
13599 let n = *n as usize;
13600 let num_experts = *num_experts as usize;
13601 unsafe {
13602 let inp = sl(*input, base, m * k_dim);
13603 let wt = sl(*weight, base, num_experts * k_dim * n);
13604 let ids = sl(*expert_idx, base, m);
13605 let out = sl_mut(*dst, base, m * n);
13606
13607 let mut counts = vec![0usize; num_experts];
13610 for i in 0..m {
13611 let e = ids[i] as usize;
13612 debug_assert!(
13613 e < num_experts,
13614 "expert_idx out of range: {e} >= {num_experts}"
13615 );
13616 counts[e] += 1;
13617 }
13618 let mut offsets = vec![0usize; num_experts + 1];
13620 for e in 0..num_experts {
13621 offsets[e + 1] = offsets[e] + counts[e];
13622 }
13623 let mut packed_in = vec![0f32; m * k_dim];
13627 let mut original_pos = vec![0usize; m];
13628 let mut write_idx = vec![0usize; num_experts];
13629 for i in 0..m {
13630 let e = ids[i] as usize;
13631 let dst_row = offsets[e] + write_idx[e];
13632 packed_in[dst_row * k_dim..(dst_row + 1) * k_dim]
13633 .copy_from_slice(&inp[i * k_dim..(i + 1) * k_dim]);
13634 original_pos[dst_row] = i;
13635 write_idx[e] += 1;
13636 }
13637
13638 let mut packed_out = vec![0f32; m * n];
13642 let expert_stride = k_dim * n;
13643 let gmm_ord = crate::moe_residency::next_gmm_ord();
13644 let moe_layer = gmm_ord / 3;
13645 for e in 0..num_experts {
13646 let count = counts[e];
13647 if count == 0 {
13648 continue;
13649 }
13650 crate::moe_residency::record_expert_tokens(moe_layer, e, count);
13651 let in_start = offsets[e];
13652 let in_slice = &packed_in[in_start * k_dim..(in_start + count) * k_dim];
13653 let w_slab: &[f32] =
13654 if !crate::moe_residency::expert_on_device_for_layer(moe_layer, e) {
13655 if let Some(ptr) =
13656 crate::moe_residency::host_expert_weight_ptr(gmm_ord, e)
13657 {
13658 std::slice::from_raw_parts(ptr, expert_stride)
13659 } else {
13660 &wt[e * expert_stride..(e + 1) * expert_stride]
13661 }
13662 } else {
13663 &wt[e * expert_stride..(e + 1) * expert_stride]
13664 };
13665 let out_slice = &mut packed_out[in_start * n..(in_start + count) * n];
13666 crate::blas::sgemm(in_slice, w_slab, out_slice, count, k_dim, n);
13667 }
13668
13669 for packed_idx in 0..m {
13671 let i = original_pos[packed_idx];
13672 out[i * n..(i + 1) * n]
13673 .copy_from_slice(&packed_out[packed_idx * n..(packed_idx + 1) * n]);
13674 }
13675 }
13676 }
13677
13678 Thunk::DequantGroupedMatMulGguf { .. } => {
13679 exec_dequant_grouped_mat_mul_gguf(thunk, base)
13680 }
13681 Thunk::DequantMoEWeightsGguf { .. } => exec_dequant_mo_e_weights_gguf(thunk, base),
13682 Thunk::TopK {
13683 src,
13684 dst,
13685 outer,
13686 axis_dim,
13687 k,
13688 indices_i64,
13689 } => {
13690 let outer = *outer as usize;
13691 let axis_dim = *axis_dim as usize;
13692 let k = *k as usize;
13693 unsafe {
13694 let inp = sl(*src, base, outer * axis_dim);
13695 let mut row_buf: Vec<f32> = vec![0.0; axis_dim];
13699 if *indices_i64 != 0 {
13700 let out = sl_mut_i64(*dst, base, outer * k);
13701 for o in 0..outer {
13702 row_buf.copy_from_slice(&inp[o * axis_dim..(o + 1) * axis_dim]);
13703 for ki in 0..k {
13704 let mut best_i = 0usize;
13705 let mut best_v = row_buf[0];
13706 for i in 1..axis_dim {
13707 let v = row_buf[i];
13708 if v > best_v {
13709 best_v = v;
13710 best_i = i;
13711 }
13712 }
13713 out[o * k + ki] = best_i as i64;
13714 row_buf[best_i] = f32::NEG_INFINITY;
13715 }
13716 }
13717 } else {
13718 let out = sl_mut(*dst, base, outer * k);
13719 for o in 0..outer {
13720 row_buf.copy_from_slice(&inp[o * axis_dim..(o + 1) * axis_dim]);
13721 for ki in 0..k {
13722 let mut best_i = 0usize;
13723 let mut best_v = row_buf[0];
13724 for i in 1..axis_dim {
13725 let v = row_buf[i];
13726 if v > best_v {
13727 best_v = v;
13728 best_i = i;
13729 }
13730 }
13731 out[o * k + ki] = best_i as f32;
13732 row_buf[best_i] = f32::NEG_INFINITY;
13733 }
13734 }
13735 if let Some(cap) = schedule.moe_topk_capture.as_ref() {
13736 cap.push_topk_f32(&out[..outer * k], axis_dim);
13737 }
13738 }
13739 }
13740 }
13741
13742 Thunk::Reduce { .. } => exec_reduce(thunk, base),
13743 Thunk::ArgReduce { .. } => exec_arg_reduce(thunk, base),
13744 Thunk::Conv2D1x1 { .. } => exec_conv2_d1x1(thunk, base),
13745 Thunk::Conv2D { .. } => exec_conv2_d(thunk, base),
13746 Thunk::Conv3d { .. } => exec_conv3d(thunk, base),
13747 Thunk::ConvTranspose3d { .. } => exec_conv_transpose3d(thunk, base),
13748 Thunk::Pool2D {
13749 src,
13750 dst,
13751 n,
13752 c,
13753 h,
13754 w,
13755 h_out,
13756 w_out,
13757 kh,
13758 kw,
13759 sh,
13760 sw,
13761 ph,
13762 pw,
13763 kind,
13764 } => {
13765 let n = *n as usize;
13766 let c = *c as usize;
13767 let h = *h as usize;
13768 let w = *w as usize;
13769 let h_out = *h_out as usize;
13770 let w_out = *w_out as usize;
13771 let kh = *kh as usize;
13772 let kw = *kw as usize;
13773 let sh = *sh as usize;
13774 let sw = *sw as usize;
13775 let ph = *ph as usize;
13776 let pw = *pw as usize;
13777 let kernel_area = (kh * kw) as f32;
13778 unsafe {
13779 let inp = sl(*src, base, n * c * h * w);
13780 let out = sl_mut(*dst, base, n * c * h_out * w_out);
13781 let out_addr = out.as_mut_ptr() as usize;
13785 let is_max = matches!(kind, ReduceOp::Max);
13786 let is_mean = matches!(kind, ReduceOp::Mean);
13787 let nopad = ph == 0 && pw == 0;
13791 let pool_plane = |nc: usize| {
13792 let ni = nc / c;
13793 let ci = nc % c;
13794 let in_chan = ni * c * h * w + ci * h * w;
13795 let out_chan = ni * c * h_out * w_out + ci * h_out * w_out;
13796 let op = out_addr as *mut f32;
13797 for ho in 0..h_out {
13798 for wo in 0..w_out {
13799 let acc = if nopad {
13800 let row0 = in_chan + (ho * sh) * w + wo * sw;
13801 let mut a = if is_max { f32::NEG_INFINITY } else { 0.0 };
13802 for ki in 0..kh {
13803 let row = row0 + ki * w;
13804 if is_max {
13805 for kj in 0..kw {
13806 a = a.max(inp[row + kj]);
13807 }
13808 } else {
13809 for kj in 0..kw {
13810 a += inp[row + kj];
13811 }
13812 }
13813 }
13814 a
13815 } else {
13816 let mut a = if is_max { f32::NEG_INFINITY } else { 0.0 };
13817 for ki in 0..kh {
13818 for kj in 0..kw {
13819 let hi = ho * sh + ki;
13820 let wi = wo * sw + kj;
13821 if hi < ph || wi < pw {
13822 continue;
13823 }
13824 let hi = hi - ph;
13825 let wi = wi - pw;
13826 if hi >= h || wi >= w {
13827 continue;
13828 }
13829 let v = inp[in_chan + hi * w + wi];
13830 if is_max {
13831 a = a.max(v);
13832 } else {
13833 a += v;
13834 }
13835 }
13836 }
13837 a
13838 };
13839 let acc = if is_mean { acc / kernel_area } else { acc };
13840 *op.add(out_chan + ho * w_out + wo) = acc;
13841 }
13842 }
13843 };
13844 if fast_conv_enabled() && crate::pool::should_parallelize(n * c * h_out * w_out)
13845 {
13846 crate::pool::par_for(
13847 n * c,
13848 crate::pool::outer_chunk(n * c),
13849 &|off, cnt| {
13850 for nc in off..off + cnt {
13851 pool_plane(nc);
13852 }
13853 },
13854 );
13855 } else {
13856 for nc in 0..n * c {
13857 pool_plane(nc);
13858 }
13859 }
13860 }
13861 }
13862
13863 Thunk::ReluBackward { .. } => exec_relu_backward(thunk, base),
13864 Thunk::ReluBackwardF64 { .. } => exec_relu_backward_f64(thunk, base),
13865 Thunk::QMatMul { .. } => exec_q_mat_mul(thunk, base),
13866 Thunk::QConv2d {
13867 x,
13868 w,
13869 bias,
13870 out,
13871 n,
13872 c_in,
13873 h,
13874 w_in,
13875 c_out,
13876 h_out,
13877 w_out,
13878 kh,
13879 kw,
13880 sh,
13881 sw,
13882 ph,
13883 pw,
13884 dh,
13885 dw,
13886 groups,
13887 x_zp,
13888 w_zp,
13889 out_zp,
13890 mult,
13891 } => {
13892 let n = *n as usize;
13893 let c_in = *c_in as usize;
13894 let h = *h as usize;
13895 let w_in = *w_in as usize;
13896 let c_out = *c_out as usize;
13897 let h_out = *h_out as usize;
13898 let w_out = *w_out as usize;
13899 let kh = *kh as usize;
13900 let kw = *kw as usize;
13901 let sh = *sh as usize;
13902 let sw = *sw as usize;
13903 let ph = *ph as usize;
13904 let pw = *pw as usize;
13905 let dh = *dh as usize;
13906 let dw = *dw as usize;
13907 let groups = *groups as usize;
13908 let c_in_per_g = c_in / groups;
13909 let c_out_per_g = c_out / groups;
13910 unsafe {
13911 let x_ptr = base.add(*x) as *const i8;
13912 let w_ptr = base.add(*w) as *const i8;
13913 let bias_ptr = base.add(*bias) as *const i32;
13914 let out_ptr = base.add(*out) as *mut i8;
13915 for ni in 0..n {
13916 for co in 0..c_out {
13917 let g = co / c_out_per_g;
13918 let ci_start = g * c_in_per_g;
13919 for ho in 0..h_out {
13920 for wo in 0..w_out {
13921 let mut acc: i32 = *bias_ptr.add(co);
13922 for ci_off in 0..c_in_per_g {
13923 let ci = ci_start + ci_off;
13924 let in_chan = ((ni * c_in) + ci) * h * w_in;
13925 let wt_chan = ((co * c_in_per_g) + ci_off) * kh * kw;
13926 for ki in 0..kh {
13927 for kj in 0..kw {
13928 let hi = ho * sh + ki * dh;
13929 let wi = wo * sw + kj * dw;
13930 if hi < ph || wi < pw {
13931 continue;
13932 }
13933 let hi = hi - ph;
13934 let wi = wi - pw;
13935 if hi >= h || wi >= w_in {
13936 continue;
13937 }
13938 let xv = *x_ptr.add(in_chan + hi * w_in + wi)
13939 as i32
13940 - *x_zp;
13941 let wv = *w_ptr.add(wt_chan + ki * kw + kj) as i32
13942 - *w_zp;
13943 acc += xv * wv;
13944 }
13945 }
13946 }
13947 let r = (acc as f32 * *mult).round() as i32 + *out_zp;
13948 let r = r.clamp(-128, 127) as i8;
13949 let dst = ((ni * c_out) + co) * h_out * w_out + ho * w_out + wo;
13950 *out_ptr.add(dst) = r;
13951 }
13952 }
13953 }
13954 }
13955 }
13956 }
13957
13958 Thunk::Quantize { .. } => exec_quantize(thunk, base),
13959 Thunk::Dequantize { .. } => exec_dequantize(thunk, base),
13960 Thunk::FakeQuantize { .. } => exec_fake_quantize(thunk, base),
13961 Thunk::ActivationBackward { .. } => exec_activation_backward(thunk, base),
13962 Thunk::ActivationBackwardF64 { .. } => exec_activation_backward_f64(thunk, base),
13963 Thunk::FakeQuantizeLSQ { .. } => exec_fake_quantize_l_s_q(thunk, base),
13964 Thunk::FakeQuantizeLSQBackwardX { .. } => {
13965 exec_fake_quantize_l_s_q_backward_x(thunk, base)
13966 }
13967 Thunk::FakeQuantizeLSQBackwardScale { .. } => {
13968 exec_fake_quantize_l_s_q_backward_scale(thunk, base)
13969 }
13970 Thunk::FakeQuantizeBackward { .. } => exec_fake_quantize_backward(thunk, base),
13971 Thunk::LayerNormBackwardInput { .. } => exec_layer_norm_backward_input(thunk, base),
13972 Thunk::BatchNormInferenceBackwardInput { .. } => {
13973 exec_batch_norm_inference_backward_input(thunk, base)
13974 }
13975 Thunk::BatchNormInferenceBackwardGamma { .. } => {
13976 exec_batch_norm_inference_backward_gamma(thunk, base)
13977 }
13978 Thunk::BatchNormInferenceBackwardBeta { .. } => {
13979 exec_batch_norm_inference_backward_beta(thunk, base)
13980 }
13981 Thunk::LayerNormBackwardGamma { .. } => exec_layer_norm_backward_gamma(thunk, base),
13982 Thunk::RmsNormBackwardInput { .. } => exec_rms_norm_backward_input(thunk, base),
13983 Thunk::RmsNormBackwardGamma { .. } => exec_rms_norm_backward_gamma(thunk, base),
13984 Thunk::RmsNormBackwardBeta { .. } => exec_rms_norm_backward_beta(thunk, base),
13985 Thunk::RopeBackward { .. } => exec_rope_backward(thunk, base),
13986 Thunk::CumsumBackward { .. } => exec_cumsum_backward(thunk, base),
13987 Thunk::GroupNormBackwardInput { .. } => exec_group_norm_backward_input(thunk, base),
13988 Thunk::GroupNormBackwardGamma { .. } => exec_group_norm_backward_gamma(thunk, base),
13989 Thunk::GroupNormBackwardBeta { .. } => exec_group_norm_backward_beta(thunk, base),
13990 Thunk::GatherBackward { .. } => exec_gather_backward(thunk, base),
13991 Thunk::MaxPool2dBackward { .. } => exec_max_pool2d_backward(thunk, base),
13992 Thunk::Conv2dBackwardInput { .. } => exec_conv2d_backward_input(thunk, base),
13993 Thunk::Conv2dBackwardWeight { .. } => exec_conv2d_backward_weight(thunk, base),
13994 Thunk::Im2Col { .. } => exec_im2_col(thunk, base),
13995 Thunk::SoftmaxCrossEntropyDense { .. } => exec_softmax_cross_entropy_dense(thunk, base),
13996 Thunk::SoftmaxCrossEntropy { .. } => exec_softmax_cross_entropy(thunk, base),
13997 Thunk::SoftmaxCrossEntropyBackward { .. } => {
13998 exec_softmax_cross_entropy_backward(thunk, base)
13999 }
14000 Thunk::GatherAxis { .. } => exec_gather_axis(thunk, base),
14001 Thunk::Transpose {
14002 src,
14003 dst,
14004 in_total,
14005 out_dims,
14006 in_strides,
14007 elem_bytes,
14008 } => {
14009 let rank = out_dims.len();
14014 let total: usize = out_dims.iter().map(|&d| d as usize).product();
14015 let in_total = *in_total as usize;
14016 unsafe {
14017 if *elem_bytes == 1 {
14018 let inp = arena_buf[*src..*src + in_total].to_vec();
14023 let out = &mut arena_buf[*dst..*dst + total];
14024 let mut idx = vec![0usize; rank];
14025 for o in 0..total {
14026 let mut src_idx = 0usize;
14027 for d in 0..rank {
14028 src_idx += idx[d] * in_strides[d] as usize;
14029 }
14030 out[o] = inp[broadcast_src_index(src_idx, in_total)];
14031 for d in (0..rank).rev() {
14032 idx[d] += 1;
14033 if idx[d] < out_dims[d] as usize {
14034 break;
14035 }
14036 idx[d] = 0;
14037 }
14038 }
14039 } else if *elem_bytes == 8 {
14040 let inp = sl_i64(*src, base, in_total);
14041 let out = sl_mut_i64(*dst, base, total);
14042 let mut idx = vec![0usize; rank];
14043 for o in 0..total {
14044 let mut src_idx = 0usize;
14045 for d in 0..rank {
14046 src_idx += idx[d] * in_strides[d] as usize;
14047 }
14048 out[o] = inp[broadcast_src_index(src_idx, in_total)];
14049 for d in (0..rank).rev() {
14050 idx[d] += 1;
14051 if idx[d] < out_dims[d] as usize {
14052 break;
14053 }
14054 idx[d] = 0;
14055 }
14056 }
14057 } else {
14058 let inp = sl(*src, base, in_total);
14059 let out = sl_mut(*dst, base, total);
14060 if rank == 4
14061 && in_strides[0] == 0
14062 && in_strides[2] == 0
14063 && in_strides[3] == 0
14064 && in_strides[1] != 0
14065 {
14066 let d1 = out_dims[1] as usize;
14072 let sc = in_strides[1] as usize;
14073 let plane = (out_dims[2] as usize) * (out_dims[3] as usize);
14074 let nc_total = (out_dims[0] as usize) * d1;
14075 let out_addr = out.as_mut_ptr() as usize;
14076 let fill = |nc0: usize, nc1: usize| {
14077 let op = out_addr as *mut f32;
14078 for nc in nc0..nc1 {
14079 let v = inp[(nc % d1) * sc];
14080 let base_off = nc * plane;
14081 for k in 0..plane {
14082 *op.add(base_off + k) = v;
14083 }
14084 }
14085 };
14086 if fast_conv_enabled() && crate::pool::should_parallelize(total) {
14087 crate::pool::par_for(
14088 nc_total,
14089 crate::pool::outer_chunk(nc_total),
14090 &|off, cnt| fill(off, off + cnt),
14091 );
14092 } else {
14093 fill(0, nc_total);
14094 }
14095 } else if rank == 2 && in_strides[0] != 0 && in_strides[1] != 0 {
14096 let d0 = out_dims[0] as usize;
14101 let d1 = out_dims[1] as usize;
14102 let s0 = in_strides[0] as usize;
14103 let s1 = in_strides[1] as usize;
14104 let out_addr = out.as_mut_ptr() as usize;
14105 let tile = |i0: usize, i1: usize| {
14106 let op = out_addr as *mut f32;
14107 const T: usize = 32;
14108 let mut j0 = 0;
14109 while j0 < d1 {
14110 let j1 = (j0 + T).min(d1);
14111 for i in i0..i1 {
14112 let inb = i * s0;
14113 let outb = i * d1;
14114 for j in j0..j1 {
14115 *op.add(outb + j) = inp[inb + j * s1];
14116 }
14117 }
14118 j0 = j1;
14119 }
14120 };
14121 if fast_conv_enabled() && crate::pool::should_parallelize(total) {
14122 crate::pool::par_for(
14123 d0,
14124 crate::pool::outer_chunk(d0),
14125 &|off, cnt| tile(off, off + cnt),
14126 );
14127 } else {
14128 tile(0, d0);
14129 }
14130 } else if fast_conv_enabled() && crate::pool::should_parallelize(total) {
14131 let out_addr = out.as_mut_ptr() as usize;
14135 crate::pool::par_for(
14136 total,
14137 crate::pool::chunk_floor(total),
14138 &|off, cnt| {
14139 let mut idx = vec![0usize; rank];
14140 let mut rem = off;
14141 for d in (0..rank).rev() {
14142 let dim = out_dims[d] as usize;
14143 idx[d] = rem % dim;
14144 rem /= dim;
14145 }
14146 for o in off..off + cnt {
14147 let mut src_idx = 0usize;
14148 for d in 0..rank {
14149 src_idx += idx[d] * in_strides[d] as usize;
14150 }
14151 let v = inp[broadcast_src_index(src_idx, in_total)];
14152 *((out_addr as *mut f32).add(o)) = v;
14153 for d in (0..rank).rev() {
14154 idx[d] += 1;
14155 if idx[d] < out_dims[d] as usize {
14156 break;
14157 }
14158 idx[d] = 0;
14159 }
14160 }
14161 },
14162 );
14163 } else {
14164 let mut idx = vec![0usize; rank];
14165 for o in 0..total {
14166 let mut src_idx = 0usize;
14167 for d in 0..rank {
14168 src_idx += idx[d] * in_strides[d] as usize;
14169 }
14170 out[o] = inp[broadcast_src_index(src_idx, in_total)];
14171 for d in (0..rank).rev() {
14172 idx[d] += 1;
14173 if idx[d] < out_dims[d] as usize {
14174 break;
14175 }
14176 idx[d] = 0;
14177 }
14178 }
14179 }
14180 }
14181 }
14182 }
14183
14184 Thunk::CustomOp { .. } => exec_custom_op(thunk, base),
14185 Thunk::Reverse { .. } => exec_reverse(thunk, base),
14186 }
14187 if trace_done {
14188 eprintln!("[thunk {i} done]");
14189 }
14190 }
14191}
14192
14193#[inline(always)]
14194fn exec_nop(t: &Thunk) {
14195 let Thunk::Nop = t else { unreachable!() };
14196 {}
14197}
14198
14199#[inline(always)]
14200fn exec_elementwise_region(t: &Thunk, base: *mut u8) {
14201 let Thunk::ElementwiseRegion {
14202 dst,
14203 len,
14204 input_offs,
14205 chain,
14206 scalar_input_mask,
14207 input_modulus,
14208 } = t
14209 else {
14210 unreachable!()
14211 };
14212 {
14213 let len = *len as usize;
14214 if !chain.is_empty() && len > 0 {
14215 let base_addr = base as usize;
14216 let dst = *dst;
14217 let scalar_mask = *scalar_input_mask;
14218 let eval = |gid: usize| {
14220 let v = region_eval_elem(
14221 gid,
14222 base_addr as *const u8,
14223 input_offs,
14224 chain,
14225 scalar_mask,
14226 input_modulus,
14227 );
14228 unsafe {
14229 *((base_addr as *mut u8).add(dst) as *mut f32).add(gid) = v;
14230 }
14231 };
14232 if fast_conv_enabled() && crate::pool::should_parallelize(len) {
14233 crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
14234 for gid in off..off + cnt {
14235 eval(gid);
14236 }
14237 });
14238 } else {
14239 for gid in 0..len {
14240 eval(gid);
14241 }
14242 }
14243 }
14244 }
14245}
14246
14247#[inline(always)]
14248fn exec_gaussian_splat_render(t: &Thunk, base: *mut u8) {
14249 let Thunk::GaussianSplatRender {
14250 positions_off,
14251 positions_len,
14252 scales_off,
14253 scales_len,
14254 rotations_off,
14255 rotations_len,
14256 opacities_off,
14257 opacities_len,
14258 colors_off,
14259 colors_len,
14260 sh_coeffs_off,
14261 sh_coeffs_len,
14262 meta_off,
14263 dst_off,
14264 dst_len,
14265 width,
14266 height,
14267 tile_size,
14268 radius_scale,
14269 alpha_cutoff,
14270 max_splat_steps,
14271 transmittance_threshold,
14272 max_list_entries,
14273 } = t
14274 else {
14275 unreachable!()
14276 };
14277 unsafe {
14278 crate::splat::execute_gaussian_splat_render(
14279 *positions_off,
14280 *positions_len,
14281 *scales_off,
14282 *scales_len,
14283 *rotations_off,
14284 *rotations_len,
14285 *opacities_off,
14286 *opacities_len,
14287 *colors_off,
14288 *colors_len,
14289 *sh_coeffs_off,
14290 *sh_coeffs_len,
14291 *meta_off,
14292 *dst_off,
14293 *dst_len,
14294 *width,
14295 *height,
14296 *tile_size,
14297 *radius_scale,
14298 *alpha_cutoff,
14299 *max_splat_steps,
14300 *transmittance_threshold,
14301 *max_list_entries,
14302 base,
14303 );
14304 }
14305}
14306
14307#[inline(always)]
14308fn exec_gaussian_splat_render_backward(t: &Thunk, base: *mut u8) {
14309 let Thunk::GaussianSplatRenderBackward {
14310 positions_off,
14311 positions_len,
14312 scales_off,
14313 scales_len,
14314 rotations_off,
14315 rotations_len,
14316 opacities_off,
14317 opacities_len,
14318 colors_off,
14319 colors_len,
14320 sh_coeffs_off,
14321 sh_coeffs_len,
14322 meta_off,
14323 d_loss_off,
14324 d_loss_len,
14325 packed_off,
14326 packed_len,
14327 width,
14328 height,
14329 tile_size,
14330 radius_scale,
14331 alpha_cutoff,
14332 max_splat_steps,
14333 transmittance_threshold,
14334 max_list_entries,
14335 loss_grad_clip,
14336 sh_band,
14337 max_anisotropy,
14338 } = t
14339 else {
14340 unreachable!()
14341 };
14342 unsafe {
14343 crate::splat::execute_gaussian_splat_render_backward(
14344 *positions_off,
14345 *positions_len,
14346 *scales_off,
14347 *scales_len,
14348 *rotations_off,
14349 *rotations_len,
14350 *opacities_off,
14351 *opacities_len,
14352 *colors_off,
14353 *colors_len,
14354 *sh_coeffs_off,
14355 *sh_coeffs_len,
14356 *meta_off,
14357 *d_loss_off,
14358 *d_loss_len,
14359 *packed_off,
14360 *packed_len,
14361 *width,
14362 *height,
14363 *tile_size,
14364 *radius_scale,
14365 *alpha_cutoff,
14366 *max_splat_steps,
14367 *transmittance_threshold,
14368 *max_list_entries,
14369 *loss_grad_clip,
14370 *sh_band,
14371 *max_anisotropy,
14372 base,
14373 );
14374 }
14375}
14376
14377#[inline(always)]
14378fn exec_gaussian_splat_prepare(t: &Thunk, base: *mut u8) {
14379 let Thunk::GaussianSplatPrepare {
14380 positions_off,
14381 positions_len,
14382 scales_off,
14383 scales_len,
14384 rotations_off,
14385 rotations_len,
14386 opacities_off,
14387 opacities_len,
14388 colors_off,
14389 colors_len,
14390 sh_coeffs_off,
14391 sh_coeffs_len,
14392 meta_off,
14393 meta_len,
14394 prep_off,
14395 prep_len,
14396 width,
14397 height,
14398 tile_size,
14399 radius_scale,
14400 alpha_cutoff,
14401 max_splat_steps,
14402 transmittance_threshold,
14403 max_list_entries,
14404 } = t
14405 else {
14406 unreachable!()
14407 };
14408 unsafe {
14409 crate::splat::execute_gaussian_splat_prepare(
14410 *positions_off,
14411 *positions_len,
14412 *scales_off,
14413 *scales_len,
14414 *rotations_off,
14415 *rotations_len,
14416 *opacities_off,
14417 *opacities_len,
14418 *colors_off,
14419 *colors_len,
14420 *sh_coeffs_off,
14421 *sh_coeffs_len,
14422 *meta_off,
14423 *meta_len,
14424 *prep_off,
14425 *prep_len,
14426 *width,
14427 *height,
14428 *tile_size,
14429 *radius_scale,
14430 *alpha_cutoff,
14431 *max_splat_steps,
14432 *transmittance_threshold,
14433 *max_list_entries,
14434 base,
14435 );
14436 }
14437}
14438
14439#[inline(always)]
14440fn exec_gaussian_splat_rasterize(t: &Thunk, base: *mut u8) {
14441 let Thunk::GaussianSplatRasterize {
14442 prep_off,
14443 prep_len,
14444 meta_off,
14445 meta_len,
14446 dst_off,
14447 dst_len,
14448 count,
14449 width,
14450 height,
14451 tile_size,
14452 alpha_cutoff,
14453 max_splat_steps,
14454 transmittance_threshold,
14455 max_list_entries,
14456 } = t
14457 else {
14458 unreachable!()
14459 };
14460 unsafe {
14461 crate::splat::execute_gaussian_splat_rasterize(
14462 *prep_off,
14463 *prep_len,
14464 *meta_off,
14465 *meta_len,
14466 *dst_off,
14467 *dst_len,
14468 *count,
14469 *width,
14470 *height,
14471 *tile_size,
14472 *alpha_cutoff,
14473 *max_splat_steps,
14474 *transmittance_threshold,
14475 *max_list_entries,
14476 base,
14477 );
14478 }
14479}
14480
14481#[inline(always)]
14482fn exec_fft1d(t: &Thunk, base: *mut u8) {
14483 let Thunk::Fft1d {
14484 src,
14485 dst,
14486 outer,
14487 n_complex,
14488 inverse,
14489 norm_tag,
14490 dtype,
14491 } = t
14492 else {
14493 unreachable!()
14494 };
14495 unsafe {
14496 match dtype {
14497 rlx_ir::DType::F64 => execute_fft1d_f64(
14498 *src,
14499 *dst,
14500 *outer as usize,
14501 *n_complex as usize,
14502 *inverse,
14503 *norm_tag,
14504 base,
14505 ),
14506 rlx_ir::DType::F32 => execute_fft1d_f32(
14507 *src,
14508 *dst,
14509 *outer as usize,
14510 *n_complex as usize,
14511 *inverse,
14512 *norm_tag,
14513 base,
14514 ),
14515 rlx_ir::DType::C64 => execute_fft1d_c64(
14516 *src,
14517 *dst,
14518 *outer as usize,
14519 *n_complex as usize,
14520 *inverse,
14521 *norm_tag,
14522 base,
14523 ),
14524 other => panic!("Op::Fft on CPU requires F32/F64/C64, got {other:?}"),
14525 }
14526 }
14527}
14528
14529#[inline(always)]
14530fn exec_fft_butterfly_stage(t: &Thunk, base: *mut u8) {
14531 let Thunk::FftButterflyStage {
14532 state_src,
14533 state_dst,
14534 gate_src,
14535 rev_src,
14536 tw_re_src,
14537 tw_im_src,
14538 batch,
14539 n_fft,
14540 stage,
14541 } = t
14542 else {
14543 unreachable!()
14544 };
14545 unsafe {
14546 execute_fft_butterfly_stage_f32(
14547 *state_src,
14548 *state_dst,
14549 *gate_src,
14550 *rev_src,
14551 *tw_re_src,
14552 *tw_im_src,
14553 *batch as usize,
14554 *n_fft as usize,
14555 *stage as usize,
14556 base,
14557 );
14558 }
14559}
14560
14561#[inline(always)]
14562fn exec_log_mel(t: &Thunk, base: *mut u8) {
14563 let Thunk::LogMel {
14564 spec,
14565 filters,
14566 dst,
14567 outer,
14568 n_fft,
14569 n_bins,
14570 n_mels,
14571 } = t
14572 else {
14573 unreachable!()
14574 };
14575 unsafe {
14576 execute_log_mel_f32(
14577 *spec,
14578 *filters,
14579 *dst,
14580 *outer as usize,
14581 *n_fft as usize,
14582 *n_bins as usize,
14583 *n_mels as usize,
14584 base,
14585 );
14586 }
14587}
14588
14589#[inline(always)]
14590fn exec_log_mel_backward(t: &Thunk, base: *mut u8) {
14591 let Thunk::LogMelBackward {
14592 spec,
14593 filters,
14594 dy,
14595 dst,
14596 outer,
14597 n_fft,
14598 n_bins,
14599 n_mels,
14600 } = t
14601 else {
14602 unreachable!()
14603 };
14604 unsafe {
14605 execute_log_mel_backward_f32(
14606 *spec,
14607 *filters,
14608 *dy,
14609 *dst,
14610 *outer as usize,
14611 *n_fft as usize,
14612 *n_bins as usize,
14613 *n_mels as usize,
14614 base,
14615 );
14616 }
14617}
14618
14619#[inline(always)]
14620fn exec_welch_peaks(t: &Thunk, base: *mut u8) {
14621 let Thunk::WelchPeaks {
14622 spec,
14623 dst,
14624 welch_batch,
14625 n_fft,
14626 n_segments,
14627 k,
14628 } = t
14629 else {
14630 unreachable!()
14631 };
14632 unsafe {
14633 execute_welch_peaks_f32(
14634 *spec,
14635 *dst,
14636 *welch_batch as usize,
14637 *n_fft as usize,
14638 *n_segments as usize,
14639 *k as usize,
14640 base,
14641 );
14642 }
14643}
14644
14645#[inline(always)]
14649fn exec_custom_fn(t: &Thunk, base: *mut u8) {
14650 let Thunk::CustomFn {
14651 body,
14652 body_init,
14653 inputs,
14654 body_output_off,
14655 outer_output_off,
14656 out_bytes,
14657 } = t
14658 else {
14659 unreachable!()
14660 };
14661 {
14662 let mut body_buf: Vec<u8> = (**body_init).clone();
14663 unsafe {
14664 for (body_in_off, outer_in_off, n_bytes) in inputs.iter() {
14665 let src = (base as *const u8).add(*outer_in_off);
14666 let dst = body_buf.as_mut_ptr().add(*body_in_off);
14667 std::ptr::copy_nonoverlapping(src, dst, *n_bytes as usize);
14668 }
14669 }
14670 execute_thunks(body, &mut body_buf);
14671 unsafe {
14672 let src = body_buf.as_ptr().add(*body_output_off);
14673 let dst = base.add(*outer_output_off);
14674 std::ptr::copy_nonoverlapping(src, dst, *out_bytes as usize);
14675 }
14676 }
14677}
14678
14679#[inline(always)]
14680fn exec_sgd_momentum(t: &Thunk, base: *mut u8) {
14681 let Thunk::SgdMomentum {
14682 param,
14683 vel,
14684 grad,
14685 p_out,
14686 v_out,
14687 lr,
14688 mom,
14689 len,
14690 } = t
14691 else {
14692 unreachable!()
14693 };
14694 {
14695 let len = *len as usize;
14700 let (lr, mom) = (*lr, *mom);
14701 unsafe {
14702 let p = sl(*param, base, len);
14703 let v = sl(*vel, base, len);
14704 let g = sl(*grad, base, len);
14705 let po = sl_mut(*p_out, base, len);
14706 let vo = sl_mut(*v_out, base, len);
14707 if fast_conv_enabled() && crate::pool::should_parallelize(len) {
14708 let poa = po.as_mut_ptr() as usize;
14709 let voa = vo.as_mut_ptr() as usize;
14710 crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
14711 for i in off..off + cnt {
14712 let vn = mom * v[i] + g[i];
14713 *((voa as *mut f32).add(i)) = vn;
14714 *((poa as *mut f32).add(i)) = p[i] - lr * vn;
14715 }
14716 });
14717 } else {
14718 for i in 0..len {
14719 let vn = mom * v[i] + g[i];
14720 vo[i] = vn;
14721 po[i] = p[i] - lr * vn;
14722 }
14723 }
14724 }
14725 }
14726}
14727
14728#[inline(always)]
14729fn exec_cgemm_c64(t: &Thunk, base: *mut u8) {
14730 let Thunk::CgemmC64 { a, b, c, m, k, n } = t else {
14731 unreachable!()
14732 };
14733 unsafe {
14734 cgemm_c64(*a, *b, *c, *m as usize, *k as usize, *n as usize, base);
14735 }
14736}
14737
14738#[inline(always)]
14739fn exec_dense_solve_f64(t: &Thunk, base: *mut u8) {
14740 let Thunk::DenseSolveF64 { a, b, x, n, nrhs } = t else {
14741 unreachable!()
14742 };
14743 {
14744 let (n_, nrhs_) = (*n as usize, *nrhs as usize);
14745 unsafe {
14751 let a_src = sl_f64(*a, base, n_ * n_);
14752 let b_src = sl_f64(*b, base, n_ * nrhs_);
14753 let mut a_scratch: Vec<f64> = a_src.to_vec();
14754 let mut x_buf: Vec<f64> = b_src.to_vec();
14755 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
14756 if info != 0 {
14757 panic!(
14758 "DenseSolveF64: dgesv reported singular matrix \
14759 (info={info}, n={n_}, nrhs={nrhs_})"
14760 );
14761 }
14762 let dst = sl_mut_f64(*x, base, n_ * nrhs_);
14763 dst.copy_from_slice(&x_buf);
14764 }
14765 }
14766}
14767
14768#[inline(always)]
14769fn exec_dense_solve_f32(t: &Thunk, base: *mut u8) {
14770 let Thunk::DenseSolveF32 { a, b, x, n, nrhs } = t else {
14771 unreachable!()
14772 };
14773 {
14774 let (n_, nrhs_) = (*n as usize, *nrhs as usize);
14775 unsafe {
14776 let a_src = sl(*a, base, n_ * n_);
14777 let b_src = sl(*b, base, n_ * nrhs_);
14778 let mut a_scratch: Vec<f32> = a_src.to_vec();
14779 let mut x_buf: Vec<f32> = b_src.to_vec();
14780 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
14781 if info != 0 {
14782 panic!(
14783 "DenseSolveF32: sgesv reported singular matrix \
14784 (info={info}, n={n_}, nrhs={nrhs_})"
14785 );
14786 }
14787 let dst = sl_mut(*x, base, n_ * nrhs_);
14788 dst.copy_from_slice(&x_buf);
14789 }
14790 }
14791}
14792
14793#[inline(always)]
14794fn exec_batched_dense_solve_f64(t: &Thunk, base: *mut u8) {
14795 let Thunk::BatchedDenseSolveF64 {
14796 a,
14797 b,
14798 x,
14799 batch,
14800 n,
14801 nrhs,
14802 } = t
14803 else {
14804 unreachable!()
14805 };
14806 {
14807 let (b_, n_, nrhs_) = (*batch as usize, *n as usize, *nrhs as usize);
14814 let a_stride = n_ * n_;
14815 let b_stride = n_ * nrhs_;
14816 unsafe {
14817 let a_full = sl_f64(*a, base, b_ * a_stride);
14818 let b_full = sl_f64(*b, base, b_ * b_stride);
14819 let x_full = sl_mut_f64(*x, base, b_ * b_stride);
14820 for bi in 0..b_ {
14821 let mut a_scratch: Vec<f64> = a_full[bi * a_stride..(bi + 1) * a_stride].to_vec();
14822 let mut x_buf: Vec<f64> = b_full[bi * b_stride..(bi + 1) * b_stride].to_vec();
14823 let info = crate::blas::dgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
14824 if info != 0 {
14825 panic!(
14826 "BatchedDenseSolveF64: slice {bi} \
14827 singular (info={info}, n={n_}, nrhs={nrhs_})"
14828 );
14829 }
14830 x_full[bi * b_stride..(bi + 1) * b_stride].copy_from_slice(&x_buf);
14831 }
14832 }
14833 }
14834}
14835
14836#[inline(always)]
14837fn exec_batched_dense_solve_f32(t: &Thunk, base: *mut u8) {
14838 let Thunk::BatchedDenseSolveF32 {
14839 a,
14840 b,
14841 x,
14842 batch,
14843 n,
14844 nrhs,
14845 } = t
14846 else {
14847 unreachable!()
14848 };
14849 {
14850 let (b_, n_, nrhs_) = (*batch as usize, *n as usize, *nrhs as usize);
14851 let a_stride = n_ * n_;
14852 let b_stride = n_ * nrhs_;
14853 unsafe {
14854 let a_full = sl(*a, base, b_ * a_stride);
14855 let b_full = sl(*b, base, b_ * b_stride);
14856 let x_full = sl_mut(*x, base, b_ * b_stride);
14857 for bi in 0..b_ {
14858 let mut a_scratch = a_full[bi * a_stride..(bi + 1) * a_stride].to_vec();
14859 let mut x_buf = b_full[bi * b_stride..(bi + 1) * b_stride].to_vec();
14860 let info = crate::blas::sgesv(&mut a_scratch, &mut x_buf, n_, nrhs_);
14861 if info != 0 {
14862 panic!("BatchedDenseSolveF32: slice {bi} singular (info={info})");
14863 }
14864 x_full[bi * b_stride..(bi + 1) * b_stride].copy_from_slice(&x_buf);
14865 }
14866 }
14867 }
14868}
14869
14870#[inline(always)]
14871fn exec_batched_dgemm_f64(t: &Thunk, base: *mut u8) {
14872 let Thunk::BatchedDgemmF64 {
14873 a,
14874 b,
14875 c,
14876 batch,
14877 m,
14878 k,
14879 n,
14880 } = t
14881 else {
14882 unreachable!()
14883 };
14884 {
14885 let (b_, m_, k_, n_) = (*batch as usize, *m as usize, *k as usize, *n as usize);
14886 let a_stride = m_ * k_;
14887 let b_stride = k_ * n_;
14888 let c_stride = m_ * n_;
14889 unsafe {
14890 let a_full = sl_f64(*a, base, b_ * a_stride);
14891 let b_full = sl_f64(*b, base, b_ * b_stride);
14892 let c_full = sl_mut_f64(*c, base, b_ * c_stride);
14893 for bi in 0..b_ {
14894 let a_slice = &a_full[bi * a_stride..(bi + 1) * a_stride];
14895 let b_slice = &b_full[bi * b_stride..(bi + 1) * b_stride];
14896 let c_slice = &mut c_full[bi * c_stride..(bi + 1) * c_stride];
14897 crate::blas::dgemm(a_slice, b_slice, c_slice, m_, k_, n_);
14898 }
14899 }
14900 }
14901}
14902
14903#[inline(always)]
14904fn exec_dgemm(t: &Thunk, base: *mut u8) {
14905 let Thunk::Dgemm { a, b, c, m, k, n } = t else {
14906 unreachable!()
14907 };
14908 {
14909 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
14910 unsafe {
14911 crate::blas::dgemm(
14912 sl_f64(*a, base, m * k),
14913 sl_f64(*b, base, k * n),
14914 sl_mut_f64(*c, base, m * n),
14915 m,
14916 k,
14917 n,
14918 );
14919 }
14920 }
14921}
14922
14923#[inline(always)]
14924fn exec_transpose_f64(t: &Thunk, base: *mut u8) {
14925 let Thunk::TransposeF64 {
14926 src,
14927 dst,
14928 in_total,
14929 out_dims,
14930 in_strides,
14931 } = t
14932 else {
14933 unreachable!()
14934 };
14935 unsafe {
14936 let inp = sl_f64(*src, base, *in_total as usize);
14937 let out_total: usize = out_dims.iter().map(|d| *d as usize).product();
14938 let out = sl_mut_f64(*dst, base, out_total);
14939 transpose_walk_f64(inp, out, out_dims, in_strides);
14940 }
14941}
14942
14943#[inline(always)]
14944fn exec_activation_f64(t: &Thunk, base: *mut u8) {
14945 let Thunk::ActivationF64 {
14946 src,
14947 dst,
14948 len,
14949 kind,
14950 } = t
14951 else {
14952 unreachable!()
14953 };
14954 {
14955 let len = *len as usize;
14956 unsafe {
14957 let inp = sl_f64(*src, base, len);
14958 let out = sl_mut_f64(*dst, base, len);
14959 apply_activation_f64(inp, out, *kind);
14960 }
14961 }
14962}
14963
14964#[inline(always)]
14965fn exec_reduce_sum_f64(t: &Thunk, base: *mut u8) {
14966 let Thunk::ReduceSumF64 {
14967 src,
14968 dst,
14969 outer,
14970 reduced,
14971 inner,
14972 } = t
14973 else {
14974 unreachable!()
14975 };
14976 {
14977 let (o, r, n) = (*outer as usize, *reduced as usize, *inner as usize);
14978 unsafe {
14979 let inp = sl_f64(*src, base, o * r * n);
14980 let out = sl_mut_f64(*dst, base, o * n);
14981 reduce_sum_f64(inp, out, o, r, n);
14982 }
14983 }
14984}
14985
14986#[inline(always)]
14987fn exec_binary_full_f64(t: &Thunk, base: *mut u8) {
14988 let Thunk::BinaryFullF64 {
14989 lhs,
14990 rhs,
14991 dst,
14992 len,
14993 lhs_len,
14994 rhs_len,
14995 op,
14996 out_dims_bcast,
14997 bcast_lhs_strides,
14998 bcast_rhs_strides,
14999 } = t
15000 else {
15001 unreachable!()
15002 };
15003 {
15004 let len = *len as usize;
15005 let lhs_len = *lhs_len as usize;
15006 let rhs_len = *rhs_len as usize;
15007 unsafe {
15008 let l = sl_f64(*lhs, base, lhs_len);
15009 let r = sl_f64(*rhs, base, rhs_len);
15010 let d = sl_mut_f64(*dst, base, len);
15011 if lhs_len == len && rhs_len == len {
15012 for i in 0..len {
15013 d[i] = binary_op_f64(*op, l[i], r[i]);
15014 }
15015 } else if !out_dims_bcast.is_empty() {
15016 let rank = out_dims_bcast.len();
15020 let mut coords = vec![0u32; rank];
15021 for i in 0..len {
15022 let mut rem = i;
15023 for ax in (0..rank).rev() {
15024 let sz = out_dims_bcast[ax] as usize;
15025 coords[ax] = (rem % sz) as u32;
15026 rem /= sz;
15027 }
15028 let mut li: usize = 0;
15029 let mut ri: usize = 0;
15030 for ax in 0..rank {
15031 li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
15032 ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
15033 }
15034 d[i] = binary_op_f64(*op, l[li], r[ri]);
15035 }
15036 } else {
15037 for i in 0..len {
15042 d[i] = binary_op_f64(*op, l[i % lhs_len], r[i % rhs_len]);
15043 }
15044 }
15045 }
15046 }
15047}
15048
15049#[inline(always)]
15050fn exec_binary_full_c64(t: &Thunk, base: *mut u8) {
15051 let Thunk::BinaryFullC64 {
15052 lhs,
15053 rhs,
15054 dst,
15055 len,
15056 lhs_len,
15057 rhs_len,
15058 op,
15059 out_dims_bcast,
15060 bcast_lhs_strides,
15061 bcast_rhs_strides,
15062 } = t
15063 else {
15064 unreachable!()
15065 };
15066 {
15067 let n_out = *len as usize;
15073 let n_l = *lhs_len as usize;
15074 let n_r = *rhs_len as usize;
15075 unsafe {
15076 let l = sl(*lhs, base, 2 * n_l);
15077 let r = sl(*rhs, base, 2 * n_r);
15078 let d = sl_mut(*dst, base, 2 * n_out);
15079 let do_c64 = |a_re: f32, a_im: f32, b_re: f32, b_im: f32| -> (f32, f32) {
15080 match op {
15081 BinaryOp::Add => (a_re + b_re, a_im + b_im),
15082 BinaryOp::Sub => (a_re - b_re, a_im - b_im),
15083 BinaryOp::Mul => (a_re * b_re - a_im * b_im, a_re * b_im + a_im * b_re),
15084 BinaryOp::Div => {
15085 let denom = b_re * b_re + b_im * b_im;
15086 (
15087 (a_re * b_re + a_im * b_im) / denom,
15088 (a_im * b_re - a_re * b_im) / denom,
15089 )
15090 }
15091 BinaryOp::Max | BinaryOp::Min | BinaryOp::Pow => {
15092 unreachable!("C64 max/min/pow rejected at lowering")
15093 }
15094 }
15095 };
15096 if n_l == n_out && n_r == n_out {
15097 for i in 0..n_out {
15098 let (re, im) = do_c64(l[2 * i], l[2 * i + 1], r[2 * i], r[2 * i + 1]);
15099 d[2 * i] = re;
15100 d[2 * i + 1] = im;
15101 }
15102 } else if !out_dims_bcast.is_empty() {
15103 let rank = out_dims_bcast.len();
15107 let mut coords = vec![0u32; rank];
15108 for i in 0..n_out {
15109 let mut rem = i;
15110 for ax in (0..rank).rev() {
15111 let sz = out_dims_bcast[ax] as usize;
15112 coords[ax] = (rem % sz) as u32;
15113 rem /= sz;
15114 }
15115 let mut li: usize = 0;
15116 let mut ri: usize = 0;
15117 for ax in 0..rank {
15118 li += coords[ax] as usize * bcast_lhs_strides[ax] as usize;
15119 ri += coords[ax] as usize * bcast_rhs_strides[ax] as usize;
15120 }
15121 let (re, im) = do_c64(l[2 * li], l[2 * li + 1], r[2 * ri], r[2 * ri + 1]);
15122 d[2 * i] = re;
15123 d[2 * i + 1] = im;
15124 }
15125 } else {
15126 for i in 0..n_out {
15128 let li = if n_l == 1 { 0 } else { i % n_l };
15129 let ri = if n_r == 1 { 0 } else { i % n_r };
15130 let (re, im) = do_c64(l[2 * li], l[2 * li + 1], r[2 * ri], r[2 * ri + 1]);
15131 d[2 * i] = re;
15132 d[2 * i + 1] = im;
15133 }
15134 }
15135 }
15136 }
15137}
15138
15139#[inline(always)]
15140fn exec_complex_norm_sq_f32(t: &Thunk, base: *mut u8) {
15141 let Thunk::ComplexNormSqF32 { src, dst, len } = t else {
15142 unreachable!()
15143 };
15144 {
15145 let n = *len as usize;
15146 unsafe {
15147 let s = sl(*src, base, 2 * n);
15148 let d = sl_mut(*dst, base, n);
15149 for i in 0..n {
15150 let re = s[2 * i];
15151 let im = s[2 * i + 1];
15152 d[i] = re * re + im * im;
15153 }
15154 }
15155 }
15156}
15157
15158#[inline(always)]
15159fn exec_complex_norm_sq_backward_f32(t: &Thunk, base: *mut u8) {
15160 let Thunk::ComplexNormSqBackwardF32 { z, g, dz, len } = t else {
15161 unreachable!()
15162 };
15163 {
15164 let n = *len as usize;
15167 unsafe {
15168 let zb = sl(*z, base, 2 * n);
15169 let gb = sl(*g, base, n);
15170 let db = sl_mut(*dz, base, 2 * n);
15171 for i in 0..n {
15172 let re = zb[2 * i];
15173 let im = zb[2 * i + 1];
15174 let gv = gb[i];
15175 db[2 * i] = gv * re;
15176 db[2 * i + 1] = gv * im;
15177 }
15178 }
15179 }
15180}
15181
15182#[inline(always)]
15183fn exec_conjugate_c64(t: &Thunk, base: *mut u8) {
15184 let Thunk::ConjugateC64 { src, dst, len } = t else {
15185 unreachable!()
15186 };
15187 {
15188 let n = *len as usize;
15189 unsafe {
15190 let s = sl(*src, base, 2 * n);
15191 let d = sl_mut(*dst, base, 2 * n);
15192 for i in 0..n {
15193 d[2 * i] = s[2 * i];
15194 d[2 * i + 1] = -s[2 * i + 1];
15195 }
15196 }
15197 }
15198}
15199
15200#[inline(always)]
15201fn exec_activation_c64(t: &Thunk, base: *mut u8) {
15202 let Thunk::ActivationC64 {
15203 src,
15204 dst,
15205 len,
15206 kind,
15207 } = t
15208 else {
15209 unreachable!()
15210 };
15211 {
15212 let n = *len as usize;
15213 unsafe {
15214 let s = sl(*src, base, 2 * n);
15215 let d = sl_mut(*dst, base, 2 * n);
15216 for i in 0..n {
15217 let a = s[2 * i];
15218 let b = s[2 * i + 1];
15219 let (re, im) = match kind {
15220 Activation::Neg => (-a, -b),
15221 Activation::Exp => {
15222 let ea = a.exp();
15224 (ea * b.cos(), ea * b.sin())
15225 }
15226 Activation::Log => {
15227 let r = (a * a + b * b).sqrt();
15229 (r.ln(), b.atan2(a))
15230 }
15231 Activation::Sqrt => {
15232 let r = (a * a + b * b).sqrt();
15235 let re = ((r + a) * 0.5).max(0.0).sqrt();
15236 let im_mag = ((r - a) * 0.5).max(0.0).sqrt();
15237 let im = if b >= 0.0 { im_mag } else { -im_mag };
15238 (re, im)
15239 }
15240 _ => unreachable!("non-C64 activation kind survived lowering"),
15241 };
15242 d[2 * i] = re;
15243 d[2 * i + 1] = im;
15244 }
15245 }
15246 }
15247}
15248
15249#[inline(always)]
15250fn exec_scan(t: &Thunk, base: *mut u8) {
15251 let Thunk::Scan {
15252 body,
15253 body_init,
15254 body_input_off,
15255 body_output_off,
15256 outer_init_off,
15257 outer_final_off,
15258 length,
15259 carry_bytes,
15260 save_trajectory,
15261 xs_inputs,
15262 bcast_inputs,
15263 num_checkpoints,
15264 } = t
15265 else {
15266 unreachable!()
15267 };
15268 {
15269 let cb = *carry_bytes as usize;
15270 let n_steps = *length as usize;
15271 let k_total = if *num_checkpoints == 0 || *num_checkpoints == *length {
15275 n_steps } else {
15277 *num_checkpoints as usize
15278 };
15279 let checkpoint_t_for_k = |k: usize| -> usize {
15280 if k_total == n_steps {
15281 k
15282 } else {
15283 ((k + 1) * n_steps)
15284 .div_ceil(k_total)
15285 .saturating_sub(1)
15286 .min(n_steps - 1)
15287 }
15288 };
15289 let mut next_k = 0usize;
15290
15291 let mut body_buf: Vec<u8> = (**body_init).clone();
15292 unsafe {
15293 std::ptr::copy_nonoverlapping(
15294 base.add(*outer_init_off),
15295 body_buf.as_mut_ptr().add(*body_input_off),
15296 cb,
15297 );
15298 for (body_b_off, outer_b_off, total_bytes) in bcast_inputs.iter() {
15302 std::ptr::copy_nonoverlapping(
15303 base.add(*outer_b_off),
15304 body_buf.as_mut_ptr().add(*body_b_off),
15305 *total_bytes as usize,
15306 );
15307 }
15308 }
15309 for t in 0..n_steps {
15310 for (body_x_off, outer_xs_off, per_step_bytes) in xs_inputs.iter() {
15311 let psb = *per_step_bytes as usize;
15312 unsafe {
15313 std::ptr::copy_nonoverlapping(
15314 base.add(*outer_xs_off + t * psb),
15315 body_buf.as_mut_ptr().add(*body_x_off),
15316 psb,
15317 );
15318 }
15319 }
15320
15321 execute_thunks(body, &mut body_buf);
15322
15323 if *save_trajectory && next_k < k_total && t == checkpoint_t_for_k(next_k) {
15324 unsafe {
15325 std::ptr::copy_nonoverlapping(
15326 body_buf.as_ptr().add(*body_output_off),
15327 base.add(*outer_final_off + next_k * cb),
15328 cb,
15329 );
15330 }
15331 next_k += 1;
15332 }
15333
15334 if *body_output_off != *body_input_off {
15335 body_buf.copy_within(*body_output_off..*body_output_off + cb, *body_input_off);
15336 }
15337 }
15338
15339 if !*save_trajectory {
15340 unsafe {
15342 std::ptr::copy_nonoverlapping(
15343 body_buf.as_ptr().add(*body_output_off),
15344 base.add(*outer_final_off),
15345 cb,
15346 );
15347 }
15348 }
15349 }
15350}
15351
15352#[inline(always)]
15353fn exec_scan_backward_xs(t: &Thunk, base: *mut u8) {
15354 let Thunk::ScanBackwardXs {
15355 body_vjp,
15356 body_init,
15357 body_carry_in_off,
15358 body_x_offs,
15359 body_d_output_off,
15360 body_dcarry_out_off,
15361 body_dxs_out_off,
15362 outer_init_off,
15363 outer_traj_off,
15364 outer_upstream_off,
15365 outer_xs_offs,
15366 outer_dxs_off,
15367 length,
15368 carry_bytes,
15369 carry_elem_size,
15370 per_step_bytes,
15371 save_trajectory,
15372 num_checkpoints,
15373 forward_body,
15374 forward_body_init,
15375 forward_body_carry_in_off,
15376 forward_body_output_off,
15377 forward_body_x_offs,
15378 } = t
15379 else {
15380 unreachable!()
15381 };
15382 {
15383 let cb = *carry_bytes as usize;
15384 let psb = *per_step_bytes as usize;
15385 let n_steps = *length as usize;
15386 let k_total = *num_checkpoints as usize;
15387 let is_recursive = k_total != 0 && k_total != n_steps;
15388 let checkpoint_t_for_k = |k: usize| -> usize {
15389 ((k + 1) * n_steps)
15390 .div_ceil(k_total)
15391 .saturating_sub(1)
15392 .min(n_steps - 1)
15393 };
15394
15395 let mut fwd_buf: Vec<u8> = if is_recursive {
15399 (**forward_body_init.as_ref().unwrap()).clone()
15400 } else {
15401 Vec::new()
15402 };
15403 let mut seg_cache: Vec<u8> = Vec::new();
15404 let mut seg_start_t: usize = usize::MAX;
15405 let mut seg_count: usize = 0;
15406 let recompute_carry_t = |t: usize,
15407 dst: &mut [u8],
15408 fwd_buf: &mut Vec<u8>,
15409 seg_cache: &mut Vec<u8>,
15410 seg_start_t: &mut usize,
15411 seg_count: &mut usize| {
15412 if !is_recursive {
15413 unsafe {
15414 let src = if t == 0 {
15415 base.add(*outer_init_off)
15416 } else {
15417 base.add(*outer_traj_off + (t - 1) * cb)
15418 };
15419 std::ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), cb);
15420 }
15421 return;
15422 }
15423 if *seg_start_t != usize::MAX && t >= *seg_start_t && t < *seg_start_t + *seg_count {
15424 let off = (t - *seg_start_t) * cb;
15425 dst.copy_from_slice(&seg_cache[off..off + cb]);
15426 return;
15427 }
15428 let seg_k = (0..k_total)
15429 .find(|&k| t <= checkpoint_t_for_k(k))
15430 .unwrap_or(k_total - 1);
15431 let (anchor_t, anchor_ptr): (usize, *const u8) = if seg_k == 0 {
15432 (0, unsafe { base.add(*outer_init_off) as *const u8 })
15433 } else {
15434 let prev_ck = checkpoint_t_for_k(seg_k - 1);
15435 (prev_ck + 1, unsafe {
15436 base.add(*outer_traj_off + (seg_k - 1) * cb) as *const u8
15437 })
15438 };
15439 let seg_end_t = checkpoint_t_for_k(seg_k);
15440 let seg_size = seg_end_t - anchor_t + 1;
15441
15442 fwd_buf.copy_from_slice(forward_body_init.as_ref().unwrap());
15443 unsafe {
15444 std::ptr::copy_nonoverlapping(
15445 anchor_ptr,
15446 fwd_buf.as_mut_ptr().add(*forward_body_carry_in_off),
15447 cb,
15448 );
15449 }
15450 seg_cache.resize(seg_size * cb, 0u8);
15451 seg_cache[0..cb].copy_from_slice(
15452 &fwd_buf[*forward_body_carry_in_off..*forward_body_carry_in_off + cb],
15453 );
15454 let fb_sched = forward_body.as_ref().unwrap();
15455 for i in 1..seg_size {
15456 let cur_iter = anchor_t + i - 1;
15457 for (idx, fb_x_off) in forward_body_x_offs.iter().enumerate() {
15458 let (outer_xs_off, x_psb) = outer_xs_offs[idx];
15459 let xb = x_psb as usize;
15460 unsafe {
15461 std::ptr::copy_nonoverlapping(
15462 base.add(outer_xs_off + cur_iter * xb),
15463 fwd_buf.as_mut_ptr().add(*fb_x_off),
15464 xb,
15465 );
15466 }
15467 }
15468 execute_thunks(fb_sched, fwd_buf);
15469 if *forward_body_output_off != *forward_body_carry_in_off {
15470 fwd_buf.copy_within(
15471 *forward_body_output_off..*forward_body_output_off + cb,
15472 *forward_body_carry_in_off,
15473 );
15474 }
15475 let cache_off = i * cb;
15476 seg_cache[cache_off..cache_off + cb].copy_from_slice(
15477 &fwd_buf[*forward_body_carry_in_off..*forward_body_carry_in_off + cb],
15478 );
15479 }
15480 *seg_start_t = anchor_t;
15481 *seg_count = seg_size;
15482
15483 let off = (t - anchor_t) * cb;
15484 dst.copy_from_slice(&seg_cache[off..off + cb]);
15485 };
15486
15487 let mut dcarry: Vec<u8> = vec![0u8; cb];
15488 if !*save_trajectory {
15489 unsafe {
15490 std::ptr::copy_nonoverlapping(
15491 base.add(*outer_upstream_off),
15492 dcarry.as_mut_ptr(),
15493 cb,
15494 );
15495 }
15496 }
15497
15498 let mut body_buf: Vec<u8> = (**body_init).clone();
15499
15500 for t in (0..n_steps).rev() {
15501 if *save_trajectory {
15502 unsafe {
15503 let up_off = *outer_upstream_off + t * cb;
15504 match *carry_elem_size {
15505 4 => {
15506 let up_ptr = base.add(up_off) as *const f32;
15507 let dc_ptr = dcarry.as_mut_ptr() as *mut f32;
15508 let n_elems = cb / 4;
15509 for i in 0..n_elems {
15510 *dc_ptr.add(i) += *up_ptr.add(i);
15511 }
15512 }
15513 8 => {
15514 let up_ptr = base.add(up_off) as *const f64;
15515 let dc_ptr = dcarry.as_mut_ptr() as *mut f64;
15516 let n_elems = cb / 8;
15517 for i in 0..n_elems {
15518 *dc_ptr.add(i) += *up_ptr.add(i);
15519 }
15520 }
15521 other => panic!(
15522 "ScanBackwardXs: unsupported carry elem size {other} \
15523 (only f32/f64 carries are supported today)"
15524 ),
15525 }
15526 }
15527 }
15528
15529 let carry_dst_start = *body_carry_in_off;
15533 {
15534 let carry_slice = &mut body_buf[carry_dst_start..carry_dst_start + cb];
15535 recompute_carry_t(
15536 t,
15537 carry_slice,
15538 &mut fwd_buf,
15539 &mut seg_cache,
15540 &mut seg_start_t,
15541 &mut seg_count,
15542 );
15543 }
15544 unsafe {
15545 for (i, body_x_off) in body_x_offs.iter().enumerate() {
15546 let (outer_xs_off, x_psb) = outer_xs_offs[i];
15547 let xb = x_psb as usize;
15548 std::ptr::copy_nonoverlapping(
15549 base.add(outer_xs_off + t * xb),
15550 body_buf.as_mut_ptr().add(*body_x_off),
15551 xb,
15552 );
15553 }
15554 std::ptr::copy_nonoverlapping(
15555 dcarry.as_ptr(),
15556 body_buf.as_mut_ptr().add(*body_d_output_off),
15557 cb,
15558 );
15559 }
15560
15561 execute_thunks(body_vjp, &mut body_buf);
15562
15563 unsafe {
15566 std::ptr::copy_nonoverlapping(
15567 body_buf.as_ptr().add(*body_dxs_out_off),
15568 base.add(*outer_dxs_off + t * psb),
15569 psb,
15570 );
15571 }
15572
15573 unsafe {
15575 std::ptr::copy_nonoverlapping(
15576 body_buf.as_ptr().add(*body_dcarry_out_off),
15577 dcarry.as_mut_ptr(),
15578 cb,
15579 );
15580 }
15581 }
15582 }
15583}
15584
15585#[inline(always)]
15586fn exec_fused_mm_bias_act(t: &Thunk, base: *mut u8) {
15587 let Thunk::FusedMmBiasAct {
15588 a,
15589 w,
15590 bias,
15591 c,
15592 m,
15593 k,
15594 n,
15595 act,
15596 } = t
15597 else {
15598 unreachable!()
15599 };
15600 {
15601 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
15602 unsafe {
15603 let out = sl_mut(*c, base, m * n);
15604 crate::blas::sgemm_auto(sl(*a, base, m * k), sl(*w, base, k * n), out, m, k, n);
15605 match act {
15606 Some(Activation::Gelu) => {
15607 crate::kernels::par_bias_gelu(out, sl(*bias, base, n), m, n)
15608 }
15609 Some(other) => {
15610 crate::blas::bias_add(out, sl(*bias, base, n), m, n);
15611 apply_activation_inplace(out, *other);
15612 }
15613 None => crate::blas::bias_add(out, sl(*bias, base, n), m, n),
15614 }
15615 }
15616 }
15617}
15618
15619#[inline(always)]
15620fn exec_bias_add(t: &Thunk, base: *mut u8) {
15621 let Thunk::BiasAdd {
15622 src,
15623 bias,
15624 dst,
15625 m,
15626 n,
15627 } = t
15628 else {
15629 unreachable!()
15630 };
15631 {
15632 let (m, n) = (*m as usize, *n as usize);
15633 let len = m * n;
15634 unsafe {
15635 let out = sl_mut(*dst, base, len);
15636 if *src != *dst {
15637 let src_ptr = base.add(*src) as *const f32;
15638 let dst_ptr = base.add(*dst) as *mut f32;
15639 if src_ptr != dst_ptr {
15640 std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, len);
15641 }
15642 }
15643 crate::blas::bias_add(out, sl(*bias, base, n), m, n);
15644 }
15645 }
15646}
15647
15648#[inline(always)]
15649fn exec_gather(t: &Thunk, base: *mut u8) {
15650 let Thunk::Gather {
15651 table,
15652 table_len,
15653 idx,
15654 dst,
15655 num_idx,
15656 trailing,
15657 idx_i64,
15658 table_bytes,
15659 } = t
15660 else {
15661 unreachable!()
15662 };
15663 {
15664 let (ni, tr) = (*num_idx as usize, *trailing as usize);
15665 let rows = *table_len as usize / tr.max(1);
15666 unsafe {
15667 if *table_bytes == 8 {
15668 let tab = sl_i64(*table, base, *table_len as usize);
15669 let out = sl_mut_i64(*dst, base, ni * tr);
15670 if *idx_i64 != 0 {
15671 let ids = sl_i64(*idx, base, ni);
15672 for i in 0..ni {
15673 let row = ids[i].max(0) as usize;
15674 if row < rows {
15675 out[i * tr..(i + 1) * tr]
15676 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
15677 }
15678 }
15679 } else {
15680 let ids = sl(*idx, base, ni);
15681 for i in 0..ni {
15682 let row = ids[i] as usize;
15683 if row < rows {
15684 out[i * tr..(i + 1) * tr]
15685 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
15686 }
15687 }
15688 }
15689 } else {
15690 let tab = sl(*table, base, *table_len as usize);
15691 let out = sl_mut(*dst, base, ni * tr);
15692 if *idx_i64 != 0 {
15693 let ids = sl_i64(*idx, base, ni);
15694 for i in 0..ni {
15695 let row = ids[i].max(0) as usize;
15696 if row < rows {
15697 out[i * tr..(i + 1) * tr]
15698 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
15699 }
15700 }
15701 } else {
15702 let ids = sl(*idx, base, ni);
15703 for i in 0..ni {
15704 let row = ids[i] as usize;
15705 if row < rows {
15706 out[i * tr..(i + 1) * tr]
15707 .copy_from_slice(&tab[row * tr..(row + 1) * tr]);
15708 }
15709 }
15710 }
15711 }
15712 }
15713 }
15714}
15715
15716#[inline(always)]
15717fn exec_layer_norm(t: &Thunk, base: *mut u8) {
15718 let Thunk::LayerNorm {
15719 src,
15720 g,
15721 b,
15722 dst,
15723 rows,
15724 h,
15725 eps,
15726 } = t
15727 else {
15728 unreachable!()
15729 };
15730 {
15731 let (rows, h) = (*rows as usize, *h as usize);
15732 unsafe {
15733 let input = sl(*src, base, rows * h);
15734 let gamma = sl(*g, base, h);
15735 let beta = sl(*b, base, h);
15736 let output = sl_mut(*dst, base, rows * h);
15737 if rows >= 4 && rows * h >= 30_000 {
15739 let i_ptr = input.as_ptr() as usize;
15740 let o_ptr = output.as_mut_ptr() as usize;
15741 let g_ptr = gamma.as_ptr() as usize;
15742 let b_ptr = beta.as_ptr() as usize;
15743 let e = *eps;
15744 crate::pool::par_for(rows, 4, &|off, cnt| {
15745 let inp =
15746 std::slice::from_raw_parts((i_ptr as *const f32).add(off * h), cnt * h);
15747 let out =
15748 std::slice::from_raw_parts_mut((o_ptr as *mut f32).add(off * h), cnt * h);
15749 let g = std::slice::from_raw_parts(g_ptr as *const f32, h);
15750 let b = std::slice::from_raw_parts(b_ptr as *const f32, h);
15751 for row in 0..cnt {
15752 crate::kernels::layer_norm_row(
15753 &inp[row * h..(row + 1) * h],
15754 g,
15755 b,
15756 &mut out[row * h..(row + 1) * h],
15757 h,
15758 e,
15759 );
15760 }
15761 });
15762 } else {
15763 for row in 0..rows {
15764 crate::kernels::layer_norm_row(
15765 &input[row * h..(row + 1) * h],
15766 gamma,
15767 beta,
15768 &mut output[row * h..(row + 1) * h],
15769 h,
15770 *eps,
15771 );
15772 }
15773 }
15774 }
15775 }
15776}
15777
15778#[inline(always)]
15779fn exec_group_norm(t: &Thunk, base: *mut u8) {
15780 let Thunk::GroupNorm {
15781 src,
15782 g,
15783 b,
15784 dst,
15785 n,
15786 c,
15787 h,
15788 w,
15789 num_groups,
15790 eps,
15791 } = t
15792 else {
15793 unreachable!()
15794 };
15795 {
15796 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
15797 let plane = c * h * w;
15798 unsafe {
15799 let stride = plane * std::mem::size_of::<f32>();
15804 for ni in 0..n {
15805 let input = sl(*src, base.add(ni * stride), plane);
15806 let gamma = sl(*g, base, c);
15807 let beta = sl(*b, base, c);
15808 let output = sl_mut(*dst, base.add(ni * stride), plane);
15809 crate::kernels::group_norm_nchw(
15810 input,
15811 gamma,
15812 beta,
15813 output,
15814 1,
15815 c,
15816 h,
15817 w,
15818 *num_groups as usize,
15819 *eps,
15820 );
15821 }
15822 }
15823 }
15824}
15825
15826#[inline(always)]
15827fn exec_batch_norm_inference(t: &Thunk, base: *mut u8) {
15828 let Thunk::BatchNormInference {
15829 src,
15830 g,
15831 b,
15832 mean,
15833 var,
15834 dst,
15835 count,
15836 channels,
15837 eps,
15838 } = t
15839 else {
15840 unreachable!()
15841 };
15842 {
15843 let count = *count as usize;
15844 let c = *channels as usize;
15845 let n = count * c;
15846 unsafe {
15847 crate::kernels::batch_norm_inference(
15848 sl(*src, base, n),
15849 sl(*g, base, c),
15850 sl(*b, base, c),
15851 sl(*mean, base, c),
15852 sl(*var, base, c),
15853 sl_mut(*dst, base, n),
15854 c,
15855 *eps,
15856 );
15857 }
15858 }
15859}
15860
15861#[inline(always)]
15862fn exec_layer_norm2d(t: &Thunk, base: *mut u8) {
15863 let Thunk::LayerNorm2d {
15864 src,
15865 g,
15866 b,
15867 dst,
15868 n,
15869 c,
15870 h,
15871 w,
15872 eps,
15873 } = t
15874 else {
15875 unreachable!()
15876 };
15877 {
15878 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
15879 let plane = c * h * w;
15880 unsafe {
15881 let input = sl(*src, base, n * plane);
15882 let gamma = sl(*g, base, c);
15883 let beta = sl(*b, base, c);
15884 let output = sl_mut(*dst, base, n * plane);
15885 crate::kernels::layer_norm2d_nchw(input, gamma, beta, output, n, c, h, w, *eps);
15886 }
15887 }
15888}
15889
15890#[inline(always)]
15891fn exec_conv_transpose2d(t: &Thunk, base: *mut u8) {
15892 let Thunk::ConvTranspose2d {
15893 src,
15894 weight,
15895 dst,
15896 n,
15897 c_in,
15898 h,
15899 w_in,
15900 c_out,
15901 h_out,
15902 w_out,
15903 kh,
15904 kw,
15905 sh,
15906 sw,
15907 ph,
15908 pw,
15909 dh,
15910 dw,
15911 groups,
15912 } = t
15913 else {
15914 unreachable!()
15915 };
15916 {
15917 let n = *n as usize;
15918 let c_in = *c_in as usize;
15919 let h = *h as usize;
15920 let w_in = *w_in as usize;
15921 let c_out = *c_out as usize;
15922 let h_out = *h_out as usize;
15923 let w_out = *w_out as usize;
15924 unsafe {
15925 let inp = sl(*src, base, n * c_in * h * w_in);
15926 let wt = sl(
15927 *weight,
15928 base,
15929 c_in * (c_out / *groups as usize) * (*kh as usize) * (*kw as usize),
15930 );
15931 let out = sl_mut(*dst, base, n * c_out * h_out * w_out);
15932 crate::kernels::conv_transpose2d_nchw(
15933 inp,
15934 wt,
15935 out,
15936 n,
15937 c_in,
15938 h,
15939 w_in,
15940 c_out,
15941 h_out,
15942 w_out,
15943 *kh as usize,
15944 *kw as usize,
15945 *sh as usize,
15946 *sw as usize,
15947 *ph as usize,
15948 *pw as usize,
15949 *dh as usize,
15950 *dw as usize,
15951 *groups as usize,
15952 );
15953 }
15954 }
15955}
15956
15957fn exec_conv3d(t: &Thunk, base: *mut u8) {
15958 let Thunk::Conv3d {
15959 src,
15960 weight,
15961 dst,
15962 n,
15963 c_in,
15964 d,
15965 h,
15966 w,
15967 c_out,
15968 d_out,
15969 h_out,
15970 w_out,
15971 kd,
15972 kh,
15973 kw,
15974 sd,
15975 sh,
15976 sw,
15977 pd,
15978 ph,
15979 pw,
15980 dd,
15981 dh,
15982 dw,
15983 groups,
15984 } = t
15985 else {
15986 unreachable!()
15987 };
15988 unsafe {
15989 execute_conv3d_forward_f32(
15990 *src, *weight, *dst, *n, *c_in, *d, *h, *w, *c_out, *d_out, *h_out, *w_out, *kd, *kh,
15991 *kw, *sd, *sh, *sw, *pd, *ph, *pw, *dd, *dh, *dw, *groups, base,
15992 );
15993 }
15994}
15995
15996fn exec_conv_transpose3d(t: &Thunk, base: *mut u8) {
15997 let Thunk::ConvTranspose3d {
15998 src,
15999 weight,
16000 dst,
16001 n,
16002 c_in,
16003 d,
16004 h,
16005 w_in,
16006 c_out,
16007 d_out,
16008 h_out,
16009 w_out,
16010 kd,
16011 kh,
16012 kw,
16013 sd,
16014 sh,
16015 sw,
16016 pd,
16017 ph,
16018 pw,
16019 dd,
16020 dh,
16021 dw,
16022 groups,
16023 } = t
16024 else {
16025 unreachable!()
16026 };
16027 unsafe {
16028 execute_conv_transpose3d_ncdhw_f32(
16029 *src, *weight, *dst, *n, *c_in, *d, *h, *w_in, *c_out, *d_out, *h_out, *w_out, *kd,
16030 *kh, *kw, *sd, *sh, *sw, *pd, *ph, *pw, *dd, *dh, *dw, *groups, base,
16031 );
16032 }
16033}
16034
16035#[inline(always)]
16036fn exec_resize_nearest2x(t: &Thunk, base: *mut u8) {
16037 let Thunk::ResizeNearest2x {
16038 src,
16039 dst,
16040 n,
16041 c,
16042 h,
16043 w,
16044 } = t
16045 else {
16046 unreachable!()
16047 };
16048 {
16049 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
16050 let in_plane = c * h * w;
16051 let out_plane = c * h * 2 * w * 2;
16052 let fsz = std::mem::size_of::<f32>();
16056 unsafe {
16057 for ni in 0..n {
16058 let input = sl(*src, base.add(ni * in_plane * fsz), in_plane);
16059 let output = sl_mut(*dst, base.add(ni * out_plane * fsz), out_plane);
16060 crate::kernels::resize_nearest_2x_nchw(input, output, c, h, w);
16061 }
16062 }
16063 }
16064}
16065
16066#[inline(always)]
16067fn exec_axial_rope2d(t: &Thunk, base: *mut u8) {
16068 let Thunk::AxialRope2d {
16069 src,
16070 dst,
16071 batch,
16072 seq,
16073 hidden,
16074 end_x,
16075 end_y,
16076 head_dim,
16077 num_heads,
16078 theta,
16079 repeat_factor,
16080 } = t
16081 else {
16082 unreachable!()
16083 };
16084 {
16085 let b = *batch as usize;
16086 let s = *seq as usize;
16087 let hdim = *head_dim as usize;
16088 let nh = *num_heads as usize;
16089 let plane = s * (*hidden as usize);
16090 let plane_bytes = plane * std::mem::size_of::<f32>();
16094 unsafe {
16095 for bi in 0..b {
16096 let input = sl(*src, base.add(bi * plane_bytes), plane);
16097 let output = sl_mut(*dst, base.add(bi * plane_bytes), plane);
16098 let rotated = rlx_ir::ops::axial_rope2d::apply_axial_rope2d(
16099 input,
16100 nh,
16101 s,
16102 hdim,
16103 *end_x as usize,
16104 *end_y as usize,
16105 *theta,
16106 *repeat_factor as usize,
16107 );
16108 output.copy_from_slice(&rotated);
16109 }
16110 }
16111 }
16112}
16113
16114#[inline(always)]
16115fn exec_rms_norm(t: &Thunk, base: *mut u8) {
16116 let Thunk::RmsNorm {
16117 src,
16118 g,
16119 b,
16120 dst,
16121 rows,
16122 h,
16123 eps,
16124 } = t
16125 else {
16126 unreachable!()
16127 };
16128 {
16129 let (rows, h) = (*rows as usize, *h as usize);
16130 unsafe {
16131 let input = sl(*src, base, rows * h);
16132 let gamma = sl(*g, base, h);
16133 let beta = sl(*b, base, h);
16134 let output = sl_mut(*dst, base, rows * h);
16135 let inv_h = 1.0 / h as f32;
16136 for row in 0..rows {
16137 let in_row = &input[row * h..(row + 1) * h];
16138 let out_row = &mut output[row * h..(row + 1) * h];
16139 let mut sumsq = 0f32;
16141 for &v in in_row {
16142 sumsq += v * v;
16143 }
16144 let inv_rms = (sumsq * inv_h + *eps).sqrt().recip();
16145 for i in 0..h {
16146 out_row[i] = in_row[i] * inv_rms * gamma[i] + beta[i];
16147 }
16148 }
16149 }
16150 }
16151}
16152
16153#[inline(always)]
16154fn exec_softmax(t: &Thunk, base: *mut u8) {
16155 let Thunk::Softmax { data, rows, cols } = t else {
16156 unreachable!()
16157 };
16158 {
16159 let (rows, cols) = (*rows as usize, *cols as usize);
16160 unsafe {
16161 crate::kernels::neon_softmax(sl_mut(*data, base, rows * cols), rows, cols);
16162 }
16163 }
16164}
16165
16166#[inline(always)]
16167fn exec_cumsum(t: &Thunk, base: *mut u8) {
16168 let Thunk::Cumsum {
16169 src,
16170 dst,
16171 rows,
16172 cols,
16173 exclusive,
16174 } = t
16175 else {
16176 unreachable!()
16177 };
16178 {
16179 let (rows, cols) = (*rows as usize, *cols as usize);
16180 unsafe {
16181 let s = sl(*src, base, rows * cols);
16182 let d = sl_mut(*dst, base, rows * cols);
16183 if *exclusive {
16184 for r in 0..rows {
16185 let mut acc = 0.0f32;
16186 for c in 0..cols {
16187 d[r * cols + c] = acc;
16188 acc += s[r * cols + c];
16189 }
16190 }
16191 } else {
16192 for r in 0..rows {
16193 let mut acc = 0.0f32;
16194 for c in 0..cols {
16195 acc += s[r * cols + c];
16196 d[r * cols + c] = acc;
16197 }
16198 }
16199 }
16200 }
16201 }
16202}
16203
16204#[inline(always)]
16205fn exec_sample(t: &Thunk, base: *mut u8) {
16206 let Thunk::Sample {
16207 logits,
16208 dst,
16209 batch,
16210 vocab,
16211 top_k,
16212 top_p,
16213 temperature,
16214 seed,
16215 } = t
16216 else {
16217 unreachable!()
16218 };
16219 unsafe {
16220 execute_sample_f32(
16221 *logits,
16222 *dst,
16223 *batch as usize,
16224 *vocab as usize,
16225 *top_k as usize,
16226 *top_p,
16227 *temperature,
16228 *seed,
16229 base,
16230 );
16231 }
16232}
16233
16234#[inline(always)]
16235fn exec_gated_delta_net(t: &Thunk, base: *mut u8) {
16236 let Thunk::GatedDeltaNet {
16237 q,
16238 k,
16239 v,
16240 g,
16241 beta,
16242 state,
16243 dst,
16244 batch,
16245 seq,
16246 heads,
16247 state_size,
16248 } = t
16249 else {
16250 unreachable!()
16251 };
16252 unsafe {
16253 execute_gated_delta_net_f32(
16254 *q,
16255 *k,
16256 *v,
16257 *g,
16258 *beta,
16259 *state,
16260 *dst,
16261 *batch as usize,
16262 *seq as usize,
16263 *heads as usize,
16264 *state_size as usize,
16265 base,
16266 );
16267 }
16268}
16269
16270#[inline(always)]
16271fn exec_lstm(t: &Thunk, base: *mut u8) {
16272 let Thunk::Lstm {
16273 x,
16274 w_ih,
16275 w_hh,
16276 bias,
16277 h0,
16278 c0,
16279 dst,
16280 batch,
16281 seq,
16282 input_size,
16283 hidden,
16284 num_layers,
16285 bidirectional,
16286 carry,
16287 } = t
16288 else {
16289 unreachable!()
16290 };
16291 unsafe {
16292 execute_lstm_f32(
16293 *x,
16294 *w_ih,
16295 *w_hh,
16296 *bias,
16297 *h0,
16298 *c0,
16299 *dst,
16300 *batch as usize,
16301 *seq as usize,
16302 *input_size as usize,
16303 *hidden as usize,
16304 *num_layers as usize,
16305 *bidirectional,
16306 *carry,
16307 base,
16308 );
16309 }
16310}
16311
16312#[inline(always)]
16313fn exec_gru(t: &Thunk, base: *mut u8) {
16314 let Thunk::Gru {
16315 x,
16316 w_ih,
16317 w_hh,
16318 b_ih,
16319 b_hh,
16320 h0,
16321 dst,
16322 batch,
16323 seq,
16324 input_size,
16325 hidden,
16326 num_layers,
16327 bidirectional,
16328 carry,
16329 } = t
16330 else {
16331 unreachable!()
16332 };
16333 unsafe {
16334 execute_gru_f32(
16335 *x,
16336 *w_ih,
16337 *w_hh,
16338 *b_ih,
16339 *b_hh,
16340 *h0,
16341 *dst,
16342 *batch as usize,
16343 *seq as usize,
16344 *input_size as usize,
16345 *hidden as usize,
16346 *num_layers as usize,
16347 *bidirectional,
16348 *carry,
16349 base,
16350 );
16351 }
16352}
16353
16354#[inline(always)]
16355fn exec_rnn(t: &Thunk, base: *mut u8) {
16356 let Thunk::Rnn {
16357 x,
16358 w_ih,
16359 w_hh,
16360 bias,
16361 h0,
16362 dst,
16363 batch,
16364 seq,
16365 input_size,
16366 hidden,
16367 num_layers,
16368 bidirectional,
16369 carry,
16370 relu,
16371 } = t
16372 else {
16373 unreachable!()
16374 };
16375 unsafe {
16376 execute_rnn_f32(
16377 *x,
16378 *w_ih,
16379 *w_hh,
16380 *bias,
16381 *h0,
16382 *dst,
16383 *batch as usize,
16384 *seq as usize,
16385 *input_size as usize,
16386 *hidden as usize,
16387 *num_layers as usize,
16388 *bidirectional,
16389 *carry,
16390 *relu,
16391 base,
16392 );
16393 }
16394}
16395
16396#[inline(always)]
16397fn exec_mamba2(t: &Thunk, base: *mut u8) {
16398 let Thunk::Mamba2 {
16399 x,
16400 dt,
16401 a,
16402 b,
16403 c,
16404 dst,
16405 batch,
16406 seq,
16407 heads,
16408 head_dim,
16409 state_size,
16410 } = t
16411 else {
16412 unreachable!()
16413 };
16414 unsafe {
16415 execute_mamba2_f32(
16416 *x,
16417 *dt,
16418 *a,
16419 *b,
16420 *c,
16421 *dst,
16422 *batch as usize,
16423 *seq as usize,
16424 *heads as usize,
16425 *head_dim as usize,
16426 *state_size as usize,
16427 base,
16428 );
16429 }
16430}
16431
16432#[inline(always)]
16433fn exec_selective_scan(t: &Thunk, base: *mut u8) {
16434 let Thunk::SelectiveScan {
16435 x,
16436 delta,
16437 a,
16438 b: bp,
16439 c: cp,
16440 dst,
16441 batch,
16442 seq,
16443 hidden,
16444 state_size,
16445 } = t
16446 else {
16447 unreachable!()
16448 };
16449 unsafe {
16450 execute_selective_scan_f32(
16451 *x,
16452 *delta,
16453 *a,
16454 *bp,
16455 *cp,
16456 *dst,
16457 *batch as usize,
16458 *seq as usize,
16459 *hidden as usize,
16460 *state_size as usize,
16461 base,
16462 );
16463 }
16464}
16465
16466#[inline(always)]
16467fn exec_dequant_mat_mul(t: &Thunk, base: *mut u8) {
16468 let Thunk::DequantMatMul {
16469 x,
16470 w_q,
16471 scale,
16472 zp,
16473 dst,
16474 m,
16475 k,
16476 n,
16477 block_size,
16478 is_asymmetric,
16479 } = t
16480 else {
16481 unreachable!()
16482 };
16483 {
16484 let (m, k, n, bs) = (*m as usize, *k as usize, *n as usize, *block_size as usize);
16485 let n_blocks = k.div_ceil(bs);
16486 unsafe {
16487 let xs = sl(*x, base, m * k);
16488 let w_bytes = std::slice::from_raw_parts(base.add(*w_q) as *const i8, k * n);
16489 let scales = sl(*scale, base, n_blocks * n);
16490 let zps = if *is_asymmetric {
16491 sl(*zp, base, n_blocks * n)
16492 } else {
16493 &[][..]
16494 };
16495 let out = sl_mut(*dst, base, m * n);
16496 dequant_matmul_int8(xs, w_bytes, scales, zps, out, m, k, n, bs, *is_asymmetric);
16497 }
16498 }
16499}
16500
16501#[inline(always)]
16502fn exec_dequant_mat_mul_gguf(t: &Thunk, base: *mut u8) {
16503 let Thunk::DequantMatMulGguf {
16504 x,
16505 w_q,
16506 dst,
16507 m,
16508 k,
16509 n,
16510 scheme,
16511 } = t
16512 else {
16513 unreachable!()
16514 };
16515 {
16516 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
16517 let block_bytes = scheme.gguf_block_bytes() as usize;
16518 let block_elems = scheme.gguf_block_size() as usize;
16519 debug_assert!(
16520 block_bytes > 0 && block_elems > 0,
16521 "non-GGUF scheme in GGUF arm"
16522 );
16523 debug_assert!(
16524 (k * n).is_multiple_of(block_elems),
16525 "k*n={} not aligned to GGUF block size {}",
16526 k * n,
16527 block_elems
16528 );
16529 let total_bytes = (k * n) / block_elems * block_bytes;
16530 unsafe {
16531 let xs = sl(*x, base, m * k);
16532 let w_bytes_ptr = base.add(*w_q) as *const u8;
16533 let w_bytes = std::slice::from_raw_parts(w_bytes_ptr, total_bytes);
16534 let out = sl_mut(*dst, base, m * n);
16535 crate::gguf_matmul::gguf_matmul_bt_dispatch(xs, w_bytes, out, m, k, n, *scheme);
16536 }
16537 }
16538}
16539
16540#[inline(always)]
16541fn exec_dequant_mat_mul_int4(t: &Thunk, base: *mut u8) {
16542 let Thunk::DequantMatMulInt4 {
16543 x,
16544 w_q,
16545 scale,
16546 zp,
16547 dst,
16548 m,
16549 k,
16550 n,
16551 block_size,
16552 is_asymmetric,
16553 } = t
16554 else {
16555 unreachable!()
16556 };
16557 {
16558 let (m, k, n, bs) = (*m as usize, *k as usize, *n as usize, *block_size as usize);
16559 let n_blocks = k.div_ceil(bs);
16560 unsafe {
16561 let xs = sl(*x, base, m * k);
16562 let w_bytes =
16563 std::slice::from_raw_parts(base.add(*w_q) as *const u8, (k * n).div_ceil(2));
16564 let scales = sl(*scale, base, n_blocks * n);
16565 let zps = if *is_asymmetric {
16566 sl(*zp, base, n_blocks * n)
16567 } else {
16568 &[][..]
16569 };
16570 let out = sl_mut(*dst, base, m * n);
16571 dequant_matmul_int4(xs, w_bytes, scales, zps, out, m, k, n, bs, *is_asymmetric);
16572 }
16573 }
16574}
16575
16576#[inline(always)]
16577fn exec_dequant_mat_mul_fp8(t: &Thunk, base: *mut u8) {
16578 let Thunk::DequantMatMulFp8 {
16579 x,
16580 w_q,
16581 scale,
16582 dst,
16583 m,
16584 k,
16585 n,
16586 e5m2,
16587 } = t
16588 else {
16589 unreachable!()
16590 };
16591 {
16592 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
16593 unsafe {
16594 let xs = sl(*x, base, m * k);
16595 let w_bytes = std::slice::from_raw_parts(base.add(*w_q) as *const u8, k * n);
16596 let scales = sl(*scale, base, n);
16597 let out = sl_mut(*dst, base, m * n);
16598 dequant_matmul_fp8(xs, w_bytes, scales, out, m, k, n, *e5m2);
16599 }
16600 }
16601}
16602
16603#[inline(always)]
16604fn exec_dequant_mat_mul_nvfp4(t: &Thunk, base: *mut u8) {
16605 let Thunk::DequantMatMulNvfp4 {
16606 x,
16607 w_q,
16608 scale,
16609 global_scale,
16610 dst,
16611 m,
16612 k,
16613 n,
16614 } = t
16615 else {
16616 unreachable!()
16617 };
16618 {
16619 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
16620 let n_scale = k.div_ceil(rlx_ir::NVFP4_GROUP_SIZE) * n;
16621 unsafe {
16622 let xs = sl(*x, base, m * k);
16623 let w_bytes =
16624 std::slice::from_raw_parts(base.add(*w_q) as *const u8, (k * n).div_ceil(2));
16625 let scale_bytes = std::slice::from_raw_parts(base.add(*scale) as *const u8, n_scale);
16626 let gs = sl(*global_scale, base, 1)[0];
16627 let out = sl_mut(*dst, base, m * n);
16628 dequant_matmul_nvfp4(xs, w_bytes, scale_bytes, gs, out, m, k, n);
16629 }
16630 }
16631}
16632
16633#[inline(always)]
16634fn exec_scaled_mat_mul(t: &Thunk, base: *mut u8) {
16635 let Thunk::ScaledMatMul {
16636 lhs,
16637 rhs,
16638 lhs_scale,
16639 rhs_scale,
16640 bias,
16641 dst,
16642 m,
16643 k,
16644 n,
16645 lhs_fmt,
16646 rhs_fmt,
16647 layout,
16648 has_bias,
16649 } = t
16650 else {
16651 unreachable!()
16652 };
16653 {
16654 let (m, k, n) = (*m as usize, *k as usize, *n as usize);
16655 let layout = *layout;
16656 let nblk = lowp_nblk(k, layout);
16657 let per_tensor = matches!(layout, rlx_ir::ScaleLayout::PerTensor);
16658 let n_lscale = if per_tensor { 1 } else { m * nblk };
16659 let n_rscale = if per_tensor { 1 } else { n * nblk };
16660 unsafe {
16661 let lhs_b = std::slice::from_raw_parts(base.add(*lhs) as *const u8, m * k);
16662 let rhs_b = std::slice::from_raw_parts(base.add(*rhs) as *const u8, n * k);
16663 let ls = lowp_read_scales(layout, base, *lhs_scale, n_lscale);
16664 let rs = lowp_read_scales(layout, base, *rhs_scale, n_rscale);
16665 let bias_s = if *has_bias {
16666 Some(sl(*bias, base, n))
16667 } else {
16668 None
16669 };
16670 let out = sl_mut(*dst, base, m * n);
16671 lowp_scaled_matmul(
16672 lhs_b, rhs_b, &ls, &rs, bias_s, out, m, n, k, layout, *lhs_fmt, *rhs_fmt,
16673 );
16674 }
16675 }
16676}
16677
16678#[inline(always)]
16679fn exec_scaled_quantize(t: &Thunk, base: *mut u8) {
16680 let Thunk::ScaledQuantize {
16681 x,
16682 scale,
16683 dst,
16684 rows,
16685 cols,
16686 fmt,
16687 layout,
16688 } = t
16689 else {
16690 unreachable!()
16691 };
16692 {
16693 let (rows, cols) = (*rows as usize, *cols as usize);
16694 let layout = *layout;
16695 let nblk = lowp_nblk(cols, layout);
16696 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
16697 1
16698 } else {
16699 rows * nblk
16700 };
16701 unsafe {
16702 let xs = sl(*x, base, rows * cols);
16703 let scales = lowp_read_scales(layout, base, *scale, n_scale);
16704 let out = std::slice::from_raw_parts_mut(base.add(*dst), rows * cols);
16705 lowp_quantize(xs, &scales, *fmt, layout, rows, cols, out);
16706 }
16707 }
16708}
16709
16710#[inline(always)]
16711fn exec_scaled_quant_scale(t: &Thunk, base: *mut u8) {
16712 let Thunk::ScaledQuantScale {
16713 x,
16714 dst,
16715 rows,
16716 cols,
16717 fmt,
16718 layout,
16719 } = t
16720 else {
16721 unreachable!()
16722 };
16723 {
16724 let (rows, cols) = (*rows as usize, *cols as usize);
16725 let layout = *layout;
16726 let nblk = lowp_nblk(cols, layout);
16727 unsafe {
16728 let xs = sl(*x, base, rows * cols);
16729 let scales = lowp_compute_scales(xs, *fmt, layout, rows, cols);
16730 match layout {
16731 rlx_ir::ScaleLayout::PerTensor => {
16732 sl_mut(*dst, base, 1)[0] = scales[0];
16733 }
16734 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
16735 let out = std::slice::from_raw_parts_mut(base.add(*dst), rows * nblk);
16736 for (o, &s) in out.iter_mut().zip(&scales) {
16737 *o = rlx_ir::lowp_codec::f32_to_e8m0(s);
16738 }
16739 }
16740 rlx_ir::ScaleLayout::Nvfp4 { .. } => {
16741 let out = std::slice::from_raw_parts_mut(base.add(*dst), rows * nblk);
16742 for (o, &s) in out.iter_mut().zip(&scales) {
16743 *o = rlx_ir::lowp_codec::encode(rlx_ir::ScaledFormat::F8E4M3, s);
16744 }
16745 }
16746 }
16747 }
16748 }
16749}
16750
16751#[inline(always)]
16752fn exec_scaled_dequantize(t: &Thunk, base: *mut u8) {
16753 let Thunk::ScaledDequantize {
16754 codes,
16755 scale,
16756 dst,
16757 rows,
16758 cols,
16759 fmt,
16760 layout,
16761 } = t
16762 else {
16763 unreachable!()
16764 };
16765 unsafe {
16766 execute_scaled_dequantize_f32(
16767 *codes,
16768 *scale,
16769 *dst,
16770 *rows as usize,
16771 *cols as usize,
16772 *fmt,
16773 *layout,
16774 base,
16775 );
16776 }
16777}
16778
16779#[inline(always)]
16780fn exec_lora_mat_mul(t: &Thunk, base: *mut u8) {
16781 let Thunk::LoraMatMul {
16782 x,
16783 w,
16784 a,
16785 b,
16786 dst,
16787 m,
16788 k,
16789 n,
16790 r,
16791 scale,
16792 } = t
16793 else {
16794 unreachable!()
16795 };
16796 {
16797 let (m, k, n, r) = (*m as usize, *k as usize, *n as usize, *r as usize);
16798 unsafe {
16799 let xs = sl(*x, base, m * k);
16800 let ws = sl(*w, base, k * n);
16801 let a_s = sl(*a, base, k * r);
16802 let bs = sl(*b, base, r * n);
16803 let out = sl_mut(*dst, base, m * n);
16804 crate::blas::sgemm(xs, ws, out, m, k, n);
16805 let mut tmp = vec![0f32; m * r];
16806 crate::blas::sgemm(xs, a_s, &mut tmp, m, k, r);
16807 if *scale != 1.0 {
16808 for v in tmp.iter_mut() {
16809 *v *= *scale;
16810 }
16811 }
16812 crate::blas::sgemm_accumulate(&tmp, bs, out, m, r, n);
16813 }
16814 }
16815}
16816
16817#[inline(always)]
16818fn exec_attention_backward(t: &Thunk, base: *mut u8) {
16819 let Thunk::AttentionBackward {
16820 q,
16821 k,
16822 v,
16823 dy,
16824 mask,
16825 out,
16826 batch,
16827 seq,
16828 kv_seq,
16829 heads,
16830 head_dim,
16831 mask_kind,
16832 wrt,
16833 bhsd,
16834 } = t
16835 else {
16836 unreachable!()
16837 };
16838 {
16839 let (b, q_s, k_s, nh, dh) = (
16840 *batch as usize,
16841 *seq as usize,
16842 *kv_seq as usize,
16843 *heads as usize,
16844 *head_dim as usize,
16845 );
16846 unsafe {
16847 let q_len = if *bhsd {
16848 b * nh * q_s * dh
16849 } else {
16850 b * q_s * nh * dh
16851 };
16852 let k_len = if *bhsd {
16853 b * nh * k_s * dh
16854 } else {
16855 b * k_s * nh * dh
16856 };
16857 let out_len = match wrt {
16858 rlx_ir::op::AttentionBwdWrt::Key | rlx_ir::op::AttentionBwdWrt::Value => k_len,
16859 rlx_ir::op::AttentionBwdWrt::Query => q_len,
16860 };
16861 let q_data = sl(*q, base, q_len);
16862 let k_data = sl(*k, base, k_len);
16863 let v_data = sl(*v, base, k_len);
16864 let dy_data = sl(*dy, base, q_len);
16865 let out_data = sl_mut(*out, base, out_len);
16866 let mask_data: &[f32] = if *mask != 0 {
16867 let ml = match mask_kind {
16868 rlx_ir::op::MaskKind::Custom => b * k_s,
16869 rlx_ir::op::MaskKind::Bias => b * nh * q_s * k_s,
16870 _ => 0,
16871 };
16872 sl(*mask, base, ml)
16873 } else {
16874 &[]
16875 };
16876 crate::attention_bwd::attention_backward(
16877 *wrt, q_data, k_data, v_data, dy_data, out_data, b, nh, q_s, k_s, dh, *mask_kind,
16878 mask_data, *bhsd,
16879 );
16880 }
16881 }
16882}
16883
16884#[inline(always)]
16885fn exec_activation_in_place(t: &Thunk, base: *mut u8) {
16886 let Thunk::ActivationInPlace { data, len, act } = t else {
16887 unreachable!()
16888 };
16889 {
16890 let len = *len as usize;
16891 unsafe {
16892 let d = sl_mut(*data, base, len);
16893 match act {
16894 Activation::Gelu => crate::kernels::par_gelu_inplace(d),
16895 Activation::GeluApprox => crate::kernels::par_gelu_approx_inplace(d),
16896 Activation::Silu => crate::kernels::par_silu_inplace(d),
16897 Activation::Relu => {
16898 for v in d.iter_mut() {
16899 *v = v.max(0.0);
16900 }
16901 }
16902 Activation::Sigmoid => {
16903 for v in d.iter_mut() {
16904 *v = 1.0 / (1.0 + (-*v).exp());
16905 }
16906 }
16907 Activation::Tanh => {
16908 for v in d.iter_mut() {
16909 *v = v.tanh();
16910 }
16911 }
16912 Activation::Exp => {
16913 for v in d.iter_mut() {
16914 *v = v.exp();
16915 }
16916 }
16917 Activation::Log => {
16918 for v in d.iter_mut() {
16919 *v = v.ln();
16920 }
16921 }
16922 Activation::Sqrt => {
16923 for v in d.iter_mut() {
16924 *v = v.sqrt();
16925 }
16926 }
16927 Activation::Rsqrt => {
16928 for v in d.iter_mut() {
16929 *v = 1.0 / v.sqrt();
16930 }
16931 }
16932 Activation::Neg => {
16933 for v in d.iter_mut() {
16934 *v = -*v;
16935 }
16936 }
16937 Activation::Abs => {
16938 for v in d.iter_mut() {
16939 *v = v.abs();
16940 }
16941 }
16942 Activation::Round => {
16943 for v in d.iter_mut() {
16944 *v = v.round();
16945 }
16946 }
16947 Activation::Sin => {
16948 for v in d.iter_mut() {
16949 *v = v.sin();
16950 }
16951 }
16952 Activation::Cos => {
16953 for v in d.iter_mut() {
16954 *v = v.cos();
16955 }
16956 }
16957 Activation::Tan => {
16958 for v in d.iter_mut() {
16959 *v = v.tan();
16960 }
16961 }
16962 Activation::Atan => {
16963 for v in d.iter_mut() {
16964 *v = v.atan();
16965 }
16966 }
16967 }
16968 }
16969 }
16970}
16971
16972#[inline(always)]
16973fn exec_rope(t: &Thunk, base: *mut u8) {
16974 let Thunk::Rope {
16975 src,
16976 cos,
16977 sin,
16978 dst,
16979 batch,
16980 seq,
16981 hidden,
16982 head_dim,
16983 n_rot,
16984 cos_len,
16985 src_row_stride,
16986 interleaved,
16987 } = t
16988 else {
16989 unreachable!()
16990 };
16991 {
16992 let interleaved = *interleaved;
16993 let (b, s, hs, dh, nr) = (
16994 *batch as usize,
16995 *seq as usize,
16996 *hidden as usize,
16997 *head_dim as usize,
16998 *n_rot as usize,
16999 );
17000 let tab_half = dh / 2;
17001 let rot_half = nr / 2;
17002 let nh = hs / dh;
17003 let cl = *cos_len as usize;
17004 let src_rs = *src_row_stride as usize;
17005 let cos_rows = cl / tab_half.max(1);
17010 let per_token = cos_rows == b * s && cos_rows != s;
17011 unsafe {
17012 let x = sl(*src, base, b * s * src_rs);
17013 let cos_tab = sl(*cos, base, cl);
17014 let sin_tab = sl(*sin, base, cl);
17015 let out = sl_mut(*dst, base, b * s * hs);
17016
17017 let total = b * s;
17018 let x_ptr = x.as_ptr() as usize;
17019 let o_ptr = out.as_mut_ptr() as usize;
17020 let c_ptr = cos_tab.as_ptr() as usize;
17021 let s_ptr = sin_tab.as_ptr() as usize;
17022
17023 crate::pool::par_for(total, 4, &|off, cnt| {
17024 for idx in off..off + cnt {
17025 let bi = idx / s;
17026 let si = idx % s;
17027 let tab_off = if per_token { idx } else { si } * tab_half;
17028
17029 for hi in 0..nh {
17030 let src_base = bi * s * src_rs + si * src_rs + hi * dh;
17031 let dst_base = bi * s * hs + si * hs + hi * dh;
17032 let xp = (x_ptr as *const f32).add(src_base);
17033 let op = (o_ptr as *mut f32).add(dst_base);
17034 let cp = (c_ptr as *const f32).add(tab_off);
17035 let sp = (s_ptr as *const f32).add(tab_off);
17036
17037 if interleaved {
17038 for i in 0..rot_half {
17041 let x1 = *xp.add(2 * i);
17042 let x2 = *xp.add(2 * i + 1);
17043 let cv = *cp.add(i);
17044 let sv = *sp.add(i);
17045 *op.add(2 * i) = x1 * cv - x2 * sv;
17046 *op.add(2 * i + 1) = x2 * cv + x1 * sv;
17047 }
17048 } else {
17049 for i in 0..rot_half {
17051 let x1 = *xp.add(i);
17052 let x2 = *xp.add(rot_half + i);
17053 let cv = *cp.add(i);
17054 let sv = *sp.add(i);
17055 *op.add(i) = x1 * cv - x2 * sv;
17056 *op.add(rot_half + i) = x2 * cv + x1 * sv;
17057 }
17058 }
17059 for j in nr..dh {
17060 *op.add(j) = *xp.add(j);
17061 }
17062 }
17063 }
17064 });
17065 }
17066 }
17067}
17068
17069#[inline(always)]
17070fn exec_fused_swi_g_l_u(t: &Thunk, base: *mut u8) {
17071 let Thunk::FusedSwiGLU {
17072 src,
17073 dst,
17074 n_half,
17075 total,
17076 gate_first,
17077 } = t
17078 else {
17079 unreachable!()
17080 };
17081 {
17082 let n = *n_half as usize;
17083 let t = *total as usize;
17084 let outer = t / n;
17085 let in_total = outer * 2 * n;
17086 let gate_first = *gate_first;
17087 unsafe {
17088 let inp = sl(*src, base, in_total);
17089 let out = sl_mut(*dst, base, t);
17090 for o in 0..outer {
17091 let in_row = &inp[o * 2 * n..(o + 1) * 2 * n];
17092 let out_row = &mut out[o * n..(o + 1) * n];
17093 for i in 0..n {
17094 let (up, gate) = if gate_first {
17095 (in_row[n + i], in_row[i])
17096 } else {
17097 (in_row[i], in_row[n + i])
17098 };
17099 out_row[i] = up * (gate / (1.0 + (-gate).exp()));
17100 }
17101 }
17102 }
17103 }
17104}
17105
17106#[inline(always)]
17107fn exec_concat(t: &Thunk, base: *mut u8) {
17108 let Thunk::Concat {
17109 dst,
17110 outer,
17111 inner,
17112 total_axis,
17113 inputs,
17114 } = t
17115 else {
17116 unreachable!()
17117 };
17118 {
17119 let outer = *outer as usize;
17120 let inner = *inner as usize;
17121 let total_axis = *total_axis as usize;
17122 let row_stride = total_axis * inner;
17123 let out_total = outer * row_stride;
17124 unsafe {
17125 let out = sl_mut(*dst, base, out_total);
17126 let mut cum: usize = 0;
17127 for (src_off, in_axis, in_numel) in inputs {
17128 let in_axis = *in_axis as usize;
17129 let copy_per_row = in_axis * inner;
17130 let dst_col_off = cum * inner;
17131 let inp = sl(*src_off, base, (*in_numel as usize).max(1));
17132 concat_copy_rows_f32(
17133 out,
17134 inp,
17135 outer,
17136 copy_per_row,
17137 row_stride,
17138 dst_col_off,
17139 *in_numel as usize,
17140 );
17141 cum += in_axis;
17142 }
17143 }
17144 }
17145}
17146
17147#[inline(always)]
17148fn exec_concat_f64(t: &Thunk, base: *mut u8) {
17149 let Thunk::ConcatF64 {
17150 dst,
17151 outer,
17152 inner,
17153 total_axis,
17154 inputs,
17155 } = t
17156 else {
17157 unreachable!()
17158 };
17159 {
17160 let outer = *outer as usize;
17161 let inner = *inner as usize;
17162 let total_axis = *total_axis as usize;
17163 let row_stride = total_axis * inner;
17164 let out_total = outer * row_stride;
17165 unsafe {
17166 let out = sl_mut_f64(*dst, base, out_total);
17167 let mut cum: usize = 0;
17168 for (src_off, in_axis, in_numel) in inputs {
17169 let in_axis = *in_axis as usize;
17170 let copy_per_row = in_axis * inner;
17171 let dst_col_off = cum * inner;
17172 let inp = sl_f64(*src_off, base, (*in_numel as usize).max(1));
17173 concat_copy_rows_f64(
17174 out,
17175 inp,
17176 outer,
17177 copy_per_row,
17178 row_stride,
17179 dst_col_off,
17180 *in_numel as usize,
17181 );
17182 cum += in_axis;
17183 }
17184 }
17185 }
17186}
17187
17188#[inline(always)]
17189fn exec_scatter_add(t: &Thunk, base: *mut u8) {
17190 let Thunk::ScatterAdd {
17191 updates,
17192 indices,
17193 dst,
17194 num_updates,
17195 out_dim,
17196 trailing,
17197 } = t
17198 else {
17199 unreachable!()
17200 };
17201 {
17202 let num_updates = *num_updates as usize;
17203 let out_dim = *out_dim as usize;
17204 let trailing = *trailing as usize;
17205 unsafe {
17206 let upd = sl(*updates, base, num_updates * trailing);
17207 let ids = sl(*indices, base, num_updates);
17208 let out = sl_mut(*dst, base, out_dim * trailing);
17209 for v in out.iter_mut() {
17211 *v = 0.0;
17212 }
17213 for i in 0..num_updates {
17214 let row = ids[i] as usize;
17215 debug_assert!(row < out_dim, "ScatterAdd index out of range");
17216 let src_off = i * trailing;
17217 let dst_off = row * trailing;
17218 for j in 0..trailing {
17219 out[dst_off + j] += upd[src_off + j];
17220 }
17221 }
17222 }
17223 }
17224}
17225
17226#[inline(always)]
17227fn exec_dequant_grouped_mat_mul_gguf(t: &Thunk, base: *mut u8) {
17228 let Thunk::DequantGroupedMatMulGguf {
17229 input,
17230 w_q,
17231 expert_idx,
17232 dst,
17233 m,
17234 k_dim,
17235 n,
17236 num_experts,
17237 scheme,
17238 } = t
17239 else {
17240 unreachable!()
17241 };
17242 {
17243 let m = *m as usize;
17244 let k_dim = *k_dim as usize;
17245 let n = *n as usize;
17246 let num_experts = *num_experts as usize;
17247 let block_elems = scheme.gguf_block_size() as usize;
17248 let block_bytes = scheme.gguf_block_bytes() as usize;
17249 let slab_bytes = (k_dim * n) / block_elems * block_bytes;
17250 unsafe {
17251 let inp = sl(*input, base, m * k_dim);
17252 let wt =
17253 std::slice::from_raw_parts(base.add(*w_q) as *const u8, num_experts * slab_bytes);
17254 let ids = sl(*expert_idx, base, m);
17255 let out = sl_mut(*dst, base, m * n);
17256 crate::gguf_matmul::gguf_grouped_matmul_bt(
17257 inp,
17258 wt,
17259 ids,
17260 out,
17261 m,
17262 k_dim,
17263 n,
17264 num_experts,
17265 *scheme,
17266 );
17267 }
17268 }
17269}
17270
17271#[inline(always)]
17272fn exec_dequant_mo_e_weights_gguf(t: &Thunk, base: *mut u8) {
17273 let Thunk::DequantMoEWeightsGguf {
17274 w_q,
17275 dst,
17276 k_dim,
17277 n,
17278 num_experts,
17279 scheme,
17280 } = t
17281 else {
17282 unreachable!()
17283 };
17284 {
17285 let k_dim = *k_dim as usize;
17286 let n = *n as usize;
17287 let num_experts = *num_experts as usize;
17288 let block_elems = scheme.gguf_block_size() as usize;
17289 let block_bytes = scheme.gguf_block_bytes() as usize;
17290 let slab_bytes = (k_dim * n) / block_elems * block_bytes;
17291 unsafe {
17292 let wt =
17293 std::slice::from_raw_parts(base.add(*w_q) as *const u8, num_experts * slab_bytes);
17294 let out = sl_mut(*dst, base, num_experts * k_dim * n);
17295 crate::gguf_matmul::dequant_moe_weights_to_grouped_f32(
17296 wt,
17297 out,
17298 num_experts,
17299 k_dim,
17300 n,
17301 *scheme,
17302 );
17303 }
17304 }
17305}
17306
17307#[inline(always)]
17308fn exec_reduce(t: &Thunk, base: *mut u8) {
17309 let Thunk::Reduce {
17310 src,
17311 dst,
17312 outer,
17313 reduced,
17314 inner,
17315 op,
17316 } = t
17317 else {
17318 unreachable!()
17319 };
17320 {
17321 let outer = *outer as usize;
17322 let reduced = *reduced as usize;
17323 let inner = *inner as usize;
17324 let in_total = outer * reduced * inner;
17325 let out_total = outer * inner;
17326 unsafe {
17327 let inp = sl(*src, base, in_total);
17328 let out = sl_mut(*dst, base, out_total);
17329 let reduce_one = |oi: usize| -> f32 {
17333 let o = oi / inner;
17334 let i = oi % inner;
17335 let mut acc = match op {
17336 ReduceOp::Max => f32::NEG_INFINITY,
17337 ReduceOp::Min => f32::INFINITY,
17338 ReduceOp::Prod => 1.0f32,
17339 _ => 0.0f32, };
17341 for r in 0..reduced {
17342 let v = inp[o * reduced * inner + r * inner + i];
17343 acc = match op {
17344 ReduceOp::Sum | ReduceOp::Mean => acc + v,
17345 ReduceOp::Max => acc.max(v),
17346 ReduceOp::Min => acc.min(v),
17347 ReduceOp::Prod => acc * v,
17348 };
17349 }
17350 if matches!(op, ReduceOp::Mean) {
17351 acc /= reduced as f32;
17352 }
17353 acc
17354 };
17355 if fast_conv_enabled() && crate::pool::should_parallelize(in_total) && out_total > 1 {
17356 let out_addr = out.as_mut_ptr() as usize;
17357 crate::pool::par_for(
17358 out_total,
17359 crate::pool::outer_chunk(out_total),
17360 &|off, cnt| {
17361 for oi in off..off + cnt {
17362 *((out_addr as *mut f32).add(oi)) = reduce_one(oi);
17363 }
17364 },
17365 );
17366 } else {
17367 for oi in 0..out_total {
17368 out[oi] = reduce_one(oi);
17369 }
17370 }
17371 }
17372 }
17373}
17374
17375#[inline(always)]
17376fn exec_arg_reduce(t: &Thunk, base: *mut u8) {
17377 let Thunk::ArgReduce {
17378 src,
17379 dst,
17380 outer,
17381 reduced,
17382 inner,
17383 is_max,
17384 } = t
17385 else {
17386 unreachable!()
17387 };
17388 {
17389 let outer = *outer as usize;
17390 let reduced = *reduced as usize;
17391 let inner = *inner as usize;
17392 let in_total = outer * reduced * inner;
17393 let out_total = outer * inner;
17394 unsafe {
17395 let inp = sl(*src, base, in_total);
17396 let out = sl_mut(*dst, base, out_total);
17397 for o in 0..outer {
17398 for i in 0..inner {
17399 let mut best = inp[o * reduced * inner + i];
17400 let mut best_idx = 0usize;
17401 for r in 1..reduced {
17402 let v = inp[o * reduced * inner + r * inner + i];
17403 let better = if *is_max { v > best } else { v < best };
17404 if better {
17405 best = v;
17406 best_idx = r;
17407 }
17408 }
17409 out[o * inner + i] = best_idx as f32;
17410 }
17411 }
17412 }
17413 }
17414}
17415
17416#[inline(always)]
17417fn exec_conv2_d1x1(t: &Thunk, base: *mut u8) {
17418 let Thunk::Conv2D1x1 {
17419 src,
17420 weight,
17421 dst,
17422 n,
17423 c_in,
17424 c_out,
17425 hw,
17426 } = t
17427 else {
17428 unreachable!()
17429 };
17430 {
17431 let n = *n as usize;
17432 let c_in = *c_in as usize;
17433 let c_out = *c_out as usize;
17434 let hw = *hw as usize;
17435 unsafe {
17436 let inp = sl(*src, base, n * c_in * hw);
17437 let wt = sl(*weight, base, c_out * c_in);
17438 let out = sl_mut(*dst, base, n * c_out * hw);
17439 for ni in 0..n {
17444 let in_off = ni * c_in * hw;
17445 let out_off = ni * c_out * hw;
17446 crate::blas::sgemm(
17447 wt,
17448 &inp[in_off..in_off + c_in * hw],
17449 &mut out[out_off..out_off + c_out * hw],
17450 c_out,
17451 c_in,
17452 hw,
17453 );
17454 }
17455 }
17456 }
17457}
17458
17459#[inline(always)]
17460fn exec_conv2_d(t: &Thunk, base: *mut u8) {
17461 let Thunk::Conv2D {
17462 src,
17463 weight,
17464 dst,
17465 n,
17466 c_in,
17467 h,
17468 w,
17469 c_out,
17470 h_out,
17471 w_out,
17472 kh,
17473 kw,
17474 sh,
17475 sw,
17476 ph,
17477 pw,
17478 dh,
17479 dw,
17480 groups,
17481 } = t
17482 else {
17483 unreachable!()
17484 };
17485 {
17486 let n = *n as usize;
17487 let c_in = *c_in as usize;
17488 let h = *h as usize;
17489 let w = *w as usize;
17490 let c_out = *c_out as usize;
17491 let h_out = *h_out as usize;
17492 let w_out = *w_out as usize;
17493 let kh = *kh as usize;
17494 let kw = *kw as usize;
17495 let sh = *sh as usize;
17496 let sw = *sw as usize;
17497 let ph = *ph as usize;
17498 let pw = *pw as usize;
17499 let dh = *dh as usize;
17500 let dw = *dw as usize;
17501 let groups = *groups as usize;
17502 let c_in_per_g = c_in / groups;
17503 unsafe {
17504 let inp = sl(*src, base, n * c_in * h * w);
17505 let wt = sl(*weight, base, c_out * c_in_per_g * kh * kw);
17506 let out = sl_mut(*dst, base, n * c_out * h_out * w_out);
17507 let s1_nopad = sh == 1 && sw == 1 && ph == 0 && pw == 0 && dh == 1 && dw == 1;
17516 let winograd_ok = s1_nopad && kh == 3 && kw == 3 && groups == 1;
17517 if fast_conv_enabled() && winograd_enabled() && winograd_ok {
17522 conv2d_forward_winograd(inp, wt, out, n, c_in, h, w, c_out, h_out, w_out);
17523 } else if fast_conv_enabled() && direct_conv_enabled() && s1_nopad {
17524 conv2d_forward_direct(
17525 inp, wt, out, n, c_in, h, w, c_out, h_out, w_out, kh, kw, groups,
17526 );
17527 } else if fast_conv_enabled() {
17528 conv2d_forward_im2col(
17529 inp, wt, out, n, c_in, h, w, c_out, h_out, w_out, kh, kw, sh, sw, ph, pw, dh,
17530 dw, groups,
17531 );
17532 } else {
17533 conv2d_forward_naive(
17534 inp, wt, out, n, c_in, h, w, c_out, h_out, w_out, kh, kw, sh, sw, ph, pw, dh,
17535 dw, groups,
17536 );
17537 }
17538 }
17539 }
17540}
17541
17542#[inline(always)]
17543fn exec_relu_backward(t: &Thunk, base: *mut u8) {
17544 let Thunk::ReluBackward { x, dy, dx, len } = t else {
17545 unreachable!()
17546 };
17547 {
17548 let len = *len as usize;
17549 unsafe {
17550 let xs = sl(*x, base, len);
17551 let dys = sl(*dy, base, len);
17552 let out = sl_mut(*dx, base, len);
17553 if fast_conv_enabled() && crate::pool::should_parallelize(len) {
17554 let oa = out.as_mut_ptr() as usize;
17555 crate::pool::par_for(len, crate::pool::chunk_floor(len), &|off, cnt| {
17556 for i in off..off + cnt {
17557 *((oa as *mut f32).add(i)) = if xs[i] > 0.0 { dys[i] } else { 0.0 };
17558 }
17559 });
17560 } else {
17561 for i in 0..len {
17562 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
17563 }
17564 }
17565 }
17566 }
17567}
17568
17569#[inline(always)]
17570fn exec_relu_backward_f64(t: &Thunk, base: *mut u8) {
17571 let Thunk::ReluBackwardF64 { x, dy, dx, len } = t else {
17572 unreachable!()
17573 };
17574 {
17575 let len = *len as usize;
17576 unsafe {
17577 let xs = sl_f64(*x, base, len);
17578 let dys = sl_f64(*dy, base, len);
17579 let out = sl_mut_f64(*dx, base, len);
17580 for i in 0..len {
17581 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
17582 }
17583 }
17584 }
17585}
17586
17587#[inline(always)]
17588fn exec_q_mat_mul(t: &Thunk, base: *mut u8) {
17589 let Thunk::QMatMul {
17590 x,
17591 w,
17592 bias,
17593 out,
17594 m,
17595 k,
17596 n,
17597 x_zp,
17598 w_zp,
17599 out_zp,
17600 mult,
17601 } = t
17602 else {
17603 unreachable!()
17604 };
17605 {
17606 let m = *m as usize;
17607 let k = *k as usize;
17608 let n = *n as usize;
17609 unsafe {
17610 let x_ptr = base.add(*x) as *const i8;
17611 let w_ptr = base.add(*w) as *const i8;
17612 let bias_ptr = base.add(*bias) as *const i32;
17613 let out_ptr = base.add(*out) as *mut i8;
17614 for mi in 0..m {
17615 for ni in 0..n {
17616 let mut acc: i32 = *bias_ptr.add(ni);
17617 for ki in 0..k {
17618 let xv = *x_ptr.add(mi * k + ki) as i32 - *x_zp;
17619 let wv = *w_ptr.add(ki * n + ni) as i32 - *w_zp;
17620 acc += xv * wv;
17621 }
17622 let r = (acc as f32 * *mult).round() as i32 + *out_zp;
17625 let r = r.clamp(-128, 127) as i8;
17626 *out_ptr.add(mi * n + ni) = r;
17627 }
17628 }
17629 }
17630 }
17631}
17632
17633#[inline(always)]
17634fn exec_quantize(t: &Thunk, base: *mut u8) {
17635 let Thunk::Quantize {
17636 x,
17637 q,
17638 len,
17639 chan_axis: _,
17640 chan_dim,
17641 inner,
17642 scales,
17643 zero_points,
17644 } = t
17645 else {
17646 unreachable!()
17647 };
17648 {
17649 let len = *len as usize;
17650 let chan_dim = *chan_dim as usize;
17651 let inner = *inner as usize;
17652 unsafe {
17653 let xs = sl(*x, base, len);
17654 let q_ptr = base.add(*q) as *mut i8;
17655 for i in 0..len {
17656 let c = if chan_dim == 1 {
17657 0
17658 } else {
17659 (i / inner) % chan_dim
17660 };
17661 let inv_scale = 1.0 / scales[c];
17662 let zp = zero_points[c];
17663 let v = (xs[i] * inv_scale).round() as i32 + zp;
17664 *q_ptr.add(i) = v.clamp(-128, 127) as i8;
17665 }
17666 }
17667 }
17668}
17669
17670#[inline(always)]
17671fn exec_dequantize(t: &Thunk, base: *mut u8) {
17672 let Thunk::Dequantize {
17673 q,
17674 x,
17675 len,
17676 chan_axis: _,
17677 chan_dim,
17678 inner,
17679 scales,
17680 zero_points,
17681 } = t
17682 else {
17683 unreachable!()
17684 };
17685 {
17686 let len = *len as usize;
17687 let chan_dim = *chan_dim as usize;
17688 let inner = *inner as usize;
17689 unsafe {
17690 let q_ptr = base.add(*q) as *const i8;
17691 let out = sl_mut(*x, base, len);
17692 for i in 0..len {
17693 let c = if chan_dim == 1 {
17694 0
17695 } else {
17696 (i / inner) % chan_dim
17697 };
17698 let scale = scales[c];
17699 let zp = zero_points[c];
17700 let qv = *q_ptr.add(i) as i32;
17701 out[i] = (qv - zp) as f32 * scale;
17702 }
17703 }
17704 }
17705}
17706
17707#[inline(always)]
17708fn exec_fake_quantize(t: &Thunk, base: *mut u8) {
17709 let Thunk::FakeQuantize {
17710 x,
17711 out,
17712 len,
17713 chan_axis: _,
17714 chan_dim,
17715 inner,
17716 bits,
17717 ste: _,
17718 scale_mode,
17719 state_off,
17720 } = t
17721 else {
17722 unreachable!()
17723 };
17724 {
17725 use rlx_ir::op::ScaleMode;
17726 let len = *len as usize;
17727 let chan_dim = *chan_dim as usize;
17728 let inner = *inner as usize;
17729 let q_max: f32 = match *bits {
17730 8 => 127.0,
17731 4 => 7.0,
17732 2 => 1.0,
17733 n => panic!("FakeQuantize: unsupported bits {n}"),
17734 };
17735 unsafe {
17736 let xs = sl(*x, base, len);
17737 let outs = sl_mut(*out, base, len);
17738
17739 let mut scale = vec![0f32; chan_dim];
17740 match scale_mode {
17741 ScaleMode::PerBatch => {
17742 let mut max_abs = vec![0f32; chan_dim];
17743 for i in 0..len {
17744 let c = if chan_dim == 1 {
17745 0
17746 } else {
17747 (i / inner) % chan_dim
17748 };
17749 let a = xs[i].abs();
17750 if a > max_abs[c] {
17751 max_abs[c] = a;
17752 }
17753 }
17754 for c in 0..chan_dim {
17755 scale[c] = (max_abs[c] / q_max).max(1e-12);
17756 }
17757 }
17758 ScaleMode::EMA { decay } => {
17759 let mut max_abs = vec![0f32; chan_dim];
17762 for i in 0..len {
17763 let c = if chan_dim == 1 {
17764 0
17765 } else {
17766 (i / inner) % chan_dim
17767 };
17768 let a = xs[i].abs();
17769 if a > max_abs[c] {
17770 max_abs[c] = a;
17771 }
17772 }
17773 let state = sl_mut(state_off.expect("EMA needs state_off"), base, chan_dim);
17774 for c in 0..chan_dim {
17775 let cur = (max_abs[c] / q_max).max(1e-12);
17776 let blended = if state[c] <= 0.0 {
17778 cur
17779 } else {
17780 *decay * state[c] + (1.0 - *decay) * cur
17781 };
17782 state[c] = blended;
17783 scale[c] = blended;
17784 }
17785 }
17786 ScaleMode::Fixed => {
17787 let state = sl(state_off.expect("Fixed needs state_off"), base, chan_dim);
17788 for c in 0..chan_dim {
17789 scale[c] = state[c].max(1e-12);
17790 }
17791 }
17792 }
17793
17794 for i in 0..len {
17795 let c = if chan_dim == 1 {
17796 0
17797 } else {
17798 (i / inner) % chan_dim
17799 };
17800 let s = scale[c];
17801 let qv = (xs[i] / s).round().clamp(-q_max, q_max);
17802 outs[i] = qv * s;
17803 }
17804 }
17805 }
17806}
17807
17808#[inline(always)]
17809fn exec_activation_backward(t: &Thunk, base: *mut u8) {
17810 let Thunk::ActivationBackward {
17811 x,
17812 dy,
17813 dx,
17814 len,
17815 kind,
17816 } = t
17817 else {
17818 unreachable!()
17819 };
17820 {
17821 let len = *len as usize;
17822 unsafe {
17823 let xs = sl(*x, base, len);
17824 let dys = sl(*dy, base, len);
17825 let out = sl_mut(*dx, base, len);
17826 activation_backward_kernel(*kind, xs, dys, out);
17827 }
17828 }
17829}
17830
17831#[inline(always)]
17832fn exec_activation_backward_f64(t: &Thunk, base: *mut u8) {
17833 let Thunk::ActivationBackwardF64 {
17834 x,
17835 dy,
17836 dx,
17837 len,
17838 kind,
17839 } = t
17840 else {
17841 unreachable!()
17842 };
17843 {
17844 let len = *len as usize;
17845 unsafe {
17846 let xs = sl_f64(*x, base, len);
17847 let dys = sl_f64(*dy, base, len);
17848 let out = sl_mut_f64(*dx, base, len);
17849 activation_backward_kernel_f64(*kind, xs, dys, out);
17850 }
17851 }
17852}
17853
17854#[inline(always)]
17855fn exec_fake_quantize_l_s_q(t: &Thunk, base: *mut u8) {
17856 let Thunk::FakeQuantizeLSQ {
17857 x,
17858 scale_off,
17859 out,
17860 len,
17861 chan_axis: _,
17862 chan_dim,
17863 inner,
17864 bits,
17865 } = t
17866 else {
17867 unreachable!()
17868 };
17869 {
17870 let len = *len as usize;
17871 let chan_dim = *chan_dim as usize;
17872 let inner = *inner as usize;
17873 let q_max: f32 = match *bits {
17874 8 => 127.0,
17875 4 => 7.0,
17876 2 => 1.0,
17877 n => panic!("FakeQuantizeLSQ: bad bits {n}"),
17878 };
17879 unsafe {
17880 let xs = sl(*x, base, len);
17881 let scale = sl(*scale_off, base, chan_dim);
17882 let outs = sl_mut(*out, base, len);
17883 for i in 0..len {
17884 let c = if chan_dim == 1 {
17885 0
17886 } else {
17887 (i / inner) % chan_dim
17888 };
17889 let s = scale[c].max(1e-12);
17890 let qv = (xs[i] / s).round().clamp(-q_max, q_max);
17891 outs[i] = qv * s;
17892 }
17893 }
17894 }
17895}
17896
17897#[inline(always)]
17898fn exec_fake_quantize_l_s_q_backward_x(t: &Thunk, base: *mut u8) {
17899 let Thunk::FakeQuantizeLSQBackwardX {
17900 x,
17901 scale_off,
17902 dy,
17903 dx,
17904 len,
17905 chan_axis: _,
17906 chan_dim,
17907 inner,
17908 bits,
17909 } = t
17910 else {
17911 unreachable!()
17912 };
17913 {
17914 let len = *len as usize;
17915 let chan_dim = *chan_dim as usize;
17916 let inner = *inner as usize;
17917 let q_max: f32 = match *bits {
17918 8 => 127.0,
17919 4 => 7.0,
17920 2 => 1.0,
17921 n => panic!("FakeQuantizeLSQBackwardX: bad bits {n}"),
17922 };
17923 unsafe {
17924 let xs = sl(*x, base, len);
17925 let scale = sl(*scale_off, base, chan_dim);
17926 let dys = sl(*dy, base, len);
17927 let outs = sl_mut(*dx, base, len);
17928 for i in 0..len {
17930 let c = if chan_dim == 1 {
17931 0
17932 } else {
17933 (i / inner) % chan_dim
17934 };
17935 let z = xs[i] / scale[c].max(1e-12);
17936 outs[i] = if z.abs() <= q_max { dys[i] } else { 0.0 };
17937 }
17938 }
17939 }
17940}
17941
17942#[inline(always)]
17943fn exec_fake_quantize_l_s_q_backward_scale(t: &Thunk, base: *mut u8) {
17944 let Thunk::FakeQuantizeLSQBackwardScale {
17945 x,
17946 scale_off,
17947 dy,
17948 dscale,
17949 len,
17950 chan_axis: _,
17951 chan_dim,
17952 inner,
17953 bits,
17954 } = t
17955 else {
17956 unreachable!()
17957 };
17958 {
17959 let len = *len as usize;
17960 let chan_dim = *chan_dim as usize;
17961 let inner = *inner as usize;
17962 let q_max: f32 = match *bits {
17963 8 => 127.0,
17964 4 => 7.0,
17965 2 => 1.0,
17966 n => panic!("FakeQuantizeLSQBackwardScale: bad bits {n}"),
17967 };
17968 unsafe {
17969 let xs = sl(*x, base, len);
17970 let scale = sl(*scale_off, base, chan_dim);
17971 let dys = sl(*dy, base, len);
17972 let outs = sl_mut(*dscale, base, chan_dim);
17973 for v in outs.iter_mut() {
17974 *v = 0.0;
17975 }
17976 for i in 0..len {
17979 let c = if chan_dim == 1 {
17980 0
17981 } else {
17982 (i / inner) % chan_dim
17983 };
17984 let s = scale[c].max(1e-12);
17985 let z = xs[i] / s;
17986 let psi = if z.abs() <= q_max {
17987 -z + z.round()
17988 } else if z > 0.0 {
17989 q_max
17990 } else {
17991 -q_max
17992 };
17993 outs[c] += psi * dys[i];
17994 }
17995 }
17996 }
17997}
17998
17999#[inline(always)]
18000fn exec_fake_quantize_backward(t: &Thunk, base: *mut u8) {
18001 let Thunk::FakeQuantizeBackward {
18002 x,
18003 dy,
18004 dx,
18005 len,
18006 chan_axis: _,
18007 chan_dim,
18008 inner,
18009 bits,
18010 ste,
18011 } = t
18012 else {
18013 unreachable!()
18014 };
18015 {
18016 use rlx_ir::op::SteKind;
18017 let len = *len as usize;
18018 let chan_dim = *chan_dim as usize;
18019 let inner = *inner as usize;
18020 let q_max: f32 = match *bits {
18021 8 => 127.0,
18022 4 => 7.0,
18023 2 => 1.0,
18024 n => panic!("FakeQuantizeBackward: bad bits {n}"),
18025 };
18026 unsafe {
18027 let xs = sl(*x, base, len);
18028 let dys = sl(*dy, base, len);
18029 let outs = sl_mut(*dx, base, len);
18030
18031 let mut max_abs = vec![0f32; chan_dim];
18033 for i in 0..len {
18034 let c = if chan_dim == 1 {
18035 0
18036 } else {
18037 (i / inner) % chan_dim
18038 };
18039 let a = xs[i].abs();
18040 if a > max_abs[c] {
18041 max_abs[c] = a;
18042 }
18043 }
18044 let mut scale = vec![0f32; chan_dim];
18045 for c in 0..chan_dim {
18046 scale[c] = (max_abs[c] / q_max).max(1e-12);
18047 }
18048
18049 match *ste {
18050 SteKind::Identity => {
18051 outs.copy_from_slice(dys);
18053 }
18054 SteKind::ClippedIdentity => {
18055 for i in 0..len {
18058 let c = if chan_dim == 1 {
18059 0
18060 } else {
18061 (i / inner) % chan_dim
18062 };
18063 let bound = q_max * scale[c];
18064 outs[i] = if xs[i].abs() <= bound { dys[i] } else { 0.0 };
18065 }
18066 }
18067 SteKind::Tanh => {
18068 for i in 0..len {
18070 let c = if chan_dim == 1 {
18071 0
18072 } else {
18073 (i / inner) % chan_dim
18074 };
18075 let t = (xs[i] / scale[c]).tanh();
18076 outs[i] = dys[i] * (1.0 - t * t);
18077 }
18078 }
18079 SteKind::HardTanh => {
18080 for i in 0..len {
18082 let c = if chan_dim == 1 {
18083 0
18084 } else {
18085 (i / inner) % chan_dim
18086 };
18087 let bound = q_max * scale[c];
18088 let attenuation = (1.0 - (xs[i] / bound).abs()).max(0.0);
18089 outs[i] = dys[i] * attenuation;
18090 }
18091 }
18092 }
18093 }
18094 }
18095}
18096
18097#[inline(always)]
18098fn exec_layer_norm_backward_input(t: &Thunk, base: *mut u8) {
18099 let Thunk::LayerNormBackwardInput {
18100 x,
18101 gamma,
18102 dy,
18103 dx,
18104 rows,
18105 h,
18106 eps,
18107 } = t
18108 else {
18109 unreachable!()
18110 };
18111 {
18112 let rows = *rows as usize;
18113 let h = *h as usize;
18114 let eps = *eps;
18115 unsafe {
18116 let xs = sl(*x, base, rows * h);
18117 let g = sl(*gamma, base, h);
18118 let dys = sl(*dy, base, rows * h);
18119 let out = sl_mut(*dx, base, rows * h);
18120 let n_inv = 1.0 / h as f32;
18121 for r in 0..rows {
18122 let xr = &xs[r * h..(r + 1) * h];
18123 let dyr = &dys[r * h..(r + 1) * h];
18124 let mut sum = 0f32;
18127 for &v in xr {
18128 sum += v;
18129 }
18130 let mean = sum * n_inv;
18131 let mut var = 0f32;
18132 for &v in xr {
18133 let d = v - mean;
18134 var += d * d;
18135 }
18136 let inv_std = 1.0 / (var * n_inv + eps).sqrt();
18137
18138 let mut s_sy = 0f32;
18141 let mut s_sxh = 0f32;
18142 for d in 0..h {
18143 let xh = (xr[d] - mean) * inv_std;
18144 let sy = dyr[d] * g[d];
18145 s_sy += sy;
18146 s_sxh += sy * xh;
18147 }
18148 let m_sy = s_sy * n_inv;
18149 let m_sxh = s_sxh * n_inv;
18150
18151 for d in 0..h {
18152 let xh = (xr[d] - mean) * inv_std;
18153 let sy = dyr[d] * g[d];
18154 out[r * h + d] = inv_std * (sy - m_sy - xh * m_sxh);
18155 }
18156 }
18157 }
18158 }
18159}
18160
18161#[inline(always)]
18162fn exec_batch_norm_inference_backward_input(t: &Thunk, base: *mut u8) {
18163 let Thunk::BatchNormInferenceBackwardInput {
18164 x,
18165 gamma,
18166 mean,
18167 var,
18168 dy,
18169 dx,
18170 count,
18171 channels,
18172 eps,
18173 } = t
18174 else {
18175 unreachable!()
18176 };
18177 {
18178 let count = *count as usize;
18179 let c = *channels as usize;
18180 let n = count * c;
18181 let eps = *eps;
18182 unsafe {
18183 crate::kernels::batch_norm_inference_backward_input(
18184 sl(*x, base, n),
18185 sl(*gamma, base, c),
18186 sl(*mean, base, c),
18187 sl(*var, base, c),
18188 sl(*dy, base, n),
18189 sl_mut(*dx, base, n),
18190 c,
18191 eps,
18192 );
18193 }
18194 }
18195}
18196
18197#[inline(always)]
18198fn exec_batch_norm_inference_backward_gamma(t: &Thunk, base: *mut u8) {
18199 let Thunk::BatchNormInferenceBackwardGamma {
18200 x,
18201 mean,
18202 var,
18203 dy,
18204 dgamma,
18205 count,
18206 channels,
18207 eps,
18208 } = t
18209 else {
18210 unreachable!()
18211 };
18212 {
18213 let count = *count as usize;
18214 let c = *channels as usize;
18215 let n = count * c;
18216 let eps = *eps;
18217 unsafe {
18218 crate::kernels::batch_norm_inference_backward_gamma(
18219 sl(*x, base, n),
18220 sl(*mean, base, c),
18221 sl(*var, base, c),
18222 sl(*dy, base, n),
18223 sl_mut(*dgamma, base, c),
18224 c,
18225 eps,
18226 );
18227 }
18228 }
18229}
18230
18231#[inline(always)]
18232fn exec_batch_norm_inference_backward_beta(t: &Thunk, base: *mut u8) {
18233 let Thunk::BatchNormInferenceBackwardBeta {
18234 dy,
18235 dbeta,
18236 count,
18237 channels,
18238 } = t
18239 else {
18240 unreachable!()
18241 };
18242 {
18243 let count = *count as usize;
18244 let c = *channels as usize;
18245 let n = count * c;
18246 unsafe {
18247 crate::kernels::batch_norm_inference_backward_beta(
18248 sl(*dy, base, n),
18249 sl_mut(*dbeta, base, c),
18250 c,
18251 );
18252 }
18253 }
18254}
18255
18256#[inline(always)]
18257fn exec_layer_norm_backward_gamma(t: &Thunk, base: *mut u8) {
18258 let Thunk::LayerNormBackwardGamma {
18259 x,
18260 dy,
18261 dgamma,
18262 rows,
18263 h,
18264 eps,
18265 } = t
18266 else {
18267 unreachable!()
18268 };
18269 {
18270 let rows = *rows as usize;
18271 let h = *h as usize;
18272 let eps = *eps;
18273 unsafe {
18274 let xs = sl(*x, base, rows * h);
18275 let dys = sl(*dy, base, rows * h);
18276 let out = sl_mut(*dgamma, base, h);
18277 for v in out.iter_mut() {
18278 *v = 0.0;
18279 }
18280 let n_inv = 1.0 / h as f32;
18281 for r in 0..rows {
18282 let xr = &xs[r * h..(r + 1) * h];
18283 let dyr = &dys[r * h..(r + 1) * h];
18284 let mut sum = 0f32;
18285 for &v in xr {
18286 sum += v;
18287 }
18288 let mean = sum * n_inv;
18289 let mut var = 0f32;
18290 for &v in xr {
18291 let d = v - mean;
18292 var += d * d;
18293 }
18294 let inv_std = 1.0 / (var * n_inv + eps).sqrt();
18295 for d in 0..h {
18296 let xh = (xr[d] - mean) * inv_std;
18297 out[d] += dyr[d] * xh;
18298 }
18299 }
18300 }
18301 }
18302}
18303
18304#[inline(always)]
18305fn exec_rms_norm_backward_input(t: &Thunk, base: *mut u8) {
18306 let Thunk::RmsNormBackwardInput {
18307 x,
18308 gamma,
18309 beta,
18310 dy,
18311 dx,
18312 rows,
18313 h,
18314 eps,
18315 } = t
18316 else {
18317 unreachable!()
18318 };
18319 {
18320 let (rows, h) = (*rows as usize, *h as usize);
18321 unsafe {
18322 let xs = sl(*x, base, rows * h);
18323 let g = sl(*gamma, base, h);
18324 let b = sl(*beta, base, h);
18325 let dys = sl(*dy, base, rows * h);
18326 let out = sl_mut(*dx, base, rows * h);
18327 let mut dg = vec![0f32; h];
18328 let mut db = vec![0f32; h];
18329 for r in 0..rows {
18330 crate::training_bwd::rms_norm_backward_row(
18331 &xs[r * h..(r + 1) * h],
18332 g,
18333 b,
18334 &dys[r * h..(r + 1) * h],
18335 &mut out[r * h..(r + 1) * h],
18336 &mut dg,
18337 &mut db,
18338 *eps,
18339 );
18340 }
18341 }
18342 }
18343}
18344
18345#[inline(always)]
18346fn exec_rms_norm_backward_gamma(t: &Thunk, base: *mut u8) {
18347 let Thunk::RmsNormBackwardGamma {
18348 x,
18349 gamma,
18350 beta,
18351 dy,
18352 dgamma,
18353 rows,
18354 h,
18355 eps,
18356 } = t
18357 else {
18358 unreachable!()
18359 };
18360 {
18361 let (rows, h) = (*rows as usize, *h as usize);
18362 unsafe {
18363 let xs = sl(*x, base, rows * h);
18364 let g = sl(*gamma, base, h);
18365 let b = sl(*beta, base, h);
18366 let dys = sl(*dy, base, rows * h);
18367 let out = sl_mut(*dgamma, base, h);
18368 for v in out.iter_mut() {
18369 *v = 0.0;
18370 }
18371 let mut dx = vec![0f32; h];
18372 let mut db = vec![0f32; h];
18373 for r in 0..rows {
18374 crate::training_bwd::rms_norm_backward_row(
18375 &xs[r * h..(r + 1) * h],
18376 g,
18377 b,
18378 &dys[r * h..(r + 1) * h],
18379 &mut dx,
18380 &mut *out,
18381 &mut db,
18382 *eps,
18383 );
18384 }
18385 }
18386 }
18387}
18388
18389#[inline(always)]
18390fn exec_rms_norm_backward_beta(t: &Thunk, base: *mut u8) {
18391 let Thunk::RmsNormBackwardBeta {
18392 x,
18393 gamma,
18394 beta,
18395 dy,
18396 dbeta,
18397 rows,
18398 h,
18399 eps,
18400 } = t
18401 else {
18402 unreachable!()
18403 };
18404 {
18405 let (rows, h) = (*rows as usize, *h as usize);
18406 unsafe {
18407 let xs = sl(*x, base, rows * h);
18408 let g = sl(*gamma, base, h);
18409 let b = sl(*beta, base, h);
18410 let dys = sl(*dy, base, rows * h);
18411 let out = sl_mut(*dbeta, base, h);
18412 for v in out.iter_mut() {
18413 *v = 0.0;
18414 }
18415 let mut dx = vec![0f32; h];
18416 let mut dg = vec![0f32; h];
18417 for r in 0..rows {
18418 crate::training_bwd::rms_norm_backward_row(
18419 &xs[r * h..(r + 1) * h],
18420 g,
18421 b,
18422 &dys[r * h..(r + 1) * h],
18423 &mut dx,
18424 &mut dg,
18425 &mut *out,
18426 *eps,
18427 );
18428 }
18429 }
18430 }
18431}
18432
18433#[inline(always)]
18434fn exec_rope_backward(t: &Thunk, base: *mut u8) {
18435 let Thunk::RopeBackward {
18436 dy,
18437 cos,
18438 sin,
18439 dx,
18440 batch,
18441 seq,
18442 hidden,
18443 head_dim,
18444 n_rot,
18445 cos_len,
18446 } = t
18447 else {
18448 unreachable!()
18449 };
18450 {
18451 let (b, s, hs, dh, nr, cl) = (
18452 *batch as usize,
18453 *seq as usize,
18454 *hidden as usize,
18455 *head_dim as usize,
18456 *n_rot as usize,
18457 *cos_len as usize,
18458 );
18459 let nh = hs / dh;
18460 let tab_half = dh / 2;
18461 unsafe {
18462 let dys = sl(*dy, base, b * s * hs);
18463 let cos_tab = sl(*cos, base, cl);
18464 let sin_tab = sl(*sin, base, cl);
18465 let out = sl_mut(*dx, base, b * s * hs);
18466 for bi in 0..b {
18467 for si in 0..s {
18468 let tab_off = si.saturating_mul(tab_half) % cl.max(1);
18469 let cp = &cos_tab[tab_off..tab_off + tab_half.min(cl)];
18470 let sp = &sin_tab[tab_off..tab_off + tab_half.min(cl)];
18471 for hi in 0..nh {
18472 let base_idx = bi * s * hs + si * hs + hi * dh;
18473 crate::training_bwd::rope_backward_row(
18474 &dys[base_idx..base_idx + dh],
18475 cp,
18476 sp,
18477 &mut out[base_idx..base_idx + dh],
18478 dh,
18479 nr,
18480 );
18481 }
18482 }
18483 }
18484 }
18485 }
18486}
18487
18488#[inline(always)]
18489fn exec_cumsum_backward(t: &Thunk, base: *mut u8) {
18490 let Thunk::CumsumBackward {
18491 dy,
18492 dx,
18493 rows,
18494 cols,
18495 exclusive,
18496 } = t
18497 else {
18498 unreachable!()
18499 };
18500 {
18501 let (rows, cols) = (*rows as usize, *cols as usize);
18502 unsafe {
18503 let dys = sl(*dy, base, rows * cols);
18504 let out = sl_mut(*dx, base, rows * cols);
18505 for r in 0..rows {
18506 crate::training_bwd::cumsum_backward_row(
18507 &dys[r * cols..(r + 1) * cols],
18508 &mut out[r * cols..(r + 1) * cols],
18509 *exclusive,
18510 );
18511 }
18512 }
18513 }
18514}
18515
18516#[inline(always)]
18517fn exec_group_norm_backward_input(t: &Thunk, base: *mut u8) {
18518 let Thunk::GroupNormBackwardInput {
18519 x,
18520 gamma,
18521 beta: _beta,
18522 dy,
18523 dx,
18524 n,
18525 c,
18526 h,
18527 w,
18528 num_groups,
18529 eps,
18530 } = t
18531 else {
18532 unreachable!()
18533 };
18534 {
18535 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
18536 let plane = c * h * w;
18537 unsafe {
18538 let xs = sl(*x, base, n * plane);
18539 let g = sl(*gamma, base, c);
18540 let dys = sl(*dy, base, n * plane);
18541 let out = sl_mut(*dx, base, n * plane);
18542 crate::training_bwd::group_norm_backward_input_nchw(
18543 xs,
18544 g,
18545 dys,
18546 out,
18547 n,
18548 c,
18549 h,
18550 w,
18551 *num_groups as usize,
18552 *eps,
18553 );
18554 }
18555 }
18556}
18557
18558#[inline(always)]
18559fn exec_group_norm_backward_gamma(t: &Thunk, base: *mut u8) {
18560 let Thunk::GroupNormBackwardGamma {
18561 x,
18562 dy,
18563 dgamma,
18564 n,
18565 c,
18566 h,
18567 w,
18568 num_groups,
18569 eps,
18570 } = t
18571 else {
18572 unreachable!()
18573 };
18574 {
18575 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
18576 let plane = c * h * w;
18577 unsafe {
18578 let xs = sl(*x, base, n * plane);
18579 let dys = sl(*dy, base, n * plane);
18580 let out = sl_mut(*dgamma, base, c);
18581 crate::training_bwd::group_norm_backward_gamma_nchw(
18582 xs,
18583 dys,
18584 out,
18585 n,
18586 c,
18587 h,
18588 w,
18589 *num_groups as usize,
18590 *eps,
18591 );
18592 }
18593 }
18594}
18595
18596#[inline(always)]
18597fn exec_group_norm_backward_beta(t: &Thunk, base: *mut u8) {
18598 let Thunk::GroupNormBackwardBeta {
18599 dy,
18600 dbeta,
18601 n,
18602 c,
18603 h,
18604 w,
18605 } = t
18606 else {
18607 unreachable!()
18608 };
18609 {
18610 let (n, c, h, w) = (*n as usize, *c as usize, *h as usize, *w as usize);
18611 let plane = c * h * w;
18612 unsafe {
18613 let dys = sl(*dy, base, n * plane);
18614 let out = sl_mut(*dbeta, base, c);
18615 crate::training_bwd::group_norm_backward_beta_nchw(dys, out, n, c, h, w);
18616 }
18617 }
18618}
18619
18620#[inline(always)]
18621fn exec_gather_backward(t: &Thunk, base: *mut u8) {
18622 let Thunk::GatherBackward {
18623 dy,
18624 indices,
18625 dst,
18626 outer,
18627 axis_dim,
18628 num_idx,
18629 trailing,
18630 } = t
18631 else {
18632 unreachable!()
18633 };
18634 {
18635 let (outer, axis_dim, num_idx, trailing) = (
18636 *outer as usize,
18637 *axis_dim as usize,
18638 *num_idx as usize,
18639 *trailing as usize,
18640 );
18641 unsafe {
18642 let dys = sl(*dy, base, outer * num_idx * trailing);
18643 let ids = sl(*indices, base, num_idx);
18644 let out = sl_mut(*dst, base, outer * axis_dim * trailing);
18645 for v in out.iter_mut() {
18646 *v = 0.0;
18647 }
18648 crate::training_bwd::gather_axis_backward(
18649 dys, ids, out, outer, axis_dim, num_idx, trailing,
18650 );
18651 }
18652 }
18653}
18654
18655#[inline(always)]
18656fn exec_max_pool2d_backward(t: &Thunk, base: *mut u8) {
18657 let Thunk::MaxPool2dBackward {
18658 x,
18659 dy,
18660 dx,
18661 n,
18662 c,
18663 h,
18664 w,
18665 h_out,
18666 w_out,
18667 kh,
18668 kw,
18669 sh,
18670 sw,
18671 ph,
18672 pw,
18673 } = t
18674 else {
18675 unreachable!()
18676 };
18677 unsafe {
18678 execute_maxpool2d_backward_f32(
18679 *x, *dy, *dx, *n, *c, *h, *w, *h_out, *w_out, *kh, *kw, *sh, *sw, *ph, *pw, base,
18680 );
18681 }
18682}
18683
18684#[inline(always)]
18685fn exec_conv2d_backward_input(t: &Thunk, base: *mut u8) {
18686 let Thunk::Conv2dBackwardInput {
18687 dy,
18688 w,
18689 dx,
18690 n,
18691 c_in,
18692 h,
18693 w_in,
18694 c_out,
18695 h_out,
18696 w_out,
18697 kh,
18698 kw,
18699 sh,
18700 sw,
18701 ph,
18702 pw,
18703 dh,
18704 dw,
18705 groups,
18706 } = t
18707 else {
18708 unreachable!()
18709 };
18710 {
18711 let n = *n as usize;
18723 let c_in = *c_in as usize;
18724 let h = *h as usize;
18725 let w_in = *w_in as usize;
18726 let c_out = *c_out as usize;
18727 let h_out = *h_out as usize;
18728 let w_out = *w_out as usize;
18729 let kh = *kh as usize;
18730 let kw = *kw as usize;
18731 let sh = *sh as usize;
18732 let sw = *sw as usize;
18733 let ph = *ph as usize;
18734 let pw = *pw as usize;
18735 let dh = *dh as usize;
18736 let dw = *dw as usize;
18737 let groups = *groups as usize;
18738 let c_in_per_g = c_in / groups;
18739 let c_out_per_g = c_out / groups;
18740
18741 let m_dim = c_in_per_g * kh * kw;
18742 let n_dim = h_out * w_out;
18743 let k_dim = c_out_per_g;
18744
18745 let dy_stride_n = c_out * h_out * w_out;
18746 let dy_stride_g = c_out_per_g * h_out * w_out;
18747 let w_stride_g = c_out_per_g * c_in_per_g * kh * kw;
18748 let dx_stride_n = c_in * h * w_in;
18749 let dx_stride_g = c_in_per_g * h * w_in;
18750
18751 unsafe {
18752 let dys = sl(*dy, base, n * c_out * h_out * w_out);
18753 let ws = sl(*w, base, c_out * c_in_per_g * kh * kw);
18754 let dxs = sl_mut(*dx, base, n * c_in * h * w_in);
18755 for v in dxs.iter_mut() {
18756 *v = 0.0;
18757 }
18758
18759 if fast_conv_enabled() {
18765 let dx_addr = dxs.as_mut_ptr() as usize;
18766 crate::pool::par_for(n, 1, &|off, cnt| {
18767 let mut dcol = vec![0f32; m_dim * n_dim];
18768 for ni in off..off + cnt {
18769 for g in 0..groups {
18770 let w_g_off = g * w_stride_g;
18771 let dy_n_g_off = ni * dy_stride_n + g * dy_stride_g;
18772 let dx_n_g_off = ni * dx_stride_n + g * dx_stride_g;
18773 crate::blas::sgemm_general(
18774 ws.as_ptr().add(w_g_off),
18775 dys.as_ptr().add(dy_n_g_off),
18776 dcol.as_mut_ptr(),
18777 m_dim,
18778 n_dim,
18779 k_dim,
18780 1.0,
18781 0.0,
18782 m_dim,
18783 n_dim,
18784 n_dim,
18785 true,
18786 false,
18787 );
18788 let dx_g = std::slice::from_raw_parts_mut(
18789 (dx_addr as *mut f32).add(dx_n_g_off),
18790 dx_stride_g,
18791 );
18792 col2im(
18793 &dcol, dx_g, c_in_per_g, h, w_in, h_out, w_out, kh, kw, sh, sw, ph,
18794 pw, dh, dw,
18795 );
18796 }
18797 }
18798 });
18799 } else {
18800 let mut dcol = vec![0f32; m_dim * n_dim];
18802 for ni in 0..n {
18803 for g in 0..groups {
18804 let w_g_off = g * w_stride_g;
18805 let dy_n_g_off = ni * dy_stride_n + g * dy_stride_g;
18806 let dx_n_g_off = ni * dx_stride_n + g * dx_stride_g;
18807
18808 crate::blas::sgemm_general(
18813 ws.as_ptr().add(w_g_off),
18814 dys.as_ptr().add(dy_n_g_off),
18815 dcol.as_mut_ptr(),
18816 m_dim,
18817 n_dim,
18818 k_dim,
18819 1.0,
18820 0.0,
18821 m_dim,
18822 n_dim,
18823 n_dim,
18824 true,
18825 false,
18826 );
18827
18828 col2im(
18830 &dcol,
18831 &mut dxs[dx_n_g_off..dx_n_g_off + dx_stride_g],
18832 c_in_per_g,
18833 h,
18834 w_in,
18835 h_out,
18836 w_out,
18837 kh,
18838 kw,
18839 sh,
18840 sw,
18841 ph,
18842 pw,
18843 dh,
18844 dw,
18845 );
18846 }
18847 }
18848 }
18849 }
18850 }
18851}
18852
18853#[inline(always)]
18854fn exec_conv2d_backward_weight(t: &Thunk, base: *mut u8) {
18855 let Thunk::Conv2dBackwardWeight {
18856 x,
18857 dy,
18858 dw,
18859 n,
18860 c_in,
18861 h,
18862 w,
18863 c_out,
18864 h_out,
18865 w_out,
18866 kh,
18867 kw,
18868 sh,
18869 sw,
18870 ph,
18871 pw,
18872 dh,
18873 dw_dil,
18874 groups,
18875 } = t
18876 else {
18877 unreachable!()
18878 };
18879 {
18880 let n = *n as usize;
18881 let c_in = *c_in as usize;
18882 let h = *h as usize;
18883 let w = *w as usize;
18884 let c_out = *c_out as usize;
18895 let h_out = *h_out as usize;
18896 let w_out = *w_out as usize;
18897 let kh = *kh as usize;
18898 let kw = *kw as usize;
18899 let sh = *sh as usize;
18900 let sw = *sw as usize;
18901 let ph = *ph as usize;
18902 let pw = *pw as usize;
18903 let dh = *dh as usize;
18904 let dw_dil = *dw_dil as usize;
18905 let groups = *groups as usize;
18906 let c_in_per_g = c_in / groups;
18907 let c_out_per_g = c_out / groups;
18908
18909 let m_dim = c_out_per_g;
18910 let n_dim = c_in_per_g * kh * kw;
18911 let k_dim = h_out * w_out;
18912
18913 let x_stride_n = c_in * h * w;
18914 let x_stride_g = c_in_per_g * h * w;
18915 let dy_stride_n = c_out * h_out * w_out;
18916 let dy_stride_g = c_out_per_g * h_out * w_out;
18917 let dw_stride_g = c_out_per_g * c_in_per_g * kh * kw;
18918
18919 unsafe {
18920 let xs = sl(*x, base, n * c_in * h * w);
18921 let dys = sl(*dy, base, n * c_out * h_out * w_out);
18922 let dws = sl_mut(*dw, base, c_out * c_in_per_g * kh * kw);
18923 for v in dws.iter_mut() {
18924 *v = 0.0;
18925 }
18926
18927 if fast_conv_enabled() {
18933 let dw_len = dws.len();
18934 let dws_addr = dws.as_mut_ptr() as usize;
18935 let lock = std::sync::Mutex::new(());
18936 crate::pool::par_for(n, 1, &|off, cnt| {
18937 let mut col = vec![0f32; n_dim * k_dim];
18938 let mut local = vec![0f32; dw_len];
18939 for ni in off..off + cnt {
18940 for g in 0..groups {
18941 let x_n_g_off = ni * x_stride_n + g * x_stride_g;
18942 let dy_n_g_off = ni * dy_stride_n + g * dy_stride_g;
18943 let dw_g_off = g * dw_stride_g;
18944 crate::im2col::im2col_rows_layout(
18949 &xs[x_n_g_off..x_n_g_off + x_stride_g],
18950 &mut col,
18951 1,
18952 c_in_per_g,
18953 h,
18954 w,
18955 h_out,
18956 w_out,
18957 kh,
18958 kw,
18959 sh,
18960 sw,
18961 ph,
18962 pw,
18963 dh,
18964 dw_dil,
18965 );
18966 crate::blas::sgemm_general(
18967 dys.as_ptr().add(dy_n_g_off),
18968 col.as_ptr(),
18969 local.as_mut_ptr().add(dw_g_off),
18970 m_dim,
18971 n_dim,
18972 k_dim,
18973 1.0,
18974 1.0,
18975 k_dim,
18976 n_dim,
18977 n_dim,
18978 false,
18979 false,
18980 );
18981 }
18982 }
18983 let _guard = lock.lock().unwrap();
18984 let dws = std::slice::from_raw_parts_mut(dws_addr as *mut f32, dw_len);
18985 for (d, l) in dws.iter_mut().zip(local.iter()) {
18986 *d += *l;
18987 }
18988 });
18989 } else {
18990 let mut col = vec![0f32; n_dim * k_dim];
18991 for ni in 0..n {
18992 for g in 0..groups {
18993 let x_n_g_off = ni * x_stride_n + g * x_stride_g;
18994 im2col(
18995 &xs[x_n_g_off..x_n_g_off + x_stride_g],
18996 &mut col,
18997 c_in_per_g,
18998 h,
18999 w,
19000 h_out,
19001 w_out,
19002 kh,
19003 kw,
19004 sh,
19005 sw,
19006 ph,
19007 pw,
19008 dh,
19009 dw_dil,
19010 );
19011
19012 let dy_n_g_off = ni * dy_stride_n + g * dy_stride_g;
19013 let dw_g_off = g * dw_stride_g;
19014
19015 crate::blas::sgemm_general(
19023 dys.as_ptr().add(dy_n_g_off),
19024 col.as_ptr(),
19025 dws.as_mut_ptr().add(dw_g_off),
19026 m_dim,
19027 n_dim,
19028 k_dim,
19029 1.0,
19030 1.0,
19031 k_dim,
19032 k_dim,
19033 n_dim,
19034 false,
19035 true,
19036 );
19037 }
19038 }
19039 }
19040 }
19041 }
19042}
19043
19044#[inline(always)]
19045fn exec_im2_col(t: &Thunk, base: *mut u8) {
19046 let Thunk::Im2Col {
19047 x,
19048 col,
19049 n,
19050 c_in,
19051 h,
19052 w,
19053 h_out,
19054 w_out,
19055 kh,
19056 kw,
19057 sh,
19058 sw,
19059 ph,
19060 pw,
19061 dh,
19062 dw_dil,
19063 } = t
19064 else {
19065 unreachable!()
19066 };
19067 {
19068 let c_in = *c_in as usize;
19069 let h = *h as usize;
19070 let w = *w as usize;
19071 let h_out = *h_out as usize;
19072 let w_out = *w_out as usize;
19073 let kh = *kh as usize;
19074 let kw = *kw as usize;
19075 let sh = *sh as usize;
19076 let sw = *sw as usize;
19077 let ph = *ph as usize;
19078 let pw = *pw as usize;
19079 let dh = *dh as usize;
19080 let dw_dil = *dw_dil as usize;
19081 let per_batch = c_in * h * w;
19082 unsafe {
19083 let n_eff = if *n == 0 { 0usize } else { *n as usize };
19084 let x_floats = if n_eff == 0 {
19085 per_batch.max(1)
19086 } else {
19087 n_eff * per_batch
19088 };
19089 let xs = sl(*x, base, x_floats);
19090 let n = if *n == 0 {
19091 xs.len() / per_batch.max(1)
19092 } else {
19093 n_eff
19094 };
19095 let m = n * h_out * w_out;
19096 let k = c_in * kh * kw;
19097 let cols = sl_mut(*col, base, m * k);
19098 crate::im2col::im2col_rows_layout(
19099 xs, cols, n, c_in, h, w, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw_dil,
19100 );
19101 }
19102 }
19103}
19104
19105#[inline(always)]
19106fn exec_softmax_cross_entropy_dense(t: &Thunk, base: *mut u8) {
19107 let Thunk::SoftmaxCrossEntropyDense {
19108 logits,
19109 targets,
19110 dst,
19111 n,
19112 c,
19113 } = t
19114 else {
19115 unreachable!()
19116 };
19117 {
19118 let n = *n as usize;
19119 let c = *c as usize;
19120 unsafe {
19121 let lg = sl(*logits, base, n * c);
19122 let tg = sl(*targets, base, n * c);
19123 let out = sl_mut(*dst, base, n);
19124 for ni in 0..n {
19125 let row = &lg[ni * c..(ni + 1) * c];
19126 let trow = &tg[ni * c..(ni + 1) * c];
19127 let mut m = f32::NEG_INFINITY;
19129 for &v in row {
19130 if v > m {
19131 m = v;
19132 }
19133 }
19134 let mut sum = 0f32;
19135 for &v in row {
19136 sum += (v - m).exp();
19137 }
19138 let lse = m + sum.ln();
19139 let mut dot = 0f32;
19141 for k in 0..c {
19142 dot += trow[k] * row[k];
19143 }
19144 out[ni] = lse - dot;
19145 }
19146 }
19147 }
19148}
19149
19150#[inline(always)]
19151fn exec_softmax_cross_entropy(t: &Thunk, base: *mut u8) {
19152 let Thunk::SoftmaxCrossEntropy {
19153 logits,
19154 labels,
19155 dst,
19156 n,
19157 c,
19158 } = t
19159 else {
19160 unreachable!()
19161 };
19162 {
19163 let n = *n as usize;
19164 let c = *c as usize;
19165 unsafe {
19166 let lg = sl(*logits, base, n * c);
19167 let lb = sl(*labels, base, n);
19168 let out = sl_mut(*dst, base, n);
19169 for ni in 0..n {
19170 let row = &lg[ni * c..(ni + 1) * c];
19171 let mut m = f32::NEG_INFINITY;
19173 for &v in row {
19174 if v > m {
19175 m = v;
19176 }
19177 }
19178 let mut sum = 0f32;
19179 for &v in row {
19180 sum += (v - m).exp();
19181 }
19182 let lse = m + sum.ln();
19183 let label_idx = lb[ni] as usize;
19184 out[ni] = lse - row[label_idx];
19186 }
19187 }
19188 }
19189}
19190
19191#[inline(always)]
19192fn exec_softmax_cross_entropy_backward(t: &Thunk, base: *mut u8) {
19193 let Thunk::SoftmaxCrossEntropyBackward {
19194 logits,
19195 labels,
19196 d_loss,
19197 dlogits,
19198 n,
19199 c,
19200 } = t
19201 else {
19202 unreachable!()
19203 };
19204 {
19205 let n = *n as usize;
19206 let c = *c as usize;
19207 unsafe {
19208 let lg = sl(*logits, base, n * c);
19209 let lb = sl(*labels, base, n);
19210 let dl = sl(*d_loss, base, n);
19211 let out = sl_mut(*dlogits, base, n * c);
19212 for ni in 0..n {
19213 let row = &lg[ni * c..(ni + 1) * c];
19214 let label_idx = lb[ni] as usize;
19215 let scale = dl[ni];
19216 let mut m = f32::NEG_INFINITY;
19217 for &v in row {
19218 if v > m {
19219 m = v;
19220 }
19221 }
19222 let mut sum = 0f32;
19223 for &v in row {
19224 sum += (v - m).exp();
19225 }
19226 let inv_sum = 1.0 / sum;
19227 let dst_row = &mut out[ni * c..(ni + 1) * c];
19228 for k in 0..c {
19229 let p = (row[k] - m).exp() * inv_sum;
19230 let one_hot = if k == label_idx { 1.0 } else { 0.0 };
19231 dst_row[k] = (p - one_hot) * scale;
19232 }
19233 }
19234 }
19235 }
19236}
19237
19238#[inline(always)]
19239fn exec_gather_axis(t: &Thunk, base: *mut u8) {
19240 let Thunk::GatherAxis {
19241 table,
19242 idx,
19243 dst,
19244 outer,
19245 axis_dim,
19246 num_idx,
19247 trailing,
19248 idx_i64,
19249 table_bytes,
19250 } = t
19251 else {
19252 unreachable!()
19253 };
19254 {
19255 let outer = *outer as usize;
19256 let axis_dim = *axis_dim as usize;
19257 let num_idx = *num_idx as usize;
19258 let trailing = *trailing as usize;
19259 unsafe {
19260 if *table_bytes == 8 {
19261 let tab = sl_i64(*table, base, outer * axis_dim * trailing);
19262 let out = sl_mut_i64(*dst, base, outer * num_idx * trailing);
19263 for o in 0..outer {
19264 let tab_outer = o * axis_dim * trailing;
19265 let out_outer = o * num_idx * trailing;
19266 if *idx_i64 != 0 {
19267 let ids = sl_i64(*idx, base, num_idx);
19268 for k in 0..num_idx {
19269 let row = ids[k].max(0) as usize;
19270 if row < axis_dim {
19271 let tab_row = tab_outer + row * trailing;
19272 let out_row = out_outer + k * trailing;
19273 out[out_row..out_row + trailing]
19274 .copy_from_slice(&tab[tab_row..tab_row + trailing]);
19275 }
19276 }
19277 } else {
19278 let ids = sl(*idx, base, num_idx);
19279 for k in 0..num_idx {
19280 let row = ids[k] as usize;
19281 if row < axis_dim {
19282 let tab_row = tab_outer + row * trailing;
19283 let out_row = out_outer + k * trailing;
19284 out[out_row..out_row + trailing]
19285 .copy_from_slice(&tab[tab_row..tab_row + trailing]);
19286 }
19287 }
19288 }
19289 }
19290 } else {
19291 let tab = sl(*table, base, outer * axis_dim * trailing);
19292 let out = sl_mut(*dst, base, outer * num_idx * trailing);
19293 for o in 0..outer {
19294 let tab_outer = o * axis_dim * trailing;
19295 let out_outer = o * num_idx * trailing;
19296 if *idx_i64 != 0 {
19297 let ids = sl_i64(*idx, base, num_idx);
19298 for k in 0..num_idx {
19299 let row = ids[k].max(0) as usize;
19300 if row < axis_dim {
19301 let tab_row = tab_outer + row * trailing;
19302 let out_row = out_outer + k * trailing;
19303 out[out_row..out_row + trailing]
19304 .copy_from_slice(&tab[tab_row..tab_row + trailing]);
19305 }
19306 }
19307 } else {
19308 let ids = sl(*idx, base, num_idx);
19309 for k in 0..num_idx {
19310 let row = ids[k] as usize;
19311 if row < axis_dim {
19312 let tab_row = tab_outer + row * trailing;
19313 let out_row = out_outer + k * trailing;
19314 out[out_row..out_row + trailing]
19315 .copy_from_slice(&tab[tab_row..tab_row + trailing]);
19316 }
19317 }
19318 }
19319 }
19320 }
19321 }
19322 }
19323}
19324
19325#[inline(always)]
19331fn exec_custom_op(t: &Thunk, base: *mut u8) {
19332 let Thunk::CustomOp {
19333 kernel,
19334 inputs,
19335 output,
19336 attrs,
19337 } = t
19338 else {
19339 unreachable!()
19340 };
19341 {
19342 let (out_off, out_len, out_shape) = output;
19343 unsafe {
19344 dispatch_custom_op(
19345 &**kernel, inputs, *out_off, *out_len, out_shape, attrs, base,
19346 );
19347 }
19348 }
19349}
19350
19351#[inline(always)]
19352fn exec_reverse(t: &Thunk, base: *mut u8) {
19353 let Thunk::Reverse {
19354 src,
19355 dst,
19356 dims,
19357 rev_mask,
19358 elem_bytes,
19359 } = t
19360 else {
19361 unreachable!()
19362 };
19363 {
19364 let eb = *elem_bytes as usize;
19365 let rank = dims.len();
19366 let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
19367 let mut strides = vec![1usize; rank];
19368 for i in (0..rank.saturating_sub(1)).rev() {
19369 strides[i] = strides[i + 1] * dims[i + 1] as usize;
19370 }
19371 unsafe {
19372 let src_base = base.add(*src);
19373 let dst_base = base.add(*dst);
19374 for o in 0..total {
19375 let mut rem = o;
19376 let mut in_flat = 0usize;
19377 for ax in 0..rank {
19378 let idx = rem / strides[ax];
19379 rem %= strides[ax];
19380 let in_idx = if rev_mask[ax] {
19381 dims[ax] as usize - 1 - idx
19382 } else {
19383 idx
19384 };
19385 in_flat += in_idx * strides[ax];
19386 }
19387 std::ptr::copy_nonoverlapping(src_base.add(in_flat * eb), dst_base.add(o * eb), eb);
19388 }
19389 }
19390 }
19391}
19392
19393#[allow(clippy::too_many_arguments)]
19408unsafe fn griewank_process_segment(
19409 t_lo: usize,
19410 t_hi: usize,
19411 anchor_carry: &[u8],
19412 cb: usize,
19413 fwd_sched: &ThunkSchedule,
19414 fwd_init: &[u8],
19415 fwd_carry_in_off: usize,
19416 fwd_output_off: usize,
19417 fwd_x_offs: &[usize],
19418 base: *mut u8,
19419 outer_xs_offs: &[(usize, u32)],
19420 fwd_buf: &mut Vec<u8>,
19421 leaf_threshold: usize,
19422 process_iter: &mut dyn FnMut(usize, &[u8]),
19423) {
19424 unsafe {
19425 let size = t_hi - t_lo + 1;
19426 if size == 1 {
19427 process_iter(t_lo, anchor_carry);
19428 return;
19429 }
19430 if size <= leaf_threshold {
19431 let mut cache: Vec<u8> = Vec::with_capacity(size * cb);
19433 cache.extend_from_slice(anchor_carry);
19434 fwd_buf.copy_from_slice(fwd_init);
19435 std::ptr::copy_nonoverlapping(
19436 anchor_carry.as_ptr(),
19437 fwd_buf.as_mut_ptr().add(fwd_carry_in_off),
19438 cb,
19439 );
19440 for i in 1..size {
19441 let cur_iter = t_lo + i - 1;
19442 for (idx, fb_x_off) in fwd_x_offs.iter().enumerate() {
19443 let (outer_xs_off, x_psb) = outer_xs_offs[idx];
19444 let xb = x_psb as usize;
19445 std::ptr::copy_nonoverlapping(
19446 base.add(outer_xs_off + cur_iter * xb),
19447 fwd_buf.as_mut_ptr().add(*fb_x_off),
19448 xb,
19449 );
19450 }
19451 execute_thunks(fwd_sched, fwd_buf);
19452 if fwd_output_off != fwd_carry_in_off {
19453 fwd_buf.copy_within(fwd_output_off..fwd_output_off + cb, fwd_carry_in_off);
19454 }
19455 cache.extend_from_slice(&fwd_buf[fwd_carry_in_off..fwd_carry_in_off + cb]);
19456 }
19457 for t in (t_lo..=t_hi).rev() {
19459 let idx = t - t_lo;
19460 let carry = &cache[idx * cb..(idx + 1) * cb];
19461 process_iter(t, carry);
19462 }
19463 return;
19464 }
19465
19466 let mid = t_lo + size / 2;
19470 fwd_buf.copy_from_slice(fwd_init);
19471 std::ptr::copy_nonoverlapping(
19472 anchor_carry.as_ptr(),
19473 fwd_buf.as_mut_ptr().add(fwd_carry_in_off),
19474 cb,
19475 );
19476 for cur_iter in t_lo..mid {
19477 for (idx, fb_x_off) in fwd_x_offs.iter().enumerate() {
19478 let (outer_xs_off, x_psb) = outer_xs_offs[idx];
19479 let xb = x_psb as usize;
19480 std::ptr::copy_nonoverlapping(
19481 base.add(outer_xs_off + cur_iter * xb),
19482 fwd_buf.as_mut_ptr().add(*fb_x_off),
19483 xb,
19484 );
19485 }
19486 execute_thunks(fwd_sched, fwd_buf);
19487 if fwd_output_off != fwd_carry_in_off {
19488 fwd_buf.copy_within(fwd_output_off..fwd_output_off + cb, fwd_carry_in_off);
19489 }
19490 }
19491 let mid_carry: Vec<u8> = fwd_buf[fwd_carry_in_off..fwd_carry_in_off + cb].to_vec();
19492
19493 griewank_process_segment(
19497 mid,
19498 t_hi,
19499 &mid_carry,
19500 cb,
19501 fwd_sched,
19502 fwd_init,
19503 fwd_carry_in_off,
19504 fwd_output_off,
19505 fwd_x_offs,
19506 base,
19507 outer_xs_offs,
19508 fwd_buf,
19509 leaf_threshold,
19510 process_iter,
19511 );
19512 griewank_process_segment(
19514 t_lo,
19515 mid - 1,
19516 anchor_carry,
19517 cb,
19518 fwd_sched,
19519 fwd_init,
19520 fwd_carry_in_off,
19521 fwd_output_off,
19522 fwd_x_offs,
19523 base,
19524 outer_xs_offs,
19525 fwd_buf,
19526 leaf_threshold,
19527 process_iter,
19528 );
19529 }
19530}
19531
19532pub unsafe fn execute_fft1d_f64(
19549 src: usize,
19550 dst: usize,
19551 outer: usize,
19552 n_complex: usize,
19553 inverse: bool,
19554 norm_tag: u32,
19555 base: *mut u8,
19556) {
19557 let row_elems = 2 * n_complex;
19558 let norm = rlx_ir::fft::FftNorm::from_tag(norm_tag);
19559 let scale = norm.output_scale(n_complex, inverse);
19560 let is_pow2 = n_complex.is_power_of_two();
19561 let use_naive = !is_pow2 && n_complex <= 16;
19562 let scratch_template = if is_pow2 || use_naive {
19563 BluesteinScratchF64::empty()
19564 } else {
19565 BluesteinScratchF64::build(n_complex, inverse)
19566 };
19567
19568 let run_row = |bp: *mut u8,
19569 re: &mut [f64],
19570 im: &mut [f64],
19571 scratch: &mut BluesteinScratchF64,
19572 o: usize| {
19573 let row_offset = src + o * row_elems * std::mem::size_of::<f64>();
19574 let s = unsafe { sl_f64(row_offset, bp, row_elems) };
19575 re.copy_from_slice(&s[..n_complex]);
19576 im.copy_from_slice(&s[n_complex..]);
19577 if is_pow2 {
19578 fft_radix2_inplace_f64(re, im, inverse);
19579 } else if use_naive {
19580 fft_naive_inplace_f64(re, im, inverse);
19581 } else {
19582 fft_bluestein_inplace_f64(re, im, inverse, scratch);
19583 }
19584 if scale != 1.0 {
19585 re.iter_mut().for_each(|v| *v *= scale);
19586 im.iter_mut().for_each(|v| *v *= scale);
19587 }
19588 let dst_offset = dst + o * row_elems * std::mem::size_of::<f64>();
19589 let d = unsafe { sl_mut_f64(dst_offset, bp, row_elems) };
19590 d[..n_complex].copy_from_slice(re);
19591 d[n_complex..].copy_from_slice(im);
19592 };
19593
19594 let parallel = outer >= 8
19595 && (outer as u64) * (n_complex as u64) >= (1 << 15)
19596 && cpu_fft_parallel_enabled();
19597 if parallel {
19598 use rayon::prelude::*;
19599 let p = FftArenaPtr(base);
19600 (0..outer).into_par_iter().for_each_init(
19601 || {
19602 (
19603 vec![0f64; n_complex],
19604 vec![0f64; n_complex],
19605 scratch_template.clone(),
19606 )
19607 },
19608 move |(re, im, scratch), o| run_row(p.ptr(), re, im, scratch, o),
19609 );
19610 } else {
19611 let mut re = vec![0f64; n_complex];
19612 let mut im = vec![0f64; n_complex];
19613 let mut scratch = scratch_template;
19614 for o in 0..outer {
19615 run_row(base, &mut re, &mut im, &mut scratch, o);
19616 }
19617 }
19618}
19619
19620unsafe fn cgemm_c64(
19630 a_off: usize,
19631 b_off: usize,
19632 c_off: usize,
19633 m: usize,
19634 k: usize,
19635 n: usize,
19636 base: *mut u8,
19637) {
19638 let bptr = base as usize;
19639 unsafe {
19640 let a = std::slice::from_raw_parts((bptr + a_off) as *const f32, 2 * m * k);
19641 let b = std::slice::from_raw_parts((bptr + b_off) as *const f32, 2 * k * n);
19642 let c_base = bptr + c_off;
19643 crate::pool::par_range(m, |i| {
19644 let crow = std::slice::from_raw_parts_mut((c_base + i * n * 8) as *mut f32, 2 * n);
19645 for j in 0..n {
19646 let mut re = 0f32;
19647 let mut im = 0f32;
19648 for l in 0..k {
19649 let ar = a[2 * (i * k + l)];
19650 let ai = a[2 * (i * k + l) + 1];
19651 let br = b[2 * (l * n + j)];
19652 let bi = b[2 * (l * n + j) + 1];
19653 re += ar * br - ai * bi;
19654 im += ar * bi + ai * br;
19655 }
19656 crow[2 * j] = re;
19657 crow[2 * j + 1] = im;
19658 }
19659 });
19660 }
19661}
19662
19663#[allow(clippy::too_many_arguments)]
19671pub unsafe fn execute_lstm_f32(
19672 x: usize,
19673 w_ih: usize,
19674 w_hh: usize,
19675 bias: usize,
19676 h0: usize,
19677 c0: usize,
19678 dst: usize,
19679 batch: usize,
19680 seq: usize,
19681 input_size: usize,
19682 hidden: usize,
19683 num_layers: usize,
19684 bidirectional: bool,
19685 carry: bool,
19686 base: *mut u8,
19687) {
19688 #[inline]
19689 fn sigmoid(z: f32) -> f32 {
19690 1.0 / (1.0 + (-z).exp())
19691 }
19692
19693 let bptr = base as usize;
19694 let four_h = 4 * hidden;
19695 let dirs = if bidirectional { 2 } else { 1 };
19696
19697 unsafe {
19698 let f32s = |off: usize, n: usize| -> &[f32] {
19699 std::slice::from_raw_parts((bptr + off) as *const f32, n)
19700 };
19701
19702 let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
19704 let mut in_l = input_size;
19705 let mut wih_cursor = 0usize;
19708
19709 for l in 0..num_layers {
19710 let out_width = dirs * hidden;
19711 let mut layer_out = vec![0f32; batch * seq * out_width];
19712 let lo_ptr = layer_out.as_mut_ptr() as usize;
19713 let li_ref: &[f32] = &layer_in;
19714 let wih_block = four_h * in_l;
19715
19716 for dir in 0..dirs {
19717 let ld = l * dirs + dir;
19718 let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
19719 let whh = f32s(w_hh + ld * four_h * hidden * 4, four_h * hidden);
19720 let bs = f32s(bias + ld * four_h * 4, four_h);
19721 let h0p = bptr + h0 + ld * batch * hidden * 4;
19722 let c0p = bptr + c0 + ld * batch * hidden * 4;
19723
19724 crate::pool::par_range(batch, |b| {
19725 let lo = lo_ptr as *mut f32;
19726 let mut h = vec![0f32; hidden];
19727 let mut c = vec![0f32; hidden];
19728 if carry {
19729 let hin = std::slice::from_raw_parts(
19730 (h0p + b * hidden * 4) as *const f32,
19731 hidden,
19732 );
19733 let cin = std::slice::from_raw_parts(
19734 (c0p + b * hidden * 4) as *const f32,
19735 hidden,
19736 );
19737 h.copy_from_slice(hin);
19738 c.copy_from_slice(cin);
19739 }
19740 let mut z = vec![0f32; four_h];
19741 for step in 0..seq {
19742 let t = if dir == 0 { step } else { seq - 1 - step };
19743 let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
19744 for r in 0..four_h {
19745 let wr = &wih[r * in_l..(r + 1) * in_l];
19746 let mut acc = bs[r];
19747 for j in 0..in_l {
19748 acc += wr[j] * x_t[j];
19749 }
19750 let hr = &whh[r * hidden..(r + 1) * hidden];
19751 for (j, &hj) in h.iter().enumerate() {
19752 acc += hr[j] * hj;
19753 }
19754 z[r] = acc;
19755 }
19756 for k in 0..hidden {
19757 let i_g = sigmoid(z[k]);
19758 let f_g = sigmoid(z[hidden + k]);
19759 let g_g = z[2 * hidden + k].tanh();
19760 let o_g = sigmoid(z[3 * hidden + k]);
19761 let c_new = f_g * c[k] + i_g * g_g;
19762 c[k] = c_new;
19763 let h_new = o_g * c_new.tanh();
19764 h[k] = h_new;
19765 *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new;
19768 }
19769 }
19770 if carry {
19771 let hout = std::slice::from_raw_parts_mut(
19772 (h0p + b * hidden * 4) as *mut f32,
19773 hidden,
19774 );
19775 let cout = std::slice::from_raw_parts_mut(
19776 (c0p + b * hidden * 4) as *mut f32,
19777 hidden,
19778 );
19779 hout.copy_from_slice(&h);
19780 cout.copy_from_slice(&c);
19781 }
19782 });
19783 }
19784
19785 wih_cursor += dirs * wih_block;
19786 layer_in = layer_out;
19787 in_l = out_width;
19788 }
19789
19790 let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
19792 dst_slice.copy_from_slice(&layer_in);
19793 }
19794}
19795
19796#[allow(clippy::too_many_arguments)]
19802pub unsafe fn execute_gru_f32(
19803 x: usize,
19804 w_ih: usize,
19805 w_hh: usize,
19806 b_ih: usize,
19807 b_hh: usize,
19808 h0: usize,
19809 dst: usize,
19810 batch: usize,
19811 seq: usize,
19812 input_size: usize,
19813 hidden: usize,
19814 num_layers: usize,
19815 bidirectional: bool,
19816 carry: bool,
19817 base: *mut u8,
19818) {
19819 #[inline]
19820 fn sigmoid(z: f32) -> f32 {
19821 1.0 / (1.0 + (-z).exp())
19822 }
19823
19824 let bptr = base as usize;
19825 let three_h = 3 * hidden;
19826 let dirs = if bidirectional { 2 } else { 1 };
19827
19828 unsafe {
19829 let f32s = |off: usize, n: usize| -> &[f32] {
19830 std::slice::from_raw_parts((bptr + off) as *const f32, n)
19831 };
19832
19833 let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
19834 let mut in_l = input_size;
19835 let mut wih_cursor = 0usize;
19836
19837 for l in 0..num_layers {
19838 let out_width = dirs * hidden;
19839 let mut layer_out = vec![0f32; batch * seq * out_width];
19840 let lo_ptr = layer_out.as_mut_ptr() as usize;
19841 let li_ref: &[f32] = &layer_in;
19842 let wih_block = three_h * in_l;
19843
19844 for dir in 0..dirs {
19845 let ld = l * dirs + dir;
19846 let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
19847 let whh = f32s(w_hh + ld * three_h * hidden * 4, three_h * hidden);
19848 let bih = f32s(b_ih + ld * three_h * 4, three_h);
19849 let bhh = f32s(b_hh + ld * three_h * 4, three_h);
19850 let h0p = bptr + h0 + ld * batch * hidden * 4;
19851
19852 crate::pool::par_range(batch, |b| {
19853 let lo = lo_ptr as *mut f32;
19854 let mut h = vec![0f32; hidden];
19855 if carry {
19856 let hin = std::slice::from_raw_parts(
19857 (h0p + b * hidden * 4) as *const f32,
19858 hidden,
19859 );
19860 h.copy_from_slice(hin);
19861 }
19862 let mut xi = vec![0f32; three_h]; let mut hi = vec![0f32; three_h]; for step in 0..seq {
19865 let t = if dir == 0 { step } else { seq - 1 - step };
19866 let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
19867 for r in 0..three_h {
19868 let wr = &wih[r * in_l..(r + 1) * in_l];
19869 let mut a = bih[r];
19870 for j in 0..in_l {
19871 a += wr[j] * x_t[j];
19872 }
19873 xi[r] = a;
19874 let hr = &whh[r * hidden..(r + 1) * hidden];
19875 let mut bb = bhh[r];
19876 for (j, &hj) in h.iter().enumerate() {
19877 bb += hr[j] * hj;
19878 }
19879 hi[r] = bb;
19880 }
19881 for k in 0..hidden {
19882 let rg = sigmoid(xi[k] + hi[k]);
19883 let zg = sigmoid(xi[hidden + k] + hi[hidden + k]);
19884 let ng = (xi[2 * hidden + k] + rg * hi[2 * hidden + k]).tanh();
19886 let h_new = (1.0 - zg) * ng + zg * h[k];
19887 h[k] = h_new;
19888 *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new;
19889 }
19890 }
19891 if carry {
19892 let hout = std::slice::from_raw_parts_mut(
19893 (h0p + b * hidden * 4) as *mut f32,
19894 hidden,
19895 );
19896 hout.copy_from_slice(&h);
19897 }
19898 });
19899 }
19900
19901 wih_cursor += dirs * wih_block;
19902 layer_in = layer_out;
19903 in_l = out_width;
19904 }
19905
19906 let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
19907 dst_slice.copy_from_slice(&layer_in);
19908 }
19909}
19910
19911#[allow(clippy::too_many_arguments)]
19916pub unsafe fn execute_rnn_f32(
19917 x: usize,
19918 w_ih: usize,
19919 w_hh: usize,
19920 bias: usize,
19921 h0: usize,
19922 dst: usize,
19923 batch: usize,
19924 seq: usize,
19925 input_size: usize,
19926 hidden: usize,
19927 num_layers: usize,
19928 bidirectional: bool,
19929 carry: bool,
19930 relu: bool,
19931 base: *mut u8,
19932) {
19933 let bptr = base as usize;
19934 let dirs = if bidirectional { 2 } else { 1 };
19935
19936 unsafe {
19937 let f32s = |off: usize, n: usize| -> &[f32] {
19938 std::slice::from_raw_parts((bptr + off) as *const f32, n)
19939 };
19940
19941 let mut layer_in: Vec<f32> = f32s(x, batch * seq * input_size).to_vec();
19942 let mut in_l = input_size;
19943 let mut wih_cursor = 0usize;
19944
19945 for l in 0..num_layers {
19946 let out_width = dirs * hidden;
19947 let mut layer_out = vec![0f32; batch * seq * out_width];
19948 let lo_ptr = layer_out.as_mut_ptr() as usize;
19949 let li_ref: &[f32] = &layer_in;
19950 let wih_block = hidden * in_l;
19951
19952 for dir in 0..dirs {
19953 let ld = l * dirs + dir;
19954 let wih = f32s((w_ih / 4 + wih_cursor + dir * wih_block) * 4, wih_block);
19955 let whh = f32s(w_hh + ld * hidden * hidden * 4, hidden * hidden);
19956 let bs = f32s(bias + ld * hidden * 4, hidden);
19957 let h0p = bptr + h0 + ld * batch * hidden * 4;
19958
19959 crate::pool::par_range(batch, |b| {
19960 let lo = lo_ptr as *mut f32;
19961 let mut h = vec![0f32; hidden];
19962 if carry {
19963 let hin = std::slice::from_raw_parts(
19964 (h0p + b * hidden * 4) as *const f32,
19965 hidden,
19966 );
19967 h.copy_from_slice(hin);
19968 }
19969 for step in 0..seq {
19970 let t = if dir == 0 { step } else { seq - 1 - step };
19971 let x_t = &li_ref[(b * seq + t) * in_l..(b * seq + t + 1) * in_l];
19972 let mut h_new = vec![0f32; hidden];
19973 for k in 0..hidden {
19974 let wr = &wih[k * in_l..(k + 1) * in_l];
19975 let mut acc = bs[k];
19976 for j in 0..in_l {
19977 acc += wr[j] * x_t[j];
19978 }
19979 let hr = &whh[k * hidden..(k + 1) * hidden];
19980 for (j, &hj) in h.iter().enumerate() {
19981 acc += hr[j] * hj;
19982 }
19983 h_new[k] = if relu { acc.max(0.0) } else { acc.tanh() };
19984 }
19985 for k in 0..hidden {
19986 h[k] = h_new[k];
19987 *lo.add((b * seq + t) * out_width + dir * hidden + k) = h_new[k];
19988 }
19989 }
19990 if carry {
19991 let hout = std::slice::from_raw_parts_mut(
19992 (h0p + b * hidden * 4) as *mut f32,
19993 hidden,
19994 );
19995 hout.copy_from_slice(&h);
19996 }
19997 });
19998 }
19999
20000 wih_cursor += dirs * wih_block;
20001 layer_in = layer_out;
20002 in_l = out_width;
20003 }
20004
20005 let dst_slice = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, layer_in.len());
20006 dst_slice.copy_from_slice(&layer_in);
20007 }
20008}
20009
20010pub unsafe fn execute_argreduce_f32(
20015 src: usize,
20016 dst: usize,
20017 outer: usize,
20018 reduced: usize,
20019 inner: usize,
20020 is_max: bool,
20021 base: *mut u8,
20022) {
20023 let bptr = base as usize;
20024 unsafe {
20025 let inp = std::slice::from_raw_parts((bptr + src) as *const f32, outer * reduced * inner);
20026 let out = std::slice::from_raw_parts_mut((bptr + dst) as *mut f32, outer * inner);
20027 for o in 0..outer {
20028 for i in 0..inner {
20029 let mut best = inp[o * reduced * inner + i];
20030 let mut best_idx = 0usize;
20031 for r in 1..reduced {
20032 let v = inp[o * reduced * inner + r * inner + i];
20033 let better = if is_max { v > best } else { v < best };
20034 if better {
20035 best = v;
20036 best_idx = r;
20037 }
20038 }
20039 out[o * inner + i] = best_idx as f32;
20040 }
20041 }
20042 }
20043}
20044
20045#[allow(clippy::too_many_arguments)]
20050pub unsafe fn execute_mamba2_f32(
20051 x: usize,
20052 dt: usize,
20053 a: usize,
20054 b: usize,
20055 c: usize,
20056 dst: usize,
20057 batch: usize,
20058 seq: usize,
20059 heads: usize,
20060 head_dim: usize,
20061 state_size: usize,
20062 base: *mut u8,
20063) {
20064 let (bn, s, h, p, n) = (batch, seq, heads, head_dim, state_size);
20065 let bptr = base as usize;
20066 unsafe {
20067 let f32s = |off: usize, len: usize| -> &[f32] {
20068 std::slice::from_raw_parts((bptr + off) as *const f32, len)
20069 };
20070 let xs = f32s(x, bn * s * h * p);
20071 let dts = f32s(dt, bn * s * h);
20072 let am = f32s(a, h);
20073 let bm = f32s(b, bn * s * h * n);
20074 let cm = f32s(c, bn * s * h * n);
20075 let out_ptr = bptr + dst;
20076
20077 crate::pool::par_range(bn * h, |bh| {
20079 let bi = bh / h;
20080 let hi = bh % h;
20081 let out = out_ptr as *mut f32;
20082 let mut state = vec![0f32; p * n];
20083 for t in 0..s {
20084 let dt_t = dts[(bi * s + t) * h + hi];
20085 let da = (dt_t * am[hi]).exp();
20086 let x_off = ((bi * s + t) * h + hi) * p;
20087 let bc_off = ((bi * s + t) * h + hi) * n;
20088 for pi in 0..p {
20089 let dtx = dt_t * xs[x_off + pi];
20090 for ni in 0..n {
20091 state[pi * n + ni] = da * state[pi * n + ni] + dtx * bm[bc_off + ni];
20092 }
20093 }
20094 for pi in 0..p {
20095 let mut acc = 0f32;
20096 for ni in 0..n {
20097 acc += state[pi * n + ni] * cm[bc_off + ni];
20098 }
20099 *out.add(x_off + pi) = acc;
20100 }
20101 }
20102 });
20103 }
20104}
20105
20106pub unsafe fn execute_reverse(
20112 src: usize,
20113 dst: usize,
20114 dims: &[u32],
20115 rev_mask: &[bool],
20116 elem_bytes: usize,
20117 base: *mut u8,
20118) {
20119 let rank = dims.len();
20120 let total: usize = dims.iter().map(|&d| d as usize).product::<usize>().max(1);
20121 let mut strides = vec![1usize; rank];
20122 for i in (0..rank.saturating_sub(1)).rev() {
20123 strides[i] = strides[i + 1] * dims[i + 1] as usize;
20124 }
20125 unsafe {
20126 let src_base = base.add(src);
20127 let dst_base = base.add(dst);
20128 for o in 0..total {
20129 let mut rem = o;
20130 let mut in_flat = 0usize;
20131 for ax in 0..rank {
20132 let idx = rem / strides[ax];
20133 rem %= strides[ax];
20134 let in_idx = if rev_mask[ax] {
20135 dims[ax] as usize - 1 - idx
20136 } else {
20137 idx
20138 };
20139 in_flat += in_idx * strides[ax];
20140 }
20141 std::ptr::copy_nonoverlapping(
20142 src_base.add(in_flat * elem_bytes),
20143 dst_base.add(o * elem_bytes),
20144 elem_bytes,
20145 );
20146 }
20147 }
20148}
20149
20150pub unsafe fn execute_sample_f32(
20155 logits: usize,
20156 dst: usize,
20157 batch: usize,
20158 vocab: usize,
20159 top_k: usize,
20160 top_p: f32,
20161 temperature: f32,
20162 seed: u64,
20163 base: *mut u8,
20164) {
20165 let (b, v) = (batch, vocab);
20166 let k = top_k.min(v);
20167 unsafe {
20168 let lg = sl(logits, base, b * v);
20169 let out = sl_mut(dst, base, b);
20170 let mut rng = rlx_ir::Philox4x32::new(if seed == 0 { 0xDEADBEEF } else { seed });
20171 for bi in 0..b {
20172 let row = &lg[bi * v..(bi + 1) * v];
20173 out[bi] = sample_row(row, k, top_p, temperature, &mut rng) as f32;
20174 }
20175 }
20176}
20177
20178pub unsafe fn execute_selective_scan_f32(
20184 x: usize,
20185 delta: usize,
20186 a: usize,
20187 b: usize,
20188 c: usize,
20189 dst: usize,
20190 batch: usize,
20191 seq: usize,
20192 hidden: usize,
20193 state_size: usize,
20194 base: *mut u8,
20195) {
20196 let (bn, s, h, n) = (batch, seq, hidden, state_size);
20197 unsafe {
20198 let xs = sl(x, base, bn * s * h);
20199 let dt = sl(delta, base, bn * s * h);
20200 let am = sl(a, base, h * n);
20201 let bm = sl(b, base, bn * s * n);
20202 let cm = sl(c, base, bn * s * n);
20203 let out = sl_mut(dst, base, bn * s * h);
20204
20205 let mut state = vec![0f32; h * n];
20208 for bi in 0..bn {
20209 for v in state.iter_mut() {
20210 *v = 0.0;
20211 }
20212 for si in 0..s {
20213 let x_row = &xs[bi * s * h + si * h..bi * s * h + (si + 1) * h];
20214 let dt_row = &dt[bi * s * h + si * h..bi * s * h + (si + 1) * h];
20215 let b_row = &bm[bi * s * n + si * n..bi * s * n + (si + 1) * n];
20216 let c_row = &cm[bi * s * n + si * n..bi * s * n + (si + 1) * n];
20217 let out_row = &mut out[bi * s * h + si * h..bi * s * h + (si + 1) * h];
20218
20219 for ci in 0..h {
20220 let d = dt_row[ci];
20221 let xv = x_row[ci];
20222 let mut acc = 0f32;
20223 for ni in 0..n {
20224 let da = (d * am[ci * n + ni]).exp();
20226 state[ci * n + ni] = da * state[ci * n + ni] + d * b_row[ni] * xv;
20227 acc += c_row[ni] * state[ci * n + ni];
20228 }
20229 out_row[ci] = acc;
20230 }
20231 }
20232 }
20233 }
20234}
20235
20236pub unsafe fn execute_gated_delta_net_f32(
20237 q: usize,
20238 k: usize,
20239 v: usize,
20240 g: usize,
20241 beta: usize,
20242 state: usize,
20243 dst: usize,
20244 batch: usize,
20245 seq: usize,
20246 heads: usize,
20247 state_size: usize,
20248 base: *mut u8,
20249) {
20250 #[derive(Copy, Clone)]
20251 struct ArenaPtr(usize);
20252 unsafe impl Send for ArenaPtr {}
20253 unsafe impl Sync for ArenaPtr {}
20254 impl ArenaPtr {
20255 #[inline]
20256 fn get(self) -> *mut u8 {
20257 self.0 as *mut u8
20258 }
20259 }
20260
20261 unsafe {
20262 let arena = ArenaPtr(base as usize);
20263 let (b, s, h, n) = (batch, seq, heads, state_size);
20264 let scale = 1.0f32 / (n as f32).sqrt();
20265 let use_external = state != 0;
20266 let mut owned_state = vec![0f32; h * n * n];
20267
20268 crate::pool::num_threads();
20269
20270 assert!(
20271 n <= crate::gdn::GDN_MAX_STATE,
20272 "GatedDeltaNet state_size={n} exceeds stack scratch ({})",
20273 crate::gdn::GDN_MAX_STATE
20274 );
20275
20276 let qs = sl(q, arena.get(), b * s * h * n);
20277 let ks = sl(k, arena.get(), b * s * h * n);
20278 let vs = sl(v, arena.get(), b * s * h * n);
20279 let gs = sl(g, arena.get(), b * s * h);
20280 let betas = sl(beta, arena.get(), b * s * h);
20281 let _out = sl_mut(dst, arena.get(), b * s * h * n);
20282 let hs_n = h * n;
20283
20284 let run_head = |bi: usize, hi: usize, s_mat: &mut [f32], sk: &mut [f32]| {
20285 for ti in 0..s {
20286 let qkv_step = bi * s * hs_n + ti * hs_n + hi * n;
20287 let gb_step = bi * s * h + ti * h + hi;
20288 let out_row = sl_mut(dst + qkv_step * std::mem::size_of::<f32>(), arena.get(), n);
20289 crate::gdn::gdn_step_blas(
20290 s_mat,
20291 &qs[qkv_step..qkv_step + n],
20292 &ks[qkv_step..qkv_step + n],
20293 &vs[qkv_step..qkv_step + n],
20294 gs[gb_step],
20295 betas[gb_step],
20296 out_row,
20297 sk,
20298 n,
20299 scale,
20300 );
20301 }
20302 };
20303
20304 if !use_external && s > 1 {
20307 for bi in 0..b {
20308 crate::pool::par_range(h, |hi| {
20309 let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
20310 let sk = &mut sk_buf[..n];
20311 let mut local_state =
20312 [0f32; crate::gdn::GDN_MAX_STATE * crate::gdn::GDN_MAX_STATE];
20313 let s_mat = &mut local_state[..n * n];
20314 s_mat.fill(0.0);
20315 run_head(bi, hi, s_mat, sk);
20316 });
20317 }
20318 return;
20319 }
20320
20321 if use_external {
20322 let state_bytes = state;
20323 crate::pool::par_range(b * h, |bhi| {
20324 let bi = bhi / h;
20325 let hi = bhi % h;
20326 let elem_off = bi * h * n * n + hi * n * n;
20327 let s_mat = sl_mut(
20328 state_bytes + elem_off * std::mem::size_of::<f32>(),
20329 arena.get(),
20330 n * n,
20331 );
20332 let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
20333 run_head(bi, hi, s_mat, &mut sk_buf[..n]);
20334 });
20335 } else {
20336 for bi in 0..b {
20337 owned_state.fill(0.0);
20338 #[cfg(not(target_arch = "wasm32"))]
20339 {
20340 use rayon::prelude::*;
20341 owned_state
20342 .par_chunks_mut(n * n)
20343 .enumerate()
20344 .for_each(|(hi, s_mat)| {
20345 let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
20346 run_head(bi, hi, s_mat, &mut sk_buf[..n]);
20347 });
20348 }
20349 #[cfg(target_arch = "wasm32")]
20350 {
20351 owned_state
20352 .chunks_mut(n * n)
20353 .enumerate()
20354 .for_each(|(hi, s_mat)| {
20355 let mut sk_buf = [0f32; crate::gdn::GDN_MAX_STATE];
20356 run_head(bi, hi, s_mat, &mut sk_buf[..n]);
20357 });
20358 }
20359 }
20360 }
20361 }
20362}
20363
20364pub unsafe fn execute_rms_norm_backward_input_f32(
20366 x: usize,
20367 gamma: usize,
20368 beta: usize,
20369 dy: usize,
20370 dx: usize,
20371 rows: u32,
20372 h: u32,
20373 eps: f32,
20374 base: *mut u8,
20375) {
20376 let (rows, h) = (rows as usize, h as usize);
20377 let mut dg = vec![0f32; h];
20378 let mut db = vec![0f32; h];
20379 let xs = sl(x, base, rows * h);
20380 let dys = sl(dy, base, rows * h);
20381 let g = sl(gamma, base, h);
20382 let b = sl(beta, base, h);
20383 let out = sl_mut(dx, base, rows * h);
20384 for r in 0..rows {
20385 crate::training_bwd::rms_norm_backward_row(
20386 &xs[r * h..(r + 1) * h],
20387 g,
20388 b,
20389 &dys[r * h..(r + 1) * h],
20390 &mut out[r * h..(r + 1) * h],
20391 &mut dg,
20392 &mut db,
20393 eps,
20394 );
20395 }
20396}
20397
20398pub unsafe fn execute_rms_norm_backward_gamma_f32(
20399 x: usize,
20400 gamma: usize,
20401 beta: usize,
20402 dy: usize,
20403 dgamma: usize,
20404 rows: u32,
20405 h: u32,
20406 eps: f32,
20407 base: *mut u8,
20408) {
20409 let (rows, h) = (rows as usize, h as usize);
20410 let out = sl_mut(dgamma, base, h);
20411 out.fill(0.0);
20412 let mut dx = vec![0f32; h];
20413 let mut db = vec![0f32; h];
20414 let xs = sl(x, base, rows * h);
20415 let dys = sl(dy, base, rows * h);
20416 let g = sl(gamma, base, h);
20417 let b = sl(beta, base, h);
20418 for r in 0..rows {
20419 crate::training_bwd::rms_norm_backward_row(
20420 &xs[r * h..(r + 1) * h],
20421 g,
20422 b,
20423 &dys[r * h..(r + 1) * h],
20424 &mut dx,
20425 out,
20426 &mut db,
20427 eps,
20428 );
20429 }
20430}
20431
20432pub unsafe fn execute_rms_norm_backward_beta_f32(
20433 x: usize,
20434 gamma: usize,
20435 beta: usize,
20436 dy: usize,
20437 dbeta: usize,
20438 rows: u32,
20439 h: u32,
20440 eps: f32,
20441 base: *mut u8,
20442) {
20443 let (rows, h) = (rows as usize, h as usize);
20444 let out = sl_mut(dbeta, base, h);
20445 out.fill(0.0);
20446 let mut dx = vec![0f32; h];
20447 let mut dg = vec![0f32; h];
20448 let xs = sl(x, base, rows * h);
20449 let dys = sl(dy, base, rows * h);
20450 let g = sl(gamma, base, h);
20451 let b = sl(beta, base, h);
20452 for r in 0..rows {
20453 crate::training_bwd::rms_norm_backward_row(
20454 &xs[r * h..(r + 1) * h],
20455 g,
20456 b,
20457 &dys[r * h..(r + 1) * h],
20458 &mut dx,
20459 &mut dg,
20460 out,
20461 eps,
20462 );
20463 }
20464}
20465
20466#[allow(clippy::too_many_arguments)]
20467pub unsafe fn execute_conv2d_forward_f32(
20468 src: usize,
20469 weight: usize,
20470 dst: usize,
20471 n: u32,
20472 c_in: u32,
20473 h: u32,
20474 w: u32,
20475 c_out: u32,
20476 h_out: u32,
20477 w_out: u32,
20478 kh: u32,
20479 kw: u32,
20480 sh: u32,
20481 sw: u32,
20482 ph: u32,
20483 pw: u32,
20484 dh: u32,
20485 dw: u32,
20486 groups: u32,
20487 base: *mut u8,
20488) {
20489 let n = n as usize;
20490 let c_in = c_in as usize;
20491 let h = h as usize;
20492 let w = w as usize;
20493 let c_out = c_out as usize;
20494 let h_out = h_out as usize;
20495 let w_out = w_out as usize;
20496 let kh = kh as usize;
20497 let kw = kw as usize;
20498 let sh = sh as usize;
20499 let sw = sw as usize;
20500 let ph = ph as usize;
20501 let pw = pw as usize;
20502 let dh = dh as usize;
20503 let dw = dw as usize;
20504 let groups = groups as usize;
20505 let c_in_per_g = c_in / groups;
20506 let inp = sl(src, base, n * c_in * h * w);
20507 let wt = sl(weight, base, c_out * c_in_per_g * kh * kw);
20508 let out = sl_mut(dst, base, n * c_out * h_out * w_out);
20509 crate::conv_fwd::conv2d_forward_nchw_f32(
20510 inp, wt, out, n, c_in, h, w, c_out, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw, groups,
20511 );
20512}
20513
20514#[allow(clippy::too_many_arguments)]
20518pub unsafe fn execute_conv3d_forward_f32(
20519 src: usize,
20520 weight: usize,
20521 dst: usize,
20522 n: u32,
20523 c_in: u32,
20524 d: u32,
20525 h: u32,
20526 w: u32,
20527 c_out: u32,
20528 d_out: u32,
20529 h_out: u32,
20530 w_out: u32,
20531 kd: u32,
20532 kh: u32,
20533 kw: u32,
20534 sd: u32,
20535 sh: u32,
20536 sw: u32,
20537 pd: u32,
20538 ph: u32,
20539 pw: u32,
20540 dd: u32,
20541 dh: u32,
20542 dw: u32,
20543 groups: u32,
20544 base: *mut u8,
20545) {
20546 let n = n as usize;
20547 let c_in = c_in as usize;
20548 let d = d as usize;
20549 let h = h as usize;
20550 let w = w as usize;
20551 let c_out = c_out as usize;
20552 let d_out = d_out as usize;
20553 let h_out = h_out as usize;
20554 let w_out = w_out as usize;
20555 let kd = kd as usize;
20556 let kh = kh as usize;
20557 let kw = kw as usize;
20558 let sd = sd as usize;
20559 let sh = sh as usize;
20560 let sw = sw as usize;
20561 let pd = pd as usize;
20562 let ph = ph as usize;
20563 let pw = pw as usize;
20564 let dd = dd as usize;
20565 let dh = dh as usize;
20566 let dw = dw as usize;
20567 let groups = groups as usize;
20568 let c_in_per_g = c_in / groups;
20569 let inp = sl(src, base, n * c_in * d * h * w);
20570 let wt = sl(weight, base, c_out * c_in_per_g * kd * kh * kw);
20571 let out = sl_mut(dst, base, n * c_out * d_out * h_out * w_out);
20572 crate::conv_fwd::conv3d_forward_ncdhw_f32(
20573 inp, wt, out, n, c_in, d, h, w, c_out, d_out, h_out, w_out, kd, kh, kw, sd, sh, sw, pd, ph,
20574 pw, dd, dh, dw, groups,
20575 );
20576}
20577
20578#[allow(clippy::too_many_arguments)]
20581pub unsafe fn execute_conv_transpose3d_ncdhw_f32(
20582 src: usize,
20583 weight: usize,
20584 dst: usize,
20585 n: u32,
20586 c_in: u32,
20587 d: u32,
20588 h: u32,
20589 w_in: u32,
20590 c_out: u32,
20591 d_out: u32,
20592 h_out: u32,
20593 w_out: u32,
20594 kd: u32,
20595 kh: u32,
20596 kw: u32,
20597 sd: u32,
20598 sh: u32,
20599 sw: u32,
20600 pd: u32,
20601 ph: u32,
20602 pw: u32,
20603 dd: u32,
20604 dh: u32,
20605 dw: u32,
20606 groups: u32,
20607 base: *mut u8,
20608) {
20609 let n = n as usize;
20610 let c_in = c_in as usize;
20611 let d = d as usize;
20612 let h = h as usize;
20613 let w_in = w_in as usize;
20614 let c_out = c_out as usize;
20615 let d_out = d_out as usize;
20616 let h_out = h_out as usize;
20617 let w_out = w_out as usize;
20618 let kd = kd as usize;
20619 let kh = kh as usize;
20620 let kw = kw as usize;
20621 let sd = sd as usize;
20622 let sh = sh as usize;
20623 let sw = sw as usize;
20624 let pd = pd as usize;
20625 let ph = ph as usize;
20626 let pw = pw as usize;
20627 let dd = dd as usize;
20628 let dh = dh as usize;
20629 let dw = dw as usize;
20630 let groups = groups as usize;
20631 let in_elems = n * c_in * d * h * w_in;
20632 let w_elems = c_in * (c_out / groups) * kd * kh * kw;
20633 let out_elems = n * c_out * d_out * h_out * w_out;
20634 let input = sl(src, base, in_elems);
20635 let wt = sl(weight, base, w_elems);
20636 let output = sl_mut(dst, base, out_elems);
20637 crate::kernels::conv_transpose3d_ncdhw(
20638 input, wt, output, n, c_in, d, h, w_in, c_out, d_out, h_out, w_out, kd, kh, kw, sd, sh, sw,
20639 pd, ph, pw, dd, dh, dw, groups,
20640 );
20641}
20642
20643pub unsafe fn execute_maxpool2d_backward_f32(
20644 x: usize,
20645 dy: usize,
20646 dx: usize,
20647 n: u32,
20648 c: u32,
20649 h: u32,
20650 w: u32,
20651 h_out: u32,
20652 w_out: u32,
20653 kh: u32,
20654 kw: u32,
20655 sh: u32,
20656 sw: u32,
20657 ph: u32,
20658 pw: u32,
20659 base: *mut u8,
20660) {
20661 let (n, c, h, w) = (n as usize, c as usize, h as usize, w as usize);
20662 let (h_out, w_out) = (h_out as usize, w_out as usize);
20663 let (kh, kw) = (kh as usize, kw as usize);
20664 let (sh, sw) = (sh as usize, sw as usize);
20665 let (ph, pw) = (ph as usize, pw as usize);
20666 let xs = sl(x, base, n * c * h * w);
20667 let dys = sl(dy, base, n * c * h_out * w_out);
20668 let dxs = sl_mut(dx, base, n * c * h * w);
20669 if fast_conv_enabled() && crate::pool::should_parallelize(n * c * h * w) {
20672 let (in_plane, out_plane) = (h * w, h_out * w_out);
20673 let x_addr = xs.as_ptr() as usize;
20674 let dy_addr = dys.as_ptr() as usize;
20675 let dx_addr = dxs.as_mut_ptr() as usize;
20676 crate::pool::par_for(n * c, crate::pool::outer_chunk(n * c), &|off, cnt| {
20677 for nc in off..off + cnt {
20678 let xp =
20679 std::slice::from_raw_parts((x_addr as *const f32).add(nc * in_plane), in_plane);
20680 let dyp = std::slice::from_raw_parts(
20681 (dy_addr as *const f32).add(nc * out_plane),
20682 out_plane,
20683 );
20684 let dxp = std::slice::from_raw_parts_mut(
20685 (dx_addr as *mut f32).add(nc * in_plane),
20686 in_plane,
20687 );
20688 crate::training_bwd::maxpool2d_backward_nchw(
20689 xp, dyp, dxp, 1, 1, h, w, h_out, w_out, kh, kw, sh, sw, ph, pw,
20690 );
20691 }
20692 });
20693 } else {
20694 crate::training_bwd::maxpool2d_backward_nchw(
20695 xs, dys, dxs, n, c, h, w, h_out, w_out, kh, kw, sh, sw, ph, pw,
20696 );
20697 }
20698}
20699
20700pub unsafe fn execute_rope_backward_f32(
20701 dy: usize,
20702 cos: usize,
20703 sin: usize,
20704 dx: usize,
20705 batch: u32,
20706 seq: u32,
20707 hidden: u32,
20708 head_dim: u32,
20709 n_rot: u32,
20710 cos_len: u32,
20711 base: *mut u8,
20712) {
20713 let (b, s, hs, dh, nr, cl) = (
20714 batch as usize,
20715 seq as usize,
20716 hidden as usize,
20717 head_dim as usize,
20718 n_rot as usize,
20719 cos_len as usize,
20720 );
20721 let nh = hs / dh;
20722 let tab_half = dh / 2;
20723 let dys = sl(dy, base, b * s * hs);
20724 let cos_tab = sl(cos, base, cl);
20725 let sin_tab = sl(sin, base, cl);
20726 let out = sl_mut(dx, base, b * s * hs);
20727 for bi in 0..b {
20728 for si in 0..s {
20729 let tab_off = si.saturating_mul(tab_half) % cl.max(1);
20730 let cp = &cos_tab[tab_off..tab_off + tab_half.min(cl)];
20731 let sp = &sin_tab[tab_off..tab_off + tab_half.min(cl)];
20732 for hi in 0..nh {
20733 let base_idx = bi * s * hs + si * hs + hi * dh;
20734 crate::training_bwd::rope_backward_row(
20735 &dys[base_idx..base_idx + dh],
20736 cp,
20737 sp,
20738 &mut out[base_idx..base_idx + dh],
20739 dh,
20740 nr,
20741 );
20742 }
20743 }
20744 }
20745}
20746
20747pub unsafe fn execute_cumsum_backward_f32(
20748 dy: usize,
20749 dx: usize,
20750 rows: u32,
20751 cols: u32,
20752 exclusive: bool,
20753 base: *mut u8,
20754) {
20755 let (rows, cols) = (rows as usize, cols as usize);
20756 let dys = sl(dy, base, rows * cols);
20757 let out = sl_mut(dx, base, rows * cols);
20758 for r in 0..rows {
20759 crate::training_bwd::cumsum_backward_row(
20760 &dys[r * cols..(r + 1) * cols],
20761 &mut out[r * cols..(r + 1) * cols],
20762 exclusive,
20763 );
20764 }
20765}
20766
20767pub unsafe fn execute_gather_backward_f32(
20768 dy: usize,
20769 indices: usize,
20770 dst: usize,
20771 outer: u32,
20772 axis_dim: u32,
20773 num_idx: u32,
20774 trailing: u32,
20775 base: *mut u8,
20776) {
20777 let (outer, axis_dim, num_idx, trailing) = (
20778 outer as usize,
20779 axis_dim as usize,
20780 num_idx as usize,
20781 trailing as usize,
20782 );
20783 let out = sl_mut(dst, base, outer * axis_dim * trailing);
20784 out.fill(0.0);
20785 crate::training_bwd::gather_axis_backward(
20786 sl(dy, base, outer * num_idx * trailing),
20787 sl(indices, base, num_idx),
20788 out,
20789 outer,
20790 axis_dim,
20791 num_idx,
20792 trailing,
20793 );
20794}
20795
20796pub unsafe fn execute_dequant_matmul_gguf_f32(
20798 x: usize,
20799 w_q: usize,
20800 dst: usize,
20801 m: usize,
20802 k: usize,
20803 n: usize,
20804 scheme: rlx_ir::quant::QuantScheme,
20805 base: *mut u8,
20806) {
20807 unsafe {
20808 let block_bytes = scheme.gguf_block_bytes() as usize;
20809 let block_elems = scheme.gguf_block_size() as usize;
20810 let total_bytes = (k * n) / block_elems * block_bytes;
20811 let xs = sl(x, base, m * k);
20812 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, total_bytes);
20813 let out = sl_mut(dst, base, m * n);
20814 crate::gguf_matmul::gguf_matmul_bt_dispatch(xs, w_bytes, out, m, k, n, scheme);
20815 }
20816}
20817
20818pub unsafe fn execute_dequant_grouped_matmul_gguf_f32(
20820 input: usize,
20821 w_q: usize,
20822 expert_idx: usize,
20823 dst: usize,
20824 m: usize,
20825 k: usize,
20826 n: usize,
20827 num_experts: usize,
20828 scheme: rlx_ir::quant::QuantScheme,
20829 base: *mut u8,
20830) {
20831 unsafe {
20832 let block_bytes = scheme.gguf_block_bytes() as usize;
20833 let block_elems = scheme.gguf_block_size() as usize;
20834 let slab_bytes = (k * n) / block_elems * block_bytes;
20835 let xs = sl(input, base, m * k);
20836 let w_bytes =
20837 std::slice::from_raw_parts(base.add(w_q) as *const u8, num_experts * slab_bytes);
20838 let ids = sl(expert_idx, base, m);
20839 let out = sl_mut(dst, base, m * n);
20840 crate::gguf_matmul::gguf_grouped_matmul_bt(
20841 xs,
20842 w_bytes,
20843 ids,
20844 out,
20845 m,
20846 k,
20847 n,
20848 num_experts,
20849 scheme,
20850 );
20851 }
20852}
20853
20854pub unsafe fn execute_dequant_matmul_int8_f32(
20856 x: usize,
20857 w_q: usize,
20858 scale: usize,
20859 zp: usize,
20860 dst: usize,
20861 m: usize,
20862 k: usize,
20863 n: usize,
20864 block_size: u32,
20865 is_asymmetric: bool,
20866 base: *mut u8,
20867) {
20868 let bs = block_size as usize;
20869 let n_blocks = k.div_ceil(bs);
20870 unsafe {
20871 let xs = sl(x, base, m * k);
20872 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const i8, k * n);
20873 let scales = sl(scale, base, n_blocks * n);
20874 let zps = if is_asymmetric {
20875 sl(zp, base, n_blocks * n)
20876 } else {
20877 &[][..]
20878 };
20879 let out = sl_mut(dst, base, m * n);
20880 dequant_matmul_int8(xs, w_bytes, scales, zps, out, m, k, n, bs, is_asymmetric);
20881 }
20882}
20883
20884pub unsafe fn execute_dequant_matmul_int4_f32(
20886 x: usize,
20887 w_q: usize,
20888 scale: usize,
20889 zp: usize,
20890 dst: usize,
20891 m: usize,
20892 k: usize,
20893 n: usize,
20894 block_size: u32,
20895 is_asymmetric: bool,
20896 base: *mut u8,
20897) {
20898 let bs = block_size as usize;
20899 let n_blocks = k.div_ceil(bs);
20900 unsafe {
20901 let xs = sl(x, base, m * k);
20902 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, (k * n).div_ceil(2));
20903 let scales = sl(scale, base, n_blocks * n);
20904 let zps = if is_asymmetric {
20905 sl(zp, base, n_blocks * n)
20906 } else {
20907 &[][..]
20908 };
20909 let out = sl_mut(dst, base, m * n);
20910 dequant_matmul_int4(xs, w_bytes, scales, zps, out, m, k, n, bs, is_asymmetric);
20911 }
20912}
20913
20914pub unsafe fn execute_dequant_matmul_fp8_f32(
20916 x: usize,
20917 w_q: usize,
20918 scale: usize,
20919 dst: usize,
20920 m: usize,
20921 k: usize,
20922 n: usize,
20923 e5m2: bool,
20924 base: *mut u8,
20925) {
20926 unsafe {
20927 let xs = sl(x, base, m * k);
20928 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, k * n);
20929 let scales = sl(scale, base, n);
20930 let out = sl_mut(dst, base, m * n);
20931 dequant_matmul_fp8(xs, w_bytes, scales, out, m, k, n, e5m2);
20932 }
20933}
20934
20935pub unsafe fn execute_dequant_matmul_nvfp4_f32(
20937 x: usize,
20938 w_q: usize,
20939 scale: usize,
20940 global_scale: usize,
20941 dst: usize,
20942 m: usize,
20943 k: usize,
20944 n: usize,
20945 base: *mut u8,
20946) {
20947 let n_scale = k.div_ceil(rlx_ir::NVFP4_GROUP_SIZE) * n;
20948 unsafe {
20949 let xs = sl(x, base, m * k);
20950 let w_bytes = std::slice::from_raw_parts(base.add(w_q) as *const u8, (k * n).div_ceil(2));
20951 let scale_bytes = std::slice::from_raw_parts(base.add(scale) as *const u8, n_scale);
20952 let gs = sl(global_scale, base, 1)[0];
20953 let out = sl_mut(dst, base, m * n);
20954 dequant_matmul_nvfp4(xs, w_bytes, scale_bytes, gs, out, m, k, n);
20955 }
20956}
20957
20958pub unsafe fn execute_scaled_quant_scale_f32(
20964 x: usize,
20965 dst: usize,
20966 rows: usize,
20967 cols: usize,
20968 fmt: rlx_ir::ScaledFormat,
20969 layout: rlx_ir::ScaleLayout,
20970 base: *mut u8,
20971) {
20972 unsafe {
20973 let xs = sl(x, base, rows * cols);
20974 let scales = lowp_compute_scales(xs, fmt, layout, rows, cols);
20975 let nblk = lowp_nblk(cols, layout);
20976 match layout {
20977 rlx_ir::ScaleLayout::PerTensor => {
20978 sl_mut(dst, base, 1)[0] = scales[0];
20979 }
20980 rlx_ir::ScaleLayout::BlockMxE8M0 { .. } => {
20981 let out = std::slice::from_raw_parts_mut(base.add(dst), rows * nblk);
20982 for (o, &s) in out.iter_mut().zip(&scales) {
20983 *o = rlx_ir::lowp_codec::f32_to_e8m0(s);
20984 }
20985 }
20986 rlx_ir::ScaleLayout::Nvfp4 { .. } => {
20987 let out = std::slice::from_raw_parts_mut(base.add(dst), rows * nblk);
20988 for (o, &s) in out.iter_mut().zip(&scales) {
20989 *o = rlx_ir::lowp_codec::encode(rlx_ir::ScaledFormat::F8E4M3, s);
20990 }
20991 }
20992 }
20993 }
20994}
20995
20996#[allow(clippy::too_many_arguments)]
20998pub unsafe fn execute_scaled_quantize_f32(
20999 x: usize,
21000 scale: usize,
21001 dst: usize,
21002 rows: usize,
21003 cols: usize,
21004 fmt: rlx_ir::ScaledFormat,
21005 layout: rlx_ir::ScaleLayout,
21006 base: *mut u8,
21007) {
21008 unsafe {
21009 let xs = sl(x, base, rows * cols);
21010 let nblk = lowp_nblk(cols, layout);
21011 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
21012 1
21013 } else {
21014 rows * nblk
21015 };
21016 let scales = lowp_read_scales(layout, base, scale, n_scale);
21017 let out = std::slice::from_raw_parts_mut(base.add(dst), rows * cols);
21018 lowp_quantize(xs, &scales, fmt, layout, rows, cols, out);
21019 }
21020}
21021
21022#[allow(clippy::too_many_arguments)]
21025pub unsafe fn execute_scaled_dequantize_f32(
21026 codes: usize,
21027 scale: usize,
21028 dst: usize,
21029 rows: usize,
21030 cols: usize,
21031 fmt: rlx_ir::ScaledFormat,
21032 layout: rlx_ir::ScaleLayout,
21033 base: *mut u8,
21034) {
21035 unsafe {
21036 let nblk = lowp_nblk(cols, layout);
21037 let n_scale = if matches!(layout, rlx_ir::ScaleLayout::PerTensor) {
21038 1
21039 } else {
21040 rows * nblk
21041 };
21042 let cs = std::slice::from_raw_parts(base.add(codes), rows * cols);
21043 let scales = lowp_read_scales(layout, base, scale, n_scale);
21044 let out = std::slice::from_raw_parts_mut(base.add(dst) as *mut f32, rows * cols);
21045 lowp_dequantize(cs, &scales, fmt, layout, rows, cols, out);
21046 }
21047}
21048
21049#[allow(clippy::too_many_arguments)]
21051pub unsafe fn execute_scaled_matmul_f32(
21052 lhs: usize,
21053 rhs: usize,
21054 lhs_scale: usize,
21055 rhs_scale: usize,
21056 bias: usize,
21057 dst: usize,
21058 m: usize,
21059 k: usize,
21060 n: usize,
21061 has_bias: bool,
21062 lhs_fmt: rlx_ir::ScaledFormat,
21063 rhs_fmt: rlx_ir::ScaledFormat,
21064 layout: rlx_ir::ScaleLayout,
21065 base: *mut u8,
21066) {
21067 unsafe {
21068 let lhs_b = std::slice::from_raw_parts(base.add(lhs), m * k);
21069 let rhs_b = std::slice::from_raw_parts(base.add(rhs), n * k);
21070 let nblk = lowp_nblk(k, layout);
21071 let per_tensor = matches!(layout, rlx_ir::ScaleLayout::PerTensor);
21072 let n_l = if per_tensor { 1 } else { m * nblk };
21073 let n_r = if per_tensor { 1 } else { n * nblk };
21074 let ls = lowp_read_scales(layout, base, lhs_scale, n_l);
21075 let rs = lowp_read_scales(layout, base, rhs_scale, n_r);
21076 let bias_s = if has_bias {
21077 Some(sl(bias, base, n))
21078 } else {
21079 None
21080 };
21081 let out = sl_mut(dst, base, m * n);
21082 lowp_scaled_matmul(
21083 lhs_b, rhs_b, &ls, &rs, bias_s, out, m, n, k, layout, lhs_fmt, rhs_fmt,
21084 );
21085 }
21086}
21087
21088pub unsafe fn execute_gated_delta_net_f16(
21090 q: usize,
21091 k: usize,
21092 v: usize,
21093 g: usize,
21094 beta: usize,
21095 state: usize,
21096 dst: usize,
21097 batch: usize,
21098 seq: usize,
21099 heads: usize,
21100 state_size: usize,
21101 base: *mut u8,
21102) {
21103 use half::f16;
21104 unsafe {
21105 let read_f16 = |off: usize, len: usize| -> Vec<f32> {
21106 let raw = std::slice::from_raw_parts(base.add(off) as *const u8, len * 2);
21107 raw.chunks_exact(2)
21108 .map(|c| f16::from_le_bytes([c[0], c[1]]).to_f32())
21109 .collect()
21110 };
21111 let write_f16 = |off: usize, data: &[f32]| {
21112 let out = std::slice::from_raw_parts_mut(base.add(off), data.len() * 2);
21113 for (i, &v) in data.iter().enumerate() {
21114 let le = f16::from_f32(v).to_le_bytes();
21115 out[i * 2] = le[0];
21116 out[i * 2 + 1] = le[1];
21117 }
21118 };
21119
21120 let (b, s, h, n) = (batch, seq, heads, state_size);
21121 let q_f = read_f16(q, b * s * h * n);
21122 let k_f = read_f16(k, b * s * h * n);
21123 let v_f = read_f16(v, b * s * h * n);
21124 let g_f = read_f16(g, b * s * h);
21125 let b_f = read_f16(beta, b * s * h);
21126 let mut state_f = if state != 0 {
21127 read_f16(state, b * h * n * n)
21128 } else {
21129 vec![0f32; b * h * n * n]
21130 };
21131 let mut out_f = vec![0f32; b * s * h * n];
21132 let scale = 1.0f32 / (n as f32).sqrt();
21133 let mut sk_buf = vec![0f32; n];
21134 let mut owned_state = vec![0f32; h * n * n];
21135
21136 for bi in 0..b {
21137 let state_slice: &mut [f32] = if state != 0 {
21138 let start = bi * h * n * n;
21139 &mut state_f[start..start + h * n * n]
21140 } else {
21141 owned_state.fill(0.0);
21142 &mut owned_state
21143 };
21144
21145 for ti in 0..s {
21146 let qkv_step_base = bi * s * h * n + ti * h * n;
21147 let gb_step_base = bi * s * h + ti * h;
21148
21149 for hi in 0..h {
21150 let q_row = &q_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
21151 let k_row = &k_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
21152 let v_row = &v_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
21153 let g_t = g_f[gb_step_base + hi];
21154 let beta_t = b_f[gb_step_base + hi];
21155
21156 let s_base = hi * n * n;
21157 let s_mat = &mut state_slice[s_base..s_base + n * n];
21158
21159 let g_exp = g_t.exp();
21160 for st in s_mat.iter_mut() {
21161 *st *= g_exp;
21162 }
21163
21164 for j in 0..n {
21165 let mut acc = 0f32;
21166 for i in 0..n {
21167 acc += s_mat[i * n + j] * k_row[i];
21168 }
21169 sk_buf[j] = acc;
21170 }
21171
21172 for j in 0..n {
21173 sk_buf[j] = (v_row[j] - sk_buf[j]) * beta_t;
21174 }
21175
21176 for i in 0..n {
21177 let ki = k_row[i];
21178 for j in 0..n {
21179 s_mat[i * n + j] += ki * sk_buf[j];
21180 }
21181 }
21182
21183 let out_row = &mut out_f[qkv_step_base + hi * n..qkv_step_base + (hi + 1) * n];
21184 for j in 0..n {
21185 let mut acc = 0f32;
21186 for i in 0..n {
21187 acc += s_mat[i * n + j] * q_row[i];
21188 }
21189 out_row[j] = acc * scale;
21190 }
21191 }
21192 }
21193 }
21194
21195 write_f16(dst, &out_f);
21196 if state != 0 {
21197 write_f16(state, &state_f);
21198 }
21199 }
21200}
21201
21202pub unsafe fn execute_group_norm_nchw_f32(
21204 src: usize,
21205 g: usize,
21206 b: usize,
21207 dst: usize,
21208 n: usize,
21209 c: usize,
21210 h: usize,
21211 w: usize,
21212 num_groups: usize,
21213 eps: f32,
21214 base: *mut u8,
21215) {
21216 let plane = c * h * w;
21217 for ni in 0..n {
21218 let input = unsafe { sl(src + ni * plane * std::mem::size_of::<f32>(), base, plane) };
21219 let gamma = unsafe { sl(g, base, c) };
21220 let beta = unsafe { sl(b, base, c) };
21221 let output = unsafe { sl_mut(dst + ni * plane * std::mem::size_of::<f32>(), base, plane) };
21222 crate::kernels::group_norm_nchw(input, gamma, beta, output, 1, c, h, w, num_groups, eps);
21223 }
21224}
21225
21226pub unsafe fn execute_layer_norm2d_nchw_f32(
21228 src: usize,
21229 g: usize,
21230 b: usize,
21231 dst: usize,
21232 n: usize,
21233 c: usize,
21234 h: usize,
21235 w: usize,
21236 eps: f32,
21237 base: *mut u8,
21238) {
21239 let plane = c * h * w;
21240 unsafe {
21241 let input = sl(src, base, n * plane);
21242 let gamma = sl(g, base, c);
21243 let beta = sl(b, base, c);
21244 let output = sl_mut(dst, base, n * plane);
21245 crate::kernels::layer_norm2d_nchw(input, gamma, beta, output, n, c, h, w, eps);
21246 }
21247}
21248
21249pub unsafe fn execute_conv_transpose2d_nchw_f32(
21251 src: usize,
21252 weight: usize,
21253 dst: usize,
21254 n: usize,
21255 c_in: usize,
21256 h: usize,
21257 w_in: usize,
21258 c_out: usize,
21259 h_out: usize,
21260 w_out: usize,
21261 kh: usize,
21262 kw: usize,
21263 sh: usize,
21264 sw: usize,
21265 ph: usize,
21266 pw: usize,
21267 dh: usize,
21268 dw: usize,
21269 groups: usize,
21270 base: *mut u8,
21271) {
21272 let in_elems = n * c_in * h * w_in;
21273 let w_elems = c_in * (c_out / groups) * kh * kw;
21274 let out_elems = n * c_out * h_out * w_out;
21275 unsafe {
21276 let input = sl(src, base, in_elems);
21277 let wt = sl(weight, base, w_elems);
21278 let output = sl_mut(dst, base, out_elems);
21279 crate::kernels::conv_transpose2d_nchw(
21280 input, wt, output, n, c_in, h, w_in, c_out, h_out, w_out, kh, kw, sh, sw, ph, pw, dh,
21281 dw, groups,
21282 );
21283 }
21284}
21285
21286pub unsafe fn execute_resize_nearest_2x_f32(
21288 src: usize,
21289 dst: usize,
21290 n: usize,
21291 c: usize,
21292 h: usize,
21293 w: usize,
21294 base: *mut u8,
21295) {
21296 let in_plane = c * h * w;
21297 let out_plane = c * h * 2 * w * 2;
21298 for ni in 0..n {
21299 let input = unsafe {
21300 sl(
21301 src + ni * in_plane * std::mem::size_of::<f32>(),
21302 base,
21303 in_plane,
21304 )
21305 };
21306 let output = unsafe {
21307 sl_mut(
21308 dst + ni * out_plane * std::mem::size_of::<f32>(),
21309 base,
21310 out_plane,
21311 )
21312 };
21313 crate::kernels::resize_nearest_2x_nchw(input, output, c, h, w);
21314 }
21315}
21316
21317pub unsafe fn execute_axial_rope2d_f32(
21319 src: usize,
21320 dst: usize,
21321 batch: usize,
21322 seq: usize,
21323 hidden: usize,
21324 end_x: usize,
21325 end_y: usize,
21326 head_dim: usize,
21327 num_heads: usize,
21328 theta: f32,
21329 repeat_factor: usize,
21330 base: *mut u8,
21331) {
21332 let plane = seq * hidden;
21333 let plane_bytes = plane * std::mem::size_of::<f32>();
21334 for bi in 0..batch {
21335 let in_off = src + bi * plane_bytes;
21336 let input = unsafe { sl(in_off, base, plane) };
21337 let rotated = rlx_ir::ops::axial_rope2d::apply_axial_rope2d(
21338 input,
21339 num_heads,
21340 seq,
21341 head_dim,
21342 end_x,
21343 end_y,
21344 theta,
21345 repeat_factor,
21346 );
21347 let out_off = dst + bi * plane_bytes;
21348 let output = unsafe { sl_mut(out_off, base, plane) };
21349 output.copy_from_slice(&rotated);
21350 }
21351}
21352
21353pub unsafe fn execute_fft_butterfly_stage_f32(
21355 state_src: usize,
21356 state_dst: usize,
21357 gate_src: usize,
21358 rev_src: usize,
21359 tw_re_src: usize,
21360 tw_im_src: usize,
21361 batch: usize,
21362 n_fft: usize,
21363 stage: usize,
21364 base: *mut u8,
21365) {
21366 let half = n_fft / 2;
21367 let stride = 1usize << stage;
21368 let gate = unsafe { sl(gate_src, base, half) };
21369 let rev = unsafe { sl(rev_src, base, half) };
21370 let tw_re = unsafe { sl(tw_re_src, base, half) };
21371 let tw_im = unsafe { sl(tw_im_src, base, half) };
21372 let row_elems = n_fft * 2;
21373 for b in 0..batch {
21374 let in_off = state_src + b * row_elems * std::mem::size_of::<f32>();
21375 let out_off = state_dst + b * row_elems * std::mem::size_of::<f32>();
21376 let inp = unsafe { sl(in_off, base, row_elems) };
21377 let out = unsafe { sl_mut(out_off, base, row_elems) };
21378 out.copy_from_slice(inp);
21379 for bf in 0..half {
21380 if gate[bf] == 0.0 {
21381 continue;
21382 }
21383 let group = bf / stride;
21384 let k = bf % stride;
21385 let i0 = group * 2 * stride + k;
21386 let i1 = i0 + stride;
21387 let w_re = tw_re[bf];
21388 let w_im = tw_im[bf];
21389 let in_a_re = inp[i0 * 2];
21390 let in_a_im = inp[i0 * 2 + 1];
21391 let in_b_re = inp[i1 * 2];
21392 let in_b_im = inp[i1 * 2 + 1];
21393 let (b_re, b_im) = (
21394 in_b_re * w_re - in_b_im * w_im,
21395 in_b_re * w_im + in_b_im * w_re,
21396 );
21397 let (top_re, top_im) = (in_a_re + b_re, in_a_im + b_im);
21398 let (bot_re, bot_im) = (in_a_re - b_re, in_a_im - b_im);
21399 let (oa_re, oa_im, ob_re, ob_im) = if rev[bf] >= 0.5 {
21400 (bot_re, bot_im, top_re, top_im)
21401 } else {
21402 (top_re, top_im, bot_re, bot_im)
21403 };
21404 out[i0 * 2] = oa_re;
21405 out[i0 * 2 + 1] = oa_im;
21406 out[i1 * 2] = ob_re;
21407 out[i1 * 2 + 1] = ob_im;
21408 }
21409 }
21410}
21411
21412fn cpu_fft_parallel_enabled() -> bool {
21416 !rlx_ir::env::var("RLX_FFT_CPU_PARALLEL").is_some_and(|v| {
21417 v == "0" || v.eq_ignore_ascii_case("off") || v.eq_ignore_ascii_case("false")
21418 })
21419}
21420
21421fn cpu_fft_radix4_enabled() -> bool {
21424 !rlx_ir::env::var("RLX_FFT_RADIX4").is_some_and(|v| {
21425 v == "0" || v.eq_ignore_ascii_case("off") || v.eq_ignore_ascii_case("false")
21426 })
21427}
21428
21429#[derive(Clone, Copy)]
21434struct FftArenaPtr(*mut u8);
21435unsafe impl Send for FftArenaPtr {}
21437unsafe impl Sync for FftArenaPtr {}
21438impl FftArenaPtr {
21439 #[inline]
21442 fn ptr(self) -> *mut u8 {
21443 self.0
21444 }
21445}
21446
21447pub unsafe fn execute_fft1d_f32(
21448 src: usize,
21449 dst: usize,
21450 outer: usize,
21451 n_complex: usize,
21452 inverse: bool,
21453 norm_tag: u32,
21454 base: *mut u8,
21455) {
21456 let row_elems = 2 * n_complex;
21457 let norm = rlx_ir::fft::FftNorm::from_tag(norm_tag);
21458 let scale = norm.output_scale(n_complex, inverse) as f32;
21459 let is_pow2 = n_complex.is_power_of_two();
21460 let use_naive = !is_pow2 && n_complex <= 16;
21461 let use_radix4 = is_pow2 && n_complex >= 16 && is_pow4(n_complex) && cpu_fft_radix4_enabled();
21464 let scratch_template = if is_pow2 || use_naive {
21465 BluesteinScratchF32::empty()
21466 } else {
21467 BluesteinScratchF32::build(n_complex, inverse)
21468 };
21469
21470 let run_row = |bp: *mut u8,
21473 re: &mut [f32],
21474 im: &mut [f32],
21475 scratch: &mut BluesteinScratchF32,
21476 o: usize| {
21477 let row_offset = src + o * row_elems * std::mem::size_of::<f32>();
21478 let s = unsafe { sl(row_offset, bp, row_elems) };
21479 re.copy_from_slice(&s[..n_complex]);
21480 im.copy_from_slice(&s[n_complex..]);
21481 if use_radix4 {
21482 fft_radix4_inplace_f32(re, im, inverse);
21483 } else if is_pow2 {
21484 fft_radix2_inplace_f32(re, im, inverse);
21485 } else if use_naive {
21486 fft_naive_inplace_f32(re, im, inverse);
21487 } else {
21488 fft_bluestein_inplace_f32(re, im, inverse, scratch);
21489 }
21490 if scale != 1.0 {
21491 re.iter_mut().for_each(|v| *v *= scale);
21492 im.iter_mut().for_each(|v| *v *= scale);
21493 }
21494 let dst_offset = dst + o * row_elems * std::mem::size_of::<f32>();
21495 let d = unsafe { sl_mut(dst_offset, bp, row_elems) };
21496 d[..n_complex].copy_from_slice(re);
21497 d[n_complex..].copy_from_slice(im);
21498 };
21499
21500 let parallel = outer >= 8
21508 && (outer as u64) * (n_complex as u64) >= (1 << 15)
21509 && cpu_fft_parallel_enabled();
21510 if parallel {
21511 use rayon::prelude::*;
21512 let p = FftArenaPtr(base);
21513 (0..outer).into_par_iter().for_each_init(
21514 || {
21515 (
21516 vec![0f32; n_complex],
21517 vec![0f32; n_complex],
21518 scratch_template.clone(),
21519 )
21520 },
21521 move |(re, im, scratch), o| run_row(p.ptr(), re, im, scratch, o),
21522 );
21523 } else {
21524 let mut re = vec![0f32; n_complex];
21525 let mut im = vec![0f32; n_complex];
21526 let mut scratch = scratch_template;
21527 for o in 0..outer {
21528 run_row(base, &mut re, &mut im, &mut scratch, o);
21529 }
21530 }
21531}
21532
21533pub unsafe fn execute_fft1d_c64(
21535 src: usize,
21536 dst: usize,
21537 outer: usize,
21538 n_complex: usize,
21539 inverse: bool,
21540 norm_tag: u32,
21541 base: *mut u8,
21542) {
21543 let row_bytes = n_complex * 8;
21544 let norm = rlx_ir::fft::FftNorm::from_tag(norm_tag);
21545 let scale = norm.output_scale(n_complex, inverse) as f32;
21546 let is_pow2 = n_complex.is_power_of_two();
21547 let use_naive = !is_pow2 && n_complex <= 16;
21548 let scratch_template = if is_pow2 || use_naive {
21549 BluesteinScratchF32::empty()
21550 } else {
21551 BluesteinScratchF32::build(n_complex, inverse)
21552 };
21553
21554 let run_row = |bp: *mut u8,
21556 re: &mut [f32],
21557 im: &mut [f32],
21558 scratch: &mut BluesteinScratchF32,
21559 o: usize| {
21560 let row_offset = src + o * row_bytes;
21561 for i in 0..n_complex {
21562 let elem_off = row_offset + i * 8;
21563 re[i] = f32::from_le_bytes([
21564 unsafe { *bp.add(elem_off) },
21565 unsafe { *bp.add(elem_off + 1) },
21566 unsafe { *bp.add(elem_off + 2) },
21567 unsafe { *bp.add(elem_off + 3) },
21568 ]);
21569 im[i] = f32::from_le_bytes([
21570 unsafe { *bp.add(elem_off + 4) },
21571 unsafe { *bp.add(elem_off + 5) },
21572 unsafe { *bp.add(elem_off + 6) },
21573 unsafe { *bp.add(elem_off + 7) },
21574 ]);
21575 }
21576 if is_pow2 {
21577 fft_radix2_inplace_f32(re, im, inverse);
21578 } else if use_naive {
21579 fft_naive_inplace_f32(re, im, inverse);
21580 } else {
21581 fft_bluestein_inplace_f32(re, im, inverse, scratch);
21582 }
21583 if scale != 1.0 {
21584 re.iter_mut().for_each(|v| *v *= scale);
21585 im.iter_mut().for_each(|v| *v *= scale);
21586 }
21587 let dst_row = dst + o * row_bytes;
21588 for i in 0..n_complex {
21589 let elem_off = dst_row + i * 8;
21590 let re_b = re[i].to_le_bytes();
21591 let im_b = im[i].to_le_bytes();
21592 for j in 0..4 {
21593 unsafe { *bp.add(elem_off + j) = re_b[j] };
21594 unsafe { *bp.add(elem_off + 4 + j) = im_b[j] };
21595 }
21596 }
21597 };
21598
21599 let parallel = outer >= 8
21600 && (outer as u64) * (n_complex as u64) >= (1 << 15)
21601 && cpu_fft_parallel_enabled();
21602 if parallel {
21603 use rayon::prelude::*;
21604 let p = FftArenaPtr(base);
21605 (0..outer).into_par_iter().for_each_init(
21606 || {
21607 (
21608 vec![0f32; n_complex],
21609 vec![0f32; n_complex],
21610 scratch_template.clone(),
21611 )
21612 },
21613 move |(re, im, scratch), o| run_row(p.ptr(), re, im, scratch, o),
21614 );
21615 } else {
21616 let mut re = vec![0f32; n_complex];
21617 let mut im = vec![0f32; n_complex];
21618 let mut scratch = scratch_template;
21619 for o in 0..outer {
21620 run_row(base, &mut re, &mut im, &mut scratch, o);
21621 }
21622 }
21623}
21624
21625pub unsafe fn execute_log_mel(
21627 spec: usize,
21628 filters: usize,
21629 dst: usize,
21630 outer: usize,
21631 n_fft: usize,
21632 n_bins: usize,
21633 n_mels: usize,
21634 base: *mut u8,
21635) {
21636 execute_log_mel_f32(spec, filters, dst, outer, n_fft, n_bins, n_mels, base);
21637}
21638
21639pub unsafe fn execute_log_mel_f32(
21640 spec: usize,
21641 filters: usize,
21642 dst: usize,
21643 outer: usize,
21644 n_fft: usize,
21645 n_bins: usize,
21646 n_mels: usize,
21647 base: *mut u8,
21648) {
21649 let spec_ptr = base.add(spec) as *const f32;
21650 let filt_ptr = base.add(filters) as *const f32;
21651 let dst_ptr = base.add(dst) as *mut f32;
21652 let spec = std::slice::from_raw_parts(spec_ptr, outer * n_fft * 2);
21653 let filters = std::slice::from_raw_parts(filt_ptr, n_mels * n_bins);
21654 let out = std::slice::from_raw_parts_mut(dst_ptr, outer * n_mels);
21655 rlx_ir::audio::log_mel_block_f32(spec, filters, outer, n_fft, n_bins, n_mels, out);
21656}
21657
21658pub unsafe fn execute_welch_peaks_f32(
21659 spec: usize,
21660 dst: usize,
21661 welch_batch: usize,
21662 n_fft: usize,
21663 n_segments: usize,
21664 k: usize,
21665 base: *mut u8,
21666) {
21667 let spec_ptr = base.add(spec) as *const f32;
21668 let dst_ptr = base.add(dst) as *mut f32;
21669 let outer = welch_batch * n_segments;
21670 let spec = std::slice::from_raw_parts(spec_ptr, outer * n_fft * 2);
21671 let out = std::slice::from_raw_parts_mut(dst_ptr, welch_batch * k * 2);
21672 rlx_ir::audio::welch_peaks_block_f32(spec, welch_batch, n_fft, n_segments, k, out);
21673}
21674
21675pub unsafe fn execute_log_mel_backward_f32(
21676 spec: usize,
21677 filters: usize,
21678 dy: usize,
21679 dst: usize,
21680 outer: usize,
21681 n_fft: usize,
21682 n_bins: usize,
21683 n_mels: usize,
21684 base: *mut u8,
21685) {
21686 let spec_ptr = base.add(spec) as *const f32;
21687 let filt_ptr = base.add(filters) as *const f32;
21688 let dy_ptr = base.add(dy) as *const f32;
21689 let dst_ptr = base.add(dst) as *mut f32;
21690 let spec = std::slice::from_raw_parts(spec_ptr, outer * n_fft * 2);
21691 let filters = std::slice::from_raw_parts(filt_ptr, n_mels * n_bins);
21692 let dy = std::slice::from_raw_parts(dy_ptr, outer * n_mels);
21693 let d_spec = std::slice::from_raw_parts_mut(dst_ptr, outer * n_fft * 2);
21694 d_spec.fill(0.0);
21695 rlx_ir::audio::log_mel_block_vjp(spec, filters, dy, outer, n_fft, n_bins, n_mels, d_spec);
21696}
21697
21698pub unsafe fn execute_fft1d(
21700 src: usize,
21701 dst: usize,
21702 outer: usize,
21703 n_complex: usize,
21704 inverse: bool,
21705 norm_tag: u32,
21706 dtype: rlx_ir::DType,
21707 base: *mut u8,
21708) {
21709 match dtype {
21710 rlx_ir::DType::F32 => {
21711 execute_fft1d_f32(src, dst, outer, n_complex, inverse, norm_tag, base)
21712 }
21713 rlx_ir::DType::F64 => {
21714 execute_fft1d_f64(src, dst, outer, n_complex, inverse, norm_tag, base)
21715 }
21716 rlx_ir::DType::C64 => {
21717 execute_fft1d_c64(src, dst, outer, n_complex, inverse, norm_tag, base)
21718 }
21719 other => panic!("execute_fft1d: unsupported dtype {other:?}"),
21720 }
21721}
21722
21723fn fft_radix2_inplace_f32(re: &mut [f32], im: &mut [f32], inverse: bool) {
21728 let n = re.len();
21729 debug_assert_eq!(im.len(), n);
21730 debug_assert!(
21731 n.is_power_of_two(),
21732 "fft_radix2_f32: n={n} must be a power of two"
21733 );
21734 if n <= 1 {
21735 return;
21736 }
21737
21738 let mut j = 0usize;
21739 for i in 1..n {
21740 let mut bit = n >> 1;
21741 while j & bit != 0 {
21742 j ^= bit;
21743 bit >>= 1;
21744 }
21745 j ^= bit;
21746 if i < j {
21747 re.swap(i, j);
21748 im.swap(i, j);
21749 }
21750 }
21751
21752 let sign = if inverse { 1.0_f64 } else { -1.0_f64 };
21753 let mut len = 2usize;
21754 while len <= n {
21755 let half = len / 2;
21756 let theta = sign * 2.0 * std::f64::consts::PI / (len as f64);
21757 let w_re_step = theta.cos();
21758 let w_im_step = theta.sin();
21759 let mut i = 0usize;
21760 while i < n {
21761 let mut wre = 1.0_f64;
21762 let mut wim = 0.0_f64;
21763 for k in 0..half {
21764 let wre_f = wre as f32;
21765 let wim_f = wim as f32;
21766 let t_re = wre_f * re[i + k + half] - wim_f * im[i + k + half];
21767 let t_im = wre_f * im[i + k + half] + wim_f * re[i + k + half];
21768 let u_re = re[i + k];
21769 let u_im = im[i + k];
21770 re[i + k] = u_re + t_re;
21771 im[i + k] = u_im + t_im;
21772 re[i + k + half] = u_re - t_re;
21773 im[i + k + half] = u_im - t_im;
21774 let new_wre = wre * w_re_step - wim * w_im_step;
21775 let new_wim = wre * w_im_step + wim * w_re_step;
21776 wre = new_wre;
21777 wim = new_wim;
21778 }
21779 i += len;
21780 }
21781 len <<= 1;
21782 }
21783}
21784
21785#[inline]
21788fn is_pow4(n: usize) -> bool {
21789 n.is_power_of_two() && n.trailing_zeros().is_multiple_of(2)
21790}
21791
21792fn fft_radix4_inplace_f32(re: &mut [f32], im: &mut [f32], inverse: bool) {
21798 let n = re.len();
21799 debug_assert_eq!(im.len(), n);
21800 debug_assert!(is_pow4(n), "fft_radix4_f32: n={n} must be a power of four");
21801 if n <= 1 {
21802 return;
21803 }
21804 let m = n.trailing_zeros() / 2; for i in 0..n {
21808 let mut x = i;
21809 let mut r = 0usize;
21810 for _ in 0..m {
21811 r = (r << 2) | (x & 3);
21812 x >>= 2;
21813 }
21814 if i < r {
21815 re.swap(i, r);
21816 im.swap(i, r);
21817 }
21818 }
21819
21820 let jsign = if inverse { 1.0_f32 } else { -1.0_f32 };
21823 let tw_sign = if inverse { 1.0_f64 } else { -1.0_f64 };
21824
21825 let mut len = 4usize;
21826 while len <= n {
21827 let q = len / 4;
21828 let theta = tw_sign * 2.0 * std::f64::consts::PI / (len as f64);
21829 let wstep_re = theta.cos();
21830 let wstep_im = theta.sin();
21831 let mut base = 0usize;
21832 while base < n {
21833 let mut w1_re = 1.0_f64;
21834 let mut w1_im = 0.0_f64;
21835 for k in 0..q {
21836 let w2_re = w1_re * w1_re - w1_im * w1_im;
21838 let w2_im = 2.0 * w1_re * w1_im;
21839 let w3_re = w2_re * w1_re - w2_im * w1_im;
21840 let w3_im = w2_re * w1_im + w2_im * w1_re;
21841
21842 let (i0, i1, i2, i3) = (base + k, base + k + q, base + k + 2 * q, base + k + 3 * q);
21843 let a_re = re[i0];
21844 let a_im = im[i0];
21845 let (w1r, w1i) = (w1_re as f32, w1_im as f32);
21846 let (w2r, w2i) = (w2_re as f32, w2_im as f32);
21847 let (w3r, w3i) = (w3_re as f32, w3_im as f32);
21848 let b_re = w1r * re[i1] - w1i * im[i1];
21849 let b_im = w1r * im[i1] + w1i * re[i1];
21850 let c_re = w2r * re[i2] - w2i * im[i2];
21851 let c_im = w2r * im[i2] + w2i * re[i2];
21852 let d_re = w3r * re[i3] - w3i * im[i3];
21853 let d_im = w3r * im[i3] + w3i * re[i3];
21854
21855 let t0_re = a_re + c_re;
21856 let t0_im = a_im + c_im;
21857 let t1_re = a_re - c_re;
21858 let t1_im = a_im - c_im;
21859 let t2_re = b_re + d_re;
21860 let t2_im = b_im + d_im;
21861 let t3_re = b_re - d_re;
21862 let t3_im = b_im - d_im;
21863 let jt3_re = -jsign * t3_im;
21866 let jt3_im = jsign * t3_re;
21867
21868 re[i0] = t0_re + t2_re;
21869 im[i0] = t0_im + t2_im;
21870 re[i2] = t0_re - t2_re;
21871 im[i2] = t0_im - t2_im;
21872 re[i1] = t1_re + jt3_re;
21873 im[i1] = t1_im + jt3_im;
21874 re[i3] = t1_re - jt3_re;
21875 im[i3] = t1_im - jt3_im;
21876
21877 let nw_re = w1_re * wstep_re - w1_im * wstep_im;
21878 let nw_im = w1_re * wstep_im + w1_im * wstep_re;
21879 w1_re = nw_re;
21880 w1_im = nw_im;
21881 }
21882 base += len;
21883 }
21884 len <<= 2;
21885 }
21886}
21887
21888fn fft_radix2_inplace_f64(re: &mut [f64], im: &mut [f64], inverse: bool) {
21892 let n = re.len();
21893 debug_assert_eq!(im.len(), n);
21894 debug_assert!(
21895 n.is_power_of_two(),
21896 "fft_radix2: n={n} must be a power of two"
21897 );
21898 if n <= 1 {
21899 return;
21900 }
21901
21902 let mut j = 0usize;
21904 for i in 1..n {
21905 let mut bit = n >> 1;
21906 while j & bit != 0 {
21907 j ^= bit;
21908 bit >>= 1;
21909 }
21910 j ^= bit;
21911 if i < j {
21912 re.swap(i, j);
21913 im.swap(i, j);
21914 }
21915 }
21916
21917 let sign = if inverse { 1.0 } else { -1.0 };
21919 let mut len = 2usize;
21920 while len <= n {
21921 let half = len / 2;
21922 let theta = sign * 2.0 * std::f64::consts::PI / (len as f64);
21923 let w_re_step = theta.cos();
21924 let w_im_step = theta.sin();
21925 let mut i = 0usize;
21926 while i < n {
21927 let mut wre = 1.0_f64;
21929 let mut wim = 0.0_f64;
21930 for k in 0..half {
21931 let t_re = wre * re[i + k + half] - wim * im[i + k + half];
21932 let t_im = wre * im[i + k + half] + wim * re[i + k + half];
21933 let u_re = re[i + k];
21934 let u_im = im[i + k];
21935 re[i + k] = u_re + t_re;
21936 im[i + k] = u_im + t_im;
21937 re[i + k + half] = u_re - t_re;
21938 im[i + k + half] = u_im - t_im;
21939 let new_wre = wre * w_re_step - wim * w_im_step;
21940 let new_wim = wre * w_im_step + wim * w_re_step;
21941 wre = new_wre;
21942 wim = new_wim;
21943 }
21944 i += len;
21945 }
21946 len <<= 1;
21947 }
21948}
21949
21950#[derive(Clone)]
21954struct BluesteinScratchF64 {
21955 m: usize,
21957 w_re: Vec<f64>,
21961 w_im: Vec<f64>,
21962 bf_re: Vec<f64>,
21965 bf_im: Vec<f64>,
21966 ar: Vec<f64>,
21968 ai: Vec<f64>,
21969}
21970
21971impl BluesteinScratchF64 {
21972 fn empty() -> Self {
21973 Self {
21974 m: 0,
21975 w_re: Vec::new(),
21976 w_im: Vec::new(),
21977 bf_re: Vec::new(),
21978 bf_im: Vec::new(),
21979 ar: Vec::new(),
21980 ai: Vec::new(),
21981 }
21982 }
21983
21984 fn build(n: usize, inverse: bool) -> Self {
21985 let m = if n <= 1 {
21988 1
21989 } else {
21990 (2 * n - 1).next_power_of_two()
21991 };
21992
21993 let mod_2n = (2 * n) as u64;
21996 let sign = if inverse { 1.0_f64 } else { -1.0_f64 };
21997 let mut w_re = vec![0.0_f64; n];
21998 let mut w_im = vec![0.0_f64; n];
21999 for k in 0..n {
22000 let k2 = (k as u64).wrapping_mul(k as u64) % mod_2n;
22001 let theta = sign * std::f64::consts::PI * (k2 as f64) / (n as f64);
22002 w_re[k] = theta.cos();
22003 w_im[k] = theta.sin();
22004 }
22005
22006 let mut bf_re = vec![0.0_f64; m];
22009 let mut bf_im = vec![0.0_f64; m];
22010 if n > 0 {
22011 bf_re[0] = w_re[0];
22012 bf_im[0] = -w_im[0];
22013 for k in 1..n {
22014 bf_re[k] = w_re[k];
22015 bf_im[k] = -w_im[k];
22016 bf_re[m - k] = w_re[k];
22017 bf_im[m - k] = -w_im[k];
22018 }
22019 }
22020 if m > 1 {
22021 fft_radix2_inplace_f64(&mut bf_re, &mut bf_im, false);
22022 }
22023
22024 Self {
22025 m,
22026 w_re,
22027 w_im,
22028 bf_re,
22029 bf_im,
22030 ar: vec![0.0_f64; m],
22031 ai: vec![0.0_f64; m],
22032 }
22033 }
22034}
22035
22036fn fft_naive_inplace_f64(re: &mut [f64], im: &mut [f64], inverse: bool) {
22038 let n = re.len();
22039 if n <= 1 {
22040 return;
22041 }
22042 let sign = if inverse { 1.0 } else { -1.0 };
22043 let mut out_re = vec![0.0_f64; n];
22044 let mut out_im = vec![0.0_f64; n];
22045 for k in 0..n {
22046 for nn in 0..n {
22047 let theta = sign * 2.0 * std::f64::consts::PI * (nn as f64) * (k as f64) / (n as f64);
22048 let c = theta.cos();
22049 let s = theta.sin();
22050 out_re[k] += re[nn] * c - im[nn] * s;
22051 out_im[k] += re[nn] * s + im[nn] * c;
22052 }
22053 }
22054 re.copy_from_slice(&out_re);
22055 im.copy_from_slice(&out_im);
22056}
22057
22058fn fft_naive_inplace_f32(re: &mut [f32], im: &mut [f32], inverse: bool) {
22059 let n = re.len();
22060 if n <= 1 {
22061 return;
22062 }
22063 let sign = if inverse { 1.0f32 } else { -1.0f32 };
22064 let mut out_re = vec![0.0_f32; n];
22065 let mut out_im = vec![0.0_f32; n];
22066 for k in 0..n {
22067 for nn in 0..n {
22068 let theta = sign * 2.0 * std::f32::consts::PI * (nn as f32) * (k as f32) / (n as f32);
22069 let c = theta.cos();
22070 let s = theta.sin();
22071 out_re[k] += re[nn] * c - im[nn] * s;
22072 out_im[k] += re[nn] * s + im[nn] * c;
22073 }
22074 }
22075 re.copy_from_slice(&out_re);
22076 im.copy_from_slice(&out_im);
22077}
22078
22079fn fft_bluestein_inplace_f64(
22088 re: &mut [f64],
22089 im: &mut [f64],
22090 _inverse: bool,
22091 s: &mut BluesteinScratchF64,
22092) {
22093 let n = re.len();
22094 debug_assert_eq!(im.len(), n);
22095 debug_assert_eq!(s.w_re.len(), n);
22096 if n <= 1 {
22097 return;
22098 }
22099 let m = s.m;
22100
22101 for k in 0..m {
22103 s.ar[k] = 0.0;
22104 s.ai[k] = 0.0;
22105 }
22106 for k in 0..n {
22107 s.ar[k] = re[k] * s.w_re[k] - im[k] * s.w_im[k];
22108 s.ai[k] = re[k] * s.w_im[k] + im[k] * s.w_re[k];
22109 }
22110
22111 fft_radix2_inplace_f64(&mut s.ar, &mut s.ai, false);
22113
22114 for k in 0..m {
22116 let ar = s.ar[k];
22117 let ai = s.ai[k];
22118 let br = s.bf_re[k];
22119 let bi = s.bf_im[k];
22120 s.ar[k] = ar * br - ai * bi;
22121 s.ai[k] = ar * bi + ai * br;
22122 }
22123
22124 fft_radix2_inplace_f64(&mut s.ar, &mut s.ai, true);
22127 let inv_m = 1.0 / (m as f64);
22128
22129 for k in 0..n {
22131 let yr = s.ar[k] * inv_m;
22132 let yi = s.ai[k] * inv_m;
22133 re[k] = yr * s.w_re[k] - yi * s.w_im[k];
22134 im[k] = yr * s.w_im[k] + yi * s.w_re[k];
22135 }
22136}
22137
22138#[derive(Clone)]
22145struct BluesteinScratchF32 {
22146 m: usize,
22147 w_re: Vec<f32>,
22148 w_im: Vec<f32>,
22149 bf_re: Vec<f32>,
22150 bf_im: Vec<f32>,
22151 ar: Vec<f32>,
22152 ai: Vec<f32>,
22153}
22154
22155impl BluesteinScratchF32 {
22156 fn empty() -> Self {
22157 Self {
22158 m: 0,
22159 w_re: Vec::new(),
22160 w_im: Vec::new(),
22161 bf_re: Vec::new(),
22162 bf_im: Vec::new(),
22163 ar: Vec::new(),
22164 ai: Vec::new(),
22165 }
22166 }
22167
22168 fn build(n: usize, inverse: bool) -> Self {
22169 let m = if n <= 1 {
22170 1
22171 } else {
22172 (2 * n - 1).next_power_of_two()
22173 };
22174
22175 let mod_2n = (2 * n) as u64;
22176 let sign = if inverse { 1.0_f64 } else { -1.0_f64 };
22177 let mut w_re = vec![0.0_f32; n];
22178 let mut w_im = vec![0.0_f32; n];
22179 for k in 0..n {
22180 let k2 = (k as u64).wrapping_mul(k as u64) % mod_2n;
22181 let theta = sign * std::f64::consts::PI * (k2 as f64) / (n as f64);
22182 w_re[k] = theta.cos() as f32;
22183 w_im[k] = theta.sin() as f32;
22184 }
22185
22186 let mut bf_re = vec![0.0_f32; m];
22187 let mut bf_im = vec![0.0_f32; m];
22188 if n > 0 {
22189 bf_re[0] = w_re[0];
22190 bf_im[0] = -w_im[0];
22191 for k in 1..n {
22192 bf_re[k] = w_re[k];
22193 bf_im[k] = -w_im[k];
22194 bf_re[m - k] = w_re[k];
22195 bf_im[m - k] = -w_im[k];
22196 }
22197 }
22198 if m > 1 {
22199 fft_radix2_inplace_f32(&mut bf_re, &mut bf_im, false);
22200 }
22201
22202 Self {
22203 m,
22204 w_re,
22205 w_im,
22206 bf_re,
22207 bf_im,
22208 ar: vec![0.0_f32; m],
22209 ai: vec![0.0_f32; m],
22210 }
22211 }
22212}
22213
22214fn fft_bluestein_inplace_f32(
22215 re: &mut [f32],
22216 im: &mut [f32],
22217 _inverse: bool,
22218 s: &mut BluesteinScratchF32,
22219) {
22220 let n = re.len();
22221 debug_assert_eq!(im.len(), n);
22222 debug_assert_eq!(s.w_re.len(), n);
22223 if n <= 1 {
22224 return;
22225 }
22226 let m = s.m;
22227
22228 for k in 0..m {
22229 s.ar[k] = 0.0;
22230 s.ai[k] = 0.0;
22231 }
22232 for k in 0..n {
22233 s.ar[k] = re[k] * s.w_re[k] - im[k] * s.w_im[k];
22234 s.ai[k] = re[k] * s.w_im[k] + im[k] * s.w_re[k];
22235 }
22236
22237 fft_radix2_inplace_f32(&mut s.ar, &mut s.ai, false);
22238
22239 for k in 0..m {
22240 let ar = s.ar[k];
22241 let ai = s.ai[k];
22242 let br = s.bf_re[k];
22243 let bi = s.bf_im[k];
22244 s.ar[k] = ar * br - ai * bi;
22245 s.ai[k] = ar * bi + ai * br;
22246 }
22247
22248 fft_radix2_inplace_f32(&mut s.ar, &mut s.ai, true);
22249 let inv_m = 1.0_f32 / (m as f32);
22250
22251 for k in 0..n {
22252 let yr = s.ar[k] * inv_m;
22253 let yi = s.ai[k] * inv_m;
22254 re[k] = yr * s.w_re[k] - yi * s.w_im[k];
22255 im[k] = yr * s.w_im[k] + yi * s.w_re[k];
22256 }
22257}
22258
22259unsafe fn dispatch_custom_op(
22265 kernel: &dyn crate::op_registry::CpuKernel,
22266 inputs: &[(usize, u32, Shape)],
22267 out_off: usize,
22268 out_len: u32,
22269 out_shape: &Shape,
22270 attrs: &[u8],
22271 base: *mut u8,
22272) {
22273 use crate::op_registry::{CpuTensorMut, CpuTensorRef};
22274 use rlx_ir::DType;
22275
22276 macro_rules! build_in_view {
22281 ($shape:expr, $off:expr, $n:expr, $variant:ident, $rust_ty:ty) => {
22282 CpuTensorRef::$variant {
22283 data: unsafe { sl_typed::<$rust_ty>($off, base, $n) },
22284 shape: $shape,
22285 }
22286 };
22287 }
22288 macro_rules! build_out_view {
22289 ($variant:ident, $rust_ty:ty) => {
22290 CpuTensorMut::$variant {
22291 data: unsafe { sl_mut_typed::<$rust_ty>(out_off, base, out_len as usize) },
22292 shape: out_shape,
22293 }
22294 };
22295 }
22296
22297 let in_views: Vec<CpuTensorRef<'_>> = inputs
22298 .iter()
22299 .map(|(off, len, shape)| {
22300 let n = *len as usize;
22301 let off = *off;
22302 match shape.dtype() {
22303 DType::F32 => build_in_view!(shape, off, n, F32, f32),
22304 DType::F64 => build_in_view!(shape, off, n, F64, f64),
22305 DType::F16 => build_in_view!(shape, off, n, F16, half::f16),
22306 DType::BF16 => build_in_view!(shape, off, n, BF16, half::bf16),
22307 DType::I8 => build_in_view!(shape, off, n, I8, i8),
22308 DType::I16 => build_in_view!(shape, off, n, I16, i16),
22309 DType::I32 => build_in_view!(shape, off, n, I32, i32),
22310 DType::I64 => build_in_view!(shape, off, n, I64, i64),
22311 DType::U8 => build_in_view!(shape, off, n, U8, u8),
22312 DType::U32 => build_in_view!(shape, off, n, U32, u32),
22313 DType::Bool => build_in_view!(shape, off, n, Bool, u8),
22314 DType::C64 => panic!(
22318 "Op::Custom kernel input has DType::C64 — built-in \
22319 complex ops handle their own kernels; user-registered \
22320 ops don't yet see complex tensors"
22321 ),
22322 }
22323 })
22324 .collect();
22325
22326 let result = match out_shape.dtype() {
22327 DType::F32 => kernel.execute(&in_views, build_out_view!(F32, f32), attrs),
22328 DType::F64 => kernel.execute(&in_views, build_out_view!(F64, f64), attrs),
22329 DType::F16 => kernel.execute(&in_views, build_out_view!(F16, half::f16), attrs),
22330 DType::BF16 => kernel.execute(&in_views, build_out_view!(BF16, half::bf16), attrs),
22331 DType::I8 => kernel.execute(&in_views, build_out_view!(I8, i8), attrs),
22332 DType::I16 => kernel.execute(&in_views, build_out_view!(I16, i16), attrs),
22333 DType::I32 => kernel.execute(&in_views, build_out_view!(I32, i32), attrs),
22334 DType::I64 => kernel.execute(&in_views, build_out_view!(I64, i64), attrs),
22335 DType::U8 => kernel.execute(&in_views, build_out_view!(U8, u8), attrs),
22336 DType::U32 => kernel.execute(&in_views, build_out_view!(U32, u32), attrs),
22337 DType::Bool => kernel.execute(&in_views, build_out_view!(Bool, u8), attrs),
22338 DType::C64 => panic!("Op::Custom output DType::C64 not supported"),
22339 };
22340 if let Err(e) = result {
22341 panic!("Op::Custom('{}') CPU kernel failed: {e}", kernel.name());
22342 }
22343}
22344
22345#[inline(always)]
22351unsafe fn sl_typed<T>(offset: usize, base: *mut u8, len: usize) -> &'static [T] {
22352 if offset == usize::MAX {
22353 return &[];
22354 }
22355 unsafe { std::slice::from_raw_parts(base.add(offset) as *const T, len) }
22356}
22357
22358#[inline(always)]
22359unsafe fn sl_mut_typed<T>(offset: usize, base: *mut u8, len: usize) -> &'static mut [T] {
22360 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut T, len) }
22361}
22362
22363#[inline]
22366fn region_activation_scalar(act: rlx_ir::op::Activation, x: f32) -> f32 {
22367 use rlx_ir::op::Activation as A;
22368 const GC: f32 = 0.797_884_6; match act {
22370 A::Relu => x.max(0.0),
22371 A::Gelu | A::GeluApprox => 0.5 * x * (1.0 + (GC * (x + 0.044715 * x * x * x)).tanh()),
22372 A::Silu => x / (1.0 + (-x).exp()),
22373 A::Sigmoid => 1.0 / (1.0 + (-x).exp()),
22374 A::Tanh => x.tanh(),
22375 A::Exp => x.exp(),
22376 A::Log => x.ln(),
22377 A::Sqrt => x.sqrt(),
22378 A::Rsqrt => 1.0 / x.sqrt(),
22379 A::Neg => -x,
22380 A::Abs => x.abs(),
22381 A::Sin => x.sin(),
22382 A::Cos => x.cos(),
22383 A::Tan => x.tan(),
22384 A::Atan => x.atan(),
22385 A::Round => x.round(),
22386 }
22387}
22388
22389#[inline]
22390fn region_binary_scalar(op: rlx_ir::op::BinaryOp, l: f32, r: f32) -> f32 {
22391 use rlx_ir::op::BinaryOp as B;
22392 match op {
22393 B::Add => l + r,
22394 B::Sub => l - r,
22395 B::Mul => l * r,
22396 B::Div => l / r,
22397 B::Max => l.max(r),
22398 B::Min => l.min(r),
22399 B::Pow => l.powf(r),
22400 }
22401}
22402
22403#[inline]
22404fn region_compare_scalar(op: rlx_ir::op::CmpOp, l: f32, r: f32) -> bool {
22405 use rlx_ir::op::CmpOp as C;
22406 match op {
22407 C::Eq => l == r,
22408 C::Ne => l != r,
22409 C::Lt => l < r,
22410 C::Le => l <= r,
22411 C::Gt => l > r,
22412 C::Ge => l >= r,
22413 }
22414}
22415
22416#[inline]
22420fn region_resolve_operand(
22421 op: &rlx_ir::op::ChainOperand,
22422 gid: usize,
22423 base: *const u8,
22424 input_offs: &[usize],
22425 scalar_mask: u32,
22426 modulus: &[u32; 16],
22427 scratch: &[f32; 32],
22428) -> f32 {
22429 use rlx_ir::op::ChainOperand as O;
22430 match op {
22431 O::Step(s) => scratch[*s as usize],
22432 O::Input(i) => {
22433 let i = *i as usize;
22434 let row = if (scalar_mask >> i) & 1 == 1 {
22435 0
22436 } else if modulus[i] != 0 {
22437 gid % modulus[i] as usize
22438 } else {
22439 gid
22440 };
22441 unsafe { *(base.add(input_offs[i]) as *const f32).add(row) }
22442 }
22443 }
22444}
22445
22446#[inline]
22449fn region_eval_elem(
22450 gid: usize,
22451 base: *const u8,
22452 input_offs: &[usize],
22453 chain: &[rlx_ir::op::ChainStep],
22454 scalar_mask: u32,
22455 modulus: &[u32; 16],
22456) -> f32 {
22457 use rlx_ir::op::ChainStep as S;
22458 let mut scratch = [0f32; 32];
22459 let r = |o: &rlx_ir::op::ChainOperand, sc: &[f32; 32]| {
22460 region_resolve_operand(o, gid, base, input_offs, scalar_mask, modulus, sc)
22461 };
22462 for (k, step) in chain.iter().enumerate() {
22463 scratch[k] = match step {
22464 S::Activation(a, x) => region_activation_scalar(*a, r(x, &scratch)),
22465 S::Cast(_, x) => r(x, &scratch), S::Binary(op, l, rr) => region_binary_scalar(*op, r(l, &scratch), r(rr, &scratch)),
22467 S::Compare(op, l, rr) => {
22468 if region_compare_scalar(*op, r(l, &scratch), r(rr, &scratch)) {
22469 1.0
22470 } else {
22471 0.0
22472 }
22473 }
22474 S::Where(c, t, f) => {
22475 if r(c, &scratch) != 0.0 {
22476 r(t, &scratch)
22477 } else {
22478 r(f, &scratch)
22479 }
22480 }
22481 };
22482 }
22483 scratch[chain.len() - 1]
22484}
22485
22486#[inline(always)]
22491fn apply_activation_inplace(d: &mut [f32], act: rlx_ir::op::Activation) {
22492 use rlx_ir::op::Activation;
22493 match act {
22494 Activation::Gelu => crate::kernels::par_gelu_inplace(d),
22495 Activation::GeluApprox => crate::kernels::par_gelu_approx_inplace(d),
22496 Activation::Silu => crate::kernels::par_silu_inplace(d),
22497 Activation::Relu => {
22498 for v in d.iter_mut() {
22499 *v = v.max(0.0);
22500 }
22501 }
22502 Activation::Sigmoid => {
22503 for v in d.iter_mut() {
22504 *v = 1.0 / (1.0 + (-*v).exp());
22505 }
22506 }
22507 Activation::Tanh => {
22508 for v in d.iter_mut() {
22509 *v = v.tanh();
22510 }
22511 }
22512 Activation::Exp => {
22513 for v in d.iter_mut() {
22514 *v = v.exp();
22515 }
22516 }
22517 Activation::Log => {
22518 for v in d.iter_mut() {
22519 *v = v.ln();
22520 }
22521 }
22522 Activation::Sqrt => {
22523 for v in d.iter_mut() {
22524 *v = v.sqrt();
22525 }
22526 }
22527 Activation::Rsqrt => {
22528 for v in d.iter_mut() {
22529 *v = 1.0 / v.sqrt();
22530 }
22531 }
22532 Activation::Neg => {
22533 for v in d.iter_mut() {
22534 *v = -*v;
22535 }
22536 }
22537 Activation::Abs => {
22538 for v in d.iter_mut() {
22539 *v = v.abs();
22540 }
22541 }
22542 Activation::Round => {
22543 for v in d.iter_mut() {
22544 *v = v.round();
22545 }
22546 }
22547 Activation::Sin => {
22548 for v in d.iter_mut() {
22549 *v = v.sin();
22550 }
22551 }
22552 Activation::Cos => {
22553 for v in d.iter_mut() {
22554 *v = v.cos();
22555 }
22556 }
22557 Activation::Tan => {
22558 for v in d.iter_mut() {
22559 *v = v.tan();
22560 }
22561 }
22562 Activation::Atan => {
22563 for v in d.iter_mut() {
22564 *v = v.atan();
22565 }
22566 }
22567 }
22568}
22569
22570#[allow(clippy::too_many_arguments)]
22579fn fast_conv_enabled() -> bool {
22585 use std::sync::OnceLock;
22586 static FAST_CONV: OnceLock<bool> = OnceLock::new();
22587 *FAST_CONV.get_or_init(|| {
22588 matches!(
22589 rlx_ir::env::var("RLX_FAST_CONV").as_deref(),
22590 Some("1") | Some("on") | Some("true") | Some("yes")
22591 )
22592 })
22593}
22594
22595#[allow(clippy::too_many_arguments)]
22600fn conv2d_forward_naive(
22601 inp: &[f32],
22602 wt: &[f32],
22603 out: &mut [f32],
22604 n: usize,
22605 c_in: usize,
22606 h: usize,
22607 w: usize,
22608 c_out: usize,
22609 h_out: usize,
22610 w_out: usize,
22611 kh: usize,
22612 kw: usize,
22613 sh: usize,
22614 sw: usize,
22615 ph: usize,
22616 pw: usize,
22617 dh: usize,
22618 dw: usize,
22619 groups: usize,
22620) {
22621 let c_in_per_g = c_in / groups;
22622 let c_out_per_g = c_out / groups;
22623 for ni in 0..n {
22624 for co in 0..c_out {
22625 let g = co / c_out_per_g;
22626 let ci_start = g * c_in_per_g;
22627 for ho in 0..h_out {
22628 for wo in 0..w_out {
22629 let mut acc = 0f32;
22630 for ci_off in 0..c_in_per_g {
22631 let ci = ci_start + ci_off;
22632 let in_chan = ((ni * c_in) + ci) * h * w;
22633 let wt_chan = ((co * c_in_per_g) + ci_off) * kh * kw;
22634 for ki in 0..kh {
22635 for kj in 0..kw {
22636 let hi = ho * sh + ki * dh;
22637 let wi = wo * sw + kj * dw;
22638 if hi < ph || wi < pw {
22639 continue;
22640 }
22641 let hi = hi - ph;
22642 let wi = wi - pw;
22643 if hi >= h || wi >= w {
22644 continue;
22645 }
22646 acc += inp[in_chan + hi * w + wi] * wt[wt_chan + ki * kw + kj];
22647 }
22648 }
22649 }
22650 out[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = acc;
22651 }
22652 }
22653 }
22654 }
22655}
22656
22657fn winograd_enabled() -> bool {
22660 use std::sync::OnceLock;
22661 static W: OnceLock<bool> = OnceLock::new();
22662 *W.get_or_init(|| {
22663 matches!(
22664 rlx_ir::env::var("RLX_WINOGRAD").as_deref(),
22665 Some("1") | Some("on") | Some("true") | Some("yes")
22666 )
22667 })
22668}
22669
22670fn direct_conv_enabled() -> bool {
22673 use std::sync::OnceLock;
22674 static D: OnceLock<bool> = OnceLock::new();
22675 *D.get_or_init(|| {
22676 matches!(
22677 rlx_ir::env::var("RLX_DIRECT_CONV").as_deref(),
22678 Some("1") | Some("on") | Some("true") | Some("yes")
22679 )
22680 })
22681}
22682
22683#[inline]
22685fn winograd_filter_transform(g: &[f32]) -> [f32; 16] {
22686 let mut tmp = [0f32; 12]; for j in 0..3 {
22688 let (g0, g1, g2) = (g[j], g[3 + j], g[6 + j]);
22689 tmp[j] = g0;
22690 tmp[3 + j] = 0.5 * (g0 + g1 + g2);
22691 tmp[6 + j] = 0.5 * (g0 - g1 + g2);
22692 tmp[9 + j] = g2;
22693 }
22694 let mut u = [0f32; 16];
22695 for i in 0..4 {
22696 let (t0, t1, t2) = (tmp[i * 3], tmp[i * 3 + 1], tmp[i * 3 + 2]);
22697 u[i * 4] = t0;
22698 u[i * 4 + 1] = 0.5 * (t0 + t1 + t2);
22699 u[i * 4 + 2] = 0.5 * (t0 - t1 + t2);
22700 u[i * 4 + 3] = t2;
22701 }
22702 u
22703}
22704
22705#[inline]
22707fn winograd_input_transform(d: &[f32; 16]) -> [f32; 16] {
22708 let mut t = [0f32; 16];
22709 for j in 0..4 {
22710 let (d0, d1, d2, d3) = (d[j], d[4 + j], d[8 + j], d[12 + j]);
22711 t[j] = d0 - d2;
22712 t[4 + j] = d1 + d2;
22713 t[8 + j] = d2 - d1;
22714 t[12 + j] = d1 - d3;
22715 }
22716 let mut v = [0f32; 16];
22717 for i in 0..4 {
22718 let (a0, a1, a2, a3) = (t[i * 4], t[i * 4 + 1], t[i * 4 + 2], t[i * 4 + 3]);
22719 v[i * 4] = a0 - a2;
22720 v[i * 4 + 1] = a1 + a2;
22721 v[i * 4 + 2] = a2 - a1;
22722 v[i * 4 + 3] = a1 - a3;
22723 }
22724 v
22725}
22726
22727#[inline]
22729fn winograd_output_transform(m: &[f32; 16]) -> [f32; 4] {
22730 let mut s = [0f32; 8];
22731 for j in 0..4 {
22732 let (m0, m1, m2, m3) = (m[j], m[4 + j], m[8 + j], m[12 + j]);
22733 s[j] = m0 + m1 + m2;
22734 s[4 + j] = m1 - m2 - m3;
22735 }
22736 let mut y = [0f32; 4];
22737 for i in 0..2 {
22738 let (a0, a1, a2, a3) = (s[i * 4], s[i * 4 + 1], s[i * 4 + 2], s[i * 4 + 3]);
22739 y[i * 2] = a0 + a1 + a2;
22740 y[i * 2 + 1] = a1 - a2 - a3;
22741 }
22742 y
22743}
22744
22745#[allow(clippy::too_many_arguments)]
22751fn conv2d_forward_winograd(
22752 inp: &[f32],
22753 wt: &[f32],
22754 out: &mut [f32],
22755 n: usize,
22756 c_in: usize,
22757 h: usize,
22758 w: usize,
22759 c_out: usize,
22760 h_out: usize,
22761 w_out: usize,
22762) {
22763 let th = h_out.div_ceil(2);
22764 let tw = w_out.div_ceil(2);
22765 let tiles_per = th * tw;
22766 let nt = n * tiles_per;
22767 if nt == 0 {
22768 return;
22769 }
22770
22771 let mut u = vec![0f32; 16 * c_out * c_in];
22773 for co in 0..c_out {
22774 for ci in 0..c_in {
22775 let uu = winograd_filter_transform(&wt[(co * c_in + ci) * 9..(co * c_in + ci) * 9 + 9]);
22776 for (p, &val) in uu.iter().enumerate() {
22777 u[p * c_out * c_in + co * c_in + ci] = val;
22778 }
22779 }
22780 }
22781
22782 let mut v = vec![0f32; 16 * c_in * nt];
22784 let v_addr = v.as_mut_ptr() as usize;
22785 let in_xform = |tile: usize| {
22786 let ni = tile / tiles_per;
22787 let rem = tile % tiles_per;
22788 let (h0, w0) = (2 * (rem / tw), 2 * (rem % tw));
22789 for ci in 0..c_in {
22790 let mut d = [0f32; 16];
22791 for di in 0..4 {
22792 let hh = h0 + di;
22793 if hh >= h {
22794 continue;
22795 }
22796 let base = ((ni * c_in + ci) * h + hh) * w;
22797 for dj in 0..4 {
22798 let ww = w0 + dj;
22799 if ww < w {
22800 d[di * 4 + dj] = inp[base + ww];
22801 }
22802 }
22803 }
22804 let vv = winograd_input_transform(&d);
22805 for (p, &val) in vv.iter().enumerate() {
22806 unsafe {
22807 *((v_addr as *mut f32).add(p * c_in * nt + ci * nt + tile)) = val;
22808 }
22809 }
22810 }
22811 };
22812 if fast_conv_enabled() && crate::pool::should_parallelize(nt * c_in * 16) {
22813 crate::pool::par_for(nt, crate::pool::outer_chunk(nt), &|off, cnt| {
22814 for t in off..off + cnt {
22815 in_xform(t);
22816 }
22817 });
22818 } else {
22819 for t in 0..nt {
22820 in_xform(t);
22821 }
22822 }
22823
22824 let mut m = vec![0f32; 16 * c_out * nt];
22826 for p in 0..16 {
22827 crate::blas::sgemm(
22828 &u[p * c_out * c_in..(p + 1) * c_out * c_in],
22829 &v[p * c_in * nt..(p + 1) * c_in * nt],
22830 &mut m[p * c_out * nt..(p + 1) * c_out * nt],
22831 c_out,
22832 c_in,
22833 nt,
22834 );
22835 }
22836
22837 let out_addr = out.as_mut_ptr() as usize;
22839 let out_xform = |tile: usize| {
22840 let ni = tile / tiles_per;
22841 let rem = tile % tiles_per;
22842 let (ho0, wo0) = (2 * (rem / tw), 2 * (rem % tw));
22843 for co in 0..c_out {
22844 let mut mm = [0f32; 16];
22845 for (p, slot) in mm.iter_mut().enumerate() {
22846 *slot = m[p * c_out * nt + co * nt + tile];
22847 }
22848 let y = winograd_output_transform(&mm);
22849 for yi in 0..2 {
22850 let oh = ho0 + yi;
22851 if oh >= h_out {
22852 continue;
22853 }
22854 for yj in 0..2 {
22855 let ow = wo0 + yj;
22856 if ow < w_out {
22857 unsafe {
22858 *((out_addr as *mut f32)
22859 .add(((ni * c_out + co) * h_out + oh) * w_out + ow)) =
22860 y[yi * 2 + yj];
22861 }
22862 }
22863 }
22864 }
22865 }
22866 };
22867 if fast_conv_enabled() && crate::pool::should_parallelize(nt * c_out * 16) {
22868 crate::pool::par_for(nt, crate::pool::outer_chunk(nt), &|off, cnt| {
22869 for t in off..off + cnt {
22870 out_xform(t);
22871 }
22872 });
22873 } else {
22874 for t in 0..nt {
22875 out_xform(t);
22876 }
22877 }
22878}
22879
22880#[allow(clippy::too_many_arguments)]
22888fn conv2d_forward_direct(
22889 inp: &[f32],
22890 wt: &[f32],
22891 out: &mut [f32],
22892 n: usize,
22893 c_in: usize,
22894 h: usize,
22895 w: usize,
22896 c_out: usize,
22897 h_out: usize,
22898 w_out: usize,
22899 kh: usize,
22900 kw: usize,
22901 groups: usize,
22902) {
22903 let c_in_per_g = c_in / groups;
22904 let c_out_per_g = c_out / groups;
22905 let out_plane = h_out * w_out;
22906 let in_plane = h * w;
22907 let out_addr = out.as_mut_ptr() as usize;
22908 let compute = |nco: usize| {
22909 let ni = nco / c_out;
22910 let co = nco % c_out;
22911 let ci_start = (co / c_out_per_g) * c_in_per_g;
22912 let out_base = (ni * c_out + co) * out_plane;
22913 let op = unsafe {
22914 std::slice::from_raw_parts_mut((out_addr as *mut f32).add(out_base), out_plane)
22915 };
22916 for v in op.iter_mut() {
22917 *v = 0.0;
22918 }
22919 for ci_off in 0..c_in_per_g {
22920 let in_base = (ni * c_in + ci_start + ci_off) * in_plane;
22921 let wt_base = (co * c_in_per_g + ci_off) * kh * kw;
22922 for ki in 0..kh {
22923 for kj in 0..kw {
22924 let wv = wt[wt_base + ki * kw + kj];
22925 for ho in 0..h_out {
22926 let in_row = in_base + (ho + ki) * w + kj;
22927 let dst = &mut op[ho * w_out..ho * w_out + w_out];
22928 let src = &inp[in_row..in_row + w_out];
22929 for wo in 0..w_out {
22930 dst[wo] += wv * src[wo];
22931 }
22932 }
22933 }
22934 }
22935 }
22936 };
22937 if fast_conv_enabled() && crate::pool::should_parallelize(n * c_out * out_plane) {
22938 crate::pool::par_for(
22939 n * c_out,
22940 crate::pool::outer_chunk(n * c_out),
22941 &|off, cnt| {
22942 for nco in off..off + cnt {
22943 compute(nco);
22944 }
22945 },
22946 );
22947 } else {
22948 for nco in 0..n * c_out {
22949 compute(nco);
22950 }
22951 }
22952}
22953
22954#[allow(clippy::too_many_arguments)]
22966fn conv2d_forward_im2col(
22967 inp: &[f32],
22968 wt: &[f32],
22969 out: &mut [f32],
22970 n: usize,
22971 c_in: usize,
22972 h: usize,
22973 w: usize,
22974 c_out: usize,
22975 h_out: usize,
22976 w_out: usize,
22977 kh: usize,
22978 kw: usize,
22979 sh: usize,
22980 sw: usize,
22981 ph: usize,
22982 pw: usize,
22983 dh: usize,
22984 dw: usize,
22985 groups: usize,
22986) {
22987 let c_in_per_g = c_in / groups;
22988 let c_out_per_g = c_out / groups;
22989 let k_dim = c_in_per_g * kh * kw; let p_dim = h_out * w_out; let x_stride_n = c_in * h * w;
22992 let x_stride_g = c_in_per_g * h * w;
22993 let out_stride_n = c_out * h_out * w_out;
22994 let out_stride_g = c_out_per_g * p_dim;
22995 let w_stride_g = c_out_per_g * k_dim;
22996
22997 if n * groups < crate::pool::num_threads() {
23005 let mut col = vec![0f32; k_dim * p_dim];
23006 for ni in 0..n {
23007 for g in 0..groups {
23008 let x_off = ni * x_stride_n + g * x_stride_g;
23009 im2col_par(
23010 &inp[x_off..x_off + x_stride_g],
23011 &mut col,
23012 c_in_per_g,
23013 h,
23014 w,
23015 h_out,
23016 w_out,
23017 kh,
23018 kw,
23019 sh,
23020 sw,
23021 ph,
23022 pw,
23023 dh,
23024 dw,
23025 );
23026 let w_off = g * w_stride_g;
23027 let o_off = ni * out_stride_n + g * out_stride_g;
23028 let out_g = &mut out[o_off..o_off + out_stride_g];
23029 crate::blas::sgemm(
23030 &wt[w_off..w_off + w_stride_g],
23031 &col,
23032 out_g,
23033 c_out_per_g,
23034 k_dim,
23035 p_dim,
23036 );
23037 }
23038 }
23039 return;
23040 }
23041
23042 let out_addr = out.as_mut_ptr() as usize;
23045 crate::pool::par_for(n, 1, &|off, cnt| {
23046 let mut col = vec![0f32; k_dim * p_dim];
23047 for ni in off..off + cnt {
23048 for g in 0..groups {
23049 let x_off = ni * x_stride_n + g * x_stride_g;
23050 im2col(
23051 &inp[x_off..x_off + x_stride_g],
23052 &mut col,
23053 c_in_per_g,
23054 h,
23055 w,
23056 h_out,
23057 w_out,
23058 kh,
23059 kw,
23060 sh,
23061 sw,
23062 ph,
23063 pw,
23064 dh,
23065 dw,
23066 );
23067 let w_off = g * w_stride_g;
23068 let o_off = ni * out_stride_n + g * out_stride_g;
23069 let out_g = unsafe {
23070 std::slice::from_raw_parts_mut((out_addr as *mut f32).add(o_off), out_stride_g)
23071 };
23072 crate::blas::sgemm(
23073 &wt[w_off..w_off + w_stride_g],
23074 &col,
23075 out_g,
23076 c_out_per_g,
23077 k_dim,
23078 p_dim,
23079 );
23080 }
23081 }
23082 });
23083}
23084
23085#[allow(clippy::too_many_arguments)]
23090fn im2col_par(
23091 x: &[f32],
23092 col: &mut [f32],
23093 c_in: usize,
23094 h: usize,
23095 w: usize,
23096 h_out: usize,
23097 w_out: usize,
23098 kh: usize,
23099 kw: usize,
23100 sh: usize,
23101 sw: usize,
23102 ph: usize,
23103 pw: usize,
23104 dh: usize,
23105 dw_dil: usize,
23106) {
23107 let n_dim = h_out * w_out;
23108 debug_assert_eq!(col.len(), c_in * kh * kw * n_dim);
23109 debug_assert_eq!(x.len(), c_in * h * w);
23110 if c_in < 2 || !crate::pool::should_parallelize(c_in * kh * kw * n_dim) {
23111 im2col(
23112 x, col, c_in, h, w, h_out, w_out, kh, kw, sh, sw, ph, pw, dh, dw_dil,
23113 );
23114 return;
23115 }
23116 let h_isz = h as isize;
23117 let w_isz = w as isize;
23118 let ph_isz = ph as isize;
23119 let pw_isz = pw as isize;
23120 let col_addr = col.as_mut_ptr() as usize;
23121 crate::pool::par_for(c_in, 1, &|off, cnt| {
23122 for ci in off..off + cnt {
23123 for ki in 0..kh {
23124 for kj in 0..kw {
23125 let row = ((ci * kh) + ki) * kw + kj;
23126 let row_off = row * n_dim;
23127 let col_row = unsafe {
23130 std::slice::from_raw_parts_mut((col_addr as *mut f32).add(row_off), n_dim)
23131 };
23132 for ho in 0..h_out {
23133 let hi = (ho * sh + ki * dh) as isize - ph_isz;
23134 if hi < 0 || hi >= h_isz {
23135 for wo in 0..w_out {
23136 col_row[ho * w_out + wo] = 0.0;
23137 }
23138 continue;
23139 }
23140 let hi = hi as usize;
23141 let in_row_off = (ci * h + hi) * w;
23142 for wo in 0..w_out {
23143 let wi = (wo * sw + kj * dw_dil) as isize - pw_isz;
23144 col_row[ho * w_out + wo] = if wi < 0 || wi >= w_isz {
23145 0.0
23146 } else {
23147 x[in_row_off + wi as usize]
23148 };
23149 }
23150 }
23151 }
23152 }
23153 }
23154 });
23155}
23156
23157fn im2col(
23158 x: &[f32],
23159 col: &mut [f32],
23160 c_in: usize,
23161 h: usize,
23162 w: usize,
23163 h_out: usize,
23164 w_out: usize,
23165 kh: usize,
23166 kw: usize,
23167 sh: usize,
23168 sw: usize,
23169 ph: usize,
23170 pw: usize,
23171 dh: usize,
23172 dw_dil: usize,
23173) {
23174 let n_dim = h_out * w_out;
23175 debug_assert_eq!(col.len(), c_in * kh * kw * n_dim);
23176 debug_assert_eq!(x.len(), c_in * h * w);
23177 let h_isz = h as isize;
23178 let w_isz = w as isize;
23179 let ph_isz = ph as isize;
23180 let pw_isz = pw as isize;
23181 for ci in 0..c_in {
23182 for ki in 0..kh {
23183 for kj in 0..kw {
23184 let row = ((ci * kh) + ki) * kw + kj;
23185 let row_off = row * n_dim;
23186 for ho in 0..h_out {
23187 let hi = (ho * sh + ki * dh) as isize - ph_isz;
23188 if hi < 0 || hi >= h_isz {
23189 for wo in 0..w_out {
23190 col[row_off + ho * w_out + wo] = 0.0;
23191 }
23192 continue;
23193 }
23194 let hi = hi as usize;
23195 let in_row_off = (ci * h + hi) * w;
23196 for wo in 0..w_out {
23197 let wi = (wo * sw + kj * dw_dil) as isize - pw_isz;
23198 col[row_off + ho * w_out + wo] = if wi < 0 || wi >= w_isz {
23199 0.0
23200 } else {
23201 x[in_row_off + wi as usize]
23202 };
23203 }
23204 }
23205 }
23206 }
23207 }
23208}
23209
23210#[allow(clippy::too_many_arguments)]
23217fn col2im(
23218 col: &[f32],
23219 x: &mut [f32],
23220 c_in: usize,
23221 h: usize,
23222 w: usize,
23223 h_out: usize,
23224 w_out: usize,
23225 kh: usize,
23226 kw: usize,
23227 sh: usize,
23228 sw: usize,
23229 ph: usize,
23230 pw: usize,
23231 dh: usize,
23232 dw_dil: usize,
23233) {
23234 let n_dim = h_out * w_out;
23235 debug_assert_eq!(col.len(), c_in * kh * kw * n_dim);
23236 debug_assert_eq!(x.len(), c_in * h * w);
23237 let h_isz = h as isize;
23238 let w_isz = w as isize;
23239 let ph_isz = ph as isize;
23240 let pw_isz = pw as isize;
23241 for ci in 0..c_in {
23242 for ki in 0..kh {
23243 for kj in 0..kw {
23244 let row = ((ci * kh) + ki) * kw + kj;
23245 let row_off = row * n_dim;
23246 for ho in 0..h_out {
23247 let hi = (ho * sh + ki * dh) as isize - ph_isz;
23248 if hi < 0 || hi >= h_isz {
23249 continue;
23250 }
23251 let hi = hi as usize;
23252 let in_row_off = (ci * h + hi) * w;
23253 for wo in 0..w_out {
23254 let wi = (wo * sw + kj * dw_dil) as isize - pw_isz;
23255 if wi < 0 || wi >= w_isz {
23256 continue;
23257 }
23258 x[in_row_off + wi as usize] += col[row_off + ho * w_out + wo];
23259 }
23260 }
23261 }
23262 }
23263 }
23264}
23265
23266fn quant_layout(shape: &rlx_ir::Shape, axis: Option<usize>) -> (usize, usize, usize) {
23276 match axis {
23277 None => (0, 1, shape.num_elements().unwrap_or(0).max(1)),
23278 Some(d) => {
23279 let chan_dim = shape.dim(d).unwrap_static();
23280 let inner: usize = (d + 1..shape.rank())
23281 .map(|i| shape.dim(i).unwrap_static())
23282 .product::<usize>()
23283 .max(1);
23284 (d, chan_dim, inner)
23285 }
23286 }
23287}
23288
23289fn activation_backward_kernel(
23290 act: rlx_ir::op::Activation,
23291 xs: &[f32],
23292 dys: &[f32],
23293 out: &mut [f32],
23294) {
23295 use rlx_ir::op::Activation;
23296 let n = xs.len();
23297 debug_assert_eq!(dys.len(), n);
23298 debug_assert_eq!(out.len(), n);
23299 match act {
23300 Activation::Relu => {
23301 for i in 0..n {
23302 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
23303 }
23304 }
23305 Activation::Sigmoid => {
23306 for i in 0..n {
23307 let s = 1.0 / (1.0 + (-xs[i]).exp());
23308 out[i] = s * (1.0 - s) * dys[i];
23309 }
23310 }
23311 Activation::Tanh => {
23312 for i in 0..n {
23313 let t = xs[i].tanh();
23314 out[i] = (1.0 - t * t) * dys[i];
23315 }
23316 }
23317 Activation::Silu => {
23318 for i in 0..n {
23320 let s = 1.0 / (1.0 + (-xs[i]).exp());
23321 out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
23322 }
23323 }
23324 Activation::Gelu => {
23325 const INV_SQRT2: f32 = 0.707_106_77;
23328 const INV_SQRT_2PI: f32 = 0.398_942_3;
23329 for i in 0..n {
23330 let x = xs[i];
23331 let phi = 0.5 * (1.0 + erf_f32(x * INV_SQRT2));
23332 let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
23333 out[i] = (phi + x * pdf) * dys[i];
23334 }
23335 }
23336 Activation::GeluApprox => {
23337 const C: f32 = 0.797_884_6; const A: f32 = 0.044_715;
23341 for i in 0..n {
23342 let x = xs[i];
23343 let inner = C * (x + A * x * x * x);
23344 let t = inner.tanh();
23345 let dinner = C * (1.0 + 3.0 * A * x * x);
23346 let d = 0.5 * (1.0 + t) + 0.5 * x * (1.0 - t * t) * dinner;
23347 out[i] = d * dys[i];
23348 }
23349 }
23350 Activation::Exp => {
23351 for i in 0..n {
23352 out[i] = xs[i].exp() * dys[i];
23353 }
23354 }
23355 Activation::Log => {
23356 for i in 0..n {
23357 out[i] = dys[i] / xs[i];
23358 }
23359 }
23360 Activation::Sqrt => {
23361 for i in 0..n {
23363 let s = xs[i].sqrt();
23364 out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
23365 }
23366 }
23367 Activation::Rsqrt => {
23368 for i in 0..n {
23370 let s = xs[i].sqrt();
23371 out[i] = if s > 0.0 {
23372 -0.5 * dys[i] / (xs[i] * s)
23373 } else {
23374 0.0
23375 };
23376 }
23377 }
23378 Activation::Neg => {
23379 for i in 0..n {
23380 out[i] = -dys[i];
23381 }
23382 }
23383 Activation::Abs => {
23384 for i in 0..n {
23386 let x = xs[i];
23387 let s = if x > 0.0 {
23388 1.0
23389 } else if x < 0.0 {
23390 -1.0
23391 } else {
23392 0.0
23393 };
23394 out[i] = s * dys[i];
23395 }
23396 }
23397 Activation::Round => {
23398 out.copy_from_slice(dys);
23403 }
23404 Activation::Sin => {
23405 for i in 0..n {
23407 out[i] = xs[i].cos() * dys[i];
23408 }
23409 }
23410 Activation::Cos => {
23411 for i in 0..n {
23412 out[i] = -xs[i].sin() * dys[i];
23413 }
23414 }
23415 Activation::Tan => {
23416 for i in 0..n {
23418 let t = xs[i].tan();
23419 out[i] = (1.0 + t * t) * dys[i];
23420 }
23421 }
23422 Activation::Atan => {
23423 for i in 0..n {
23425 let x = xs[i];
23426 out[i] = dys[i] / (1.0 + x * x);
23427 }
23428 }
23429 }
23430}
23431
23432fn activation_backward_kernel_f64(
23436 act: rlx_ir::op::Activation,
23437 xs: &[f64],
23438 dys: &[f64],
23439 out: &mut [f64],
23440) {
23441 use rlx_ir::op::Activation;
23442 let n = xs.len();
23443 debug_assert_eq!(dys.len(), n);
23444 debug_assert_eq!(out.len(), n);
23445 match act {
23446 Activation::Relu => {
23447 for i in 0..n {
23448 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
23449 }
23450 }
23451 Activation::Sigmoid => {
23452 for i in 0..n {
23453 let s = 1.0 / (1.0 + (-xs[i]).exp());
23454 out[i] = s * (1.0 - s) * dys[i];
23455 }
23456 }
23457 Activation::Tanh => {
23458 for i in 0..n {
23459 let t = xs[i].tanh();
23460 out[i] = (1.0 - t * t) * dys[i];
23461 }
23462 }
23463 Activation::Silu => {
23464 for i in 0..n {
23465 let s = 1.0 / (1.0 + (-xs[i]).exp());
23466 out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
23467 }
23468 }
23469 Activation::Gelu | Activation::GeluApprox => {
23470 const INV_SQRT2: f64 = std::f64::consts::FRAC_1_SQRT_2;
23472 const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
23473 for i in 0..n {
23474 let x = xs[i];
23475 let phi = 0.5 * (1.0 + erf_f64(x * INV_SQRT2));
23476 let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
23477 out[i] = (phi + x * pdf) * dys[i];
23478 }
23479 }
23480 Activation::Exp => {
23481 for i in 0..n {
23482 out[i] = xs[i].exp() * dys[i];
23483 }
23484 }
23485 Activation::Log => {
23486 for i in 0..n {
23487 out[i] = dys[i] / xs[i];
23488 }
23489 }
23490 Activation::Sqrt => {
23491 for i in 0..n {
23492 let s = xs[i].sqrt();
23493 out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
23494 }
23495 }
23496 Activation::Rsqrt => {
23497 for i in 0..n {
23498 let s = xs[i].sqrt();
23499 out[i] = if s > 0.0 {
23500 -0.5 * dys[i] / (xs[i] * s)
23501 } else {
23502 0.0
23503 };
23504 }
23505 }
23506 Activation::Neg => {
23507 for i in 0..n {
23508 out[i] = -dys[i];
23509 }
23510 }
23511 Activation::Abs => {
23512 for i in 0..n {
23513 let x = xs[i];
23514 let s = if x > 0.0 {
23515 1.0
23516 } else if x < 0.0 {
23517 -1.0
23518 } else {
23519 0.0
23520 };
23521 out[i] = s * dys[i];
23522 }
23523 }
23524 Activation::Round => {
23525 out.copy_from_slice(dys);
23526 }
23527 Activation::Sin => {
23528 for i in 0..n {
23529 out[i] = xs[i].cos() * dys[i];
23530 }
23531 }
23532 Activation::Cos => {
23533 for i in 0..n {
23534 out[i] = -xs[i].sin() * dys[i];
23535 }
23536 }
23537 Activation::Tan => {
23538 for i in 0..n {
23539 let t = xs[i].tan();
23540 out[i] = (1.0 + t * t) * dys[i];
23541 }
23542 }
23543 Activation::Atan => {
23544 for i in 0..n {
23545 let x = xs[i];
23546 out[i] = dys[i] / (1.0 + x * x);
23547 }
23548 }
23549 }
23550}
23551
23552#[inline(always)]
23557fn erf_f64(x: f64) -> f64 {
23558 let s = x.signum();
23559 let x = x.abs();
23560 let t = 1.0 / (1.0 + 0.327_591_1 * x);
23561 let y = 1.0
23562 - (((((1.061_405_43 * t - 1.453_152_03) * t) + 1.421_413_75) * t - 0.284_496_74) * t
23563 + 0.254_829_59)
23564 * t
23565 * (-x * x).exp();
23566 s * y
23567}
23568
23569#[inline(always)]
23572fn erf_f32(x: f32) -> f32 {
23573 let s = x.signum();
23574 let x = x.abs();
23575 let t = 1.0 / (1.0 + 0.327_591_1 * x);
23576 let y = 1.0
23577 - (((((1.061_405_4 * t - 1.453_152_1) * t) + 1.421_413_8) * t - 0.284_496_74) * t
23578 + 0.254_829_6)
23579 * t
23580 * (-x * x).exp();
23581 s * y
23582}
23583
23584fn narrow_thunk_closure(
23585 src: usize,
23586 dst: usize,
23587 outer: u32,
23588 src_stride: u32,
23589 dst_stride: u32,
23590 inner: u32,
23591 elem_bytes: u8,
23592) -> Arc<dyn Fn(*mut u8) + Send + Sync> {
23593 let (outer, ss, ds, inner, eb) = (
23594 outer as usize,
23595 src_stride as usize,
23596 dst_stride as usize,
23597 inner as usize,
23598 elem_bytes as usize,
23599 );
23600 let row_bytes = inner.saturating_mul(eb);
23601 let src_row_stride = ss.saturating_mul(eb);
23602 let dst_row_stride = ds.saturating_mul(eb);
23603 Arc::new(move |base: *mut u8| unsafe {
23604 if row_bytes == 0 || src == dst {
23605 return;
23606 }
23607 let arena_len = usize::MAX;
23609 for o in 0..outer {
23610 let s_off = src + o * src_row_stride;
23611 let d_off = dst + o * dst_row_stride;
23612 if s_off == d_off {
23613 continue;
23614 }
23615 if s_off.saturating_add(row_bytes) > arena_len
23616 || d_off.saturating_add(row_bytes) > arena_len
23617 {
23618 break;
23619 }
23620 std::ptr::copy_nonoverlapping(base.add(s_off), base.add(d_off), row_bytes);
23621 }
23622 })
23623}
23624
23625unsafe fn sl(offset: usize, base: *mut u8, len: usize) -> &'static [f32] {
23626 if offset == usize::MAX {
23627 return &[];
23628 }
23629 unsafe { std::slice::from_raw_parts(base.add(offset) as *const f32, len) }
23630}
23631
23632#[inline(always)]
23633unsafe fn sl_mut(offset: usize, base: *mut u8, len: usize) -> &'static mut [f32] {
23634 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut f32, len) }
23635}
23636
23637#[inline(always)]
23638unsafe fn sl_f64(offset: usize, base: *mut u8, len: usize) -> &'static [f64] {
23639 if offset == usize::MAX {
23640 return &[];
23641 }
23642 unsafe { std::slice::from_raw_parts(base.add(offset) as *const f64, len) }
23643}
23644
23645#[inline(always)]
23646unsafe fn sl_mut_f64(offset: usize, base: *mut u8, len: usize) -> &'static mut [f64] {
23647 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut f64, len) }
23648}
23649
23650#[inline(always)]
23655#[allow(dead_code)]
23656unsafe fn sl_i32(offset: usize, base: *mut u8, len: usize) -> &'static [i32] {
23657 if offset == usize::MAX {
23658 return &[];
23659 }
23660 unsafe { std::slice::from_raw_parts(base.add(offset) as *const i32, len) }
23661}
23662
23663#[inline(always)]
23664#[allow(dead_code)]
23665unsafe fn sl_mut_i32(offset: usize, base: *mut u8, len: usize) -> &'static mut [i32] {
23666 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut i32, len) }
23667}
23668
23669#[inline(always)]
23670unsafe fn sl_i64(offset: usize, base: *mut u8, len: usize) -> &'static [i64] {
23671 if offset == usize::MAX {
23672 return &[];
23673 }
23674 unsafe { std::slice::from_raw_parts(base.add(offset) as *const i64, len) }
23675}
23676
23677#[inline(always)]
23678unsafe fn sl_mut_i64(offset: usize, base: *mut u8, len: usize) -> &'static mut [i64] {
23679 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut i64, len) }
23680}
23681
23682fn transpose_walk_f64(inp: &[f64], out: &mut [f64], out_dims: &[u32], in_strides: &[u32]) {
23686 let rank = out_dims.len();
23687 let mut idx = vec![0u32; rank];
23688 for o in 0..out.len() {
23689 let mut src_off = 0usize;
23690 for d in 0..rank {
23691 src_off += idx[d] as usize * in_strides[d] as usize;
23692 }
23693 out[o] = inp[broadcast_src_index(src_off, inp.len())];
23694 for d in (0..rank).rev() {
23696 idx[d] += 1;
23697 if idx[d] < out_dims[d] {
23698 break;
23699 }
23700 idx[d] = 0;
23701 }
23702 }
23703}
23704
23705fn apply_activation_f64(inp: &[f64], out: &mut [f64], kind: Activation) {
23711 match kind {
23712 Activation::Neg => {
23713 for (o, &v) in out.iter_mut().zip(inp) {
23714 *o = -v;
23715 }
23716 }
23717 Activation::Exp => {
23718 for (o, &v) in out.iter_mut().zip(inp) {
23719 *o = v.exp();
23720 }
23721 }
23722 Activation::Log => {
23723 for (o, &v) in out.iter_mut().zip(inp) {
23724 *o = v.ln();
23725 }
23726 }
23727 Activation::Sqrt => {
23728 for (o, &v) in out.iter_mut().zip(inp) {
23729 *o = v.sqrt();
23730 }
23731 }
23732 Activation::Rsqrt => {
23733 for (o, &v) in out.iter_mut().zip(inp) {
23734 *o = 1.0 / v.sqrt();
23735 }
23736 }
23737 Activation::Abs => {
23738 for (o, &v) in out.iter_mut().zip(inp) {
23739 *o = v.abs();
23740 }
23741 }
23742 Activation::Tanh => {
23743 for (o, &v) in out.iter_mut().zip(inp) {
23744 *o = v.tanh();
23745 }
23746 }
23747 Activation::Sigmoid => {
23748 for (o, &v) in out.iter_mut().zip(inp) {
23749 *o = 1.0 / (1.0 + (-v).exp());
23750 }
23751 }
23752 Activation::Relu => {
23753 for (o, &v) in out.iter_mut().zip(inp) {
23754 *o = v.max(0.0);
23755 }
23756 }
23757 Activation::Round => {
23758 for (o, &v) in out.iter_mut().zip(inp) {
23759 *o = v.round_ties_even();
23760 }
23761 }
23762 Activation::Sin => {
23763 for (o, &v) in out.iter_mut().zip(inp) {
23764 *o = v.sin();
23765 }
23766 }
23767 Activation::Cos => {
23768 for (o, &v) in out.iter_mut().zip(inp) {
23769 *o = v.cos();
23770 }
23771 }
23772 Activation::Tan => {
23773 for (o, &v) in out.iter_mut().zip(inp) {
23774 *o = v.tan();
23775 }
23776 }
23777 Activation::Atan => {
23778 for (o, &v) in out.iter_mut().zip(inp) {
23779 *o = v.atan();
23780 }
23781 }
23782 Activation::Gelu | Activation::GeluApprox | Activation::Silu => {
23783 panic!(
23784 "apply_activation_f64: {kind:?} not yet implemented at f64. \
23785 Add when a workload needs it."
23786 );
23787 }
23788 }
23789}
23790
23791#[inline]
23792fn binary_op_f64(op: BinaryOp, a: f64, b: f64) -> f64 {
23793 match op {
23794 BinaryOp::Add => a + b,
23795 BinaryOp::Sub => a - b,
23796 BinaryOp::Mul => a * b,
23797 BinaryOp::Div => a / b,
23798 BinaryOp::Max => a.max(b),
23799 BinaryOp::Min => a.min(b),
23800 BinaryOp::Pow => a.powf(b),
23801 }
23802}
23803
23804fn reduce_sum_f64(inp: &[f64], out: &mut [f64], outer: usize, reduced: usize, inner: usize) {
23807 for o in 0..outer {
23808 for n in 0..inner {
23809 let mut acc = 0.0_f64;
23810 for r in 0..reduced {
23811 acc += inp[o * reduced * inner + r * inner + n];
23812 }
23813 out[o * inner + n] = acc;
23814 }
23815 }
23816}
23817
23818pub unsafe fn fill_rng_normal_arena(
23824 dst_off: usize,
23825 len: usize,
23826 mean: f32,
23827 scale: f32,
23828 key: u64,
23829 op_seed: Option<f32>,
23830 opts: rlx_ir::RngOptions,
23831 arena: *mut u8,
23832) {
23833 if len == 0 {
23834 return;
23835 }
23836 unsafe {
23837 let out = std::slice::from_raw_parts_mut((arena.add(dst_off)) as *mut f32, len);
23838 rlx_ir::fill_normal_like(out, mean, scale, opts, key, op_seed);
23839 }
23840}
23841
23842pub unsafe fn fill_rng_uniform_arena(
23843 dst_off: usize,
23844 len: usize,
23845 low: f32,
23846 high: f32,
23847 key: u64,
23848 op_seed: Option<f32>,
23849 opts: rlx_ir::RngOptions,
23850 arena: *mut u8,
23851) {
23852 if len == 0 {
23853 return;
23854 }
23855 unsafe {
23856 let out = std::slice::from_raw_parts_mut((arena.add(dst_off)) as *mut f32, len);
23857 rlx_ir::fill_uniform_like(out, low, high, opts, key, op_seed);
23858 }
23859}
23860
23861#[cfg(test)]
23862mod tests {
23863 use super::*;
23864 use rlx_ir::*;
23865
23866 #[test]
23871 fn conv2d_im2col_matches_naive() {
23872 let cases = [
23874 (1, 1, 28, 28, 8, 3, 3, 1, 1, 0, 0, 1, 1, 1), (4, 8, 13, 13, 16, 3, 3, 1, 1, 0, 0, 1, 1, 1), (2, 3, 16, 16, 6, 3, 3, 2, 2, 1, 1, 1, 1, 1), (1, 4, 12, 12, 4, 3, 3, 1, 1, 2, 2, 2, 2, 1), (3, 8, 10, 10, 8, 3, 3, 1, 1, 1, 1, 1, 1, 2), (1, 2, 7, 7, 5, 1, 1, 1, 1, 0, 0, 1, 1, 1), ];
23881 for (idx, &(n, c_in, h, w, c_out, kh, kw, sh, sw, ph, pw, dh, dw, groups)) in
23882 cases.iter().enumerate()
23883 {
23884 let c_in_per_g = c_in / groups;
23885 let h_out = (h + 2 * ph - dh * (kh - 1) - 1) / sh + 1;
23886 let w_out = (w + 2 * pw - dw * (kw - 1) - 1) / sw + 1;
23887 let mut s: u32 = 0x9e37_79b9 ^ (idx as u32 + 1);
23889 let mut rand = || {
23890 s ^= s << 13;
23891 s ^= s >> 17;
23892 s ^= s << 5;
23893 (s as f32 / u32::MAX as f32) - 0.5
23894 };
23895 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
23896 let wt: Vec<f32> = (0..c_out * c_in_per_g * kh * kw).map(|_| rand()).collect();
23897 let mut out_ref = vec![0f32; n * c_out * h_out * w_out];
23898 let mut out_fast = vec![0f32; n * c_out * h_out * w_out];
23899
23900 conv2d_forward_naive(
23901 &inp,
23902 &wt,
23903 &mut out_ref,
23904 n,
23905 c_in,
23906 h,
23907 w,
23908 c_out,
23909 h_out,
23910 w_out,
23911 kh,
23912 kw,
23913 sh,
23914 sw,
23915 ph,
23916 pw,
23917 dh,
23918 dw,
23919 groups,
23920 );
23921 conv2d_forward_im2col(
23922 &inp,
23923 &wt,
23924 &mut out_fast,
23925 n,
23926 c_in,
23927 h,
23928 w,
23929 c_out,
23930 h_out,
23931 w_out,
23932 kh,
23933 kw,
23934 sh,
23935 sw,
23936 ph,
23937 pw,
23938 dh,
23939 dw,
23940 groups,
23941 );
23942
23943 let max_abs = out_ref
23944 .iter()
23945 .zip(&out_fast)
23946 .map(|(a, b)| (a - b).abs())
23947 .fold(0f32, f32::max);
23948 assert!(
23949 max_abs < 1e-3,
23950 "case {idx}: im2col vs naive max abs diff {max_abs}"
23951 );
23952 }
23953 }
23954
23955 #[test]
23958 fn conv2d_direct_matches_naive() {
23959 let cases = [
23961 (1, 1, 28, 28, 8, 3, 3, 1), (4, 8, 13, 13, 16, 3, 3, 1), (2, 6, 10, 10, 9, 3, 3, 3), (1, 4, 9, 9, 4, 5, 5, 1), (3, 2, 7, 7, 2, 1, 1, 1), ];
23967 for (idx, &(n, c_in, h, w, c_out, kh, kw, groups)) in cases.iter().enumerate() {
23968 let h_out = h - kh + 1;
23969 let w_out = w - kw + 1;
23970 let c_in_per_g = c_in / groups;
23971 let mut s: u32 = 0xfeed_1234 ^ (idx as u32 + 1);
23972 let mut rand = || {
23973 s ^= s << 13;
23974 s ^= s >> 17;
23975 s ^= s << 5;
23976 (s as f32 / u32::MAX as f32) - 0.5
23977 };
23978 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
23979 let wt: Vec<f32> = (0..c_out * c_in_per_g * kh * kw).map(|_| rand()).collect();
23980 let mut r = vec![0f32; n * c_out * h_out * w_out];
23981 let mut d = vec![0f32; n * c_out * h_out * w_out];
23982 conv2d_forward_naive(
23983 &inp, &wt, &mut r, n, c_in, h, w, c_out, h_out, w_out, kh, kw, 1, 1, 0, 0, 1, 1,
23984 groups,
23985 );
23986 conv2d_forward_direct(
23987 &inp, &wt, &mut d, n, c_in, h, w, c_out, h_out, w_out, kh, kw, groups,
23988 );
23989 let mx = r
23990 .iter()
23991 .zip(&d)
23992 .map(|(a, b)| (a - b).abs())
23993 .fold(0f32, f32::max);
23994 assert!(mx < 1e-4, "case {idx}: direct vs naive max abs diff {mx}");
23995 }
23996 }
23997
23998 #[test]
24002 fn conv2d_winograd_matches_naive() {
24003 let cases = [
24006 (1, 1, 28, 28, 8), (4, 8, 13, 13, 16), (2, 3, 9, 9, 5), (1, 4, 8, 8, 4), ];
24011 for (idx, &(n, c_in, h, w, c_out)) in cases.iter().enumerate() {
24012 let h_out = h - 2;
24013 let w_out = w - 2;
24014 let mut s: u32 = 0x1234_5678 ^ (idx as u32 + 1);
24015 let mut rand = || {
24016 s ^= s << 13;
24017 s ^= s >> 17;
24018 s ^= s << 5;
24019 (s as f32 / u32::MAX as f32) - 0.5
24020 };
24021 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
24022 let wt: Vec<f32> = (0..c_out * c_in * 9).map(|_| rand()).collect();
24023 let mut out_ref = vec![0f32; n * c_out * h_out * w_out];
24024 let mut out_win = vec![0f32; n * c_out * h_out * w_out];
24025 conv2d_forward_naive(
24026 &inp,
24027 &wt,
24028 &mut out_ref,
24029 n,
24030 c_in,
24031 h,
24032 w,
24033 c_out,
24034 h_out,
24035 w_out,
24036 3,
24037 3,
24038 1,
24039 1,
24040 0,
24041 0,
24042 1,
24043 1,
24044 1,
24045 );
24046 conv2d_forward_winograd(&inp, &wt, &mut out_win, n, c_in, h, w, c_out, h_out, w_out);
24047 let max_abs = out_ref
24048 .iter()
24049 .zip(&out_win)
24050 .map(|(a, b)| (a - b).abs())
24051 .fold(0f32, f32::max);
24052 assert!(
24053 max_abs < 1e-3,
24054 "case {idx}: winograd vs naive max abs diff {max_abs}"
24055 );
24056 }
24057 }
24058
24059 #[test]
24065 fn narrow_rope_fuses_in_unfused_path() {
24066 let f = DType::F32;
24067 let mut g = Graph::new("nr_fuse");
24068 let qkv = g.input("qkv", Shape::new(&[16, 8, 192], f)); let cos = g.input("cos", Shape::new(&[16], f));
24071 let sin = g.input("sin", Shape::new(&[16], f));
24072 let q = g.narrow_(qkv, 2, 0, 64);
24074 let q_rope = g.rope(q, cos, sin, 16);
24075 g.set_outputs(vec![q_rope]);
24076
24077 let plan = rlx_opt::memory::plan_memory(&g);
24078 let arena = crate::arena::Arena::from_plan(plan);
24079 let sched = compile_thunks(&g, &arena);
24080
24081 let mut narrow_count = 0;
24082 let mut rope_with_stride: Option<u32> = None;
24083 for t in &sched.thunks {
24084 match t {
24085 Thunk::Narrow { .. } => narrow_count += 1,
24086 Thunk::Rope { src_row_stride, .. } => rope_with_stride = Some(*src_row_stride),
24087 _ => {}
24088 }
24089 }
24090 assert_eq!(
24093 narrow_count, 0,
24094 "Narrow→Rope fusion should leave zero Narrow thunks; saw {narrow_count}"
24095 );
24096 assert_eq!(
24097 rope_with_stride,
24098 Some(192),
24099 "Rope's src_row_stride should be 192 (parent qkv axis), saw {rope_with_stride:?}"
24100 );
24101 }
24102
24103 #[test]
24106 fn ssm_selective_scan_matches_reference() {
24107 use rlx_ir::Philox4x32;
24108 let bch = 1usize;
24109 let s = 4usize;
24110 let h = 3usize;
24111 let n = 2usize;
24112
24113 let mut rng = Philox4x32::new(13);
24114 let mut x = vec![0f32; bch * s * h];
24115 rng.fill_normal(&mut x);
24116 let mut delta = vec![0f32; bch * s * h];
24117 for v in delta.iter_mut() {
24119 *v = (rng.next_f32() - 0.5) * 0.1;
24120 }
24121 let mut a = vec![0f32; h * n];
24122 for v in a.iter_mut() {
24123 *v = -(rng.next_f32() * 0.5 + 0.1);
24124 } let mut b = vec![0f32; bch * s * n];
24126 rng.fill_normal(&mut b);
24127 let mut c = vec![0f32; bch * s * n];
24128 rng.fill_normal(&mut c);
24129
24130 let mut expected = vec![0f32; bch * s * h];
24132 for bi in 0..bch {
24133 let mut state = vec![0f32; h * n];
24134 for si in 0..s {
24135 for ci in 0..h {
24136 let d = delta[bi * s * h + si * h + ci];
24137 let xv = x[bi * s * h + si * h + ci];
24138 let mut acc = 0f32;
24139 for ni in 0..n {
24140 let da = (d * a[ci * n + ni]).exp();
24141 state[ci * n + ni] =
24142 da * state[ci * n + ni] + d * b[bi * s * n + si * n + ni] * xv;
24143 acc += c[bi * s * n + si * n + ni] * state[ci * n + ni];
24144 }
24145 expected[bi * s * h + si * h + ci] = acc;
24146 }
24147 }
24148 }
24149
24150 let f = DType::F32;
24152 let mut g = Graph::new("ssm");
24153 let xn = g.input("x", Shape::new(&[bch, s, h], f));
24154 let dn = g.input("delta", Shape::new(&[bch, s, h], f));
24155 let an = g.param("a", Shape::new(&[h, n], f));
24156 let bn = g.param("b", Shape::new(&[bch, s, n], f));
24157 let cn = g.param("c", Shape::new(&[bch, s, n], f));
24158 let yn = g.selective_scan(xn, dn, an, bn, cn, n, Shape::new(&[bch, s, h], f));
24159 g.set_outputs(vec![yn]);
24160
24161 let plan = rlx_opt::memory::plan_memory(&g);
24162 let mut arena = crate::arena::Arena::from_plan(plan);
24163 let sched = compile_thunks(&g, &arena);
24164
24165 let xn_off = arena.byte_offset(xn);
24166 let dn_off = arena.byte_offset(dn);
24167 let an_off = arena.byte_offset(an);
24168 let bn_off = arena.byte_offset(bn);
24169 let cn_off = arena.byte_offset(cn);
24170 let yn_off = arena.byte_offset(yn);
24171 let buf = arena.raw_buf_mut();
24172 unsafe {
24173 let copy = |dst: *mut f32, data: &[f32]| {
24174 for (i, &v) in data.iter().enumerate() {
24175 *dst.add(i) = v;
24176 }
24177 };
24178 copy(buf.as_mut_ptr().add(xn_off) as *mut f32, &x);
24179 copy(buf.as_mut_ptr().add(dn_off) as *mut f32, &delta);
24180 copy(buf.as_mut_ptr().add(an_off) as *mut f32, &a);
24181 copy(buf.as_mut_ptr().add(bn_off) as *mut f32, &b);
24182 copy(buf.as_mut_ptr().add(cn_off) as *mut f32, &c);
24183 }
24184 execute_thunks(&sched, arena.raw_buf_mut());
24185
24186 let actual: Vec<f32> = unsafe {
24187 let p = arena.raw_buf().as_ptr().add(yn_off) as *const f32;
24188 (0..bch * s * h).map(|i| *p.add(i)).collect()
24189 };
24190
24191 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
24192 assert!(
24193 (e - a).abs() < 1e-3,
24194 "mismatch at {i}: expected {e}, got {a}"
24195 );
24196 }
24197 }
24198
24199 #[test]
24202 fn conv_1x1_fast_path_matches_scalar() {
24203 use rlx_ir::Philox4x32;
24204 let n = 2usize;
24206 let c_in = 4usize;
24207 let h = 3usize;
24208 let w = 3usize;
24209 let c_out = 5usize;
24210 let mut rng = Philox4x32::new(31);
24211 let mut x = vec![0f32; n * c_in * h * w];
24212 rng.fill_normal(&mut x);
24213 let mut weight = vec![0f32; c_out * c_in];
24214 rng.fill_normal(&mut weight);
24215
24216 let mut expected = vec![0f32; n * c_out * h * w];
24219 for ni in 0..n {
24220 for co in 0..c_out {
24221 for hi in 0..h {
24222 for wi in 0..w {
24223 let mut acc = 0f32;
24224 for ci in 0..c_in {
24225 acc += weight[co * c_in + ci]
24226 * x[((ni * c_in) + ci) * h * w + hi * w + wi];
24227 }
24228 expected[((ni * c_out) + co) * h * w + hi * w + wi] = acc;
24229 }
24230 }
24231 }
24232 }
24233
24234 let f = DType::F32;
24236 let mut g = Graph::new("conv1x1");
24237 let xn = g.input("x", Shape::new(&[n, c_in, h, w], f));
24238 let wn = g.param("w", Shape::new(&[c_out, c_in, 1, 1], f));
24239 let cn = g.add_node(
24241 rlx_ir::Op::Conv {
24242 kernel_size: vec![1, 1],
24243 stride: vec![1, 1],
24244 padding: vec![0, 0],
24245 dilation: vec![1, 1],
24246 groups: 1,
24247 },
24248 vec![xn, wn],
24249 Shape::new(&[n, c_out, h, w], f),
24250 );
24251 g.set_outputs(vec![cn]);
24252
24253 let plan = rlx_opt::memory::plan_memory(&g);
24254 let mut arena = crate::arena::Arena::from_plan(plan);
24255 let sched = compile_thunks(&g, &arena);
24256
24257 let saw_fast = sched
24259 .thunks
24260 .iter()
24261 .any(|t| matches!(t, Thunk::Conv2D1x1 { .. }));
24262 let saw_slow = sched
24263 .thunks
24264 .iter()
24265 .any(|t| matches!(t, Thunk::Conv2D { .. }));
24266 assert!(saw_fast, "1×1 conv should emit Conv2D1x1");
24267 assert!(!saw_slow, "1×1 conv must not fall through to scalar Conv2D");
24268
24269 let xn_off = arena.byte_offset(xn);
24270 let wn_off = arena.byte_offset(wn);
24271 let cn_off = arena.byte_offset(cn);
24272 let buf = arena.raw_buf_mut();
24273 unsafe {
24274 let xp = buf.as_mut_ptr().add(xn_off) as *mut f32;
24275 for (i, &v) in x.iter().enumerate() {
24276 *xp.add(i) = v;
24277 }
24278 let wp = buf.as_mut_ptr().add(wn_off) as *mut f32;
24279 for (i, &v) in weight.iter().enumerate() {
24280 *wp.add(i) = v;
24281 }
24282 }
24283 execute_thunks(&sched, arena.raw_buf_mut());
24284
24285 let actual: Vec<f32> = unsafe {
24286 let p = arena.raw_buf().as_ptr().add(cn_off) as *const f32;
24287 (0..(n * c_out * h * w)).map(|i| *p.add(i)).collect()
24288 };
24289
24290 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
24291 assert!(
24292 (e - a).abs() < 1e-3,
24293 "mismatch at {i}: expected {e}, got {a}"
24294 );
24295 }
24296 }
24297
24298 #[test]
24301 fn dequant_matmul_int8_sym_matches_reference() {
24302 use rlx_ir::Philox4x32;
24303 use rlx_ir::quant::QuantScheme;
24304
24305 let m = 3usize;
24306 let k = 8usize;
24307 let n = 4usize;
24308 let block_size = 4usize; let blocks_per_col = k / block_size;
24310
24311 let mut rng = Philox4x32::new(99);
24313 let mut x = vec![0f32; m * k];
24314 rng.fill_normal(&mut x);
24315 let w_q: Vec<i8> = (0..(k * n))
24316 .map(|i| ((i as i32 * 13 + 7) % 127 - 63) as i8)
24317 .collect();
24318 let scales: Vec<f32> = (0..(blocks_per_col * n))
24319 .map(|i| 0.01 + 0.001 * i as f32)
24320 .collect();
24321
24322 let mut w_f32 = vec![0f32; k * n];
24324 for p in 0..k {
24325 let block = p / block_size;
24326 for j in 0..n {
24327 let s = scales[block * n + j];
24328 w_f32[p * n + j] = w_q[p * n + j] as f32 * s;
24329 }
24330 }
24331 let mut expected = vec![0f32; m * n];
24332 for i in 0..m {
24333 for j in 0..n {
24334 let mut acc = 0f32;
24335 for p in 0..k {
24336 acc += x[i * k + p] * w_f32[p * n + j];
24337 }
24338 expected[i * n + j] = acc;
24339 }
24340 }
24341
24342 let f = DType::F32;
24344 let mut g = Graph::new("dq");
24345 let xn = g.input("x", Shape::new(&[m, k], f));
24346 let wn = g.param("w", Shape::new(&[k, n], DType::I8));
24347 let sn = g.param("scale", Shape::new(&[blocks_per_col, n], f));
24348 let zn = g.param("zp", Shape::new(&[blocks_per_col, n], f)); let dq = g.dequant_matmul(
24350 xn,
24351 wn,
24352 sn,
24353 zn,
24354 QuantScheme::Int8Block {
24355 block_size: block_size as u32,
24356 },
24357 Shape::new(&[m, n], f),
24358 );
24359 g.set_outputs(vec![dq]);
24360
24361 let plan = rlx_opt::memory::plan_memory(&g);
24362 let mut arena = crate::arena::Arena::from_plan(plan);
24363 let sched = compile_thunks(&g, &arena);
24364
24365 let xn_off = arena.byte_offset(xn);
24366 let wn_off = arena.byte_offset(wn);
24367 let sn_off = arena.byte_offset(sn);
24368 let zn_off = arena.byte_offset(zn);
24369 let dq_off = arena.byte_offset(dq);
24370 let buf = arena.raw_buf_mut();
24371 unsafe {
24372 let xp = buf.as_mut_ptr().add(xn_off) as *mut f32;
24374 for (i, &v) in x.iter().enumerate() {
24375 *xp.add(i) = v;
24376 }
24377 let sp = buf.as_mut_ptr().add(sn_off) as *mut f32;
24378 for (i, &v) in scales.iter().enumerate() {
24379 *sp.add(i) = v;
24380 }
24381 let zp = buf.as_mut_ptr().add(zn_off) as *mut f32;
24382 for i in 0..(blocks_per_col * n) {
24383 *zp.add(i) = 0.0;
24384 }
24385 let wp = buf.as_mut_ptr().add(wn_off) as *mut i8;
24387 for (i, &v) in w_q.iter().enumerate() {
24388 *wp.add(i) = v;
24389 }
24390 }
24391 execute_thunks(&sched, arena.raw_buf_mut());
24392
24393 let actual: Vec<f32> = unsafe {
24394 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
24395 (0..m * n).map(|i| *p.add(i)).collect()
24396 };
24397
24398 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
24399 assert!(
24400 (e - a).abs() < 1e-3,
24401 "mismatch at {i}: expected {e}, got {a}"
24402 );
24403 }
24404 }
24405
24406 #[test]
24408 fn lora_matmul_matches_unfused_reference() {
24409 use rlx_ir::Philox4x32;
24410
24411 let m = 4usize;
24412 let k = 8usize;
24413 let n = 6usize;
24414 let r = 2usize;
24415 let scale = 0.5f32;
24416
24417 let mut rng = Philox4x32::new(42);
24419 let mut x = vec![0f32; m * k];
24420 rng.fill_normal(&mut x);
24421 let mut w = vec![0f32; k * n];
24422 rng.fill_normal(&mut w);
24423 let mut a = vec![0f32; k * r];
24424 rng.fill_normal(&mut a);
24425 let mut b = vec![0f32; r * n];
24426 rng.fill_normal(&mut b);
24427
24428 let naive = |a_buf: &[f32], b_buf: &[f32], rows: usize, inner: usize, cols: usize| {
24430 let mut o = vec![0f32; rows * cols];
24431 for i in 0..rows {
24432 for j in 0..cols {
24433 let mut acc = 0f32;
24434 for p in 0..inner {
24435 acc += a_buf[i * inner + p] * b_buf[p * cols + j];
24436 }
24437 o[i * cols + j] = acc;
24438 }
24439 }
24440 o
24441 };
24442 let xw = naive(&x, &w, m, k, n);
24443 let xa = naive(&x, &a, m, k, r);
24444 let xab = naive(&xa, &b, m, r, n);
24445 let mut expected = xw;
24446 for i in 0..(m * n) {
24447 expected[i] += scale * xab[i];
24448 }
24449
24450 let f = DType::F32;
24452 let mut g = Graph::new("lora");
24453 let xn = g.input("x", Shape::new(&[m, k], f));
24454 let wn = g.param("w", Shape::new(&[k, n], f));
24455 let an = g.param("a", Shape::new(&[k, r], f));
24456 let bn = g.param("b", Shape::new(&[r, n], f));
24457 let lm = g.lora_matmul(xn, wn, an, bn, scale, Shape::new(&[m, n], f));
24458 g.set_outputs(vec![lm]);
24459
24460 let plan = rlx_opt::memory::plan_memory(&g);
24461 let mut arena = crate::arena::Arena::from_plan(plan);
24462 let sched = compile_thunks(&g, &arena);
24463
24464 let xn_off = arena.byte_offset(xn);
24465 let wn_off = arena.byte_offset(wn);
24466 let an_off = arena.byte_offset(an);
24467 let bn_off = arena.byte_offset(bn);
24468 let lm_off = arena.byte_offset(lm);
24469 let buf = arena.raw_buf_mut();
24470 unsafe {
24471 let copy = |dst: *mut f32, data: &[f32]| {
24472 for (i, &v) in data.iter().enumerate() {
24473 *dst.add(i) = v;
24474 }
24475 };
24476 copy(buf.as_mut_ptr().add(xn_off) as *mut f32, &x);
24477 copy(buf.as_mut_ptr().add(wn_off) as *mut f32, &w);
24478 copy(buf.as_mut_ptr().add(an_off) as *mut f32, &a);
24479 copy(buf.as_mut_ptr().add(bn_off) as *mut f32, &b);
24480 }
24481 execute_thunks(&sched, arena.raw_buf_mut());
24482
24483 let actual: Vec<f32> = unsafe {
24484 let p = arena.raw_buf().as_ptr().add(lm_off) as *const f32;
24485 (0..m * n).map(|i| *p.add(i)).collect()
24486 };
24487
24488 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
24489 assert!(
24490 (e - a).abs() < 1e-3,
24491 "mismatch at {i}: expected {e}, got {a}"
24492 );
24493 }
24494 }
24495
24496 #[test]
24498 fn sample_temperature_zero_is_argmax() {
24499 let f = DType::F32;
24502 let mut g = Graph::new("samp");
24503 let logits = g.input("logits", Shape::new(&[1, 8], f));
24504 let s = g.sample(logits, 0, 1.0, 1e-3, 42, Shape::new(&[1], f));
24505 g.set_outputs(vec![s]);
24506 let plan = rlx_opt::memory::plan_memory(&g);
24507 let mut arena = crate::arena::Arena::from_plan(plan);
24508 let sched = compile_thunks(&g, &arena);
24509
24510 let logits_off = arena.byte_offset(logits);
24511 let s_off = arena.byte_offset(s);
24512 let buf = arena.raw_buf_mut();
24513 unsafe {
24514 let p = buf.as_mut_ptr().add(logits_off) as *mut f32;
24515 let inputs = [0.1f32, 0.2, 0.3, 0.4, 0.5, 9.0, 0.7, 0.8];
24517 for (i, &v) in inputs.iter().enumerate() {
24518 *p.add(i) = v;
24519 }
24520 }
24521 execute_thunks(&sched, arena.raw_buf_mut());
24522
24523 let token = unsafe {
24524 let p = arena.raw_buf().as_ptr().add(s_off) as *const f32;
24525 *p as usize
24526 };
24527 assert_eq!(token, 5, "low-temp sampling should pick the argmax");
24528 }
24529
24530 #[test]
24531 fn sample_top_k_one_is_deterministic() {
24532 let f = DType::F32;
24534 let mut g = Graph::new("samp_k1");
24535 let logits = g.input("logits", Shape::new(&[1, 4], f));
24536 let s = g.sample(logits, 1, 1.0, 1.0, 7, Shape::new(&[1], f));
24537 g.set_outputs(vec![s]);
24538 let plan = rlx_opt::memory::plan_memory(&g);
24539 let mut arena = crate::arena::Arena::from_plan(plan);
24540 let sched = compile_thunks(&g, &arena);
24541
24542 let logits_off = arena.byte_offset(logits);
24543 let s_off = arena.byte_offset(s);
24544 let buf = arena.raw_buf_mut();
24545 unsafe {
24546 let p = buf.as_mut_ptr().add(logits_off) as *mut f32;
24547 let inputs = [0.1f32, 5.0, 0.3, 0.4]; for (i, &v) in inputs.iter().enumerate() {
24549 *p.add(i) = v;
24550 }
24551 }
24552 execute_thunks(&sched, arena.raw_buf_mut());
24553 let token = unsafe {
24554 let p = arena.raw_buf().as_ptr().add(s_off) as *const f32;
24555 *p as usize
24556 };
24557 assert_eq!(token, 1);
24558 }
24559
24560 #[test]
24562 fn cumsum_inclusive_matches_naive() {
24563 let f = DType::F32;
24564 let mut g = Graph::new("cumsum");
24565 let x = g.input("x", Shape::new(&[2, 4], f));
24566 let cs = g.cumsum(x, -1, false, Shape::new(&[2, 4], f));
24567 g.set_outputs(vec![cs]);
24568 let plan = rlx_opt::memory::plan_memory(&g);
24569 let mut arena = crate::arena::Arena::from_plan(plan);
24570 let sched = compile_thunks(&g, &arena);
24571
24572 let x_off = arena.byte_offset(x);
24574 let out_off = arena.byte_offset(cs);
24575 let buf = arena.raw_buf_mut();
24576 unsafe {
24577 let p = buf.as_mut_ptr().add(x_off) as *mut f32;
24578 let inputs = [1.0f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0];
24579 for (i, &v) in inputs.iter().enumerate() {
24580 *p.add(i) = v;
24581 }
24582 }
24583 execute_thunks(&sched, arena.raw_buf_mut());
24584
24585 let out: Vec<f32> = unsafe {
24586 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
24587 (0..8).map(|i| *p.add(i)).collect()
24588 };
24589 assert_eq!(out, vec![1.0, 3.0, 6.0, 10.0, 10.0, 30.0, 60.0, 100.0]);
24590 }
24591
24592 #[test]
24596 fn narrow_attention_fuses_in_unfused_path() {
24597 let f = DType::F32;
24598 let mut g = Graph::new("nattn_fuse");
24599 let qkv = g.input("qkv", Shape::new(&[8, 16, 192], f)); let mask = g.input("mask", Shape::new(&[8, 16], f));
24602 let q = g.narrow_(qkv, 2, 0, 64);
24603 let k = g.narrow_(qkv, 2, 64, 64);
24604 let v = g.narrow_(qkv, 2, 128, 64);
24605 let attn = g.attention(q, k, v, mask, 4, 16, Shape::new(&[8, 16, 64], f));
24606 g.set_outputs(vec![attn]);
24607
24608 let plan = rlx_opt::memory::plan_memory(&g);
24609 let arena = crate::arena::Arena::from_plan(plan);
24610 let sched = compile_thunks(&g, &arena);
24611
24612 let mut narrow_count = 0;
24613 let mut attn_strides: Option<(u32, u32, u32)> = None;
24614 for t in &sched.thunks {
24615 match t {
24616 Thunk::Narrow { .. } => narrow_count += 1,
24617 Thunk::Attention {
24618 q_row_stride,
24619 k_row_stride,
24620 v_row_stride,
24621 ..
24622 } => attn_strides = Some((*q_row_stride, *k_row_stride, *v_row_stride)),
24623 _ => {}
24624 }
24625 }
24626 assert_eq!(
24629 narrow_count, 0,
24630 "Narrow×3→Attention fusion should eliminate all 3 narrows; saw {narrow_count}"
24631 );
24632 assert_eq!(
24633 attn_strides,
24634 Some((192, 192, 192)),
24635 "Attention should walk Q/K/V with parent row stride 192"
24636 );
24637 }
24638
24639 #[test]
24645 fn fused_attn_block_respects_causal_mask() {
24646 let f = DType::F32;
24647 let (s, d, nh, dh) = (5usize, 8usize, 2usize, 4usize);
24648 let half = dh / 2;
24649
24650 let mut g = Graph::new("fused_causal");
24651 let hidden = g.input("hidden", Shape::new(&[s, d], f));
24652 let wqkv = g.input("wqkv", Shape::new(&[d, 3 * d], f));
24653 let wo = g.input("wo", Shape::new(&[d, d], f));
24654 let cos = g.input("cos", Shape::new(&[s, half], f));
24655 let sin = g.input("sin", Shape::new(&[s, half], f));
24656 let qkv = g.matmul(hidden, wqkv, Shape::new(&[s, 3 * d], f));
24657 let q = g.narrow_(qkv, 1, 0, d);
24658 let k = g.narrow_(qkv, 1, d, d);
24659 let v = g.narrow_(qkv, 1, 2 * d, d);
24660 let q3 = g.reshape(q, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
24661 let k3 = g.reshape(k, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
24662 let v3 = g.reshape(v, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
24663 let qr = g.rope(q3, cos, sin, dh);
24664 let kr = g.rope(k3, cos, sin, dh);
24665 let attn = g.attention_kind(
24666 qr,
24667 kr,
24668 v3,
24669 nh,
24670 dh,
24671 rlx_ir::op::MaskKind::Causal,
24672 Shape::new(&[1, s, d], f),
24673 );
24674 let a2 = g.reshape(attn, vec![s as i64, d as i64], Shape::new(&[s, d], f));
24675 let out = g.matmul(a2, wo, Shape::new(&[s, d], f));
24676 g.set_outputs(vec![out]);
24677
24678 let plan = rlx_opt::memory::plan_memory(&g);
24680 let arena = crate::arena::Arena::from_plan(plan);
24681 let sched = compile_thunks(&g, &arena);
24682 assert!(
24683 sched.thunks.iter().any(|t| matches!(
24684 t,
24685 Thunk::FusedAttnBlock {
24686 mask_kind: rlx_ir::op::MaskKind::Causal,
24687 ..
24688 }
24689 )),
24690 "expected a FusedAttnBlock carrying MaskKind::Causal"
24691 );
24692
24693 let wqkv_d: Vec<f32> = (0..d * 3 * d)
24694 .map(|i| ((i % 7) as f32 - 3.0) * 0.05)
24695 .collect();
24696 let wo_d: Vec<f32> = (0..d * d).map(|i| ((i % 5) as f32 - 2.0) * 0.05).collect();
24697 let mut cos_d = vec![0f32; s * half];
24698 let mut sin_d = vec![0f32; s * half];
24699 for p in 0..s {
24700 for i in 0..half {
24701 let fr = 1.0f32 / 10000f32.powf(2.0 * i as f32 / dh as f32);
24702 cos_d[p * half + i] = (p as f32 * fr).cos();
24703 sin_d[p * half + i] = (p as f32 * fr).sin();
24704 }
24705 }
24706 let base_h: Vec<f32> = (0..s * d).map(|i| ((i % 11) as f32 - 5.0) * 0.1).collect();
24707 let run = |hin: &[f32]| {
24708 run_graph(
24709 &g,
24710 &[
24711 (hidden, hin),
24712 (wqkv, &wqkv_d),
24713 (wo, &wo_d),
24714 (cos, &cos_d),
24715 (sin, &sin_d),
24716 ],
24717 out,
24718 s * d,
24719 )
24720 };
24721 let a = run(&base_h);
24722 let mut changed = base_h.clone();
24724 for j in 0..d {
24725 changed[4 * d + j] += 1.0;
24726 }
24727 let b = run(&changed);
24728 for pos in 0..4 {
24729 for j in 0..d {
24730 let i = pos * d + j;
24731 assert!(
24732 (a[i] - b[i]).abs() < 1e-5,
24733 "causal leak at pos {pos}: {} vs {}",
24734 a[i],
24735 b[i]
24736 );
24737 }
24738 }
24739 let last: f32 = (0..d).map(|j| (a[4 * d + j] - b[4 * d + j]).abs()).sum();
24740 assert!(last > 1e-4, "last position must react to its own token");
24741 }
24742
24743 fn run_graph(
24754 g: &Graph,
24755 inputs: &[(NodeId, &[f32])],
24756 out_id: NodeId,
24757 out_len: usize,
24758 ) -> Vec<f32> {
24759 let plan = rlx_opt::memory::plan_memory(g);
24760 let mut arena = crate::arena::Arena::from_plan(plan);
24761 let sched = compile_thunks(g, &arena);
24762 for &(id, data) in inputs {
24763 let off = arena.byte_offset(id);
24764 let buf = arena.raw_buf_mut();
24765 unsafe {
24766 let p = buf.as_mut_ptr().add(off) as *mut f32;
24767 for (i, &v) in data.iter().enumerate() {
24768 *p.add(i) = v;
24769 }
24770 }
24771 }
24772 execute_thunks(&sched, arena.raw_buf_mut());
24773 let off = arena.byte_offset(out_id);
24774 unsafe {
24775 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
24776 (0..out_len).map(|i| *p.add(i)).collect()
24777 }
24778 }
24779
24780 #[test]
24781 fn relu_backward_matches_mask() {
24782 let f = DType::F32;
24783 let len = 7usize;
24784 let x: Vec<f32> = vec![-2.0, -0.1, 0.0, 0.1, 1.0, 3.0, -5.0];
24785 let dy: Vec<f32> = vec![0.5, 1.5, 2.5, -0.7, 4.0, -1.0, 9.0];
24786
24787 let mut g = Graph::new("relu_bw");
24788 let xn = g.input("x", Shape::new(&[len], f));
24789 let dyn_ = g.input("dy", Shape::new(&[len], f));
24790 let dx = g.relu_backward(xn, dyn_);
24791 g.set_outputs(vec![dx]);
24792
24793 let actual = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dx, len);
24794 let expected: Vec<f32> = x
24798 .iter()
24799 .zip(&dy)
24800 .map(|(&xi, &dyi)| if xi > 0.0 { dyi } else { 0.0 })
24801 .collect();
24802 for (a, e) in actual.iter().zip(&expected) {
24803 assert!((a - e).abs() < 1e-6, "relu_bw mismatch: {a} vs {e}");
24804 }
24805 }
24806
24807 #[test]
24808 fn maxpool2d_backward_routes_to_argmax() {
24809 let f = DType::F32;
24810 let x: Vec<f32> = vec![
24812 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
24813 ];
24814 let dy: Vec<f32> = vec![0.5, 1.0, 2.0, 4.0];
24818
24819 let mut g = Graph::new("maxpool_bw");
24820 let xn = g.input("x", Shape::new(&[1, 1, 4, 4], f));
24821 let dyn_ = g.input("dy", Shape::new(&[1, 1, 2, 2], f));
24822 let dx = g.maxpool2d_backward(xn, dyn_, vec![2, 2], vec![2, 2], vec![0, 0]);
24823 g.set_outputs(vec![dx]);
24824
24825 let actual = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dx, 16);
24826 let mut expected = vec![0f32; 16];
24827 expected[5] = 0.5;
24828 expected[7] = 1.0;
24829 expected[13] = 2.0;
24830 expected[15] = 4.0;
24831 for (i, (a, e)) in actual.iter().zip(&expected).enumerate() {
24832 assert!((a - e).abs() < 1e-6, "maxpool_bw[{i}] mismatch: {a} vs {e}");
24833 }
24834 }
24835
24836 #[test]
24837 fn conv2d_backward_input_matches_numerical_gradient() {
24838 use rlx_ir::Philox4x32;
24839 let n = 1usize;
24842 let c_in = 2usize;
24843 let h = 4usize;
24844 let w = 4usize;
24845 let c_out = 3usize;
24846 let kh = 3usize;
24847 let kw = 3usize;
24848 let ph = 1usize;
24849 let pw = 1usize;
24850 let sh = 1usize;
24851 let sw = 1usize;
24852 let h_out = (h + 2 * ph - kh) / sh + 1;
24854 let w_out = (w + 2 * pw - kw) / sw + 1;
24855 assert_eq!(h_out, 4);
24856 assert_eq!(w_out, 4);
24857
24858 let mut rng = Philox4x32::new(7);
24859 let mut x = vec![0f32; n * c_in * h * w];
24860 rng.fill_normal(&mut x);
24861 let mut wt = vec![0f32; c_out * c_in * kh * kw];
24862 rng.fill_normal(&mut wt);
24863 let mut dy = vec![0f32; n * c_out * h_out * w_out];
24864 rng.fill_normal(&mut dy);
24865
24866 let f = DType::F32;
24868 let mut g = Graph::new("conv_bwi");
24869 let dy_in = g.input("dy", Shape::new(&[n, c_out, h_out, w_out], f));
24870 let w_in = g.input("w", Shape::new(&[c_out, c_in, kh, kw], f));
24871 let dx = g.conv2d_backward_input(
24872 dy_in,
24873 w_in,
24874 Shape::new(&[n, c_in, h, w], f),
24875 vec![kh, kw],
24876 vec![sh, sw],
24877 vec![ph, pw],
24878 vec![1, 1],
24879 1,
24880 );
24881 g.set_outputs(vec![dx]);
24882 let analytical = run_graph(&g, &[(dy_in, &dy), (w_in, &wt)], dx, n * c_in * h * w);
24883
24884 let forward = |x: &[f32]| -> Vec<f32> {
24888 let mut out = vec![0f32; n * c_out * h_out * w_out];
24889 for ni in 0..n {
24890 for co in 0..c_out {
24891 for ho in 0..h_out {
24892 for wo in 0..w_out {
24893 let mut acc = 0f32;
24894 for ci in 0..c_in {
24895 for ki in 0..kh {
24896 for kj in 0..kw {
24897 let hi = ho * sh + ki;
24898 let wi = wo * sw + kj;
24899 if hi < ph || wi < pw {
24900 continue;
24901 }
24902 let hi = hi - ph;
24903 let wi = wi - pw;
24904 if hi >= h || wi >= w {
24905 continue;
24906 }
24907 let xv = x[((ni * c_in) + ci) * h * w + hi * w + wi];
24908 let wv = wt[((co * c_in) + ci) * kh * kw + ki * kw + kj];
24909 acc += xv * wv;
24910 }
24911 }
24912 }
24913 out[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = acc;
24914 }
24915 }
24916 }
24917 }
24918 out
24919 };
24920 let dot = |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(&u, &v)| u * v).sum() };
24921 let eps = 1e-3f32;
24922 let mut numerical = vec![0f32; x.len()];
24923 for i in 0..x.len() {
24924 let saved = x[i];
24925 x[i] = saved + eps;
24926 let plus = dot(&forward(&x), &dy);
24927 x[i] = saved - eps;
24928 let minus = dot(&forward(&x), &dy);
24929 x[i] = saved;
24930 numerical[i] = (plus - minus) / (2.0 * eps);
24931 }
24932 for (i, (a, n)) in analytical.iter().zip(&numerical).enumerate() {
24933 assert!(
24935 (a - n).abs() < 5e-3,
24936 "conv_bw_input[{i}]: analytical {a} vs numerical {n}"
24937 );
24938 }
24939 }
24940
24941 #[test]
24942 fn conv2d_backward_weight_matches_numerical_gradient() {
24943 use rlx_ir::Philox4x32;
24944 let n = 2usize;
24945 let c_in = 2usize;
24946 let h = 4usize;
24947 let w = 4usize;
24948 let c_out = 2usize;
24949 let kh = 3usize;
24950 let kw = 3usize;
24951 let ph = 0usize;
24952 let pw = 0usize;
24953 let sh = 1usize;
24954 let sw = 1usize;
24955 let h_out = (h + 2 * ph - kh) / sh + 1;
24956 let w_out = (w + 2 * pw - kw) / sw + 1;
24957
24958 let mut rng = Philox4x32::new(11);
24959 let mut x = vec![0f32; n * c_in * h * w];
24960 rng.fill_normal(&mut x);
24961 let mut wt = vec![0f32; c_out * c_in * kh * kw];
24962 rng.fill_normal(&mut wt);
24963 let mut dy = vec![0f32; n * c_out * h_out * w_out];
24964 rng.fill_normal(&mut dy);
24965
24966 let f = DType::F32;
24967 let mut g = Graph::new("conv_bww");
24968 let xn = g.input("x", Shape::new(&[n, c_in, h, w], f));
24969 let dyn_ = g.input("dy", Shape::new(&[n, c_out, h_out, w_out], f));
24970 let dwn = g.conv2d_backward_weight(
24971 xn,
24972 dyn_,
24973 Shape::new(&[c_out, c_in, kh, kw], f),
24974 vec![kh, kw],
24975 vec![sh, sw],
24976 vec![ph, pw],
24977 vec![1, 1],
24978 1,
24979 );
24980 g.set_outputs(vec![dwn]);
24981 let analytical = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dwn, c_out * c_in * kh * kw);
24982
24983 let forward = |wt: &[f32]| -> Vec<f32> {
24984 let mut out = vec![0f32; n * c_out * h_out * w_out];
24985 for ni in 0..n {
24986 for co in 0..c_out {
24987 for ho in 0..h_out {
24988 for wo in 0..w_out {
24989 let mut acc = 0f32;
24990 for ci in 0..c_in {
24991 for ki in 0..kh {
24992 for kj in 0..kw {
24993 let hi = ho + ki;
24994 let wi = wo + kj;
24995 let xv = x[((ni * c_in) + ci) * h * w + hi * w + wi];
24996 let wv = wt[((co * c_in) + ci) * kh * kw + ki * kw + kj];
24997 acc += xv * wv;
24998 }
24999 }
25000 }
25001 out[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = acc;
25002 }
25003 }
25004 }
25005 }
25006 out
25007 };
25008 let dot = |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(&u, &v)| u * v).sum() };
25009 let eps = 1e-3f32;
25010 let mut numerical = vec![0f32; wt.len()];
25011 for i in 0..wt.len() {
25012 let saved = wt[i];
25013 wt[i] = saved + eps;
25014 let plus = dot(&forward(&wt), &dy);
25015 wt[i] = saved - eps;
25016 let minus = dot(&forward(&wt), &dy);
25017 wt[i] = saved;
25018 numerical[i] = (plus - minus) / (2.0 * eps);
25019 }
25020 for (i, (a, n)) in analytical.iter().zip(&numerical).enumerate() {
25021 assert!(
25022 (a - n).abs() < 5e-3,
25023 "conv_bw_weight[{i}]: analytical {a} vs numerical {n}"
25024 );
25025 }
25026 }
25027
25028 #[test]
25029 fn softmax_cross_entropy_matches_reference() {
25030 let f = DType::F32;
25031 let logits: Vec<f32> = vec![
25032 1.0, 2.0, 3.0, -1.0, 0.0, 4.0, 5.0, 5.0, 5.0, ];
25036 let labels: Vec<f32> = vec![2.0, 0.0, 1.0];
25037
25038 let mut g = Graph::new("sce");
25039 let lg = g.input("logits", Shape::new(&[3, 3], f));
25040 let lb = g.input("labels", Shape::new(&[3], f));
25041 let loss = g.softmax_cross_entropy_with_logits(lg, lb);
25042 g.set_outputs(vec![loss]);
25043 let actual = run_graph(&g, &[(lg, &logits), (lb, &labels)], loss, 3);
25044
25045 let mut expected = vec![0f32; 3];
25047 for ni in 0..3 {
25048 let row = &logits[ni * 3..(ni + 1) * 3];
25049 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
25050 let sum: f32 = row.iter().map(|&v| (v - m).exp()).sum();
25051 let lse = m + sum.ln();
25052 let label_idx = labels[ni] as usize;
25053 expected[ni] = lse - row[label_idx];
25054 }
25055 for (i, (a, e)) in actual.iter().zip(&expected).enumerate() {
25056 assert!((a - e).abs() < 1e-5, "sce loss[{i}]: {a} vs {e}");
25057 }
25058 }
25059
25060 #[test]
25061 fn softmax_cross_entropy_backward_matches_numerical_gradient() {
25062 use rlx_ir::Philox4x32;
25063 let n = 4usize;
25064 let c = 5usize;
25065 let mut rng = Philox4x32::new(23);
25066 let mut logits = vec![0f32; n * c];
25067 rng.fill_normal(&mut logits);
25068 let labels: Vec<f32> = (0..n).map(|i| (i % c) as f32).collect();
25069 let mut d_loss = vec![0f32; n];
25070 rng.fill_normal(&mut d_loss);
25071
25072 let f = DType::F32;
25073 let mut g = Graph::new("sce_bw");
25074 let lg = g.input("logits", Shape::new(&[n, c], f));
25075 let lb = g.input("labels", Shape::new(&[n], f));
25076 let dl = g.input("d_loss", Shape::new(&[n], f));
25077 let dlogits = g.softmax_cross_entropy_backward(lg, lb, dl);
25078 g.set_outputs(vec![dlogits]);
25079 let analytical = run_graph(
25080 &g,
25081 &[(lg, &logits), (lb, &labels), (dl, &d_loss)],
25082 dlogits,
25083 n * c,
25084 );
25085
25086 let sce_loss = |logits: &[f32]| -> Vec<f32> {
25088 let mut out = vec![0f32; n];
25089 for ni in 0..n {
25090 let row = &logits[ni * c..(ni + 1) * c];
25091 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
25092 let sum: f32 = row.iter().map(|&v| (v - m).exp()).sum();
25093 out[ni] = (m + sum.ln()) - row[labels[ni] as usize];
25094 }
25095 out
25096 };
25097 let dot = |a: &[f32], b: &[f32]| a.iter().zip(b).map(|(&u, &v)| u * v).sum::<f32>();
25098 let eps = 1e-3f32;
25099 let mut numerical = vec![0f32; logits.len()];
25100 for i in 0..logits.len() {
25101 let saved = logits[i];
25102 logits[i] = saved + eps;
25103 let plus = dot(&sce_loss(&logits), &d_loss);
25104 logits[i] = saved - eps;
25105 let minus = dot(&sce_loss(&logits), &d_loss);
25106 logits[i] = saved;
25107 numerical[i] = (plus - minus) / (2.0 * eps);
25108 }
25109 for (i, (a, num)) in analytical.iter().zip(&numerical).enumerate() {
25110 assert!(
25111 (a - num).abs() < 5e-3,
25112 "sce_bw[{i}]: analytical {a} vs numerical {num}"
25113 );
25114 }
25115 }
25116
25117 fn fill_constants_into_arena(graph: &Graph, arena: &mut crate::arena::Arena) {
25130 for node in graph.nodes() {
25131 if let Op::Constant { data } = &node.op
25132 && arena.has_buffer(node.id)
25133 && !data.is_empty()
25134 {
25135 let buf = arena.slice_mut(node.id);
25136 let n_floats = data.len() / 4;
25137 let n = buf.len().min(n_floats);
25138 for i in 0..n {
25139 let bytes = [
25140 data[i * 4],
25141 data[i * 4 + 1],
25142 data[i * 4 + 2],
25143 data[i * 4 + 3],
25144 ];
25145 buf[i] = f32::from_le_bytes(bytes);
25146 }
25147 }
25148 }
25149 }
25150
25151 fn prepare(
25155 graph: &Graph,
25156 seed_inputs: &[(NodeId, &[f32])],
25157 ) -> (ThunkSchedule, crate::arena::Arena) {
25158 let plan = rlx_opt::memory::plan_memory(graph);
25159 let mut arena = crate::arena::Arena::from_plan(plan);
25160 let sched = compile_thunks(graph, &arena);
25161 fill_constants_into_arena(graph, &mut arena);
25162 for &(id, data) in seed_inputs {
25163 let off = arena.byte_offset(id);
25164 let buf = arena.raw_buf_mut();
25165 unsafe {
25166 let p = buf.as_mut_ptr().add(off) as *mut f32;
25167 for (i, &v) in data.iter().enumerate() {
25168 *p.add(i) = v;
25169 }
25170 }
25171 }
25172 (sched, arena)
25173 }
25174
25175 fn read_arena(arena: &crate::arena::Arena, id: NodeId, len: usize) -> Vec<f32> {
25176 let off = arena.byte_offset(id);
25177 unsafe {
25178 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
25179 (0..len).map(|i| *p.add(i)).collect()
25180 }
25181 }
25182
25183 fn write_arena(arena: &mut crate::arena::Arena, id: NodeId, data: &[f32]) {
25184 let off = arena.byte_offset(id);
25185 let buf = arena.raw_buf_mut();
25186 unsafe {
25187 let p = buf.as_mut_ptr().add(off) as *mut f32;
25188 for (i, &v) in data.iter().enumerate() {
25189 *p.add(i) = v;
25190 }
25191 }
25192 }
25193
25194 fn prepare_f64(
25196 graph: &Graph,
25197 seed_inputs: &[(NodeId, &[f64])],
25198 ) -> (ThunkSchedule, crate::arena::Arena) {
25199 let plan = rlx_opt::memory::plan_memory(graph);
25200 let mut arena = crate::arena::Arena::from_plan(plan);
25201 let sched = compile_thunks(graph, &arena);
25202 fill_constants_into_arena(graph, &mut arena);
25203 for &(id, data) in seed_inputs {
25204 let off = arena.byte_offset(id);
25205 let buf = arena.raw_buf_mut();
25206 unsafe {
25207 let p = buf.as_mut_ptr().add(off) as *mut f64;
25208 for (i, &v) in data.iter().enumerate() {
25209 *p.add(i) = v;
25210 }
25211 }
25212 }
25213 (sched, arena)
25214 }
25215
25216 fn read_arena_f64(arena: &crate::arena::Arena, id: NodeId, len: usize) -> Vec<f64> {
25217 let off = arena.byte_offset(id);
25218 unsafe {
25219 let p = arena.raw_buf().as_ptr().add(off) as *const f64;
25220 (0..len).map(|i| *p.add(i)).collect()
25221 }
25222 }
25223
25224 #[test]
25234 fn dense_solve_f64_end_to_end() {
25235 let mut g = Graph::new("solve_e2e");
25236 let a = g.input("A", Shape::new(&[2, 2], DType::F64));
25237 let b = g.input("b", Shape::new(&[2], DType::F64));
25238 let x = g.dense_solve(a, b, Shape::new(&[2], DType::F64));
25239 g.set_outputs(vec![x]);
25240
25241 let a_data = [2.0, 1.0, 1.0, 3.0_f64];
25242 let b_data = [5.0, 10.0_f64];
25243 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
25244 execute_thunks(&sched, arena.raw_buf_mut());
25245
25246 let got = read_arena_f64(&arena, x, 2);
25247 let want = [1.0, 3.0_f64];
25248 for i in 0..2 {
25249 assert!(
25250 (got[i] - want[i]).abs() < 1e-12,
25251 "x[{i}] = {} (expected {})",
25252 got[i],
25253 want[i]
25254 );
25255 }
25256 }
25257
25258 #[test]
25264 fn dense_solve_f64_5x5_laplacian() {
25265 let n = 5usize;
25266 let mut g = Graph::new("solve_5x5");
25267 let a = g.input("A", Shape::new(&[n, n], DType::F64));
25268 let b = g.input("b", Shape::new(&[n], DType::F64));
25269 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
25270 g.set_outputs(vec![x]);
25271
25272 let mut a_data = vec![0.0_f64; n * n];
25274 for i in 0..n {
25275 a_data[i * n + i] = 2.0;
25276 if i > 0 {
25277 a_data[i * n + (i - 1)] = -1.0;
25278 }
25279 if i + 1 < n {
25280 a_data[i * n + (i + 1)] = -1.0;
25281 }
25282 }
25283 let b_data: Vec<f64> = (0..n).map(|i| (i + 1) as f64).collect();
25284 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
25285 execute_thunks(&sched, arena.raw_buf_mut());
25286
25287 let got = read_arena_f64(&arena, x, n);
25288 let mut residual = vec![0.0_f64; n];
25290 for i in 0..n {
25291 for j in 0..n {
25292 residual[i] += a_data[i * n + j] * got[j];
25293 }
25294 }
25295 for i in 0..n {
25296 assert!(
25297 (residual[i] - b_data[i]).abs() < 1e-10,
25298 "row {i}: residual {} vs b {}",
25299 residual[i],
25300 b_data[i]
25301 );
25302 }
25303 }
25304
25305 #[test]
25324 fn hello_resistor_gradient_end_to_end() {
25325 use rlx_opt::autodiff::grad_with_loss;
25326 let n = 3usize;
25327
25328 let mut g = Graph::new("hello_resistor");
25330 let a = g.param("A", Shape::new(&[n, n], DType::F64));
25331 let b = g.input("b", Shape::new(&[n], DType::F64));
25332 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
25333 let loss = g.reduce(
25334 x,
25335 ReduceOp::Sum,
25336 vec![0],
25337 false,
25338 Shape::new(&[1], DType::F64),
25339 );
25340 g.set_outputs(vec![loss]);
25341
25342 let bwd = grad_with_loss(&g, &[a, b]);
25344 assert_eq!(bwd.outputs.len(), 3, "expect [loss, dA, db]");
25345
25346 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
25350 for node in graph.nodes() {
25351 let name = match &node.op {
25352 rlx_ir::Op::Input { name } => Some(name.as_str()),
25353 rlx_ir::Op::Param { name } => Some(name.as_str()),
25354 _ => None,
25355 };
25356 if name == Some(want) {
25357 return node.id;
25358 }
25359 }
25360 panic!("no node named {want:?} in bwd graph");
25361 };
25362 let a_bwd = find_by_name(&bwd, "A");
25363 let b_bwd = find_by_name(&bwd, "b");
25364 let d_out_bwd = find_by_name(&bwd, "d_output");
25365
25366 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
25370 let b_data = [1.0, 2.0, 3.0_f64];
25371 let d_output = [1.0_f64]; let (sched, mut arena) = prepare_f64(
25375 &bwd,
25376 &[(a_bwd, &a_data), (b_bwd, &b_data), (d_out_bwd, &d_output)],
25377 );
25378 execute_thunks(&sched, arena.raw_buf_mut());
25379
25380 let loss_out = read_arena_f64(&arena, bwd.outputs[0], 1);
25381 let da_out = read_arena_f64(&arena, bwd.outputs[1], n * n);
25382 let db_out = read_arena_f64(&arena, bwd.outputs[2], n);
25383
25384 let x_ref = {
25387 let mut a = a_data;
25388 let mut b = b_data;
25389 let info = crate::blas::dgesv(&mut a, &mut b, n, 1);
25390 assert_eq!(info, 0);
25391 b
25392 };
25393 let loss_ref: f64 = x_ref.iter().sum();
25394 let db_ref = {
25396 let mut at = [0.0_f64; 9];
25397 for i in 0..n {
25398 for j in 0..n {
25399 at[i * n + j] = a_data[j * n + i];
25400 }
25401 }
25402 let mut ones = [1.0_f64; 3];
25403 let info = crate::blas::dgesv(&mut at, &mut ones, n, 1);
25404 assert_eq!(info, 0);
25405 ones
25406 };
25407 let mut da_ref = [0.0_f64; 9];
25409 for i in 0..n {
25410 for j in 0..n {
25411 da_ref[i * n + j] = -db_ref[i] * x_ref[j];
25412 }
25413 }
25414
25415 assert!(
25417 (loss_out[0] - loss_ref).abs() < 1e-10,
25418 "loss: got {}, want {}",
25419 loss_out[0],
25420 loss_ref
25421 );
25422 for i in 0..n {
25423 assert!(
25424 (db_out[i] - db_ref[i]).abs() < 1e-10,
25425 "db[{i}]: got {}, want {}",
25426 db_out[i],
25427 db_ref[i]
25428 );
25429 }
25430 for i in 0..n * n {
25431 assert!(
25432 (da_out[i] - da_ref[i]).abs() < 1e-10,
25433 "dA[{i}]: got {}, want {}",
25434 da_out[i],
25435 da_ref[i]
25436 );
25437 }
25438
25439 let h = 1e-6_f64;
25442 for k in 0..n {
25443 let mut bp = b_data;
25444 bp[k] += h;
25445 let mut bm = b_data;
25446 bm[k] -= h;
25447 let lp = {
25448 let mut ac = a_data;
25449 let info = crate::blas::dgesv(&mut ac, &mut bp, n, 1);
25450 assert_eq!(info, 0);
25451 bp.iter().sum::<f64>()
25452 };
25453 let lm = {
25454 let mut ac = a_data;
25455 let info = crate::blas::dgesv(&mut ac, &mut bm, n, 1);
25456 assert_eq!(info, 0);
25457 bm.iter().sum::<f64>()
25458 };
25459 let fd = (lp - lm) / (2.0 * h);
25460 assert!(
25461 (db_out[k] - fd).abs() < 1e-7,
25462 "FD mismatch on db[{k}]: AD={} FD={}",
25463 db_out[k],
25464 fd
25465 );
25466 }
25467 }
25468
25469 #[test]
25474 fn scan_geometric_growth_f64() {
25475 let n = 3usize;
25476 let length = 10u32;
25477
25478 let mut body = Graph::new("scan_body");
25480 let x = body.input("carry", Shape::new(&[n], DType::F64));
25481 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 0.1_f64.to_le_bytes()).collect();
25482 let scale = body.add_node(
25483 Op::Constant { data: scale_bytes },
25484 vec![],
25485 Shape::new(&[n], DType::F64),
25486 );
25487 let scaled = body.binary(BinaryOp::Mul, x, scale, Shape::new(&[n], DType::F64));
25488 let next = body.binary(BinaryOp::Add, x, scaled, Shape::new(&[n], DType::F64));
25489 body.set_outputs(vec![next]);
25490
25491 let mut g = Graph::new("scan_outer");
25493 let init = g.input("init", Shape::new(&[n], DType::F64));
25494 let final_carry = g.scan(init, body, length);
25495 g.set_outputs(vec![final_carry]);
25496
25497 let init_data = vec![1.0_f64; n];
25498 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
25499 execute_thunks(&sched, arena.raw_buf_mut());
25500 let got = read_arena_f64(&arena, final_carry, n);
25501 let want: f64 = 1.1_f64.powi(length as i32);
25502 for i in 0..n {
25503 assert!(
25504 (got[i] - want).abs() < 1e-12,
25505 "got[{i}] = {} want {}",
25506 got[i],
25507 want
25508 );
25509 }
25510 }
25511
25512 #[test]
25519 fn scan_with_xs_cumulative_sum() {
25520 let n = 3usize;
25521 let length = 4u32;
25522
25523 let mut body = Graph::new("cumsum_body");
25524 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25526 let x_t = body.input("x_t", Shape::new(&[n], DType::F64));
25527 let next = body.binary(BinaryOp::Add, carry, x_t, Shape::new(&[n], DType::F64));
25528 body.set_outputs(vec![next]);
25529
25530 let mut g = Graph::new("cumsum_outer");
25531 let init = g.input("init", Shape::new(&[n], DType::F64));
25532 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
25533 let final_carry = g.scan_with_xs(init, &[xs], body, length);
25534 g.set_outputs(vec![final_carry]);
25535
25536 let init_data = vec![0.0_f64; n];
25537 let xs_data: Vec<f64> = (0..length as usize * n).map(|i| (i + 1) as f64).collect(); let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data), (xs, &xs_data)]);
25539 execute_thunks(&sched, arena.raw_buf_mut());
25540 let got = read_arena_f64(&arena, final_carry, n);
25541
25542 let mut want = init_data.clone();
25546 for t in 0..length as usize {
25547 for j in 0..n {
25548 want[j] += xs_data[t * n + j];
25549 }
25550 }
25551 for i in 0..n {
25552 assert!(
25553 (got[i] - want[i]).abs() < 1e-12,
25554 "got[{i}] = {} want {}",
25555 got[i],
25556 want[i]
25557 );
25558 }
25559 }
25560
25561 #[test]
25565 fn scan_with_xs_be_with_drive() {
25566 let n = 3usize;
25567 let length = 4u32;
25568 let dt = 0.1_f64;
25569
25570 let mut m_data = vec![0.0_f64; n * n];
25571 for i in 0..n {
25572 m_data[i * n + i] = 1.0 + dt * 2.0;
25573 if i > 0 {
25574 m_data[i * n + (i - 1)] = -dt;
25575 }
25576 if i + 1 < n {
25577 m_data[i * n + (i + 1)] = -dt;
25578 }
25579 }
25580 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
25581
25582 let mut body = Graph::new("be_drive_body");
25583 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25584 let drive = body.input("drive", Shape::new(&[n], DType::F64));
25585 let m = body.add_node(
25586 Op::Constant { data: m_bytes },
25587 vec![],
25588 Shape::new(&[n, n], DType::F64),
25589 );
25590 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
25591 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
25592 body.set_outputs(vec![next]);
25593
25594 let mut g = Graph::new("be_drive_outer");
25595 let init = g.input("init", Shape::new(&[n], DType::F64));
25596 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
25597 let final_carry = g.scan_with_xs(init, &[xs], body, length);
25598 g.set_outputs(vec![final_carry]);
25599
25600 let init_data = vec![0.0_f64; n];
25601 let mut xs_data = vec![0.0_f64; length as usize * n];
25604 xs_data[0] = 1.0;
25605
25606 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data), (xs, &xs_data)]);
25607 execute_thunks(&sched, arena.raw_buf_mut());
25608 let got = read_arena_f64(&arena, final_carry, n);
25609
25610 let mut x = init_data.clone();
25612 for t in 0..length as usize {
25613 for j in 0..n {
25614 x[j] += xs_data[t * n + j];
25615 }
25616 let mut a_copy = m_data.clone();
25617 crate::blas::dgesv(&mut a_copy, &mut x, n, 1);
25618 }
25619 for i in 0..n {
25620 assert!(
25621 (got[i] - x[i]).abs() < 1e-12,
25622 "got[{i}] = {} ref {}",
25623 got[i],
25624 x[i]
25625 );
25626 }
25627 }
25628
25629 #[test]
25635 fn batched_dense_solve_gradient_matches_per_batch_analytic() {
25636 use rlx_opt::autodiff::grad_with_loss;
25637 let n = 3usize;
25638 let batch = 4usize;
25639
25640 let mut g = Graph::new("bds_grad");
25641 let a = g.param("A", Shape::new(&[batch, n, n], DType::F64));
25642 let b = g.input("b", Shape::new(&[batch, n], DType::F64));
25643 let x = g.batched_dense_solve(a, b, Shape::new(&[batch, n], DType::F64));
25644 let loss = g.reduce(
25645 x,
25646 ReduceOp::Sum,
25647 vec![0, 1],
25648 false,
25649 Shape::new(&[1], DType::F64),
25650 );
25651 g.set_outputs(vec![loss]);
25652
25653 let bwd = grad_with_loss(&g, &[a, b]);
25654
25655 let find = |graph: &Graph, want: &str| -> NodeId {
25656 for node in graph.nodes() {
25657 let name = match &node.op {
25658 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
25659 _ => None,
25660 };
25661 if name == Some(want) {
25662 return node.id;
25663 }
25664 }
25665 panic!("no node named {want}");
25666 };
25667 let a_id = find(&bwd, "A");
25668 let b_id = find(&bwd, "b");
25669 let d_out_id = find(&bwd, "d_output");
25670
25671 let mut rng = rlx_ir::Philox4x32::new(0x57e1_u64);
25672 let mut a_data = vec![0.0_f64; batch * n * n];
25673 let mut b_data = vec![0.0_f64; batch * n];
25674 for bi in 0..batch {
25675 for i in 0..n {
25676 for j in 0..n {
25677 a_data[bi * n * n + i * n + j] = rng.next_f32() as f64 * 0.1;
25678 }
25679 a_data[bi * n * n + i * n + i] += 1.0 + n as f64;
25680 }
25681 for i in 0..n {
25682 b_data[bi * n + i] = rng.next_f32() as f64;
25683 }
25684 }
25685 let d_seed = [1.0_f64];
25686
25687 let (sched, mut arena) = prepare_f64(
25688 &bwd,
25689 &[(a_id, &a_data), (b_id, &b_data), (d_out_id, &d_seed)],
25690 );
25691 execute_thunks(&sched, arena.raw_buf_mut());
25692 let da_out = read_arena_f64(&arena, bwd.outputs[1], batch * n * n);
25693 let db_out = read_arena_f64(&arena, bwd.outputs[2], batch * n);
25694
25695 for bi in 0..batch {
25698 let a_slice: Vec<f64> = a_data[bi * n * n..(bi + 1) * n * n].to_vec();
25699 let mut b_slice: Vec<f64> = b_data[bi * n..(bi + 1) * n].to_vec();
25700 let mut a_copy = a_slice.clone();
25701 crate::blas::dgesv(&mut a_copy, &mut b_slice, n, 1);
25702 let x_ref = b_slice.clone();
25703 let mut at = vec![0.0_f64; n * n];
25705 for i in 0..n {
25706 for j in 0..n {
25707 at[i * n + j] = a_slice[j * n + i];
25708 }
25709 }
25710 let mut ones = vec![1.0_f64; n];
25711 crate::blas::dgesv(&mut at, &mut ones, n, 1);
25712 let db_ref = ones;
25713 for i in 0..n {
25714 let got = db_out[bi * n + i];
25715 assert!(
25716 (got - db_ref[i]).abs() < 1e-10,
25717 "batch {bi}, db[{i}]: got {got} ref {}",
25718 db_ref[i]
25719 );
25720 }
25721 for i in 0..n {
25723 for j in 0..n {
25724 let got = da_out[bi * n * n + i * n + j];
25725 let want = -db_ref[i] * x_ref[j];
25726 assert!(
25727 (got - want).abs() < 1e-10,
25728 "batch {bi}, dA[{i},{j}]: got {got} ref {want}"
25729 );
25730 }
25731 }
25732 }
25733 }
25734
25735 #[test]
25740 fn scan_checkpointed_grad_matches_plain_scan_grad() {
25741 use rlx_opt::autodiff::grad_with_loss;
25742 let n = 2usize;
25743 let length = 6u32;
25744
25745 let make_body = || {
25746 let mut body = Graph::new("ck_body");
25747 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25748 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 1.05_f64.to_le_bytes()).collect();
25749 let scale = body.add_node(
25750 Op::Constant { data: scale_bytes },
25751 vec![],
25752 Shape::new(&[n], DType::F64),
25753 );
25754 let next = body.binary(BinaryOp::Mul, carry, scale, Shape::new(&[n], DType::F64));
25755 body.set_outputs(vec![next]);
25756 body
25757 };
25758
25759 let mut g_plain = Graph::new("ck_plain");
25761 let init_p = g_plain.input("init", Shape::new(&[n], DType::F64));
25762 let final_p = g_plain.scan(init_p, make_body(), length);
25763 let loss_p = g_plain.reduce(
25764 final_p,
25765 ReduceOp::Sum,
25766 vec![0],
25767 false,
25768 Shape::new(&[1], DType::F64),
25769 );
25770 g_plain.set_outputs(vec![loss_p]);
25771 let bwd_p = grad_with_loss(&g_plain, &[init_p]);
25772
25773 let mut g_ck = Graph::new("ck_ckpt");
25775 let init_c = g_ck.input("init", Shape::new(&[n], DType::F64));
25776 let final_c = g_ck.scan_checkpointed(init_c, make_body(), length, 2);
25777 let loss_c = g_ck.reduce(
25778 final_c,
25779 ReduceOp::Sum,
25780 vec![0],
25781 false,
25782 Shape::new(&[1], DType::F64),
25783 );
25784 g_ck.set_outputs(vec![loss_c]);
25785 let bwd_c = grad_with_loss(&g_ck, &[init_c]);
25786
25787 let find = |graph: &Graph, want: &str| -> NodeId {
25788 for node in graph.nodes() {
25789 let name = match &node.op {
25790 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
25791 _ => None,
25792 };
25793 if name == Some(want) {
25794 return node.id;
25795 }
25796 }
25797 panic!("no {want}");
25798 };
25799
25800 let init_data = vec![0.5_f64, -0.5];
25801 let d_seed = [1.0_f64];
25802
25803 let (s_p, mut a_p) = prepare_f64(
25804 &bwd_p,
25805 &[
25806 (find(&bwd_p, "init"), &init_data),
25807 (find(&bwd_p, "d_output"), &d_seed),
25808 ],
25809 );
25810 execute_thunks(&s_p, a_p.raw_buf_mut());
25811 let dinit_p = read_arena_f64(&a_p, bwd_p.outputs[1], n);
25812
25813 let (s_c, mut a_c) = prepare_f64(
25814 &bwd_c,
25815 &[
25816 (find(&bwd_c, "init"), &init_data),
25817 (find(&bwd_c, "d_output"), &d_seed),
25818 ],
25819 );
25820 execute_thunks(&s_c, a_c.raw_buf_mut());
25821 let dinit_c = read_arena_f64(&a_c, bwd_c.outputs[1], n);
25822
25823 for i in 0..n {
25824 assert!(
25825 (dinit_p[i] - dinit_c[i]).abs() < 1e-12,
25826 "dinit[{i}]: plain={} checkpointed={}",
25827 dinit_p[i],
25828 dinit_c[i]
25829 );
25830 }
25831 }
25832
25833 #[test]
25839 fn recursive_checkpointing_matches_full_trajectory() {
25840 let n = 2usize;
25841 let length = 4u32;
25842
25843 let build_body = || -> Graph {
25845 let mut body = Graph::new("rc_body");
25846 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25847 let ones_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
25848 let ones = body.add_node(
25849 Op::Constant { data: ones_bytes },
25850 vec![],
25851 Shape::new(&[n], DType::F64),
25852 );
25853 let next = body.binary(BinaryOp::Add, carry, ones, Shape::new(&[n], DType::F64));
25854 body.set_outputs(vec![next]);
25855 body
25856 };
25857
25858 let body_vjp_for = || -> Graph {
25861 use rlx_opt::autodiff::grad;
25862 let body = build_body();
25863 let carry_id = body
25865 .nodes()
25866 .iter()
25867 .find(|n| matches!(n.op, Op::Input { .. }))
25868 .map(|n| n.id)
25869 .unwrap();
25870 grad(&body, &[carry_id])
25871 };
25872
25873 let mut g_full = Graph::new("rc_outer_full");
25875 let init_full = g_full.input("init", Shape::new(&[n], DType::F64));
25876 let traj_full_id = g_full.scan_trajectory(init_full, build_body(), length);
25877 let upstream_full = g_full.input("upstream", Shape::new(&[length as usize, n], DType::F64));
25879 let dinit_full_id = g_full.scan_backward(
25880 init_full,
25881 traj_full_id,
25882 upstream_full,
25883 &[],
25884 body_vjp_for(),
25885 length,
25886 true,
25887 Shape::new(&[n], DType::F64),
25888 );
25889 g_full.set_outputs(vec![dinit_full_id]);
25890
25891 let k = 2u32;
25894 let mut g_rec = Graph::new("rc_outer_rec");
25895 let init_rec = g_rec.input("init", Shape::new(&[n], DType::F64));
25896 let traj_rec_id = g_rec.add_node(
25897 Op::Scan {
25898 body: Box::new(build_body()),
25899 length,
25900 save_trajectory: true,
25901 num_bcast: 0,
25902 num_xs: 0,
25903 num_checkpoints: k,
25904 },
25905 vec![init_rec],
25906 Shape::new(&[k as usize, n], DType::F64),
25907 );
25908 let upstream_rec = g_rec.input("upstream", Shape::new(&[length as usize, n], DType::F64));
25911 let dinit_rec_id = g_rec.add_node(
25912 Op::ScanBackward {
25913 body_vjp: Box::new(body_vjp_for()),
25914 length,
25915 save_trajectory: true,
25916 num_xs: 0,
25917 num_checkpoints: k,
25918 forward_body: Some(Box::new(build_body())),
25919 },
25920 vec![init_rec, traj_rec_id, upstream_rec],
25921 Shape::new(&[n], DType::F64),
25922 );
25923 g_rec.set_outputs(vec![dinit_rec_id]);
25924
25925 let init_data = vec![0.5_f64, -0.5];
25927 let upstream_data: Vec<f64> = (0..length as usize * n).map(|i| (i as f64) * 0.1).collect();
25928
25929 let find = |graph: &Graph, want: &str| -> NodeId {
25930 for node in graph.nodes() {
25931 if let Op::Input { name } = &node.op
25932 && name == want
25933 {
25934 return node.id;
25935 }
25936 }
25937 panic!("no input {want}");
25938 };
25939
25940 let (s_full, mut a_full) = prepare_f64(
25941 &g_full,
25942 &[
25943 (find(&g_full, "init"), &init_data),
25944 (find(&g_full, "upstream"), &upstream_data),
25945 ],
25946 );
25947 execute_thunks(&s_full, a_full.raw_buf_mut());
25948 let dinit_full = read_arena_f64(&a_full, g_full.outputs[0], n);
25949
25950 let (s_rec, mut a_rec) = prepare_f64(
25951 &g_rec,
25952 &[
25953 (find(&g_rec, "init"), &init_data),
25954 (find(&g_rec, "upstream"), &upstream_data),
25955 ],
25956 );
25957 execute_thunks(&s_rec, a_rec.raw_buf_mut());
25958 let dinit_rec = read_arena_f64(&a_rec, g_rec.outputs[0], n);
25959
25960 for i in 0..n {
25961 assert!(
25962 (dinit_full[i] - dinit_rec[i]).abs() < 1e-12,
25963 "i={i}: full={} rec={}",
25964 dinit_full[i],
25965 dinit_rec[i]
25966 );
25967 }
25968 }
25969
25970 #[test]
25979 fn vmap_of_grad_scan_matches_per_row_runs() {
25980 use rlx_opt::autodiff::grad_with_loss;
25981 use rlx_opt::vmap::vmap;
25982 let n = 2usize;
25983 let length = 3u32;
25984 let batch = 3usize;
25985
25986 let mut body = Graph::new("scan_grad_body");
25987 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25988 let ones_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
25989 let ones = body.add_node(
25990 Op::Constant { data: ones_bytes },
25991 vec![],
25992 Shape::new(&[n], DType::F64),
25993 );
25994 let next = body.binary(BinaryOp::Add, carry, ones, Shape::new(&[n], DType::F64));
25995 body.set_outputs(vec![next]);
25996
25997 let mut g = Graph::new("scan_grad_outer");
25998 let init = g.input("init", Shape::new(&[n], DType::F64));
25999 let final_x = g.scan(init, body, length);
26000 let loss = g.reduce(
26001 final_x,
26002 ReduceOp::Sum,
26003 vec![0],
26004 false,
26005 Shape::new(&[1], DType::F64),
26006 );
26007 g.set_outputs(vec![loss]);
26008
26009 let bwd = grad_with_loss(&g, &[init]);
26010 let bg = vmap(&bwd, &["init"], batch);
26011
26012 let find = |graph: &Graph, want: &str| -> NodeId {
26013 for node in graph.nodes() {
26014 let name = match &node.op {
26015 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26016 _ => None,
26017 };
26018 if name == Some(want) {
26019 return node.id;
26020 }
26021 }
26022 panic!("no node named {want}");
26023 };
26024 let init_b = find(&bg, "init");
26025 let d_out_b = find(&bg, "d_output");
26026
26027 let init_data: Vec<f64> = (0..batch * n).map(|i| (i as f64) * 0.5).collect();
26028 let d_seed = [1.0_f64];
26029
26030 let (sched, mut arena) = prepare_f64(&bg, &[(init_b, &init_data), (d_out_b, &d_seed)]);
26031 execute_thunks(&sched, arena.raw_buf_mut());
26032 let dinit_b = read_arena_f64(&arena, bg.outputs[1], batch * n);
26033
26034 for i in 0..batch * n {
26035 assert!(
26036 (dinit_b[i] - 1.0).abs() < 1e-12,
26037 "dinit[{i}] = {} (expected 1.0)",
26038 dinit_b[i]
26039 );
26040 }
26041
26042 for bi in 0..batch {
26044 let row = &init_data[bi * n..(bi + 1) * n];
26045 let mut g2 = Graph::new("per_row_grad");
26046 let init2 = g2.input("init", Shape::new(&[n], DType::F64));
26047 let mut body2 = Graph::new("per_row_body");
26048 let c2 = body2.input("carry", Shape::new(&[n], DType::F64));
26049 let ones2_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
26050 let ones2 = body2.add_node(
26051 Op::Constant { data: ones2_bytes },
26052 vec![],
26053 Shape::new(&[n], DType::F64),
26054 );
26055 let next2 = body2.binary(BinaryOp::Add, c2, ones2, Shape::new(&[n], DType::F64));
26056 body2.set_outputs(vec![next2]);
26057 let final2 = g2.scan(init2, body2, length);
26058 let loss2 = g2.reduce(
26059 final2,
26060 ReduceOp::Sum,
26061 vec![0],
26062 false,
26063 Shape::new(&[1], DType::F64),
26064 );
26065 g2.set_outputs(vec![loss2]);
26066 let bwd2 = grad_with_loss(&g2, &[init2]);
26067 let init2_id = find(&bwd2, "init");
26068 let d_out2_id = find(&bwd2, "d_output");
26069 let (s2, mut a2) = prepare_f64(&bwd2, &[(init2_id, row), (d_out2_id, &d_seed)]);
26070 execute_thunks(&s2, a2.raw_buf_mut());
26071 let row_dinit = read_arena_f64(&a2, bwd2.outputs[1], n);
26072 for j in 0..n {
26073 let got = dinit_b[bi * n + j];
26074 let want = row_dinit[j];
26075 assert!(
26076 (got - want).abs() < 1e-12,
26077 "row {bi}, j {j}: vmap'd={got} per-row={want}"
26078 );
26079 }
26080 }
26081 }
26082
26083 #[test]
26089 fn vmap_scan_cumulative_sum_matches_scalar_runs() {
26090 use rlx_opt::vmap::vmap;
26091 let n = 2usize;
26092 let length = 4u32;
26093 let batch = 3usize;
26094
26095 let mut body = Graph::new("scan_body_cumsum");
26097 let carry = body.input("carry", Shape::new(&[n], DType::F64));
26098 let x_t = body.input("x_t", Shape::new(&[n], DType::F64));
26099 let next = body.binary(BinaryOp::Add, carry, x_t, Shape::new(&[n], DType::F64));
26100 body.set_outputs(vec![next]);
26101
26102 let mut g = Graph::new("scan_outer_cumsum");
26103 let init = g.input("init", Shape::new(&[n], DType::F64));
26104 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
26105 let final_carry = g.scan_with_xs(init, &[xs], body, length);
26106 g.set_outputs(vec![final_carry]);
26107
26108 let bg = vmap(&g, &["init", "xs"], batch);
26110
26111 let init_data: Vec<f64> = (0..batch * n).map(|i| (i + 1) as f64).collect();
26113 let xs_data: Vec<f64> = (0..batch * length as usize * n)
26116 .map(|i| 0.1 * (i as f64))
26117 .collect();
26118
26119 let find = |graph: &Graph, want: &str| -> NodeId {
26120 for node in graph.nodes() {
26121 if let Op::Input { name } = &node.op
26122 && name == want
26123 {
26124 return node.id;
26125 }
26126 }
26127 panic!("no input {want}");
26128 };
26129 let init_b = find(&bg, "init");
26130 let xs_b = find(&bg, "xs");
26131 let (sched, mut arena) = prepare_f64(&bg, &[(init_b, &init_data), (xs_b, &xs_data)]);
26132 execute_thunks(&sched, arena.raw_buf_mut());
26133 let batched_out = read_arena_f64(&arena, bg.outputs[0], batch * n);
26134
26135 for bi in 0..batch {
26137 let init_slice = &init_data[bi * n..(bi + 1) * n];
26138 let mut x = init_slice.to_vec();
26139 for t in 0..length as usize {
26140 for j in 0..n {
26141 x[j] += xs_data[bi * length as usize * n + t * n + j];
26142 }
26143 }
26144
26145 for i in 0..n {
26146 let got = batched_out[bi * n + i];
26147 assert!(
26148 (got - x[i]).abs() < 1e-12,
26149 "row {bi}, i {i}: got {got} ref {}",
26150 x[i]
26151 );
26152 }
26153 }
26154 }
26155
26156 #[test]
26161 fn vmap_dense_solve_matches_scalar_runs() {
26162 use rlx_opt::vmap::vmap;
26163 let n = 3usize;
26164 let batch = 4usize;
26165
26166 let mut g = Graph::new("solve_forward");
26167 let a = g.input("A", Shape::new(&[n, n], DType::F64));
26168 let b = g.input("b", Shape::new(&[n], DType::F64));
26169 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
26170 g.set_outputs(vec![x]);
26171
26172 let bg = vmap(&g, &["A", "b"], batch);
26174
26175 let mut rng = rlx_ir::Philox4x32::new(0xb47c_u64);
26177 let mut a_data = vec![0.0_f64; batch * n * n];
26178 let mut b_data = vec![0.0_f64; batch * n];
26179 for bi in 0..batch {
26180 for i in 0..n {
26182 for j in 0..n {
26183 a_data[bi * n * n + i * n + j] = rng.next_f32() as f64 * 0.1;
26184 }
26185 a_data[bi * n * n + i * n + i] += 1.0 + n as f64;
26186 }
26187 for i in 0..n {
26188 b_data[bi * n + i] = rng.next_f32() as f64;
26189 }
26190 }
26191
26192 let find = |graph: &Graph, want: &str| -> NodeId {
26193 for node in graph.nodes() {
26194 if let Op::Input { name } = &node.op
26195 && name == want
26196 {
26197 return node.id;
26198 }
26199 }
26200 panic!("no input named {want}");
26201 };
26202 let ba = find(&bg, "A");
26203 let bb = find(&bg, "b");
26204 let (sched, mut arena) = prepare_f64(&bg, &[(ba, &a_data), (bb, &b_data)]);
26205 execute_thunks(&sched, arena.raw_buf_mut());
26206 let batched_x = read_arena_f64(&arena, bg.outputs[0], batch * n);
26207
26208 for bi in 0..batch {
26210 let mut a_slice: Vec<f64> = a_data[bi * n * n..(bi + 1) * n * n].to_vec();
26211 let mut b_slice: Vec<f64> = b_data[bi * n..(bi + 1) * n].to_vec();
26212 crate::blas::dgesv(&mut a_slice, &mut b_slice, n, 1);
26213 for i in 0..n {
26214 let got = batched_x[bi * n + i];
26215 let want = b_slice[i];
26216 assert!(
26217 (got - want).abs() < 1e-12,
26218 "row {bi}, i {i}: got {got} want {want}"
26219 );
26220 }
26221 }
26222 }
26223
26224 #[test]
26231 fn vmap_matmul_add_reduce_matches_scalar_runs() {
26232 use rlx_opt::vmap::vmap;
26233 let n = 3usize;
26234 let batch = 4usize;
26235
26236 let mut g = Graph::new("vmap_e2e_forward");
26238 let x = g.input("x", Shape::new(&[n], DType::F64));
26239 let w = g.input("w", Shape::new(&[n, n], DType::F64));
26240 let b = g.input("b", Shape::new(&[n], DType::F64));
26241 let x_row = g.add_node(
26242 Op::Reshape {
26243 new_shape: vec![1, n as i64],
26244 },
26245 vec![x],
26246 Shape::new(&[1, n], DType::F64),
26247 );
26248 let mm = g.matmul(x_row, w, Shape::new(&[1, n], DType::F64));
26249 let mm_flat = g.add_node(
26250 Op::Reshape {
26251 new_shape: vec![n as i64],
26252 },
26253 vec![mm],
26254 Shape::new(&[n], DType::F64),
26255 );
26256 let yv = g.binary(BinaryOp::Add, mm_flat, b, Shape::new(&[n], DType::F64));
26257 let loss = g.reduce(
26258 yv,
26259 ReduceOp::Sum,
26260 vec![0],
26261 false,
26262 Shape::new(&[1], DType::F64),
26263 );
26264 g.set_outputs(vec![loss]);
26265
26266 let bg = vmap(&g, &["x"], batch);
26268
26269 let mut rng = rlx_ir::Philox4x32::new(0xc1c0_u64);
26271 let n_w = n * n;
26272 let w_data: Vec<f64> = (0..n_w).map(|_| rng.next_f32() as f64).collect();
26273 let b_data: Vec<f64> = (0..n).map(|_| rng.next_f32() as f64).collect();
26274 let mut x_data_batched: Vec<f64> = Vec::with_capacity(batch * n);
26275 for _ in 0..batch * n {
26276 x_data_batched.push(rng.next_f32() as f64);
26277 }
26278
26279 let find = |graph: &Graph, want: &str| -> NodeId {
26281 for node in graph.nodes() {
26282 if let Op::Input { name } = &node.op
26283 && name == want
26284 {
26285 return node.id;
26286 }
26287 }
26288 panic!("no input named {want}");
26289 };
26290 let bx = find(&bg, "x");
26291 let bw = find(&bg, "w");
26292 let bb = find(&bg, "b");
26293 let (sched, mut arena) =
26294 prepare_f64(&bg, &[(bx, &x_data_batched), (bw, &w_data), (bb, &b_data)]);
26295 execute_thunks(&sched, arena.raw_buf_mut());
26296 let batched_out = read_arena_f64(&arena, bg.outputs[0], batch);
26302
26303 for bi in 0..batch {
26305 let xs_slice = &x_data_batched[bi * n..(bi + 1) * n];
26306 let mut g2 = Graph::new("scalar_run");
26307 let x2 = g2.input("x", Shape::new(&[n], DType::F64));
26308 let w2 = g2.input("w", Shape::new(&[n, n], DType::F64));
26309 let b2 = g2.input("b", Shape::new(&[n], DType::F64));
26310 let xr = g2.add_node(
26311 Op::Reshape {
26312 new_shape: vec![1, n as i64],
26313 },
26314 vec![x2],
26315 Shape::new(&[1, n], DType::F64),
26316 );
26317 let m = g2.matmul(xr, w2, Shape::new(&[1, n], DType::F64));
26318 let mf = g2.add_node(
26319 Op::Reshape {
26320 new_shape: vec![n as i64],
26321 },
26322 vec![m],
26323 Shape::new(&[n], DType::F64),
26324 );
26325 let yv2 = g2.binary(BinaryOp::Add, mf, b2, Shape::new(&[n], DType::F64));
26326 let l2 = g2.reduce(
26327 yv2,
26328 ReduceOp::Sum,
26329 vec![0],
26330 false,
26331 Shape::new(&[1], DType::F64),
26332 );
26333 g2.set_outputs(vec![l2]);
26334 let (s2, mut a2) = prepare_f64(&g2, &[(x2, xs_slice), (w2, &w_data), (b2, &b_data)]);
26335 execute_thunks(&s2, a2.raw_buf_mut());
26336 let scalar_out = read_arena_f64(&a2, l2, 1);
26337 assert!(
26338 (batched_out[bi] - scalar_out[0]).abs() < 1e-12,
26339 "row {bi}: batched={} scalar={}",
26340 batched_out[bi],
26341 scalar_out[0]
26342 );
26343 }
26344 }
26345
26346 #[test]
26353 fn scan_with_xs_dxs_matches_fd() {
26354 use rlx_opt::autodiff::grad_with_loss;
26355 let n = 3usize;
26356 let length = 3u32;
26357 let dt = 0.1_f64;
26358
26359 let mut m_data = vec![0.0_f64; n * n];
26360 for i in 0..n {
26361 m_data[i * n + i] = 1.0 + dt * 2.0;
26362 if i > 0 {
26363 m_data[i * n + (i - 1)] = -dt;
26364 }
26365 if i + 1 < n {
26366 m_data[i * n + (i + 1)] = -dt;
26367 }
26368 }
26369 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26370
26371 let mut body = Graph::new("be_dxs_body");
26372 let carry = body.input("carry", Shape::new(&[n], DType::F64));
26373 let drive = body.input("drive", Shape::new(&[n], DType::F64));
26374 let m = body.add_node(
26375 Op::Constant { data: m_bytes },
26376 vec![],
26377 Shape::new(&[n, n], DType::F64),
26378 );
26379 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
26380 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
26381 body.set_outputs(vec![next]);
26382
26383 let mut g = Graph::new("be_dxs_outer");
26384 let init = g.input("init", Shape::new(&[n], DType::F64));
26385 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
26386 let final_carry = g.scan_with_xs(init, &[xs], body, length);
26387 let loss = g.reduce(
26388 final_carry,
26389 ReduceOp::Sum,
26390 vec![0],
26391 false,
26392 Shape::new(&[1], DType::F64),
26393 );
26394 g.set_outputs(vec![loss]);
26395
26396 let bwd = grad_with_loss(&g, &[init, xs]);
26398 assert_eq!(bwd.outputs.len(), 3, "[loss, dinit, dxs]");
26399
26400 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26401 for node in graph.nodes() {
26402 let name = match &node.op {
26403 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26404 _ => None,
26405 };
26406 if name == Some(want) {
26407 return node.id;
26408 }
26409 }
26410 panic!("no node named {want:?}");
26411 };
26412 let init_bwd = find_by_name(&bwd, "init");
26413 let xs_bwd = find_by_name(&bwd, "xs");
26414 let d_out_bwd = find_by_name(&bwd, "d_output");
26415
26416 let init_data = vec![0.5_f64, 0.0, -0.5];
26417 let xs_data: Vec<f64> = (0..length as usize * n)
26418 .map(|i| 0.1_f64 * ((i as f64) - 4.0))
26419 .collect();
26420 let d_seed = [1.0_f64];
26421
26422 let (sched, mut arena) = prepare_f64(
26423 &bwd,
26424 &[
26425 (init_bwd, &init_data),
26426 (xs_bwd, &xs_data),
26427 (d_out_bwd, &d_seed),
26428 ],
26429 );
26430 execute_thunks(&sched, arena.raw_buf_mut());
26431 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
26432 let dxs = read_arena_f64(&arena, bwd.outputs[2], length as usize * n);
26433
26434 let h = 1e-6;
26435 let loss_at = |x0: &[f64], xs_in: &[f64]| -> f64 {
26436 let mut acc = x0.to_vec();
26437 for t in 0..length as usize {
26438 for j in 0..n {
26439 acc[j] += xs_in[t * n + j];
26440 }
26441 let mut a_copy = m_data.clone();
26442 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
26443 }
26444 acc.iter().sum()
26445 };
26446
26447 for i in 0..n {
26449 let mut ip = init_data.to_vec();
26450 ip[i] += h;
26451 let mut im = init_data.to_vec();
26452 im[i] -= h;
26453 let fd = (loss_at(&ip, &xs_data) - loss_at(&im, &xs_data)) / (2.0 * h);
26454 assert!(
26455 (dinit[i] - fd).abs() < 1e-7,
26456 "FD dinit[{i}]: AD={} FD={}",
26457 dinit[i],
26458 fd
26459 );
26460 }
26461
26462 for t in 0..length as usize {
26464 for j in 0..n {
26465 let idx = t * n + j;
26466 let mut xp = xs_data.clone();
26467 xp[idx] += h;
26468 let mut xm = xs_data.clone();
26469 xm[idx] -= h;
26470 let fd = (loss_at(&init_data, &xp) - loss_at(&init_data, &xm)) / (2.0 * h);
26471 assert!(
26472 (dxs[idx] - fd).abs() < 1e-7,
26473 "FD dxs[t={t},j={j}]: AD={} FD={}",
26474 dxs[idx],
26475 fd
26476 );
26477 }
26478 }
26479 }
26480
26481 #[test]
26489 fn scan_with_xs_gradient_dinit_matches_fd() {
26490 use rlx_opt::autodiff::grad_with_loss;
26491 let n = 3usize;
26492 let length = 3u32;
26493 let dt = 0.1_f64;
26494
26495 let mut m_data = vec![0.0_f64; n * n];
26496 for i in 0..n {
26497 m_data[i * n + i] = 1.0 + dt * 2.0;
26498 if i > 0 {
26499 m_data[i * n + (i - 1)] = -dt;
26500 }
26501 if i + 1 < n {
26502 m_data[i * n + (i + 1)] = -dt;
26503 }
26504 }
26505 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26506
26507 let mut body = Graph::new("be_xs_grad_body");
26508 let carry = body.input("carry", Shape::new(&[n], DType::F64));
26509 let drive = body.input("drive", Shape::new(&[n], DType::F64));
26510 let m = body.add_node(
26511 Op::Constant { data: m_bytes },
26512 vec![],
26513 Shape::new(&[n, n], DType::F64),
26514 );
26515 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
26516 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
26517 body.set_outputs(vec![next]);
26518
26519 let mut g = Graph::new("be_xs_grad_outer");
26520 let init = g.input("init", Shape::new(&[n], DType::F64));
26521 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
26522 let final_carry = g.scan_with_xs(init, &[xs], body, length);
26523 let loss = g.reduce(
26524 final_carry,
26525 ReduceOp::Sum,
26526 vec![0],
26527 false,
26528 Shape::new(&[1], DType::F64),
26529 );
26530 g.set_outputs(vec![loss]);
26531
26532 let bwd = grad_with_loss(&g, &[init]);
26533
26534 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26535 for node in graph.nodes() {
26536 let name = match &node.op {
26537 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26538 _ => None,
26539 };
26540 if name == Some(want) {
26541 return node.id;
26542 }
26543 }
26544 panic!("no node named {want:?}");
26545 };
26546 let init_bwd = find_by_name(&bwd, "init");
26547 let xs_bwd = find_by_name(&bwd, "xs");
26548 let d_out_bwd = find_by_name(&bwd, "d_output");
26549
26550 let init_data = vec![0.5_f64, 0.0, -0.5];
26551 let xs_data: Vec<f64> = (0..length as usize * n)
26553 .map(|i| 0.1_f64 * ((i as f64) - 4.0))
26554 .collect();
26555 let d_seed = [1.0_f64];
26556
26557 let (sched, mut arena) = prepare_f64(
26558 &bwd,
26559 &[
26560 (init_bwd, &init_data),
26561 (xs_bwd, &xs_data),
26562 (d_out_bwd, &d_seed),
26563 ],
26564 );
26565 execute_thunks(&sched, arena.raw_buf_mut());
26566 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
26567
26568 let h = 1e-6;
26569 let loss_at = |x0: &[f64]| -> f64 {
26570 let mut acc = x0.to_vec();
26571 for t in 0..length as usize {
26572 for j in 0..n {
26573 acc[j] += xs_data[t * n + j];
26574 }
26575 let mut a_copy = m_data.clone();
26576 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
26577 }
26578 acc.iter().sum()
26579 };
26580 for i in 0..n {
26581 let mut ip = init_data.to_vec();
26582 ip[i] += h;
26583 let mut im = init_data.to_vec();
26584 im[i] -= h;
26585 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
26586 assert!(
26587 (dinit[i] - fd).abs() < 1e-7,
26588 "FD dinit[{i}]: AD={} FD={}",
26589 dinit[i],
26590 fd
26591 );
26592 }
26593 }
26594
26595 #[test]
26603 fn scan_gradient_geometric_matches_closed_form() {
26604 use rlx_opt::autodiff::grad_with_loss;
26605 let n = 3usize;
26606 let length = 5u32;
26607
26608 let mut body = Graph::new("scan_grad_body");
26609 let x = body.input("carry", Shape::new(&[n], DType::F64));
26610 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 1.1_f64.to_le_bytes()).collect();
26611 let scale = body.add_node(
26612 Op::Constant { data: scale_bytes },
26613 vec![],
26614 Shape::new(&[n], DType::F64),
26615 );
26616 let next = body.binary(BinaryOp::Mul, x, scale, Shape::new(&[n], DType::F64));
26617 body.set_outputs(vec![next]);
26618
26619 let mut g = Graph::new("scan_grad_outer");
26620 let init = g.input("init", Shape::new(&[n], DType::F64));
26621 let final_x = g.scan(init, body, length);
26622 let loss = g.reduce(
26623 final_x,
26624 ReduceOp::Sum,
26625 vec![0],
26626 false,
26627 Shape::new(&[1], DType::F64),
26628 );
26629 g.set_outputs(vec![loss]);
26630
26631 let bwd = grad_with_loss(&g, &[init]);
26632 assert_eq!(bwd.outputs.len(), 2);
26633
26634 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26635 for node in graph.nodes() {
26636 let name = match &node.op {
26637 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26638 _ => None,
26639 };
26640 if name == Some(want) {
26641 return node.id;
26642 }
26643 }
26644 panic!("no node named {want:?}");
26645 };
26646 let init_bwd = find_by_name(&bwd, "init");
26647 let d_out_bwd = find_by_name(&bwd, "d_output");
26648
26649 let init_data = vec![1.0_f64; n];
26650 let d_seed = [1.0_f64];
26651 let (sched, mut arena) = prepare_f64(&bwd, &[(init_bwd, &init_data), (d_out_bwd, &d_seed)]);
26652 execute_thunks(&sched, arena.raw_buf_mut());
26653 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
26654
26655 let want = 1.1_f64.powi(length as i32);
26656 for i in 0..n {
26657 assert!(
26658 (dinit[i] - want).abs() < 1e-12,
26659 "dinit[{i}] = {} want {}",
26660 dinit[i],
26661 want
26662 );
26663 }
26664
26665 let h = 1e-6;
26667 let loss_at = |x: &[f64]| -> f64 {
26668 let mut acc = x.to_vec();
26669 for _ in 0..length {
26670 for v in acc.iter_mut() {
26671 *v *= 1.1;
26672 }
26673 }
26674 acc.iter().sum()
26675 };
26676 let mut ip = init_data.clone();
26677 ip[0] += h;
26678 let mut im = init_data.clone();
26679 im[0] -= h;
26680 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
26681 assert!(
26682 (dinit[0] - fd).abs() < 1e-7,
26683 "FD dinit[0]: AD={} FD={}",
26684 dinit[0],
26685 fd
26686 );
26687 }
26688
26689 #[test]
26692 fn scan_gradient_backward_euler_matches_fd() {
26693 use rlx_opt::autodiff::grad_with_loss;
26694 let n = 4usize;
26695 let length = 3u32;
26696 let dt = 0.05_f64;
26697
26698 let mut m_data = vec![0.0_f64; n * n];
26699 for i in 0..n {
26700 m_data[i * n + i] = 1.0 + dt * 2.0;
26701 if i > 0 {
26702 m_data[i * n + (i - 1)] = -dt;
26703 }
26704 if i + 1 < n {
26705 m_data[i * n + (i + 1)] = -dt;
26706 }
26707 }
26708 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26709
26710 let mut body = Graph::new("be_grad_body");
26711 let x = body.input("x", Shape::new(&[n], DType::F64));
26712 let m = body.add_node(
26713 Op::Constant { data: m_bytes },
26714 vec![],
26715 Shape::new(&[n, n], DType::F64),
26716 );
26717 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
26718 body.set_outputs(vec![next]);
26719
26720 let mut g = Graph::new("be_grad_outer");
26721 let init = g.input("x0", Shape::new(&[n], DType::F64));
26722 let final_x = g.scan(init, body, length);
26723 let loss = g.reduce(
26724 final_x,
26725 ReduceOp::Sum,
26726 vec![0],
26727 false,
26728 Shape::new(&[1], DType::F64),
26729 );
26730 g.set_outputs(vec![loss]);
26731
26732 let bwd = grad_with_loss(&g, &[init]);
26733
26734 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26735 for node in graph.nodes() {
26736 let name = match &node.op {
26737 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26738 _ => None,
26739 };
26740 if name == Some(want) {
26741 return node.id;
26742 }
26743 }
26744 panic!("no node named {want:?}");
26745 };
26746 let init_bwd = find_by_name(&bwd, "x0");
26747 let d_out_bwd = find_by_name(&bwd, "d_output");
26748
26749 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
26750 let d_seed = [1.0_f64];
26751 let (sched, mut arena) = prepare_f64(&bwd, &[(init_bwd, &init_data), (d_out_bwd, &d_seed)]);
26752 execute_thunks(&sched, arena.raw_buf_mut());
26753 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
26754
26755 let h = 1e-6;
26756 let loss_at = |x0: &[f64]| -> f64 {
26757 let mut acc = x0.to_vec();
26758 for _ in 0..length {
26759 let mut a_copy = m_data.clone();
26760 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
26761 }
26762 acc.iter().sum()
26763 };
26764 for i in 0..n {
26765 let mut ip = init_data.to_vec();
26766 ip[i] += h;
26767 let mut im = init_data.to_vec();
26768 im[i] -= h;
26769 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
26770 assert!(
26771 (dinit[i] - fd).abs() < 1e-7,
26772 "FD dinit[{i}]: AD={} FD={}",
26773 dinit[i],
26774 fd
26775 );
26776 }
26777 }
26778
26779 #[test]
26785 fn scan_trajectory_backward_euler_records_waveform() {
26786 let n = 4usize;
26787 let length = 5u32;
26788 let dt = 0.05_f64;
26789
26790 let mut m_data = vec![0.0_f64; n * n];
26791 for i in 0..n {
26792 m_data[i * n + i] = 1.0 + dt * 2.0;
26793 if i > 0 {
26794 m_data[i * n + (i - 1)] = -dt;
26795 }
26796 if i + 1 < n {
26797 m_data[i * n + (i + 1)] = -dt;
26798 }
26799 }
26800 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26801
26802 let mut body = Graph::new("be_traj_body");
26803 let x = body.input("x", Shape::new(&[n], DType::F64));
26804 let m = body.add_node(
26805 Op::Constant { data: m_bytes },
26806 vec![],
26807 Shape::new(&[n, n], DType::F64),
26808 );
26809 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
26810 body.set_outputs(vec![next]);
26811
26812 let mut g = Graph::new("be_traj_outer");
26813 let init = g.input("x0", Shape::new(&[n], DType::F64));
26814 let traj = g.scan_trajectory(init, body, length);
26815 g.set_outputs(vec![traj]);
26816
26817 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
26818 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
26819 execute_thunks(&sched, arena.raw_buf_mut());
26820 let got = read_arena_f64(&arena, traj, length as usize * n);
26821
26822 let mut want = Vec::<f64>::with_capacity(length as usize * n);
26824 let mut x_ref = init_data.to_vec();
26825 for _ in 0..length {
26826 let mut a_copy = m_data.clone();
26827 crate::blas::dgesv(&mut a_copy, &mut x_ref, n, 1);
26828 want.extend_from_slice(&x_ref);
26829 }
26830 for i in 0..length as usize * n {
26831 assert!(
26832 (got[i] - want[i]).abs() < 1e-12,
26833 "got[{i}] = {} ref {}",
26834 got[i],
26835 want[i]
26836 );
26837 }
26838
26839 for t in 1..length as usize {
26842 let prev: f64 = got[(t - 1) * n..t * n].iter().sum();
26843 let curr: f64 = got[t * n..(t + 1) * n].iter().sum();
26844 assert!(
26845 curr <= prev + 1e-15,
26846 "mass should decay: row {} sum {prev}, row {t} sum {curr}",
26847 t - 1
26848 );
26849 }
26850
26851 let mut body2 = Graph::new("be_final_body");
26855 let x2 = body2.input("x", Shape::new(&[n], DType::F64));
26856 let m_bytes2: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26857 let m2 = body2.add_node(
26858 Op::Constant { data: m_bytes2 },
26859 vec![],
26860 Shape::new(&[n, n], DType::F64),
26861 );
26862 let next2 = body2.dense_solve(m2, x2, Shape::new(&[n], DType::F64));
26863 body2.set_outputs(vec![next2]);
26864
26865 let mut g2 = Graph::new("be_final_outer");
26866 let init2 = g2.input("x0", Shape::new(&[n], DType::F64));
26867 let final_x = g2.scan(init2, body2, length);
26868 g2.set_outputs(vec![final_x]);
26869 let (sched2, mut arena2) = prepare_f64(&g2, &[(init2, &init_data)]);
26870 execute_thunks(&sched2, arena2.raw_buf_mut());
26871 let final_got = read_arena_f64(&arena2, final_x, n);
26872
26873 let last_row = &got[(length as usize - 1) * n..length as usize * n];
26874 for i in 0..n {
26875 assert!(
26876 (last_row[i] - final_got[i]).abs() < 1e-15,
26877 "last trajectory row[{i}] = {} vs final-scan = {}",
26878 last_row[i],
26879 final_got[i]
26880 );
26881 }
26882 }
26883
26884 #[test]
26890 fn scan_backward_euler_heat_f64() {
26891 let n = 4usize;
26892 let length = 5u32;
26893 let dt = 0.05_f64;
26894
26895 let mut m_data = vec![0.0_f64; n * n];
26898 for i in 0..n {
26899 m_data[i * n + i] = 1.0 + dt * 2.0;
26900 if i > 0 {
26901 m_data[i * n + (i - 1)] = -dt;
26902 }
26903 if i + 1 < n {
26904 m_data[i * n + (i + 1)] = -dt;
26905 }
26906 }
26907 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26908
26909 let mut body = Graph::new("be_body");
26910 let x = body.input("x", Shape::new(&[n], DType::F64));
26911 let m = body.add_node(
26912 Op::Constant { data: m_bytes },
26913 vec![],
26914 Shape::new(&[n, n], DType::F64),
26915 );
26916 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
26917 body.set_outputs(vec![next]);
26918
26919 let mut g = Graph::new("be_outer");
26920 let init = g.input("x0", Shape::new(&[n], DType::F64));
26921 let final_x = g.scan(init, body, length);
26922 g.set_outputs(vec![final_x]);
26923
26924 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
26926 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
26927 execute_thunks(&sched, arena.raw_buf_mut());
26928 let got = read_arena_f64(&arena, final_x, n);
26929
26930 let mut ref_x = init_data.to_vec();
26932 for _ in 0..length {
26933 let mut a_copy = m_data.clone();
26934 crate::blas::dgesv(&mut a_copy, &mut ref_x, n, 1);
26935 }
26936 for i in 0..n {
26937 assert!(
26938 (got[i] - ref_x[i]).abs() < 1e-12,
26939 "got[{i}] = {} ref {}",
26940 got[i],
26941 ref_x[i]
26942 );
26943 }
26944 let mass: f64 = got.iter().sum();
26949 assert!(mass > 0.0 && mass < 1.0, "diffusion mass: {mass}");
26950 }
26951
26952 #[test]
26956 fn dense_solve_f64_multi_rhs_forward() {
26957 let n = 3usize;
26958 let k = 2usize;
26959 let mut g = Graph::new("solve_multi_rhs");
26960 let a = g.input("A", Shape::new(&[n, n], DType::F64));
26961 let b = g.input("B", Shape::new(&[n, k], DType::F64));
26962 let x = g.dense_solve(a, b, Shape::new(&[n, k], DType::F64));
26963 g.set_outputs(vec![x]);
26964
26965 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
26966 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
26967 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
26968 execute_thunks(&sched, arena.raw_buf_mut());
26969 let x_got = read_arena_f64(&arena, x, n * k);
26970 for c in 0..k {
26971 for i in 0..n {
26972 let mut acc = 0.0_f64;
26973 for j in 0..n {
26974 acc += a_data[i * n + j] * x_got[j * k + c];
26975 }
26976 let want = b_data[i * k + c];
26977 assert!(
26978 (acc - want).abs() < 1e-10,
26979 "col {c} row {i}: got {acc} want {want}"
26980 );
26981 }
26982 }
26983 }
26984
26985 #[test]
26988 fn dense_solve_f64_multi_rhs_gradient() {
26989 use rlx_opt::autodiff::grad_with_loss;
26990 let n = 3usize;
26991 let k = 2usize;
26992 let mut g = Graph::new("solve_mrhs_grad");
26993 let a = g.param("A", Shape::new(&[n, n], DType::F64));
26994 let b = g.input("B", Shape::new(&[n, k], DType::F64));
26995 let x = g.dense_solve(a, b, Shape::new(&[n, k], DType::F64));
26996 let loss = g.reduce(
26997 x,
26998 ReduceOp::Sum,
26999 vec![0, 1],
27000 false,
27001 Shape::new(&[1], DType::F64),
27002 );
27003 g.set_outputs(vec![loss]);
27004
27005 let bwd = grad_with_loss(&g, &[a, b]);
27006 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
27007 for node in graph.nodes() {
27008 let name = match &node.op {
27009 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
27010 _ => None,
27011 };
27012 if name == Some(want) {
27013 return node.id;
27014 }
27015 }
27016 panic!("no node named {want:?}");
27017 };
27018 let a_bwd = find_by_name(&bwd, "A");
27019 let b_bwd = find_by_name(&bwd, "B");
27020 let d_out = find_by_name(&bwd, "d_output");
27021
27022 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
27023 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
27024 let d_seed = [1.0_f64];
27025
27026 let (sched, mut arena) = prepare_f64(
27027 &bwd,
27028 &[(a_bwd, &a_data), (b_bwd, &b_data), (d_out, &d_seed)],
27029 );
27030 execute_thunks(&sched, arena.raw_buf_mut());
27031 let da_got = read_arena_f64(&arena, bwd.outputs[1], n * n);
27032 let db_got = read_arena_f64(&arena, bwd.outputs[2], n * k);
27033
27034 let mut x_ref = b_data;
27036 {
27037 let mut a_copy = a_data;
27038 crate::blas::dgesv(&mut a_copy, &mut x_ref, n, k);
27039 }
27040 let mut at = [0.0_f64; 9];
27041 for i in 0..n {
27042 for j in 0..n {
27043 at[i * n + j] = a_data[j * n + i];
27044 }
27045 }
27046 let mut ones_nk = vec![1.0_f64; n * k];
27047 crate::blas::dgesv(&mut at, &mut ones_nk, n, k);
27048 let db_ref = ones_nk;
27049 let mut da_ref = [0.0_f64; 9];
27050 for i in 0..n {
27051 for j in 0..n {
27052 let mut acc = 0.0_f64;
27053 for c in 0..k {
27054 acc += db_ref[i * k + c] * x_ref[j * k + c];
27055 }
27056 da_ref[i * n + j] = -acc;
27057 }
27058 }
27059 for i in 0..n * k {
27060 assert!(
27061 (db_got[i] - db_ref[i]).abs() < 1e-10,
27062 "dB[{i}]: got {} want {}",
27063 db_got[i],
27064 db_ref[i]
27065 );
27066 }
27067 for i in 0..n * n {
27068 assert!(
27069 (da_got[i] - da_ref[i]).abs() < 1e-10,
27070 "dA[{i}]: got {} want {}",
27071 da_got[i],
27072 da_ref[i]
27073 );
27074 }
27075
27076 let h = 1e-6;
27078 let mut bp = b_data;
27079 bp[0] += h;
27080 let mut bm = b_data;
27081 bm[0] -= h;
27082 let xp = {
27083 let mut a_copy = a_data;
27084 crate::blas::dgesv(&mut a_copy, &mut bp, n, k);
27085 bp
27086 };
27087 let xm = {
27088 let mut a_copy = a_data;
27089 crate::blas::dgesv(&mut a_copy, &mut bm, n, k);
27090 bm
27091 };
27092 let lp: f64 = xp.iter().sum();
27093 let lm: f64 = xm.iter().sum();
27094 let fd = (lp - lm) / (2.0 * h);
27095 assert!(
27096 (db_got[0] - fd).abs() < 1e-7,
27097 "FD dB[0,0]: AD={} FD={}",
27098 db_got[0],
27099 fd
27100 );
27101 }
27102
27103 #[test]
27105 fn dense_solve_f64_multi_rhs_jvp() {
27106 use rlx_opt::autodiff_fwd::jvp;
27107 let n = 3usize;
27108 let k = 2usize;
27109 let mut g = Graph::new("solve_mrhs_jvp");
27110 let a = g.input("A", Shape::new(&[n, n], DType::F64));
27111 let b = g.input("B", Shape::new(&[n, k], DType::F64));
27112 let x = g.dense_solve(a, b, Shape::new(&[n, k], DType::F64));
27113 g.set_outputs(vec![x]);
27114
27115 let jg = jvp(&g, &[b]);
27116 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
27117 for node in graph.nodes() {
27118 let name = match &node.op {
27119 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
27120 _ => None,
27121 };
27122 if name == Some(want) {
27123 return node.id;
27124 }
27125 }
27126 panic!("no node named {want:?}");
27127 };
27128 let a_id = find_by_name(&jg, "A");
27129 let b_id = find_by_name(&jg, "B");
27130 let tb_id = find_by_name(&jg, "tangent_B");
27131
27132 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
27133 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
27134 let tb_data = [0.5, 0.0, -0.25, 1.0, 1.0, -0.5_f64];
27135
27136 let (sched, mut arena) =
27137 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (tb_id, &tb_data)]);
27138 execute_thunks(&sched, arena.raw_buf_mut());
27139 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n * k);
27140
27141 let mut a_copy = a_data;
27142 let mut tb_copy = tb_data;
27143 crate::blas::dgesv(&mut a_copy, &mut tb_copy, n, k);
27144 for i in 0..n * k {
27145 assert!(
27146 (tangent_x[i] - tb_copy[i]).abs() < 1e-10,
27147 "t_X[{i}]: AD={} ref={}",
27148 tangent_x[i],
27149 tb_copy[i]
27150 );
27151 }
27152
27153 let h = 1e-6;
27154 let mut bp = b_data;
27155 let mut bm = b_data;
27156 for i in 0..n * k {
27157 bp[i] += h * tb_data[i];
27158 bm[i] -= h * tb_data[i];
27159 }
27160 let xp = {
27161 let mut a_copy = a_data;
27162 crate::blas::dgesv(&mut a_copy, &mut bp, n, k);
27163 bp
27164 };
27165 let xm = {
27166 let mut a_copy = a_data;
27167 crate::blas::dgesv(&mut a_copy, &mut bm, n, k);
27168 bm
27169 };
27170 for i in 0..n * k {
27171 let fd = (xp[i] - xm[i]) / (2.0 * h);
27172 assert!(
27173 (tangent_x[i] - fd).abs() < 1e-7,
27174 "FD t_X[{i}]: AD={} FD={}",
27175 tangent_x[i],
27176 fd
27177 );
27178 }
27179 }
27180
27181 #[test]
27188 fn jvp_dense_solve_b_runs_and_matches_fd() {
27189 use rlx_opt::autodiff_fwd::jvp;
27190 let n = 3usize;
27191
27192 let mut g = Graph::new("jvp_b_e2e");
27194 let a = g.input("A", Shape::new(&[n, n], DType::F64));
27195 let b = g.input("b", Shape::new(&[n], DType::F64));
27196 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
27197 g.set_outputs(vec![x]);
27198
27199 let jg = jvp(&g, &[b]);
27201 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
27203 for node in graph.nodes() {
27204 let name = match &node.op {
27205 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
27206 _ => None,
27207 };
27208 if name == Some(want) {
27209 return node.id;
27210 }
27211 }
27212 panic!("no node named {want:?}");
27213 };
27214 let a_id = find_by_name(&jg, "A");
27215 let b_id = find_by_name(&jg, "b");
27216 let tb_id = find_by_name(&jg, "tangent_b");
27217
27218 let a_data: [f64; 9] = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0];
27219 let b_data: [f64; 3] = [1.0, 2.0, 3.0];
27220 let tb_data: [f64; 3] = [0.5, -0.25, 1.0];
27222
27223 let (sched, mut arena) =
27224 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (tb_id, &tb_data)]);
27225 execute_thunks(&sched, arena.raw_buf_mut());
27226
27227 let primal_x = read_arena_f64(&arena, jg.outputs[0], n);
27229 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n);
27230
27231 let t_x_ref = {
27233 let mut a = a_data;
27234 let mut tb = tb_data;
27235 let info = crate::blas::dgesv(&mut a, &mut tb, n, 1);
27236 assert_eq!(info, 0);
27237 tb
27238 };
27239 for i in 0..n {
27240 assert!(
27241 (tangent_x[i] - t_x_ref[i]).abs() < 1e-10,
27242 "t_x[{i}]: got {} want {}",
27243 tangent_x[i],
27244 t_x_ref[i]
27245 );
27246 }
27247
27248 let h = 1e-6;
27250 let mut bp = b_data;
27251 let mut bm = b_data;
27252 for i in 0..n {
27253 bp[i] += h * tb_data[i];
27254 bm[i] -= h * tb_data[i];
27255 }
27256 let xp = {
27257 let mut a = a_data;
27258 let info = crate::blas::dgesv(&mut a, &mut bp, n, 1);
27259 assert_eq!(info, 0);
27260 bp
27261 };
27262 let xm = {
27263 let mut a = a_data;
27264 let info = crate::blas::dgesv(&mut a, &mut bm, n, 1);
27265 assert_eq!(info, 0);
27266 bm
27267 };
27268 let fd: Vec<f64> = (0..n).map(|i| (xp[i] - xm[i]) / (2.0 * h)).collect();
27269 for i in 0..n {
27270 assert!(
27271 (tangent_x[i] - fd[i]).abs() < 1e-7,
27272 "FD mismatch t_x[{i}]: AD={} FD={}",
27273 tangent_x[i],
27274 fd[i]
27275 );
27276 }
27277 let primal_ref = {
27279 let mut a = a_data;
27280 let mut b = b_data;
27281 crate::blas::dgesv(&mut a, &mut b, n, 1);
27282 b
27283 };
27284 for i in 0..n {
27285 assert!((primal_x[i] - primal_ref[i]).abs() < 1e-10);
27286 }
27287 }
27288
27289 #[test]
27295 fn jvp_dense_solve_a_runs_and_matches_fd() {
27296 use rlx_opt::autodiff_fwd::jvp;
27297 let n = 3usize;
27298
27299 let mut g = Graph::new("jvp_a_e2e");
27300 let a = g.input("A", Shape::new(&[n, n], DType::F64));
27301 let b = g.input("b", Shape::new(&[n], DType::F64));
27302 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
27303 g.set_outputs(vec![x]);
27304
27305 let jg = jvp(&g, &[a]);
27306 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
27307 for node in graph.nodes() {
27308 let name = match &node.op {
27309 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
27310 _ => None,
27311 };
27312 if name == Some(want) {
27313 return node.id;
27314 }
27315 }
27316 panic!("no node named {want:?}");
27317 };
27318 let a_id = find_by_name(&jg, "A");
27319 let b_id = find_by_name(&jg, "b");
27320 let ta_id = find_by_name(&jg, "tangent_A");
27321
27322 let a_data: [f64; 9] = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0];
27323 let b_data: [f64; 3] = [1.0, 2.0, 3.0];
27324 let ta_data: [f64; 9] = [0.10, -0.05, 0.02, 0.03, 0.20, -0.04, -0.01, 0.07, 0.15];
27326
27327 let (sched, mut arena) =
27328 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (ta_id, &ta_data)]);
27329 execute_thunks(&sched, arena.raw_buf_mut());
27330
27331 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n);
27332
27333 let x_ref = {
27335 let mut a = a_data;
27336 let mut b = b_data;
27337 crate::blas::dgesv(&mut a, &mut b, n, 1);
27338 b
27339 };
27340 let mut prod = [0.0_f64; 3];
27341 for i in 0..n {
27342 for j in 0..n {
27343 prod[i] += ta_data[i * n + j] * x_ref[j];
27344 }
27345 }
27346 let t_x_ref = {
27347 let mut a = a_data;
27348 let mut p = prod;
27349 crate::blas::dgesv(&mut a, &mut p, n, 1);
27350 [-p[0], -p[1], -p[2]]
27351 };
27352 for i in 0..n {
27353 assert!(
27354 (tangent_x[i] - t_x_ref[i]).abs() < 1e-10,
27355 "closed-form t_x[{i}]: AD={} ref={}",
27356 tangent_x[i],
27357 t_x_ref[i]
27358 );
27359 }
27360
27361 let h = 1e-6;
27363 let mut ap = a_data;
27364 let mut am = a_data;
27365 for i in 0..n * n {
27366 ap[i] += h * ta_data[i];
27367 am[i] -= h * ta_data[i];
27368 }
27369 let xp = {
27370 let mut a = ap;
27371 let mut b = b_data;
27372 crate::blas::dgesv(&mut a, &mut b, n, 1);
27373 b
27374 };
27375 let xm = {
27376 let mut a = am;
27377 let mut b = b_data;
27378 crate::blas::dgesv(&mut a, &mut b, n, 1);
27379 b
27380 };
27381 for i in 0..n {
27382 let fd = (xp[i] - xm[i]) / (2.0 * h);
27383 assert!(
27384 (tangent_x[i] - fd).abs() < 1e-7,
27385 "FD t_x[{i}]: AD={} FD={}",
27386 tangent_x[i],
27387 fd
27388 );
27389 }
27390 }
27391
27392 #[test]
27398 fn q_conv2d_matches_reference() {
27399 use rlx_ir::Philox4x32;
27400 let n = 1usize;
27402 let c_in = 2usize;
27403 let h = 5usize;
27404 let w_in = 5usize;
27405 let c_out = 3usize;
27406 let kh = 3usize;
27407 let kw = 3usize;
27408 let ph = 1usize;
27409 let pw = 1usize;
27410 let sh = 1usize;
27411 let sw = 1usize;
27412 let h_out = (h + 2 * ph - kh) / sh + 1;
27413 let w_out = (w_in + 2 * pw - kw) / sw + 1;
27414
27415 let x_scale = 0.04f32;
27416 let w_scale = 0.02f32;
27417 let out_scale = 0.5f32;
27418 let mult = x_scale * w_scale / out_scale;
27419
27420 let mut rng = Philox4x32::new(2099);
27421 let mut xf = vec![0f32; n * c_in * h * w_in];
27422 rng.fill_normal(&mut xf);
27423 let mut wf = vec![0f32; c_out * c_in * kh * kw];
27424 rng.fill_normal(&mut wf);
27425 let xq: Vec<i8> = xf
27426 .iter()
27427 .map(|&v| ((v / x_scale).round() as i32).clamp(-128, 127) as i8)
27428 .collect();
27429 let wq: Vec<i8> = wf
27430 .iter()
27431 .map(|&v| ((v / w_scale).round() as i32).clamp(-128, 127) as i8)
27432 .collect();
27433 let bias: Vec<i32> = vec![0i32; c_out];
27434
27435 let mut g = Graph::new("qconv");
27436 let xn = g.input("x", Shape::new(&[n, c_in, h, w_in], DType::I8));
27437 let wn = g.input("w", Shape::new(&[c_out, c_in, kh, kw], DType::I8));
27438 let bn = g.input("b", Shape::new(&[c_out], DType::I32));
27439 let out = g.q_conv2d(
27440 xn,
27441 wn,
27442 bn,
27443 vec![kh, kw],
27444 vec![sh, sw],
27445 vec![ph, pw],
27446 vec![1, 1],
27447 1,
27448 0,
27449 0,
27450 0,
27451 mult,
27452 Shape::new(&[n, c_out, h_out, w_out], DType::I8),
27453 );
27454 g.set_outputs(vec![out]);
27455
27456 let plan = rlx_opt::memory::plan_memory(&g);
27457 let mut arena = crate::arena::Arena::from_plan(plan);
27458 let sched = compile_thunks(&g, &arena);
27459 let xn_off = arena.byte_offset(xn);
27462 let wn_off = arena.byte_offset(wn);
27463 let bn_off = arena.byte_offset(bn);
27464 let out_off = arena.byte_offset(out);
27465 let buf = arena.raw_buf_mut();
27466 unsafe {
27467 let p = buf.as_mut_ptr().add(xn_off) as *mut i8;
27468 for (i, &v) in xq.iter().enumerate() {
27469 *p.add(i) = v;
27470 }
27471 let p = buf.as_mut_ptr().add(wn_off) as *mut i8;
27472 for (i, &v) in wq.iter().enumerate() {
27473 *p.add(i) = v;
27474 }
27475 let p = buf.as_mut_ptr().add(bn_off) as *mut i32;
27476 for (i, &v) in bias.iter().enumerate() {
27477 *p.add(i) = v;
27478 }
27479 }
27480 execute_thunks(&sched, arena.raw_buf_mut());
27481 let out_q: Vec<i8> = unsafe {
27482 let p = arena.raw_buf().as_ptr().add(out_off) as *const i8;
27483 (0..n * c_out * h_out * w_out).map(|i| *p.add(i)).collect()
27484 };
27485
27486 let mut out_ref = vec![0i8; n * c_out * h_out * w_out];
27488 for ni in 0..n {
27489 for co in 0..c_out {
27490 for ho in 0..h_out {
27491 for wo in 0..w_out {
27492 let mut acc: i32 = 0;
27493 for ci in 0..c_in {
27494 for ki in 0..kh {
27495 for kj in 0..kw {
27496 let hi = ho * sh + ki;
27497 let wi = wo * sw + kj;
27498 if hi < ph || wi < pw {
27499 continue;
27500 }
27501 let hi = hi - ph;
27502 let wi = wi - pw;
27503 if hi >= h || wi >= w_in {
27504 continue;
27505 }
27506 let xv =
27507 xq[((ni * c_in) + ci) * h * w_in + hi * w_in + wi] as i32;
27508 let wv = wq[((co * c_in) + ci) * kh * kw + ki * kw + kj] as i32;
27509 acc += xv * wv;
27510 }
27511 }
27512 }
27513 let r = (acc as f32 * mult).round() as i32;
27514 let r = r.clamp(-128, 127) as i8;
27515 out_ref[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = r;
27516 }
27517 }
27518 }
27519 }
27520
27521 for (i, (a, r)) in out_q.iter().zip(&out_ref).enumerate() {
27522 assert_eq!(a, r, "q_conv2d[{i}]: kernel {a} vs reference {r}");
27523 }
27524 }
27525
27526 #[test]
27534 fn q_matmul_matches_fake_quant_reference() {
27535 use rlx_ir::Philox4x32;
27536 let m = 3usize;
27537 let k = 8usize;
27538 let n = 5usize;
27539 let mut rng = Philox4x32::new(2031);
27540
27541 let x_scale = 0.05f32;
27543 let w_scale = 0.03f32;
27544 let out_scale = 0.4f32;
27545 let mult = x_scale * w_scale / out_scale;
27546 let mut xf = vec![0f32; m * k];
27547 rng.fill_normal(&mut xf);
27548 let mut wf = vec![0f32; k * n];
27549 rng.fill_normal(&mut wf);
27550 let xq: Vec<i8> = xf
27551 .iter()
27552 .map(|&v| ((v / x_scale).round() as i32).clamp(-128, 127) as i8)
27553 .collect();
27554 let wq: Vec<i8> = wf
27555 .iter()
27556 .map(|&v| ((v / w_scale).round() as i32).clamp(-128, 127) as i8)
27557 .collect();
27558 let bias: Vec<i32> = vec![0i32; n];
27559
27560 let _f = DType::F32;
27562 let mut g_q = Graph::new("qmm_direct");
27563 let xn = g_q.input("x", Shape::new(&[m, k], DType::I8));
27564 let wn = g_q.input("w", Shape::new(&[k, n], DType::I8));
27565 let bn = g_q.input("b", Shape::new(&[n], DType::I32));
27566 let out = g_q.q_matmul(xn, wn, bn, 0, 0, 0, mult, Shape::new(&[m, n], DType::I8));
27567 g_q.set_outputs(vec![out]);
27568 let plan = rlx_opt::memory::plan_memory(&g_q);
27569 let mut arena = crate::arena::Arena::from_plan(plan);
27570 let sched = compile_thunks(&g_q, &arena);
27571
27572 let xn_off = arena.byte_offset(xn);
27574 let wn_off = arena.byte_offset(wn);
27575 let bn_off = arena.byte_offset(bn);
27576 let out_off = arena.byte_offset(out);
27577 let buf = arena.raw_buf_mut();
27578 unsafe {
27579 let p = buf.as_mut_ptr().add(xn_off) as *mut i8;
27580 for (i, &v) in xq.iter().enumerate() {
27581 *p.add(i) = v;
27582 }
27583 let p = buf.as_mut_ptr().add(wn_off) as *mut i8;
27584 for (i, &v) in wq.iter().enumerate() {
27585 *p.add(i) = v;
27586 }
27587 let p = buf.as_mut_ptr().add(bn_off) as *mut i32;
27588 for (i, &v) in bias.iter().enumerate() {
27589 *p.add(i) = v;
27590 }
27591 }
27592 execute_thunks(&sched, arena.raw_buf_mut());
27593 let out_q: Vec<i8> = unsafe {
27594 let p = arena.raw_buf().as_ptr().add(out_off) as *const i8;
27595 (0..m * n).map(|i| *p.add(i)).collect()
27596 };
27597
27598 let mut out_ref = vec![0i8; m * n];
27603 for mi in 0..m {
27604 for ni in 0..n {
27605 let mut acc: i32 = 0;
27606 for ki in 0..k {
27607 acc += (xq[mi * k + ki] as i32) * (wq[ki * n + ni] as i32);
27608 }
27609 let r = (acc as f32 * mult).round() as i32;
27610 out_ref[mi * n + ni] = r.clamp(-128, 127) as i8;
27611 }
27612 }
27613
27614 for (i, (a, r)) in out_q.iter().zip(&out_ref).enumerate() {
27615 assert_eq!(a, r, "q_matmul[{i}]: kernel {a} vs reference {r}");
27616 }
27617 }
27618
27619 #[test]
27624 fn quantize_dequantize_round_trip() {
27625 use rlx_ir::Philox4x32;
27626 let len = 64;
27627 let mut rng = Philox4x32::new(2027);
27628 let mut x = vec![0f32; len];
27629 rng.fill_normal(&mut x);
27630 x[0] = 999.0;
27633 x[1] = -999.0;
27634
27635 let scale = 0.05f32;
27636 let zp = 3i32;
27637
27638 let f = DType::F32;
27639 let mut g = Graph::new("qdq");
27640 let xn = g.input("x", Shape::new(&[len], f));
27641 let q = g.quantize(xn, scale, zp);
27642 let dq = g.dequantize(q, scale, zp);
27643 g.set_outputs(vec![dq]);
27644
27645 let plan = rlx_opt::memory::plan_memory(&g);
27646 let mut arena = crate::arena::Arena::from_plan(plan);
27647 let sched = compile_thunks(&g, &arena);
27648 let xn_off = arena.byte_offset(xn);
27649 let dq_off = arena.byte_offset(dq);
27650 let buf = arena.raw_buf_mut();
27651 unsafe {
27652 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
27653 for (i, &v) in x.iter().enumerate() {
27654 *p.add(i) = v;
27655 }
27656 }
27657 execute_thunks(&sched, arena.raw_buf_mut());
27658 let out: Vec<f32> = unsafe {
27659 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
27660 (0..len).map(|i| *p.add(i)).collect()
27661 };
27662
27663 let sat_pos = (127 - zp) as f32 * scale;
27666 let sat_neg = (-128 - zp) as f32 * scale;
27667 assert!((out[0] - sat_pos).abs() < 1e-6, "+sat: {}", out[0]);
27668 assert!((out[1] - sat_neg).abs() < 1e-6, "-sat: {}", out[1]);
27669
27670 for i in 2..len {
27673 assert!(
27674 (out[i] - x[i]).abs() <= scale + 1e-5,
27675 "qdq[{i}]: {} → {}, scale={scale}",
27676 x[i],
27677 out[i]
27678 );
27679 }
27680 }
27681
27682 #[test]
27688 fn quantize_per_channel_round_trip() {
27689 let c = 4usize;
27690 let inner = 5usize;
27691 let mags = [0.01f32, 0.5, 5.0, 50.0];
27694 let mut x = vec![0f32; c * inner];
27695 for ci in 0..c {
27696 for ii in 0..inner {
27697 x[ci * inner + ii] = match ii {
27701 0 => -mags[ci],
27702 1 => 0.0,
27703 2 => mags[ci],
27704 3 => mags[ci] * 1000.0, _ => -mags[ci] * 1000.0, };
27707 }
27708 }
27709 let scales: Vec<f32> = mags.iter().map(|&m| m / 127.0).collect();
27710 let zps: Vec<i32> = vec![0, 0, 0, 0];
27711
27712 let f = DType::F32;
27713 let mut g = Graph::new("qdq_pc");
27714 let xn = g.input("x", Shape::new(&[c, inner], f));
27715 let q = g.quantize_per_channel(xn, 0, scales.clone(), zps.clone());
27716 let dq = g.dequantize_per_channel(q, 0, scales.clone(), zps);
27717 g.set_outputs(vec![dq]);
27718
27719 let plan = rlx_opt::memory::plan_memory(&g);
27720 let mut arena = crate::arena::Arena::from_plan(plan);
27721 let sched = compile_thunks(&g, &arena);
27722 let xn_off = arena.byte_offset(xn);
27723 let dq_off = arena.byte_offset(dq);
27724 let buf = arena.raw_buf_mut();
27725 unsafe {
27726 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
27727 for (i, &v) in x.iter().enumerate() {
27728 *p.add(i) = v;
27729 }
27730 }
27731 execute_thunks(&sched, arena.raw_buf_mut());
27732 let out: Vec<f32> = unsafe {
27733 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
27734 (0..c * inner).map(|i| *p.add(i)).collect()
27735 };
27736
27737 for ci in 0..c {
27738 for ii in 0..3 {
27741 let idx = ci * inner + ii;
27742 assert!(
27743 (out[idx] - x[idx]).abs() <= scales[ci] + 1e-5,
27744 "ch {ci} idx {ii}: {} vs {}",
27745 x[idx],
27746 out[idx]
27747 );
27748 }
27749 let sat_pos = 127.0 * scales[ci];
27751 let sat_neg = -128.0 * scales[ci];
27752 assert!(
27753 (out[ci * inner + 3] - sat_pos).abs() < 1e-5,
27754 "ch {ci} +sat: {}",
27755 out[ci * inner + 3]
27756 );
27757 assert!(
27758 (out[ci * inner + 4] - sat_neg).abs() < 1e-5,
27759 "ch {ci} -sat: {}",
27760 out[ci * inner + 4]
27761 );
27762 }
27763 }
27764
27765 #[test]
27771 fn activation_backward_matches_numerical_per_kind() {
27772 use rlx_ir::Philox4x32;
27773 use rlx_ir::op::Activation;
27774 let mut rng = Philox4x32::new(91);
27775 let len = 32;
27776 let mut x_pos = vec![0f32; len];
27781 rng.fill_normal(&mut x_pos);
27782 for v in x_pos.iter_mut() {
27783 *v = v.abs() + 0.5;
27784 }
27785 let mut x_any = vec![0f32; len];
27786 rng.fill_normal(&mut x_any);
27787 let mut dy = vec![0f32; len];
27788 rng.fill_normal(&mut dy);
27789
27790 for &(kind, x_data, eps, tol) in &[
27791 (Activation::Sigmoid, &x_any[..], 1e-3, 5e-3),
27792 (Activation::Tanh, &x_any[..], 1e-3, 5e-3),
27793 (Activation::Silu, &x_any[..], 1e-3, 5e-3),
27794 (Activation::Gelu, &x_any[..], 1e-3, 5e-3),
27795 (Activation::GeluApprox, &x_any[..], 1e-3, 5e-3),
27796 (Activation::Exp, &x_any[..], 1e-4, 5e-3),
27797 (Activation::Log, &x_pos[..], 1e-4, 5e-3),
27798 (Activation::Sqrt, &x_pos[..], 1e-4, 5e-3),
27799 (Activation::Rsqrt, &x_pos[..], 1e-4, 5e-3),
27800 (Activation::Neg, &x_any[..], 1e-3, 5e-4),
27801 ] {
27802 let f = DType::F32;
27803 let mut g = Graph::new("act_bw");
27804 let xn = g.input("x", Shape::new(&[len], f));
27805 let dyn_ = g.input("dy", Shape::new(&[len], f));
27806 let dx = g.activation_backward(kind, xn, dyn_);
27807 g.set_outputs(vec![dx]);
27808
27809 let plan = rlx_opt::memory::plan_memory(&g);
27810 let mut arena = crate::arena::Arena::from_plan(plan);
27811 let sched = compile_thunks(&g, &arena);
27812
27813 let xn_off = arena.byte_offset(xn);
27814 let dyn_off = arena.byte_offset(dyn_);
27815 let dx_off = arena.byte_offset(dx);
27816 let buf = arena.raw_buf_mut();
27817 unsafe {
27818 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
27819 for (i, &v) in x_data.iter().enumerate() {
27820 *p.add(i) = v;
27821 }
27822 let p = buf.as_mut_ptr().add(dyn_off) as *mut f32;
27823 for (i, &v) in dy.iter().enumerate() {
27824 *p.add(i) = v;
27825 }
27826 }
27827 execute_thunks(&sched, arena.raw_buf_mut());
27828 let analytical: Vec<f32> = unsafe {
27829 let p = arena.raw_buf().as_ptr().add(dx_off) as *const f32;
27830 (0..len).map(|i| *p.add(i)).collect()
27831 };
27832
27833 let act_apply = |kind: Activation, x: f32| -> f32 {
27836 match kind {
27837 Activation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
27838 Activation::Tanh => x.tanh(),
27839 Activation::Silu => x / (1.0 + (-x).exp()),
27840 Activation::Gelu => {
27841 const INV_SQRT2: f32 = 0.707_106_77;
27843 0.5 * x * (1.0 + erf_f32(x * INV_SQRT2))
27844 }
27845 Activation::GeluApprox => {
27846 const C: f32 = 0.797_884_6;
27847 const A: f32 = 0.044_715;
27848 let inner = C * (x + A * x * x * x);
27849 0.5 * x * (1.0 + inner.tanh())
27850 }
27851 Activation::Exp => x.exp(),
27852 Activation::Log => x.ln(),
27853 Activation::Sqrt => x.sqrt(),
27854 Activation::Rsqrt => 1.0 / x.sqrt(),
27855 Activation::Neg => -x,
27856 Activation::Relu => x.max(0.0),
27857 Activation::Abs => x.abs(),
27858 Activation::Round => x.round(),
27859 Activation::Sin => x.sin(),
27860 Activation::Cos => x.cos(),
27861 Activation::Tan => x.tan(),
27862 Activation::Atan => x.atan(),
27863 }
27864 };
27865 for i in 0..len {
27866 let xv = x_data[i];
27867 let plus = act_apply(kind, xv + eps);
27868 let minus = act_apply(kind, xv - eps);
27869 let num = (plus - minus) / (2.0 * eps) * dy[i];
27870 assert!(
27871 (analytical[i] - num).abs() < tol,
27872 "{kind:?}[{i}]: analytical {} vs numerical {num}",
27873 analytical[i]
27874 );
27875 }
27876 }
27877 }
27878
27879 #[test]
27883 fn matmul_3d_gradient_matches_numerical() {
27884 use rlx_ir::Philox4x32;
27885 let batch = 2usize;
27886 let m = 3usize;
27887 let k = 4usize;
27888 let n = 5usize;
27889 let mut rng = Philox4x32::new(101);
27890 let mut a_data = vec![0f32; batch * m * k];
27891 rng.fill_normal(&mut a_data);
27892 let mut b_data = vec![0f32; batch * k * n];
27893 rng.fill_normal(&mut b_data);
27894
27895 let f = DType::F32;
27896 let mut fwd = Graph::new("matmul_3d");
27897 let an = fwd.input("a", Shape::new(&[batch, m, k], f));
27898 let bp = fwd.param("b", Shape::new(&[batch, k, n], f));
27899 let mm = fwd.matmul(an, bp, Shape::new(&[batch, m, n], f));
27900 let loss = fwd.add_node(
27901 Op::Reduce {
27902 op: ReduceOp::Sum,
27903 axes: vec![0, 1, 2],
27904 keep_dim: false,
27905 },
27906 vec![mm],
27907 Shape::from_dims(&[], f),
27908 );
27909 fwd.set_outputs(vec![loss]);
27910
27911 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[bp]);
27912 let d_out = bwd_graph
27913 .nodes()
27914 .iter()
27915 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
27916 .map(|n| n.id)
27917 .unwrap();
27918
27919 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
27920 let mut arena = crate::arena::Arena::from_plan(plan);
27921 let sched = compile_thunks(&bwd_graph, &arena);
27922 for &(id, data) in &[(an, &a_data), (bp, &b_data), (d_out, &vec![1.0f32])] {
27923 let off = arena.byte_offset(id);
27924 let buf = arena.raw_buf_mut();
27925 unsafe {
27926 let p = buf.as_mut_ptr().add(off) as *mut f32;
27927 for (i, &v) in data.iter().enumerate() {
27928 *p.add(i) = v;
27929 }
27930 }
27931 }
27932 execute_thunks(&sched, arena.raw_buf_mut());
27933 let gb_id = bwd_graph.outputs[1];
27934 let g_b: Vec<f32> = unsafe {
27935 let p = arena.raw_buf().as_ptr().add(arena.byte_offset(gb_id)) as *const f32;
27936 (0..batch * k * n).map(|i| *p.add(i)).collect()
27937 };
27938
27939 let forward_loss = |b_vals: &[f32]| -> f32 {
27941 let mut out = vec![0f32; batch * m * n];
27942 for bi in 0..batch {
27943 for mi in 0..m {
27944 for ni in 0..n {
27945 let mut acc = 0f32;
27946 for ki in 0..k {
27947 acc +=
27948 a_data[bi * m * k + mi * k + ki] * b_vals[bi * k * n + ki * n + ni];
27949 }
27950 out[bi * m * n + mi * n + ni] = acc;
27951 }
27952 }
27953 }
27954 out.iter().sum()
27955 };
27956 let eps = 1e-3f32;
27957 let mut bp_p = b_data.clone();
27958 let mut g_b_num = vec![0f32; b_data.len()];
27959 for i in 0..b_data.len() {
27960 let s = bp_p[i];
27961 bp_p[i] = s + eps;
27962 let lp = forward_loss(&bp_p);
27963 bp_p[i] = s - eps;
27964 let lm = forward_loss(&bp_p);
27965 bp_p[i] = s;
27966 g_b_num[i] = (lp - lm) / (2.0 * eps);
27967 }
27968 for (i, (a, n)) in g_b.iter().zip(&g_b_num).enumerate() {
27969 assert!(
27970 (a - n).abs() < 5e-3,
27971 "matmul_3d g_b[{i}]: analytical {a} vs numerical {n}"
27972 );
27973 }
27974 }
27975
27976 #[test]
27982 fn softmax_gradient_matches_numerical() {
27983 use rlx_ir::Philox4x32;
27984 let n = 3usize;
27985 let c = 5usize;
27986 let mut rng = Philox4x32::new(57);
27987 let mut x_data = vec![0f32; n * c];
27988 rng.fill_normal(&mut x_data);
27989
27990 let f = DType::F32;
27991 let mut fwd = Graph::new("softmax_only");
27992 let xn = fwd.input("x", Shape::new(&[n, c], f));
27993 let sm = fwd.add_node(Op::Softmax { axis: -1 }, vec![xn], Shape::new(&[n, c], f));
27994 let loss = fwd.add_node(
27998 Op::Reduce {
27999 op: ReduceOp::Sum,
28000 axes: vec![0, 1],
28001 keep_dim: false,
28002 },
28003 vec![sm],
28004 Shape::from_dims(&[], f),
28005 );
28006 fwd.set_outputs(vec![loss]);
28007
28008 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[xn]);
28012 let d_out = bwd_graph
28013 .nodes()
28014 .iter()
28015 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28016 .map(|n| n.id)
28017 .unwrap();
28018
28019 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
28020 let mut arena = crate::arena::Arena::from_plan(plan);
28021 let sched = compile_thunks(&bwd_graph, &arena);
28022 for &(id, data) in &[(xn, &x_data), (d_out, &vec![1.0f32])] {
28023 let off = arena.byte_offset(id);
28024 let buf = arena.raw_buf_mut();
28025 unsafe {
28026 let p = buf.as_mut_ptr().add(off) as *mut f32;
28027 for (i, &v) in data.iter().enumerate() {
28028 *p.add(i) = v;
28029 }
28030 }
28031 }
28032 execute_thunks(&sched, arena.raw_buf_mut());
28033 let g_x_id = bwd_graph.outputs[1];
28034 let g_x: Vec<f32> = unsafe {
28035 let p = arena.raw_buf().as_ptr().add(arena.byte_offset(g_x_id)) as *const f32;
28036 (0..n * c).map(|i| *p.add(i)).collect()
28037 };
28038
28039 let forward_loss = |x: &[f32]| -> f32 {
28043 let mut total = 0f32;
28044 for ni in 0..n {
28045 let row = &x[ni * c..(ni + 1) * c];
28046 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
28047 let denom: f32 = row.iter().map(|&v| (v - m).exp()).sum();
28048 for &v in row {
28049 total += (v - m).exp() / denom;
28050 }
28051 }
28052 total
28053 };
28054 let eps = 1e-3f32;
28055 let mut p = x_data.clone();
28056 for i in 0..x_data.len() {
28057 let s = p[i];
28058 p[i] = s + eps;
28059 let lp = forward_loss(&p);
28060 p[i] = s - eps;
28061 let lm = forward_loss(&p);
28062 p[i] = s;
28063 let num = (lp - lm) / (2.0 * eps);
28064 assert!(
28065 (g_x[i] - num).abs() < 5e-3,
28066 "softmax g_x[{i}]: analytical {} vs numerical {num}",
28067 g_x[i]
28068 );
28069 }
28070 }
28071
28072 #[test]
28077 fn layer_norm_gradient_matches_numerical() {
28078 use rlx_ir::Philox4x32;
28079 let rows = 3usize;
28080 let h = 6usize;
28081 let mut rng = Philox4x32::new(1009);
28082 let mut x_data = vec![0f32; rows * h];
28083 rng.fill_normal(&mut x_data);
28084 let mut g_data = vec![0f32; h];
28085 rng.fill_normal(&mut g_data);
28086 for v in g_data.iter_mut() {
28087 *v = v.abs() + 0.5;
28088 }
28089 let mut b_data = vec![0f32; h];
28090 rng.fill_normal(&mut b_data);
28091 let eps = 1e-5f32;
28092
28093 let f = DType::F32;
28094 let mut fwd = Graph::new("ln_only");
28095 let xn = fwd.input("x", Shape::new(&[rows, h], f));
28096 let gp = fwd.param("gamma", Shape::new(&[h], f));
28097 let bp = fwd.param("beta", Shape::new(&[h], f));
28098 let ln = fwd.add_node(
28099 Op::LayerNorm { axis: -1, eps },
28100 vec![xn, gp, bp],
28101 Shape::new(&[rows, h], f),
28102 );
28103 let loss = fwd.add_node(
28104 Op::Reduce {
28105 op: ReduceOp::Sum,
28106 axes: vec![0, 1],
28107 keep_dim: false,
28108 },
28109 vec![ln],
28110 Shape::from_dims(&[], f),
28111 );
28112 fwd.set_outputs(vec![loss]);
28113
28114 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[xn, gp, bp]);
28115 let d_out = bwd_graph
28116 .nodes()
28117 .iter()
28118 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28119 .map(|n| n.id)
28120 .unwrap();
28121
28122 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
28123 let mut arena = crate::arena::Arena::from_plan(plan);
28124 let sched = compile_thunks(&bwd_graph, &arena);
28125 for &(id, data) in &[
28126 (xn, &x_data),
28127 (gp, &g_data),
28128 (bp, &b_data),
28129 (d_out, &vec![1.0f32]),
28130 ] {
28131 let off = arena.byte_offset(id);
28132 let buf = arena.raw_buf_mut();
28133 unsafe {
28134 let p = buf.as_mut_ptr().add(off) as *mut f32;
28135 for (i, &v) in data.iter().enumerate() {
28136 *p.add(i) = v;
28137 }
28138 }
28139 }
28140 execute_thunks(&sched, arena.raw_buf_mut());
28141 let read = |id: NodeId, n: usize| -> Vec<f32> {
28142 let off = arena.byte_offset(id);
28143 unsafe {
28144 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
28145 (0..n).map(|i| *p.add(i)).collect()
28146 }
28147 };
28148 let dx_a = read(bwd_graph.outputs[1], rows * h);
28149 let dg_a = read(bwd_graph.outputs[2], h);
28150 let db_a = read(bwd_graph.outputs[3], h);
28151
28152 let forward_loss = |x: &[f32], g: &[f32], b: &[f32]| -> f32 {
28153 let mut total = 0f32;
28154 for r in 0..rows {
28155 let row = &x[r * h..(r + 1) * h];
28156 let mean = row.iter().sum::<f32>() / h as f32;
28157 let var = row.iter().map(|&v| (v - mean) * (v - mean)).sum::<f32>() / h as f32;
28158 let inv_std = 1.0 / (var + eps).sqrt();
28159 for d in 0..h {
28160 total += ((row[d] - mean) * inv_std) * g[d] + b[d];
28161 }
28162 }
28163 total
28164 };
28165 let h_eps = 1e-3f32;
28166
28167 let mut x_p = x_data.clone();
28168 for i in 0..x_p.len() {
28169 let s = x_p[i];
28170 x_p[i] = s + h_eps;
28171 let lp = forward_loss(&x_p, &g_data, &b_data);
28172 x_p[i] = s - h_eps;
28173 let lm = forward_loss(&x_p, &g_data, &b_data);
28174 x_p[i] = s;
28175 let num = (lp - lm) / (2.0 * h_eps);
28176 assert!(
28177 (dx_a[i] - num).abs() < 5e-3,
28178 "ln dx[{i}]: analytical {} vs numerical {num}",
28179 dx_a[i]
28180 );
28181 }
28182 let mut g_p = g_data.clone();
28183 for i in 0..g_p.len() {
28184 let s = g_p[i];
28185 g_p[i] = s + h_eps;
28186 let lp = forward_loss(&x_data, &g_p, &b_data);
28187 g_p[i] = s - h_eps;
28188 let lm = forward_loss(&x_data, &g_p, &b_data);
28189 g_p[i] = s;
28190 let num = (lp - lm) / (2.0 * h_eps);
28191 assert!(
28192 (dg_a[i] - num).abs() < 5e-3,
28193 "ln dg[{i}]: analytical {} vs numerical {num}",
28194 dg_a[i]
28195 );
28196 }
28197 let mut b_p = b_data.clone();
28198 for i in 0..b_p.len() {
28199 let s = b_p[i];
28200 b_p[i] = s + h_eps;
28201 let lp = forward_loss(&x_data, &g_data, &b_p);
28202 b_p[i] = s - h_eps;
28203 let lm = forward_loss(&x_data, &g_data, &b_p);
28204 b_p[i] = s;
28205 let num = (lp - lm) / (2.0 * h_eps);
28206 assert!(
28207 (db_a[i] - num).abs() < 5e-3,
28208 "ln db[{i}]: analytical {} vs numerical {num}",
28209 db_a[i]
28210 );
28211 }
28212 }
28213
28214 #[test]
28219 fn dense_sce_mean_gradient_matches_numerical() {
28220 use rlx_ir::Philox4x32;
28221 let bs = 4usize;
28222 let k_in = 3usize;
28223 let c = 5usize;
28224 let mut rng = Philox4x32::new(7);
28225 let mut x = vec![0f32; bs * k_in];
28226 rng.fill_normal(&mut x);
28227 let mut w_init = vec![0f32; k_in * c];
28228 rng.fill_normal(&mut w_init);
28229 let mut b_init = vec![0f32; c];
28230 rng.fill_normal(&mut b_init);
28231 let labels: Vec<f32> = (0..bs).map(|i| (i % c) as f32).collect();
28232
28233 let f = DType::F32;
28235 let mut fwd = Graph::new("dense_sce");
28236 let xn = fwd.input("x", Shape::new(&[bs, k_in], f));
28237 let lb = fwd.input("labels", Shape::new(&[bs], f));
28238 let wp = fwd.param("w", Shape::new(&[k_in, c], f));
28239 let bp = fwd.param("b", Shape::new(&[c], f));
28240 let mm = fwd.matmul(xn, wp, Shape::new(&[bs, c], f));
28241 let logits = fwd.binary(BinaryOp::Add, mm, bp, Shape::new(&[bs, c], f));
28242 let loss_per = fwd.softmax_cross_entropy_with_logits(logits, lb);
28243 let loss = fwd.add_node(
28244 Op::Reduce {
28245 op: ReduceOp::Sum,
28246 axes: vec![0],
28247 keep_dim: false,
28248 },
28249 vec![loss_per],
28250 Shape::from_dims(&[], f),
28252 );
28253 fwd.set_outputs(vec![loss]);
28261
28262 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wp, bp]);
28264 let d_out = bwd_graph
28267 .nodes()
28268 .iter()
28269 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28270 .map(|n| n.id)
28271 .expect("d_output input");
28272
28273 let (sched, mut arena) = prepare(
28274 &bwd_graph,
28275 &[
28276 (xn, &x),
28277 (lb, &labels),
28278 (wp, &w_init),
28279 (bp, &b_init),
28280 (d_out, &[1.0]),
28281 ],
28282 );
28283 execute_thunks(&sched, arena.raw_buf_mut());
28284
28285 let outs = &bwd_graph.outputs;
28286 let loss_id = outs[0];
28287 let gw_id = outs[1];
28288 let gb_id = outs[2];
28289 let loss_actual = read_arena(&arena, loss_id, 1)[0];
28290 let gw_actual = read_arena(&arena, gw_id, k_in * c);
28291 let gb_actual = read_arena(&arena, gb_id, c);
28292
28293 let plan = rlx_opt::memory::plan_memory(&fwd);
28297 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
28298 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
28299 write_arena(&mut fwd_arena, xn, &x);
28300 write_arena(&mut fwd_arena, lb, &labels);
28301
28302 let run_loss = |arena: &mut crate::arena::Arena, w: &[f32], b: &[f32]| -> f32 {
28303 write_arena(arena, wp, w);
28304 write_arena(arena, bp, b);
28305 execute_thunks(&fwd_sched, arena.raw_buf_mut());
28306 read_arena(arena, loss, 1)[0]
28307 };
28308
28309 let loss_check = run_loss(&mut fwd_arena, &w_init, &b_init);
28312 assert!(
28313 (loss_actual - loss_check).abs() < 1e-4,
28314 "loss mismatch: bwd graph {loss_actual} vs fwd-only {loss_check}"
28315 );
28316
28317 let eps = 1e-3f32;
28318 let mut w_perturbed = w_init.clone();
28319 let mut gw_numerical = vec![0f32; w_init.len()];
28320 for i in 0..w_init.len() {
28321 let saved = w_perturbed[i];
28322 w_perturbed[i] = saved + eps;
28323 let lp = run_loss(&mut fwd_arena, &w_perturbed, &b_init);
28324 w_perturbed[i] = saved - eps;
28325 let lm = run_loss(&mut fwd_arena, &w_perturbed, &b_init);
28326 w_perturbed[i] = saved;
28327 gw_numerical[i] = (lp - lm) / (2.0 * eps);
28328 }
28329 for (i, (a, n)) in gw_actual.iter().zip(&gw_numerical).enumerate() {
28330 assert!(
28331 (a - n).abs() < 5e-3,
28332 "grad_w[{i}]: analytical {a} vs numerical {n}"
28333 );
28334 }
28335
28336 let mut b_perturbed = b_init.clone();
28337 let mut gb_numerical = vec![0f32; b_init.len()];
28338 for i in 0..b_init.len() {
28339 let saved = b_perturbed[i];
28340 b_perturbed[i] = saved + eps;
28341 let lp = run_loss(&mut fwd_arena, &w_init, &b_perturbed);
28342 b_perturbed[i] = saved - eps;
28343 let lm = run_loss(&mut fwd_arena, &w_init, &b_perturbed);
28344 b_perturbed[i] = saved;
28345 gb_numerical[i] = (lp - lm) / (2.0 * eps);
28346 }
28347 for (i, (a, n)) in gb_actual.iter().zip(&gb_numerical).enumerate() {
28348 assert!(
28349 (a - n).abs() < 5e-3,
28350 "grad_b[{i}]: analytical {a} vs numerical {n}"
28351 );
28352 }
28353 }
28354
28355 #[test]
28358 fn dense_sce_mean_reduce_gradient_matches_numerical() {
28359 use rlx_ir::Philox4x32;
28360 let bs = 3usize;
28361 let k_in = 2usize;
28362 let c = 4usize;
28363 let mut rng = Philox4x32::new(13);
28364 let mut x = vec![0f32; bs * k_in];
28365 rng.fill_normal(&mut x);
28366 let mut w_init = vec![0f32; k_in * c];
28367 rng.fill_normal(&mut w_init);
28368 let labels: Vec<f32> = (0..bs).map(|i| (i % c) as f32).collect();
28369
28370 let f = DType::F32;
28371 let mut fwd = Graph::new("dense_sce_mean");
28372 let xn = fwd.input("x", Shape::new(&[bs, k_in], f));
28373 let lb = fwd.input("labels", Shape::new(&[bs], f));
28374 let wp = fwd.param("w", Shape::new(&[k_in, c], f));
28375 let mm = fwd.matmul(xn, wp, Shape::new(&[bs, c], f));
28376 let loss_per = fwd.softmax_cross_entropy_with_logits(mm, lb);
28377 let loss = fwd.add_node(
28378 Op::Reduce {
28379 op: ReduceOp::Mean,
28380 axes: vec![0],
28381 keep_dim: false,
28382 },
28383 vec![loss_per],
28384 Shape::from_dims(&[], f),
28385 );
28386 fwd.set_outputs(vec![loss]);
28387
28388 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wp]);
28389 let d_out = bwd_graph
28390 .nodes()
28391 .iter()
28392 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28393 .map(|n| n.id)
28394 .unwrap();
28395
28396 let (sched, mut arena) = prepare(
28397 &bwd_graph,
28398 &[(xn, &x), (lb, &labels), (wp, &w_init), (d_out, &[1.0])],
28399 );
28400 execute_thunks(&sched, arena.raw_buf_mut());
28401
28402 let outs = &bwd_graph.outputs;
28403 let loss_id = outs[0];
28404 let gw_id = outs[1];
28405 let _ = read_arena(&arena, loss_id, 1)[0];
28406 let gw_actual = read_arena(&arena, gw_id, k_in * c);
28407
28408 let plan = rlx_opt::memory::plan_memory(&fwd);
28409 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
28410 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
28411 write_arena(&mut fwd_arena, xn, &x);
28412 write_arena(&mut fwd_arena, lb, &labels);
28413
28414 let run_loss = |arena: &mut crate::arena::Arena, w: &[f32]| -> f32 {
28415 write_arena(arena, wp, w);
28416 execute_thunks(&fwd_sched, arena.raw_buf_mut());
28417 read_arena(arena, loss, 1)[0]
28418 };
28419
28420 let eps = 1e-3f32;
28421 let mut wp_p = w_init.clone();
28422 let mut gw_num = vec![0f32; w_init.len()];
28423 for i in 0..w_init.len() {
28424 let s = wp_p[i];
28425 wp_p[i] = s + eps;
28426 let lp = run_loss(&mut fwd_arena, &wp_p);
28427 wp_p[i] = s - eps;
28428 let lm = run_loss(&mut fwd_arena, &wp_p);
28429 wp_p[i] = s;
28430 gw_num[i] = (lp - lm) / (2.0 * eps);
28431 }
28432 for (i, (a, n)) in gw_actual.iter().zip(&gw_num).enumerate() {
28433 assert!((a - n).abs() < 5e-3, "mean reduce grad_w[{i}]: {a} vs {n}");
28434 }
28435 }
28436 #[test]
28441 fn tinyconv_full_gradient_matches_numerical() {
28442 use rlx_ir::Philox4x32;
28443 let n = 1usize;
28445 let c_in = 1usize;
28446 let h = 6usize;
28447 let w_in = 6usize;
28448 let c_mid = 2usize; let kh = 3;
28450 let kw = 3;
28451 let h1 = h - kh + 1; let w1 = w_in - kw + 1; let h2 = h1 / 2;
28454 let w2 = w1 / 2; let flat = c_mid * h2 * w2; let num_classes = 3usize;
28457
28458 let mut rng = Philox4x32::new(31);
28459 let mut x = vec![0f32; n * c_in * h * w_in];
28460 rng.fill_normal(&mut x);
28461 let mut wc = vec![0f32; c_mid * c_in * kh * kw];
28462 rng.fill_normal(&mut wc);
28463 for v in wc.iter_mut() {
28464 *v *= 0.2;
28465 }
28466 let bc: Vec<f32> = (0..c_mid).map(|i| 5.0 + 0.1 * i as f32).collect();
28475 let mut wfc = vec![0f32; flat * num_classes];
28476 rng.fill_normal(&mut wfc);
28477 for v in wfc.iter_mut() {
28478 *v *= 0.5;
28479 }
28480 let mut bfc = vec![0f32; num_classes];
28481 rng.fill_normal(&mut bfc);
28482 let labels: Vec<f32> = vec![1.0]; let f = DType::F32;
28485 let mut fwd = Graph::new("tinyconv");
28486 let xn = fwd.input("x", Shape::new(&[n, c_in, h, w_in], f));
28487 let lb = fwd.input("labels", Shape::new(&[n], f));
28488 let wcp = fwd.param("wc", Shape::new(&[c_mid, c_in, kh, kw], f));
28489 let bcp = fwd.param("bc", Shape::new(&[c_mid], f));
28490 let wfp = fwd.param("wfc", Shape::new(&[flat, num_classes], f));
28491 let bfp = fwd.param("bfc", Shape::new(&[num_classes], f));
28492
28493 let conv = fwd.add_node(
28495 Op::Conv {
28496 kernel_size: vec![kh, kw],
28497 stride: vec![1, 1],
28498 padding: vec![0, 0],
28499 dilation: vec![1, 1],
28500 groups: 1,
28501 },
28502 vec![xn, wcp],
28503 Shape::new(&[n, c_mid, h1, w1], f),
28504 );
28505 let bc_4d = fwd.add_node(
28517 Op::Reshape {
28518 new_shape: vec![1, c_mid as i64, 1, 1],
28519 },
28520 vec![bcp],
28521 Shape::new(&[1, c_mid, 1, 1], f),
28522 );
28523 let bc_expanded = fwd.add_node(
28524 Op::Expand {
28525 target_shape: vec![n as i64, c_mid as i64, h1 as i64, w1 as i64],
28526 },
28527 vec![bc_4d],
28528 Shape::new(&[n, c_mid, h1, w1], f),
28529 );
28530 let conv_b = fwd.binary(
28531 BinaryOp::Add,
28532 conv,
28533 bc_expanded,
28534 Shape::new(&[n, c_mid, h1, w1], f),
28535 );
28536 let relu = fwd.activation(Activation::Relu, conv_b, Shape::new(&[n, c_mid, h1, w1], f));
28537 let pool = fwd.add_node(
28538 Op::Pool {
28539 kind: ReduceOp::Max,
28540 kernel_size: vec![2, 2],
28541 stride: vec![2, 2],
28542 padding: vec![0, 0],
28543 },
28544 vec![relu],
28545 Shape::new(&[n, c_mid, h2, w2], f),
28546 );
28547 let flatn = fwd.add_node(
28548 Op::Reshape {
28549 new_shape: vec![n as i64, flat as i64],
28550 },
28551 vec![pool],
28552 Shape::new(&[n, flat], f),
28553 );
28554 let mm = fwd.matmul(flatn, wfp, Shape::new(&[n, num_classes], f));
28555 let logits = fwd.binary(BinaryOp::Add, mm, bfp, Shape::new(&[n, num_classes], f));
28556 let loss_per = fwd.softmax_cross_entropy_with_logits(logits, lb);
28557 let loss = fwd.add_node(
28558 Op::Reduce {
28559 op: ReduceOp::Mean,
28560 axes: vec![0],
28561 keep_dim: false,
28562 },
28563 vec![loss_per],
28564 Shape::from_dims(&[], f),
28565 );
28566 fwd.set_outputs(vec![loss]);
28567
28568 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wcp, bcp, wfp, bfp]);
28569 let d_out = bwd_graph
28570 .nodes()
28571 .iter()
28572 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28573 .map(|n| n.id)
28574 .unwrap();
28575
28576 let (sched, mut arena) = prepare(
28577 &bwd_graph,
28578 &[
28579 (xn, &x),
28580 (lb, &labels),
28581 (wcp, &wc),
28582 (bcp, &bc),
28583 (wfp, &wfc),
28584 (bfp, &bfc),
28585 (d_out, &[1.0]),
28586 ],
28587 );
28588 execute_thunks(&sched, arena.raw_buf_mut());
28589
28590 let outs = bwd_graph.outputs.clone();
28591 let loss_id = outs[0];
28592 let g_wc_id = outs[1];
28593 let g_bc_id = outs[2];
28594 let g_wfc_id = outs[3];
28595 let g_bfc_id = outs[4];
28596 let loss_actual = read_arena(&arena, loss_id, 1)[0];
28597 let g_wc = read_arena(&arena, g_wc_id, wc.len());
28598 let g_bc = read_arena(&arena, g_bc_id, bc.len());
28599 let g_wfc = read_arena(&arena, g_wfc_id, wfc.len());
28600 let g_bfc = read_arena(&arena, g_bfc_id, bfc.len());
28601
28602 let plan = rlx_opt::memory::plan_memory(&fwd);
28604 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
28605 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
28606 write_arena(&mut fwd_arena, xn, &x);
28607 write_arena(&mut fwd_arena, lb, &labels);
28608
28609 let run_loss = |arena: &mut crate::arena::Arena,
28612 wc: &[f32],
28613 bc: &[f32],
28614 wfc: &[f32],
28615 bfc: &[f32]|
28616 -> f32 {
28617 write_arena(arena, wcp, wc);
28618 write_arena(arena, bcp, bc);
28619 write_arena(arena, wfp, wfc);
28620 write_arena(arena, bfp, bfc);
28621 execute_thunks(&fwd_sched, arena.raw_buf_mut());
28622 read_arena(arena, loss, 1)[0]
28623 };
28624
28625 let loss_check = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &bfc);
28626 assert!(
28627 (loss_actual - loss_check).abs() < 1e-4,
28628 "tinyconv loss mismatch: bwd {loss_actual} vs fwd {loss_check}"
28629 );
28630
28631 let eps = 1e-3f32;
28632 let check_grad = |arena: &mut crate::arena::Arena,
28633 name: &str,
28634 analytical: &[f32],
28635 mut perturb: Box<
28636 dyn FnMut(&mut [f32], usize, f32, &mut crate::arena::Arena) -> f32 + '_,
28637 >,
28638 n: usize| {
28639 for i in 0..n {
28640 let lp = perturb(&mut analytical.to_vec(), i, eps, arena);
28641 let lm = perturb(&mut analytical.to_vec(), i, -eps, arena);
28642 let num = (lp - lm) / (2.0 * eps);
28643 assert!(
28644 (analytical[i] - num).abs() < 5e-3,
28645 "{name}[{i}]: analytical {} vs numerical {num}",
28646 analytical[i]
28647 );
28648 }
28649 };
28650
28651 #[allow(unused_macros)]
28654 macro_rules! sweep {
28655 ($name:expr, $base:expr, $analytical:expr, $set_param:ident) => {{
28656 let n = $base.len();
28657 for i in 0..n {
28658 let mut p = $base.clone();
28659 let s = p[i];
28660 p[i] = s + eps;
28661 let lp = {
28662 let $set_param = &p;
28663 run_loss(&mut fwd_arena, &wc, &bc, &wfc, &bfc).max(f32::NEG_INFINITY);
28664 let _ = $set_param;
28667 0.0_f32
28669 };
28670 let _ = lp;
28671 }
28672 }};
28673 }
28674 let _ = check_grad; for i in 0..wc.len() {
28678 let mut p = wc.clone();
28679 let s = p[i];
28680 p[i] = s + eps;
28681 let lp = run_loss(&mut fwd_arena, &p, &bc, &wfc, &bfc);
28682 p[i] = s - eps;
28683 let lm = run_loss(&mut fwd_arena, &p, &bc, &wfc, &bfc);
28684 let num = (lp - lm) / (2.0 * eps);
28685 assert!(
28686 (g_wc[i] - num).abs() < 5e-3,
28687 "g_wc[{i}]: {} vs {num}",
28688 g_wc[i]
28689 );
28690 }
28691 for i in 0..bc.len() {
28692 let mut p = bc.clone();
28693 let s = p[i];
28694 p[i] = s + eps;
28695 let lp = run_loss(&mut fwd_arena, &wc, &p, &wfc, &bfc);
28696 p[i] = s - eps;
28697 let lm = run_loss(&mut fwd_arena, &wc, &p, &wfc, &bfc);
28698 let num = (lp - lm) / (2.0 * eps);
28699 assert!(
28700 (g_bc[i] - num).abs() < 5e-3,
28701 "g_bc[{i}]: {} vs {num}",
28702 g_bc[i]
28703 );
28704 }
28705 for i in 0..wfc.len() {
28706 let mut p = wfc.clone();
28707 let s = p[i];
28708 p[i] = s + eps;
28709 let lp = run_loss(&mut fwd_arena, &wc, &bc, &p, &bfc);
28710 p[i] = s - eps;
28711 let lm = run_loss(&mut fwd_arena, &wc, &bc, &p, &bfc);
28712 let num = (lp - lm) / (2.0 * eps);
28713 assert!(
28714 (g_wfc[i] - num).abs() < 5e-3,
28715 "g_wfc[{i}]: {} vs {num}",
28716 g_wfc[i]
28717 );
28718 }
28719 for i in 0..bfc.len() {
28720 let mut p = bfc.clone();
28721 let s = p[i];
28722 p[i] = s + eps;
28723 let lp = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &p);
28724 p[i] = s - eps;
28725 let lm = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &p);
28726 let num = (lp - lm) / (2.0 * eps);
28727 assert!(
28728 (g_bfc[i] - num).abs() < 5e-3,
28729 "g_bfc[{i}]: {} vs {num}",
28730 g_bfc[i]
28731 );
28732 }
28733 }
28734
28735 #[test]
28739 fn narrow_rope_skips_when_narrow_has_multiple_consumers() {
28740 let f = DType::F32;
28741 let mut g = Graph::new("nr_skip");
28742 let qkv = g.input("qkv", Shape::new(&[16, 8, 192], f));
28743 let cos = g.input("cos", Shape::new(&[16], f));
28744 let sin = g.input("sin", Shape::new(&[16], f));
28745 let q = g.narrow_(qkv, 2, 0, 64);
28746 let q_rope = g.rope(q, cos, sin, 16);
28747 let q_dup = g.activation(rlx_ir::op::Activation::Relu, q, Shape::new(&[16, 8, 64], f));
28749 g.set_outputs(vec![q_rope, q_dup]);
28750
28751 let plan = rlx_opt::memory::plan_memory(&g);
28752 let arena = crate::arena::Arena::from_plan(plan);
28753 let sched = compile_thunks(&g, &arena);
28754
28755 let narrow_count = sched
28756 .thunks
28757 .iter()
28758 .filter(|t| matches!(t, Thunk::Narrow { .. }))
28759 .count();
28760 assert!(
28761 narrow_count >= 1,
28762 "Narrow with multiple consumers must NOT be fused away"
28763 );
28764 }
28765
28766 #[test]
28779 fn custom_fn_forward_inlines_body() {
28780 let s = Shape::new(&[3], DType::F32);
28781
28782 let mut body = Graph::new("addone_body");
28784 let x = body.input("x", s.clone());
28785 let one_data: Vec<u8> = (0..3).flat_map(|_| 1.0_f32.to_le_bytes()).collect();
28786 let one = body.add_node(Op::Constant { data: one_data }, vec![], s.clone());
28787 let y = body.binary(BinaryOp::Add, x, one, s.clone());
28788 body.set_outputs(vec![y]);
28789
28790 let mut g = Graph::new("custom_fn_outer");
28791 let xin = g.input("x_in", s.clone());
28792 let cf = g.custom_fn(vec![xin], body, None, None);
28793 g.set_outputs(vec![cf]);
28794
28795 let xs = vec![10.0_f32, 20.0, 30.0];
28796 let (sched, mut arena) = prepare(&g, &[(xin, &xs)]);
28797 execute_thunks(&sched, arena.raw_buf_mut());
28798 let got = read_arena(&arena, cf, 3);
28799 assert_eq!(got, vec![11.0, 21.0, 31.0]);
28800 }
28801
28802 fn find_named(graph: &Graph, want: &str) -> NodeId {
28804 for n in graph.nodes() {
28805 let name = match &n.op {
28806 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
28807 _ => None,
28808 };
28809 if name == Some(want) {
28810 return n.id;
28811 }
28812 }
28813 panic!("no node named {want:?} in graph");
28814 }
28815
28816 #[test]
28820 fn custom_fn_vjp_overrides_natural_gradient() {
28821 use rlx_opt::autodiff::grad_with_loss;
28822 let s = Shape::new(&[1], DType::F32);
28823
28824 let mut fwd = Graph::new("id_fwd");
28825 let x = fwd.input("x", s.clone());
28826 fwd.set_outputs(vec![x]);
28827
28828 let mut vjp_g = Graph::new("id_vjp");
28829 let _x_p = vjp_g.input("x", s.clone());
28830 let _y_p = vjp_g.input("primal_output", s.clone());
28831 let dy = vjp_g.input("d_output", s.clone());
28832 let two_data: Vec<u8> = 2.0_f32.to_le_bytes().to_vec();
28833 let two = vjp_g.add_node(Op::Constant { data: two_data }, vec![], s.clone());
28834 let dx = vjp_g.binary(BinaryOp::Mul, dy, two, s.clone());
28835 vjp_g.set_outputs(vec![dx]);
28836
28837 let mut g = Graph::new("outer");
28838 let xp = g.param("x", s.clone());
28839 let cf = g.custom_fn(vec![xp], fwd, Some(vjp_g), None);
28840 g.set_outputs(vec![cf]);
28841
28842 let bwd = grad_with_loss(&g, &[xp]);
28843 assert_eq!(bwd.outputs.len(), 2, "expect [loss, dx]");
28844
28845 let xb = find_named(&bwd, "x");
28846 let dout = find_named(&bwd, "d_output");
28847 let (sched, mut arena) = prepare(&bwd, &[(xb, &[7.0]), (dout, &[1.0])]);
28848 execute_thunks(&sched, arena.raw_buf_mut());
28849 let loss = read_arena(&arena, bwd.outputs[0], 1);
28850 let dx_v = read_arena(&arena, bwd.outputs[1], 1);
28851 assert!((loss[0] - 7.0).abs() < 1e-6, "loss should be 7.0");
28852 assert!(
28853 (dx_v[0] - 2.0).abs() < 1e-6,
28854 "vjp override should yield dx=2.0, got {} (natural autodiff would give 1.0)",
28855 dx_v[0]
28856 );
28857 }
28858
28859 #[test]
28864 fn custom_fn_vjp_two_inputs_matches_mul_autodiff() {
28865 use rlx_opt::autodiff::grad_with_loss;
28866 let s = Shape::new(&[1], DType::F32);
28867
28868 let mut fwd = Graph::new("mul_fwd");
28869 let a_f = fwd.input("a", s.clone());
28870 let b_f = fwd.input("b", s.clone());
28871 let y_f = fwd.binary(BinaryOp::Mul, a_f, b_f, s.clone());
28872 fwd.set_outputs(vec![y_f]);
28873
28874 let mut vjp_g = Graph::new("mul_vjp");
28875 let a_v = vjp_g.input("a", s.clone());
28876 let b_v = vjp_g.input("b", s.clone());
28877 let _y_v = vjp_g.input("primal_output", s.clone());
28878 let dy_v = vjp_g.input("d_output", s.clone());
28879 let da = vjp_g.binary(BinaryOp::Mul, b_v, dy_v, s.clone());
28880 let db = vjp_g.binary(BinaryOp::Mul, a_v, dy_v, s.clone());
28881 vjp_g.set_outputs(vec![da, db]);
28882
28883 let mut g = Graph::new("outer");
28884 let ap = g.param("a", s.clone());
28885 let bp = g.param("b", s.clone());
28886 let cf = g.custom_fn(vec![ap, bp], fwd, Some(vjp_g), None);
28887 g.set_outputs(vec![cf]);
28888
28889 let bwd = grad_with_loss(&g, &[ap, bp]);
28890 assert_eq!(bwd.outputs.len(), 3, "expect [loss, da, db]");
28891
28892 let ab = find_named(&bwd, "a");
28893 let bb = find_named(&bwd, "b");
28894 let dout = find_named(&bwd, "d_output");
28895 let (sched, mut arena) = prepare(&bwd, &[(ab, &[3.0]), (bb, &[5.0]), (dout, &[1.0])]);
28896 execute_thunks(&sched, arena.raw_buf_mut());
28897 let loss = read_arena(&arena, bwd.outputs[0], 1);
28898 let da_v = read_arena(&arena, bwd.outputs[1], 1);
28899 let db_v = read_arena(&arena, bwd.outputs[2], 1);
28900 assert!((loss[0] - 15.0).abs() < 1e-5);
28901 assert!(
28902 (da_v[0] - 5.0).abs() < 1e-5,
28903 "da should be b=5.0, got {}",
28904 da_v[0]
28905 );
28906 assert!(
28907 (db_v[0] - 3.0).abs() < 1e-5,
28908 "db should be a=3.0, got {}",
28909 db_v[0]
28910 );
28911 }
28912
28913 #[test]
28916 fn custom_fn_jvp_overrides_natural_tangent() {
28917 use rlx_opt::autodiff_fwd::jvp;
28918 let s = Shape::new(&[1], DType::F32);
28919
28920 let mut fwd = Graph::new("id_fwd");
28921 let x = fwd.input("x", s.clone());
28922 fwd.set_outputs(vec![x]);
28923
28924 let mut jvp_g = Graph::new("id_jvp");
28925 let _x_p = jvp_g.input("x", s.clone());
28926 let tx = jvp_g.input("tangent_0", s.clone());
28927 let two_data: Vec<u8> = 2.0_f32.to_le_bytes().to_vec();
28928 let two = jvp_g.add_node(Op::Constant { data: two_data }, vec![], s.clone());
28929 let ty = jvp_g.binary(BinaryOp::Mul, tx, two, s.clone());
28930 jvp_g.set_outputs(vec![ty]);
28931
28932 let mut g = Graph::new("outer");
28933 let xin = g.input("x_in", s.clone());
28934 let cf = g.custom_fn(vec![xin], fwd, None, Some(jvp_g));
28935 g.set_outputs(vec![cf]);
28936
28937 let fwd_g = jvp(&g, &[xin]);
28938 assert_eq!(fwd_g.outputs.len(), 2, "expect [primal_y, tangent_y]");
28939
28940 let xb = find_named(&fwd_g, "x_in");
28941 let tan = find_named(&fwd_g, "tangent_x_in");
28942 let (sched, mut arena) = prepare(&fwd_g, &[(xb, &[7.0]), (tan, &[1.0])]);
28943 execute_thunks(&sched, arena.raw_buf_mut());
28944 let y = read_arena(&arena, fwd_g.outputs[0], 1);
28945 let ty_v = read_arena(&arena, fwd_g.outputs[1], 1);
28946 assert!((y[0] - 7.0).abs() < 1e-6);
28947 assert!(
28948 (ty_v[0] - 2.0).abs() < 1e-6,
28949 "jvp override should yield t_y=2.0 (natural autodiff would give 1.0), got {}",
28950 ty_v[0]
28951 );
28952 }
28953
28954 #[test]
28959 fn c64_dtype_storage_layout() {
28960 assert_eq!(
28961 DType::C64.size_bytes(),
28962 8,
28963 "C64 should be 8 bytes (f32 real + f32 imag)"
28964 );
28965 assert!(DType::C64.is_complex());
28966 assert!(!DType::C64.is_float());
28967
28968 let s = Shape::new(&[2], DType::C64);
28970 assert_eq!(s.size_bytes().unwrap(), 16);
28971 }
28972
28973 fn run_c64_binary(op: BinaryOp, a: &[(f32, f32)], b: &[(f32, f32)]) -> Vec<(f32, f32)> {
28980 let n = a.len();
28981 let s = Shape::new(&[n], DType::C64);
28982 let mut g = Graph::new("c64_bin");
28983 let in_a = g.input("a", s.clone());
28984 let in_b = g.input("b", s.clone());
28985 let out = g.binary(op, in_a, in_b, s.clone());
28986 g.set_outputs(vec![out]);
28987
28988 let plan = rlx_opt::memory::plan_memory(&g);
28989 let mut arena = crate::arena::Arena::from_plan(plan);
28990 let sched = compile_thunks(&g, &arena);
28991
28992 let a_off = arena.byte_offset(in_a);
28993 let b_off = arena.byte_offset(in_b);
28994 let out_off = arena.byte_offset(out);
28995 let buf = arena.raw_buf_mut();
28997 unsafe {
28998 let pa = buf.as_mut_ptr().add(a_off) as *mut f32;
28999 let pb = buf.as_mut_ptr().add(b_off) as *mut f32;
29000 for (i, &(re, im)) in a.iter().enumerate() {
29001 *pa.add(2 * i) = re;
29002 *pa.add(2 * i + 1) = im;
29003 }
29004 for (i, &(re, im)) in b.iter().enumerate() {
29005 *pb.add(2 * i) = re;
29006 *pb.add(2 * i + 1) = im;
29007 }
29008 }
29009 execute_thunks(&sched, arena.raw_buf_mut());
29010 let raw_out: Vec<f32> = unsafe {
29011 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29012 (0..(2 * n)).map(|i| *p.add(i)).collect()
29013 };
29014 (0..n)
29015 .map(|i| (raw_out[2 * i], raw_out[2 * i + 1]))
29016 .collect()
29017 }
29018
29019 #[track_caller]
29020 fn assert_close_c(got: (f32, f32), expected: (f32, f32), tol: f32, label: &str) {
29021 let dr = (got.0 - expected.0).abs();
29022 let di = (got.1 - expected.1).abs();
29023 assert!(
29024 dr < tol && di < tol,
29025 "[{label}] got ({:+.4}, {:+.4}), expected ({:+.4}, {:+.4})",
29026 got.0,
29027 got.1,
29028 expected.0,
29029 expected.1
29030 );
29031 }
29032
29033 #[test]
29034 fn c64_binary_add_matches_complex_arithmetic() {
29035 let a = [(1.0_f32, 2.0_f32), (3.0_f32, -1.0_f32)];
29036 let b = [(4.0_f32, -1.0_f32), (0.5_f32, 0.5_f32)];
29037 let out = run_c64_binary(BinaryOp::Add, &a, &b);
29038 assert_close_c(out[0], (5.0, 1.0), 1e-6, "add[0]");
29039 assert_close_c(out[1], (3.5, -0.5), 1e-6, "add[1]");
29040 }
29041
29042 #[test]
29043 fn c64_binary_sub_matches_complex_arithmetic() {
29044 let a = [(5.0_f32, 1.0_f32)];
29045 let b = [(2.0_f32, 3.0_f32)];
29046 let out = run_c64_binary(BinaryOp::Sub, &a, &b);
29047 assert_close_c(out[0], (3.0, -2.0), 1e-6, "sub");
29048 }
29049
29050 #[test]
29051 fn c64_binary_mul_matches_complex_arithmetic() {
29052 let a = [(1.0_f32, 2.0_f32)];
29054 let b = [(3.0_f32, 4.0_f32)];
29055 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
29056 assert_close_c(out[0], (-5.0, 10.0), 1e-5, "mul");
29057 }
29058
29059 #[test]
29060 fn c64_binary_div_matches_complex_arithmetic() {
29061 let a = [(1.0_f32, 2.0_f32)];
29065 let b = [(3.0_f32, 4.0_f32)];
29066 let out = run_c64_binary(BinaryOp::Div, &a, &b);
29067 assert_close_c(out[0], (0.44, 0.08), 1e-5, "div");
29068 }
29069
29070 #[test]
29071 fn c64_binary_mul_identity_one_is_no_op() {
29072 let a = [(3.5_f32, -1.25_f32), (-2.0_f32, 7.0_f32)];
29074 let b = [(1.0_f32, 0.0_f32), (1.0_f32, 0.0_f32)];
29075 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
29076 assert_close_c(out[0], a[0], 1e-6, "mul·1[0]");
29077 assert_close_c(out[1], a[1], 1e-6, "mul·1[1]");
29078 }
29079
29080 #[test]
29081 fn c64_binary_mul_by_i_rotates_90_degrees() {
29082 let a = [(1.0_f32, 0.0_f32)];
29084 let b = [(0.0_f32, 1.0_f32)];
29085 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
29086 assert_close_c(out[0], (0.0, 1.0), 1e-6, "1·i");
29087 }
29088
29089 #[test]
29090 fn c64_binary_div_by_self_gives_unity() {
29091 let a = [(2.5_f32, -1.5_f32), (-0.7_f32, 4.2_f32)];
29092 let out = run_c64_binary(BinaryOp::Div, &a, &a);
29093 assert_close_c(out[0], (1.0, 0.0), 1e-5, "div_self[0]");
29094 assert_close_c(out[1], (1.0, 0.0), 1e-5, "div_self[1]");
29095 }
29096
29097 #[test]
29098 #[should_panic(expected = "C64: complex max/min/pow")]
29099 fn c64_binary_max_is_rejected_at_lowering() {
29100 run_c64_binary(BinaryOp::Max, &[(1.0_f32, 2.0_f32)], &[(3.0_f32, 4.0_f32)]);
29101 }
29102
29103 fn run_c64_activation(act: Activation, a: &[(f32, f32)]) -> Vec<(f32, f32)> {
29104 let n = a.len();
29105 let s = Shape::new(&[n], DType::C64);
29106 let mut g = Graph::new("c64_act");
29107 let in_a = g.input("a", s.clone());
29108 let out = g.activation(act, in_a, s.clone());
29109 g.set_outputs(vec![out]);
29110 let plan = rlx_opt::memory::plan_memory(&g);
29111 let mut arena = crate::arena::Arena::from_plan(plan);
29112 let sched = compile_thunks(&g, &arena);
29113 let a_off = arena.byte_offset(in_a);
29114 let out_off = arena.byte_offset(out);
29115 let buf = arena.raw_buf_mut();
29116 unsafe {
29117 let pa = buf.as_mut_ptr().add(a_off) as *mut f32;
29118 for (i, &(re, im)) in a.iter().enumerate() {
29119 *pa.add(2 * i) = re;
29120 *pa.add(2 * i + 1) = im;
29121 }
29122 }
29123 execute_thunks(&sched, arena.raw_buf_mut());
29124 let raw: Vec<f32> = unsafe {
29125 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29126 (0..(2 * n)).map(|i| *p.add(i)).collect()
29127 };
29128 (0..n).map(|i| (raw[2 * i], raw[2 * i + 1])).collect()
29129 }
29130
29131 #[test]
29132 fn c64_activation_neg_negates_both_components() {
29133 let inp = [(3.5_f32, -1.25_f32), (-2.0_f32, 0.0_f32)];
29134 let out = run_c64_activation(Activation::Neg, &inp);
29135 assert_close_c(out[0], (-3.5, 1.25), 1e-6, "neg[0]");
29136 assert_close_c(out[1], (2.0, 0.0), 1e-6, "neg[1]");
29137 }
29138
29139 #[test]
29140 fn c64_activation_exp_matches_euler() {
29141 let inp = [(0.0_f32, std::f32::consts::PI), (1.0_f32, 0.0_f32)];
29144 let out = run_c64_activation(Activation::Exp, &inp);
29145 assert_close_c(out[0], (-1.0, 0.0), 1e-5, "exp(iπ)");
29146 assert_close_c(out[1], (std::f32::consts::E, 0.0), 1e-5, "exp(1)");
29147 }
29148
29149 #[test]
29150 fn c64_activation_log_matches_principal_branch() {
29151 let inp = [(1.0_f32, 0.0_f32), (0.0_f32, 1.0_f32), (-1.0_f32, 0.0_f32)];
29155 let out = run_c64_activation(Activation::Log, &inp);
29156 assert_close_c(out[0], (0.0, 0.0), 1e-5, "log(1)");
29157 assert_close_c(out[1], (0.0, std::f32::consts::FRAC_PI_2), 1e-5, "log(i)");
29158 assert_close_c(out[2], (0.0, std::f32::consts::PI), 1e-5, "log(-1)");
29159 }
29160
29161 #[test]
29162 fn c64_activation_sqrt_squared_recovers_input() {
29163 let inp = [(4.0_f32, 0.0_f32), (3.0_f32, 4.0_f32)];
29166 let roots = run_c64_activation(Activation::Sqrt, &inp);
29167 assert_close_c(roots[0], (2.0, 0.0), 1e-5, "sqrt(4)");
29169 assert_close_c(roots[1], (2.0, 1.0), 1e-5, "sqrt(3+4i)");
29170 }
29171
29172 #[test]
29173 #[should_panic(expected = "no natural complex extension")]
29174 fn c64_activation_relu_is_rejected_at_lowering() {
29175 run_c64_activation(Activation::Relu, &[(1.0_f32, 2.0_f32)]);
29176 }
29177
29178 fn run_complex_norm_sq(z: &[(f32, f32)]) -> Vec<f32> {
29182 let n = z.len();
29183 let mut g = Graph::new("cns_fwd");
29184 let in_z = g.input("z", Shape::new(&[n], DType::C64));
29185 let out = g.complex_norm_sq(in_z);
29186 g.set_outputs(vec![out]);
29187 let plan = rlx_opt::memory::plan_memory(&g);
29188 let mut arena = crate::arena::Arena::from_plan(plan);
29189 let sched = compile_thunks(&g, &arena);
29190 let z_off = arena.byte_offset(in_z);
29191 let out_off = arena.byte_offset(out);
29192 let buf = arena.raw_buf_mut();
29193 unsafe {
29194 let pz = buf.as_mut_ptr().add(z_off) as *mut f32;
29195 for (i, &(re, im)) in z.iter().enumerate() {
29196 *pz.add(2 * i) = re;
29197 *pz.add(2 * i + 1) = im;
29198 }
29199 }
29200 execute_thunks(&sched, arena.raw_buf_mut());
29201 unsafe {
29202 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29203 (0..n).map(|i| *p.add(i)).collect()
29204 }
29205 }
29206
29207 fn run_complex_norm_sq_bwd(z: &[(f32, f32)], g: &[f32]) -> Vec<(f32, f32)> {
29209 let n = z.len();
29210 let mut gr = Graph::new("cns_bwd");
29211 let in_z = gr.input("z", Shape::new(&[n], DType::C64));
29212 let in_g = gr.input("g", Shape::new(&[n], DType::F32));
29213 let out = gr.complex_norm_sq_backward(in_z, in_g);
29214 gr.set_outputs(vec![out]);
29215 let plan = rlx_opt::memory::plan_memory(&gr);
29216 let mut arena = crate::arena::Arena::from_plan(plan);
29217 let sched = compile_thunks(&gr, &arena);
29218 let z_off = arena.byte_offset(in_z);
29219 let g_off = arena.byte_offset(in_g);
29220 let out_off = arena.byte_offset(out);
29221 let buf = arena.raw_buf_mut();
29222 unsafe {
29223 let pz = buf.as_mut_ptr().add(z_off) as *mut f32;
29224 let pg = buf.as_mut_ptr().add(g_off) as *mut f32;
29225 for (i, &(re, im)) in z.iter().enumerate() {
29226 *pz.add(2 * i) = re;
29227 *pz.add(2 * i + 1) = im;
29228 }
29229 for (i, &v) in g.iter().enumerate() {
29230 *pg.add(i) = v;
29231 }
29232 }
29233 execute_thunks(&sched, arena.raw_buf_mut());
29234 unsafe {
29235 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29236 (0..n).map(|i| (*p.add(2 * i), *p.add(2 * i + 1))).collect()
29237 }
29238 }
29239
29240 #[test]
29241 fn complex_norm_sq_matches_textbook() {
29242 let z = [(3.0_f32, 4.0_f32), (1.0_f32, 0.0_f32), (0.0_f32, 0.0_f32)];
29246 let out = run_complex_norm_sq(&z);
29247 assert!((out[0] - 25.0).abs() < 1e-5);
29248 assert!((out[1] - 1.0).abs() < 1e-6);
29249 assert!(out[2].abs() < 1e-6);
29250 }
29251
29252 #[test]
29253 fn complex_norm_sq_backward_matches_wirtinger_formula() {
29254 let z = [(3.0_f32, 4.0_f32), (1.5_f32, -2.5_f32)];
29256 let g = [1.0_f32, 1.0_f32];
29257 let dz = run_complex_norm_sq_bwd(&z, &g);
29258 assert_close_c(dz[0], z[0], 1e-6, "dz[0] = g·z[0]");
29259 assert_close_c(dz[1], z[1], 1e-6, "dz[1] = g·z[1]");
29260 }
29261
29262 #[test]
29263 fn complex_norm_sq_backward_scales_with_upstream() {
29264 let z = [(2.0_f32, 1.0_f32), (-1.0_f32, 3.0_f32)];
29266 let g = [0.5_f32, -2.0_f32];
29267 let dz = run_complex_norm_sq_bwd(&z, &g);
29268 assert_close_c(dz[0], (1.0, 0.5), 1e-6, "g=0.5 · (2,1)");
29269 assert_close_c(dz[1], (2.0, -6.0), 1e-6, "g=-2 · (-1,3)");
29270 }
29271
29272 #[test]
29277 fn custom_fn_multi_extracts_each_subgraph_output() {
29278 use rlx_ir::ops::special::MultiOutputHandle;
29279
29280 let _ = MultiOutputHandle {
29281 source: NodeId(0),
29282 sub_shapes: vec![],
29283 offsets: vec![],
29284 }; let mut body = Graph::new("multi_body");
29288 let s3 = Shape::new(&[3], DType::F32);
29289 let x = body.input("x", s3.clone());
29290 let x_sq = body.binary(BinaryOp::Mul, x, x, s3.clone());
29291 let two = body.add_node(
29292 Op::Constant {
29293 data: vec![
29294 2.0_f32.to_le_bytes(),
29295 2.0_f32.to_le_bytes(),
29296 2.0_f32.to_le_bytes(),
29297 ]
29298 .into_iter()
29299 .flatten()
29300 .collect(),
29301 },
29302 vec![],
29303 s3.clone(),
29304 );
29305 let two_x = body.binary(BinaryOp::Mul, two, x, s3.clone());
29306 body.set_outputs(vec![x_sq, two_x]);
29307
29308 let mut outer = Graph::new("multi_outer");
29310 let in_x = outer.input("xin", s3.clone());
29311 let handle = outer.custom_fn_multi(vec![in_x], body);
29312 assert_eq!(handle.n_outputs(), 2);
29313 let out0 = handle.output(&mut outer, 0); let out1 = handle.output(&mut outer, 1); outer.set_outputs(vec![out0, out1]);
29316
29317 let plan = rlx_opt::memory::plan_memory(&outer);
29318 let mut arena = crate::arena::Arena::from_plan(plan);
29319 let sched = compile_thunks(&outer, &arena);
29320 let xin_off = arena.byte_offset(in_x);
29321 let out0_off = arena.byte_offset(out0);
29322 let out1_off = arena.byte_offset(out1);
29323 let xs = [1.0_f32, 2.0, 3.0];
29324 unsafe {
29325 let p = arena.raw_buf_mut().as_mut_ptr().add(xin_off) as *mut f32;
29326 for (i, &v) in xs.iter().enumerate() {
29327 *p.add(i) = v;
29328 }
29329 }
29330 execute_thunks(&sched, arena.raw_buf_mut());
29331 let out0_v: Vec<f32> = unsafe {
29332 let p = arena.raw_buf().as_ptr().add(out0_off) as *const f32;
29333 (0..3).map(|i| *p.add(i)).collect()
29334 };
29335 let out1_v: Vec<f32> = unsafe {
29336 let p = arena.raw_buf().as_ptr().add(out1_off) as *const f32;
29337 (0..3).map(|i| *p.add(i)).collect()
29338 };
29339 for i in 0..3 {
29341 assert!(
29342 (out0_v[i] - xs[i] * xs[i]).abs() < 1e-5,
29343 "out0[{i}] = {} != x² = {}",
29344 out0_v[i],
29345 xs[i] * xs[i]
29346 );
29347 assert!(
29348 (out1_v[i] - 2.0 * xs[i]).abs() < 1e-5,
29349 "out1[{i}] = {} != 2x = {}",
29350 out1_v[i],
29351 2.0 * xs[i]
29352 );
29353 }
29354 }
29355
29356 #[test]
29357 fn complex_norm_sq_gradient_matches_finite_difference() {
29358 let z = [(3.0_f32, 4.0_f32)];
29360 let eps = 1e-3_f32;
29361 let v0 = run_complex_norm_sq(&z)[0];
29362 let z_pert = [(3.0_f32 + eps, 4.0_f32)];
29363 let v1 = run_complex_norm_sq(&z_pert)[0];
29364 let fd_re = (v1 - v0) / eps;
29365 let analytic_re = 2.0 * z[0].0;
29366 assert!((fd_re - analytic_re).abs() < 1e-2);
29367
29368 let z_pert_im = [(3.0_f32, 4.0_f32 + eps)];
29370 let v2 = run_complex_norm_sq(&z_pert_im)[0];
29371 let fd_im = (v2 - v0) / eps;
29372 let analytic_im = 2.0 * z[0].1;
29373 assert!((fd_im - analytic_im).abs() < 1e-2);
29374
29375 let dz = run_complex_norm_sq_bwd(&z, &[1.0_f32]);
29381 assert!((2.0 * dz[0].0 - analytic_re).abs() < 1e-5);
29382 assert!((2.0 * dz[0].1 - analytic_im).abs() < 1e-5);
29383 }
29384
29385 #[test]
29390 fn binary_full_5d_mid_singleton_broadcast() {
29391 let bh = 2usize;
29392 let h = 3;
29393 let w = 4;
29394 let f = DType::F32;
29395
29396 let mut g = Graph::new("bcast_5d");
29397 let lhs = g.input("lhs", Shape::new(&[bh, h, w, h, w], f));
29398 let rhs = g.input("rhs", Shape::new(&[bh, h, w, 1, w], f));
29400 let out = g.binary(BinaryOp::Add, lhs, rhs, Shape::new(&[bh, h, w, h, w], f));
29401 g.set_outputs(vec![out]);
29402
29403 let lhs_data: Vec<f32> = (0..bh * h * w * h * w).map(|i| i as f32 * 0.01).collect();
29405 let rhs_data: Vec<f32> = (0..bh * h * w * w)
29406 .map(|i| (i as f32 + 100.0) * 0.01)
29407 .collect();
29408
29409 let mut expected = vec![0f32; bh * h * w * h * w];
29411 for b_ in 0..bh {
29412 for hq in 0..h {
29413 for wq in 0..w {
29414 for hk in 0..h {
29415 for wk in 0..w {
29416 let li = (((b_ * h + hq) * w + wq) * h + hk) * w + wk;
29417 let ri = ((b_ * h + hq) * w + wq) * w + wk;
29419 expected[li] = lhs_data[li] + rhs_data[ri];
29420 }
29421 }
29422 }
29423 }
29424 }
29425
29426 let plan = rlx_opt::memory::plan_memory(&g);
29427 let mut arena = crate::arena::Arena::from_plan(plan);
29428 let sched = compile_thunks(&g, &arena);
29429 let lhs_off = arena.byte_offset(lhs);
29430 let rhs_off = arena.byte_offset(rhs);
29431 let out_off = arena.byte_offset(out);
29432 let buf = arena.raw_buf_mut();
29433 unsafe {
29434 let p = buf.as_mut_ptr().add(lhs_off) as *mut f32;
29435 for (i, &v) in lhs_data.iter().enumerate() {
29436 *p.add(i) = v;
29437 }
29438 let p = buf.as_mut_ptr().add(rhs_off) as *mut f32;
29439 for (i, &v) in rhs_data.iter().enumerate() {
29440 *p.add(i) = v;
29441 }
29442 }
29443 execute_thunks(&sched, arena.raw_buf_mut());
29444 let actual: Vec<f32> = unsafe {
29445 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29446 (0..bh * h * w * h * w).map(|i| *p.add(i)).collect()
29447 };
29448
29449 let mut max_diff = 0f32;
29451 let mut max_idx = 0;
29452 for i in 0..actual.len() {
29453 let d = (actual[i] - expected[i]).abs();
29454 if d > max_diff {
29455 max_diff = d;
29456 max_idx = i;
29457 }
29458 }
29459 assert!(
29460 max_diff < 1e-6,
29461 "5D mid-shape singleton broadcast wrong: max |Δ| = {max_diff} at idx {max_idx} \
29462 (actual={}, expected={})",
29463 actual[max_idx],
29464 expected[max_idx]
29465 );
29466 }
29467
29468 #[test]
29469 fn layer_norm2d_and_conv_transpose2d_kernels() {
29470 let mut out = vec![0f32; 8];
29471 crate::kernels::layer_norm2d_nchw(
29472 &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
29473 &[1.0, 1.0],
29474 &[0.0, 0.0],
29475 &mut out,
29476 1,
29477 2,
29478 2,
29479 2,
29480 1e-5,
29481 );
29482 let mean0: f32 = (1.0 + 3.0) / 2.0;
29483 assert!((out[0] - mean0).abs() > 0.1);
29484
29485 let mut up = vec![0f32; 4];
29486 crate::kernels::conv_transpose2d_nchw(
29487 &[2.0],
29488 &[1.0, 0.0, 0.0, 1.0],
29489 &mut up,
29490 1,
29491 1,
29492 1,
29493 1,
29494 1,
29495 2,
29496 2,
29497 2,
29498 2,
29499 2,
29500 2,
29501 0,
29502 0,
29503 1,
29504 1,
29505 1,
29506 );
29507 assert!((up[0] - 2.0).abs() < 1e-5);
29508 assert!((up[3] - 2.0).abs() < 1e-5);
29509 }
29510
29511 #[test]
29517 fn scaled_matmul_oracle_matches_f32() {
29518 use rlx_ir::ScaledFormat::*;
29519 use rlx_ir::{ScaleLayout, ScaledFormat};
29520
29521 fn cosine(a: &[f32], b: &[f32]) -> f32 {
29522 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
29523 let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
29524 let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
29525 dot / (na * nb)
29526 }
29527
29528 #[allow(clippy::too_many_arguments)]
29529 fn run_scaled(
29530 lhs: &[f32],
29531 rhs: &[f32],
29532 m: usize,
29533 k: usize,
29534 n: usize,
29535 lf: ScaledFormat,
29536 rf: ScaledFormat,
29537 layout: ScaleLayout,
29538 ) -> Vec<f32> {
29539 let f = DType::F32;
29540 let u8t = DType::U8;
29541 let mut g = Graph::new("scaled");
29542 let lhs_in = g.input("lhs", Shape::new(&[m, k], f));
29543 let rhs_in = g.input("rhs", Shape::new(&[n, k], f));
29544 let (ls_shape, rs_shape) = match layout {
29545 ScaleLayout::PerTensor => (Shape::new(&[1], f), Shape::new(&[1], f)),
29546 _ => {
29547 let nb = k.div_ceil(layout.block() as usize);
29548 (Shape::new(&[m, nb], u8t), Shape::new(&[n, nb], u8t))
29549 }
29550 };
29551 let ls = g.add_node(
29552 Op::ScaledQuantScale {
29553 format: lf,
29554 scale_layout: layout,
29555 },
29556 vec![lhs_in],
29557 ls_shape,
29558 );
29559 let lq = g.add_node(
29560 Op::ScaledQuantize {
29561 format: lf,
29562 scale_layout: layout,
29563 },
29564 vec![lhs_in, ls],
29565 Shape::new(&[m, k], u8t),
29566 );
29567 let rs = g.add_node(
29568 Op::ScaledQuantScale {
29569 format: rf,
29570 scale_layout: layout,
29571 },
29572 vec![rhs_in],
29573 rs_shape,
29574 );
29575 let rq = g.add_node(
29576 Op::ScaledQuantize {
29577 format: rf,
29578 scale_layout: layout,
29579 },
29580 vec![rhs_in, rs],
29581 Shape::new(&[n, k], u8t),
29582 );
29583 let out = g.add_node(
29584 Op::ScaledMatMul {
29585 lhs_format: lf,
29586 rhs_format: rf,
29587 scale_layout: layout,
29588 has_bias: false,
29589 },
29590 vec![lq, rq, ls, rs],
29591 Shape::new(&[m, n], f),
29592 );
29593 g.set_outputs(vec![out]);
29594
29595 let plan = rlx_opt::memory::plan_memory(&g);
29596 let mut arena = crate::arena::Arena::from_plan(plan);
29597 let sched = compile_thunks(&g, &arena);
29598 let lhs_off = arena.byte_offset(lhs_in);
29599 let rhs_off = arena.byte_offset(rhs_in);
29600 let out_off = arena.byte_offset(out);
29601 let buf = arena.raw_buf_mut();
29602 unsafe {
29603 let lp = buf.as_mut_ptr().add(lhs_off) as *mut f32;
29604 for (i, &v) in lhs.iter().enumerate() {
29605 *lp.add(i) = v;
29606 }
29607 let rp = buf.as_mut_ptr().add(rhs_off) as *mut f32;
29608 for (i, &v) in rhs.iter().enumerate() {
29609 *rp.add(i) = v;
29610 }
29611 }
29612 execute_thunks(&sched, arena.raw_buf_mut());
29613 unsafe {
29614 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29615 (0..m * n).map(|i| *p.add(i)).collect()
29616 }
29617 }
29618
29619 let (m, k, n) = (4usize, 64usize, 8usize);
29620 let lhs: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.13).sin() * 1.5).collect();
29621 let rhs: Vec<f32> = (0..n * k).map(|i| (i as f32 * 0.07).cos() * 1.2).collect();
29622 let mut reference = vec![0f32; m * n];
29623 for i in 0..m {
29624 for j in 0..n {
29625 let mut acc = 0f32;
29626 for p in 0..k {
29627 acc += lhs[i * k + p] * rhs[j * k + p];
29628 }
29629 reference[i * n + j] = acc;
29630 }
29631 }
29632
29633 let cases = [
29635 (F8E4M3, 0.999f32),
29636 (F8E5M2, 0.99),
29637 (F8E4M3Fnuz, 0.999),
29638 (F8E5M2Fnuz, 0.99),
29639 (F6E2M3, 0.99),
29640 (F6E3M2, 0.98),
29641 (F4E2M1, 0.90),
29642 ];
29643 for (fmt, thresh) in cases {
29644 let out = run_scaled(&lhs, &rhs, m, k, n, fmt, fmt, ScaleLayout::PerTensor);
29645 let c = cosine(&out, &reference);
29646 assert!(c >= thresh, "{fmt} per-tensor cosine {c} < {thresh}");
29647 }
29648
29649 let out_mx = run_scaled(&lhs, &rhs, m, k, n, F8E4M3, F8E4M3, ScaleLayout::mx());
29651 let c_mx = cosine(&out_mx, &reference);
29652 assert!(c_mx >= 0.999, "mx-e8m0 e4m3 cosine {c_mx}");
29653 let out_nv = run_scaled(&lhs, &rhs, m, k, n, F4E2M1, F4E2M1, ScaleLayout::nvfp4());
29654 let c_nv = cosine(&out_nv, &reference);
29655 assert!(c_nv >= 0.95, "nvfp4 e2m1 cosine {c_nv}");
29656 }
29657
29658 #[test]
29663 fn scaled_dequantize_inverts_quantize() {
29664 use rlx_ir::ScaledFormat::*;
29665 use rlx_ir::{ScaleLayout, ScaledFormat};
29666
29667 fn cosine(a: &[f32], b: &[f32]) -> f32 {
29668 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
29669 let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
29670 let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
29671 dot / (na * nb)
29672 }
29673
29674 fn roundtrip(x: &[f32], rows: usize, cols: usize, fmt: ScaledFormat) -> Vec<f32> {
29675 let f = DType::F32;
29676 let u8t = DType::U8;
29677 let layout = ScaleLayout::PerTensor;
29678 let mut g = Graph::new("dequant_rt");
29679 let x_in = g.input("x", Shape::new(&[rows, cols], f));
29680 let scale = g.add_node(
29681 Op::ScaledQuantScale {
29682 format: fmt,
29683 scale_layout: layout,
29684 },
29685 vec![x_in],
29686 Shape::new(&[1], f),
29687 );
29688 let codes = g.add_node(
29689 Op::ScaledQuantize {
29690 format: fmt,
29691 scale_layout: layout,
29692 },
29693 vec![x_in, scale],
29694 Shape::new(&[rows, cols], u8t),
29695 );
29696 let recon = g.add_node(
29697 Op::ScaledDequantize {
29698 format: fmt,
29699 scale_layout: layout,
29700 },
29701 vec![codes, scale],
29702 Shape::new(&[rows, cols], f),
29703 );
29704 g.set_outputs(vec![recon]);
29705
29706 let plan = rlx_opt::memory::plan_memory(&g);
29707 let mut arena = crate::arena::Arena::from_plan(plan);
29708 let sched = compile_thunks(&g, &arena);
29709 let x_off = arena.byte_offset(x_in);
29710 let r_off = arena.byte_offset(recon);
29711 unsafe {
29712 let p = arena.raw_buf_mut().as_mut_ptr().add(x_off) as *mut f32;
29713 for (i, &v) in x.iter().enumerate() {
29714 *p.add(i) = v;
29715 }
29716 }
29717 execute_thunks(&sched, arena.raw_buf_mut());
29718 unsafe {
29719 let p = arena.raw_buf().as_ptr().add(r_off) as *const f32;
29720 (0..rows * cols).map(|i| *p.add(i)).collect()
29721 }
29722 }
29723
29724 let (rows, cols) = (4usize, 16usize);
29725 let x: Vec<f32> = (0..rows * cols)
29726 .map(|i| (i as f32 * 0.21).sin() * 1.7)
29727 .collect();
29728 for (fmt, min_cos) in [(F8E4M3, 0.999f32), (F8E5M2, 0.99), (F4E2M1, 0.93)] {
29730 let recon = roundtrip(&x, rows, cols, fmt);
29731 assert_eq!(recon.len(), x.len());
29732 assert!(
29733 recon.iter().all(|v| v.is_finite()),
29734 "{fmt:?} produced non-finite"
29735 );
29736 let c = cosine(&recon, &x);
29737 assert!(c >= min_cos, "{fmt:?} round-trip cosine {c} < {min_cos}");
29738 }
29739 }
29740
29741 #[test]
29749 fn scaled_custom_f4e3m0_round_trip_is_exact() {
29750 use rlx_ir::{ScaleLayout, ScaledFormat};
29751
29752 let fmt = ScaledFormat::custom(3, 0); assert_eq!(fmt.to_string(), "f4e3m0");
29754 let (rows, cols) = (3usize, 8usize);
29755 let grid = [16.0f32, -8.0, 4.0, -2.0, 1.0, -0.5, 0.25, 0.0];
29757 let x: Vec<f32> = (0..rows * cols).map(|i| grid[i % grid.len()]).collect();
29758
29759 let f = DType::F32;
29760 let u8t = DType::U8;
29761 let layout = ScaleLayout::PerTensor;
29762 let mut g = Graph::new("f4e3m0_rt");
29763 let x_in = g.input("x", Shape::new(&[rows, cols], f));
29764 let scale = g.add_node(
29765 Op::ScaledQuantScale {
29766 format: fmt,
29767 scale_layout: layout,
29768 },
29769 vec![x_in],
29770 Shape::new(&[1], f),
29771 );
29772 let codes = g.add_node(
29773 Op::ScaledQuantize {
29774 format: fmt,
29775 scale_layout: layout,
29776 },
29777 vec![x_in, scale],
29778 Shape::new(&[rows, cols], u8t),
29779 );
29780 let recon = g.add_node(
29781 Op::ScaledDequantize {
29782 format: fmt,
29783 scale_layout: layout,
29784 },
29785 vec![codes, scale],
29786 Shape::new(&[rows, cols], f),
29787 );
29788 g.set_outputs(vec![recon]);
29789
29790 let plan = rlx_opt::memory::plan_memory(&g);
29791 let mut arena = crate::arena::Arena::from_plan(plan);
29792 let sched = compile_thunks(&g, &arena);
29793 let x_off = arena.byte_offset(x_in);
29794 let r_off = arena.byte_offset(recon);
29795 unsafe {
29796 let p = arena.raw_buf_mut().as_mut_ptr().add(x_off) as *mut f32;
29797 for (i, &v) in x.iter().enumerate() {
29798 *p.add(i) = v;
29799 }
29800 }
29801 execute_thunks(&sched, arena.raw_buf_mut());
29802 let recon_vals: Vec<f32> = unsafe {
29803 let p = arena.raw_buf().as_ptr().add(r_off) as *const f32;
29804 (0..rows * cols).map(|i| *p.add(i)).collect()
29805 };
29806 assert_eq!(recon_vals, x, "f4e3m0 grid values must round-trip exactly");
29807 }
29808
29809 #[test]
29813 fn scaled_matmul_builder_helper_tracks_f32() {
29814 use rlx_ir::{ScaleLayout, ScaledFormat};
29815
29816 let (m, k, n) = (4usize, 64usize, 8usize);
29817 let lhs: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.13).sin() * 1.5).collect();
29818 let rhs: Vec<f32> = (0..n * k).map(|i| (i as f32 * 0.07).cos() * 1.2).collect();
29819
29820 let mut g = Graph::new("scaled_builder");
29821 let lhs_in = g.input("lhs", Shape::new(&[m, k], DType::F32));
29822 let rhs_in = g.input("rhs", Shape::new(&[n, k], DType::F32));
29823 let y = g.scaled_matmul(
29825 lhs_in,
29826 rhs_in,
29827 ScaledFormat::custom(3, 0),
29828 ScaleLayout::mx(),
29829 );
29830 g.set_outputs(vec![y]);
29831
29832 let count = |k: rlx_ir::OpKind| g.nodes().iter().filter(|nd| nd.op.kind() == k).count();
29834 assert_eq!(count(rlx_ir::OpKind::ScaledMatMul), 1);
29835 assert_eq!(count(rlx_ir::OpKind::ScaledQuantize), 2);
29836 assert_eq!(count(rlx_ir::OpKind::ScaledQuantScale), 2);
29837
29838 let plan = rlx_opt::memory::plan_memory(&g);
29839 let mut arena = crate::arena::Arena::from_plan(plan);
29840 let sched = compile_thunks(&g, &arena);
29841 let lhs_off = arena.byte_offset(lhs_in);
29842 let rhs_off = arena.byte_offset(rhs_in);
29843 let y_off = arena.byte_offset(y);
29844 unsafe {
29845 let p = arena.raw_buf_mut().as_mut_ptr();
29846 let lp = p.add(lhs_off) as *mut f32;
29847 for (i, &v) in lhs.iter().enumerate() {
29848 *lp.add(i) = v;
29849 }
29850 let rp = p.add(rhs_off) as *mut f32;
29851 for (i, &v) in rhs.iter().enumerate() {
29852 *rp.add(i) = v;
29853 }
29854 }
29855 execute_thunks(&sched, arena.raw_buf_mut());
29856 let out: Vec<f32> = unsafe {
29857 let p = arena.raw_buf().as_ptr().add(y_off) as *const f32;
29858 (0..m * n).map(|i| *p.add(i)).collect()
29859 };
29860
29861 let mut reference = vec![0f32; m * n];
29863 for i in 0..m {
29864 for j in 0..n {
29865 let mut acc = 0f32;
29866 for p in 0..k {
29867 acc += lhs[i * k + p] * rhs[j * k + p];
29868 }
29869 reference[i * n + j] = acc;
29870 }
29871 }
29872 let dot: f32 = out.iter().zip(&reference).map(|(a, b)| a * b).sum();
29873 let na = out.iter().map(|x| x * x).sum::<f32>().sqrt();
29874 let nb = reference.iter().map(|x| x * x).sum::<f32>().sqrt();
29875 let cos = dot / (na * nb);
29876 assert!(cos >= 0.9, "scaled_matmul builder cosine {cos} < 0.9");
29877 }
29878
29879 #[test]
29883 fn fma_matches_mul_add() {
29884 let f = DType::F32;
29885 let n = 9usize;
29886 let a: Vec<f32> = vec![1.5, -2.0, 0.0, 3.25, -1.1, 7.0, -0.5, 2.2, 9.9];
29887 let b: Vec<f32> = vec![2.0, 0.5, 4.0, -1.0, 6.0, -2.5, 8.0, -3.3, 0.1];
29888 let c: Vec<f32> = vec![0.25, 1.0, -3.0, 2.0, -0.5, 4.0, 1.5, -2.2, 0.0];
29889
29890 let mut g = Graph::new("fma");
29891 let an = g.input("a", Shape::new(&[n], f));
29892 let bn = g.input("b", Shape::new(&[n], f));
29893 let cn = g.input("c", Shape::new(&[n], f));
29894 let out = g.add_node(Op::Fma, vec![an, bn, cn], Shape::new(&[n], f));
29895 g.set_outputs(vec![out]);
29896
29897 let actual = run_graph(&g, &[(an, &a), (bn, &b), (cn, &c)], out, n);
29898 for i in 0..n {
29899 let expected = a[i].mul_add(b[i], c[i]);
29900 assert!(
29901 (actual[i] - expected).abs() <= f32::EPSILON * (1.0 + expected.abs()),
29902 "fma[{i}]: {} vs mul_add {expected}",
29903 actual[i]
29904 );
29905 }
29906 }
29907
29908 #[test]
29913 fn scaled_quant_pass_runs_end_to_end() {
29914 use rlx_opt::rlx_compile::scaled_quant_insert::{ScaledQuantConfig, insert_scaled_matmul};
29915
29916 let f = DType::F32;
29917 let (m, k, n) = (3usize, 16usize, 5usize);
29918 let mut g = Graph::new("amp_fp8");
29919 let x = g.input("x", Shape::new(&[m, k], f));
29920 let w = g.param("w", Shape::new(&[k, n], f));
29921 let mm = g.matmul(x, w, Shape::new(&[m, n], f));
29922 g.set_outputs(vec![mm]);
29923
29924 let g = insert_scaled_matmul(g, ScaledQuantConfig::fp8_e4m3());
29925
29926 let x_data: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.11).sin()).collect();
29927 let w_data: Vec<f32> = (0..k * n).map(|i| (i as f32 * 0.05).cos()).collect();
29928 let mut reference = vec![0f32; m * n];
29930 for i in 0..m {
29931 for j in 0..n {
29932 let mut acc = 0f32;
29933 for p in 0..k {
29934 acc += x_data[i * k + p] * w_data[p * n + j];
29935 }
29936 reference[i * n + j] = acc;
29937 }
29938 }
29939
29940 let mut x_id = None;
29942 let mut w_id = None;
29943 for node in g.nodes() {
29944 match &node.op {
29945 Op::Input { name } if name == "x" => x_id = Some(node.id),
29946 Op::Param { name } if name == "w" => w_id = Some(node.id),
29947 _ => {}
29948 }
29949 }
29950 let (x_id, w_id) = (x_id.unwrap(), w_id.unwrap());
29951 let out_id = g.outputs[0];
29952
29953 let plan = rlx_opt::memory::plan_memory(&g);
29954 let mut arena = crate::arena::Arena::from_plan(plan);
29955 let sched = compile_thunks(&g, &arena);
29956 let x_off = arena.byte_offset(x_id);
29957 let w_off = arena.byte_offset(w_id);
29958 let out_off = arena.byte_offset(out_id);
29959 let buf = arena.raw_buf_mut();
29960 unsafe {
29961 let xp = buf.as_mut_ptr().add(x_off) as *mut f32;
29962 for (i, &v) in x_data.iter().enumerate() {
29963 *xp.add(i) = v;
29964 }
29965 let wp = buf.as_mut_ptr().add(w_off) as *mut f32;
29966 for (i, &v) in w_data.iter().enumerate() {
29967 *wp.add(i) = v;
29968 }
29969 }
29970 execute_thunks(&sched, arena.raw_buf_mut());
29971 let actual: Vec<f32> = unsafe {
29972 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29973 (0..m * n).map(|i| *p.add(i)).collect()
29974 };
29975
29976 let dot: f32 = actual.iter().zip(&reference).map(|(a, b)| a * b).sum();
29977 let na = actual.iter().map(|x| x * x).sum::<f32>().sqrt();
29978 let nb = reference.iter().map(|x| x * x).sum::<f32>().sqrt();
29979 let cos = dot / (na * nb);
29980 assert!(cos >= 0.999, "AMP-fp8 e2e cosine {cos}");
29981 }
29982}