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 let out_addr = out.as_mut_ptr() as usize;
23000 crate::pool::par_for(n, 1, &|off, cnt| {
23001 let mut col = vec![0f32; k_dim * p_dim];
23002 for ni in off..off + cnt {
23003 for g in 0..groups {
23004 let x_off = ni * x_stride_n + g * x_stride_g;
23005 im2col(
23006 &inp[x_off..x_off + x_stride_g],
23007 &mut col,
23008 c_in_per_g,
23009 h,
23010 w,
23011 h_out,
23012 w_out,
23013 kh,
23014 kw,
23015 sh,
23016 sw,
23017 ph,
23018 pw,
23019 dh,
23020 dw,
23021 );
23022 let w_off = g * w_stride_g;
23023 let o_off = ni * out_stride_n + g * out_stride_g;
23024 let out_g = unsafe {
23025 std::slice::from_raw_parts_mut((out_addr as *mut f32).add(o_off), out_stride_g)
23026 };
23027 crate::blas::sgemm(
23028 &wt[w_off..w_off + w_stride_g],
23029 &col,
23030 out_g,
23031 c_out_per_g,
23032 k_dim,
23033 p_dim,
23034 );
23035 }
23036 }
23037 });
23038}
23039
23040fn im2col(
23041 x: &[f32],
23042 col: &mut [f32],
23043 c_in: usize,
23044 h: usize,
23045 w: usize,
23046 h_out: usize,
23047 w_out: usize,
23048 kh: usize,
23049 kw: usize,
23050 sh: usize,
23051 sw: usize,
23052 ph: usize,
23053 pw: usize,
23054 dh: usize,
23055 dw_dil: usize,
23056) {
23057 let n_dim = h_out * w_out;
23058 debug_assert_eq!(col.len(), c_in * kh * kw * n_dim);
23059 debug_assert_eq!(x.len(), c_in * h * w);
23060 let h_isz = h as isize;
23061 let w_isz = w as isize;
23062 let ph_isz = ph as isize;
23063 let pw_isz = pw as isize;
23064 for ci in 0..c_in {
23065 for ki in 0..kh {
23066 for kj in 0..kw {
23067 let row = ((ci * kh) + ki) * kw + kj;
23068 let row_off = row * n_dim;
23069 for ho in 0..h_out {
23070 let hi = (ho * sh + ki * dh) as isize - ph_isz;
23071 if hi < 0 || hi >= h_isz {
23072 for wo in 0..w_out {
23073 col[row_off + ho * w_out + wo] = 0.0;
23074 }
23075 continue;
23076 }
23077 let hi = hi as usize;
23078 let in_row_off = (ci * h + hi) * w;
23079 for wo in 0..w_out {
23080 let wi = (wo * sw + kj * dw_dil) as isize - pw_isz;
23081 col[row_off + ho * w_out + wo] = if wi < 0 || wi >= w_isz {
23082 0.0
23083 } else {
23084 x[in_row_off + wi as usize]
23085 };
23086 }
23087 }
23088 }
23089 }
23090 }
23091}
23092
23093#[allow(clippy::too_many_arguments)]
23100fn col2im(
23101 col: &[f32],
23102 x: &mut [f32],
23103 c_in: usize,
23104 h: usize,
23105 w: usize,
23106 h_out: usize,
23107 w_out: usize,
23108 kh: usize,
23109 kw: usize,
23110 sh: usize,
23111 sw: usize,
23112 ph: usize,
23113 pw: usize,
23114 dh: usize,
23115 dw_dil: usize,
23116) {
23117 let n_dim = h_out * w_out;
23118 debug_assert_eq!(col.len(), c_in * kh * kw * n_dim);
23119 debug_assert_eq!(x.len(), c_in * h * w);
23120 let h_isz = h as isize;
23121 let w_isz = w as isize;
23122 let ph_isz = ph as isize;
23123 let pw_isz = pw as isize;
23124 for ci in 0..c_in {
23125 for ki in 0..kh {
23126 for kj in 0..kw {
23127 let row = ((ci * kh) + ki) * kw + kj;
23128 let row_off = row * n_dim;
23129 for ho in 0..h_out {
23130 let hi = (ho * sh + ki * dh) as isize - ph_isz;
23131 if hi < 0 || hi >= h_isz {
23132 continue;
23133 }
23134 let hi = hi as usize;
23135 let in_row_off = (ci * h + hi) * w;
23136 for wo in 0..w_out {
23137 let wi = (wo * sw + kj * dw_dil) as isize - pw_isz;
23138 if wi < 0 || wi >= w_isz {
23139 continue;
23140 }
23141 x[in_row_off + wi as usize] += col[row_off + ho * w_out + wo];
23142 }
23143 }
23144 }
23145 }
23146 }
23147}
23148
23149fn quant_layout(shape: &rlx_ir::Shape, axis: Option<usize>) -> (usize, usize, usize) {
23159 match axis {
23160 None => (0, 1, shape.num_elements().unwrap_or(0).max(1)),
23161 Some(d) => {
23162 let chan_dim = shape.dim(d).unwrap_static();
23163 let inner: usize = (d + 1..shape.rank())
23164 .map(|i| shape.dim(i).unwrap_static())
23165 .product::<usize>()
23166 .max(1);
23167 (d, chan_dim, inner)
23168 }
23169 }
23170}
23171
23172fn activation_backward_kernel(
23173 act: rlx_ir::op::Activation,
23174 xs: &[f32],
23175 dys: &[f32],
23176 out: &mut [f32],
23177) {
23178 use rlx_ir::op::Activation;
23179 let n = xs.len();
23180 debug_assert_eq!(dys.len(), n);
23181 debug_assert_eq!(out.len(), n);
23182 match act {
23183 Activation::Relu => {
23184 for i in 0..n {
23185 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
23186 }
23187 }
23188 Activation::Sigmoid => {
23189 for i in 0..n {
23190 let s = 1.0 / (1.0 + (-xs[i]).exp());
23191 out[i] = s * (1.0 - s) * dys[i];
23192 }
23193 }
23194 Activation::Tanh => {
23195 for i in 0..n {
23196 let t = xs[i].tanh();
23197 out[i] = (1.0 - t * t) * dys[i];
23198 }
23199 }
23200 Activation::Silu => {
23201 for i in 0..n {
23203 let s = 1.0 / (1.0 + (-xs[i]).exp());
23204 out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
23205 }
23206 }
23207 Activation::Gelu => {
23208 const INV_SQRT2: f32 = 0.707_106_77;
23211 const INV_SQRT_2PI: f32 = 0.398_942_3;
23212 for i in 0..n {
23213 let x = xs[i];
23214 let phi = 0.5 * (1.0 + erf_f32(x * INV_SQRT2));
23215 let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
23216 out[i] = (phi + x * pdf) * dys[i];
23217 }
23218 }
23219 Activation::GeluApprox => {
23220 const C: f32 = 0.797_884_6; const A: f32 = 0.044_715;
23224 for i in 0..n {
23225 let x = xs[i];
23226 let inner = C * (x + A * x * x * x);
23227 let t = inner.tanh();
23228 let dinner = C * (1.0 + 3.0 * A * x * x);
23229 let d = 0.5 * (1.0 + t) + 0.5 * x * (1.0 - t * t) * dinner;
23230 out[i] = d * dys[i];
23231 }
23232 }
23233 Activation::Exp => {
23234 for i in 0..n {
23235 out[i] = xs[i].exp() * dys[i];
23236 }
23237 }
23238 Activation::Log => {
23239 for i in 0..n {
23240 out[i] = dys[i] / xs[i];
23241 }
23242 }
23243 Activation::Sqrt => {
23244 for i in 0..n {
23246 let s = xs[i].sqrt();
23247 out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
23248 }
23249 }
23250 Activation::Rsqrt => {
23251 for i in 0..n {
23253 let s = xs[i].sqrt();
23254 out[i] = if s > 0.0 {
23255 -0.5 * dys[i] / (xs[i] * s)
23256 } else {
23257 0.0
23258 };
23259 }
23260 }
23261 Activation::Neg => {
23262 for i in 0..n {
23263 out[i] = -dys[i];
23264 }
23265 }
23266 Activation::Abs => {
23267 for i in 0..n {
23269 let x = xs[i];
23270 let s = if x > 0.0 {
23271 1.0
23272 } else if x < 0.0 {
23273 -1.0
23274 } else {
23275 0.0
23276 };
23277 out[i] = s * dys[i];
23278 }
23279 }
23280 Activation::Round => {
23281 out.copy_from_slice(dys);
23286 }
23287 Activation::Sin => {
23288 for i in 0..n {
23290 out[i] = xs[i].cos() * dys[i];
23291 }
23292 }
23293 Activation::Cos => {
23294 for i in 0..n {
23295 out[i] = -xs[i].sin() * dys[i];
23296 }
23297 }
23298 Activation::Tan => {
23299 for i in 0..n {
23301 let t = xs[i].tan();
23302 out[i] = (1.0 + t * t) * dys[i];
23303 }
23304 }
23305 Activation::Atan => {
23306 for i in 0..n {
23308 let x = xs[i];
23309 out[i] = dys[i] / (1.0 + x * x);
23310 }
23311 }
23312 }
23313}
23314
23315fn activation_backward_kernel_f64(
23319 act: rlx_ir::op::Activation,
23320 xs: &[f64],
23321 dys: &[f64],
23322 out: &mut [f64],
23323) {
23324 use rlx_ir::op::Activation;
23325 let n = xs.len();
23326 debug_assert_eq!(dys.len(), n);
23327 debug_assert_eq!(out.len(), n);
23328 match act {
23329 Activation::Relu => {
23330 for i in 0..n {
23331 out[i] = if xs[i] > 0.0 { dys[i] } else { 0.0 };
23332 }
23333 }
23334 Activation::Sigmoid => {
23335 for i in 0..n {
23336 let s = 1.0 / (1.0 + (-xs[i]).exp());
23337 out[i] = s * (1.0 - s) * dys[i];
23338 }
23339 }
23340 Activation::Tanh => {
23341 for i in 0..n {
23342 let t = xs[i].tanh();
23343 out[i] = (1.0 - t * t) * dys[i];
23344 }
23345 }
23346 Activation::Silu => {
23347 for i in 0..n {
23348 let s = 1.0 / (1.0 + (-xs[i]).exp());
23349 out[i] = s * (1.0 + xs[i] * (1.0 - s)) * dys[i];
23350 }
23351 }
23352 Activation::Gelu | Activation::GeluApprox => {
23353 const INV_SQRT2: f64 = std::f64::consts::FRAC_1_SQRT_2;
23355 const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
23356 for i in 0..n {
23357 let x = xs[i];
23358 let phi = 0.5 * (1.0 + erf_f64(x * INV_SQRT2));
23359 let pdf = INV_SQRT_2PI * (-(x * x) * 0.5).exp();
23360 out[i] = (phi + x * pdf) * dys[i];
23361 }
23362 }
23363 Activation::Exp => {
23364 for i in 0..n {
23365 out[i] = xs[i].exp() * dys[i];
23366 }
23367 }
23368 Activation::Log => {
23369 for i in 0..n {
23370 out[i] = dys[i] / xs[i];
23371 }
23372 }
23373 Activation::Sqrt => {
23374 for i in 0..n {
23375 let s = xs[i].sqrt();
23376 out[i] = if s > 0.0 { 0.5 * dys[i] / s } else { 0.0 };
23377 }
23378 }
23379 Activation::Rsqrt => {
23380 for i in 0..n {
23381 let s = xs[i].sqrt();
23382 out[i] = if s > 0.0 {
23383 -0.5 * dys[i] / (xs[i] * s)
23384 } else {
23385 0.0
23386 };
23387 }
23388 }
23389 Activation::Neg => {
23390 for i in 0..n {
23391 out[i] = -dys[i];
23392 }
23393 }
23394 Activation::Abs => {
23395 for i in 0..n {
23396 let x = xs[i];
23397 let s = if x > 0.0 {
23398 1.0
23399 } else if x < 0.0 {
23400 -1.0
23401 } else {
23402 0.0
23403 };
23404 out[i] = s * dys[i];
23405 }
23406 }
23407 Activation::Round => {
23408 out.copy_from_slice(dys);
23409 }
23410 Activation::Sin => {
23411 for i in 0..n {
23412 out[i] = xs[i].cos() * dys[i];
23413 }
23414 }
23415 Activation::Cos => {
23416 for i in 0..n {
23417 out[i] = -xs[i].sin() * dys[i];
23418 }
23419 }
23420 Activation::Tan => {
23421 for i in 0..n {
23422 let t = xs[i].tan();
23423 out[i] = (1.0 + t * t) * dys[i];
23424 }
23425 }
23426 Activation::Atan => {
23427 for i in 0..n {
23428 let x = xs[i];
23429 out[i] = dys[i] / (1.0 + x * x);
23430 }
23431 }
23432 }
23433}
23434
23435#[inline(always)]
23440fn erf_f64(x: f64) -> f64 {
23441 let s = x.signum();
23442 let x = x.abs();
23443 let t = 1.0 / (1.0 + 0.327_591_1 * x);
23444 let y = 1.0
23445 - (((((1.061_405_43 * t - 1.453_152_03) * t) + 1.421_413_75) * t - 0.284_496_74) * t
23446 + 0.254_829_59)
23447 * t
23448 * (-x * x).exp();
23449 s * y
23450}
23451
23452#[inline(always)]
23455fn erf_f32(x: f32) -> f32 {
23456 let s = x.signum();
23457 let x = x.abs();
23458 let t = 1.0 / (1.0 + 0.327_591_1 * x);
23459 let y = 1.0
23460 - (((((1.061_405_4 * t - 1.453_152_1) * t) + 1.421_413_8) * t - 0.284_496_74) * t
23461 + 0.254_829_6)
23462 * t
23463 * (-x * x).exp();
23464 s * y
23465}
23466
23467fn narrow_thunk_closure(
23468 src: usize,
23469 dst: usize,
23470 outer: u32,
23471 src_stride: u32,
23472 dst_stride: u32,
23473 inner: u32,
23474 elem_bytes: u8,
23475) -> Arc<dyn Fn(*mut u8) + Send + Sync> {
23476 let (outer, ss, ds, inner, eb) = (
23477 outer as usize,
23478 src_stride as usize,
23479 dst_stride as usize,
23480 inner as usize,
23481 elem_bytes as usize,
23482 );
23483 let row_bytes = inner.saturating_mul(eb);
23484 let src_row_stride = ss.saturating_mul(eb);
23485 let dst_row_stride = ds.saturating_mul(eb);
23486 Arc::new(move |base: *mut u8| unsafe {
23487 if row_bytes == 0 || src == dst {
23488 return;
23489 }
23490 let arena_len = usize::MAX;
23492 for o in 0..outer {
23493 let s_off = src + o * src_row_stride;
23494 let d_off = dst + o * dst_row_stride;
23495 if s_off == d_off {
23496 continue;
23497 }
23498 if s_off.saturating_add(row_bytes) > arena_len
23499 || d_off.saturating_add(row_bytes) > arena_len
23500 {
23501 break;
23502 }
23503 std::ptr::copy_nonoverlapping(base.add(s_off), base.add(d_off), row_bytes);
23504 }
23505 })
23506}
23507
23508unsafe fn sl(offset: usize, base: *mut u8, len: usize) -> &'static [f32] {
23509 if offset == usize::MAX {
23510 return &[];
23511 }
23512 unsafe { std::slice::from_raw_parts(base.add(offset) as *const f32, len) }
23513}
23514
23515#[inline(always)]
23516unsafe fn sl_mut(offset: usize, base: *mut u8, len: usize) -> &'static mut [f32] {
23517 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut f32, len) }
23518}
23519
23520#[inline(always)]
23521unsafe fn sl_f64(offset: usize, base: *mut u8, len: usize) -> &'static [f64] {
23522 if offset == usize::MAX {
23523 return &[];
23524 }
23525 unsafe { std::slice::from_raw_parts(base.add(offset) as *const f64, len) }
23526}
23527
23528#[inline(always)]
23529unsafe fn sl_mut_f64(offset: usize, base: *mut u8, len: usize) -> &'static mut [f64] {
23530 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut f64, len) }
23531}
23532
23533#[inline(always)]
23538#[allow(dead_code)]
23539unsafe fn sl_i32(offset: usize, base: *mut u8, len: usize) -> &'static [i32] {
23540 if offset == usize::MAX {
23541 return &[];
23542 }
23543 unsafe { std::slice::from_raw_parts(base.add(offset) as *const i32, len) }
23544}
23545
23546#[inline(always)]
23547#[allow(dead_code)]
23548unsafe fn sl_mut_i32(offset: usize, base: *mut u8, len: usize) -> &'static mut [i32] {
23549 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut i32, len) }
23550}
23551
23552#[inline(always)]
23553unsafe fn sl_i64(offset: usize, base: *mut u8, len: usize) -> &'static [i64] {
23554 if offset == usize::MAX {
23555 return &[];
23556 }
23557 unsafe { std::slice::from_raw_parts(base.add(offset) as *const i64, len) }
23558}
23559
23560#[inline(always)]
23561unsafe fn sl_mut_i64(offset: usize, base: *mut u8, len: usize) -> &'static mut [i64] {
23562 unsafe { std::slice::from_raw_parts_mut(base.add(offset) as *mut i64, len) }
23563}
23564
23565fn transpose_walk_f64(inp: &[f64], out: &mut [f64], out_dims: &[u32], in_strides: &[u32]) {
23569 let rank = out_dims.len();
23570 let mut idx = vec![0u32; rank];
23571 for o in 0..out.len() {
23572 let mut src_off = 0usize;
23573 for d in 0..rank {
23574 src_off += idx[d] as usize * in_strides[d] as usize;
23575 }
23576 out[o] = inp[broadcast_src_index(src_off, inp.len())];
23577 for d in (0..rank).rev() {
23579 idx[d] += 1;
23580 if idx[d] < out_dims[d] {
23581 break;
23582 }
23583 idx[d] = 0;
23584 }
23585 }
23586}
23587
23588fn apply_activation_f64(inp: &[f64], out: &mut [f64], kind: Activation) {
23594 match kind {
23595 Activation::Neg => {
23596 for (o, &v) in out.iter_mut().zip(inp) {
23597 *o = -v;
23598 }
23599 }
23600 Activation::Exp => {
23601 for (o, &v) in out.iter_mut().zip(inp) {
23602 *o = v.exp();
23603 }
23604 }
23605 Activation::Log => {
23606 for (o, &v) in out.iter_mut().zip(inp) {
23607 *o = v.ln();
23608 }
23609 }
23610 Activation::Sqrt => {
23611 for (o, &v) in out.iter_mut().zip(inp) {
23612 *o = v.sqrt();
23613 }
23614 }
23615 Activation::Rsqrt => {
23616 for (o, &v) in out.iter_mut().zip(inp) {
23617 *o = 1.0 / v.sqrt();
23618 }
23619 }
23620 Activation::Abs => {
23621 for (o, &v) in out.iter_mut().zip(inp) {
23622 *o = v.abs();
23623 }
23624 }
23625 Activation::Tanh => {
23626 for (o, &v) in out.iter_mut().zip(inp) {
23627 *o = v.tanh();
23628 }
23629 }
23630 Activation::Sigmoid => {
23631 for (o, &v) in out.iter_mut().zip(inp) {
23632 *o = 1.0 / (1.0 + (-v).exp());
23633 }
23634 }
23635 Activation::Relu => {
23636 for (o, &v) in out.iter_mut().zip(inp) {
23637 *o = v.max(0.0);
23638 }
23639 }
23640 Activation::Round => {
23641 for (o, &v) in out.iter_mut().zip(inp) {
23642 *o = v.round_ties_even();
23643 }
23644 }
23645 Activation::Sin => {
23646 for (o, &v) in out.iter_mut().zip(inp) {
23647 *o = v.sin();
23648 }
23649 }
23650 Activation::Cos => {
23651 for (o, &v) in out.iter_mut().zip(inp) {
23652 *o = v.cos();
23653 }
23654 }
23655 Activation::Tan => {
23656 for (o, &v) in out.iter_mut().zip(inp) {
23657 *o = v.tan();
23658 }
23659 }
23660 Activation::Atan => {
23661 for (o, &v) in out.iter_mut().zip(inp) {
23662 *o = v.atan();
23663 }
23664 }
23665 Activation::Gelu | Activation::GeluApprox | Activation::Silu => {
23666 panic!(
23667 "apply_activation_f64: {kind:?} not yet implemented at f64. \
23668 Add when a workload needs it."
23669 );
23670 }
23671 }
23672}
23673
23674#[inline]
23675fn binary_op_f64(op: BinaryOp, a: f64, b: f64) -> f64 {
23676 match op {
23677 BinaryOp::Add => a + b,
23678 BinaryOp::Sub => a - b,
23679 BinaryOp::Mul => a * b,
23680 BinaryOp::Div => a / b,
23681 BinaryOp::Max => a.max(b),
23682 BinaryOp::Min => a.min(b),
23683 BinaryOp::Pow => a.powf(b),
23684 }
23685}
23686
23687fn reduce_sum_f64(inp: &[f64], out: &mut [f64], outer: usize, reduced: usize, inner: usize) {
23690 for o in 0..outer {
23691 for n in 0..inner {
23692 let mut acc = 0.0_f64;
23693 for r in 0..reduced {
23694 acc += inp[o * reduced * inner + r * inner + n];
23695 }
23696 out[o * inner + n] = acc;
23697 }
23698 }
23699}
23700
23701pub unsafe fn fill_rng_normal_arena(
23707 dst_off: usize,
23708 len: usize,
23709 mean: f32,
23710 scale: f32,
23711 key: u64,
23712 op_seed: Option<f32>,
23713 opts: rlx_ir::RngOptions,
23714 arena: *mut u8,
23715) {
23716 if len == 0 {
23717 return;
23718 }
23719 unsafe {
23720 let out = std::slice::from_raw_parts_mut((arena.add(dst_off)) as *mut f32, len);
23721 rlx_ir::fill_normal_like(out, mean, scale, opts, key, op_seed);
23722 }
23723}
23724
23725pub unsafe fn fill_rng_uniform_arena(
23726 dst_off: usize,
23727 len: usize,
23728 low: f32,
23729 high: f32,
23730 key: u64,
23731 op_seed: Option<f32>,
23732 opts: rlx_ir::RngOptions,
23733 arena: *mut u8,
23734) {
23735 if len == 0 {
23736 return;
23737 }
23738 unsafe {
23739 let out = std::slice::from_raw_parts_mut((arena.add(dst_off)) as *mut f32, len);
23740 rlx_ir::fill_uniform_like(out, low, high, opts, key, op_seed);
23741 }
23742}
23743
23744#[cfg(test)]
23745mod tests {
23746 use super::*;
23747 use rlx_ir::*;
23748
23749 #[test]
23754 fn conv2d_im2col_matches_naive() {
23755 let cases = [
23757 (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), ];
23764 for (idx, &(n, c_in, h, w, c_out, kh, kw, sh, sw, ph, pw, dh, dw, groups)) in
23765 cases.iter().enumerate()
23766 {
23767 let c_in_per_g = c_in / groups;
23768 let h_out = (h + 2 * ph - dh * (kh - 1) - 1) / sh + 1;
23769 let w_out = (w + 2 * pw - dw * (kw - 1) - 1) / sw + 1;
23770 let mut s: u32 = 0x9e37_79b9 ^ (idx as u32 + 1);
23772 let mut rand = || {
23773 s ^= s << 13;
23774 s ^= s >> 17;
23775 s ^= s << 5;
23776 (s as f32 / u32::MAX as f32) - 0.5
23777 };
23778 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
23779 let wt: Vec<f32> = (0..c_out * c_in_per_g * kh * kw).map(|_| rand()).collect();
23780 let mut out_ref = vec![0f32; n * c_out * h_out * w_out];
23781 let mut out_fast = vec![0f32; n * c_out * h_out * w_out];
23782
23783 conv2d_forward_naive(
23784 &inp,
23785 &wt,
23786 &mut out_ref,
23787 n,
23788 c_in,
23789 h,
23790 w,
23791 c_out,
23792 h_out,
23793 w_out,
23794 kh,
23795 kw,
23796 sh,
23797 sw,
23798 ph,
23799 pw,
23800 dh,
23801 dw,
23802 groups,
23803 );
23804 conv2d_forward_im2col(
23805 &inp,
23806 &wt,
23807 &mut out_fast,
23808 n,
23809 c_in,
23810 h,
23811 w,
23812 c_out,
23813 h_out,
23814 w_out,
23815 kh,
23816 kw,
23817 sh,
23818 sw,
23819 ph,
23820 pw,
23821 dh,
23822 dw,
23823 groups,
23824 );
23825
23826 let max_abs = out_ref
23827 .iter()
23828 .zip(&out_fast)
23829 .map(|(a, b)| (a - b).abs())
23830 .fold(0f32, f32::max);
23831 assert!(
23832 max_abs < 1e-3,
23833 "case {idx}: im2col vs naive max abs diff {max_abs}"
23834 );
23835 }
23836 }
23837
23838 #[test]
23841 fn conv2d_direct_matches_naive() {
23842 let cases = [
23844 (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), ];
23850 for (idx, &(n, c_in, h, w, c_out, kh, kw, groups)) in cases.iter().enumerate() {
23851 let h_out = h - kh + 1;
23852 let w_out = w - kw + 1;
23853 let c_in_per_g = c_in / groups;
23854 let mut s: u32 = 0xfeed_1234 ^ (idx as u32 + 1);
23855 let mut rand = || {
23856 s ^= s << 13;
23857 s ^= s >> 17;
23858 s ^= s << 5;
23859 (s as f32 / u32::MAX as f32) - 0.5
23860 };
23861 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
23862 let wt: Vec<f32> = (0..c_out * c_in_per_g * kh * kw).map(|_| rand()).collect();
23863 let mut r = vec![0f32; n * c_out * h_out * w_out];
23864 let mut d = vec![0f32; n * c_out * h_out * w_out];
23865 conv2d_forward_naive(
23866 &inp, &wt, &mut r, n, c_in, h, w, c_out, h_out, w_out, kh, kw, 1, 1, 0, 0, 1, 1,
23867 groups,
23868 );
23869 conv2d_forward_direct(
23870 &inp, &wt, &mut d, n, c_in, h, w, c_out, h_out, w_out, kh, kw, groups,
23871 );
23872 let mx = r
23873 .iter()
23874 .zip(&d)
23875 .map(|(a, b)| (a - b).abs())
23876 .fold(0f32, f32::max);
23877 assert!(mx < 1e-4, "case {idx}: direct vs naive max abs diff {mx}");
23878 }
23879 }
23880
23881 #[test]
23885 fn conv2d_winograd_matches_naive() {
23886 let cases = [
23889 (1, 1, 28, 28, 8), (4, 8, 13, 13, 16), (2, 3, 9, 9, 5), (1, 4, 8, 8, 4), ];
23894 for (idx, &(n, c_in, h, w, c_out)) in cases.iter().enumerate() {
23895 let h_out = h - 2;
23896 let w_out = w - 2;
23897 let mut s: u32 = 0x1234_5678 ^ (idx as u32 + 1);
23898 let mut rand = || {
23899 s ^= s << 13;
23900 s ^= s >> 17;
23901 s ^= s << 5;
23902 (s as f32 / u32::MAX as f32) - 0.5
23903 };
23904 let inp: Vec<f32> = (0..n * c_in * h * w).map(|_| rand()).collect();
23905 let wt: Vec<f32> = (0..c_out * c_in * 9).map(|_| rand()).collect();
23906 let mut out_ref = vec![0f32; n * c_out * h_out * w_out];
23907 let mut out_win = vec![0f32; n * c_out * h_out * w_out];
23908 conv2d_forward_naive(
23909 &inp,
23910 &wt,
23911 &mut out_ref,
23912 n,
23913 c_in,
23914 h,
23915 w,
23916 c_out,
23917 h_out,
23918 w_out,
23919 3,
23920 3,
23921 1,
23922 1,
23923 0,
23924 0,
23925 1,
23926 1,
23927 1,
23928 );
23929 conv2d_forward_winograd(&inp, &wt, &mut out_win, n, c_in, h, w, c_out, h_out, w_out);
23930 let max_abs = out_ref
23931 .iter()
23932 .zip(&out_win)
23933 .map(|(a, b)| (a - b).abs())
23934 .fold(0f32, f32::max);
23935 assert!(
23936 max_abs < 1e-3,
23937 "case {idx}: winograd vs naive max abs diff {max_abs}"
23938 );
23939 }
23940 }
23941
23942 #[test]
23948 fn narrow_rope_fuses_in_unfused_path() {
23949 let f = DType::F32;
23950 let mut g = Graph::new("nr_fuse");
23951 let qkv = g.input("qkv", Shape::new(&[16, 8, 192], f)); let cos = g.input("cos", Shape::new(&[16], f));
23954 let sin = g.input("sin", Shape::new(&[16], f));
23955 let q = g.narrow_(qkv, 2, 0, 64);
23957 let q_rope = g.rope(q, cos, sin, 16);
23958 g.set_outputs(vec![q_rope]);
23959
23960 let plan = rlx_opt::memory::plan_memory(&g);
23961 let arena = crate::arena::Arena::from_plan(plan);
23962 let sched = compile_thunks(&g, &arena);
23963
23964 let mut narrow_count = 0;
23965 let mut rope_with_stride: Option<u32> = None;
23966 for t in &sched.thunks {
23967 match t {
23968 Thunk::Narrow { .. } => narrow_count += 1,
23969 Thunk::Rope { src_row_stride, .. } => rope_with_stride = Some(*src_row_stride),
23970 _ => {}
23971 }
23972 }
23973 assert_eq!(
23976 narrow_count, 0,
23977 "Narrow→Rope fusion should leave zero Narrow thunks; saw {narrow_count}"
23978 );
23979 assert_eq!(
23980 rope_with_stride,
23981 Some(192),
23982 "Rope's src_row_stride should be 192 (parent qkv axis), saw {rope_with_stride:?}"
23983 );
23984 }
23985
23986 #[test]
23989 fn ssm_selective_scan_matches_reference() {
23990 use rlx_ir::Philox4x32;
23991 let bch = 1usize;
23992 let s = 4usize;
23993 let h = 3usize;
23994 let n = 2usize;
23995
23996 let mut rng = Philox4x32::new(13);
23997 let mut x = vec![0f32; bch * s * h];
23998 rng.fill_normal(&mut x);
23999 let mut delta = vec![0f32; bch * s * h];
24000 for v in delta.iter_mut() {
24002 *v = (rng.next_f32() - 0.5) * 0.1;
24003 }
24004 let mut a = vec![0f32; h * n];
24005 for v in a.iter_mut() {
24006 *v = -(rng.next_f32() * 0.5 + 0.1);
24007 } let mut b = vec![0f32; bch * s * n];
24009 rng.fill_normal(&mut b);
24010 let mut c = vec![0f32; bch * s * n];
24011 rng.fill_normal(&mut c);
24012
24013 let mut expected = vec![0f32; bch * s * h];
24015 for bi in 0..bch {
24016 let mut state = vec![0f32; h * n];
24017 for si in 0..s {
24018 for ci in 0..h {
24019 let d = delta[bi * s * h + si * h + ci];
24020 let xv = x[bi * s * h + si * h + ci];
24021 let mut acc = 0f32;
24022 for ni in 0..n {
24023 let da = (d * a[ci * n + ni]).exp();
24024 state[ci * n + ni] =
24025 da * state[ci * n + ni] + d * b[bi * s * n + si * n + ni] * xv;
24026 acc += c[bi * s * n + si * n + ni] * state[ci * n + ni];
24027 }
24028 expected[bi * s * h + si * h + ci] = acc;
24029 }
24030 }
24031 }
24032
24033 let f = DType::F32;
24035 let mut g = Graph::new("ssm");
24036 let xn = g.input("x", Shape::new(&[bch, s, h], f));
24037 let dn = g.input("delta", Shape::new(&[bch, s, h], f));
24038 let an = g.param("a", Shape::new(&[h, n], f));
24039 let bn = g.param("b", Shape::new(&[bch, s, n], f));
24040 let cn = g.param("c", Shape::new(&[bch, s, n], f));
24041 let yn = g.selective_scan(xn, dn, an, bn, cn, n, Shape::new(&[bch, s, h], f));
24042 g.set_outputs(vec![yn]);
24043
24044 let plan = rlx_opt::memory::plan_memory(&g);
24045 let mut arena = crate::arena::Arena::from_plan(plan);
24046 let sched = compile_thunks(&g, &arena);
24047
24048 let xn_off = arena.byte_offset(xn);
24049 let dn_off = arena.byte_offset(dn);
24050 let an_off = arena.byte_offset(an);
24051 let bn_off = arena.byte_offset(bn);
24052 let cn_off = arena.byte_offset(cn);
24053 let yn_off = arena.byte_offset(yn);
24054 let buf = arena.raw_buf_mut();
24055 unsafe {
24056 let copy = |dst: *mut f32, data: &[f32]| {
24057 for (i, &v) in data.iter().enumerate() {
24058 *dst.add(i) = v;
24059 }
24060 };
24061 copy(buf.as_mut_ptr().add(xn_off) as *mut f32, &x);
24062 copy(buf.as_mut_ptr().add(dn_off) as *mut f32, &delta);
24063 copy(buf.as_mut_ptr().add(an_off) as *mut f32, &a);
24064 copy(buf.as_mut_ptr().add(bn_off) as *mut f32, &b);
24065 copy(buf.as_mut_ptr().add(cn_off) as *mut f32, &c);
24066 }
24067 execute_thunks(&sched, arena.raw_buf_mut());
24068
24069 let actual: Vec<f32> = unsafe {
24070 let p = arena.raw_buf().as_ptr().add(yn_off) as *const f32;
24071 (0..bch * s * h).map(|i| *p.add(i)).collect()
24072 };
24073
24074 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
24075 assert!(
24076 (e - a).abs() < 1e-3,
24077 "mismatch at {i}: expected {e}, got {a}"
24078 );
24079 }
24080 }
24081
24082 #[test]
24085 fn conv_1x1_fast_path_matches_scalar() {
24086 use rlx_ir::Philox4x32;
24087 let n = 2usize;
24089 let c_in = 4usize;
24090 let h = 3usize;
24091 let w = 3usize;
24092 let c_out = 5usize;
24093 let mut rng = Philox4x32::new(31);
24094 let mut x = vec![0f32; n * c_in * h * w];
24095 rng.fill_normal(&mut x);
24096 let mut weight = vec![0f32; c_out * c_in];
24097 rng.fill_normal(&mut weight);
24098
24099 let mut expected = vec![0f32; n * c_out * h * w];
24102 for ni in 0..n {
24103 for co in 0..c_out {
24104 for hi in 0..h {
24105 for wi in 0..w {
24106 let mut acc = 0f32;
24107 for ci in 0..c_in {
24108 acc += weight[co * c_in + ci]
24109 * x[((ni * c_in) + ci) * h * w + hi * w + wi];
24110 }
24111 expected[((ni * c_out) + co) * h * w + hi * w + wi] = acc;
24112 }
24113 }
24114 }
24115 }
24116
24117 let f = DType::F32;
24119 let mut g = Graph::new("conv1x1");
24120 let xn = g.input("x", Shape::new(&[n, c_in, h, w], f));
24121 let wn = g.param("w", Shape::new(&[c_out, c_in, 1, 1], f));
24122 let cn = g.add_node(
24124 rlx_ir::Op::Conv {
24125 kernel_size: vec![1, 1],
24126 stride: vec![1, 1],
24127 padding: vec![0, 0],
24128 dilation: vec![1, 1],
24129 groups: 1,
24130 },
24131 vec![xn, wn],
24132 Shape::new(&[n, c_out, h, w], f),
24133 );
24134 g.set_outputs(vec![cn]);
24135
24136 let plan = rlx_opt::memory::plan_memory(&g);
24137 let mut arena = crate::arena::Arena::from_plan(plan);
24138 let sched = compile_thunks(&g, &arena);
24139
24140 let saw_fast = sched
24142 .thunks
24143 .iter()
24144 .any(|t| matches!(t, Thunk::Conv2D1x1 { .. }));
24145 let saw_slow = sched
24146 .thunks
24147 .iter()
24148 .any(|t| matches!(t, Thunk::Conv2D { .. }));
24149 assert!(saw_fast, "1×1 conv should emit Conv2D1x1");
24150 assert!(!saw_slow, "1×1 conv must not fall through to scalar Conv2D");
24151
24152 let xn_off = arena.byte_offset(xn);
24153 let wn_off = arena.byte_offset(wn);
24154 let cn_off = arena.byte_offset(cn);
24155 let buf = arena.raw_buf_mut();
24156 unsafe {
24157 let xp = buf.as_mut_ptr().add(xn_off) as *mut f32;
24158 for (i, &v) in x.iter().enumerate() {
24159 *xp.add(i) = v;
24160 }
24161 let wp = buf.as_mut_ptr().add(wn_off) as *mut f32;
24162 for (i, &v) in weight.iter().enumerate() {
24163 *wp.add(i) = v;
24164 }
24165 }
24166 execute_thunks(&sched, arena.raw_buf_mut());
24167
24168 let actual: Vec<f32> = unsafe {
24169 let p = arena.raw_buf().as_ptr().add(cn_off) as *const f32;
24170 (0..(n * c_out * h * w)).map(|i| *p.add(i)).collect()
24171 };
24172
24173 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
24174 assert!(
24175 (e - a).abs() < 1e-3,
24176 "mismatch at {i}: expected {e}, got {a}"
24177 );
24178 }
24179 }
24180
24181 #[test]
24184 fn dequant_matmul_int8_sym_matches_reference() {
24185 use rlx_ir::Philox4x32;
24186 use rlx_ir::quant::QuantScheme;
24187
24188 let m = 3usize;
24189 let k = 8usize;
24190 let n = 4usize;
24191 let block_size = 4usize; let blocks_per_col = k / block_size;
24193
24194 let mut rng = Philox4x32::new(99);
24196 let mut x = vec![0f32; m * k];
24197 rng.fill_normal(&mut x);
24198 let w_q: Vec<i8> = (0..(k * n))
24199 .map(|i| ((i as i32 * 13 + 7) % 127 - 63) as i8)
24200 .collect();
24201 let scales: Vec<f32> = (0..(blocks_per_col * n))
24202 .map(|i| 0.01 + 0.001 * i as f32)
24203 .collect();
24204
24205 let mut w_f32 = vec![0f32; k * n];
24207 for p in 0..k {
24208 let block = p / block_size;
24209 for j in 0..n {
24210 let s = scales[block * n + j];
24211 w_f32[p * n + j] = w_q[p * n + j] as f32 * s;
24212 }
24213 }
24214 let mut expected = vec![0f32; m * n];
24215 for i in 0..m {
24216 for j in 0..n {
24217 let mut acc = 0f32;
24218 for p in 0..k {
24219 acc += x[i * k + p] * w_f32[p * n + j];
24220 }
24221 expected[i * n + j] = acc;
24222 }
24223 }
24224
24225 let f = DType::F32;
24227 let mut g = Graph::new("dq");
24228 let xn = g.input("x", Shape::new(&[m, k], f));
24229 let wn = g.param("w", Shape::new(&[k, n], DType::I8));
24230 let sn = g.param("scale", Shape::new(&[blocks_per_col, n], f));
24231 let zn = g.param("zp", Shape::new(&[blocks_per_col, n], f)); let dq = g.dequant_matmul(
24233 xn,
24234 wn,
24235 sn,
24236 zn,
24237 QuantScheme::Int8Block {
24238 block_size: block_size as u32,
24239 },
24240 Shape::new(&[m, n], f),
24241 );
24242 g.set_outputs(vec![dq]);
24243
24244 let plan = rlx_opt::memory::plan_memory(&g);
24245 let mut arena = crate::arena::Arena::from_plan(plan);
24246 let sched = compile_thunks(&g, &arena);
24247
24248 let xn_off = arena.byte_offset(xn);
24249 let wn_off = arena.byte_offset(wn);
24250 let sn_off = arena.byte_offset(sn);
24251 let zn_off = arena.byte_offset(zn);
24252 let dq_off = arena.byte_offset(dq);
24253 let buf = arena.raw_buf_mut();
24254 unsafe {
24255 let xp = buf.as_mut_ptr().add(xn_off) as *mut f32;
24257 for (i, &v) in x.iter().enumerate() {
24258 *xp.add(i) = v;
24259 }
24260 let sp = buf.as_mut_ptr().add(sn_off) as *mut f32;
24261 for (i, &v) in scales.iter().enumerate() {
24262 *sp.add(i) = v;
24263 }
24264 let zp = buf.as_mut_ptr().add(zn_off) as *mut f32;
24265 for i in 0..(blocks_per_col * n) {
24266 *zp.add(i) = 0.0;
24267 }
24268 let wp = buf.as_mut_ptr().add(wn_off) as *mut i8;
24270 for (i, &v) in w_q.iter().enumerate() {
24271 *wp.add(i) = v;
24272 }
24273 }
24274 execute_thunks(&sched, arena.raw_buf_mut());
24275
24276 let actual: Vec<f32> = unsafe {
24277 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
24278 (0..m * n).map(|i| *p.add(i)).collect()
24279 };
24280
24281 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
24282 assert!(
24283 (e - a).abs() < 1e-3,
24284 "mismatch at {i}: expected {e}, got {a}"
24285 );
24286 }
24287 }
24288
24289 #[test]
24291 fn lora_matmul_matches_unfused_reference() {
24292 use rlx_ir::Philox4x32;
24293
24294 let m = 4usize;
24295 let k = 8usize;
24296 let n = 6usize;
24297 let r = 2usize;
24298 let scale = 0.5f32;
24299
24300 let mut rng = Philox4x32::new(42);
24302 let mut x = vec![0f32; m * k];
24303 rng.fill_normal(&mut x);
24304 let mut w = vec![0f32; k * n];
24305 rng.fill_normal(&mut w);
24306 let mut a = vec![0f32; k * r];
24307 rng.fill_normal(&mut a);
24308 let mut b = vec![0f32; r * n];
24309 rng.fill_normal(&mut b);
24310
24311 let naive = |a_buf: &[f32], b_buf: &[f32], rows: usize, inner: usize, cols: usize| {
24313 let mut o = vec![0f32; rows * cols];
24314 for i in 0..rows {
24315 for j in 0..cols {
24316 let mut acc = 0f32;
24317 for p in 0..inner {
24318 acc += a_buf[i * inner + p] * b_buf[p * cols + j];
24319 }
24320 o[i * cols + j] = acc;
24321 }
24322 }
24323 o
24324 };
24325 let xw = naive(&x, &w, m, k, n);
24326 let xa = naive(&x, &a, m, k, r);
24327 let xab = naive(&xa, &b, m, r, n);
24328 let mut expected = xw;
24329 for i in 0..(m * n) {
24330 expected[i] += scale * xab[i];
24331 }
24332
24333 let f = DType::F32;
24335 let mut g = Graph::new("lora");
24336 let xn = g.input("x", Shape::new(&[m, k], f));
24337 let wn = g.param("w", Shape::new(&[k, n], f));
24338 let an = g.param("a", Shape::new(&[k, r], f));
24339 let bn = g.param("b", Shape::new(&[r, n], f));
24340 let lm = g.lora_matmul(xn, wn, an, bn, scale, Shape::new(&[m, n], f));
24341 g.set_outputs(vec![lm]);
24342
24343 let plan = rlx_opt::memory::plan_memory(&g);
24344 let mut arena = crate::arena::Arena::from_plan(plan);
24345 let sched = compile_thunks(&g, &arena);
24346
24347 let xn_off = arena.byte_offset(xn);
24348 let wn_off = arena.byte_offset(wn);
24349 let an_off = arena.byte_offset(an);
24350 let bn_off = arena.byte_offset(bn);
24351 let lm_off = arena.byte_offset(lm);
24352 let buf = arena.raw_buf_mut();
24353 unsafe {
24354 let copy = |dst: *mut f32, data: &[f32]| {
24355 for (i, &v) in data.iter().enumerate() {
24356 *dst.add(i) = v;
24357 }
24358 };
24359 copy(buf.as_mut_ptr().add(xn_off) as *mut f32, &x);
24360 copy(buf.as_mut_ptr().add(wn_off) as *mut f32, &w);
24361 copy(buf.as_mut_ptr().add(an_off) as *mut f32, &a);
24362 copy(buf.as_mut_ptr().add(bn_off) as *mut f32, &b);
24363 }
24364 execute_thunks(&sched, arena.raw_buf_mut());
24365
24366 let actual: Vec<f32> = unsafe {
24367 let p = arena.raw_buf().as_ptr().add(lm_off) as *const f32;
24368 (0..m * n).map(|i| *p.add(i)).collect()
24369 };
24370
24371 for (i, (e, a)) in expected.iter().zip(&actual).enumerate() {
24372 assert!(
24373 (e - a).abs() < 1e-3,
24374 "mismatch at {i}: expected {e}, got {a}"
24375 );
24376 }
24377 }
24378
24379 #[test]
24381 fn sample_temperature_zero_is_argmax() {
24382 let f = DType::F32;
24385 let mut g = Graph::new("samp");
24386 let logits = g.input("logits", Shape::new(&[1, 8], f));
24387 let s = g.sample(logits, 0, 1.0, 1e-3, 42, Shape::new(&[1], f));
24388 g.set_outputs(vec![s]);
24389 let plan = rlx_opt::memory::plan_memory(&g);
24390 let mut arena = crate::arena::Arena::from_plan(plan);
24391 let sched = compile_thunks(&g, &arena);
24392
24393 let logits_off = arena.byte_offset(logits);
24394 let s_off = arena.byte_offset(s);
24395 let buf = arena.raw_buf_mut();
24396 unsafe {
24397 let p = buf.as_mut_ptr().add(logits_off) as *mut f32;
24398 let inputs = [0.1f32, 0.2, 0.3, 0.4, 0.5, 9.0, 0.7, 0.8];
24400 for (i, &v) in inputs.iter().enumerate() {
24401 *p.add(i) = v;
24402 }
24403 }
24404 execute_thunks(&sched, arena.raw_buf_mut());
24405
24406 let token = unsafe {
24407 let p = arena.raw_buf().as_ptr().add(s_off) as *const f32;
24408 *p as usize
24409 };
24410 assert_eq!(token, 5, "low-temp sampling should pick the argmax");
24411 }
24412
24413 #[test]
24414 fn sample_top_k_one_is_deterministic() {
24415 let f = DType::F32;
24417 let mut g = Graph::new("samp_k1");
24418 let logits = g.input("logits", Shape::new(&[1, 4], f));
24419 let s = g.sample(logits, 1, 1.0, 1.0, 7, Shape::new(&[1], f));
24420 g.set_outputs(vec![s]);
24421 let plan = rlx_opt::memory::plan_memory(&g);
24422 let mut arena = crate::arena::Arena::from_plan(plan);
24423 let sched = compile_thunks(&g, &arena);
24424
24425 let logits_off = arena.byte_offset(logits);
24426 let s_off = arena.byte_offset(s);
24427 let buf = arena.raw_buf_mut();
24428 unsafe {
24429 let p = buf.as_mut_ptr().add(logits_off) as *mut f32;
24430 let inputs = [0.1f32, 5.0, 0.3, 0.4]; for (i, &v) in inputs.iter().enumerate() {
24432 *p.add(i) = v;
24433 }
24434 }
24435 execute_thunks(&sched, arena.raw_buf_mut());
24436 let token = unsafe {
24437 let p = arena.raw_buf().as_ptr().add(s_off) as *const f32;
24438 *p as usize
24439 };
24440 assert_eq!(token, 1);
24441 }
24442
24443 #[test]
24445 fn cumsum_inclusive_matches_naive() {
24446 let f = DType::F32;
24447 let mut g = Graph::new("cumsum");
24448 let x = g.input("x", Shape::new(&[2, 4], f));
24449 let cs = g.cumsum(x, -1, false, Shape::new(&[2, 4], f));
24450 g.set_outputs(vec![cs]);
24451 let plan = rlx_opt::memory::plan_memory(&g);
24452 let mut arena = crate::arena::Arena::from_plan(plan);
24453 let sched = compile_thunks(&g, &arena);
24454
24455 let x_off = arena.byte_offset(x);
24457 let out_off = arena.byte_offset(cs);
24458 let buf = arena.raw_buf_mut();
24459 unsafe {
24460 let p = buf.as_mut_ptr().add(x_off) as *mut f32;
24461 let inputs = [1.0f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0];
24462 for (i, &v) in inputs.iter().enumerate() {
24463 *p.add(i) = v;
24464 }
24465 }
24466 execute_thunks(&sched, arena.raw_buf_mut());
24467
24468 let out: Vec<f32> = unsafe {
24469 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
24470 (0..8).map(|i| *p.add(i)).collect()
24471 };
24472 assert_eq!(out, vec![1.0, 3.0, 6.0, 10.0, 10.0, 30.0, 60.0, 100.0]);
24473 }
24474
24475 #[test]
24479 fn narrow_attention_fuses_in_unfused_path() {
24480 let f = DType::F32;
24481 let mut g = Graph::new("nattn_fuse");
24482 let qkv = g.input("qkv", Shape::new(&[8, 16, 192], f)); let mask = g.input("mask", Shape::new(&[8, 16], f));
24485 let q = g.narrow_(qkv, 2, 0, 64);
24486 let k = g.narrow_(qkv, 2, 64, 64);
24487 let v = g.narrow_(qkv, 2, 128, 64);
24488 let attn = g.attention(q, k, v, mask, 4, 16, Shape::new(&[8, 16, 64], f));
24489 g.set_outputs(vec![attn]);
24490
24491 let plan = rlx_opt::memory::plan_memory(&g);
24492 let arena = crate::arena::Arena::from_plan(plan);
24493 let sched = compile_thunks(&g, &arena);
24494
24495 let mut narrow_count = 0;
24496 let mut attn_strides: Option<(u32, u32, u32)> = None;
24497 for t in &sched.thunks {
24498 match t {
24499 Thunk::Narrow { .. } => narrow_count += 1,
24500 Thunk::Attention {
24501 q_row_stride,
24502 k_row_stride,
24503 v_row_stride,
24504 ..
24505 } => attn_strides = Some((*q_row_stride, *k_row_stride, *v_row_stride)),
24506 _ => {}
24507 }
24508 }
24509 assert_eq!(
24512 narrow_count, 0,
24513 "Narrow×3→Attention fusion should eliminate all 3 narrows; saw {narrow_count}"
24514 );
24515 assert_eq!(
24516 attn_strides,
24517 Some((192, 192, 192)),
24518 "Attention should walk Q/K/V with parent row stride 192"
24519 );
24520 }
24521
24522 #[test]
24528 fn fused_attn_block_respects_causal_mask() {
24529 let f = DType::F32;
24530 let (s, d, nh, dh) = (5usize, 8usize, 2usize, 4usize);
24531 let half = dh / 2;
24532
24533 let mut g = Graph::new("fused_causal");
24534 let hidden = g.input("hidden", Shape::new(&[s, d], f));
24535 let wqkv = g.input("wqkv", Shape::new(&[d, 3 * d], f));
24536 let wo = g.input("wo", Shape::new(&[d, d], f));
24537 let cos = g.input("cos", Shape::new(&[s, half], f));
24538 let sin = g.input("sin", Shape::new(&[s, half], f));
24539 let qkv = g.matmul(hidden, wqkv, Shape::new(&[s, 3 * d], f));
24540 let q = g.narrow_(qkv, 1, 0, d);
24541 let k = g.narrow_(qkv, 1, d, d);
24542 let v = g.narrow_(qkv, 1, 2 * d, d);
24543 let q3 = g.reshape(q, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
24544 let k3 = g.reshape(k, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
24545 let v3 = g.reshape(v, vec![1, s as i64, d as i64], Shape::new(&[1, s, d], f));
24546 let qr = g.rope(q3, cos, sin, dh);
24547 let kr = g.rope(k3, cos, sin, dh);
24548 let attn = g.attention_kind(
24549 qr,
24550 kr,
24551 v3,
24552 nh,
24553 dh,
24554 rlx_ir::op::MaskKind::Causal,
24555 Shape::new(&[1, s, d], f),
24556 );
24557 let a2 = g.reshape(attn, vec![s as i64, d as i64], Shape::new(&[s, d], f));
24558 let out = g.matmul(a2, wo, Shape::new(&[s, d], f));
24559 g.set_outputs(vec![out]);
24560
24561 let plan = rlx_opt::memory::plan_memory(&g);
24563 let arena = crate::arena::Arena::from_plan(plan);
24564 let sched = compile_thunks(&g, &arena);
24565 assert!(
24566 sched.thunks.iter().any(|t| matches!(
24567 t,
24568 Thunk::FusedAttnBlock {
24569 mask_kind: rlx_ir::op::MaskKind::Causal,
24570 ..
24571 }
24572 )),
24573 "expected a FusedAttnBlock carrying MaskKind::Causal"
24574 );
24575
24576 let wqkv_d: Vec<f32> = (0..d * 3 * d)
24577 .map(|i| ((i % 7) as f32 - 3.0) * 0.05)
24578 .collect();
24579 let wo_d: Vec<f32> = (0..d * d).map(|i| ((i % 5) as f32 - 2.0) * 0.05).collect();
24580 let mut cos_d = vec![0f32; s * half];
24581 let mut sin_d = vec![0f32; s * half];
24582 for p in 0..s {
24583 for i in 0..half {
24584 let fr = 1.0f32 / 10000f32.powf(2.0 * i as f32 / dh as f32);
24585 cos_d[p * half + i] = (p as f32 * fr).cos();
24586 sin_d[p * half + i] = (p as f32 * fr).sin();
24587 }
24588 }
24589 let base_h: Vec<f32> = (0..s * d).map(|i| ((i % 11) as f32 - 5.0) * 0.1).collect();
24590 let run = |hin: &[f32]| {
24591 run_graph(
24592 &g,
24593 &[
24594 (hidden, hin),
24595 (wqkv, &wqkv_d),
24596 (wo, &wo_d),
24597 (cos, &cos_d),
24598 (sin, &sin_d),
24599 ],
24600 out,
24601 s * d,
24602 )
24603 };
24604 let a = run(&base_h);
24605 let mut changed = base_h.clone();
24607 for j in 0..d {
24608 changed[4 * d + j] += 1.0;
24609 }
24610 let b = run(&changed);
24611 for pos in 0..4 {
24612 for j in 0..d {
24613 let i = pos * d + j;
24614 assert!(
24615 (a[i] - b[i]).abs() < 1e-5,
24616 "causal leak at pos {pos}: {} vs {}",
24617 a[i],
24618 b[i]
24619 );
24620 }
24621 }
24622 let last: f32 = (0..d).map(|j| (a[4 * d + j] - b[4 * d + j]).abs()).sum();
24623 assert!(last > 1e-4, "last position must react to its own token");
24624 }
24625
24626 fn run_graph(
24637 g: &Graph,
24638 inputs: &[(NodeId, &[f32])],
24639 out_id: NodeId,
24640 out_len: usize,
24641 ) -> Vec<f32> {
24642 let plan = rlx_opt::memory::plan_memory(g);
24643 let mut arena = crate::arena::Arena::from_plan(plan);
24644 let sched = compile_thunks(g, &arena);
24645 for &(id, data) in inputs {
24646 let off = arena.byte_offset(id);
24647 let buf = arena.raw_buf_mut();
24648 unsafe {
24649 let p = buf.as_mut_ptr().add(off) as *mut f32;
24650 for (i, &v) in data.iter().enumerate() {
24651 *p.add(i) = v;
24652 }
24653 }
24654 }
24655 execute_thunks(&sched, arena.raw_buf_mut());
24656 let off = arena.byte_offset(out_id);
24657 unsafe {
24658 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
24659 (0..out_len).map(|i| *p.add(i)).collect()
24660 }
24661 }
24662
24663 #[test]
24664 fn relu_backward_matches_mask() {
24665 let f = DType::F32;
24666 let len = 7usize;
24667 let x: Vec<f32> = vec![-2.0, -0.1, 0.0, 0.1, 1.0, 3.0, -5.0];
24668 let dy: Vec<f32> = vec![0.5, 1.5, 2.5, -0.7, 4.0, -1.0, 9.0];
24669
24670 let mut g = Graph::new("relu_bw");
24671 let xn = g.input("x", Shape::new(&[len], f));
24672 let dyn_ = g.input("dy", Shape::new(&[len], f));
24673 let dx = g.relu_backward(xn, dyn_);
24674 g.set_outputs(vec![dx]);
24675
24676 let actual = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dx, len);
24677 let expected: Vec<f32> = x
24681 .iter()
24682 .zip(&dy)
24683 .map(|(&xi, &dyi)| if xi > 0.0 { dyi } else { 0.0 })
24684 .collect();
24685 for (a, e) in actual.iter().zip(&expected) {
24686 assert!((a - e).abs() < 1e-6, "relu_bw mismatch: {a} vs {e}");
24687 }
24688 }
24689
24690 #[test]
24691 fn maxpool2d_backward_routes_to_argmax() {
24692 let f = DType::F32;
24693 let x: Vec<f32> = vec![
24695 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,
24696 ];
24697 let dy: Vec<f32> = vec![0.5, 1.0, 2.0, 4.0];
24701
24702 let mut g = Graph::new("maxpool_bw");
24703 let xn = g.input("x", Shape::new(&[1, 1, 4, 4], f));
24704 let dyn_ = g.input("dy", Shape::new(&[1, 1, 2, 2], f));
24705 let dx = g.maxpool2d_backward(xn, dyn_, vec![2, 2], vec![2, 2], vec![0, 0]);
24706 g.set_outputs(vec![dx]);
24707
24708 let actual = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dx, 16);
24709 let mut expected = vec![0f32; 16];
24710 expected[5] = 0.5;
24711 expected[7] = 1.0;
24712 expected[13] = 2.0;
24713 expected[15] = 4.0;
24714 for (i, (a, e)) in actual.iter().zip(&expected).enumerate() {
24715 assert!((a - e).abs() < 1e-6, "maxpool_bw[{i}] mismatch: {a} vs {e}");
24716 }
24717 }
24718
24719 #[test]
24720 fn conv2d_backward_input_matches_numerical_gradient() {
24721 use rlx_ir::Philox4x32;
24722 let n = 1usize;
24725 let c_in = 2usize;
24726 let h = 4usize;
24727 let w = 4usize;
24728 let c_out = 3usize;
24729 let kh = 3usize;
24730 let kw = 3usize;
24731 let ph = 1usize;
24732 let pw = 1usize;
24733 let sh = 1usize;
24734 let sw = 1usize;
24735 let h_out = (h + 2 * ph - kh) / sh + 1;
24737 let w_out = (w + 2 * pw - kw) / sw + 1;
24738 assert_eq!(h_out, 4);
24739 assert_eq!(w_out, 4);
24740
24741 let mut rng = Philox4x32::new(7);
24742 let mut x = vec![0f32; n * c_in * h * w];
24743 rng.fill_normal(&mut x);
24744 let mut wt = vec![0f32; c_out * c_in * kh * kw];
24745 rng.fill_normal(&mut wt);
24746 let mut dy = vec![0f32; n * c_out * h_out * w_out];
24747 rng.fill_normal(&mut dy);
24748
24749 let f = DType::F32;
24751 let mut g = Graph::new("conv_bwi");
24752 let dy_in = g.input("dy", Shape::new(&[n, c_out, h_out, w_out], f));
24753 let w_in = g.input("w", Shape::new(&[c_out, c_in, kh, kw], f));
24754 let dx = g.conv2d_backward_input(
24755 dy_in,
24756 w_in,
24757 Shape::new(&[n, c_in, h, w], f),
24758 vec![kh, kw],
24759 vec![sh, sw],
24760 vec![ph, pw],
24761 vec![1, 1],
24762 1,
24763 );
24764 g.set_outputs(vec![dx]);
24765 let analytical = run_graph(&g, &[(dy_in, &dy), (w_in, &wt)], dx, n * c_in * h * w);
24766
24767 let forward = |x: &[f32]| -> Vec<f32> {
24771 let mut out = vec![0f32; n * c_out * h_out * w_out];
24772 for ni in 0..n {
24773 for co in 0..c_out {
24774 for ho in 0..h_out {
24775 for wo in 0..w_out {
24776 let mut acc = 0f32;
24777 for ci in 0..c_in {
24778 for ki in 0..kh {
24779 for kj in 0..kw {
24780 let hi = ho * sh + ki;
24781 let wi = wo * sw + kj;
24782 if hi < ph || wi < pw {
24783 continue;
24784 }
24785 let hi = hi - ph;
24786 let wi = wi - pw;
24787 if hi >= h || wi >= w {
24788 continue;
24789 }
24790 let xv = x[((ni * c_in) + ci) * h * w + hi * w + wi];
24791 let wv = wt[((co * c_in) + ci) * kh * kw + ki * kw + kj];
24792 acc += xv * wv;
24793 }
24794 }
24795 }
24796 out[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = acc;
24797 }
24798 }
24799 }
24800 }
24801 out
24802 };
24803 let dot = |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(&u, &v)| u * v).sum() };
24804 let eps = 1e-3f32;
24805 let mut numerical = vec![0f32; x.len()];
24806 for i in 0..x.len() {
24807 let saved = x[i];
24808 x[i] = saved + eps;
24809 let plus = dot(&forward(&x), &dy);
24810 x[i] = saved - eps;
24811 let minus = dot(&forward(&x), &dy);
24812 x[i] = saved;
24813 numerical[i] = (plus - minus) / (2.0 * eps);
24814 }
24815 for (i, (a, n)) in analytical.iter().zip(&numerical).enumerate() {
24816 assert!(
24818 (a - n).abs() < 5e-3,
24819 "conv_bw_input[{i}]: analytical {a} vs numerical {n}"
24820 );
24821 }
24822 }
24823
24824 #[test]
24825 fn conv2d_backward_weight_matches_numerical_gradient() {
24826 use rlx_ir::Philox4x32;
24827 let n = 2usize;
24828 let c_in = 2usize;
24829 let h = 4usize;
24830 let w = 4usize;
24831 let c_out = 2usize;
24832 let kh = 3usize;
24833 let kw = 3usize;
24834 let ph = 0usize;
24835 let pw = 0usize;
24836 let sh = 1usize;
24837 let sw = 1usize;
24838 let h_out = (h + 2 * ph - kh) / sh + 1;
24839 let w_out = (w + 2 * pw - kw) / sw + 1;
24840
24841 let mut rng = Philox4x32::new(11);
24842 let mut x = vec![0f32; n * c_in * h * w];
24843 rng.fill_normal(&mut x);
24844 let mut wt = vec![0f32; c_out * c_in * kh * kw];
24845 rng.fill_normal(&mut wt);
24846 let mut dy = vec![0f32; n * c_out * h_out * w_out];
24847 rng.fill_normal(&mut dy);
24848
24849 let f = DType::F32;
24850 let mut g = Graph::new("conv_bww");
24851 let xn = g.input("x", Shape::new(&[n, c_in, h, w], f));
24852 let dyn_ = g.input("dy", Shape::new(&[n, c_out, h_out, w_out], f));
24853 let dwn = g.conv2d_backward_weight(
24854 xn,
24855 dyn_,
24856 Shape::new(&[c_out, c_in, kh, kw], f),
24857 vec![kh, kw],
24858 vec![sh, sw],
24859 vec![ph, pw],
24860 vec![1, 1],
24861 1,
24862 );
24863 g.set_outputs(vec![dwn]);
24864 let analytical = run_graph(&g, &[(xn, &x), (dyn_, &dy)], dwn, c_out * c_in * kh * kw);
24865
24866 let forward = |wt: &[f32]| -> Vec<f32> {
24867 let mut out = vec![0f32; n * c_out * h_out * w_out];
24868 for ni in 0..n {
24869 for co in 0..c_out {
24870 for ho in 0..h_out {
24871 for wo in 0..w_out {
24872 let mut acc = 0f32;
24873 for ci in 0..c_in {
24874 for ki in 0..kh {
24875 for kj in 0..kw {
24876 let hi = ho + ki;
24877 let wi = wo + kj;
24878 let xv = x[((ni * c_in) + ci) * h * w + hi * w + wi];
24879 let wv = wt[((co * c_in) + ci) * kh * kw + ki * kw + kj];
24880 acc += xv * wv;
24881 }
24882 }
24883 }
24884 out[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = acc;
24885 }
24886 }
24887 }
24888 }
24889 out
24890 };
24891 let dot = |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(&u, &v)| u * v).sum() };
24892 let eps = 1e-3f32;
24893 let mut numerical = vec![0f32; wt.len()];
24894 for i in 0..wt.len() {
24895 let saved = wt[i];
24896 wt[i] = saved + eps;
24897 let plus = dot(&forward(&wt), &dy);
24898 wt[i] = saved - eps;
24899 let minus = dot(&forward(&wt), &dy);
24900 wt[i] = saved;
24901 numerical[i] = (plus - minus) / (2.0 * eps);
24902 }
24903 for (i, (a, n)) in analytical.iter().zip(&numerical).enumerate() {
24904 assert!(
24905 (a - n).abs() < 5e-3,
24906 "conv_bw_weight[{i}]: analytical {a} vs numerical {n}"
24907 );
24908 }
24909 }
24910
24911 #[test]
24912 fn softmax_cross_entropy_matches_reference() {
24913 let f = DType::F32;
24914 let logits: Vec<f32> = vec![
24915 1.0, 2.0, 3.0, -1.0, 0.0, 4.0, 5.0, 5.0, 5.0, ];
24919 let labels: Vec<f32> = vec![2.0, 0.0, 1.0];
24920
24921 let mut g = Graph::new("sce");
24922 let lg = g.input("logits", Shape::new(&[3, 3], f));
24923 let lb = g.input("labels", Shape::new(&[3], f));
24924 let loss = g.softmax_cross_entropy_with_logits(lg, lb);
24925 g.set_outputs(vec![loss]);
24926 let actual = run_graph(&g, &[(lg, &logits), (lb, &labels)], loss, 3);
24927
24928 let mut expected = vec![0f32; 3];
24930 for ni in 0..3 {
24931 let row = &logits[ni * 3..(ni + 1) * 3];
24932 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
24933 let sum: f32 = row.iter().map(|&v| (v - m).exp()).sum();
24934 let lse = m + sum.ln();
24935 let label_idx = labels[ni] as usize;
24936 expected[ni] = lse - row[label_idx];
24937 }
24938 for (i, (a, e)) in actual.iter().zip(&expected).enumerate() {
24939 assert!((a - e).abs() < 1e-5, "sce loss[{i}]: {a} vs {e}");
24940 }
24941 }
24942
24943 #[test]
24944 fn softmax_cross_entropy_backward_matches_numerical_gradient() {
24945 use rlx_ir::Philox4x32;
24946 let n = 4usize;
24947 let c = 5usize;
24948 let mut rng = Philox4x32::new(23);
24949 let mut logits = vec![0f32; n * c];
24950 rng.fill_normal(&mut logits);
24951 let labels: Vec<f32> = (0..n).map(|i| (i % c) as f32).collect();
24952 let mut d_loss = vec![0f32; n];
24953 rng.fill_normal(&mut d_loss);
24954
24955 let f = DType::F32;
24956 let mut g = Graph::new("sce_bw");
24957 let lg = g.input("logits", Shape::new(&[n, c], f));
24958 let lb = g.input("labels", Shape::new(&[n], f));
24959 let dl = g.input("d_loss", Shape::new(&[n], f));
24960 let dlogits = g.softmax_cross_entropy_backward(lg, lb, dl);
24961 g.set_outputs(vec![dlogits]);
24962 let analytical = run_graph(
24963 &g,
24964 &[(lg, &logits), (lb, &labels), (dl, &d_loss)],
24965 dlogits,
24966 n * c,
24967 );
24968
24969 let sce_loss = |logits: &[f32]| -> Vec<f32> {
24971 let mut out = vec![0f32; n];
24972 for ni in 0..n {
24973 let row = &logits[ni * c..(ni + 1) * c];
24974 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
24975 let sum: f32 = row.iter().map(|&v| (v - m).exp()).sum();
24976 out[ni] = (m + sum.ln()) - row[labels[ni] as usize];
24977 }
24978 out
24979 };
24980 let dot = |a: &[f32], b: &[f32]| a.iter().zip(b).map(|(&u, &v)| u * v).sum::<f32>();
24981 let eps = 1e-3f32;
24982 let mut numerical = vec![0f32; logits.len()];
24983 for i in 0..logits.len() {
24984 let saved = logits[i];
24985 logits[i] = saved + eps;
24986 let plus = dot(&sce_loss(&logits), &d_loss);
24987 logits[i] = saved - eps;
24988 let minus = dot(&sce_loss(&logits), &d_loss);
24989 logits[i] = saved;
24990 numerical[i] = (plus - minus) / (2.0 * eps);
24991 }
24992 for (i, (a, num)) in analytical.iter().zip(&numerical).enumerate() {
24993 assert!(
24994 (a - num).abs() < 5e-3,
24995 "sce_bw[{i}]: analytical {a} vs numerical {num}"
24996 );
24997 }
24998 }
24999
25000 fn fill_constants_into_arena(graph: &Graph, arena: &mut crate::arena::Arena) {
25013 for node in graph.nodes() {
25014 if let Op::Constant { data } = &node.op
25015 && arena.has_buffer(node.id)
25016 && !data.is_empty()
25017 {
25018 let buf = arena.slice_mut(node.id);
25019 let n_floats = data.len() / 4;
25020 let n = buf.len().min(n_floats);
25021 for i in 0..n {
25022 let bytes = [
25023 data[i * 4],
25024 data[i * 4 + 1],
25025 data[i * 4 + 2],
25026 data[i * 4 + 3],
25027 ];
25028 buf[i] = f32::from_le_bytes(bytes);
25029 }
25030 }
25031 }
25032 }
25033
25034 fn prepare(
25038 graph: &Graph,
25039 seed_inputs: &[(NodeId, &[f32])],
25040 ) -> (ThunkSchedule, crate::arena::Arena) {
25041 let plan = rlx_opt::memory::plan_memory(graph);
25042 let mut arena = crate::arena::Arena::from_plan(plan);
25043 let sched = compile_thunks(graph, &arena);
25044 fill_constants_into_arena(graph, &mut arena);
25045 for &(id, data) in seed_inputs {
25046 let off = arena.byte_offset(id);
25047 let buf = arena.raw_buf_mut();
25048 unsafe {
25049 let p = buf.as_mut_ptr().add(off) as *mut f32;
25050 for (i, &v) in data.iter().enumerate() {
25051 *p.add(i) = v;
25052 }
25053 }
25054 }
25055 (sched, arena)
25056 }
25057
25058 fn read_arena(arena: &crate::arena::Arena, id: NodeId, len: usize) -> Vec<f32> {
25059 let off = arena.byte_offset(id);
25060 unsafe {
25061 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
25062 (0..len).map(|i| *p.add(i)).collect()
25063 }
25064 }
25065
25066 fn write_arena(arena: &mut crate::arena::Arena, id: NodeId, data: &[f32]) {
25067 let off = arena.byte_offset(id);
25068 let buf = arena.raw_buf_mut();
25069 unsafe {
25070 let p = buf.as_mut_ptr().add(off) as *mut f32;
25071 for (i, &v) in data.iter().enumerate() {
25072 *p.add(i) = v;
25073 }
25074 }
25075 }
25076
25077 fn prepare_f64(
25079 graph: &Graph,
25080 seed_inputs: &[(NodeId, &[f64])],
25081 ) -> (ThunkSchedule, crate::arena::Arena) {
25082 let plan = rlx_opt::memory::plan_memory(graph);
25083 let mut arena = crate::arena::Arena::from_plan(plan);
25084 let sched = compile_thunks(graph, &arena);
25085 fill_constants_into_arena(graph, &mut arena);
25086 for &(id, data) in seed_inputs {
25087 let off = arena.byte_offset(id);
25088 let buf = arena.raw_buf_mut();
25089 unsafe {
25090 let p = buf.as_mut_ptr().add(off) as *mut f64;
25091 for (i, &v) in data.iter().enumerate() {
25092 *p.add(i) = v;
25093 }
25094 }
25095 }
25096 (sched, arena)
25097 }
25098
25099 fn read_arena_f64(arena: &crate::arena::Arena, id: NodeId, len: usize) -> Vec<f64> {
25100 let off = arena.byte_offset(id);
25101 unsafe {
25102 let p = arena.raw_buf().as_ptr().add(off) as *const f64;
25103 (0..len).map(|i| *p.add(i)).collect()
25104 }
25105 }
25106
25107 #[test]
25117 fn dense_solve_f64_end_to_end() {
25118 let mut g = Graph::new("solve_e2e");
25119 let a = g.input("A", Shape::new(&[2, 2], DType::F64));
25120 let b = g.input("b", Shape::new(&[2], DType::F64));
25121 let x = g.dense_solve(a, b, Shape::new(&[2], DType::F64));
25122 g.set_outputs(vec![x]);
25123
25124 let a_data = [2.0, 1.0, 1.0, 3.0_f64];
25125 let b_data = [5.0, 10.0_f64];
25126 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
25127 execute_thunks(&sched, arena.raw_buf_mut());
25128
25129 let got = read_arena_f64(&arena, x, 2);
25130 let want = [1.0, 3.0_f64];
25131 for i in 0..2 {
25132 assert!(
25133 (got[i] - want[i]).abs() < 1e-12,
25134 "x[{i}] = {} (expected {})",
25135 got[i],
25136 want[i]
25137 );
25138 }
25139 }
25140
25141 #[test]
25147 fn dense_solve_f64_5x5_laplacian() {
25148 let n = 5usize;
25149 let mut g = Graph::new("solve_5x5");
25150 let a = g.input("A", Shape::new(&[n, n], DType::F64));
25151 let b = g.input("b", Shape::new(&[n], DType::F64));
25152 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
25153 g.set_outputs(vec![x]);
25154
25155 let mut a_data = vec![0.0_f64; n * n];
25157 for i in 0..n {
25158 a_data[i * n + i] = 2.0;
25159 if i > 0 {
25160 a_data[i * n + (i - 1)] = -1.0;
25161 }
25162 if i + 1 < n {
25163 a_data[i * n + (i + 1)] = -1.0;
25164 }
25165 }
25166 let b_data: Vec<f64> = (0..n).map(|i| (i + 1) as f64).collect();
25167 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
25168 execute_thunks(&sched, arena.raw_buf_mut());
25169
25170 let got = read_arena_f64(&arena, x, n);
25171 let mut residual = vec![0.0_f64; n];
25173 for i in 0..n {
25174 for j in 0..n {
25175 residual[i] += a_data[i * n + j] * got[j];
25176 }
25177 }
25178 for i in 0..n {
25179 assert!(
25180 (residual[i] - b_data[i]).abs() < 1e-10,
25181 "row {i}: residual {} vs b {}",
25182 residual[i],
25183 b_data[i]
25184 );
25185 }
25186 }
25187
25188 #[test]
25207 fn hello_resistor_gradient_end_to_end() {
25208 use rlx_opt::autodiff::grad_with_loss;
25209 let n = 3usize;
25210
25211 let mut g = Graph::new("hello_resistor");
25213 let a = g.param("A", Shape::new(&[n, n], DType::F64));
25214 let b = g.input("b", Shape::new(&[n], DType::F64));
25215 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
25216 let loss = g.reduce(
25217 x,
25218 ReduceOp::Sum,
25219 vec![0],
25220 false,
25221 Shape::new(&[1], DType::F64),
25222 );
25223 g.set_outputs(vec![loss]);
25224
25225 let bwd = grad_with_loss(&g, &[a, b]);
25227 assert_eq!(bwd.outputs.len(), 3, "expect [loss, dA, db]");
25228
25229 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
25233 for node in graph.nodes() {
25234 let name = match &node.op {
25235 rlx_ir::Op::Input { name } => Some(name.as_str()),
25236 rlx_ir::Op::Param { name } => Some(name.as_str()),
25237 _ => None,
25238 };
25239 if name == Some(want) {
25240 return node.id;
25241 }
25242 }
25243 panic!("no node named {want:?} in bwd graph");
25244 };
25245 let a_bwd = find_by_name(&bwd, "A");
25246 let b_bwd = find_by_name(&bwd, "b");
25247 let d_out_bwd = find_by_name(&bwd, "d_output");
25248
25249 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
25253 let b_data = [1.0, 2.0, 3.0_f64];
25254 let d_output = [1.0_f64]; let (sched, mut arena) = prepare_f64(
25258 &bwd,
25259 &[(a_bwd, &a_data), (b_bwd, &b_data), (d_out_bwd, &d_output)],
25260 );
25261 execute_thunks(&sched, arena.raw_buf_mut());
25262
25263 let loss_out = read_arena_f64(&arena, bwd.outputs[0], 1);
25264 let da_out = read_arena_f64(&arena, bwd.outputs[1], n * n);
25265 let db_out = read_arena_f64(&arena, bwd.outputs[2], n);
25266
25267 let x_ref = {
25270 let mut a = a_data;
25271 let mut b = b_data;
25272 let info = crate::blas::dgesv(&mut a, &mut b, n, 1);
25273 assert_eq!(info, 0);
25274 b
25275 };
25276 let loss_ref: f64 = x_ref.iter().sum();
25277 let db_ref = {
25279 let mut at = [0.0_f64; 9];
25280 for i in 0..n {
25281 for j in 0..n {
25282 at[i * n + j] = a_data[j * n + i];
25283 }
25284 }
25285 let mut ones = [1.0_f64; 3];
25286 let info = crate::blas::dgesv(&mut at, &mut ones, n, 1);
25287 assert_eq!(info, 0);
25288 ones
25289 };
25290 let mut da_ref = [0.0_f64; 9];
25292 for i in 0..n {
25293 for j in 0..n {
25294 da_ref[i * n + j] = -db_ref[i] * x_ref[j];
25295 }
25296 }
25297
25298 assert!(
25300 (loss_out[0] - loss_ref).abs() < 1e-10,
25301 "loss: got {}, want {}",
25302 loss_out[0],
25303 loss_ref
25304 );
25305 for i in 0..n {
25306 assert!(
25307 (db_out[i] - db_ref[i]).abs() < 1e-10,
25308 "db[{i}]: got {}, want {}",
25309 db_out[i],
25310 db_ref[i]
25311 );
25312 }
25313 for i in 0..n * n {
25314 assert!(
25315 (da_out[i] - da_ref[i]).abs() < 1e-10,
25316 "dA[{i}]: got {}, want {}",
25317 da_out[i],
25318 da_ref[i]
25319 );
25320 }
25321
25322 let h = 1e-6_f64;
25325 for k in 0..n {
25326 let mut bp = b_data;
25327 bp[k] += h;
25328 let mut bm = b_data;
25329 bm[k] -= h;
25330 let lp = {
25331 let mut ac = a_data;
25332 let info = crate::blas::dgesv(&mut ac, &mut bp, n, 1);
25333 assert_eq!(info, 0);
25334 bp.iter().sum::<f64>()
25335 };
25336 let lm = {
25337 let mut ac = a_data;
25338 let info = crate::blas::dgesv(&mut ac, &mut bm, n, 1);
25339 assert_eq!(info, 0);
25340 bm.iter().sum::<f64>()
25341 };
25342 let fd = (lp - lm) / (2.0 * h);
25343 assert!(
25344 (db_out[k] - fd).abs() < 1e-7,
25345 "FD mismatch on db[{k}]: AD={} FD={}",
25346 db_out[k],
25347 fd
25348 );
25349 }
25350 }
25351
25352 #[test]
25357 fn scan_geometric_growth_f64() {
25358 let n = 3usize;
25359 let length = 10u32;
25360
25361 let mut body = Graph::new("scan_body");
25363 let x = body.input("carry", Shape::new(&[n], DType::F64));
25364 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 0.1_f64.to_le_bytes()).collect();
25365 let scale = body.add_node(
25366 Op::Constant { data: scale_bytes },
25367 vec![],
25368 Shape::new(&[n], DType::F64),
25369 );
25370 let scaled = body.binary(BinaryOp::Mul, x, scale, Shape::new(&[n], DType::F64));
25371 let next = body.binary(BinaryOp::Add, x, scaled, Shape::new(&[n], DType::F64));
25372 body.set_outputs(vec![next]);
25373
25374 let mut g = Graph::new("scan_outer");
25376 let init = g.input("init", Shape::new(&[n], DType::F64));
25377 let final_carry = g.scan(init, body, length);
25378 g.set_outputs(vec![final_carry]);
25379
25380 let init_data = vec![1.0_f64; n];
25381 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
25382 execute_thunks(&sched, arena.raw_buf_mut());
25383 let got = read_arena_f64(&arena, final_carry, n);
25384 let want: f64 = 1.1_f64.powi(length as i32);
25385 for i in 0..n {
25386 assert!(
25387 (got[i] - want).abs() < 1e-12,
25388 "got[{i}] = {} want {}",
25389 got[i],
25390 want
25391 );
25392 }
25393 }
25394
25395 #[test]
25402 fn scan_with_xs_cumulative_sum() {
25403 let n = 3usize;
25404 let length = 4u32;
25405
25406 let mut body = Graph::new("cumsum_body");
25407 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25409 let x_t = body.input("x_t", Shape::new(&[n], DType::F64));
25410 let next = body.binary(BinaryOp::Add, carry, x_t, Shape::new(&[n], DType::F64));
25411 body.set_outputs(vec![next]);
25412
25413 let mut g = Graph::new("cumsum_outer");
25414 let init = g.input("init", Shape::new(&[n], DType::F64));
25415 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
25416 let final_carry = g.scan_with_xs(init, &[xs], body, length);
25417 g.set_outputs(vec![final_carry]);
25418
25419 let init_data = vec![0.0_f64; n];
25420 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)]);
25422 execute_thunks(&sched, arena.raw_buf_mut());
25423 let got = read_arena_f64(&arena, final_carry, n);
25424
25425 let mut want = init_data.clone();
25429 for t in 0..length as usize {
25430 for j in 0..n {
25431 want[j] += xs_data[t * n + j];
25432 }
25433 }
25434 for i in 0..n {
25435 assert!(
25436 (got[i] - want[i]).abs() < 1e-12,
25437 "got[{i}] = {} want {}",
25438 got[i],
25439 want[i]
25440 );
25441 }
25442 }
25443
25444 #[test]
25448 fn scan_with_xs_be_with_drive() {
25449 let n = 3usize;
25450 let length = 4u32;
25451 let dt = 0.1_f64;
25452
25453 let mut m_data = vec![0.0_f64; n * n];
25454 for i in 0..n {
25455 m_data[i * n + i] = 1.0 + dt * 2.0;
25456 if i > 0 {
25457 m_data[i * n + (i - 1)] = -dt;
25458 }
25459 if i + 1 < n {
25460 m_data[i * n + (i + 1)] = -dt;
25461 }
25462 }
25463 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
25464
25465 let mut body = Graph::new("be_drive_body");
25466 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25467 let drive = body.input("drive", Shape::new(&[n], DType::F64));
25468 let m = body.add_node(
25469 Op::Constant { data: m_bytes },
25470 vec![],
25471 Shape::new(&[n, n], DType::F64),
25472 );
25473 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
25474 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
25475 body.set_outputs(vec![next]);
25476
25477 let mut g = Graph::new("be_drive_outer");
25478 let init = g.input("init", Shape::new(&[n], DType::F64));
25479 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
25480 let final_carry = g.scan_with_xs(init, &[xs], body, length);
25481 g.set_outputs(vec![final_carry]);
25482
25483 let init_data = vec![0.0_f64; n];
25484 let mut xs_data = vec![0.0_f64; length as usize * n];
25487 xs_data[0] = 1.0;
25488
25489 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data), (xs, &xs_data)]);
25490 execute_thunks(&sched, arena.raw_buf_mut());
25491 let got = read_arena_f64(&arena, final_carry, n);
25492
25493 let mut x = init_data.clone();
25495 for t in 0..length as usize {
25496 for j in 0..n {
25497 x[j] += xs_data[t * n + j];
25498 }
25499 let mut a_copy = m_data.clone();
25500 crate::blas::dgesv(&mut a_copy, &mut x, n, 1);
25501 }
25502 for i in 0..n {
25503 assert!(
25504 (got[i] - x[i]).abs() < 1e-12,
25505 "got[{i}] = {} ref {}",
25506 got[i],
25507 x[i]
25508 );
25509 }
25510 }
25511
25512 #[test]
25518 fn batched_dense_solve_gradient_matches_per_batch_analytic() {
25519 use rlx_opt::autodiff::grad_with_loss;
25520 let n = 3usize;
25521 let batch = 4usize;
25522
25523 let mut g = Graph::new("bds_grad");
25524 let a = g.param("A", Shape::new(&[batch, n, n], DType::F64));
25525 let b = g.input("b", Shape::new(&[batch, n], DType::F64));
25526 let x = g.batched_dense_solve(a, b, Shape::new(&[batch, n], DType::F64));
25527 let loss = g.reduce(
25528 x,
25529 ReduceOp::Sum,
25530 vec![0, 1],
25531 false,
25532 Shape::new(&[1], DType::F64),
25533 );
25534 g.set_outputs(vec![loss]);
25535
25536 let bwd = grad_with_loss(&g, &[a, b]);
25537
25538 let find = |graph: &Graph, want: &str| -> NodeId {
25539 for node in graph.nodes() {
25540 let name = match &node.op {
25541 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
25542 _ => None,
25543 };
25544 if name == Some(want) {
25545 return node.id;
25546 }
25547 }
25548 panic!("no node named {want}");
25549 };
25550 let a_id = find(&bwd, "A");
25551 let b_id = find(&bwd, "b");
25552 let d_out_id = find(&bwd, "d_output");
25553
25554 let mut rng = rlx_ir::Philox4x32::new(0x57e1_u64);
25555 let mut a_data = vec![0.0_f64; batch * n * n];
25556 let mut b_data = vec![0.0_f64; batch * n];
25557 for bi in 0..batch {
25558 for i in 0..n {
25559 for j in 0..n {
25560 a_data[bi * n * n + i * n + j] = rng.next_f32() as f64 * 0.1;
25561 }
25562 a_data[bi * n * n + i * n + i] += 1.0 + n as f64;
25563 }
25564 for i in 0..n {
25565 b_data[bi * n + i] = rng.next_f32() as f64;
25566 }
25567 }
25568 let d_seed = [1.0_f64];
25569
25570 let (sched, mut arena) = prepare_f64(
25571 &bwd,
25572 &[(a_id, &a_data), (b_id, &b_data), (d_out_id, &d_seed)],
25573 );
25574 execute_thunks(&sched, arena.raw_buf_mut());
25575 let da_out = read_arena_f64(&arena, bwd.outputs[1], batch * n * n);
25576 let db_out = read_arena_f64(&arena, bwd.outputs[2], batch * n);
25577
25578 for bi in 0..batch {
25581 let a_slice: Vec<f64> = a_data[bi * n * n..(bi + 1) * n * n].to_vec();
25582 let mut b_slice: Vec<f64> = b_data[bi * n..(bi + 1) * n].to_vec();
25583 let mut a_copy = a_slice.clone();
25584 crate::blas::dgesv(&mut a_copy, &mut b_slice, n, 1);
25585 let x_ref = b_slice.clone();
25586 let mut at = vec![0.0_f64; n * n];
25588 for i in 0..n {
25589 for j in 0..n {
25590 at[i * n + j] = a_slice[j * n + i];
25591 }
25592 }
25593 let mut ones = vec![1.0_f64; n];
25594 crate::blas::dgesv(&mut at, &mut ones, n, 1);
25595 let db_ref = ones;
25596 for i in 0..n {
25597 let got = db_out[bi * n + i];
25598 assert!(
25599 (got - db_ref[i]).abs() < 1e-10,
25600 "batch {bi}, db[{i}]: got {got} ref {}",
25601 db_ref[i]
25602 );
25603 }
25604 for i in 0..n {
25606 for j in 0..n {
25607 let got = da_out[bi * n * n + i * n + j];
25608 let want = -db_ref[i] * x_ref[j];
25609 assert!(
25610 (got - want).abs() < 1e-10,
25611 "batch {bi}, dA[{i},{j}]: got {got} ref {want}"
25612 );
25613 }
25614 }
25615 }
25616 }
25617
25618 #[test]
25623 fn scan_checkpointed_grad_matches_plain_scan_grad() {
25624 use rlx_opt::autodiff::grad_with_loss;
25625 let n = 2usize;
25626 let length = 6u32;
25627
25628 let make_body = || {
25629 let mut body = Graph::new("ck_body");
25630 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25631 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 1.05_f64.to_le_bytes()).collect();
25632 let scale = body.add_node(
25633 Op::Constant { data: scale_bytes },
25634 vec![],
25635 Shape::new(&[n], DType::F64),
25636 );
25637 let next = body.binary(BinaryOp::Mul, carry, scale, Shape::new(&[n], DType::F64));
25638 body.set_outputs(vec![next]);
25639 body
25640 };
25641
25642 let mut g_plain = Graph::new("ck_plain");
25644 let init_p = g_plain.input("init", Shape::new(&[n], DType::F64));
25645 let final_p = g_plain.scan(init_p, make_body(), length);
25646 let loss_p = g_plain.reduce(
25647 final_p,
25648 ReduceOp::Sum,
25649 vec![0],
25650 false,
25651 Shape::new(&[1], DType::F64),
25652 );
25653 g_plain.set_outputs(vec![loss_p]);
25654 let bwd_p = grad_with_loss(&g_plain, &[init_p]);
25655
25656 let mut g_ck = Graph::new("ck_ckpt");
25658 let init_c = g_ck.input("init", Shape::new(&[n], DType::F64));
25659 let final_c = g_ck.scan_checkpointed(init_c, make_body(), length, 2);
25660 let loss_c = g_ck.reduce(
25661 final_c,
25662 ReduceOp::Sum,
25663 vec![0],
25664 false,
25665 Shape::new(&[1], DType::F64),
25666 );
25667 g_ck.set_outputs(vec![loss_c]);
25668 let bwd_c = grad_with_loss(&g_ck, &[init_c]);
25669
25670 let find = |graph: &Graph, want: &str| -> NodeId {
25671 for node in graph.nodes() {
25672 let name = match &node.op {
25673 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
25674 _ => None,
25675 };
25676 if name == Some(want) {
25677 return node.id;
25678 }
25679 }
25680 panic!("no {want}");
25681 };
25682
25683 let init_data = vec![0.5_f64, -0.5];
25684 let d_seed = [1.0_f64];
25685
25686 let (s_p, mut a_p) = prepare_f64(
25687 &bwd_p,
25688 &[
25689 (find(&bwd_p, "init"), &init_data),
25690 (find(&bwd_p, "d_output"), &d_seed),
25691 ],
25692 );
25693 execute_thunks(&s_p, a_p.raw_buf_mut());
25694 let dinit_p = read_arena_f64(&a_p, bwd_p.outputs[1], n);
25695
25696 let (s_c, mut a_c) = prepare_f64(
25697 &bwd_c,
25698 &[
25699 (find(&bwd_c, "init"), &init_data),
25700 (find(&bwd_c, "d_output"), &d_seed),
25701 ],
25702 );
25703 execute_thunks(&s_c, a_c.raw_buf_mut());
25704 let dinit_c = read_arena_f64(&a_c, bwd_c.outputs[1], n);
25705
25706 for i in 0..n {
25707 assert!(
25708 (dinit_p[i] - dinit_c[i]).abs() < 1e-12,
25709 "dinit[{i}]: plain={} checkpointed={}",
25710 dinit_p[i],
25711 dinit_c[i]
25712 );
25713 }
25714 }
25715
25716 #[test]
25722 fn recursive_checkpointing_matches_full_trajectory() {
25723 let n = 2usize;
25724 let length = 4u32;
25725
25726 let build_body = || -> Graph {
25728 let mut body = Graph::new("rc_body");
25729 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25730 let ones_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
25731 let ones = body.add_node(
25732 Op::Constant { data: ones_bytes },
25733 vec![],
25734 Shape::new(&[n], DType::F64),
25735 );
25736 let next = body.binary(BinaryOp::Add, carry, ones, Shape::new(&[n], DType::F64));
25737 body.set_outputs(vec![next]);
25738 body
25739 };
25740
25741 let body_vjp_for = || -> Graph {
25744 use rlx_opt::autodiff::grad;
25745 let body = build_body();
25746 let carry_id = body
25748 .nodes()
25749 .iter()
25750 .find(|n| matches!(n.op, Op::Input { .. }))
25751 .map(|n| n.id)
25752 .unwrap();
25753 grad(&body, &[carry_id])
25754 };
25755
25756 let mut g_full = Graph::new("rc_outer_full");
25758 let init_full = g_full.input("init", Shape::new(&[n], DType::F64));
25759 let traj_full_id = g_full.scan_trajectory(init_full, build_body(), length);
25760 let upstream_full = g_full.input("upstream", Shape::new(&[length as usize, n], DType::F64));
25762 let dinit_full_id = g_full.scan_backward(
25763 init_full,
25764 traj_full_id,
25765 upstream_full,
25766 &[],
25767 body_vjp_for(),
25768 length,
25769 true,
25770 Shape::new(&[n], DType::F64),
25771 );
25772 g_full.set_outputs(vec![dinit_full_id]);
25773
25774 let k = 2u32;
25777 let mut g_rec = Graph::new("rc_outer_rec");
25778 let init_rec = g_rec.input("init", Shape::new(&[n], DType::F64));
25779 let traj_rec_id = g_rec.add_node(
25780 Op::Scan {
25781 body: Box::new(build_body()),
25782 length,
25783 save_trajectory: true,
25784 num_bcast: 0,
25785 num_xs: 0,
25786 num_checkpoints: k,
25787 },
25788 vec![init_rec],
25789 Shape::new(&[k as usize, n], DType::F64),
25790 );
25791 let upstream_rec = g_rec.input("upstream", Shape::new(&[length as usize, n], DType::F64));
25794 let dinit_rec_id = g_rec.add_node(
25795 Op::ScanBackward {
25796 body_vjp: Box::new(body_vjp_for()),
25797 length,
25798 save_trajectory: true,
25799 num_xs: 0,
25800 num_checkpoints: k,
25801 forward_body: Some(Box::new(build_body())),
25802 },
25803 vec![init_rec, traj_rec_id, upstream_rec],
25804 Shape::new(&[n], DType::F64),
25805 );
25806 g_rec.set_outputs(vec![dinit_rec_id]);
25807
25808 let init_data = vec![0.5_f64, -0.5];
25810 let upstream_data: Vec<f64> = (0..length as usize * n).map(|i| (i as f64) * 0.1).collect();
25811
25812 let find = |graph: &Graph, want: &str| -> NodeId {
25813 for node in graph.nodes() {
25814 if let Op::Input { name } = &node.op
25815 && name == want
25816 {
25817 return node.id;
25818 }
25819 }
25820 panic!("no input {want}");
25821 };
25822
25823 let (s_full, mut a_full) = prepare_f64(
25824 &g_full,
25825 &[
25826 (find(&g_full, "init"), &init_data),
25827 (find(&g_full, "upstream"), &upstream_data),
25828 ],
25829 );
25830 execute_thunks(&s_full, a_full.raw_buf_mut());
25831 let dinit_full = read_arena_f64(&a_full, g_full.outputs[0], n);
25832
25833 let (s_rec, mut a_rec) = prepare_f64(
25834 &g_rec,
25835 &[
25836 (find(&g_rec, "init"), &init_data),
25837 (find(&g_rec, "upstream"), &upstream_data),
25838 ],
25839 );
25840 execute_thunks(&s_rec, a_rec.raw_buf_mut());
25841 let dinit_rec = read_arena_f64(&a_rec, g_rec.outputs[0], n);
25842
25843 for i in 0..n {
25844 assert!(
25845 (dinit_full[i] - dinit_rec[i]).abs() < 1e-12,
25846 "i={i}: full={} rec={}",
25847 dinit_full[i],
25848 dinit_rec[i]
25849 );
25850 }
25851 }
25852
25853 #[test]
25862 fn vmap_of_grad_scan_matches_per_row_runs() {
25863 use rlx_opt::autodiff::grad_with_loss;
25864 use rlx_opt::vmap::vmap;
25865 let n = 2usize;
25866 let length = 3u32;
25867 let batch = 3usize;
25868
25869 let mut body = Graph::new("scan_grad_body");
25870 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25871 let ones_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
25872 let ones = body.add_node(
25873 Op::Constant { data: ones_bytes },
25874 vec![],
25875 Shape::new(&[n], DType::F64),
25876 );
25877 let next = body.binary(BinaryOp::Add, carry, ones, Shape::new(&[n], DType::F64));
25878 body.set_outputs(vec![next]);
25879
25880 let mut g = Graph::new("scan_grad_outer");
25881 let init = g.input("init", Shape::new(&[n], DType::F64));
25882 let final_x = g.scan(init, body, length);
25883 let loss = g.reduce(
25884 final_x,
25885 ReduceOp::Sum,
25886 vec![0],
25887 false,
25888 Shape::new(&[1], DType::F64),
25889 );
25890 g.set_outputs(vec![loss]);
25891
25892 let bwd = grad_with_loss(&g, &[init]);
25893 let bg = vmap(&bwd, &["init"], batch);
25894
25895 let find = |graph: &Graph, want: &str| -> NodeId {
25896 for node in graph.nodes() {
25897 let name = match &node.op {
25898 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
25899 _ => None,
25900 };
25901 if name == Some(want) {
25902 return node.id;
25903 }
25904 }
25905 panic!("no node named {want}");
25906 };
25907 let init_b = find(&bg, "init");
25908 let d_out_b = find(&bg, "d_output");
25909
25910 let init_data: Vec<f64> = (0..batch * n).map(|i| (i as f64) * 0.5).collect();
25911 let d_seed = [1.0_f64];
25912
25913 let (sched, mut arena) = prepare_f64(&bg, &[(init_b, &init_data), (d_out_b, &d_seed)]);
25914 execute_thunks(&sched, arena.raw_buf_mut());
25915 let dinit_b = read_arena_f64(&arena, bg.outputs[1], batch * n);
25916
25917 for i in 0..batch * n {
25918 assert!(
25919 (dinit_b[i] - 1.0).abs() < 1e-12,
25920 "dinit[{i}] = {} (expected 1.0)",
25921 dinit_b[i]
25922 );
25923 }
25924
25925 for bi in 0..batch {
25927 let row = &init_data[bi * n..(bi + 1) * n];
25928 let mut g2 = Graph::new("per_row_grad");
25929 let init2 = g2.input("init", Shape::new(&[n], DType::F64));
25930 let mut body2 = Graph::new("per_row_body");
25931 let c2 = body2.input("carry", Shape::new(&[n], DType::F64));
25932 let ones2_bytes: Vec<u8> = (0..n).flat_map(|_| 1.0_f64.to_le_bytes()).collect();
25933 let ones2 = body2.add_node(
25934 Op::Constant { data: ones2_bytes },
25935 vec![],
25936 Shape::new(&[n], DType::F64),
25937 );
25938 let next2 = body2.binary(BinaryOp::Add, c2, ones2, Shape::new(&[n], DType::F64));
25939 body2.set_outputs(vec![next2]);
25940 let final2 = g2.scan(init2, body2, length);
25941 let loss2 = g2.reduce(
25942 final2,
25943 ReduceOp::Sum,
25944 vec![0],
25945 false,
25946 Shape::new(&[1], DType::F64),
25947 );
25948 g2.set_outputs(vec![loss2]);
25949 let bwd2 = grad_with_loss(&g2, &[init2]);
25950 let init2_id = find(&bwd2, "init");
25951 let d_out2_id = find(&bwd2, "d_output");
25952 let (s2, mut a2) = prepare_f64(&bwd2, &[(init2_id, row), (d_out2_id, &d_seed)]);
25953 execute_thunks(&s2, a2.raw_buf_mut());
25954 let row_dinit = read_arena_f64(&a2, bwd2.outputs[1], n);
25955 for j in 0..n {
25956 let got = dinit_b[bi * n + j];
25957 let want = row_dinit[j];
25958 assert!(
25959 (got - want).abs() < 1e-12,
25960 "row {bi}, j {j}: vmap'd={got} per-row={want}"
25961 );
25962 }
25963 }
25964 }
25965
25966 #[test]
25972 fn vmap_scan_cumulative_sum_matches_scalar_runs() {
25973 use rlx_opt::vmap::vmap;
25974 let n = 2usize;
25975 let length = 4u32;
25976 let batch = 3usize;
25977
25978 let mut body = Graph::new("scan_body_cumsum");
25980 let carry = body.input("carry", Shape::new(&[n], DType::F64));
25981 let x_t = body.input("x_t", Shape::new(&[n], DType::F64));
25982 let next = body.binary(BinaryOp::Add, carry, x_t, Shape::new(&[n], DType::F64));
25983 body.set_outputs(vec![next]);
25984
25985 let mut g = Graph::new("scan_outer_cumsum");
25986 let init = g.input("init", Shape::new(&[n], DType::F64));
25987 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
25988 let final_carry = g.scan_with_xs(init, &[xs], body, length);
25989 g.set_outputs(vec![final_carry]);
25990
25991 let bg = vmap(&g, &["init", "xs"], batch);
25993
25994 let init_data: Vec<f64> = (0..batch * n).map(|i| (i + 1) as f64).collect();
25996 let xs_data: Vec<f64> = (0..batch * length as usize * n)
25999 .map(|i| 0.1 * (i as f64))
26000 .collect();
26001
26002 let find = |graph: &Graph, want: &str| -> NodeId {
26003 for node in graph.nodes() {
26004 if let Op::Input { name } = &node.op
26005 && name == want
26006 {
26007 return node.id;
26008 }
26009 }
26010 panic!("no input {want}");
26011 };
26012 let init_b = find(&bg, "init");
26013 let xs_b = find(&bg, "xs");
26014 let (sched, mut arena) = prepare_f64(&bg, &[(init_b, &init_data), (xs_b, &xs_data)]);
26015 execute_thunks(&sched, arena.raw_buf_mut());
26016 let batched_out = read_arena_f64(&arena, bg.outputs[0], batch * n);
26017
26018 for bi in 0..batch {
26020 let init_slice = &init_data[bi * n..(bi + 1) * n];
26021 let mut x = init_slice.to_vec();
26022 for t in 0..length as usize {
26023 for j in 0..n {
26024 x[j] += xs_data[bi * length as usize * n + t * n + j];
26025 }
26026 }
26027
26028 for i in 0..n {
26029 let got = batched_out[bi * n + i];
26030 assert!(
26031 (got - x[i]).abs() < 1e-12,
26032 "row {bi}, i {i}: got {got} ref {}",
26033 x[i]
26034 );
26035 }
26036 }
26037 }
26038
26039 #[test]
26044 fn vmap_dense_solve_matches_scalar_runs() {
26045 use rlx_opt::vmap::vmap;
26046 let n = 3usize;
26047 let batch = 4usize;
26048
26049 let mut g = Graph::new("solve_forward");
26050 let a = g.input("A", Shape::new(&[n, n], DType::F64));
26051 let b = g.input("b", Shape::new(&[n], DType::F64));
26052 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
26053 g.set_outputs(vec![x]);
26054
26055 let bg = vmap(&g, &["A", "b"], batch);
26057
26058 let mut rng = rlx_ir::Philox4x32::new(0xb47c_u64);
26060 let mut a_data = vec![0.0_f64; batch * n * n];
26061 let mut b_data = vec![0.0_f64; batch * n];
26062 for bi in 0..batch {
26063 for i in 0..n {
26065 for j in 0..n {
26066 a_data[bi * n * n + i * n + j] = rng.next_f32() as f64 * 0.1;
26067 }
26068 a_data[bi * n * n + i * n + i] += 1.0 + n as f64;
26069 }
26070 for i in 0..n {
26071 b_data[bi * n + i] = rng.next_f32() as f64;
26072 }
26073 }
26074
26075 let find = |graph: &Graph, want: &str| -> NodeId {
26076 for node in graph.nodes() {
26077 if let Op::Input { name } = &node.op
26078 && name == want
26079 {
26080 return node.id;
26081 }
26082 }
26083 panic!("no input named {want}");
26084 };
26085 let ba = find(&bg, "A");
26086 let bb = find(&bg, "b");
26087 let (sched, mut arena) = prepare_f64(&bg, &[(ba, &a_data), (bb, &b_data)]);
26088 execute_thunks(&sched, arena.raw_buf_mut());
26089 let batched_x = read_arena_f64(&arena, bg.outputs[0], batch * n);
26090
26091 for bi in 0..batch {
26093 let mut a_slice: Vec<f64> = a_data[bi * n * n..(bi + 1) * n * n].to_vec();
26094 let mut b_slice: Vec<f64> = b_data[bi * n..(bi + 1) * n].to_vec();
26095 crate::blas::dgesv(&mut a_slice, &mut b_slice, n, 1);
26096 for i in 0..n {
26097 let got = batched_x[bi * n + i];
26098 let want = b_slice[i];
26099 assert!(
26100 (got - want).abs() < 1e-12,
26101 "row {bi}, i {i}: got {got} want {want}"
26102 );
26103 }
26104 }
26105 }
26106
26107 #[test]
26114 fn vmap_matmul_add_reduce_matches_scalar_runs() {
26115 use rlx_opt::vmap::vmap;
26116 let n = 3usize;
26117 let batch = 4usize;
26118
26119 let mut g = Graph::new("vmap_e2e_forward");
26121 let x = g.input("x", Shape::new(&[n], DType::F64));
26122 let w = g.input("w", Shape::new(&[n, n], DType::F64));
26123 let b = g.input("b", Shape::new(&[n], DType::F64));
26124 let x_row = g.add_node(
26125 Op::Reshape {
26126 new_shape: vec![1, n as i64],
26127 },
26128 vec![x],
26129 Shape::new(&[1, n], DType::F64),
26130 );
26131 let mm = g.matmul(x_row, w, Shape::new(&[1, n], DType::F64));
26132 let mm_flat = g.add_node(
26133 Op::Reshape {
26134 new_shape: vec![n as i64],
26135 },
26136 vec![mm],
26137 Shape::new(&[n], DType::F64),
26138 );
26139 let yv = g.binary(BinaryOp::Add, mm_flat, b, Shape::new(&[n], DType::F64));
26140 let loss = g.reduce(
26141 yv,
26142 ReduceOp::Sum,
26143 vec![0],
26144 false,
26145 Shape::new(&[1], DType::F64),
26146 );
26147 g.set_outputs(vec![loss]);
26148
26149 let bg = vmap(&g, &["x"], batch);
26151
26152 let mut rng = rlx_ir::Philox4x32::new(0xc1c0_u64);
26154 let n_w = n * n;
26155 let w_data: Vec<f64> = (0..n_w).map(|_| rng.next_f32() as f64).collect();
26156 let b_data: Vec<f64> = (0..n).map(|_| rng.next_f32() as f64).collect();
26157 let mut x_data_batched: Vec<f64> = Vec::with_capacity(batch * n);
26158 for _ in 0..batch * n {
26159 x_data_batched.push(rng.next_f32() as f64);
26160 }
26161
26162 let find = |graph: &Graph, want: &str| -> NodeId {
26164 for node in graph.nodes() {
26165 if let Op::Input { name } = &node.op
26166 && name == want
26167 {
26168 return node.id;
26169 }
26170 }
26171 panic!("no input named {want}");
26172 };
26173 let bx = find(&bg, "x");
26174 let bw = find(&bg, "w");
26175 let bb = find(&bg, "b");
26176 let (sched, mut arena) =
26177 prepare_f64(&bg, &[(bx, &x_data_batched), (bw, &w_data), (bb, &b_data)]);
26178 execute_thunks(&sched, arena.raw_buf_mut());
26179 let batched_out = read_arena_f64(&arena, bg.outputs[0], batch);
26185
26186 for bi in 0..batch {
26188 let xs_slice = &x_data_batched[bi * n..(bi + 1) * n];
26189 let mut g2 = Graph::new("scalar_run");
26190 let x2 = g2.input("x", Shape::new(&[n], DType::F64));
26191 let w2 = g2.input("w", Shape::new(&[n, n], DType::F64));
26192 let b2 = g2.input("b", Shape::new(&[n], DType::F64));
26193 let xr = g2.add_node(
26194 Op::Reshape {
26195 new_shape: vec![1, n as i64],
26196 },
26197 vec![x2],
26198 Shape::new(&[1, n], DType::F64),
26199 );
26200 let m = g2.matmul(xr, w2, Shape::new(&[1, n], DType::F64));
26201 let mf = g2.add_node(
26202 Op::Reshape {
26203 new_shape: vec![n as i64],
26204 },
26205 vec![m],
26206 Shape::new(&[n], DType::F64),
26207 );
26208 let yv2 = g2.binary(BinaryOp::Add, mf, b2, Shape::new(&[n], DType::F64));
26209 let l2 = g2.reduce(
26210 yv2,
26211 ReduceOp::Sum,
26212 vec![0],
26213 false,
26214 Shape::new(&[1], DType::F64),
26215 );
26216 g2.set_outputs(vec![l2]);
26217 let (s2, mut a2) = prepare_f64(&g2, &[(x2, xs_slice), (w2, &w_data), (b2, &b_data)]);
26218 execute_thunks(&s2, a2.raw_buf_mut());
26219 let scalar_out = read_arena_f64(&a2, l2, 1);
26220 assert!(
26221 (batched_out[bi] - scalar_out[0]).abs() < 1e-12,
26222 "row {bi}: batched={} scalar={}",
26223 batched_out[bi],
26224 scalar_out[0]
26225 );
26226 }
26227 }
26228
26229 #[test]
26236 fn scan_with_xs_dxs_matches_fd() {
26237 use rlx_opt::autodiff::grad_with_loss;
26238 let n = 3usize;
26239 let length = 3u32;
26240 let dt = 0.1_f64;
26241
26242 let mut m_data = vec![0.0_f64; n * n];
26243 for i in 0..n {
26244 m_data[i * n + i] = 1.0 + dt * 2.0;
26245 if i > 0 {
26246 m_data[i * n + (i - 1)] = -dt;
26247 }
26248 if i + 1 < n {
26249 m_data[i * n + (i + 1)] = -dt;
26250 }
26251 }
26252 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26253
26254 let mut body = Graph::new("be_dxs_body");
26255 let carry = body.input("carry", Shape::new(&[n], DType::F64));
26256 let drive = body.input("drive", Shape::new(&[n], DType::F64));
26257 let m = body.add_node(
26258 Op::Constant { data: m_bytes },
26259 vec![],
26260 Shape::new(&[n, n], DType::F64),
26261 );
26262 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
26263 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
26264 body.set_outputs(vec![next]);
26265
26266 let mut g = Graph::new("be_dxs_outer");
26267 let init = g.input("init", Shape::new(&[n], DType::F64));
26268 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
26269 let final_carry = g.scan_with_xs(init, &[xs], body, length);
26270 let loss = g.reduce(
26271 final_carry,
26272 ReduceOp::Sum,
26273 vec![0],
26274 false,
26275 Shape::new(&[1], DType::F64),
26276 );
26277 g.set_outputs(vec![loss]);
26278
26279 let bwd = grad_with_loss(&g, &[init, xs]);
26281 assert_eq!(bwd.outputs.len(), 3, "[loss, dinit, dxs]");
26282
26283 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26284 for node in graph.nodes() {
26285 let name = match &node.op {
26286 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26287 _ => None,
26288 };
26289 if name == Some(want) {
26290 return node.id;
26291 }
26292 }
26293 panic!("no node named {want:?}");
26294 };
26295 let init_bwd = find_by_name(&bwd, "init");
26296 let xs_bwd = find_by_name(&bwd, "xs");
26297 let d_out_bwd = find_by_name(&bwd, "d_output");
26298
26299 let init_data = vec![0.5_f64, 0.0, -0.5];
26300 let xs_data: Vec<f64> = (0..length as usize * n)
26301 .map(|i| 0.1_f64 * ((i as f64) - 4.0))
26302 .collect();
26303 let d_seed = [1.0_f64];
26304
26305 let (sched, mut arena) = prepare_f64(
26306 &bwd,
26307 &[
26308 (init_bwd, &init_data),
26309 (xs_bwd, &xs_data),
26310 (d_out_bwd, &d_seed),
26311 ],
26312 );
26313 execute_thunks(&sched, arena.raw_buf_mut());
26314 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
26315 let dxs = read_arena_f64(&arena, bwd.outputs[2], length as usize * n);
26316
26317 let h = 1e-6;
26318 let loss_at = |x0: &[f64], xs_in: &[f64]| -> f64 {
26319 let mut acc = x0.to_vec();
26320 for t in 0..length as usize {
26321 for j in 0..n {
26322 acc[j] += xs_in[t * n + j];
26323 }
26324 let mut a_copy = m_data.clone();
26325 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
26326 }
26327 acc.iter().sum()
26328 };
26329
26330 for i in 0..n {
26332 let mut ip = init_data.to_vec();
26333 ip[i] += h;
26334 let mut im = init_data.to_vec();
26335 im[i] -= h;
26336 let fd = (loss_at(&ip, &xs_data) - loss_at(&im, &xs_data)) / (2.0 * h);
26337 assert!(
26338 (dinit[i] - fd).abs() < 1e-7,
26339 "FD dinit[{i}]: AD={} FD={}",
26340 dinit[i],
26341 fd
26342 );
26343 }
26344
26345 for t in 0..length as usize {
26347 for j in 0..n {
26348 let idx = t * n + j;
26349 let mut xp = xs_data.clone();
26350 xp[idx] += h;
26351 let mut xm = xs_data.clone();
26352 xm[idx] -= h;
26353 let fd = (loss_at(&init_data, &xp) - loss_at(&init_data, &xm)) / (2.0 * h);
26354 assert!(
26355 (dxs[idx] - fd).abs() < 1e-7,
26356 "FD dxs[t={t},j={j}]: AD={} FD={}",
26357 dxs[idx],
26358 fd
26359 );
26360 }
26361 }
26362 }
26363
26364 #[test]
26372 fn scan_with_xs_gradient_dinit_matches_fd() {
26373 use rlx_opt::autodiff::grad_with_loss;
26374 let n = 3usize;
26375 let length = 3u32;
26376 let dt = 0.1_f64;
26377
26378 let mut m_data = vec![0.0_f64; n * n];
26379 for i in 0..n {
26380 m_data[i * n + i] = 1.0 + dt * 2.0;
26381 if i > 0 {
26382 m_data[i * n + (i - 1)] = -dt;
26383 }
26384 if i + 1 < n {
26385 m_data[i * n + (i + 1)] = -dt;
26386 }
26387 }
26388 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26389
26390 let mut body = Graph::new("be_xs_grad_body");
26391 let carry = body.input("carry", Shape::new(&[n], DType::F64));
26392 let drive = body.input("drive", Shape::new(&[n], DType::F64));
26393 let m = body.add_node(
26394 Op::Constant { data: m_bytes },
26395 vec![],
26396 Shape::new(&[n, n], DType::F64),
26397 );
26398 let driven = body.binary(BinaryOp::Add, carry, drive, Shape::new(&[n], DType::F64));
26399 let next = body.dense_solve(m, driven, Shape::new(&[n], DType::F64));
26400 body.set_outputs(vec![next]);
26401
26402 let mut g = Graph::new("be_xs_grad_outer");
26403 let init = g.input("init", Shape::new(&[n], DType::F64));
26404 let xs = g.input("xs", Shape::new(&[length as usize, n], DType::F64));
26405 let final_carry = g.scan_with_xs(init, &[xs], body, length);
26406 let loss = g.reduce(
26407 final_carry,
26408 ReduceOp::Sum,
26409 vec![0],
26410 false,
26411 Shape::new(&[1], DType::F64),
26412 );
26413 g.set_outputs(vec![loss]);
26414
26415 let bwd = grad_with_loss(&g, &[init]);
26416
26417 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26418 for node in graph.nodes() {
26419 let name = match &node.op {
26420 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26421 _ => None,
26422 };
26423 if name == Some(want) {
26424 return node.id;
26425 }
26426 }
26427 panic!("no node named {want:?}");
26428 };
26429 let init_bwd = find_by_name(&bwd, "init");
26430 let xs_bwd = find_by_name(&bwd, "xs");
26431 let d_out_bwd = find_by_name(&bwd, "d_output");
26432
26433 let init_data = vec![0.5_f64, 0.0, -0.5];
26434 let xs_data: Vec<f64> = (0..length as usize * n)
26436 .map(|i| 0.1_f64 * ((i as f64) - 4.0))
26437 .collect();
26438 let d_seed = [1.0_f64];
26439
26440 let (sched, mut arena) = prepare_f64(
26441 &bwd,
26442 &[
26443 (init_bwd, &init_data),
26444 (xs_bwd, &xs_data),
26445 (d_out_bwd, &d_seed),
26446 ],
26447 );
26448 execute_thunks(&sched, arena.raw_buf_mut());
26449 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
26450
26451 let h = 1e-6;
26452 let loss_at = |x0: &[f64]| -> f64 {
26453 let mut acc = x0.to_vec();
26454 for t in 0..length as usize {
26455 for j in 0..n {
26456 acc[j] += xs_data[t * n + j];
26457 }
26458 let mut a_copy = m_data.clone();
26459 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
26460 }
26461 acc.iter().sum()
26462 };
26463 for i in 0..n {
26464 let mut ip = init_data.to_vec();
26465 ip[i] += h;
26466 let mut im = init_data.to_vec();
26467 im[i] -= h;
26468 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
26469 assert!(
26470 (dinit[i] - fd).abs() < 1e-7,
26471 "FD dinit[{i}]: AD={} FD={}",
26472 dinit[i],
26473 fd
26474 );
26475 }
26476 }
26477
26478 #[test]
26486 fn scan_gradient_geometric_matches_closed_form() {
26487 use rlx_opt::autodiff::grad_with_loss;
26488 let n = 3usize;
26489 let length = 5u32;
26490
26491 let mut body = Graph::new("scan_grad_body");
26492 let x = body.input("carry", Shape::new(&[n], DType::F64));
26493 let scale_bytes: Vec<u8> = (0..n).flat_map(|_| 1.1_f64.to_le_bytes()).collect();
26494 let scale = body.add_node(
26495 Op::Constant { data: scale_bytes },
26496 vec![],
26497 Shape::new(&[n], DType::F64),
26498 );
26499 let next = body.binary(BinaryOp::Mul, x, scale, Shape::new(&[n], DType::F64));
26500 body.set_outputs(vec![next]);
26501
26502 let mut g = Graph::new("scan_grad_outer");
26503 let init = g.input("init", Shape::new(&[n], DType::F64));
26504 let final_x = g.scan(init, body, length);
26505 let loss = g.reduce(
26506 final_x,
26507 ReduceOp::Sum,
26508 vec![0],
26509 false,
26510 Shape::new(&[1], DType::F64),
26511 );
26512 g.set_outputs(vec![loss]);
26513
26514 let bwd = grad_with_loss(&g, &[init]);
26515 assert_eq!(bwd.outputs.len(), 2);
26516
26517 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26518 for node in graph.nodes() {
26519 let name = match &node.op {
26520 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26521 _ => None,
26522 };
26523 if name == Some(want) {
26524 return node.id;
26525 }
26526 }
26527 panic!("no node named {want:?}");
26528 };
26529 let init_bwd = find_by_name(&bwd, "init");
26530 let d_out_bwd = find_by_name(&bwd, "d_output");
26531
26532 let init_data = vec![1.0_f64; n];
26533 let d_seed = [1.0_f64];
26534 let (sched, mut arena) = prepare_f64(&bwd, &[(init_bwd, &init_data), (d_out_bwd, &d_seed)]);
26535 execute_thunks(&sched, arena.raw_buf_mut());
26536 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
26537
26538 let want = 1.1_f64.powi(length as i32);
26539 for i in 0..n {
26540 assert!(
26541 (dinit[i] - want).abs() < 1e-12,
26542 "dinit[{i}] = {} want {}",
26543 dinit[i],
26544 want
26545 );
26546 }
26547
26548 let h = 1e-6;
26550 let loss_at = |x: &[f64]| -> f64 {
26551 let mut acc = x.to_vec();
26552 for _ in 0..length {
26553 for v in acc.iter_mut() {
26554 *v *= 1.1;
26555 }
26556 }
26557 acc.iter().sum()
26558 };
26559 let mut ip = init_data.clone();
26560 ip[0] += h;
26561 let mut im = init_data.clone();
26562 im[0] -= h;
26563 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
26564 assert!(
26565 (dinit[0] - fd).abs() < 1e-7,
26566 "FD dinit[0]: AD={} FD={}",
26567 dinit[0],
26568 fd
26569 );
26570 }
26571
26572 #[test]
26575 fn scan_gradient_backward_euler_matches_fd() {
26576 use rlx_opt::autodiff::grad_with_loss;
26577 let n = 4usize;
26578 let length = 3u32;
26579 let dt = 0.05_f64;
26580
26581 let mut m_data = vec![0.0_f64; n * n];
26582 for i in 0..n {
26583 m_data[i * n + i] = 1.0 + dt * 2.0;
26584 if i > 0 {
26585 m_data[i * n + (i - 1)] = -dt;
26586 }
26587 if i + 1 < n {
26588 m_data[i * n + (i + 1)] = -dt;
26589 }
26590 }
26591 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26592
26593 let mut body = Graph::new("be_grad_body");
26594 let x = body.input("x", Shape::new(&[n], DType::F64));
26595 let m = body.add_node(
26596 Op::Constant { data: m_bytes },
26597 vec![],
26598 Shape::new(&[n, n], DType::F64),
26599 );
26600 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
26601 body.set_outputs(vec![next]);
26602
26603 let mut g = Graph::new("be_grad_outer");
26604 let init = g.input("x0", Shape::new(&[n], DType::F64));
26605 let final_x = g.scan(init, body, length);
26606 let loss = g.reduce(
26607 final_x,
26608 ReduceOp::Sum,
26609 vec![0],
26610 false,
26611 Shape::new(&[1], DType::F64),
26612 );
26613 g.set_outputs(vec![loss]);
26614
26615 let bwd = grad_with_loss(&g, &[init]);
26616
26617 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26618 for node in graph.nodes() {
26619 let name = match &node.op {
26620 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26621 _ => None,
26622 };
26623 if name == Some(want) {
26624 return node.id;
26625 }
26626 }
26627 panic!("no node named {want:?}");
26628 };
26629 let init_bwd = find_by_name(&bwd, "x0");
26630 let d_out_bwd = find_by_name(&bwd, "d_output");
26631
26632 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
26633 let d_seed = [1.0_f64];
26634 let (sched, mut arena) = prepare_f64(&bwd, &[(init_bwd, &init_data), (d_out_bwd, &d_seed)]);
26635 execute_thunks(&sched, arena.raw_buf_mut());
26636 let dinit = read_arena_f64(&arena, bwd.outputs[1], n);
26637
26638 let h = 1e-6;
26639 let loss_at = |x0: &[f64]| -> f64 {
26640 let mut acc = x0.to_vec();
26641 for _ in 0..length {
26642 let mut a_copy = m_data.clone();
26643 crate::blas::dgesv(&mut a_copy, &mut acc, n, 1);
26644 }
26645 acc.iter().sum()
26646 };
26647 for i in 0..n {
26648 let mut ip = init_data.to_vec();
26649 ip[i] += h;
26650 let mut im = init_data.to_vec();
26651 im[i] -= h;
26652 let fd = (loss_at(&ip) - loss_at(&im)) / (2.0 * h);
26653 assert!(
26654 (dinit[i] - fd).abs() < 1e-7,
26655 "FD dinit[{i}]: AD={} FD={}",
26656 dinit[i],
26657 fd
26658 );
26659 }
26660 }
26661
26662 #[test]
26668 fn scan_trajectory_backward_euler_records_waveform() {
26669 let n = 4usize;
26670 let length = 5u32;
26671 let dt = 0.05_f64;
26672
26673 let mut m_data = vec![0.0_f64; n * n];
26674 for i in 0..n {
26675 m_data[i * n + i] = 1.0 + dt * 2.0;
26676 if i > 0 {
26677 m_data[i * n + (i - 1)] = -dt;
26678 }
26679 if i + 1 < n {
26680 m_data[i * n + (i + 1)] = -dt;
26681 }
26682 }
26683 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26684
26685 let mut body = Graph::new("be_traj_body");
26686 let x = body.input("x", Shape::new(&[n], DType::F64));
26687 let m = body.add_node(
26688 Op::Constant { data: m_bytes },
26689 vec![],
26690 Shape::new(&[n, n], DType::F64),
26691 );
26692 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
26693 body.set_outputs(vec![next]);
26694
26695 let mut g = Graph::new("be_traj_outer");
26696 let init = g.input("x0", Shape::new(&[n], DType::F64));
26697 let traj = g.scan_trajectory(init, body, length);
26698 g.set_outputs(vec![traj]);
26699
26700 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
26701 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
26702 execute_thunks(&sched, arena.raw_buf_mut());
26703 let got = read_arena_f64(&arena, traj, length as usize * n);
26704
26705 let mut want = Vec::<f64>::with_capacity(length as usize * n);
26707 let mut x_ref = init_data.to_vec();
26708 for _ in 0..length {
26709 let mut a_copy = m_data.clone();
26710 crate::blas::dgesv(&mut a_copy, &mut x_ref, n, 1);
26711 want.extend_from_slice(&x_ref);
26712 }
26713 for i in 0..length as usize * n {
26714 assert!(
26715 (got[i] - want[i]).abs() < 1e-12,
26716 "got[{i}] = {} ref {}",
26717 got[i],
26718 want[i]
26719 );
26720 }
26721
26722 for t in 1..length as usize {
26725 let prev: f64 = got[(t - 1) * n..t * n].iter().sum();
26726 let curr: f64 = got[t * n..(t + 1) * n].iter().sum();
26727 assert!(
26728 curr <= prev + 1e-15,
26729 "mass should decay: row {} sum {prev}, row {t} sum {curr}",
26730 t - 1
26731 );
26732 }
26733
26734 let mut body2 = Graph::new("be_final_body");
26738 let x2 = body2.input("x", Shape::new(&[n], DType::F64));
26739 let m_bytes2: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26740 let m2 = body2.add_node(
26741 Op::Constant { data: m_bytes2 },
26742 vec![],
26743 Shape::new(&[n, n], DType::F64),
26744 );
26745 let next2 = body2.dense_solve(m2, x2, Shape::new(&[n], DType::F64));
26746 body2.set_outputs(vec![next2]);
26747
26748 let mut g2 = Graph::new("be_final_outer");
26749 let init2 = g2.input("x0", Shape::new(&[n], DType::F64));
26750 let final_x = g2.scan(init2, body2, length);
26751 g2.set_outputs(vec![final_x]);
26752 let (sched2, mut arena2) = prepare_f64(&g2, &[(init2, &init_data)]);
26753 execute_thunks(&sched2, arena2.raw_buf_mut());
26754 let final_got = read_arena_f64(&arena2, final_x, n);
26755
26756 let last_row = &got[(length as usize - 1) * n..length as usize * n];
26757 for i in 0..n {
26758 assert!(
26759 (last_row[i] - final_got[i]).abs() < 1e-15,
26760 "last trajectory row[{i}] = {} vs final-scan = {}",
26761 last_row[i],
26762 final_got[i]
26763 );
26764 }
26765 }
26766
26767 #[test]
26773 fn scan_backward_euler_heat_f64() {
26774 let n = 4usize;
26775 let length = 5u32;
26776 let dt = 0.05_f64;
26777
26778 let mut m_data = vec![0.0_f64; n * n];
26781 for i in 0..n {
26782 m_data[i * n + i] = 1.0 + dt * 2.0;
26783 if i > 0 {
26784 m_data[i * n + (i - 1)] = -dt;
26785 }
26786 if i + 1 < n {
26787 m_data[i * n + (i + 1)] = -dt;
26788 }
26789 }
26790 let m_bytes: Vec<u8> = m_data.iter().flat_map(|x| x.to_le_bytes()).collect();
26791
26792 let mut body = Graph::new("be_body");
26793 let x = body.input("x", Shape::new(&[n], DType::F64));
26794 let m = body.add_node(
26795 Op::Constant { data: m_bytes },
26796 vec![],
26797 Shape::new(&[n, n], DType::F64),
26798 );
26799 let next = body.dense_solve(m, x, Shape::new(&[n], DType::F64));
26800 body.set_outputs(vec![next]);
26801
26802 let mut g = Graph::new("be_outer");
26803 let init = g.input("x0", Shape::new(&[n], DType::F64));
26804 let final_x = g.scan(init, body, length);
26805 g.set_outputs(vec![final_x]);
26806
26807 let init_data: [f64; 4] = [0.0, 1.0, 0.0, 0.0];
26809 let (sched, mut arena) = prepare_f64(&g, &[(init, &init_data)]);
26810 execute_thunks(&sched, arena.raw_buf_mut());
26811 let got = read_arena_f64(&arena, final_x, n);
26812
26813 let mut ref_x = init_data.to_vec();
26815 for _ in 0..length {
26816 let mut a_copy = m_data.clone();
26817 crate::blas::dgesv(&mut a_copy, &mut ref_x, n, 1);
26818 }
26819 for i in 0..n {
26820 assert!(
26821 (got[i] - ref_x[i]).abs() < 1e-12,
26822 "got[{i}] = {} ref {}",
26823 got[i],
26824 ref_x[i]
26825 );
26826 }
26827 let mass: f64 = got.iter().sum();
26832 assert!(mass > 0.0 && mass < 1.0, "diffusion mass: {mass}");
26833 }
26834
26835 #[test]
26839 fn dense_solve_f64_multi_rhs_forward() {
26840 let n = 3usize;
26841 let k = 2usize;
26842 let mut g = Graph::new("solve_multi_rhs");
26843 let a = g.input("A", Shape::new(&[n, n], DType::F64));
26844 let b = g.input("B", Shape::new(&[n, k], DType::F64));
26845 let x = g.dense_solve(a, b, Shape::new(&[n, k], DType::F64));
26846 g.set_outputs(vec![x]);
26847
26848 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
26849 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
26850 let (sched, mut arena) = prepare_f64(&g, &[(a, &a_data), (b, &b_data)]);
26851 execute_thunks(&sched, arena.raw_buf_mut());
26852 let x_got = read_arena_f64(&arena, x, n * k);
26853 for c in 0..k {
26854 for i in 0..n {
26855 let mut acc = 0.0_f64;
26856 for j in 0..n {
26857 acc += a_data[i * n + j] * x_got[j * k + c];
26858 }
26859 let want = b_data[i * k + c];
26860 assert!(
26861 (acc - want).abs() < 1e-10,
26862 "col {c} row {i}: got {acc} want {want}"
26863 );
26864 }
26865 }
26866 }
26867
26868 #[test]
26871 fn dense_solve_f64_multi_rhs_gradient() {
26872 use rlx_opt::autodiff::grad_with_loss;
26873 let n = 3usize;
26874 let k = 2usize;
26875 let mut g = Graph::new("solve_mrhs_grad");
26876 let a = g.param("A", Shape::new(&[n, n], DType::F64));
26877 let b = g.input("B", Shape::new(&[n, k], DType::F64));
26878 let x = g.dense_solve(a, b, Shape::new(&[n, k], DType::F64));
26879 let loss = g.reduce(
26880 x,
26881 ReduceOp::Sum,
26882 vec![0, 1],
26883 false,
26884 Shape::new(&[1], DType::F64),
26885 );
26886 g.set_outputs(vec![loss]);
26887
26888 let bwd = grad_with_loss(&g, &[a, b]);
26889 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
26890 for node in graph.nodes() {
26891 let name = match &node.op {
26892 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
26893 _ => None,
26894 };
26895 if name == Some(want) {
26896 return node.id;
26897 }
26898 }
26899 panic!("no node named {want:?}");
26900 };
26901 let a_bwd = find_by_name(&bwd, "A");
26902 let b_bwd = find_by_name(&bwd, "B");
26903 let d_out = find_by_name(&bwd, "d_output");
26904
26905 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
26906 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
26907 let d_seed = [1.0_f64];
26908
26909 let (sched, mut arena) = prepare_f64(
26910 &bwd,
26911 &[(a_bwd, &a_data), (b_bwd, &b_data), (d_out, &d_seed)],
26912 );
26913 execute_thunks(&sched, arena.raw_buf_mut());
26914 let da_got = read_arena_f64(&arena, bwd.outputs[1], n * n);
26915 let db_got = read_arena_f64(&arena, bwd.outputs[2], n * k);
26916
26917 let mut x_ref = b_data;
26919 {
26920 let mut a_copy = a_data;
26921 crate::blas::dgesv(&mut a_copy, &mut x_ref, n, k);
26922 }
26923 let mut at = [0.0_f64; 9];
26924 for i in 0..n {
26925 for j in 0..n {
26926 at[i * n + j] = a_data[j * n + i];
26927 }
26928 }
26929 let mut ones_nk = vec![1.0_f64; n * k];
26930 crate::blas::dgesv(&mut at, &mut ones_nk, n, k);
26931 let db_ref = ones_nk;
26932 let mut da_ref = [0.0_f64; 9];
26933 for i in 0..n {
26934 for j in 0..n {
26935 let mut acc = 0.0_f64;
26936 for c in 0..k {
26937 acc += db_ref[i * k + c] * x_ref[j * k + c];
26938 }
26939 da_ref[i * n + j] = -acc;
26940 }
26941 }
26942 for i in 0..n * k {
26943 assert!(
26944 (db_got[i] - db_ref[i]).abs() < 1e-10,
26945 "dB[{i}]: got {} want {}",
26946 db_got[i],
26947 db_ref[i]
26948 );
26949 }
26950 for i in 0..n * n {
26951 assert!(
26952 (da_got[i] - da_ref[i]).abs() < 1e-10,
26953 "dA[{i}]: got {} want {}",
26954 da_got[i],
26955 da_ref[i]
26956 );
26957 }
26958
26959 let h = 1e-6;
26961 let mut bp = b_data;
26962 bp[0] += h;
26963 let mut bm = b_data;
26964 bm[0] -= h;
26965 let xp = {
26966 let mut a_copy = a_data;
26967 crate::blas::dgesv(&mut a_copy, &mut bp, n, k);
26968 bp
26969 };
26970 let xm = {
26971 let mut a_copy = a_data;
26972 crate::blas::dgesv(&mut a_copy, &mut bm, n, k);
26973 bm
26974 };
26975 let lp: f64 = xp.iter().sum();
26976 let lm: f64 = xm.iter().sum();
26977 let fd = (lp - lm) / (2.0 * h);
26978 assert!(
26979 (db_got[0] - fd).abs() < 1e-7,
26980 "FD dB[0,0]: AD={} FD={}",
26981 db_got[0],
26982 fd
26983 );
26984 }
26985
26986 #[test]
26988 fn dense_solve_f64_multi_rhs_jvp() {
26989 use rlx_opt::autodiff_fwd::jvp;
26990 let n = 3usize;
26991 let k = 2usize;
26992 let mut g = Graph::new("solve_mrhs_jvp");
26993 let a = g.input("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 g.set_outputs(vec![x]);
26997
26998 let jg = jvp(&g, &[b]);
26999 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
27000 for node in graph.nodes() {
27001 let name = match &node.op {
27002 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
27003 _ => None,
27004 };
27005 if name == Some(want) {
27006 return node.id;
27007 }
27008 }
27009 panic!("no node named {want:?}");
27010 };
27011 let a_id = find_by_name(&jg, "A");
27012 let b_id = find_by_name(&jg, "B");
27013 let tb_id = find_by_name(&jg, "tangent_B");
27014
27015 let a_data = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0_f64];
27016 let b_data = [1.0, 4.0, 2.0, -1.0, 3.0, 2.0_f64];
27017 let tb_data = [0.5, 0.0, -0.25, 1.0, 1.0, -0.5_f64];
27018
27019 let (sched, mut arena) =
27020 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (tb_id, &tb_data)]);
27021 execute_thunks(&sched, arena.raw_buf_mut());
27022 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n * k);
27023
27024 let mut a_copy = a_data;
27025 let mut tb_copy = tb_data;
27026 crate::blas::dgesv(&mut a_copy, &mut tb_copy, n, k);
27027 for i in 0..n * k {
27028 assert!(
27029 (tangent_x[i] - tb_copy[i]).abs() < 1e-10,
27030 "t_X[{i}]: AD={} ref={}",
27031 tangent_x[i],
27032 tb_copy[i]
27033 );
27034 }
27035
27036 let h = 1e-6;
27037 let mut bp = b_data;
27038 let mut bm = b_data;
27039 for i in 0..n * k {
27040 bp[i] += h * tb_data[i];
27041 bm[i] -= h * tb_data[i];
27042 }
27043 let xp = {
27044 let mut a_copy = a_data;
27045 crate::blas::dgesv(&mut a_copy, &mut bp, n, k);
27046 bp
27047 };
27048 let xm = {
27049 let mut a_copy = a_data;
27050 crate::blas::dgesv(&mut a_copy, &mut bm, n, k);
27051 bm
27052 };
27053 for i in 0..n * k {
27054 let fd = (xp[i] - xm[i]) / (2.0 * h);
27055 assert!(
27056 (tangent_x[i] - fd).abs() < 1e-7,
27057 "FD t_X[{i}]: AD={} FD={}",
27058 tangent_x[i],
27059 fd
27060 );
27061 }
27062 }
27063
27064 #[test]
27071 fn jvp_dense_solve_b_runs_and_matches_fd() {
27072 use rlx_opt::autodiff_fwd::jvp;
27073 let n = 3usize;
27074
27075 let mut g = Graph::new("jvp_b_e2e");
27077 let a = g.input("A", Shape::new(&[n, n], DType::F64));
27078 let b = g.input("b", Shape::new(&[n], DType::F64));
27079 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
27080 g.set_outputs(vec![x]);
27081
27082 let jg = jvp(&g, &[b]);
27084 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
27086 for node in graph.nodes() {
27087 let name = match &node.op {
27088 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
27089 _ => None,
27090 };
27091 if name == Some(want) {
27092 return node.id;
27093 }
27094 }
27095 panic!("no node named {want:?}");
27096 };
27097 let a_id = find_by_name(&jg, "A");
27098 let b_id = find_by_name(&jg, "b");
27099 let tb_id = find_by_name(&jg, "tangent_b");
27100
27101 let a_data: [f64; 9] = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0];
27102 let b_data: [f64; 3] = [1.0, 2.0, 3.0];
27103 let tb_data: [f64; 3] = [0.5, -0.25, 1.0];
27105
27106 let (sched, mut arena) =
27107 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (tb_id, &tb_data)]);
27108 execute_thunks(&sched, arena.raw_buf_mut());
27109
27110 let primal_x = read_arena_f64(&arena, jg.outputs[0], n);
27112 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n);
27113
27114 let t_x_ref = {
27116 let mut a = a_data;
27117 let mut tb = tb_data;
27118 let info = crate::blas::dgesv(&mut a, &mut tb, n, 1);
27119 assert_eq!(info, 0);
27120 tb
27121 };
27122 for i in 0..n {
27123 assert!(
27124 (tangent_x[i] - t_x_ref[i]).abs() < 1e-10,
27125 "t_x[{i}]: got {} want {}",
27126 tangent_x[i],
27127 t_x_ref[i]
27128 );
27129 }
27130
27131 let h = 1e-6;
27133 let mut bp = b_data;
27134 let mut bm = b_data;
27135 for i in 0..n {
27136 bp[i] += h * tb_data[i];
27137 bm[i] -= h * tb_data[i];
27138 }
27139 let xp = {
27140 let mut a = a_data;
27141 let info = crate::blas::dgesv(&mut a, &mut bp, n, 1);
27142 assert_eq!(info, 0);
27143 bp
27144 };
27145 let xm = {
27146 let mut a = a_data;
27147 let info = crate::blas::dgesv(&mut a, &mut bm, n, 1);
27148 assert_eq!(info, 0);
27149 bm
27150 };
27151 let fd: Vec<f64> = (0..n).map(|i| (xp[i] - xm[i]) / (2.0 * h)).collect();
27152 for i in 0..n {
27153 assert!(
27154 (tangent_x[i] - fd[i]).abs() < 1e-7,
27155 "FD mismatch t_x[{i}]: AD={} FD={}",
27156 tangent_x[i],
27157 fd[i]
27158 );
27159 }
27160 let primal_ref = {
27162 let mut a = a_data;
27163 let mut b = b_data;
27164 crate::blas::dgesv(&mut a, &mut b, n, 1);
27165 b
27166 };
27167 for i in 0..n {
27168 assert!((primal_x[i] - primal_ref[i]).abs() < 1e-10);
27169 }
27170 }
27171
27172 #[test]
27178 fn jvp_dense_solve_a_runs_and_matches_fd() {
27179 use rlx_opt::autodiff_fwd::jvp;
27180 let n = 3usize;
27181
27182 let mut g = Graph::new("jvp_a_e2e");
27183 let a = g.input("A", Shape::new(&[n, n], DType::F64));
27184 let b = g.input("b", Shape::new(&[n], DType::F64));
27185 let x = g.dense_solve(a, b, Shape::new(&[n], DType::F64));
27186 g.set_outputs(vec![x]);
27187
27188 let jg = jvp(&g, &[a]);
27189 let find_by_name = |graph: &Graph, want: &str| -> NodeId {
27190 for node in graph.nodes() {
27191 let name = match &node.op {
27192 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
27193 _ => None,
27194 };
27195 if name == Some(want) {
27196 return node.id;
27197 }
27198 }
27199 panic!("no node named {want:?}");
27200 };
27201 let a_id = find_by_name(&jg, "A");
27202 let b_id = find_by_name(&jg, "b");
27203 let ta_id = find_by_name(&jg, "tangent_A");
27204
27205 let a_data: [f64; 9] = [2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0];
27206 let b_data: [f64; 3] = [1.0, 2.0, 3.0];
27207 let ta_data: [f64; 9] = [0.10, -0.05, 0.02, 0.03, 0.20, -0.04, -0.01, 0.07, 0.15];
27209
27210 let (sched, mut arena) =
27211 prepare_f64(&jg, &[(a_id, &a_data), (b_id, &b_data), (ta_id, &ta_data)]);
27212 execute_thunks(&sched, arena.raw_buf_mut());
27213
27214 let tangent_x = read_arena_f64(&arena, jg.outputs[1], n);
27215
27216 let x_ref = {
27218 let mut a = a_data;
27219 let mut b = b_data;
27220 crate::blas::dgesv(&mut a, &mut b, n, 1);
27221 b
27222 };
27223 let mut prod = [0.0_f64; 3];
27224 for i in 0..n {
27225 for j in 0..n {
27226 prod[i] += ta_data[i * n + j] * x_ref[j];
27227 }
27228 }
27229 let t_x_ref = {
27230 let mut a = a_data;
27231 let mut p = prod;
27232 crate::blas::dgesv(&mut a, &mut p, n, 1);
27233 [-p[0], -p[1], -p[2]]
27234 };
27235 for i in 0..n {
27236 assert!(
27237 (tangent_x[i] - t_x_ref[i]).abs() < 1e-10,
27238 "closed-form t_x[{i}]: AD={} ref={}",
27239 tangent_x[i],
27240 t_x_ref[i]
27241 );
27242 }
27243
27244 let h = 1e-6;
27246 let mut ap = a_data;
27247 let mut am = a_data;
27248 for i in 0..n * n {
27249 ap[i] += h * ta_data[i];
27250 am[i] -= h * ta_data[i];
27251 }
27252 let xp = {
27253 let mut a = ap;
27254 let mut b = b_data;
27255 crate::blas::dgesv(&mut a, &mut b, n, 1);
27256 b
27257 };
27258 let xm = {
27259 let mut a = am;
27260 let mut b = b_data;
27261 crate::blas::dgesv(&mut a, &mut b, n, 1);
27262 b
27263 };
27264 for i in 0..n {
27265 let fd = (xp[i] - xm[i]) / (2.0 * h);
27266 assert!(
27267 (tangent_x[i] - fd).abs() < 1e-7,
27268 "FD t_x[{i}]: AD={} FD={}",
27269 tangent_x[i],
27270 fd
27271 );
27272 }
27273 }
27274
27275 #[test]
27281 fn q_conv2d_matches_reference() {
27282 use rlx_ir::Philox4x32;
27283 let n = 1usize;
27285 let c_in = 2usize;
27286 let h = 5usize;
27287 let w_in = 5usize;
27288 let c_out = 3usize;
27289 let kh = 3usize;
27290 let kw = 3usize;
27291 let ph = 1usize;
27292 let pw = 1usize;
27293 let sh = 1usize;
27294 let sw = 1usize;
27295 let h_out = (h + 2 * ph - kh) / sh + 1;
27296 let w_out = (w_in + 2 * pw - kw) / sw + 1;
27297
27298 let x_scale = 0.04f32;
27299 let w_scale = 0.02f32;
27300 let out_scale = 0.5f32;
27301 let mult = x_scale * w_scale / out_scale;
27302
27303 let mut rng = Philox4x32::new(2099);
27304 let mut xf = vec![0f32; n * c_in * h * w_in];
27305 rng.fill_normal(&mut xf);
27306 let mut wf = vec![0f32; c_out * c_in * kh * kw];
27307 rng.fill_normal(&mut wf);
27308 let xq: Vec<i8> = xf
27309 .iter()
27310 .map(|&v| ((v / x_scale).round() as i32).clamp(-128, 127) as i8)
27311 .collect();
27312 let wq: Vec<i8> = wf
27313 .iter()
27314 .map(|&v| ((v / w_scale).round() as i32).clamp(-128, 127) as i8)
27315 .collect();
27316 let bias: Vec<i32> = vec![0i32; c_out];
27317
27318 let mut g = Graph::new("qconv");
27319 let xn = g.input("x", Shape::new(&[n, c_in, h, w_in], DType::I8));
27320 let wn = g.input("w", Shape::new(&[c_out, c_in, kh, kw], DType::I8));
27321 let bn = g.input("b", Shape::new(&[c_out], DType::I32));
27322 let out = g.q_conv2d(
27323 xn,
27324 wn,
27325 bn,
27326 vec![kh, kw],
27327 vec![sh, sw],
27328 vec![ph, pw],
27329 vec![1, 1],
27330 1,
27331 0,
27332 0,
27333 0,
27334 mult,
27335 Shape::new(&[n, c_out, h_out, w_out], DType::I8),
27336 );
27337 g.set_outputs(vec![out]);
27338
27339 let plan = rlx_opt::memory::plan_memory(&g);
27340 let mut arena = crate::arena::Arena::from_plan(plan);
27341 let sched = compile_thunks(&g, &arena);
27342 let xn_off = arena.byte_offset(xn);
27345 let wn_off = arena.byte_offset(wn);
27346 let bn_off = arena.byte_offset(bn);
27347 let out_off = arena.byte_offset(out);
27348 let buf = arena.raw_buf_mut();
27349 unsafe {
27350 let p = buf.as_mut_ptr().add(xn_off) as *mut i8;
27351 for (i, &v) in xq.iter().enumerate() {
27352 *p.add(i) = v;
27353 }
27354 let p = buf.as_mut_ptr().add(wn_off) as *mut i8;
27355 for (i, &v) in wq.iter().enumerate() {
27356 *p.add(i) = v;
27357 }
27358 let p = buf.as_mut_ptr().add(bn_off) as *mut i32;
27359 for (i, &v) in bias.iter().enumerate() {
27360 *p.add(i) = v;
27361 }
27362 }
27363 execute_thunks(&sched, arena.raw_buf_mut());
27364 let out_q: Vec<i8> = unsafe {
27365 let p = arena.raw_buf().as_ptr().add(out_off) as *const i8;
27366 (0..n * c_out * h_out * w_out).map(|i| *p.add(i)).collect()
27367 };
27368
27369 let mut out_ref = vec![0i8; n * c_out * h_out * w_out];
27371 for ni in 0..n {
27372 for co in 0..c_out {
27373 for ho in 0..h_out {
27374 for wo in 0..w_out {
27375 let mut acc: i32 = 0;
27376 for ci in 0..c_in {
27377 for ki in 0..kh {
27378 for kj in 0..kw {
27379 let hi = ho * sh + ki;
27380 let wi = wo * sw + kj;
27381 if hi < ph || wi < pw {
27382 continue;
27383 }
27384 let hi = hi - ph;
27385 let wi = wi - pw;
27386 if hi >= h || wi >= w_in {
27387 continue;
27388 }
27389 let xv =
27390 xq[((ni * c_in) + ci) * h * w_in + hi * w_in + wi] as i32;
27391 let wv = wq[((co * c_in) + ci) * kh * kw + ki * kw + kj] as i32;
27392 acc += xv * wv;
27393 }
27394 }
27395 }
27396 let r = (acc as f32 * mult).round() as i32;
27397 let r = r.clamp(-128, 127) as i8;
27398 out_ref[((ni * c_out) + co) * h_out * w_out + ho * w_out + wo] = r;
27399 }
27400 }
27401 }
27402 }
27403
27404 for (i, (a, r)) in out_q.iter().zip(&out_ref).enumerate() {
27405 assert_eq!(a, r, "q_conv2d[{i}]: kernel {a} vs reference {r}");
27406 }
27407 }
27408
27409 #[test]
27417 fn q_matmul_matches_fake_quant_reference() {
27418 use rlx_ir::Philox4x32;
27419 let m = 3usize;
27420 let k = 8usize;
27421 let n = 5usize;
27422 let mut rng = Philox4x32::new(2031);
27423
27424 let x_scale = 0.05f32;
27426 let w_scale = 0.03f32;
27427 let out_scale = 0.4f32;
27428 let mult = x_scale * w_scale / out_scale;
27429 let mut xf = vec![0f32; m * k];
27430 rng.fill_normal(&mut xf);
27431 let mut wf = vec![0f32; k * n];
27432 rng.fill_normal(&mut wf);
27433 let xq: Vec<i8> = xf
27434 .iter()
27435 .map(|&v| ((v / x_scale).round() as i32).clamp(-128, 127) as i8)
27436 .collect();
27437 let wq: Vec<i8> = wf
27438 .iter()
27439 .map(|&v| ((v / w_scale).round() as i32).clamp(-128, 127) as i8)
27440 .collect();
27441 let bias: Vec<i32> = vec![0i32; n];
27442
27443 let _f = DType::F32;
27445 let mut g_q = Graph::new("qmm_direct");
27446 let xn = g_q.input("x", Shape::new(&[m, k], DType::I8));
27447 let wn = g_q.input("w", Shape::new(&[k, n], DType::I8));
27448 let bn = g_q.input("b", Shape::new(&[n], DType::I32));
27449 let out = g_q.q_matmul(xn, wn, bn, 0, 0, 0, mult, Shape::new(&[m, n], DType::I8));
27450 g_q.set_outputs(vec![out]);
27451 let plan = rlx_opt::memory::plan_memory(&g_q);
27452 let mut arena = crate::arena::Arena::from_plan(plan);
27453 let sched = compile_thunks(&g_q, &arena);
27454
27455 let xn_off = arena.byte_offset(xn);
27457 let wn_off = arena.byte_offset(wn);
27458 let bn_off = arena.byte_offset(bn);
27459 let out_off = arena.byte_offset(out);
27460 let buf = arena.raw_buf_mut();
27461 unsafe {
27462 let p = buf.as_mut_ptr().add(xn_off) as *mut i8;
27463 for (i, &v) in xq.iter().enumerate() {
27464 *p.add(i) = v;
27465 }
27466 let p = buf.as_mut_ptr().add(wn_off) as *mut i8;
27467 for (i, &v) in wq.iter().enumerate() {
27468 *p.add(i) = v;
27469 }
27470 let p = buf.as_mut_ptr().add(bn_off) as *mut i32;
27471 for (i, &v) in bias.iter().enumerate() {
27472 *p.add(i) = v;
27473 }
27474 }
27475 execute_thunks(&sched, arena.raw_buf_mut());
27476 let out_q: Vec<i8> = unsafe {
27477 let p = arena.raw_buf().as_ptr().add(out_off) as *const i8;
27478 (0..m * n).map(|i| *p.add(i)).collect()
27479 };
27480
27481 let mut out_ref = vec![0i8; m * n];
27486 for mi in 0..m {
27487 for ni in 0..n {
27488 let mut acc: i32 = 0;
27489 for ki in 0..k {
27490 acc += (xq[mi * k + ki] as i32) * (wq[ki * n + ni] as i32);
27491 }
27492 let r = (acc as f32 * mult).round() as i32;
27493 out_ref[mi * n + ni] = r.clamp(-128, 127) as i8;
27494 }
27495 }
27496
27497 for (i, (a, r)) in out_q.iter().zip(&out_ref).enumerate() {
27498 assert_eq!(a, r, "q_matmul[{i}]: kernel {a} vs reference {r}");
27499 }
27500 }
27501
27502 #[test]
27507 fn quantize_dequantize_round_trip() {
27508 use rlx_ir::Philox4x32;
27509 let len = 64;
27510 let mut rng = Philox4x32::new(2027);
27511 let mut x = vec![0f32; len];
27512 rng.fill_normal(&mut x);
27513 x[0] = 999.0;
27516 x[1] = -999.0;
27517
27518 let scale = 0.05f32;
27519 let zp = 3i32;
27520
27521 let f = DType::F32;
27522 let mut g = Graph::new("qdq");
27523 let xn = g.input("x", Shape::new(&[len], f));
27524 let q = g.quantize(xn, scale, zp);
27525 let dq = g.dequantize(q, scale, zp);
27526 g.set_outputs(vec![dq]);
27527
27528 let plan = rlx_opt::memory::plan_memory(&g);
27529 let mut arena = crate::arena::Arena::from_plan(plan);
27530 let sched = compile_thunks(&g, &arena);
27531 let xn_off = arena.byte_offset(xn);
27532 let dq_off = arena.byte_offset(dq);
27533 let buf = arena.raw_buf_mut();
27534 unsafe {
27535 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
27536 for (i, &v) in x.iter().enumerate() {
27537 *p.add(i) = v;
27538 }
27539 }
27540 execute_thunks(&sched, arena.raw_buf_mut());
27541 let out: Vec<f32> = unsafe {
27542 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
27543 (0..len).map(|i| *p.add(i)).collect()
27544 };
27545
27546 let sat_pos = (127 - zp) as f32 * scale;
27549 let sat_neg = (-128 - zp) as f32 * scale;
27550 assert!((out[0] - sat_pos).abs() < 1e-6, "+sat: {}", out[0]);
27551 assert!((out[1] - sat_neg).abs() < 1e-6, "-sat: {}", out[1]);
27552
27553 for i in 2..len {
27556 assert!(
27557 (out[i] - x[i]).abs() <= scale + 1e-5,
27558 "qdq[{i}]: {} → {}, scale={scale}",
27559 x[i],
27560 out[i]
27561 );
27562 }
27563 }
27564
27565 #[test]
27571 fn quantize_per_channel_round_trip() {
27572 let c = 4usize;
27573 let inner = 5usize;
27574 let mags = [0.01f32, 0.5, 5.0, 50.0];
27577 let mut x = vec![0f32; c * inner];
27578 for ci in 0..c {
27579 for ii in 0..inner {
27580 x[ci * inner + ii] = match ii {
27584 0 => -mags[ci],
27585 1 => 0.0,
27586 2 => mags[ci],
27587 3 => mags[ci] * 1000.0, _ => -mags[ci] * 1000.0, };
27590 }
27591 }
27592 let scales: Vec<f32> = mags.iter().map(|&m| m / 127.0).collect();
27593 let zps: Vec<i32> = vec![0, 0, 0, 0];
27594
27595 let f = DType::F32;
27596 let mut g = Graph::new("qdq_pc");
27597 let xn = g.input("x", Shape::new(&[c, inner], f));
27598 let q = g.quantize_per_channel(xn, 0, scales.clone(), zps.clone());
27599 let dq = g.dequantize_per_channel(q, 0, scales.clone(), zps);
27600 g.set_outputs(vec![dq]);
27601
27602 let plan = rlx_opt::memory::plan_memory(&g);
27603 let mut arena = crate::arena::Arena::from_plan(plan);
27604 let sched = compile_thunks(&g, &arena);
27605 let xn_off = arena.byte_offset(xn);
27606 let dq_off = arena.byte_offset(dq);
27607 let buf = arena.raw_buf_mut();
27608 unsafe {
27609 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
27610 for (i, &v) in x.iter().enumerate() {
27611 *p.add(i) = v;
27612 }
27613 }
27614 execute_thunks(&sched, arena.raw_buf_mut());
27615 let out: Vec<f32> = unsafe {
27616 let p = arena.raw_buf().as_ptr().add(dq_off) as *const f32;
27617 (0..c * inner).map(|i| *p.add(i)).collect()
27618 };
27619
27620 for ci in 0..c {
27621 for ii in 0..3 {
27624 let idx = ci * inner + ii;
27625 assert!(
27626 (out[idx] - x[idx]).abs() <= scales[ci] + 1e-5,
27627 "ch {ci} idx {ii}: {} vs {}",
27628 x[idx],
27629 out[idx]
27630 );
27631 }
27632 let sat_pos = 127.0 * scales[ci];
27634 let sat_neg = -128.0 * scales[ci];
27635 assert!(
27636 (out[ci * inner + 3] - sat_pos).abs() < 1e-5,
27637 "ch {ci} +sat: {}",
27638 out[ci * inner + 3]
27639 );
27640 assert!(
27641 (out[ci * inner + 4] - sat_neg).abs() < 1e-5,
27642 "ch {ci} -sat: {}",
27643 out[ci * inner + 4]
27644 );
27645 }
27646 }
27647
27648 #[test]
27654 fn activation_backward_matches_numerical_per_kind() {
27655 use rlx_ir::Philox4x32;
27656 use rlx_ir::op::Activation;
27657 let mut rng = Philox4x32::new(91);
27658 let len = 32;
27659 let mut x_pos = vec![0f32; len];
27664 rng.fill_normal(&mut x_pos);
27665 for v in x_pos.iter_mut() {
27666 *v = v.abs() + 0.5;
27667 }
27668 let mut x_any = vec![0f32; len];
27669 rng.fill_normal(&mut x_any);
27670 let mut dy = vec![0f32; len];
27671 rng.fill_normal(&mut dy);
27672
27673 for &(kind, x_data, eps, tol) in &[
27674 (Activation::Sigmoid, &x_any[..], 1e-3, 5e-3),
27675 (Activation::Tanh, &x_any[..], 1e-3, 5e-3),
27676 (Activation::Silu, &x_any[..], 1e-3, 5e-3),
27677 (Activation::Gelu, &x_any[..], 1e-3, 5e-3),
27678 (Activation::GeluApprox, &x_any[..], 1e-3, 5e-3),
27679 (Activation::Exp, &x_any[..], 1e-4, 5e-3),
27680 (Activation::Log, &x_pos[..], 1e-4, 5e-3),
27681 (Activation::Sqrt, &x_pos[..], 1e-4, 5e-3),
27682 (Activation::Rsqrt, &x_pos[..], 1e-4, 5e-3),
27683 (Activation::Neg, &x_any[..], 1e-3, 5e-4),
27684 ] {
27685 let f = DType::F32;
27686 let mut g = Graph::new("act_bw");
27687 let xn = g.input("x", Shape::new(&[len], f));
27688 let dyn_ = g.input("dy", Shape::new(&[len], f));
27689 let dx = g.activation_backward(kind, xn, dyn_);
27690 g.set_outputs(vec![dx]);
27691
27692 let plan = rlx_opt::memory::plan_memory(&g);
27693 let mut arena = crate::arena::Arena::from_plan(plan);
27694 let sched = compile_thunks(&g, &arena);
27695
27696 let xn_off = arena.byte_offset(xn);
27697 let dyn_off = arena.byte_offset(dyn_);
27698 let dx_off = arena.byte_offset(dx);
27699 let buf = arena.raw_buf_mut();
27700 unsafe {
27701 let p = buf.as_mut_ptr().add(xn_off) as *mut f32;
27702 for (i, &v) in x_data.iter().enumerate() {
27703 *p.add(i) = v;
27704 }
27705 let p = buf.as_mut_ptr().add(dyn_off) as *mut f32;
27706 for (i, &v) in dy.iter().enumerate() {
27707 *p.add(i) = v;
27708 }
27709 }
27710 execute_thunks(&sched, arena.raw_buf_mut());
27711 let analytical: Vec<f32> = unsafe {
27712 let p = arena.raw_buf().as_ptr().add(dx_off) as *const f32;
27713 (0..len).map(|i| *p.add(i)).collect()
27714 };
27715
27716 let act_apply = |kind: Activation, x: f32| -> f32 {
27719 match kind {
27720 Activation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
27721 Activation::Tanh => x.tanh(),
27722 Activation::Silu => x / (1.0 + (-x).exp()),
27723 Activation::Gelu => {
27724 const INV_SQRT2: f32 = 0.707_106_77;
27726 0.5 * x * (1.0 + erf_f32(x * INV_SQRT2))
27727 }
27728 Activation::GeluApprox => {
27729 const C: f32 = 0.797_884_6;
27730 const A: f32 = 0.044_715;
27731 let inner = C * (x + A * x * x * x);
27732 0.5 * x * (1.0 + inner.tanh())
27733 }
27734 Activation::Exp => x.exp(),
27735 Activation::Log => x.ln(),
27736 Activation::Sqrt => x.sqrt(),
27737 Activation::Rsqrt => 1.0 / x.sqrt(),
27738 Activation::Neg => -x,
27739 Activation::Relu => x.max(0.0),
27740 Activation::Abs => x.abs(),
27741 Activation::Round => x.round(),
27742 Activation::Sin => x.sin(),
27743 Activation::Cos => x.cos(),
27744 Activation::Tan => x.tan(),
27745 Activation::Atan => x.atan(),
27746 }
27747 };
27748 for i in 0..len {
27749 let xv = x_data[i];
27750 let plus = act_apply(kind, xv + eps);
27751 let minus = act_apply(kind, xv - eps);
27752 let num = (plus - minus) / (2.0 * eps) * dy[i];
27753 assert!(
27754 (analytical[i] - num).abs() < tol,
27755 "{kind:?}[{i}]: analytical {} vs numerical {num}",
27756 analytical[i]
27757 );
27758 }
27759 }
27760 }
27761
27762 #[test]
27766 fn matmul_3d_gradient_matches_numerical() {
27767 use rlx_ir::Philox4x32;
27768 let batch = 2usize;
27769 let m = 3usize;
27770 let k = 4usize;
27771 let n = 5usize;
27772 let mut rng = Philox4x32::new(101);
27773 let mut a_data = vec![0f32; batch * m * k];
27774 rng.fill_normal(&mut a_data);
27775 let mut b_data = vec![0f32; batch * k * n];
27776 rng.fill_normal(&mut b_data);
27777
27778 let f = DType::F32;
27779 let mut fwd = Graph::new("matmul_3d");
27780 let an = fwd.input("a", Shape::new(&[batch, m, k], f));
27781 let bp = fwd.param("b", Shape::new(&[batch, k, n], f));
27782 let mm = fwd.matmul(an, bp, Shape::new(&[batch, m, n], f));
27783 let loss = fwd.add_node(
27784 Op::Reduce {
27785 op: ReduceOp::Sum,
27786 axes: vec![0, 1, 2],
27787 keep_dim: false,
27788 },
27789 vec![mm],
27790 Shape::from_dims(&[], f),
27791 );
27792 fwd.set_outputs(vec![loss]);
27793
27794 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[bp]);
27795 let d_out = bwd_graph
27796 .nodes()
27797 .iter()
27798 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
27799 .map(|n| n.id)
27800 .unwrap();
27801
27802 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
27803 let mut arena = crate::arena::Arena::from_plan(plan);
27804 let sched = compile_thunks(&bwd_graph, &arena);
27805 for &(id, data) in &[(an, &a_data), (bp, &b_data), (d_out, &vec![1.0f32])] {
27806 let off = arena.byte_offset(id);
27807 let buf = arena.raw_buf_mut();
27808 unsafe {
27809 let p = buf.as_mut_ptr().add(off) as *mut f32;
27810 for (i, &v) in data.iter().enumerate() {
27811 *p.add(i) = v;
27812 }
27813 }
27814 }
27815 execute_thunks(&sched, arena.raw_buf_mut());
27816 let gb_id = bwd_graph.outputs[1];
27817 let g_b: Vec<f32> = unsafe {
27818 let p = arena.raw_buf().as_ptr().add(arena.byte_offset(gb_id)) as *const f32;
27819 (0..batch * k * n).map(|i| *p.add(i)).collect()
27820 };
27821
27822 let forward_loss = |b_vals: &[f32]| -> f32 {
27824 let mut out = vec![0f32; batch * m * n];
27825 for bi in 0..batch {
27826 for mi in 0..m {
27827 for ni in 0..n {
27828 let mut acc = 0f32;
27829 for ki in 0..k {
27830 acc +=
27831 a_data[bi * m * k + mi * k + ki] * b_vals[bi * k * n + ki * n + ni];
27832 }
27833 out[bi * m * n + mi * n + ni] = acc;
27834 }
27835 }
27836 }
27837 out.iter().sum()
27838 };
27839 let eps = 1e-3f32;
27840 let mut bp_p = b_data.clone();
27841 let mut g_b_num = vec![0f32; b_data.len()];
27842 for i in 0..b_data.len() {
27843 let s = bp_p[i];
27844 bp_p[i] = s + eps;
27845 let lp = forward_loss(&bp_p);
27846 bp_p[i] = s - eps;
27847 let lm = forward_loss(&bp_p);
27848 bp_p[i] = s;
27849 g_b_num[i] = (lp - lm) / (2.0 * eps);
27850 }
27851 for (i, (a, n)) in g_b.iter().zip(&g_b_num).enumerate() {
27852 assert!(
27853 (a - n).abs() < 5e-3,
27854 "matmul_3d g_b[{i}]: analytical {a} vs numerical {n}"
27855 );
27856 }
27857 }
27858
27859 #[test]
27865 fn softmax_gradient_matches_numerical() {
27866 use rlx_ir::Philox4x32;
27867 let n = 3usize;
27868 let c = 5usize;
27869 let mut rng = Philox4x32::new(57);
27870 let mut x_data = vec![0f32; n * c];
27871 rng.fill_normal(&mut x_data);
27872
27873 let f = DType::F32;
27874 let mut fwd = Graph::new("softmax_only");
27875 let xn = fwd.input("x", Shape::new(&[n, c], f));
27876 let sm = fwd.add_node(Op::Softmax { axis: -1 }, vec![xn], Shape::new(&[n, c], f));
27877 let loss = fwd.add_node(
27881 Op::Reduce {
27882 op: ReduceOp::Sum,
27883 axes: vec![0, 1],
27884 keep_dim: false,
27885 },
27886 vec![sm],
27887 Shape::from_dims(&[], f),
27888 );
27889 fwd.set_outputs(vec![loss]);
27890
27891 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[xn]);
27895 let d_out = bwd_graph
27896 .nodes()
27897 .iter()
27898 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
27899 .map(|n| n.id)
27900 .unwrap();
27901
27902 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
27903 let mut arena = crate::arena::Arena::from_plan(plan);
27904 let sched = compile_thunks(&bwd_graph, &arena);
27905 for &(id, data) in &[(xn, &x_data), (d_out, &vec![1.0f32])] {
27906 let off = arena.byte_offset(id);
27907 let buf = arena.raw_buf_mut();
27908 unsafe {
27909 let p = buf.as_mut_ptr().add(off) as *mut f32;
27910 for (i, &v) in data.iter().enumerate() {
27911 *p.add(i) = v;
27912 }
27913 }
27914 }
27915 execute_thunks(&sched, arena.raw_buf_mut());
27916 let g_x_id = bwd_graph.outputs[1];
27917 let g_x: Vec<f32> = unsafe {
27918 let p = arena.raw_buf().as_ptr().add(arena.byte_offset(g_x_id)) as *const f32;
27919 (0..n * c).map(|i| *p.add(i)).collect()
27920 };
27921
27922 let forward_loss = |x: &[f32]| -> f32 {
27926 let mut total = 0f32;
27927 for ni in 0..n {
27928 let row = &x[ni * c..(ni + 1) * c];
27929 let m = row.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
27930 let denom: f32 = row.iter().map(|&v| (v - m).exp()).sum();
27931 for &v in row {
27932 total += (v - m).exp() / denom;
27933 }
27934 }
27935 total
27936 };
27937 let eps = 1e-3f32;
27938 let mut p = x_data.clone();
27939 for i in 0..x_data.len() {
27940 let s = p[i];
27941 p[i] = s + eps;
27942 let lp = forward_loss(&p);
27943 p[i] = s - eps;
27944 let lm = forward_loss(&p);
27945 p[i] = s;
27946 let num = (lp - lm) / (2.0 * eps);
27947 assert!(
27948 (g_x[i] - num).abs() < 5e-3,
27949 "softmax g_x[{i}]: analytical {} vs numerical {num}",
27950 g_x[i]
27951 );
27952 }
27953 }
27954
27955 #[test]
27960 fn layer_norm_gradient_matches_numerical() {
27961 use rlx_ir::Philox4x32;
27962 let rows = 3usize;
27963 let h = 6usize;
27964 let mut rng = Philox4x32::new(1009);
27965 let mut x_data = vec![0f32; rows * h];
27966 rng.fill_normal(&mut x_data);
27967 let mut g_data = vec![0f32; h];
27968 rng.fill_normal(&mut g_data);
27969 for v in g_data.iter_mut() {
27970 *v = v.abs() + 0.5;
27971 }
27972 let mut b_data = vec![0f32; h];
27973 rng.fill_normal(&mut b_data);
27974 let eps = 1e-5f32;
27975
27976 let f = DType::F32;
27977 let mut fwd = Graph::new("ln_only");
27978 let xn = fwd.input("x", Shape::new(&[rows, h], f));
27979 let gp = fwd.param("gamma", Shape::new(&[h], f));
27980 let bp = fwd.param("beta", Shape::new(&[h], f));
27981 let ln = fwd.add_node(
27982 Op::LayerNorm { axis: -1, eps },
27983 vec![xn, gp, bp],
27984 Shape::new(&[rows, h], f),
27985 );
27986 let loss = fwd.add_node(
27987 Op::Reduce {
27988 op: ReduceOp::Sum,
27989 axes: vec![0, 1],
27990 keep_dim: false,
27991 },
27992 vec![ln],
27993 Shape::from_dims(&[], f),
27994 );
27995 fwd.set_outputs(vec![loss]);
27996
27997 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[xn, gp, bp]);
27998 let d_out = bwd_graph
27999 .nodes()
28000 .iter()
28001 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28002 .map(|n| n.id)
28003 .unwrap();
28004
28005 let plan = rlx_opt::memory::plan_memory(&bwd_graph);
28006 let mut arena = crate::arena::Arena::from_plan(plan);
28007 let sched = compile_thunks(&bwd_graph, &arena);
28008 for &(id, data) in &[
28009 (xn, &x_data),
28010 (gp, &g_data),
28011 (bp, &b_data),
28012 (d_out, &vec![1.0f32]),
28013 ] {
28014 let off = arena.byte_offset(id);
28015 let buf = arena.raw_buf_mut();
28016 unsafe {
28017 let p = buf.as_mut_ptr().add(off) as *mut f32;
28018 for (i, &v) in data.iter().enumerate() {
28019 *p.add(i) = v;
28020 }
28021 }
28022 }
28023 execute_thunks(&sched, arena.raw_buf_mut());
28024 let read = |id: NodeId, n: usize| -> Vec<f32> {
28025 let off = arena.byte_offset(id);
28026 unsafe {
28027 let p = arena.raw_buf().as_ptr().add(off) as *const f32;
28028 (0..n).map(|i| *p.add(i)).collect()
28029 }
28030 };
28031 let dx_a = read(bwd_graph.outputs[1], rows * h);
28032 let dg_a = read(bwd_graph.outputs[2], h);
28033 let db_a = read(bwd_graph.outputs[3], h);
28034
28035 let forward_loss = |x: &[f32], g: &[f32], b: &[f32]| -> f32 {
28036 let mut total = 0f32;
28037 for r in 0..rows {
28038 let row = &x[r * h..(r + 1) * h];
28039 let mean = row.iter().sum::<f32>() / h as f32;
28040 let var = row.iter().map(|&v| (v - mean) * (v - mean)).sum::<f32>() / h as f32;
28041 let inv_std = 1.0 / (var + eps).sqrt();
28042 for d in 0..h {
28043 total += ((row[d] - mean) * inv_std) * g[d] + b[d];
28044 }
28045 }
28046 total
28047 };
28048 let h_eps = 1e-3f32;
28049
28050 let mut x_p = x_data.clone();
28051 for i in 0..x_p.len() {
28052 let s = x_p[i];
28053 x_p[i] = s + h_eps;
28054 let lp = forward_loss(&x_p, &g_data, &b_data);
28055 x_p[i] = s - h_eps;
28056 let lm = forward_loss(&x_p, &g_data, &b_data);
28057 x_p[i] = s;
28058 let num = (lp - lm) / (2.0 * h_eps);
28059 assert!(
28060 (dx_a[i] - num).abs() < 5e-3,
28061 "ln dx[{i}]: analytical {} vs numerical {num}",
28062 dx_a[i]
28063 );
28064 }
28065 let mut g_p = g_data.clone();
28066 for i in 0..g_p.len() {
28067 let s = g_p[i];
28068 g_p[i] = s + h_eps;
28069 let lp = forward_loss(&x_data, &g_p, &b_data);
28070 g_p[i] = s - h_eps;
28071 let lm = forward_loss(&x_data, &g_p, &b_data);
28072 g_p[i] = s;
28073 let num = (lp - lm) / (2.0 * h_eps);
28074 assert!(
28075 (dg_a[i] - num).abs() < 5e-3,
28076 "ln dg[{i}]: analytical {} vs numerical {num}",
28077 dg_a[i]
28078 );
28079 }
28080 let mut b_p = b_data.clone();
28081 for i in 0..b_p.len() {
28082 let s = b_p[i];
28083 b_p[i] = s + h_eps;
28084 let lp = forward_loss(&x_data, &g_data, &b_p);
28085 b_p[i] = s - h_eps;
28086 let lm = forward_loss(&x_data, &g_data, &b_p);
28087 b_p[i] = s;
28088 let num = (lp - lm) / (2.0 * h_eps);
28089 assert!(
28090 (db_a[i] - num).abs() < 5e-3,
28091 "ln db[{i}]: analytical {} vs numerical {num}",
28092 db_a[i]
28093 );
28094 }
28095 }
28096
28097 #[test]
28102 fn dense_sce_mean_gradient_matches_numerical() {
28103 use rlx_ir::Philox4x32;
28104 let bs = 4usize;
28105 let k_in = 3usize;
28106 let c = 5usize;
28107 let mut rng = Philox4x32::new(7);
28108 let mut x = vec![0f32; bs * k_in];
28109 rng.fill_normal(&mut x);
28110 let mut w_init = vec![0f32; k_in * c];
28111 rng.fill_normal(&mut w_init);
28112 let mut b_init = vec![0f32; c];
28113 rng.fill_normal(&mut b_init);
28114 let labels: Vec<f32> = (0..bs).map(|i| (i % c) as f32).collect();
28115
28116 let f = DType::F32;
28118 let mut fwd = Graph::new("dense_sce");
28119 let xn = fwd.input("x", Shape::new(&[bs, k_in], f));
28120 let lb = fwd.input("labels", Shape::new(&[bs], f));
28121 let wp = fwd.param("w", Shape::new(&[k_in, c], f));
28122 let bp = fwd.param("b", Shape::new(&[c], f));
28123 let mm = fwd.matmul(xn, wp, Shape::new(&[bs, c], f));
28124 let logits = fwd.binary(BinaryOp::Add, mm, bp, Shape::new(&[bs, c], f));
28125 let loss_per = fwd.softmax_cross_entropy_with_logits(logits, lb);
28126 let loss = fwd.add_node(
28127 Op::Reduce {
28128 op: ReduceOp::Sum,
28129 axes: vec![0],
28130 keep_dim: false,
28131 },
28132 vec![loss_per],
28133 Shape::from_dims(&[], f),
28135 );
28136 fwd.set_outputs(vec![loss]);
28144
28145 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wp, bp]);
28147 let d_out = bwd_graph
28150 .nodes()
28151 .iter()
28152 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28153 .map(|n| n.id)
28154 .expect("d_output input");
28155
28156 let (sched, mut arena) = prepare(
28157 &bwd_graph,
28158 &[
28159 (xn, &x),
28160 (lb, &labels),
28161 (wp, &w_init),
28162 (bp, &b_init),
28163 (d_out, &[1.0]),
28164 ],
28165 );
28166 execute_thunks(&sched, arena.raw_buf_mut());
28167
28168 let outs = &bwd_graph.outputs;
28169 let loss_id = outs[0];
28170 let gw_id = outs[1];
28171 let gb_id = outs[2];
28172 let loss_actual = read_arena(&arena, loss_id, 1)[0];
28173 let gw_actual = read_arena(&arena, gw_id, k_in * c);
28174 let gb_actual = read_arena(&arena, gb_id, c);
28175
28176 let plan = rlx_opt::memory::plan_memory(&fwd);
28180 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
28181 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
28182 write_arena(&mut fwd_arena, xn, &x);
28183 write_arena(&mut fwd_arena, lb, &labels);
28184
28185 let run_loss = |arena: &mut crate::arena::Arena, w: &[f32], b: &[f32]| -> f32 {
28186 write_arena(arena, wp, w);
28187 write_arena(arena, bp, b);
28188 execute_thunks(&fwd_sched, arena.raw_buf_mut());
28189 read_arena(arena, loss, 1)[0]
28190 };
28191
28192 let loss_check = run_loss(&mut fwd_arena, &w_init, &b_init);
28195 assert!(
28196 (loss_actual - loss_check).abs() < 1e-4,
28197 "loss mismatch: bwd graph {loss_actual} vs fwd-only {loss_check}"
28198 );
28199
28200 let eps = 1e-3f32;
28201 let mut w_perturbed = w_init.clone();
28202 let mut gw_numerical = vec![0f32; w_init.len()];
28203 for i in 0..w_init.len() {
28204 let saved = w_perturbed[i];
28205 w_perturbed[i] = saved + eps;
28206 let lp = run_loss(&mut fwd_arena, &w_perturbed, &b_init);
28207 w_perturbed[i] = saved - eps;
28208 let lm = run_loss(&mut fwd_arena, &w_perturbed, &b_init);
28209 w_perturbed[i] = saved;
28210 gw_numerical[i] = (lp - lm) / (2.0 * eps);
28211 }
28212 for (i, (a, n)) in gw_actual.iter().zip(&gw_numerical).enumerate() {
28213 assert!(
28214 (a - n).abs() < 5e-3,
28215 "grad_w[{i}]: analytical {a} vs numerical {n}"
28216 );
28217 }
28218
28219 let mut b_perturbed = b_init.clone();
28220 let mut gb_numerical = vec![0f32; b_init.len()];
28221 for i in 0..b_init.len() {
28222 let saved = b_perturbed[i];
28223 b_perturbed[i] = saved + eps;
28224 let lp = run_loss(&mut fwd_arena, &w_init, &b_perturbed);
28225 b_perturbed[i] = saved - eps;
28226 let lm = run_loss(&mut fwd_arena, &w_init, &b_perturbed);
28227 b_perturbed[i] = saved;
28228 gb_numerical[i] = (lp - lm) / (2.0 * eps);
28229 }
28230 for (i, (a, n)) in gb_actual.iter().zip(&gb_numerical).enumerate() {
28231 assert!(
28232 (a - n).abs() < 5e-3,
28233 "grad_b[{i}]: analytical {a} vs numerical {n}"
28234 );
28235 }
28236 }
28237
28238 #[test]
28241 fn dense_sce_mean_reduce_gradient_matches_numerical() {
28242 use rlx_ir::Philox4x32;
28243 let bs = 3usize;
28244 let k_in = 2usize;
28245 let c = 4usize;
28246 let mut rng = Philox4x32::new(13);
28247 let mut x = vec![0f32; bs * k_in];
28248 rng.fill_normal(&mut x);
28249 let mut w_init = vec![0f32; k_in * c];
28250 rng.fill_normal(&mut w_init);
28251 let labels: Vec<f32> = (0..bs).map(|i| (i % c) as f32).collect();
28252
28253 let f = DType::F32;
28254 let mut fwd = Graph::new("dense_sce_mean");
28255 let xn = fwd.input("x", Shape::new(&[bs, k_in], f));
28256 let lb = fwd.input("labels", Shape::new(&[bs], f));
28257 let wp = fwd.param("w", Shape::new(&[k_in, c], f));
28258 let mm = fwd.matmul(xn, wp, Shape::new(&[bs, c], f));
28259 let loss_per = fwd.softmax_cross_entropy_with_logits(mm, lb);
28260 let loss = fwd.add_node(
28261 Op::Reduce {
28262 op: ReduceOp::Mean,
28263 axes: vec![0],
28264 keep_dim: false,
28265 },
28266 vec![loss_per],
28267 Shape::from_dims(&[], f),
28268 );
28269 fwd.set_outputs(vec![loss]);
28270
28271 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wp]);
28272 let d_out = bwd_graph
28273 .nodes()
28274 .iter()
28275 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28276 .map(|n| n.id)
28277 .unwrap();
28278
28279 let (sched, mut arena) = prepare(
28280 &bwd_graph,
28281 &[(xn, &x), (lb, &labels), (wp, &w_init), (d_out, &[1.0])],
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 _ = read_arena(&arena, loss_id, 1)[0];
28289 let gw_actual = read_arena(&arena, gw_id, k_in * c);
28290
28291 let plan = rlx_opt::memory::plan_memory(&fwd);
28292 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
28293 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
28294 write_arena(&mut fwd_arena, xn, &x);
28295 write_arena(&mut fwd_arena, lb, &labels);
28296
28297 let run_loss = |arena: &mut crate::arena::Arena, w: &[f32]| -> f32 {
28298 write_arena(arena, wp, w);
28299 execute_thunks(&fwd_sched, arena.raw_buf_mut());
28300 read_arena(arena, loss, 1)[0]
28301 };
28302
28303 let eps = 1e-3f32;
28304 let mut wp_p = w_init.clone();
28305 let mut gw_num = vec![0f32; w_init.len()];
28306 for i in 0..w_init.len() {
28307 let s = wp_p[i];
28308 wp_p[i] = s + eps;
28309 let lp = run_loss(&mut fwd_arena, &wp_p);
28310 wp_p[i] = s - eps;
28311 let lm = run_loss(&mut fwd_arena, &wp_p);
28312 wp_p[i] = s;
28313 gw_num[i] = (lp - lm) / (2.0 * eps);
28314 }
28315 for (i, (a, n)) in gw_actual.iter().zip(&gw_num).enumerate() {
28316 assert!((a - n).abs() < 5e-3, "mean reduce grad_w[{i}]: {a} vs {n}");
28317 }
28318 }
28319 #[test]
28324 fn tinyconv_full_gradient_matches_numerical() {
28325 use rlx_ir::Philox4x32;
28326 let n = 1usize;
28328 let c_in = 1usize;
28329 let h = 6usize;
28330 let w_in = 6usize;
28331 let c_mid = 2usize; let kh = 3;
28333 let kw = 3;
28334 let h1 = h - kh + 1; let w1 = w_in - kw + 1; let h2 = h1 / 2;
28337 let w2 = w1 / 2; let flat = c_mid * h2 * w2; let num_classes = 3usize;
28340
28341 let mut rng = Philox4x32::new(31);
28342 let mut x = vec![0f32; n * c_in * h * w_in];
28343 rng.fill_normal(&mut x);
28344 let mut wc = vec![0f32; c_mid * c_in * kh * kw];
28345 rng.fill_normal(&mut wc);
28346 for v in wc.iter_mut() {
28347 *v *= 0.2;
28348 }
28349 let bc: Vec<f32> = (0..c_mid).map(|i| 5.0 + 0.1 * i as f32).collect();
28358 let mut wfc = vec![0f32; flat * num_classes];
28359 rng.fill_normal(&mut wfc);
28360 for v in wfc.iter_mut() {
28361 *v *= 0.5;
28362 }
28363 let mut bfc = vec![0f32; num_classes];
28364 rng.fill_normal(&mut bfc);
28365 let labels: Vec<f32> = vec![1.0]; let f = DType::F32;
28368 let mut fwd = Graph::new("tinyconv");
28369 let xn = fwd.input("x", Shape::new(&[n, c_in, h, w_in], f));
28370 let lb = fwd.input("labels", Shape::new(&[n], f));
28371 let wcp = fwd.param("wc", Shape::new(&[c_mid, c_in, kh, kw], f));
28372 let bcp = fwd.param("bc", Shape::new(&[c_mid], f));
28373 let wfp = fwd.param("wfc", Shape::new(&[flat, num_classes], f));
28374 let bfp = fwd.param("bfc", Shape::new(&[num_classes], f));
28375
28376 let conv = fwd.add_node(
28378 Op::Conv {
28379 kernel_size: vec![kh, kw],
28380 stride: vec![1, 1],
28381 padding: vec![0, 0],
28382 dilation: vec![1, 1],
28383 groups: 1,
28384 },
28385 vec![xn, wcp],
28386 Shape::new(&[n, c_mid, h1, w1], f),
28387 );
28388 let bc_4d = fwd.add_node(
28400 Op::Reshape {
28401 new_shape: vec![1, c_mid as i64, 1, 1],
28402 },
28403 vec![bcp],
28404 Shape::new(&[1, c_mid, 1, 1], f),
28405 );
28406 let bc_expanded = fwd.add_node(
28407 Op::Expand {
28408 target_shape: vec![n as i64, c_mid as i64, h1 as i64, w1 as i64],
28409 },
28410 vec![bc_4d],
28411 Shape::new(&[n, c_mid, h1, w1], f),
28412 );
28413 let conv_b = fwd.binary(
28414 BinaryOp::Add,
28415 conv,
28416 bc_expanded,
28417 Shape::new(&[n, c_mid, h1, w1], f),
28418 );
28419 let relu = fwd.activation(Activation::Relu, conv_b, Shape::new(&[n, c_mid, h1, w1], f));
28420 let pool = fwd.add_node(
28421 Op::Pool {
28422 kind: ReduceOp::Max,
28423 kernel_size: vec![2, 2],
28424 stride: vec![2, 2],
28425 padding: vec![0, 0],
28426 },
28427 vec![relu],
28428 Shape::new(&[n, c_mid, h2, w2], f),
28429 );
28430 let flatn = fwd.add_node(
28431 Op::Reshape {
28432 new_shape: vec![n as i64, flat as i64],
28433 },
28434 vec![pool],
28435 Shape::new(&[n, flat], f),
28436 );
28437 let mm = fwd.matmul(flatn, wfp, Shape::new(&[n, num_classes], f));
28438 let logits = fwd.binary(BinaryOp::Add, mm, bfp, Shape::new(&[n, num_classes], f));
28439 let loss_per = fwd.softmax_cross_entropy_with_logits(logits, lb);
28440 let loss = fwd.add_node(
28441 Op::Reduce {
28442 op: ReduceOp::Mean,
28443 axes: vec![0],
28444 keep_dim: false,
28445 },
28446 vec![loss_per],
28447 Shape::from_dims(&[], f),
28448 );
28449 fwd.set_outputs(vec![loss]);
28450
28451 let bwd_graph = rlx_opt::autodiff::grad_with_loss(&fwd, &[wcp, bcp, wfp, bfp]);
28452 let d_out = bwd_graph
28453 .nodes()
28454 .iter()
28455 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
28456 .map(|n| n.id)
28457 .unwrap();
28458
28459 let (sched, mut arena) = prepare(
28460 &bwd_graph,
28461 &[
28462 (xn, &x),
28463 (lb, &labels),
28464 (wcp, &wc),
28465 (bcp, &bc),
28466 (wfp, &wfc),
28467 (bfp, &bfc),
28468 (d_out, &[1.0]),
28469 ],
28470 );
28471 execute_thunks(&sched, arena.raw_buf_mut());
28472
28473 let outs = bwd_graph.outputs.clone();
28474 let loss_id = outs[0];
28475 let g_wc_id = outs[1];
28476 let g_bc_id = outs[2];
28477 let g_wfc_id = outs[3];
28478 let g_bfc_id = outs[4];
28479 let loss_actual = read_arena(&arena, loss_id, 1)[0];
28480 let g_wc = read_arena(&arena, g_wc_id, wc.len());
28481 let g_bc = read_arena(&arena, g_bc_id, bc.len());
28482 let g_wfc = read_arena(&arena, g_wfc_id, wfc.len());
28483 let g_bfc = read_arena(&arena, g_bfc_id, bfc.len());
28484
28485 let plan = rlx_opt::memory::plan_memory(&fwd);
28487 let mut fwd_arena = crate::arena::Arena::from_plan(plan);
28488 let fwd_sched = compile_thunks(&fwd, &fwd_arena);
28489 write_arena(&mut fwd_arena, xn, &x);
28490 write_arena(&mut fwd_arena, lb, &labels);
28491
28492 let run_loss = |arena: &mut crate::arena::Arena,
28495 wc: &[f32],
28496 bc: &[f32],
28497 wfc: &[f32],
28498 bfc: &[f32]|
28499 -> f32 {
28500 write_arena(arena, wcp, wc);
28501 write_arena(arena, bcp, bc);
28502 write_arena(arena, wfp, wfc);
28503 write_arena(arena, bfp, bfc);
28504 execute_thunks(&fwd_sched, arena.raw_buf_mut());
28505 read_arena(arena, loss, 1)[0]
28506 };
28507
28508 let loss_check = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &bfc);
28509 assert!(
28510 (loss_actual - loss_check).abs() < 1e-4,
28511 "tinyconv loss mismatch: bwd {loss_actual} vs fwd {loss_check}"
28512 );
28513
28514 let eps = 1e-3f32;
28515 let check_grad = |arena: &mut crate::arena::Arena,
28516 name: &str,
28517 analytical: &[f32],
28518 mut perturb: Box<
28519 dyn FnMut(&mut [f32], usize, f32, &mut crate::arena::Arena) -> f32 + '_,
28520 >,
28521 n: usize| {
28522 for i in 0..n {
28523 let lp = perturb(&mut analytical.to_vec(), i, eps, arena);
28524 let lm = perturb(&mut analytical.to_vec(), i, -eps, arena);
28525 let num = (lp - lm) / (2.0 * eps);
28526 assert!(
28527 (analytical[i] - num).abs() < 5e-3,
28528 "{name}[{i}]: analytical {} vs numerical {num}",
28529 analytical[i]
28530 );
28531 }
28532 };
28533
28534 #[allow(unused_macros)]
28537 macro_rules! sweep {
28538 ($name:expr, $base:expr, $analytical:expr, $set_param:ident) => {{
28539 let n = $base.len();
28540 for i in 0..n {
28541 let mut p = $base.clone();
28542 let s = p[i];
28543 p[i] = s + eps;
28544 let lp = {
28545 let $set_param = &p;
28546 run_loss(&mut fwd_arena, &wc, &bc, &wfc, &bfc).max(f32::NEG_INFINITY);
28547 let _ = $set_param;
28550 0.0_f32
28552 };
28553 let _ = lp;
28554 }
28555 }};
28556 }
28557 let _ = check_grad; for i in 0..wc.len() {
28561 let mut p = wc.clone();
28562 let s = p[i];
28563 p[i] = s + eps;
28564 let lp = run_loss(&mut fwd_arena, &p, &bc, &wfc, &bfc);
28565 p[i] = s - eps;
28566 let lm = run_loss(&mut fwd_arena, &p, &bc, &wfc, &bfc);
28567 let num = (lp - lm) / (2.0 * eps);
28568 assert!(
28569 (g_wc[i] - num).abs() < 5e-3,
28570 "g_wc[{i}]: {} vs {num}",
28571 g_wc[i]
28572 );
28573 }
28574 for i in 0..bc.len() {
28575 let mut p = bc.clone();
28576 let s = p[i];
28577 p[i] = s + eps;
28578 let lp = run_loss(&mut fwd_arena, &wc, &p, &wfc, &bfc);
28579 p[i] = s - eps;
28580 let lm = run_loss(&mut fwd_arena, &wc, &p, &wfc, &bfc);
28581 let num = (lp - lm) / (2.0 * eps);
28582 assert!(
28583 (g_bc[i] - num).abs() < 5e-3,
28584 "g_bc[{i}]: {} vs {num}",
28585 g_bc[i]
28586 );
28587 }
28588 for i in 0..wfc.len() {
28589 let mut p = wfc.clone();
28590 let s = p[i];
28591 p[i] = s + eps;
28592 let lp = run_loss(&mut fwd_arena, &wc, &bc, &p, &bfc);
28593 p[i] = s - eps;
28594 let lm = run_loss(&mut fwd_arena, &wc, &bc, &p, &bfc);
28595 let num = (lp - lm) / (2.0 * eps);
28596 assert!(
28597 (g_wfc[i] - num).abs() < 5e-3,
28598 "g_wfc[{i}]: {} vs {num}",
28599 g_wfc[i]
28600 );
28601 }
28602 for i in 0..bfc.len() {
28603 let mut p = bfc.clone();
28604 let s = p[i];
28605 p[i] = s + eps;
28606 let lp = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &p);
28607 p[i] = s - eps;
28608 let lm = run_loss(&mut fwd_arena, &wc, &bc, &wfc, &p);
28609 let num = (lp - lm) / (2.0 * eps);
28610 assert!(
28611 (g_bfc[i] - num).abs() < 5e-3,
28612 "g_bfc[{i}]: {} vs {num}",
28613 g_bfc[i]
28614 );
28615 }
28616 }
28617
28618 #[test]
28622 fn narrow_rope_skips_when_narrow_has_multiple_consumers() {
28623 let f = DType::F32;
28624 let mut g = Graph::new("nr_skip");
28625 let qkv = g.input("qkv", Shape::new(&[16, 8, 192], f));
28626 let cos = g.input("cos", Shape::new(&[16], f));
28627 let sin = g.input("sin", Shape::new(&[16], f));
28628 let q = g.narrow_(qkv, 2, 0, 64);
28629 let q_rope = g.rope(q, cos, sin, 16);
28630 let q_dup = g.activation(rlx_ir::op::Activation::Relu, q, Shape::new(&[16, 8, 64], f));
28632 g.set_outputs(vec![q_rope, q_dup]);
28633
28634 let plan = rlx_opt::memory::plan_memory(&g);
28635 let arena = crate::arena::Arena::from_plan(plan);
28636 let sched = compile_thunks(&g, &arena);
28637
28638 let narrow_count = sched
28639 .thunks
28640 .iter()
28641 .filter(|t| matches!(t, Thunk::Narrow { .. }))
28642 .count();
28643 assert!(
28644 narrow_count >= 1,
28645 "Narrow with multiple consumers must NOT be fused away"
28646 );
28647 }
28648
28649 #[test]
28662 fn custom_fn_forward_inlines_body() {
28663 let s = Shape::new(&[3], DType::F32);
28664
28665 let mut body = Graph::new("addone_body");
28667 let x = body.input("x", s.clone());
28668 let one_data: Vec<u8> = (0..3).flat_map(|_| 1.0_f32.to_le_bytes()).collect();
28669 let one = body.add_node(Op::Constant { data: one_data }, vec![], s.clone());
28670 let y = body.binary(BinaryOp::Add, x, one, s.clone());
28671 body.set_outputs(vec![y]);
28672
28673 let mut g = Graph::new("custom_fn_outer");
28674 let xin = g.input("x_in", s.clone());
28675 let cf = g.custom_fn(vec![xin], body, None, None);
28676 g.set_outputs(vec![cf]);
28677
28678 let xs = vec![10.0_f32, 20.0, 30.0];
28679 let (sched, mut arena) = prepare(&g, &[(xin, &xs)]);
28680 execute_thunks(&sched, arena.raw_buf_mut());
28681 let got = read_arena(&arena, cf, 3);
28682 assert_eq!(got, vec![11.0, 21.0, 31.0]);
28683 }
28684
28685 fn find_named(graph: &Graph, want: &str) -> NodeId {
28687 for n in graph.nodes() {
28688 let name = match &n.op {
28689 Op::Input { name } | Op::Param { name } => Some(name.as_str()),
28690 _ => None,
28691 };
28692 if name == Some(want) {
28693 return n.id;
28694 }
28695 }
28696 panic!("no node named {want:?} in graph");
28697 }
28698
28699 #[test]
28703 fn custom_fn_vjp_overrides_natural_gradient() {
28704 use rlx_opt::autodiff::grad_with_loss;
28705 let s = Shape::new(&[1], DType::F32);
28706
28707 let mut fwd = Graph::new("id_fwd");
28708 let x = fwd.input("x", s.clone());
28709 fwd.set_outputs(vec![x]);
28710
28711 let mut vjp_g = Graph::new("id_vjp");
28712 let _x_p = vjp_g.input("x", s.clone());
28713 let _y_p = vjp_g.input("primal_output", s.clone());
28714 let dy = vjp_g.input("d_output", s.clone());
28715 let two_data: Vec<u8> = 2.0_f32.to_le_bytes().to_vec();
28716 let two = vjp_g.add_node(Op::Constant { data: two_data }, vec![], s.clone());
28717 let dx = vjp_g.binary(BinaryOp::Mul, dy, two, s.clone());
28718 vjp_g.set_outputs(vec![dx]);
28719
28720 let mut g = Graph::new("outer");
28721 let xp = g.param("x", s.clone());
28722 let cf = g.custom_fn(vec![xp], fwd, Some(vjp_g), None);
28723 g.set_outputs(vec![cf]);
28724
28725 let bwd = grad_with_loss(&g, &[xp]);
28726 assert_eq!(bwd.outputs.len(), 2, "expect [loss, dx]");
28727
28728 let xb = find_named(&bwd, "x");
28729 let dout = find_named(&bwd, "d_output");
28730 let (sched, mut arena) = prepare(&bwd, &[(xb, &[7.0]), (dout, &[1.0])]);
28731 execute_thunks(&sched, arena.raw_buf_mut());
28732 let loss = read_arena(&arena, bwd.outputs[0], 1);
28733 let dx_v = read_arena(&arena, bwd.outputs[1], 1);
28734 assert!((loss[0] - 7.0).abs() < 1e-6, "loss should be 7.0");
28735 assert!(
28736 (dx_v[0] - 2.0).abs() < 1e-6,
28737 "vjp override should yield dx=2.0, got {} (natural autodiff would give 1.0)",
28738 dx_v[0]
28739 );
28740 }
28741
28742 #[test]
28747 fn custom_fn_vjp_two_inputs_matches_mul_autodiff() {
28748 use rlx_opt::autodiff::grad_with_loss;
28749 let s = Shape::new(&[1], DType::F32);
28750
28751 let mut fwd = Graph::new("mul_fwd");
28752 let a_f = fwd.input("a", s.clone());
28753 let b_f = fwd.input("b", s.clone());
28754 let y_f = fwd.binary(BinaryOp::Mul, a_f, b_f, s.clone());
28755 fwd.set_outputs(vec![y_f]);
28756
28757 let mut vjp_g = Graph::new("mul_vjp");
28758 let a_v = vjp_g.input("a", s.clone());
28759 let b_v = vjp_g.input("b", s.clone());
28760 let _y_v = vjp_g.input("primal_output", s.clone());
28761 let dy_v = vjp_g.input("d_output", s.clone());
28762 let da = vjp_g.binary(BinaryOp::Mul, b_v, dy_v, s.clone());
28763 let db = vjp_g.binary(BinaryOp::Mul, a_v, dy_v, s.clone());
28764 vjp_g.set_outputs(vec![da, db]);
28765
28766 let mut g = Graph::new("outer");
28767 let ap = g.param("a", s.clone());
28768 let bp = g.param("b", s.clone());
28769 let cf = g.custom_fn(vec![ap, bp], fwd, Some(vjp_g), None);
28770 g.set_outputs(vec![cf]);
28771
28772 let bwd = grad_with_loss(&g, &[ap, bp]);
28773 assert_eq!(bwd.outputs.len(), 3, "expect [loss, da, db]");
28774
28775 let ab = find_named(&bwd, "a");
28776 let bb = find_named(&bwd, "b");
28777 let dout = find_named(&bwd, "d_output");
28778 let (sched, mut arena) = prepare(&bwd, &[(ab, &[3.0]), (bb, &[5.0]), (dout, &[1.0])]);
28779 execute_thunks(&sched, arena.raw_buf_mut());
28780 let loss = read_arena(&arena, bwd.outputs[0], 1);
28781 let da_v = read_arena(&arena, bwd.outputs[1], 1);
28782 let db_v = read_arena(&arena, bwd.outputs[2], 1);
28783 assert!((loss[0] - 15.0).abs() < 1e-5);
28784 assert!(
28785 (da_v[0] - 5.0).abs() < 1e-5,
28786 "da should be b=5.0, got {}",
28787 da_v[0]
28788 );
28789 assert!(
28790 (db_v[0] - 3.0).abs() < 1e-5,
28791 "db should be a=3.0, got {}",
28792 db_v[0]
28793 );
28794 }
28795
28796 #[test]
28799 fn custom_fn_jvp_overrides_natural_tangent() {
28800 use rlx_opt::autodiff_fwd::jvp;
28801 let s = Shape::new(&[1], DType::F32);
28802
28803 let mut fwd = Graph::new("id_fwd");
28804 let x = fwd.input("x", s.clone());
28805 fwd.set_outputs(vec![x]);
28806
28807 let mut jvp_g = Graph::new("id_jvp");
28808 let _x_p = jvp_g.input("x", s.clone());
28809 let tx = jvp_g.input("tangent_0", s.clone());
28810 let two_data: Vec<u8> = 2.0_f32.to_le_bytes().to_vec();
28811 let two = jvp_g.add_node(Op::Constant { data: two_data }, vec![], s.clone());
28812 let ty = jvp_g.binary(BinaryOp::Mul, tx, two, s.clone());
28813 jvp_g.set_outputs(vec![ty]);
28814
28815 let mut g = Graph::new("outer");
28816 let xin = g.input("x_in", s.clone());
28817 let cf = g.custom_fn(vec![xin], fwd, None, Some(jvp_g));
28818 g.set_outputs(vec![cf]);
28819
28820 let fwd_g = jvp(&g, &[xin]);
28821 assert_eq!(fwd_g.outputs.len(), 2, "expect [primal_y, tangent_y]");
28822
28823 let xb = find_named(&fwd_g, "x_in");
28824 let tan = find_named(&fwd_g, "tangent_x_in");
28825 let (sched, mut arena) = prepare(&fwd_g, &[(xb, &[7.0]), (tan, &[1.0])]);
28826 execute_thunks(&sched, arena.raw_buf_mut());
28827 let y = read_arena(&arena, fwd_g.outputs[0], 1);
28828 let ty_v = read_arena(&arena, fwd_g.outputs[1], 1);
28829 assert!((y[0] - 7.0).abs() < 1e-6);
28830 assert!(
28831 (ty_v[0] - 2.0).abs() < 1e-6,
28832 "jvp override should yield t_y=2.0 (natural autodiff would give 1.0), got {}",
28833 ty_v[0]
28834 );
28835 }
28836
28837 #[test]
28842 fn c64_dtype_storage_layout() {
28843 assert_eq!(
28844 DType::C64.size_bytes(),
28845 8,
28846 "C64 should be 8 bytes (f32 real + f32 imag)"
28847 );
28848 assert!(DType::C64.is_complex());
28849 assert!(!DType::C64.is_float());
28850
28851 let s = Shape::new(&[2], DType::C64);
28853 assert_eq!(s.size_bytes().unwrap(), 16);
28854 }
28855
28856 fn run_c64_binary(op: BinaryOp, a: &[(f32, f32)], b: &[(f32, f32)]) -> Vec<(f32, f32)> {
28863 let n = a.len();
28864 let s = Shape::new(&[n], DType::C64);
28865 let mut g = Graph::new("c64_bin");
28866 let in_a = g.input("a", s.clone());
28867 let in_b = g.input("b", s.clone());
28868 let out = g.binary(op, in_a, in_b, s.clone());
28869 g.set_outputs(vec![out]);
28870
28871 let plan = rlx_opt::memory::plan_memory(&g);
28872 let mut arena = crate::arena::Arena::from_plan(plan);
28873 let sched = compile_thunks(&g, &arena);
28874
28875 let a_off = arena.byte_offset(in_a);
28876 let b_off = arena.byte_offset(in_b);
28877 let out_off = arena.byte_offset(out);
28878 let buf = arena.raw_buf_mut();
28880 unsafe {
28881 let pa = buf.as_mut_ptr().add(a_off) as *mut f32;
28882 let pb = buf.as_mut_ptr().add(b_off) as *mut f32;
28883 for (i, &(re, im)) in a.iter().enumerate() {
28884 *pa.add(2 * i) = re;
28885 *pa.add(2 * i + 1) = im;
28886 }
28887 for (i, &(re, im)) in b.iter().enumerate() {
28888 *pb.add(2 * i) = re;
28889 *pb.add(2 * i + 1) = im;
28890 }
28891 }
28892 execute_thunks(&sched, arena.raw_buf_mut());
28893 let raw_out: Vec<f32> = unsafe {
28894 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
28895 (0..(2 * n)).map(|i| *p.add(i)).collect()
28896 };
28897 (0..n)
28898 .map(|i| (raw_out[2 * i], raw_out[2 * i + 1]))
28899 .collect()
28900 }
28901
28902 #[track_caller]
28903 fn assert_close_c(got: (f32, f32), expected: (f32, f32), tol: f32, label: &str) {
28904 let dr = (got.0 - expected.0).abs();
28905 let di = (got.1 - expected.1).abs();
28906 assert!(
28907 dr < tol && di < tol,
28908 "[{label}] got ({:+.4}, {:+.4}), expected ({:+.4}, {:+.4})",
28909 got.0,
28910 got.1,
28911 expected.0,
28912 expected.1
28913 );
28914 }
28915
28916 #[test]
28917 fn c64_binary_add_matches_complex_arithmetic() {
28918 let a = [(1.0_f32, 2.0_f32), (3.0_f32, -1.0_f32)];
28919 let b = [(4.0_f32, -1.0_f32), (0.5_f32, 0.5_f32)];
28920 let out = run_c64_binary(BinaryOp::Add, &a, &b);
28921 assert_close_c(out[0], (5.0, 1.0), 1e-6, "add[0]");
28922 assert_close_c(out[1], (3.5, -0.5), 1e-6, "add[1]");
28923 }
28924
28925 #[test]
28926 fn c64_binary_sub_matches_complex_arithmetic() {
28927 let a = [(5.0_f32, 1.0_f32)];
28928 let b = [(2.0_f32, 3.0_f32)];
28929 let out = run_c64_binary(BinaryOp::Sub, &a, &b);
28930 assert_close_c(out[0], (3.0, -2.0), 1e-6, "sub");
28931 }
28932
28933 #[test]
28934 fn c64_binary_mul_matches_complex_arithmetic() {
28935 let a = [(1.0_f32, 2.0_f32)];
28937 let b = [(3.0_f32, 4.0_f32)];
28938 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
28939 assert_close_c(out[0], (-5.0, 10.0), 1e-5, "mul");
28940 }
28941
28942 #[test]
28943 fn c64_binary_div_matches_complex_arithmetic() {
28944 let a = [(1.0_f32, 2.0_f32)];
28948 let b = [(3.0_f32, 4.0_f32)];
28949 let out = run_c64_binary(BinaryOp::Div, &a, &b);
28950 assert_close_c(out[0], (0.44, 0.08), 1e-5, "div");
28951 }
28952
28953 #[test]
28954 fn c64_binary_mul_identity_one_is_no_op() {
28955 let a = [(3.5_f32, -1.25_f32), (-2.0_f32, 7.0_f32)];
28957 let b = [(1.0_f32, 0.0_f32), (1.0_f32, 0.0_f32)];
28958 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
28959 assert_close_c(out[0], a[0], 1e-6, "mul·1[0]");
28960 assert_close_c(out[1], a[1], 1e-6, "mul·1[1]");
28961 }
28962
28963 #[test]
28964 fn c64_binary_mul_by_i_rotates_90_degrees() {
28965 let a = [(1.0_f32, 0.0_f32)];
28967 let b = [(0.0_f32, 1.0_f32)];
28968 let out = run_c64_binary(BinaryOp::Mul, &a, &b);
28969 assert_close_c(out[0], (0.0, 1.0), 1e-6, "1·i");
28970 }
28971
28972 #[test]
28973 fn c64_binary_div_by_self_gives_unity() {
28974 let a = [(2.5_f32, -1.5_f32), (-0.7_f32, 4.2_f32)];
28975 let out = run_c64_binary(BinaryOp::Div, &a, &a);
28976 assert_close_c(out[0], (1.0, 0.0), 1e-5, "div_self[0]");
28977 assert_close_c(out[1], (1.0, 0.0), 1e-5, "div_self[1]");
28978 }
28979
28980 #[test]
28981 #[should_panic(expected = "C64: complex max/min/pow")]
28982 fn c64_binary_max_is_rejected_at_lowering() {
28983 run_c64_binary(BinaryOp::Max, &[(1.0_f32, 2.0_f32)], &[(3.0_f32, 4.0_f32)]);
28984 }
28985
28986 fn run_c64_activation(act: Activation, a: &[(f32, f32)]) -> Vec<(f32, f32)> {
28987 let n = a.len();
28988 let s = Shape::new(&[n], DType::C64);
28989 let mut g = Graph::new("c64_act");
28990 let in_a = g.input("a", s.clone());
28991 let out = g.activation(act, in_a, s.clone());
28992 g.set_outputs(vec![out]);
28993 let plan = rlx_opt::memory::plan_memory(&g);
28994 let mut arena = crate::arena::Arena::from_plan(plan);
28995 let sched = compile_thunks(&g, &arena);
28996 let a_off = arena.byte_offset(in_a);
28997 let out_off = arena.byte_offset(out);
28998 let buf = arena.raw_buf_mut();
28999 unsafe {
29000 let pa = buf.as_mut_ptr().add(a_off) as *mut f32;
29001 for (i, &(re, im)) in a.iter().enumerate() {
29002 *pa.add(2 * i) = re;
29003 *pa.add(2 * i + 1) = im;
29004 }
29005 }
29006 execute_thunks(&sched, arena.raw_buf_mut());
29007 let raw: Vec<f32> = unsafe {
29008 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29009 (0..(2 * n)).map(|i| *p.add(i)).collect()
29010 };
29011 (0..n).map(|i| (raw[2 * i], raw[2 * i + 1])).collect()
29012 }
29013
29014 #[test]
29015 fn c64_activation_neg_negates_both_components() {
29016 let inp = [(3.5_f32, -1.25_f32), (-2.0_f32, 0.0_f32)];
29017 let out = run_c64_activation(Activation::Neg, &inp);
29018 assert_close_c(out[0], (-3.5, 1.25), 1e-6, "neg[0]");
29019 assert_close_c(out[1], (2.0, 0.0), 1e-6, "neg[1]");
29020 }
29021
29022 #[test]
29023 fn c64_activation_exp_matches_euler() {
29024 let inp = [(0.0_f32, std::f32::consts::PI), (1.0_f32, 0.0_f32)];
29027 let out = run_c64_activation(Activation::Exp, &inp);
29028 assert_close_c(out[0], (-1.0, 0.0), 1e-5, "exp(iπ)");
29029 assert_close_c(out[1], (std::f32::consts::E, 0.0), 1e-5, "exp(1)");
29030 }
29031
29032 #[test]
29033 fn c64_activation_log_matches_principal_branch() {
29034 let inp = [(1.0_f32, 0.0_f32), (0.0_f32, 1.0_f32), (-1.0_f32, 0.0_f32)];
29038 let out = run_c64_activation(Activation::Log, &inp);
29039 assert_close_c(out[0], (0.0, 0.0), 1e-5, "log(1)");
29040 assert_close_c(out[1], (0.0, std::f32::consts::FRAC_PI_2), 1e-5, "log(i)");
29041 assert_close_c(out[2], (0.0, std::f32::consts::PI), 1e-5, "log(-1)");
29042 }
29043
29044 #[test]
29045 fn c64_activation_sqrt_squared_recovers_input() {
29046 let inp = [(4.0_f32, 0.0_f32), (3.0_f32, 4.0_f32)];
29049 let roots = run_c64_activation(Activation::Sqrt, &inp);
29050 assert_close_c(roots[0], (2.0, 0.0), 1e-5, "sqrt(4)");
29052 assert_close_c(roots[1], (2.0, 1.0), 1e-5, "sqrt(3+4i)");
29053 }
29054
29055 #[test]
29056 #[should_panic(expected = "no natural complex extension")]
29057 fn c64_activation_relu_is_rejected_at_lowering() {
29058 run_c64_activation(Activation::Relu, &[(1.0_f32, 2.0_f32)]);
29059 }
29060
29061 fn run_complex_norm_sq(z: &[(f32, f32)]) -> Vec<f32> {
29065 let n = z.len();
29066 let mut g = Graph::new("cns_fwd");
29067 let in_z = g.input("z", Shape::new(&[n], DType::C64));
29068 let out = g.complex_norm_sq(in_z);
29069 g.set_outputs(vec![out]);
29070 let plan = rlx_opt::memory::plan_memory(&g);
29071 let mut arena = crate::arena::Arena::from_plan(plan);
29072 let sched = compile_thunks(&g, &arena);
29073 let z_off = arena.byte_offset(in_z);
29074 let out_off = arena.byte_offset(out);
29075 let buf = arena.raw_buf_mut();
29076 unsafe {
29077 let pz = buf.as_mut_ptr().add(z_off) as *mut f32;
29078 for (i, &(re, im)) in z.iter().enumerate() {
29079 *pz.add(2 * i) = re;
29080 *pz.add(2 * i + 1) = im;
29081 }
29082 }
29083 execute_thunks(&sched, arena.raw_buf_mut());
29084 unsafe {
29085 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29086 (0..n).map(|i| *p.add(i)).collect()
29087 }
29088 }
29089
29090 fn run_complex_norm_sq_bwd(z: &[(f32, f32)], g: &[f32]) -> Vec<(f32, f32)> {
29092 let n = z.len();
29093 let mut gr = Graph::new("cns_bwd");
29094 let in_z = gr.input("z", Shape::new(&[n], DType::C64));
29095 let in_g = gr.input("g", Shape::new(&[n], DType::F32));
29096 let out = gr.complex_norm_sq_backward(in_z, in_g);
29097 gr.set_outputs(vec![out]);
29098 let plan = rlx_opt::memory::plan_memory(&gr);
29099 let mut arena = crate::arena::Arena::from_plan(plan);
29100 let sched = compile_thunks(&gr, &arena);
29101 let z_off = arena.byte_offset(in_z);
29102 let g_off = arena.byte_offset(in_g);
29103 let out_off = arena.byte_offset(out);
29104 let buf = arena.raw_buf_mut();
29105 unsafe {
29106 let pz = buf.as_mut_ptr().add(z_off) as *mut f32;
29107 let pg = buf.as_mut_ptr().add(g_off) as *mut f32;
29108 for (i, &(re, im)) in z.iter().enumerate() {
29109 *pz.add(2 * i) = re;
29110 *pz.add(2 * i + 1) = im;
29111 }
29112 for (i, &v) in g.iter().enumerate() {
29113 *pg.add(i) = v;
29114 }
29115 }
29116 execute_thunks(&sched, arena.raw_buf_mut());
29117 unsafe {
29118 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29119 (0..n).map(|i| (*p.add(2 * i), *p.add(2 * i + 1))).collect()
29120 }
29121 }
29122
29123 #[test]
29124 fn complex_norm_sq_matches_textbook() {
29125 let z = [(3.0_f32, 4.0_f32), (1.0_f32, 0.0_f32), (0.0_f32, 0.0_f32)];
29129 let out = run_complex_norm_sq(&z);
29130 assert!((out[0] - 25.0).abs() < 1e-5);
29131 assert!((out[1] - 1.0).abs() < 1e-6);
29132 assert!(out[2].abs() < 1e-6);
29133 }
29134
29135 #[test]
29136 fn complex_norm_sq_backward_matches_wirtinger_formula() {
29137 let z = [(3.0_f32, 4.0_f32), (1.5_f32, -2.5_f32)];
29139 let g = [1.0_f32, 1.0_f32];
29140 let dz = run_complex_norm_sq_bwd(&z, &g);
29141 assert_close_c(dz[0], z[0], 1e-6, "dz[0] = g·z[0]");
29142 assert_close_c(dz[1], z[1], 1e-6, "dz[1] = g·z[1]");
29143 }
29144
29145 #[test]
29146 fn complex_norm_sq_backward_scales_with_upstream() {
29147 let z = [(2.0_f32, 1.0_f32), (-1.0_f32, 3.0_f32)];
29149 let g = [0.5_f32, -2.0_f32];
29150 let dz = run_complex_norm_sq_bwd(&z, &g);
29151 assert_close_c(dz[0], (1.0, 0.5), 1e-6, "g=0.5 · (2,1)");
29152 assert_close_c(dz[1], (2.0, -6.0), 1e-6, "g=-2 · (-1,3)");
29153 }
29154
29155 #[test]
29160 fn custom_fn_multi_extracts_each_subgraph_output() {
29161 use rlx_ir::ops::special::MultiOutputHandle;
29162
29163 let _ = MultiOutputHandle {
29164 source: NodeId(0),
29165 sub_shapes: vec![],
29166 offsets: vec![],
29167 }; let mut body = Graph::new("multi_body");
29171 let s3 = Shape::new(&[3], DType::F32);
29172 let x = body.input("x", s3.clone());
29173 let x_sq = body.binary(BinaryOp::Mul, x, x, s3.clone());
29174 let two = body.add_node(
29175 Op::Constant {
29176 data: vec![
29177 2.0_f32.to_le_bytes(),
29178 2.0_f32.to_le_bytes(),
29179 2.0_f32.to_le_bytes(),
29180 ]
29181 .into_iter()
29182 .flatten()
29183 .collect(),
29184 },
29185 vec![],
29186 s3.clone(),
29187 );
29188 let two_x = body.binary(BinaryOp::Mul, two, x, s3.clone());
29189 body.set_outputs(vec![x_sq, two_x]);
29190
29191 let mut outer = Graph::new("multi_outer");
29193 let in_x = outer.input("xin", s3.clone());
29194 let handle = outer.custom_fn_multi(vec![in_x], body);
29195 assert_eq!(handle.n_outputs(), 2);
29196 let out0 = handle.output(&mut outer, 0); let out1 = handle.output(&mut outer, 1); outer.set_outputs(vec![out0, out1]);
29199
29200 let plan = rlx_opt::memory::plan_memory(&outer);
29201 let mut arena = crate::arena::Arena::from_plan(plan);
29202 let sched = compile_thunks(&outer, &arena);
29203 let xin_off = arena.byte_offset(in_x);
29204 let out0_off = arena.byte_offset(out0);
29205 let out1_off = arena.byte_offset(out1);
29206 let xs = [1.0_f32, 2.0, 3.0];
29207 unsafe {
29208 let p = arena.raw_buf_mut().as_mut_ptr().add(xin_off) as *mut f32;
29209 for (i, &v) in xs.iter().enumerate() {
29210 *p.add(i) = v;
29211 }
29212 }
29213 execute_thunks(&sched, arena.raw_buf_mut());
29214 let out0_v: Vec<f32> = unsafe {
29215 let p = arena.raw_buf().as_ptr().add(out0_off) as *const f32;
29216 (0..3).map(|i| *p.add(i)).collect()
29217 };
29218 let out1_v: Vec<f32> = unsafe {
29219 let p = arena.raw_buf().as_ptr().add(out1_off) as *const f32;
29220 (0..3).map(|i| *p.add(i)).collect()
29221 };
29222 for i in 0..3 {
29224 assert!(
29225 (out0_v[i] - xs[i] * xs[i]).abs() < 1e-5,
29226 "out0[{i}] = {} != x² = {}",
29227 out0_v[i],
29228 xs[i] * xs[i]
29229 );
29230 assert!(
29231 (out1_v[i] - 2.0 * xs[i]).abs() < 1e-5,
29232 "out1[{i}] = {} != 2x = {}",
29233 out1_v[i],
29234 2.0 * xs[i]
29235 );
29236 }
29237 }
29238
29239 #[test]
29240 fn complex_norm_sq_gradient_matches_finite_difference() {
29241 let z = [(3.0_f32, 4.0_f32)];
29243 let eps = 1e-3_f32;
29244 let v0 = run_complex_norm_sq(&z)[0];
29245 let z_pert = [(3.0_f32 + eps, 4.0_f32)];
29246 let v1 = run_complex_norm_sq(&z_pert)[0];
29247 let fd_re = (v1 - v0) / eps;
29248 let analytic_re = 2.0 * z[0].0;
29249 assert!((fd_re - analytic_re).abs() < 1e-2);
29250
29251 let z_pert_im = [(3.0_f32, 4.0_f32 + eps)];
29253 let v2 = run_complex_norm_sq(&z_pert_im)[0];
29254 let fd_im = (v2 - v0) / eps;
29255 let analytic_im = 2.0 * z[0].1;
29256 assert!((fd_im - analytic_im).abs() < 1e-2);
29257
29258 let dz = run_complex_norm_sq_bwd(&z, &[1.0_f32]);
29264 assert!((2.0 * dz[0].0 - analytic_re).abs() < 1e-5);
29265 assert!((2.0 * dz[0].1 - analytic_im).abs() < 1e-5);
29266 }
29267
29268 #[test]
29273 fn binary_full_5d_mid_singleton_broadcast() {
29274 let bh = 2usize;
29275 let h = 3;
29276 let w = 4;
29277 let f = DType::F32;
29278
29279 let mut g = Graph::new("bcast_5d");
29280 let lhs = g.input("lhs", Shape::new(&[bh, h, w, h, w], f));
29281 let rhs = g.input("rhs", Shape::new(&[bh, h, w, 1, w], f));
29283 let out = g.binary(BinaryOp::Add, lhs, rhs, Shape::new(&[bh, h, w, h, w], f));
29284 g.set_outputs(vec![out]);
29285
29286 let lhs_data: Vec<f32> = (0..bh * h * w * h * w).map(|i| i as f32 * 0.01).collect();
29288 let rhs_data: Vec<f32> = (0..bh * h * w * w)
29289 .map(|i| (i as f32 + 100.0) * 0.01)
29290 .collect();
29291
29292 let mut expected = vec![0f32; bh * h * w * h * w];
29294 for b_ in 0..bh {
29295 for hq in 0..h {
29296 for wq in 0..w {
29297 for hk in 0..h {
29298 for wk in 0..w {
29299 let li = (((b_ * h + hq) * w + wq) * h + hk) * w + wk;
29300 let ri = ((b_ * h + hq) * w + wq) * w + wk;
29302 expected[li] = lhs_data[li] + rhs_data[ri];
29303 }
29304 }
29305 }
29306 }
29307 }
29308
29309 let plan = rlx_opt::memory::plan_memory(&g);
29310 let mut arena = crate::arena::Arena::from_plan(plan);
29311 let sched = compile_thunks(&g, &arena);
29312 let lhs_off = arena.byte_offset(lhs);
29313 let rhs_off = arena.byte_offset(rhs);
29314 let out_off = arena.byte_offset(out);
29315 let buf = arena.raw_buf_mut();
29316 unsafe {
29317 let p = buf.as_mut_ptr().add(lhs_off) as *mut f32;
29318 for (i, &v) in lhs_data.iter().enumerate() {
29319 *p.add(i) = v;
29320 }
29321 let p = buf.as_mut_ptr().add(rhs_off) as *mut f32;
29322 for (i, &v) in rhs_data.iter().enumerate() {
29323 *p.add(i) = v;
29324 }
29325 }
29326 execute_thunks(&sched, arena.raw_buf_mut());
29327 let actual: Vec<f32> = unsafe {
29328 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29329 (0..bh * h * w * h * w).map(|i| *p.add(i)).collect()
29330 };
29331
29332 let mut max_diff = 0f32;
29334 let mut max_idx = 0;
29335 for i in 0..actual.len() {
29336 let d = (actual[i] - expected[i]).abs();
29337 if d > max_diff {
29338 max_diff = d;
29339 max_idx = i;
29340 }
29341 }
29342 assert!(
29343 max_diff < 1e-6,
29344 "5D mid-shape singleton broadcast wrong: max |Δ| = {max_diff} at idx {max_idx} \
29345 (actual={}, expected={})",
29346 actual[max_idx],
29347 expected[max_idx]
29348 );
29349 }
29350
29351 #[test]
29352 fn layer_norm2d_and_conv_transpose2d_kernels() {
29353 let mut out = vec![0f32; 8];
29354 crate::kernels::layer_norm2d_nchw(
29355 &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
29356 &[1.0, 1.0],
29357 &[0.0, 0.0],
29358 &mut out,
29359 1,
29360 2,
29361 2,
29362 2,
29363 1e-5,
29364 );
29365 let mean0: f32 = (1.0 + 3.0) / 2.0;
29366 assert!((out[0] - mean0).abs() > 0.1);
29367
29368 let mut up = vec![0f32; 4];
29369 crate::kernels::conv_transpose2d_nchw(
29370 &[2.0],
29371 &[1.0, 0.0, 0.0, 1.0],
29372 &mut up,
29373 1,
29374 1,
29375 1,
29376 1,
29377 1,
29378 2,
29379 2,
29380 2,
29381 2,
29382 2,
29383 2,
29384 0,
29385 0,
29386 1,
29387 1,
29388 1,
29389 );
29390 assert!((up[0] - 2.0).abs() < 1e-5);
29391 assert!((up[3] - 2.0).abs() < 1e-5);
29392 }
29393
29394 #[test]
29400 fn scaled_matmul_oracle_matches_f32() {
29401 use rlx_ir::ScaledFormat::*;
29402 use rlx_ir::{ScaleLayout, ScaledFormat};
29403
29404 fn cosine(a: &[f32], b: &[f32]) -> f32 {
29405 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
29406 let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
29407 let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
29408 dot / (na * nb)
29409 }
29410
29411 #[allow(clippy::too_many_arguments)]
29412 fn run_scaled(
29413 lhs: &[f32],
29414 rhs: &[f32],
29415 m: usize,
29416 k: usize,
29417 n: usize,
29418 lf: ScaledFormat,
29419 rf: ScaledFormat,
29420 layout: ScaleLayout,
29421 ) -> Vec<f32> {
29422 let f = DType::F32;
29423 let u8t = DType::U8;
29424 let mut g = Graph::new("scaled");
29425 let lhs_in = g.input("lhs", Shape::new(&[m, k], f));
29426 let rhs_in = g.input("rhs", Shape::new(&[n, k], f));
29427 let (ls_shape, rs_shape) = match layout {
29428 ScaleLayout::PerTensor => (Shape::new(&[1], f), Shape::new(&[1], f)),
29429 _ => {
29430 let nb = k.div_ceil(layout.block() as usize);
29431 (Shape::new(&[m, nb], u8t), Shape::new(&[n, nb], u8t))
29432 }
29433 };
29434 let ls = g.add_node(
29435 Op::ScaledQuantScale {
29436 format: lf,
29437 scale_layout: layout,
29438 },
29439 vec![lhs_in],
29440 ls_shape,
29441 );
29442 let lq = g.add_node(
29443 Op::ScaledQuantize {
29444 format: lf,
29445 scale_layout: layout,
29446 },
29447 vec![lhs_in, ls],
29448 Shape::new(&[m, k], u8t),
29449 );
29450 let rs = g.add_node(
29451 Op::ScaledQuantScale {
29452 format: rf,
29453 scale_layout: layout,
29454 },
29455 vec![rhs_in],
29456 rs_shape,
29457 );
29458 let rq = g.add_node(
29459 Op::ScaledQuantize {
29460 format: rf,
29461 scale_layout: layout,
29462 },
29463 vec![rhs_in, rs],
29464 Shape::new(&[n, k], u8t),
29465 );
29466 let out = g.add_node(
29467 Op::ScaledMatMul {
29468 lhs_format: lf,
29469 rhs_format: rf,
29470 scale_layout: layout,
29471 has_bias: false,
29472 },
29473 vec![lq, rq, ls, rs],
29474 Shape::new(&[m, n], f),
29475 );
29476 g.set_outputs(vec![out]);
29477
29478 let plan = rlx_opt::memory::plan_memory(&g);
29479 let mut arena = crate::arena::Arena::from_plan(plan);
29480 let sched = compile_thunks(&g, &arena);
29481 let lhs_off = arena.byte_offset(lhs_in);
29482 let rhs_off = arena.byte_offset(rhs_in);
29483 let out_off = arena.byte_offset(out);
29484 let buf = arena.raw_buf_mut();
29485 unsafe {
29486 let lp = buf.as_mut_ptr().add(lhs_off) as *mut f32;
29487 for (i, &v) in lhs.iter().enumerate() {
29488 *lp.add(i) = v;
29489 }
29490 let rp = buf.as_mut_ptr().add(rhs_off) as *mut f32;
29491 for (i, &v) in rhs.iter().enumerate() {
29492 *rp.add(i) = v;
29493 }
29494 }
29495 execute_thunks(&sched, arena.raw_buf_mut());
29496 unsafe {
29497 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29498 (0..m * n).map(|i| *p.add(i)).collect()
29499 }
29500 }
29501
29502 let (m, k, n) = (4usize, 64usize, 8usize);
29503 let lhs: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.13).sin() * 1.5).collect();
29504 let rhs: Vec<f32> = (0..n * k).map(|i| (i as f32 * 0.07).cos() * 1.2).collect();
29505 let mut reference = vec![0f32; m * n];
29506 for i in 0..m {
29507 for j in 0..n {
29508 let mut acc = 0f32;
29509 for p in 0..k {
29510 acc += lhs[i * k + p] * rhs[j * k + p];
29511 }
29512 reference[i * n + j] = acc;
29513 }
29514 }
29515
29516 let cases = [
29518 (F8E4M3, 0.999f32),
29519 (F8E5M2, 0.99),
29520 (F8E4M3Fnuz, 0.999),
29521 (F8E5M2Fnuz, 0.99),
29522 (F6E2M3, 0.99),
29523 (F6E3M2, 0.98),
29524 (F4E2M1, 0.90),
29525 ];
29526 for (fmt, thresh) in cases {
29527 let out = run_scaled(&lhs, &rhs, m, k, n, fmt, fmt, ScaleLayout::PerTensor);
29528 let c = cosine(&out, &reference);
29529 assert!(c >= thresh, "{fmt} per-tensor cosine {c} < {thresh}");
29530 }
29531
29532 let out_mx = run_scaled(&lhs, &rhs, m, k, n, F8E4M3, F8E4M3, ScaleLayout::mx());
29534 let c_mx = cosine(&out_mx, &reference);
29535 assert!(c_mx >= 0.999, "mx-e8m0 e4m3 cosine {c_mx}");
29536 let out_nv = run_scaled(&lhs, &rhs, m, k, n, F4E2M1, F4E2M1, ScaleLayout::nvfp4());
29537 let c_nv = cosine(&out_nv, &reference);
29538 assert!(c_nv >= 0.95, "nvfp4 e2m1 cosine {c_nv}");
29539 }
29540
29541 #[test]
29546 fn scaled_dequantize_inverts_quantize() {
29547 use rlx_ir::ScaledFormat::*;
29548 use rlx_ir::{ScaleLayout, ScaledFormat};
29549
29550 fn cosine(a: &[f32], b: &[f32]) -> f32 {
29551 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
29552 let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
29553 let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
29554 dot / (na * nb)
29555 }
29556
29557 fn roundtrip(x: &[f32], rows: usize, cols: usize, fmt: ScaledFormat) -> Vec<f32> {
29558 let f = DType::F32;
29559 let u8t = DType::U8;
29560 let layout = ScaleLayout::PerTensor;
29561 let mut g = Graph::new("dequant_rt");
29562 let x_in = g.input("x", Shape::new(&[rows, cols], f));
29563 let scale = g.add_node(
29564 Op::ScaledQuantScale {
29565 format: fmt,
29566 scale_layout: layout,
29567 },
29568 vec![x_in],
29569 Shape::new(&[1], f),
29570 );
29571 let codes = g.add_node(
29572 Op::ScaledQuantize {
29573 format: fmt,
29574 scale_layout: layout,
29575 },
29576 vec![x_in, scale],
29577 Shape::new(&[rows, cols], u8t),
29578 );
29579 let recon = g.add_node(
29580 Op::ScaledDequantize {
29581 format: fmt,
29582 scale_layout: layout,
29583 },
29584 vec![codes, scale],
29585 Shape::new(&[rows, cols], f),
29586 );
29587 g.set_outputs(vec![recon]);
29588
29589 let plan = rlx_opt::memory::plan_memory(&g);
29590 let mut arena = crate::arena::Arena::from_plan(plan);
29591 let sched = compile_thunks(&g, &arena);
29592 let x_off = arena.byte_offset(x_in);
29593 let r_off = arena.byte_offset(recon);
29594 unsafe {
29595 let p = arena.raw_buf_mut().as_mut_ptr().add(x_off) as *mut f32;
29596 for (i, &v) in x.iter().enumerate() {
29597 *p.add(i) = v;
29598 }
29599 }
29600 execute_thunks(&sched, arena.raw_buf_mut());
29601 unsafe {
29602 let p = arena.raw_buf().as_ptr().add(r_off) as *const f32;
29603 (0..rows * cols).map(|i| *p.add(i)).collect()
29604 }
29605 }
29606
29607 let (rows, cols) = (4usize, 16usize);
29608 let x: Vec<f32> = (0..rows * cols)
29609 .map(|i| (i as f32 * 0.21).sin() * 1.7)
29610 .collect();
29611 for (fmt, min_cos) in [(F8E4M3, 0.999f32), (F8E5M2, 0.99), (F4E2M1, 0.93)] {
29613 let recon = roundtrip(&x, rows, cols, fmt);
29614 assert_eq!(recon.len(), x.len());
29615 assert!(
29616 recon.iter().all(|v| v.is_finite()),
29617 "{fmt:?} produced non-finite"
29618 );
29619 let c = cosine(&recon, &x);
29620 assert!(c >= min_cos, "{fmt:?} round-trip cosine {c} < {min_cos}");
29621 }
29622 }
29623
29624 #[test]
29628 fn fma_matches_mul_add() {
29629 let f = DType::F32;
29630 let n = 9usize;
29631 let a: Vec<f32> = vec![1.5, -2.0, 0.0, 3.25, -1.1, 7.0, -0.5, 2.2, 9.9];
29632 let b: Vec<f32> = vec![2.0, 0.5, 4.0, -1.0, 6.0, -2.5, 8.0, -3.3, 0.1];
29633 let c: Vec<f32> = vec![0.25, 1.0, -3.0, 2.0, -0.5, 4.0, 1.5, -2.2, 0.0];
29634
29635 let mut g = Graph::new("fma");
29636 let an = g.input("a", Shape::new(&[n], f));
29637 let bn = g.input("b", Shape::new(&[n], f));
29638 let cn = g.input("c", Shape::new(&[n], f));
29639 let out = g.add_node(Op::Fma, vec![an, bn, cn], Shape::new(&[n], f));
29640 g.set_outputs(vec![out]);
29641
29642 let actual = run_graph(&g, &[(an, &a), (bn, &b), (cn, &c)], out, n);
29643 for i in 0..n {
29644 let expected = a[i].mul_add(b[i], c[i]);
29645 assert!(
29646 (actual[i] - expected).abs() <= f32::EPSILON * (1.0 + expected.abs()),
29647 "fma[{i}]: {} vs mul_add {expected}",
29648 actual[i]
29649 );
29650 }
29651 }
29652
29653 #[test]
29658 fn scaled_quant_pass_runs_end_to_end() {
29659 use rlx_opt::rlx_compile::scaled_quant_insert::{ScaledQuantConfig, insert_scaled_matmul};
29660
29661 let f = DType::F32;
29662 let (m, k, n) = (3usize, 16usize, 5usize);
29663 let mut g = Graph::new("amp_fp8");
29664 let x = g.input("x", Shape::new(&[m, k], f));
29665 let w = g.param("w", Shape::new(&[k, n], f));
29666 let mm = g.matmul(x, w, Shape::new(&[m, n], f));
29667 g.set_outputs(vec![mm]);
29668
29669 let g = insert_scaled_matmul(g, ScaledQuantConfig::fp8_e4m3());
29670
29671 let x_data: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.11).sin()).collect();
29672 let w_data: Vec<f32> = (0..k * n).map(|i| (i as f32 * 0.05).cos()).collect();
29673 let mut reference = vec![0f32; m * n];
29675 for i in 0..m {
29676 for j in 0..n {
29677 let mut acc = 0f32;
29678 for p in 0..k {
29679 acc += x_data[i * k + p] * w_data[p * n + j];
29680 }
29681 reference[i * n + j] = acc;
29682 }
29683 }
29684
29685 let mut x_id = None;
29687 let mut w_id = None;
29688 for node in g.nodes() {
29689 match &node.op {
29690 Op::Input { name } if name == "x" => x_id = Some(node.id),
29691 Op::Param { name } if name == "w" => w_id = Some(node.id),
29692 _ => {}
29693 }
29694 }
29695 let (x_id, w_id) = (x_id.unwrap(), w_id.unwrap());
29696 let out_id = g.outputs[0];
29697
29698 let plan = rlx_opt::memory::plan_memory(&g);
29699 let mut arena = crate::arena::Arena::from_plan(plan);
29700 let sched = compile_thunks(&g, &arena);
29701 let x_off = arena.byte_offset(x_id);
29702 let w_off = arena.byte_offset(w_id);
29703 let out_off = arena.byte_offset(out_id);
29704 let buf = arena.raw_buf_mut();
29705 unsafe {
29706 let xp = buf.as_mut_ptr().add(x_off) as *mut f32;
29707 for (i, &v) in x_data.iter().enumerate() {
29708 *xp.add(i) = v;
29709 }
29710 let wp = buf.as_mut_ptr().add(w_off) as *mut f32;
29711 for (i, &v) in w_data.iter().enumerate() {
29712 *wp.add(i) = v;
29713 }
29714 }
29715 execute_thunks(&sched, arena.raw_buf_mut());
29716 let actual: Vec<f32> = unsafe {
29717 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
29718 (0..m * n).map(|i| *p.add(i)).collect()
29719 };
29720
29721 let dot: f32 = actual.iter().zip(&reference).map(|(a, b)| a * b).sum();
29722 let na = actual.iter().map(|x| x * x).sum::<f32>().sqrt();
29723 let nb = reference.iter().map(|x| x * x).sum::<f32>().sqrt();
29724 let cos = dot / (na * nb);
29725 assert!(cos >= 0.999, "AMP-fp8 e2e cosine {cos}");
29726 }
29727}